@pepperhorn/chordl-core 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chord-sheet/codec.d.ts +1 -1
- package/dist/chord-sheet/codec.js +1 -1
- package/dist/chord-sheet/codec.js.map +1 -1
- package/dist/chord-sheet/defaults.d.ts +1 -1
- package/dist/chord-sheet/index.d.ts +3 -3
- package/dist/chord-sheet/index.js +3 -3
- package/dist/chord-sheet/index.js.map +1 -1
- package/dist/engine/auto-fingering.js +1 -1
- package/dist/engine/auto-fingering.js.map +1 -1
- package/dist/engine/highlight-mapper.d.ts +1 -1
- package/dist/engine/highlight-mapper.js +2 -2
- package/dist/engine/highlight-mapper.js.map +1 -1
- package/dist/engine/keyboard-layout.d.ts +1 -1
- package/dist/engine/keyboard-layout.js +1 -1
- package/dist/engine/keyboard-layout.js.map +1 -1
- package/dist/engine/mei-builder.d.ts +27 -0
- package/dist/engine/mei-builder.js +138 -0
- package/dist/engine/mei-builder.js.map +1 -0
- package/dist/engine/staff-glyphs.js +5 -10
- package/dist/engine/staff-glyphs.js.map +1 -1
- package/dist/engine/staff-layout.js +2 -2
- package/dist/engine/staff-layout.js.map +1 -1
- package/dist/index.d.ts +48 -42
- package/dist/index.js +32 -28
- package/dist/index.js.map +1 -1
- package/dist/parser/natural-language.d.ts +1 -1
- package/dist/parser/natural-language.js +241 -31
- package/dist/parser/natural-language.js.map +1 -1
- package/dist/parser/progression-parser.d.ts +1 -1
- package/dist/parser/progression-parser.js +1 -1
- package/dist/parser/progression-parser.js.map +1 -1
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +46 -25
- package/dist/pipeline.js.map +1 -1
- package/dist/progression/index.d.ts +5 -5
- package/dist/progression/index.js +3 -3
- package/dist/progression/index.js.map +1 -1
- package/dist/progression/progression-resolver.js +5 -5
- package/dist/progression/progression-resolver.js.map +1 -1
- package/dist/progression/roman-numeral.js +1 -1
- package/dist/progression/roman-numeral.js.map +1 -1
- package/dist/resolver/auto-layout.d.ts +1 -1
- package/dist/resolver/auto-layout.js +2 -2
- package/dist/resolver/auto-layout.js.map +1 -1
- package/dist/resolver/chord-resolver.d.ts +11 -1
- package/dist/resolver/chord-resolver.js +28 -2
- package/dist/resolver/chord-resolver.js.map +1 -1
- package/dist/resolver/scale-resolver.d.ts +5 -2
- package/dist/resolver/scale-resolver.js +28 -8
- package/dist/resolver/scale-resolver.js.map +1 -1
- package/dist/themes/boomwhacker.d.ts +1 -1
- package/dist/themes/boomwhacker.js +1 -1
- package/dist/themes/boomwhacker.js.map +1 -1
- package/dist/themes/crf.d.ts +1 -1
- package/dist/themes/crf.js +1 -1
- package/dist/themes/crf.js.map +1 -1
- package/dist/themes/index.d.ts +1 -1
- package/dist/themes/index.js +3 -3
- package/dist/themes/index.js.map +1 -1
- package/dist/themes/simple.d.ts +1 -1
- package/dist/themes/simple.js +1 -1
- package/dist/themes/simple.js.map +1 -1
- package/dist/theory/constrained-variants.d.ts +46 -0
- package/dist/theory/constrained-variants.js +117 -0
- package/dist/theory/constrained-variants.js.map +1 -0
- package/dist/theory/degree-labels.d.ts +39 -0
- package/dist/theory/degree-labels.js +102 -1
- package/dist/theory/degree-labels.js.map +1 -1
- package/dist/theory/hand-constraints.d.ts +104 -0
- package/dist/theory/hand-constraints.js +214 -0
- package/dist/theory/hand-constraints.js.map +1 -0
- package/dist/types.d.ts +9 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,37 +1,41 @@
|
|
|
1
|
-
export { computeKeyboard, computeSvgDimensions } from "./engine/keyboard-layout";
|
|
2
|
-
export { mapHighlights, normalizeNote } from "./engine/highlight-mapper";
|
|
3
|
-
export { FLAT_TO_SHARP, SHARP_TO_FLAT, FLAT_KEYS, normalizeToSharps, spellForKey, spellWithPreference, PC_SEMITONES, MIDI_A3, assignAscendingOctaves, } from "./engine/note-spelling";
|
|
4
|
-
export { autoFingering, assignFingering } from "./engine/auto-fingering";
|
|
5
|
-
export { scaleAutoFingering } from "./engine/scale-fingering";
|
|
6
|
-
export { computeStaffLayout } from "./engine/staff-layout";
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
10
|
-
export {
|
|
11
|
-
export {
|
|
12
|
-
export {
|
|
13
|
-
export {
|
|
14
|
-
export {
|
|
1
|
+
export { computeKeyboard, computeSvgDimensions } from "./engine/keyboard-layout.js";
|
|
2
|
+
export { mapHighlights, normalizeNote } from "./engine/highlight-mapper.js";
|
|
3
|
+
export { FLAT_TO_SHARP, SHARP_TO_FLAT, FLAT_KEYS, normalizeToSharps, spellForKey, spellWithPreference, PC_SEMITONES, MIDI_A3, assignAscendingOctaves, } from "./engine/note-spelling.js";
|
|
4
|
+
export { autoFingering, assignFingering } from "./engine/auto-fingering.js";
|
|
5
|
+
export { scaleAutoFingering } from "./engine/scale-fingering.js";
|
|
6
|
+
export { computeStaffLayout } from "./engine/staff-layout.js";
|
|
7
|
+
export { buildMei } from "./engine/mei-builder.js";
|
|
8
|
+
export { getDefaultGlyphs, setDefaultGlyphs, BRAVURA_GLYPHS, PETALUMA_GLYPHS } from "./engine/staff-glyphs.js";
|
|
9
|
+
export { STAFF_LINE_SPACING, HALF_STAFF_SPACING, STAFF_WIDTH, CLEF_AREA_WIDTH, NOTE_COLUMN_X, NOTE_HEAD_RX, ACCIDENTAL_OFFSET, LEDGER_LINE_EXTEND, LEDGER_LINE_STROKE, STAFF_GAP, STAFF_TOP_MARGIN, STAFF_BOTTOM_MARGIN, STAFF_LINE_STROKE, BRACE_WIDTH, SECOND_OFFSET, ACCIDENTAL_COL_WIDTH, } from "./engine/staff-constants.js";
|
|
10
|
+
export { WHITE_KEY_WIDTH, WHITE_KEY_WIDTH_EXACT, WHITE_KEY_HEIGHT_COMPACT, WHITE_KEY_HEIGHT_EXACT, WHITE_KEY_RY, BLACK_KEY_WIDTH, BLACK_KEY_WIDTH_EXACT, BLACK_KEY_HEIGHT_COMPACT, BLACK_KEY_HEIGHT_EXACT, BLACK_KEY_RY, BLACK_KEY_OFFSETS, BLACK_KEY_OFFSETS_EXACT, WHITE_NOTES_WITH_SHARPS, WHITE_NOTE_ORDER, DEFAULT_WHITE_FILL, DEFAULT_BLACK_FILL, DEFAULT_STROKE, DEFAULT_STROKE_WIDTH, } from "./engine/svg-constants.js";
|
|
11
|
+
export { parseChordDescription } from "./parser/natural-language.js";
|
|
12
|
+
export { isProgressionRequest, parseProgressionRequest } from "./parser/progression-parser.js";
|
|
13
|
+
export { resolveChord } from "./resolver/chord-resolver.js";
|
|
14
|
+
export { calculateLayout, whiteIdxHasSharp } from "./resolver/auto-layout.js";
|
|
15
|
+
export { MAX_EXAMPLES, ENABLE_CHORD_LOGGING } from "./config.js";
|
|
15
16
|
// Progression
|
|
16
|
-
export { resolveProgression, tokenizeProgression } from "./progression/roman-numeral";
|
|
17
|
-
export { FORM_TEMPLATES, findTemplate } from "./progression/form-templates";
|
|
18
|
-
export { resolveProgressionRequest } from "./progression/progression-resolver";
|
|
17
|
+
export { resolveProgression, tokenizeProgression } from "./progression/roman-numeral.js";
|
|
18
|
+
export { FORM_TEMPLATES, findTemplate } from "./progression/form-templates.js";
|
|
19
|
+
export { resolveProgressionRequest } from "./progression/progression-resolver.js";
|
|
19
20
|
// Audio
|
|
20
|
-
export { generateMidiFile, downloadMidi } from "./audio/midi-export";
|
|
21
|
+
export { generateMidiFile, downloadMidi } from "./audio/midi-export.js";
|
|
21
22
|
// Logging
|
|
22
|
-
export { logChordRequest, LOG_SCHEMA_VERSION } from "./logging";
|
|
23
|
+
export { logChordRequest, LOG_SCHEMA_VERSION } from "./logging.js";
|
|
23
24
|
// Theory
|
|
24
|
-
export { classifyTones, parseInterval, minimalVoicing, dropOrder } from "./theory/chord-tones";
|
|
25
|
-
export { intervalToDegreeLabel, degreesForIntervals } from "./theory/degree-labels";
|
|
25
|
+
export { classifyTones, parseInterval, minimalVoicing, dropOrder } from "./theory/chord-tones.js";
|
|
26
|
+
export { intervalToDegreeLabel, degreesForIntervals, degreeLabelsForNotes } from "./theory/degree-labels.js";
|
|
26
27
|
// Scale resolver
|
|
27
|
-
export { resolveScale } from "./resolver/scale-resolver";
|
|
28
|
+
export { resolveScale } from "./resolver/scale-resolver.js";
|
|
28
29
|
// Themes
|
|
29
|
-
export { getTheme, resolveTheme } from "./themes";
|
|
30
|
-
export { CRF_PITCH_PALETTE, formatCmyk } from "./themes/crf";
|
|
30
|
+
export { getTheme, resolveTheme } from "./themes/index.js";
|
|
31
|
+
export { CRF_PITCH_PALETTE, formatCmyk } from "./themes/crf.js";
|
|
31
32
|
// Pipeline
|
|
32
|
-
export { processChordRequest } from "./pipeline";
|
|
33
|
+
export { processChordRequest } from "./pipeline.js";
|
|
33
34
|
// ChordSheet
|
|
34
|
-
export { resolveDefaults, chordRef, SYSTEM_DEFAULTS } from "./chord-sheet/defaults";
|
|
35
|
-
export { CHORD_SHEET_SCHEMA_VERSION, validateVersion } from "./chord-sheet/schema";
|
|
36
|
-
export { encodeChordSheet, decodeChordSheet } from "./chord-sheet/codec";
|
|
35
|
+
export { resolveDefaults, chordRef, SYSTEM_DEFAULTS } from "./chord-sheet/defaults.js";
|
|
36
|
+
export { CHORD_SHEET_SCHEMA_VERSION, validateVersion } from "./chord-sheet/schema.js";
|
|
37
|
+
export { encodeChordSheet, decodeChordSheet } from "./chord-sheet/codec.js";
|
|
38
|
+
// Hand Constraints
|
|
39
|
+
export { midiOf, placeAscending, spanOf, constrainVoicing } from "./theory/hand-constraints.js";
|
|
40
|
+
export { generateConstrainedVariants } from "./theory/constrained-variants.js";
|
|
37
41
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC5E,OAAO,EACL,aAAa,EAAE,aAAa,EAAE,SAAS,EACvC,iBAAiB,EAAE,WAAW,EAAE,mBAAmB,EACnD,YAAY,EAAE,OAAO,EAAE,sBAAsB,GAC9C,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAEjE,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE9D,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAE/G,OAAO,EACL,kBAAkB,EAAE,kBAAkB,EAAE,WAAW,EAAE,eAAe,EACpE,aAAa,EAAE,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAClE,kBAAkB,EAAE,SAAS,EAAE,gBAAgB,EAAE,mBAAmB,EACpE,iBAAiB,EAAE,WAAW,EAAE,aAAa,EAAE,oBAAoB,GACpE,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,eAAe,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,sBAAsB,EACxF,YAAY,EACZ,eAAe,EAAE,qBAAqB,EAAE,wBAAwB,EAAE,sBAAsB,EACxF,YAAY,EACZ,iBAAiB,EAAE,uBAAuB,EAC1C,uBAAuB,EAAE,gBAAgB,EACzC,kBAAkB,EAAE,kBAAkB,EAAE,cAAc,EAAE,oBAAoB,GAC7E,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAE/F,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAE5D,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAE9E,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEjE,cAAc;AACd,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACzF,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAE/E,OAAO,EAAE,yBAAyB,EAAE,MAAM,uCAAuC,CAAC;AAGlF,QAAQ;AACR,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAExE,UAAU;AACV,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGnE,SAAS;AACT,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAElG,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AAE7G,iBAAiB;AACjB,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAG5D,SAAS;AACT,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAGhE,WAAW;AACX,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAGpD,aAAa;AACb,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAAE,0BAA0B,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AACtF,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE5E,mBAAmB;AACnB,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAShG,OAAO,EAAE,2BAA2B,EAAE,MAAM,kCAAkC,CAAC"}
|
|
@@ -1,8 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// "
|
|
5
|
-
|
|
1
|
+
import { resolveChord } from "../resolver/chord-resolver.js";
|
|
2
|
+
// Filler runs in two passes, and the order is load-bearing.
|
|
3
|
+
//
|
|
4
|
+
// The articles "a"/"an" are also the note A, and by the time filler is stripped
|
|
5
|
+
// every modifier has already been cut out — so "A with note names" arrives as
|
|
6
|
+
// "A ", and so does "a compact". The residual cannot tell them apart, because
|
|
7
|
+
// the article's own noun is one of the things that was deleted.
|
|
8
|
+
//
|
|
9
|
+
// So the article pass decides on evidence the stripping cannot destroy: the
|
|
10
|
+
// character after it, the capital letter the user typed, and where the token
|
|
11
|
+
// sat in the original input. See `stripArticles`.
|
|
12
|
+
const FILLER_WORDS = /\b(show\s+me|draw|display|render|please|the|with|that|this|me|of)\b/gi;
|
|
13
|
+
const ARTICLE_RE = /\b(an?)\b/gi;
|
|
14
|
+
/** An article at the very end of the *original* input is the note A: "a", "an A". */
|
|
15
|
+
const ARTICLE_AT_INPUT_END = /\ban?(?=[#/]|\s*$)/i;
|
|
16
|
+
/**
|
|
17
|
+
* Remove English articles without eating the note A.
|
|
18
|
+
*
|
|
19
|
+
* Three things say "this is the note, not an article", none of which an earlier
|
|
20
|
+
* `.replace` can have invented:
|
|
21
|
+
*
|
|
22
|
+
* - what follows is `#` or `/` — only a root can be ("A#dim", "A/C#");
|
|
23
|
+
* - the token is a capital `A`, which is how a chord is written and not how an
|
|
24
|
+
* article is ("A minor", "A with note names", "A in second inversion");
|
|
25
|
+
* - the article ends the original input ("a", "show me an A").
|
|
26
|
+
*
|
|
27
|
+
* Anything else is an article, including one left trailing because its noun was
|
|
28
|
+
* stripped — "in a compact layout" must not resolve to the chord A.
|
|
29
|
+
*/
|
|
30
|
+
function stripArticles(text, input) {
|
|
31
|
+
const bareArticleAtEnd = ARTICLE_AT_INPUT_END.test(input);
|
|
32
|
+
return text.replace(ARTICLE_RE, (token, _g, offset, whole) => {
|
|
33
|
+
const rest = whole.slice(offset + token.length);
|
|
34
|
+
if (/^[#/]/.test(rest))
|
|
35
|
+
return token;
|
|
36
|
+
if (token === "A")
|
|
37
|
+
return token;
|
|
38
|
+
if (/^\s*$/.test(rest) && bareArticleAtEnd)
|
|
39
|
+
return token;
|
|
40
|
+
return "";
|
|
41
|
+
});
|
|
42
|
+
}
|
|
6
43
|
const FORMAT_RE = /\b(compact|exact|full)\b/i;
|
|
7
44
|
const FORMAT_FULL_RE = /\bfull\s+(?:layout|height|size|keys?)\b/i;
|
|
8
45
|
// "size 80" / "size 80%" / "size 0.8" / "size xl" / "keys xl"
|
|
@@ -55,7 +92,7 @@ const FINGERING_RE = /(?:with\s+)?finger(?:ing|s)?\s+([\dxX\-](?:[,\s\-]*[\dxX\-
|
|
|
55
92
|
const VALID_FINGERING = new Set(["0", "1", "2", "3", "4", "5", "-", "x"]);
|
|
56
93
|
// "custom fingering "A0,A1,A2"" / "fingering "G3-G0-E1"" / "fingering xl "1-2-3"" / "fingering "1-2-3" 2xl"
|
|
57
94
|
// Quoted strings after "fingering" or "custom fingering" are treated as free-form labels.
|
|
58
|
-
const CUSTOM_FINGERING_RE = /(?:with\s+)?(?:custom\s+)?finger(?:ing|s)?\s+(?:(?:in\s+)?(base|lg|xl|2xl)\s+)?["
|
|
95
|
+
const CUSTOM_FINGERING_RE = /(?:with\s+)?(?:custom\s+)?finger(?:ing|s)?\s+(?:(?:in\s+)?(base|lg|xl|2xl)\s+)?["“”']([^"“”']+)["“”'](?:\s+(?:in\s+)?(base|lg|xl|2xl))?/i;
|
|
59
96
|
// "with fingerings" / "show fingering" / "with fingering in xl" / "fingering 2xl" (no explicit numbers → auto)
|
|
60
97
|
// Negative lookahead: don't match when followed by digits (explicit fingering like "1 2 3 5")
|
|
61
98
|
const AUTO_FINGERING_RE = /(?:with\s+)?(?:show\s+)?finger(?:ings?|s)?(?:\s+(?:in\s+)?(base|lg|xl|2xl)|\s+(base|lg|xl|2xl))?(?!\s+\d)(?:\s|$)/i;
|
|
@@ -73,10 +110,34 @@ const UNAMBIGUOUS_SCALE_TYPES = [
|
|
|
73
110
|
"major\\s+pentatonic", "minor\\s+pentatonic", "pentatonic",
|
|
74
111
|
"blues", "whole\\s+tone", "bebop", "diminished",
|
|
75
112
|
];
|
|
76
|
-
// Scale patterns: "D major scale", "C blues", "A dorian",
|
|
113
|
+
// Scale patterns: "D major scale", "C blues", "A dorian", "Dm harmonic minor".
|
|
77
114
|
// For "major"/"minor" alone, require the trailing word "scale" to avoid chord collision.
|
|
78
|
-
|
|
79
|
-
|
|
115
|
+
//
|
|
116
|
+
// The root must not be preceded by a letter, or the trailing "c" of
|
|
117
|
+
// "harmonic"/"melodic" is read as a root note and "dm harmonic minor" resolves
|
|
118
|
+
// to C minor. An optional chord-quality marker may sit between the root and the
|
|
119
|
+
// scale type, so shorthand roots ("dm", "f#m", "dmaj") work the same as bare
|
|
120
|
+
// ones — the named scale type wins, the marker is only there to be absorbed.
|
|
121
|
+
const ROOT = "(?<![A-Za-z#b])([A-Ga-g][#b]?)";
|
|
122
|
+
const QUALITY_MARKER = "(?:\\s*(?:maj|min|m|M)\\b)?";
|
|
123
|
+
const SCALE_UNAMBIGUOUS_RE = new RegExp(`${ROOT}${QUALITY_MARKER}\\s+(${UNAMBIGUOUS_SCALE_TYPES.join("|")})(?:\\s+scale)?`, "i");
|
|
124
|
+
const SCALE_EXPLICIT_RE = new RegExp(`${ROOT}${QUALITY_MARKER}\\s+(major|minor)\\s+scale`, "i");
|
|
125
|
+
// Chord-shorthand scales: "dm scale", "d maj scale", "dmaj scale", "d scale".
|
|
126
|
+
// Only the major/minor markers — anything richer stays a chord. The word
|
|
127
|
+
// "scale" is required, so a bare "Dm" is still a chord.
|
|
128
|
+
// The quality marker is matched case-insensitively but read case-sensitively,
|
|
129
|
+
// since "M" means major and "m" means minor.
|
|
130
|
+
const SCALE_SHORTHAND_RE = /(?<![A-Za-z#b])([A-Ga-g][#b]?)\s*(major|maj|minor|min|M|m)?\s+scale\b/i;
|
|
131
|
+
/** Resolve a shorthand quality marker to a scale type. Absent marker = major. */
|
|
132
|
+
function shorthandScaleType(marker) {
|
|
133
|
+
if (!marker)
|
|
134
|
+
return "major";
|
|
135
|
+
if (marker === "M")
|
|
136
|
+
return "major";
|
|
137
|
+
if (marker === "m")
|
|
138
|
+
return "minor";
|
|
139
|
+
return marker.toLowerCase().startsWith("min") ? "minor" : "major";
|
|
140
|
+
}
|
|
80
141
|
// "ascending" / "descending" for melodic minor
|
|
81
142
|
const DIRECTION_RE = /\b(ascending|descending)\b/i;
|
|
82
143
|
// "in N octaves" / "N octaves" — works for both scales and arpeggios
|
|
@@ -84,7 +145,17 @@ const OCTAVES_RE = /\b(?:in\s+)?(\d+)\s+octaves?\b/i;
|
|
|
84
145
|
// "degrees" anywhere — combined with note names if present, standalone otherwise.
|
|
85
146
|
// The literal word "degree(s)" isn't used by any other parser rule
|
|
86
147
|
// (bass-degree / starting-degree use "5th", "9th", etc.), so a broad match is safe.
|
|
87
|
-
|
|
148
|
+
/*
|
|
149
|
+
* Two forms, because only one of them is unambiguous.
|
|
150
|
+
*
|
|
151
|
+
* "degrees in lg" names the degrees' own size and always keeps it. "degrees lg"
|
|
152
|
+
* is a bare size between two keywords, and NOTE_NAMES_RE accepts exactly that
|
|
153
|
+
* shape in front of its own keyword ("lg note names") — so in "degrees 2xl note
|
|
154
|
+
* names" the size belongs to what follows it, not to what precedes it. Without
|
|
155
|
+
* the lookahead the degrees clause swallowed it and the note names silently
|
|
156
|
+
* fell back to the default.
|
|
157
|
+
*/
|
|
158
|
+
const DEGREES_KEYWORD_RE = /\bdegrees?(?:\s+(?:in\s+(base|lg|xl|2xl)|(base|lg|xl|2xl)(?!\s+(?:midi\s+)?note\s*names?)))?\b/i;
|
|
88
159
|
// "with heading" / "with a heading" / "show heading"
|
|
89
160
|
const HEADING_RE = /\b(?:with\s+)?(?:a\s+)?(?:show\s+)?heading\b/i;
|
|
90
161
|
// All recognized hand/clef phrases — long forms before shorthand to ensure
|
|
@@ -93,10 +164,16 @@ const HAND_CLEF_PATTERN = "left\\s+hand|right\\s+hand|bottom\\s+hand|top\\s+hand
|
|
|
93
164
|
// 2+ note tokens separated by spaces/commas/dashes; "and" allowed between tokens.
|
|
94
165
|
// Single-letter notes (A-G) plus optional accidental (#/b) and optional single-digit octave.
|
|
95
166
|
const NOTE_LIST_PATTERN = "[A-Ga-g][#b]?\\d?(?:[\\s,\\-]+(?:and\\s+)?[A-Ga-g][#b]?\\d?)+";
|
|
96
|
-
//
|
|
97
|
-
|
|
167
|
+
// After the explicit "notes" keyword, a token may be a run-together sequence
|
|
168
|
+
// of notes with no separators ("cdefgabc", "EbGbBb", "E4G4C5"). The trailing
|
|
169
|
+
// lookahead stops the run at any non-note word ("and", "in", "with", ...)
|
|
170
|
+
// while still allowing a run to end in an accidental ("C#").
|
|
171
|
+
const NOTE_RUN_PATTERN = "(?:[A-Ga-g][#b]?\\d?)+(?![A-Za-z0-9])";
|
|
172
|
+
// "with notes C E G" / "notes E4 G4 C5" / "notes cdefgabc" — optional
|
|
173
|
+
// trailing "in <hand|clef>".
|
|
174
|
+
const NOTES_GROUP_RE = new RegExp(`(?:with\\s+|and\\s+)?notes\\s+(${NOTE_RUN_PATTERN}(?:[\\s,\\-]+${NOTE_RUN_PATTERN})*)(?:\\s+in\\s+(?:the\\s+)?(${HAND_CLEF_PATTERN}))?`, "gi");
|
|
98
175
|
// "notes in lh Eb Gb Bb" / "notes in the right hand D F A" — prefix form.
|
|
99
|
-
const NOTES_GROUP_PREFIX_RE = new RegExp(`(?:with\\s+|and\\s+)?notes\\s+in\\s+(?:the\\s+)?(${HAND_CLEF_PATTERN})\\s+(
|
|
176
|
+
const NOTES_GROUP_PREFIX_RE = new RegExp(`(?:with\\s+|and\\s+)?notes\\s+in\\s+(?:the\\s+)?(${HAND_CLEF_PATTERN})\\s+(${NOTE_RUN_PATTERN}(?:[\\s,\\-]+${NOTE_RUN_PATTERN})*)`, "gi");
|
|
100
177
|
// "lh Eb Gb Bb" / "lh: Eb Gb Bb" / "left Eb Gb Bb" / "bass clef C E G"
|
|
101
178
|
// Hand keyword first, no "notes" word. Optional colon between hand and notes.
|
|
102
179
|
const HAND_PREFIX_BARE_RE = new RegExp(`\\b(${HAND_CLEF_PATTERN})\\s*(?::\\s*|\\s+)(${NOTE_LIST_PATTERN})`, "gi");
|
|
@@ -114,6 +191,34 @@ const SINGLE_NOTE_PATTERN = "[A-Ga-g][#b]?";
|
|
|
114
191
|
const HAND_PREFIX_SINGLE_RE = new RegExp(`\\b(${HAND_CLEF_PATTERN})\\s*(?::\\s*|\\s+)(${SINGLE_NOTE_PATTERN})(?=[\\s,;]|$)`, "gi");
|
|
115
192
|
// "Bb in lh" / "Bb lh" / "(Bb) lh" — single bare note + hand.
|
|
116
193
|
const NOTE_BARE_HAND_SINGLE_RE = new RegExp(`\\(?\\b(${SINGLE_NOTE_PATTERN})\\b\\)?\\s*(?:in\\s+(?:the\\s+)?)?(${HAND_CLEF_PATTERN})\\b`, "gi");
|
|
194
|
+
// A chord symbol following a hand keyword: "rh Cmaj7", "lh: Bbm7b5",
|
|
195
|
+
// "left hand Am", "bass clef G7/B". Deliberately permissive — the token is
|
|
196
|
+
// validated by actually resolving it as a chord, so junk never becomes a group.
|
|
197
|
+
const CHORD_TOKEN_PATTERN = "[A-Ga-g][#b]?[A-Za-z0-9#b\\u00b0\\u00f8+\\-/]*";
|
|
198
|
+
// "rh Cmaj7 lh Dm7" — hand keyword + chord symbol(s). The trailing group
|
|
199
|
+
// captures any further chord tokens so "rh Cmaj7 Dm7" can drop the second one:
|
|
200
|
+
// only the first chord after a hand keyword is used.
|
|
201
|
+
const HAND_PREFIX_CHORD_RE = new RegExp(`\\b(${HAND_CLEF_PATTERN})\\s*(?::\\s*|\\s+)(${CHORD_TOKEN_PATTERN})((?:\\s+${CHORD_TOKEN_PATTERN})*)`, "gi");
|
|
202
|
+
// A plain pitch class ("C", "Eb") — the single-note pass already claims those.
|
|
203
|
+
// Tokens carrying a digit ("G7", "C9") read as chords here, matching how
|
|
204
|
+
// "G7 in lh" / "Bb7 in lh" are treated elsewhere in this parser; an
|
|
205
|
+
// octave-qualified note needs the explicit "notes" keyword ("notes G7 in lh").
|
|
206
|
+
const BARE_NOTE_TOKEN_RE = /^[A-Ga-g][#b]?$/;
|
|
207
|
+
/**
|
|
208
|
+
* Resolve a token as a chord symbol, or undefined when it isn't one.
|
|
209
|
+
* Bare pitches are rejected so "lh Bb" stays a single-note assignment.
|
|
210
|
+
*/
|
|
211
|
+
function chordTokenNotes(token) {
|
|
212
|
+
if (BARE_NOTE_TOKEN_RE.test(token))
|
|
213
|
+
return undefined;
|
|
214
|
+
try {
|
|
215
|
+
const { notes } = resolveChord(token);
|
|
216
|
+
return notes.length > 0 ? notes : undefined;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
117
222
|
// "Eb Gb Bb // Db Eb F Gb" — polychord-style "top over bottom" (rh then lh).
|
|
118
223
|
const POLYCHORD_SLASH_RE = new RegExp(`(${NOTE_LIST_PATTERN})\\s*\\/\\/\\s*(${NOTE_LIST_PATTERN})`, "gi");
|
|
119
224
|
// "Eb Gb Bb; Db Eb F Gb" — semicolon separator; reading order (lh then rh).
|
|
@@ -237,8 +342,16 @@ export function parseChordDescription(input) {
|
|
|
237
342
|
const isDown = direction === "down" || direction === "lower";
|
|
238
343
|
result.bassOctaveShift = isDown ? -count : count;
|
|
239
344
|
}
|
|
345
|
+
/*
|
|
346
|
+
* Note-name sizes are read from the input with the degrees clause taken out.
|
|
347
|
+
* NOTE_NAMES_RE accepts a size *before* the keyword ("xl note names"), so in
|
|
348
|
+
* "with degrees in base note names in 2xl" it read the degrees' own "base" as
|
|
349
|
+
* the note-name size. Two clauses that each carry a size must not be able to
|
|
350
|
+
* read each other's.
|
|
351
|
+
*/
|
|
352
|
+
const sizeSource = input.replace(DEGREES_KEYWORD_RE, " ");
|
|
240
353
|
// Extract "midi note names" / "with midi names" (check before regular note names)
|
|
241
|
-
const midiNamesMatch =
|
|
354
|
+
const midiNamesMatch = sizeSource.match(MIDI_NAMES_RE);
|
|
242
355
|
if (midiNamesMatch) {
|
|
243
356
|
result.showNoteNames = true;
|
|
244
357
|
result.noteNameMode = "midi";
|
|
@@ -246,7 +359,7 @@ export function parseChordDescription(input) {
|
|
|
246
359
|
}
|
|
247
360
|
// Extract "with note names" / "note names in xl" / "xl note names"
|
|
248
361
|
if (!midiNamesMatch) {
|
|
249
|
-
const noteNamesMatch =
|
|
362
|
+
const noteNamesMatch = sizeSource.match(NOTE_NAMES_RE);
|
|
250
363
|
if (noteNamesMatch) {
|
|
251
364
|
result.showNoteNames = true;
|
|
252
365
|
// Size can be in group 1 (before "note names"), 2 (after), or 3 ("name the notes in xl")
|
|
@@ -261,7 +374,13 @@ export function parseChordDescription(input) {
|
|
|
261
374
|
// Check custom fingering FIRST ("custom fingering A0,A1,A2")
|
|
262
375
|
const customFingerMatch = input.match(CUSTOM_FINGERING_RE);
|
|
263
376
|
if (customFingerMatch) {
|
|
264
|
-
|
|
377
|
+
// Comma-separated lists split ONLY on commas/spaces so "-" placeholders
|
|
378
|
+
// survive as positional blanks ("1,-,3"); dash-separated lists keep the
|
|
379
|
+
// documented "D2-D1-D0-D1" behavior.
|
|
380
|
+
const raw = customFingerMatch[2];
|
|
381
|
+
result.customFingering = (raw.includes(",")
|
|
382
|
+
? raw.split(/[,\s]+/)
|
|
383
|
+
: raw.split(/[\-\s]+/)).filter(Boolean);
|
|
265
384
|
result.fingeringSize = toTextSize(customFingerMatch[1]) ?? toTextSize(customFingerMatch[3]);
|
|
266
385
|
}
|
|
267
386
|
// Check auto fingering ("with fingering", "fingering 2xl", "fingering in xl")
|
|
@@ -330,27 +449,64 @@ export function parseChordDescription(input) {
|
|
|
330
449
|
// segments separated by "and" / "with". Paired example:
|
|
331
450
|
// "notes C E G in bass clef and notes B D F in treble clef"
|
|
332
451
|
const groups = [];
|
|
452
|
+
// Explode a run-together token ("cdefgabc", "EbGbBb", "E4G4C5") into
|
|
453
|
+
// individual notes. In runs, a lowercase 'b' binds as a flat only after
|
|
454
|
+
// an UPPERCASE letter ("EbGbBb") — in lowercase runs like "cdefgabc"
|
|
455
|
+
// every letter is its own note, so 'b' stays the note B.
|
|
456
|
+
const explodeNoteRun = (token) => {
|
|
457
|
+
const out = [];
|
|
458
|
+
let i = 0;
|
|
459
|
+
while (i < token.length) {
|
|
460
|
+
const letter = token[i];
|
|
461
|
+
if (!/[A-Ga-g]/.test(letter))
|
|
462
|
+
return [token]; // not a pure note run
|
|
463
|
+
i++;
|
|
464
|
+
let accidental = "";
|
|
465
|
+
if (token[i] === "#") {
|
|
466
|
+
accidental = "#";
|
|
467
|
+
i++;
|
|
468
|
+
}
|
|
469
|
+
else if (token[i] === "b" && letter === letter.toUpperCase()) {
|
|
470
|
+
accidental = "b";
|
|
471
|
+
i++;
|
|
472
|
+
}
|
|
473
|
+
let octave = "";
|
|
474
|
+
if (/\d/.test(token[i] ?? "")) {
|
|
475
|
+
octave = token[i];
|
|
476
|
+
i++;
|
|
477
|
+
}
|
|
478
|
+
out.push(letter.toUpperCase() + accidental + octave);
|
|
479
|
+
}
|
|
480
|
+
return out;
|
|
481
|
+
};
|
|
333
482
|
const normalizeTokens = (raw) => raw
|
|
334
483
|
.split(/[\s,\-]+/)
|
|
335
484
|
.filter(Boolean)
|
|
336
|
-
.
|
|
485
|
+
.flatMap((t) => {
|
|
486
|
+
// Single-note tokens keep their exact old handling ("eb5" → Eb5,
|
|
487
|
+
// "bb" → Bb) — run explosion applies only to longer sequences.
|
|
337
488
|
const nm = t.match(/^([A-Ga-g])([#b])?(\d)?$/);
|
|
338
|
-
if (
|
|
339
|
-
return
|
|
340
|
-
return
|
|
489
|
+
if (nm)
|
|
490
|
+
return [nm[1].toUpperCase() + (nm[2] ?? "") + (nm[3] ?? "")];
|
|
491
|
+
return explodeNoteRun(t);
|
|
341
492
|
});
|
|
342
493
|
// Note-group extraction runs in priority order. Each pass strips its matches
|
|
343
494
|
// from `residual` so later (looser) passes can't re-consume the same notes.
|
|
344
495
|
let residual = input;
|
|
496
|
+
// Hand-prefixed chord phrases actually consumed by pass 4d, so the chord-name
|
|
497
|
+
// pass below strips exactly those (and not look-alikes it rejected).
|
|
498
|
+
const handChordMatches = [];
|
|
345
499
|
const stripFromResidual = (match) => {
|
|
346
500
|
residual = residual.replace(match, " ");
|
|
347
501
|
};
|
|
348
|
-
const pushGroup = (tokens, hand, clef) => {
|
|
502
|
+
const pushGroup = (tokens, hand, clef, chord) => {
|
|
349
503
|
const g = { notes: tokens };
|
|
350
504
|
if (hand)
|
|
351
505
|
g.hand = hand;
|
|
352
506
|
if (clef)
|
|
353
507
|
g.clef = clef;
|
|
508
|
+
if (chord)
|
|
509
|
+
g.chord = chord;
|
|
354
510
|
groups.push(g);
|
|
355
511
|
};
|
|
356
512
|
// 1. "notes in <hand> <notes>" (prefix with explicit "notes" keyword)
|
|
@@ -424,6 +580,32 @@ export function parseChordDescription(input) {
|
|
|
424
580
|
pushGroup(tokens, hand, clef);
|
|
425
581
|
stripFromResidual(m[0]);
|
|
426
582
|
}
|
|
583
|
+
// 4d. "rh Cmaj7 lh Dm7" — hand keyword + chord symbol. Runs after every
|
|
584
|
+
// bare-note pass so note lists still win; whatever is left here is a chord.
|
|
585
|
+
// Only the first chord after a hand keyword is used — trailing chords are
|
|
586
|
+
// dropped (and removed from the residual so they don't become the chord name).
|
|
587
|
+
HAND_PREFIX_CHORD_RE.lastIndex = 0;
|
|
588
|
+
for (const m of [...residual.matchAll(HAND_PREFIX_CHORD_RE)]) {
|
|
589
|
+
const { hand, clef } = parseHandOrClef(m[1]);
|
|
590
|
+
if (!hand && !clef)
|
|
591
|
+
continue;
|
|
592
|
+
const notes = chordTokenNotes(m[2]);
|
|
593
|
+
if (!notes)
|
|
594
|
+
continue;
|
|
595
|
+
pushGroup(notes, hand, clef, m[2]);
|
|
596
|
+
// Drop the run of extra chord tokens that directly follows, stopping at
|
|
597
|
+
// the first token that isn't a chord so unrelated text survives.
|
|
598
|
+
let extras = m[3] ?? "";
|
|
599
|
+
while (extras.length > 0) {
|
|
600
|
+
const next = extras.match(/^\s+(\S+)/);
|
|
601
|
+
if (!next || !chordTokenNotes(next[1]))
|
|
602
|
+
break;
|
|
603
|
+
extras = extras.slice(next[0].length);
|
|
604
|
+
}
|
|
605
|
+
const consumed = m[0].slice(0, m[0].length - extras.length);
|
|
606
|
+
handChordMatches.push(consumed);
|
|
607
|
+
stripFromResidual(consumed);
|
|
608
|
+
}
|
|
427
609
|
// 5. "Eb Gb Bb // Db Eb F Gb" — polychord-style "top over bottom" (rh, lh).
|
|
428
610
|
POLYCHORD_SLASH_RE.lastIndex = 0;
|
|
429
611
|
for (const m of [...residual.matchAll(POLYCHORD_SLASH_RE)]) {
|
|
@@ -454,21 +636,26 @@ export function parseChordDescription(input) {
|
|
|
454
636
|
if (themeMatch) {
|
|
455
637
|
result.colorTheme = THEME_MAP[themeMatch[1].toLowerCase()] ?? themeMatch[1].toLowerCase();
|
|
456
638
|
}
|
|
457
|
-
// Degrees keyword is independent — combines with note names
|
|
458
|
-
// otherwise stands alone as degree-only mode.
|
|
459
|
-
//
|
|
639
|
+
// Degrees keyword is independent — combines with whichever note names are
|
|
640
|
+
// already detected, otherwise stands alone as degree-only mode. Midi names
|
|
641
|
+
// combine too: "midi note names with degrees" is a real request for two rows,
|
|
642
|
+
// and skipping the clause there dropped both the mode and the degree size, so
|
|
643
|
+
// the degrees the user asked for were silently never drawn.
|
|
460
644
|
const degMatch = input.match(DEGREES_KEYWORD_RE);
|
|
461
|
-
if (degMatch
|
|
645
|
+
if (degMatch) {
|
|
462
646
|
if (result.showNoteNames) {
|
|
463
|
-
result.noteNameMode = "pitch-class+degree";
|
|
647
|
+
result.noteNameMode = result.noteNameMode === "midi" ? "midi+degree" : "pitch-class+degree";
|
|
464
648
|
}
|
|
465
649
|
else {
|
|
466
650
|
result.showNoteNames = true;
|
|
467
651
|
result.noteNameMode = "degree";
|
|
468
652
|
}
|
|
469
|
-
|
|
653
|
+
// Its own field. Writing it to noteNameSize meant the second size in
|
|
654
|
+
// "note names in xl with degrees in lg" overwrote the first, and both rows
|
|
655
|
+
// came out at lg — the note names silently demoted to the degrees' size.
|
|
656
|
+
const sz = toTextSize(degMatch[1]) ?? toTextSize(degMatch[2]);
|
|
470
657
|
if (sz)
|
|
471
|
-
result.
|
|
658
|
+
result.degreeSize = sz;
|
|
472
659
|
}
|
|
473
660
|
// Extract scale direction
|
|
474
661
|
const dirMatch = input.match(DIRECTION_RE);
|
|
@@ -478,10 +665,12 @@ export function parseChordDescription(input) {
|
|
|
478
665
|
// Extract octave count (used by both scales and arpeggios)
|
|
479
666
|
const octavesMatch = input.match(OCTAVES_RE);
|
|
480
667
|
const octaveCount = octavesMatch ? parseInt(octavesMatch[1], 10) : undefined;
|
|
481
|
-
// Detect scales — check BEFORE chord extraction
|
|
668
|
+
// Detect scales — check BEFORE chord extraction. Spelled-out forms first,
|
|
669
|
+
// then chord shorthand ("dm scale"), so "d minor scale" keeps its own path.
|
|
482
670
|
const scaleUnambig = input.match(SCALE_UNAMBIGUOUS_RE);
|
|
483
671
|
const scaleExplicit = input.match(SCALE_EXPLICIT_RE);
|
|
484
672
|
const scaleMatch = scaleUnambig || scaleExplicit;
|
|
673
|
+
const scaleShorthand = scaleMatch ? null : input.match(SCALE_SHORTHAND_RE);
|
|
485
674
|
if (scaleMatch) {
|
|
486
675
|
const scaleRoot = capitalizeNote(scaleMatch[1]);
|
|
487
676
|
const scaleType = scaleMatch[2].toLowerCase().replace(/\s+/g, " ").trim();
|
|
@@ -489,6 +678,12 @@ export function parseChordDescription(input) {
|
|
|
489
678
|
result.scaleName = `${scaleRoot} ${scaleType}`;
|
|
490
679
|
result.scaleOctaves = octaveCount ?? 1;
|
|
491
680
|
}
|
|
681
|
+
else if (scaleShorthand) {
|
|
682
|
+
const scaleRoot = capitalizeNote(scaleShorthand[1]);
|
|
683
|
+
result.isScale = true;
|
|
684
|
+
result.scaleName = `${scaleRoot} ${shorthandScaleType(scaleShorthand[2])}`;
|
|
685
|
+
result.scaleOctaves = octaveCount ?? 1;
|
|
686
|
+
}
|
|
492
687
|
else if (octaveCount) {
|
|
493
688
|
// Not a scale but has "N octaves" → arpeggio mode for chord
|
|
494
689
|
result.chordOctaves = octaveCount;
|
|
@@ -502,8 +697,11 @@ export function parseChordDescription(input) {
|
|
|
502
697
|
else if (startMatch) {
|
|
503
698
|
result.startingNote = capitalizeNote(startMatch[1]);
|
|
504
699
|
}
|
|
505
|
-
// Strip extracted patterns and filler for chord name extraction
|
|
506
|
-
|
|
700
|
+
// Strip extracted patterns and filler for chord name extraction. Chord
|
|
701
|
+
// phrases already claimed by a hand go first — they're plain substrings of
|
|
702
|
+
// the input, so removing them here keeps them out of the chord name.
|
|
703
|
+
let cleaned = handChordMatches
|
|
704
|
+
.reduce((text, phrase) => text.replace(phrase, " "), input)
|
|
507
705
|
.replace(FORMAT_FULL_RE, "")
|
|
508
706
|
.replace(FORMAT_RE, "")
|
|
509
707
|
.replace(SIZE_NAME_RE, "")
|
|
@@ -516,6 +714,11 @@ export function parseChordDescription(input) {
|
|
|
516
714
|
.replace(PADDING_RE, "")
|
|
517
715
|
.replace(CHORD_OCTAVE_RE, "")
|
|
518
716
|
.replace(BASS_OCTAVE_RE, "")
|
|
717
|
+
// Before NOTE_NAMES_RE, which matches the "note names" half on its own and
|
|
718
|
+
// would leave "midi" behind. The leftover word is not inert: CHORD_RE's
|
|
719
|
+
// quality class contains `m`, so "C midi" parsed as Cm, and an A root lost
|
|
720
|
+
// to FILLER_WORDS let the extractor find its root in the `d` of "midi".
|
|
721
|
+
.replace(MIDI_NAMES_RE, "")
|
|
519
722
|
.replace(NOTE_NAMES_RE, "")
|
|
520
723
|
.replace(CUSTOM_FINGERING_RE, "")
|
|
521
724
|
.replace(FINGERING_RE, "")
|
|
@@ -532,6 +735,10 @@ export function parseChordDescription(input) {
|
|
|
532
735
|
.replace(OCTAVES_RE, "")
|
|
533
736
|
.replace(SCALE_UNAMBIGUOUS_RE, "")
|
|
534
737
|
.replace(SCALE_EXPLICIT_RE, "")
|
|
738
|
+
.replace(SCALE_SHORTHAND_RE, "")
|
|
739
|
+
// A loose "midi" is itself the request for midi note names (see the
|
|
740
|
+
// fallback above), so it has to leave with the rest of the request.
|
|
741
|
+
.replace(/\bmidi\b/gi, "")
|
|
535
742
|
.replace(/\bscale\b/gi, "")
|
|
536
743
|
.replace(HEADING_RE, "")
|
|
537
744
|
.replace(NOTES_GROUP_PREFIX_RE, "")
|
|
@@ -557,6 +764,9 @@ export function parseChordDescription(input) {
|
|
|
557
764
|
.replace(/\bin\b/gi, "")
|
|
558
765
|
.replace(/\s+/g, " ")
|
|
559
766
|
.trim();
|
|
767
|
+
// Last, so the article test is asked of text that is finished — and of the
|
|
768
|
+
// original input, which still knows what the article was standing in front of.
|
|
769
|
+
cleaned = stripArticles(cleaned, input).replace(/\s+/g, " ").trim();
|
|
560
770
|
// Replace word-based accidentals before chord extraction
|
|
561
771
|
cleaned = cleaned
|
|
562
772
|
.replace(/\bsharp\b/gi, "#")
|