├── .dockerignore ├── .eslintignore ├── .github ├── ISSUE_TEMPLATE │ └── bug-report.md └── workflows │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode ├── launch.json ├── settings.json └── vexml.code-snippets ├── Dockerfile ├── LICENSE ├── PuppeteerEnvironment.js ├── README.md ├── babel.config.json ├── eslint.config.js ├── globalSetup.js ├── globalTeardown.js ├── jest.config.js ├── jest.setup.js ├── package-lock.json ├── package.json ├── scripts ├── extract-w3c-mnx-examples.js ├── release.js └── resnap.js ├── site ├── index.html ├── public │ └── favicon.ico ├── src │ ├── App.tsx │ ├── components │ │ ├── ConfigForm.tsx │ │ ├── DragUpload.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── EventLogCard.tsx │ │ ├── EventTypeForm.tsx │ │ ├── Select.tsx │ │ ├── SourceDisplay.tsx │ │ ├── SourceForm.tsx │ │ ├── SourceInfo.tsx │ │ ├── SourceWorkspace.tsx │ │ ├── Title.tsx │ │ └── Vexml.tsx │ ├── constants.ts │ ├── examples │ ├── hooks │ │ ├── useJsonLocalStorage.ts │ │ ├── useLocalStorage.ts │ │ ├── useLocalStorageCleanup.ts │ │ ├── useModal.ts │ │ ├── useMusicXML.ts │ │ ├── useNextKey.ts │ │ ├── usePending.ts │ │ ├── useSources.ts │ │ ├── useTimeAgo.ts │ │ ├── useTooltip.ts │ │ └── useWidth.ts │ ├── lib │ │ └── Player.ts │ ├── main.tsx │ ├── style.css │ ├── types.ts │ ├── util │ │ ├── convertFontToBase64.ts │ │ ├── downloadCanvasAsImage.ts │ │ ├── downloadSvgAsImage.ts │ │ ├── errors.ts │ │ ├── getDevice.ts │ │ └── isEqual.ts │ └── vite-env.d.ts └── vite.config.js ├── src ├── README.md ├── components │ ├── README.md │ ├── index.ts │ ├── overlay.ts │ ├── root.ts │ └── simplecursor.ts ├── config.ts ├── data │ ├── README.md │ ├── document.ts │ ├── enums.ts │ ├── index.ts │ └── types.ts ├── debug │ ├── README.md │ ├── consolelogger.ts │ ├── index.ts │ ├── memorylogger.ts │ ├── nooplogger.ts │ ├── performancemonitor.ts │ ├── stopwatch.ts │ └── types.ts ├── elements │ ├── README.md │ ├── eventmappingfactory.ts │ ├── events.ts │ ├── fragment.ts │ ├── index.ts │ ├── locator.ts │ ├── measure.ts │ ├── note.ts │ ├── part.ts │ ├── rest.ts │ ├── score.ts │ ├── stave.ts │ ├── system.ts │ ├── types.ts │ └── voice.ts ├── errors │ ├── README.md │ ├── errors.ts │ └── index.ts ├── events │ ├── README.md │ ├── index.ts │ ├── nativebridge.ts │ ├── topic.ts │ └── types.ts ├── formatting │ ├── README.md │ ├── defaultformatter.ts │ ├── index.ts │ ├── maxmeasureformatter.ts │ ├── monitoredformatter.ts │ ├── panoramicformatter.ts │ └── types.ts ├── index.ts ├── musicxml │ ├── README.md │ ├── accidentalmark.ts │ ├── articulations.ts │ ├── attributes.ts │ ├── backup.ts │ ├── barline.ts │ ├── beam.ts │ ├── bend.ts │ ├── clef.ts │ ├── coda.ts │ ├── defaults.ts │ ├── delayedturn.ts │ ├── direction.ts │ ├── directiontype.ts │ ├── doubletongue.ts │ ├── downbow.ts │ ├── dynamics.ts │ ├── enums.ts │ ├── fermata.ts │ ├── fingering.ts │ ├── fingernails.ts │ ├── forward.ts │ ├── fret.ts │ ├── hammeron.ts │ ├── harmonic.ts │ ├── heel.ts │ ├── index.ts │ ├── invertedmordent.ts │ ├── invertedturn.ts │ ├── key.ts │ ├── lyric.ts │ ├── measure.ts │ ├── measurestyle.ts │ ├── metronome.ts │ ├── mordent.ts │ ├── musicxml.ts │ ├── notations.ts │ ├── note.ts │ ├── octaveshift.ts │ ├── openstring.ts │ ├── ornaments.ts │ ├── part.ts │ ├── pedal.ts │ ├── pluck.ts │ ├── print.ts │ ├── pulloff.ts │ ├── rehearsal.ts │ ├── scorepartwise.ts │ ├── segno.ts │ ├── slide.ts │ ├── slur.ts │ ├── snappizzicato.ts │ ├── stavedetails.ts │ ├── stopped.ts │ ├── symbolic.ts │ ├── tabstring.ts │ ├── tap.ts │ ├── technical.ts │ ├── thumbposition.ts │ ├── tied.ts │ ├── time.ts │ ├── timemodification.ts │ ├── toe.ts │ ├── tremolo.ts │ ├── trillmark.ts │ ├── tripletongue.ts │ ├── tuplet.ts │ ├── turn.ts │ ├── types.ts │ ├── upbow.ts │ ├── wavyline.ts │ ├── wedge.ts │ └── words.ts ├── mxl │ ├── README.md │ ├── container.ts │ ├── index.ts │ ├── mxl.ts │ └── rootfile.ts ├── parsing │ ├── README.md │ ├── index.ts │ ├── musicxml │ │ ├── accidental.ts │ │ ├── annotation.ts │ │ ├── articulation.ts │ │ ├── beam.ts │ │ ├── bend.ts │ │ ├── chord.ts │ │ ├── clef.ts │ │ ├── contexts.ts │ │ ├── conversions.ts │ │ ├── curve.ts │ │ ├── dynamics.ts │ │ ├── enums.ts │ │ ├── eventcalculator.ts │ │ ├── fraction.ts │ │ ├── fragment.ts │ │ ├── fragmentsignature.ts │ │ ├── idprovider.ts │ │ ├── index.ts │ │ ├── jumpgroup.ts │ │ ├── key.ts │ │ ├── measure.ts │ │ ├── metronome.ts │ │ ├── multirest.ts │ │ ├── musicxmlparser.ts │ │ ├── note.ts │ │ ├── octaveshift.ts │ │ ├── part.ts │ │ ├── partsignature.ts │ │ ├── pedal.ts │ │ ├── pitch.ts │ │ ├── rest.ts │ │ ├── score.ts │ │ ├── signature.ts │ │ ├── stave.ts │ │ ├── stavecount.ts │ │ ├── stavelinecount.ts │ │ ├── stavesignature.ts │ │ ├── system.ts │ │ ├── tabposition.ts │ │ ├── textstatemachine.ts │ │ ├── time.ts │ │ ├── tuplet.ts │ │ ├── types.ts │ │ ├── vibrato.ts │ │ ├── voice.ts │ │ └── wedge.ts │ └── mxl │ │ ├── index.ts │ │ └── mxlparser.ts ├── playback │ ├── README.md │ ├── bsearchcursorframelocator.ts │ ├── cursor.ts │ ├── cursorpath.ts │ ├── defaultcursorframe.ts │ ├── duration.ts │ ├── durationrange.ts │ ├── elementdescriber.ts │ ├── emptycursorframe.ts │ ├── fastcursorframelocator.ts │ ├── hintdescriber.ts │ ├── index.ts │ ├── lazycursorstatehintprovider.ts │ ├── legacymeasuresequenceiterator.ts │ ├── scroller.ts │ ├── timeline.ts │ ├── timestamplocator.ts │ └── types.ts ├── render.ts ├── rendering │ ├── README.md │ ├── articulation.ts │ ├── beam.ts │ ├── bend.ts │ ├── budget.ts │ ├── clef.ts │ ├── curve.ts │ ├── debugrect.ts │ ├── document.ts │ ├── dynamics.ts │ ├── ensemble.ts │ ├── enums.ts │ ├── fragment.ts │ ├── gapoverlay.ts │ ├── index.ts │ ├── key.ts │ ├── label.ts │ ├── measure.ts │ ├── nooprenderctx.ts │ ├── note.ts │ ├── octaveshift.ts │ ├── part.ts │ ├── partlabelgroup.ts │ ├── pedal.ts │ ├── pen.ts │ ├── renderer.ts │ ├── renderregistry.ts │ ├── rest.ts │ ├── score.ts │ ├── stave.ts │ ├── system.ts │ ├── systemrendermover.ts │ ├── textmeasurer.ts │ ├── time.ts │ ├── tuplet.ts │ ├── types.ts │ ├── vibrato.ts │ ├── voice.ts │ └── wedge.ts ├── schema │ ├── README.md │ ├── index.ts │ └── t.ts ├── spatial │ ├── README.md │ ├── circle.ts │ ├── collision.ts │ ├── index.ts │ ├── point.ts │ ├── quadtree.ts │ ├── rect.ts │ └── types.ts └── util │ ├── array.ts │ ├── assert.ts │ ├── decorators.ts │ ├── device.ts │ ├── enum.ts │ ├── fraction.ts │ ├── index.ts │ ├── lru.ts │ ├── math.ts │ ├── namedelement.ts │ ├── numberrange.ts │ ├── object.ts │ ├── stack.ts │ ├── value.ts │ └── xml.ts ├── tests ├── __data__ │ ├── lilypond │ │ ├── 01a-Pitches-Pitches.musicxml │ │ ├── 01b-Pitches-Intervals.musicxml │ │ ├── 01c-Pitches-NoVoiceElement.musicxml │ │ ├── 01d-Pitches-Microtones.musicxml │ │ ├── 01e-Pitches-ParenthesizedAccidentals.musicxml │ │ ├── 01f-Pitches-ParenthesizedMicrotoneAccidentals.musicxml │ │ ├── 02a-Rests-Durations.musicxml │ │ ├── 02b-Rests-PitchedRests.musicxml │ │ ├── 02c-Rests-MultiMeasureRests.musicxml │ │ ├── 02d-Rests-Multimeasure-TimeSignatures.musicxml │ │ ├── 02e-Rests-NoType.musicxml │ │ ├── 03a-Rhythm-Durations.musicxml │ │ ├── 03b-Rhythm-Backup.musicxml │ │ ├── 03c-Rhythm-DivisionChange.musicxml │ │ ├── 03d-Rhythm-DottedDurations-Factors.musicxml │ │ ├── 11a-TimeSignatures.musicxml │ │ ├── 11b-TimeSignatures-NoTime.musicxml │ │ ├── 11c-TimeSignatures-CompoundSimple.musicxml │ │ ├── 11d-TimeSignatures-CompoundMultiple.musicxml │ │ ├── 11e-TimeSignatures-CompoundMixed.musicxml │ │ ├── 11f-TimeSignatures-SymbolMeaning.musicxml │ │ ├── 11g-TimeSignatures-SingleNumber.musicxml │ │ ├── 11h-TimeSignatures-SenzaMisura.musicxml │ │ ├── 12a-Clefs.musicxml │ │ ├── 12b-Clefs-NoKeyOrClef.musicxml │ │ ├── 13a-KeySignatures.musicxml │ │ ├── 13b-KeySignatures-ChurchModes.musicxml │ │ ├── 13c-KeySignatures-NonTraditional.musicxml │ │ ├── 13d-KeySignatures-Microtones.musicxml │ │ ├── 14a-StaffDetails-LineChanges.musicxml │ │ ├── 21a-Chord-Basic.musicxml │ │ ├── 21b-Chords-TwoNotes.musicxml │ │ ├── 21c-Chords-ThreeNotesDuration.musicxml │ │ ├── 21d-Chords-SchubertStabatMater.musicxml │ │ ├── 21e-Chords-PickupMeasures.musicxml │ │ ├── 21f-Chord-ElementInBetween.musicxml │ │ ├── 22a-Noteheads.musicxml │ │ ├── 22b-Staff-Notestyles.musicxml │ │ ├── 22c-Noteheads-Chords.musicxml │ │ ├── 22d-Parenthesized-Noteheads.musicxml │ │ ├── 23a-Tuplets.musicxml │ │ ├── 23b-Tuplets-Styles.musicxml │ │ ├── 23c-Tuplet-Display-NonStandard.musicxml │ │ ├── 23d-Tuplets-Nested.musicxml │ │ ├── 23e-Tuplets-Tremolo.musicxml │ │ ├── 23f-Tuplets-DurationButNoBracket.musicxml │ │ ├── 24a-GraceNotes.musicxml │ │ ├── 24b-ChordAsGraceNote.musicxml │ │ ├── 24c-GraceNote-MeasureEnd.musicxml │ │ ├── 24d-AfterGrace.musicxml │ │ ├── 24e-GraceNote-StaffChange.musicxml │ │ ├── 24f-GraceNote-Slur.musicxml │ │ ├── 31a-Directions.musicxml │ │ ├── 31c-MetronomeMarks.musicxml │ │ ├── 32a-Notations.musicxml │ │ ├── 32b-Articulations-Texts.musicxml │ │ ├── 32c-MultipleNotationChildren.musicxml │ │ ├── 32d-Arpeggio.musicxml │ │ ├── 33a-Spanners.musicxml │ │ ├── 33b-Spanners-Tie.musicxml │ │ ├── 33c-Spanners-Slurs.musicxml │ │ ├── 33d-Spanners-OctaveShifts.musicxml │ │ ├── 33e-Spanners-OctaveShifts-InvalidSize.musicxml │ │ ├── 33f-Trill-EndingOnGraceNote.musicxml │ │ ├── 33g-Slur-ChordedNotes.musicxml │ │ ├── 33h-Spanners-Glissando.musicxml │ │ ├── 33i-Ties-NotEnded.musicxml │ │ ├── 41a-MultiParts-Partorder.musicxml │ │ ├── 41b-MultiParts-MoreThan10.musicxml │ │ ├── 41c-StaffGroups.musicxml │ │ ├── 41d-StaffGroups-Nested.musicxml │ │ ├── 41e-StaffGroups-InstrumentNames-Linebroken.musicxml │ │ ├── 41f-StaffGroups-Overlapping.musicxml │ │ ├── 41g-PartNoId.musicxml │ │ ├── 41h-TooManyParts.musicxml │ │ ├── 41i-PartNameDisplay-Override.musicxml │ │ ├── 42a-MultiVoice-TwoVoicesOnStaff-Lyrics.musicxml │ │ ├── 42b-MultiVoice-MidMeasureClefChange.musicxml │ │ ├── 43a-PianoStaff.musicxml │ │ ├── 43b-MultiStaff-DifferentKeys.musicxml │ │ ├── 43c-MultiStaff-DifferentKeysAfterBackup.musicxml │ │ ├── 43d-MultiStaff-StaffChange.musicxml │ │ ├── 43e-Multistaff-ClefDynamics.musicxml │ │ ├── 45a-SimpleRepeat.musicxml │ │ ├── 45b-RepeatWithAlternatives.musicxml │ │ ├── 45c-RepeatMultipleTimes.musicxml │ │ ├── 45d-Repeats-Nested-Alternatives.musicxml │ │ ├── 45e-Repeats-Nested-Alternatives.musicxml │ │ ├── 45f-Repeats-InvalidEndings.musicxml │ │ ├── 45g-Repeats-NotEnded.musicxml │ │ ├── 46a-Barlines.musicxml │ │ ├── 46b-MidmeasureBarline.musicxml │ │ ├── 46c-Midmeasure-Clef.musicxml │ │ ├── 46d-PickupMeasure-ImplicitMeasures.musicxml │ │ ├── 46e-PickupMeasure-SecondVoiceStartsLater.musicxml │ │ ├── 46f-IncompleteMeasures.musicxml │ │ ├── 46g-PickupMeasure-Chordnames-FiguredBass.musicxml │ │ ├── 51b-Header-Quotes.musicxml │ │ ├── 51c-MultipleRights.musicxml │ │ ├── 51d-EmptyTitle.musicxml │ │ ├── 52a-PageLayout.musicxml │ │ ├── 52b-Breaks.musicxml │ │ ├── 61a-Lyrics.musicxml │ │ ├── 61b-MultipleLyrics.musicxml │ │ ├── 61c-Lyrics-Pianostaff.musicxml │ │ ├── 61d-Lyrics-Melisma.musicxml │ │ ├── 61e-Lyrics-Chords.musicxml │ │ ├── 61f-Lyrics-GracedNotes.musicxml │ │ ├── 61g-Lyrics-NameNumber.musicxml │ │ ├── 61h-Lyrics-BeamsMelismata.musicxml │ │ ├── 61i-Lyrics-Chords.musicxml │ │ ├── 61j-Lyrics-Elisions.musicxml │ │ ├── 61k-Lyrics-SpannersExtenders.musicxml │ │ ├── 71a-Chordnames.musicxml │ │ ├── 71c-ChordsFrets.musicxml │ │ ├── 71d-ChordsFrets-Multistaff.musicxml │ │ ├── 71e-TabStaves.musicxml │ │ ├── 71f-AllChordTypes.musicxml │ │ ├── 71g-MultipleChordnames.musicxml │ │ ├── 72a-TransposingInstruments.musicxml │ │ ├── 72b-TransposingInstruments-Full.musicxml │ │ ├── 72c-TransposingInstruments-Change.musicxml │ │ ├── 73a-Percussion.musicxml │ │ ├── 74a-FiguredBass.musicxml │ │ ├── 75a-AccordionRegistrations.musicxml │ │ ├── 99a-Sibelius5-IgnoreBeaming.musicxml │ │ └── 99b-Lyrics-BeamsMelismata-IgnoreBeams.musicxml │ ├── musicxml │ │ ├── ActorPreludeSample.musicxml │ │ ├── BeetAnGeSample.musicxml │ │ ├── Binchois.musicxml │ │ ├── BrahWiMeSample.musicxml │ │ ├── BrookeWestSample.musicxml │ │ ├── DebuMandSample.musicxml │ │ ├── Dichterliebe01.musicxml │ │ ├── Echigo-Jishi.musicxml │ │ ├── FaurReveSample.musicxml │ │ ├── MahlFaGe4Sample.musicxml │ │ ├── MozaChloSample.musicxml │ │ ├── MozaVeilSample.musicxml │ │ ├── MozartPianoSonata.musicxml │ │ ├── MozartTrio.musicxml │ │ ├── Saltarello.musicxml │ │ ├── SchbAvMaSample.musicxml │ │ └── Telemann.musicxml │ ├── vexml │ │ ├── complex_formatting.musicxml │ │ ├── events.musicxml │ │ ├── multi_part_formatting.musicxml │ │ ├── multi_stave_single_part_formatting.musicxml │ │ ├── multi_system_spanners.musicxml │ │ ├── playback_backwards_formatting.musicxml │ │ ├── playback_chords.musicxml │ │ ├── playback_multi_measure.musicxml │ │ ├── playback_multi_part.musicxml │ │ ├── playback_multi_stave.musicxml │ │ ├── playback_multi_system.musicxml │ │ ├── playback_repeat.musicxml │ │ ├── playback_repeat_endings.musicxml │ │ ├── playback_same_note.musicxml │ │ ├── playback_simple.musicxml │ │ ├── prelude_no_1_snippet.musicxml │ │ ├── tabs_basic.musicxml │ │ ├── tabs_bends.musicxml │ │ ├── tabs_dead_notes.musicxml │ │ ├── tabs_fingerings.musicxml │ │ ├── tabs_grace_notes.musicxml │ │ ├── tabs_multi_voice.musicxml │ │ ├── tabs_natural_harmonics.musicxml │ │ ├── tabs_slides.musicxml │ │ ├── tabs_slurs.musicxml │ │ ├── tabs_stroke_direction.musicxml │ │ ├── tabs_taps.musicxml │ │ ├── tabs_ties.musicxml │ │ ├── tabs_vibrato.musicxml │ │ └── tabs_with_stave.musicxml │ ├── w3c-mnx │ │ ├── accidentals.mnx.json │ │ ├── accidentals.musicxml │ │ ├── beam-hooks.mnx.json │ │ ├── beam-hooks.musicxml │ │ ├── beams-across-barlines.mnx.json │ │ ├── beams-across-barlines.musicxml │ │ ├── beams-inner-grace-notes.mnx.json │ │ ├── beams-inner-grace-notes.musicxml │ │ ├── beams-secondary-beam-breaks.mnx.json │ │ ├── beams-secondary-beam-breaks.musicxml │ │ ├── beams.mnx.json │ │ ├── beams.musicxml │ │ ├── dotted-notes.mnx.json │ │ ├── dotted-notes.musicxml │ │ ├── hello-world.mnx.json │ │ ├── hello-world.musicxml │ │ ├── jumps-dal-segno.mnx.json │ │ ├── jumps-dal-segno.musicxml │ │ ├── jumps-ds-al-fine.mnx.json │ │ ├── jumps-ds-al-fine.musicxml │ │ ├── key-signatures.mnx.json │ │ ├── key-signatures.musicxml │ │ ├── multiple-voices.mnx.json │ │ ├── multiple-voices.musicxml │ │ ├── octave-shifts-8va.mnx.json │ │ ├── octave-shifts-8va.musicxml │ │ ├── parts.mnx.json │ │ ├── parts.musicxml │ │ ├── repeats-alternate-endings-advanced.mnx.json │ │ ├── repeats-alternate-endings-advanced.musicxml │ │ ├── repeats-alternate-endings-simple.mnx.json │ │ ├── repeats-alternate-endings-simple.musicxml │ │ ├── repeats-implied-start-repeat.mnx.json │ │ ├── repeats-implied-start-repeat.musicxml │ │ ├── repeats-more-once-repeated.mnx.json │ │ ├── repeats-more-once-repeated.musicxml │ │ ├── repeats.mnx.json │ │ ├── repeats.musicxml │ │ ├── slurs-chords.mnx.json │ │ ├── slurs-chords.musicxml │ │ ├── slurs-incomplete-slurs.mnx.json │ │ ├── slurs-incomplete-slurs.musicxml │ │ ├── slurs-targeting-specific-notes.mnx.json │ │ ├── slurs-targeting-specific-notes.musicxml │ │ ├── slurs.mnx.json │ │ ├── slurs.musicxml │ │ ├── three-note-chord-and-half-rest.mnx.json │ │ ├── three-note-chord-and-half-rest.musicxml │ │ ├── ties.mnx.json │ │ ├── ties.musicxml │ │ ├── time-signatures.mnx.json │ │ ├── time-signatures.musicxml │ │ ├── tuplets.mnx.json │ │ ├── tuplets.musicxml │ │ ├── two-bar-c-major-scale.mnx.json │ │ └── two-bar-c-major-scale.musicxml │ └── w3c-musicxml │ │ ├── accent-element.musicxml │ │ ├── accidental-element.musicxml │ │ ├── accidental-mark-element-notation.musicxml │ │ ├── accidental-mark-element-ornament.musicxml │ │ ├── accordion-high-element.musicxml │ │ ├── accordion-low-element.musicxml │ │ ├── accordion-middle-element.musicxml │ │ ├── accordion-registration-element.musicxml │ │ ├── alter-element-microtones.musicxml │ │ ├── alter-element-semitones.musicxml │ │ ├── alto-clef.musicxml │ │ ├── arpeggiate-element.musicxml │ │ ├── arrow-element.musicxml │ │ ├── arrowhead-element.musicxml │ │ ├── articulations-element.musicxml │ │ ├── artificial-element.musicxml │ │ ├── assess-and-player-elements.musicxml │ │ ├── attributes-element.musicxml │ │ ├── backup-element.musicxml │ │ ├── baritone-c-clef.musicxml │ │ ├── baritone-f-clef.musicxml │ │ ├── barline-element.musicxml │ │ ├── barre-element.musicxml │ │ ├── bass-alter-element.musicxml │ │ ├── bass-clef-down-octave.musicxml │ │ ├── bass-clef.musicxml │ │ ├── bass-separator-element.musicxml │ │ ├── bass-step-element.musicxml │ │ ├── beam-element.musicxml │ │ ├── beat-repeat-element.musicxml │ │ ├── beat-type-element.musicxml │ │ ├── beat-unit-dot-element.musicxml │ │ ├── beat-unit-element.musicxml │ │ ├── beat-unit-tied-element.musicxml │ │ ├── beater-element.musicxml │ │ ├── beats-element.musicxml │ │ ├── bend-element.musicxml │ │ ├── bookmark-element.musicxml │ │ ├── bracket-element.musicxml │ │ ├── brass-bend-element.musicxml │ │ ├── breath-mark-element.musicxml │ │ ├── caesura-element.musicxml │ │ ├── cancel-element.musicxml │ │ ├── capo-element.musicxml │ │ ├── chord-element-multiple-stop.musicxml │ │ ├── chord-element.musicxml │ │ ├── circular-arrow-element.musicxml │ │ ├── coda-element.musicxml │ │ ├── concert-score-and-for-part-elements.musicxml │ │ ├── credit-element.musicxml │ │ ├── credit-image-element.musicxml │ │ ├── credit-symbol-element.musicxml │ │ ├── cue-element.musicxml │ │ ├── damp-all-element.musicxml │ │ ├── damp-element.musicxml │ │ ├── dashes-element.musicxml │ │ ├── degree-alter-element.musicxml │ │ ├── degree-type-element.musicxml │ │ ├── degree-value-element.musicxml │ │ ├── delayed-inverted-turn-element.musicxml │ │ ├── delayed-turn-element.musicxml │ │ ├── detached-legato-element.musicxml │ │ ├── divisions-and-duration-elements.musicxml │ │ ├── doit-element.musicxml │ │ ├── dot-element.musicxml │ │ ├── double-element.musicxml │ │ ├── double-tongue-element.musicxml │ │ ├── down-bow-element.musicxml │ │ ├── effect-element.musicxml │ │ ├── elision-element.musicxml │ │ ├── end-line-element.musicxml │ │ ├── end-paragraph-element.musicxml │ │ ├── ending-element.musicxml │ │ ├── ensemble-element.musicxml │ │ ├── except-voice-element.musicxml │ │ ├── extend-element-figure.musicxml │ │ ├── extend-element-lyric.musicxml │ │ ├── eyeglasses-element.musicxml │ │ ├── f-element.musicxml │ │ ├── falloff-element.musicxml │ │ ├── fermata-element.musicxml │ │ ├── ff-element.musicxml │ │ ├── fff-element.musicxml │ │ ├── ffff-element.musicxml │ │ ├── fffff-element.musicxml │ │ ├── ffffff-element.musicxml │ │ ├── figure-number-element.musicxml │ │ ├── fingering-element-frame.musicxml │ │ ├── fingering-element-notation.musicxml │ │ ├── fingernails-element.musicxml │ │ ├── flip-element.musicxml │ │ ├── forward-element.musicxml │ │ ├── fp-element.musicxml │ │ ├── fret-element-frame.musicxml │ │ ├── fz-element.musicxml │ │ ├── glass-element.musicxml │ │ ├── glissando-element-multiple.musicxml │ │ ├── glissando-element-single.musicxml │ │ ├── glyph-element.musicxml │ │ ├── golpe-element.musicxml │ │ ├── grace-element-appoggiatura.musicxml │ │ ├── grace-element.musicxml │ │ ├── group-abbreviation-display-element.musicxml │ │ ├── group-abbreviation-element.musicxml │ │ ├── group-barline-element.musicxml │ │ ├── group-name-display-element.musicxml │ │ ├── group-time-element.musicxml │ │ ├── grouping-element.musicxml │ │ ├── half-muted-element.musicxml │ │ ├── handbell-element.musicxml │ │ ├── harmon-mute-element.musicxml │ │ ├── harp-pedals-element.musicxml │ │ ├── haydn-element.musicxml │ │ ├── heel-element.musicxml │ │ ├── heel-toe-substitution.musicxml │ │ ├── hole-element.musicxml │ │ ├── hole-type-element.musicxml │ │ ├── humming-element.musicxml │ │ ├── image-element.musicxml │ │ ├── instrument-change-element.musicxml │ │ ├── instrument-link-element.musicxml │ │ ├── interchangeable-element.musicxml │ │ ├── inversion-element.musicxml │ │ ├── inverted-mordent-element.musicxml │ │ ├── inverted-turn-element.musicxml │ │ ├── inverted-vertical-turn-element.musicxml │ │ ├── ipa-element.musicxml │ │ ├── key-element-non-traditional.musicxml │ │ ├── key-element-traditional.musicxml │ │ ├── key-octave-element.musicxml │ │ ├── kind-element.musicxml │ │ ├── laughing-element.musicxml │ │ ├── level-element.musicxml │ │ ├── line-detail-element.musicxml │ │ ├── line-element.musicxml │ │ ├── link-element.musicxml │ │ ├── lyric-element.musicxml │ │ ├── measure-distance-element.musicxml │ │ ├── measure-numbering-element.musicxml │ │ ├── measure-repeat-element.musicxml │ │ ├── membrane-element.musicxml │ │ ├── metal-element.musicxml │ │ ├── metronome-arrows-element.musicxml │ │ ├── metronome-element.musicxml │ │ ├── metronome-note-element.musicxml │ │ ├── metronome-tied-element.musicxml │ │ ├── mezzo-soprano-clef.musicxml │ │ ├── mf-element.musicxml │ │ ├── mordent-element.musicxml │ │ ├── mp-element.musicxml │ │ ├── multiple-rest-element.musicxml │ │ ├── n-element.musicxml │ │ ├── natural-element.musicxml │ │ ├── non-arpeggiate-element.musicxml │ │ ├── normal-dot-element.musicxml │ │ ├── notehead-text-element.musicxml │ │ ├── numeral-alter-element.musicxml │ │ ├── numeral-key-element.musicxml │ │ ├── numeral-root-element.musicxml │ │ ├── octave-element.musicxml │ │ ├── octave-shift-element.musicxml │ │ ├── open-element.musicxml │ │ ├── open-string-element.musicxml │ │ ├── p-element.musicxml │ │ ├── pan-and-elevation-elements.musicxml │ │ ├── part-abbreviation-display-element.musicxml │ │ ├── part-link-element.musicxml │ │ ├── part-name-display-element.musicxml │ │ ├── part-symbol-element.musicxml │ │ ├── pedal-element-lines.musicxml │ │ ├── pedal-element-symbols.musicxml │ │ ├── per-minute-element.musicxml │ │ ├── percussion-clef.musicxml │ │ ├── pf-element.musicxml │ │ ├── pitch-element.musicxml │ │ ├── pitched-element.musicxml │ │ ├── plop-element.musicxml │ │ ├── pluck-element.musicxml │ │ ├── pp-element.musicxml │ │ ├── ppp-element.musicxml │ │ ├── pppp-element.musicxml │ │ ├── ppppp-element.musicxml │ │ ├── pppppp-element.musicxml │ │ ├── pre-bend-element.musicxml │ │ ├── prefix-element.musicxml │ │ ├── principal-voice-element.musicxml │ │ ├── rehearsal-element.musicxml │ │ ├── release-element.musicxml │ │ ├── repeat-element.musicxml │ │ ├── rest-element.musicxml │ │ ├── rf-element.musicxml │ │ ├── rfz-element.musicxml │ │ ├── root-alter-element.musicxml │ │ ├── root-step-element.musicxml │ │ ├── schleifer-element.musicxml │ │ ├── scoop-element.musicxml │ │ ├── scordatura-element.musicxml │ │ ├── score-timewise-element.musicxml │ │ ├── segno-element.musicxml │ │ ├── senza-misura-element.musicxml │ │ ├── sf-element.musicxml │ │ ├── sffz-element.musicxml │ │ ├── sfp-element.musicxml │ │ ├── sfpp-element.musicxml │ │ ├── sfz-element.musicxml │ │ ├── sfzp-element.musicxml │ │ ├── shake-element.musicxml │ │ ├── slash-element.musicxml │ │ ├── slash-type-and-slash-dot-elements.musicxml │ │ ├── slide-element.musicxml │ │ ├── slur-element.musicxml │ │ ├── smear-element.musicxml │ │ ├── snap-pizzicato-element.musicxml │ │ ├── soft-accent-element.musicxml │ │ ├── soprano-clef.musicxml │ │ ├── spiccato-element.musicxml │ │ ├── staccatissimo-element.musicxml │ │ ├── staccato-element.musicxml │ │ ├── staff-distance-element.musicxml │ │ ├── staff-divide-element.musicxml │ │ ├── staff-element.musicxml │ │ ├── staff-lines-element.musicxml │ │ ├── staff-size-element.musicxml │ │ ├── staff-tuning-element.musicxml │ │ ├── staff-type-element.musicxml │ │ ├── staves-element.musicxml │ │ ├── step-element.musicxml │ │ ├── stick-element.musicxml │ │ ├── stick-location-element.musicxml │ │ ├── stopped-element.musicxml │ │ ├── straight-element.musicxml │ │ ├── stress-element.musicxml │ │ ├── string-mute-element-off.musicxml │ │ ├── string-mute-element-on.musicxml │ │ ├── strong-accent-element.musicxml │ │ ├── suffix-element.musicxml │ │ ├── swing-element.musicxml │ │ ├── syllabic-element.musicxml │ │ ├── symbol-element.musicxml │ │ ├── sync-element.musicxml │ │ ├── system-attribute-also-top.musicxml │ │ ├── system-attribute-only-top.musicxml │ │ ├── system-distance-element.musicxml │ │ ├── system-dividers-element.musicxml │ │ ├── tab-clef.musicxml │ │ ├── tap-element.musicxml │ │ ├── technical-element-tablature.musicxml │ │ ├── tenor-clef.musicxml │ │ ├── tenuto-element.musicxml │ │ ├── thumb-position-element.musicxml │ │ ├── tied-element.musicxml │ │ ├── time-modification-element.musicxml │ │ ├── timpani-element.musicxml │ │ ├── toe-element.musicxml │ │ ├── transpose-element.musicxml │ │ ├── treble-clef.musicxml │ │ ├── tremolo-element-double.musicxml │ │ ├── tremolo-element-single.musicxml │ │ ├── trill-mark-element.musicxml │ │ ├── triple-tongue-element.musicxml │ │ ├── tuplet-dot-element.musicxml │ │ ├── tuplet-element-nested.musicxml │ │ ├── tuplet-element-regular.musicxml │ │ ├── turn-element.musicxml │ │ ├── tutorial-apres-un-reve.musicxml │ │ ├── tutorial-chopin-prelude.musicxml │ │ ├── tutorial-chord-symbols.musicxml │ │ ├── tutorial-hello-world.musicxml │ │ ├── tutorial-percussion.musicxml │ │ ├── tutorial-tablature.musicxml │ │ ├── unpitched-element.musicxml │ │ ├── unstress-element.musicxml │ │ ├── up-bow-element.musicxml │ │ ├── vertical-turn-element.musicxml │ │ ├── vocal-tenor-clef.musicxml │ │ ├── voice-element.musicxml │ │ ├── wait-element.musicxml │ │ ├── wavy-line-element.musicxml │ │ ├── wedge-element.musicxml │ │ ├── with-bar-element.musicxml │ │ └── wood-element.musicxml ├── integration │ ├── __image_snapshots__ │ │ ├── 01a-Pitches-Pitches_900px.png │ │ ├── 01b-Pitches-Intervals_900px.png │ │ ├── 01c-Pitches-NoVoiceElement_900px.png │ │ ├── 01d-Pitches-Microtones_900px.png │ │ ├── 01e-Pitches-ParenthesizedAccidentals_900px.png │ │ ├── 01f-Pitches-ParenthesizedMicrotoneAccidentals_900px.png │ │ ├── 02a-Rests-Durations_900px.png │ │ ├── 02b-Rests-PitchedRests_900px.png │ │ ├── 02c-Rests-MultiMeasureRests_900px.png │ │ ├── 02d-Rests-Multimeasure-TimeSignatures_900px.png │ │ ├── 02e-Rests-NoType_900px.png │ │ ├── 03a-Rhythm-Durations_900px.png │ │ ├── 03b-Rhythm-Backup_900px.png │ │ ├── 03c-Rhythm-DivisionChange_900px.png │ │ ├── 03d-Rhythm-DottedDurations-Factors_900px.png │ │ ├── 11a-TimeSignatures_900px.png │ │ ├── 11b-TimeSignatures-NoTime_900px.png │ │ ├── 11c-TimeSignatures-CompoundSimple_900px.png │ │ ├── 11d-TimeSignatures-CompoundMultiple_900px.png │ │ ├── 11e-TimeSignatures-CompoundMixed_900px.png │ │ ├── 11f-TimeSignatures-SymbolMeaning_900px.png │ │ ├── 11g-TimeSignatures-SingleNumber_900px.png │ │ ├── 11h-TimeSignatures-SenzaMisura_900px.png │ │ ├── 12a-Clefs_900px.png │ │ ├── 12b-Clefs-NoKeyOrClef_900px.png │ │ ├── 13a-KeySignatures_900px.png │ │ ├── 13b-KeySignatures-ChurchModes_900px.png │ │ ├── 13c-KeySignatures-NonTraditional_900px.png │ │ ├── 13d-KeySignatures-Microtones_900px.png │ │ ├── 14a-StaffDetails-LineChanges_900px.png │ │ ├── 21a-Chord-Basic_900px.png │ │ ├── 21b-Chords-TwoNotes_900px.png │ │ ├── 21c-Chords-ThreeNotesDuration_900px.png │ │ ├── 21d-Chords-SchubertStabatMater_900px.png │ │ ├── 21e-Chords-PickupMeasures_900px.png │ │ ├── 21f-Chord-ElementInBetween_900px.png │ │ ├── 22a-Noteheads_900px.png │ │ ├── 22c-Noteheads-Chords_900px.png │ │ ├── 22d-Parenthesized-Noteheads_900px.png │ │ ├── 23a-Tuplets_900px.png │ │ ├── 24a-GraceNotes_900px.png │ │ ├── 24b-ChordAsGraceNote_900px.png │ │ ├── 24f-GraceNote-Slur_900px.png │ │ ├── 31a-Directions_900px.png │ │ ├── 31c-MetronomeMarks_900px.png │ │ ├── 32a-Notations_900px.png │ │ ├── 33a-Spanners_900px.png │ │ ├── 33b-Spanners-Tie_900px.png │ │ ├── 33c-Spanners-Slurs_900px.png │ │ ├── 33g-Slur-ChordedNotes_900px.png │ │ ├── 41a-MultiParts-Partorder_900px.png │ │ ├── 43a-PianoStaff_900px.png │ │ ├── 45a-SimpleRepeat_900px.png │ │ ├── 45b-RepeatWithAlternatives_900px.png │ │ ├── 45c-RepeatMultipleTimes_900px.png │ │ ├── 45d-Repeats-Nested-Alternatives_900px.png │ │ ├── 71e-TabStaves_900px.png │ │ ├── accent-element_900px.png │ │ ├── complex_formatting_900px.png │ │ ├── multi_part_formatting_900px.png │ │ ├── multi_stave_single_part_formatting_900px.png │ │ ├── multi_system_spanners_400px.png │ │ ├── prelude_no_1_snippet_900px.png │ │ ├── tabs_basic_900px.png │ │ ├── tabs_bends_900px.png │ │ ├── tabs_dead_notes_900px.png │ │ ├── tabs_grace_notes_900px.png │ │ ├── tabs_multi_voice_900px.png │ │ ├── tabs_natural_harmonics_900px.png │ │ ├── tabs_slides_900px.png │ │ ├── tabs_slurs_900px.png │ │ ├── tabs_stroke_direction_900px.png │ │ ├── tabs_taps_900px.png │ │ ├── tabs_ties_900px.png │ │ ├── tabs_vibrato_900px.png │ │ └── tabs_with_stave_900px.png │ ├── events.test.ts │ ├── helpers.ts │ ├── lilypond.test.ts │ ├── vexml.test.ts │ └── w3c-musicxml.test.ts ├── jest.d.ts └── unit │ ├── musicxml │ ├── accidentalmark.test.ts │ ├── articulations.test.ts │ ├── attributes.test.ts │ ├── backup.test.ts │ ├── barline.test.ts │ ├── beam.test.ts │ ├── clef.test.ts │ ├── defaults.test.ts │ ├── direction.test.ts │ ├── directiontype.test.ts │ ├── enums.test.ts │ ├── fermata.test.ts │ ├── forward.test.ts │ ├── key.test.ts │ ├── lyric.test.ts │ ├── measure.test.ts │ ├── measurestyle.test.ts │ ├── metronome.test.ts │ ├── musicxml.test.ts │ ├── notations.test.ts │ ├── note.test.ts │ ├── octaveshift.test.ts │ ├── ornaments.test.ts │ ├── part.test.ts │ ├── pedal.test.ts │ ├── print.test.ts │ ├── scorepartwise.test.ts │ ├── slur.test.ts │ ├── staffdetails.test.ts │ ├── symbolic.test.ts │ ├── tied.test.ts │ ├── time.test.ts │ ├── timemodification.test.ts │ ├── tuplet.test.ts │ ├── types.test.ts │ ├── value.test.ts │ ├── wavyline.test.ts │ ├── wedge.test.ts │ └── words.test.ts │ ├── mxl │ ├── container.test.ts │ └── rootfile.test.ts │ ├── parsing │ └── musicxml │ │ └── musicxmlparser.test.ts │ ├── playback │ ├── defaultcursorframe.test.ts │ ├── lazycursorstatehintprovider.test.ts │ ├── measuresequenceiterator.test.ts │ └── timeline.test.ts │ ├── spatial │ └── rect.test.ts │ └── util │ ├── array.test.ts │ ├── assert.test.ts │ ├── decorators.test.ts │ ├── enum.test.ts │ ├── fraction.test.ts │ ├── lru.test.ts │ ├── math.test.ts │ ├── namedelement.test.ts │ ├── numberrange.test.ts │ ├── value.test.ts │ └── xml.test.ts ├── tsconfig.json └── tsconfig.package.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .github 2 | .vscode 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Expected behavior** 10 | A clear and concise description of what you expected to happen. 11 | 12 | **Actual behavior** 13 | A clear and concise description of what actually happened. 14 | 15 | **Screenshot** 16 | A screenshot of the `vexml` rendering. 17 | 18 | **Hints** 19 | The more you provide, the faster we can fix it. 20 | 21 | - [ ] Attach the MusicXML file used to produce the bug. 22 | - [ ] Attach the error messages. 23 | - [ ] Include any `vexml` code references where the bug may be. 24 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | premerge: 10 | name: run premerge code checks 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: checkout 14 | uses: actions/checkout@v4 15 | - name: install 16 | run: npm ci 17 | - name: lint 18 | run: npm run lint 19 | - name: check format 20 | run: npm run formatcheck 21 | - name: typecheck 22 | run: npm run typecheck 23 | - name: test 24 | run: npm run test:ci 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .DS_STORE 4 | __diff_output__ 5 | coverage 6 | __tmp_image_snapshots__ 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.xml 2 | *.musicxml 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "bracketSpacing": true, 5 | "arrowParens": "always", 6 | "semi": true, 7 | "printWidth": 120 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Attach to Process", 6 | "type": "node", 7 | "request": "attach", 8 | "port": 9229 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "typescript.preferences.importModuleSpecifier": "relative", 4 | "javascript.preferences.importModuleSpecifier": "relative", 5 | "editor.formatOnSave": true, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": "explicit" 8 | }, 9 | "editor.rulers": [120], 10 | "[dockerfile]": { 11 | "editor.defaultFormatter": "ms-azuretools.vscode-docker" 12 | }, 13 | "[ignore]": { 14 | "editor.defaultFormatter": "foxundermoon.shell-format" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/vexml.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "@/import": { 3 | "description": "Import a module using @ syntax", 4 | "prefix": "@import", 5 | "body": "import * as ${1:module} from '@/${1:module}';" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/puppeteer/puppeteer:23.9.0 2 | 3 | ENV VEXML_CANONICAL_TEST_ENV=true 4 | 5 | # Workaround for puppeteer base image setting USER: 6 | # https://github.com/puppeteer/puppeteer/blob/c764f82c7435bdc10e6a9007892ab8dba111d21c/docker/Dockerfile# 7 | # Also see: https://github.com/nodejs/docker-node/issues/740 8 | USER root 9 | 10 | WORKDIR /vexml 11 | 12 | # Install dependencies. 13 | COPY package.json . 14 | COPY package-lock.json . 15 | RUN npm install 16 | 17 | # Copy config. 18 | COPY jest.config.js . 19 | COPY babel.config.json . 20 | COPY PuppeteerEnvironment.js . 21 | COPY globalSetup.js . 22 | COPY globalTeardown.js . 23 | COPY jest.setup.js . 24 | 25 | # Copy the code needed to run the dev server and tests. 26 | COPY src src 27 | COPY tests tests 28 | 29 | # Run the test by default. 30 | CMD [ "npm", "run", "jest" ] 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jared Johnson & Rodrigo Jorge Vilar de Linares 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PuppeteerEnvironment.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | // Adapted from https://jestjs.io/docs/puppeteer. 3 | 4 | import { TestEnvironment } from 'jest-environment-jsdom'; 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | import puppeteer from 'puppeteer'; 8 | import os from 'os'; 9 | 10 | const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup'); 11 | 12 | export default class PuppeteerEnvironment extends TestEnvironment { 13 | constructor(...args) { 14 | super(...args); 15 | 16 | this.global.structuredClone = globalThis.structuredClone; 17 | } 18 | 19 | async setup() { 20 | await super.setup(); 21 | // get the wsEndpoint 22 | const wsEndpoint = fs.readFileSync(path.join(DIR, 'wsEndpoint'), 'utf8'); 23 | if (!wsEndpoint) { 24 | throw new Error('wsEndpoint not found'); 25 | } 26 | 27 | // connect to puppeteer 28 | this.global.__BROWSER_GLOBAL__ = await puppeteer.connect({ 29 | browserWSEndpoint: wsEndpoint, 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/preset-env", { "targets": { "node": "current" } }], "@babel/preset-typescript"], 3 | "plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]] 4 | } 5 | -------------------------------------------------------------------------------- /globalSetup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | // Adapted from https://jestjs.io/docs/puppeteer. 3 | 4 | import { promises } from 'fs'; 5 | import os from 'os'; 6 | import path from 'path'; 7 | import puppeteer from 'puppeteer'; 8 | 9 | const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup'); 10 | 11 | export default async function () { 12 | const browser = await puppeteer.launch({ 13 | headless: 'new', 14 | // Required for Docker version of Puppeteer 15 | args: ['--no-sandbox', '--disable-setuid-sandbox', '--font-render-hinting=none'], 16 | }); 17 | // store the browser instance so we can teardown it later 18 | // this global is only available in the teardown but not in TestEnvironments 19 | globalThis.__BROWSER_GLOBAL__ = browser; 20 | 21 | // use the file system to expose the wsEndpoint for TestEnvironments 22 | await promises.mkdir(DIR, { recursive: true }); 23 | await promises.writeFile(path.join(DIR, 'wsEndpoint'), browser.wsEndpoint()); 24 | } 25 | -------------------------------------------------------------------------------- /globalTeardown.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | // Adapted from https://jestjs.io/docs/puppeteer. 3 | 4 | import { promises } from 'fs'; 5 | import os from 'os'; 6 | import path from 'path'; 7 | 8 | const DIR = path.join(os.tmpdir(), 'jest_puppeteer_global_setup'); 9 | export default async function () { 10 | // close the browser instance 11 | await globalThis.__BROWSER_GLOBAL__.close(); 12 | 13 | // clean-up the wsEndpoint file 14 | await promises.rm(DIR, { recursive: true, force: true }); 15 | } 16 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | globalSetup: './globalSetup.js', 3 | globalTeardown: './globalTeardown.js', 4 | setupFilesAfterEnv: ['jest-extended/all', './jest.setup.js'], 5 | testEnvironment: './PuppeteerEnvironment.js', 6 | testTimeout: 30000, 7 | moduleNameMapper: { 8 | '@/(.*)': '/src/$1', 9 | }, 10 | reporters: ['default', 'jest-image-snapshot/src/outdated-snapshot-reporter.js'], 11 | }; 12 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { configureToMatchImageSnapshot } from 'jest-image-snapshot'; 3 | 4 | jest.setTimeout(60000); 5 | 6 | const customSnapshotsDir = 7 | process.env.VEXML_CANONICAL_TEST_ENV === 'true' 8 | ? undefined 9 | : path.join(__dirname, 'tests', 'integration', '__tmp_image_snapshots__'); 10 | 11 | const toMatchImageSnapshot = configureToMatchImageSnapshot({ 12 | customSnapshotsDir, 13 | customDiffConfig: { 14 | threshold: 0.01, // 1% 15 | }, 16 | }); 17 | 18 | expect.extend({ toMatchImageSnapshot }); 19 | -------------------------------------------------------------------------------- /scripts/resnap.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | import { execSync } from 'child_process'; 3 | import readline from 'readline'; 4 | 5 | const rl = readline.createInterface({ 6 | input: process.stdin, 7 | output: process.stdout, 8 | }); 9 | 10 | try { 11 | const hasChanges = execSync('git status --porcelain').toString().trim().length > 0; 12 | 13 | rl.question(`WARNING: Any staged git changes will become unstaged. Do you want to continue? (y/n): `, (answer) => { 14 | if (answer === 'y') { 15 | if (hasChanges) { 16 | console.log('Stashing any pending git changes...'); 17 | execSync('git stash'); 18 | } 19 | 20 | console.log('Getting current branch...'); 21 | const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); 22 | 23 | console.log('Checking out origin/master...'); 24 | execSync('git -c advice.detachedHead=false checkout origin/master'); 25 | 26 | console.log('Updating snapshots...'); 27 | execSync('JEST_IMAGE_SNAPSHOT_TRACK_OBSOLETE=1 npm run jest integration --updateSnapshot --silent'); 28 | 29 | console.log(`Checking out ${branch}...`); 30 | execSync(`git checkout ${branch}`); 31 | 32 | if (hasChanges) { 33 | console.log('Unstashing changes...'); 34 | execSync('git stash pop'); 35 | } 36 | } 37 | rl.close(); 38 | }); 39 | } catch (error) { 40 | console.error(`Error executing a command: ${error.message}`); 41 | } 42 | -------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | vexml 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /site/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/site/public/favicon.ico -------------------------------------------------------------------------------- /site/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Title } from './components/Title'; 2 | import { SourceWorkspace } from './components/SourceWorkspace'; 3 | import { DEPRECATED_LOCAL_STORAGE_KEYS } from './constants'; 4 | import { useLocalStorageCleanup } from './hooks/useLocalStorageCleanup'; 5 | import { useSources } from './hooks/useSources'; 6 | import { Source } from './types'; 7 | import './style.css'; 8 | 9 | export const App = () => { 10 | useLocalStorageCleanup(DEPRECATED_LOCAL_STORAGE_KEYS); 11 | 12 | const [sources, setSources] = useSources(); 13 | const onSourcesChange = (sources: Source[]) => { 14 | setSources(sources); 15 | }; 16 | 17 | return ( 18 |
19 | 20 | 21 | <br /> 22 | 23 | <SourceWorkspace sources={sources} onSourcesChange={onSourcesChange} /> 24 | </div> 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /site/src/components/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ErrorInfo, ReactNode } from 'react'; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | } 6 | 7 | interface State { 8 | hasError: boolean; 9 | } 10 | 11 | export class ErrorBoundary extends Component<Props, State> { 12 | state: State = { 13 | hasError: false, 14 | }; 15 | 16 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 17 | static getDerivedStateFromError(error: Error): State { 18 | return { hasError: true }; 19 | } 20 | 21 | componentDidCatch(error: Error, errorInfo: ErrorInfo) { 22 | console.error('Uncaught error:', error, errorInfo); 23 | } 24 | 25 | render() { 26 | if (this.state.hasError) { 27 | return <h1>Something went wrong.</h1>; 28 | } 29 | 30 | return this.props.children; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /site/src/components/Title.tsx: -------------------------------------------------------------------------------- 1 | import { VEXML_VERSION } from '../constants'; 2 | 3 | export const Title = () => { 4 | return ( 5 | <div className="container-fluid py-5"> 6 | <h1 className="display-4">vexml {VEXML_VERSION}</h1> 7 | <p className="fs-4">A MusicXML rendering library</p> 8 | 9 | <a href="https://github.com/stringsync/vexml" target="_blank" rel="noreferrer"> 10 | <img src="https://img.shields.io/github/stars/stringsync/vexml?style=social" alt="GitHub stars" /> 11 | </a> 12 | </div> 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /site/src/constants.ts: -------------------------------------------------------------------------------- 1 | import * as vexml from '@/index'; 2 | 3 | export const VEXML_VERSION = VITE_VEXML_VERSION; 4 | 5 | export const LOCAL_STORAGE_VEXML_SOURCES_KEY = 'vexml:sources'; 6 | 7 | const LOCAL_STORAGE_SAVED_MUSICXML_KEY = 'vexml:saved_musicxml'; 8 | const LOCAL_STORAGE_USE_DEFAULT_MUSICXML_KEY = 'vexml:use_default_musicxml'; 9 | export const DEPRECATED_LOCAL_STORAGE_KEYS = [LOCAL_STORAGE_SAVED_MUSICXML_KEY, LOCAL_STORAGE_USE_DEFAULT_MUSICXML_KEY]; 10 | 11 | export const EXAMPLES = Object.entries(import.meta.glob('./examples/**/*.musicxml', { as: 'raw' })).map( 12 | ([path, get]) => ({ path, get }) 13 | ); 14 | 15 | export const DEFAULT_EXAMPLE_PATH = './examples/lilypond/01a-Pitches-Pitches.musicxml'; 16 | 17 | export const DEFAULT_CONFIG: vexml.Config = { 18 | ...vexml.DEFAULT_CONFIG, 19 | HEIGHT: 300, 20 | }; 21 | -------------------------------------------------------------------------------- /site/src/examples: -------------------------------------------------------------------------------- 1 | ../../tests/__data__ -------------------------------------------------------------------------------- /site/src/hooks/useJsonLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useLocalStorage } from './useLocalStorage'; 3 | 4 | export type Json = string | number | boolean | null | Json[] | { [key: string]: Json }; 5 | 6 | export const useJsonLocalStorage = <T extends Json>( 7 | key: string, 8 | initialValue: T, 9 | typeGuard: (data: unknown) => data is T 10 | ): [value: T, setValue: (value: T) => void] => { 11 | const [stored, setStored] = useLocalStorage(key, JSON.stringify(initialValue)); 12 | 13 | const data = JSON.parse(stored); 14 | const value = typeGuard(data) ? data : initialValue; 15 | 16 | const setValue = useCallback( 17 | (value: T) => { 18 | setStored(JSON.stringify(value)); 19 | }, 20 | [setStored] 21 | ); 22 | 23 | return [value, setValue]; 24 | }; 25 | -------------------------------------------------------------------------------- /site/src/hooks/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | // Adapted from https://usehooks.com/useLocalStorage/ 4 | export const useLocalStorage = ( 5 | key: string, 6 | initialValue: string 7 | ): [value: string, setValue: (value: string) => void] => { 8 | const [stored, setStored] = useState(() => { 9 | const value = window.localStorage.getItem(key); 10 | return typeof value === 'string' ? value : initialValue; 11 | }); 12 | 13 | const setValue = useCallback( 14 | (value: string) => { 15 | window.localStorage.setItem(key, value); 16 | setStored(value); 17 | }, 18 | [key] 19 | ); 20 | 21 | return [stored, setValue]; 22 | }; 23 | -------------------------------------------------------------------------------- /site/src/hooks/useLocalStorageCleanup.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export const useLocalStorageCleanup = (removeKeys: string[]) => { 4 | useEffect(() => { 5 | for (const key of removeKeys) { 6 | localStorage.removeItem(key); 7 | } 8 | }, [removeKeys]); 9 | }; 10 | -------------------------------------------------------------------------------- /site/src/hooks/useModal.ts: -------------------------------------------------------------------------------- 1 | import { Modal } from 'bootstrap'; 2 | import { RefObject, useEffect, useState } from 'react'; 3 | 4 | export const useModal = (ref: RefObject<Element>) => { 5 | const [modal, setModal] = useState<Modal>(() => new NoopModal()); 6 | 7 | useEffect(() => { 8 | const element = ref.current; 9 | if (!element) { 10 | return; 11 | } 12 | 13 | const modal = new Modal(element); 14 | setModal(modal); 15 | 16 | return () => { 17 | modal.dispose(); 18 | setModal(new NoopModal()); 19 | }; 20 | }, [ref]); 21 | 22 | return modal; 23 | }; 24 | 25 | class NoopModal implements Modal { 26 | dispose(): void {} 27 | handleUpdate(): void {} 28 | hide(): void {} 29 | show(): void {} 30 | toggle(): void {} 31 | } 32 | -------------------------------------------------------------------------------- /site/src/hooks/useNextKey.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react'; 2 | 3 | export const useNextKey = (prefix: string) => { 4 | const index = useRef(0); 5 | const nextKey = useCallback(() => `${prefix}-${index.current++}`, [prefix]); 6 | return nextKey; 7 | }; 8 | -------------------------------------------------------------------------------- /site/src/hooks/usePending.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | export const usePending = () => { 4 | const [count, setCount] = useState(0); 5 | 6 | const isPending = count > 0; 7 | 8 | const withPending = useCallback(async (task: () => void | Promise<void>) => { 9 | setCount((count) => count + 1); 10 | try { 11 | await task(); 12 | } finally { 13 | setCount((count) => count - 1); 14 | } 15 | }, []); 16 | 17 | return [isPending, withPending] as const; 18 | }; 19 | -------------------------------------------------------------------------------- /site/src/hooks/useTimeAgo.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const useTimeAgo = (since: Date) => { 4 | const [timeAgo, setTimeAgo] = useState<string>(() => getTimeAgo(0)); 5 | 6 | useEffect(() => { 7 | const handle = setInterval(() => { 8 | const dt = Date.now() - since.getTime(); 9 | const nextTimeAgo = getTimeAgo(dt); 10 | setTimeAgo(nextTimeAgo); 11 | }, 60000); 12 | return () => { 13 | clearInterval(handle); 14 | }; 15 | }, [since]); 16 | 17 | return timeAgo; 18 | }; 19 | 20 | const getTimeAgo = (dt: number) => { 21 | if (dt < 60 * 1000) { 22 | return 'just now'; 23 | } else if (dt < 60 * 60 * 1000) { 24 | const minutes = Math.floor(dt / (60 * 1000)); 25 | return `${minutes} minute${minutes === 1 ? '' : 's'} ago`; 26 | } else if (dt < 24 * 60 * 60 * 1000) { 27 | const hours = Math.floor(dt / (60 * 60 * 1000)); 28 | return `${hours} hour${hours === 1 ? '' : 's'} ago`; 29 | } else { 30 | const days = Math.floor(dt / (24 * 60 * 60 * 1000)); 31 | return `${days} day${days === 1 ? '' : 's'} ago`; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /site/src/hooks/useTooltip.ts: -------------------------------------------------------------------------------- 1 | import { Tooltip } from 'bootstrap'; 2 | import { RefObject, useEffect } from 'react'; 3 | 4 | export type TooltipPlacement = 'auto' | 'top' | 'bottom' | 'left' | 'right'; 5 | 6 | export const useTooltip = (ref: RefObject<Element>, placement: TooltipPlacement, text: string) => { 7 | useEffect(() => { 8 | const element = ref.current; 9 | if (!element) { 10 | return; 11 | } 12 | 13 | const tooltip = new Tooltip(element, { 14 | title: text, 15 | placement, 16 | }); 17 | 18 | return () => { 19 | tooltip.dispose(); 20 | }; 21 | }, [ref, text, placement]); 22 | }; 23 | -------------------------------------------------------------------------------- /site/src/hooks/useWidth.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useLayoutEffect, useState } from 'react'; 2 | 3 | export const useWidth = (elementRef: RefObject<HTMLElement>): number => { 4 | const [width, setWidth] = useState(0); 5 | 6 | useLayoutEffect(() => { 7 | const element = elementRef.current; 8 | if (!element) { 9 | return; 10 | } 11 | 12 | setWidth(element.clientWidth); 13 | 14 | const resizeObserver = new ResizeObserver((entries) => { 15 | setWidth(entries[0].contentRect.width); 16 | }); 17 | 18 | resizeObserver.observe(element); 19 | 20 | return () => { 21 | resizeObserver.disconnect(); 22 | }; 23 | }, [elementRef]); 24 | 25 | return width; 26 | }; 27 | -------------------------------------------------------------------------------- /site/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { App } from './App'; 4 | 5 | import './style.css'; 6 | import 'bootstrap/dist/css/bootstrap.css'; 7 | 8 | ReactDOM.createRoot(document.getElementById('root')!).render( 9 | <React.StrictMode> 10 | <App /> 11 | </React.StrictMode> 12 | ); 13 | -------------------------------------------------------------------------------- /site/src/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Bravura'; 3 | src: url('https://cdn.jsdelivr.net/npm/vexflow-fonts@1.0.6/bravura/Bravura_1.392.otf') format('opentype'); 4 | } 5 | 6 | /* Adapted from https://getbootstrap.com/docs/5.3/customize/components/#creating-your-own */ 7 | .callout { 8 | padding: 1.25rem; 9 | margin-top: 1.25rem; 10 | margin-bottom: 1.25rem; 11 | color: var(--bd-callout-color, inherit); 12 | background-color: var(--bd-callout-bg, var(--bs-gray-100)); 13 | border-left: 0.25rem solid var(--bd-callout-border, var(--bs-gray-300)); 14 | } 15 | 16 | .btn-ghost { 17 | opacity: 0.5; 18 | } 19 | 20 | .btn-ghost:hover { 21 | opacity: 1; 22 | } 23 | -------------------------------------------------------------------------------- /site/src/types.ts: -------------------------------------------------------------------------------- 1 | import * as vexml from '@/index'; 2 | 3 | /** A MusicXML data source. */ 4 | export type Source = 5 | | { 6 | type: 'local'; 7 | musicXML: string; 8 | config: vexml.Config; 9 | } 10 | | { 11 | type: 'remote'; 12 | url: string; 13 | config: vexml.Config; 14 | } 15 | | { 16 | type: 'example'; 17 | path: string; 18 | config: vexml.Config; 19 | }; 20 | 21 | /** A wrapper for keying values. */ 22 | export type Keyed<T> = { 23 | key: string; 24 | value: T; 25 | }; 26 | -------------------------------------------------------------------------------- /site/src/util/convertFontToBase64.ts: -------------------------------------------------------------------------------- 1 | export async function convertFontToBase64(fontUrl: string) { 2 | const response = await fetch(fontUrl); 3 | const blob = await response.blob(); 4 | 5 | return new Promise<string>((resolve) => { 6 | const reader = new FileReader(); 7 | reader.onloadend = () => { 8 | // Convert blob to base64 9 | const base64data = reader.result as string; 10 | resolve(base64data.split(',')[1]); // Split to remove data URL prefix and pass only the base64 string to the callback 11 | }; 12 | reader.readAsDataURL(blob); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /site/src/util/downloadCanvasAsImage.ts: -------------------------------------------------------------------------------- 1 | export function downloadCanvasAsImage(canvas: HTMLCanvasElement, imageName: string) { 2 | // Create a new canvas to draw the image with a white background 3 | const newCanvas = document.createElement('canvas'); 4 | newCanvas.width = canvas.width; 5 | newCanvas.height = canvas.height; 6 | const context = newCanvas.getContext('2d')!; 7 | 8 | // Fill the new canvas with a white background 9 | context.fillStyle = 'white'; 10 | context.fillRect(0, 0, newCanvas.width, newCanvas.height); 11 | 12 | // Draw the original canvas onto the new canvas 13 | context.drawImage(canvas, 0, 0); 14 | 15 | // Convert the new canvas to a data URL 16 | const dataUrl = newCanvas.toDataURL('image/png'); 17 | 18 | // Create a link element for downloading 19 | const link = document.createElement('a'); 20 | document.body.appendChild(link); // Firefox requires the link to be in the body 21 | link.setAttribute('href', dataUrl); 22 | link.setAttribute('download', imageName); 23 | link.click(); 24 | document.body.removeChild(link); // Remove the link when done 25 | } 26 | -------------------------------------------------------------------------------- /site/src/util/errors.ts: -------------------------------------------------------------------------------- 1 | export const wrap = (e: any): Error => (e instanceof Error ? e : new Error(String(e))); 2 | -------------------------------------------------------------------------------- /site/src/util/isEqual.ts: -------------------------------------------------------------------------------- 1 | export const isEqual = (obj1: any, obj2: any): boolean => { 2 | // Check if both objects are null or undefined 3 | if (obj1 === null || obj1 === undefined || obj2 === null || obj2 === undefined) { 4 | return obj1 === obj2; 5 | } 6 | 7 | // Check if both objects are of the same type 8 | if (typeof obj1 !== typeof obj2) { 9 | return false; 10 | } 11 | 12 | // Check if both objects are arrays 13 | if (Array.isArray(obj1) && Array.isArray(obj2)) { 14 | if (obj1.length !== obj2.length) { 15 | return false; 16 | } 17 | 18 | for (let i = 0; i < obj1.length; i++) { 19 | if (!isEqual(obj1[i], obj2[i])) { 20 | return false; 21 | } 22 | } 23 | 24 | return true; 25 | } 26 | 27 | // Check if both objects are objects 28 | if (typeof obj1 === 'object' && typeof obj2 === 'object') { 29 | const keys1 = Object.keys(obj1); 30 | const keys2 = Object.keys(obj2); 31 | 32 | if (keys1.length !== keys2.length) { 33 | return false; 34 | } 35 | 36 | for (const key of keys1) { 37 | if (!isEqual(obj1[key], obj2[key])) { 38 | return false; 39 | } 40 | } 41 | 42 | return true; 43 | } 44 | 45 | // Check if both objects are primitive values 46 | return obj1 === obj2; 47 | }; 48 | -------------------------------------------------------------------------------- /site/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="vite/client" /> 2 | 3 | declare const VITE_VEXML_VERSION: string; 4 | -------------------------------------------------------------------------------- /site/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'path'; 4 | 5 | export default defineConfig({ 6 | root: __dirname, 7 | plugins: [react()], 8 | base: '/', 9 | define: { 10 | VITE_VEXML_VERSION: JSON.stringify(process.env.npm_package_version), 11 | }, 12 | resolve: { 13 | preserveSymlinks: true, 14 | alias: { 15 | '@': path.resolve(__dirname, '..', 'src'), 16 | }, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # src 2 | 3 | `vexml` is composed of: 4 | 5 | - [components](./components/README.md): Wraps simple HTML operations. 6 | - [data](./data/README.md): Declares the data input for the rendering engine. 7 | - [debug](./debug/README.md): Provides basic debugging utilities. 8 | - [elements](./elements/README.md): Wraps the rendering engine output. 9 | - [errors](./errors/README.md): Houses vexml errors. 10 | - [events](./events/README.md): Provides utilities for pubsub-like communication. 11 | - [formatting](./formatting/README.md): Formats data documents to different schemes. 12 | - [musicxml](./musicxml/README.md): Makes MusicXML data more ergonomic. 13 | - [mxl](./mxl/README.md): Makes MXL data more ergonomic. 14 | - [parsing](./parsing/README.md): Transforms different music encodings to a data document. 15 | - [playback](./playback/README.md): Provide data structures for playing a rendering. 16 | - [rendering](./rendering/README.md): Transforms a data Document into `vexflow` objects. 17 | - [schema](./schema/README.md): Reifies TypeScript types. 18 | - [spatial](./spatial/README.md): Provides data structures for spatial algorithms. 19 | - `util`: Miscellaneous functionality. 20 | 21 | - [config.ts](./config.ts): Declares the central configuration. 22 | - [render.ts](./render.ts): Orchestrates vexml components to render. 23 | -------------------------------------------------------------------------------- /src/components/README.md: -------------------------------------------------------------------------------- 1 | # components 2 | 3 | ## Intent 4 | 5 | ### Goals 6 | 7 | - **DO** insulate the rest of vexml from low-level HTML management. 8 | 9 | ### Non-goals 10 | 11 | - **DO NOT** be concerned with the actual vexflow rendering. 12 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './root'; 2 | export * from './simplecursor'; 3 | -------------------------------------------------------------------------------- /src/components/overlay.ts: -------------------------------------------------------------------------------- 1 | /** A component that covers a parent component. */ 2 | export class Overlay { 3 | private element: HTMLDivElement; 4 | 5 | private constructor(element: HTMLDivElement) { 6 | this.element = element; 7 | } 8 | 9 | static render(parent: HTMLElement) { 10 | const element = document.createElement('div'); 11 | element.classList.add('vexml-overlay'); 12 | element.style.position = 'absolute'; 13 | element.style.top = '0'; 14 | element.style.left = '0'; 15 | element.style.width = '100%'; 16 | element.style.height = '100%'; 17 | 18 | parent.append(element); 19 | 20 | return new Overlay(element); 21 | } 22 | 23 | getElement(): HTMLDivElement { 24 | return this.element; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/simplecursor.ts: -------------------------------------------------------------------------------- 1 | const STRINGSYNC_RED = 'rgba(252, 53, 76, 0.8)'; 2 | 3 | export class SimpleCursor { 4 | private element: HTMLElement; 5 | 6 | private constructor(element: HTMLElement) { 7 | this.element = element; 8 | } 9 | 10 | static render(parent: HTMLElement, color = STRINGSYNC_RED) { 11 | const element = document.createElement('div'); 12 | element.classList.add('vexml-cursor'); 13 | element.style.display = 'block'; 14 | element.style.position = 'absolute'; 15 | element.style.backgroundColor = color; 16 | 17 | parent.insertBefore(element, parent.firstChild); 18 | 19 | return new SimpleCursor(element); 20 | } 21 | 22 | /** Moves the cursor's position to the given rect. */ 23 | update(opts: { x: number; y: number; w: number; h: number }) { 24 | this.element.style.left = `${opts.x}px`; 25 | this.element.style.top = `${opts.y}px`; 26 | this.element.style.width = `${opts.w}px`; 27 | this.element.style.height = `${opts.h}px`; 28 | } 29 | 30 | remove() { 31 | this.element.remove(); 32 | } 33 | 34 | show() { 35 | this.element.style.display = 'block'; 36 | } 37 | 38 | hide() { 39 | this.element.style.display = 'none'; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/data/README.md: -------------------------------------------------------------------------------- 1 | # data 2 | 3 | ## Intent 4 | 5 | ### Goals 6 | 7 | - **DO** define data structures for handling musical data. 8 | - **DO** provide utilities for manipulating and accessing data. 9 | - **DO** ensure data integrity and consistency. 10 | 11 | ### Non-goals 12 | 13 | - **DO NOT** handle rendering or presentation logic. 14 | - **DO NOT** provide complex data transformation logic. 15 | - **DO NOT** manage external data sources or APIs. 16 | -------------------------------------------------------------------------------- /src/data/index.ts: -------------------------------------------------------------------------------- 1 | export * from './enums'; 2 | export * from './types'; 3 | export * from './document'; 4 | -------------------------------------------------------------------------------- /src/debug/README.md: -------------------------------------------------------------------------------- 1 | # components 2 | 3 | ## Intent 4 | 5 | ### Goals 6 | 7 | - **DO** provide context-agnostic tools for debugging. 8 | 9 | ### Non-goals 10 | 11 | - **DO NOT** perform vexml logic. 12 | -------------------------------------------------------------------------------- /src/debug/consolelogger.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LogLevel } from './types'; 2 | 3 | export class ConsoleLogger implements Logger { 4 | constructor(private levels: LogLevel[] = ['debug', 'info', 'warn', 'error']) {} 5 | 6 | debug(message: string, meta?: Record<string, any>): void { 7 | if (this.levels.includes('debug')) { 8 | console.debug(this.toCompleteMessage(message, meta)); 9 | } 10 | } 11 | 12 | info(message: string, meta?: Record<string, any>): void { 13 | if (this.levels.includes('info')) { 14 | console.log(this.toCompleteMessage(message, meta)); 15 | } 16 | } 17 | 18 | warn(message: string, meta?: Record<string, any>): void { 19 | if (this.levels.includes('warn')) { 20 | console.warn(this.toCompleteMessage(message, meta)); 21 | } 22 | } 23 | 24 | error(message: string, meta?: Record<string, any>): void { 25 | if (this.levels.includes('error')) { 26 | console.error(this.toCompleteMessage(message, meta)); 27 | } 28 | } 29 | 30 | private toCompleteMessage(message: string, meta?: Record<string, any>): string { 31 | if (meta) { 32 | return `[vexml] ${message} ${Object.entries(meta) 33 | .map(([key, value]) => `${key}=${value}`) 34 | .join(' ')}`; 35 | } 36 | return `[vexml] ${message}`; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/debug/index.ts: -------------------------------------------------------------------------------- 1 | export * from './consolelogger'; 2 | export * from './nooplogger'; 3 | export * from './memorylogger'; 4 | export * from './types'; 5 | export * from './stopwatch'; 6 | export * from './performancemonitor'; 7 | -------------------------------------------------------------------------------- /src/debug/memorylogger.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LogLevel } from './types'; 2 | 3 | export type MemoryLog = { level: LogLevel; message: string; meta?: Record<string, string> }; 4 | 5 | export class MemoryLogger implements Logger { 6 | private logs = new Array<MemoryLog>(); 7 | 8 | getLogs(): MemoryLog[] { 9 | return this.logs; 10 | } 11 | 12 | debug(message: string, meta?: Record<string, any>): void { 13 | this.logs.push({ level: 'debug', message, meta: { callsite: this.getCallsite(), ...meta } }); 14 | } 15 | 16 | info(message: string, meta?: Record<string, any>): void { 17 | this.logs.push({ level: 'info', message, meta: { callsite: this.getCallsite(), ...meta } }); 18 | } 19 | 20 | warn(message: string, meta?: Record<string, any>): void { 21 | this.logs.push({ level: 'warn', message, meta: { callsite: this.getCallsite(), ...meta } }); 22 | } 23 | 24 | error(message: string, meta?: Record<string, any>): void { 25 | this.logs.push({ level: 'error', message, meta: { callsite: this.getCallsite(), ...meta } }); 26 | } 27 | 28 | private getCallsite(): string { 29 | const stack = new Error().stack; 30 | if (!stack) return ''; 31 | const stackLines = stack.split('\n'); 32 | // Return the third line which is the callsite 33 | return stackLines[3] ? stackLines[3].trim() : ''; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/debug/nooplogger.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Logger } from './types'; 3 | 4 | export class NoopLogger implements Logger { 5 | debug(message: string, meta?: Record<string, any>): void {} 6 | info(message: string, meta?: Record<string, any>): void {} 7 | warn(message: string, meta?: Record<string, any>): void {} 8 | error(message: string, meta?: Record<string, any>): void {} 9 | } 10 | -------------------------------------------------------------------------------- /src/debug/performancemonitor.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from './types'; 2 | 3 | export class PerformanceMonitor { 4 | constructor(private log: Logger, private thresholdMs: number) {} 5 | 6 | check(elapsedMs: number, meta?: Record<string, any>): void { 7 | if (elapsedMs >= this.thresholdMs) { 8 | this.log.warn(`[SLOW WARNING] ${this.inferMethodName()} took ${this.getElapsedStr(elapsedMs)}`, meta); 9 | } 10 | } 11 | 12 | private inferMethodName(): string { 13 | try { 14 | return ( 15 | new Error().stack 16 | ?.split('\n') 17 | .at(3) 18 | ?.trimStart() 19 | .replace('at ', '') 20 | ?.match(/(.+)\s/) 21 | ?.at(0) 22 | ?.trimEnd() ?? '<unknown>' 23 | ); 24 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 25 | } catch (e) { 26 | // Fallback if stack parsing fails 27 | } 28 | return '<unknown>'; 29 | } 30 | 31 | private getElapsedStr(elapsedMs: number): string { 32 | return elapsedMs > 1 ? `${Math.round(elapsedMs)}ms` : `${elapsedMs.toFixed(3)}ms`; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/debug/stopwatch.ts: -------------------------------------------------------------------------------- 1 | export class Stopwatch { 2 | private start = performance.now(); 3 | 4 | private constructor() {} 5 | 6 | static start(): Stopwatch { 7 | return new Stopwatch(); 8 | } 9 | 10 | lap(): number { 11 | const now = performance.now(); 12 | const result = now - this.start; 13 | this.start = now; 14 | return result; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/debug/types.ts: -------------------------------------------------------------------------------- 1 | export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; 2 | 3 | export interface Logger { 4 | debug(message: string, meta?: Record<string, any>): void; 5 | info(message: string, meta?: Record<string, any>): void; 6 | warn(message: string, meta?: Record<string, any>): void; 7 | error(message: string, meta?: Record<string, any>): void; 8 | } 9 | -------------------------------------------------------------------------------- /src/elements/README.md: -------------------------------------------------------------------------------- 1 | # elements 2 | 3 | ## Intent 4 | 5 | ### Goals 6 | 7 | - **DO** provide a public interface for consuming the output from the rendering engine. 8 | - **DO** draw vexflow objects to a canvas or SVG. 9 | - **DO** expose hooks for vexml users to listen to events. 10 | 11 | ### Non-goals 12 | 13 | - **DO NOT** mutate the rendering output data. 14 | -------------------------------------------------------------------------------- /src/elements/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './score'; 3 | export * from './system'; 4 | export * from './measure'; 5 | export * from './fragment'; 6 | export * from './part'; 7 | export * from './stave'; 8 | export * from './voice'; 9 | export * from './note'; 10 | export * from './rest'; 11 | -------------------------------------------------------------------------------- /src/errors/README.md: -------------------------------------------------------------------------------- 1 | # errors 2 | 3 | ## Intent 4 | 5 | ### Goals 6 | 7 | - **DO** Define a set of error classes for handling different error scenarios. 8 | - **DO** Provide meaningful error messages and context. 9 | - **DO** Ensure errors are easily catchable and debuggable. 10 | 11 | ### Non-goals 12 | 13 | - **DO NOT** Handle logging or reporting of errors. 14 | - **DO NOT** Provide custom error handling logic. 15 | - **DO NOT** Manage error recovery mechanisms. 16 | -------------------------------------------------------------------------------- /src/errors/errors.ts: -------------------------------------------------------------------------------- 1 | export type VexmlErrorCode = 'GENERIC_ERROR' | 'DOCUMENT_ERROR' | 'PARSE_ERROR'; 2 | 3 | /** A generic vexml error. */ 4 | export class VexmlError extends Error { 5 | public readonly code: VexmlErrorCode; 6 | 7 | constructor(message: string, code: VexmlErrorCode = 'GENERIC_ERROR') { 8 | super(message); 9 | this.name = 'VexmlError'; 10 | this.code = code; 11 | } 12 | } 13 | 14 | /** An error thrown when attempting to mutate the document. */ 15 | export class DocumentError extends VexmlError { 16 | constructor(message: string) { 17 | super(message, 'DOCUMENT_ERROR'); 18 | this.name = 'DocumentError'; 19 | } 20 | } 21 | 22 | /** An error thrown during the parsing process. */ 23 | export class ParseError extends VexmlError { 24 | constructor(message: string) { 25 | super(message, 'PARSE_ERROR'); 26 | this.name = 'ParseError'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors'; 2 | -------------------------------------------------------------------------------- /src/events/README.md: -------------------------------------------------------------------------------- 1 | # events 2 | 3 | ## Intent 4 | 5 | ### Goals 6 | 7 | - **DO** convert (x,y) points to vexflow elements in the rendering. 8 | - **DO** provide a single event listener on the SVG for handling user interactions. 9 | - **DO** connect the rendering output to user interactions. 10 | 11 | ### Non-goals 12 | 13 | - **DO NOT** depend on the MusicXML document to make calculations. 14 | - **DO NOT** declare events — other modules should declare the events they need. 15 | 16 | ## Design 17 | 18 | vexml uses a single event listener for an entire score. See https://github.com/stringsync/vexml/issues/159#issuecomment-2144005865. 19 | -------------------------------------------------------------------------------- /src/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from './topic'; 2 | export * from './types'; 3 | export * from './nativebridge'; 4 | -------------------------------------------------------------------------------- /src/events/topic.ts: -------------------------------------------------------------------------------- 1 | import { EventListener, AnyEventMap } from './types'; 2 | 3 | export type Subscription<T extends AnyEventMap, N extends keyof T> = { 4 | id: number; 5 | name: N; 6 | listener: EventListener<T[N]>; 7 | }; 8 | 9 | /** Class that tracks pubsub subscribers. */ 10 | export class Topic<T extends AnyEventMap> { 11 | private id = 0; 12 | private subscriptions = new Array<Subscription<T, keyof T>>(); 13 | 14 | hasSubscribers<N extends keyof T>(name: N): boolean { 15 | return this.subscriptions.some((s) => s.name === name); 16 | } 17 | 18 | publish<N extends keyof T>(name: N, payload: T[N]): void { 19 | this.subscriptions.filter((s) => s.name === name).forEach((s) => s.listener(payload)); 20 | } 21 | 22 | subscribe<N extends keyof T>(name: N, listener: EventListener<T[N]>): number { 23 | const id = this.id++; 24 | this.subscriptions.push({ id, name, listener: listener as EventListener<T[keyof T]> }); 25 | return id; 26 | } 27 | 28 | unsubscribe(id: number): Subscription<T, keyof T> | null { 29 | const subscription = this.subscriptions.find((s) => s.id === id) ?? null; 30 | this.subscriptions = this.subscriptions.filter((s) => s.id !== id); 31 | return subscription; 32 | } 33 | 34 | unsubscribeAll(): void { 35 | this.subscriptions = []; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/events/types.ts: -------------------------------------------------------------------------------- 1 | export type AnyEventMap = { [eventName: string]: any }; 2 | 3 | export type EventListener<E> = E extends undefined ? () => void : (event: E) => void; 4 | -------------------------------------------------------------------------------- /src/formatting/README.md: -------------------------------------------------------------------------------- 1 | # formatting 2 | 3 | ## Intent 4 | 5 | ### Goals 6 | 7 | - **DO** provide default mechanisms for restructuring a vexml data document. 8 | 9 | ### Non-goals 10 | 11 | - **DO NOT** make grannular render-time formatting decisions (e.g. where to render a part label). See [ensemble.ts](/src/rendering/ensemble.ts). 12 | -------------------------------------------------------------------------------- /src/formatting/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './panoramicformatter'; 3 | export * from './defaultformatter'; 4 | export * from './maxmeasureformatter'; 5 | export * from './monitoredformatter'; 6 | -------------------------------------------------------------------------------- /src/formatting/maxmeasureformatter.ts: -------------------------------------------------------------------------------- 1 | import * as data from '@/data'; 2 | import * as util from '@/util'; 3 | import { Config, DEFAULT_CONFIG } from '@/config'; 4 | import { Formatter } from './types'; 5 | import { Logger, NoopLogger } from '@/debug'; 6 | 7 | export type MaxMeasureFormatterOptions = { 8 | config?: Config; 9 | logger?: Logger; 10 | }; 11 | 12 | /** 13 | * A formatter that limits the number of measures per system. 14 | */ 15 | export class MaxMeasureFormatter implements Formatter { 16 | private config: Config; 17 | private log: Logger; 18 | 19 | constructor(private maxMeasuresPerSystemCount: number, opts?: MaxMeasureFormatterOptions) { 20 | this.config = { ...DEFAULT_CONFIG, ...opts?.config }; 21 | this.log = opts?.logger ?? new NoopLogger(); 22 | 23 | util.assert(maxMeasuresPerSystemCount > 0, 'maxMeasuresPerSystemCount must be greater than 0'); 24 | } 25 | 26 | format(document: data.Document): data.Document { 27 | const clone = document.clone(); 28 | 29 | const measures = clone.score.systems.flatMap((system) => system.measures); 30 | 31 | clone.score.systems = []; 32 | 33 | for (let index = 0; index < measures.length; index += this.maxMeasuresPerSystemCount) { 34 | const systemMeasures = measures.slice(index, index + this.maxMeasuresPerSystemCount); 35 | clone.score.systems.push({ type: 'system', measures: systemMeasures }); 36 | } 37 | 38 | return clone; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/formatting/monitoredformatter.ts: -------------------------------------------------------------------------------- 1 | import * as data from '@/data'; 2 | import { Config, DEFAULT_CONFIG } from '@/config'; 3 | import { Formatter } from './types'; 4 | import { Logger, Stopwatch } from '@/debug'; 5 | 6 | export type MonitoredFormatterOptions = { 7 | config?: Config; 8 | }; 9 | 10 | /** 11 | * A formatter that tracks how long its child formatter takes to format a document. 12 | */ 13 | export class MonitoredFormatter implements Formatter { 14 | private config: Config; 15 | private log: Logger; 16 | 17 | constructor(private formatter: Formatter, logger: Logger, opts?: MonitoredFormatterOptions) { 18 | this.config = { ...DEFAULT_CONFIG, ...opts?.config }; 19 | this.log = logger; 20 | } 21 | 22 | format(document: data.Document): data.Document { 23 | const stopwatch = Stopwatch.start(); 24 | 25 | const formatted = this.formatter.format(document); 26 | 27 | const lap = stopwatch.lap(); 28 | if (lap < 1) { 29 | this.log.info(`formatted score in <1ms`); 30 | } else { 31 | this.log.info(`formatted score in ${Math.round(lap)}ms`); 32 | } 33 | 34 | return formatted; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/formatting/panoramicformatter.ts: -------------------------------------------------------------------------------- 1 | import * as data from '@/data'; 2 | import * as util from '@/util'; 3 | import { Config, DEFAULT_CONFIG } from '@/config'; 4 | import { Formatter } from './types'; 5 | import { Logger, NoopLogger } from '@/debug'; 6 | 7 | export type PanoramicFormatterOptions = { 8 | config?: Config; 9 | logger?: Logger; 10 | }; 11 | 12 | /** 13 | * A formatter formats a document for infinite x-scrolling as a single system. 14 | */ 15 | export class PanoramicFormatter implements Formatter { 16 | private config: Config; 17 | private log: Logger; 18 | 19 | constructor(opts?: PanoramicFormatterOptions) { 20 | this.config = { ...DEFAULT_CONFIG, ...opts?.config }; 21 | this.log = opts?.logger ?? new NoopLogger(); 22 | 23 | util.assertNull(this.config.WIDTH, 'WIDTH must be null for PanoramicFormatter'); 24 | } 25 | 26 | format(document: data.Document): data.Document { 27 | const clone = document.clone(); 28 | const measures = clone.score.systems.flatMap((system) => system.measures); 29 | clone.score.systems = [{ type: 'system', measures }]; 30 | return clone; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/formatting/types.ts: -------------------------------------------------------------------------------- 1 | import * as data from '@/data'; 2 | 3 | /** Formatter produces a new formatted document from an unformatted one. */ 4 | export interface Formatter { 5 | format(document: data.Document): data.Document; 6 | } 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { type RenderMusicXMLOptions, renderMusicXML, type RenderMXLOptions, renderMXL } from './render'; 2 | export * from './elements/eventmappingfactory'; 3 | export { MusicXMLParser, MXLParser } from './parsing'; 4 | export { type Config, CONFIG, DEFAULT_CONFIG } from './config'; 5 | export { Renderer } from './rendering'; 6 | export { Score } from './elements'; 7 | export type { 8 | EventMap, 9 | EventType, 10 | ClickEvent, 11 | LongPressEvent, 12 | EnterEvent, 13 | ExitEvent, 14 | ScrollEvent, 15 | AnyEventListener, 16 | ClickEventListener, 17 | LongpressEventListener, 18 | EnterEventListener, 19 | ExitEventListener, 20 | ScrollEventListener, 21 | AnyEvent, 22 | } from './elements'; 23 | export { 24 | type Formatter, 25 | DefaultFormatter, 26 | PanoramicFormatter, 27 | MonitoredFormatter, 28 | MaxMeasureFormatter, 29 | } from './formatting'; 30 | export { type SchemaDescriptor, type SchemaType, type SchemaConfig } from './schema'; 31 | export { SimpleCursor } from './components'; 32 | export { type Cursor } from './playback'; 33 | export { type Logger, type LogLevel, ConsoleLogger, MemoryLogger, type MemoryLog, NoopLogger } from './debug'; 34 | -------------------------------------------------------------------------------- /src/musicxml/accidentalmark.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { ACCIDENTAL_TYPES, AccidentalType } from './enums'; 3 | 4 | /** 5 | * An `<accidental-mark>` element can be used as a separate notation or as part of an ornament. When used in an 6 | * ornament, position and placement are relative to the ornament, not relative to the note. 7 | * 8 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/accidental-mark/ 9 | */ 10 | export class AccidentalMark { 11 | constructor(private element: NamedElement<'accidental-mark'>) {} 12 | 13 | /** Returns the type of the accidental mark. Defaults to null. */ 14 | getType(): AccidentalType | null { 15 | return this.element.content().enum(ACCIDENTAL_TYPES); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/musicxml/backup.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '../util'; 2 | 3 | /** 4 | * Coordinates multiple voices in a single part. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/backup/. 7 | */ 8 | export class Backup { 9 | constructor(private element: NamedElement<'backup'>) {} 10 | 11 | /** Returns the duration of the backup. Defaults to 4 */ 12 | getDuration(): number { 13 | return this.element.first('duration')?.content().int() ?? 4; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/musicxml/beam.ts: -------------------------------------------------------------------------------- 1 | import { BEAM_VALUES, BeamValue } from './enums'; 2 | import { NamedElement, clamp } from '@/util'; 3 | 4 | /** 5 | * Beam is a note connector that indicates a rhythmic relationship amongst a group of notes. 6 | * 7 | * https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/beam/ 8 | */ 9 | export class Beam { 10 | constructor(private element: NamedElement<'beam'>) {} 11 | 12 | /** Returns the beam level of the beam. */ 13 | getNumber(): number { 14 | const number = this.element.attr('number').withDefault(1).int(); 15 | return clamp(1, 8, number); 16 | } 17 | 18 | /** Returns the beam value of the beam. */ 19 | getBeamValue(): BeamValue { 20 | return this.element.content().withDefault(BEAM_VALUES.values[0]).enum(BEAM_VALUES); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/musicxml/bend.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { BendType } from './enums'; 3 | 4 | /** 5 | * The `<bend>` element is used in guitar notation and tablature. A single note with a bend and release will contain two 6 | * `<bend>` elements: the first to represent the bend and the second to represent the release. 7 | * 8 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/bend/ 9 | */ 10 | export class Bend { 11 | constructor(private element: NamedElement<'bend'>) {} 12 | 13 | /** Returns the number of semitones to bend. */ 14 | getAlter(): number { 15 | return this.element.first('bend-alter')?.content().float() ?? 1; 16 | } 17 | 18 | /** Returns the type of bend. */ 19 | getType(): BendType { 20 | if (this.element.first('pre-bend')) { 21 | return 'pre-bend'; 22 | } 23 | if (this.element.first('release')) { 24 | return 'release'; 25 | } 26 | return 'normal'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/musicxml/clef.ts: -------------------------------------------------------------------------------- 1 | import { ClefSign, CLEF_SIGNS } from './enums'; 2 | import { NamedElement } from '@/util'; 3 | 4 | /** 5 | * A symbol placed at the left-hand end of the stave, indicating the pitch of the notes written. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/data-types/clef-sign/ 8 | */ 9 | export class Clef { 10 | constructor(private element: NamedElement<'clef'>) {} 11 | 12 | /** Returns the stave number this clef belongs to. */ 13 | getStaveNumber(): number { 14 | return this.element.attr('number').withDefault(1).int(); 15 | } 16 | 17 | /** Returns the clef sign. Defaults to null. */ 18 | getSign(): ClefSign | null { 19 | return this.element.first('sign')?.content().enum(CLEF_SIGNS) ?? null; 20 | } 21 | 22 | /** Returns the line of the clef. Defaults to null. */ 23 | getLine(): number | null { 24 | return this.element.first('line')?.content().int() ?? null; 25 | } 26 | 27 | /** Returns the octave change of the clef. Defaults to null. */ 28 | getOctaveChange(): number | null { 29 | return this.element.first('clef-octave-change')?.content().int() ?? null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/musicxml/coda.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** The `<coda>` element is the visual indicator of a coda sign. */ 4 | export class Coda { 5 | constructor(private element: NamedElement<'coda'>) {} 6 | } 7 | -------------------------------------------------------------------------------- /src/musicxml/defaults.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { StaveLayout, SystemLayout } from './types'; 3 | 4 | export class Defaults { 5 | constructor(private element: NamedElement<'defaults'>) {} 6 | 7 | /** Returns stave layouts of the defaults element. */ 8 | getStaveLayouts(): StaveLayout[] { 9 | return this.element.all('staff-layout').map((element) => ({ 10 | staveNumber: element.attr('number').withDefault(1).int(), 11 | staveDistance: element.first('staff-distance')?.content().withDefault(0).int() ?? null, 12 | })); 13 | } 14 | 15 | /** Returns system layouts of the defaults element. */ 16 | getSystemLayout(): SystemLayout { 17 | const leftMargin = this.element.first('left-margin')?.content().withDefault(0) ?? null; 18 | const rightMargin = this.element.first('right-margin')?.content().withDefault(0) ?? null; 19 | const topSystemDistance = this.element.first('top-system-distance')?.content().withDefault(0) ?? null; 20 | const systemDistance = this.element.first('system-distance')?.content().withDefault(0) ?? null; 21 | return { 22 | leftMargin: leftMargin ? leftMargin.int() : null, 23 | rightMargin: rightMargin ? rightMargin.int() : null, 24 | topSystemDistance: topSystemDistance ? topSystemDistance.int() : null, 25 | systemDistance: systemDistance ? systemDistance.int() : null, 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/musicxml/delayedturn.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<delayed-turn>` element indicates a normal turn that is delayed until the end of the current note. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/delayed-turn/ 7 | */ 8 | export class DelayedTurn { 9 | constructor(private element: NamedElement<'delayed-turn'>) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/musicxml/doubletongue.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<double-tongue>` element represents the double tongue symbol (two dots arranged horizontally). 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/double-tongue/ 7 | */ 8 | export class DoubleTongue { 9 | constructor(private element: NamedElement<'double-tongue'>) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/musicxml/downbow.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { ABOVE_BELOW, AboveBelow } from './enums'; 3 | 4 | /** 5 | * The `<down-bow>` element represents the symbol that is used both for down-bowing on bowed instruments, and 6 | * down-stroke on plucked instruments. 7 | * 8 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/down-bow/ 9 | */ 10 | export class DownBow { 11 | constructor(private element: NamedElement<'down-bow'>) {} 12 | 13 | /** Returns the placement of the upbow. Defaults to above. */ 14 | getPlacement(): AboveBelow { 15 | return this.element.attr('placement').withDefault<AboveBelow>('above').enum(ABOVE_BELOW); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/musicxml/dynamics.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { DYNAMIC_TYPES, DynamicType } from './enums'; 3 | 4 | /** 5 | * Dynamics can be associated either with a note or a general musical direction. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/dynamics/ 8 | */ 9 | export class Dynamics { 10 | constructor(private element: NamedElement<'dynamics'>) {} 11 | 12 | /** Returns the dynamic types associated with this element. */ 13 | getTypes(): DynamicType[] { 14 | return this.element.children(...DYNAMIC_TYPES.values).map((child) => child.name as DynamicType); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/musicxml/fermata.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { FERMATA_SHAPES, FERMATA_TYPES, FermataShape, FermataType } from './enums'; 3 | 4 | /** 5 | * The `<fermata>` element content represents the shape of the fermata sign. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/fermata/ 8 | */ 9 | export class Fermata { 10 | constructor(private element: NamedElement<'fermata'>) {} 11 | 12 | /** Returns the shape of the fermata. Defaults to normal. */ 13 | getShape(): FermataShape { 14 | return this.element.content().enum(FERMATA_SHAPES) ?? 'normal'; 15 | } 16 | 17 | /** Returns the type of fermata. Defaults to upright. */ 18 | getType(): FermataType { 19 | return this.element.attr('type').withDefault<FermataType>('upright').enum(FERMATA_TYPES); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/musicxml/fingering.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * Fingering is typically indicated 1, 2, 3, 4, 5. Multiple fingerings may be given, typically to substitute 5 | * fingerings in the middle of a note. For guitar and other fretted instruments, the `<fingering>` element 6 | * represents the fretting finger; the `<pluck>` element represents the plucking finger. 7 | * 8 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/fingering/ 9 | */ 10 | export class Fingering { 11 | constructor(private element: NamedElement<'fingering'>) {} 12 | 13 | /** Returns the fingering number. Defaults to null. */ 14 | getNumber(): number | null { 15 | return this.element.content().int(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/musicxml/fingernails.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<fingernails>` element is used in notation for harp and other plucked string instruments. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/fingernails/ 7 | */ 8 | export class Fingernails { 9 | constructor(private element: NamedElement<'fingernails'>) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/musicxml/forward.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '../util'; 2 | 3 | /** 4 | * Coordinates multiple voices in a single part. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/forward/. 7 | */ 8 | export class Forward { 9 | constructor(private element: NamedElement<'forward'>) {} 10 | 11 | /** Returns the duration of the backup. Defaults to 4 */ 12 | getDuration(): number { 13 | return this.element.first('duration')?.content().int() ?? 4; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/musicxml/fret.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<fret>` element is used with tablature notation and chord diagrams. Fret numbers start with 0 for an open string 5 | * and 1 for the first fret. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/fret/ 8 | */ 9 | export class Fret { 10 | constructor(private element: NamedElement<'fret'>) {} 11 | 12 | /** Returns the number of the fret. */ 13 | getNumber(): number | null { 14 | return this.element.content().int(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/musicxml/hammeron.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { START_STOP, StartStop } from './enums'; 3 | 4 | /** 5 | * The `<hammer-on>` element is used in guitar and fretted instrument notation. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/hammer-on/ 8 | */ 9 | export class HammerOn { 10 | constructor(private element: NamedElement<'hammer-on'>) {} 11 | 12 | /** Returns the number of the hammer-on. Defaults to null. */ 13 | getNumber(): number | null { 14 | return this.element.attr('number').int(); 15 | } 16 | 17 | /** Returns the type of hammer-on. */ 18 | getType(): StartStop | null { 19 | return this.element.attr('type').enum(START_STOP); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/musicxml/harmonic.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { HarmonicPitchType, HarmonicType } from './enums'; 3 | 4 | /** 5 | * The `<harmonic>` element indicates natural and artificial harmonics. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/harmonic/ 8 | */ 9 | export class Harmonic { 10 | constructor(private element: NamedElement<'harmonic'>) {} 11 | 12 | /** Returns the type of harmonic. Defaults to 'unspecified'. */ 13 | getType(): HarmonicType { 14 | if (this.element.first('natural')) { 15 | return 'natural'; 16 | } 17 | if (this.element.first('artificial')) { 18 | return 'artificial'; 19 | } 20 | return 'unspecified'; 21 | } 22 | 23 | /** Returns the pitch type of the harmonic. Defaults to 'unspecified'. */ 24 | getPitchType(): HarmonicPitchType { 25 | if (this.element.first('base-pitch')) { 26 | return 'base'; 27 | } 28 | if (this.element.first('touching-pitch')) { 29 | return 'touching'; 30 | } 31 | if (this.element.first('sounding-pitch')) { 32 | return 'sounding'; 33 | } 34 | return 'unspecified'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/musicxml/heel.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<heel>` element is used with organ pedals. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/heel/ 7 | */ 8 | export class Heel { 9 | constructor(private element: NamedElement<'heel'>) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/musicxml/invertedmordent.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<inverted-mordent>` element represents the sign without the vertical line. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/inverted-mordent/ 7 | */ 8 | export class InvertedMordent { 9 | constructor(private element: NamedElement<'inverted-mordent'>) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/musicxml/invertedturn.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<inverted-turn>` element has the shape which goes down and then up. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/inverted-turn/ 7 | */ 8 | export class InvertedTurn { 9 | constructor(private element: NamedElement<'inverted-turn'>) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/musicxml/key.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { KEY_MODES, KeyMode } from './enums'; 3 | 4 | /** 5 | * Key represents a key signature. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/key/ 8 | */ 9 | export class Key { 10 | constructor(private element: NamedElement<'key'>) {} 11 | 12 | /** Returns the fifths count of the key. Defaults to 0. */ 13 | getFifthsCount(): number { 14 | return this.element.first('fifths')?.content().int() ?? 0; 15 | } 16 | 17 | /** Returns the mode of the key. Defaults to 'none'. */ 18 | getMode(): KeyMode { 19 | return this.element.first('mode')?.content().enum(KEY_MODES) ?? 'none'; 20 | } 21 | 22 | /** Returns the stave number this key belongs to. */ 23 | getStaveNumber(): number | null { 24 | return this.element.attr('number').int(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/musicxml/measurestyle.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The <measure-style> element indicates a special way to print partial to multiple measures within a part. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/measure-style/ 7 | */ 8 | export class MeasureStyle { 9 | constructor(private element: NamedElement<'measure-style'>) {} 10 | 11 | /** 12 | * Returns the stave number this measure style belongs to. Defaults to null, implying that it should apply to all 13 | * staves. 14 | */ 15 | getStaveNumber(): number | null { 16 | return this.element.attr('number').int(); 17 | } 18 | 19 | /** 20 | * Returns how many measures the rest spans. 21 | * 22 | * Defaults to 0. A value of 0 indicates that there are no multiple rests. 23 | */ 24 | getMultipleRestCount(): number { 25 | return this.element.first('multiple-rest')?.content().int() ?? 0; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/musicxml/mordent.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<mordent>` element represents the sign with the vertical line. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/mordent/ 7 | */ 8 | export class Mordent { 9 | constructor(private element: NamedElement<'mordent'>) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/musicxml/musicxml.ts: -------------------------------------------------------------------------------- 1 | import * as errors from '@/errors'; 2 | import { NamedElement } from '@/util'; 3 | import { ScorePartwise } from './scorepartwise'; 4 | 5 | /** 6 | * A wrapper around a root node that corresponds to a MusicXML document. 7 | * 8 | * See https://www.w3.org/2021/06/musicxml40/ 9 | */ 10 | export class MusicXML { 11 | constructor(private root: Document) {} 12 | 13 | /** 14 | * Returns the first <score-partwise> of the document. 15 | * 16 | * @throws {errors.ParseError} when the root does not contain a <score-partwise> element. It does not check for deep validity. 17 | */ 18 | getScorePartwise(): ScorePartwise { 19 | const node = this.root.getElementsByTagName('score-partwise').item(0); 20 | if (!node) { 21 | throw new errors.ParseError('could not find a <score-partwise> element'); 22 | } 23 | return new ScorePartwise(NamedElement.of(node)); 24 | } 25 | 26 | /** Returns the string representation of the document. */ 27 | getDocumentString(): string { 28 | const serializer = new XMLSerializer(); 29 | return serializer.serializeToString(this.root); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/musicxml/octaveshift.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '../util'; 2 | import { UP_DOWN_STOP_CONTINUE, UpDownStopContinue } from './enums'; 3 | 4 | /** 5 | * The <octave-shift> element indicates where notes are shifted up or down from their performed values because of 6 | * printing difficulty. 7 | * 8 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/octave-shift/ 9 | */ 10 | export class OctaveShift { 11 | constructor(private element: NamedElement<'octave-shift'>) {} 12 | 13 | /** Returns the octave shift type. Defaults to 'up'. */ 14 | getType(): UpDownStopContinue { 15 | return this.element.attr('type').withDefault<UpDownStopContinue>('up').enum(UP_DOWN_STOP_CONTINUE); 16 | } 17 | 18 | /** Returns the size of the octave shift. Defaults to 8. */ 19 | getSize(): number { 20 | return this.element.attr('size').withDefault(8).int(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/musicxml/openstring.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<open-string>` element represents the zero-shaped open string symbol. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/open-string/ 7 | */ 8 | export class OpenString { 9 | constructor(private element: NamedElement<'open-string'>) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/musicxml/part.ts: -------------------------------------------------------------------------------- 1 | import { Measure } from './measure'; 2 | import { NamedElement } from '@/util'; 3 | 4 | /** 5 | * The top level of musical organization below the Score that contains a sequence of Measures. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/part-partwise/ 8 | */ 9 | export class Part { 10 | constructor(private element: NamedElement<'part'>) {} 11 | 12 | /** Returns the ID of the part or an empty string if missing. */ 13 | getId(): string { 14 | return this.element.attr('id').withDefault('').str(); 15 | } 16 | 17 | /** Returns an array of measures in the order they appear. */ 18 | getMeasures(): Measure[] { 19 | return this.element.all('measure').map((element) => new Measure(element)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/musicxml/pedal.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { PEDAL_TYPES, PedalType } from './enums'; 3 | 4 | /** 5 | * The <pedal> element represents piano pedal marks, including damper and sostenuto pedal marks. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/pedal/ 8 | */ 9 | export class Pedal { 10 | constructor(private element: NamedElement<'pedal'>) {} 11 | 12 | /** Returns the type of pedal. Defaults to 'start'. */ 13 | getType(): PedalType { 14 | return this.element.attr('type').withDefault<PedalType>('start').enum(PEDAL_TYPES); 15 | } 16 | 17 | /** Whether to show pedal signs. Defaults to false. */ 18 | sign(): boolean { 19 | return this.element.attr('sign').str() === 'yes'; 20 | } 21 | 22 | /** Whether to show pedal lines. Defaults to false. */ 23 | line(): boolean { 24 | return this.element.attr('line').str() === 'yes'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/musicxml/pluck.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<pluck>` element is used to specify the plucking fingering on a fretted instrument, where the fingering element 5 | * refers to the fretting fingering. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/pluck/ 8 | */ 9 | export class Pluck { 10 | constructor(private element: NamedElement<'pluck'>) {} 11 | 12 | /** Returns the plucking finger. Defaults to null. */ 13 | getFinger(): string | null { 14 | return this.element.content().str(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/musicxml/pulloff.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { START_STOP, StartStop } from './enums'; 3 | 4 | /** 5 | * The `<pull-off>` element is used in guitar and fretted instrument notation. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/pull-off/ 8 | */ 9 | export class PullOff { 10 | constructor(private element: NamedElement<'pull-off'>) {} 11 | 12 | /** Returns the number of the pull-off. Defaults to null;. */ 13 | getNumber(): number | null { 14 | return this.element.attr('number').int(); 15 | } 16 | 17 | /** Returns the type of pull-off. */ 18 | getType(): StartStop | null { 19 | return this.element.attr('type').enum(START_STOP); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/musicxml/rehearsal.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<rehearsal>` element specifies letters, numbers, and section names that are notated in the score for reference 5 | * during rehearsal. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/rehearsal/ 8 | */ 9 | export class Rehearsal { 10 | constructor(private element: NamedElement<'rehearsal'>) {} 11 | 12 | /** Returns the content of the element. */ 13 | getText(): string { 14 | return this.element.content().withDefault('').str(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/musicxml/segno.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** The `<segno>` element is the visual indicator of a segno sign. */ 4 | export class Segno { 5 | constructor(private element: NamedElement<'segno'>) {} 6 | } 7 | -------------------------------------------------------------------------------- /src/musicxml/slide.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { START_STOP, StartStop } from './enums'; 3 | 4 | /** 5 | * A `<slide>` is continuous between the two pitches and defaults to a solid line. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/slide/ 8 | */ 9 | export class Slide { 10 | constructor(private element: NamedElement<'slide'>) {} 11 | 12 | /** Returns the slide type. Defaults to null. */ 13 | getType(): StartStop | null { 14 | return this.element.attr('type').enum(START_STOP) ?? null; 15 | } 16 | 17 | /** Returns the slide number. Defaults to 1. */ 18 | getNumber(): number { 19 | return this.element.attr('number').int() ?? 1; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/musicxml/slur.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { 3 | ABOVE_BELOW, 4 | AboveBelow, 5 | LINE_TYPES, 6 | LineType, 7 | OVER_UNDER, 8 | OverUnder, 9 | START_STOP_CONTINUE, 10 | StartStopContinue, 11 | } from './enums'; 12 | 13 | /** 14 | * Most slurs are represented with two <slur> elements: one with a start type, and one with a stop type. 15 | * 16 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/slur/ 17 | */ 18 | export class Slur { 19 | constructor(private element: NamedElement<'slur'>) {} 20 | 21 | /** Returns the type of slur. Defaults to null. */ 22 | getType(): StartStopContinue | null { 23 | return this.element.attr('type').enum(START_STOP_CONTINUE); 24 | } 25 | 26 | /** Returns the placement of the slur. Defaults to null. */ 27 | getPlacement(): AboveBelow | null { 28 | return this.element.attr('placement').enum(ABOVE_BELOW); 29 | } 30 | 31 | /** Returns the orientation of the slur. Defaults to null. */ 32 | getOrientation(): OverUnder | null { 33 | return this.element.attr('orientation').enum(OVER_UNDER); 34 | } 35 | 36 | /** Returns the number of the slur. Defaults to 1. */ 37 | getNumber(): number { 38 | return this.element.attr('number').withDefault(1).int(); 39 | } 40 | 41 | /** Returns the line type of the slur. Defaults to solid. */ 42 | getLineType(): LineType { 43 | return this.element.attr('line-type').withDefault<LineType>('solid').enum(LINE_TYPES); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/musicxml/snappizzicato.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<snap-pizzicato>` element represents the snap pizzicato symbol. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/snap-pizzicato/ 7 | */ 8 | export class SnapPizzicato { 9 | constructor(private element: NamedElement<'snap-pizzicato'>) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/musicxml/stavedetails.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { STAVE_TYPES, StaveType } from './enums'; 3 | 4 | /** 5 | * Indicates different stave types. A stave is the set of five horizontal lines where notes and other musical 6 | * symbols are placed. 7 | * 8 | * https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/staff-details/ 9 | */ 10 | export class StaveDetails { 11 | constructor(private element: NamedElement<'staff-details'>) {} 12 | 13 | /** Returns the stave type. */ 14 | getStaveType(): StaveType { 15 | return this.element.first('staff-type')?.content().enum(STAVE_TYPES) ?? 'regular'; 16 | } 17 | 18 | /** Returns the number of the stave. */ 19 | getStaveNumber(): number { 20 | return this.element.attr('number').withDefault(1).int(); 21 | } 22 | 23 | /** Returns the number of lines of the stave. */ 24 | getStaveLines(): number { 25 | return this.element.first('staff-lines')?.content().withDefault(5).int() ?? 5; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/musicxml/stopped.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<stopped>` element represents the stopped symbol, which looks like a plus sign. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/stopped/ 7 | */ 8 | export class Stopped { 9 | constructor(private element: NamedElement<'stopped'>) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/musicxml/symbolic.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The <symbol> element specifies a musical symbol using a canonical SMuFL glyph name. 5 | * 6 | * `Symbol` is part of the standard library in JavaScript, so we resort to `Symbolic` instead. 7 | * 8 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/symbol/ 9 | */ 10 | export class Symbolic { 11 | constructor(private element: NamedElement<'symbol'>) {} 12 | 13 | /** Returns a specific Standard Music Font Layout (SMuFL) character. Defaults to empty string. */ 14 | getSmulfGlyphName(): string { 15 | return this.element.content().withDefault('').str(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/musicxml/tabstring.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<string>` element is used with tablature notation, regular notation (where it is often circled), and chord 5 | * diagrams. String numbers start with 1 for the highest pitched full-length string. 6 | * 7 | * NOTE: TabString is used to not conflict with the String type. 8 | * 9 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/string/ 10 | */ 11 | export class TabString { 12 | constructor(private element: NamedElement<'string'>) {} 13 | 14 | /** Returns the number of the string. */ 15 | getNumber(): number | null { 16 | return this.element.content().int(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/musicxml/tap.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<tap>` element indicates a tap on the fretboard. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/technical/ 7 | */ 8 | export class Tap { 9 | constructor(private element: NamedElement<'tap'>) {} 10 | 11 | /** Returns the symbol for the tap. Defaults to 'T'. */ 12 | getSymbol(): string { 13 | return this.element.content().withDefault('T').str(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/musicxml/thumbposition.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<thumb-position>` element represents the thumb position symbol. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/thumb-position/ 7 | */ 8 | export class ThumbPosition { 9 | constructor(private element: NamedElement<'thumb-position'>) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/musicxml/tied.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { ABOVE_BELOW, AboveBelow, LINE_TYPES, LineType, OVER_UNDER, OverUnder, TIED_TYPES, TiedType } from './enums'; 3 | 4 | /** 5 | * The <tied> element represents the notated tie. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/tied/. 8 | */ 9 | export class Tied { 10 | constructor(private element: NamedElement<'tied'>) {} 11 | 12 | /** Returns the type of tie. Defaults to null. */ 13 | getType(): TiedType | null { 14 | return this.element.attr('type').enum(TIED_TYPES); 15 | } 16 | 17 | /** Returns the placement of the tie. Defaults to null. */ 18 | getPlacement(): AboveBelow | null { 19 | return this.element.attr('placement').enum(ABOVE_BELOW); 20 | } 21 | 22 | getOrientation(): OverUnder | null { 23 | return this.element.attr('orientation').enum(OVER_UNDER); 24 | } 25 | 26 | /** Returns the number of the tie. Defaults to 1. */ 27 | getNumber(): number { 28 | return this.element.attr('number').withDefault(1).int(); 29 | } 30 | 31 | /** Returns the line type of the tie. Defaults to solid. */ 32 | getLineType(): LineType { 33 | return this.element.attr('line-type').withDefault<LineType>('solid').enum(LINE_TYPES); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/musicxml/time.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { TIME_SYMBOLS, TimeSymbol } from './enums'; 3 | 4 | /** 5 | * Time represents a time signature element. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/time/ 8 | */ 9 | export class Time { 10 | constructor(private element: NamedElement<'time'>) {} 11 | 12 | /** Returns the stave number this time belongs to. Defaults to null. */ 13 | getStaveNumber(): number | null { 14 | return this.element.attr('number').int(); 15 | } 16 | 17 | /** Returns the beats of the time. */ 18 | getBeats(): string[] { 19 | return this.element 20 | .all('beats') 21 | .map((beats) => beats.content().str()) 22 | .filter((content): content is string => typeof content === 'string'); 23 | } 24 | 25 | /** Returns the beat types of the time. */ 26 | getBeatTypes(): string[] { 27 | return this.element 28 | .all('beat-type') 29 | .map((beatType) => beatType.content().str()) 30 | .filter((content): content is string => typeof content === 'string'); 31 | } 32 | 33 | /** Returns whether the time signature is hidden. */ 34 | isHidden(): boolean { 35 | return !!this.element.first('senza-misura'); 36 | } 37 | 38 | /** Returns the symbol of the time. */ 39 | getSymbol(): TimeSymbol | null { 40 | return this.element.attr('symbol').enum(TIME_SYMBOLS) ?? null; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/musicxml/timemodification.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * Time modification indicates tuplets, double-note tremolos, and other durational changes. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/time-modification/ 7 | */ 8 | export class TimeModification { 9 | constructor(private element: NamedElement<'time-modification'>) {} 10 | 11 | /** Describes how many notes are played in the time usually occupied by the number in the `<normal-notes>` element. */ 12 | getActualNotes(): number { 13 | return this.element.first('actual-notes')?.content().int() ?? 1; 14 | } 15 | 16 | /** Describes how many notes are usually played in the time occupied by the number in the `<actual-notes>` element. */ 17 | getNormalNotes(): number { 18 | return this.element.first('normal-notes')?.content().int() ?? 1; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/musicxml/toe.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<toe>` element is used with organ pedals. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/toe/ 7 | */ 8 | export class Toe { 9 | constructor(private element: NamedElement<'toe'>) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/musicxml/tremolo.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<tremolo>` element can be used to indicate single-note, double-note, or unmeasured tremolos. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/tremolo/ 7 | */ 8 | export class Tremolo { 9 | constructor(private element: NamedElement<'tremolo'>) {} 10 | 11 | /** Returns the number of tremolo marks. Defaults to 0. */ 12 | getTremoloMarksCount(): number { 13 | return this.element.content().withDefault(0).int(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/musicxml/trillmark.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '../util'; 2 | 3 | /** 4 | * The <trill-mark> element represents the trill symbol. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/trill-mark/ 7 | */ 8 | export class TrillMark { 9 | constructor(private element: NamedElement<'trill-mark'>) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/musicxml/tripletongue.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<triple-tongue>` element represents the triple tongue symbol (three dots arranged horizontally). 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/triple-tongue/ 7 | */ 8 | export class TripleTongue { 9 | constructor(private element: NamedElement<'triple-tongue'>) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/musicxml/tuplet.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { ABOVE_BELOW, START_STOP, AboveBelow, StartStop, ShowTuplet, SHOW_TUPLET } from './enums'; 3 | 4 | /** 5 | * A <tuplet> element is present when a tuplet is to be displayed graphically, in addition to the sound data provided by 6 | * the <time-modification> elements. 7 | * 8 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/tuplet/. 9 | */ 10 | export class Tuplet { 11 | constructor(private element: NamedElement<'tuplet'>) {} 12 | 13 | /** Returns the type of tuplet. */ 14 | getType(): StartStop | null { 15 | return this.element.attr('type').enum(START_STOP); 16 | } 17 | 18 | /** Returns the placement of the tuplet. Defaults to 'below'. */ 19 | getPlacement(): AboveBelow { 20 | return this.element.attr('placement').enum(ABOVE_BELOW) ?? 'below'; 21 | } 22 | 23 | /** Returns the number of the tuplet. Defaults to 1. */ 24 | getNumber(): number { 25 | return this.element.attr('number').withDefault(1).int(); 26 | } 27 | 28 | /** Returns how the tuplet number should be displayed. */ 29 | getShowNumber(): ShowTuplet { 30 | return this.element.attr('show-number').withDefault<ShowTuplet>('actual').enum(SHOW_TUPLET); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/musicxml/turn.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<turn>` element is the normal turn shape which goes up then down. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/turn/ 7 | */ 8 | export class Turn { 9 | constructor(private element: NamedElement<'turn'>) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/musicxml/types.ts: -------------------------------------------------------------------------------- 1 | import { Measure } from './measure'; 2 | import { Part } from './part'; 3 | 4 | /** 5 | * StaveLayout describes how a stave is positioned. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/staff-layout/ 8 | */ 9 | export type StaveLayout = { 10 | staveNumber: number; 11 | staveDistance: number | null; 12 | }; 13 | 14 | /** 15 | * SystemLayout is a group of staves that are read and played simultaneously. 16 | * 17 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/system-layout/ 18 | */ 19 | export type SystemLayout = { 20 | leftMargin: number | null; 21 | rightMargin: number | null; 22 | topSystemDistance: number | null; 23 | systemDistance: number | null; 24 | }; 25 | 26 | /** A part and measure. */ 27 | export type PartMeasure = { 28 | part: Part; 29 | measure: Measure; 30 | }; 31 | -------------------------------------------------------------------------------- /src/musicxml/upbow.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { ABOVE_BELOW, AboveBelow } from './enums'; 3 | 4 | /** 5 | * The `<up-bow>` element represents the symbol that is used both for up-bowing on bowed instruments, and up-stroke on 6 | * plucked instruments. 7 | * 8 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/up-bow/ 9 | */ 10 | export class UpBow { 11 | constructor(private element: NamedElement<'up-bow'>) {} 12 | 13 | /** Returns the placement of the upbow. Defaults to above. */ 14 | getPlacement(): AboveBelow { 15 | return this.element.attr('placement').withDefault<AboveBelow>('above').enum(ABOVE_BELOW); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/musicxml/wavyline.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '../util'; 2 | import { START_STOP_CONTINUE, StartStopContinue } from './enums'; 3 | 4 | /** 5 | * Wavy lines are one way to indicate trills and vibrato. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/wavy-line/. 8 | */ 9 | export class WavyLine { 10 | constructor(private element: NamedElement<'wavy-line'>) {} 11 | 12 | /** Returns the number of the wavy line. Defaults to 1. */ 13 | getNumber(): number { 14 | return this.element.attr('number').withDefault(1).int(); 15 | } 16 | 17 | /** Returns the type of the wavy line. Defaults to start. */ 18 | getType(): StartStopContinue { 19 | return this.element.attr('type').withDefault<StartStopContinue>('start').enum(START_STOP_CONTINUE); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/musicxml/wedge.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { WEDGE_TYPES, WedgeType } from './enums'; 3 | 4 | /** 5 | * Represents a cescendo or diminuendo wedge symbols. 6 | * 7 | * See https://www.w3.org/2021/06/musicxml40/musicxml-reference/elements/wedge/ 8 | */ 9 | export class Wedge { 10 | constructor(private element: NamedElement<'wedge'>) {} 11 | 12 | /** Returns the type of the wedge. Defaults to null. */ 13 | getType(): WedgeType | null { 14 | return this.element.attr('type').enum(WEDGE_TYPES); 15 | } 16 | 17 | /** Indicates the gap between the top and bottom of the wedge. Defaults to 0. */ 18 | getSpread(): number { 19 | return this.element.attr('spread').withDefault(0).int(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/musicxml/words.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | export class Words { 4 | constructor(private element: NamedElement<'words'>) {} 5 | 6 | /** Returns the content of the words. Defaults to empty string. */ 7 | getContent(): string { 8 | return this.element.content().withDefault('').str(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/mxl/README.md: -------------------------------------------------------------------------------- 1 | # mxl 2 | 3 | The `mxl` library is responsible for interfacing with [MXL](https://www.w3.org/2021/06/musicxml40/tutorial/compressed-mxl-files/) compressed files. 4 | 5 | ## Intent 6 | 7 | ### Goals 8 | 9 | - **DO** provide an interface for getting data from an MXL archive. 10 | - **DO** artifically flatten the XML structure of a MXL document. 11 | - **DO** conform data from the MXL document to reasonable defaults when needed. 12 | 13 | ### Non-goals 14 | 15 | - **DO NOT** map 1:1 TypeScript classes to MXL elements. 16 | - **DO NOT** mutate data in a MXL document. 17 | - **DO NOT** parse its MusicXML data into `musicxml` elements. 18 | -------------------------------------------------------------------------------- /src/mxl/container.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | import { Rootfile } from './rootfile'; 3 | 4 | /** 5 | * Starting with Version 2.0, the MusicXML format includes a standard zip compressed version. These zip files can 6 | * contain multiple MusicXML files as well as other media files for images and sound. The container element is the 7 | * document element for the META-INF/container.xml file. The container describes the starting point for the MusicXML 8 | * version of the file, as well as alternate renditions such as PDF and audio versions of the musical score. 9 | * 10 | * See https://www.w3.org/2021/06/musicxml40/container-reference/elements/container/. 11 | */ 12 | export class Container { 13 | constructor(private element: NamedElement<'container'>) {} 14 | 15 | /** Returns the rootfiles of the container. Defaults to empty array. */ 16 | getRootfiles(): Rootfile[] { 17 | return this.element.all('rootfile').map((node) => new Rootfile(node)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/mxl/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mxl'; 2 | export * from './container'; 3 | export * from './rootfile'; 4 | -------------------------------------------------------------------------------- /src/mxl/rootfile.ts: -------------------------------------------------------------------------------- 1 | import { NamedElement } from '@/util'; 2 | 3 | /** 4 | * The `<rootfile>` element describes each top-level file in the MusicXML container. 5 | * 6 | * See https://www.w3.org/2021/06/musicxml40/container-reference/elements/rootfile/. 7 | */ 8 | export class Rootfile { 9 | constructor(private element: NamedElement<'rootfile'>) {} 10 | 11 | /** Returns the full path of the root file. Defaults to empty string. */ 12 | getFullPath(): string { 13 | return this.element.attr('full-path').withDefault('').str(); 14 | } 15 | 16 | /** Returns the media type of the root file. Defaults to 'application/vnd.recordare.musicxml+xml'. */ 17 | getMediaType(): string { 18 | return this.element.attr('media-type').withDefault('application/vnd.recordare.musicxml+xml').str(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/parsing/README.md: -------------------------------------------------------------------------------- 1 | # parsing 2 | 3 | ## Intent 4 | 5 | ### Goals 6 | 7 | - **DO** transform any arbitrary data to a vexml data document. 8 | 9 | ### Non-goals 10 | 11 | - **DO NOT** create vexflow objects. 12 | -------------------------------------------------------------------------------- /src/parsing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mxl'; 2 | export * from './musicxml'; 3 | -------------------------------------------------------------------------------- /src/parsing/musicxml/accidental.ts: -------------------------------------------------------------------------------- 1 | import * as data from '@/data'; 2 | import { VoiceEntryContext } from './contexts'; 3 | import { Config } from '@/config'; 4 | import { Logger } from '@/debug'; 5 | 6 | export class Accidental { 7 | constructor( 8 | private config: Config, 9 | private log: Logger, 10 | public readonly code: data.AccidentalCode, 11 | public readonly isCautionary: boolean 12 | ) {} 13 | 14 | parse(voiceEntryCtx: VoiceEntryContext): data.Accidental { 15 | voiceEntryCtx.setActiveAccidental(this.code); 16 | 17 | return { 18 | type: 'accidental', 19 | code: this.code, 20 | isCautionary: this.isCautionary, 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/parsing/musicxml/annotation.ts: -------------------------------------------------------------------------------- 1 | import * as data from '@/data'; 2 | import * as musicxml from '@/musicxml'; 3 | import { TextStateMachine } from './textstatemachine'; 4 | import { Config } from '@/config'; 5 | import { Logger } from '@/debug'; 6 | 7 | export class Annotation { 8 | constructor( 9 | private config: Config, 10 | private log: Logger, 11 | private text: string, 12 | private horizontalJustification: data.AnnotationHorizontalJustification | null, 13 | private verticalJustification: data.AnnotationVerticalJustification | null 14 | ) {} 15 | 16 | static fromLyric(config: Config, log: Logger, musicXML: { lyric: musicxml.Lyric }): Annotation { 17 | const machine = new TextStateMachine(); 18 | for (const component of musicXML.lyric.getComponents()) { 19 | machine.process(component); 20 | } 21 | return new Annotation(config, log, machine.getText(), 'center', 'bottom'); 22 | } 23 | 24 | parse(): data.Annotation { 25 | return { 26 | type: 'annotation', 27 | text: this.text, 28 | horizontalJustification: this.horizontalJustification, 29 | verticalJustification: this.verticalJustification, 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/parsing/musicxml/beam.ts: -------------------------------------------------------------------------------- 1 | import * as musicxml from '@/musicxml'; 2 | import { VoiceEntryContext } from './contexts'; 3 | import { Config } from '@/config'; 4 | import { Logger } from '@/debug'; 5 | 6 | type BeamPhase = 'start' | 'continue'; 7 | 8 | export class Beam { 9 | constructor(private config: Config, private log: Logger, private phase: BeamPhase) {} 10 | 11 | static create(config: Config, log: Logger, musicXML: { beam: musicxml.Beam }): Beam { 12 | let phase: BeamPhase; 13 | switch (musicXML.beam.getBeamValue()) { 14 | case 'begin': 15 | phase = 'start'; 16 | break; 17 | default: 18 | phase = 'continue'; 19 | break; 20 | } 21 | 22 | return new Beam(config, log, phase); 23 | } 24 | 25 | parse(voiceEntryCtx: VoiceEntryContext): string { 26 | if (this.phase === 'start') { 27 | return voiceEntryCtx.beginBeam(); 28 | } 29 | return voiceEntryCtx.continueBeam() ?? voiceEntryCtx.beginBeam(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/parsing/musicxml/bend.ts: -------------------------------------------------------------------------------- 1 | import * as data from '@/data'; 2 | import * as musicxml from '@/musicxml'; 3 | import { Config } from '@/config'; 4 | import { Logger } from '@/debug'; 5 | 6 | export class Bend { 7 | private constructor( 8 | private config: Config, 9 | private log: Logger, 10 | private bendType: data.BendType, 11 | private semitones: number 12 | ) {} 13 | 14 | static create(config: Config, log: Logger, musicXML: { bend: musicxml.Bend }): Bend { 15 | const semitones = musicXML.bend.getAlter(); 16 | 17 | let bendType: data.BendType; 18 | switch (musicXML.bend.getType()) { 19 | case 'pre-bend': 20 | bendType = 'prebend'; 21 | break; 22 | case 'release': 23 | bendType = 'release'; 24 | break; 25 | default: 26 | bendType = 'normal'; 27 | break; 28 | } 29 | 30 | return new Bend(config, log, bendType, semitones); 31 | } 32 | 33 | parse(): data.Bend { 34 | return { 35 | type: 'bend', 36 | bendType: this.bendType, 37 | semitones: this.semitones, 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/parsing/musicxml/dynamics.ts: -------------------------------------------------------------------------------- 1 | import * as data from '@/data'; 2 | import * as util from '@/util'; 3 | import { Fraction } from './fraction'; 4 | import { DynamicType } from './enums'; 5 | import { Config } from '@/config'; 6 | import { Logger } from '@/debug'; 7 | 8 | export class Dynamics { 9 | constructor( 10 | private config: Config, 11 | private log: Logger, 12 | private measureBeat: util.Fraction, 13 | private dynamicType: DynamicType 14 | ) {} 15 | 16 | parse(): data.Dynamics { 17 | const duration = new Fraction(util.Fraction.zero()).parse(); 18 | const measureBeat = new Fraction(this.measureBeat).parse(); 19 | 20 | return { 21 | type: 'dynamics', 22 | duration, 23 | dynamicType: this.dynamicType, 24 | measureBeat, 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/parsing/musicxml/fraction.ts: -------------------------------------------------------------------------------- 1 | import * as data from '@/data'; 2 | import * as util from '@/util'; 3 | 4 | export class Fraction { 5 | constructor(private fraction: util.Fraction) {} 6 | 7 | reify(): util.Fraction { 8 | return this.fraction; 9 | } 10 | 11 | parse(): data.Fraction { 12 | return { 13 | type: 'fraction', 14 | numerator: this.fraction.numerator, 15 | denominator: this.fraction.denominator, 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/parsing/musicxml/fragment.ts: -------------------------------------------------------------------------------- 1 | import * as data from '@/data'; 2 | import { Part } from './part'; 3 | import { Signature } from './signature'; 4 | import { StaveEvent } from './types'; 5 | import { FragmentContext, MeasureContext } from './contexts'; 6 | import { Config } from '@/config'; 7 | import { Logger } from '@/debug'; 8 | 9 | export class Fragment { 10 | private constructor( 11 | private config: Config, 12 | private log: Logger, 13 | private signature: Signature, 14 | private parts: Part[] 15 | ) {} 16 | 17 | static create(config: Config, log: Logger, signature: Signature, events: StaveEvent[], partIds: string[]) { 18 | const parts = partIds.map((partId) => 19 | Part.create( 20 | config, 21 | log, 22 | partId, 23 | signature, 24 | events.filter((e) => e.partId === partId) 25 | ) 26 | ); 27 | 28 | return new Fragment(config, log, signature, parts); 29 | } 30 | 31 | getSignature(): Signature { 32 | return this.signature; 33 | } 34 | 35 | parse(ctx: MeasureContext): data.Fragment { 36 | const fragmentCtx = new FragmentContext(ctx, this.signature); 37 | 38 | return { 39 | type: 'fragment', 40 | kind: 'musical', 41 | minWidth: null, 42 | signature: this.signature.asFragmentSignature().parse(), 43 | parts: this.parts.map((part) => part.parse(fragmentCtx)), 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/parsing/musicxml/fragmentsignature.ts: -------------------------------------------------------------------------------- 1 | import * as data from '@/data'; 2 | import { Metronome } from './metronome'; 3 | import { Config } from '@/config'; 4 | import { Logger } from '@/debug'; 5 | 6 | export class FragmentSignature { 7 | constructor(private config: Config, private log: Logger, private metronome: Metronome) {} 8 | 9 | parse(): data.FragmentSignature { 10 | return { 11 | type: 'fragmentsignature', 12 | metronome: this.metronome.parse(), 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/parsing/musicxml/idprovider.ts: -------------------------------------------------------------------------------- 1 | export class IdProvider { 2 | private id = 1; 3 | 4 | next(): string { 5 | return `${this.id++}`; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/parsing/musicxml/index.ts: -------------------------------------------------------------------------------- 1 | export * from './musicxmlparser'; 2 | -------------------------------------------------------------------------------- /src/parsing/musicxml/multirest.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@/config'; 2 | import { Logger } from '@/debug'; 3 | 4 | export class MultiRest { 5 | constructor(private config: Config, private log: Logger, private measureCount: number) {} 6 | 7 | getMeasureCount(): number { 8 | return this.measureCount; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/parsing/musicxml/musicxmlparser.ts: -------------------------------------------------------------------------------- 1 | import * as data from '@/data'; 2 | import * as musicxml from '@/musicxml'; 3 | import * as errors from '@/errors'; 4 | import { Score } from './score'; 5 | import { Config, DEFAULT_CONFIG } from '@/config'; 6 | import { Logger, NoopLogger } from '@/debug'; 7 | 8 | export type MusicXMLParserOptions = { 9 | config?: Partial<Config>; 10 | logger?: Logger; 11 | }; 12 | 13 | export class MusicXMLParser { 14 | private config: Config; 15 | private log: Logger; 16 | 17 | constructor(opts?: MusicXMLParserOptions) { 18 | this.config = { ...DEFAULT_CONFIG, ...opts?.config }; 19 | this.log = opts?.logger ?? new NoopLogger(); 20 | } 21 | 22 | /** Parses a MusicXML source into a vexml data document. */ 23 | parse(musicXMLSrc: string | Document): data.Document { 24 | let musicXML: musicxml.MusicXML; 25 | if (musicXMLSrc instanceof Document) { 26 | musicXML = new musicxml.MusicXML(musicXMLSrc); 27 | } else if (typeof musicXMLSrc === 'string') { 28 | musicXML = new musicxml.MusicXML(new DOMParser().parseFromString(musicXMLSrc, 'application/xml')); 29 | } else { 30 | throw new errors.VexmlError(`Invalid source type: ${musicXMLSrc}`); 31 | } 32 | 33 | const scorePartwise = musicXML.getScorePartwise(); 34 | const score = Score.create(this.config, this.log, { scorePartwise }).parse(); 35 | 36 | return new data.Document(score); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/parsing/musicxml/octaveshift.ts: -------------------------------------------------------------------------------- 1 | import * as musicxml from '@/musicxml'; 2 | import { VoiceContext } from './contexts'; 3 | import { Config } from '@/config'; 4 | import { Logger } from '@/debug'; 5 | 6 | type OctaveShiftPhase = 'start' | 'continue' | 'stop'; 7 | 8 | export class OctaveShift { 9 | private constructor( 10 | private config: Config, 11 | private log: Logger, 12 | private phase: OctaveShiftPhase, 13 | private size: number 14 | ) {} 15 | 16 | static create(config: Config, log: Logger, musicXML: { octaveShift: musicxml.OctaveShift }): OctaveShift { 17 | const type = musicXML.octaveShift.getType(); 18 | const factor = type === 'down' ? -1 : 1; 19 | const size = factor * musicXML.octaveShift.getSize(); 20 | 21 | let phase: OctaveShiftPhase; 22 | switch (type) { 23 | case 'down': 24 | phase = 'start'; 25 | break; 26 | case 'up': 27 | phase = 'start'; 28 | break; 29 | case 'stop': 30 | phase = 'stop'; 31 | break; 32 | default: 33 | phase = 'continue'; 34 | break; 35 | } 36 | 37 | return new OctaveShift(config, log, phase, size); 38 | } 39 | 40 | parse(voiceCtx: VoiceContext): void { 41 | if (this.phase === 'start') { 42 | voiceCtx.beginOctaveShift(this.size); 43 | } 44 | 45 | if (this.phase === 'stop') { 46 | voiceCtx.closeOctaveShift(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/parsing/musicxml/partsignature.ts: -------------------------------------------------------------------------------- 1 | import * as data from '@/data'; 2 | import { StaveCount } from './stavecount'; 3 | import { Config } from '@/config'; 4 | import { Logger } from '@/debug'; 5 | 6 | export class PartSignature { 7 | constructor(private config: Config, private log: Logger, private staveCount: StaveCount) {} 8 | 9 | parse(): data.PartSignature { 10 | return { 11 | type: 'partsignature', 12 | staveCount: this.staveCount.getValue(), 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/parsing/musicxml/pitch.ts: -------------------------------------------------------------------------------- 1 | import * as data from '@/data'; 2 | import { Config } from '@/config'; 3 | import { Logger } from '@/debug'; 4 | 5 | export class Pitch { 6 | constructor(private config: Config, private log: Logger, private step: string, private octave: number) {} 7 | 8 | getStep(): string { 9 | return this.step; 10 | } 11 | 12 | getOctave(): number { 13 | return this.octave; 14 | } 15 | 16 | parse(): data.Pitch { 17 | return { 18 | type: 'pitch', 19 | step: this.step, 20 | octave: this.octave, 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/parsing/musicxml/stavecount.ts: -------------------------------------------------------------------------------- 1 | import { Config } from '@/config'; 2 | import { Logger } from '@/debug'; 3 | 4 | export class StaveCount { 5 | constructor(private config: Config, private log: Logger, private partId: string, private value: number) {} 6 | 7 | static default(config: Config, log: Logger, partId: string): StaveCount { 8 | return new StaveCount(config, log, partId, 1); 9 | } 10 | 11 | getPartId(): string { 12 | return this.partId; 13 | } 14 | 15 | getValue(): number { 16 | return this.value; 17 | } 18 | 19 | isEqual(staveCount: StaveCount): boolean { 20 | return this.partId === staveCount.partId && this.isEquivalent(staveCount); 21 | } 22 | 23 | isEquivalent(staveCount: StaveCount): boolean { 24 | return this.value === staveCount.value; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/parsing/musicxml/stavesignature.ts: -------------------------------------------------------------------------------- 1 | import * as data from '@/data'; 2 | import { Clef } from './clef'; 3 | import { Key } from './key'; 4 | import { StaveLineCount } from './stavelinecount'; 5 | import { Time } from './time'; 6 | import { Config } from '@/config'; 7 | import { Logger } from '@/debug'; 8 | 9 | export class StaveSignature { 10 | constructor( 11 | private config: Config, 12 | private log: Logger, 13 | private staveLineCount: StaveLineCount, 14 | private clef: Clef, 15 | private key: Key, 16 | private time: Time 17 | ) {} 18 | 19 | parse(): data.StaveSignature { 20 | return { 21 | type: 'stavesignature', 22 | lineCount: this.staveLineCount.getValue(), 23 | clef: this.clef.parse(), 24 | key: this.key.parse(), 25 | time: this.time.parse(), 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/parsing/musicxml/tuplet.ts: -------------------------------------------------------------------------------- 1 | import * as musicxml from '@/musicxml'; 2 | import * as data from '@/data'; 3 | import { VoiceEntryContext } from './contexts'; 4 | import { Config } from '@/config'; 5 | import { Logger } from '@/debug'; 6 | 7 | type TupletPhase = 'start' | 'stop'; 8 | 9 | export class Tuplet { 10 | constructor( 11 | private config: Config, 12 | private log: Logger, 13 | private number: number, 14 | private phase: TupletPhase, 15 | private showNumber: boolean, 16 | private placement: data.TupletPlacement 17 | ) {} 18 | 19 | static create(config: Config, log: Logger, musicXML: { tuplet: musicxml.Tuplet }): Tuplet { 20 | let phase: TupletPhase; 21 | switch (musicXML.tuplet.getType()) { 22 | case 'start': 23 | phase = 'start'; 24 | break; 25 | default: 26 | phase = 'stop'; 27 | break; 28 | } 29 | 30 | const number = musicXML.tuplet.getNumber(); 31 | const showNumber = musicXML.tuplet.getShowNumber() === 'both'; 32 | const placement = musicXML.tuplet.getPlacement(); 33 | 34 | return new Tuplet(config, log, number, phase, showNumber, placement); 35 | } 36 | 37 | parse(voiceEntryCtx: VoiceEntryContext): string | null { 38 | if (this.phase === 'start') { 39 | return voiceEntryCtx.beginTuplet(this.number, this.showNumber, this.placement); 40 | } 41 | return voiceEntryCtx.closeTuplet(this.number); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/parsing/musicxml/vibrato.ts: -------------------------------------------------------------------------------- 1 | import * as musicxml from '@/musicxml'; 2 | import { VoiceEntryContext } from './contexts'; 3 | import { Config } from '@/config'; 4 | import { Logger } from '@/debug'; 5 | 6 | type VibratoPhase = 'start' | 'continue'; 7 | 8 | export class Vibrato { 9 | private constructor( 10 | private config: Config, 11 | private log: Logger, 12 | private number: number, 13 | private phase: VibratoPhase 14 | ) {} 15 | 16 | static create(config: Config, log: Logger, musicXML: { wavyLine: musicxml.WavyLine }): Vibrato { 17 | let phase: VibratoPhase; 18 | switch (musicXML.wavyLine.getType()) { 19 | case 'start': 20 | phase = 'start'; 21 | break; 22 | default: 23 | phase = 'continue'; 24 | break; 25 | } 26 | 27 | const number = musicXML.wavyLine.getNumber(); 28 | 29 | return new Vibrato(config, log, number, phase); 30 | } 31 | 32 | parse(voiceEntryCtx: VoiceEntryContext): string { 33 | if (this.phase === 'start') { 34 | return voiceEntryCtx.beginVibrato(this.number); 35 | } else { 36 | return voiceEntryCtx.continueVibrato(this.number) ?? voiceEntryCtx.beginVibrato(this.number); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/parsing/mxl/index.ts: -------------------------------------------------------------------------------- 1 | export * from './mxlparser'; 2 | -------------------------------------------------------------------------------- /src/parsing/mxl/mxlparser.ts: -------------------------------------------------------------------------------- 1 | import * as mxl from '@/mxl'; 2 | import { MusicXMLParser } from '@/parsing/musicxml'; 3 | import { Document } from '@/data'; 4 | import { Config, DEFAULT_CONFIG } from '@/config'; 5 | import { Logger, NoopLogger } from '@/debug'; 6 | 7 | export type MXLParserOptions = { 8 | config?: Partial<Config>; 9 | logger?: Logger; 10 | }; 11 | 12 | /** Parses an MXL blob. */ 13 | export class MXLParser { 14 | private config: Config; 15 | private log: Logger; 16 | 17 | constructor(opts?: MXLParserOptions) { 18 | this.config = { ...DEFAULT_CONFIG, ...opts?.config }; 19 | this.log = opts?.logger ?? new NoopLogger(); 20 | } 21 | 22 | async parse(blob: Blob): Promise<Document> { 23 | const musicXML = await this.raw(blob); 24 | const musicXMLParser = new MusicXMLParser({ config: this.config, logger: this.log }); 25 | return musicXMLParser.parse(musicXML); 26 | } 27 | 28 | /** Returns the MusicXML document as a string. */ 29 | async raw(blob: Blob): Promise<string> { 30 | return new mxl.MXL(blob).getMusicXML(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/playback/README.md: -------------------------------------------------------------------------------- 1 | # playback 2 | 3 | ## Intent 4 | 5 | ### Goals 6 | 7 | - **DO** house the logic for constructing valid playback sequences. 8 | - **DO** provide data structures to discretely navigate a rendering. 9 | - **DO** expose tools to convert between ticks and time. 10 | 11 | ### Non-goals 12 | 13 | - **DO NOT** contain cursor-specific implementations. 14 | - **DO NOT** provide the machinery to interpolate between playback steps. 15 | - **DO NOT** commit a sequence to a prescribed bpm. 16 | -------------------------------------------------------------------------------- /src/playback/bsearchcursorframelocator.ts: -------------------------------------------------------------------------------- 1 | import * as util from '@/util'; 2 | import { CursorFrameLocator } from './types'; 3 | import { Duration } from './duration'; 4 | import { CursorPath } from './cursorpath'; 5 | 6 | /** 7 | * A CursorFrameLocator that uses binary search to locate the frame at a given time. 8 | */ 9 | export class BSearchCursorFrameLocator implements CursorFrameLocator { 10 | constructor(private path: CursorPath) {} 11 | 12 | locate(time: Duration): number | null { 13 | const frames = this.path.getFrames(); 14 | 15 | let left = 0; 16 | let right = frames.length - 1; 17 | 18 | while (left <= right) { 19 | const mid = Math.floor((left + right) / 2); 20 | const entry = frames.at(mid); 21 | 22 | util.assertDefined(entry); 23 | 24 | if (entry.tRange.includes(time)) { 25 | return mid; 26 | } 27 | 28 | if (entry.tRange.end.isGreaterThanOrEqual(time)) { 29 | right = mid - 1; 30 | } else { 31 | left = mid + 1; 32 | } 33 | } 34 | 35 | return null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/playback/cursorpath.ts: -------------------------------------------------------------------------------- 1 | import { CursorFrame } from './types'; 2 | 3 | /** A collection of cursor frames for a given part index.. */ 4 | export class CursorPath { 5 | constructor(private partIndex: number, private frames: CursorFrame[]) {} 6 | 7 | getPartIndex(): number { 8 | return this.partIndex; 9 | } 10 | 11 | getFrames(): CursorFrame[] { 12 | return this.frames; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/playback/durationrange.ts: -------------------------------------------------------------------------------- 1 | import * as util from '@/util'; 2 | import { Duration } from './duration'; 3 | 4 | export class DurationRange { 5 | private numberRange: util.NumberRange; 6 | 7 | constructor(start: Duration, end: Duration) { 8 | this.numberRange = new util.NumberRange(start.ms, end.ms); 9 | } 10 | 11 | public get start(): Duration { 12 | return Duration.ms(this.numberRange.start); 13 | } 14 | 15 | public get end(): Duration { 16 | return Duration.ms(this.numberRange.end); 17 | } 18 | 19 | getSize(): Duration { 20 | return Duration.ms(this.numberRange.getSize()); 21 | } 22 | 23 | includes(duration: Duration): boolean { 24 | return this.numberRange.includes(duration.ms); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/playback/emptycursorframe.ts: -------------------------------------------------------------------------------- 1 | import { NumberRange } from '@/util'; 2 | import { Duration } from './duration'; 3 | import { DurationRange } from './durationrange'; 4 | import { CursorFrame } from './types'; 5 | 6 | export class EmptyCursorFrame implements CursorFrame { 7 | tRange = new DurationRange(Duration.zero(), Duration.zero()); 8 | xRange = new NumberRange(0, 0); 9 | yRange = new NumberRange(0, 0); 10 | 11 | getActiveElements() { 12 | return []; 13 | } 14 | 15 | toHumanReadable(): string[] { 16 | return ['[empty]']; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/playback/hintdescriber.ts: -------------------------------------------------------------------------------- 1 | import { ElementDescriber } from './elementdescriber'; 2 | import { CursorStateHint } from './types'; 3 | 4 | export class HintDescriber { 5 | constructor(private elementDescriber: ElementDescriber) {} 6 | 7 | static noop(): HintDescriber { 8 | return new HintDescriber(ElementDescriber.noop()); 9 | } 10 | 11 | describe(hint: CursorStateHint): string { 12 | switch (hint.type) { 13 | case 'start': 14 | return `start(${this.elementDescriber.describe(hint.element)})`; 15 | case 'stop': 16 | return `stop(${this.elementDescriber.describe(hint.element)})`; 17 | case 'retrigger': 18 | return `retrigger(${this.elementDescriber.describe(hint.untriggerElement)}, ${this.elementDescriber.describe( 19 | hint.retriggerElement 20 | )})`; 21 | case 'sustain': 22 | return `sustain(${this.elementDescriber.describe(hint.previousElement)}, ${this.elementDescriber.describe( 23 | hint.currentElement 24 | )})`; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/playback/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cursor'; 2 | export * from './cursorpath'; 3 | export * from './duration'; 4 | export * from './durationrange'; 5 | export * from './timestamplocator'; 6 | export * from './timeline'; 7 | export * from './types'; 8 | export * from './defaultcursorframe'; 9 | export * from './lazycursorstatehintprovider'; 10 | export * from './emptycursorframe'; 11 | export * from './elementdescriber'; 12 | export * from './hintdescriber'; 13 | -------------------------------------------------------------------------------- /src/playback/scroller.ts: -------------------------------------------------------------------------------- 1 | import * as spatial from '@/spatial'; 2 | 3 | const SCROLLER_HORIZONTAL_PADDING = 20; 4 | const SCROLLER_VERTICAL_PADDING = 20; 5 | 6 | export class Scroller { 7 | constructor(private scrollContainer: HTMLElement) {} 8 | 9 | isFullyVisible(rect: spatial.Rect): boolean { 10 | const visibleRect = this.getVisibleRect(); 11 | return ( 12 | rect.x >= visibleRect.x && 13 | rect.y >= visibleRect.y && 14 | rect.x + rect.w <= visibleRect.x + visibleRect.w && 15 | rect.y + rect.h <= visibleRect.y + visibleRect.h 16 | ); 17 | } 18 | 19 | scrollTo(position: spatial.Point, behavior: ScrollBehavior = 'auto') { 20 | if (!this.isAt(position)) { 21 | this.scrollContainer.scrollTo({ 22 | top: position.y - SCROLLER_VERTICAL_PADDING, 23 | left: position.x - SCROLLER_HORIZONTAL_PADDING, 24 | behavior, 25 | }); 26 | } 27 | } 28 | 29 | private isAt(position: spatial.Point): boolean { 30 | return this.scrollContainer.scrollLeft === position.x && this.scrollContainer.scrollTop === position.y; 31 | } 32 | 33 | private getVisibleRect(): spatial.Rect { 34 | const scrollLeft = this.scrollContainer.scrollLeft; 35 | const scrollTop = this.scrollContainer.scrollTop; 36 | const scrollWidth = this.scrollContainer.clientWidth; 37 | const scrollHeight = this.scrollContainer.clientHeight; 38 | return new spatial.Rect(scrollLeft, scrollTop, scrollWidth, scrollHeight); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/rendering/README.md: -------------------------------------------------------------------------------- 1 | # rendering 2 | 3 | ## Intent 4 | 5 | ### Goals 6 | 7 | - **DO** transform a vexml data document to rendering engine output. 8 | 9 | ### Non-goals 10 | 11 | - **DO NOT** provide an interface for direct vexml user consumption. 12 | -------------------------------------------------------------------------------- /src/rendering/beam.ts: -------------------------------------------------------------------------------- 1 | import * as util from '@/util'; 2 | import * as vexflow from 'vexflow'; 3 | import { Config } from '@/config'; 4 | import { Logger } from '@/debug'; 5 | import { Rect } from '@/spatial'; 6 | import { Document } from './document'; 7 | import { BeamKey, BeamRender } from './types'; 8 | 9 | interface VexflowStemmableNoteRegistry { 10 | get(beamId: string): vexflow.StemmableNote[] | undefined; 11 | } 12 | 13 | export class Beam { 14 | constructor( 15 | private config: Config, 16 | private log: Logger, 17 | private document: Document, 18 | private key: BeamKey, 19 | private registry: VexflowStemmableNoteRegistry 20 | ) {} 21 | 22 | render(): BeamRender { 23 | const beam = this.document.getBeam(this.key); 24 | const vexflowStemmableNotes = this.registry.get(beam.id); 25 | util.assertDefined(vexflowStemmableNotes); 26 | util.assert(vexflowStemmableNotes.length > 1, 'Beam must have more than one voice entry'); 27 | 28 | const vexflowBeams = [new vexflow.Beam(vexflowStemmableNotes)]; 29 | 30 | return { 31 | type: 'beam', 32 | rect: Rect.empty(), 33 | key: this.key, 34 | vexflowBeams, 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/rendering/budget.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A generic utility for tracking a metric across scopes. 3 | */ 4 | export class Budget { 5 | private amount = 0; 6 | 7 | constructor(initial: number) { 8 | this.amount = initial; 9 | } 10 | 11 | static unlimited() { 12 | return new Budget(Infinity); 13 | } 14 | 15 | isUnlimited(): boolean { 16 | return this.amount === Infinity; 17 | } 18 | 19 | remaining(): number { 20 | return this.amount; 21 | } 22 | 23 | spend(amount: number): void { 24 | this.amount -= amount; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/rendering/clef.ts: -------------------------------------------------------------------------------- 1 | import * as vexflow from 'vexflow'; 2 | import { ClefRender, StaveKey } from './types'; 3 | import { Config } from '@/config'; 4 | import { Logger } from '@/debug'; 5 | import { Document } from './document'; 6 | 7 | const ADDITIONAL_CLEF_WIDTH = 10; 8 | 9 | export class Clef { 10 | constructor(private config: Config, private log: Logger, private document: Document, private key: StaveKey) {} 11 | 12 | render(): ClefRender { 13 | const clef = this.document.getStave(this.key).signature.clef; 14 | 15 | let annotation: string | undefined; 16 | if (clef.octaveShift) { 17 | const direction = clef.octaveShift > 0 ? 'va' : 'vb'; 18 | annotation = `8${direction}`; 19 | } 20 | 21 | const vexflowClef = new vexflow.Clef(clef.sign, 'default', annotation); 22 | const width = vexflowClef.getWidth() + ADDITIONAL_CLEF_WIDTH; 23 | 24 | return { 25 | type: 'clef', 26 | key: this.key, 27 | width, 28 | sign: clef.sign, 29 | vexflowClef, 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/rendering/dynamics.ts: -------------------------------------------------------------------------------- 1 | import * as vexflow from 'vexflow'; 2 | import { Config } from '@/config'; 3 | import { Logger } from '@/debug'; 4 | import { Rect } from '@/spatial'; 5 | import { Document } from './document'; 6 | import { DynamicsRender, VoiceEntryKey } from './types'; 7 | 8 | export class Dynamics { 9 | constructor(private config: Config, private log: Logger, private document: Document, private key: VoiceEntryKey) {} 10 | 11 | render(): DynamicsRender { 12 | const dynamics = this.document.getDynamics(this.key); 13 | 14 | const vexflowTextDynamics = new vexflow.TextDynamics({ 15 | text: dynamics.dynamicType, 16 | duration: '4', 17 | }); 18 | 19 | return { 20 | type: 'dynamics', 21 | key: this.key, 22 | rect: Rect.empty(), 23 | dynamicType: dynamics.dynamicType, 24 | vexflowNote: vexflowTextDynamics, 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/rendering/index.ts: -------------------------------------------------------------------------------- 1 | export * from './score'; 2 | export * from './renderer'; 3 | export * from './types'; 4 | export * from './document'; 5 | export * from './debugrect'; 6 | export * from './enums'; 7 | -------------------------------------------------------------------------------- /src/rendering/textmeasurer.ts: -------------------------------------------------------------------------------- 1 | import * as util from '@/util'; 2 | import { LabelFont } from './label'; 3 | 4 | export class TextMeasurer { 5 | constructor(private font: LabelFont) {} 6 | 7 | measure(text: string) { 8 | const ctx = document.createElement('canvas').getContext('2d'); 9 | util.assertNotNull(ctx); 10 | 11 | const fontSize = this.font.size || '16px'; 12 | const fontFamily = this.font.family || 'Arial, sans-serif'; 13 | ctx.font = `${fontSize} ${fontFamily}`; 14 | 15 | const metrics = ctx.measureText(text); 16 | 17 | return { 18 | width: metrics.width, 19 | approximateHeight: metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent, 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/schema/README.md: -------------------------------------------------------------------------------- 1 | # schema 2 | 3 | ## Intent 4 | 5 | ### Goals 6 | 7 | - **DO** reify TypeScript types so that vexml users can create dynamic sites that consume the config (e.g. [vexml.dev](https://vexml.dev)). 8 | 9 | ### Non-goals 10 | 11 | - **DO NOT** declare config. 12 | -------------------------------------------------------------------------------- /src/schema/index.ts: -------------------------------------------------------------------------------- 1 | export * from './t'; 2 | -------------------------------------------------------------------------------- /src/spatial/README.md: -------------------------------------------------------------------------------- 1 | # spatial 2 | 3 | ## Intent 4 | 5 | ### Goals 6 | 7 | - **DO** provide music-agnostic utility classes for spatial contexts. 8 | - **DO** encapsulate spatial algorithms. 9 | - **DO** support 2D contexts. 10 | 11 | ### Non-goals 12 | 13 | - **DO NOT** provide music-engraving-specific solutions. 14 | - **DO NOT** have any references to vexflow. 15 | -------------------------------------------------------------------------------- /src/spatial/circle.ts: -------------------------------------------------------------------------------- 1 | import { Point } from './point'; 2 | import { Shape } from './types'; 3 | 4 | export class Circle implements Shape { 5 | constructor( 6 | /** center x-coordinate */ 7 | public readonly x: number, 8 | /** center y-coordinate */ 9 | public readonly y: number, 10 | /** radius */ 11 | public readonly r: number 12 | ) {} 13 | 14 | center(): Point { 15 | return new Point(this.x, this.y); 16 | } 17 | 18 | contains(point: Point): boolean { 19 | return this.center().distance(point) <= this.r; 20 | } 21 | 22 | left() { 23 | return this.x - this.r; 24 | } 25 | 26 | right() { 27 | return this.x + this.r; 28 | } 29 | 30 | top() { 31 | return this.y - this.r; 32 | } 33 | 34 | bottom() { 35 | return this.y + this.r; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/spatial/index.ts: -------------------------------------------------------------------------------- 1 | export * from './point'; 2 | export * from './circle'; 3 | export * from './types'; 4 | export * from './quadtree'; 5 | export * from './rect'; 6 | -------------------------------------------------------------------------------- /src/spatial/point.ts: -------------------------------------------------------------------------------- 1 | /** Represents a point in 2D space. */ 2 | export class Point { 3 | constructor(public readonly x: number, public readonly y: number) {} 4 | 5 | static origin(): Point { 6 | return new Point(0, 0); 7 | } 8 | 9 | distance(other: Point): number { 10 | return Math.sqrt((this.x - other.x) ** 2 + (this.y - other.y) ** 2); 11 | } 12 | 13 | isEqual(other: Point): boolean { 14 | return this.x === other.x && this.y === other.y; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/spatial/types.ts: -------------------------------------------------------------------------------- 1 | import { Point } from './point'; 2 | 3 | /** Represents a 2D shape in a spatial system. */ 4 | export interface Shape { 5 | /** Checks if the shape contains a given point. */ 6 | contains(point: Point): boolean; 7 | 8 | /** Returns the minimum X. */ 9 | left(): number; 10 | 11 | /** Returns the maximum X. */ 12 | right(): number; 13 | 14 | /** Returns the minimum Y. */ 15 | top(): number; 16 | 17 | /** Returns the maximum Y. */ 18 | bottom(): number; 19 | } 20 | -------------------------------------------------------------------------------- /src/util/assert.ts: -------------------------------------------------------------------------------- 1 | export function assertNotNull<T>(value: T, msg?: string): asserts value is Exclude<T, null> { 2 | if (value === null) { 3 | throw new Error(msg ?? `expected value to be present, got: ${value}`); 4 | } 5 | } 6 | 7 | export function assertNull(value: any, msg?: string): asserts value is null { 8 | if (value !== null) { 9 | throw new Error(msg ?? `expected value to be null, got: ${value}`); 10 | } 11 | } 12 | 13 | export function assertDefined<T>(value: T, msg?: string): asserts value is Exclude<T, undefined> { 14 | if (typeof value === 'undefined') { 15 | throw new Error(msg ?? `expected value to be defined, got: ${value}`); 16 | } 17 | } 18 | 19 | export function assert(condition: any, msg?: string): asserts condition { 20 | if (!condition) { 21 | throw new Error(msg ?? 'assertion failed'); 22 | } 23 | } 24 | 25 | export function assertUnreachable(): never { 26 | throw new Error('expected to be unreachable'); 27 | } 28 | -------------------------------------------------------------------------------- /src/util/enum.ts: -------------------------------------------------------------------------------- 1 | type Values<T extends readonly any[]> = T extends readonly (infer U)[] ? U : never; 2 | 3 | export type EnumValues<T extends Enum<any>> = T extends Enum<infer U> ? Values<U> : never; 4 | 5 | /** An enumeration of string values. */ 6 | export class Enum<T extends readonly string[]> { 7 | constructor(public readonly values: T) {} 8 | 9 | /** Type predicate that returns whether or not the value is one of the choices. */ 10 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 11 | includes(value: any): value is Values<T> { 12 | return this.values.includes(value); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from './enum'; 2 | export * from './namedelement'; 3 | export * from './value'; 4 | export * as xml from './xml'; 5 | export * from './math'; 6 | export * from './decorators'; 7 | export * from './array'; 8 | export * from './fraction'; 9 | export * from './assert'; 10 | export * from './lru'; 11 | export * from './device'; 12 | export * from './numberrange'; 13 | export * from './stack'; 14 | export * from './object'; 15 | -------------------------------------------------------------------------------- /src/util/lru.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a least-recently-used (LRU) cache. 3 | * 4 | * When at capacity, this cache will evict the least recently used key-value pair. This is particularly useful if you 5 | * don't intend a cache to grow in size indefinitely. 6 | */ 7 | export class LRU<K, V> { 8 | private capacity: number; 9 | private map: Map<K, V>; 10 | 11 | constructor(capacity: number) { 12 | this.capacity = capacity; 13 | this.map = new Map<K, V>(); 14 | } 15 | 16 | /** Returns the value corresponding to the key, if it exists. Defaults to null. */ 17 | get(key: K): V | null { 18 | let value = null; 19 | if (this.has(key)) { 20 | value = this.map.get(key)!; 21 | this.map.delete(key); 22 | this.map.set(key, value as V); 23 | } 24 | return value; 25 | } 26 | 27 | /** Puts the key-value pair, updating how recently "used" a key was used if applicable. */ 28 | put(key: K, value: V): void { 29 | if (this.map.has(key)) { 30 | this.map.delete(key); 31 | } 32 | 33 | this.map.set(key, value); 34 | 35 | if (this.map.size > this.capacity) { 36 | const { value } = this.map.entries().next(); 37 | if (typeof value !== 'undefined') { 38 | this.map.delete(value[0]); 39 | } 40 | } 41 | } 42 | 43 | /** Returns whether the key is in the cache. */ 44 | has(key: K): boolean { 45 | return this.map.has(key); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/util/numberrange.ts: -------------------------------------------------------------------------------- 1 | import { lerp } from './math'; 2 | 3 | export class NumberRange { 4 | public readonly start: number; 5 | public readonly end: number; 6 | 7 | constructor(start: number, end: number) { 8 | if (start > end) { 9 | throw new Error('Invalid range: start bound must be less than or equal to end bound.'); 10 | } 11 | this.start = start; 12 | this.end = end; 13 | } 14 | 15 | getSize(): number { 16 | return this.end - this.start; 17 | } 18 | 19 | lerp(alpha: number): number { 20 | return lerp(this.start, this.end, alpha); 21 | } 22 | 23 | includes(value: number): boolean { 24 | return value >= this.start && value <= this.end; 25 | } 26 | 27 | contains(range: NumberRange): boolean { 28 | return this.includes(range.start) && this.includes(range.end); 29 | } 30 | 31 | overlaps(range: NumberRange): boolean { 32 | return ( 33 | this.includes(range.start) || this.includes(range.end) || range.includes(this.start) || range.includes(this.end) 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/util/stack.ts: -------------------------------------------------------------------------------- 1 | export class Stack<T> { 2 | private items = new Array<T>(); 3 | 4 | push(item: T): void { 5 | this.items.push(item); 6 | } 7 | 8 | pop(): T | undefined { 9 | return this.items.pop(); 10 | } 11 | 12 | peek(): T | undefined { 13 | return this.items[this.items.length - 1]; 14 | } 15 | 16 | isEmpty(): boolean { 17 | return this.items.length === 0; 18 | } 19 | 20 | size(): number { 21 | return this.items.length; 22 | } 23 | 24 | clear(): void { 25 | this.items = []; 26 | } 27 | 28 | clone(): Stack<T> { 29 | const stack = new Stack<T>(); 30 | stack.items = [...this.items]; 31 | return stack; 32 | } 33 | } 34 | 35 | export default Stack; 36 | -------------------------------------------------------------------------------- /tests/__data__/lilypond/11b-TimeSignatures-NoTime.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="ISO-8859-1" standalone="no"?> 2 | <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 2.0 Partwise//EN" 3 | "http://www.musicxml.org/dtds/partwise.dtd"> 4 | <score-partwise version="2.0"> 5 | <identification> 6 | <miscellaneous> 7 | <miscellaneous-field name="description">A score without 8 | a time signature (but with a key and clefs)</miscellaneous-field> 9 | </miscellaneous> 10 | </identification> 11 | <part-list> 12 | <score-part id="P1"> 13 | <part-name></part-name> 14 | </score-part> 15 | </part-list> 16 | <part id="P1"> 17 | <measure number="1"> 18 | <attributes> 19 | <divisions>1</divisions> 20 | <key><fifths>0</fifths></key> 21 | <staves>2</staves> 22 | <clef number="1"><sign>G</sign><line>2</line></clef> 23 | <clef number="2"><sign>F</sign><line>4</line></clef> 24 | </attributes> 25 | <note> 26 | <pitch><step>F</step><octave>4</octave></pitch> 27 | <duration>4</duration> 28 | <voice>1</voice> 29 | <type>whole</type> 30 | <staff>1</staff> 31 | </note> 32 | <backup><duration>384</duration></backup> 33 | <note> 34 | <pitch><step>B</step><octave>2</octave></pitch> 35 | <duration>4</duration> 36 | <voice>2</voice> 37 | <type>whole</type> 38 | <staff>2</staff> 39 | </note> 40 | </measure> 41 | </part> 42 | </score-partwise> 43 | -------------------------------------------------------------------------------- /tests/__data__/lilypond/12b-Clefs-NoKeyOrClef.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0"?> 2 | <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 2.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd"> 3 | <score-partwise version="2.0"> 4 | <identification> 5 | <miscellaneous> 6 | <miscellaneous-field name="description">A score without 7 | any key or clef defined. The default (4/4 in treble 8 | clef) should be used.</miscellaneous-field> 9 | </miscellaneous> 10 | </identification> 11 | <part-list> 12 | <score-part id="P0"> 13 | <part-name></part-name> 14 | </score-part> 15 | </part-list> 16 | <part id="P0"> 17 | <measure number="1"> 18 | <attributes> 19 | <divisions>1</divisions> 20 | <time> 21 | <beats>4</beats> 22 | <beat-type>4</beat-type> 23 | </time> 24 | </attributes> 25 | <note> 26 | <pitch> 27 | <step>C</step> 28 | <octave>4</octave> 29 | </pitch> 30 | <duration>4</duration> 31 | <voice>1</voice> 32 | <type>whole</type> 33 | </note> 34 | </measure> 35 | <measure number="2"> 36 | <note> 37 | <pitch> 38 | <step>C</step> 39 | <octave>4</octave> 40 | </pitch> 41 | <duration>4</duration> 42 | <voice>1</voice> 43 | <type>whole</type> 44 | </note> 45 | </measure> 46 | </part> 47 | </score-partwise> 48 | -------------------------------------------------------------------------------- /tests/__data__/lilypond/21a-Chord-Basic.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0"?> 2 | <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 0.6 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd"> 3 | <score-partwise> 4 | <identification> 5 | <miscellaneous> 6 | <miscellaneous-field name="description">One simple chord 7 | consisting of two notes.</miscellaneous-field> 8 | </miscellaneous> 9 | </identification> 10 | <part-list> 11 | <score-part id="P0"> 12 | <part-name>MusicXML Part</part-name> 13 | </score-part> 14 | </part-list> 15 | <part id="P0"> 16 | <measure number="1"> 17 | <attributes> 18 | <divisions>960</divisions> 19 | <time> 20 | <beats>4</beats> 21 | <beat-type>4</beat-type> 22 | </time> 23 | <clef> 24 | <sign>G</sign> 25 | <line>2</line> 26 | </clef> 27 | </attributes> 28 | <note> 29 | <pitch> 30 | <step>A</step> 31 | <octave>4</octave> 32 | </pitch> 33 | <duration>960</duration> 34 | <voice>1</voice> 35 | <type>quarter</type> 36 | </note> 37 | <note> 38 | <chord/> 39 | <pitch> 40 | <step>F</step> 41 | <octave>4</octave> 42 | </pitch> 43 | <duration>960</duration> 44 | <voice>1</voice> 45 | <type>quarter</type> 46 | </note> 47 | <note> 48 | <rest/> 49 | <duration>960</duration> 50 | <voice>1</voice> 51 | <type>quarter</type> 52 | </note> 53 | </measure> 54 | </part> 55 | </score-partwise> 56 | -------------------------------------------------------------------------------- /tests/__data__/lilypond/33b-Spanners-Tie.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="ISO-8859-1" standalone="no"?> 2 | <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 0.6b Partwise//EN" 3 | "http://www.musicxml.org/dtds/partwise.dtd"> 4 | <score-partwise> 5 | <identification> 6 | <miscellaneous> 7 | <miscellaneous-field name="description">Two simple tied whole notes</miscellaneous-field> 8 | </miscellaneous> 9 | </identification> 10 | <part-list> 11 | <score-part id="P1"><part-name></part-name></score-part> 12 | </part-list> 13 | <part id="P1"> 14 | <measure number="1"> 15 | <attributes> 16 | <divisions>1</divisions> 17 | <key><fifths>0</fifths></key> 18 | <time><beats>4</beats><beat-type>4</beat-type></time> 19 | <staves>1</staves> 20 | <clef number="1"><sign>G</sign><line>2</line></clef> 21 | </attributes> 22 | <note> 23 | <pitch><step>F</step><octave>4</octave></pitch> 24 | <duration>4</duration> 25 | <tie type="start"/> 26 | <voice>1</voice> 27 | <type>whole</type> 28 | <notations><tied type="start"/></notations> 29 | </note> 30 | </measure> 31 | <measure number="2"> 32 | <note> 33 | <pitch><step>F</step><octave>4</octave></pitch> 34 | <duration>4</duration> 35 | <tie type="stop"/> 36 | <voice>1</voice> 37 | <type>whole</type> 38 | <notations><tied type="stop"/></notations> 39 | </note> 40 | </measure> 41 | </part> 42 | </score-partwise> 43 | -------------------------------------------------------------------------------- /tests/__data__/lilypond/41g-PartNoId.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 2.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd"> 3 | <score-partwise version="2.0"> 4 | <identification> 5 | <miscellaneous> 6 | <miscellaneous-field name="description">A part with no id attribute. 7 | Since this piece has only one part, it is clear which part 8 | is described by the one part element.</miscellaneous-field> 9 | </miscellaneous> 10 | </identification> 11 | <part-list> 12 | <score-part id="P1"> 13 | <part-name print-object="no">MusicXML Part</part-name> 14 | </score-part> 15 | </part-list> 16 | <part> 17 | <measure number="1"> 18 | <note> 19 | <rest/> 20 | <duration>4</duration> 21 | <voice>1</voice> 22 | <type>whole</type> 23 | </note> 24 | </measure> 25 | </part> 26 | </score-partwise> 27 | -------------------------------------------------------------------------------- /tests/__data__/lilypond/43a-PianoStaff.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="ISO-8859-1" standalone="no"?> 2 | <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 0.6b Partwise//EN" 3 | "http://www.musicxml.org/dtds/partwise.dtd"> 4 | <score-partwise> 5 | <identification> 6 | <miscellaneous> 7 | <miscellaneous-field name="description">A simple piano staff</miscellaneous-field> 8 | </miscellaneous> 9 | </identification> 10 | <part-list> 11 | <score-part id="P1"> 12 | <part-name>MusicXML Part</part-name> 13 | </score-part> 14 | </part-list> 15 | <part id="P1"> 16 | <measure number="1"> 17 | <attributes> 18 | <divisions>96</divisions> 19 | <key><fifths>0</fifths></key> 20 | <time><beats>4</beats><beat-type>4</beat-type></time> 21 | <staves>2</staves> 22 | <clef number="1"><sign>G</sign><line>2</line></clef> 23 | <clef number="2"><sign>F</sign><line>4</line></clef> 24 | </attributes> 25 | <note> 26 | <pitch><step>F</step><octave>4</octave></pitch> 27 | <duration>384</duration> 28 | <voice>1</voice> 29 | <type>whole</type> 30 | <staff>1</staff> 31 | </note> 32 | <backup><duration>384</duration></backup> 33 | <note> 34 | <pitch><step>B</step><octave>2</octave></pitch> 35 | <duration>384</duration> 36 | <voice>2</voice> 37 | <type>whole</type> 38 | <staff>2</staff> 39 | </note> 40 | </measure> 41 | </part> 42 | </score-partwise> 43 | -------------------------------------------------------------------------------- /tests/__data__/lilypond/51c-MultipleRights.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8" standalone='no'?> 2 | <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 1.0 Partwise//EN" "http://www.musicxml.org/dtds/partwise.dtd"> 3 | <score-partwise> 4 | <identification> 5 | <rights>Copyright © XXXX by Y. ZZZZ.</rights> 6 | <rights>Released To The Public Domain.</rights> 7 | <miscellaneous> 8 | <miscellaneous-field name="description">There can be multiple 9 | <rights> tags in the identification element of the score. The 10 | conversion shall still work, ideally using both of 11 | them.</miscellaneous-field> 12 | </miscellaneous> 13 | </identification> 14 | <part-list> 15 | <score-part id="P1"> 16 | <part-name>MusicXML Part</part-name> 17 | </score-part> 18 | </part-list> 19 | <part id="P1"> 20 | <measure number="1"> 21 | <note> 22 | <rest/> 23 | <duration>4</duration> 24 | <voice>1</voice> 25 | <type>whole</type> 26 | </note> 27 | <barline location="right"> 28 | <bar-style>light-heavy</bar-style> 29 | </barline> 30 | </measure> 31 | </part> 32 | </score-partwise> 33 | -------------------------------------------------------------------------------- /tests/__data__/lilypond/51d-EmptyTitle.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="UTF-8"?> 2 | <!DOCTYPE score-partwise PUBLIC "-//Recordare//DTD MusicXML 2.0 Partwise//EN" 3 | "http://www.musicxml.org/dtds/partwise.dtd"> 4 | <score-partwise version="2.0"> 5 | <work> 6 | <work-number></work-number> 7 | <work-title></work-title> 8 | </work> 9 | <movement-number></movement-number> 10 | <movement-title>Empty work-title, non-empty movement-title</movement-title> 11 | <identification> 12 | <miscellaneous> 13 | <miscellaneous-field name="description">A piece with an empty (but 14 | existing) work-title, but a non-empty movement-title. In this case 15 | the movement-title should be chosen, even though the work-title 16 | exists.</miscellaneous-field> 17 | </miscellaneous> 18 | </identification> 19 | <part-list> 20 | <score-part id="P1"> 21 | <part-name print-object="no">MusicXML Part</part-name> 22 | </score-part> 23 | </part-list> 24 | <!--=========================================================--> 25 | <part id="P1"> 26 | <measure number="1"> 27 | <note> 28 | <rest/> 29 | <duration>4</duration> 30 | <voice>1</voice> 31 | <type>whole</type> 32 | </note> 33 | </measure> 34 | </part> 35 | <!--=========================================================--> 36 | </score-partwise> 37 | -------------------------------------------------------------------------------- /tests/__data__/musicxml/MozaChloSample.musicxml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/__data__/musicxml/MozaChloSample.musicxml -------------------------------------------------------------------------------- /tests/__data__/musicxml/MozaVeilSample.musicxml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/__data__/musicxml/MozaVeilSample.musicxml -------------------------------------------------------------------------------- /tests/__data__/w3c-mnx/hello-world.mnx.json: -------------------------------------------------------------------------------- 1 | { 2 | "mnx": { 3 | "version": 1 4 | }, 5 | "global": { 6 | "measures": [ 7 | { 8 | "barline": { 9 | "type": "regular" 10 | }, 11 | "time": { 12 | "count": 4, 13 | "unit": 4 14 | } 15 | } 16 | ] 17 | }, 18 | "parts": [ 19 | { 20 | "measures": [ 21 | { 22 | "clefs": [ 23 | { 24 | "clef": { 25 | "line": 2, 26 | "sign": "G" 27 | } 28 | } 29 | ], 30 | "sequences": [ 31 | { 32 | "content": [ 33 | { 34 | "type": "event", 35 | "duration": { 36 | "base": "whole" 37 | }, 38 | "notes": [ 39 | { 40 | "pitch": { 41 | "octave": 4, 42 | "step": "C" 43 | } 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | ] 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /tests/__data__/w3c-mnx/hello-world.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <score-partwise version="3.1"> 3 | <part-list> 4 | <score-part id="P1"> 5 | <part-name>Music</part-name> 6 | </score-part> 7 | </part-list> 8 | <part id="P1"> 9 | <measure number="1"> 10 | <attributes> 11 | <divisions>1</divisions> 12 | <key> 13 | <fifths>0</fifths> 14 | </key> 15 | <time> 16 | <beats>4</beats> 17 | <beat-type>4</beat-type> 18 | </time> 19 | <clef> 20 | <sign>G</sign> 21 | <line>2</line> 22 | </clef> 23 | </attributes> 24 | <note> 25 | <pitch> 26 | <step>C</step> 27 | <octave>4</octave> 28 | </pitch> 29 | <duration>4</duration> 30 | <type>whole</type> 31 | </note> 32 | </measure> 33 | </part> 34 | </score-partwise> -------------------------------------------------------------------------------- /tests/__data__/w3c-mnx/repeats-implied-start-repeat.mnx.json: -------------------------------------------------------------------------------- 1 | { 2 | "mnx": { 3 | "version": 1 4 | }, 5 | "global": { 6 | "measures": [ 7 | { 8 | "repeat-end": {}, 9 | "time": { 10 | "count": 4, 11 | "unit": 4 12 | } 13 | } 14 | ] 15 | }, 16 | "parts": [ 17 | { 18 | "measures": [ 19 | { 20 | "clefs": [ 21 | { 22 | "clef": { 23 | "line": 2, 24 | "sign": "G" 25 | } 26 | } 27 | ], 28 | "sequences": [ 29 | { 30 | "content": [ 31 | { 32 | "type": "event", 33 | "duration": { 34 | "base": "whole" 35 | }, 36 | "notes": [ 37 | { 38 | "pitch": { 39 | "octave": 5, 40 | "step": "C" 41 | } 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | ] 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /tests/__data__/w3c-mnx/repeats-implied-start-repeat.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <score-partwise version="3.1"> 3 | <part-list> 4 | <score-part id="P1"> 5 | <part-name>Music</part-name> 6 | </score-part> 7 | </part-list> 8 | <part id="P1"> 9 | <measure number="1"> 10 | <attributes> 11 | <divisions>256</divisions> 12 | <key> 13 | <fifths>0</fifths> 14 | <mode>major</mode> 15 | </key> 16 | <time> 17 | <beats>4</beats> 18 | <beat-type>4</beat-type> 19 | </time> 20 | <clef> 21 | <sign>G</sign> 22 | <line>2</line> 23 | </clef> 24 | </attributes> 25 | <note> 26 | <pitch> 27 | <step>C</step> 28 | <octave>5</octave> 29 | </pitch> 30 | <duration>1024</duration> 31 | <type>whole</type> 32 | </note> 33 | <barline location="right"> 34 | <bar-style>light-heavy</bar-style> 35 | <repeat direction="backward"></repeat> 36 | </barline> 37 | </measure> 38 | </part> 39 | </score-partwise> -------------------------------------------------------------------------------- /tests/__data__/w3c-mnx/repeats-more-once-repeated.mnx.json: -------------------------------------------------------------------------------- 1 | { 2 | "mnx": { 3 | "version": 1 4 | }, 5 | "global": { 6 | "measures": [ 7 | { 8 | "repeat-end": { 9 | "times": 4 10 | }, 11 | "time": { 12 | "count": 4, 13 | "unit": 4 14 | } 15 | } 16 | ] 17 | }, 18 | "parts": [ 19 | { 20 | "measures": [ 21 | { 22 | "clefs": [ 23 | { 24 | "clef": { 25 | "line": 2, 26 | "sign": "G" 27 | } 28 | } 29 | ], 30 | "sequences": [ 31 | { 32 | "content": [ 33 | { 34 | "type": "event", 35 | "duration": { 36 | "base": "whole" 37 | }, 38 | "notes": [ 39 | { 40 | "pitch": { 41 | "octave": 5, 42 | "step": "C" 43 | } 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | ] 50 | } 51 | ] 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /tests/__data__/w3c-mnx/repeats-more-once-repeated.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <score-partwise version="3.1"> 3 | <part-list> 4 | <score-part id="P1"> 5 | <part-name>Music</part-name> 6 | </score-part> 7 | </part-list> 8 | <part id="P1"> 9 | <measure number="1"> 10 | <attributes> 11 | <divisions>256</divisions> 12 | <key> 13 | <fifths>0</fifths> 14 | <mode>major</mode> 15 | </key> 16 | <time> 17 | <beats>4</beats> 18 | <beat-type>4</beat-type> 19 | </time> 20 | <clef> 21 | <sign>G</sign> 22 | <line>2</line> 23 | </clef> 24 | </attributes> 25 | <note> 26 | <pitch> 27 | <step>C</step> 28 | <octave>5</octave> 29 | </pitch> 30 | <duration>1024</duration> 31 | <type>whole</type> 32 | </note> 33 | <barline location="right"> 34 | <bar-style>light-heavy</bar-style> 35 | <repeat direction="backward" times="4"></repeat> 36 | </barline> 37 | </measure> 38 | </part> 39 | </score-partwise> -------------------------------------------------------------------------------- /tests/__data__/w3c-mnx/repeats.mnx.json: -------------------------------------------------------------------------------- 1 | { 2 | "mnx": { 3 | "version": 1 4 | }, 5 | "global": { 6 | "measures": [ 7 | { 8 | "repeat-end": {}, 9 | "repeat-start": {}, 10 | "time": { 11 | "count": 4, 12 | "unit": 4 13 | } 14 | } 15 | ] 16 | }, 17 | "parts": [ 18 | { 19 | "measures": [ 20 | { 21 | "clefs": [ 22 | { 23 | "clef": { 24 | "line": 2, 25 | "sign": "G" 26 | } 27 | } 28 | ], 29 | "sequences": [ 30 | { 31 | "content": [ 32 | { 33 | "type": "event", 34 | "duration": { 35 | "base": "whole" 36 | }, 37 | "notes": [ 38 | { 39 | "pitch": { 40 | "octave": 5, 41 | "step": "C" 42 | } 43 | } 44 | ] 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /tests/__data__/w3c-mnx/repeats.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <score-partwise version="3.1"> 3 | <part-list> 4 | <score-part id="P1"> 5 | <part-name>Music</part-name> 6 | </score-part> 7 | </part-list> 8 | <part id="P1"> 9 | <measure number="1"> 10 | <barline location="left"> 11 | <bar-style>heavy-light</bar-style> 12 | <repeat direction="forward"></repeat> 13 | </barline> 14 | <attributes> 15 | <divisions>256</divisions> 16 | <key> 17 | <fifths>0</fifths> 18 | <mode>major</mode> 19 | </key> 20 | <time> 21 | <beats>4</beats> 22 | <beat-type>4</beat-type> 23 | </time> 24 | <clef> 25 | <sign>G</sign> 26 | <line>2</line> 27 | </clef> 28 | </attributes> 29 | <note> 30 | <pitch> 31 | <step>C</step> 32 | <octave>5</octave> 33 | </pitch> 34 | <duration>1024</duration> 35 | <type>whole</type> 36 | </note> 37 | <barline location="right"> 38 | <bar-style>light-heavy</bar-style> 39 | <repeat direction="backward"></repeat> 40 | </barline> 41 | </measure> 42 | </part> 43 | </score-partwise> -------------------------------------------------------------------------------- /tests/__data__/w3c-musicxml/bookmark-element.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8" standalone="no"?> 2 | <!DOCTYPE score-partwise PUBLIC 3 | "-//Recordare//DTD MusicXML 4.0 Partwise//EN" 4 | "http://www.musicxml.org/dtds/partwise.dtd"> 5 | <score-partwise version="4.0"> 6 | <defaults> 7 | </defaults> 8 | <part-list> 9 | <score-part id="P1"> 10 | <part-name>placeholder</part-name> 11 | </score-part> 12 | </part-list> 13 | <part id="P1"> 14 | <measure number="73"> 15 | <bookmark id="Variation-VI-P1"/> 16 | <direction placement="above"> 17 | <direction-type> 18 | <rehearsal default-y="40" font-weight="bold">Variation VI</rehearsal> 19 | </direction-type> 20 | </direction> 21 | <note> 22 | <pitch> 23 | <step>D</step> 24 | <octave>5</octave> 25 | </pitch> 26 | <duration>8</duration> 27 | <voice>1</voice> 28 | <type>whole</type> 29 | </note> 30 | </measure> 31 | </part> 32 | </score-partwise> -------------------------------------------------------------------------------- /tests/__data__/w3c-musicxml/cancel-element.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8" standalone="no"?> 2 | <!DOCTYPE score-partwise PUBLIC 3 | "-//Recordare//DTD MusicXML 4.0 Partwise//EN" 4 | "http://www.musicxml.org/dtds/partwise.dtd"> 5 | <score-partwise version="4.0"> 6 | <defaults> 7 | </defaults> 8 | <part-list> 9 | <score-part id="P1"> 10 | <part-name>placeholder</part-name> 11 | </score-part> 12 | </part-list> 13 | <part id="P1"> 14 | <measure number="1"> 15 | <attributes> 16 | <key> 17 | <cancel>-2</cancel> 18 | <fifths>-1</fifths> 19 | <mode>major</mode> 20 | </key> 21 | </attributes> 22 | <harmony> 23 | <root> 24 | <root-step>C</root-step> 25 | </root> 26 | <kind use-symbols="yes">major-seventh</kind> 27 | </harmony> 28 | <note> 29 | <pitch> 30 | <step>C</step> 31 | <octave>4</octave> 32 | </pitch> 33 | <duration>4</duration> 34 | <type>whole</type> 35 | <notations> 36 | </notations> 37 | </note> 38 | <print> 39 | </print> 40 | </measure> 41 | </part> 42 | </score-partwise> -------------------------------------------------------------------------------- /tests/__data__/w3c-musicxml/damp-element.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8" standalone="no"?> 2 | <!DOCTYPE score-partwise PUBLIC 3 | "-//Recordare//DTD MusicXML 4.0 Partwise//EN" 4 | "http://www.musicxml.org/dtds/partwise.dtd"> 5 | <score-partwise version="4.0"> 6 | <defaults> 7 | </defaults> 8 | <part-list> 9 | <score-part id="P1"> 10 | <part-name>placeholder</part-name> 11 | </score-part> 12 | </part-list> 13 | <part id="P1"> 14 | <measure number="10"> 15 | <note> 16 | <pitch> 17 | <step>C</step> 18 | <octave>5</octave> 19 | </pitch> 20 | <duration>16</duration> 21 | <voice>1</voice> 22 | <type>half</type> 23 | <stem default-y="-50">down</stem> 24 | </note> 25 | <direction placement="above"> 26 | <direction-type> 27 | <damp/> 28 | </direction-type> 29 | <offset>-2</offset> 30 | </direction> 31 | </measure> 32 | </part> 33 | </score-partwise> -------------------------------------------------------------------------------- /tests/__data__/w3c-musicxml/divisions-and-duration-elements.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8" standalone="no"?> 2 | <!DOCTYPE score-partwise PUBLIC 3 | "-//Recordare//DTD MusicXML 4.0 Partwise//EN" 4 | "http://www.musicxml.org/dtds/partwise.dtd"> 5 | <score-partwise version="4.0"> 6 | <defaults> 7 | </defaults> 8 | <part-list> 9 | <score-part id="P1"> 10 | <part-name>placeholder</part-name> 11 | </score-part> 12 | </part-list> 13 | <part id="P1"> 14 | <measure number="1"> 15 | <attributes> 16 | <divisions>32</divisions> 17 | </attributes> 18 | <note> 19 | <pitch> 20 | <step>A</step> 21 | <octave>4</octave> 22 | </pitch> 23 | <duration>128</duration> 24 | <voice>1</voice> 25 | <type>whole</type> 26 | </note> 27 | </measure> 28 | </part> 29 | </score-partwise> -------------------------------------------------------------------------------- /tests/__data__/w3c-musicxml/link-element.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8" standalone="no"?> 2 | <!DOCTYPE score-partwise PUBLIC 3 | "-//Recordare//DTD MusicXML 4.0 Partwise//EN" 4 | "http://www.musicxml.org/dtds/partwise.dtd"> 5 | <score-partwise version="4.0"> 6 | <defaults> 7 | </defaults> 8 | <part-list> 9 | <score-part id="P1"> 10 | <part-name>placeholder</part-name> 11 | </score-part> 12 | </part-list> 13 | <part id="P1"> 14 | <measure number="2" width="305"> 15 | <link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="https://www.w3.org/" xlink:show="new"/> 16 | <direction placement="above"> 17 | <direction-type> 18 | <image source="images/w3c_home_nb.png" type="image/png" height="43" width="77" default-x="2" default-y="12" halign="left" valign="bottom"/> 19 | </direction-type> 20 | </direction> 21 | <note> 22 | <rest measure="yes"/> 23 | <duration>8</duration> 24 | <voice>1</voice> 25 | </note> 26 | </measure> 27 | </part> 28 | </score-partwise> -------------------------------------------------------------------------------- /tests/__data__/w3c-musicxml/multiple-rest-element.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8" standalone="no"?> 2 | <!DOCTYPE score-partwise PUBLIC 3 | "-//Recordare//DTD MusicXML 4.0 Partwise//EN" 4 | "http://www.musicxml.org/dtds/partwise.dtd"> 5 | <score-partwise version="4.0"> 6 | <defaults> 7 | </defaults> 8 | <part-list> 9 | <score-part id="P1"> 10 | <part-name>placeholder</part-name> 11 | </score-part> 12 | </part-list> 13 | <part id="P1"> 14 | <measure number="1"> 15 | <attributes> 16 | <measure-style> 17 | <multiple-rest>4</multiple-rest> 18 | </measure-style> 19 | </attributes> 20 | <harmony> 21 | <root> 22 | <root-step>C</root-step> 23 | </root> 24 | <kind use-symbols="yes">major-seventh</kind> 25 | </harmony> 26 | <note> 27 | <pitch> 28 | <step>C</step> 29 | <octave>4</octave> 30 | </pitch> 31 | <duration>4</duration> 32 | <type>whole</type> 33 | <notations> 34 | </notations> 35 | </note> 36 | <print> 37 | </print> 38 | </measure> 39 | </part> 40 | </score-partwise> -------------------------------------------------------------------------------- /tests/__data__/w3c-musicxml/score-timewise-element.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8"?> 2 | <score-timewise version="4.0"> 3 | <part-list> 4 | <score-part id="P1"> 5 | <part-name>Music</part-name> 6 | </score-part> 7 | </part-list> 8 | <measure number="1"> 9 | <part id="P1"> 10 | <attributes> 11 | <divisions>1</divisions> 12 | <key> 13 | <fifths>0</fifths> 14 | </key> 15 | <time> 16 | <beats>4</beats> 17 | <beat-type>4</beat-type> 18 | </time> 19 | <clef> 20 | <sign>G</sign> 21 | <line>2</line> 22 | </clef> 23 | </attributes> 24 | <note> 25 | <pitch> 26 | <step>C</step> 27 | <octave>4</octave> 28 | </pitch> 29 | <duration>4</duration> 30 | <type>whole</type> 31 | </note> 32 | </part> 33 | </measure> 34 | </score-timewise> -------------------------------------------------------------------------------- /tests/__data__/w3c-musicxml/tutorial-hello-world.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8" standalone="no"?> 2 | <!DOCTYPE score-partwise PUBLIC 3 | "-//Recordare//DTD MusicXML 4.0 Partwise//EN" 4 | "http://www.musicxml.org/dtds/partwise.dtd"> 5 | <score-partwise version="4.0"> 6 | <part-list> 7 | <score-part id="P1"> 8 | <part-name>Music</part-name> 9 | </score-part> 10 | </part-list> 11 | <part id="P1"> 12 | <measure number="1"> 13 | <attributes> 14 | <divisions>1</divisions> 15 | <key> 16 | <fifths>0</fifths> 17 | </key> 18 | <time> 19 | <beats>4</beats> 20 | <beat-type>4</beat-type> 21 | </time> 22 | <clef> 23 | <sign>G</sign> 24 | <line>2</line> 25 | </clef> 26 | </attributes> 27 | <note> 28 | <pitch> 29 | <step>C</step> 30 | <octave>4</octave> 31 | </pitch> 32 | <duration>4</duration> 33 | <type>whole</type> 34 | </note> 35 | </measure> 36 | </part> 37 | </score-partwise> -------------------------------------------------------------------------------- /tests/__data__/w3c-musicxml/wait-element.musicxml: -------------------------------------------------------------------------------- 1 | <?xml version="1.0" encoding="utf-8" standalone="no"?> 2 | <!DOCTYPE score-partwise PUBLIC 3 | "-//Recordare//DTD MusicXML 4.0 Partwise//EN" 4 | "http://www.musicxml.org/dtds/partwise.dtd"> 5 | <score-partwise version="4.0"> 6 | <defaults> 7 | </defaults> 8 | <part-list> 9 | <score-part id="P1"> 10 | <part-name>placeholder</part-name> 11 | </score-part> 12 | </part-list> 13 | <part id="P1"> 14 | <measure number="146"> 15 | <direction placement="above"> 16 | <direction-type> 17 | <rehearsal default-y="40" font-weight="bold">Variation IX</rehearsal> 18 | </direction-type> 19 | </direction> 20 | <note> 21 | <pitch> 22 | <step>A</step> 23 | <octave>4</octave> 24 | </pitch> 25 | <duration>8</duration> 26 | <voice>1</voice> 27 | <type>whole</type> 28 | <listen> 29 | <wait/> 30 | </listen> 31 | </note> 32 | </measure> 33 | </part> 34 | </score-partwise> -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/01a-Pitches-Pitches_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/01a-Pitches-Pitches_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/01b-Pitches-Intervals_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/01b-Pitches-Intervals_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/01c-Pitches-NoVoiceElement_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/01c-Pitches-NoVoiceElement_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/01d-Pitches-Microtones_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/01d-Pitches-Microtones_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/01e-Pitches-ParenthesizedAccidentals_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/01e-Pitches-ParenthesizedAccidentals_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/01f-Pitches-ParenthesizedMicrotoneAccidentals_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/01f-Pitches-ParenthesizedMicrotoneAccidentals_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/02a-Rests-Durations_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/02a-Rests-Durations_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/02b-Rests-PitchedRests_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/02b-Rests-PitchedRests_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/02c-Rests-MultiMeasureRests_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/02c-Rests-MultiMeasureRests_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/02d-Rests-Multimeasure-TimeSignatures_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/02d-Rests-Multimeasure-TimeSignatures_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/02e-Rests-NoType_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/02e-Rests-NoType_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/03a-Rhythm-Durations_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/03a-Rhythm-Durations_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/03b-Rhythm-Backup_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/03b-Rhythm-Backup_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/03c-Rhythm-DivisionChange_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/03c-Rhythm-DivisionChange_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/03d-Rhythm-DottedDurations-Factors_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/03d-Rhythm-DottedDurations-Factors_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/11a-TimeSignatures_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/11a-TimeSignatures_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/11b-TimeSignatures-NoTime_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/11b-TimeSignatures-NoTime_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/11c-TimeSignatures-CompoundSimple_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/11c-TimeSignatures-CompoundSimple_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/11d-TimeSignatures-CompoundMultiple_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/11d-TimeSignatures-CompoundMultiple_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/11e-TimeSignatures-CompoundMixed_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/11e-TimeSignatures-CompoundMixed_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/11f-TimeSignatures-SymbolMeaning_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/11f-TimeSignatures-SymbolMeaning_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/11g-TimeSignatures-SingleNumber_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/11g-TimeSignatures-SingleNumber_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/11h-TimeSignatures-SenzaMisura_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/11h-TimeSignatures-SenzaMisura_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/12a-Clefs_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/12a-Clefs_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/12b-Clefs-NoKeyOrClef_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/12b-Clefs-NoKeyOrClef_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/13a-KeySignatures_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/13a-KeySignatures_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/13b-KeySignatures-ChurchModes_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/13b-KeySignatures-ChurchModes_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/13c-KeySignatures-NonTraditional_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/13c-KeySignatures-NonTraditional_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/13d-KeySignatures-Microtones_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/13d-KeySignatures-Microtones_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/14a-StaffDetails-LineChanges_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/14a-StaffDetails-LineChanges_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/21a-Chord-Basic_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/21a-Chord-Basic_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/21b-Chords-TwoNotes_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/21b-Chords-TwoNotes_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/21c-Chords-ThreeNotesDuration_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/21c-Chords-ThreeNotesDuration_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/21d-Chords-SchubertStabatMater_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/21d-Chords-SchubertStabatMater_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/21e-Chords-PickupMeasures_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/21e-Chords-PickupMeasures_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/21f-Chord-ElementInBetween_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/21f-Chord-ElementInBetween_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/22a-Noteheads_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/22a-Noteheads_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/22c-Noteheads-Chords_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/22c-Noteheads-Chords_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/22d-Parenthesized-Noteheads_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/22d-Parenthesized-Noteheads_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/23a-Tuplets_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/23a-Tuplets_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/24a-GraceNotes_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/24a-GraceNotes_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/24b-ChordAsGraceNote_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/24b-ChordAsGraceNote_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/24f-GraceNote-Slur_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/24f-GraceNote-Slur_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/31a-Directions_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/31a-Directions_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/31c-MetronomeMarks_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/31c-MetronomeMarks_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/32a-Notations_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/32a-Notations_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/33a-Spanners_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/33a-Spanners_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/33b-Spanners-Tie_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/33b-Spanners-Tie_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/33c-Spanners-Slurs_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/33c-Spanners-Slurs_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/33g-Slur-ChordedNotes_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/33g-Slur-ChordedNotes_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/41a-MultiParts-Partorder_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/41a-MultiParts-Partorder_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/43a-PianoStaff_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/43a-PianoStaff_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/45a-SimpleRepeat_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/45a-SimpleRepeat_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/45b-RepeatWithAlternatives_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/45b-RepeatWithAlternatives_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/45c-RepeatMultipleTimes_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/45c-RepeatMultipleTimes_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/45d-Repeats-Nested-Alternatives_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/45d-Repeats-Nested-Alternatives_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/71e-TabStaves_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/71e-TabStaves_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/accent-element_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/accent-element_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/complex_formatting_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/complex_formatting_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/multi_part_formatting_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/multi_part_formatting_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/multi_stave_single_part_formatting_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/multi_stave_single_part_formatting_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/multi_system_spanners_400px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/multi_system_spanners_400px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/prelude_no_1_snippet_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/prelude_no_1_snippet_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/tabs_basic_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/tabs_basic_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/tabs_bends_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/tabs_bends_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/tabs_dead_notes_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/tabs_dead_notes_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/tabs_grace_notes_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/tabs_grace_notes_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/tabs_multi_voice_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/tabs_multi_voice_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/tabs_natural_harmonics_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/tabs_natural_harmonics_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/tabs_slides_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/tabs_slides_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/tabs_slurs_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/tabs_slurs_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/tabs_stroke_direction_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/tabs_stroke_direction_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/tabs_taps_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/tabs_taps_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/tabs_ties_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/tabs_ties_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/tabs_vibrato_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/tabs_vibrato_900px.png -------------------------------------------------------------------------------- /tests/integration/__image_snapshots__/tabs_with_stave_900px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stringsync/vexml/429f3175b0e0de9da5035e6cd42062fbae0e6bb2/tests/integration/__image_snapshots__/tabs_with_stave_900px.png -------------------------------------------------------------------------------- /tests/jest.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace jest { 2 | interface Matchers<R> { 3 | toMatchImageSnapshot(opts: { customSnapshotIdentifier?: string }): R; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/unit/musicxml/accidentalmark.test.ts: -------------------------------------------------------------------------------- 1 | import { ACCIDENTAL_TYPES, AccidentalMark } from '@/musicxml'; 2 | import { xml } from '@/util'; 3 | 4 | describe(AccidentalMark, () => { 5 | describe('getType', () => { 6 | it.each(ACCIDENTAL_TYPES.values)(`returns the type: '%s'`, (type) => { 7 | const node = xml.accidentalMark({ type }); 8 | const accidentalMark = new AccidentalMark(node); 9 | expect(accidentalMark.getType()).toBe(type); 10 | }); 11 | 12 | it(`defaults to null when the type is missing`, () => { 13 | const node = xml.accidentalMark(); 14 | const accidentalMark = new AccidentalMark(node); 15 | expect(accidentalMark.getType()).toBeNull(); 16 | }); 17 | 18 | it(`defaults to null when the type is invalid`, () => { 19 | const node = xml.accidentalMark({ type: 'foo' }); 20 | const accidentalMark = new AccidentalMark(node); 21 | expect(accidentalMark.getType()).toBeNull(); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/unit/musicxml/backup.test.ts: -------------------------------------------------------------------------------- 1 | import { xml } from '@/util'; 2 | import { Backup } from '@/musicxml'; 3 | 4 | describe('backup', () => { 5 | describe('getDuration', () => { 6 | it('returns the number of divisions to backup when processing notes', () => { 7 | const node = xml.backup({ duration: xml.duration({ positiveDivisions: 2 }) }); 8 | const backup = new Backup(node); 9 | expect(backup.getDuration()).toBe(2); 10 | }); 11 | 12 | it('defaults to 4 when not specified', () => { 13 | const node = xml.backup(); 14 | const backup = new Backup(node); 15 | expect(backup.getDuration()).toBe(4); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/unit/musicxml/enums.test.ts: -------------------------------------------------------------------------------- 1 | import { Enum } from '@/util'; 2 | 3 | describe(Enum, () => { 4 | describe('includes', () => { 5 | it('returns true when the value is part of the choices', () => { 6 | const foo = new Enum(['foo'] as const); 7 | expect(foo.includes('foo')).toBeTrue(); 8 | }); 9 | 10 | it('returns false when the value is not part of the choices', () => { 11 | const foo = new Enum(['foo'] as const); 12 | expect(foo.includes('bar')).toBeFalse(); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/unit/musicxml/forward.test.ts: -------------------------------------------------------------------------------- 1 | import { xml } from '@/util'; 2 | import { Forward } from '@/musicxml'; 3 | 4 | describe('forward', () => { 5 | describe('getDuration', () => { 6 | it('returns the number of divisions to forward when processing notes', () => { 7 | const node = xml.forward({ duration: xml.duration({ positiveDivisions: 2 }) }); 8 | const forward = new Forward(node); 9 | expect(forward.getDuration()).toBe(2); 10 | }); 11 | 12 | it('defaults to 4 when not specified', () => { 13 | const node = xml.forward(); 14 | const forward = new Forward(node); 15 | expect(forward.getDuration()).toBe(4); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/unit/musicxml/key.test.ts: -------------------------------------------------------------------------------- 1 | import { Key } from '@/musicxml/key'; 2 | import { xml } from '@/util'; 3 | import { KEY_MODES } from '@/musicxml/enums'; 4 | 5 | describe(Key, () => { 6 | describe('getFifthsCount', () => { 7 | it('returns the fifths count of the key', () => { 8 | const node = xml.key({ fifths: xml.fifths({ value: '5' }) }); 9 | const key = new Key(node); 10 | expect(key.getFifthsCount()).toBe(5); 11 | }); 12 | 13 | it('defaults to 0 when missing fifths', () => { 14 | const node = xml.key(); 15 | const key = new Key(node); 16 | expect(key.getFifthsCount()).toBe(0); 17 | }); 18 | }); 19 | 20 | describe('getMode', () => { 21 | it.each(KEY_MODES.values)(`returns the mode of the key: '%s'`, (keyMode) => { 22 | const node = xml.key({ mode: xml.mode({ value: keyMode }) }); 23 | const key = new Key(node); 24 | expect(key.getMode()).toBe(keyMode); 25 | }); 26 | 27 | it(`defaults to 'none' when missing`, () => { 28 | const node = xml.key(); 29 | const key = new Key(node); 30 | expect(key.getMode()).toBe('none'); 31 | }); 32 | 33 | it(`defaults to 'none' when invalid`, () => { 34 | const node = xml.key({ mode: xml.mode({ value: 'foo' }) }); 35 | const key = new Key(node); 36 | expect(key.getMode()).toBe('none'); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /tests/unit/musicxml/musicxml.test.ts: -------------------------------------------------------------------------------- 1 | import { MusicXML } from '@/musicxml/musicxml'; 2 | import { ScorePartwise } from '@/musicxml/scorepartwise'; 3 | import { xml } from '@/util'; 4 | 5 | describe(MusicXML, () => { 6 | describe('getScorePartwise', () => { 7 | it('returns the score of the musicxml document', () => { 8 | const scorePartwise = xml.scorePartwise(); 9 | const document = xml.musicXML(scorePartwise); 10 | 11 | const musicXML = new MusicXML(document); 12 | 13 | expect(musicXML.getScorePartwise()).toStrictEqual(new ScorePartwise(scorePartwise)); 14 | }); 15 | 16 | it('throws when <score-partwise> is missing', () => { 17 | const root = xml.createDocument(); 18 | const musicXML = new MusicXML(root); 19 | expect(() => musicXML.getScorePartwise()).toThrow(); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/unit/musicxml/part.test.ts: -------------------------------------------------------------------------------- 1 | import { Measure } from '@/musicxml/measure'; 2 | import { Part } from '@/musicxml/part'; 3 | import { xml } from '@/util'; 4 | 5 | describe(Part, () => { 6 | describe('getId', () => { 7 | it('returns the ID of the part', () => { 8 | const node = xml.part({ id: 'foo' }); 9 | const part = new Part(node); 10 | expect(part.getId()).toBe('foo'); 11 | }); 12 | }); 13 | 14 | describe('getMeasures', () => { 15 | it('returns the measures of the part', () => { 16 | const measure1 = xml.measure(); 17 | const measure2 = xml.measure(); 18 | const node = xml.part({ measures: [measure1, measure2] }); 19 | 20 | const part = new Part(node); 21 | 22 | expect(part.getMeasures()).toStrictEqual([new Measure(measure1), new Measure(measure2)]); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/unit/musicxml/symbolic.test.ts: -------------------------------------------------------------------------------- 1 | import { Symbolic } from '@/musicxml'; 2 | import { xml } from '@/util'; 3 | 4 | describe(Symbolic, () => { 5 | describe('getSmulfGlyphName', () => { 6 | it('returns the content of the symbolic', () => { 7 | const node = xml.symbolic({ smuflGlyphName: 'foo' }); 8 | const symbolic = new Symbolic(node); 9 | expect(symbolic.getSmulfGlyphName()).toBe('foo'); 10 | }); 11 | 12 | it('defaults to empty string when content is not provided', () => { 13 | const node = xml.symbolic(); 14 | const symbolic = new Symbolic(node); 15 | expect(symbolic.getSmulfGlyphName()).toBe(''); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/unit/musicxml/types.test.ts: -------------------------------------------------------------------------------- 1 | describe('types', () => { 2 | it('loads', () => { 3 | expect(() => import('@/musicxml/types')).not.toThrow(); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /tests/unit/musicxml/wavyline.test.ts: -------------------------------------------------------------------------------- 1 | import { START_STOP_CONTINUE, WavyLine } from '@/musicxml'; 2 | import { xml } from '@/util'; 3 | 4 | describe(WavyLine, () => { 5 | describe('getNumber', () => { 6 | it('returns the number of the wavy line', () => { 7 | const node = xml.wavyLine({ number: 42 }); 8 | const wavyLine = new WavyLine(node); 9 | expect(wavyLine.getNumber()).toBe(42); 10 | }); 11 | 12 | it('defaults to 1 when the number is not specified', () => { 13 | const node = xml.wavyLine(); 14 | const wavyLine = new WavyLine(node); 15 | expect(wavyLine.getNumber()).toBe(1); 16 | }); 17 | }); 18 | 19 | describe('getType', () => { 20 | it.each(START_STOP_CONTINUE.values)(`returns the type of the wavy line: '%s'`, (type) => { 21 | const node = xml.wavyLine({ type }); 22 | const wavyLine = new WavyLine(node); 23 | expect(wavyLine.getType()).toBe(type); 24 | }); 25 | 26 | it(`defaults to 'start' when not specified`, () => { 27 | const node = xml.wavyLine(); 28 | const wavyLine = new WavyLine(node); 29 | expect(wavyLine.getType()).toBe('start'); 30 | }); 31 | 32 | it(`defaults to 'start' when invalid`, () => { 33 | const node = xml.wavyLine({ type: 'foo' }); 34 | const wavyLine = new WavyLine(node); 35 | expect(wavyLine.getType()).toBe('start'); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/unit/musicxml/wedge.test.ts: -------------------------------------------------------------------------------- 1 | import { WEDGE_TYPES, Wedge } from '@/musicxml'; 2 | import { xml } from '@/util'; 3 | 4 | describe(Wedge, () => { 5 | describe('getType', () => { 6 | it.each(WEDGE_TYPES.values)(`returns the type of the wedge: '%s'`, (type) => { 7 | const node = xml.wedge({ type }); 8 | const wedge = new Wedge(node); 9 | expect(wedge.getType()).toBe(type); 10 | }); 11 | 12 | it('defaults to crescendo when invalid', () => { 13 | const node = xml.wedge({ type: 'foo' }); 14 | const wedge = new Wedge(node); 15 | expect(wedge.getType()).toBeNull(); 16 | }); 17 | 18 | it('defaults to null when missing', () => { 19 | const node = xml.wedge(); 20 | const wedge = new Wedge(node); 21 | expect(wedge.getType()).toBeNull(); 22 | }); 23 | }); 24 | 25 | describe('getSpread', () => { 26 | it('returns the spread of the wedge', () => { 27 | const node = xml.wedge({ spread: 10 }); 28 | const wedge = new Wedge(node); 29 | expect(wedge.getSpread()).toBe(10); 30 | }); 31 | 32 | it('defaults to 0 when invalid', () => { 33 | const node = xml.wedge({ spread: NaN }); 34 | const wedge = new Wedge(node); 35 | expect(wedge.getSpread()).toBe(0); 36 | }); 37 | 38 | it('defaults to 0 when missing', () => { 39 | const node = xml.wedge(); 40 | const wedge = new Wedge(node); 41 | expect(wedge.getSpread()).toBe(0); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/unit/musicxml/words.test.ts: -------------------------------------------------------------------------------- 1 | import { Words } from '@/musicxml'; 2 | import { xml } from '@/util'; 3 | 4 | describe(Words, () => { 5 | describe('getContent', () => { 6 | it('returns the content of the words', () => { 7 | const node = xml.words({ content: 'foo' }); 8 | const words = new Words(node); 9 | expect(words.getContent()).toBe('foo'); 10 | }); 11 | 12 | it('defaults to empty string when content is not provided', () => { 13 | const node = xml.words(); 14 | const words = new Words(node); 15 | expect(words.getContent()).toBe(''); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/unit/mxl/container.test.ts: -------------------------------------------------------------------------------- 1 | import { Container, Rootfile } from '@/mxl'; 2 | import { xml } from '@/util'; 3 | 4 | describe(Container, () => { 5 | describe('getRootfiles', () => { 6 | it('returns the rootfiles of the container', () => { 7 | const rootfile1 = xml.rootfile({ fullPath: 'foo.musicxml' }); 8 | const rootfile2 = xml.rootfile({ fullPath: 'bar.musicxml' }); 9 | const node = xml.container({ rootfiles: xml.rootfiles({ rootfiles: [rootfile1, rootfile2] }) }); 10 | 11 | const container = new Container(node); 12 | 13 | expect(container.getRootfiles()).toStrictEqual([new Rootfile(rootfile1), new Rootfile(rootfile2)]); 14 | }); 15 | 16 | it('defaults to an empty array when missing', () => { 17 | const node = xml.container(); 18 | const container = new Container(node); 19 | expect(container.getRootfiles()).toStrictEqual([]); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/unit/mxl/rootfile.test.ts: -------------------------------------------------------------------------------- 1 | import { Rootfile } from '@/mxl'; 2 | import { xml } from '@/util'; 3 | 4 | describe(Rootfile, () => { 5 | describe('getFullPath', () => { 6 | it('returns the full path of the rootfile', () => { 7 | const node = xml.rootfile({ fullPath: 'foo' }); 8 | const rootfile = new Rootfile(node); 9 | expect(rootfile.getFullPath()).toBe('foo'); 10 | }); 11 | 12 | it('defaults to an empty string when missing', () => { 13 | const node = xml.rootfile(); 14 | const rootfile = new Rootfile(node); 15 | expect(rootfile.getFullPath()).toBe(''); 16 | }); 17 | }); 18 | 19 | describe('getMediaType', () => { 20 | it('returns the media type of the rootfile', () => { 21 | const node = xml.rootfile({ mediaType: 'text/foo' }); 22 | const rootfile = new Rootfile(node); 23 | expect(rootfile.getMediaType()).toBe('text/foo'); 24 | }); 25 | 26 | it('defaults to musicxml mime type when missing', () => {}); 27 | const node = xml.rootfile(); 28 | const rootfile = new Rootfile(node); 29 | expect(rootfile.getMediaType()).toBe('application/vnd.recordare.musicxml+xml'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/unit/parsing/musicxml/musicxmlparser.test.ts: -------------------------------------------------------------------------------- 1 | import { MusicXMLParser } from '@/parsing'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | 5 | const MUSICXML_PATH = path.resolve(__dirname, '..', '..', '..', '__data__', 'lilypond', '01a-Pitches-Pitches.musicxml'); 6 | 7 | describe(MusicXMLParser, () => { 8 | let musicXMLString: string; 9 | 10 | beforeAll(() => { 11 | musicXMLString = fs.readFileSync(MUSICXML_PATH).toString(); 12 | }); 13 | 14 | it('parses musicXML as a string', () => { 15 | const parser = new MusicXMLParser(); 16 | expect(() => parser.parse(musicXMLString)).not.toThrow(); 17 | }); 18 | 19 | it('parses musicXML as a Document', () => { 20 | const parser = new MusicXMLParser(); 21 | const document = new DOMParser().parseFromString(musicXMLString, 'application/xml'); 22 | expect(() => parser.parse(document)).not.toThrow(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /tests/unit/util/assert.test.ts: -------------------------------------------------------------------------------- 1 | import * as util from '@/util'; 2 | 3 | describe('assertNotNull', () => { 4 | it('throws when the value is null', () => { 5 | const value = (): string | null => null; 6 | expect(() => util.assertNotNull(value())).toThrow(); 7 | }); 8 | 9 | it('does not throw when value is undefined', () => { 10 | const value = (): undefined | null => undefined; 11 | expect(() => util.assertNotNull(value())).not.toThrow(); 12 | }); 13 | 14 | it('does not throw when value is truthy', () => { 15 | const value = (): boolean | null => true; 16 | expect(() => util.assertNotNull(value())).not.toThrow(); 17 | }); 18 | 19 | it('does not throw for falsy values', () => { 20 | const value = (): boolean | null => false; 21 | expect(() => util.assertNotNull(value())).not.toThrow(); 22 | }); 23 | }); 24 | 25 | describe('assert', () => { 26 | it('throws when the value is false', () => { 27 | expect(() => util.assert(false, 'foo')).toThrowError(new Error('foo')); 28 | }); 29 | 30 | it('does not throw when the value is true', () => { 31 | expect(() => util.assert(true, 'foo')).not.toThrow(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /tests/unit/util/enum.test.ts: -------------------------------------------------------------------------------- 1 | import { Enum } from '@/util'; 2 | 3 | describe(Enum, () => { 4 | describe('includes', () => { 5 | const METASYNTATIC_VARIABLES = new Enum(['foo', 'bar', 'baz']); 6 | 7 | it('returns true when the value is part of the values', () => { 8 | const result = METASYNTATIC_VARIABLES.includes('foo'); 9 | expect(result).toBeTrue(); 10 | }); 11 | 12 | it('returns false when the value is not part of the values', () => { 13 | const result = METASYNTATIC_VARIABLES.includes('hello world'); 14 | expect(result).toBeFalse(); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/unit/util/xml.test.ts: -------------------------------------------------------------------------------- 1 | import { xml } from '@/util'; 2 | 3 | type FnNames = keyof typeof xml; 4 | 5 | const AUTO_CRASH_TEST_SKIP_NAMES: Set<FnNames> = new Set(['createElement', 'createNamedElement', 'musicXML']); 6 | 7 | describe('xml', () => { 8 | describe('createElement', () => { 9 | it('runs without crashing', () => { 10 | expect(() => xml.createElement('foo')).not.toThrow(); 11 | }); 12 | }); 13 | 14 | describe('createNamedElement', () => { 15 | it('runs without crashing', () => { 16 | expect(() => xml.createNamedElement('foo')).not.toThrow(); 17 | }); 18 | }); 19 | 20 | for (const [name, fn] of Object.entries(xml)) { 21 | if (!AUTO_CRASH_TEST_SKIP_NAMES.has(name as FnNames)) { 22 | describe(name, () => { 23 | it('runs without crashing', () => { 24 | expect(fn).not.toThrow(); 25 | }); 26 | }); 27 | } 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "declaration": true, 8 | "skipLibCheck": true, 9 | "sourceMap": true, 10 | "allowJs": true, 11 | "types": ["node", "jest", "jest-extended", "bootstrap"], 12 | "experimentalDecorators": true, 13 | "jsx": "react-jsx", 14 | "paths": { 15 | "@/*": ["./src/*"] 16 | } 17 | }, 18 | "include": ["src/**/*", "tests/**/*", "site/src/**/*"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.package.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "ESNext", 5 | "moduleResolution": "node", 6 | "strict": false, 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "experimentalDecorators": true, 11 | "paths": { 12 | "@/*": ["./src/*"] 13 | } 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["node_modules"] 17 | } 18 | --------------------------------------------------------------------------------