@k-l-lambda/lilylet 0.1.70 → 0.1.72
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/gmInstruments.d.ts +1 -0
- package/lib/gmInstruments.js +1 -0
- package/lib/highlight.d.ts +1 -0
- package/lib/highlight.js +1 -0
- package/lib/lilylet/abcDecoder.js +16 -7
- package/lib/lilylet/gmInstruments.d.ts +1 -0
- package/lib/lilylet/gmInstruments.js +295 -0
- package/lib/lilylet/highlight.d.ts +29 -0
- package/lib/lilylet/highlight.js +145 -0
- package/lib/lilylet/meiEncoder.js +126 -14
- package/lib/lilylet/staffLayout.d.ts +5 -0
- package/lib/lilylet/staffLayout.js +62 -0
- package/package.json +8 -2
- package/source/lilylet/abcDecoder.ts +14 -7
- package/source/lilylet/gmInstruments.ts +305 -0
- package/source/lilylet/highlight.ts +192 -0
- package/source/lilylet/meiEncoder.ts +135 -11
- package/source/lilylet/staffLayout.ts +76 -0
- package/lib/source/abc/abc.d.ts +0 -102
- package/lib/source/abc/abc.js +0 -25
- package/lib/source/abc/parser.d.ts +0 -3
- package/lib/source/abc/parser.js +0 -6
- package/lib/source/lilylet/abcDecoder.d.ts +0 -25
- package/lib/source/lilylet/abcDecoder.js +0 -1035
- package/lib/source/lilylet/index.d.ts +0 -10
- package/lib/source/lilylet/index.js +0 -10
- package/lib/source/lilylet/lilypondDecoder.d.ts +0 -29
- package/lib/source/lilylet/lilypondDecoder.js +0 -1223
- package/lib/source/lilylet/lilypondEncoder.d.ts +0 -34
- package/lib/source/lilylet/lilypondEncoder.js +0 -893
- package/lib/source/lilylet/meiEncoder.d.ts +0 -8
- package/lib/source/lilylet/meiEncoder.js +0 -1985
- package/lib/source/lilylet/musicXmlDecoder.d.ts +0 -20
- package/lib/source/lilylet/musicXmlDecoder.js +0 -1195
- package/lib/source/lilylet/musicXmlEncoder.d.ts +0 -15
- package/lib/source/lilylet/musicXmlEncoder.js +0 -701
- package/lib/source/lilylet/musicXmlTypes.d.ts +0 -199
- package/lib/source/lilylet/musicXmlTypes.js +0 -7
- package/lib/source/lilylet/musicXmlUtils.d.ts +0 -92
- package/lib/source/lilylet/musicXmlUtils.js +0 -469
- package/lib/source/lilylet/parser.d.ts +0 -14
- package/lib/source/lilylet/parser.js +0 -161
- package/lib/source/lilylet/serializer.d.ts +0 -11
- package/lib/source/lilylet/serializer.js +0 -791
- package/lib/source/lilylet/types.d.ts +0 -253
- package/lib/source/lilylet/types.js +0 -100
- package/lib/tests/abc-abcjs-parse.d.ts +0 -8
- package/lib/tests/abc-abcjs-parse.js +0 -90
- package/lib/tests/abc-abcjs-svg.d.ts +0 -1
- package/lib/tests/abc-abcjs-svg.js +0 -143
- package/lib/tests/abc-decoder.d.ts +0 -1
- package/lib/tests/abc-decoder.js +0 -67
- package/lib/tests/abc-mei-compare.d.ts +0 -1
- package/lib/tests/abc-mei-compare.js +0 -525
- package/lib/tests/auto-beam.d.ts +0 -9
- package/lib/tests/auto-beam.js +0 -151
- package/lib/tests/computeMeiHashes.d.ts +0 -1
- package/lib/tests/computeMeiHashes.js +0 -87
- package/lib/tests/encoder-mutation.d.ts +0 -9
- package/lib/tests/encoder-mutation.js +0 -110
- package/lib/tests/gpt-review-issues.d.ts +0 -5
- package/lib/tests/gpt-review-issues.js +0 -255
- package/lib/tests/json-to-lyl.d.ts +0 -1
- package/lib/tests/json-to-lyl.js +0 -18
- package/lib/tests/lilypond-roundtrip.d.ts +0 -7
- package/lib/tests/lilypond-roundtrip.js +0 -558
- package/lib/tests/lilypondDecoder.d.ts +0 -6
- package/lib/tests/lilypondDecoder.js +0 -95
- package/lib/tests/ly-to-lyl.d.ts +0 -1
- package/lib/tests/ly-to-lyl.js +0 -12
- package/lib/tests/mei.d.ts +0 -1
- package/lib/tests/mei.js +0 -278
- package/lib/tests/musicxml-decoder.d.ts +0 -4
- package/lib/tests/musicxml-decoder.js +0 -61
- package/lib/tests/musicxml-detail.d.ts +0 -4
- package/lib/tests/musicxml-detail.js +0 -85
- package/lib/tests/musicxml-fprod.d.ts +0 -9
- package/lib/tests/musicxml-fprod.js +0 -153
- package/lib/tests/musicxml-roundtrip.d.ts +0 -7
- package/lib/tests/musicxml-roundtrip.js +0 -296
- package/lib/tests/musicxml-to-mei.d.ts +0 -6
- package/lib/tests/musicxml-to-mei.js +0 -115
- package/lib/tests/parser.d.ts +0 -1
- package/lib/tests/parser.js +0 -17
- package/lib/tests/render-k283.d.ts +0 -1
- package/lib/tests/render-k283.js +0 -33
- package/lib/tests/render-lyl.d.ts +0 -1
- package/lib/tests/render-lyl.js +0 -35
- package/lib/tests/unit/afterGraceInsideTuplet.test.d.ts +0 -23
- package/lib/tests/unit/afterGraceInsideTuplet.test.js +0 -186
- package/lib/tests/unit/changeStaffBeforeTuplet.test.d.ts +0 -21
- package/lib/tests/unit/changeStaffBeforeTuplet.test.js +0 -356
- package/lib/tests/unit/crossStaffDecoder.test.d.ts +0 -15
- package/lib/tests/unit/crossStaffDecoder.test.js +0 -147
- package/lib/tests/unit/crossStaffEdgeCases.test.d.ts +0 -1
- package/lib/tests/unit/crossStaffEdgeCases.test.js +0 -209
- package/lib/tests/unit/crossStaffMultiMeasure.test.d.ts +0 -15
- package/lib/tests/unit/crossStaffMultiMeasure.test.js +0 -231
- package/lib/tests/unit/fullMeasureRestDecoder.test.d.ts +0 -11
- package/lib/tests/unit/fullMeasureRestDecoder.test.js +0 -154
- package/lib/tests/unit/gptReviewIssues.test.d.ts +0 -8
- package/lib/tests/unit/gptReviewIssues.test.js +0 -240
- package/lib/tests/unit/parallelMusicDecoder.test.d.ts +0 -13
- package/lib/tests/unit/parallelMusicDecoder.test.js +0 -261
- package/lib/tests/unit/partialWarning.test.d.ts +0 -4
- package/lib/tests/unit/partialWarning.test.js +0 -65
- package/lib/tests/unit/serializerRoundTrip.test.d.ts +0 -8
- package/lib/tests/unit/serializerRoundTrip.test.js +0 -263
- package/lib/tests/unit/staffInsideTuplet.test.d.ts +0 -25
- package/lib/tests/unit/staffInsideTuplet.test.js +0 -133
- package/lib/tests/unit/timesFirstNoteEscape.test.d.ts +0 -16
- package/lib/tests/unit/timesFirstNoteEscape.test.js +0 -152
- package/lib/tests/unit/tupletWithBaseDuration.test.d.ts +0 -17
- package/lib/tests/unit/tupletWithBaseDuration.test.js +0 -139
- package/lib/tests/unit/voiceStaffParsing.test.d.ts +0 -13
- package/lib/tests/unit/voiceStaffParsing.test.js +0 -118
|
@@ -1,701 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lilylet to MusicXML Encoder
|
|
3
|
-
*
|
|
4
|
-
* Converts LilyletDoc to MusicXML format.
|
|
5
|
-
* Produces valid MusicXML 4.0 partwise documents.
|
|
6
|
-
*/
|
|
7
|
-
import { StemDirection, Accidental, HairpinType, PedalType, } from "./types.js";
|
|
8
|
-
import { DIVISIONS, DIVISION_TO_TYPE, calculateDuration, } from "./musicXmlUtils.js";
|
|
9
|
-
// === Constants and Reverse Mappings ===
|
|
10
|
-
// Phonet to MusicXML step
|
|
11
|
-
const PHONET_TO_STEP = {
|
|
12
|
-
c: 'C',
|
|
13
|
-
d: 'D',
|
|
14
|
-
e: 'E',
|
|
15
|
-
f: 'F',
|
|
16
|
-
g: 'G',
|
|
17
|
-
a: 'A',
|
|
18
|
-
b: 'B',
|
|
19
|
-
};
|
|
20
|
-
// Accidental to MusicXML alter
|
|
21
|
-
const ACCIDENTAL_TO_ALTER = {
|
|
22
|
-
sharp: 1,
|
|
23
|
-
flat: -1,
|
|
24
|
-
doubleSharp: 2,
|
|
25
|
-
doubleFlat: -2,
|
|
26
|
-
natural: 0,
|
|
27
|
-
};
|
|
28
|
-
// Key signature to fifths (major keys)
|
|
29
|
-
const KEY_TO_FIFTHS = {
|
|
30
|
-
'c': 0,
|
|
31
|
-
'g': 1,
|
|
32
|
-
'd': 2,
|
|
33
|
-
'a': 3,
|
|
34
|
-
'e': 4,
|
|
35
|
-
'b': 5,
|
|
36
|
-
'f#': 6, 'fs': 6,
|
|
37
|
-
'c#': 7, 'cs': 7,
|
|
38
|
-
'f': -1,
|
|
39
|
-
'bb': -2, 'bf': -2,
|
|
40
|
-
'eb': -3, 'ef': -3,
|
|
41
|
-
'ab': -4, 'af': -4,
|
|
42
|
-
'db': -5, 'df': -5,
|
|
43
|
-
'gb': -6, 'gf': -6,
|
|
44
|
-
'cb': -7, 'cf': -7,
|
|
45
|
-
};
|
|
46
|
-
// Clef to MusicXML sign
|
|
47
|
-
const CLEF_TO_SIGN = {
|
|
48
|
-
treble: { sign: 'G', line: 2 },
|
|
49
|
-
bass: { sign: 'F', line: 4 },
|
|
50
|
-
alto: { sign: 'C', line: 3 },
|
|
51
|
-
};
|
|
52
|
-
// Articulation to MusicXML element name
|
|
53
|
-
const ARTICULATION_TO_XML = {
|
|
54
|
-
staccato: 'staccato',
|
|
55
|
-
staccatissimo: 'staccatissimo',
|
|
56
|
-
tenuto: 'tenuto',
|
|
57
|
-
accent: 'accent',
|
|
58
|
-
marcato: 'strong-accent',
|
|
59
|
-
portato: 'detached-legato',
|
|
60
|
-
};
|
|
61
|
-
// Ornament to MusicXML element name
|
|
62
|
-
const ORNAMENT_TO_XML = {
|
|
63
|
-
trill: 'trill-mark',
|
|
64
|
-
turn: 'turn',
|
|
65
|
-
mordent: 'mordent',
|
|
66
|
-
prall: 'inverted-mordent',
|
|
67
|
-
};
|
|
68
|
-
// Dynamic to MusicXML element name
|
|
69
|
-
const DYNAMIC_TO_XML = {
|
|
70
|
-
ppp: 'ppp',
|
|
71
|
-
pp: 'pp',
|
|
72
|
-
p: 'p',
|
|
73
|
-
mp: 'mp',
|
|
74
|
-
mf: 'mf',
|
|
75
|
-
f: 'f',
|
|
76
|
-
ff: 'ff',
|
|
77
|
-
fff: 'fff',
|
|
78
|
-
sfz: 'sfz',
|
|
79
|
-
rfz: 'rfz',
|
|
80
|
-
};
|
|
81
|
-
// Barline style to MusicXML
|
|
82
|
-
const BARLINE_TO_XML = {
|
|
83
|
-
'|': { barStyle: 'regular' },
|
|
84
|
-
'||': { barStyle: 'light-light' },
|
|
85
|
-
'|.': { barStyle: 'light-heavy' },
|
|
86
|
-
'.|:': { barStyle: 'heavy-light', repeat: 'forward' },
|
|
87
|
-
':|.': { barStyle: 'light-heavy', repeat: 'backward' },
|
|
88
|
-
':..:': { barStyle: 'light-heavy', repeat: 'backward' }, // Will need special handling
|
|
89
|
-
};
|
|
90
|
-
// === XML Helper Functions ===
|
|
91
|
-
const escapeXml = (text) => {
|
|
92
|
-
return text
|
|
93
|
-
.replace(/&/g, '&')
|
|
94
|
-
.replace(/</g, '<')
|
|
95
|
-
.replace(/>/g, '>')
|
|
96
|
-
.replace(/"/g, '"')
|
|
97
|
-
.replace(/'/g, ''');
|
|
98
|
-
};
|
|
99
|
-
const indent = (level) => ' '.repeat(level);
|
|
100
|
-
// === Encoding Functions ===
|
|
101
|
-
/**
|
|
102
|
-
* Encode pitch to MusicXML
|
|
103
|
-
*/
|
|
104
|
-
const encodePitch = (pitch, level) => {
|
|
105
|
-
const step = PHONET_TO_STEP[pitch.phonet] || 'C';
|
|
106
|
-
const octave = pitch.octave + 4; // Lilylet octave 0 = MusicXML octave 4
|
|
107
|
-
const alter = pitch.accidental ? ACCIDENTAL_TO_ALTER[pitch.accidental] : undefined;
|
|
108
|
-
let xml = `${indent(level)}<pitch>\n`;
|
|
109
|
-
xml += `${indent(level + 1)}<step>${step}</step>\n`;
|
|
110
|
-
if (alter !== undefined && alter !== 0) {
|
|
111
|
-
xml += `${indent(level + 1)}<alter>${alter}</alter>\n`;
|
|
112
|
-
}
|
|
113
|
-
xml += `${indent(level + 1)}<octave>${octave}</octave>\n`;
|
|
114
|
-
xml += `${indent(level)}</pitch>\n`;
|
|
115
|
-
return xml;
|
|
116
|
-
};
|
|
117
|
-
/**
|
|
118
|
-
* Encode duration elements
|
|
119
|
-
*/
|
|
120
|
-
const encodeDuration = (duration, level) => {
|
|
121
|
-
const dur = calculateDuration(duration);
|
|
122
|
-
const type = DIVISION_TO_TYPE[duration.division] || 'quarter';
|
|
123
|
-
let xml = `${indent(level)}<duration>${dur}</duration>\n`;
|
|
124
|
-
xml += `${indent(level)}<type>${type}</type>\n`;
|
|
125
|
-
for (let i = 0; i < duration.dots; i++) {
|
|
126
|
-
xml += `${indent(level)}<dot/>\n`;
|
|
127
|
-
}
|
|
128
|
-
if (duration.tuplet) {
|
|
129
|
-
// MusicXML: actual-notes = notes played (Lilylet denominator)
|
|
130
|
-
// normal-notes = normal count (Lilylet numerator)
|
|
131
|
-
// e.g., \times 2/3 → actual=3, normal=2
|
|
132
|
-
xml += `${indent(level)}<time-modification>\n`;
|
|
133
|
-
xml += `${indent(level + 1)}<actual-notes>${duration.tuplet.denominator}</actual-notes>\n`;
|
|
134
|
-
xml += `${indent(level + 1)}<normal-notes>${duration.tuplet.numerator}</normal-notes>\n`;
|
|
135
|
-
xml += `${indent(level)}</time-modification>\n`;
|
|
136
|
-
}
|
|
137
|
-
return xml;
|
|
138
|
-
};
|
|
139
|
-
/**
|
|
140
|
-
* Encode key signature to fifths
|
|
141
|
-
*/
|
|
142
|
-
const getKeyFifths = (key) => {
|
|
143
|
-
let keyStr = key.pitch;
|
|
144
|
-
if (key.accidental === Accidental.sharp) {
|
|
145
|
-
keyStr += 's';
|
|
146
|
-
}
|
|
147
|
-
else if (key.accidental === Accidental.flat) {
|
|
148
|
-
keyStr += 'f';
|
|
149
|
-
}
|
|
150
|
-
let fifths = KEY_TO_FIFTHS[keyStr.toLowerCase()] ?? 0;
|
|
151
|
-
// Adjust for minor mode (relative minor is 3 fifths down)
|
|
152
|
-
if (key.mode === 'minor') {
|
|
153
|
-
// Minor keys have same fifths as their relative major
|
|
154
|
-
// e.g., A minor = C major = 0 fifths
|
|
155
|
-
}
|
|
156
|
-
return fifths;
|
|
157
|
-
};
|
|
158
|
-
/**
|
|
159
|
-
* Encode attributes element (key, time, clef, divisions)
|
|
160
|
-
*/
|
|
161
|
-
const encodeAttributes = (level, options) => {
|
|
162
|
-
let xml = `${indent(level)}<attributes>\n`;
|
|
163
|
-
if (options.divisions) {
|
|
164
|
-
xml += `${indent(level + 1)}<divisions>${DIVISIONS}</divisions>\n`;
|
|
165
|
-
}
|
|
166
|
-
if (options.key) {
|
|
167
|
-
const fifths = getKeyFifths(options.key);
|
|
168
|
-
xml += `${indent(level + 1)}<key>\n`;
|
|
169
|
-
xml += `${indent(level + 2)}<fifths>${fifths}</fifths>\n`;
|
|
170
|
-
xml += `${indent(level + 2)}<mode>${options.key.mode}</mode>\n`;
|
|
171
|
-
xml += `${indent(level + 1)}</key>\n`;
|
|
172
|
-
}
|
|
173
|
-
if (options.time) {
|
|
174
|
-
xml += `${indent(level + 1)}<time>\n`;
|
|
175
|
-
xml += `${indent(level + 2)}<beats>${options.time.numerator}</beats>\n`;
|
|
176
|
-
xml += `${indent(level + 2)}<beat-type>${options.time.denominator}</beat-type>\n`;
|
|
177
|
-
xml += `${indent(level + 1)}</time>\n`;
|
|
178
|
-
}
|
|
179
|
-
if (options.staves && options.staves > 1) {
|
|
180
|
-
xml += `${indent(level + 1)}<staves>${options.staves}</staves>\n`;
|
|
181
|
-
}
|
|
182
|
-
if (options.clef) {
|
|
183
|
-
const clefInfo = CLEF_TO_SIGN[options.clef];
|
|
184
|
-
if (clefInfo) {
|
|
185
|
-
xml += `${indent(level + 1)}<clef>\n`;
|
|
186
|
-
xml += `${indent(level + 2)}<sign>${clefInfo.sign}</sign>\n`;
|
|
187
|
-
xml += `${indent(level + 2)}<line>${clefInfo.line}</line>\n`;
|
|
188
|
-
xml += `${indent(level + 1)}</clef>\n`;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
xml += `${indent(level)}</attributes>\n`;
|
|
192
|
-
return xml;
|
|
193
|
-
};
|
|
194
|
-
/**
|
|
195
|
-
* Encode notations (articulations, ornaments, ties, slurs, etc.)
|
|
196
|
-
*/
|
|
197
|
-
const encodeNotations = (marks, level) => {
|
|
198
|
-
const articulations = [];
|
|
199
|
-
const ornaments = [];
|
|
200
|
-
const otherNotations = [];
|
|
201
|
-
for (const mark of marks) {
|
|
202
|
-
switch (mark.markType) {
|
|
203
|
-
case 'articulation':
|
|
204
|
-
const artXml = ARTICULATION_TO_XML[mark.type];
|
|
205
|
-
if (artXml) {
|
|
206
|
-
articulations.push(`<${artXml}/>`);
|
|
207
|
-
}
|
|
208
|
-
break;
|
|
209
|
-
case 'ornament':
|
|
210
|
-
const ornXml = ORNAMENT_TO_XML[mark.type];
|
|
211
|
-
if (ornXml) {
|
|
212
|
-
if (mark.type === 'fermata') {
|
|
213
|
-
otherNotations.push('<fermata/>');
|
|
214
|
-
}
|
|
215
|
-
else if (mark.type === 'arpeggio') {
|
|
216
|
-
otherNotations.push('<arpeggiate/>');
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
ornaments.push(`<${ornXml}/>`);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
break;
|
|
223
|
-
case 'tie':
|
|
224
|
-
otherNotations.push(`<tied type="${mark.start ? 'start' : 'stop'}"/>`);
|
|
225
|
-
break;
|
|
226
|
-
case 'slur':
|
|
227
|
-
otherNotations.push(`<slur type="${mark.start ? 'start' : 'stop'}" number="1"/>`);
|
|
228
|
-
break;
|
|
229
|
-
case 'tuplet':
|
|
230
|
-
otherNotations.push(`<tuplet type="${mark.start ? 'start' : 'stop'}"/>`);
|
|
231
|
-
break;
|
|
232
|
-
case 'fingering':
|
|
233
|
-
// Fingering goes in technical
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (articulations.length === 0 && ornaments.length === 0 && otherNotations.length === 0) {
|
|
238
|
-
return '';
|
|
239
|
-
}
|
|
240
|
-
let xml = `${indent(level)}<notations>\n`;
|
|
241
|
-
for (const notation of otherNotations) {
|
|
242
|
-
xml += `${indent(level + 1)}${notation}\n`;
|
|
243
|
-
}
|
|
244
|
-
if (articulations.length > 0) {
|
|
245
|
-
xml += `${indent(level + 1)}<articulations>\n`;
|
|
246
|
-
for (const art of articulations) {
|
|
247
|
-
xml += `${indent(level + 2)}${art}\n`;
|
|
248
|
-
}
|
|
249
|
-
xml += `${indent(level + 1)}</articulations>\n`;
|
|
250
|
-
}
|
|
251
|
-
if (ornaments.length > 0) {
|
|
252
|
-
xml += `${indent(level + 1)}<ornaments>\n`;
|
|
253
|
-
for (const orn of ornaments) {
|
|
254
|
-
xml += `${indent(level + 2)}${orn}\n`;
|
|
255
|
-
}
|
|
256
|
-
xml += `${indent(level + 1)}</ornaments>\n`;
|
|
257
|
-
}
|
|
258
|
-
xml += `${indent(level)}</notations>\n`;
|
|
259
|
-
return xml;
|
|
260
|
-
};
|
|
261
|
-
/**
|
|
262
|
-
* Encode a note event
|
|
263
|
-
*/
|
|
264
|
-
const encodeNote = (event, voice, staff, level, isChord = false) => {
|
|
265
|
-
let xml = `${indent(level)}<note>\n`;
|
|
266
|
-
if (isChord) {
|
|
267
|
-
xml += `${indent(level + 1)}<chord/>\n`;
|
|
268
|
-
}
|
|
269
|
-
if (event.grace) {
|
|
270
|
-
xml += `${indent(level + 1)}<grace/>\n`;
|
|
271
|
-
}
|
|
272
|
-
// Pitch (use first pitch, additional pitches become chord notes)
|
|
273
|
-
const pitch = isChord ? event.pitches[0] : event.pitches[0];
|
|
274
|
-
xml += encodePitch(pitch, level + 1);
|
|
275
|
-
// Duration (not for grace notes)
|
|
276
|
-
if (!event.grace) {
|
|
277
|
-
xml += encodeDuration(event.duration, level + 1);
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
// Grace notes still need type
|
|
281
|
-
const type = DIVISION_TO_TYPE[event.duration.division] || 'eighth';
|
|
282
|
-
xml += `${indent(level + 1)}<type>${type}</type>\n`;
|
|
283
|
-
}
|
|
284
|
-
// Tie notation in note element
|
|
285
|
-
const hasTieStart = event.marks?.some(m => m.markType === 'tie' && m.start);
|
|
286
|
-
const hasTieStop = event.marks?.some(m => m.markType === 'tie' && !m.start);
|
|
287
|
-
if (hasTieStart) {
|
|
288
|
-
xml += `${indent(level + 1)}<tie type="start"/>\n`;
|
|
289
|
-
}
|
|
290
|
-
if (hasTieStop) {
|
|
291
|
-
xml += `${indent(level + 1)}<tie type="stop"/>\n`;
|
|
292
|
-
}
|
|
293
|
-
// Voice
|
|
294
|
-
xml += `${indent(level + 1)}<voice>${voice}</voice>\n`;
|
|
295
|
-
// Staff (for grand staff)
|
|
296
|
-
if (staff > 0) {
|
|
297
|
-
xml += `${indent(level + 1)}<staff>${staff}</staff>\n`;
|
|
298
|
-
}
|
|
299
|
-
// Stem direction
|
|
300
|
-
if (event.stemDirection && event.stemDirection !== StemDirection.auto) {
|
|
301
|
-
xml += `${indent(level + 1)}<stem>${event.stemDirection}</stem>\n`;
|
|
302
|
-
}
|
|
303
|
-
// Beam marks
|
|
304
|
-
const beamStart = event.marks?.find(m => m.markType === 'beam' && m.start);
|
|
305
|
-
const beamEnd = event.marks?.find(m => m.markType === 'beam' && !m.start);
|
|
306
|
-
if (beamStart) {
|
|
307
|
-
xml += `${indent(level + 1)}<beam number="1">begin</beam>\n`;
|
|
308
|
-
}
|
|
309
|
-
else if (beamEnd) {
|
|
310
|
-
xml += `${indent(level + 1)}<beam number="1">end</beam>\n`;
|
|
311
|
-
}
|
|
312
|
-
// Notations
|
|
313
|
-
if (event.marks && event.marks.length > 0) {
|
|
314
|
-
xml += encodeNotations(event.marks, level + 1);
|
|
315
|
-
}
|
|
316
|
-
xml += `${indent(level)}</note>\n`;
|
|
317
|
-
return xml;
|
|
318
|
-
};
|
|
319
|
-
/**
|
|
320
|
-
* Encode a rest event
|
|
321
|
-
*/
|
|
322
|
-
const encodeRest = (event, voice, staff, level) => {
|
|
323
|
-
let xml = `${indent(level)}<note>\n`;
|
|
324
|
-
xml += `${indent(level + 1)}<rest`;
|
|
325
|
-
if (event.fullMeasure) {
|
|
326
|
-
xml += ' measure="yes"';
|
|
327
|
-
}
|
|
328
|
-
xml += '/>\n';
|
|
329
|
-
xml += encodeDuration(event.duration, level + 1);
|
|
330
|
-
xml += `${indent(level + 1)}<voice>${voice}</voice>\n`;
|
|
331
|
-
if (staff > 0) {
|
|
332
|
-
xml += `${indent(level + 1)}<staff>${staff}</staff>\n`;
|
|
333
|
-
}
|
|
334
|
-
xml += `${indent(level)}</note>\n`;
|
|
335
|
-
return xml;
|
|
336
|
-
};
|
|
337
|
-
/**
|
|
338
|
-
* Encode a rest event with tuplet notation start/stop
|
|
339
|
-
*/
|
|
340
|
-
const encodeRestWithTuplet = (event, voice, staff, level, isFirst, isLast) => {
|
|
341
|
-
let xml = `${indent(level)}<note>\n`;
|
|
342
|
-
xml += `${indent(level + 1)}<rest`;
|
|
343
|
-
if (event.fullMeasure) {
|
|
344
|
-
xml += ' measure="yes"';
|
|
345
|
-
}
|
|
346
|
-
xml += '/>\n';
|
|
347
|
-
xml += encodeDuration(event.duration, level + 1);
|
|
348
|
-
xml += `${indent(level + 1)}<voice>${voice}</voice>\n`;
|
|
349
|
-
if (staff > 0) {
|
|
350
|
-
xml += `${indent(level + 1)}<staff>${staff}</staff>\n`;
|
|
351
|
-
}
|
|
352
|
-
// Add tuplet notations
|
|
353
|
-
xml += `${indent(level + 1)}<notations>\n`;
|
|
354
|
-
if (isFirst) {
|
|
355
|
-
xml += `${indent(level + 2)}<tuplet type="start"/>\n`;
|
|
356
|
-
}
|
|
357
|
-
if (isLast) {
|
|
358
|
-
xml += `${indent(level + 2)}<tuplet type="stop"/>\n`;
|
|
359
|
-
}
|
|
360
|
-
xml += `${indent(level + 1)}</notations>\n`;
|
|
361
|
-
xml += `${indent(level)}</note>\n`;
|
|
362
|
-
return xml;
|
|
363
|
-
};
|
|
364
|
-
/**
|
|
365
|
-
* Encode direction element (dynamics, tempo, etc.)
|
|
366
|
-
*/
|
|
367
|
-
const encodeDirection = (marks, level) => {
|
|
368
|
-
let xml = '';
|
|
369
|
-
for (const mark of marks) {
|
|
370
|
-
if (mark.markType === 'dynamic') {
|
|
371
|
-
const dynXml = DYNAMIC_TO_XML[mark.type];
|
|
372
|
-
if (dynXml) {
|
|
373
|
-
xml += `${indent(level)}<direction placement="below">\n`;
|
|
374
|
-
xml += `${indent(level + 1)}<direction-type>\n`;
|
|
375
|
-
xml += `${indent(level + 2)}<dynamics>\n`;
|
|
376
|
-
xml += `${indent(level + 3)}<${dynXml}/>\n`;
|
|
377
|
-
xml += `${indent(level + 2)}</dynamics>\n`;
|
|
378
|
-
xml += `${indent(level + 1)}</direction-type>\n`;
|
|
379
|
-
xml += `${indent(level)}</direction>\n`;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
else if (mark.markType === 'hairpin') {
|
|
383
|
-
let wedgeType = '';
|
|
384
|
-
if (mark.type === HairpinType.crescendoStart) {
|
|
385
|
-
wedgeType = 'crescendo';
|
|
386
|
-
}
|
|
387
|
-
else if (mark.type === HairpinType.diminuendoStart) {
|
|
388
|
-
wedgeType = 'diminuendo';
|
|
389
|
-
}
|
|
390
|
-
else if (mark.type === HairpinType.crescendoEnd || mark.type === HairpinType.diminuendoEnd) {
|
|
391
|
-
wedgeType = 'stop';
|
|
392
|
-
}
|
|
393
|
-
if (wedgeType) {
|
|
394
|
-
xml += `${indent(level)}<direction>\n`;
|
|
395
|
-
xml += `${indent(level + 1)}<direction-type>\n`;
|
|
396
|
-
xml += `${indent(level + 2)}<wedge type="${wedgeType}"/>\n`;
|
|
397
|
-
xml += `${indent(level + 1)}</direction-type>\n`;
|
|
398
|
-
xml += `${indent(level)}</direction>\n`;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
else if (mark.markType === 'pedal') {
|
|
402
|
-
let pedalType = '';
|
|
403
|
-
if (mark.type === PedalType.sustainOn) {
|
|
404
|
-
pedalType = 'start';
|
|
405
|
-
}
|
|
406
|
-
else if (mark.type === PedalType.sustainOff) {
|
|
407
|
-
pedalType = 'stop';
|
|
408
|
-
}
|
|
409
|
-
if (pedalType) {
|
|
410
|
-
xml += `${indent(level)}<direction>\n`;
|
|
411
|
-
xml += `${indent(level + 1)}<direction-type>\n`;
|
|
412
|
-
xml += `${indent(level + 2)}<pedal type="${pedalType}"/>\n`;
|
|
413
|
-
xml += `${indent(level + 1)}</direction-type>\n`;
|
|
414
|
-
xml += `${indent(level)}</direction>\n`;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return xml;
|
|
419
|
-
};
|
|
420
|
-
/**
|
|
421
|
-
* Encode tempo marking
|
|
422
|
-
*/
|
|
423
|
-
const encodeTempo = (tempo, level) => {
|
|
424
|
-
let xml = `${indent(level)}<direction placement="above">\n`;
|
|
425
|
-
xml += `${indent(level + 1)}<direction-type>\n`;
|
|
426
|
-
if (tempo.beat && tempo.bpm) {
|
|
427
|
-
xml += `${indent(level + 2)}<metronome>\n`;
|
|
428
|
-
const beatUnit = DIVISION_TO_TYPE[tempo.beat.division] || 'quarter';
|
|
429
|
-
xml += `${indent(level + 3)}<beat-unit>${beatUnit}</beat-unit>\n`;
|
|
430
|
-
if (tempo.beat.dots) {
|
|
431
|
-
for (let i = 0; i < tempo.beat.dots; i++) {
|
|
432
|
-
xml += `${indent(level + 3)}<beat-unit-dot/>\n`;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
xml += `${indent(level + 3)}<per-minute>${tempo.bpm}</per-minute>\n`;
|
|
436
|
-
xml += `${indent(level + 2)}</metronome>\n`;
|
|
437
|
-
}
|
|
438
|
-
if (tempo.text) {
|
|
439
|
-
xml += `${indent(level + 2)}<words>${escapeXml(tempo.text)}</words>\n`;
|
|
440
|
-
}
|
|
441
|
-
xml += `${indent(level + 1)}</direction-type>\n`;
|
|
442
|
-
if (tempo.bpm) {
|
|
443
|
-
xml += `${indent(level + 1)}<sound tempo="${tempo.bpm}"/>\n`;
|
|
444
|
-
}
|
|
445
|
-
xml += `${indent(level)}</direction>\n`;
|
|
446
|
-
return xml;
|
|
447
|
-
};
|
|
448
|
-
/**
|
|
449
|
-
* Encode barline
|
|
450
|
-
*/
|
|
451
|
-
const encodeBarline = (event, level) => {
|
|
452
|
-
const barInfo = BARLINE_TO_XML[event.style];
|
|
453
|
-
if (!barInfo || event.style === '|') {
|
|
454
|
-
return ''; // Regular barline, no need to encode
|
|
455
|
-
}
|
|
456
|
-
let xml = `${indent(level)}<barline location="right">\n`;
|
|
457
|
-
xml += `${indent(level + 1)}<bar-style>${barInfo.barStyle}</bar-style>\n`;
|
|
458
|
-
if (barInfo.repeat) {
|
|
459
|
-
xml += `${indent(level + 1)}<repeat direction="${barInfo.repeat}"/>\n`;
|
|
460
|
-
}
|
|
461
|
-
xml += `${indent(level)}</barline>\n`;
|
|
462
|
-
return xml;
|
|
463
|
-
};
|
|
464
|
-
/**
|
|
465
|
-
* Encode harmony (chord symbol)
|
|
466
|
-
*/
|
|
467
|
-
const encodeHarmony = (event, level) => {
|
|
468
|
-
// Simple text-based harmony for now
|
|
469
|
-
let xml = `${indent(level)}<harmony>\n`;
|
|
470
|
-
xml += `${indent(level + 1)}<root>\n`;
|
|
471
|
-
xml += `${indent(level + 2)}<root-step>C</root-step>\n`; // Placeholder
|
|
472
|
-
xml += `${indent(level + 1)}</root>\n`;
|
|
473
|
-
xml += `${indent(level + 1)}<kind text="${escapeXml(event.text)}">major</kind>\n`;
|
|
474
|
-
xml += `${indent(level)}</harmony>\n`;
|
|
475
|
-
return xml;
|
|
476
|
-
};
|
|
477
|
-
/**
|
|
478
|
-
* Encode a complete measure for a single part
|
|
479
|
-
*/
|
|
480
|
-
const encodeMeasure = (measure, partIndex, measureNumber, isFirst, prevKey, prevTime, level) => {
|
|
481
|
-
let xml = `${indent(level)}<measure number="${measureNumber}">\n`;
|
|
482
|
-
const part = measure.parts[partIndex];
|
|
483
|
-
if (!part) {
|
|
484
|
-
xml += `${indent(level)}</measure>\n`;
|
|
485
|
-
return xml;
|
|
486
|
-
}
|
|
487
|
-
// Determine if we need attributes
|
|
488
|
-
const needAttributes = isFirst ||
|
|
489
|
-
(measure.key && JSON.stringify(measure.key) !== JSON.stringify(prevKey)) ||
|
|
490
|
-
(measure.timeSig && JSON.stringify(measure.timeSig) !== JSON.stringify(prevTime));
|
|
491
|
-
// Find max staff number within this part
|
|
492
|
-
let maxStaff = 1;
|
|
493
|
-
for (const voice of part.voices) {
|
|
494
|
-
maxStaff = Math.max(maxStaff, voice.staff || 1);
|
|
495
|
-
}
|
|
496
|
-
// Encode attributes if needed
|
|
497
|
-
if (needAttributes) {
|
|
498
|
-
// Find clef from first voice of this part
|
|
499
|
-
let clef;
|
|
500
|
-
for (const voice of part.voices) {
|
|
501
|
-
for (const event of voice.events) {
|
|
502
|
-
if (event.type === 'context' && event.clef) {
|
|
503
|
-
clef = event.clef;
|
|
504
|
-
break;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
if (clef)
|
|
508
|
-
break;
|
|
509
|
-
}
|
|
510
|
-
xml += encodeAttributes(level + 1, {
|
|
511
|
-
divisions: isFirst,
|
|
512
|
-
key: measure.key || prevKey,
|
|
513
|
-
time: measure.timeSig || prevTime,
|
|
514
|
-
clef: clef,
|
|
515
|
-
staves: maxStaff > 1 ? maxStaff : undefined,
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
// Encode voices (voice numbering starts at 1 for each part)
|
|
519
|
-
let voiceNum = 1;
|
|
520
|
-
let currentPosition = 0;
|
|
521
|
-
for (const voice of part.voices) {
|
|
522
|
-
let currentStaff = voice.staff || 1;
|
|
523
|
-
let voicePosition = 0;
|
|
524
|
-
// Backup if needed
|
|
525
|
-
if (currentPosition > 0 && voiceNum > 1) {
|
|
526
|
-
xml += `${indent(level + 1)}<backup>\n`;
|
|
527
|
-
xml += `${indent(level + 2)}<duration>${currentPosition}</duration>\n`;
|
|
528
|
-
xml += `${indent(level + 1)}</backup>\n`;
|
|
529
|
-
voicePosition = 0;
|
|
530
|
-
}
|
|
531
|
-
for (const event of voice.events) {
|
|
532
|
-
switch (event.type) {
|
|
533
|
-
case 'note': {
|
|
534
|
-
// Check for direction marks (dynamics, hairpins, pedals)
|
|
535
|
-
const directionMarks = event.marks?.filter(m => m.markType === 'dynamic' || m.markType === 'hairpin' || m.markType === 'pedal') || [];
|
|
536
|
-
if (directionMarks.length > 0) {
|
|
537
|
-
xml += encodeDirection(directionMarks, level + 1);
|
|
538
|
-
}
|
|
539
|
-
// Encode main note
|
|
540
|
-
xml += encodeNote(event, voiceNum, currentStaff, level + 1);
|
|
541
|
-
const dur = calculateDuration(event.duration);
|
|
542
|
-
voicePosition += dur;
|
|
543
|
-
// Encode chord notes
|
|
544
|
-
for (let i = 1; i < event.pitches.length; i++) {
|
|
545
|
-
const chordEvent = {
|
|
546
|
-
...event,
|
|
547
|
-
pitches: [event.pitches[i]],
|
|
548
|
-
};
|
|
549
|
-
xml += encodeNote(chordEvent, voiceNum, currentStaff, level + 1, true);
|
|
550
|
-
}
|
|
551
|
-
break;
|
|
552
|
-
}
|
|
553
|
-
case 'rest': {
|
|
554
|
-
xml += encodeRest(event, voiceNum, currentStaff, level + 1);
|
|
555
|
-
const dur = calculateDuration(event.duration);
|
|
556
|
-
voicePosition += dur;
|
|
557
|
-
break;
|
|
558
|
-
}
|
|
559
|
-
case 'context': {
|
|
560
|
-
if (event.tempo) {
|
|
561
|
-
xml += encodeTempo(event.tempo, level + 1);
|
|
562
|
-
}
|
|
563
|
-
if (event.staff) {
|
|
564
|
-
currentStaff = event.staff;
|
|
565
|
-
}
|
|
566
|
-
// Other context changes are handled in attributes
|
|
567
|
-
break;
|
|
568
|
-
}
|
|
569
|
-
case 'tuplet': {
|
|
570
|
-
const tupletEvents = event.events.filter(e => e.type === 'note' || e.type === 'rest');
|
|
571
|
-
for (let ti = 0; ti < tupletEvents.length; ti++) {
|
|
572
|
-
const subEvent = tupletEvents[ti];
|
|
573
|
-
// Set tuplet ratio on duration so encodeDuration emits <time-modification>
|
|
574
|
-
const originalTuplet = subEvent.duration.tuplet;
|
|
575
|
-
subEvent.duration.tuplet = event.ratio;
|
|
576
|
-
const isFirst = ti === 0;
|
|
577
|
-
const isLast = ti === tupletEvents.length - 1;
|
|
578
|
-
if (subEvent.type === 'note') {
|
|
579
|
-
// Add tuplet notation marks
|
|
580
|
-
const tupletMarks = [];
|
|
581
|
-
if (isFirst)
|
|
582
|
-
tupletMarks.push({ markType: 'tuplet', start: true });
|
|
583
|
-
if (isLast)
|
|
584
|
-
tupletMarks.push({ markType: 'tuplet', start: false });
|
|
585
|
-
if (tupletMarks.length > 0) {
|
|
586
|
-
const origMarks = subEvent.marks;
|
|
587
|
-
subEvent.marks = [...(subEvent.marks || []), ...tupletMarks];
|
|
588
|
-
xml += encodeNote(subEvent, voiceNum, currentStaff, level + 1);
|
|
589
|
-
subEvent.marks = origMarks;
|
|
590
|
-
}
|
|
591
|
-
else {
|
|
592
|
-
xml += encodeNote(subEvent, voiceNum, currentStaff, level + 1);
|
|
593
|
-
}
|
|
594
|
-
const dur = calculateDuration(subEvent.duration);
|
|
595
|
-
voicePosition += dur;
|
|
596
|
-
}
|
|
597
|
-
else if (subEvent.type === 'rest') {
|
|
598
|
-
if (isFirst || isLast) {
|
|
599
|
-
xml += encodeRestWithTuplet(subEvent, voiceNum, currentStaff, level + 1, isFirst, isLast);
|
|
600
|
-
}
|
|
601
|
-
else {
|
|
602
|
-
xml += encodeRest(subEvent, voiceNum, currentStaff, level + 1);
|
|
603
|
-
}
|
|
604
|
-
const dur = calculateDuration(subEvent.duration);
|
|
605
|
-
voicePosition += dur;
|
|
606
|
-
}
|
|
607
|
-
// Restore original tuplet value
|
|
608
|
-
subEvent.duration.tuplet = originalTuplet;
|
|
609
|
-
}
|
|
610
|
-
break;
|
|
611
|
-
}
|
|
612
|
-
case 'barline': {
|
|
613
|
-
xml += encodeBarline(event, level + 1);
|
|
614
|
-
break;
|
|
615
|
-
}
|
|
616
|
-
case 'harmony': {
|
|
617
|
-
xml += encodeHarmony(event, level + 1);
|
|
618
|
-
break;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
currentPosition = Math.max(currentPosition, voicePosition);
|
|
623
|
-
voiceNum++;
|
|
624
|
-
}
|
|
625
|
-
xml += `${indent(level)}</measure>\n`;
|
|
626
|
-
return xml;
|
|
627
|
-
};
|
|
628
|
-
/**
|
|
629
|
-
* Encode metadata to MusicXML elements
|
|
630
|
-
*/
|
|
631
|
-
const encodeMetadata = (metadata, level) => {
|
|
632
|
-
let xml = '';
|
|
633
|
-
if (metadata.title) {
|
|
634
|
-
xml += `${indent(level)}<work>\n`;
|
|
635
|
-
xml += `${indent(level + 1)}<work-title>${escapeXml(metadata.title)}</work-title>\n`;
|
|
636
|
-
xml += `${indent(level)}</work>\n`;
|
|
637
|
-
}
|
|
638
|
-
xml += `${indent(level)}<identification>\n`;
|
|
639
|
-
if (metadata.composer) {
|
|
640
|
-
xml += `${indent(level + 1)}<creator type="composer">${escapeXml(metadata.composer)}</creator>\n`;
|
|
641
|
-
}
|
|
642
|
-
if (metadata.arranger) {
|
|
643
|
-
xml += `${indent(level + 1)}<creator type="arranger">${escapeXml(metadata.arranger)}</creator>\n`;
|
|
644
|
-
}
|
|
645
|
-
if (metadata.lyricist) {
|
|
646
|
-
xml += `${indent(level + 1)}<creator type="lyricist">${escapeXml(metadata.lyricist)}</creator>\n`;
|
|
647
|
-
}
|
|
648
|
-
xml += `${indent(level + 1)}<encoding>\n`;
|
|
649
|
-
xml += `${indent(level + 2)}<software>Lilylet</software>\n`;
|
|
650
|
-
xml += `${indent(level + 2)}<encoding-date>${new Date().toISOString().split('T')[0]}</encoding-date>\n`;
|
|
651
|
-
xml += `${indent(level + 1)}</encoding>\n`;
|
|
652
|
-
xml += `${indent(level)}</identification>\n`;
|
|
653
|
-
return xml;
|
|
654
|
-
};
|
|
655
|
-
/**
|
|
656
|
-
* Encode complete LilyletDoc to MusicXML
|
|
657
|
-
*/
|
|
658
|
-
export const encode = (doc) => {
|
|
659
|
-
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
660
|
-
xml += '<!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 4.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd">\n';
|
|
661
|
-
xml += '<score-partwise version="4.0">\n';
|
|
662
|
-
// Metadata
|
|
663
|
-
if (doc.metadata) {
|
|
664
|
-
xml += encodeMetadata(doc.metadata, 1);
|
|
665
|
-
}
|
|
666
|
-
// Determine number of parts from first measure
|
|
667
|
-
const numParts = doc.measures.length > 0 ? doc.measures[0].parts.length : 1;
|
|
668
|
-
// Part list
|
|
669
|
-
xml += `${indent(1)}<part-list>\n`;
|
|
670
|
-
for (let pi = 0; pi < numParts; pi++) {
|
|
671
|
-
const partId = `P${pi + 1}`;
|
|
672
|
-
const partName = doc.measures[0]?.parts[pi]?.name
|
|
673
|
-
|| (numParts === 1 ? (doc.metadata?.title ? escapeXml(doc.metadata.title) : 'Music') : `Part ${pi + 1}`);
|
|
674
|
-
xml += `${indent(2)}<score-part id="${partId}">\n`;
|
|
675
|
-
xml += `${indent(3)}<part-name>${escapeXml(partName)}</part-name>\n`;
|
|
676
|
-
xml += `${indent(2)}</score-part>\n`;
|
|
677
|
-
}
|
|
678
|
-
xml += `${indent(1)}</part-list>\n`;
|
|
679
|
-
// Encode each part
|
|
680
|
-
for (let pi = 0; pi < numParts; pi++) {
|
|
681
|
-
const partId = `P${pi + 1}`;
|
|
682
|
-
xml += `${indent(1)}<part id="${partId}">\n`;
|
|
683
|
-
let prevKey;
|
|
684
|
-
let prevTime;
|
|
685
|
-
for (let i = 0; i < doc.measures.length; i++) {
|
|
686
|
-
const measure = doc.measures[i];
|
|
687
|
-
const isFirst = i === 0;
|
|
688
|
-
xml += encodeMeasure(measure, pi, i + 1, isFirst, prevKey, prevTime, 2);
|
|
689
|
-
if (measure.key)
|
|
690
|
-
prevKey = measure.key;
|
|
691
|
-
if (measure.timeSig)
|
|
692
|
-
prevTime = measure.timeSig;
|
|
693
|
-
}
|
|
694
|
-
xml += `${indent(1)}</part>\n`;
|
|
695
|
-
}
|
|
696
|
-
xml += '</score-partwise>\n';
|
|
697
|
-
return xml;
|
|
698
|
-
};
|
|
699
|
-
export default {
|
|
700
|
-
encode,
|
|
701
|
-
};
|