musicxml-io 0.9.4 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ import {
6
6
  ValidationException,
7
7
  generateId,
8
8
  validate
9
- } from "./chunk-T6KNPR45.mjs";
9
+ } from "./chunk-SQSIGPRD.mjs";
10
10
  import {
11
11
  bpmToUsPerQuarter,
12
12
  buildGridTimeline,
@@ -45,12 +45,6 @@ var RBRACKET = 93;
45
45
  function isXmlWhitespace(code) {
46
46
  return code === 32 || code === 10 || code === 9 || code === 13;
47
47
  }
48
- function isWhitespaceOnly(s) {
49
- for (let i = 0; i < s.length; i++) {
50
- if (!isXmlWhitespace(s.charCodeAt(i))) return false;
51
- }
52
- return true;
53
- }
54
48
  function parseXml(xml) {
55
49
  const root = [];
56
50
  const stack = [];
@@ -61,14 +55,17 @@ function parseXml(xml) {
61
55
  const lt = xml.indexOf("<", pos);
62
56
  if (lt === -1) break;
63
57
  if (lt > pos) {
64
- let text = xml.slice(pos, lt);
65
- if (!isWhitespaceOnly(text)) {
66
- text = decodeXmlEntities(text);
67
- children.push(
68
- INVALID_XML_CHARS_TEST.test(text) ? text.replace(INVALID_XML_CHARS_RE, "") : text
69
- );
58
+ let whitespaceOnly = true;
59
+ for (let i = pos; i < lt; i++) {
60
+ if (!isXmlWhitespace(xml.charCodeAt(i))) {
61
+ whitespaceOnly = false;
62
+ break;
63
+ }
64
+ }
65
+ if (!whitespaceOnly) {
66
+ children.push(decodeXmlEntities(xml.slice(pos, lt)));
70
67
  } else if (children.length === 0 && xml.charCodeAt(lt + 1) === SLASH) {
71
- children.push(text);
68
+ children.push(xml.slice(pos, lt));
72
69
  }
73
70
  }
74
71
  const c = xml.charCodeAt(lt + 1);
@@ -95,9 +92,7 @@ function parseXml(xml) {
95
92
  pos = end === -1 ? len : end + 3;
96
93
  } else if (xml.startsWith("<![CDATA[", lt)) {
97
94
  const end = xml.indexOf("]]>", lt + 9);
98
- let text = xml.slice(lt + 9, end === -1 ? len : end);
99
- if (INVALID_XML_CHARS_TEST.test(text)) text = text.replace(INVALID_XML_CHARS_RE, "");
100
- children.push(text);
95
+ children.push(xml.slice(lt + 9, end === -1 ? len : end));
101
96
  pos = end === -1 ? len : end + 3;
102
97
  } else {
103
98
  let i = lt + 2;
@@ -563,6 +558,7 @@ function parseCredits(elements) {
563
558
  if (a["letter-spacing"]) cw.letterSpacing = a["letter-spacing"];
564
559
  if (a["xml:lang"]) cw.xmlLang = a["xml:lang"];
565
560
  if (a["xml:space"]) cw.xmlSpace = a["xml:space"];
561
+ if (a["color"]) cw.color = a["color"];
566
562
  return cw;
567
563
  });
568
564
  const images = collectElements(content, "credit-image", (_c, a) => {
@@ -592,6 +588,7 @@ function parseDisplayTexts(elements) {
592
588
  if (a["font-style"]) dt.fontStyle = a["font-style"];
593
589
  if (a["font-weight"]) dt.fontWeight = a["font-weight"];
594
590
  if (a["xml:space"]) dt.xmlSpace = a["xml:space"];
591
+ if (a["color"]) dt.color = a["color"];
595
592
  return dt;
596
593
  });
597
594
  }
@@ -615,6 +612,7 @@ function parsePartList(elements) {
615
612
  if (pnAttrs["print-object"] === "no") {
616
613
  partInfo.namePrintObject = false;
617
614
  }
615
+ if (pnAttrs["color"]) partInfo.nameColor = pnAttrs["color"];
618
616
  break;
619
617
  }
620
618
  }
@@ -633,6 +631,7 @@ function parsePartList(elements) {
633
631
  if (paAttrs["print-object"] === "no") {
634
632
  partInfo.abbreviationPrintObject = false;
635
633
  }
634
+ if (paAttrs["color"]) partInfo.abbreviationColor = paAttrs["color"];
636
635
  break;
637
636
  }
638
637
  }
@@ -707,12 +706,18 @@ function parsePartList(elements) {
707
706
  groupType: attrs["type"] === "stop" ? "stop" : "start"
708
707
  };
709
708
  if (attrs["number"]) group.number = parseInt(attrs["number"], 10);
710
- const name = getElementText(content, "group-name");
711
- if (name) group.groupName = name;
709
+ parseFirstElement(content, "group-name", (c, a) => {
710
+ const name = extractText(c);
711
+ if (name) group.groupName = name;
712
+ if (a["color"]) group.groupNameColor = a["color"];
713
+ });
712
714
  const gnd = parseFirstElement(content, "group-name-display", (c) => parseDisplayTexts(c));
713
715
  if (gnd) group.groupNameDisplay = gnd;
714
- const abbr = getElementText(content, "group-abbreviation");
715
- if (abbr) group.groupAbbreviation = abbr;
716
+ parseFirstElement(content, "group-abbreviation", (c, a) => {
717
+ const abbr = extractText(c);
718
+ if (abbr) group.groupAbbreviation = abbr;
719
+ if (a["color"]) group.groupAbbreviationColor = a["color"];
720
+ });
716
721
  const gad = parseFirstElement(content, "group-abbreviation-display", (c) => parseDisplayTexts(c));
717
722
  if (gad) group.groupAbbreviationDisplay = gad;
718
723
  parseFirstElement(content, "group-symbol", (c, a) => {
@@ -721,6 +726,7 @@ function parsePartList(elements) {
721
726
  group.groupSymbol = symbol;
722
727
  }
723
728
  if (a["default-x"]) group.groupSymbolDefaultX = parseFloat(a["default-x"]);
729
+ if (a["color"]) group.groupSymbolColor = a["color"];
724
730
  });
725
731
  const barline = getElementText(content, "group-barline");
726
732
  if (barline && ["yes", "no", "Mensurstrich"].includes(barline)) {
@@ -878,6 +884,7 @@ function parseAttributes(elements, xmlAttrs = {}) {
878
884
  if (keyAttrs["number"]) key.number = parseInt(keyAttrs["number"], 10);
879
885
  if (keyAttrs["print-object"] === "no") key.printObject = false;
880
886
  else if (keyAttrs["print-object"] === "yes") key.printObject = true;
887
+ if (keyAttrs["color"]) key.color = keyAttrs["color"];
881
888
  keys.push(key);
882
889
  }
883
890
  }
@@ -954,6 +961,7 @@ function parseTimeSignature(elements, parentElements) {
954
961
  if (attrs["print-object"] === "no") {
955
962
  time.printObject = false;
956
963
  }
964
+ if (attrs["color"]) time.color = attrs["color"];
957
965
  break;
958
966
  }
959
967
  }
@@ -1008,6 +1016,9 @@ function parseClef(elements, attrs) {
1008
1016
  if (attrs["after-barline"] === "yes") {
1009
1017
  clef.afterBarline = true;
1010
1018
  }
1019
+ if (attrs["color"]) {
1020
+ clef.color = attrs["color"];
1021
+ }
1011
1022
  return clef;
1012
1023
  }
1013
1024
  function parseTranspose(elements) {
@@ -1037,6 +1048,7 @@ function parseNote(elements, attrs) {
1037
1048
  if (attrs["print-dot"] === "yes") note.printDot = true;
1038
1049
  if (attrs["print-spacing"] === "yes") note.printSpacing = true;
1039
1050
  if (attrs["print-spacing"] === "no") note.printSpacing = false;
1051
+ if (attrs["color"]) note.color = attrs["color"];
1040
1052
  let dotCount = 0;
1041
1053
  let notationsIndex = 0;
1042
1054
  let tieElements;
@@ -1069,6 +1081,7 @@ function parseNote(elements, attrs) {
1069
1081
  break;
1070
1082
  case "dot":
1071
1083
  dotCount++;
1084
+ if (elAttrs["color"] && note.dotColor === void 0) note.dotColor = elAttrs["color"];
1072
1085
  break;
1073
1086
  case "instrument":
1074
1087
  if (elAttrs["id"]) note.instrument = elAttrs["id"];
@@ -1106,6 +1119,7 @@ function parseNote(elements, attrs) {
1106
1119
  const noteType = extractText(c);
1107
1120
  if (isValidNoteType(noteType)) note.noteType = noteType;
1108
1121
  if (elAttrs["size"]) note.noteTypeSize = elAttrs["size"];
1122
+ if (elAttrs["color"]) note.noteTypeColor = elAttrs["color"];
1109
1123
  break;
1110
1124
  }
1111
1125
  case "accidental": {
@@ -1131,6 +1145,7 @@ function parseNote(elements, attrs) {
1131
1145
  note.stem = { value: stemValue };
1132
1146
  if (elAttrs["default-x"]) note.stem.defaultX = parseFloat(elAttrs["default-x"]);
1133
1147
  if (elAttrs["default-y"]) note.stem.defaultY = parseFloat(elAttrs["default-y"]);
1148
+ if (elAttrs["color"]) note.stem.color = elAttrs["color"];
1134
1149
  }
1135
1150
  break;
1136
1151
  }
@@ -1141,6 +1156,7 @@ function parseNote(elements, attrs) {
1141
1156
  if (elAttrs["filled"] === "yes") nhInfo.filled = true;
1142
1157
  else if (elAttrs["filled"] === "no") nhInfo.filled = false;
1143
1158
  if (elAttrs["parentheses"] === "yes") nhInfo.parentheses = true;
1159
+ if (elAttrs["color"]) nhInfo.color = elAttrs["color"];
1144
1160
  note.notehead = nhInfo;
1145
1161
  }
1146
1162
  break;
@@ -1240,6 +1256,7 @@ function parseBeam(elements, attrs) {
1240
1256
  if (attrs["fan"] && validFans.includes(attrs["fan"])) {
1241
1257
  beam.fan = attrs["fan"];
1242
1258
  }
1259
+ if (attrs["color"]) beam.color = attrs["color"];
1243
1260
  return beam;
1244
1261
  }
1245
1262
  function parseNotations(elements, notationsIndex = 0) {
@@ -1254,6 +1271,7 @@ function parseNotations(elements, notationsIndex = 0) {
1254
1271
  tiedType: attrs["type"] || "start",
1255
1272
  number: attrs["number"] ? parseInt(attrs["number"], 10) : void 0,
1256
1273
  orientation: attrs["orientation"],
1274
+ color: attrs["color"],
1257
1275
  notationsIndex
1258
1276
  };
1259
1277
  notations.push(tied);
@@ -1272,6 +1290,7 @@ function parseNotations(elements, notationsIndex = 0) {
1272
1290
  bezierX2: attrs["bezier-x2"] ? parseFloat(attrs["bezier-x2"]) : void 0,
1273
1291
  bezierY2: attrs["bezier-y2"] ? parseFloat(attrs["bezier-y2"]) : void 0,
1274
1292
  placement: attrs["placement"],
1293
+ color: attrs["color"],
1275
1294
  notationsIndex
1276
1295
  };
1277
1296
  notations.push(slur);
@@ -1366,6 +1385,7 @@ function parseNotations(elements, notationsIndex = 0) {
1366
1385
  if (artAttrs["default-y"]) {
1367
1386
  artNotation.defaultY = parseFloat(artAttrs["default-y"]);
1368
1387
  }
1388
+ if (artAttrs["color"]) artNotation.color = artAttrs["color"];
1369
1389
  notations.push(artNotation);
1370
1390
  }
1371
1391
  }
@@ -1387,7 +1407,11 @@ function parseNotations(elements, notationsIndex = 0) {
1387
1407
  ];
1388
1408
  const accidentalMarks = collectElements(ornContent, "accidental-mark", (c, a) => {
1389
1409
  const value = extractText(c);
1390
- return isValidAccidental(value) ? { value, placement: a["placement"] } : null;
1410
+ return isValidAccidental(value) ? {
1411
+ value,
1412
+ placement: a["placement"],
1413
+ color: a["color"]
1414
+ } : null;
1391
1415
  }).filter((m) => m !== null);
1392
1416
  for (const orn of ornContent) {
1393
1417
  if (typeof orn === "string") continue;
@@ -1403,6 +1427,7 @@ function parseNotations(elements, notationsIndex = 0) {
1403
1427
  if (ornAttrs["default-y"]) {
1404
1428
  ornNotation.defaultY = parseFloat(ornAttrs["default-y"]);
1405
1429
  }
1430
+ if (ornAttrs["color"]) ornNotation.color = ornAttrs["color"];
1406
1431
  if (accidentalMarks.length > 0 && notations.filter((n) => n.type === "ornament").length === 0) {
1407
1432
  ornNotation.accidentalMarks = accidentalMarks;
1408
1433
  }
@@ -1422,6 +1447,7 @@ function parseNotations(elements, notationsIndex = 0) {
1422
1447
  if (wlAttrs["default-y"]) {
1423
1448
  wlNotation.defaultY = parseFloat(wlAttrs["default-y"]);
1424
1449
  }
1450
+ if (wlAttrs["color"]) wlNotation.color = wlAttrs["color"];
1425
1451
  notations.push(wlNotation);
1426
1452
  }
1427
1453
  if (orn.tagName === "tremolo") {
@@ -1437,6 +1463,7 @@ function parseNotations(elements, notationsIndex = 0) {
1437
1463
  };
1438
1464
  if (tremAttrs["default-x"]) tremNotation.defaultX = parseFloat(tremAttrs["default-x"]);
1439
1465
  if (tremAttrs["default-y"]) tremNotation.defaultY = parseFloat(tremAttrs["default-y"]);
1466
+ if (tremAttrs["color"]) tremNotation.color = tremAttrs["color"];
1440
1467
  notations.push(tremNotation);
1441
1468
  }
1442
1469
  }
@@ -1499,6 +1526,7 @@ function parseNotations(elements, notationsIndex = 0) {
1499
1526
  if (hasElement(bendContent, "pre-bend")) techNotation.preBend = true;
1500
1527
  if (hasElement(bendContent, "release")) techNotation.release = true;
1501
1528
  if (hasElement(bendContent, "with-bar")) techNotation.withBar = true;
1529
+ if (techAttrs["color"]) techNotation.color = techAttrs["color"];
1502
1530
  notations.push(techNotation);
1503
1531
  }
1504
1532
  for (const techType of technicalTypes) {
@@ -1553,6 +1581,9 @@ function parseNotations(elements, notationsIndex = 0) {
1553
1581
  if (techAttrs["font-size"]) {
1554
1582
  notation.fontSize = techAttrs["font-size"];
1555
1583
  }
1584
+ if (techAttrs["color"]) {
1585
+ notation.color = techAttrs["color"];
1586
+ }
1556
1587
  notations.push(notation);
1557
1588
  }
1558
1589
  }
@@ -1605,6 +1636,7 @@ function parseNotations(elements, notationsIndex = 0) {
1605
1636
  notationsIndex
1606
1637
  };
1607
1638
  if (otherDynamics) dynNotation.otherDynamics = otherDynamics;
1639
+ if (dynAttrs["color"]) dynNotation.color = dynAttrs["color"];
1608
1640
  notations.push(dynNotation);
1609
1641
  }
1610
1642
  } else if (el.tagName === "fermata") {
@@ -1615,6 +1647,7 @@ function parseNotations(elements, notationsIndex = 0) {
1615
1647
  shape: shape || void 0,
1616
1648
  fermataType: a["type"],
1617
1649
  placement: a["placement"],
1650
+ color: a["color"],
1618
1651
  notationsIndex
1619
1652
  };
1620
1653
  if (a["default-x"]) fermataNotation.defaultX = parseFloat(a["default-x"]);
@@ -1630,6 +1663,7 @@ function parseNotations(elements, notationsIndex = 0) {
1630
1663
  };
1631
1664
  if (arpAttrs["default-x"]) arpNotation.defaultX = parseFloat(arpAttrs["default-x"]);
1632
1665
  if (arpAttrs["default-y"]) arpNotation.defaultY = parseFloat(arpAttrs["default-y"]);
1666
+ if (arpAttrs["color"]) arpNotation.color = arpAttrs["color"];
1633
1667
  notations.push(arpNotation);
1634
1668
  } else if (el.tagName === "non-arpeggiate") {
1635
1669
  const nonArpAttrs = el.attributes;
@@ -1638,6 +1672,7 @@ function parseNotations(elements, notationsIndex = 0) {
1638
1672
  nonArpeggiateType: nonArpAttrs["type"],
1639
1673
  number: nonArpAttrs["number"] ? parseInt(nonArpAttrs["number"], 10) : void 0,
1640
1674
  placement: nonArpAttrs["placement"],
1675
+ color: nonArpAttrs["color"],
1641
1676
  notationsIndex
1642
1677
  });
1643
1678
  } else if (el.tagName === "accidental-mark") {
@@ -1648,6 +1683,7 @@ function parseNotations(elements, notationsIndex = 0) {
1648
1683
  type: "accidental-mark",
1649
1684
  value,
1650
1685
  placement: amAttrs["placement"],
1686
+ color: amAttrs["color"],
1651
1687
  notationsIndex
1652
1688
  });
1653
1689
  } else if (el.tagName === "glissando") {
@@ -1666,6 +1702,7 @@ function parseNotations(elements, notationsIndex = 0) {
1666
1702
  number: glissAttrs["number"] ? parseInt(glissAttrs["number"], 10) : void 0,
1667
1703
  lineType: glissAttrs["line-type"],
1668
1704
  text,
1705
+ color: glissAttrs["color"],
1669
1706
  notationsIndex
1670
1707
  });
1671
1708
  } else if (el.tagName === "slide") {
@@ -1677,6 +1714,7 @@ function parseNotations(elements, notationsIndex = 0) {
1677
1714
  slideType: slideAttrs["type"] === "stop" ? "stop" : "start",
1678
1715
  number: slideAttrs["number"] ? parseInt(slideAttrs["number"], 10) : void 0,
1679
1716
  lineType: slideAttrs["line-type"],
1717
+ color: slideAttrs["color"],
1680
1718
  notationsIndex
1681
1719
  };
1682
1720
  if (slideText) slideNotation.text = slideText;
@@ -1704,12 +1742,14 @@ function parseLyric(elements, attrs) {
1704
1742
  }
1705
1743
  } else if (el.tagName === "text") {
1706
1744
  const content = el.children;
1745
+ const textColor = el.attributes["color"];
1707
1746
  let foundText = false;
1708
1747
  for (const item of content) {
1709
1748
  if (typeof item === "string") {
1710
1749
  textElements.push({
1711
1750
  text: item,
1712
- syllabic: currentSyllabic
1751
+ syllabic: currentSyllabic,
1752
+ color: textColor
1713
1753
  });
1714
1754
  currentSyllabic = void 0;
1715
1755
  foundText = true;
@@ -1719,7 +1759,8 @@ function parseLyric(elements, attrs) {
1719
1759
  if (!foundText) {
1720
1760
  textElements.push({
1721
1761
  text: "",
1722
- syllabic: currentSyllabic
1762
+ syllabic: currentSyllabic,
1763
+ color: textColor
1723
1764
  });
1724
1765
  currentSyllabic = void 0;
1725
1766
  }
@@ -1748,9 +1789,15 @@ function parseLyric(elements, attrs) {
1748
1789
  if (attrs["placement"]) {
1749
1790
  lyric.placement = attrs["placement"];
1750
1791
  }
1792
+ if (attrs["color"]) {
1793
+ lyric.color = attrs["color"];
1794
+ }
1751
1795
  if (textElements.length > 0 && textElements[0].syllabic) {
1752
1796
  lyric.syllabic = textElements[0].syllabic;
1753
1797
  }
1798
+ if (textElements.length > 0 && textElements[0].color) {
1799
+ lyric.textColor = textElements[0].color;
1800
+ }
1754
1801
  if (textElements.length > 1) {
1755
1802
  lyric.textElements = textElements;
1756
1803
  }
@@ -1762,8 +1809,9 @@ function parseLyric(elements, attrs) {
1762
1809
  if (el.tagName === "extend") {
1763
1810
  const extendAttrs = el.attributes;
1764
1811
  const extendType = extendAttrs["type"];
1765
- if (extendType) {
1766
- lyric.extend = { type: extendType };
1812
+ const extendColor = extendAttrs["color"];
1813
+ if (extendType || extendColor) {
1814
+ lyric.extend = { type: extendType, color: extendColor };
1767
1815
  } else {
1768
1816
  lyric.extend = true;
1769
1817
  }
@@ -1905,6 +1953,7 @@ function parseDirectionTypes(elements) {
1905
1953
  if (dynAttrs["default-y"]) result.defaultY = parseFloat(dynAttrs["default-y"]);
1906
1954
  if (dynAttrs["relative-x"]) result.relativeX = parseFloat(dynAttrs["relative-x"]);
1907
1955
  if (dynAttrs["halign"]) result.halign = dynAttrs["halign"];
1956
+ if (dynAttrs["color"]) result.color = dynAttrs["color"];
1908
1957
  results.push(result);
1909
1958
  foundStandard = true;
1910
1959
  break;
@@ -1918,6 +1967,7 @@ function parseDirectionTypes(elements) {
1918
1967
  if (dynAttrs["default-y"]) result.defaultY = parseFloat(dynAttrs["default-y"]);
1919
1968
  if (dynAttrs["relative-x"]) result.relativeX = parseFloat(dynAttrs["relative-x"]);
1920
1969
  if (dynAttrs["halign"]) result.halign = dynAttrs["halign"];
1970
+ if (dynAttrs["color"]) result.color = dynAttrs["color"];
1921
1971
  results.push(result);
1922
1972
  }
1923
1973
  }
@@ -1932,6 +1982,7 @@ function parseDirectionTypes(elements) {
1932
1982
  if (wedgeAttrs["spread"]) result.spread = parseFloat(wedgeAttrs["spread"]);
1933
1983
  if (wedgeAttrs["default-y"]) result.defaultY = parseFloat(wedgeAttrs["default-y"]);
1934
1984
  if (wedgeAttrs["relative-x"]) result.relativeX = parseFloat(wedgeAttrs["relative-x"]);
1985
+ if (wedgeAttrs["color"]) result.color = wedgeAttrs["color"];
1935
1986
  results.push(result);
1936
1987
  }
1937
1988
  continue;
@@ -1976,6 +2027,7 @@ function parseDirectionTypes(elements) {
1976
2027
  if (metAttrs["default-y"]) result.defaultY = parseFloat(metAttrs["default-y"]);
1977
2028
  if (metAttrs["font-family"]) result.fontFamily = metAttrs["font-family"];
1978
2029
  if (metAttrs["font-size"]) result.fontSize = metAttrs["font-size"];
2030
+ if (metAttrs["color"]) result.color = metAttrs["color"];
1979
2031
  results.push(result);
1980
2032
  }
1981
2033
  continue;
@@ -2010,6 +2062,7 @@ function parseDirectionTypes(elements) {
2010
2062
  if (a["default-y"]) result.defaultY = parseFloat(a["default-y"]);
2011
2063
  if (a["font-size"]) result.fontSize = a["font-size"];
2012
2064
  if (a["font-weight"]) result.fontWeight = a["font-weight"];
2065
+ if (a["color"]) result.color = a["color"];
2013
2066
  results.push(result);
2014
2067
  }
2015
2068
  continue;
@@ -2024,6 +2077,7 @@ function parseDirectionTypes(elements) {
2024
2077
  if (bracketAttrs["line-type"]) result.lineType = bracketAttrs["line-type"];
2025
2078
  if (bracketAttrs["default-y"]) result.defaultY = parseFloat(bracketAttrs["default-y"]);
2026
2079
  if (bracketAttrs["relative-x"]) result.relativeX = parseFloat(bracketAttrs["relative-x"]);
2080
+ if (bracketAttrs["color"]) result.color = bracketAttrs["color"];
2027
2081
  results.push(result);
2028
2082
  }
2029
2083
  continue;
@@ -2037,13 +2091,16 @@ function parseDirectionTypes(elements) {
2037
2091
  if (dashAttrs["dash-length"]) result.dashLength = parseFloat(dashAttrs["dash-length"]);
2038
2092
  if (dashAttrs["default-y"]) result.defaultY = parseFloat(dashAttrs["default-y"]);
2039
2093
  if (dashAttrs["space-length"]) result.spaceLength = parseFloat(dashAttrs["space-length"]);
2094
+ if (dashAttrs["color"]) result.color = dashAttrs["color"];
2040
2095
  results.push(result);
2041
2096
  }
2042
2097
  continue;
2043
2098
  }
2044
2099
  if (el.tagName === "accordion-registration") {
2045
2100
  const accContent = el.children;
2101
+ const accRegAttrs = el.attributes;
2046
2102
  const result = { kind: "accordion-registration" };
2103
+ if (accRegAttrs["color"]) result.color = accRegAttrs["color"];
2047
2104
  for (const acc of accContent) {
2048
2105
  if (typeof acc === "string") continue;
2049
2106
  if (acc.tagName === "accordion-high") {
@@ -2076,34 +2133,23 @@ function parseDirectionTypes(elements) {
2076
2133
  if (otherAttrs["default-y"]) result.defaultY = parseFloat(otherAttrs["default-y"]);
2077
2134
  if (otherAttrs["halign"]) result.halign = otherAttrs["halign"];
2078
2135
  if (otherAttrs["print-object"] === "no") result.printObject = false;
2136
+ if (otherAttrs["color"]) result.color = otherAttrs["color"];
2079
2137
  results.push(result);
2080
2138
  break;
2081
2139
  }
2082
2140
  }
2083
2141
  continue;
2084
2142
  }
2085
- if (el.tagName === "segno") {
2086
- results.push({ kind: "segno" });
2087
- continue;
2088
- }
2089
- if (el.tagName === "coda") {
2090
- results.push({ kind: "coda" });
2091
- continue;
2092
- }
2093
- if (el.tagName === "eyeglasses") {
2094
- results.push({ kind: "eyeglasses" });
2095
- continue;
2096
- }
2097
- if (el.tagName === "damp") {
2098
- results.push({ kind: "damp" });
2099
- continue;
2100
- }
2101
- if (el.tagName === "damp-all") {
2102
- results.push({ kind: "damp-all" });
2143
+ if (el.tagName === "segno" || el.tagName === "coda" || el.tagName === "eyeglasses" || el.tagName === "damp" || el.tagName === "damp-all") {
2144
+ const markerColor = el.attributes["color"];
2145
+ const result = { kind: el.tagName };
2146
+ if (markerColor) result.color = markerColor;
2147
+ results.push(result);
2103
2148
  continue;
2104
2149
  }
2105
2150
  if (el.tagName === "scordatura") {
2106
2151
  const scordContent = el.children;
2152
+ const scordColor = el.attributes["color"];
2107
2153
  const accords = [];
2108
2154
  for (const sc of scordContent) {
2109
2155
  if (typeof sc === "string") continue;
@@ -2124,11 +2170,12 @@ function parseDirectionTypes(elements) {
2124
2170
  }
2125
2171
  }
2126
2172
  }
2127
- results.push({ kind: "scordatura", accords: accords.length > 0 ? accords : void 0 });
2173
+ results.push({ kind: "scordatura", accords: accords.length > 0 ? accords : void 0, color: scordColor });
2128
2174
  continue;
2129
2175
  }
2130
2176
  if (el.tagName === "harp-pedals") {
2131
2177
  const harpContent = el.children;
2178
+ const harpColor = el.attributes["color"];
2132
2179
  const pedalTunings = [];
2133
2180
  for (const hp of harpContent) {
2134
2181
  if (typeof hp === "string") continue;
@@ -2144,7 +2191,7 @@ function parseDirectionTypes(elements) {
2144
2191
  }
2145
2192
  }
2146
2193
  }
2147
- results.push({ kind: "harp-pedals", pedalTunings: pedalTunings.length > 0 ? pedalTunings : void 0 });
2194
+ results.push({ kind: "harp-pedals", pedalTunings: pedalTunings.length > 0 ? pedalTunings : void 0, color: harpColor });
2148
2195
  continue;
2149
2196
  }
2150
2197
  if (el.tagName === "image") {
@@ -2166,6 +2213,7 @@ function parseDirectionTypes(elements) {
2166
2213
  if (pedalAttrs["default-y"]) result.defaultY = parseFloat(pedalAttrs["default-y"]);
2167
2214
  if (pedalAttrs["relative-x"]) result.relativeX = parseFloat(pedalAttrs["relative-x"]);
2168
2215
  if (pedalAttrs["halign"]) result.halign = pedalAttrs["halign"];
2216
+ if (pedalAttrs["color"]) result.color = pedalAttrs["color"];
2169
2217
  results.push(result);
2170
2218
  }
2171
2219
  continue;
@@ -2176,6 +2224,7 @@ function parseDirectionTypes(elements) {
2176
2224
  if (shiftType === "up" || shiftType === "down" || shiftType === "stop") {
2177
2225
  const result = { kind: "octave-shift", type: shiftType };
2178
2226
  if (shiftAttrs["size"]) result.size = parseInt(shiftAttrs["size"], 10);
2227
+ if (shiftAttrs["color"]) result.color = shiftAttrs["color"];
2179
2228
  results.push(result);
2180
2229
  }
2181
2230
  continue;
@@ -2222,10 +2271,13 @@ function parseDirectionTypes(elements) {
2222
2271
  function parseBarline(elements, attrs) {
2223
2272
  const location = attrs["location"] || "right";
2224
2273
  const barline = { _id: attrs["id"] || generateId(), location };
2225
- const barStyle = getElementText(elements, "bar-style");
2226
- if (barStyle && isValidBarStyle(barStyle)) {
2227
- barline.barStyle = barStyle;
2228
- }
2274
+ parseFirstElement(elements, "bar-style", (c, a) => {
2275
+ const barStyle = extractText(c);
2276
+ if (barStyle && isValidBarStyle(barStyle)) {
2277
+ barline.barStyle = barStyle;
2278
+ if (a["color"]) barline.barStyleColor = a["color"];
2279
+ }
2280
+ });
2229
2281
  for (const el of elements) {
2230
2282
  if (typeof el === "string") continue;
2231
2283
  if (el.tagName === "repeat") {
@@ -2251,6 +2303,7 @@ function parseBarline(elements, attrs) {
2251
2303
  if (endingText) barline.ending.text = endingText;
2252
2304
  if (endingAttrs["default-y"]) barline.ending.defaultY = parseFloat(endingAttrs["default-y"]);
2253
2305
  if (endingAttrs["end-length"]) barline.ending.endLength = parseFloat(endingAttrs["end-length"]);
2306
+ if (endingAttrs["color"]) barline.ending.color = endingAttrs["color"];
2254
2307
  }
2255
2308
  }
2256
2309
  }
@@ -2428,6 +2481,7 @@ function parseStaffDetails(elements, attrs) {
2428
2481
  function parseMeasureStyle(elements, attrs) {
2429
2482
  const ms = {};
2430
2483
  if (attrs["number"]) ms.number = parseInt(attrs["number"], 10);
2484
+ if (attrs["color"]) ms.color = attrs["color"];
2431
2485
  const multipleRest = getElementText(elements, "multiple-rest");
2432
2486
  if (multipleRest) ms.multipleRest = parseInt(multipleRest, 10);
2433
2487
  for (const el of elements) {
@@ -2469,6 +2523,7 @@ function parseHarmony(elements, attrs) {
2469
2523
  if (attrs["default-y"]) harmony.defaultY = parseFloat(attrs["default-y"]);
2470
2524
  if (attrs["halign"]) harmony.halign = attrs["halign"];
2471
2525
  if (attrs["font-size"]) harmony.fontSize = attrs["font-size"];
2526
+ if (attrs["color"]) harmony.color = attrs["color"];
2472
2527
  const root = getElementContent(elements, "root");
2473
2528
  if (root) {
2474
2529
  const rootStep = getElementText(root, "root-step");
@@ -2489,6 +2544,7 @@ function parseHarmony(elements, attrs) {
2489
2544
  }
2490
2545
  if (kindAttrs["text"] !== void 0) harmony.kindText = kindAttrs["text"];
2491
2546
  if (kindAttrs["halign"]) harmony.kindHalign = kindAttrs["halign"];
2547
+ if (kindAttrs["color"]) harmony.kindColor = kindAttrs["color"];
2492
2548
  break;
2493
2549
  }
2494
2550
  }
@@ -2583,6 +2639,8 @@ function parseHarmony(elements, attrs) {
2583
2639
  }
2584
2640
  }
2585
2641
  if (frameNotes.length > 0) frameObj.frameNotes = frameNotes;
2642
+ const frameColor = parseFirstElement(elements, "frame", (_c, a) => a["color"]);
2643
+ if (frameColor) frameObj.color = frameColor;
2586
2644
  if (Object.keys(frameObj).length > 0) harmony.frame = frameObj;
2587
2645
  }
2588
2646
  const offset = getElementText(elements, "offset");
@@ -2598,6 +2656,7 @@ function parseFiguredBass(elements, attrs) {
2598
2656
  figures: []
2599
2657
  };
2600
2658
  if (attrs["parentheses"] === "yes") fb.parentheses = true;
2659
+ if (attrs["color"]) fb.color = attrs["color"];
2601
2660
  const duration = getElementText(elements, "duration");
2602
2661
  if (duration) fb.duration = parseInt(duration, 10);
2603
2662
  for (const el of elements) {
@@ -2628,8 +2687,9 @@ function parseFiguredBass(elements, attrs) {
2628
2687
  } else if (figEl.tagName === "extend") {
2629
2688
  const extendAttrs = figEl.attributes;
2630
2689
  const extendType = extendAttrs["type"];
2631
- if (extendType) {
2632
- figure.extend = { type: extendType };
2690
+ const extendColor = extendAttrs["color"];
2691
+ if (extendType || extendColor) {
2692
+ figure.extend = { type: extendType, color: extendColor };
2633
2693
  } else {
2634
2694
  figure.extend = true;
2635
2695
  }
@@ -2760,6 +2820,148 @@ function parseAuto(data) {
2760
2820
  return parse(xmlString);
2761
2821
  }
2762
2822
 
2823
+ // src/abc-decorations.ts
2824
+ var ABC_BODY_FIELD_MARKER = "__abc_field__";
2825
+ var DECORATIONS = [
2826
+ // --- Articulations ---------------------------------------------------
2827
+ { spellings: [".", "staccato"], canonical: ".", target: { kind: "articulation", articulation: "staccato" } },
2828
+ { spellings: ["L", "accent", "emphasis", ">"], canonical: "L", target: { kind: "articulation", articulation: "accent" } },
2829
+ { spellings: ["marcato"], canonical: "!marcato!", target: { kind: "articulation", articulation: "strong-accent" } },
2830
+ { spellings: ["tenuto"], canonical: "!tenuto!", target: { kind: "articulation", articulation: "tenuto" } },
2831
+ { spellings: ["breath"], canonical: "!breath!", target: { kind: "articulation", articulation: "breath-mark" } },
2832
+ // --- Ornaments -------------------------------------------------------
2833
+ { spellings: ["T", "trill"], canonical: "T", target: { kind: "ornament", ornament: "trill-mark" } },
2834
+ { spellings: ["M", "lowermordent", "mordent"], canonical: "M", target: { kind: "ornament", ornament: "mordent" } },
2835
+ { spellings: ["P", "uppermordent", "pralltriller"], canonical: "P", target: { kind: "ornament", ornament: "inverted-mordent" } },
2836
+ { spellings: ["turn", "gruppetto"], canonical: "!turn!", target: { kind: "ornament", ornament: "turn" } },
2837
+ { spellings: ["invertedturn"], canonical: "!invertedturn!", target: { kind: "ornament", ornament: "inverted-turn" } },
2838
+ { spellings: ["trill("], canonical: "!trill(!", target: { kind: "trill-line", wavyLineType: "start" } },
2839
+ { spellings: ["trill)"], canonical: "!trill)!", target: { kind: "trill-line", wavyLineType: "stop" } },
2840
+ { spellings: ["arpeggio"], canonical: "!arpeggio!", target: { kind: "arpeggiate" } },
2841
+ // --- Technical -------------------------------------------------------
2842
+ { spellings: ["u", "upbow"], canonical: "u", target: { kind: "technical", technical: "up-bow" } },
2843
+ { spellings: ["v", "downbow"], canonical: "v", target: { kind: "technical", technical: "down-bow" } },
2844
+ { spellings: ["open"], canonical: "!open!", target: { kind: "technical", technical: "open-string" } },
2845
+ { spellings: ["thumb"], canonical: "!thumb!", target: { kind: "technical", technical: "thumb-position" } },
2846
+ { spellings: ["snap"], canonical: "!snap!", target: { kind: "technical", technical: "snap-pizzicato" } },
2847
+ { spellings: ["+", "plus"], canonical: "!plus!", target: { kind: "technical", technical: "stopped" } },
2848
+ // --- Fermatas --------------------------------------------------------
2849
+ { spellings: ["H", "fermata"], canonical: "H", target: { kind: "fermata" } },
2850
+ { spellings: ["invertedfermata"], canonical: "!invertedfermata!", target: { kind: "fermata", inverted: true } },
2851
+ // --- Wedges (hairpins) -----------------------------------------------
2852
+ { spellings: ["crescendo(", "<("], canonical: "!crescendo(!", target: { kind: "wedge", wedge: "crescendo", stop: false } },
2853
+ { spellings: ["crescendo)", "<)"], canonical: "!crescendo)!", target: { kind: "wedge", wedge: "crescendo", stop: true } },
2854
+ { spellings: ["diminuendo(", ">("], canonical: "!diminuendo(!", target: { kind: "wedge", wedge: "diminuendo", stop: false } },
2855
+ { spellings: ["diminuendo)", ">)"], canonical: "!diminuendo)!", target: { kind: "wedge", wedge: "diminuendo", stop: true } },
2856
+ // --- Navigation marks ------------------------------------------------
2857
+ { spellings: ["S", "segno"], canonical: "S", target: { kind: "segno" } },
2858
+ { spellings: ["O", "coda"], canonical: "O", target: { kind: "coda" } },
2859
+ { spellings: ["D.S."], canonical: "!D.S.!", target: { kind: "words", text: "D.S." } },
2860
+ { spellings: ["D.C."], canonical: "!D.C.!", target: { kind: "words", text: "D.C." } },
2861
+ { spellings: ["dacoda"], canonical: "!dacoda!", target: { kind: "words", text: "Da Coda" } },
2862
+ { spellings: ["dacapo"], canonical: "!dacapo!", target: { kind: "words", text: "Da Capo" } },
2863
+ { spellings: ["fine"], canonical: "!fine!", target: { kind: "words", text: "Fine" } }
2864
+ ];
2865
+ for (let n = 0; n <= 5; n++) {
2866
+ DECORATIONS.push({
2867
+ spellings: [String(n)],
2868
+ canonical: `!${n}!`,
2869
+ target: { kind: "fingering", fingering: String(n) }
2870
+ });
2871
+ }
2872
+ var ABC_DYNAMICS = /* @__PURE__ */ new Set([
2873
+ "pppppp",
2874
+ "ppppp",
2875
+ "pppp",
2876
+ "ppp",
2877
+ "pp",
2878
+ "p",
2879
+ "mp",
2880
+ "mf",
2881
+ "f",
2882
+ "ff",
2883
+ "fff",
2884
+ "ffff",
2885
+ "fffff",
2886
+ "ffffff",
2887
+ "sf",
2888
+ "sfz",
2889
+ "sffz",
2890
+ "sfp",
2891
+ "sfpp",
2892
+ "fp",
2893
+ "rf",
2894
+ "rfz",
2895
+ "fz",
2896
+ "pf"
2897
+ ]);
2898
+ var ABC_SHORTHAND_DECORATIONS = /* @__PURE__ */ new Set([
2899
+ ".",
2900
+ "~",
2901
+ "H",
2902
+ "J",
2903
+ "L",
2904
+ "M",
2905
+ "O",
2906
+ "P",
2907
+ "R",
2908
+ "S",
2909
+ "T",
2910
+ "u",
2911
+ "v"
2912
+ ]);
2913
+ var BY_SPELLING = /* @__PURE__ */ new Map();
2914
+ for (const def of DECORATIONS) {
2915
+ for (const spelling of def.spellings) {
2916
+ BY_SPELLING.set(spelling, def);
2917
+ }
2918
+ }
2919
+ function targetKey(target) {
2920
+ switch (target.kind) {
2921
+ case "articulation":
2922
+ return `articulation:${target.articulation}`;
2923
+ case "ornament":
2924
+ return `ornament:${target.ornament}`;
2925
+ case "technical":
2926
+ return `technical:${target.technical}`;
2927
+ case "fingering":
2928
+ return `fingering:${target.fingering}`;
2929
+ case "fermata":
2930
+ return `fermata:${target.inverted ? "inverted" : "upright"}`;
2931
+ case "arpeggiate":
2932
+ return "arpeggiate";
2933
+ case "trill-line":
2934
+ return `trill-line:${target.wavyLineType}`;
2935
+ case "dynamics":
2936
+ return `dynamics:${target.value}`;
2937
+ case "wedge":
2938
+ return `wedge:${target.wedge}:${target.stop ? "stop" : "start"}`;
2939
+ case "segno":
2940
+ return "segno";
2941
+ case "coda":
2942
+ return "coda";
2943
+ case "words":
2944
+ return `words:${target.text}`;
2945
+ }
2946
+ }
2947
+ var CANONICAL_BY_TARGET = /* @__PURE__ */ new Map();
2948
+ for (const def of DECORATIONS) {
2949
+ CANONICAL_BY_TARGET.set(targetKey(def.target), def.canonical);
2950
+ }
2951
+ function lookupAbcDecoration(spelling) {
2952
+ if (ABC_DYNAMICS.has(spelling)) {
2953
+ return { kind: "dynamics", value: spelling };
2954
+ }
2955
+ const def = BY_SPELLING.get(spelling);
2956
+ return def ? def.target : null;
2957
+ }
2958
+ function abcDecorationFor(target) {
2959
+ if (target.kind === "dynamics") {
2960
+ return ABC_DYNAMICS.has(target.value) ? `!${target.value}!` : null;
2961
+ }
2962
+ return CANONICAL_BY_TARGET.get(targetKey(target)) ?? null;
2963
+ }
2964
+
2763
2965
  // src/importers/abc.ts
2764
2966
  var DIVISIONS = 960;
2765
2967
  var NOTE_TYPE_MAP = {
@@ -2774,6 +2976,7 @@ var NOTE_TYPE_MAP = {
2774
2976
  0.125: "32nd",
2775
2977
  0.0625: "64th"
2776
2978
  };
2979
+ var NOTE_TYPE_ENTRIES = Object.entries(NOTE_TYPE_MAP).map(([quarterNotes, type]) => [parseFloat(quarterNotes), type]);
2777
2980
  var KEY_FIFTHS = {
2778
2981
  "Cb": -7,
2779
2982
  "Gb": -6,
@@ -2835,38 +3038,38 @@ var MODE_NAME = {
2835
3038
  "locrian": "locrian",
2836
3039
  "loc": "locrian"
2837
3040
  };
2838
- var DYNAMICS_VALUES = /* @__PURE__ */ new Set([
2839
- "pppppp",
2840
- "ppppp",
2841
- "pppp",
2842
- "ppp",
2843
- "pp",
2844
- "p",
2845
- "mp",
2846
- "mf",
2847
- "f",
2848
- "ff",
2849
- "fff",
2850
- "ffff",
2851
- "fffff",
2852
- "ffffff",
2853
- "sf",
2854
- "sfz",
2855
- "sffz",
2856
- "sfp",
2857
- "sfpp",
2858
- "fp",
2859
- "rf",
2860
- "rfz",
2861
- "fz",
2862
- "pf"
2863
- ]);
3041
+ var DYNAMICS_VALUES = ABC_DYNAMICS;
3042
+ var ENDING_SPEC = /^(\d+(?:[,-]\d+)*)/;
3043
+ var MICROTONE_ACCIDENTALS = {
3044
+ "0.5": "quarter-sharp",
3045
+ "-0.5": "quarter-flat",
3046
+ "1.5": "three-quarters-sharp",
3047
+ "-1.5": "three-quarters-flat"
3048
+ };
3049
+ function appendToHeaderField(header, field, text) {
3050
+ switch (field) {
3051
+ case "T":
3052
+ header.title = (header.title ?? "") + text;
3053
+ break;
3054
+ case "C":
3055
+ header.composer = (header.composer ?? "") + text;
3056
+ break;
3057
+ default: {
3058
+ const extra = header.extraFields;
3059
+ if (extra && extra.length > 0 && extra[extra.length - 1].field === field) {
3060
+ extra[extra.length - 1].value += text;
3061
+ }
3062
+ break;
3063
+ }
3064
+ }
3065
+ }
2864
3066
  function parseHeader(lines) {
2865
3067
  const header = { voices: [], extraFields: [], directives: [] };
2866
3068
  let bodyStartIndex = 0;
2867
3069
  let foundKey = false;
2868
3070
  let postKHeaderDone = false;
2869
3071
  const headerFieldOrder = [];
3072
+ let lastField = null;
2870
3073
  for (let i = 0; i < lines.length; i++) {
2871
3074
  const line = lines[i].trim();
2872
3075
  if (line === "") continue;
@@ -2890,10 +3093,18 @@ function parseHeader(lines) {
2890
3093
  }
2891
3094
  continue;
2892
3095
  }
2893
- const fieldMatch = line.match(/^([A-Za-z]):\s*(.*)/);
3096
+ const continuationMatch = line.match(/^\+:(.*)$/);
3097
+ if (continuationMatch && !foundKey && lastField) {
3098
+ appendToHeaderField(header, lastField, continuationMatch[1]);
3099
+ headerFieldOrder.push(line);
3100
+ continue;
3101
+ }
3102
+ const fieldMatch = line.match(/^([A-Za-z]):(.*)/);
2894
3103
  const postKFields = /* @__PURE__ */ new Set(["I", "N"]);
2895
3104
  if (fieldMatch && (!foundKey || fieldMatch[1] === "V" || foundKey && postKFields.has(fieldMatch[1]))) {
2896
- const [, field, value] = fieldMatch;
3105
+ const [, field, rawFieldValue] = fieldMatch;
3106
+ const value = rawFieldValue.trimStart();
3107
+ lastField = field;
2897
3108
  if (foundKey && field !== "V") {
2898
3109
  if (!postKHeaderDone) {
2899
3110
  header.extraFields.push({ field, value: value.trim() });
@@ -2929,6 +3140,7 @@ function parseHeader(lines) {
2929
3140
  break;
2930
3141
  case "K":
2931
3142
  header.key = value.trim();
3143
+ header.clef = line;
2932
3144
  foundKey = true;
2933
3145
  bodyStartIndex = i + 1;
2934
3146
  headerFieldOrder.push(line);
@@ -2939,6 +3151,7 @@ function parseHeader(lines) {
2939
3151
  const nameMatch = voiceValue.match(/name=["']?([^"'\s]+)["']?/i);
2940
3152
  const nmMatch = voiceValue.match(/nm=["']([^"']*)["']/i);
2941
3153
  const clefMatch = voiceValue.match(/clef=(\S+)/i);
3154
+ const octaveMatch = voiceValue.match(/octave\s*=\s*(-?\d+)/i);
2942
3155
  const displayName = nameMatch ? nameMatch[1] : nmMatch ? nmMatch[1] : voiceId;
2943
3156
  const hasParams = clefMatch || nameMatch || nmMatch || /\b(Program|merge|up|down|bass|treble|alto|tenor|soprano|octave|snm|stem)\b/i.test(voiceValue);
2944
3157
  const isBodyVoiceSwitch = foundKey && !hasParams;
@@ -2949,12 +3162,14 @@ function parseHeader(lines) {
2949
3162
  if (existingVoice) {
2950
3163
  if (nameMatch || nmMatch) existingVoice.name = displayName;
2951
3164
  if (clefMatch) existingVoice.clef = clefMatch[1];
3165
+ if (octaveMatch) existingVoice.octave = parseInt(octaveMatch[1], 10);
2952
3166
  if (foundKey && !isBodyVoiceSwitch) existingVoice.fullLine = lines[i];
2953
3167
  } else {
2954
3168
  header.voices.push({
2955
3169
  id: voiceId,
2956
3170
  name: displayName,
2957
3171
  clef: clefMatch ? clefMatch[1] : void 0,
3172
+ octave: octaveMatch ? parseInt(octaveMatch[1], 10) : void 0,
2958
3173
  fullLine: lines[i]
2959
3174
  });
2960
3175
  }
@@ -3020,12 +3235,29 @@ function parseTimeSignature2(meterStr) {
3020
3235
  if (trimmed === "C|" || trimmed.toLowerCase() === "cut") {
3021
3236
  return { beats: "2", beatType: 2, symbol: "cut" };
3022
3237
  }
3238
+ if (trimmed.toLowerCase() === "none") {
3239
+ return { beats: "4", beatType: 4, senzaMisura: true };
3240
+ }
3023
3241
  const match = trimmed.match(/^(\d+)\/(\d+)$/);
3024
3242
  if (match) {
3025
3243
  return { beats: match[1], beatType: parseInt(match[2], 10) };
3026
3244
  }
3245
+ const additive = trimmed.match(/^\(?\s*(\d+(?:\s*\+\s*\d+)+)\s*\)?\/(\d+)$/);
3246
+ if (additive) {
3247
+ const groups = additive[1].split("+").map((g) => parseInt(g.trim(), 10));
3248
+ return {
3249
+ beats: groups.join("+"),
3250
+ beatType: parseInt(additive[2], 10),
3251
+ beatsList: groups
3252
+ };
3253
+ }
3027
3254
  return { beats: "4", beatType: 4 };
3028
3255
  }
3256
+ function timeSignatureWholeNotes(time) {
3257
+ const beats = time.beatsList ? time.beatsList.reduce((sum, b) => sum + b, 0) : parseInt(time.beats, 10);
3258
+ if (!Number.isFinite(beats) || time.beatType === 0) return 1;
3259
+ return beats / time.beatType;
3260
+ }
3029
3261
  function parseUnitNoteLength(lengthStr, meterStr) {
3030
3262
  if (lengthStr) {
3031
3263
  const match = lengthStr.trim().match(/^(\d+)\/(\d+)$/);
@@ -3033,12 +3265,9 @@ function parseUnitNoteLength(lengthStr, meterStr) {
3033
3265
  return { num: parseInt(match[1], 10), den: parseInt(match[2], 10) };
3034
3266
  }
3035
3267
  }
3036
- if (meterStr) {
3037
- const mMatch = meterStr.trim().match(/^(\d+)\/(\d+)$/);
3038
- if (mMatch) {
3039
- const ratio = parseInt(mMatch[1], 10) / parseInt(mMatch[2], 10);
3040
- return ratio >= 0.75 ? { num: 1, den: 8 } : { num: 1, den: 16 };
3041
- }
3268
+ if (meterStr && meterStr.trim() !== "") {
3269
+ const ratio = timeSignatureWholeNotes(parseTimeSignature2(meterStr));
3270
+ return ratio >= 0.75 ? { num: 1, den: 8 } : { num: 1, den: 16 };
3042
3271
  }
3043
3272
  return { num: 1, den: 8 };
3044
3273
  }
@@ -3048,8 +3277,7 @@ function lengthToDuration(num, den, unitNote) {
3048
3277
  }
3049
3278
  function durationToNoteType(duration) {
3050
3279
  const quarterNotes = duration / DIVISIONS;
3051
- for (const [qnStr, type] of Object.entries(NOTE_TYPE_MAP)) {
3052
- const qn = parseFloat(qnStr);
3280
+ for (const [qn, type] of NOTE_TYPE_ENTRIES) {
3053
3281
  if (Math.abs(quarterNotes - qn) < 1e-3) {
3054
3282
  return { noteType: type, dots: 0 };
3055
3283
  }
@@ -3062,8 +3290,8 @@ function durationToNoteType(duration) {
3062
3290
  }
3063
3291
  let bestType = "quarter";
3064
3292
  let bestDiff = Infinity;
3065
- for (const [qnStr, type] of Object.entries(NOTE_TYPE_MAP)) {
3066
- const diff = Math.abs(quarterNotes - parseFloat(qnStr));
3293
+ for (const [qn, type] of NOTE_TYPE_ENTRIES) {
3294
+ const diff = Math.abs(quarterNotes - qn);
3067
3295
  if (diff < bestDiff) {
3068
3296
  bestDiff = diff;
3069
3297
  bestType = type;
@@ -3165,18 +3393,18 @@ function tokenizeBody(bodyLines) {
3165
3393
  isContinuation = false;
3166
3394
  continue;
3167
3395
  }
3168
- const lyricsMatch = line.match(/^w:\s*(.*)/);
3396
+ const lyricsMatch = line.match(/^w:(.*)/);
3169
3397
  if (lyricsMatch) {
3170
3398
  const syllables = parseLyricLine(lyricsMatch[1]);
3171
3399
  voiceTokens.get(currentVoice).push({
3172
3400
  type: "lyrics",
3173
- value: lyricsMatch[1],
3401
+ value: lyricsMatch[1].trimStart(),
3174
3402
  syllables
3175
3403
  });
3176
3404
  isContinuation = false;
3177
3405
  continue;
3178
3406
  }
3179
- const bodyKeyMatch = line.match(/^K:\s*(.*)/);
3407
+ const bodyKeyMatch = line.match(/^K:(.*)/);
3180
3408
  if (bodyKeyMatch) {
3181
3409
  const currentTokens2 = voiceTokens.get(currentVoice);
3182
3410
  if (currentTokens2.length > 0) {
@@ -3185,12 +3413,17 @@ function tokenizeBody(bodyLines) {
3185
3413
  currentTokens2.push({ type: "line_break", value: "\n" });
3186
3414
  }
3187
3415
  }
3188
- currentTokens2.push({ type: "inline_field", value: `K:${bodyKeyMatch[1]}` });
3416
+ currentTokens2.push({ type: "inline_field", value: `K:${bodyKeyMatch[1]}`, standalone: true });
3189
3417
  currentTokens2.push({ type: "line_break", value: "\n" });
3190
3418
  isContinuation = false;
3191
3419
  continue;
3192
3420
  }
3193
- if (/^[A-Za-z]:\s*/.test(line) && !/^\[/.test(line)) {
3421
+ const bodyFieldMatch = /^[A-Za-z]:/.test(line);
3422
+ if (bodyFieldMatch && !/^\[/.test(line)) {
3423
+ voiceTokens.get(currentVoice).push({
3424
+ type: "inline_field",
3425
+ value: `${ABC_BODY_FIELD_MARKER}${line}`
3426
+ });
3194
3427
  isContinuation = false;
3195
3428
  continue;
3196
3429
  }
@@ -3213,7 +3446,7 @@ function tokenizeBody(bodyLines) {
3213
3446
  }
3214
3447
  for (const token of tokens) {
3215
3448
  if (token.type === "inline_field") {
3216
- const fieldMatch = token.value.match(/^V:\s*(.+)/);
3449
+ const fieldMatch = token.value.match(/^V:(.+)/);
3217
3450
  if (fieldMatch) {
3218
3451
  const voiceId = fieldMatch[1].trim().split(/\s+/)[0];
3219
3452
  currentVoice = voiceId;
@@ -3287,13 +3520,22 @@ function tokenizeMusicLine(line) {
3287
3520
  if (ch === '"') {
3288
3521
  const end = line.indexOf('"', i + 1);
3289
3522
  if (end !== -1) {
3290
- tokens.push({ type: "chord_symbol", value: line.slice(i + 1, end) });
3523
+ const text = line.slice(i + 1, end);
3524
+ if (/^[\^_<>@]/.test(text)) {
3525
+ tokens.push({
3526
+ type: "annotation",
3527
+ value: text.slice(1),
3528
+ annotationPlacement: text[0]
3529
+ });
3530
+ } else {
3531
+ tokens.push({ type: "chord_symbol", value: text });
3532
+ }
3291
3533
  i = end + 1;
3292
3534
  continue;
3293
3535
  }
3294
3536
  }
3295
- if (ch === "!") {
3296
- const end = line.indexOf("!", i + 1);
3537
+ if (ch === "!" || ch === "+") {
3538
+ const end = line.indexOf(ch, i + 1);
3297
3539
  if (end !== -1) {
3298
3540
  tokens.push({ type: "decoration", value: line.slice(i + 1, end) });
3299
3541
  i = end + 1;
@@ -3301,8 +3543,9 @@ function tokenizeMusicLine(line) {
3301
3543
  }
3302
3544
  }
3303
3545
  if (ch === "{") {
3304
- tokens.push({ type: "grace_start", value: "{" });
3305
- i++;
3546
+ const isSlash = i + 1 < line.length && line[i + 1] === "/";
3547
+ tokens.push({ type: "grace_start", value: isSlash ? "{/" : "{", slashGrace: isSlash });
3548
+ i += isSlash ? 2 : 1;
3306
3549
  continue;
3307
3550
  }
3308
3551
  if (ch === "}") {
@@ -3310,6 +3553,22 @@ function tokenizeMusicLine(line) {
3310
3553
  i++;
3311
3554
  continue;
3312
3555
  }
3556
+ if (ch === ".") {
3557
+ const next = i + 1 < line.length ? line[i + 1] : "";
3558
+ if (next === "(") {
3559
+ tokens.push({ type: "slur_start", value: ".(", dotted: true });
3560
+ i += 2;
3561
+ continue;
3562
+ }
3563
+ if (next === "|") {
3564
+ const dottedBar = parseBarLine(line, i + 1);
3565
+ if (dottedBar) {
3566
+ tokens.push({ type: "bar", value: "." + dottedBar.token.value, barType: "dotted" });
3567
+ i = dottedBar.nextIndex;
3568
+ continue;
3569
+ }
3570
+ }
3571
+ }
3313
3572
  if (ch === "(" && i + 1 < line.length && /\d/.test(line[i + 1])) {
3314
3573
  const tupletResult = parseTuplet(line, i);
3315
3574
  if (tupletResult) {
@@ -3339,7 +3598,7 @@ function tokenizeMusicLine(line) {
3339
3598
  tokens.push(barResult.token);
3340
3599
  i = barResult.nextIndex;
3341
3600
  if (i < line.length && /\d/.test(line[i])) {
3342
- const numMatch = line.slice(i).match(/^(\d+)/);
3601
+ const numMatch = line.slice(i).match(ENDING_SPEC);
3343
3602
  if (numMatch) {
3344
3603
  tokens.push({ type: "ending", value: numMatch[1] });
3345
3604
  i += numMatch[1].length;
@@ -3350,7 +3609,7 @@ function tokenizeMusicLine(line) {
3350
3609
  }
3351
3610
  }
3352
3611
  if (ch === "[" && i + 1 < line.length && /\d/.test(line[i + 1])) {
3353
- const numMatch = line.slice(i + 1).match(/^(\d+)/);
3612
+ const numMatch = line.slice(i + 1).match(ENDING_SPEC);
3354
3613
  if (numMatch) {
3355
3614
  tokens.push({ type: "ending", value: numMatch[1], bracket: true });
3356
3615
  i += 1 + numMatch[1].length;
@@ -3397,14 +3656,22 @@ function tokenizeMusicLine(line) {
3397
3656
  tokens.push({ type: "chord_end", value: "]", durationNum: dur.num, durationDen: dur.den });
3398
3657
  continue;
3399
3658
  }
3400
- if ((ch === "v" || ch === "u" || ch === "T" || ch === "M") && i + 1 < line.length) {
3659
+ if (ABC_SHORTHAND_DECORATIONS.has(ch) && i + 1 < line.length) {
3401
3660
  const nextCh = line[i + 1];
3402
- if (isNoteStart(nextCh) || nextCh === "[" || nextCh === "z" || nextCh === "x" || nextCh === "v" || nextCh === "u" || nextCh === "T" || nextCh === "M" || nextCh === "(" || nextCh === "!" || nextCh === '"') {
3661
+ if (isNoteStart(nextCh) || nextCh === "[" || nextCh === "{" || nextCh === "z" || nextCh === "Z" || nextCh === "x" || nextCh === "X" || nextCh === "y" || ABC_SHORTHAND_DECORATIONS.has(nextCh) || nextCh === "(" || nextCh === "!" || nextCh === "+" || nextCh === '"') {
3403
3662
  tokens.push({ type: "decoration", value: ch });
3404
3663
  i++;
3405
3664
  continue;
3406
3665
  }
3407
3666
  }
3667
+ if (ch === "y") {
3668
+ const start = i;
3669
+ i++;
3670
+ const dur = parseDuration(line, i);
3671
+ i = dur.nextIndex;
3672
+ tokens.push({ type: "spacer", value: line.slice(start, i) });
3673
+ continue;
3674
+ }
3408
3675
  if (isNoteStart(ch) || ch === "z" || ch === "Z" || ch === "x" || ch === "X") {
3409
3676
  const noteResult = parseNoteToken(line, i);
3410
3677
  if (noteResult) {
@@ -3450,11 +3717,13 @@ function parseTuplet(line, i) {
3450
3717
  }
3451
3718
  function parseBarLine(line, i) {
3452
3719
  const patterns = [
3720
+ ["[|]", "invisible"],
3453
3721
  [":|]", "end-repeat-final"],
3454
3722
  [":||:", "double-repeat"],
3455
3723
  ["::", "double-repeat"],
3456
3724
  [":|:", "double-repeat"],
3457
3725
  ["|>|", "thick-thin"],
3726
+ ["[|:", "start-repeat"],
3458
3727
  ["|:", "start-repeat"],
3459
3728
  [":|", "end-repeat"],
3460
3729
  ["||", "double"],
@@ -3481,15 +3750,18 @@ function parseBarLine(line, i) {
3481
3750
  function parseNoteToken(line, i) {
3482
3751
  const start = i;
3483
3752
  if (line[i] === "z" || line[i] === "Z" || line[i] === "x" || line[i] === "X") {
3753
+ const restLetter = line[i];
3484
3754
  i++;
3485
3755
  const dur2 = parseDuration(line, i);
3486
3756
  i = dur2.nextIndex;
3757
+ const isMultiMeasure = restLetter === "Z";
3487
3758
  return {
3488
3759
  token: {
3489
3760
  type: "rest",
3490
3761
  value: line.slice(start, i),
3491
- durationNum: dur2.num,
3492
- durationDen: dur2.den
3762
+ durationNum: isMultiMeasure ? 1 : dur2.num,
3763
+ durationDen: isMultiMeasure ? 1 : dur2.den,
3764
+ restMeasures: isMultiMeasure ? Math.max(1, dur2.num) : void 0
3493
3765
  },
3494
3766
  nextIndex: i
3495
3767
  };
@@ -3503,6 +3775,9 @@ function parseNoteToken(line, i) {
3503
3775
  accidental = 2;
3504
3776
  i++;
3505
3777
  }
3778
+ const micro = parseMicrotoneFactor(line, i);
3779
+ accidental *= micro.factor;
3780
+ i = micro.nextIndex;
3506
3781
  } else if (line[i] === "_") {
3507
3782
  accidental = -1;
3508
3783
  i++;
@@ -3510,6 +3785,9 @@ function parseNoteToken(line, i) {
3510
3785
  accidental = -2;
3511
3786
  i++;
3512
3787
  }
3788
+ const micro = parseMicrotoneFactor(line, i);
3789
+ accidental *= micro.factor;
3790
+ i = micro.nextIndex;
3513
3791
  } else if (line[i] === "=") {
3514
3792
  accidental = 0;
3515
3793
  explicitNatural = true;
@@ -3547,6 +3825,14 @@ function parseNoteToken(line, i) {
3547
3825
  nextIndex: i
3548
3826
  };
3549
3827
  }
3828
+ function parseMicrotoneFactor(line, i) {
3829
+ const match = line.slice(i).match(/^(\d{0,3})\/(\d{0,3})/);
3830
+ if (!match) return { factor: 1, nextIndex: i };
3831
+ const num = match[1] === "" ? 1 : parseInt(match[1], 10);
3832
+ const den = match[2] === "" ? 2 : parseInt(match[2], 10);
3833
+ if (den === 0) return { factor: 1, nextIndex: i };
3834
+ return { factor: num / den, nextIndex: i + match[0].length };
3835
+ }
3550
3836
  function abcNoteToPitch(letter, accidental) {
3551
3837
  const isLower = letter === letter.toLowerCase();
3552
3838
  const step = letter.toUpperCase();
@@ -3585,9 +3871,8 @@ function buildScore(header, voiceTokensList, voiceIds, headerFieldOrder, inlineV
3585
3871
  const unitNote = parseUnitNoteLength(header.unitNoteLength, header.meter);
3586
3872
  const timeSignature = parseTimeSignature2(header.meter || "4/4");
3587
3873
  const keySignature = parseKeySignature2(header.key || "C");
3588
- const beatsNum = parseInt(timeSignature.beats, 10);
3589
- const beatType = timeSignature.beatType;
3590
- const measureDuration = Math.round(beatsNum / beatType * 4 * DIVISIONS);
3874
+ const headerClef = header.clef ? parseAbcClefSpec(header.clef) : void 0;
3875
+ const measureDuration = Math.round(timeSignatureWholeNotes(timeSignature) * 4 * DIVISIONS);
3591
3876
  const parts = [];
3592
3877
  const partListEntries = [];
3593
3878
  const miscellaneous = [];
@@ -3688,8 +3973,12 @@ function buildScore(header, voiceTokensList, voiceIds, headerFieldOrder, inlineV
3688
3973
  id: partId,
3689
3974
  name: voiceName
3690
3975
  });
3691
- const voiceClef = headerVoice ? abcClefToMusicXml(headerVoice.clef) : void 0;
3692
- const buildResult = buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDuration, voiceClef);
3976
+ const voiceClef = headerVoice?.clef ? abcClefToMusicXml(headerVoice.clef) : headerClef;
3977
+ const octaveShift = headerVoice?.octave ?? 0;
3978
+ const buildResult = buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDuration, voiceClef, octaveShift);
3979
+ if (octaveShift !== 0) {
3980
+ miscellaneous.push({ name: `abc-voice-octave-${voiceIndex}`, value: String(octaveShift) });
3981
+ }
3693
3982
  parts.push({
3694
3983
  _id: generateId(),
3695
3984
  id: partId,
@@ -3709,15 +3998,23 @@ function buildScore(header, voiceTokensList, voiceIds, headerFieldOrder, inlineV
3709
3998
  let lyricsAfterAll = true;
3710
3999
  let seenLyrics = false;
3711
4000
  const lyricLineCounts = [];
3712
- for (const token of tokens) {
4001
+ const lyricLineVerses = [];
4002
+ let verse = 0;
4003
+ for (let ti = 0; ti < tokens.length; ti++) {
4004
+ const token = tokens[ti];
3713
4005
  if (token.type === "lyrics") {
3714
4006
  hasLyrics2 = true;
3715
4007
  seenLyrics = true;
4008
+ verse = ti > 0 && tokens[ti - 1].type === "lyrics" ? verse + 1 : 1;
3716
4009
  lyricLineCounts.push(token.syllables?.length || 0);
4010
+ lyricLineVerses.push(verse);
3717
4011
  } else if (seenLyrics && (token.type === "note" || token.type === "rest" || token.type === "bar")) {
3718
4012
  lyricsAfterAll = false;
3719
4013
  }
3720
4014
  }
4015
+ if (hasLyrics2 && lyricLineVerses.some((v) => v > 1)) {
4016
+ miscellaneous.push({ name: "abc-lyrics-line-verses", value: JSON.stringify(lyricLineVerses) });
4017
+ }
3721
4018
  if (hasLyrics2 && lyricsAfterAll) {
3722
4019
  miscellaneous.push({ name: "abc-lyrics-after-all", value: "true" });
3723
4020
  }
@@ -3758,13 +4055,14 @@ function buildScore(header, voiceTokensList, voiceIds, headerFieldOrder, inlineV
3758
4055
  };
3759
4056
  }
3760
4057
  function parseTempoToDirection(tempoStr) {
3761
- const match = tempoStr.match(/(?:(\d+)\/(\d+)\s*=\s*)?(\d+)/);
3762
- if (!match) return null;
3763
- const perMinute = parseInt(match[3], 10);
4058
+ const withUnit = tempoStr.match(/(\d{1,4})\/(\d{1,4})\s*=\s*(\d{1,6})/);
4059
+ const rateOnly = withUnit ? null : tempoStr.match(/\d{1,6}/);
4060
+ if (!withUnit && !rateOnly) return null;
4061
+ const perMinute = parseInt(withUnit ? withUnit[3] : rateOnly[0], 10);
3764
4062
  let beatUnit = "quarter";
3765
- if (match[1] && match[2]) {
3766
- const num = parseInt(match[1], 10);
3767
- const den = parseInt(match[2], 10);
4063
+ if (withUnit) {
4064
+ const num = parseInt(withUnit[1], 10);
4065
+ const den = parseInt(withUnit[2], 10);
3768
4066
  const quarterNotes = num / den * 4;
3769
4067
  const found = NOTE_TYPE_MAP[quarterNotes];
3770
4068
  if (found) beatUnit = found;
@@ -3777,20 +4075,44 @@ function parseTempoToDirection(tempoStr) {
3777
4075
  sound: { tempo: perMinute }
3778
4076
  };
3779
4077
  }
4078
+ var ABC_CLEF_NAMES = {
4079
+ "treble": { sign: "G", line: 2 },
4080
+ "bass": { sign: "F", line: 4 },
4081
+ "bass3": { sign: "F", line: 3 },
4082
+ "alto": { sign: "C", line: 3 },
4083
+ "tenor": { sign: "C", line: 4 },
4084
+ "soprano": { sign: "C", line: 1 },
4085
+ "mezzo": { sign: "C", line: 2 },
4086
+ "mezzo-soprano": { sign: "C", line: 2 },
4087
+ "baritone": { sign: "C", line: 5 },
4088
+ "perc": { sign: "percussion" },
4089
+ "percussion": { sign: "percussion" },
4090
+ "none": { sign: "G", line: 2 }
4091
+ };
3780
4092
  function abcClefToMusicXml(abcClef) {
3781
4093
  if (!abcClef) return { sign: "G", line: 2 };
3782
- const c = abcClef.toLowerCase();
3783
- if (c === "treble" || c === "treble-8va" || c === "treble+8") return { sign: "G", line: 2 };
3784
- if (c === "bass" || c === "bass3") return { sign: "F", line: 4 };
3785
- if (c === "alto") return { sign: "C", line: 3 };
3786
- if (c === "tenor") return { sign: "C", line: 4 };
3787
- if (c === "soprano") return { sign: "C", line: 1 };
3788
- if (c === "mezzo" || c === "mezzo-soprano") return { sign: "C", line: 2 };
3789
- if (c === "baritone") return { sign: "C", line: 5 };
3790
- if (c === "perc" || c === "percussion") return { sign: "percussion" };
3791
- return { sign: "G", line: 2 };
3792
- }
3793
- function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDuration, clef) {
4094
+ const match = abcClef.trim().toLowerCase().match(/^([a-z-]*?)\s*([+-])?(8|15)(?:va|vb)?$/);
4095
+ const baseName = match ? match[1] : abcClef.trim().toLowerCase();
4096
+ const base = ABC_CLEF_NAMES[baseName] ?? ABC_CLEF_NAMES[abcClef.trim().toLowerCase()];
4097
+ if (!base) return { sign: "G", line: 2 };
4098
+ const clef = { ...base };
4099
+ if (match && match[3]) {
4100
+ const octaves = match[3] === "15" ? 2 : 1;
4101
+ clef.clefOctaveChange = match[2] === "+" ? octaves : -octaves;
4102
+ }
4103
+ return clef;
4104
+ }
4105
+ function parseAbcClefSpec(fieldValue) {
4106
+ const explicit = fieldValue.match(/clef\s*=\s*(\S+)/i);
4107
+ if (explicit) return abcClefToMusicXml(explicit[1]);
4108
+ for (const word of fieldValue.trim().split(/\s+/).slice(1)) {
4109
+ if (word.includes("=")) continue;
4110
+ const bare = word.toLowerCase().replace(/([+-]?(?:8|15)(?:va|vb)?)$/, "");
4111
+ if (ABC_CLEF_NAMES[bare] && bare !== "none") return abcClefToMusicXml(word);
4112
+ }
4113
+ return void 0;
4114
+ }
4115
+ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDuration, clef, octaveShift = 0) {
3794
4116
  const measures = [];
3795
4117
  let currentEntries = [];
3796
4118
  let hasIndividualChordDurations = false;
@@ -3801,40 +4123,40 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
3801
4123
  let pendingTie = false;
3802
4124
  let slurDepth = 0;
3803
4125
  let pendingSlurStarts = 0;
4126
+ const pendingSlurDotted = [];
3804
4127
  let slurStartNotes = [];
3805
4128
  let inGrace = false;
3806
4129
  let pendingBrokenRhythm = null;
3807
4130
  let tupletState = null;
3808
4131
  const pendingPreNoteItems = [];
4132
+ let pendingNotations = [];
4133
+ let graceSlash = false;
3809
4134
  let pendingEndingNumber = null;
3810
4135
  let currentLyrics = [];
3811
4136
  let noteCountForLyrics = 0;
4137
+ let lyricVerse = 0;
4138
+ let lyricTargets = [];
3812
4139
  let inChord = false;
3813
4140
  let chordNotes = [];
3814
4141
  let chordNoteTies = [];
3815
4142
  let currentUnitNote = { ...unitNote };
3816
4143
  let pendingTupletStart = false;
3817
4144
  let pendingKeyChange = null;
4145
+ let pendingTimeChange = null;
3818
4146
  const lineBreaks = [];
4147
+ let pendingMultipleRest = null;
3819
4148
  function flushPendingPreNoteItems() {
3820
4149
  for (const item of pendingPreNoteItems) {
3821
- if (item.kind === "harmony") {
3822
- const harmony = createHarmonyEntry(item.value);
3823
- if (harmony) currentEntries.push(harmony);
3824
- } else if (item.kind === "dynamic") {
3825
- const dynDir = createDynamicsDirection(item.value);
3826
- if (dynDir) currentEntries.push(dynDir);
3827
- } else if (item.kind === "decoration") {
3828
- const decoDir = {
3829
- _id: generateId(),
3830
- type: "direction",
3831
- directionTypes: [{ kind: "words", text: item.value }]
3832
- };
3833
- currentEntries.push(decoDir);
3834
- }
4150
+ currentEntries.push(item);
3835
4151
  }
3836
4152
  pendingPreNoteItems.length = 0;
3837
4153
  }
4154
+ function attachPendingNotations(entry) {
4155
+ if (pendingNotations.length === 0) return;
4156
+ if (!entry.notations) entry.notations = [];
4157
+ entry.notations.push(...pendingNotations);
4158
+ pendingNotations = [];
4159
+ }
3838
4160
  function finalizeMeasure(endBarType) {
3839
4161
  const measure = {
3840
4162
  _id: generateId(),
@@ -3851,6 +4173,13 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
3851
4173
  };
3852
4174
  isFirstMeasure = false;
3853
4175
  }
4176
+ if (pendingMultipleRest !== null) {
4177
+ if (!measure.attributes) {
4178
+ measure.attributes = { _id: generateId() };
4179
+ }
4180
+ measure.attributes.measureStyle = [{ multipleRest: pendingMultipleRest }];
4181
+ pendingMultipleRest = null;
4182
+ }
3854
4183
  if (pendingKeyChange) {
3855
4184
  if (!measure.attributes) {
3856
4185
  measure.attributes = { _id: generateId() };
@@ -3859,6 +4188,13 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
3859
4188
  measure.attributes.key = parseKeySignature2(kValue);
3860
4189
  pendingKeyChange = null;
3861
4190
  }
4191
+ if (pendingTimeChange) {
4192
+ if (!measure.attributes) {
4193
+ measure.attributes = { _id: generateId() };
4194
+ }
4195
+ measure.attributes.time = pendingTimeChange;
4196
+ pendingTimeChange = null;
4197
+ }
3862
4198
  if (endBarType || currentBarlines.length > 0) {
3863
4199
  measure.barlines = [...currentBarlines];
3864
4200
  if (endBarType) {
@@ -3883,8 +4219,9 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
3883
4219
  chordNotes.push(token);
3884
4220
  break;
3885
4221
  }
3886
- const entry = createNoteEntry(token, currentUnitNote, pendingTie, inGrace, tupletState);
4222
+ const entry = createNoteEntry(token, currentUnitNote, pendingTie, inGrace, tupletState, graceSlash, octaveShift);
3887
4223
  pendingTie = false;
4224
+ attachPendingNotations(entry);
3888
4225
  if (pendingBrokenRhythm) {
3889
4226
  const n = pendingBrokenRhythm.length;
3890
4227
  const divisor = Math.pow(2, n);
@@ -3904,7 +4241,7 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
3904
4241
  flushPendingPreNoteItems();
3905
4242
  while (pendingSlurStarts > 0) {
3906
4243
  if (!entry.notations) entry.notations = [];
3907
- entry.notations.push({ type: "slur", slurType: "start", number: slurDepth - pendingSlurStarts + 1 });
4244
+ entry.notations.push(makeSlurStart(slurDepth - pendingSlurStarts + 1, pendingSlurDotted.shift() === true));
3908
4245
  slurStartNotes.push(entry);
3909
4246
  pendingSlurStarts--;
3910
4247
  }
@@ -3947,11 +4284,25 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
3947
4284
  break;
3948
4285
  }
3949
4286
  case "rest": {
3950
- if (inChord) break;
4287
+ if (inChord) {
4288
+ chordNotes.push(token);
4289
+ break;
4290
+ }
3951
4291
  const restEntry = createRestEntry(token, currentUnitNote, tupletState, measureDuration);
3952
4292
  flushPendingPreNoteItems();
4293
+ attachPendingNotations(restEntry);
3953
4294
  currentEntries.push(restEntry);
3954
4295
  currentPosition += restEntry.duration;
4296
+ const restMeasures = token.restMeasures || 1;
4297
+ if (restMeasures > 1) {
4298
+ pendingMultipleRest = restMeasures;
4299
+ for (let extra = 1; extra < restMeasures; extra++) {
4300
+ finalizeMeasure();
4301
+ const filler = createRestEntry(token, currentUnitNote, null, measureDuration);
4302
+ currentEntries.push(filler);
4303
+ currentPosition += filler.duration;
4304
+ }
4305
+ }
3955
4306
  if (tupletState) {
3956
4307
  tupletState.remaining--;
3957
4308
  if (tupletState.remaining <= 0) {
@@ -3968,16 +4319,7 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
3968
4319
  case "chord_end": {
3969
4320
  inChord = false;
3970
4321
  if (chordNotes.length > 0) {
3971
- for (const item of pendingPreNoteItems) {
3972
- if (item.kind === "harmony") {
3973
- const harmony = createHarmonyEntry(item.value);
3974
- if (harmony) currentEntries.push(harmony);
3975
- } else if (item.kind === "dynamic") {
3976
- const dynDir = createDynamicsDirection(item.value);
3977
- if (dynDir) currentEntries.push(dynDir);
3978
- }
3979
- }
3980
- pendingPreNoteItems.length = 0;
4322
+ flushPendingPreNoteItems();
3981
4323
  const chordDurNum = token.durationNum || 1;
3982
4324
  const chordDurDen = token.durationDen || 1;
3983
4325
  const hasIndividualDurations = chordNotes.some(
@@ -3993,13 +4335,14 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
3993
4335
  chordToken.durationNum = chordDurNum;
3994
4336
  chordToken.durationDen = chordDurDen;
3995
4337
  }
3996
- const entry = createNoteEntry(chordToken, currentUnitNote, false, inGrace, tupletState);
4338
+ const entry = chordToken.type === "rest" ? createRestEntry(chordToken, currentUnitNote, tupletState, measureDuration) : createNoteEntry(chordToken, currentUnitNote, false, inGrace, tupletState, graceSlash, octaveShift);
3997
4339
  chordToken.durationNum = originalNum;
3998
4340
  chordToken.durationDen = originalDen;
3999
4341
  if (ci === 0) {
4342
+ attachPendingNotations(entry);
4000
4343
  while (pendingSlurStarts > 0) {
4001
4344
  if (!entry.notations) entry.notations = [];
4002
- entry.notations.push({ type: "slur", slurType: "start", number: slurDepth - pendingSlurStarts + 1 });
4345
+ entry.notations.push(makeSlurStart(slurDepth - pendingSlurStarts + 1, pendingSlurDotted.shift() === true));
4003
4346
  slurStartNotes.push(entry);
4004
4347
  pendingSlurStarts--;
4005
4348
  }
@@ -4075,7 +4418,7 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
4075
4418
  type: "start"
4076
4419
  }
4077
4420
  };
4078
- if (ti > 0 && tokens[ti - 1].type === "bar" && (!tokens[ti - 1].barType || tokens[ti - 1].barType === "regular") && currentEntries.length === 0) {
4421
+ if (!token.bracket && ti > 0 && tokens[ti - 1].type === "bar" && (!tokens[ti - 1].barType || tokens[ti - 1].barType === "regular") && currentEntries.length === 0) {
4079
4422
  endingBarline.barStyle = "regular";
4080
4423
  }
4081
4424
  currentBarlines.push(endingBarline);
@@ -4103,6 +4446,7 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
4103
4446
  case "slur_start":
4104
4447
  slurDepth++;
4105
4448
  pendingSlurStarts++;
4449
+ pendingSlurDotted.push(token.dotted === true);
4106
4450
  break;
4107
4451
  case "slur_end":
4108
4452
  if (slurDepth > 0) {
@@ -4119,9 +4463,11 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
4119
4463
  break;
4120
4464
  case "grace_start":
4121
4465
  inGrace = true;
4466
+ graceSlash = token.slashGrace === true;
4122
4467
  break;
4123
4468
  case "grace_end":
4124
4469
  inGrace = false;
4470
+ graceSlash = false;
4125
4471
  break;
4126
4472
  case "broken_rhythm": {
4127
4473
  const brokenN = token.value.length;
@@ -4151,21 +4497,47 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
4151
4497
  };
4152
4498
  pendingTupletStart = true;
4153
4499
  break;
4154
- case "chord_symbol":
4155
- pendingPreNoteItems.push({ kind: "harmony", value: token.value });
4500
+ case "chord_symbol": {
4501
+ const harmony = createHarmonyEntry(token.value);
4502
+ if (harmony) pendingPreNoteItems.push(harmony);
4156
4503
  break;
4157
- case "decoration":
4158
- if (DYNAMICS_VALUES.has(token.value)) {
4159
- pendingPreNoteItems.push({ kind: "dynamic", value: token.value });
4160
- } else {
4161
- const isShorthand = token.value.length === 1 && /^[vuTM]$/.test(token.value);
4162
- const decoText = isShorthand ? token.value : `!${token.value}!`;
4163
- pendingPreNoteItems.push({ kind: "decoration", value: decoText });
4504
+ }
4505
+ case "annotation":
4506
+ pendingPreNoteItems.push(
4507
+ createAnnotationDirection(token.value, token.annotationPlacement || "@")
4508
+ );
4509
+ break;
4510
+ case "spacer":
4511
+ pendingPreNoteItems.push(createRawWordsDirection(token.value));
4512
+ break;
4513
+ case "decoration": {
4514
+ const target = lookupAbcDecoration(token.value);
4515
+ if (!target) {
4516
+ const isShorthand = token.value.length === 1 && ABC_SHORTHAND_DECORATIONS.has(token.value);
4517
+ pendingPreNoteItems.push(
4518
+ createRawWordsDirection(isShorthand ? token.value : `!${token.value}!`)
4519
+ );
4520
+ break;
4521
+ }
4522
+ const notation = decorationToNotation(target);
4523
+ if (notation) {
4524
+ pendingNotations.push(notation);
4525
+ break;
4164
4526
  }
4527
+ const direction = decorationToDirection(target);
4528
+ if (direction) pendingPreNoteItems.push(direction);
4165
4529
  break;
4530
+ }
4166
4531
  case "lyrics": {
4167
4532
  const syllables = token.syllables || [];
4168
- applyLyricsToExistingNotes(measures, currentEntries, syllables);
4533
+ const previousWasLyrics = ti > 0 && tokens[ti - 1].type === "lyrics";
4534
+ if (previousWasLyrics) {
4535
+ lyricVerse++;
4536
+ } else {
4537
+ lyricVerse = 1;
4538
+ lyricTargets = collectUnlyricedNotes(measures, currentEntries);
4539
+ }
4540
+ applyLyricsToNotes(lyricTargets, syllables, lyricVerse);
4169
4541
  break;
4170
4542
  }
4171
4543
  case "overlay": {
@@ -4181,25 +4553,37 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
4181
4553
  break;
4182
4554
  }
4183
4555
  case "inline_field": {
4184
- const lMatch = token.value.match(/^L:\s*(\d+)\/(\d+)/);
4185
- if (lMatch) {
4186
- currentUnitNote = { num: parseInt(lMatch[1], 10), den: parseInt(lMatch[2], 10) };
4187
- const inlineEntry = {
4188
- _id: generateId(),
4189
- type: "direction",
4190
- directionTypes: [{ kind: "words", text: `[L:${lMatch[1]}/${lMatch[2]}]` }]
4191
- };
4192
- currentEntries.push(inlineEntry);
4556
+ if (token.value.startsWith(ABC_BODY_FIELD_MARKER)) {
4557
+ currentEntries.push(createRawWordsDirection(token.value));
4558
+ break;
4559
+ }
4560
+ const fieldMatch = token.value.match(/^([A-Za-z]):(.*)$/);
4561
+ if (!fieldMatch) break;
4562
+ const [, field, rawValue] = fieldMatch;
4563
+ const value = rawValue.trim();
4564
+ if (!token.standalone) {
4565
+ currentEntries.push(createRawWordsDirection(`[${field}:${value}]`));
4566
+ }
4567
+ if (field === "K") {
4568
+ pendingKeyChange = `K:${rawValue}`;
4569
+ break;
4193
4570
  }
4194
- const kMatch = token.value.match(/^K:\s*(.*)/);
4195
- if (kMatch) {
4196
- pendingKeyChange = `K:${kMatch[1]}`;
4571
+ if (field === "L") {
4572
+ const lMatch = value.match(/^(\d+)\/(\d+)/);
4573
+ if (lMatch) {
4574
+ currentUnitNote = { num: parseInt(lMatch[1], 10), den: parseInt(lMatch[2], 10) };
4575
+ }
4576
+ } else if (field === "M") {
4577
+ pendingTimeChange = parseTimeSignature2(value);
4578
+ } else if (field === "Q") {
4579
+ const tempoDirection = parseTempoToDirection(value);
4580
+ if (tempoDirection) currentEntries.push(tempoDirection);
4197
4581
  }
4198
4582
  break;
4199
4583
  }
4200
4584
  case "space":
4201
4585
  break;
4202
- case "line_break":
4586
+ case "line_break": {
4203
4587
  flushPendingPreNoteItems();
4204
4588
  if (currentEntries.length > 0) {
4205
4589
  const breakText = token.value === "\\\n" ? "__abc_line_cont__" : "__abc_line_break__";
@@ -4210,7 +4594,8 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
4210
4594
  };
4211
4595
  currentEntries.push(lineBreakDir);
4212
4596
  }
4213
- if (measures.length > 0) {
4597
+ const onlyMarkersSoFar = currentEntries.length > 0 && !currentEntries.some((e) => e.type === "note");
4598
+ if (measures.length > 0 && !onlyMarkersSoFar) {
4214
4599
  if (token.value === "\\\n") {
4215
4600
  lineBreaks.push(-measures.length);
4216
4601
  } else {
@@ -4218,6 +4603,7 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
4218
4603
  }
4219
4604
  }
4220
4605
  break;
4606
+ }
4221
4607
  default:
4222
4608
  break;
4223
4609
  }
@@ -4228,22 +4614,20 @@ function buildMeasures(tokens, unitNote, keySignature, timeSignature, measureDur
4228
4614
  applyTieStops(measures);
4229
4615
  return { measures, lineBreaks, hasIndividualChordDurations };
4230
4616
  }
4231
- function applyLyricsToExistingNotes(finalizedMeasures, currentEntries, syllables) {
4232
- const allNotes = [];
4233
- for (const measure of finalizedMeasures) {
4234
- for (const entry of measure.entries) {
4235
- if (entry.type === "note" && !entry.rest && !entry.grace && !entry.chord) {
4236
- allNotes.push(entry);
4617
+ function collectUnlyricedNotes(finalizedMeasures, currentEntries) {
4618
+ const notes = [];
4619
+ const collect = (entries) => {
4620
+ for (const entry of entries) {
4621
+ if (entry.type === "note" && !entry.rest && !entry.grace && !entry.chord && (!entry.lyrics || entry.lyrics.length === 0)) {
4622
+ notes.push(entry);
4237
4623
  }
4238
4624
  }
4239
- }
4240
- for (const entry of currentEntries) {
4241
- if (entry.type === "note" && !entry.rest && !entry.grace && !entry.chord) {
4242
- allNotes.push(entry);
4243
- }
4244
- }
4245
- const unlyricedNotes = allNotes.filter((n) => !n.lyrics || n.lyrics.length === 0);
4246
- const targetNotes = unlyricedNotes.slice(0, syllables.length);
4625
+ };
4626
+ for (const measure of finalizedMeasures) collect(measure.entries);
4627
+ collect(currentEntries);
4628
+ return notes;
4629
+ }
4630
+ function applyLyricsToNotes(targetNotes, syllables, verse) {
4247
4631
  for (let si = 0; si < syllables.length && si < targetNotes.length; si++) {
4248
4632
  const syllable = syllables[si];
4249
4633
  if (!syllable || syllable === "" || syllable === "*") continue;
@@ -4257,11 +4641,8 @@ function applyLyricsToExistingNotes(finalizedMeasures, currentEntries, syllables
4257
4641
  syllabic = isHyphenated ? "middle" : "end";
4258
4642
  }
4259
4643
  }
4260
- note.lyrics = [{
4261
- number: 1,
4262
- text,
4263
- syllabic
4264
- }];
4644
+ if (!note.lyrics) note.lyrics = [];
4645
+ note.lyrics.push({ number: verse, text, syllabic });
4265
4646
  }
4266
4647
  }
4267
4648
  function applyTieStops(measures) {
@@ -4294,7 +4675,7 @@ function applyTieStops(measures) {
4294
4675
  }
4295
4676
  }
4296
4677
  }
4297
- function createNoteEntry(token, unitNote, _hasTieStop, isGrace, tupletState) {
4678
+ function createNoteEntry(token, unitNote, _hasTieStop, isGrace, tupletState, graceSlash = false, octaveShift = 0) {
4298
4679
  const num = token.durationNum || 1;
4299
4680
  const den = token.durationDen || 1;
4300
4681
  let duration;
@@ -4307,17 +4688,18 @@ function createNoteEntry(token, unitNote, _hasTieStop, isGrace, tupletState) {
4307
4688
  }
4308
4689
  }
4309
4690
  const { noteType, dots } = durationToNoteType(isGrace ? lengthToDuration(num, den, unitNote) : duration);
4691
+ const pitch = token.pitch && octaveShift !== 0 ? { ...token.pitch, octave: token.pitch.octave + octaveShift } : token.pitch;
4310
4692
  const entry = {
4311
4693
  _id: generateId(),
4312
4694
  type: "note",
4313
- pitch: token.pitch,
4695
+ pitch,
4314
4696
  duration,
4315
4697
  voice: "1",
4316
4698
  noteType,
4317
4699
  dots: dots > 0 ? dots : void 0
4318
4700
  };
4319
4701
  if (isGrace) {
4320
- entry.grace = { slash: true };
4702
+ entry.grace = { slash: graceSlash };
4321
4703
  entry.noteType = "eighth";
4322
4704
  }
4323
4705
  if (tupletState && !isGrace) {
@@ -4342,6 +4724,11 @@ function createNoteEntry(token, unitNote, _hasTieStop, isGrace, tupletState) {
4342
4724
  case -2:
4343
4725
  entry.accidental = { value: "double-flat" };
4344
4726
  break;
4727
+ default: {
4728
+ const named = MICROTONE_ACCIDENTALS[String(token.accidental)];
4729
+ if (named) entry.accidental = { value: named };
4730
+ break;
4731
+ }
4345
4732
  }
4346
4733
  }
4347
4734
  return entry;
@@ -4401,6 +4788,12 @@ function createBarline(barType, location, endingNumber) {
4401
4788
  case "thick-thin":
4402
4789
  barline.barStyle = "heavy-light";
4403
4790
  break;
4791
+ case "invisible":
4792
+ barline.barStyle = "none";
4793
+ break;
4794
+ case "dotted":
4795
+ barline.barStyle = "dotted";
4796
+ break;
4404
4797
  default:
4405
4798
  return null;
4406
4799
  }
@@ -4484,6 +4877,85 @@ function createHarmonyEntry(chordStr) {
4484
4877
  }
4485
4878
  return entry;
4486
4879
  }
4880
+ function makeSlurStart(number, dotted) {
4881
+ return dotted ? { type: "slur", slurType: "start", number, lineType: "dotted" } : { type: "slur", slurType: "start", number };
4882
+ }
4883
+ function decorationToNotation(target) {
4884
+ switch (target.kind) {
4885
+ case "articulation":
4886
+ return { type: "articulation", articulation: target.articulation };
4887
+ case "ornament":
4888
+ return { type: "ornament", ornament: target.ornament };
4889
+ case "technical":
4890
+ return { type: "technical", technical: target.technical };
4891
+ case "fingering":
4892
+ return { type: "technical", technical: "fingering", text: target.fingering };
4893
+ case "fermata":
4894
+ return target.inverted ? { type: "fermata", fermataType: "inverted" } : { type: "fermata" };
4895
+ case "arpeggiate":
4896
+ return { type: "arpeggiate" };
4897
+ case "trill-line":
4898
+ return { type: "ornament", ornament: "wavy-line", wavyLineType: target.wavyLineType };
4899
+ default:
4900
+ return null;
4901
+ }
4902
+ }
4903
+ function decorationToDirection(target) {
4904
+ switch (target.kind) {
4905
+ case "dynamics":
4906
+ return createDynamicsDirection(target.value);
4907
+ case "wedge":
4908
+ return {
4909
+ _id: generateId(),
4910
+ type: "direction",
4911
+ directionTypes: [{ kind: "wedge", type: target.stop ? "stop" : target.wedge }],
4912
+ placement: "below"
4913
+ };
4914
+ case "segno":
4915
+ return { _id: generateId(), type: "direction", directionTypes: [{ kind: "segno" }], placement: "above" };
4916
+ case "coda":
4917
+ return { _id: generateId(), type: "direction", directionTypes: [{ kind: "coda" }], placement: "above" };
4918
+ case "words":
4919
+ return {
4920
+ _id: generateId(),
4921
+ type: "direction",
4922
+ directionTypes: [{ kind: "words", text: target.text }],
4923
+ placement: "above"
4924
+ };
4925
+ default:
4926
+ return null;
4927
+ }
4928
+ }
4929
+ function annotationPlacement(marker) {
4930
+ switch (marker) {
4931
+ case "^":
4932
+ return { placement: "above" };
4933
+ case "_":
4934
+ return { placement: "below" };
4935
+ case "<":
4936
+ return { halign: "right" };
4937
+ case ">":
4938
+ return { halign: "left" };
4939
+ default:
4940
+ return { halign: "center" };
4941
+ }
4942
+ }
4943
+ function createAnnotationDirection(text, marker) {
4944
+ const { placement, halign } = annotationPlacement(marker);
4945
+ return {
4946
+ _id: generateId(),
4947
+ type: "direction",
4948
+ directionTypes: [{ kind: "words", text, ...halign ? { halign } : {} }],
4949
+ ...placement ? { placement } : {}
4950
+ };
4951
+ }
4952
+ function createRawWordsDirection(text) {
4953
+ return {
4954
+ _id: generateId(),
4955
+ type: "direction",
4956
+ directionTypes: [{ kind: "words", text }]
4957
+ };
4958
+ }
4487
4959
  function createDynamicsDirection(dynamic) {
4488
4960
  if (!DYNAMICS_VALUES.has(dynamic)) return null;
4489
4961
  return {
@@ -4496,8 +4968,30 @@ function createDynamicsDirection(dynamic) {
4496
4968
  placement: "below"
4497
4969
  };
4498
4970
  }
4971
+ function splitTunes(lines) {
4972
+ const tunes = [];
4973
+ let fileHeader = [];
4974
+ let current = null;
4975
+ for (const line of lines) {
4976
+ if (/^X:/.test(line.trim())) {
4977
+ if (current) tunes.push(current);
4978
+ current = [...fileHeader, line];
4979
+ fileHeader = [];
4980
+ continue;
4981
+ }
4982
+ if (current) current.push(line);
4983
+ else fileHeader.push(line);
4984
+ }
4985
+ if (current) tunes.push(current);
4986
+ return tunes.length > 0 ? tunes : [lines];
4987
+ }
4988
+ function parseAbcTunes(abcString) {
4989
+ return splitTunes(abcString.split("\n")).map((tuneLines) => parseTune(tuneLines));
4990
+ }
4499
4991
  function parseAbc(abcString) {
4500
- const lines = abcString.split("\n");
4992
+ return parseTune(splitTunes(abcString.split("\n"))[0]);
4993
+ }
4994
+ function parseTune(lines) {
4501
4995
  const { header, bodyStartIndex, headerFieldOrder } = parseHeader(lines);
4502
4996
  const bodyLines = lines.slice(bodyStartIndex);
4503
4997
  const { tokens: voiceTokensList, voiceIds, inlineVoiceMarkers, voiceDeclarationLines, bodyComments, bodyDirectives, wFields, voiceInterleavePattern, groupBarCounts, voiceComments, preVoiceComments, trailingComments } = tokenizeBody(bodyLines);
@@ -4822,6 +5316,7 @@ function serializeCredit(credit, indent, out) {
4822
5316
  if (cw.letterSpacing) attrs2 += ` letter-spacing="${escapeXml(cw.letterSpacing)}"`;
4823
5317
  if (cw.xmlLang) attrs2 += ` xml:lang="${escapeXml(cw.xmlLang)}"`;
4824
5318
  if (cw.xmlSpace) attrs2 += ` xml:space="${escapeXml(cw.xmlSpace)}"`;
5319
+ if (cw.color) attrs2 += ` color="${escapeXml(cw.color)}"`;
4825
5320
  out.push(`${indent}${indent}<credit-words${attrs2}>${escapeXml(cw.text)}</credit-words>`);
4826
5321
  }
4827
5322
  }
@@ -4846,6 +5341,7 @@ function serializeDisplayTexts(texts, indent, out) {
4846
5341
  if (dt.fontStyle) attrs += ` font-style="${escapeXml(dt.fontStyle)}"`;
4847
5342
  if (dt.fontWeight) attrs += ` font-weight="${escapeXml(dt.fontWeight)}"`;
4848
5343
  if (dt.xmlSpace) attrs += ` xml:space="${escapeXml(dt.xmlSpace)}"`;
5344
+ if (dt.color) attrs += ` color="${escapeXml(dt.color)}"`;
4849
5345
  out.push(`${indent}<display-text${attrs}>${escapeXml(dt.text)}</display-text>`);
4850
5346
  }
4851
5347
  }
@@ -4854,6 +5350,7 @@ function serializeScorePart(part, indent, out) {
4854
5350
  if (part.name !== void 0) {
4855
5351
  let pnAttrs = "";
4856
5352
  if (part.namePrintObject === false) pnAttrs += ' print-object="no"';
5353
+ if (part.nameColor) pnAttrs += ` color="${escapeXml(part.nameColor)}"`;
4857
5354
  out.push(`${indent} <part-name${pnAttrs}>${escapeXml(part.name)}</part-name>`);
4858
5355
  }
4859
5356
  if (part.partNameDisplay && part.partNameDisplay.length > 0) {
@@ -4864,6 +5361,7 @@ function serializeScorePart(part, indent, out) {
4864
5361
  if (part.abbreviation !== void 0) {
4865
5362
  let paAttrs = "";
4866
5363
  if (part.abbreviationPrintObject === false) paAttrs += ' print-object="no"';
5364
+ if (part.abbreviationColor) paAttrs += ` color="${escapeXml(part.abbreviationColor)}"`;
4867
5365
  out.push(`${indent} <part-abbreviation${paAttrs}>${escapeXml(part.abbreviation)}</part-abbreviation>`);
4868
5366
  }
4869
5367
  if (part.partAbbreviationDisplay && part.partAbbreviationDisplay.length > 0) {
@@ -4937,7 +5435,8 @@ function serializePartGroup(group, indent, out) {
4937
5435
  if (group._id) attrs += ` id="${escapeXml(group._id)}"`;
4938
5436
  out.push(`${indent}<part-group${attrs}>`);
4939
5437
  if (group.groupName) {
4940
- out.push(`${indent} <group-name>${escapeXml(group.groupName)}</group-name>`);
5438
+ const gnAttrs = group.groupNameColor ? ` color="${escapeXml(group.groupNameColor)}"` : "";
5439
+ out.push(`${indent} <group-name${gnAttrs}>${escapeXml(group.groupName)}</group-name>`);
4941
5440
  }
4942
5441
  if (group.groupNameDisplay && group.groupNameDisplay.length > 0) {
4943
5442
  out.push(`${indent} <group-name-display>`);
@@ -4945,7 +5444,8 @@ function serializePartGroup(group, indent, out) {
4945
5444
  out.push(`${indent} </group-name-display>`);
4946
5445
  }
4947
5446
  if (group.groupAbbreviation) {
4948
- out.push(`${indent} <group-abbreviation>${escapeXml(group.groupAbbreviation)}</group-abbreviation>`);
5447
+ const gaAttrs = group.groupAbbreviationColor ? ` color="${escapeXml(group.groupAbbreviationColor)}"` : "";
5448
+ out.push(`${indent} <group-abbreviation${gaAttrs}>${escapeXml(group.groupAbbreviation)}</group-abbreviation>`);
4949
5449
  }
4950
5450
  if (group.groupAbbreviationDisplay && group.groupAbbreviationDisplay.length > 0) {
4951
5451
  out.push(`${indent} <group-abbreviation-display>`);
@@ -4953,8 +5453,9 @@ function serializePartGroup(group, indent, out) {
4953
5453
  out.push(`${indent} </group-abbreviation-display>`);
4954
5454
  }
4955
5455
  if (group.groupSymbol) {
4956
- const defaultXAttr = group.groupSymbolDefaultX !== void 0 ? ` default-x="${group.groupSymbolDefaultX}"` : "";
4957
- out.push(`${indent} <group-symbol${defaultXAttr}>${group.groupSymbol}</group-symbol>`);
5456
+ let gsAttrs = group.groupSymbolDefaultX !== void 0 ? ` default-x="${group.groupSymbolDefaultX}"` : "";
5457
+ if (group.groupSymbolColor) gsAttrs += ` color="${escapeXml(group.groupSymbolColor)}"`;
5458
+ out.push(`${indent} <group-symbol${gsAttrs}>${group.groupSymbol}</group-symbol>`);
4958
5459
  }
4959
5460
  if (group.groupBarline) {
4960
5461
  out.push(`${indent} <group-barline>${group.groupBarline}</group-barline>`);
@@ -5090,6 +5591,7 @@ function serializeKey(key, indent, out) {
5090
5591
  if (key.number !== void 0) keyAttrs += ` number="${key.number}"`;
5091
5592
  if (key.printObject === false) keyAttrs += ' print-object="no"';
5092
5593
  else if (key.printObject === true) keyAttrs += ' print-object="yes"';
5594
+ if (key.color) keyAttrs += ` color="${escapeXml(key.color)}"`;
5093
5595
  out.push(`${indent}<key${keyAttrs}>`);
5094
5596
  if (key.cancel !== void 0) {
5095
5597
  const locationAttr = key.cancelLocation ? ` location="${key.cancelLocation}"` : "";
@@ -5128,6 +5630,7 @@ function serializeTime(time, indent, out) {
5128
5630
  let attrs = "";
5129
5631
  if (time.symbol) attrs += ` symbol="${time.symbol}"`;
5130
5632
  if (time.printObject === false) attrs += ' print-object="no"';
5633
+ if (time.color) attrs += ` color="${escapeXml(time.color)}"`;
5131
5634
  out.push(`${indent}<time${attrs}>`);
5132
5635
  if (time.senzaMisura) {
5133
5636
  out.push(`${indent} <senza-misura/>`);
@@ -5153,6 +5656,7 @@ function serializeClef(clef, indent, out) {
5153
5656
  if (clef.printObject === false) attrs += ' print-object="no"';
5154
5657
  else if (clef.printObject === true) attrs += ' print-object="yes"';
5155
5658
  if (clef.afterBarline) attrs += ' after-barline="yes"';
5659
+ if (clef.color) attrs += ` color="${escapeXml(clef.color)}"`;
5156
5660
  out.push(`${indent}<clef${attrs}>`);
5157
5661
  out.push(`${indent} <sign>${clef.sign}</sign>`);
5158
5662
  if (clef.line !== void 0) {
@@ -5219,7 +5723,8 @@ function serializeNote(note, indent, out) {
5219
5723
  "dynamics": note.dynamics,
5220
5724
  "print-object": note.printObject === false ? false : void 0,
5221
5725
  "print-dot": note.printDot !== void 0 ? note.printDot : void 0,
5222
- "print-spacing": note.printSpacing
5726
+ "print-spacing": note.printSpacing,
5727
+ "color": note.color
5223
5728
  });
5224
5729
  out.push(`${indent}<note${noteAttrs}>`);
5225
5730
  if (note.grace) {
@@ -5283,12 +5788,16 @@ function serializeNote(note, indent, out) {
5283
5788
  out.push(`${indent} <voice>${note.voice}</voice>`);
5284
5789
  }
5285
5790
  if (note.noteType) {
5286
- const typeAttrs = note.noteTypeSize ? ` size="${escapeXml(note.noteTypeSize)}"` : "";
5791
+ const typeAttrs = buildAttrs({
5792
+ "size": note.noteTypeSize,
5793
+ "color": note.noteTypeColor
5794
+ });
5287
5795
  out.push(`${indent} <type${typeAttrs}>${note.noteType}</type>`);
5288
5796
  }
5289
5797
  if (note.dots) {
5798
+ const dotAttrs = note.dotColor ? ` color="${escapeXml(note.dotColor)}"` : "";
5290
5799
  for (let i = 0; i < note.dots; i++) {
5291
- out.push(`${indent} <dot/>`);
5800
+ out.push(`${indent} <dot${dotAttrs}/>`);
5292
5801
  }
5293
5802
  }
5294
5803
  if (note.accidental) {
@@ -5322,14 +5831,16 @@ function serializeNote(note, indent, out) {
5322
5831
  if (note.stem) {
5323
5832
  const stemAttrs = buildAttrs({
5324
5833
  "default-x": note.stem.defaultX,
5325
- "default-y": note.stem.defaultY
5834
+ "default-y": note.stem.defaultY,
5835
+ "color": note.stem.color
5326
5836
  });
5327
5837
  out.push(`${indent} <stem${stemAttrs}>${note.stem.value}</stem>`);
5328
5838
  }
5329
5839
  if (note.notehead) {
5330
5840
  const nhAttrs = buildAttrs({
5331
5841
  "filled": note.notehead.filled,
5332
- "parentheses": note.notehead.parentheses || void 0
5842
+ "parentheses": note.notehead.parentheses || void 0,
5843
+ "color": note.notehead.color
5333
5844
  });
5334
5845
  out.push(`${indent} <notehead${nhAttrs}>${note.notehead.value}</notehead>`);
5335
5846
  }
@@ -5364,8 +5875,9 @@ function serializePitch(pitch, indent, out) {
5364
5875
  out.push(`${indent}</pitch>`);
5365
5876
  }
5366
5877
  function serializeBeam(beam, indent, out) {
5367
- const fanAttr = beam.fan ? ` fan="${beam.fan}"` : "";
5368
- out.push(`${indent}<beam number="${beam.number}"${fanAttr}>${beam.type}</beam>`);
5878
+ let beamAttrs = beam.fan ? ` fan="${beam.fan}"` : "";
5879
+ if (beam.color) beamAttrs += ` color="${escapeXml(beam.color)}"`;
5880
+ out.push(`${indent}<beam number="${beam.number}"${beamAttrs}>${beam.type}</beam>`);
5369
5881
  }
5370
5882
  function serializeNotations(notations, indent, out) {
5371
5883
  const notationsGroups = /* @__PURE__ */ new Map();
@@ -5430,6 +5942,7 @@ function serializeStandaloneNotation(notation, indent, out) {
5430
5942
  let attrs = ` type="${notation.tiedType}"`;
5431
5943
  if (notation.number !== void 0) attrs += ` number="${notation.number}"`;
5432
5944
  if (notation.orientation) attrs += ` orientation="${notation.orientation}"`;
5945
+ attrs += colorAttr(notation.color);
5433
5946
  out.push(`${indent} <tied${attrs}/>`);
5434
5947
  } else if (notation.type === "slur") {
5435
5948
  let attrs = "";
@@ -5444,6 +5957,7 @@ function serializeStandaloneNotation(notation, indent, out) {
5444
5957
  if (notation.bezierX2 !== void 0) attrs += ` bezier-x2="${notation.bezierX2}"`;
5445
5958
  if (notation.bezierY2 !== void 0) attrs += ` bezier-y2="${notation.bezierY2}"`;
5446
5959
  if (notation.placement) attrs += ` placement="${notation.placement}"`;
5960
+ attrs += colorAttr(notation.color);
5447
5961
  out.push(`${indent} <slur${attrs}/>`);
5448
5962
  } else if (notation.type === "tuplet") {
5449
5963
  let attrs = ` type="${notation.tupletType}"`;
@@ -5491,8 +6005,8 @@ function serializeStandaloneNotation(notation, indent, out) {
5491
6005
  out.push(`${indent} <tuplet${attrs}/>`);
5492
6006
  }
5493
6007
  } else if (notation.type === "dynamics") {
5494
- const placementAttr = notation.placement ? ` placement="${notation.placement}"` : "";
5495
- out.push(`${indent} <dynamics${placementAttr}>`);
6008
+ const dynAttrs = (notation.placement ? ` placement="${notation.placement}"` : "") + colorAttr(notation.color);
6009
+ out.push(`${indent} <dynamics${dynAttrs}>`);
5496
6010
  for (const dyn of notation.dynamics) {
5497
6011
  out.push(`${indent} <${dyn}/>`);
5498
6012
  }
@@ -5506,6 +6020,7 @@ function serializeStandaloneNotation(notation, indent, out) {
5506
6020
  if (notation.placement) attrs += ` placement="${notation.placement}"`;
5507
6021
  if (notation.defaultX !== void 0) attrs += ` default-x="${notation.defaultX}"`;
5508
6022
  if (notation.defaultY !== void 0) attrs += ` default-y="${notation.defaultY}"`;
6023
+ attrs += colorAttr(notation.color);
5509
6024
  if (notation.shape) {
5510
6025
  out.push(`${indent} <fermata${attrs}>${notation.shape}</fermata>`);
5511
6026
  } else {
@@ -5517,20 +6032,24 @@ function serializeStandaloneNotation(notation, indent, out) {
5517
6032
  if (notation.number !== void 0) attrs += ` number="${notation.number}"`;
5518
6033
  if (notation.defaultX !== void 0) attrs += ` default-x="${notation.defaultX}"`;
5519
6034
  if (notation.defaultY !== void 0) attrs += ` default-y="${notation.defaultY}"`;
6035
+ attrs += colorAttr(notation.color);
5520
6036
  out.push(`${indent} <arpeggiate${attrs}/>`);
5521
6037
  } else if (notation.type === "non-arpeggiate") {
5522
6038
  let attrs = ` type="${notation.nonArpeggiateType}"`;
5523
6039
  if (notation.number !== void 0) attrs += ` number="${notation.number}"`;
5524
6040
  if (notation.placement) attrs += ` placement="${notation.placement}"`;
6041
+ attrs += colorAttr(notation.color);
5525
6042
  out.push(`${indent} <non-arpeggiate${attrs}/>`);
5526
6043
  } else if (notation.type === "accidental-mark") {
5527
6044
  let attrs = "";
5528
6045
  if (notation.placement) attrs += ` placement="${notation.placement}"`;
6046
+ attrs += colorAttr(notation.color);
5529
6047
  out.push(`${indent} <accidental-mark${attrs}>${escapeXml(notation.value)}</accidental-mark>`);
5530
6048
  } else if (notation.type === "glissando") {
5531
6049
  let attrs = ` type="${notation.glissandoType}"`;
5532
6050
  if (notation.number !== void 0) attrs += ` number="${notation.number}"`;
5533
6051
  if (notation.lineType) attrs += ` line-type="${notation.lineType}"`;
6052
+ attrs += colorAttr(notation.color);
5534
6053
  if (notation.text) {
5535
6054
  out.push(`${indent} <glissando${attrs}>${escapeXml(notation.text)}</glissando>`);
5536
6055
  } else {
@@ -5540,6 +6059,7 @@ function serializeStandaloneNotation(notation, indent, out) {
5540
6059
  let attrs = ` type="${notation.slideType}"`;
5541
6060
  if (notation.number !== void 0) attrs += ` number="${notation.number}"`;
5542
6061
  if (notation.lineType) attrs += ` line-type="${notation.lineType}"`;
6062
+ attrs += colorAttr(notation.color);
5543
6063
  if (notation.text) {
5544
6064
  out.push(`${indent} <slide${attrs}>${escapeXml(notation.text)}</slide>`);
5545
6065
  } else {
@@ -5557,6 +6077,7 @@ function serializeArticulationsGroup(artGroup, indent, out) {
5557
6077
  }
5558
6078
  if (art.defaultX !== void 0) artAttrs += ` default-x="${art.defaultX}"`;
5559
6079
  if (art.defaultY !== void 0) artAttrs += ` default-y="${art.defaultY}"`;
6080
+ artAttrs += colorAttr(art.color);
5560
6081
  out.push(`${indent} <${art.articulation}${artAttrs}/>`);
5561
6082
  }
5562
6083
  }
@@ -5579,6 +6100,7 @@ function serializeOrnamentsGroup(ornaments, indent, out) {
5579
6100
  if (orn.number !== void 0) wlAttrs += ` number="${orn.number}"`;
5580
6101
  wlAttrs += placementAttr;
5581
6102
  if (orn.defaultY !== void 0) wlAttrs += ` default-y="${orn.defaultY}"`;
6103
+ wlAttrs += colorAttr(orn.color);
5582
6104
  out.push(`${indent} <wavy-line${wlAttrs}/>`);
5583
6105
  } else if (orn.ornament === "tremolo") {
5584
6106
  let tremAttrs = "";
@@ -5586,6 +6108,7 @@ function serializeOrnamentsGroup(ornaments, indent, out) {
5586
6108
  tremAttrs += placementAttr;
5587
6109
  if (orn.defaultX !== void 0) tremAttrs += ` default-x="${orn.defaultX}"`;
5588
6110
  if (orn.defaultY !== void 0) tremAttrs += ` default-y="${orn.defaultY}"`;
6111
+ tremAttrs += colorAttr(orn.color);
5589
6112
  if (orn.tremoloMarks !== void 0) {
5590
6113
  out.push(`${indent} <tremolo${tremAttrs}>${orn.tremoloMarks}</tremolo>`);
5591
6114
  } else {
@@ -5594,6 +6117,7 @@ function serializeOrnamentsGroup(ornaments, indent, out) {
5594
6117
  } else {
5595
6118
  let ornAttrs = placementAttr;
5596
6119
  if (orn.defaultY !== void 0) ornAttrs += ` default-y="${orn.defaultY}"`;
6120
+ ornAttrs += colorAttr(orn.color);
5597
6121
  out.push(`${indent} <${orn.ornament}${ornAttrs}/>`);
5598
6122
  }
5599
6123
  if (orn.accidentalMarks) {
@@ -5602,8 +6126,8 @@ function serializeOrnamentsGroup(ornaments, indent, out) {
5602
6126
  }
5603
6127
  }
5604
6128
  for (const am of allAccidentalMarks) {
5605
- const amPlacement = am.placement ? ` placement="${am.placement}"` : "";
5606
- out.push(`${indent} <accidental-mark${amPlacement}>${am.value}</accidental-mark>`);
6129
+ const amAttrs = (am.placement ? ` placement="${am.placement}"` : "") + colorAttr(am.color);
6130
+ out.push(`${indent} <accidental-mark${amAttrs}>${am.value}</accidental-mark>`);
5607
6131
  }
5608
6132
  out.push(`${indent} </ornaments>`);
5609
6133
  }
@@ -5617,6 +6141,7 @@ function serializeTechnicalGroup(technicals, indent, out) {
5617
6141
  if (techNotation.defaultX !== void 0) placementAttr += ` default-x="${techNotation.defaultX}"`;
5618
6142
  if (techNotation.defaultY !== void 0) placementAttr += ` default-y="${techNotation.defaultY}"`;
5619
6143
  if (techNotation.fontSize !== void 0) placementAttr += ` font-size="${techNotation.fontSize}"`;
6144
+ placementAttr += colorAttr(techNotation.color);
5620
6145
  if (tech.technical === "bend" && (techNotation.bendAlter !== void 0 || techNotation.preBend || techNotation.release)) {
5621
6146
  out.push(`${indent} <bend${placementAttr}>`);
5622
6147
  if (techNotation.bendAlter !== void 0) {
@@ -5689,6 +6214,7 @@ function serializeLyric(lyric, indent, out) {
5689
6214
  if (lyric.relativeX !== void 0) attrs += ` relative-x="${lyric.relativeX}"`;
5690
6215
  if (lyric.justify) attrs += ` justify="${escapeXml(lyric.justify)}"`;
5691
6216
  if (lyric.placement) attrs += ` placement="${lyric.placement}"`;
6217
+ attrs += colorAttr(lyric.color);
5692
6218
  out.push(`${indent}<lyric${attrs}>`);
5693
6219
  if (lyric.textElements && lyric.textElements.length > 1) {
5694
6220
  for (let i = 0; i < lyric.textElements.length; i++) {
@@ -5696,7 +6222,7 @@ function serializeLyric(lyric, indent, out) {
5696
6222
  if (te.syllabic) {
5697
6223
  out.push(`${indent} <syllabic>${te.syllabic}</syllabic>`);
5698
6224
  }
5699
- out.push(`${indent} <text>${escapeXml(te.text)}</text>`);
6225
+ out.push(`${indent} <text${colorAttr(te.color)}>${escapeXml(te.text)}</text>`);
5700
6226
  if (i < lyric.textElements.length - 1) {
5701
6227
  out.push(`${indent} <elision/>`);
5702
6228
  }
@@ -5705,11 +6231,13 @@ function serializeLyric(lyric, indent, out) {
5705
6231
  if (lyric.syllabic) {
5706
6232
  out.push(`${indent} <syllabic>${lyric.syllabic}</syllabic>`);
5707
6233
  }
5708
- out.push(`${indent} <text>${escapeXml(lyric.text)}</text>`);
6234
+ const textColor = lyric.textColor ?? lyric.textElements?.[0]?.color;
6235
+ out.push(`${indent} <text${colorAttr(textColor)}>${escapeXml(lyric.text)}</text>`);
5709
6236
  }
5710
6237
  if (lyric.extend) {
5711
- if (typeof lyric.extend === "object" && lyric.extend.type) {
5712
- out.push(`${indent} <extend type="${lyric.extend.type}"/>`);
6238
+ if (typeof lyric.extend === "object") {
6239
+ const extendAttrs = (lyric.extend.type ? ` type="${lyric.extend.type}"` : "") + colorAttr(lyric.extend.color);
6240
+ out.push(`${indent} <extend${extendAttrs}/>`);
5713
6241
  } else {
5714
6242
  out.push(`${indent} <extend/>`);
5715
6243
  }
@@ -5807,6 +6335,7 @@ function serializeDirectionType(dirType, indent, out) {
5807
6335
  if (dirType.defaultY !== void 0) dynAttrs += ` default-y="${dirType.defaultY}"`;
5808
6336
  if (dirType.relativeX !== void 0) dynAttrs += ` relative-x="${dirType.relativeX}"`;
5809
6337
  if (dirType.halign) dynAttrs += ` halign="${dirType.halign}"`;
6338
+ dynAttrs += colorAttr(dirType.color);
5810
6339
  out.push(`${indent} <dynamics${dynAttrs}>`);
5811
6340
  if (dirType.value) {
5812
6341
  out.push(`${indent} <${dirType.value}/>`);
@@ -5822,6 +6351,7 @@ function serializeDirectionType(dirType, indent, out) {
5822
6351
  if (dirType.spread !== void 0) wedgeAttrs += ` spread="${dirType.spread}"`;
5823
6352
  if (dirType.defaultY !== void 0) wedgeAttrs += ` default-y="${dirType.defaultY}"`;
5824
6353
  if (dirType.relativeX !== void 0) wedgeAttrs += ` relative-x="${dirType.relativeX}"`;
6354
+ wedgeAttrs += colorAttr(dirType.color);
5825
6355
  out.push(`${indent} <wedge${wedgeAttrs}/>`);
5826
6356
  break;
5827
6357
  }
@@ -5832,6 +6362,7 @@ function serializeDirectionType(dirType, indent, out) {
5832
6362
  if (dirType.defaultY !== void 0) metAttrs += ` default-y="${dirType.defaultY}"`;
5833
6363
  if (dirType.fontFamily) metAttrs += ` font-family="${escapeXml(dirType.fontFamily)}"`;
5834
6364
  if (dirType.fontSize) metAttrs += ` font-size="${escapeXml(dirType.fontSize)}"`;
6365
+ metAttrs += colorAttr(dirType.color);
5835
6366
  out.push(`${indent} <metronome${metAttrs}>`);
5836
6367
  out.push(`${indent} <beat-unit>${dirType.beatUnit}</beat-unit>`);
5837
6368
  if (dirType.beatUnitDot) {
@@ -5874,6 +6405,7 @@ function serializeDirectionType(dirType, indent, out) {
5874
6405
  if (dirType.defaultY !== void 0) rehAttrs += ` default-y="${dirType.defaultY}"`;
5875
6406
  if (dirType.fontSize) rehAttrs += ` font-size="${escapeXml(dirType.fontSize)}"`;
5876
6407
  if (dirType.fontWeight) rehAttrs += ` font-weight="${escapeXml(dirType.fontWeight)}"`;
6408
+ rehAttrs += colorAttr(dirType.color);
5877
6409
  out.push(`${indent} <rehearsal${rehAttrs}>${escapeXml(dirType.text)}</rehearsal>`);
5878
6410
  break;
5879
6411
  }
@@ -5884,6 +6416,7 @@ function serializeDirectionType(dirType, indent, out) {
5884
6416
  if (dirType.lineType) bracketAttrs += ` line-type="${dirType.lineType}"`;
5885
6417
  if (dirType.defaultY !== void 0) bracketAttrs += ` default-y="${dirType.defaultY}"`;
5886
6418
  if (dirType.relativeX !== void 0) bracketAttrs += ` relative-x="${dirType.relativeX}"`;
6419
+ bracketAttrs += colorAttr(dirType.color);
5887
6420
  out.push(`${indent} <bracket${bracketAttrs}/>`);
5888
6421
  break;
5889
6422
  }
@@ -5893,11 +6426,12 @@ function serializeDirectionType(dirType, indent, out) {
5893
6426
  if (dirType.dashLength !== void 0) dashAttrs += ` dash-length="${dirType.dashLength}"`;
5894
6427
  if (dirType.defaultY !== void 0) dashAttrs += ` default-y="${dirType.defaultY}"`;
5895
6428
  if (dirType.spaceLength !== void 0) dashAttrs += ` space-length="${dirType.spaceLength}"`;
6429
+ dashAttrs += colorAttr(dirType.color);
5896
6430
  out.push(`${indent} <dashes${dashAttrs}/>`);
5897
6431
  break;
5898
6432
  }
5899
6433
  case "accordion-registration":
5900
- out.push(`${indent} <accordion-registration>`);
6434
+ out.push(`${indent} <accordion-registration${colorAttr(dirType.color)}>`);
5901
6435
  if (dirType.high) {
5902
6436
  out.push(`${indent} <accordion-high/>`);
5903
6437
  }
@@ -5920,27 +6454,28 @@ function serializeDirectionType(dirType, indent, out) {
5920
6454
  if (dirType.defaultY !== void 0) otherAttrs += ` default-y="${dirType.defaultY}"`;
5921
6455
  if (dirType.halign) otherAttrs += ` halign="${escapeXml(dirType.halign)}"`;
5922
6456
  if (dirType.printObject === false) otherAttrs += ' print-object="no"';
6457
+ otherAttrs += colorAttr(dirType.color);
5923
6458
  out.push(`${indent} <other-direction${otherAttrs}>${escapeXml(dirType.text)}</other-direction>`);
5924
6459
  }
5925
6460
  break;
5926
6461
  case "segno":
5927
- out.push(`${indent} <segno/>`);
6462
+ out.push(`${indent} <segno${colorAttr(dirType.color)}/>`);
5928
6463
  break;
5929
6464
  case "coda":
5930
- out.push(`${indent} <coda/>`);
6465
+ out.push(`${indent} <coda${colorAttr(dirType.color)}/>`);
5931
6466
  break;
5932
6467
  case "eyeglasses":
5933
- out.push(`${indent} <eyeglasses/>`);
6468
+ out.push(`${indent} <eyeglasses${colorAttr(dirType.color)}/>`);
5934
6469
  break;
5935
6470
  case "damp":
5936
- out.push(`${indent} <damp/>`);
6471
+ out.push(`${indent} <damp${colorAttr(dirType.color)}/>`);
5937
6472
  break;
5938
6473
  case "damp-all":
5939
- out.push(`${indent} <damp-all/>`);
6474
+ out.push(`${indent} <damp-all${colorAttr(dirType.color)}/>`);
5940
6475
  break;
5941
6476
  case "scordatura":
5942
6477
  if (dirType.accords && dirType.accords.length > 0) {
5943
- out.push(`${indent} <scordatura>`);
6478
+ out.push(`${indent} <scordatura${colorAttr(dirType.color)}>`);
5944
6479
  for (const accord of dirType.accords) {
5945
6480
  out.push(`${indent} <accord string="${accord.string}">`);
5946
6481
  out.push(`${indent} <tuning-step>${accord.tuningStep}</tuning-step>`);
@@ -5952,12 +6487,12 @@ function serializeDirectionType(dirType, indent, out) {
5952
6487
  }
5953
6488
  out.push(`${indent} </scordatura>`);
5954
6489
  } else {
5955
- out.push(`${indent} <scordatura/>`);
6490
+ out.push(`${indent} <scordatura${colorAttr(dirType.color)}/>`);
5956
6491
  }
5957
6492
  break;
5958
6493
  case "harp-pedals":
5959
6494
  if (dirType.pedalTunings && dirType.pedalTunings.length > 0) {
5960
- out.push(`${indent} <harp-pedals>`);
6495
+ out.push(`${indent} <harp-pedals${colorAttr(dirType.color)}>`);
5961
6496
  for (const pt of dirType.pedalTunings) {
5962
6497
  out.push(`${indent} <pedal-tuning>`);
5963
6498
  out.push(`${indent} <pedal-step>${pt.pedalStep}</pedal-step>`);
@@ -5966,7 +6501,7 @@ function serializeDirectionType(dirType, indent, out) {
5966
6501
  }
5967
6502
  out.push(`${indent} </harp-pedals>`);
5968
6503
  } else {
5969
- out.push(`${indent} <harp-pedals/>`);
6504
+ out.push(`${indent} <harp-pedals${colorAttr(dirType.color)}/>`);
5970
6505
  }
5971
6506
  break;
5972
6507
  case "image":
@@ -5981,12 +6516,13 @@ function serializeDirectionType(dirType, indent, out) {
5981
6516
  if (dirType.defaultY !== void 0) pedalAttrs += ` default-y="${dirType.defaultY}"`;
5982
6517
  if (dirType.relativeX !== void 0) pedalAttrs += ` relative-x="${dirType.relativeX}"`;
5983
6518
  if (dirType.halign) pedalAttrs += ` halign="${dirType.halign}"`;
6519
+ pedalAttrs += colorAttr(dirType.color);
5984
6520
  out.push(`${indent} <pedal${pedalAttrs}/>`);
5985
6521
  break;
5986
6522
  }
5987
6523
  case "octave-shift": {
5988
6524
  const sizeAttr = dirType.size !== void 0 ? ` size="${dirType.size}"` : "";
5989
- out.push(`${indent} <octave-shift type="${dirType.type}"${sizeAttr}/>`);
6525
+ out.push(`${indent} <octave-shift type="${dirType.type}"${sizeAttr}${colorAttr(dirType.color)}/>`);
5990
6526
  break;
5991
6527
  }
5992
6528
  case "swing":
@@ -6014,12 +6550,13 @@ function serializeBarline(barline, indent, out) {
6014
6550
  if (barline._id) attrs += ` id="${escapeXml(barline._id)}"`;
6015
6551
  out.push(`${indent}<barline${attrs}>`);
6016
6552
  if (barline.barStyle) {
6017
- out.push(`${indent} <bar-style>${barline.barStyle}</bar-style>`);
6553
+ out.push(`${indent} <bar-style${colorAttr(barline.barStyleColor)}>${barline.barStyle}</bar-style>`);
6018
6554
  }
6019
6555
  if (barline.ending) {
6020
6556
  let endingAttrs = ` number="${barline.ending.number}" type="${barline.ending.type}"`;
6021
6557
  if (barline.ending.defaultY !== void 0) endingAttrs += ` default-y="${barline.ending.defaultY}"`;
6022
6558
  if (barline.ending.endLength !== void 0) endingAttrs += ` end-length="${barline.ending.endLength}"`;
6559
+ endingAttrs += colorAttr(barline.ending.color);
6023
6560
  if (barline.ending.text) {
6024
6561
  out.push(`${indent} <ending${endingAttrs}>${escapeXml(barline.ending.text)}</ending>`);
6025
6562
  } else {
@@ -6047,6 +6584,9 @@ function escapeXml(str) {
6047
6584
  if (!XML_ESCAPE_TEST.test(str)) return str;
6048
6585
  return str.replace(XML_ESCAPE_RE, (ch) => XML_ESCAPE_MAP[ch]);
6049
6586
  }
6587
+ function colorAttr(color) {
6588
+ return color ? ` color="${escapeXml(color)}"` : "";
6589
+ }
6050
6590
  function buildAttrs(attrs) {
6051
6591
  let result = "";
6052
6592
  for (const key in attrs) {
@@ -6095,7 +6635,7 @@ function serializeStaffDetails(sd, indent, out) {
6095
6635
  out.push(`${indent}</staff-details>`);
6096
6636
  }
6097
6637
  function serializeMeasureStyle(ms, indent, out) {
6098
- out.push(`${indent}<measure-style${buildAttrs({ number: ms.number })}>`);
6638
+ out.push(`${indent}<measure-style${buildAttrs({ number: ms.number, color: ms.color })}>`);
6099
6639
  pushOptionalElement(out, `${indent} `, "multiple-rest", ms.multipleRest);
6100
6640
  if (ms.measureRepeat) {
6101
6641
  const mrAttrs = buildAttrs({
@@ -6125,7 +6665,8 @@ function serializeHarmony(harmony, indent, out) {
6125
6665
  "print-frame": harmony.printFrame,
6126
6666
  "default-y": harmony.defaultY,
6127
6667
  halign: harmony.halign,
6128
- "font-size": harmony.fontSize
6668
+ "font-size": harmony.fontSize,
6669
+ color: harmony.color
6129
6670
  });
6130
6671
  out.push(`${indent}<harmony${attrs}>`);
6131
6672
  out.push(`${indent} <root>`);
@@ -6137,6 +6678,7 @@ function serializeHarmony(harmony, indent, out) {
6137
6678
  let kindAttrs = "";
6138
6679
  if (harmony.kindText !== void 0) kindAttrs += ` text="${escapeXml(harmony.kindText)}"`;
6139
6680
  if (harmony.kindHalign) kindAttrs += ` halign="${escapeXml(harmony.kindHalign)}"`;
6681
+ kindAttrs += colorAttr(harmony.kindColor);
6140
6682
  out.push(`${indent} <kind${kindAttrs}>${escapeXml(harmony.kind)}</kind>`);
6141
6683
  if (harmony.bass) {
6142
6684
  let bassAttrs = "";
@@ -6163,7 +6705,7 @@ function serializeHarmony(harmony, indent, out) {
6163
6705
  }
6164
6706
  }
6165
6707
  if (harmony.frame) {
6166
- out.push(`${indent} <frame>`);
6708
+ out.push(`${indent} <frame${colorAttr(harmony.frame.color)}>`);
6167
6709
  if (harmony.frame.frameStrings !== void 0) {
6168
6710
  out.push(`${indent} <frame-strings>${harmony.frame.frameStrings}</frame-strings>`);
6169
6711
  }
@@ -6204,6 +6746,7 @@ function serializeFiguredBass(fb, indent, out) {
6204
6746
  let attrs = "";
6205
6747
  if (fb._id) attrs += ` id="${escapeXml(fb._id)}"`;
6206
6748
  if (fb.parentheses) attrs += ' parentheses="yes"';
6749
+ attrs += colorAttr(fb.color);
6207
6750
  out.push(`${indent}<figured-bass${attrs}>`);
6208
6751
  for (const fig of fb.figures) {
6209
6752
  out.push(`${indent} <figure>`);
@@ -6217,8 +6760,9 @@ function serializeFiguredBass(fb, indent, out) {
6217
6760
  out.push(`${indent} <suffix>${escapeXml(fig.suffix)}</suffix>`);
6218
6761
  }
6219
6762
  if (fig.extend) {
6220
- if (typeof fig.extend === "object" && fig.extend.type) {
6221
- out.push(`${indent} <extend type="${fig.extend.type}"/>`);
6763
+ if (typeof fig.extend === "object") {
6764
+ const extendAttrs = (fig.extend.type ? ` type="${fig.extend.type}"` : "") + colorAttr(fig.extend.color);
6765
+ out.push(`${indent} <extend${extendAttrs}/>`);
6222
6766
  } else {
6223
6767
  out.push(`${indent} <extend/>`);
6224
6768
  }
@@ -6401,8 +6945,10 @@ function buildMidiTracks(score, options) {
6401
6945
  let program = 1;
6402
6946
  if (partEntry && partEntry.midiInstruments?.[0]) {
6403
6947
  const midiInst = partEntry.midiInstruments[0];
6404
- channel = midiInst.channel ?? channel;
6405
- program = midiInst.program ?? program;
6948
+ if (midiInst.channel !== void 0) {
6949
+ channel = Math.min(16, Math.max(1, midiInst.channel)) - 1;
6950
+ }
6951
+ program = Math.min(128, Math.max(1, midiInst.program ?? program));
6406
6952
  }
6407
6953
  const trackData = createPartTrack(
6408
6954
  part,
@@ -6682,6 +7228,8 @@ function buildMidiFile(tracks, ticksPerQuarterNote) {
6682
7228
  var DIVISIONS_PER_QUARTER = 960;
6683
7229
  var useExplicitHalf = false;
6684
7230
  var useIndividualChordDurations = false;
7231
+ var openWedge = null;
7232
+ var voiceOctaveShift = 0;
6685
7233
  var FIFTHS_TO_KEY_MAJOR = {
6686
7234
  [-7]: "Cb",
6687
7235
  [-6]: "Gb",
@@ -6740,15 +7288,21 @@ var NOTE_TYPE_TO_QUARTER_LENGTH = {
6740
7288
  "128th": 0.03125
6741
7289
  };
6742
7290
  function musicXmlClefToAbc(clef) {
6743
- if (clef.sign === "G" && (clef.line === 2 || clef.line === void 0)) return "treble";
6744
- if (clef.sign === "F" && (clef.line === 4 || clef.line === void 0)) return "bass";
6745
- if (clef.sign === "C" && clef.line === 3) return "alto";
6746
- if (clef.sign === "C" && clef.line === 4) return "tenor";
6747
- if (clef.sign === "C" && clef.line === 1) return "soprano";
6748
- if (clef.sign === "C" && clef.line === 2) return "mezzo-soprano";
6749
- if (clef.sign === "C" && clef.line === 5) return "baritone";
6750
- if (clef.sign === "percussion") return "perc";
6751
- return null;
7291
+ let name = null;
7292
+ if (clef.sign === "G" && (clef.line === 2 || clef.line === void 0)) name = "treble";
7293
+ else if (clef.sign === "F" && clef.line === 3) name = "bass3";
7294
+ else if (clef.sign === "F" && (clef.line === 4 || clef.line === void 0)) name = "bass";
7295
+ else if (clef.sign === "C" && clef.line === 3) name = "alto";
7296
+ else if (clef.sign === "C" && clef.line === 4) name = "tenor";
7297
+ else if (clef.sign === "C" && clef.line === 1) name = "soprano";
7298
+ else if (clef.sign === "C" && clef.line === 2) name = "mezzo-soprano";
7299
+ else if (clef.sign === "C" && clef.line === 5) name = "baritone";
7300
+ else if (clef.sign === "percussion") name = "perc";
7301
+ if (name === null) return null;
7302
+ const shift = clef.clefOctaveChange;
7303
+ if (shift === void 0 || shift === 0) return name;
7304
+ const steps = Math.abs(shift) === 2 ? "15" : "8";
7305
+ return `${name}${shift > 0 ? "+" : "-"}${steps}`;
6752
7306
  }
6753
7307
  function serializeKey2(key) {
6754
7308
  const mode = key.mode || "major";
@@ -6786,17 +7340,18 @@ function modeToAbcString(mode) {
6786
7340
  }
6787
7341
  }
6788
7342
  function serializeTimeSignature(time) {
7343
+ if (time.senzaMisura) return "none";
6789
7344
  if (time.symbol === "common") return "C";
6790
7345
  if (time.symbol === "cut") return "C|";
7346
+ if (time.beats.includes("+")) return `(${time.beats})/${time.beatType}`;
6791
7347
  return `${time.beats}/${time.beatType}`;
6792
7348
  }
6793
7349
  function computeUnitNoteLength(score) {
6794
7350
  const firstMeasure = score.parts[0]?.measures[0];
6795
7351
  const time = firstMeasure?.attributes?.time;
6796
7352
  if (time) {
6797
- const beats = parseInt(time.beats, 10);
6798
- const beatType = time.beatType;
6799
- const ratio = beats / beatType;
7353
+ const beats = time.beats.split("+").reduce((sum, b) => sum + parseInt(b, 10), 0);
7354
+ const ratio = beats / time.beatType;
6800
7355
  if (ratio < 0.75) {
6801
7356
  return { num: 1, den: 16 };
6802
7357
  }
@@ -6826,6 +7381,17 @@ function formatAbcDuration(num, den) {
6826
7381
  }
6827
7382
  return `${num}/${den}`;
6828
7383
  }
7384
+ function serializeMicrotone(alter) {
7385
+ const symbol = alter > 0 ? "^" : "_";
7386
+ const magnitude = Math.abs(alter);
7387
+ for (let den = 2; den <= 64; den++) {
7388
+ const num = magnitude * den;
7389
+ if (Math.abs(num - Math.round(num)) < 1e-9) {
7390
+ return `${symbol}${Math.round(num)}/${den}`;
7391
+ }
7392
+ }
7393
+ return magnitude >= 1.5 ? symbol + symbol : symbol;
7394
+ }
6829
7395
  function serializePitch2(pitch, explicitNatural) {
6830
7396
  let result = "";
6831
7397
  if (explicitNatural) {
@@ -6835,9 +7401,10 @@ function serializePitch2(pitch, explicitNatural) {
6835
7401
  else if (pitch.alter === 2) result += "^^";
6836
7402
  else if (pitch.alter === -1) result += "_";
6837
7403
  else if (pitch.alter === -2) result += "__";
7404
+ else result += serializeMicrotone(pitch.alter);
6838
7405
  }
6839
7406
  const step = pitch.step;
6840
- const octave = pitch.octave;
7407
+ const octave = pitch.octave - voiceOctaveShift;
6841
7408
  if (octave >= 5) {
6842
7409
  result += step.toLowerCase();
6843
7410
  for (let o = 6; o <= octave; o++) {
@@ -6912,14 +7479,85 @@ function serializeHarmony2(harmony) {
6912
7479
  }
6913
7480
  return '"' + result + '"';
6914
7481
  }
6915
- function serializeDynamics(direction) {
7482
+ function notationToDecoration(notation) {
7483
+ switch (notation.type) {
7484
+ case "articulation":
7485
+ return { kind: "articulation", articulation: notation.articulation };
7486
+ case "ornament":
7487
+ if (notation.ornament === "wavy-line") {
7488
+ if (notation.wavyLineType === "start" || notation.wavyLineType === "stop") {
7489
+ return { kind: "trill-line", wavyLineType: notation.wavyLineType };
7490
+ }
7491
+ return null;
7492
+ }
7493
+ return { kind: "ornament", ornament: notation.ornament };
7494
+ case "technical":
7495
+ if (notation.technical === "fingering") {
7496
+ const digits = notation.text ?? notation.fingering;
7497
+ return digits !== void 0 ? { kind: "fingering", fingering: digits } : null;
7498
+ }
7499
+ return { kind: "technical", technical: notation.technical };
7500
+ case "fermata":
7501
+ return { kind: "fermata", inverted: notation.fermataType === "inverted" };
7502
+ case "arpeggiate":
7503
+ return { kind: "arpeggiate" };
7504
+ default:
7505
+ return null;
7506
+ }
7507
+ }
7508
+ function serializeNoteDecorations(note) {
7509
+ if (!note.notations) return "";
7510
+ let result = "";
7511
+ for (const notation of note.notations) {
7512
+ const target = notationToDecoration(notation);
7513
+ if (!target) continue;
7514
+ const spelling = abcDecorationFor(target);
7515
+ if (spelling) result += spelling;
7516
+ }
7517
+ return result;
7518
+ }
7519
+ function serializeDirectionText(direction, opts) {
6916
7520
  for (const dt of direction.directionTypes) {
6917
- if (dt.kind === "dynamics" && dt.value) {
6918
- return `!${dt.value}!`;
7521
+ switch (dt.kind) {
7522
+ case "dynamics":
7523
+ return opts.includeDynamics && dt.value ? `!${dt.value}!` : null;
7524
+ case "wedge": {
7525
+ if (dt.type === "stop") {
7526
+ const closing = openWedge ?? "crescendo";
7527
+ openWedge = null;
7528
+ return abcDecorationFor({ kind: "wedge", wedge: closing, stop: true });
7529
+ }
7530
+ openWedge = dt.type;
7531
+ return abcDecorationFor({ kind: "wedge", wedge: dt.type, stop: false });
7532
+ }
7533
+ case "segno":
7534
+ return abcDecorationFor({ kind: "segno" });
7535
+ case "coda":
7536
+ return abcDecorationFor({ kind: "coda" });
7537
+ case "words":
7538
+ return serializeWordsDirection(dt.text, direction);
7539
+ default:
7540
+ break;
6919
7541
  }
6920
7542
  }
6921
7543
  return null;
6922
7544
  }
7545
+ function serializeWordsDirection(text, direction) {
7546
+ if (/^![^!]+!$/.test(text)) return text;
7547
+ if (text.length === 1 && ABC_SHORTHAND_DECORATIONS.has(text)) return text;
7548
+ if (/^y\d*$/.test(text)) return text;
7549
+ const asDecoration = abcDecorationFor({ kind: "words", text });
7550
+ if (asDecoration) return asDecoration;
7551
+ if (text === "") return null;
7552
+ const halign = direction.directionTypes.find((d) => d.kind === "words" && d.text === text);
7553
+ let marker = "^";
7554
+ if (direction.placement === "below") marker = "_";
7555
+ else if (direction.placement === "above") marker = "^";
7556
+ else if (halign?.halign === "right") marker = "<";
7557
+ else if (halign?.halign === "left") marker = ">";
7558
+ else if (halign?.halign === "center") marker = "@";
7559
+ return `"${marker}${text}"`;
7560
+ }
6923
7561
  function serializeTempo(direction) {
6924
7562
  for (const dt of direction.directionTypes) {
6925
7563
  if (dt.kind === "metronome") {
@@ -6933,6 +7571,13 @@ function serializeTempo(direction) {
6933
7571
  }
6934
7572
  return null;
6935
7573
  }
7574
+ function serializeAbcTunes(scores, options) {
7575
+ return scores.map((score, index) => serializeAbc(score, {
7576
+ ...options,
7577
+ // Number the tunes when they carry no reference number of their own
7578
+ referenceNumber: options?.referenceNumber ?? index + 1
7579
+ })).join("\n");
7580
+ }
6936
7581
  function serializeAbc(score, options) {
6937
7582
  const opts = {
6938
7583
  referenceNumber: options?.referenceNumber ?? 1,
@@ -6941,6 +7586,8 @@ function serializeAbc(score, options) {
6941
7586
  includeDynamics: options?.includeDynamics ?? true,
6942
7587
  includeLyrics: options?.includeLyrics ?? true
6943
7588
  };
7589
+ openWedge = null;
7590
+ voiceOctaveShift = 0;
6944
7591
  useExplicitHalf = score.metadata.miscellaneous?.find((m) => m.name === "abc-explicit-half")?.value === "true";
6945
7592
  useIndividualChordDurations = score.metadata.miscellaneous?.some((m) => m.name === "abc-chord-individual-durations" && m.value === "true") ?? false;
6946
7593
  const storedUnitNote = score.metadata.miscellaneous?.find((m) => m.name === "abc-unit-note-length")?.value;
@@ -7015,11 +7662,13 @@ function serializeAbc(score, options) {
7015
7662
  lines.push(`Q:${tempoStr}`);
7016
7663
  }
7017
7664
  }
7018
- if (attrs?.key) {
7019
- lines.push(`K:${serializeKey2(attrs.key)}`);
7020
- } else {
7021
- lines.push("K:C");
7665
+ let clefSuffix = "";
7666
+ if (score.parts.length === 1) {
7667
+ const clef = attrs?.clef?.[0];
7668
+ const clefName = clef ? musicXmlClefToAbc(clef) : null;
7669
+ if (clefName && clefName !== "treble") clefSuffix = ` clef=${clefName}`;
7022
7670
  }
7671
+ lines.push(`K:${attrs?.key ? serializeKey2(attrs.key) : "C"}${clefSuffix}`);
7023
7672
  }
7024
7673
  const lineBreaksStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-line-breaks")?.value;
7025
7674
  const lineBreaks = lineBreaksStr ? JSON.parse(lineBreaksStr) : [];
@@ -7028,6 +7677,8 @@ function serializeAbc(score, options) {
7028
7677
  const lyricsAfterAll = score.metadata.miscellaneous?.find((m) => m.name === "abc-lyrics-after-all")?.value === "true";
7029
7678
  const lyricsLineCountsStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-lyrics-line-counts")?.value;
7030
7679
  const lyricsLineCounts = lyricsLineCountsStr ? JSON.parse(lyricsLineCountsStr) : [];
7680
+ const lyricsLineVersesStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-lyrics-line-verses")?.value;
7681
+ const lyricsLineVerses = lyricsLineVersesStr ? JSON.parse(lyricsLineVersesStr) : [];
7031
7682
  const inlineVoiceMarkersStr = score.metadata.miscellaneous?.find((m) => m.name === "abc-inline-voice-markers")?.value;
7032
7683
  const inlineVoiceMarkers = inlineVoiceMarkersStr ? JSON.parse(inlineVoiceMarkersStr) : {};
7033
7684
  const useInlineVoiceMarkers = Object.keys(inlineVoiceMarkers).length > 0;
@@ -7063,19 +7714,27 @@ function serializeAbc(score, options) {
7063
7714
  const partDivisions = [];
7064
7715
  for (let partIdx = 0; partIdx < score.parts.length; partIdx++) {
7065
7716
  const part = score.parts[partIdx];
7717
+ voiceOctaveShift = readVoiceOctaveShift(score, partIdx);
7066
7718
  const divisions = getPartDivisions(part);
7067
7719
  partDivisions.push(divisions);
7068
7720
  const measStrings = [];
7069
7721
  let currentUnitNote = { ...unitNote };
7070
7722
  let currentKey = part.measures[0]?.attributes?.key;
7723
+ const absorbedMeasures = /* @__PURE__ */ new Set();
7071
7724
  for (let mi = 0; mi < part.measures.length; mi++) {
7725
+ if (absorbedMeasures.has(mi)) {
7726
+ measStrings.push("");
7727
+ continue;
7728
+ }
7072
7729
  const measure = part.measures[mi];
7073
7730
  const measDivisions = measure.attributes?.divisions ?? divisions;
7074
7731
  let measureStr = "";
7075
7732
  if (mi > 0 && measure.attributes?.key) {
7076
7733
  const newKey = measure.attributes.key;
7077
7734
  if (currentKey === void 0 || newKey.fifths !== currentKey.fifths || (newKey.mode || "major") !== (currentKey.mode || "major")) {
7078
- measureStr += "\nK:" + serializeKey2(newKey) + "\n";
7735
+ if (!hasInlineKeyMarker(measure)) {
7736
+ measureStr += "\nK:" + serializeKey2(newKey) + "\n";
7737
+ }
7079
7738
  currentKey = newKey;
7080
7739
  }
7081
7740
  }
@@ -7085,10 +7744,13 @@ function serializeAbc(score, options) {
7085
7744
  }
7086
7745
  const { noteStr, updatedUnitNote } = serializeMeasureEntries(measure, measDivisions, currentUnitNote, opts);
7087
7746
  if (updatedUnitNote) currentUnitNote = updatedUnitNote;
7088
- measureStr += noteStr;
7747
+ const collapsed = applyMultiMeasureRest(measure, noteStr);
7748
+ for (let k = 1; k <= collapsed.absorbed; k++) absorbedMeasures.add(mi + k);
7749
+ measureStr += collapsed.text;
7089
7750
  const rightBarline = measure.barlines?.find((b) => b.location === "right");
7090
7751
  if (rightBarline) {
7091
7752
  measureStr += serializeBarline2(rightBarline);
7753
+ } else if (!measureHasNotes(measure)) {
7092
7754
  } else if (mi < part.measures.length - 1) {
7093
7755
  const nextMeasure = part.measures[mi + 1];
7094
7756
  const nextLeftBarline = nextMeasure?.barlines?.find((b) => b.location === "left" && (b.barStyle || b.repeat));
@@ -7177,7 +7839,7 @@ function serializeAbc(score, options) {
7177
7839
  }
7178
7840
  }
7179
7841
  voiceMeasureCursor[voiceId] = endMeasure;
7180
- lines.push(groupMusic);
7842
+ lines.push(stripLeadingBlankLines(groupMusic));
7181
7843
  }
7182
7844
  if (gi < voiceInterleavePattern.length - 1 && commentIdx < bodyComments.length) {
7183
7845
  lines.push(bodyComments[commentIdx]);
@@ -7204,14 +7866,15 @@ function serializeAbc(score, options) {
7204
7866
  }
7205
7867
  }
7206
7868
  const divisions = partDivisions[partIdx];
7207
- const bodyResult = serializePartBody(part, divisions, unitNote, opts, lineBreaks, lyricsAfterAll, lyricsLineCounts);
7869
+ voiceOctaveShift = readVoiceOctaveShift(score, partIdx);
7870
+ const bodyResult = serializePartBody(part, divisions, unitNote, opts, lineBreaks, lyricsAfterAll, lyricsLineCounts, lyricsLineVerses);
7208
7871
  let musicLine = bodyResult.music;
7209
7872
  if (multiVoice && useInlineVoiceMarkers) {
7210
7873
  const voiceId = storedVoiceIds[partIdx] || String(partIdx + 1);
7211
7874
  const marker = inlineVoiceMarkers[voiceId] || `[V:${voiceId}]`;
7212
7875
  musicLine = marker + musicLine;
7213
7876
  }
7214
- lines.push(musicLine);
7877
+ lines.push(stripLeadingBlankLines(musicLine));
7215
7878
  if (opts.includeLyrics && bodyResult.lyrics) {
7216
7879
  lines.push(bodyResult.lyrics);
7217
7880
  }
@@ -7230,6 +7893,16 @@ function serializeAbc(score, options) {
7230
7893
  }
7231
7894
  return lines.join("\n") + "\n";
7232
7895
  }
7896
+ function stripLeadingBlankLines(text) {
7897
+ return text.replace(/^\n+/, "");
7898
+ }
7899
+ function readVoiceOctaveShift(score, partIndex) {
7900
+ const stored = score.metadata.miscellaneous?.find(
7901
+ (m) => m.name === `abc-voice-octave-${partIndex}`
7902
+ )?.value;
7903
+ const shift = stored ? parseInt(stored, 10) : 0;
7904
+ return Number.isFinite(shift) ? shift : 0;
7905
+ }
7233
7906
  function findTempoInMeasure(measure) {
7234
7907
  if (!measure) return null;
7235
7908
  for (const entry of measure.entries) {
@@ -7253,18 +7926,22 @@ function getPartDivisions(part) {
7253
7926
  }
7254
7927
  return DIVISIONS_PER_QUARTER;
7255
7928
  }
7256
- function serializePartBody(part, divisions, initialUnitNote, opts, lineBreaks = [], lyricsAfterAll = false, lyricsLineCounts = []) {
7929
+ function serializePartBody(part, divisions, initialUnitNote, opts, lineBreaks = [], lyricsAfterAll = false, lyricsLineCounts = [], lyricsLineVerses = []) {
7257
7930
  let unitNote = { ...initialUnitNote };
7258
7931
  const musicParts = [];
7259
7932
  const allLyrics = /* @__PURE__ */ new Map();
7260
7933
  let currentKey = part.measures[0]?.attributes?.key;
7934
+ const absorbedMeasures = /* @__PURE__ */ new Set();
7261
7935
  for (let mi = 0; mi < part.measures.length; mi++) {
7936
+ if (absorbedMeasures.has(mi)) continue;
7262
7937
  const measure = part.measures[mi];
7263
7938
  const measDivisions = measure.attributes?.divisions ?? divisions;
7264
7939
  if (mi > 0 && measure.attributes?.key) {
7265
7940
  const newKey = measure.attributes.key;
7266
7941
  if (currentKey === void 0 || newKey.fifths !== currentKey.fifths || (newKey.mode || "major") !== (currentKey.mode || "major")) {
7267
- musicParts.push("\nK:" + serializeKey2(newKey) + "\n");
7942
+ if (!hasInlineKeyMarker(measure)) {
7943
+ musicParts.push("\nK:" + serializeKey2(newKey) + "\n");
7944
+ }
7268
7945
  currentKey = newKey;
7269
7946
  }
7270
7947
  }
@@ -7281,13 +7958,16 @@ function serializePartBody(part, divisions, initialUnitNote, opts, lineBreaks =
7281
7958
  if (updatedUnitNote) {
7282
7959
  unitNote = updatedUnitNote;
7283
7960
  }
7284
- musicParts.push(noteStr);
7285
- if (lyrics.length > 0) {
7961
+ const collapsed = applyMultiMeasureRest(measure, noteStr);
7962
+ for (let k = 1; k <= collapsed.absorbed; k++) absorbedMeasures.add(mi + k);
7963
+ musicParts.push(collapsed.text);
7964
+ if (lyrics.size > 0) {
7286
7965
  allLyrics.set(mi, lyrics);
7287
7966
  }
7288
7967
  const rightBarline = measure.barlines?.find((b) => b.location === "right");
7289
7968
  if (rightBarline) {
7290
7969
  musicParts.push(serializeBarline2(rightBarline));
7970
+ } else if (!measureHasNotes(measure)) {
7291
7971
  } else if (mi < part.measures.length - 1) {
7292
7972
  const nextMeas = part.measures[mi + 1];
7293
7973
  const nextLeftBar = nextMeas?.barlines?.find((b) => b.location === "left" && (b.barStyle || b.repeat));
@@ -7314,18 +7994,27 @@ function serializePartBody(part, divisions, initialUnitNote, opts, lineBreaks =
7314
7994
  if (allLyrics.size === 0) {
7315
7995
  return { music: musicStr, lyrics: null };
7316
7996
  }
7997
+ const verses = /* @__PURE__ */ new Set();
7998
+ for (const byVerse of allLyrics.values()) {
7999
+ for (const verse of byVerse.keys()) verses.add(verse);
8000
+ }
8001
+ const sortedVerses = Array.from(verses).sort((a, b) => a - b);
8002
+ function syllablesIn(verse, from, to) {
8003
+ const syllables = [];
8004
+ for (let m = from; m <= to; m++) {
8005
+ const forVerse = allLyrics.get(m)?.get(verse);
8006
+ if (forVerse) syllables.push(...forVerse);
8007
+ }
8008
+ return syllables;
8009
+ }
7317
8010
  const lineBreakSet = new Set(lineBreaks.map((v) => Math.abs(v)));
7318
- const lyricsByLine = [];
8011
+ const lyricLineRanges = [];
7319
8012
  let lineStart = 0;
7320
8013
  for (let mi = 0; mi < part.measures.length; mi++) {
7321
8014
  if (lineBreakSet.has(mi + 1) || mi === part.measures.length - 1) {
7322
- const syllables = [];
7323
- for (let m = lineStart; m <= mi; m++) {
7324
- const ls = allLyrics.get(m);
7325
- if (ls) syllables.push(...ls);
7326
- }
7327
- if (syllables.length > 0) {
7328
- lyricsByLine.push({ startMeasure: lineStart, endMeasure: mi, syllables });
8015
+ const hasAny = sortedVerses.some((v) => syllablesIn(v, lineStart, mi).length > 0);
8016
+ if (hasAny) {
8017
+ lyricLineRanges.push({ startMeasure: lineStart, endMeasure: mi });
7329
8018
  }
7330
8019
  lineStart = mi + 1;
7331
8020
  }
@@ -7345,57 +8034,60 @@ function serializePartBody(part, divisions, initialUnitNote, opts, lineBreaks =
7345
8034
  }
7346
8035
  return result;
7347
8036
  }
7348
- if (lyricsAfterAll) {
7349
- const allSyllables2 = [];
7350
- const sortedMeasures2 = Array.from(allLyrics.keys()).sort((a, b) => a - b);
7351
- for (const mi of sortedMeasures2) {
7352
- allSyllables2.push(...allLyrics.get(mi));
8037
+ function lyricLinesFor(from, to) {
8038
+ const out = [];
8039
+ for (const verse of sortedVerses) {
8040
+ const syllables = syllablesIn(verse, from, to);
8041
+ if (syllables.length > 0) out.push(formatLyrics(syllables));
7353
8042
  }
8043
+ return out;
8044
+ }
8045
+ const lastMeasure = part.measures.length - 1;
8046
+ if (lyricsAfterAll) {
7354
8047
  if (lyricsLineCounts.length > 0) {
7355
8048
  const lyricsLines = [];
7356
- let offset = 0;
7357
- for (const count of lyricsLineCounts) {
7358
- const chunk = allSyllables2.slice(offset, offset + count);
7359
- if (chunk.length > 0) {
7360
- lyricsLines.push(formatLyrics(chunk));
7361
- }
7362
- offset += count;
7363
- }
7364
- if (offset < allSyllables2.length) {
7365
- lyricsLines.push(formatLyrics(allSyllables2.slice(offset)));
8049
+ const offsets = /* @__PURE__ */ new Map();
8050
+ for (let li = 0; li < lyricsLineCounts.length; li++) {
8051
+ const verse = lyricsLineVerses[li] ?? 1;
8052
+ const all = syllablesIn(verse, 0, lastMeasure);
8053
+ const offset = offsets.get(verse) ?? 0;
8054
+ const chunk = all.slice(offset, offset + lyricsLineCounts[li]);
8055
+ if (chunk.length > 0) lyricsLines.push(formatLyrics(chunk));
8056
+ offsets.set(verse, offset + lyricsLineCounts[li]);
8057
+ }
8058
+ for (const verse of sortedVerses) {
8059
+ const all = syllablesIn(verse, 0, lastMeasure);
8060
+ const offset = offsets.get(verse) ?? 0;
8061
+ if (offset < all.length) lyricsLines.push(formatLyrics(all.slice(offset)));
7366
8062
  }
7367
8063
  return { music: musicStr, lyrics: lyricsLines.join("\n") };
7368
8064
  }
7369
- return { music: musicStr, lyrics: formatLyrics(allSyllables2) };
8065
+ return { music: musicStr, lyrics: lyricLinesFor(0, lastMeasure).join("\n") };
7370
8066
  }
7371
- if (lyricsByLine.length > 0 && lineBreaks.length > 0) {
8067
+ if (lyricLineRanges.length > 0 && lineBreaks.length > 0) {
7372
8068
  const musicLines = musicStr.split("\n");
7373
8069
  const resultLines = [];
7374
8070
  let lyricIdx = 0;
7375
8071
  for (let li = 0; li < musicLines.length; li++) {
7376
8072
  resultLines.push(musicLines[li]);
7377
- if (lyricIdx < lyricsByLine.length) {
7378
- const lyricGroup = lyricsByLine[lyricIdx];
7379
- resultLines.push(formatLyrics(lyricGroup.syllables));
8073
+ if (lyricIdx < lyricLineRanges.length) {
8074
+ const range = lyricLineRanges[lyricIdx];
8075
+ resultLines.push(...lyricLinesFor(range.startMeasure, range.endMeasure));
7380
8076
  lyricIdx++;
7381
8077
  }
7382
8078
  }
7383
- while (lyricIdx < lyricsByLine.length) {
7384
- resultLines.push(formatLyrics(lyricsByLine[lyricIdx].syllables));
8079
+ while (lyricIdx < lyricLineRanges.length) {
8080
+ const range = lyricLineRanges[lyricIdx];
8081
+ resultLines.push(...lyricLinesFor(range.startMeasure, range.endMeasure));
7385
8082
  lyricIdx++;
7386
8083
  }
7387
8084
  return { music: resultLines.join("\n"), lyrics: null };
7388
8085
  }
7389
- const allSyllables = [];
7390
- const sortedMeasures = Array.from(allLyrics.keys()).sort((a, b) => a - b);
7391
- for (const mi of sortedMeasures) {
7392
- allSyllables.push(...allLyrics.get(mi));
7393
- }
7394
- return { music: musicStr, lyrics: formatLyrics(allSyllables) };
8086
+ return { music: musicStr, lyrics: lyricLinesFor(0, lastMeasure).join("\n") };
7395
8087
  }
7396
8088
  function serializeMeasureEntries(measure, divisions, unitNote, opts) {
7397
8089
  const parts = [];
7398
- const lyrics = [];
8090
+ const lyrics = /* @__PURE__ */ new Map();
7399
8091
  let currentUnitNote = { ...unitNote };
7400
8092
  let chordPitches = [];
7401
8093
  let chordDurationStr = "";
@@ -7413,17 +8105,28 @@ function serializeMeasureEntries(measure, divisions, unitNote, opts) {
7413
8105
  if (note.grace) {
7414
8106
  const graceNotes = [];
7415
8107
  let gi = ei;
8108
+ let openChord = false;
7416
8109
  while (gi < measure.entries.length) {
7417
8110
  const ge = measure.entries[gi];
7418
8111
  if (ge.type === "note" && ge.grace) {
7419
8112
  const gs = serializeNote2(ge, divisions, currentUnitNote, false);
8113
+ const startsChord = !ge.chord && isChordHead(measure.entries, gi);
8114
+ if (startsChord && !openChord) {
8115
+ graceNotes.push("[");
8116
+ openChord = true;
8117
+ } else if (openChord && !ge.chord) {
8118
+ graceNotes.push("]");
8119
+ openChord = startsChord;
8120
+ if (startsChord) graceNotes.push("[");
8121
+ }
7420
8122
  graceNotes.push(gs.pitch);
7421
8123
  gi++;
7422
8124
  } else {
7423
8125
  break;
7424
8126
  }
7425
8127
  }
7426
- parts.push("{" + graceNotes.join("") + "}");
8128
+ if (openChord) graceNotes.push("]");
8129
+ parts.push((note.grace.slash ? "{/" : "{") + graceNotes.join("") + "}");
7427
8130
  ei = gi - 1;
7428
8131
  break;
7429
8132
  }
@@ -7453,11 +8156,14 @@ function serializeMeasureEntries(measure, divisions, unitNote, opts) {
7453
8156
  chordSlurEnd = "";
7454
8157
  }
7455
8158
  if (note.lyrics && note.lyrics.length > 0 && opts.includeLyrics) {
7456
- const lyric = note.lyrics[0];
7457
- if (lyric.text) {
8159
+ for (const lyric of note.lyrics) {
8160
+ if (!lyric.text) continue;
7458
8161
  const syllabic = lyric.syllabic || "single";
7459
8162
  const suffix = syllabic === "begin" || syllabic === "middle" ? "-" : "";
7460
- lyrics.push(lyric.text + suffix);
8163
+ const verse = lyric.number ?? 1;
8164
+ const bucket = lyrics.get(verse);
8165
+ if (bucket) bucket.push(lyric.text + suffix);
8166
+ else lyrics.set(verse, [lyric.text + suffix]);
7461
8167
  }
7462
8168
  }
7463
8169
  let tupletPrefix = "";
@@ -7484,18 +8190,20 @@ function serializeMeasureEntries(measure, divisions, unitNote, opts) {
7484
8190
  if (note.notations) {
7485
8191
  for (const notation of note.notations) {
7486
8192
  if (notation.type === "slur") {
7487
- if (notation.slurType === "start") slurStart += "(";
8193
+ if (notation.slurType === "start") slurStart += notation.lineType === "dotted" ? ".(" : "(";
7488
8194
  if (notation.slurType === "stop") slurEnd += ")";
7489
8195
  }
7490
8196
  }
7491
8197
  }
8198
+ const decorations = serializeNoteDecorations(note);
7492
8199
  effectiveSerialized = {
7493
- full: slurStart + pitchStr + baseDurationStr + tieStr + slurEnd,
8200
+ full: slurStart + decorations + pitchStr + baseDurationStr + tieStr + slurEnd,
7494
8201
  pitch: pitchStr,
7495
8202
  duration: baseDurationStr,
7496
8203
  slurStart,
7497
8204
  slurEnd,
7498
- tieStr
8205
+ tieStr,
8206
+ decorations
7499
8207
  };
7500
8208
  }
7501
8209
  const nextEntry = ei + 1 < measure.entries.length ? measure.entries[ei + 1] : null;
@@ -7529,7 +8237,7 @@ function serializeMeasureEntries(measure, divisions, unitNote, opts) {
7529
8237
  let insertIdx = parts.length;
7530
8238
  for (let pi = parts.length - 1; pi >= 0; pi--) {
7531
8239
  const p = parts[pi];
7532
- if (/^[vuTM]$/.test(p) || /^![^!]+!$/.test(p) || /^"[^"]*"$/.test(p)) {
8240
+ if (p.length === 1 && ABC_SHORTHAND_DECORATIONS.has(p) || /^![^!]+!$/.test(p) || /^"[^"]*"$/.test(p)) {
7533
8241
  insertIdx = pi;
7534
8242
  } else {
7535
8243
  break;
@@ -7537,11 +8245,11 @@ function serializeMeasureEntries(measure, divisions, unitNote, opts) {
7537
8245
  }
7538
8246
  if (insertIdx < parts.length) {
7539
8247
  parts.splice(insertIdx, 0, chordSlurStart);
7540
- parts.push(tupletPrefix);
8248
+ parts.push(tupletPrefix + effectiveSerialized.decorations);
7541
8249
  break;
7542
8250
  }
7543
8251
  }
7544
- parts.push(tupletPrefix + chordSlurStart);
8252
+ parts.push(tupletPrefix + chordSlurStart + effectiveSerialized.decorations);
7545
8253
  break;
7546
8254
  }
7547
8255
  if (effectiveSerialized.slurStart) {
@@ -7556,7 +8264,7 @@ function serializeMeasureEntries(measure, divisions, unitNote, opts) {
7556
8264
  }
7557
8265
  if (insertIdx < parts.length) {
7558
8266
  parts.splice(insertIdx, 0, effectiveSerialized.slurStart);
7559
- const noteOnly = effectiveSerialized.pitch + effectiveSerialized.duration + effectiveSerialized.tieStr + effectiveSerialized.slurEnd;
8267
+ const noteOnly = effectiveSerialized.decorations + effectiveSerialized.pitch + effectiveSerialized.duration + effectiveSerialized.tieStr + effectiveSerialized.slurEnd;
7560
8268
  parts.push(tupletPrefix + noteOnly);
7561
8269
  break;
7562
8270
  }
@@ -7568,9 +8276,9 @@ function serializeMeasureEntries(measure, divisions, unitNote, opts) {
7568
8276
  const tie1 = effectiveSerialized.tieStr;
7569
8277
  const slurS1 = effectiveSerialized.slurStart;
7570
8278
  const slurE1 = effectiveSerialized.slurEnd;
7571
- parts.push(tupletPrefix + slurS1 + pitchStr1 + baseDurStr1 + tie1 + brokenResult.marker);
8279
+ parts.push(tupletPrefix + slurS1 + effectiveSerialized.decorations + pitchStr1 + baseDurStr1 + tie1 + brokenResult.marker);
7572
8280
  const note2 = brokenResult.nextNote;
7573
- const pitchStr2 = serializePitch2(note2.pitch, note2.accidental?.value === "natural");
8281
+ const pitchStr2 = serializeNoteDecorations(note2) + serializePitch2(note2.pitch, note2.accidental?.value === "natural");
7574
8282
  let tieStr2 = "";
7575
8283
  if (note2.tie?.type === "start" || note2.ties?.some((t) => t.type === "start")) tieStr2 = "-";
7576
8284
  let slurEnd2 = "";
@@ -7595,42 +8303,40 @@ function serializeMeasureEntries(measure, divisions, unitNote, opts) {
7595
8303
  case "direction": {
7596
8304
  let handledAsSpecial = false;
7597
8305
  for (const dt of entry.directionTypes) {
7598
- if (dt.kind === "words") {
7599
- if (dt.text === "__abc_line_cont__") {
7600
- parts.push("\\\n");
7601
- handledAsSpecial = true;
7602
- break;
7603
- }
7604
- if (dt.text === "__abc_line_break__") {
7605
- parts.push("\n");
7606
- handledAsSpecial = true;
7607
- break;
7608
- }
7609
- const lMatch = dt.text.match(/^\[L:\s*(\d+)\/(\d+)\]$/);
7610
- if (lMatch) {
7611
- parts.push(dt.text);
7612
- currentUnitNote = { num: parseInt(lMatch[1], 10), den: parseInt(lMatch[2], 10) };
7613
- handledAsSpecial = true;
7614
- break;
7615
- }
7616
- const decoMatch = dt.text.match(/^!([^!]+)!$/);
7617
- if (decoMatch) {
7618
- parts.push(dt.text);
7619
- handledAsSpecial = true;
7620
- break;
7621
- }
7622
- if (dt.text.length === 1 && /^[vuTM]$/.test(dt.text)) {
7623
- parts.push(dt.text);
7624
- handledAsSpecial = true;
7625
- break;
8306
+ if (dt.kind !== "words") continue;
8307
+ if (dt.text === "__abc_line_cont__") {
8308
+ parts.push("\\\n");
8309
+ handledAsSpecial = true;
8310
+ break;
8311
+ }
8312
+ if (dt.text === "__abc_line_break__") {
8313
+ parts.push("\n");
8314
+ handledAsSpecial = true;
8315
+ break;
8316
+ }
8317
+ if (dt.text.startsWith(ABC_BODY_FIELD_MARKER)) {
8318
+ parts.push("\n" + dt.text.slice(ABC_BODY_FIELD_MARKER.length));
8319
+ handledAsSpecial = true;
8320
+ break;
8321
+ }
8322
+ const inlineField = dt.text.match(/^\[([A-Za-z]):([^\]]*)\]$/);
8323
+ if (inlineField) {
8324
+ parts.push(dt.text);
8325
+ if (inlineField[1] === "L") {
8326
+ const lMatch = inlineField[2].trim().match(/^(\d+)\/(\d+)$/);
8327
+ if (lMatch) {
8328
+ currentUnitNote = { num: parseInt(lMatch[1], 10), den: parseInt(lMatch[2], 10) };
8329
+ }
7626
8330
  }
8331
+ handledAsSpecial = true;
8332
+ break;
7627
8333
  }
7628
8334
  }
7629
8335
  if (handledAsSpecial) break;
7630
- if (opts.includeDynamics) {
7631
- const dynStr = serializeDynamics(entry);
7632
- if (dynStr) {
7633
- parts.push(dynStr);
8336
+ const text = serializeDirectionText(entry, opts);
8337
+ if (text) {
8338
+ if (opts.includeChordSymbols || !text.startsWith('"')) {
8339
+ parts.push(text);
7634
8340
  }
7635
8341
  }
7636
8342
  break;
@@ -7650,6 +8356,29 @@ function serializeMeasureEntries(measure, divisions, unitNote, opts) {
7650
8356
  const unitNoteChanged = currentUnitNote.num !== unitNote.num || currentUnitNote.den !== unitNote.den;
7651
8357
  return { noteStr: parts.join(""), lyrics, updatedUnitNote: unitNoteChanged ? currentUnitNote : void 0 };
7652
8358
  }
8359
+ function multipleRestCount(measure) {
8360
+ const styles = measure.attributes?.measureStyle;
8361
+ if (!styles) return 0;
8362
+ for (const style of styles) {
8363
+ if (style.multipleRest && style.multipleRest > 1) return style.multipleRest;
8364
+ }
8365
+ return 0;
8366
+ }
8367
+ function applyMultiMeasureRest(measure, noteStr) {
8368
+ const count = multipleRestCount(measure);
8369
+ if (count < 2 || noteStr.trim() !== "Z") return { text: noteStr, absorbed: 0 };
8370
+ return { text: `Z${count}`, absorbed: count - 1 };
8371
+ }
8372
+ function hasInlineKeyMarker(measure) {
8373
+ return measure.entries.some((e) => e.type === "direction" && e.directionTypes.some((dt) => dt.kind === "words" && /^\[K:/.test(dt.text)));
8374
+ }
8375
+ function measureHasNotes(measure) {
8376
+ return measure.entries.some((e) => e.type === "note");
8377
+ }
8378
+ function isChordHead(entries, index) {
8379
+ const next = entries[index + 1];
8380
+ return next !== void 0 && next.type === "note" && next.chord === true;
8381
+ }
7653
8382
  function detectBrokenRhythm(entries, currentIdx, currentNote, divisions, unitNote) {
7654
8383
  if (currentNote.chord || currentNote.grace || currentNote.rest || currentNote.timeModification) return null;
7655
8384
  let nextIdx = currentIdx + 1;
@@ -7751,13 +8480,14 @@ function serializeNote2(note, divisions, unitNote, _inChord) {
7751
8480
  if (note.notations) {
7752
8481
  for (const notation of note.notations) {
7753
8482
  if (notation.type === "slur") {
7754
- if (notation.slurType === "start") slurStart += "(";
8483
+ if (notation.slurType === "start") slurStart += notation.lineType === "dotted" ? ".(" : "(";
7755
8484
  if (notation.slurType === "stop") slurEnd += ")";
7756
8485
  }
7757
8486
  }
7758
8487
  }
7759
- const full = slurStart + pitchStr + durationStr + tieStr + slurEnd;
7760
- return { full, pitch: pitchStr, duration: durationStr, slurStart, slurEnd, tieStr };
8488
+ const decorations = serializeNoteDecorations(note);
8489
+ const full = slurStart + decorations + pitchStr + durationStr + tieStr + slurEnd;
8490
+ return { full, pitch: pitchStr, duration: durationStr, slurStart, slurEnd, tieStr, decorations };
7761
8491
  }
7762
8492
  function serializeBarline2(barline) {
7763
8493
  const hasRepeatForward = barline.repeat?.direction === "forward";
@@ -7780,6 +8510,13 @@ function serializeBarline2(barline) {
7780
8510
  case "heavy-light":
7781
8511
  result += "[|";
7782
8512
  break;
8513
+ case "none":
8514
+ result += "[|]";
8515
+ break;
8516
+ case "dotted":
8517
+ case "dashed":
8518
+ result += ".|";
8519
+ break;
7783
8520
  default:
7784
8521
  result += "|";
7785
8522
  break;
@@ -7800,10 +8537,12 @@ export {
7800
8537
  parseCompressed,
7801
8538
  isCompressed,
7802
8539
  parseAuto,
8540
+ parseAbcTunes,
7803
8541
  parseAbc,
7804
8542
  serialize,
7805
8543
  serializeCompressed,
7806
8544
  exportMidi,
7807
8545
  exportMidiWithTimingMap,
8546
+ serializeAbcTunes,
7808
8547
  serializeAbc
7809
8548
  };