From c14698f8ce3c14c350e59ac8b0c7952595cf8d3d Mon Sep 17 00:00:00 2001 From: Matthew James Briggs Date: Mon, 22 Jun 2026 06:40:20 +0000 Subject: [PATCH] feat: model editorial (footnote/level) in mx::api Add a reusable api::EditorialData (footnote + level) and attach it to PartGroupData (the start group) and DirectionData, threading it through the impl reader/writer layers so / survive an api round-trip instead of being silently dropped. - New api types: EditorialData, FootnoteData (formatted-text, mirroring WordsData), and LevelData with its StartStopSingle/SymbolSize enums. - Shared EditorialFunctions.h translates the core editorial groups (EditorialGroup, EditorialVoiceDirectionGroup) in both directions. - Converter gains StartStopSingle and SymbolSize bridge maps. - EditorialApiTest proves footnote+level round-trip on a part-group and a direction, using distinct non-default values per field. Part-group stop editorial is intentionally not modeled, since MusicXML ignores child element values at a group stop. --- src/include/mx/api/DirectionData.h | 7 + src/include/mx/api/EditorialData.h | 40 ++++++ src/include/mx/api/FootnoteData.h | 43 +++++++ src/include/mx/api/LevelData.h | 66 ++++++++++ src/include/mx/api/PartGroupData.h | 8 +- src/private/mx/impl/Converter.cpp | 33 +++++ src/private/mx/impl/Converter.h | 10 ++ src/private/mx/impl/DirectionReader.cpp | 3 + src/private/mx/impl/DirectionWriter.cpp | 8 ++ src/private/mx/impl/EditorialFunctions.h | 136 ++++++++++++++++++++ src/private/mx/impl/ScoreReader.cpp | 4 +- src/private/mx/impl/ScoreWriter.cpp | 8 ++ src/private/mxtest/api/EditorialApiTest.cpp | 125 ++++++++++++++++++ 13 files changed, 489 insertions(+), 2 deletions(-) create mode 100644 src/include/mx/api/EditorialData.h create mode 100644 src/include/mx/api/FootnoteData.h create mode 100644 src/include/mx/api/LevelData.h create mode 100644 src/private/mx/impl/EditorialFunctions.h create mode 100644 src/private/mxtest/api/EditorialApiTest.cpp diff --git a/src/include/mx/api/DirectionData.h b/src/include/mx/api/DirectionData.h index e9591c71..c1a42378 100644 --- a/src/include/mx/api/DirectionData.h +++ b/src/include/mx/api/DirectionData.h @@ -7,6 +7,7 @@ #include "mx/api/ApiCommon.h" #include "mx/api/ChordData.h" #include "mx/api/CodaData.h" +#include "mx/api/EditorialData.h" #include "mx/api/FiguredBassData.h" #include "mx/api/MarkData.h" #include "mx/api/OttavaData.h" @@ -102,6 +103,10 @@ struct DirectionData bool isSoundDataSpecified; SoundData soundData; + // Editorial (/) carried on the . The editorial-voice is + // already represented by the `voice` member above. + EditorialData editorial; + std::vector tempos; std::vector marks; std::vector wedgeStarts; @@ -142,6 +147,7 @@ inline bool isDirectionDataEmpty(const DirectionData &directionData) directionData.ottavaStops.size() == 0 && directionData.words.size() == 0 && directionData.segnos.size() == 0 && directionData.codas.size() == 0 && directionData.figuredBasses.size() == 0 && !directionData.isSoundDataSpecified && + !directionData.editorial.isFootnoteSpecified && !directionData.editorial.isLevelSpecified && directionData.orderedComponents.size() == 0; } @@ -152,6 +158,7 @@ MXAPI_EQUALS_MEMBER(voice) MXAPI_EQUALS_MEMBER(isStaffValueSpecified) MXAPI_EQUALS_MEMBER(isSoundDataSpecified) MXAPI_EQUALS_MEMBER(soundData) +MXAPI_EQUALS_MEMBER(editorial) MXAPI_EQUALS_MEMBER(tempos) MXAPI_EQUALS_MEMBER(marks) MXAPI_EQUALS_MEMBER(wedgeStarts) diff --git a/src/include/mx/api/EditorialData.h b/src/include/mx/api/EditorialData.h new file mode 100644 index 00000000..28fddc39 --- /dev/null +++ b/src/include/mx/api/EditorialData.h @@ -0,0 +1,40 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/ApiCommon.h" +#include "mx/api/FootnoteData.h" +#include "mx/api/LevelData.h" + +namespace mx +{ +namespace api +{ +// The MusicXML editorial group ( + ) carries editorial information for a parent +// element. It appears on several elements (e.g. , ); this reusable type +// captures it once. The is...Specified flags distinguish "absent" (write nothing back) from a +// present-but-empty child. +class EditorialData +{ + public: + bool isFootnoteSpecified; + FootnoteData footnote; + bool isLevelSpecified; + LevelData level; + + EditorialData() : isFootnoteSpecified{false}, footnote{}, isLevelSpecified{false}, level{} + { + } +}; + +MXAPI_EQUALS_BEGIN(EditorialData) +MXAPI_EQUALS_MEMBER(isFootnoteSpecified) +MXAPI_EQUALS_MEMBER(footnote) +MXAPI_EQUALS_MEMBER(isLevelSpecified) +MXAPI_EQUALS_MEMBER(level) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(EditorialData); +} // namespace api +} // namespace mx diff --git a/src/include/mx/api/FootnoteData.h b/src/include/mx/api/FootnoteData.h new file mode 100644 index 00000000..278188c4 --- /dev/null +++ b/src/include/mx/api/FootnoteData.h @@ -0,0 +1,43 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/ApiCommon.h" +#include "mx/api/ColorData.h" +#include "mx/api/FontData.h" +#include "mx/api/PositionData.h" + +#include + +namespace mx +{ +namespace api +{ +// The MusicXML element is a formatted-text: text content plus the position, font, and +// color attribute groups. Modeled to match WordsData (the api's other formatted-text surface). +class FootnoteData +{ + public: + std::string text; + PositionData positionData; + FontData fontData; + bool isColorSpecified; + ColorData colorData; + + FootnoteData() : text{}, positionData{}, fontData{}, isColorSpecified{false}, colorData{} + { + } +}; + +MXAPI_EQUALS_BEGIN(FootnoteData) +MXAPI_EQUALS_MEMBER(text) +MXAPI_EQUALS_MEMBER(positionData) +MXAPI_EQUALS_MEMBER(fontData) +MXAPI_EQUALS_MEMBER(isColorSpecified) +MXAPI_EQUALS_MEMBER(colorData) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(FootnoteData); +} // namespace api +} // namespace mx diff --git a/src/include/mx/api/LevelData.h b/src/include/mx/api/LevelData.h new file mode 100644 index 00000000..e34829db --- /dev/null +++ b/src/include/mx/api/LevelData.h @@ -0,0 +1,66 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/ApiCommon.h" + +#include + +namespace mx +{ +namespace api +{ +// Mirrors the MusicXML 'type' attribute (start-stop-single): whether the editorial +// information applies to the start of a series of symbols, the end, or a single symbol. +enum class StartStopSingle +{ + unspecified, + start, + stop, + single +}; + +// Mirrors the MusicXML 'size' attribute (symbol-size). +enum class SymbolSize +{ + unspecified, + full, + cue, + graceCue, + large +}; + +// The MusicXML element specifies editorial information for the parent element. Its text +// content is descriptive; the attributes control how the editorial marking is rendered. An +// `unspecified` enum (or empty `value`) means the source carried no such attribute and none is +// written back. +class LevelData +{ + public: + std::string value; + Bool reference; + StartStopSingle type; + Bool parentheses; + Bool bracket; + SymbolSize size; + + LevelData() + : value{}, reference{Bool::unspecified}, type{StartStopSingle::unspecified}, parentheses{Bool::unspecified}, + bracket{Bool::unspecified}, size{SymbolSize::unspecified} + { + } +}; + +MXAPI_EQUALS_BEGIN(LevelData) +MXAPI_EQUALS_MEMBER(value) +MXAPI_EQUALS_MEMBER(reference) +MXAPI_EQUALS_MEMBER(type) +MXAPI_EQUALS_MEMBER(parentheses) +MXAPI_EQUALS_MEMBER(bracket) +MXAPI_EQUALS_MEMBER(size) +MXAPI_EQUALS_END; +MXAPI_NOT_EQUALS_AND_VECTORS(LevelData); +} // namespace api +} // namespace mx diff --git a/src/include/mx/api/PartGroupData.h b/src/include/mx/api/PartGroupData.h index 8743865b..ef032805 100644 --- a/src/include/mx/api/PartGroupData.h +++ b/src/include/mx/api/PartGroupData.h @@ -4,6 +4,8 @@ #pragma once +#include "mx/api/EditorialData.h" + #include #include @@ -59,7 +61,10 @@ class PartGroupData BracketType bracketType; GroupBarline groupBarline; // TODO - group time - // TODO - group editorial + + // Editorial (/) carried on the part-group *start*. MusicXML ignores child + // values at the group stop, so stop-group editorial is intentionally not modeled. + EditorialData editorial; // The number attribute is used to distinguish overlapping // and nested part-groups, not the sequence of groups. @@ -82,6 +87,7 @@ MXAPI_EQUALS_MEMBER(abbreviation) MXAPI_EQUALS_MEMBER(displayAbbreviation) MXAPI_EQUALS_MEMBER(bracketType) MXAPI_EQUALS_MEMBER(groupBarline) +MXAPI_EQUALS_MEMBER(editorial) MXAPI_EQUALS_END; MXAPI_NOT_EQUALS_AND_VECTORS(PartGroupData); } // namespace api diff --git a/src/private/mx/impl/Converter.cpp b/src/private/mx/impl/Converter.cpp index ecf1e656..fd312614 100644 --- a/src/private/mx/impl/Converter.cpp +++ b/src/private/mx/impl/Converter.cpp @@ -137,6 +137,19 @@ const Converter::EnumMap Converter::boolMap = { {core::YesNo::no(), api::Bool::no}, }; +const Converter::EnumMap Converter::startStopSingleMap = { + {core::StartStopSingle::start(), api::StartStopSingle::start}, + {core::StartStopSingle::stop(), api::StartStopSingle::stop}, + {core::StartStopSingle::single(), api::StartStopSingle::single}, +}; + +const Converter::EnumMap Converter::symbolSizeMap = { + {core::SymbolSize::full(), api::SymbolSize::full}, + {core::SymbolSize::cue(), api::SymbolSize::cue}, + {core::SymbolSize::graceCue(), api::SymbolSize::graceCue}, + {core::SymbolSize::large(), api::SymbolSize::large}, +}; + const Converter::EnumMap Converter::valignMap = { {core::Valign::baseline(), api::VerticalAlignment::baseline}, {core::Valign::bottom(), api::VerticalAlignment::bottom}, @@ -1412,6 +1425,26 @@ core::YesNo Converter::convert(api::Bool value) const return findCoreItem(boolMap, core::YesNo::no(), value); } +core::StartStopSingle Converter::convert(api::StartStopSingle value) const +{ + return findCoreItem(startStopSingleMap, core::StartStopSingle::single(), value); +} + +api::StartStopSingle Converter::convert(core::StartStopSingle value) const +{ + return findApiItem(startStopSingleMap, api::StartStopSingle::unspecified, value); +} + +core::SymbolSize Converter::convert(api::SymbolSize value) const +{ + return findCoreItem(symbolSizeMap, core::SymbolSize::full(), value); +} + +api::SymbolSize Converter::convert(core::SymbolSize value) const +{ + return findApiItem(symbolSizeMap, api::SymbolSize::unspecified, value); +} + api::VerticalAlignment Converter::convert(core::Valign value) const { return findApiItem(valignMap, api::VerticalAlignment::unspecified, value); diff --git a/src/private/mx/impl/Converter.h b/src/private/mx/impl/Converter.h index 37052855..02174a02 100644 --- a/src/private/mx/impl/Converter.h +++ b/src/private/mx/impl/Converter.h @@ -35,8 +35,10 @@ #include "mx/core/generated/RightLeftMiddle.h" #include "mx/core/generated/SoundID.h" #include "mx/core/generated/StartStopDiscontinue.h" +#include "mx/core/generated/StartStopSingle.h" #include "mx/core/generated/StemValue.h" #include "mx/core/generated/Step.h" +#include "mx/core/generated/SymbolSize.h" #include "mx/core/generated/TechnicalChoice.h" #include "mx/core/generated/Transpose.h" #include "mx/core/generated/Valign.h" @@ -86,6 +88,12 @@ class Converter core::YesNo convert(api::Bool value) const; api::Bool convert(core::YesNo value) const; + core::StartStopSingle convert(api::StartStopSingle value) const; + api::StartStopSingle convert(core::StartStopSingle value) const; + + core::SymbolSize convert(api::SymbolSize value) const; + api::SymbolSize convert(core::SymbolSize value) const; + core::Valign convert(api::VerticalAlignment value) const; api::VerticalAlignment convert(core::Valign value) const; @@ -169,6 +177,8 @@ class Converter const static EnumMap clefMap; const static EnumMap placementMap; const static EnumMap boolMap; + const static EnumMap startStopSingleMap; + const static EnumMap symbolSizeMap; const static EnumMap valignMap; const static EnumMap halignMap; const static EnumMap cssMap; diff --git a/src/private/mx/impl/DirectionReader.cpp b/src/private/mx/impl/DirectionReader.cpp index 4241b4ef..44b331b4 100644 --- a/src/private/mx/impl/DirectionReader.cpp +++ b/src/private/mx/impl/DirectionReader.cpp @@ -62,6 +62,7 @@ #include "mx/core/generated/WedgeType.h" #include "mx/core/generated/YesNo.h" #include "mx/impl/DynamicsReader.h" +#include "mx/impl/EditorialFunctions.h" #include "mx/impl/MarkDataFunctions.h" #include "mx/impl/MetronomeReader.h" #include "mx/impl/PrintFunctions.h" @@ -171,6 +172,8 @@ void DirectionReader::parseValues() myOutDirectionData.soundData = std::move(soundData); } } + + myOutDirectionData.editorial = getEditorialData(myDirection->editorialVoiceDirection()); } else if (myHarmony) { diff --git a/src/private/mx/impl/DirectionWriter.cpp b/src/private/mx/impl/DirectionWriter.cpp index 3b86c1a3..57ec6ce3 100644 --- a/src/private/mx/impl/DirectionWriter.cpp +++ b/src/private/mx/impl/DirectionWriter.cpp @@ -66,6 +66,7 @@ #include "mx/core/generated/WedgeType.h" #include "mx/core/generated/YesNo.h" #include "mx/impl/DynamicsWriter.h" +#include "mx/impl/EditorialFunctions.h" #include "mx/impl/FontFunctions.h" #include "mx/impl/LineFunctions.h" #include "mx/impl/MarkDataFunctions.h" @@ -112,6 +113,13 @@ std::vector DirectionWriter::getDirectionLikeThings() direction.setOffset(coreOffset); } + if (myDirectionData.editorial.isFootnoteSpecified || myDirectionData.editorial.isLevelSpecified) + { + core::EditorialVoiceDirectionGroup editorial{}; + setEditorial(myDirectionData.editorial, editorial); + direction.setEditorialVoiceDirection(std::move(editorial)); + } + for (auto mark : myDirectionData.marks) { mark.tickTimePosition = myDirectionData.tickTimePosition; diff --git a/src/private/mx/impl/EditorialFunctions.h b/src/private/mx/impl/EditorialFunctions.h new file mode 100644 index 00000000..f2b4f6e2 --- /dev/null +++ b/src/private/mx/impl/EditorialFunctions.h @@ -0,0 +1,136 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#pragma once + +#include "mx/api/EditorialData.h" +#include "mx/core/generated/FormattedText.h" +#include "mx/core/generated/Level.h" +#include "mx/impl/Converter.h" +#include "mx/impl/FontFunctions.h" +#include "mx/impl/PositionFunctions.h" +#include "mx/impl/PrintFunctions.h" + +namespace mx +{ +namespace impl +{ +// The MusicXML editorial group ( + ) is shared by several elements (, +// , ...). These helpers translate it in both directions, templated on the core group +// type (EditorialGroup, EditorialVoiceDirectionGroup, ...) which all expose footnote()/level(). + +inline api::FootnoteData getFootnoteData(const core::FormattedText &ft) +{ + api::FootnoteData out; + out.text = ft.value(); + out.positionData = getPositionData(ft); + out.fontData = getFontData(ft); + out.isColorSpecified = ft.color().has_value(); + if (out.isColorSpecified) + { + out.colorData = getColor(ft); + } + return out; +} + +inline core::FormattedText makeFootnote(const api::FootnoteData &fn) +{ + core::FormattedText ft; + ft.setValue(fn.text); + setAttributesFromPositionData(fn.positionData, ft); + setAttributesFromFontData(fn.fontData, ft); + if (fn.isColorSpecified) + { + setAttributesFromColorData(fn.colorData, ft); + } + return ft; +} + +inline api::LevelData getLevelData(const core::Level &lvl) +{ + const Converter c; + api::LevelData out; + out.value = lvl.value(); + if (lvl.reference().has_value()) + { + out.reference = c.convert(*lvl.reference()); + } + if (lvl.type().has_value()) + { + out.type = c.convert(*lvl.type()); + } + if (lvl.parentheses().has_value()) + { + out.parentheses = c.convert(*lvl.parentheses()); + } + if (lvl.bracket().has_value()) + { + out.bracket = c.convert(*lvl.bracket()); + } + if (lvl.size().has_value()) + { + out.size = c.convert(*lvl.size()); + } + return out; +} + +inline core::Level makeLevel(const api::LevelData &lvl) +{ + const Converter c; + core::Level out; + out.setValue(lvl.value); + if (lvl.reference != api::Bool::unspecified) + { + out.setReference(c.convert(lvl.reference)); + } + if (lvl.type != api::StartStopSingle::unspecified) + { + out.setType(c.convert(lvl.type)); + } + if (lvl.parentheses != api::Bool::unspecified) + { + out.setParentheses(c.convert(lvl.parentheses)); + } + if (lvl.bracket != api::Bool::unspecified) + { + out.setBracket(c.convert(lvl.bracket)); + } + if (lvl.size != api::SymbolSize::unspecified) + { + out.setSize(c.convert(lvl.size)); + } + return out; +} + +// Reads footnote/level off any core editorial group into an api::EditorialData. +template api::EditorialData getEditorialData(const GROUP &group) +{ + api::EditorialData out; + if (group.footnote().has_value()) + { + out.isFootnoteSpecified = true; + out.footnote = getFootnoteData(*group.footnote()); + } + if (group.level().has_value()) + { + out.isLevelSpecified = true; + out.level = getLevelData(*group.level()); + } + return out; +} + +// Writes footnote/level from an api::EditorialData onto any core editorial group. +template void setEditorial(const api::EditorialData &ed, GROUP &group) +{ + if (ed.isFootnoteSpecified) + { + group.setFootnote(makeFootnote(ed.footnote)); + } + if (ed.isLevelSpecified) + { + group.setLevel(makeLevel(ed.level)); + } +} +} // namespace impl +} // namespace mx diff --git a/src/private/mx/impl/ScoreReader.cpp b/src/private/mx/impl/ScoreReader.cpp index b1756cea..f93ea385 100644 --- a/src/private/mx/impl/ScoreReader.cpp +++ b/src/private/mx/impl/ScoreReader.cpp @@ -30,6 +30,7 @@ #include "mx/core/generated/Work.h" #include "mx/core/generated/YesNo.h" #include "mx/impl/Converter.h" +#include "mx/impl/EditorialFunctions.h" #include "mx/impl/EncodingFunctions.h" #include "mx/impl/LayoutFunctions.h" #include "mx/impl/LcmGcd.h" @@ -322,10 +323,11 @@ void ScoreReader::startPartGroup(int partIndex, const core::PartGroup &inPartGro grpData.groupBarline = c.convert(inPartGroup.groupBarline()->value()); } + grpData.editorial = getEditorialData(inPartGroup.editorial()); + grpData.firstPartIndex = partIndex; // TODO - group time - // TODO - editorial (footnote/level) myPartGroupStack.push_front(grpData); } diff --git a/src/private/mx/impl/ScoreWriter.cpp b/src/private/mx/impl/ScoreWriter.cpp index 8183918a..0db40b6d 100644 --- a/src/private/mx/impl/ScoreWriter.cpp +++ b/src/private/mx/impl/ScoreWriter.cpp @@ -19,6 +19,7 @@ #include "mx/core/generated/TypedText.h" #include "mx/core/generated/Work.h" #include "mx/impl/Converter.h" +#include "mx/impl/EditorialFunctions.h" #include "mx/impl/EncodingFunctions.h" #include "mx/impl/LayoutFunctions.h" #include "mx/impl/NameDisplayFunctions.h" @@ -330,6 +331,13 @@ core::PartGroup ScoreWriter::makePartGroupStart(const api::PartGroupData &apiGrp mxGrp.setGroupBarline(groupBarline); } + if (apiGrp.editorial.isFootnoteSpecified || apiGrp.editorial.isLevelSpecified) + { + core::EditorialGroup editorial{}; + setEditorial(apiGrp.editorial, editorial); + mxGrp.setEditorial(std::move(editorial)); + } + return mxGrp; } diff --git a/src/private/mxtest/api/EditorialApiTest.cpp b/src/private/mxtest/api/EditorialApiTest.cpp new file mode 100644 index 00000000..0d16e1bd --- /dev/null +++ b/src/private/mxtest/api/EditorialApiTest.cpp @@ -0,0 +1,125 @@ +// MusicXML Class Library +// Copyright (c) by Matthew James Briggs +// Distributed under the MIT License + +#include "mxtest/control/CompileControl.h" +#ifdef MX_COMPILE_API_TESTS + +#include "cpul/cpulTestHarness.h" +#include "mx/api/DocumentManager.h" +#include "mx/api/ScoreData.h" +#include "mxtest/api/RoundTrip.h" + +using namespace mx::api; + +namespace +{ +PartData makeSimplePart(const std::string &id, const std::string &name) +{ + VoiceData voice; + NoteData n; + n.tickTimePosition = 0; + n.pitchData.step = Step::c; + n.pitchData.octave = 5; + n.durationData.durationName = DurationName::quarter; + n.durationData.durationTimeTicks = DEFAULT_TICKS_PER_QUARTER; + voice.notes.push_back(n); + StaffData staff{}; + staff.voices.emplace(0, voice); + MeasureData m; + m.staves.push_back(staff); + PartData pd; + pd.uniqueId = id; + pd.name = name; + pd.measures.push_back(m); + return pd; +} + +// Distinct, non-default editorial so a wrong-field assignment in the reader/writer +// would surface as a value mismatch rather than passing by coincidence. +EditorialData makeEditorial(const std::string &footnoteText, const std::string &levelText) +{ + EditorialData ed; + ed.isFootnoteSpecified = true; + ed.footnote.text = footnoteText; + ed.footnote.isColorSpecified = true; + ed.footnote.colorData.red = 0x12; + ed.footnote.colorData.green = 0x34; + ed.footnote.colorData.blue = 0x56; + + ed.isLevelSpecified = true; + ed.level.value = levelText; + ed.level.reference = Bool::yes; + ed.level.type = StartStopSingle::start; + ed.level.parentheses = Bool::no; + ed.level.bracket = Bool::yes; + ed.level.size = SymbolSize::cue; + return ed; +} + +void checkEditorial(const EditorialData &got, const std::string &footnoteText, const std::string &levelText) +{ + CHECK(got.isFootnoteSpecified); + CHECK_EQUAL(footnoteText, got.footnote.text); + CHECK(got.footnote.isColorSpecified); + CHECK_EQUAL(0x12, static_cast(got.footnote.colorData.red)); + CHECK_EQUAL(0x34, static_cast(got.footnote.colorData.green)); + CHECK_EQUAL(0x56, static_cast(got.footnote.colorData.blue)); + + CHECK(got.isLevelSpecified); + CHECK_EQUAL(levelText, got.level.value); + CHECK(Bool::yes == got.level.reference); + CHECK(StartStopSingle::start == got.level.type); + CHECK(Bool::no == got.level.parentheses); + CHECK(Bool::yes == got.level.bracket); + CHECK(SymbolSize::cue == got.level.size); +} +} // namespace + +// Editorial (/) on a must survive the api round-trip. Before the +// EditorialData feature the api dropped both elements (PartGroupData had a `// TODO - group +// editorial`). +TEST(editorialRoundTrip, partGroup) +{ + ScoreData in; + in.parts.push_back(makeSimplePart("P1", "Violin I")); + in.parts.push_back(makeSimplePart("P2", "Violin II")); + + PartGroupData grp; + grp.firstPartIndex = 0; + grp.lastPartIndex = 1; + grp.number = 1; + grp.name = "Violins"; + grp.bracketType = BracketType::bracket; + grp.editorial = makeEditorial("pg-footnote", "pg-level"); + in.partGroups.push_back(grp); + + const auto out = mxtest::roundTrip(in); + + REQUIRE(out.partGroups.size() == 1); + checkEditorial(out.partGroups.at(0).editorial, "pg-footnote", "pg-level"); +} + +// Editorial (/) on a must survive the api round-trip. +TEST(editorialRoundTrip, direction) +{ + ScoreData in; + in.ticksPerQuarter = DEFAULT_TICKS_PER_QUARTER; + in.parts.push_back(makeSimplePart("P1", "Flute")); + + DirectionData dir; + dir.tickTimePosition = 0; + dir.marks.push_back(MarkData{MarkType::f}); + dir.marks.back().tickTimePosition = 0; + dir.editorial = makeEditorial("dir-footnote", "dir-level"); + in.parts.at(0).measures.at(0).staves.at(0).directions.push_back(dir); + + const auto out = mxtest::roundTrip(in); + + REQUIRE(out.parts.size() == 1); + const auto &staff = out.parts.at(0).measures.at(0).staves.at(0); + REQUIRE(staff.directions.size() >= 1); + checkEditorial(staff.directions.at(0).editorial, "dir-footnote", "dir-level"); +} + +#endif