@k-l-lambda/lilylet 0.1.88 → 0.1.90
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.
- package/lib/abc/grammar.jison.js +218 -213
- package/lib/clefTransposition.d.ts +1 -0
- package/lib/clefTransposition.js +1 -0
- package/lib/lilylet/abcDecoder.js +60 -35
- package/lib/lilylet/clefTransposition.d.ts +111 -0
- package/lib/lilylet/clefTransposition.js +175 -0
- package/lib/lilylet/index.d.ts +1 -0
- package/lib/lilylet/index.js +1 -0
- package/lib/lilylet/lilypondDecoder.js +88 -2
- package/lib/lilylet/lilypondEncoder.js +32 -2
- package/lib/lilylet/meiEncoder.js +11 -19
- package/lib/lilylet/musicXmlDecoder.js +56 -3
- package/lib/lilylet/musicXmlEncoder.js +33 -1
- package/lib/lilylet/musicXmlTypes.d.ts +6 -0
- package/lib/lilylet/onsets.d.ts +5 -4
- package/lib/lilylet/onsets.js +7 -14
- package/package.json +4 -1
- package/source/abc/abc.jison +2 -0
- package/source/abc/grammar.jison.js +218 -213
- package/source/lilylet/abcDecoder.ts +60 -35
- package/source/lilylet/clefTransposition.ts +197 -0
- package/source/lilylet/index.ts +1 -0
- package/source/lilylet/lilypondDecoder.ts +89 -2
- package/source/lilylet/lilypondEncoder.ts +36 -2
- package/source/lilylet/meiEncoder.ts +13 -18
- package/source/lilylet/musicXmlDecoder.ts +67 -3
- package/source/lilylet/musicXmlEncoder.ts +35 -1
- package/source/lilylet/musicXmlTypes.ts +10 -0
- package/source/lilylet/onsets.ts +7 -14
|
@@ -2,6 +2,7 @@ import { Clef, Accidental, OrnamentType, StemDirection, HairpinType, PedalType,
|
|
|
2
2
|
import { parseStaffLayout, StaffGroupType } from "./staffLayout.js";
|
|
3
3
|
import { gmProgramOf } from "./gmInstruments.js";
|
|
4
4
|
import { parseMeasureLayout, expandMeasureLayout, decomposeToSegments } from "./measureLayout.js";
|
|
5
|
+
import { clefBaseName, clefSuffixTransposition } from "./clefTransposition.js";
|
|
5
6
|
// MEI key signatures: positive = sharps, negative = flats
|
|
6
7
|
const KEY_SIGS = {
|
|
7
8
|
0: "0",
|
|
@@ -49,36 +50,27 @@ const CLEF_SHAPES = {
|
|
|
49
50
|
};
|
|
50
51
|
// Semitone offsets of the major/perfect intervals within one diatonic octave,
|
|
51
52
|
// indexed by diatonic step (0 = unison, 1 = 2nd, … 6 = 7th).
|
|
52
|
-
const DIATONIC_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
|
|
53
53
|
// Convert a LilyPond clef interval-number suffix into an MEI written→sounding
|
|
54
54
|
// transposition. The number N is a diatonic interval number (2 = 2nd, 3 = 3rd,
|
|
55
55
|
// 5 = 5th, 8 = octave, 15 = two octaves); "_" lowers the sounding pitch, "^"
|
|
56
56
|
// raises it. Returns { diat, semi } where diat is the diatonic step shift
|
|
57
57
|
// (N - 1, signed) and semi is the corresponding chromatic shift in semitones,
|
|
58
58
|
// extended octave-wise for compound intervals.
|
|
59
|
-
const clefTransposition = (intervalNumber, up) => {
|
|
60
|
-
const k = intervalNumber - 1; // diatonic steps
|
|
61
|
-
const semis = DIATONIC_SEMITONES[k % 7] + 12 * Math.floor(k / 7);
|
|
62
|
-
const sign = up ? 1 : -1;
|
|
63
|
-
return { diat: sign * k, semi: sign * semis };
|
|
64
|
-
};
|
|
65
59
|
// Resolve a clef string into MEI shape/line plus optional written→sounding
|
|
66
|
-
// transposition.
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
60
|
+
// transposition. A "_N"/"^N" suffix transposes the clef down/up by the diatonic
|
|
61
|
+
// interval N ("treble_8" octave down, "treble_5" fifth down), and "_mN"/"^mN"
|
|
62
|
+
// selects that interval's minor form ("treble_m3" = minor third down) — see
|
|
63
|
+
// clefTransposition.ts. MEI's clef.dis only covers octave displacement
|
|
64
|
+
// (8|15|22), so all clef transposition — octaves included — is encoded uniformly
|
|
65
|
+
// via att.transposition (trans.diat / trans.semi) on staffDef. A minor interval is
|
|
66
|
+
// exactly why both fields are needed: a minor third is {diat:-2, semi:-3}.
|
|
71
67
|
const resolveClef = (clefStr) => {
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const clefInfo = CLEF_SHAPES[base] || CLEF_SHAPES.treble;
|
|
75
|
-
if (!match)
|
|
76
|
-
return { shape: clefInfo.shape, line: clefInfo.line };
|
|
77
|
-
const trans = clefTransposition(Number(match[3]), match[2] === "^");
|
|
68
|
+
const clefInfo = CLEF_SHAPES[clefBaseName(clefStr)] || CLEF_SHAPES.treble;
|
|
69
|
+
const trans = clefSuffixTransposition(clefStr);
|
|
78
70
|
return {
|
|
79
71
|
shape: clefInfo.shape,
|
|
80
72
|
line: clefInfo.line,
|
|
81
|
-
trans,
|
|
73
|
+
...(trans ? { trans } : {}),
|
|
82
74
|
};
|
|
83
75
|
};
|
|
84
76
|
// Attributes for a standalone <clef> element (mid-measure clef change).
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { DOMParser } from '@xmldom/xmldom';
|
|
8
8
|
import { HairpinType, NavigationMarkType, OrnamentType, Placement, } from "./types.js";
|
|
9
|
+
import { withClefTransposition } from "./clefTransposition.js";
|
|
9
10
|
import { getElementText, getElementInt, getElements, getDirectChildren, getChildElements, getAttribute, getAttributeNumber, hasElement, convertPitch, convertDuration, convertKeySignature, convertClef, convertStemDirection, convertArticulation, convertOrnament, convertDynamic, convertPedal, convertBarlineStyle, convertHarmonyToText, createFraction, TYPE_TO_DIVISION, } from "./musicXmlUtils.js";
|
|
10
11
|
import { measureLayoutFromPart } from "./measureLayoutFromXml.js";
|
|
11
12
|
// ============ Spanner Tracker ============
|
|
@@ -491,6 +492,20 @@ const parseAttributes = (attrEl) => {
|
|
|
491
492
|
}
|
|
492
493
|
}
|
|
493
494
|
}
|
|
495
|
+
// Instrument transposition. This is what carries a transposing part's sounding
|
|
496
|
+
// pitch in MusicXML (<clef-octave-change> is only the glyph), and it was never
|
|
497
|
+
// read, so every transposing instrument decoded as sounding-as-written.
|
|
498
|
+
const transposeEl = getElements(attrEl, 'transpose')[0];
|
|
499
|
+
if (transposeEl) {
|
|
500
|
+
const chromatic = getElementInt(transposeEl, 'chromatic');
|
|
501
|
+
if (chromatic != null) {
|
|
502
|
+
result.transpose = {
|
|
503
|
+
diatonic: getElementInt(transposeEl, 'diatonic') ?? undefined,
|
|
504
|
+
chromatic,
|
|
505
|
+
octaveChange: getElementInt(transposeEl, 'octave-change') ?? undefined,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
}
|
|
494
509
|
// Staves
|
|
495
510
|
const staves = getElementInt(attrEl, 'staves');
|
|
496
511
|
if (staves !== undefined) {
|
|
@@ -1038,6 +1053,35 @@ const forwardGapToRests = (gapTicks, divisions) => {
|
|
|
1038
1053
|
}
|
|
1039
1054
|
return rests;
|
|
1040
1055
|
};
|
|
1056
|
+
/**
|
|
1057
|
+
* Fold a MusicXML transposition onto a lilylet clef as a suffix.
|
|
1058
|
+
*
|
|
1059
|
+
* MusicXML keeps the two apart: <transpose> is the sounding shift and moves pitch,
|
|
1060
|
+
* <clef-octave-change> is the "8" glyph under the clef and moves nothing. A lilylet
|
|
1061
|
+
* suffix means sounding shift, so only ONE of them may be folded in — adding both
|
|
1062
|
+
* would double an octave clef that also declares <transpose> (the shape this
|
|
1063
|
+
* encoder itself now emits) into two octaves.
|
|
1064
|
+
*
|
|
1065
|
+
* <transpose> wins when present, being the explicit statement of sounding pitch.
|
|
1066
|
+
* Absent it, an octave clef is read as implying its own shift, which is what a
|
|
1067
|
+
* tenor-voice or guitar treble_8 part means.
|
|
1068
|
+
*
|
|
1069
|
+
* A shift with no exact suffix (only the tritone) leaves the clef plain rather than
|
|
1070
|
+
* rounding: a silently wrong sounding pitch is worse than a visibly absent shift.
|
|
1071
|
+
*/
|
|
1072
|
+
const clefWithShift = (clef, transposeSemitones, clefOctaveChange) => {
|
|
1073
|
+
if (!clef)
|
|
1074
|
+
return clef;
|
|
1075
|
+
const semitones = transposeSemitones !== 0 ? transposeSemitones : 12 * (clefOctaveChange ?? 0);
|
|
1076
|
+
if (semitones === 0)
|
|
1077
|
+
return clef;
|
|
1078
|
+
const shifted = withClefTransposition(clef, semitones);
|
|
1079
|
+
if (shifted === undefined) {
|
|
1080
|
+
console.warn(`musicXmlDecoder: transposition ${semitones} has no exact clef suffix; dropped for clef "${clef}"`);
|
|
1081
|
+
return clef;
|
|
1082
|
+
}
|
|
1083
|
+
return shifted;
|
|
1084
|
+
};
|
|
1041
1085
|
/**
|
|
1042
1086
|
* Total time a TupletEvent advances the voice, in voiceTracker duration units.
|
|
1043
1087
|
* Sum the inner note/rest values then apply the tuplet ratio (triplet etc.).
|
|
@@ -1054,7 +1098,10 @@ const tupletAdvanceDuration = (tupletEvent, divisions) => {
|
|
|
1054
1098
|
/**
|
|
1055
1099
|
* Convert a MusicXML measure to Lilylet events, grouped by voice
|
|
1056
1100
|
*/
|
|
1057
|
-
const convertMeasure = (measureEl, voiceTracker, spannerTracker, ottavaTracker, tupletTracker
|
|
1101
|
+
const convertMeasure = (measureEl, voiceTracker, spannerTracker, ottavaTracker, tupletTracker,
|
|
1102
|
+
// A part's <transpose> is declared once, usually in measure 1, and stays in force
|
|
1103
|
+
// for the whole part, so it has to survive across measures like the ottava does.
|
|
1104
|
+
transposeTracker) => {
|
|
1058
1105
|
let key;
|
|
1059
1106
|
let timeSig;
|
|
1060
1107
|
let barline;
|
|
@@ -1136,10 +1183,15 @@ const convertMeasure = (measureEl, voiceTracker, spannerTracker, ottavaTracker,
|
|
|
1136
1183
|
if (attrs.time) {
|
|
1137
1184
|
timeSig = createFraction(attrs.time.beats, attrs.time.beatType);
|
|
1138
1185
|
}
|
|
1186
|
+
// Instrument transposition, in force from here to the end of the part.
|
|
1187
|
+
if (attrs.transpose) {
|
|
1188
|
+
const { chromatic, octaveChange } = attrs.transpose;
|
|
1189
|
+
transposeTracker.semitones = chromatic + 12 * (octaveChange ?? 0);
|
|
1190
|
+
}
|
|
1139
1191
|
// Clefs - store by staff number
|
|
1140
1192
|
if (attrs.clefs) {
|
|
1141
1193
|
for (const clefEntry of attrs.clefs) {
|
|
1142
|
-
const clef = convertClef(clefEntry.clef.sign, clefEntry.clef.line);
|
|
1194
|
+
const clef = clefWithShift(convertClef(clefEntry.clef.sign, clefEntry.clef.line), transposeTracker.semitones, clefEntry.clef.clefOctaveChange);
|
|
1143
1195
|
if (clef) {
|
|
1144
1196
|
const staff = clefEntry.staff;
|
|
1145
1197
|
if (staffHasNotes.has(staff)) {
|
|
@@ -1508,6 +1560,7 @@ const convertPart = (partEl) => {
|
|
|
1508
1560
|
const voiceTracker = new VoiceTracker();
|
|
1509
1561
|
const spannerTracker = new SpannerTracker();
|
|
1510
1562
|
const ottavaTracker = { current: 0 };
|
|
1563
|
+
const transposeTracker = { semitones: 0 };
|
|
1511
1564
|
const tupletTracker = new TupletTracker();
|
|
1512
1565
|
let lastKey;
|
|
1513
1566
|
let lastTimeSig;
|
|
@@ -1523,7 +1576,7 @@ const convertPart = (partEl) => {
|
|
|
1523
1576
|
const measureEls = getDirectChildren(partEl, 'measure');
|
|
1524
1577
|
for (const measureEl of measureEls) {
|
|
1525
1578
|
voiceTracker.reset();
|
|
1526
|
-
const { voiceMap, key, timeSig, barline, leadingBarline, harmonies, clefs } = convertMeasure(measureEl, voiceTracker, spannerTracker, ottavaTracker, tupletTracker);
|
|
1579
|
+
const { voiceMap, key, timeSig, barline, leadingBarline, harmonies, clefs } = convertMeasure(measureEl, voiceTracker, spannerTracker, ottavaTracker, tupletTracker, transposeTracker);
|
|
1527
1580
|
// A forward-repeat (or any `location="left"`) barline belongs at the END of
|
|
1528
1581
|
// the PREVIOUS measure in lilylet's "\bar = current measure's end bar" model.
|
|
1529
1582
|
// Append it to the previous measure's first voice; if this is the first
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { StemDirection, Accidental, HairpinType, PedalType, NavigationMarkType, } from "./types.js";
|
|
8
8
|
import { DIVISIONS, DIVISION_TO_TYPE, calculateDuration, } from "./musicXmlUtils.js";
|
|
9
9
|
import { parseStaffLayout } from "./staffLayout.js";
|
|
10
|
+
import { clefBaseName, clefSuffixSemitones, clefSuffixTransposition } from "./clefTransposition.js";
|
|
10
11
|
// === Constants and Reverse Mappings ===
|
|
11
12
|
// Phonet to MusicXML step
|
|
12
13
|
const PHONET_TO_STEP = {
|
|
@@ -210,13 +211,44 @@ const encodeAttributes = (level, options) => {
|
|
|
210
211
|
xml += `${indent(level + 1)}<staves>${options.staves}</staves>\n`;
|
|
211
212
|
}
|
|
212
213
|
if (options.clef) {
|
|
213
|
-
|
|
214
|
+
// Look the sign/line up under the BASE name. A lilylet clef may carry a
|
|
215
|
+
// transposition suffix ("treble_8", "treble_m3"), and keying the table on the
|
|
216
|
+
// whole string missed every one of them — `clefInfo` came back undefined and
|
|
217
|
+
// the <clef> element was dropped entirely, sign and line with it.
|
|
218
|
+
const clefStr = options.clef;
|
|
219
|
+
const clefInfo = CLEF_TO_SIGN[clefBaseName(clefStr)];
|
|
214
220
|
if (clefInfo) {
|
|
215
221
|
xml += `${indent(level + 1)}<clef>\n`;
|
|
216
222
|
xml += `${indent(level + 2)}<sign>${clefInfo.sign}</sign>\n`;
|
|
217
223
|
xml += `${indent(level + 2)}<line>${clefInfo.line}</line>\n`;
|
|
224
|
+
// A whole-octave shift also has a notational form: the "8" drawn under or
|
|
225
|
+
// over the clef. This is the glyph only — sounding pitch rides on
|
|
226
|
+
// <transpose> below, exactly as in LilyPond.
|
|
227
|
+
const semitones = clefSuffixSemitones(clefStr);
|
|
228
|
+
if (semitones % 12 === 0 && semitones !== 0)
|
|
229
|
+
xml += `${indent(level + 2)}<clef-octave-change>${semitones / 12}</clef-octave-change>\n`;
|
|
218
230
|
xml += `${indent(level + 1)}</clef>\n`;
|
|
219
231
|
}
|
|
232
|
+
// The written->sounding transposition. MusicXML carries the diatonic step count
|
|
233
|
+
// and the chromatic semitone count separately, which is what lets a minor third
|
|
234
|
+
// (-2 steps, -3 semitones) be expressed exactly. <transpose> follows <clef> in
|
|
235
|
+
// the attributes content order.
|
|
236
|
+
const trans = clefSuffixTransposition(clefStr);
|
|
237
|
+
if (trans) {
|
|
238
|
+
// <diatonic> and <chromatic> hold the WITHIN-OCTAVE remainder and whole
|
|
239
|
+
// octaves go in <octave-change>, so both must be reduced by the same octave
|
|
240
|
+
// count: 7 diatonic steps and 12 semitones each. Reducing only the semitones
|
|
241
|
+
// would make an octave clef read as two octaves.
|
|
242
|
+
const octaveChange = Math.trunc(trans.semi / 12);
|
|
243
|
+
const diatonic = trans.diat - 7 * octaveChange;
|
|
244
|
+
const chromatic = trans.semi - 12 * octaveChange;
|
|
245
|
+
xml += `${indent(level + 1)}<transpose>\n`;
|
|
246
|
+
xml += `${indent(level + 2)}<diatonic>${diatonic}</diatonic>\n`;
|
|
247
|
+
xml += `${indent(level + 2)}<chromatic>${chromatic}</chromatic>\n`;
|
|
248
|
+
if (octaveChange !== 0)
|
|
249
|
+
xml += `${indent(level + 2)}<octave-change>${octaveChange}</octave-change>\n`;
|
|
250
|
+
xml += `${indent(level + 1)}</transpose>\n`;
|
|
251
|
+
}
|
|
220
252
|
}
|
|
221
253
|
xml += `${indent(level)}</attributes>\n`;
|
|
222
254
|
return xml;
|
|
@@ -31,6 +31,11 @@ export interface MusicXmlClef {
|
|
|
31
31
|
line?: number;
|
|
32
32
|
clefOctaveChange?: number;
|
|
33
33
|
}
|
|
34
|
+
export interface MusicXmlTranspose {
|
|
35
|
+
diatonic?: number;
|
|
36
|
+
chromatic: number;
|
|
37
|
+
octaveChange?: number;
|
|
38
|
+
}
|
|
34
39
|
export interface MusicXmlAttributes {
|
|
35
40
|
divisions?: number;
|
|
36
41
|
key?: MusicXmlKey;
|
|
@@ -40,6 +45,7 @@ export interface MusicXmlAttributes {
|
|
|
40
45
|
clef: MusicXmlClef;
|
|
41
46
|
}[];
|
|
42
47
|
staves?: number;
|
|
48
|
+
transpose?: MusicXmlTranspose;
|
|
43
49
|
}
|
|
44
50
|
export type MusicXmlStemDirection = 'up' | 'down';
|
|
45
51
|
export type MusicXmlLyricSyllabic = 'single' | 'begin' | 'middle' | 'end';
|
package/lib/lilylet/onsets.d.ts
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* Shared by tools/astServer.ts (the HTTP onset API) and the clef-onset unit test.
|
|
10
10
|
*/
|
|
11
11
|
/**
|
|
12
|
-
* The written->sounding semitone shift a clef string declares.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* A plain clef
|
|
12
|
+
* The written->sounding semitone shift a clef string declares. A "_N"/"^N" suffix transposes
|
|
13
|
+
* DOWN/UP by the diatonic interval N ("treble_8" = octave down, -12; "treble_5" = fifth down,
|
|
14
|
+
* -7), and an "m" before the number selects that interval's MINOR form ("treble_m3" = minor
|
|
15
|
+
* third down, -3). A plain clef declares 0. See clefTransposition.ts for the grammar; this is
|
|
16
|
+
* a thin alias kept because clefShift is the name the onset API and its tests already use.
|
|
16
17
|
*/
|
|
17
18
|
export declare const clefShift: (clefStr: string) => number;
|
|
18
19
|
/**
|
package/lib/lilylet/onsets.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { calculateDuration } from "./musicXmlUtils.js";
|
|
12
12
|
import { Accidental } from "./types.js";
|
|
13
|
+
import { clefSuffixSemitones } from "./clefTransposition.js";
|
|
13
14
|
// Onset resolution in ticks per quarter note. calculateDuration() rounds its result to an
|
|
14
15
|
// integer, so it must run at a resolution fine enough to represent every note value exactly:
|
|
15
16
|
// the shared DIVISIONS=4 (MusicXML export) rounds a 32nd (0.5) and a dotted 16th (1.5) to
|
|
@@ -21,22 +22,14 @@ const ACCIDENTAL_SEMITONE = {
|
|
|
21
22
|
[Accidental.natural]: 0, [Accidental.sharp]: 1, [Accidental.flat]: -1,
|
|
22
23
|
[Accidental.doubleSharp]: 2, [Accidental.doubleFlat]: -2,
|
|
23
24
|
};
|
|
24
|
-
// Diatonic-step -> semitones within an octave (unison..seventh), for "_N"/"^N" clef suffixes.
|
|
25
|
-
const DIATONIC_SEMITONES = [0, 2, 4, 5, 7, 9, 11];
|
|
26
25
|
/**
|
|
27
|
-
* The written->sounding semitone shift a clef string declares.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* A plain clef
|
|
26
|
+
* The written->sounding semitone shift a clef string declares. A "_N"/"^N" suffix transposes
|
|
27
|
+
* DOWN/UP by the diatonic interval N ("treble_8" = octave down, -12; "treble_5" = fifth down,
|
|
28
|
+
* -7), and an "m" before the number selects that interval's MINOR form ("treble_m3" = minor
|
|
29
|
+
* third down, -3). A plain clef declares 0. See clefTransposition.ts for the grammar; this is
|
|
30
|
+
* a thin alias kept because clefShift is the name the onset API and its tests already use.
|
|
31
31
|
*/
|
|
32
|
-
export const clefShift = (clefStr) =>
|
|
33
|
-
const m = /^.*?([_^])(\d+)$/.exec(clefStr || "");
|
|
34
|
-
if (!m)
|
|
35
|
-
return 0;
|
|
36
|
-
const k = parseInt(m[2], 10) - 1; // diatonic steps above unison
|
|
37
|
-
const semis = DIATONIC_SEMITONES[k % 7] + 12 * Math.floor(k / 7);
|
|
38
|
-
return (m[1] === "^" ? 1 : -1) * semis;
|
|
39
|
-
};
|
|
32
|
+
export const clefShift = (clefStr) => clefSuffixSemitones(clefStr);
|
|
40
33
|
/**
|
|
41
34
|
* Sounding MIDI pitch of a resolved (absolute-octave) Lilylet pitch. Octave 0 = middle-C
|
|
42
35
|
* octave = MIDI 60 (see parser.resolveRelativePitch). `shift` is the active clef's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k-l-lambda/lilylet",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.90",
|
|
4
4
|
"description": "Lilylet is a lilyopnd-like sheet music language designed for Markdown rendering and symbolic music representation in AIGC applications.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
"test:abc-instrument": "tsx ./tests/unit/abcInstrumentName.test.ts",
|
|
56
56
|
"test:abc-partial": "tsx ./tests/unit/abcPartialPickup.test.ts",
|
|
57
57
|
"test:clef-onset": "tsx ./tests/unit/clefOnsetTranspose.test.ts",
|
|
58
|
+
"test:abc-transpose": "tsx ./tests/unit/abcTransposingClef.test.ts",
|
|
59
|
+
"test:clef-transposition": "tsx ./tests/unit/clefTransposition.test.ts",
|
|
60
|
+
"test:clef-translation": "tsx ./tests/unit/clefTranslation.test.ts",
|
|
58
61
|
"test:fine-rhythm-onset": "tsx ./tests/unit/fineRhythmOnset.test.ts",
|
|
59
62
|
"test:transpose": "tsx ./tests/unit/astTranspose.test.ts",
|
|
60
63
|
"build:tests": "tsc -p tsconfig.tests.json; cp source/lilylet/grammar.jison.js lib-tests/source/lilylet/ && cp source/abc/grammar.jison.js lib-tests/source/abc/ && node tools/fixEsmExtensions.cjs lib-tests && ln -sfn ../../tests/assets lib-tests/tests/assets",
|
package/source/abc/abc.jison
CHANGED
|
@@ -213,6 +213,7 @@ SPECIAL [:!^_,'/<>={}()\[\]|.\-+~&]
|
|
|
213
213
|
|
|
214
214
|
^[T][:][\s]* { var pre = this.matched.slice(0, this.matched.length - yytext.length); if (pre === '' || pre.slice(-1) === '\n') { this.pushState('title_string'); return 'T:'; } this.unput(yytext.slice(1)); yytext = 'T'; return 'T'; }
|
|
215
215
|
^[C][:][\s]* { var pre = this.matched.slice(0, this.matched.length - yytext.length); if (pre === '' || pre.slice(-1) === '\n') { this.pushState('title_string'); return 'C:'; } this.unput(yytext.slice(1)); yytext = 'C'; return 'A'; }
|
|
216
|
+
^[ABDFGHINORSWZ][:](?![|])[ \t]* { var pre = this.matched.slice(0, this.matched.length - yytext.length); if (pre === '' || pre.slice(-1) === '\n') { this.pushState('title_string'); yytext = yytext[0]; return 'TEXT_FIELD'; } this.unput(yytext.slice(1)); yytext = yytext[0]; return 'H'; }
|
|
216
217
|
<title_string>\n { this.popState(); }
|
|
217
218
|
<title_string>[^\n]+ return 'STR_CONTENT'
|
|
218
219
|
|
|
@@ -379,6 +380,7 @@ head_line
|
|
|
379
380
|
| 'K:' key_signature -> header('K', $2)
|
|
380
381
|
| 'V:' header_value -> header('V', $2)
|
|
381
382
|
| H ':' header_value -> header($1, $3)
|
|
383
|
+
| TEXT_FIELD string_content -> header($1, $2)
|
|
382
384
|
;
|
|
383
385
|
|
|
384
386
|
header_value
|