songsheet 7.4.0 → 7.6.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/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/lexer.js +60 -3
- package/src/parser.js +133 -9
- package/src/playback.js +2 -1
- package/test/lexer.test.js +59 -0
- package/test/parser.test.js +57 -0
- package/.claude/settings.local.json +0 -18
package/index.d.ts
CHANGED
package/package.json
CHANGED
package/src/lexer.js
CHANGED
|
@@ -62,13 +62,49 @@ const DECORATOR_STOP_CHARS = new Set(['>', ']', '!'])
|
|
|
62
62
|
*
|
|
63
63
|
* A chord line must contain at least one chord, and every non-whitespace
|
|
64
64
|
* token must parse as a valid chord, decorator, or `|`.
|
|
65
|
+
*
|
|
66
|
+
* Options:
|
|
67
|
+
* continueSplit: true — line is a continuation of a cross-line split measure.
|
|
68
|
+
* Pre-scans for chords before `]`, emits SPLIT_CLOSE, then continues normal scanning.
|
|
65
69
|
*/
|
|
66
|
-
export function scanChordLine(line) {
|
|
70
|
+
export function scanChordLine(line, options = {}) {
|
|
67
71
|
if (!line || line.trim().length === 0) return null
|
|
68
72
|
|
|
69
73
|
const tokens = []
|
|
70
74
|
let i = 0
|
|
71
75
|
|
|
76
|
+
// When continueSplit is set, we expect chords followed by `]` at the start of the line
|
|
77
|
+
if (options.continueSplit) {
|
|
78
|
+
const closeChords = []
|
|
79
|
+
// scan for chords before `]`
|
|
80
|
+
while (i < line.length && line[i] !== ']') {
|
|
81
|
+
if (line[i] === ' ' || line[i] === '\t') { i++; continue }
|
|
82
|
+
const result = parseChordAt(line, i, DECORATOR_STOP_CHARS)
|
|
83
|
+
if (!result) return null // non-chord content before ] means not a valid continuation
|
|
84
|
+
closeChords.push(result.token)
|
|
85
|
+
i = result.end
|
|
86
|
+
}
|
|
87
|
+
if (i >= line.length || line[i] !== ']') return null // no ] found
|
|
88
|
+
if (closeChords.length === 0) return null // must have at least one chord
|
|
89
|
+
const column = closeChords[0].column
|
|
90
|
+
i++ // consume ]
|
|
91
|
+
// next char must be whitespace, end-of-line, or |
|
|
92
|
+
if (i < line.length && line[i] !== ' ' && line[i] !== '\t' && line[i] !== '|') {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
tokens.push({
|
|
96
|
+
type: 'SPLIT_CLOSE',
|
|
97
|
+
column,
|
|
98
|
+
chords: closeChords.map(c => {
|
|
99
|
+
const sc = { root: c.root, type: c.quality }
|
|
100
|
+
if (c.bass) sc.bass = c.bass
|
|
101
|
+
if (c.nashville) sc.nashville = true
|
|
102
|
+
return sc
|
|
103
|
+
}),
|
|
104
|
+
})
|
|
105
|
+
// continue scanning the rest of the line normally
|
|
106
|
+
}
|
|
107
|
+
|
|
72
108
|
while (i < line.length) {
|
|
73
109
|
// skip whitespace
|
|
74
110
|
if (line[i] === ' ' || line[i] === '\t') {
|
|
@@ -125,6 +161,7 @@ export function scanChordLine(line) {
|
|
|
125
161
|
|
|
126
162
|
// split measure: [chord chord ...]
|
|
127
163
|
if (line[i] === '[') {
|
|
164
|
+
const openColumn = column
|
|
128
165
|
i++ // consume [
|
|
129
166
|
const chords = []
|
|
130
167
|
while (i < line.length && line[i] !== ']') {
|
|
@@ -134,7 +171,27 @@ export function scanChordLine(line) {
|
|
|
134
171
|
chords.push(result.token)
|
|
135
172
|
i = result.end
|
|
136
173
|
}
|
|
137
|
-
if (i >= line.length
|
|
174
|
+
if (i >= line.length) {
|
|
175
|
+
// No ] found — this is a cross-line split open
|
|
176
|
+
if (chords.length < 1) return null // need at least 1 chord after [
|
|
177
|
+
const first = chords[0]
|
|
178
|
+
const token = {
|
|
179
|
+
type: 'SPLIT_OPEN',
|
|
180
|
+
column: openColumn,
|
|
181
|
+
root: first.root,
|
|
182
|
+
quality: first.quality,
|
|
183
|
+
chords: chords.map(c => {
|
|
184
|
+
const sc = { root: c.root, type: c.quality }
|
|
185
|
+
if (c.bass) sc.bass = c.bass
|
|
186
|
+
if (c.nashville) sc.nashville = true
|
|
187
|
+
return sc
|
|
188
|
+
}),
|
|
189
|
+
}
|
|
190
|
+
if (first.bass) token.bass = first.bass
|
|
191
|
+
if (first.nashville) token.nashville = true
|
|
192
|
+
tokens.push(token)
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
138
195
|
i++ // consume ]
|
|
139
196
|
if (chords.length < 2) return null
|
|
140
197
|
// next char must be whitespace, end-of-line, or |
|
|
@@ -145,7 +202,7 @@ export function scanChordLine(line) {
|
|
|
145
202
|
const first = chords[0]
|
|
146
203
|
const token = {
|
|
147
204
|
type: 'CHORD',
|
|
148
|
-
column,
|
|
205
|
+
column: openColumn,
|
|
149
206
|
root: first.root,
|
|
150
207
|
quality: first.quality,
|
|
151
208
|
splitMeasure: chords.map(c => {
|
package/src/parser.js
CHANGED
|
@@ -262,15 +262,111 @@ function parseChordLyricBlock(text) {
|
|
|
262
262
|
const allLyrics = []
|
|
263
263
|
let i = 0
|
|
264
264
|
let lastChord = null
|
|
265
|
+
let pendingSplit = null // tracks cross-line split measure state
|
|
265
266
|
|
|
266
267
|
while (i < rawLines.length) {
|
|
267
268
|
const line = rawLines[i]
|
|
269
|
+
|
|
270
|
+
// If we have a pending split open, try to parse this line with continueSplit first
|
|
271
|
+
if (pendingSplit) {
|
|
272
|
+
const contTokens = scanChordLine(line, { continueSplit: true })
|
|
273
|
+
if (contTokens) {
|
|
274
|
+
// Found the SPLIT_CLOSE — merge with the open chords
|
|
275
|
+
const closeToken = contTokens.find(t => t.type === 'SPLIT_CLOSE')
|
|
276
|
+
if (closeToken) {
|
|
277
|
+
const allSplitChords = [...pendingSplit.openChords, ...closeToken.chords]
|
|
278
|
+
const splitMeasure = allSplitChords
|
|
279
|
+
// Update the chord on the earlier line with the complete splitMeasure
|
|
280
|
+
const earlierChord = pendingSplit.lineData.chords[pendingSplit.chordIndex]
|
|
281
|
+
earlierChord.splitMeasure = splitMeasure
|
|
282
|
+
// Also update the allChords entry
|
|
283
|
+
const allChordsEntry = allChords[pendingSplit.allChordsIndex]
|
|
284
|
+
if (allChordsEntry) {
|
|
285
|
+
allChordsEntry.splitMeasure = splitMeasure
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Now process the rest of the tokens on this line (chords/bars after the ] )
|
|
289
|
+
const restTokens = contTokens.filter(t => t.type !== 'SPLIT_CLOSE')
|
|
290
|
+
const restChords = restTokens.filter(t => t.type === 'CHORD').map(t => tokenToPositionedChord(t))
|
|
291
|
+
|
|
292
|
+
// Build barLines for this line
|
|
293
|
+
const sortedTokens = [...contTokens].sort((a, b) => a.column - b.column)
|
|
294
|
+
let currentChord = lastChord
|
|
295
|
+
const barLines = []
|
|
296
|
+
for (const tok of sortedTokens) {
|
|
297
|
+
if (tok.type === 'CHORD') {
|
|
298
|
+
currentChord = tokenToChord(tok)
|
|
299
|
+
} else if (tok.type === 'BAR_LINE') {
|
|
300
|
+
const bar = { column: tok.column }
|
|
301
|
+
if (currentChord) bar.chord = currentChord
|
|
302
|
+
barLines.push(bar)
|
|
303
|
+
}
|
|
304
|
+
// SPLIT_CLOSE doesn't update currentChord (those chords belong to the previous line)
|
|
305
|
+
}
|
|
306
|
+
if (currentChord) lastChord = currentChord
|
|
307
|
+
|
|
308
|
+
pendingSplit = null
|
|
309
|
+
i++
|
|
310
|
+
|
|
311
|
+
// Find the paired lyric line
|
|
312
|
+
let lyricLine = ''
|
|
313
|
+
if (i < rawLines.length) {
|
|
314
|
+
// Check if next line is a chord line (with or without continueSplit)
|
|
315
|
+
const nextIsChord = scanChordLine(rawLines[i])
|
|
316
|
+
if (!nextIsChord) {
|
|
317
|
+
lyricLine = rawLines[i]
|
|
318
|
+
i++
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
allChords.push(...restChords.map(c => {
|
|
323
|
+
const ch = { root: c.root, type: c.type }
|
|
324
|
+
if (c.bass) ch.bass = c.bass
|
|
325
|
+
if (c.nashville) ch.nashville = true
|
|
326
|
+
if (c.diamond) ch.diamond = true
|
|
327
|
+
if (c.push) ch.push = true
|
|
328
|
+
if (c.stop) ch.stop = true
|
|
329
|
+
if (c.splitMeasure) ch.splitMeasure = c.splitMeasure
|
|
330
|
+
return ch
|
|
331
|
+
}))
|
|
332
|
+
if (lyricLine) allLyrics.push(lyricLine)
|
|
333
|
+
|
|
334
|
+
lines.push({
|
|
335
|
+
chords: restChords,
|
|
336
|
+
barLines,
|
|
337
|
+
lyrics: lyricLine,
|
|
338
|
+
characters: buildCharacterAlignment(restTokens, lyricLine),
|
|
339
|
+
})
|
|
340
|
+
continue
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// If continueSplit didn't match, this line is a lyric line between split lines
|
|
344
|
+
// (fall through to normal handling below)
|
|
345
|
+
}
|
|
346
|
+
|
|
268
347
|
const tokens = scanChordLine(line)
|
|
269
348
|
|
|
270
349
|
if (tokens) {
|
|
350
|
+
// Check for SPLIT_OPEN token (cross-line split)
|
|
351
|
+
const splitOpenToken = tokens.find(t => t.type === 'SPLIT_OPEN')
|
|
352
|
+
|
|
271
353
|
// This is a chord line — next non-chord line is its paired lyric
|
|
272
|
-
const chordTokens = tokens
|
|
273
|
-
|
|
354
|
+
const chordTokens = tokens.filter(t => t.type !== 'SPLIT_OPEN')
|
|
355
|
+
// For SPLIT_OPEN, create a positioned chord from the first open chord at the [ column
|
|
356
|
+
const chords = []
|
|
357
|
+
for (const t of tokens) {
|
|
358
|
+
if (t.type === 'CHORD') {
|
|
359
|
+
chords.push(tokenToPositionedChord(t))
|
|
360
|
+
} else if (t.type === 'SPLIT_OPEN') {
|
|
361
|
+
// Add the first chord of the split as a positioned chord
|
|
362
|
+
const first = t.chords[0]
|
|
363
|
+
const chord = { root: first.root, type: first.type, column: t.column }
|
|
364
|
+
if (first.bass) chord.bass = first.bass
|
|
365
|
+
if (first.nashville) chord.nashville = true
|
|
366
|
+
// splitMeasure will be set later when we find the SPLIT_CLOSE
|
|
367
|
+
chords.push(chord)
|
|
368
|
+
}
|
|
369
|
+
}
|
|
274
370
|
|
|
275
371
|
// Build barLines as objects with chord context
|
|
276
372
|
// Sort all tokens by column, walk left-to-right tracking currentChord
|
|
@@ -278,8 +374,16 @@ function parseChordLyricBlock(text) {
|
|
|
278
374
|
let currentChord = lastChord
|
|
279
375
|
const barLines = []
|
|
280
376
|
for (const tok of sortedTokens) {
|
|
281
|
-
if (tok.type === 'CHORD') {
|
|
282
|
-
|
|
377
|
+
if (tok.type === 'CHORD' || tok.type === 'SPLIT_OPEN') {
|
|
378
|
+
if (tok.type === 'CHORD') {
|
|
379
|
+
currentChord = tokenToChord(tok)
|
|
380
|
+
} else {
|
|
381
|
+
// Use first chord of split open
|
|
382
|
+
const first = tok.chords[0]
|
|
383
|
+
currentChord = { root: first.root, type: first.type }
|
|
384
|
+
if (first.bass) currentChord.bass = first.bass
|
|
385
|
+
if (first.nashville) currentChord.nashville = true
|
|
386
|
+
}
|
|
283
387
|
} else if (tok.type === 'BAR_LINE') {
|
|
284
388
|
const bar = { column: tok.column }
|
|
285
389
|
if (currentChord) bar.chord = currentChord
|
|
@@ -292,11 +396,19 @@ function parseChordLyricBlock(text) {
|
|
|
292
396
|
i++
|
|
293
397
|
// Find the paired lyric line (next line that isn't a chord line)
|
|
294
398
|
let lyricLine = ''
|
|
295
|
-
if (i < rawLines.length
|
|
296
|
-
|
|
297
|
-
i
|
|
399
|
+
if (i < rawLines.length) {
|
|
400
|
+
// When pendingSplit would be set, also check continueSplit before treating as lyrics
|
|
401
|
+
const nextIsChord = scanChordLine(rawLines[i])
|
|
402
|
+
if (!nextIsChord) {
|
|
403
|
+
// Also check if it's a split continuation line
|
|
404
|
+
if (!splitOpenToken || !scanChordLine(rawLines[i], { continueSplit: true })) {
|
|
405
|
+
lyricLine = rawLines[i]
|
|
406
|
+
i++
|
|
407
|
+
}
|
|
408
|
+
}
|
|
298
409
|
}
|
|
299
410
|
|
|
411
|
+
const allChordsStartIndex = allChords.length
|
|
300
412
|
allChords.push(...chords.map(c => {
|
|
301
413
|
const ch = { root: c.root, type: c.type }
|
|
302
414
|
if (c.bass) ch.bass = c.bass
|
|
@@ -309,12 +421,24 @@ function parseChordLyricBlock(text) {
|
|
|
309
421
|
}))
|
|
310
422
|
if (lyricLine) allLyrics.push(lyricLine)
|
|
311
423
|
|
|
312
|
-
|
|
424
|
+
const lineData = {
|
|
313
425
|
chords,
|
|
314
426
|
barLines,
|
|
315
427
|
lyrics: lyricLine,
|
|
316
428
|
characters: buildCharacterAlignment(chordTokens, lyricLine),
|
|
317
|
-
}
|
|
429
|
+
}
|
|
430
|
+
lines.push(lineData)
|
|
431
|
+
|
|
432
|
+
// If we found a SPLIT_OPEN, set up pendingSplit for cross-line merging
|
|
433
|
+
if (splitOpenToken) {
|
|
434
|
+
const splitChordIndex = chords.length - 1 // the split chord is the last one we added
|
|
435
|
+
pendingSplit = {
|
|
436
|
+
lineData,
|
|
437
|
+
chordIndex: splitChordIndex,
|
|
438
|
+
allChordsIndex: allChordsStartIndex + splitChordIndex,
|
|
439
|
+
openChords: splitOpenToken.chords,
|
|
440
|
+
}
|
|
441
|
+
}
|
|
318
442
|
} else {
|
|
319
443
|
// Lyric-only line (no chord line above it)
|
|
320
444
|
if (line.trim().length > 0) {
|
package/src/playback.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
function expandChord(chord, markerIndex) {
|
|
6
6
|
if (chord.splitMeasure && chord.splitMeasure.length > 0) {
|
|
7
|
-
return chord.splitMeasure.map(sc => {
|
|
7
|
+
return chord.splitMeasure.map((sc, index) => {
|
|
8
8
|
const pc = { root: sc.root, type: sc.type }
|
|
9
9
|
if (sc.bass) pc.bass = sc.bass
|
|
10
10
|
if (sc.nashville) pc.nashville = true
|
|
@@ -12,6 +12,7 @@ function expandChord(chord, markerIndex) {
|
|
|
12
12
|
if (sc.push) pc.push = true
|
|
13
13
|
if (sc.stop) pc.stop = true
|
|
14
14
|
if (markerIndex !== undefined) pc.markerIndex = markerIndex
|
|
15
|
+
pc.splitIndex = index
|
|
15
16
|
return pc
|
|
16
17
|
})
|
|
17
18
|
}
|
package/test/lexer.test.js
CHANGED
|
@@ -130,6 +130,65 @@ describe('scanChordLine slash chords', () => {
|
|
|
130
130
|
})
|
|
131
131
|
})
|
|
132
132
|
|
|
133
|
+
describe('scanChordLine split measures', () => {
|
|
134
|
+
it('parses same-line split measure [G C]', () => {
|
|
135
|
+
const tokens = scanChordLine('[G C]')
|
|
136
|
+
expect(tokens).not.toBeNull()
|
|
137
|
+
expect(tokens.length).toBe(1)
|
|
138
|
+
expect(tokens[0].type).toBe('CHORD')
|
|
139
|
+
expect(tokens[0].splitMeasure).toEqual([
|
|
140
|
+
{ root: 'G', type: '' },
|
|
141
|
+
{ root: 'C', type: '' },
|
|
142
|
+
])
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('parses cross-line split open: [F at end of line', () => {
|
|
146
|
+
const tokens = scanChordLine('G [F')
|
|
147
|
+
expect(tokens).not.toBeNull()
|
|
148
|
+
expect(tokens.length).toBe(2)
|
|
149
|
+
expect(tokens[0]).toMatchObject({ type: 'CHORD', root: 'G' })
|
|
150
|
+
expect(tokens[1].type).toBe('SPLIT_OPEN')
|
|
151
|
+
expect(tokens[1].chords).toEqual([{ root: 'F', type: '' }])
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('parses cross-line split close with continueSplit', () => {
|
|
155
|
+
const tokens = scanChordLine(' C] G', { continueSplit: true })
|
|
156
|
+
expect(tokens).not.toBeNull()
|
|
157
|
+
const closeToken = tokens.find(t => t.type === 'SPLIT_CLOSE')
|
|
158
|
+
expect(closeToken).toBeDefined()
|
|
159
|
+
expect(closeToken.chords).toEqual([{ root: 'C', type: '' }])
|
|
160
|
+
// Rest of line parsed normally
|
|
161
|
+
const chordTokens = tokens.filter(t => t.type === 'CHORD')
|
|
162
|
+
expect(chordTokens.length).toBe(1)
|
|
163
|
+
expect(chordTokens[0].root).toBe('G')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('returns null for continueSplit when no ] found', () => {
|
|
167
|
+
const tokens = scanChordLine('C G', { continueSplit: true })
|
|
168
|
+
expect(tokens).toBeNull()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('returns null for continueSplit on a lyric line', () => {
|
|
172
|
+
const tokens = scanChordLine('Blue mountain road', { continueSplit: true })
|
|
173
|
+
expect(tokens).toBeNull()
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('parses split open with multiple chords: [F C', () => {
|
|
177
|
+
const tokens = scanChordLine('[F C')
|
|
178
|
+
expect(tokens).not.toBeNull()
|
|
179
|
+
expect(tokens[0].type).toBe('SPLIT_OPEN')
|
|
180
|
+
expect(tokens[0].chords).toEqual([
|
|
181
|
+
{ root: 'F', type: '' },
|
|
182
|
+
{ root: 'C', type: '' },
|
|
183
|
+
])
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('returns null for [ with no chords', () => {
|
|
187
|
+
const tokens = scanChordLine('[')
|
|
188
|
+
expect(tokens).toBeNull()
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
|
|
133
192
|
describe('lexExpression', () => {
|
|
134
193
|
it('tokenizes simple chord list', () => {
|
|
135
194
|
const tokens = lexExpression('D G D A')
|
package/test/parser.test.js
CHANGED
|
@@ -302,6 +302,63 @@ describe('time signature parsing', () => {
|
|
|
302
302
|
})
|
|
303
303
|
})
|
|
304
304
|
|
|
305
|
+
describe('cross-line split measure parsing', () => {
|
|
306
|
+
it('parses cross-line split [F ... C] across chord lines', () => {
|
|
307
|
+
const song = parse(
|
|
308
|
+
'TEST - AUTHOR\n\n' +
|
|
309
|
+
'G [F\n' +
|
|
310
|
+
' Blue mountain road, North Carolina,\n' +
|
|
311
|
+
' C] G\n' +
|
|
312
|
+
" I've been to Asheville once before"
|
|
313
|
+
)
|
|
314
|
+
const lines = song.structure[0].lines
|
|
315
|
+
// First line should have G and the split chord (F + C)
|
|
316
|
+
expect(lines[0].chords.length).toBe(2)
|
|
317
|
+
expect(lines[0].chords[0].root).toBe('G')
|
|
318
|
+
expect(lines[0].chords[1].root).toBe('F')
|
|
319
|
+
expect(lines[0].chords[1].splitMeasure).toEqual([
|
|
320
|
+
{ root: 'F', type: '' },
|
|
321
|
+
{ root: 'C', type: '' },
|
|
322
|
+
])
|
|
323
|
+
// Second line should have G (after the ])
|
|
324
|
+
expect(lines[1].chords.length).toBe(1)
|
|
325
|
+
expect(lines[1].chords[0].root).toBe('G')
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
it('pairs lyrics correctly with cross-line split', () => {
|
|
329
|
+
const song = parse(
|
|
330
|
+
'TEST - AUTHOR\n\n' +
|
|
331
|
+
'G [F\n' +
|
|
332
|
+
' Blue mountain road\n' +
|
|
333
|
+
' C] G\n' +
|
|
334
|
+
' Asheville town'
|
|
335
|
+
)
|
|
336
|
+
const lines = song.structure[0].lines
|
|
337
|
+
expect(lines[0].lyrics).toBe(' Blue mountain road')
|
|
338
|
+
expect(lines[1].lyrics).toBe(' Asheville town')
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('includes split measure chords in section allChords', () => {
|
|
342
|
+
const song = parse(
|
|
343
|
+
'TEST - AUTHOR\n\n' +
|
|
344
|
+
'G [F\n' +
|
|
345
|
+
' Lyrics one\n' +
|
|
346
|
+
'C] D\n' +
|
|
347
|
+
' Lyrics two'
|
|
348
|
+
)
|
|
349
|
+
// allChords should include G, the split chord (F+C), and D
|
|
350
|
+
const chords = song.structure[0].chords
|
|
351
|
+
expect(chords.length).toBe(3)
|
|
352
|
+
expect(chords[0].root).toBe('G')
|
|
353
|
+
expect(chords[1].root).toBe('F')
|
|
354
|
+
expect(chords[1].splitMeasure).toEqual([
|
|
355
|
+
{ root: 'F', type: '' },
|
|
356
|
+
{ root: 'C', type: '' },
|
|
357
|
+
])
|
|
358
|
+
expect(chords[2].root).toBe('D')
|
|
359
|
+
})
|
|
360
|
+
})
|
|
361
|
+
|
|
305
362
|
describe('bar line parsing', () => {
|
|
306
363
|
it('parses bar lines in chord lines', () => {
|
|
307
364
|
const song = parse('TEST SONG - AUTHOR\n\n| G | C | D |\nSome lyrics here')
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(/usr/bin/git -C /Users/administrator/Projects/songsheet log --oneline --all)",
|
|
5
|
-
"Bash(/usr/bin/git -C /Users/administrator/Projects/songsheet diff v4.0.0..v5.0.0 -- index.js)",
|
|
6
|
-
"Bash(npm install:*)",
|
|
7
|
-
"Bash(npx vitest run:*)",
|
|
8
|
-
"Bash(node -e:*)",
|
|
9
|
-
"WebSearch",
|
|
10
|
-
"WebFetch(domain:tonejs.github.io)",
|
|
11
|
-
"WebFetch(domain:github.com)",
|
|
12
|
-
"WebFetch(domain:www.andronio.me)",
|
|
13
|
-
"WebFetch(domain:www.guitarland.com)",
|
|
14
|
-
"Bash(npm test:*)",
|
|
15
|
-
"Bash(/usr/bin/git diff HEAD~2..HEAD -- package.json)"
|
|
16
|
-
]
|
|
17
|
-
}
|
|
18
|
-
}
|