@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/src/math.js CHANGED
@@ -3,88 +3,88 @@
3
3
  // ============================================================================
4
4
 
5
5
  class Fraction {
6
- constructor(numerator, denominator = 1) {
7
- if (denominator === 0) {
8
- throw new Error("Denominator cannot be zero");
9
- }
6
+ constructor(numerator, denominator = 1) {
7
+ if (denominator === 0) {
8
+ throw new Error("Denominator cannot be zero");
9
+ }
10
10
 
11
- const g = gcd(Math.abs(numerator), Math.abs(denominator));
12
- this.num = numerator / g;
13
- this.den = denominator / g;
11
+ const g = gcd(Math.abs(numerator), Math.abs(denominator));
12
+ this.num = numerator / g;
13
+ this.den = denominator / g;
14
14
 
15
- // Keep denominator positive
16
- if (this.den < 0) {
17
- this.num = -this.num;
18
- this.den = -this.den;
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
- clone() {
23
- return new Fraction(this.num, this.den);
24
- }
22
+ clone() {
23
+ return new Fraction(this.num, this.den);
24
+ }
25
25
 
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
- }
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
- 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
- }
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
- 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
- }
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
- 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
- }
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
- 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
- }
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
- equals(other) {
61
- return this.num === other.num && this.den === other.den;
62
- }
60
+ equals(other) {
61
+ return this.num === other.num && this.den === other.den;
62
+ }
63
63
 
64
- isGreaterThan(other) {
65
- return this.compare(other) > 0;
66
- }
64
+ isGreaterThan(other) {
65
+ return this.compare(other) > 0;
66
+ }
67
67
 
68
- isLessThan(other) {
69
- return this.compare(other) < 0;
70
- }
68
+ isLessThan(other) {
69
+ return this.compare(other) < 0;
70
+ }
71
71
 
72
- toString() {
73
- return this.den === 1 ? `${this.num}` : `${this.num}/${this.den}`;
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
- return b === 0 ? a : gcd(b, a % b);
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
- module.exports = {
86
- // lcm,
87
- // gcd,
88
- Fraction,
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
+ };