@goplayerjuggler/abc-tools 1.0.4 → 1.0.6
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/.editorconfig +11 -2
- package/eslint.config.js +41 -44
- package/package.json +1 -1
- package/src/incipit.js +152 -10
- package/src/index.js +10 -1
- package/src/javascriptify.js +84 -0
- package/src/manipulator.js +1 -1
- package/src/math.js +16 -5
- package/src/parse/note-parser.js +0 -1
- package/src/parse/parser.js +43 -3
- package/src/parse/token-utils.js +72 -3
- package/src/{contour-sort.js → sort/contour-sort.js} +73 -96
- package/src/sort/contour-svg.js +238 -0
- package/src/sort/display-contour.js +67 -0
- package/src/sort/encode.js +60 -0
- package/test-output/svg/comparison-high-range-auto-range.svg +11 -0
- package/test-output/svg/comparison-high-range.svg +11 -0
- package/test-output/svg/comparison-low-range-auto-range.svg +11 -0
- package/test-output/svg/comparison-low-range.svg +11 -0
- package/test-output/svg/comparison-mid-range-auto-range.svg +11 -0
- package/test-output/svg/comparison-mid-range.svg +11 -0
- package/test-output/svg/held-vs-repeated-auto-range.svg +9 -0
- package/test-output/svg/held-vs-repeated.svg +9 -0
- package/test-output/svg/multiple-silences-auto-range.svg +9 -0
- package/test-output/svg/multiple-silences.svg +9 -0
- package/test-output/svg/simple-ascending-auto-range.svg +17 -0
- package/test-output/svg/simple-ascending.svg +17 -0
- package/test-output/svg/the-munster-auto-range.svg +21 -0
- package/test-output/svg/the-munster.svg +21 -0
- package/test-output/svg/with-silences-auto-range.svg +9 -0
- package/test-output/svg/with-silences.svg +9 -0
- package/test-output/svg/with-subdivisions-auto-range.svg +17 -0
- package/test-output/svg/with-subdivisions.svg +17 -0
package/.editorconfig
CHANGED
|
@@ -4,7 +4,7 @@ root = true
|
|
|
4
4
|
|
|
5
5
|
# Unix-style newlines with a newline ending every file
|
|
6
6
|
[*]
|
|
7
|
-
end_of_line =
|
|
7
|
+
end_of_line = crlf
|
|
8
8
|
insert_final_newline = true
|
|
9
9
|
|
|
10
10
|
# Matches multiple files with brace expansion notation
|
|
@@ -14,4 +14,13 @@ charset = utf-8
|
|
|
14
14
|
|
|
15
15
|
# Indentation override for all JS
|
|
16
16
|
[**.js]
|
|
17
|
-
indent_style = tab
|
|
17
|
+
indent_style = tab
|
|
18
|
+
|
|
19
|
+
# Ignore paths
|
|
20
|
+
[{tunes.json,**/*tunebook*}.js]
|
|
21
|
+
charset = unset
|
|
22
|
+
end_of_line = unset
|
|
23
|
+
insert_final_newline = unset
|
|
24
|
+
trim_trailing_whitespace = unset
|
|
25
|
+
indent_style = unset
|
|
26
|
+
indent_size = unset
|
package/eslint.config.js
CHANGED
|
@@ -1,44 +1,41 @@
|
|
|
1
|
-
const js = require("@eslint/js");
|
|
2
|
-
const globals = require("globals");
|
|
3
|
-
const pluginJest = require(
|
|
4
|
-
|
|
5
|
-
module.exports = [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
];
|
|
1
|
+
const js = require("@eslint/js");
|
|
2
|
+
const globals = require("globals");
|
|
3
|
+
const pluginJest = require("eslint-plugin-jest");
|
|
4
|
+
|
|
5
|
+
module.exports = [
|
|
6
|
+
{
|
|
7
|
+
...js.configs.recommended,
|
|
8
|
+
plugins: { jest: pluginJest },
|
|
9
|
+
languageOptions: {
|
|
10
|
+
...js.configs.recommended.languageOptions,
|
|
11
|
+
globals: {
|
|
12
|
+
...globals.node,
|
|
13
|
+
...pluginJest.environments.globals.globals,
|
|
14
|
+
},
|
|
15
|
+
ecmaVersion: 12,
|
|
16
|
+
sourceType: "module",
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
ignores: ["**/node_modules/", "**/coverage/", "**/dist/"],
|
|
20
|
+
rules: {
|
|
21
|
+
...js.configs.recommended.rules,
|
|
22
|
+
// "arrow-spacing": "error"
|
|
23
|
+
//curly: ["error", "all"],
|
|
24
|
+
// "eol-last": ["error", "always"],
|
|
25
|
+
eqeqeq: ["error", "always"],
|
|
26
|
+
"no-console": "off",
|
|
27
|
+
"no-eval": "error",
|
|
28
|
+
"no-implied-eval": "error",
|
|
29
|
+
// "no-trailing-spaces": "error",
|
|
30
|
+
"no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
|
|
31
|
+
"no-var": "error",
|
|
32
|
+
"no-with": "error",
|
|
33
|
+
"prefer-const": "error",
|
|
34
|
+
// "prefer-template": "error",
|
|
35
|
+
// "comma-dangle": ["error", "never"],
|
|
36
|
+
// "indent": ["error", 2],
|
|
37
|
+
// "quotes": ["error", "single", { avoidEscape: true }],
|
|
38
|
+
// "semi": ["error", "always"],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
];
|
package/package.json
CHANGED
package/src/incipit.js
CHANGED
|
@@ -4,6 +4,72 @@ const { getFirstBars } = require("./manipulator.js");
|
|
|
4
4
|
|
|
5
5
|
const { getUnitLength, getMeter } = require("./parse/parser.js");
|
|
6
6
|
|
|
7
|
+
const { getContour } = require("./sort/contour-sort.js");
|
|
8
|
+
|
|
9
|
+
//this file has code that's a fork of some code in Michael Eskin's abctools
|
|
10
|
+
|
|
11
|
+
//
|
|
12
|
+
// Clean an incipit line
|
|
13
|
+
//
|
|
14
|
+
function cleanIncipitLine(theTextIncipit) {
|
|
15
|
+
//console.log("Starting incipit:");
|
|
16
|
+
//console.log(theTextIncipit);
|
|
17
|
+
|
|
18
|
+
// Strip any embedded voice [V:*]
|
|
19
|
+
let searchRegExp = /\[V:\s*\d+\]/gm;
|
|
20
|
+
theTextIncipit = theTextIncipit.replace(searchRegExp, "");
|
|
21
|
+
//console.log(theTextIncipit);
|
|
22
|
+
|
|
23
|
+
// Strip any embedded voice V: *
|
|
24
|
+
//searchRegExp = /V: [^ ]+ /gm
|
|
25
|
+
searchRegExp = /V:\s+\S+\s/gm;
|
|
26
|
+
theTextIncipit = theTextIncipit.replace(searchRegExp, "");
|
|
27
|
+
//console.log(theTextIncipit);
|
|
28
|
+
|
|
29
|
+
// Strip any embedded voice V:*
|
|
30
|
+
searchRegExp = /V:[^ ]+ /gm;
|
|
31
|
+
theTextIncipit = theTextIncipit.replace(searchRegExp, "");
|
|
32
|
+
//console.log(theTextIncipit);
|
|
33
|
+
|
|
34
|
+
// Sanitize !*! style annotations, but keep !fermata!
|
|
35
|
+
searchRegExp = /!(?!fermata!)[^!\n]*!/gm;
|
|
36
|
+
theTextIncipit = theTextIncipit.replace(searchRegExp, "");
|
|
37
|
+
//console.log(theTextIncipit);
|
|
38
|
+
|
|
39
|
+
// Strip out repeat marks
|
|
40
|
+
theTextIncipit = theTextIncipit.replaceAll("|:", "|");
|
|
41
|
+
theTextIncipit = theTextIncipit.replaceAll(":|", "|");
|
|
42
|
+
|
|
43
|
+
// strip out 1st 2nd etc time repeats
|
|
44
|
+
searchRegExp = /\[\d(,\d)*/gm;
|
|
45
|
+
theTextIncipit = theTextIncipit.replace(searchRegExp, "");
|
|
46
|
+
|
|
47
|
+
//console.log(theTextIncipit);
|
|
48
|
+
|
|
49
|
+
// Strip out brackets
|
|
50
|
+
// theTextIncipit = theTextIncipit.replaceAll("[", "");
|
|
51
|
+
//console.log(theTextIncipit);
|
|
52
|
+
|
|
53
|
+
// Strip out brackets
|
|
54
|
+
// theTextIncipit = theTextIncipit.replaceAll("]", "");
|
|
55
|
+
//console.log(theTextIncipit);
|
|
56
|
+
|
|
57
|
+
// Strip out continuations
|
|
58
|
+
theTextIncipit = theTextIncipit.replaceAll("\\", "");
|
|
59
|
+
|
|
60
|
+
// Segno
|
|
61
|
+
theTextIncipit = theTextIncipit.replaceAll("S", "");
|
|
62
|
+
|
|
63
|
+
// Strip out comments
|
|
64
|
+
theTextIncipit = theTextIncipit.replace(/"[^"]+"/gm, "");
|
|
65
|
+
// Strip out inline parts
|
|
66
|
+
theTextIncipit = theTextIncipit.replace(/\[P:[Ⅰ\w]\]/gm, "");
|
|
67
|
+
|
|
68
|
+
//
|
|
69
|
+
theTextIncipit = theTextIncipit.replace(/^\|[:|]?/, "");
|
|
70
|
+
return theTextIncipit;
|
|
71
|
+
}
|
|
72
|
+
|
|
7
73
|
function StripAnnotationsOneForIncipits(theNotes) {
|
|
8
74
|
// Strip out tempo markings
|
|
9
75
|
let searchRegExp = /^Q:.*[\r\n]*/gm;
|
|
@@ -153,29 +219,85 @@ function StripChordsOne(theNotes) {
|
|
|
153
219
|
}
|
|
154
220
|
|
|
155
221
|
function sanitise(theTune) {
|
|
222
|
+
let j,
|
|
223
|
+
k,
|
|
224
|
+
theTextIncipits = [];
|
|
156
225
|
// Strip out annotations
|
|
157
226
|
theTune = StripAnnotationsOneForIncipits(theTune);
|
|
158
227
|
|
|
159
|
-
// Strip out
|
|
228
|
+
// Strip out atextnnotations
|
|
160
229
|
theTune = StripTextAnnotationsOne(theTune);
|
|
161
230
|
|
|
162
231
|
// Strip out chord markings
|
|
163
232
|
theTune = StripChordsOne(theTune);
|
|
164
233
|
|
|
165
|
-
|
|
234
|
+
// Parse out the first few measures
|
|
235
|
+
const theLines = theTune.split("\n"),
|
|
236
|
+
nLines = theLines.length;
|
|
237
|
+
|
|
238
|
+
// Find the key
|
|
239
|
+
let theKey = "";
|
|
240
|
+
let indexOfTheKey;
|
|
241
|
+
|
|
242
|
+
for (j = 0; j < nLines; ++j) {
|
|
243
|
+
theKey = theLines[j];
|
|
244
|
+
|
|
245
|
+
if (theKey.indexOf("K:") !== -1) {
|
|
246
|
+
indexOfTheKey = j;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Find the L: parameter
|
|
251
|
+
let theL = "";
|
|
252
|
+
|
|
253
|
+
for (j = 0; j < nLines; ++j) {
|
|
254
|
+
theL = theLines[j];
|
|
255
|
+
|
|
256
|
+
if (theL.indexOf("L:") !== -1) {
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Find the M: parameter
|
|
261
|
+
let theM = "";
|
|
262
|
+
|
|
263
|
+
for (j = 0; j < nLines; ++j) {
|
|
264
|
+
theM = theLines[j];
|
|
265
|
+
|
|
266
|
+
if (theM.indexOf("M:") !== -1) {
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Use at most the first three lines following the header K:
|
|
271
|
+
let added = 0;
|
|
272
|
+
for (k = indexOfTheKey + 1; k < nLines; ++k) {
|
|
273
|
+
const theTextIncipit = theLines[k];
|
|
274
|
+
|
|
275
|
+
// Clean out the incipit line of any annotations besides notes and bar lines
|
|
276
|
+
theTextIncipits.push(cleanIncipitLine(theTextIncipit));
|
|
277
|
+
added++;
|
|
278
|
+
if (added === 3) break;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return `X:1\n${theM}\n${theL}\n${theKey}\n${theTextIncipits.join("\n")}`;
|
|
166
282
|
}
|
|
167
283
|
|
|
168
284
|
/**
|
|
169
285
|
* Get incipit (opening bars) of a tune for display/search purposes
|
|
170
|
-
* @param {object} Object of the form {abc} with optional property: numBars
|
|
286
|
+
* @param {object|string} Object of the form {abc} with optional property: numBars, or a string in ABC format
|
|
171
287
|
* @param {string} params.abc - ABC notation
|
|
172
|
-
* @param {number|Fraction} params.numBars - Number of bars to return, counting the anacrucis if there is one.
|
|
288
|
+
* @param {number|Fraction} params.numBars - Number of bars to return, counting the anacrucis if there is one.
|
|
289
|
+
* (default: 1.5 for some cases like M:4/4 L:1/16; 3 for M:3/4; otherwise 2)
|
|
173
290
|
* @returns {string} - ABC incipit
|
|
174
291
|
*/
|
|
175
|
-
function getIncipit({
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
292
|
+
function getIncipit(data) {
|
|
293
|
+
let {
|
|
294
|
+
abc,
|
|
295
|
+
numBars, //, part=null
|
|
296
|
+
} = typeof data === "string" ? { abc: data } : data;
|
|
297
|
+
|
|
298
|
+
const { withAnacrucis = true } =
|
|
299
|
+
typeof data === "string" ? { abc: data } : data;
|
|
300
|
+
|
|
179
301
|
if (!numBars) {
|
|
180
302
|
numBars = 2;
|
|
181
303
|
const currentMeter = getMeter(abc);
|
|
@@ -195,7 +317,27 @@ function getIncipit({
|
|
|
195
317
|
}
|
|
196
318
|
}
|
|
197
319
|
abc = sanitise(abc);
|
|
198
|
-
return getFirstBars(abc, numBars,
|
|
320
|
+
return getFirstBars(abc, numBars, withAnacrucis, false, { all: true });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function getIncipitForContourGeneration(abc) {
|
|
324
|
+
return getIncipit({
|
|
325
|
+
abc,
|
|
326
|
+
withAnacrucis: false,
|
|
327
|
+
numBars: 1,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function getContourFromFullAbc(abc) {
|
|
332
|
+
if (Array.isArray(abc)) {
|
|
333
|
+
if (abc.length === 0) return null;
|
|
334
|
+
abc = abc[0];
|
|
335
|
+
}
|
|
336
|
+
return getContour(getIncipitForContourGeneration(abc));
|
|
199
337
|
}
|
|
200
338
|
|
|
201
|
-
module.exports = {
|
|
339
|
+
module.exports = {
|
|
340
|
+
getIncipit,
|
|
341
|
+
getIncipitForContourGeneration,
|
|
342
|
+
getContourFromFullAbc,
|
|
343
|
+
};
|
package/src/index.js
CHANGED
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
const parser = require("./parse/parser.js");
|
|
7
7
|
const manipulator = require("./manipulator.js");
|
|
8
|
-
const sort = require("./contour-sort.js");
|
|
8
|
+
const sort = require("./sort/contour-sort.js");
|
|
9
|
+
const contourToSvg = require("./sort/contour-svg.js");
|
|
10
|
+
const displayContour = require("./sort/display-contour.js");
|
|
11
|
+
|
|
9
12
|
const incipit = require("./incipit.js");
|
|
13
|
+
const javascriptify = require("./javascriptify.js");
|
|
10
14
|
|
|
11
15
|
module.exports = {
|
|
12
16
|
// Parser functions
|
|
@@ -17,7 +21,12 @@ module.exports = {
|
|
|
17
21
|
|
|
18
22
|
// Sort functions
|
|
19
23
|
...sort,
|
|
24
|
+
...displayContour,
|
|
25
|
+
...contourToSvg,
|
|
20
26
|
|
|
21
27
|
// Incipit functions
|
|
22
28
|
...incipit,
|
|
29
|
+
|
|
30
|
+
//
|
|
31
|
+
javascriptify,
|
|
23
32
|
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a JavaScript value to a string representation using JavaScript syntax.
|
|
3
|
+
* Uses template literals for multiline strings and avoids quotes on property names where possible.
|
|
4
|
+
* Object properties with values that are falsey or empty arrays are omitted.
|
|
5
|
+
*
|
|
6
|
+
* @param {*} value - The value to convert to JavaScript syntax
|
|
7
|
+
* @param {number} indent - Current indentation level (for internal use)
|
|
8
|
+
* @returns {string} JavaScript code representation of the value
|
|
9
|
+
*/
|
|
10
|
+
function javascriptify(value, indent = 0) {
|
|
11
|
+
const indentStr = " ".repeat(indent);
|
|
12
|
+
const nextIndentStr = " ".repeat(indent + 1);
|
|
13
|
+
|
|
14
|
+
// Handle null and undefined
|
|
15
|
+
if (value === null) {return "null";}
|
|
16
|
+
if (value === undefined) {return "undefined";}
|
|
17
|
+
|
|
18
|
+
// Handle primitives
|
|
19
|
+
if (typeof value === "number") {
|
|
20
|
+
return String(value);
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "boolean") {
|
|
23
|
+
return String(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Handle strings
|
|
27
|
+
if (typeof value === "string") {
|
|
28
|
+
// Check if string contains newlines
|
|
29
|
+
if (value.includes("\n")) {
|
|
30
|
+
// Escape backslashes, backticks, and template literal interpolations
|
|
31
|
+
const escaped = value
|
|
32
|
+
.replace(/\\/g, "\\\\")
|
|
33
|
+
.replace(/`/g, "\\`")
|
|
34
|
+
.replace(/\$/g, "\\$");
|
|
35
|
+
return `\`${escaped}\``;
|
|
36
|
+
}
|
|
37
|
+
return JSON.stringify(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Handle arrays
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
if (value.length === 0) {return "[]";}
|
|
43
|
+
|
|
44
|
+
const items = value
|
|
45
|
+
.map((item) => nextIndentStr + javascriptify(item, indent + 1))
|
|
46
|
+
.join(",\n");
|
|
47
|
+
|
|
48
|
+
return `[\n${items}\n${indentStr}]`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle objects
|
|
52
|
+
if (typeof value === "object") {
|
|
53
|
+
// Filter out falsey values and empty arrays
|
|
54
|
+
const keys = Object.keys(value).filter((k) => {
|
|
55
|
+
const val = value[k];
|
|
56
|
+
// Omit all falsey values (including false, but keep 0)
|
|
57
|
+
if (val === 0) {return true;}
|
|
58
|
+
//if (val === null || val === undefined || val === "") return false;
|
|
59
|
+
if (!val) {return false;}
|
|
60
|
+
// Omit empty arrays
|
|
61
|
+
if (Array.isArray(val) && val.length === 0) {return false;}
|
|
62
|
+
return true;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (keys.length === 0) {return "{}";}
|
|
66
|
+
|
|
67
|
+
const properties = keys
|
|
68
|
+
.map((key) => {
|
|
69
|
+
const propValue = javascriptify(value[key], indent + 1);
|
|
70
|
+
// Check if key is a valid identifier (can be unquoted)
|
|
71
|
+
const validIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key);
|
|
72
|
+
const keyStr = validIdentifier ? key : JSON.stringify(key);
|
|
73
|
+
return `${nextIndentStr}${keyStr}: ${propValue}`;
|
|
74
|
+
})
|
|
75
|
+
.join(",\n");
|
|
76
|
+
|
|
77
|
+
return `{\n${properties}\n${indentStr}}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fallback for functions and other types
|
|
81
|
+
return String(value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = javascriptify;
|
package/src/manipulator.js
CHANGED
|
@@ -159,7 +159,7 @@ function insertCharsAtIndexes(originalString, charToInsert, indexes) {
|
|
|
159
159
|
/**
|
|
160
160
|
* Toggle meter by doubling or halving bar length
|
|
161
161
|
* Supports 4/4↔4/2 and 6/8↔12/8 transformations
|
|
162
|
-
* This is
|
|
162
|
+
* This is nearly a true inverse operation - going there and back preserves the ABC except for some
|
|
163
163
|
* edge cases involving spaces around the bar lines. No need to handle them.
|
|
164
164
|
* Handles anacrusis correctly and preserves line breaks
|
|
165
165
|
*
|
package/src/math.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
// ============================================================================
|
|
2
|
-
// FRACTION CLASS (to avoid floating point errors)
|
|
3
|
-
// ============================================================================
|
|
4
|
-
|
|
5
1
|
class Fraction {
|
|
6
2
|
constructor(numerator, denominator = 1) {
|
|
7
3
|
if (denominator === 0) {
|
|
8
4
|
throw new Error("Denominator cannot be zero");
|
|
9
5
|
}
|
|
10
6
|
|
|
7
|
+
if (
|
|
8
|
+
isNaN(numerator) ||
|
|
9
|
+
isNaN(denominator) ||
|
|
10
|
+
numerator === null ||
|
|
11
|
+
denominator === null
|
|
12
|
+
) {
|
|
13
|
+
throw new Error("invalid argument");
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
const g = gcd(Math.abs(numerator), Math.abs(denominator));
|
|
12
17
|
this.num = numerator / g;
|
|
13
18
|
this.den = denominator / g;
|
|
@@ -53,7 +58,10 @@ class Fraction {
|
|
|
53
58
|
|
|
54
59
|
compare(other) {
|
|
55
60
|
// Returns -1 if this < other, 0 if equal, 1 if this > other
|
|
56
|
-
const diff =
|
|
61
|
+
const diff =
|
|
62
|
+
typeof n === "number"
|
|
63
|
+
? this.num - other * this.den
|
|
64
|
+
: this.num * other.den - other.num * this.den;
|
|
57
65
|
return diff < 0 ? -1 : diff > 0 ? 1 : 0;
|
|
58
66
|
}
|
|
59
67
|
|
|
@@ -72,6 +80,9 @@ class Fraction {
|
|
|
72
80
|
toString() {
|
|
73
81
|
return this.den === 1 ? `${this.num}` : `${this.num}/${this.den}`;
|
|
74
82
|
}
|
|
83
|
+
toNumber() {
|
|
84
|
+
return this.num / this.den;
|
|
85
|
+
}
|
|
75
86
|
}
|
|
76
87
|
|
|
77
88
|
function gcd(a, b) {
|
package/src/parse/note-parser.js
CHANGED
package/src/parse/parser.js
CHANGED
|
@@ -10,7 +10,6 @@ const {
|
|
|
10
10
|
parseTuplet,
|
|
11
11
|
parseBrokenRhythm,
|
|
12
12
|
applyBrokenRhythm,
|
|
13
|
-
NOTE_TO_DEGREE,
|
|
14
13
|
} = require("./note-parser.js");
|
|
15
14
|
const { classifyBarLine } = require("./barline-parser.js");
|
|
16
15
|
const {
|
|
@@ -233,7 +232,7 @@ function parseABCWithBars(abc, options = {}) {
|
|
|
233
232
|
const newlineSet = new Set(newlinePositions);
|
|
234
233
|
|
|
235
234
|
// Comprehensive bar line regex - includes trailing spaces
|
|
236
|
-
const barLineRegex = /(
|
|
235
|
+
const barLineRegex = /(\|\||\|\]|\[\|\]|(\|:?)|(:?\|)|:\|\|:) */g;
|
|
237
236
|
|
|
238
237
|
const bars = [];
|
|
239
238
|
const barLines = [];
|
|
@@ -516,7 +515,49 @@ function calculateBarDurations(parsedData) {
|
|
|
516
515
|
return result;
|
|
517
516
|
}
|
|
518
517
|
|
|
518
|
+
/**
|
|
519
|
+
* Extracts all ABC music notation tunes from a string.
|
|
520
|
+
*
|
|
521
|
+
* @param {string} text - The input string containing one or more ABC tunes
|
|
522
|
+
* @returns {string[]} An array of ABC tune strings, each containing a complete tune
|
|
523
|
+
* @throws {TypeError} If the input is not a string
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* const abcText = `X: 1
|
|
527
|
+
* T: Example Tune
|
|
528
|
+
* M: 4/4
|
|
529
|
+
* K: C
|
|
530
|
+
* CDEF|
|
|
531
|
+
*
|
|
532
|
+
* X: 2
|
|
533
|
+
* T: Another Tune
|
|
534
|
+
* K: G
|
|
535
|
+
* GABc|`;
|
|
536
|
+
*
|
|
537
|
+
* const tunes = getTunes(abcText);
|
|
538
|
+
* // Returns: ['X: 1\nT: Example Tune\nM: 4/4\nK: C\nCDEF|', 'X: 2\nT: Another Tune\nK: G\nGABc|']
|
|
539
|
+
*/
|
|
540
|
+
function getTunes(text) {
|
|
541
|
+
if (typeof text !== "string") {
|
|
542
|
+
throw new TypeError("Input must be a string");
|
|
543
|
+
}
|
|
544
|
+
// Regex pattern to match ABC tunes:
|
|
545
|
+
// ^X:\s*\d+ - Matches lines starting with "X:" followed by optional whitespace and digits
|
|
546
|
+
// .*$ - Matches the rest of that line
|
|
547
|
+
// (?:\n(?!\n).*)* - Matches subsequent lines that are NOT empty lines (non-capturing group)
|
|
548
|
+
// \n(?!\n) ensures we have a newline NOT followed by another newline
|
|
549
|
+
// .* matches the content of that non-empty line
|
|
550
|
+
// * repeats for all consecutive non-empty lines
|
|
551
|
+
// Flags: g (global), m (multiline)
|
|
552
|
+
const getAbc = /^X:\s*\d+.*$(?:\n(?!\n).*)*$/gm;
|
|
553
|
+
// Extract all matches and return as an array of strings
|
|
554
|
+
const matches = [...text.matchAll(getAbc)];
|
|
555
|
+
|
|
556
|
+
return matches.map((match) => match[0]);
|
|
557
|
+
}
|
|
558
|
+
|
|
519
559
|
module.exports = {
|
|
560
|
+
getTunes,
|
|
520
561
|
parseABCWithBars,
|
|
521
562
|
calculateBarDurations,
|
|
522
563
|
// Re-export utilities for convenience
|
|
@@ -526,5 +567,4 @@ module.exports = {
|
|
|
526
567
|
getMusicLines,
|
|
527
568
|
analyzeSpacing,
|
|
528
569
|
classifyBarLine,
|
|
529
|
-
NOTE_TO_DEGREE,
|
|
530
570
|
};
|
package/src/parse/token-utils.js
CHANGED
|
@@ -9,14 +9,83 @@
|
|
|
9
9
|
//
|
|
10
10
|
// ============================================================================
|
|
11
11
|
|
|
12
|
+
const TokenRegexComponents = {
|
|
13
|
+
// Tuplet notation: (3, (3:2, (3:2:4
|
|
14
|
+
tuplet: String.raw`\(\d(?::\d?){0,2}`,
|
|
15
|
+
|
|
16
|
+
// Inline field changes: [K:D], [L:1/4], [M:3/4], [P:A]
|
|
17
|
+
inlineField: String.raw`\[(?:[KLMP]):[^\]]+\]`,
|
|
18
|
+
|
|
19
|
+
// Text in quotes: chord symbols "Dm7" or annotations "^text"
|
|
20
|
+
quotedText: String.raw`"[^"]+"`,
|
|
21
|
+
|
|
22
|
+
// Bang decoration: !trill!, !fermata!, etc.
|
|
23
|
+
bangDecoration: String.raw`![^!]+!`,
|
|
24
|
+
|
|
25
|
+
// Symbol decorations before note: ~, ., M, P, S, T, H, U, V
|
|
26
|
+
// 0..N of them, with optional following white space
|
|
27
|
+
symbolDecoration: String.raw`[~.MPSTHUV]`,
|
|
28
|
+
|
|
29
|
+
symbolDecorations() {
|
|
30
|
+
return String.raw`(?:${this.symbolDecoration}\s*)*`;
|
|
31
|
+
},
|
|
32
|
+
bangDecorations() {
|
|
33
|
+
return String.raw`(?:${this.bangDecoration}\s*)*`;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// Accidental: :, ^, _ (natural, sharp, flat)
|
|
37
|
+
accidental: String.raw`[:^_]?`,
|
|
38
|
+
|
|
39
|
+
// Note pitch: A-G (lower octave), a-g (middle octave), z/x (rest), y (dummy)
|
|
40
|
+
// Or chord in brackets: [CEG], [DF#A]
|
|
41
|
+
pitch: String.raw`(?:[A-Ga-gzxy]|\[[A-Ga-g]+\])`,
|
|
42
|
+
|
|
43
|
+
// Octave modifiers: ' (up), , (down)
|
|
44
|
+
octave: String.raw`[',]*`,
|
|
45
|
+
|
|
46
|
+
// Duration: 2, /2, 3/4, /, //, etc.
|
|
47
|
+
// include all digits, even though things like A0, A5,... are not allowed
|
|
48
|
+
// but we could have A16, or z10 (full bar rest for M:5/4; L:1/8)
|
|
49
|
+
duration: String.raw`[0-9]*\/?[0-9]*`,
|
|
50
|
+
|
|
51
|
+
// Tie (-) or broken rhythm (>, >>, >>>, <, <<, <<<)
|
|
52
|
+
// Optional: may have neither, or may have whitespace before broken rhythm
|
|
53
|
+
tieOrBroken: String.raw`(?:-|\s*(?:<{1,3}|>{1,3}))?`,
|
|
54
|
+
};
|
|
55
|
+
|
|
12
56
|
/**
|
|
13
57
|
* Get regex for matching ABC music tokens
|
|
14
|
-
*
|
|
58
|
+
* Built from documented components for maintainability
|
|
59
|
+
*
|
|
60
|
+
* Matches: tuplets, inline fields, chord symbols, notes, rests, chords in brackets,
|
|
61
|
+
* decorations, ties, and broken rhythms
|
|
15
62
|
*
|
|
16
63
|
* @returns {RegExp} - Regular expression for tokenising ABC music
|
|
17
64
|
*/
|
|
18
|
-
const getTokenRegex = () =>
|
|
19
|
-
|
|
65
|
+
const getTokenRegex = () => {
|
|
66
|
+
const s = TokenRegexComponents;
|
|
67
|
+
// Complete note/rest/chord pattern with optional leading decoration
|
|
68
|
+
const notePattern =
|
|
69
|
+
s.bangDecorations() +
|
|
70
|
+
s.symbolDecorations() +
|
|
71
|
+
s.accidental +
|
|
72
|
+
s.pitch +
|
|
73
|
+
s.octave +
|
|
74
|
+
s.duration +
|
|
75
|
+
s.tieOrBroken;
|
|
76
|
+
|
|
77
|
+
// Combine all patterns with alternation
|
|
78
|
+
const fullPattern = [
|
|
79
|
+
s.tuplet,
|
|
80
|
+
s.inlineField,
|
|
81
|
+
s.quotedText,
|
|
82
|
+
//allow standalone bang and symbol decorations (?)
|
|
83
|
+
notePattern,
|
|
84
|
+
s.bangDecoration,
|
|
85
|
+
].join("|");
|
|
86
|
+
|
|
87
|
+
return new RegExp(fullPattern, "g");
|
|
88
|
+
};
|
|
20
89
|
|
|
21
90
|
/**
|
|
22
91
|
* Parse inline field from music section
|