@goplayerjuggler/abc-tools 1.0.2 → 1.0.4
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 +17 -0
- package/package.json +8 -7
- package/src/contour-sort.js +399 -384
- package/src/incipit.js +134 -201
- package/src/index.js +23 -23
- package/src/manipulator.js +448 -449
- package/src/math.js +63 -63
- package/src/parse/barline-parser.js +115 -0
- package/src/parse/header-parser.js +140 -0
- package/src/parse/note-parser.js +463 -0
- package/src/parse/parser.js +530 -0
- package/src/parse/token-utils.js +106 -0
- package/src/parser.js +0 -996
package/src/math.js
CHANGED
|
@@ -3,88 +3,88 @@
|
|
|
3
3
|
// ============================================================================
|
|
4
4
|
|
|
5
5
|
class Fraction {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
constructor(numerator, denominator = 1) {
|
|
7
|
+
if (denominator === 0) {
|
|
8
|
+
throw new Error("Denominator cannot be zero");
|
|
9
|
+
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
const g = gcd(Math.abs(numerator), Math.abs(denominator));
|
|
12
|
+
this.num = numerator / g;
|
|
13
|
+
this.den = denominator / g;
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
// Keep denominator positive
|
|
16
|
+
if (this.den < 0) {
|
|
17
|
+
this.num = -this.num;
|
|
18
|
+
this.den = -this.den;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
clone() {
|
|
23
|
+
return new Fraction(this.num, this.den);
|
|
24
|
+
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
multiply(n) {
|
|
27
|
+
if (typeof n === "number") {
|
|
28
|
+
return new Fraction(this.num * n, this.den);
|
|
29
|
+
}
|
|
30
|
+
return new Fraction(this.num * n.num, this.den * n.den);
|
|
31
|
+
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
divide(n) {
|
|
34
|
+
if (typeof n === "number") {
|
|
35
|
+
return new Fraction(this.num, this.den * n);
|
|
36
|
+
}
|
|
37
|
+
return new Fraction(this.num * n.den, this.den * n.num);
|
|
38
|
+
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
add(n) {
|
|
41
|
+
if (typeof n === "number") {
|
|
42
|
+
return new Fraction(this.num + n * this.den, this.den);
|
|
43
|
+
}
|
|
44
|
+
return new Fraction(this.num * n.den + n.num * this.den, this.den * n.den);
|
|
45
|
+
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
subtract(n) {
|
|
48
|
+
if (typeof n === "number") {
|
|
49
|
+
return new Fraction(this.num - n * this.den, this.den);
|
|
50
|
+
}
|
|
51
|
+
return new Fraction(this.num * n.den - n.num * this.den, this.den * n.den);
|
|
52
|
+
}
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
compare(other) {
|
|
55
|
+
// Returns -1 if this < other, 0 if equal, 1 if this > other
|
|
56
|
+
const diff = this.num * other.den - other.num * this.den;
|
|
57
|
+
return diff < 0 ? -1 : diff > 0 ? 1 : 0;
|
|
58
|
+
}
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
equals(other) {
|
|
61
|
+
return this.num === other.num && this.den === other.den;
|
|
62
|
+
}
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
isGreaterThan(other) {
|
|
65
|
+
return this.compare(other) > 0;
|
|
66
|
+
}
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
isLessThan(other) {
|
|
69
|
+
return this.compare(other) < 0;
|
|
70
|
+
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
toString() {
|
|
73
|
+
return this.den === 1 ? `${this.num}` : `${this.num}/${this.den}`;
|
|
74
|
+
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
function gcd(a, b) {
|
|
78
|
-
|
|
78
|
+
return b === 0 ? a : gcd(b, a % b);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// function lcm(a, b) {
|
|
82
82
|
// return Math.abs(a * b) / gcd(a, b);
|
|
83
83
|
// }
|
|
84
84
|
if (typeof module !== "undefined" && module.exports) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
module.exports = {
|
|
86
|
+
// lcm,
|
|
87
|
+
// gcd,
|
|
88
|
+
Fraction,
|
|
89
|
+
};
|
|
90
90
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// ABC BAR LINE PARSING
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Handles classification of bar lines and repeat notation:
|
|
6
|
+
// - Regular bars: |
|
|
7
|
+
// - Double bars: ||
|
|
8
|
+
// - Final bars: |]
|
|
9
|
+
// - Repeat starts: |:, [|
|
|
10
|
+
// - Repeat ends: :|, |]
|
|
11
|
+
// - Double repeats: ::, :|:
|
|
12
|
+
// - Repeat endings: |1, |2, etc.
|
|
13
|
+
//
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Classify bar line type
|
|
18
|
+
*
|
|
19
|
+
* @param {string} barLineStr - Bar line string from ABC notation
|
|
20
|
+
* @returns {object} - Classification with type, text, and properties
|
|
21
|
+
*
|
|
22
|
+
* Return object structure:
|
|
23
|
+
* {
|
|
24
|
+
* type: string, // 'regular', 'double', 'final', 'repeat-start', 'repeat-end', 'repeat-both', 'repeat-ending', 'other'
|
|
25
|
+
* text: string, // Original bar line string
|
|
26
|
+
* isRepeat: boolean, // Whether this bar line involves repeats
|
|
27
|
+
* ending?: number // For repeat-ending type, which ending (1-6)
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
function classifyBarLine(barLineStr) {
|
|
31
|
+
const trimmed = barLineStr.trim();
|
|
32
|
+
|
|
33
|
+
// Repeat endings
|
|
34
|
+
if (trimmed.match(/^\|[1-6]$/)) {
|
|
35
|
+
return {
|
|
36
|
+
type: "repeat-ending",
|
|
37
|
+
ending: parseInt(trimmed[1]),
|
|
38
|
+
text: barLineStr,
|
|
39
|
+
isRepeat: true,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Start repeat
|
|
44
|
+
if (trimmed.match(/^\|:/) || trimmed.match(/^\[\|/)) {
|
|
45
|
+
return {
|
|
46
|
+
type: "repeat-start",
|
|
47
|
+
text: barLineStr,
|
|
48
|
+
isRepeat: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// End repeat
|
|
53
|
+
if (
|
|
54
|
+
trimmed.match(/^:\|/) ||
|
|
55
|
+
(trimmed.match(/^\|\]/) && !trimmed.match(/^\|\]$/))
|
|
56
|
+
) {
|
|
57
|
+
return {
|
|
58
|
+
type: "repeat-end",
|
|
59
|
+
text: barLineStr,
|
|
60
|
+
isRepeat: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Double repeat
|
|
65
|
+
if (
|
|
66
|
+
trimmed.match(/^::/) ||
|
|
67
|
+
trimmed.match(/^:\|:/) ||
|
|
68
|
+
trimmed.match(/^::\|:?/) ||
|
|
69
|
+
trimmed.match(/^::\|\|:?/)
|
|
70
|
+
) {
|
|
71
|
+
return {
|
|
72
|
+
type: "repeat-both",
|
|
73
|
+
text: barLineStr,
|
|
74
|
+
isRepeat: true,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Final bar
|
|
79
|
+
if (trimmed === "|]") {
|
|
80
|
+
return {
|
|
81
|
+
type: "final",
|
|
82
|
+
text: barLineStr,
|
|
83
|
+
isRepeat: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Double bar
|
|
88
|
+
if (trimmed === "||") {
|
|
89
|
+
return {
|
|
90
|
+
type: "double",
|
|
91
|
+
text: barLineStr,
|
|
92
|
+
isRepeat: false,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Regular bar
|
|
97
|
+
if (trimmed === "|") {
|
|
98
|
+
return {
|
|
99
|
+
type: "regular",
|
|
100
|
+
text: barLineStr,
|
|
101
|
+
isRepeat: false,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Unknown/complex bar line
|
|
106
|
+
return {
|
|
107
|
+
type: "other",
|
|
108
|
+
text: barLineStr,
|
|
109
|
+
isRepeat: trimmed.includes(":"),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
classifyBarLine,
|
|
115
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
const { Fraction } = require("../math.js");
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// ABC HEADER PARSING
|
|
5
|
+
// ============================================================================
|
|
6
|
+
//
|
|
7
|
+
// Handles extraction of header fields from ABC notation:
|
|
8
|
+
// - Key signature (K:)
|
|
9
|
+
// - Meter/time signature (M:)
|
|
10
|
+
// - Unit note length (L:)
|
|
11
|
+
// - Line metadata (comments, continuations)
|
|
12
|
+
//
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract key signature from ABC header
|
|
17
|
+
*
|
|
18
|
+
* @param {string} abc - ABC notation string
|
|
19
|
+
* @returns {string} - Tonic note (e.g., 'C', 'D', 'G')
|
|
20
|
+
* @throws {Error} - If no key signature found
|
|
21
|
+
*/
|
|
22
|
+
function getTonalBase(abc) {
|
|
23
|
+
const keyMatch = abc.match(/^K:\s*([A-G])/m);
|
|
24
|
+
if (!keyMatch) {
|
|
25
|
+
throw new Error("No key signature found in ABC");
|
|
26
|
+
}
|
|
27
|
+
return keyMatch[1].toUpperCase();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Extract meter/time signature from ABC header
|
|
32
|
+
*
|
|
33
|
+
* @param {string} abc - ABC notation string
|
|
34
|
+
* @returns {[number, number]} - Meter as [numerator, denominator] (e.g., [3, 4] for 3/4 time)
|
|
35
|
+
*/
|
|
36
|
+
function getMeter(abc) {
|
|
37
|
+
const meterMatch = abc.match(/^M:\s*(\d+)\/(\d+)/m);
|
|
38
|
+
if (meterMatch) {
|
|
39
|
+
return [parseInt(meterMatch[1]), parseInt(meterMatch[2])];
|
|
40
|
+
}
|
|
41
|
+
return [4, 4]; // Default to 4/4
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract unit note length as a Fraction object
|
|
46
|
+
*
|
|
47
|
+
* @param {string} abc - ABC notation string
|
|
48
|
+
* @returns {Fraction} - Unit length (e.g., 1/8, 1/4)
|
|
49
|
+
*/
|
|
50
|
+
function getUnitLength(abc) {
|
|
51
|
+
const lengthMatch = abc.match(/^L:\s*(\d+)\/(\d+)/m);
|
|
52
|
+
if (lengthMatch) {
|
|
53
|
+
return new Fraction(parseInt(lengthMatch[1]), parseInt(lengthMatch[2]));
|
|
54
|
+
}
|
|
55
|
+
return new Fraction(1, 8); // Default to 1/8
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Process ABC lines: extract music lines with metadata
|
|
60
|
+
* Handles comments, line continuations, and separates headers from music
|
|
61
|
+
* Preserves newline positions for layout tracking
|
|
62
|
+
*
|
|
63
|
+
* @param {string} abc - ABC notation string
|
|
64
|
+
* @returns {object} - { musicText, lineMetadata, newlinePositions, headerLines, headerEndIndex }
|
|
65
|
+
*/
|
|
66
|
+
function getMusicLines(abc) {
|
|
67
|
+
const lines = abc.split("\n");
|
|
68
|
+
const musicLines = [];
|
|
69
|
+
const lineMetadata = [];
|
|
70
|
+
const newlinePositions = [];
|
|
71
|
+
const headerLines = [];
|
|
72
|
+
let headerEndIndex = 0;
|
|
73
|
+
let inHeaders = true;
|
|
74
|
+
let currentPos = 0;
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < lines.length; i++) {
|
|
77
|
+
const line = lines[i];
|
|
78
|
+
let trimmed = line.trim();
|
|
79
|
+
|
|
80
|
+
// Skip empty lines and comment-only lines
|
|
81
|
+
if (trimmed === "" || trimmed.startsWith("%")) {
|
|
82
|
+
if (inHeaders) {
|
|
83
|
+
headerEndIndex = i + 1;
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check for header lines
|
|
89
|
+
if (inHeaders && trimmed.match(/^[A-Z]:/)) {
|
|
90
|
+
headerLines.push(line);
|
|
91
|
+
headerEndIndex = i + 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
inHeaders = false;
|
|
95
|
+
|
|
96
|
+
// Extract inline comment if present
|
|
97
|
+
const commentMatch = trimmed.match(/\s*%(.*)$/);
|
|
98
|
+
const comment = commentMatch ? commentMatch[1].trim() : null;
|
|
99
|
+
|
|
100
|
+
// Check for line continuation
|
|
101
|
+
const hasContinuation = trimmed.match(/\\\s*(%|$)/) !== null;
|
|
102
|
+
|
|
103
|
+
// Remove inline comments and line continuation marker
|
|
104
|
+
trimmed = trimmed.replace(/\s*%.*$/, "").trim();
|
|
105
|
+
trimmed = trimmed.replace(/\\\s*$/, "").trim();
|
|
106
|
+
|
|
107
|
+
if (trimmed) {
|
|
108
|
+
musicLines.push(trimmed);
|
|
109
|
+
lineMetadata.push({
|
|
110
|
+
lineIndex: i,
|
|
111
|
+
originalLine: line,
|
|
112
|
+
content: trimmed,
|
|
113
|
+
comment,
|
|
114
|
+
hasContinuation,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Track position where newline would be (unless continuation)
|
|
118
|
+
if (!hasContinuation && musicLines.length > 1) {
|
|
119
|
+
newlinePositions.push(currentPos);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
currentPos += trimmed.length + 1; // +1 for the space we'll add when joining
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
musicText: musicLines.join("\n"),
|
|
128
|
+
lineMetadata,
|
|
129
|
+
newlinePositions,
|
|
130
|
+
headerLines,
|
|
131
|
+
headerEndIndex,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
getTonalBase,
|
|
137
|
+
getMeter,
|
|
138
|
+
getUnitLength,
|
|
139
|
+
getMusicLines,
|
|
140
|
+
};
|