Skip to content

Commit bb86a85

Browse files
Treat commas as soft line breaks
1 parent 14df60b commit bb86a85

File tree

5 files changed

+136
-80
lines changed

5 files changed

+136
-80
lines changed

src/formatter/pdf_formatter.ts

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import TextFormatter from './text_formatter';
77
import Paragraph from '../chord_sheet/paragraph';
88
import Line from '../chord_sheet/line';
99
import { ChordLyricsPair, Comment, SoftLineBreak, Tag } from '../index';
10+
import Item from '../chord_sheet/item';
11+
import { stringSplitReplace } from '../helpers';
1012

1113
type FontSection = 'title' | 'subtitle' | 'metadata' | 'text' | 'chord' | 'comment' | 'annotation';
1214
type LayoutSection = 'header' | 'footer';
@@ -67,7 +69,7 @@ type LayoutItem = {
6769
};
6870

6971
type MeasuredItem = {
70-
item: ChordLyricsPair | Comment | SoftLineBreak,
72+
item: ChordLyricsPair | Comment | SoftLineBreak | Tag | Item,
7173
width: number,
7274
chordHeight?: number,
7375
};
@@ -361,36 +363,88 @@ class PdfFormatter extends Formatter {
361363
const lyricsFont: FontConfiguration = this.getFontConfiguration('text');
362364
const commentFont: FontConfiguration = this.getFontConfiguration('comment');
363365

364-
const renderedLine = line.items.map((item) => {
366+
const measuredItems: MeasuredItem[] = line.items.flatMap((item: Item): MeasuredItem[] => {
365367
if (isChordLyricsPair(item)) {
366-
const chordLyricsPair = item as ChordLyricsPair;
367-
const { chords, lyrics } = chordLyricsPair;
368-
const chordWidth = this.getTextDimensions(chords, chordFont).w;
369-
const lyricWidth = this.getTextDimensions(lyrics, lyricsFont).w;
370-
371-
const pairWidth = (chordWidth > lyricWidth)
372-
? this.getTextDimensions(`${chords}${this.spaces}`, chordFont).w
373-
: lyricWidth;
374-
375-
return {
376-
item: chordLyricsPair,
377-
width: pairWidth,
378-
chordHeight: this.getTextDimensions(chords, chordFont).h,
379-
};
380-
} else if (isTag(item) && isComment(item as Tag)) {
381-
const comment = item as Tag;
382-
const commentWidth = this.getTextDimensions(comment.value, commentFont).w;
368+
const items: Array<ChordLyricsPair | SoftLineBreak> =
369+
this.addSoftLineBreaksToChordLyricsPair(item as ChordLyricsPair);
383370

384-
return {
385-
item: comment,
386-
width: commentWidth,
387-
};
371+
return items.flatMap((i): MeasuredItem[] => (
372+
this.measureItem(i, chordFont, lyricsFont, commentFont)
373+
));
374+
} else if (isTag(item) && isComment(item as Tag)) {
375+
return this.measureTag(item as Tag, commentFont);
388376
} else {
389-
return { item, width: 0 };
377+
return [{ item, width: 0 }];
390378
}
391379
});
392380

393-
this.renderLineItems(renderedLine);
381+
this.renderLineItems(measuredItems);
382+
}
383+
384+
measureItem(
385+
item: ChordLyricsPair | SoftLineBreak,
386+
chordFont: FontConfiguration,
387+
lyricFont: FontConfiguration,
388+
commentFont: FontConfiguration,
389+
): MeasuredItem[] {
390+
if (item instanceof ChordLyricsPair) {
391+
return this.measureChordLyricsPair(item, chordFont, lyricFont);
392+
}
393+
394+
if (item instanceof Tag && isComment(item)) {
395+
return this.measureTag(item, commentFont);
396+
}
397+
398+
return [];
399+
}
400+
401+
addSoftLineBreaksToChordLyricsPair(chordLyricsPair: ChordLyricsPair): Array<ChordLyricsPair | SoftLineBreak> {
402+
const { annotation, chords, lyrics } = chordLyricsPair;
403+
404+
const items: Array<ChordLyricsPair | SoftLineBreak> = stringSplitReplace(
405+
lyrics || '',
406+
/,\s*/,
407+
(content) => [new SoftLineBreak(), new ChordLyricsPair('', content)],
408+
(lyric) => new ChordLyricsPair('', lyric),
409+
).flat();
410+
411+
let [first, ...rest] = items;
412+
let addedLeadingPair: ChordLyricsPair | null = null;
413+
414+
if (chords !== '' || annotation !== '') {
415+
if (!first || first instanceof SoftLineBreak) {
416+
addedLeadingPair = new ChordLyricsPair(chords, lyrics, annotation);
417+
} else {
418+
first = new ChordLyricsPair(chords, first.lyrics, annotation);
419+
}
420+
}
421+
422+
return [addedLeadingPair, first, ...rest].filter((item) => item !== null);
423+
}
424+
425+
measureChordLyricsPair(
426+
item: ChordLyricsPair,
427+
chordFont: FontConfiguration,
428+
lyricsFont: FontConfiguration,
429+
): MeasuredItem[] {
430+
const { chords, lyrics } = item;
431+
const chordWidth = this.getTextDimensions(chords, chordFont).w;
432+
const lyricWidth = this.getTextDimensions(lyrics, lyricsFont).w;
433+
434+
const pairWidth = (chordWidth > lyricWidth)
435+
? this.getTextDimensions(`${chords}${this.spaces}`, chordFont).w
436+
: lyricWidth;
437+
438+
return [{
439+
item,
440+
width: pairWidth,
441+
chordHeight: this.getTextDimensions(chords, chordFont).h,
442+
}];
443+
}
444+
445+
measureTag(item: Tag, font: FontConfiguration): MeasuredItem[] {
446+
const tagWidth = this.getTextDimensions(item.value, font).w;
447+
return [{ item, width: tagWidth }];
394448
}
395449

396450
get spaces() {

src/helpers.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,31 @@ export function renderChord(
8787
return changeChordType(normalizedChord, chordStyle, effectiveKey).toString({ useUnicodeModifier });
8888
}
8989

90+
export function stringSplitReplace(
91+
string: string,
92+
search: string | RegExp,
93+
replaceMatch: (subString: string) => any,
94+
replaceRest: (subString: string, match: string | null) => any = (subString) => subString,
95+
): any[] {
96+
const regExp = new RegExp(search, 'g');
97+
const occurrences = Array.from(string.matchAll(regExp));
98+
const result: string[] = [];
99+
let index = 0;
100+
101+
occurrences.forEach((match) => {
102+
const before = string.slice(index, match.index);
103+
const matchedPart = match[0];
104+
if (before !== '') result.push(replaceRest(before, matchedPart));
105+
result.push(replaceMatch(matchedPart));
106+
index = match.index + matchedPart.length;
107+
});
108+
109+
const rest = string.slice(index);
110+
if (rest !== '') result.push(replaceRest(rest, null));
111+
112+
return result;
113+
}
114+
90115
/**
91116
* Returns applicable capos for the provided key
92117
* @param {Key|string} key The key to get capos for

src/parser/chord_pro/helpers.ts

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from '../../serialized_types';
88

99
import { FileRange } from './peg_parser';
10+
import { stringSplitReplace } from '../../helpers';
1011

1112
function splitSectionContent(content: string): string[] {
1213
return content
@@ -38,30 +39,6 @@ export function buildTag(name: string, value: string | null, location: FileRange
3839
};
3940
}
4041

41-
export function stringSplitReplace(
42-
string: string,
43-
search: string,
44-
replaceMatch: (subString: string) => any,
45-
replaceRest: (subString: string) => any = (subString) => subString,
46-
): any[] {
47-
const regExp = new RegExp(search, 'g');
48-
const occurrences = Array.from(string.matchAll(regExp));
49-
const result: string[] = [];
50-
let index = 0;
51-
52-
occurrences.forEach((match) => {
53-
const before = string.slice(index, match.index);
54-
if (before !== '') result.push(replaceRest(before));
55-
result.push(replaceMatch(match[0]));
56-
index = match.index + match[0].length;
57-
});
58-
59-
const rest = string.slice(index);
60-
if (rest !== '') result.push(replaceRest(rest));
61-
62-
return result;
63-
}
64-
6542
export function applySoftLineBreaks(lyrics: string): SerializedChordLyricsPair[] {
6643
return stringSplitReplace(
6744
lyrics,

test/helpers.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Song from '../src/chord_sheet/song';
22
import { createLine } from './utilities';
3-
import { renderChord } from '../src/helpers';
3+
import { renderChord, stringSplitReplace } from '../src/helpers';
44
import Key from '../src/key';
55

66
describe('renderChord', () => {
@@ -29,3 +29,32 @@ describe('renderChord', () => {
2929
expect(renderChord('Dm7', line, song, { renderKey: Key.parse('B'), useUnicodeModifier: true })).toEqual('G♯m7');
3030
});
3131
});
32+
33+
describe('stringSplitReplace', () => {
34+
it('should replace all instances of a match', () => {
35+
const testString = 'I am a barber';
36+
37+
const result =
38+
stringSplitReplace(
39+
testString,
40+
'a',
41+
(_match) => 'BREAK',
42+
);
43+
44+
expect(result).toEqual(['I ', 'BREAK', 'm ', 'BREAK', ' b', 'BREAK', 'rber']);
45+
});
46+
47+
it('should replace all instances of a match and the rest of the string', () => {
48+
const testString = 'ai am a barber';
49+
50+
const result =
51+
stringSplitReplace(
52+
testString,
53+
'a',
54+
(_match) => 'BREAK',
55+
(match) => match.toUpperCase(),
56+
);
57+
58+
expect(result).toEqual(['BREAK', 'I ', 'BREAK', 'M ', 'BREAK', ' B', 'BREAK', 'RBER']);
59+
});
60+
});

test/parser/chord_pro/helpers.test.ts

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,6 @@
1-
import { breakChordLyricsPairOnSoftLineBreak, stringSplitReplace } from '../../../src/parser/chord_pro/helpers';
1+
import { breakChordLyricsPairOnSoftLineBreak } from '../../../src/parser/chord_pro/helpers';
22
import { SerializedChordLyricsPair, SerializedSoftLineBreak } from '../../../src/serialized_types';
33

4-
describe('stringSplitReplace', () => {
5-
it('should replace all instances of a match', () => {
6-
const testString = 'I am a barber';
7-
8-
const result =
9-
stringSplitReplace(
10-
testString,
11-
'a',
12-
(_match) => 'BREAK',
13-
);
14-
15-
expect(result).toEqual(['I ', 'BREAK', 'm ', 'BREAK', ' b', 'BREAK', 'rber']);
16-
});
17-
18-
it('should replace all instances of a match and the rest of the string', () => {
19-
const testString = 'ai am a barber';
20-
21-
const result =
22-
stringSplitReplace(
23-
testString,
24-
'a',
25-
(_match) => 'BREAK',
26-
(match) => match.toUpperCase(),
27-
);
28-
29-
expect(result).toEqual(['BREAK', 'I ', 'BREAK', 'M ', 'BREAK', ' B', 'BREAK', 'RBER']);
30-
});
31-
});
32-
334
describe('breakChordLyricsPairOnSoftLineBreak', () => {
345
it('supports breaking a pair\'s lyrics on a soft line break', () => {
356
const chords = 'D/A';

0 commit comments

Comments
 (0)