@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.
@@ -0,0 +1,530 @@
1
+ const { Fraction } = require("../math.js");
2
+ const {
3
+ getTonalBase,
4
+ getMeter,
5
+ getUnitLength,
6
+ getMusicLines,
7
+ } = require("./header-parser.js");
8
+ const {
9
+ parseNote,
10
+ parseTuplet,
11
+ parseBrokenRhythm,
12
+ applyBrokenRhythm,
13
+ NOTE_TO_DEGREE,
14
+ } = require("./note-parser.js");
15
+ const { classifyBarLine } = require("./barline-parser.js");
16
+ const {
17
+ getTokenRegex,
18
+ parseInlineField,
19
+ analyzeSpacing,
20
+ } = require("./token-utils.js");
21
+
22
+ // ============================================================================
23
+ // ABC PARSER - MAIN MODULE
24
+ // ============================================================================
25
+ //
26
+ // Main parsing entry point that orchestrates the parsing of ABC notation
27
+ // into structured bar data. This module coordinates the header parsing,
28
+ // note parsing, and bar line processing.
29
+ //
30
+ // SUPPORTED FEATURES (ABC v2.1):
31
+ // - Basic note notation (pitch, octave markers, accidentals)
32
+ // - Duration modifiers (explicit numbers, fractions, slashes)
33
+ // - Rests/silences (z, x)
34
+ // - Dummy note: y (for spacing/alignment)
35
+ // - Back quotes: ` (ignored spacing for legibility, preserved in metadata)
36
+ // - Triplets: (3ABC, (3A/B/C/, (3A2B2C2
37
+ // - Broken rhythms: A>B, C<<D, etc. (>, >>, >>>, <, <<, <<<)
38
+ // - Repeat notation: |:, :|, |1, |2, etc.
39
+ // - Bar lines: |, ||, |], [|, etc.
40
+ // - Decorations: symbol decorations (~.MPSTHUV) and !name! decorations
41
+ // - Chord symbols: "Dm7", "G", etc.
42
+ // - Chords (multiple notes): [CEG], [CEG]2, etc.
43
+ // - Annotations: "^text", "<text", etc. (parsed but position markers preserved)
44
+ // - Inline fields: [K:...], [L:...], [M:...], [P:...]
45
+ // - Inline comments: % comment text
46
+ // - Line continuations: \ at end of line
47
+ // - Beaming: tracks whitespace between notes for beam grouping
48
+ // - Line breaks: preserves information about newlines in music
49
+ //
50
+ // NOT YET SUPPORTED:
51
+ // - Grace notes: {ABC}
52
+ // - Slurs: ()
53
+ // - Lyrics: w: lines
54
+ // - Multiple voices: V: fields
55
+ // - Macros and user-defined symbols
56
+ // - MIDI directives
57
+ // - Stylesheet directives
58
+ // - Many header fields (only X, T, M, L, K extracted)
59
+ //
60
+ // ============================================================================
61
+
62
+ /**
63
+ * Parse ABC into structured data with bars
64
+ *
65
+ * @param {string} abc - ABC notation string
66
+ * @param {object} options - Parsing options
67
+ * @param {number} options.maxBars - Maximum number of bars to parse (optional)
68
+ * @returns {object} - Parsed structure
69
+ *
70
+ * Returns object with:
71
+ * {
72
+ * bars: Array<Array<NoteObject>>, // Array of bars, each bar is array of notes/chords/fields
73
+ * barLines: Array<BarLineObject>, // Array of bar line information
74
+ * unitLength: Fraction, // The L: field value (default 1/8)
75
+ * meter: [number, number], // The M: field value (default [4,4])
76
+ * tonalBase: string, // The tonic from K: field (e.g., 'D', 'G')
77
+ * lineMetadata: Array<LineMetadata>,// Info about original lines (comments, continuations)
78
+ * headerLines: Array<string>, // Original header lines
79
+ * headerEndIndex: number, // Index where headers end
80
+ * musicText: string // Processed music text (headers removed)
81
+ * }
82
+ *
83
+ * NoteObject structure (regular note):
84
+ * {
85
+ * pitch: string, // 'A'-'G' (uppercase for low octave, lowercase for middle)
86
+ * octave: number, // Relative octave offset (0 = middle, +1 = high, -1 = low)
87
+ * duration: Fraction, // Note duration as fraction of whole note
88
+ * isSilence: boolean, // Always false for pitched notes
89
+ * tied: boolean, // The ABC fragment `A-B` maps to [note1, note2] with note1.tied === true
90
+ * token: string, // Original ABC token (e.g., 'D2', '^F/')
91
+ * sourceIndex: number, // Position in musicText where token starts
92
+ * sourceLength: number, // Length of original token
93
+ * spacing: { // Whitespace/beaming info after this token
94
+ * whitespace: string, // Actual whitespace characters (back quotes removed)
95
+ * backquotes: number, // Number of ` characters for reconstruction
96
+ * beamBreak: boolean, // True if beam should break (multiple spaces/newline)
97
+ * lineBreak: boolean // True if there was a newline after this token
98
+ * },
99
+ *
100
+ * // Optional properties (only present if applicable):
101
+ * decorations: Array<string>, // e.g., ['trill', 'staccato']
102
+ * chordSymbol: string, // e.g., 'Dm7', 'G'
103
+ * annotation: { // Text annotation with position
104
+ * position: string, // '^' (above), '_' (below), '<' (left), '>' (right), '@' (auto)
105
+ * text: string
106
+ * },
107
+ * isChord: true, // Present if this is a chord [CEG]
108
+ * chordNotes: Array<NoteObject> // All notes in the chord (when isChord=true)
109
+ * }
110
+ *
111
+ * NoteObject structure (silence/rest):
112
+ * {
113
+ * isSilence: true,
114
+ * duration: Fraction,
115
+ * token: string,
116
+ * sourceIndex: number,
117
+ * sourceLength: number,
118
+ * spacing: { ... }, // Same as regular note
119
+ * // Optional: decorations, chordSymbol, annotation (same as above)
120
+ * }
121
+ *
122
+ * NoteObject structure (dummy note):
123
+ * {
124
+ * isDummy: true,
125
+ * duration: Fraction,
126
+ * token: string,
127
+ * sourceIndex: number,
128
+ * sourceLength: number,
129
+ * spacing: { ... }
130
+ * }
131
+ *
132
+ * NoteObject structure (inline field change):
133
+ * {
134
+ * isInlineField: true,
135
+ * field: string, // 'K', 'L', 'M', or 'P'
136
+ * value: string, // The field value (e.g., 'G major', '3/4')
137
+ * token: string, // Original token (e.g., '[K:G]')
138
+ * sourceIndex: number,
139
+ * sourceLength: number,
140
+ * spacing: { ... }
141
+ * }
142
+ *
143
+ * NoteObject structure (standalone chord symbol):
144
+ * {
145
+ * isChordSymbol: true,
146
+ * chordSymbol: string, // The chord name
147
+ * token: string,
148
+ * sourceIndex: number,
149
+ * sourceLength: number,
150
+ * spacing: { ... }
151
+ * }
152
+ *
153
+ * Tuplet structure:
154
+ * {
155
+ * isTuple: true,
156
+ * p: number, // Tuplet ratio numerator
157
+ * q: number, // Tuplet ratio denominator
158
+ * r: number, // Number of notes in tuplet
159
+ * token: string,
160
+ * sourceIndex: number,
161
+ * sourceLength: number
162
+ * }
163
+ *
164
+ * BrokenRhythm structure:
165
+ * {
166
+ * isBrokenRhythm: true,
167
+ * direction: string, // '>' or '<'
168
+ * dots: number, // 1, 2, or 3
169
+ * token: string,
170
+ * sourceIndex: number,
171
+ * sourceLength: number
172
+ * }
173
+ *
174
+ * BarLineObject structure:
175
+ * {
176
+ * type: string, // 'regular', 'double', 'final', 'repeat-start', etc.
177
+ * text: string, // Original bar line string
178
+ * isRepeat: boolean, // Whether this involves repeats
179
+ * sourceIndex: number, // Position in musicText
180
+ * sourceLength: number, // Length of bar line
181
+ * barNumber: number, // Which bar this terminates (0-indexed)
182
+ * hasLineBreak: boolean, // Whether there's a newline after this bar line
183
+ * ending?: number // For repeat-ending type, which ending (1-6)
184
+ * }
185
+ *
186
+ * LineMetadata structure:
187
+ * {
188
+ * lineIndex: number, // Original line number in ABC
189
+ * originalLine: string, // Complete original line from ABC
190
+ * content: string, // Line content (comments/continuations removed)
191
+ * comment: string | null, // Text after % (null if no comment)
192
+ * hasContinuation: boolean // Whether line had \ continuation marker
193
+ * }
194
+ *
195
+ * Example:
196
+ * parseABCWithBars('X:1\nL:1/4\nK:D\n"Dm"D2 [DF]A | ~B4 |]')
197
+ * // Returns:
198
+ * {
199
+ * bars: [
200
+ * [
201
+ * { isChordSymbol: true, chordSymbol: 'Dm', spacing: {...}, ... },
202
+ * { pitch: 'D', octave: 0, duration: Fraction(1,2), chordSymbol: 'Dm', spacing: {...}, ... },
203
+ * { pitch: 'F', octave: 0, duration: Fraction(1,4), isChord: true, chordNotes: [...], spacing: {...}, ... },
204
+ * { pitch: 'A', octave: 0, duration: Fraction(1,4), spacing: {...}, ... }
205
+ * ],
206
+ * [
207
+ * { pitch: 'B', octave: 0, duration: Fraction(1,1), decorations: ['roll'], spacing: {...}, ... }
208
+ * ]
209
+ * ],
210
+ * barLines: [...],
211
+ * unitLength: Fraction(1,4),
212
+ * meter: [4,4],
213
+ * tonalBase: 'D',
214
+ * lineMetadata: [...]
215
+ * }
216
+ */
217
+ function parseABCWithBars(abc, options = {}) {
218
+ const { maxBars = Infinity } = options;
219
+
220
+ let unitLength = getUnitLength(abc);
221
+ let meter = getMeter(abc);
222
+ let tonalBase = getTonalBase(abc);
223
+
224
+ const {
225
+ musicText,
226
+ lineMetadata,
227
+ headerLines,
228
+ headerEndIndex,
229
+ newlinePositions,
230
+ } = getMusicLines(abc);
231
+
232
+ // Create a Set of newline positions for O(1) lookup
233
+ const newlineSet = new Set(newlinePositions);
234
+
235
+ // Comprehensive bar line regex - includes trailing spaces
236
+ const barLineRegex = /(\|\]|\[\||(\|:?)|(:?\|)|::|(\|[1-6])) */g;
237
+
238
+ const bars = [];
239
+ const barLines = [];
240
+ let currentBar = [];
241
+ let barCount = 0;
242
+
243
+ // Split music text by bar lines while preserving positions
244
+ let lastBarPos = 0;
245
+ let match;
246
+ let first = true;
247
+
248
+ while ((match = barLineRegex.exec(musicText)) !== null || first) {
249
+ first = false;
250
+ const { barLineText, barLinePos } =
251
+ match === null
252
+ ? { barLineText: musicText, barLinePos: musicText.length }
253
+ : {
254
+ barLineText: match[0],
255
+ barLinePos: match.index,
256
+ };
257
+
258
+ // Process segment before this bar line
259
+ const segment = musicText.substring(lastBarPos, barLinePos);
260
+
261
+ if (segment.trim()) {
262
+ // Parse tokens in this segment
263
+ const tokenRegex = getTokenRegex();
264
+
265
+ let tokenMatch;
266
+ let currentTuple = null;
267
+ // let previousNote = null; // Track previous note for broken rhythms
268
+
269
+ while ((tokenMatch = tokenRegex.exec(segment)) !== null) {
270
+ // Check if all notes of the tuple have been parsed
271
+ if (currentTuple && currentTuple.r === 0) {
272
+ currentTuple = null;
273
+ }
274
+ const fullToken = tokenMatch[0];
275
+ const tokenStartPos = lastBarPos + tokenMatch.index;
276
+ const spacing = analyzeSpacing(
277
+ segment,
278
+ tokenMatch.index + fullToken.length
279
+ );
280
+
281
+ // Check for inline field
282
+ const inlineField = parseInlineField(fullToken);
283
+ if (inlineField) {
284
+ // Update context based on inline field
285
+ if (inlineField.field === "L") {
286
+ const lengthMatch = inlineField.value.match(/1\/(\d+)/);
287
+ if (lengthMatch) {
288
+ unitLength = new Fraction(1, parseInt(lengthMatch[1]));
289
+ }
290
+ } else if (inlineField.field === "M") {
291
+ const meterMatch = inlineField.value.match(/(\d+)\/(\d+)/);
292
+ if (meterMatch) {
293
+ meter = [parseInt(meterMatch[1]), parseInt(meterMatch[2])];
294
+ }
295
+ } else if (inlineField.field === "K") {
296
+ const keyMatch = inlineField.value.match(/^([A-G])/);
297
+ if (keyMatch) {
298
+ tonalBase = keyMatch[1].toUpperCase();
299
+ }
300
+ }
301
+
302
+ const inlineFieldObj = {
303
+ isInlineField: true,
304
+ field: inlineField.field,
305
+ value: inlineField.value,
306
+ token: fullToken,
307
+ sourceIndex: tokenStartPos,
308
+ sourceLength: fullToken.length,
309
+ spacing,
310
+ };
311
+ currentBar.push(inlineFieldObj);
312
+ // previousNote = null; // Inline fields break note sequences
313
+ continue;
314
+ }
315
+
316
+ // Check for broken rhythm
317
+ const brokenRhythm = parseBrokenRhythm(fullToken);
318
+ if (brokenRhythm) {
319
+ const { firstNoteToken } = brokenRhythm;
320
+ const firstNote = parseNote(firstNoteToken, unitLength, currentTuple);
321
+ // Find the next note token
322
+ const nextTokenMatch = tokenRegex.exec(segment);
323
+ if (nextTokenMatch) {
324
+ const nextToken = nextTokenMatch[0];
325
+ const nextTokenStartPos = lastBarPos + nextTokenMatch.index;
326
+ const nextSpacing = analyzeSpacing(
327
+ segment,
328
+ nextTokenMatch.index + nextToken.length
329
+ );
330
+
331
+ // Parse the next note
332
+ const nextNote = parseNote(nextToken, unitLength, currentTuple);
333
+ if (nextNote && nextNote.duration && firstNote.duration) {
334
+ // Apply broken rhythm to both notes
335
+ applyBrokenRhythm(firstNote, nextNote, brokenRhythm);
336
+
337
+ //add the firstNote
338
+ currentBar.push({
339
+ ...firstNote,
340
+ token: fullToken,
341
+ sourceIndex: tokenStartPos,
342
+ sourceLength: fullToken.length,
343
+ });
344
+
345
+ // Add the broken rhythm marker to the bar
346
+ currentBar.push({
347
+ ...brokenRhythm,
348
+ });
349
+
350
+ // Add the next note to the bar
351
+ const nextNoteObj = {
352
+ ...nextNote,
353
+ token: nextToken,
354
+ sourceIndex: nextTokenStartPos,
355
+ sourceLength: nextToken.length,
356
+ spacing: nextSpacing,
357
+ };
358
+ currentBar.push(nextNoteObj);
359
+ // previousNote = nextNoteObj;
360
+ continue;
361
+ }
362
+ }
363
+ // If we couldn't apply the broken rhythm, just skip it
364
+ // previousNote = null;
365
+ continue;
366
+ }
367
+
368
+ // Tuplets
369
+ if (fullToken.match(/\(\d(?::\d?){0,2}/g)) {
370
+ const tuple = parseTuplet(fullToken);
371
+ if (tuple) {
372
+ if (currentTuple) {
373
+ throw new Error("nested tuples not handled");
374
+ }
375
+ currentTuple = tuple;
376
+ currentBar.push({
377
+ ...tuple,
378
+ token: fullToken,
379
+ sourceIndex: tokenStartPos,
380
+ sourceLength: fullToken.length,
381
+ });
382
+ // previousNote = null; // Tuplet markers break note sequences
383
+ continue;
384
+ }
385
+ }
386
+
387
+ // Standalone chord symbol
388
+ if (fullToken.match(/^"[^"]+"$/)) {
389
+ currentBar.push({
390
+ isChordSymbol: true,
391
+ chordSymbol: fullToken.slice(1, -1),
392
+ token: fullToken,
393
+ sourceIndex: tokenStartPos,
394
+ sourceLength: fullToken.length,
395
+ spacing,
396
+ });
397
+ // Chord symbols do note break note sequences - leave previousNote
398
+ continue;
399
+ }
400
+
401
+ // Standalone decoration
402
+ if (fullToken.match(/^!([^!]+)!$/)) {
403
+ currentBar.push({
404
+ isDecoration: true,
405
+ decoration: fullToken.slice(1, -1),
406
+ token: fullToken,
407
+ sourceIndex: tokenStartPos,
408
+ sourceLength: fullToken.length,
409
+ spacing,
410
+ });
411
+ // Chord symbols do note break note sequences - leave previousNote
412
+ continue;
413
+ }
414
+
415
+ // Regular note, rest, or dummy, or chord in brackets
416
+ const note = parseNote(fullToken, unitLength, currentTuple);
417
+ if (note) {
418
+ const noteObj = {
419
+ ...note,
420
+ token: fullToken,
421
+ sourceIndex: tokenStartPos,
422
+ sourceLength: fullToken.length,
423
+ spacing,
424
+ };
425
+ currentBar.push(noteObj);
426
+ // Only track as previous note if it has duration (for broken rhythms)
427
+ // previousNote = note.duration ? noteObj : null;
428
+ }
429
+ }
430
+ }
431
+
432
+ // Check if bar line has a newline after it
433
+ const barLineEndPos = barLinePos + barLineText.length;
434
+ const hasLineBreakAfterBar =
435
+ newlineSet.has(barLineEndPos + 1) ||
436
+ (barLineEndPos < musicText.length && musicText[barLineEndPos] === "\n");
437
+
438
+ // Store bar line information
439
+ const barLineInfo = classifyBarLine(barLineText);
440
+ barLines.push({
441
+ ...barLineInfo,
442
+ sourceIndex: barLinePos,
443
+ sourceLength: barLineText.length,
444
+ barNumber: barCount,
445
+ hasLineBreak: hasLineBreakAfterBar,
446
+ });
447
+
448
+ // Update the last token in current bar to mark lineBreak if bar line has one
449
+ if (currentBar.length > 0 && hasLineBreakAfterBar) {
450
+ const lastToken = currentBar[currentBar.length - 1];
451
+ if (lastToken.spacing) {
452
+ lastToken.spacing.lineBreak = true;
453
+ }
454
+ }
455
+
456
+ // Save current bar if it has content
457
+ if (currentBar.length > 0) {
458
+ bars.push(currentBar);
459
+ barCount++;
460
+ currentBar = [];
461
+
462
+ // Check if we've reached max bars
463
+ if (barCount >= maxBars) {
464
+ break;
465
+ }
466
+ }
467
+
468
+ lastBarPos = barLineEndPos;
469
+ }
470
+
471
+ // Add final bar if it has content and we haven't reached max
472
+ if (currentBar.length > 0 && barCount < maxBars) {
473
+ bars.push(currentBar);
474
+ }
475
+
476
+ return {
477
+ bars,
478
+ barLines,
479
+ unitLength,
480
+ meter,
481
+ tonalBase,
482
+ lineMetadata,
483
+ headerLines,
484
+ headerEndIndex,
485
+ musicText,
486
+ };
487
+ }
488
+
489
+ /**
490
+ * Calculate bar durations from parsed ABC data
491
+ * Returns duration for each bar as a Fraction
492
+ *
493
+ * @param {object} parsedData - Output from parseABCWithBars
494
+ * @returns {Array<Fraction>} - Array of bar durations
495
+ */
496
+ function calculateBarDurations(parsedData) {
497
+ const { bars, barLines } = parsedData;
498
+ const result = [];
499
+
500
+ // If there's a bar line at the start, add a zero-duration entry
501
+ if (barLines && barLines[0] && barLines[0].sourceIndex === 0) {
502
+ result.push(new Fraction(0, 1));
503
+ }
504
+
505
+ bars.forEach((bar) => {
506
+ let total = new Fraction(0, 1);
507
+ for (const note of bar) {
508
+ if (!note.duration) {
509
+ continue;
510
+ }
511
+ total = total.add(note.duration);
512
+ }
513
+ result.push(total);
514
+ });
515
+
516
+ return result;
517
+ }
518
+
519
+ module.exports = {
520
+ parseABCWithBars,
521
+ calculateBarDurations,
522
+ // Re-export utilities for convenience
523
+ getTonalBase,
524
+ getMeter,
525
+ getUnitLength,
526
+ getMusicLines,
527
+ analyzeSpacing,
528
+ classifyBarLine,
529
+ NOTE_TO_DEGREE,
530
+ };
@@ -0,0 +1,106 @@
1
+ // ============================================================================
2
+ // ABC TOKEN UTILITIES
3
+ // ============================================================================
4
+ //
5
+ // Utilities for tokenising ABC music notation:
6
+ // - Token regex generation
7
+ // - Inline field parsing
8
+ // - Whitespace and beaming analysis
9
+ //
10
+ // ============================================================================
11
+
12
+ /**
13
+ * Get regex for matching ABC music tokens
14
+ * Matches: tuplets, inline fields, chord symbols, notes, rests, chords in brackets, decorations, broken rhythms
15
+ *
16
+ * @returns {RegExp} - Regular expression for tokenising ABC music
17
+ */
18
+ const getTokenRegex = () =>
19
+ /\(\d(?::\d?){0,2}|\[([KLMP]):[^\]]+\]|"[^"]+"|(?:!([^!]+)!\s*)?[~.MPSTHUV]*[=^_]?(?:[A-Ga-gzxy]|\[[A-Ga-gzxy]+\])[',]*[0-9]*\/?[0-9]*(?:-|\s*(?:<{1,3}|>{1,3}))?|!([^!]+)!/g;
20
+
21
+ /**
22
+ * Parse inline field from music section
23
+ * Inline fields allow changing key, meter, length, or part mid-tune
24
+ *
25
+ * @param {string} token - Token string to parse
26
+ * @returns {object|null} - { field, value } or null if not an inline field
27
+ *
28
+ * Supported inline fields:
29
+ * - [K:...] - Key signature change
30
+ * - [L:...] - Unit length change
31
+ * - [M:...] - Meter change
32
+ * - [P:...] - Part marker
33
+ */
34
+ function parseInlineField(token) {
35
+ const fieldMatch = token.match(/^\[([KLMP]):\s*([^\]]+)\]$/);
36
+ if (fieldMatch) {
37
+ return {
38
+ field: fieldMatch[1],
39
+ value: fieldMatch[2].trim(),
40
+ };
41
+ }
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * Analyze whitespace and back quotes after a token
47
+ * Returns object describing the spacing/beaming context
48
+ * Back quotes (`) are ignored for beaming but preserved for reconstruction
49
+ *
50
+ * @param {string} segment - The music segment to analyze
51
+ * @param {number} tokenEndPos - Position where the token ends
52
+ * @returns {object} - Spacing analysis object
53
+ *
54
+ * Return object structure:
55
+ * {
56
+ * whitespace: string, // Actual whitespace characters (back quotes removed)
57
+ * backquotes: number, // Number of ` characters for reconstruction
58
+ * beamBreak: boolean, // True if beam should break (multiple spaces/newline)
59
+ * lineBreak: boolean // True if there was a newline after this token
60
+ * }
61
+ */
62
+ function analyzeSpacing(segment, tokenEndPos) {
63
+ if (tokenEndPos >= segment.length) {
64
+ return {
65
+ whitespace: "",
66
+ backquotes: 0,
67
+ beamBreak: false,
68
+ lineBreak: false,
69
+ };
70
+ }
71
+
72
+ const remaining = segment.substring(tokenEndPos);
73
+
74
+ // Match whitespace and/or back quotes
75
+ const spacingMatch = remaining.match(/^([\s`]+)/);
76
+
77
+ if (!spacingMatch) {
78
+ return {
79
+ whitespace: "",
80
+ backquotes: 0,
81
+ beamBreak: false,
82
+ lineBreak: false,
83
+ };
84
+ }
85
+
86
+ const fullSpacing = spacingMatch[1];
87
+
88
+ // Count back quotes
89
+ const backquotes = (fullSpacing.match(/`/g) || []).length;
90
+
91
+ // Extract just whitespace (no back quotes)
92
+ const whitespace = fullSpacing.replace(/`/g, "");
93
+
94
+ return {
95
+ whitespace,
96
+ backquotes,
97
+ beamBreak: whitespace.length > 1 || whitespace.includes("\n"), // Multiple spaces or newline breaks beam
98
+ lineBreak: whitespace.includes("\n"),
99
+ };
100
+ }
101
+
102
+ module.exports = {
103
+ getTokenRegex,
104
+ parseInlineField,
105
+ analyzeSpacing,
106
+ };