@scorelabs/core 1.0.7 → 1.0.10
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/dist/importers/MusicXMLParser.d.ts +7 -2
- package/dist/importers/MusicXMLParser.js +261 -103
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/models/Instrument.d.ts +8 -19
- package/dist/models/Instrument.js +77 -23
- package/dist/models/Measure.d.ts +5 -1
- package/dist/models/Measure.js +132 -19
- package/dist/models/Note.d.ts +7 -7
- package/dist/models/Note.js +2 -2
- package/dist/models/NoteSet.d.ts +44 -42
- package/dist/models/NoteSet.js +5 -2
- package/dist/models/Pitch.js +4 -0
- package/dist/models/PreMeasure.d.ts +24 -0
- package/dist/models/PreMeasure.js +30 -0
- package/dist/models/Score.d.ts +20 -5
- package/dist/models/Score.js +190 -5
- package/dist/models/index.d.ts +1 -0
- package/dist/models/index.js +1 -0
- package/dist/models/types.d.ts +2 -213
- package/dist/models/types.js +2 -213
- package/dist/types/Accidental.d.ts +7 -0
- package/dist/types/Accidental.js +8 -0
- package/dist/types/Arpeggio.d.ts +5 -0
- package/dist/types/Arpeggio.js +6 -0
- package/dist/types/Articulation.d.ts +10 -0
- package/dist/types/Articulation.js +11 -0
- package/dist/types/BarlineStyle.d.ts +9 -0
- package/dist/types/BarlineStyle.js +10 -0
- package/dist/types/Bowing.d.ts +4 -0
- package/dist/types/Bowing.js +5 -0
- package/dist/types/Clef.d.ts +10 -0
- package/dist/types/Clef.js +11 -0
- package/dist/types/Duration.d.ts +20 -0
- package/dist/types/Duration.js +60 -0
- package/dist/types/Dynamic.d.ts +12 -0
- package/dist/types/Dynamic.js +13 -0
- package/dist/types/Fretboard.d.ts +19 -0
- package/dist/types/Fretboard.js +1 -0
- package/dist/types/Genre.d.ts +27 -0
- package/dist/types/Genre.js +28 -0
- package/dist/types/Glissando.d.ts +8 -0
- package/dist/types/Glissando.js +5 -0
- package/dist/types/Hairpin.d.ts +10 -0
- package/dist/types/Hairpin.js +11 -0
- package/dist/types/InstrumentPreset.d.ts +11 -0
- package/dist/types/InstrumentPreset.js +12 -0
- package/dist/types/InstrumentType.d.ts +8 -0
- package/dist/types/InstrumentType.js +9 -0
- package/dist/types/KeySignature.d.ts +3 -0
- package/dist/types/KeySignature.js +1 -0
- package/dist/types/Lyric.d.ts +11 -0
- package/dist/types/Lyric.js +7 -0
- package/dist/types/NoteheadShape.d.ts +8 -0
- package/dist/types/NoteheadShape.js +9 -0
- package/dist/types/Ornament.d.ts +8 -0
- package/dist/types/Ornament.js +9 -0
- package/dist/types/Ottava.d.ts +10 -0
- package/dist/types/Ottava.js +7 -0
- package/dist/types/Pedal.d.ts +4 -0
- package/dist/types/Pedal.js +1 -0
- package/dist/types/Repeat.d.ts +8 -0
- package/dist/types/Repeat.js +1 -0
- package/dist/types/Slur.d.ts +4 -0
- package/dist/types/Slur.js +1 -0
- package/dist/types/StemDirection.d.ts +4 -0
- package/dist/types/StemDirection.js +5 -0
- package/dist/types/Tempo.d.ts +8 -0
- package/dist/types/Tempo.js +1 -0
- package/dist/types/TimeSignature.d.ts +6 -0
- package/dist/types/TimeSignature.js +3 -0
- package/dist/types/Tuplet.d.ts +5 -0
- package/dist/types/Tuplet.js +1 -0
- package/dist/types/User.d.ts +21 -0
- package/dist/types/User.js +7 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/index.js +27 -0
- package/package.json +3 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import JSZip from 'jszip';
|
|
2
|
-
import { getInstrumentByProgram } from '../models/Instrument';
|
|
3
|
-
import { Accidental, Arpeggio, Articulation, BarlineStyle, Bowing, Clef, Duration, GlissandoType, HairpinType, NoteheadShape, Ornament, OttavaType, StemDirection, } from '../models/types';
|
|
2
|
+
import { getInstrumentByProgram, guessInstrumentByName } from '../models/Instrument';
|
|
3
|
+
import { Accidental, Arpeggio, Articulation, BarlineStyle, Bowing, Clef, DURATION_VALUES, Duration, GlissandoType, HairpinType, NoteheadShape, Ornament, OttavaType, StemDirection, decomposeDuration, } from '../models/types';
|
|
4
4
|
/**
|
|
5
5
|
* MusicXML Parser
|
|
6
6
|
*
|
|
@@ -18,6 +18,52 @@ export class MusicXMLParser {
|
|
|
18
18
|
this._domParser =
|
|
19
19
|
domParserInstance || (typeof DOMParser !== 'undefined' ? new DOMParser() : null);
|
|
20
20
|
}
|
|
21
|
+
querySelector(el, selector) {
|
|
22
|
+
if (el.querySelector)
|
|
23
|
+
return el.querySelector(selector);
|
|
24
|
+
const parts = selector.split(' ');
|
|
25
|
+
let current = el;
|
|
26
|
+
for (const part of parts) {
|
|
27
|
+
if (!current)
|
|
28
|
+
return null;
|
|
29
|
+
const results = current.getElementsByTagName(part);
|
|
30
|
+
if (results.length === 0)
|
|
31
|
+
return null;
|
|
32
|
+
current = results[0];
|
|
33
|
+
}
|
|
34
|
+
return current;
|
|
35
|
+
}
|
|
36
|
+
getChildren(el) {
|
|
37
|
+
const parent = el;
|
|
38
|
+
if (parent.children)
|
|
39
|
+
return Array.from(parent.children);
|
|
40
|
+
return Array.from(el.childNodes).filter((node) => node.nodeType === 1);
|
|
41
|
+
}
|
|
42
|
+
getFirstElementChild(el) {
|
|
43
|
+
if (el.firstElementChild)
|
|
44
|
+
return el.firstElementChild;
|
|
45
|
+
const children = this.getChildren(el);
|
|
46
|
+
return children.length > 0 ? children[0] : null;
|
|
47
|
+
}
|
|
48
|
+
querySelectorAll(el, selector) {
|
|
49
|
+
if (el.querySelectorAll)
|
|
50
|
+
return Array.from(el.querySelectorAll(selector));
|
|
51
|
+
const parts = selector.split(' ');
|
|
52
|
+
if (parts.length === 1) {
|
|
53
|
+
return Array.from(el.getElementsByTagName(parts[0]));
|
|
54
|
+
}
|
|
55
|
+
// Simple nested selector support (only two levels for now as needed)
|
|
56
|
+
if (parts.length === 2) {
|
|
57
|
+
const firstLevel = Array.from(el.getElementsByTagName(parts[0]));
|
|
58
|
+
const results = [];
|
|
59
|
+
firstLevel.forEach((parent) => {
|
|
60
|
+
const children = Array.from(parent.getElementsByTagName(parts[1]));
|
|
61
|
+
results.push(...children);
|
|
62
|
+
});
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
21
67
|
parseFromString(xmlString) {
|
|
22
68
|
if (!this._domParser) {
|
|
23
69
|
throw new Error('No DOMParser available. Please provide one in the constructor for Node.js environments.');
|
|
@@ -35,7 +81,7 @@ export class MusicXMLParser {
|
|
|
35
81
|
if (!_parser)
|
|
36
82
|
throw new Error('No DOMParser available');
|
|
37
83
|
const containerDoc = _parser.parseFromString(containerXml, 'application/xml');
|
|
38
|
-
const rootfile =
|
|
84
|
+
const rootfile = (new MusicXMLParser(_parser)).querySelector(containerDoc, 'rootfile');
|
|
39
85
|
const fullPath = rootfile?.getAttribute('full-path');
|
|
40
86
|
if (!fullPath)
|
|
41
87
|
throw new Error('Invalid MXL: Could not find main score file path');
|
|
@@ -63,7 +109,7 @@ export class MusicXMLParser {
|
|
|
63
109
|
}
|
|
64
110
|
xmlString = xmlString.substring(firstBracket).trim();
|
|
65
111
|
const doc = this.parseFromString(xmlString);
|
|
66
|
-
const parseError =
|
|
112
|
+
const parseError = this.querySelector(doc, 'parsererror');
|
|
67
113
|
if (parseError) {
|
|
68
114
|
const preview = xmlString.substring(0, 100).replace(/\n/g, ' ');
|
|
69
115
|
throw new Error(`XML parsing error: ${parseError.textContent}. Content preview: "${preview}..."`);
|
|
@@ -74,6 +120,7 @@ export class MusicXMLParser {
|
|
|
74
120
|
const title = this.parseTitle(doc);
|
|
75
121
|
const subtitle = this.parseSubtitle(doc);
|
|
76
122
|
const composer = this.parseComposer(doc);
|
|
123
|
+
const copyright = this.parseCopyright(doc);
|
|
77
124
|
const partInfo = this.parsePartList(doc);
|
|
78
125
|
const parts = this.parseParts(doc, partInfo);
|
|
79
126
|
let globalBpm = 120;
|
|
@@ -104,6 +151,7 @@ export class MusicXMLParser {
|
|
|
104
151
|
tempoDuration: globalTempoDuration,
|
|
105
152
|
tempoIsDotted: globalTempoIsDotted,
|
|
106
153
|
tempoText: globalTempoText,
|
|
154
|
+
copyright,
|
|
107
155
|
};
|
|
108
156
|
return this.postProcess(score);
|
|
109
157
|
}
|
|
@@ -111,7 +159,7 @@ export class MusicXMLParser {
|
|
|
111
159
|
let subtitle = this.getText(doc.documentElement, 'movement-subtitle');
|
|
112
160
|
if (subtitle)
|
|
113
161
|
return subtitle;
|
|
114
|
-
const work =
|
|
162
|
+
const work = this.querySelector(doc, 'work');
|
|
115
163
|
if (work) {
|
|
116
164
|
subtitle = this.getText(work, 'work-subtitle');
|
|
117
165
|
if (subtitle)
|
|
@@ -120,11 +168,11 @@ export class MusicXMLParser {
|
|
|
120
168
|
if (subtitle)
|
|
121
169
|
return subtitle;
|
|
122
170
|
}
|
|
123
|
-
const credits =
|
|
171
|
+
const credits = this.querySelectorAll(doc, 'credit');
|
|
124
172
|
for (const credit of Array.from(credits)) {
|
|
125
|
-
const type =
|
|
173
|
+
const type = this.querySelector(credit, 'credit-type');
|
|
126
174
|
if (type?.textContent?.toLowerCase() === 'subtitle') {
|
|
127
|
-
const words =
|
|
175
|
+
const words = this.querySelector(credit, 'credit-words');
|
|
128
176
|
if (words?.textContent)
|
|
129
177
|
return words.textContent;
|
|
130
178
|
}
|
|
@@ -137,37 +185,65 @@ export class MusicXMLParser {
|
|
|
137
185
|
title = this.getText(doc.documentElement, 'movement-title');
|
|
138
186
|
return title ?? 'Untitled';
|
|
139
187
|
}
|
|
188
|
+
parseCopyright(doc) {
|
|
189
|
+
const identification = this.querySelector(doc, 'identification');
|
|
190
|
+
if (identification) {
|
|
191
|
+
const rights = this.getText(identification, 'rights');
|
|
192
|
+
if (rights)
|
|
193
|
+
return rights;
|
|
194
|
+
}
|
|
195
|
+
const credits = this.querySelectorAll(doc, 'credit');
|
|
196
|
+
for (const credit of Array.from(credits)) {
|
|
197
|
+
const type = this.querySelector(credit, 'credit-type');
|
|
198
|
+
if (type?.textContent?.toLowerCase() === 'rights' ||
|
|
199
|
+
type?.textContent?.toLowerCase() === 'copyright') {
|
|
200
|
+
const words = this.querySelector(credit, 'credit-words');
|
|
201
|
+
if (words?.textContent)
|
|
202
|
+
return words.textContent;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return '';
|
|
206
|
+
}
|
|
140
207
|
parseComposer(doc) {
|
|
141
|
-
const creators =
|
|
208
|
+
const creators = this.querySelectorAll(doc, 'identification creator');
|
|
142
209
|
for (const creator of Array.from(creators)) {
|
|
143
210
|
if (creator.getAttribute('type') === 'composer')
|
|
144
211
|
return creator.textContent ?? 'Unknown';
|
|
145
212
|
}
|
|
146
|
-
return
|
|
213
|
+
return this.querySelector(doc, 'identification creator')?.textContent ?? 'Unknown';
|
|
147
214
|
}
|
|
148
215
|
parsePartList(doc) {
|
|
149
216
|
const partInfo = new Map();
|
|
150
|
-
const scoreParts =
|
|
217
|
+
const scoreParts = this.querySelectorAll(doc, 'part-list score-part');
|
|
151
218
|
for (const scorePart of Array.from(scoreParts)) {
|
|
152
219
|
const id = scorePart.getAttribute('id');
|
|
153
220
|
const name = this.getText(scorePart, 'part-name') || 'Part';
|
|
154
|
-
let instrument
|
|
221
|
+
let instrument;
|
|
155
222
|
// Find first midi-instrument to determine main instrument
|
|
156
|
-
const firstMidiInst =
|
|
223
|
+
const firstMidiInst = this.querySelector(scorePart, 'midi-instrument');
|
|
157
224
|
if (firstMidiInst) {
|
|
158
225
|
const programStr = this.getText(firstMidiInst, 'midi-program');
|
|
159
226
|
if (programStr) {
|
|
160
227
|
const program = parseInt(programStr);
|
|
161
|
-
// MusicXML uses 1-128, MIDI uses 0-127. Adjust if needed.
|
|
162
|
-
// Often it's accurate to 1-based, so checking bounds.
|
|
163
|
-
// Assuming input is 1-based as per standard
|
|
164
228
|
instrument = getInstrumentByProgram(Math.max(0, program - 1));
|
|
165
229
|
}
|
|
166
230
|
}
|
|
231
|
+
// Fallback: guess by part name
|
|
232
|
+
if (!instrument || instrument.sound === 'piano') {
|
|
233
|
+
const guessed = guessInstrumentByName(name);
|
|
234
|
+
// Only override if the guess is more specific than default piano
|
|
235
|
+
if (guessed.sound !== 'piano') {
|
|
236
|
+
instrument = guessed;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Final default
|
|
240
|
+
if (!instrument) {
|
|
241
|
+
instrument = getInstrumentByProgram(0);
|
|
242
|
+
}
|
|
167
243
|
if (id) {
|
|
168
244
|
partInfo.set(id, { name: name.trim(), instrument });
|
|
169
245
|
}
|
|
170
|
-
const midiInstruments =
|
|
246
|
+
const midiInstruments = this.querySelectorAll(scorePart, 'midi-instrument');
|
|
171
247
|
for (const midiInst of Array.from(midiInstruments)) {
|
|
172
248
|
const instId = midiInst.getAttribute('id');
|
|
173
249
|
const unpitched = this.getNumber(midiInst, 'midi-unpitched');
|
|
@@ -179,7 +255,7 @@ export class MusicXMLParser {
|
|
|
179
255
|
}
|
|
180
256
|
parseParts(doc, partInfo) {
|
|
181
257
|
const parts = [];
|
|
182
|
-
const partElements =
|
|
258
|
+
const partElements = this.querySelectorAll(doc, 'part');
|
|
183
259
|
for (const partElement of Array.from(partElements)) {
|
|
184
260
|
const id = partElement.getAttribute('id');
|
|
185
261
|
const info = partInfo.get(id ?? '');
|
|
@@ -190,11 +266,11 @@ export class MusicXMLParser {
|
|
|
190
266
|
return parts;
|
|
191
267
|
}
|
|
192
268
|
parsePart(partElement) {
|
|
193
|
-
const measureElements =
|
|
269
|
+
const measureElements = this.querySelectorAll(partElement, 'measure');
|
|
194
270
|
let numStaves = 1;
|
|
195
|
-
const firstMeasure =
|
|
271
|
+
const firstMeasure = this.querySelector(partElement, 'measure');
|
|
196
272
|
if (firstMeasure) {
|
|
197
|
-
const stavesNode =
|
|
273
|
+
const stavesNode = this.querySelector(firstMeasure, 'attributes staves');
|
|
198
274
|
if (stavesNode)
|
|
199
275
|
numStaves = parseInt(stavesNode.textContent || '1');
|
|
200
276
|
}
|
|
@@ -203,12 +279,17 @@ export class MusicXMLParser {
|
|
|
203
279
|
measures: [],
|
|
204
280
|
}));
|
|
205
281
|
const staffClefs = Array(numStaves).fill(Clef.Treble);
|
|
206
|
-
|
|
282
|
+
const lastVoicePerStaff = new Map();
|
|
283
|
+
let divisions = 1;
|
|
284
|
+
for (const [mIdx, measureElement] of Array.from(measureElements).entries()) {
|
|
207
285
|
let measureTimeSignature;
|
|
208
286
|
let measureKeySignature;
|
|
209
|
-
|
|
210
|
-
const attributes =
|
|
287
|
+
let isPickup = measureElement.getAttribute('implicit') === 'yes';
|
|
288
|
+
const attributes = this.querySelector(measureElement, 'attributes');
|
|
211
289
|
if (attributes) {
|
|
290
|
+
const divNode = this.querySelector(attributes, 'divisions');
|
|
291
|
+
if (divNode)
|
|
292
|
+
divisions = parseInt(divNode.textContent || '1');
|
|
212
293
|
const fifths = this.getNumber(attributes, 'key fifths');
|
|
213
294
|
if (fifths !== null) {
|
|
214
295
|
this.currentKeyFifths = fifths;
|
|
@@ -220,7 +301,7 @@ export class MusicXMLParser {
|
|
|
220
301
|
this.currentBeats = beats;
|
|
221
302
|
if (beatType !== null)
|
|
222
303
|
this.currentBeatType = beatType;
|
|
223
|
-
const timeElement =
|
|
304
|
+
const timeElement = this.querySelector(attributes, 'time');
|
|
224
305
|
if (timeElement) {
|
|
225
306
|
const symAttr = timeElement.getAttribute('symbol');
|
|
226
307
|
if (symAttr === 'common')
|
|
@@ -236,16 +317,18 @@ export class MusicXMLParser {
|
|
|
236
317
|
symbol: this.currentSymbol !== 'normal' ? this.currentSymbol : undefined,
|
|
237
318
|
};
|
|
238
319
|
}
|
|
239
|
-
const clefElements =
|
|
320
|
+
const clefElements = this.querySelectorAll(attributes, 'clef');
|
|
240
321
|
clefElements.forEach((clef) => {
|
|
241
|
-
const
|
|
322
|
+
const numberAttrs = clef.getAttribute('number');
|
|
323
|
+
const number = numberAttrs ? parseInt(numberAttrs) : 1;
|
|
242
324
|
const sign = this.getText(clef, 'sign');
|
|
243
325
|
if (sign && number <= numStaves) {
|
|
244
326
|
const line = this.getNumber(clef, 'line') || 0;
|
|
245
|
-
|
|
327
|
+
const octaveChange = this.getNumber(clef, 'clef-octave-change') || 0;
|
|
328
|
+
staffClefs[number - 1] = this.mapClef(sign, line, octaveChange);
|
|
246
329
|
}
|
|
247
330
|
});
|
|
248
|
-
const staffDetails =
|
|
331
|
+
const staffDetails = this.querySelectorAll(attributes, 'staff-details');
|
|
249
332
|
staffDetails.forEach((sd) => {
|
|
250
333
|
const number = parseInt(sd.getAttribute('number') || '1');
|
|
251
334
|
const lines = this.getNumber(sd, 'staff-lines');
|
|
@@ -258,8 +341,11 @@ export class MusicXMLParser {
|
|
|
258
341
|
let systemText = undefined;
|
|
259
342
|
let contextDynamic = undefined;
|
|
260
343
|
const { repeats, volta, barlineStyle } = this.parseBarlines(measureElement);
|
|
261
|
-
|
|
262
|
-
const
|
|
344
|
+
// Map of staffIndex -> Map of voiceId -> NoteSetJSON[]
|
|
345
|
+
const staffVoices = new Map();
|
|
346
|
+
for (let i = 0; i < numStaves; i++)
|
|
347
|
+
staffVoices.set(i, new Map());
|
|
348
|
+
const children = this.getChildren(measureElement);
|
|
263
349
|
let pendingChord = undefined;
|
|
264
350
|
let currentNoteSetMap = new Map();
|
|
265
351
|
const beamGroupIdMap = new Map();
|
|
@@ -274,24 +360,24 @@ export class MusicXMLParser {
|
|
|
274
360
|
pendingChord = harmonyData;
|
|
275
361
|
}
|
|
276
362
|
else if (child.nodeName === 'direction') {
|
|
277
|
-
const metronome =
|
|
363
|
+
const metronome = this.querySelector(child, 'metronome');
|
|
278
364
|
if (metronome) {
|
|
279
365
|
const beatUnit = this.getText(metronome, 'beat-unit') || 'quarter';
|
|
280
366
|
const bpm = parseInt(this.getText(metronome, 'per-minute') || '120');
|
|
281
|
-
const isDotted =
|
|
367
|
+
const isDotted = this.querySelector(metronome, 'beat-unit-dot') !== null;
|
|
282
368
|
measureTempo = { bpm, duration: this.mapDuration(beatUnit), isDotted };
|
|
283
369
|
}
|
|
284
|
-
const rehearsal =
|
|
370
|
+
const rehearsal = this.querySelector(child, 'rehearsal');
|
|
285
371
|
if (rehearsal)
|
|
286
372
|
rehearsalMark = rehearsal.textContent || undefined;
|
|
287
|
-
const words =
|
|
373
|
+
const words = this.querySelector(child, 'words');
|
|
288
374
|
if (words) {
|
|
289
375
|
if (measureTempo)
|
|
290
376
|
measureTempo.text = words.textContent || undefined;
|
|
291
377
|
else
|
|
292
378
|
systemText = words.textContent || undefined;
|
|
293
379
|
}
|
|
294
|
-
const wedge =
|
|
380
|
+
const wedge = this.querySelector(child, 'wedge');
|
|
295
381
|
if (wedge) {
|
|
296
382
|
const typeAttr = wedge.getAttribute('type');
|
|
297
383
|
if (typeAttr === 'crescendo' || typeAttr === 'diminuendo' || typeAttr === 'stop') {
|
|
@@ -301,7 +387,7 @@ export class MusicXMLParser {
|
|
|
301
387
|
};
|
|
302
388
|
}
|
|
303
389
|
}
|
|
304
|
-
const octShift =
|
|
390
|
+
const octShift = this.querySelector(child, 'octave-shift');
|
|
305
391
|
if (octShift) {
|
|
306
392
|
const typeAttr = octShift.getAttribute('type');
|
|
307
393
|
const size = parseInt(octShift.getAttribute('size') || '8');
|
|
@@ -318,25 +404,27 @@ export class MusicXMLParser {
|
|
|
318
404
|
};
|
|
319
405
|
}
|
|
320
406
|
}
|
|
321
|
-
const pedalNode =
|
|
407
|
+
const pedalNode = this.querySelector(child, 'pedal');
|
|
322
408
|
if (pedalNode) {
|
|
323
409
|
const typeAttr = pedalNode.getAttribute('type');
|
|
324
410
|
if (typeAttr === 'start' || typeAttr === 'stop') {
|
|
325
411
|
currentPedal = { type: 'sustain', placement: typeAttr };
|
|
326
412
|
}
|
|
327
413
|
}
|
|
328
|
-
const dynamics =
|
|
329
|
-
if (dynamics
|
|
330
|
-
|
|
414
|
+
const dynamics = this.querySelector(child, 'dynamics');
|
|
415
|
+
if (dynamics) {
|
|
416
|
+
const firstChild = this.getFirstElementChild(dynamics);
|
|
417
|
+
if (firstChild)
|
|
418
|
+
contextDynamic = firstChild.nodeName.toLowerCase();
|
|
331
419
|
}
|
|
332
420
|
}
|
|
333
421
|
else if (child.nodeName === 'note') {
|
|
334
422
|
const staffNum = parseInt(this.getText(child, 'staff') || '1');
|
|
335
423
|
const staffIdx = Math.min(Math.max(staffNum - 1, 0), numStaves - 1);
|
|
336
|
-
const isChord =
|
|
424
|
+
const isChord = this.querySelector(child, 'chord') !== null;
|
|
337
425
|
const note = this.parseNote(child);
|
|
338
426
|
if (note) {
|
|
339
|
-
const beamEl =
|
|
427
|
+
const beamEl = this.querySelector(child, 'beam[number="1"]');
|
|
340
428
|
if (beamEl) {
|
|
341
429
|
const beamType = beamEl.textContent;
|
|
342
430
|
if (beamType === 'begin') {
|
|
@@ -375,6 +463,12 @@ export class MusicXMLParser {
|
|
|
375
463
|
note.pedal = currentPedal;
|
|
376
464
|
currentPedal = undefined;
|
|
377
465
|
}
|
|
466
|
+
const voiceId = this.getText(child, 'voice') || '1';
|
|
467
|
+
lastVoicePerStaff.set(staffIdx, voiceId);
|
|
468
|
+
const voiceMap = staffVoices.get(staffIdx);
|
|
469
|
+
if (!voiceMap.has(voiceId))
|
|
470
|
+
voiceMap.set(voiceId, []);
|
|
471
|
+
const voiceNoteSets = voiceMap.get(voiceId);
|
|
378
472
|
if (isChord) {
|
|
379
473
|
const existing = currentNoteSetMap.get(staffIdx);
|
|
380
474
|
if (existing)
|
|
@@ -385,26 +479,65 @@ export class MusicXMLParser {
|
|
|
385
479
|
else {
|
|
386
480
|
const existing = currentNoteSetMap.get(staffIdx);
|
|
387
481
|
if (existing)
|
|
388
|
-
|
|
482
|
+
voiceNoteSets.push({ notes: existing });
|
|
389
483
|
currentNoteSetMap.set(staffIdx, [note]);
|
|
390
484
|
}
|
|
391
485
|
}
|
|
392
486
|
}
|
|
393
487
|
else if (child.nodeName === 'backup' || child.nodeName === 'forward') {
|
|
394
488
|
for (const [sIdx, notes] of currentNoteSetMap.entries()) {
|
|
395
|
-
if (notes.length > 0)
|
|
396
|
-
|
|
489
|
+
if (notes.length > 0) {
|
|
490
|
+
const voiceId = lastVoicePerStaff.get(sIdx) || '1';
|
|
491
|
+
const voiceMap = staffVoices.get(sIdx);
|
|
492
|
+
if (!voiceMap.has(voiceId))
|
|
493
|
+
voiceMap.set(voiceId, []);
|
|
494
|
+
voiceMap.get(voiceId).push({ notes });
|
|
495
|
+
}
|
|
397
496
|
}
|
|
398
497
|
currentNoteSetMap.clear();
|
|
399
498
|
}
|
|
400
499
|
});
|
|
500
|
+
// Cleanup: push remaining notes
|
|
401
501
|
for (const [sIdx, notes] of currentNoteSetMap.entries()) {
|
|
402
|
-
if (notes.length > 0)
|
|
403
|
-
|
|
502
|
+
if (notes.length > 0) {
|
|
503
|
+
const voiceId = lastVoicePerStaff.get(sIdx) || '1';
|
|
504
|
+
const voiceMap = staffVoices.get(sIdx);
|
|
505
|
+
if (!voiceMap.has(voiceId))
|
|
506
|
+
voiceMap.set(voiceId, []);
|
|
507
|
+
voiceMap.get(voiceId).push({ notes });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// Check if it's a pickup measure if mIdx is 0 and implicit is not set
|
|
511
|
+
if (mIdx === 0 && !isPickup) {
|
|
512
|
+
// Calculate the actual duration of the first staff's first voice
|
|
513
|
+
const vMap0 = staffVoices.get(0);
|
|
514
|
+
const v1 = vMap0.get('1') || vMap0.get(Array.from(vMap0.keys())[0]);
|
|
515
|
+
if (v1) {
|
|
516
|
+
// Check duration of first voice in first staff
|
|
517
|
+
const expectedDivisions = this.currentBeats * (divisions * 4 / this.currentBeatType);
|
|
518
|
+
let totalMeasureDivs = 0;
|
|
519
|
+
const notes = this.querySelectorAll(measureElement, 'note');
|
|
520
|
+
// Find first voice in first staff
|
|
521
|
+
for (const n of notes) {
|
|
522
|
+
if ((this.getText(n, 'staff') || '1') === '1' && (this.getText(n, 'voice') || '1') === (Array.from(vMap0.keys())[0] || '1')) {
|
|
523
|
+
if (this.querySelector(n, 'chord'))
|
|
524
|
+
continue;
|
|
525
|
+
totalMeasureDivs += this.getNumber(n, 'duration') || 0;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (totalMeasureDivs > 0 && totalMeasureDivs < expectedDivisions - 1) {
|
|
529
|
+
isPickup = true;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
404
532
|
}
|
|
405
533
|
for (let i = 0; i < numStaves; i++) {
|
|
534
|
+
const vMap = staffVoices.get(i);
|
|
535
|
+
let voicesIndices = Array.from(vMap.keys()).sort();
|
|
536
|
+
if (voicesIndices.length === 0)
|
|
537
|
+
voicesIndices = ['1'];
|
|
538
|
+
const voices = voicesIndices.map(id => vMap.get(id) || []);
|
|
406
539
|
const measure = {
|
|
407
|
-
voices:
|
|
540
|
+
voices: voices,
|
|
408
541
|
timeSignature: measureTimeSignature,
|
|
409
542
|
keySignature: measureKeySignature,
|
|
410
543
|
isPickup: isPickup || undefined,
|
|
@@ -415,6 +548,30 @@ export class MusicXMLParser {
|
|
|
415
548
|
systemText: i === 0 ? systemText : undefined,
|
|
416
549
|
barlineStyle,
|
|
417
550
|
};
|
|
551
|
+
// Padding with rests if not a pickup
|
|
552
|
+
if (!isPickup) {
|
|
553
|
+
const targetDur = (this.currentBeats * 4) / this.currentBeatType;
|
|
554
|
+
measure.voices = measure.voices.map((v) => {
|
|
555
|
+
const currentDur = v.reduce((sum, ns) => {
|
|
556
|
+
const base = DURATION_VALUES[ns.notes[0].duration];
|
|
557
|
+
return sum + (ns.notes[0].isDotted ? base * 1.5 : base);
|
|
558
|
+
}, 0);
|
|
559
|
+
if (currentDur < targetDur - 0.001) {
|
|
560
|
+
const gap = targetDur - currentDur;
|
|
561
|
+
const rests = decomposeDuration(gap).map((d) => ({
|
|
562
|
+
notes: [
|
|
563
|
+
{
|
|
564
|
+
duration: d.duration,
|
|
565
|
+
isRest: true,
|
|
566
|
+
isDotted: d.isDotted,
|
|
567
|
+
},
|
|
568
|
+
],
|
|
569
|
+
}));
|
|
570
|
+
return [...v, ...rests];
|
|
571
|
+
}
|
|
572
|
+
return v;
|
|
573
|
+
});
|
|
574
|
+
}
|
|
418
575
|
staves[i].measures.push(measure);
|
|
419
576
|
staves[i].clef = staffClefs[i];
|
|
420
577
|
}
|
|
@@ -422,21 +579,21 @@ export class MusicXMLParser {
|
|
|
422
579
|
return staves;
|
|
423
580
|
}
|
|
424
581
|
parseNote(noteElement) {
|
|
425
|
-
const isRest =
|
|
426
|
-
const isGrace =
|
|
582
|
+
const isRest = this.querySelector(noteElement, 'rest') !== null;
|
|
583
|
+
const isGrace = this.querySelector(noteElement, 'grace') !== null;
|
|
427
584
|
const type = this.getText(noteElement, 'type');
|
|
428
585
|
const duration = type ? this.mapDuration(type) : Duration.Quarter;
|
|
429
|
-
const isDotted =
|
|
586
|
+
const isDotted = this.querySelector(noteElement, 'dot') !== null;
|
|
430
587
|
if (isRest)
|
|
431
588
|
return { duration, isRest: true, isDotted };
|
|
432
589
|
let step = 'C', octave = 4, alter = 0;
|
|
433
|
-
const unpitched =
|
|
590
|
+
const unpitched = this.querySelector(noteElement, 'unpitched');
|
|
434
591
|
if (unpitched) {
|
|
435
592
|
step = this.getText(unpitched, 'display-step') || 'C';
|
|
436
593
|
octave = this.getNumber(unpitched, 'display-octave') ?? 4;
|
|
437
594
|
}
|
|
438
595
|
else {
|
|
439
|
-
const pitch =
|
|
596
|
+
const pitch = this.querySelector(noteElement, 'pitch');
|
|
440
597
|
if (!pitch)
|
|
441
598
|
return null;
|
|
442
599
|
step = this.getText(pitch, 'step') || 'C';
|
|
@@ -444,7 +601,7 @@ export class MusicXMLParser {
|
|
|
444
601
|
alter = this.getNumber(pitch, 'alter') ?? 0;
|
|
445
602
|
}
|
|
446
603
|
let midiNumber = this.pitchToMidi(step, octave, alter);
|
|
447
|
-
const instrument =
|
|
604
|
+
const instrument = this.querySelector(noteElement, 'instrument');
|
|
448
605
|
if (instrument) {
|
|
449
606
|
const instId = instrument.getAttribute('id');
|
|
450
607
|
if (instId && this.instrumentPitchMap.has(instId))
|
|
@@ -478,17 +635,17 @@ export class MusicXMLParser {
|
|
|
478
635
|
stemDirection = StemDirection.Up;
|
|
479
636
|
else if (stem === 'down')
|
|
480
637
|
stemDirection = StemDirection.Down;
|
|
481
|
-
const notations =
|
|
638
|
+
const notations = this.querySelector(noteElement, 'notations');
|
|
482
639
|
// Tuplet ratio
|
|
483
640
|
let tuplet = undefined;
|
|
484
|
-
const timeMod =
|
|
641
|
+
const timeMod = this.querySelector(noteElement, 'time-modification');
|
|
485
642
|
if (timeMod) {
|
|
486
643
|
const actual = this.getNumber(timeMod, 'actual-notes') || 3;
|
|
487
644
|
const normal = this.getNumber(timeMod, 'normal-notes') || 2;
|
|
488
645
|
tuplet = { actual, normal, type: 'middle' };
|
|
489
646
|
}
|
|
490
647
|
if (notations) {
|
|
491
|
-
const tupletEl =
|
|
648
|
+
const tupletEl = this.querySelector(notations, 'tuplet');
|
|
492
649
|
if (tupletEl && tuplet) {
|
|
493
650
|
const type = tupletEl.getAttribute('type');
|
|
494
651
|
if (type === 'start')
|
|
@@ -498,7 +655,7 @@ export class MusicXMLParser {
|
|
|
498
655
|
}
|
|
499
656
|
}
|
|
500
657
|
let slur = undefined;
|
|
501
|
-
const slurEl = notations
|
|
658
|
+
const slurEl = notations ? this.querySelector(notations, 'slur') : null;
|
|
502
659
|
if (slurEl) {
|
|
503
660
|
const type = slurEl.getAttribute('type');
|
|
504
661
|
if (type === 'start' || type === 'stop') {
|
|
@@ -510,29 +667,29 @@ export class MusicXMLParser {
|
|
|
510
667
|
slur.direction = 'down';
|
|
511
668
|
}
|
|
512
669
|
}
|
|
513
|
-
const tie =
|
|
670
|
+
const tie = this.querySelector(noteElement, 'tie')?.getAttribute('type') === 'start' || undefined;
|
|
514
671
|
const articulations = [];
|
|
515
|
-
const articEl = notations
|
|
672
|
+
const articEl = notations ? this.querySelector(notations, 'articulations') : null;
|
|
516
673
|
if (articEl) {
|
|
517
|
-
if (
|
|
674
|
+
if (this.querySelector(articEl, 'staccato'))
|
|
518
675
|
articulations.push(Articulation.Staccato);
|
|
519
|
-
if (
|
|
676
|
+
if (this.querySelector(articEl, 'accent'))
|
|
520
677
|
articulations.push(Articulation.Accent);
|
|
521
|
-
if (
|
|
678
|
+
if (this.querySelector(articEl, 'tenuto'))
|
|
522
679
|
articulations.push(Articulation.Tenuto);
|
|
523
|
-
if (
|
|
680
|
+
if (this.querySelector(articEl, 'marcato'))
|
|
524
681
|
articulations.push(Articulation.Marcato);
|
|
525
|
-
if (
|
|
682
|
+
if (this.querySelector(articEl, 'staccatissimo'))
|
|
526
683
|
articulations.push(Articulation.Staccatissimo);
|
|
527
|
-
if (
|
|
684
|
+
if (this.querySelector(articEl, 'caesura'))
|
|
528
685
|
articulations.push(Articulation.Caesura);
|
|
529
|
-
if (
|
|
686
|
+
if (this.querySelector(articEl, 'breath-mark'))
|
|
530
687
|
articulations.push(Articulation.BreathMark);
|
|
531
688
|
}
|
|
532
|
-
if (notations
|
|
689
|
+
if (notations && this.querySelector(notations, 'fermata'))
|
|
533
690
|
articulations.push(Articulation.Fermata);
|
|
534
691
|
let arpeggio = undefined;
|
|
535
|
-
const arpeggiateEl = notations
|
|
692
|
+
const arpeggiateEl = notations ? this.querySelector(notations, 'arpeggiate') : null;
|
|
536
693
|
if (arpeggiateEl) {
|
|
537
694
|
const dir = arpeggiateEl.getAttribute('direction');
|
|
538
695
|
if (dir === 'up')
|
|
@@ -542,23 +699,23 @@ export class MusicXMLParser {
|
|
|
542
699
|
else
|
|
543
700
|
arpeggio = Arpeggio.Normal;
|
|
544
701
|
}
|
|
545
|
-
const ornaments = notations
|
|
702
|
+
const ornaments = notations ? this.querySelector(notations, 'ornaments') : null;
|
|
546
703
|
let ornament = undefined;
|
|
547
704
|
if (ornaments) {
|
|
548
|
-
if (
|
|
705
|
+
if (this.querySelector(ornaments, 'trill-mark'))
|
|
549
706
|
ornament = Ornament.Trill;
|
|
550
|
-
else if (
|
|
707
|
+
else if (this.querySelector(ornaments, 'mordent'))
|
|
551
708
|
ornament = Ornament.Mordent;
|
|
552
|
-
else if (
|
|
709
|
+
else if (this.querySelector(ornaments, 'inverted-mordent'))
|
|
553
710
|
ornament = Ornament.InvertedMordent;
|
|
554
|
-
else if (
|
|
711
|
+
else if (this.querySelector(ornaments, 'turn'))
|
|
555
712
|
ornament = Ornament.Turn;
|
|
556
|
-
else if (
|
|
713
|
+
else if (this.querySelector(ornaments, 'inverted-turn'))
|
|
557
714
|
ornament = Ornament.InvertedTurn;
|
|
558
|
-
else if (
|
|
715
|
+
else if (this.querySelector(ornaments, 'tremolo'))
|
|
559
716
|
ornament = Ornament.Tremolo;
|
|
560
717
|
}
|
|
561
|
-
const technical = notations
|
|
718
|
+
const technical = notations ? this.querySelector(notations, 'technical') : null;
|
|
562
719
|
let bowing = undefined;
|
|
563
720
|
let fingering = undefined;
|
|
564
721
|
let fret = undefined;
|
|
@@ -568,33 +725,33 @@ export class MusicXMLParser {
|
|
|
568
725
|
let hammerOn = undefined;
|
|
569
726
|
let pullOff = undefined;
|
|
570
727
|
if (technical) {
|
|
571
|
-
if (
|
|
728
|
+
if (this.querySelector(technical, 'up-bow'))
|
|
572
729
|
bowing = Bowing.UpBow;
|
|
573
|
-
else if (
|
|
730
|
+
else if (this.querySelector(technical, 'down-bow'))
|
|
574
731
|
bowing = Bowing.DownBow;
|
|
575
|
-
const fingeringEl =
|
|
732
|
+
const fingeringEl = this.querySelector(technical, 'fingering');
|
|
576
733
|
if (fingeringEl)
|
|
577
734
|
fingering = parseInt(fingeringEl.textContent || '0');
|
|
578
|
-
const fretEl =
|
|
735
|
+
const fretEl = this.querySelector(technical, 'fret');
|
|
579
736
|
if (fretEl)
|
|
580
737
|
fret = parseInt(fretEl.textContent || '0');
|
|
581
|
-
const stringEl =
|
|
738
|
+
const stringEl = this.querySelector(technical, 'string');
|
|
582
739
|
if (stringEl) {
|
|
583
740
|
stringNumber = parseInt(stringEl.textContent || '0');
|
|
584
741
|
isStringCircled = true;
|
|
585
742
|
}
|
|
586
|
-
const palmMuteEl =
|
|
743
|
+
const palmMuteEl = this.querySelector(technical, 'palm-mute');
|
|
587
744
|
if (palmMuteEl)
|
|
588
745
|
palmMute = palmMuteEl.getAttribute('type') || 'start';
|
|
589
|
-
const hammerOnEl =
|
|
746
|
+
const hammerOnEl = this.querySelector(technical, 'hammer-on');
|
|
590
747
|
if (hammerOnEl)
|
|
591
748
|
hammerOn = hammerOnEl.getAttribute('type') || 'start';
|
|
592
|
-
const pullOffEl =
|
|
749
|
+
const pullOffEl = this.querySelector(technical, 'pull-off');
|
|
593
750
|
if (pullOffEl)
|
|
594
751
|
pullOff = pullOffEl.getAttribute('type') || 'start';
|
|
595
752
|
}
|
|
596
753
|
let glissando = undefined;
|
|
597
|
-
const glissEl = notations
|
|
754
|
+
const glissEl = notations ? (this.querySelector(notations, 'glissando') || this.querySelector(notations, 'slide')) : null;
|
|
598
755
|
if (glissEl) {
|
|
599
756
|
const type = glissEl.getAttribute('type');
|
|
600
757
|
if (type === 'start' || type === 'stop') {
|
|
@@ -604,13 +761,14 @@ export class MusicXMLParser {
|
|
|
604
761
|
};
|
|
605
762
|
}
|
|
606
763
|
}
|
|
607
|
-
const notesDyn = notations
|
|
608
|
-
const
|
|
764
|
+
const notesDyn = notations ? this.querySelector(notations, 'dynamics') : null;
|
|
765
|
+
const firstChild = notesDyn ? this.getFirstElementChild(notesDyn) : null;
|
|
766
|
+
const dynamic = firstChild?.nodeName.toLowerCase();
|
|
609
767
|
const lyrics = [];
|
|
610
|
-
|
|
768
|
+
this.querySelectorAll(noteElement, 'lyric').forEach((lyricEl) => {
|
|
611
769
|
const text = this.getText(lyricEl, 'text') || '';
|
|
612
770
|
const syllabic = this.getText(lyricEl, 'syllabic');
|
|
613
|
-
const extend =
|
|
771
|
+
const extend = this.querySelector(lyricEl, 'extend') !== null;
|
|
614
772
|
const number = parseInt(lyricEl.getAttribute('number') || '1');
|
|
615
773
|
if (text || extend) {
|
|
616
774
|
lyrics[number - 1] = {
|
|
@@ -648,7 +806,7 @@ export class MusicXMLParser {
|
|
|
648
806
|
};
|
|
649
807
|
}
|
|
650
808
|
parseHarmony(harmonyElement) {
|
|
651
|
-
const root =
|
|
809
|
+
const root = this.querySelector(harmonyElement, 'root');
|
|
652
810
|
if (!root)
|
|
653
811
|
return null;
|
|
654
812
|
const step = this.getText(root, 'root-step');
|
|
@@ -709,11 +867,11 @@ export class MusicXMLParser {
|
|
|
709
867
|
}
|
|
710
868
|
// Parse Frame (Fretboard Diagram)
|
|
711
869
|
let diagram = undefined;
|
|
712
|
-
const frame =
|
|
870
|
+
const frame = this.querySelector(harmonyElement, 'frame');
|
|
713
871
|
if (frame) {
|
|
714
872
|
diagram = this.parseFrame(frame);
|
|
715
873
|
}
|
|
716
|
-
const bass =
|
|
874
|
+
const bass = this.querySelector(harmonyElement, 'bass');
|
|
717
875
|
let bassPart = '';
|
|
718
876
|
if (bass) {
|
|
719
877
|
const bStep = this.getText(bass, 'bass-step');
|
|
@@ -738,11 +896,11 @@ export class MusicXMLParser {
|
|
|
738
896
|
const openStrings = [];
|
|
739
897
|
const mutedStrings = [];
|
|
740
898
|
const openBarres = new Map(); // fret -> startString
|
|
741
|
-
|
|
899
|
+
this.querySelectorAll(frame, 'frame-note').forEach((fn) => {
|
|
742
900
|
const string = this.getNumber(fn, 'string');
|
|
743
901
|
const fret = this.getNumber(fn, 'fret');
|
|
744
902
|
const fingering = this.getText(fn, 'fingering');
|
|
745
|
-
const barre =
|
|
903
|
+
const barre = this.querySelector(fn, 'barre')?.getAttribute('type'); // start/stop
|
|
746
904
|
if (string !== null && fret !== null) {
|
|
747
905
|
if (fret === 0) {
|
|
748
906
|
openStrings.push(string);
|
|
@@ -784,7 +942,7 @@ export class MusicXMLParser {
|
|
|
784
942
|
const repeats = [];
|
|
785
943
|
let volta = undefined;
|
|
786
944
|
let barlineStyle = BarlineStyle.Regular;
|
|
787
|
-
|
|
945
|
+
this.querySelectorAll(measureElement, 'barline').forEach((barline) => {
|
|
788
946
|
const location = barline.getAttribute('location') || 'right';
|
|
789
947
|
const barStyle = this.getText(barline, 'bar-style');
|
|
790
948
|
if (location === 'right') {
|
|
@@ -795,7 +953,7 @@ export class MusicXMLParser {
|
|
|
795
953
|
else if (barStyle === 'dotted')
|
|
796
954
|
barlineStyle = BarlineStyle.Dotted;
|
|
797
955
|
}
|
|
798
|
-
const repeatEl =
|
|
956
|
+
const repeatEl = this.querySelector(barline, 'repeat');
|
|
799
957
|
if (repeatEl) {
|
|
800
958
|
const direction = repeatEl.getAttribute('direction');
|
|
801
959
|
const times = repeatEl.getAttribute('times');
|
|
@@ -804,7 +962,7 @@ export class MusicXMLParser {
|
|
|
804
962
|
times: times ? parseInt(times) : undefined,
|
|
805
963
|
});
|
|
806
964
|
}
|
|
807
|
-
const ending =
|
|
965
|
+
const ending = this.querySelector(barline, 'ending');
|
|
808
966
|
if (ending) {
|
|
809
967
|
const type = ending.getAttribute('type');
|
|
810
968
|
const numberAttr = ending.getAttribute('number') || '1';
|
|
@@ -891,12 +1049,12 @@ export class MusicXMLParser {
|
|
|
891
1049
|
};
|
|
892
1050
|
return map[type] ?? Duration.Quarter;
|
|
893
1051
|
}
|
|
894
|
-
mapClef(sign, line = 0) {
|
|
1052
|
+
mapClef(sign, line = 0, octaveChange = 0) {
|
|
895
1053
|
switch (sign.toUpperCase()) {
|
|
896
1054
|
case 'G':
|
|
897
|
-
return Clef.Treble;
|
|
1055
|
+
return octaveChange === -1 ? Clef.Treble8vaBassa : Clef.Treble;
|
|
898
1056
|
case 'F':
|
|
899
|
-
return Clef.Bass;
|
|
1057
|
+
return octaveChange === -1 ? Clef.Bass8vaBassa : Clef.Bass;
|
|
900
1058
|
case 'C':
|
|
901
1059
|
return line === 4 ? Clef.Tenor : Clef.Alto;
|
|
902
1060
|
case 'PERCUSSION':
|
|
@@ -925,7 +1083,7 @@ export class MusicXMLParser {
|
|
|
925
1083
|
return map[step.toUpperCase()] ?? 0;
|
|
926
1084
|
}
|
|
927
1085
|
getText(element, tagName) {
|
|
928
|
-
const child =
|
|
1086
|
+
const child = this.querySelector(element, tagName);
|
|
929
1087
|
return child?.textContent ?? null;
|
|
930
1088
|
}
|
|
931
1089
|
getNumber(element, tagName) {
|