Skip to content

Commit

Permalink
Implement ChordPro annotations (#1127)
Browse files Browse the repository at this point in the history
Quoted from the ChordPro docs
(https://www.chordpro.org/chordpro/chordpro-introduction/#markup-and-annotations):

Annotations are textual remarks placed above the lyrics, just like chords.
Annotations are specified with [*text], again just like chords. Depending
on the software used to process the ChordPro data, annotations may be
rendered in an outstanding manner.

This is a first iteration on annotation support, where the annotation is
added to a `ChordLyricsPair`. The pair can either have `chords` or
`annotation`.

In `TextFormatter` and `ChordsOverWordsFormatter` annotations are
treated equally to lyrics. In `HtmlDivFormatter` and
`HtmlTableFormatter` annotatioins are rendered similar to how lyrics are
rendered, but the wrapping element is given the css class `annotation`
instead of `lyrics`.

See https://www.chordpro.org/chordpro/chordpro-introduction/#markup-and-annotations

Resolves #980
  • Loading branch information
martijnversluis authored Mar 13, 2024
1 parent a134f58 commit 3a6e238
Show file tree
Hide file tree
Showing 26 changed files with 368 additions and 115 deletions.
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -575,22 +575,24 @@ Possible values are 'solfege', 'symbol', 'numeral' and 'number'</p></dd>
**Kind**: global class

* [ChordLyricsPair](#ChordLyricsPair)
* [new ChordLyricsPair(chords, lyrics)](#new_ChordLyricsPair_new)
* [new ChordLyricsPair(chords, lyrics, annotation)](#new_ChordLyricsPair_new)
* [.chords](#ChordLyricsPair+chords) : <code>string</code>
* [.lyrics](#ChordLyricsPair+lyrics) : <code>string</code>
* [.annotation](#ChordLyricsPair+annotation) : <code>string</code>
* [.isRenderable()](#ChordLyricsPair+isRenderable) ⇒ <code>boolean</code>
* [.clone()](#ChordLyricsPair+clone)[<code>ChordLyricsPair</code>](#ChordLyricsPair)

<a name="new_ChordLyricsPair_new"></a>

### new ChordLyricsPair(chords, lyrics)
### new ChordLyricsPair(chords, lyrics, annotation)
<p>Initialises a ChordLyricsPair</p>


| Param | Type | Default | Description |
| --- | --- | --- | --- |
| chords | <code>string</code> | | <p>The chords</p> |
| lyrics | <code>string</code> | <code>null</code> | <p>The lyrics</p> |
| lyrics | <code>string</code> \| <code>null</code> | <code>null</code> | <p>The lyrics</p> |
| annotation | <code>string</code> \| <code>null</code> | <code>null</code> | <p>The annotation</p> |

<a name="ChordLyricsPair+chords"></a>

Expand All @@ -603,6 +605,12 @@ Possible values are 'solfege', 'symbol', 'numeral' and 'number'</p></dd>
### chordLyricsPair.lyrics : <code>string</code>
<p>The lyrics</p>

**Kind**: instance property of [<code>ChordLyricsPair</code>](#ChordLyricsPair)
<a name="ChordLyricsPair+annotation"></a>

### chordLyricsPair.annotation : <code>string</code>
<p>The annotation</p>

**Kind**: instance property of [<code>ChordLyricsPair</code>](#ChordLyricsPair)
<a name="ChordLyricsPair+isRenderable"></a>

Expand Down
23 changes: 19 additions & 4 deletions src/chord_sheet/chord_lyrics_pair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ class ChordLyricsPair {

lyrics: string | null;

annotation: string | null;

/**
* Initialises a ChordLyricsPair
* @param {string} chords The chords
* @param {string} lyrics The lyrics
* @param {string | null} lyrics The lyrics
* @param {string | null} annotation The annotation
*/
constructor(chords = '', lyrics: string | null = null) {
constructor(chords = '', lyrics: string | null = null, annotation: string | null = null) {
/**
* The chords
* @member
Expand All @@ -28,6 +31,13 @@ class ChordLyricsPair {
* @type {string}
*/
this.lyrics = lyrics || '';

/**
* The annotation
* @member
* @type {string}
*/
this.annotation = annotation || '';
}

/**
Expand All @@ -43,24 +53,29 @@ class ChordLyricsPair {
* @returns {ChordLyricsPair}
*/
clone(): ChordLyricsPair {
return new ChordLyricsPair(this.chords, this.lyrics);
return new ChordLyricsPair(this.chords, this.lyrics, this.annotation);
}

toString(): string {
return `ChordLyricsPair(chords=${this.chords}, lyrics=${this.lyrics})`;
}

set({ chords, lyrics }: { chords?: string, lyrics?: string }): ChordLyricsPair {
set({ chords, lyrics, annotation }: { chords?: string, lyrics?: string, annotation?: string }): ChordLyricsPair {
return new ChordLyricsPair(
chords || this.chords,
lyrics || this.lyrics,
annotation || this.annotation,
);
}

setLyrics(lyrics: string): ChordLyricsPair {
return this.set({ lyrics });
}

setAnnotation(annotation: string): ChordLyricsPair {
return this.set({ annotation });
}

transpose(
delta: number,
key: string | Key | null = null,
Expand Down
7 changes: 6 additions & 1 deletion src/chord_sheet_serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type SerializedChordLyricsPair = {
chord?: SerializedChord | null,
chords: string,
lyrics: string | null,
annotation?: string | null,
};

export type SerializedTag = SerializedTraceInfo & {
Expand Down Expand Up @@ -150,6 +151,7 @@ class ChordSheetSerializer {
chords: chordLyricsPair.chords,
chord: null,
lyrics: chordLyricsPair.lyrics,
annotation: chordLyricsPair.annotation,
};
}

Expand Down Expand Up @@ -232,11 +234,14 @@ class ChordSheetSerializer {
}

parseChordLyricsPair(astComponent: SerializedChordLyricsPair): ChordLyricsPair {
const { chord, chords, lyrics } = astComponent;
const {
chord, chords, lyrics, annotation,
} = astComponent;

return new ChordLyricsPair(
chord ? new Chord(chord).toString() : chords,
lyrics,
annotation,
);
}

Expand Down
4 changes: 4 additions & 0 deletions src/formatter/chord_pro_formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ class ChordProFormatter extends Formatter {
return `[${chordLyricsPair.chords}]`;
}

if (chordLyricsPair.annotation) {
return `[*${chordLyricsPair.annotation}]`;
}

return '';
}

Expand Down
19 changes: 10 additions & 9 deletions src/formatter/chords_over_words_formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Tag from '../chord_sheet/tag';
import { renderChord } from '../helpers';
import { hasTextContents } from '../template_helpers';
import Song from '../chord_sheet/song';
import { hasChordContents, isEmptyString, padLeft } from '../utilities';
import { hasRemarkContents, isEmptyString, padLeft } from '../utilities';
import Paragraph from '../chord_sheet/paragraph';
import Metadata from '../chord_sheet/metadata';
import Line from '../chord_sheet/line';
Expand Down Expand Up @@ -87,30 +87,30 @@ class ChordsOverWordsFormatter extends Formatter {
}

formatLineTop(line: Line, metadata: Metadata): string | null {
if (hasChordContents(line)) {
if (hasRemarkContents(line)) {
return this.formatLineWithFormatter(line, this.formatItemTop, metadata);
}

return null;
}

chordLyricsPairLength(chordLyricsPair: ChordLyricsPair, line: Line): number {
const chords = renderChord(
const content = chordLyricsPair.annotation || renderChord(
chordLyricsPair.chords,
line,
this.song,
{ renderKey: this.configuration.key },
);

const { lyrics } = chordLyricsPair;
const chordsLength = (chords || '').length;
const contentLength = (content || '').length;
const lyricsLength = (lyrics || '').length;

if (chordsLength >= lyricsLength) {
return chordsLength + 1;
if (contentLength >= lyricsLength) {
return contentLength + 1;
}

return Math.max(chordsLength, lyricsLength);
return Math.max(contentLength, lyricsLength);
}

formatItemTop(item: Item, _metadata: Metadata, line: Line): string {
Expand All @@ -119,8 +119,9 @@ class ChordsOverWordsFormatter extends Formatter {
}

if (item instanceof ChordLyricsPair) {
const chords = renderChord(item.chords, line, this.song, { renderKey: this.configuration.key });
return padLeft(chords, this.chordLyricsPairLength(item, line));
const content = item.annotation
|| renderChord(item.chords, line, this.song, { renderKey: this.configuration.key });
return padLeft(content, this.chordLyricsPairLength(item, line));
}

return '';
Expand Down
32 changes: 18 additions & 14 deletions src/formatter/templates/html_div_formatter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HtmlTemplateArgs } from "../html_formatter";
import { renderChord } from '../../helpers';
import {HtmlTemplateArgs} from "../html_formatter";
import {renderChord} from '../../helpers';

import {
each,
Expand Down Expand Up @@ -44,18 +44,22 @@ export default (
${ each(line.items, (item) => `
${ when(isChordLyricsPair(item), () => `
<div class="column">
<div class="chord"${ fontStyleTag(line.chordFont) }>${
renderChord(
item.chords,
line,
song,
{
renderKey: key,
useUnicodeModifier: configuration.useUnicodeModifiers,
normalizeChords: configuration.normalizeChords,
}
)
}</div>
${ when(item.annotation).then(() => `
<div class="annotation"${ fontStyleTag(line.chordFont) }>${item.annotation}</div>
`).else(() => `
<div class="chord"${ fontStyleTag(line.chordFont) }>${
renderChord(
item.chords,
line,
song,
{
renderKey: key,
useUnicodeModifier: configuration.useUnicodeModifiers,
normalizeChords: configuration.normalizeChords,
}
)
}</div>
`) }
<div class="lyrics"${ fontStyleTag(line.textFont) }>${ item.lyrics }</div>
</div>
`) }
Expand Down
34 changes: 19 additions & 15 deletions src/formatter/templates/html_table_formatter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { hasChordContents, isEvaluatable } from '../../utilities';
import { renderChord } from '../../helpers';
import { HtmlTemplateArgs } from '../html_formatter';
import {hasChordContents, isEvaluatable} from '../../utilities';
import {renderChord} from '../../helpers';
import {HtmlTemplateArgs} from '../html_formatter';

import {
each,
Expand Down Expand Up @@ -48,18 +48,22 @@ export default (
<tr>
${ each(line.items, (item) => `
${ when(isChordLyricsPair(item), () => `
<td class="chord"${fontStyleTag(line.chordFont)}>${
renderChord(
item.chords,
line,
song,
{
renderKey: key,
useUnicodeModifier: configuration.useUnicodeModifiers,
normalizeChords: configuration.normalizeChords,
},
)
}</td>
${when(item.annotation).then(() => `
<td class="annotation"${fontStyleTag(line.chordFont)}>${item.annotation}</td>
`).else(() => `
<td class="chord"${fontStyleTag(line.chordFont)}>${
renderChord(
item.chords,
line,
song,
{
renderKey: key,
useUnicodeModifier: configuration.useUnicodeModifiers,
normalizeChords: configuration.normalizeChords,
},
)
}</td>
`)}
`)}
`)}
</tr>
Expand Down
20 changes: 10 additions & 10 deletions src/formatter/text_formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Tag from '../chord_sheet/tag';
import { renderChord } from '../helpers';
import { hasTextContents } from '../template_helpers';
import Song from '../chord_sheet/song';
import { hasChordContents, isEmptyString, padLeft } from '../utilities';
import { hasRemarkContents, isEmptyString, padLeft } from '../utilities';
import Paragraph from '../chord_sheet/paragraph';
import Metadata from '../chord_sheet/metadata';
import Line from '../chord_sheet/line';
Expand Down Expand Up @@ -65,7 +65,7 @@ class TextFormatter extends Formatter {

return parts
.filter((p) => !isEmptyString(p))
.map((part) => (part || '').trimRight())
.map((part) => (part || '').trimEnd())
.join('\n');
}

Expand All @@ -86,24 +86,24 @@ class TextFormatter extends Formatter {
}

formatLineTop(line: Line, metadata: Metadata): string | null {
if (hasChordContents(line)) {
if (hasRemarkContents(line)) {
return this.formatLineWithFormatter(line, this.formatItemTop, metadata);
}

return null;
}

chordLyricsPairLength(chordLyricsPair: ChordLyricsPair, line: Line): number {
const chords = this.renderChords(chordLyricsPair, line);
const content = chordLyricsPair.annotation || this.renderChords(chordLyricsPair, line);
const { lyrics } = chordLyricsPair;
const chordsLength = (chords || '').length;
const contentLength = (content || '').length;
const lyricsLength = (lyrics || '').length;

if (chordsLength >= lyricsLength) {
return chordsLength + 1;
if (contentLength >= lyricsLength) {
return contentLength + 1;
}

return Math.max(chordsLength, lyricsLength);
return Math.max(contentLength, lyricsLength);
}

private renderChords(chordLyricsPair: ChordLyricsPair, line: Line) {
Expand All @@ -126,8 +126,8 @@ class TextFormatter extends Formatter {
}

if (item instanceof ChordLyricsPair) {
const chords = this.renderChords(item, line);
return padLeft(chords, this.chordLyricsPairLength(item, line));
const content = item.annotation || this.renderChords(item, line);
return padLeft(content, this.chordLyricsPairLength(item, line));
}

return '';
Expand Down
17 changes: 16 additions & 1 deletion src/parser/chord_pro_grammar.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Line

Token
= Tag
/ AnnotationLyricsPair
/ ChordLyricsPair
/ MetaTernary
/ lyrics:Lyrics {
Expand All @@ -37,8 +38,17 @@ Comment
return comment;
}

AnnotationLyricsPair
= annotation:Annotation lyrics:$(Lyrics*) space:$(Space*) {
return {
type: 'chordLyricsPair',
annotation: annotation || '',
lyrics: lyrics + (space || ''),
};
}

ChordLyricsPair
= chords: Chord lyrics:$(LyricsChar*) space:$(Space*) {
= chords:Chord lyrics:$(LyricsChar*) space:$(Space*) {
return {
type: 'chordLyricsPair',
chords: chords || '',
Expand All @@ -54,6 +64,11 @@ Lyrics
LyricsCharOrSpace
= (LyricsChar / Space)

Annotation
= !Escape "[*" annotation:ChordChar+ "]" {
return annotation.map(c => c.char || c).join('');
}

Chord
= !Escape "[" chords:ChordChar* "]" {
return chords.map(c => c.char || c).join('');
Expand Down
Loading

0 comments on commit 3a6e238

Please sign in to comment.