diff --git a/package.json b/package.json index d0623b2c9..b1531c6ba 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,8 @@ "build": "yarn unibuild", "build:release": "yarn unibuild --force --release", "ci": "yarn install && yarn unibuild lint && yarn unibuild test && yarn build:release", - "debug:chordpro": "tsx script/debug_parser.ts chord_pro --skip-chord-grammar", + "debug:chordpro": "yarn build && tsx script/debug_parser.ts chord_pro --skip-chord-grammar", + "eslint": "node_modules/.bin/eslint", "prepare": "yarn install && yarn build", "prepublishOnly": "yarn install && yarn test && yarn build:release", "readme": "yarn unibuild build readme -f", diff --git a/src/chord_sheet/paragraph.ts b/src/chord_sheet/paragraph.ts index c766c9230..97ab8c74c 100644 --- a/src/chord_sheet/paragraph.ts +++ b/src/chord_sheet/paragraph.ts @@ -73,7 +73,7 @@ class Paragraph { const startTag = this.lines[0].items.find((item: Item) => item instanceof Tag && item.isSectionDelimiter()); if (startTag) { - return (startTag as Tag).value; + return (startTag as Tag).label; } return null; diff --git a/src/chord_sheet/tag.ts b/src/chord_sheet/tag.ts index 5918db1ef..18cded3e0 100644 --- a/src/chord_sheet/tag.ts +++ b/src/chord_sheet/tag.ts @@ -409,9 +409,22 @@ class Tag extends AstComponent { chordDefinition?: ChordDefinition; - constructor(name: string, value: string | null = null, traceInfo: TraceInfo | null = null) { + /** + * The tag attributes. For example, section related tags can have a label: + * `{start_of_verse: label="Verse 1"}` + * @type {Record} + */ + attributes: Record = {}; + + constructor( + name: string, + value: string | null = null, + traceInfo: TraceInfo | null = null, + attributes: Record = {}, + ) { super(traceInfo); this.parseNameValue(name, value); + this.attributes = attributes; } private parseNameValue(name: string, value: string | null): void { @@ -462,6 +475,16 @@ class Tag extends AstComponent { return parsed; } + get label() { + const labelAttribute = this.attributes.label; + + if (labelAttribute && labelAttribute.length > 0) { + return labelAttribute; + } + + return this.value || ''; + } + isSectionDelimiter(): boolean { return this.isSectionStart() || this.isSectionEnd(); } @@ -522,6 +545,14 @@ class Tag extends AstComponent { return this.value.length > 0; } + hasAttributes() { + return Object.keys(this.attributes).length > 0; + } + + hasLabel(): boolean { + return this.label.length > 0; + } + /** * Checks whether the tag is usually rendered inline. It currently only applies to comment tags. * @returns {boolean} @@ -537,7 +568,7 @@ class Tag extends AstComponent { * https://chordpro.org/chordpro/directives-env_bridge/, https://chordpro.org/chordpro/directives-env_tab/ */ hasRenderableLabel(): boolean { - return DIRECTIVES_WITH_RENDERABLE_LABEL.includes(this.name) && this.hasValue(); + return DIRECTIVES_WITH_RENDERABLE_LABEL.includes(this.name) && this.hasLabel(); } /** @@ -553,7 +584,7 @@ class Tag extends AstComponent { * @returns {Tag} The cloned tag */ clone(): Tag { - return new Tag(this._originalName, this.value); + return new Tag(this._originalName, this.value, null, this.attributes); } toString(): string { @@ -561,7 +592,19 @@ class Tag extends AstComponent { } set({ value }: { value: string }): Tag { - return new Tag(this._originalName, value); + return new Tag(this._originalName, value, null, this.attributes); + } + + setAttribute(name: string, value: string) { + return new Tag( + this._originalName, + this.value, + null, + { + ...this.attributes, + [name]: value, + }, + ); } } diff --git a/src/chord_sheet_serializer.ts b/src/chord_sheet_serializer.ts index f5739e0cb..0199cc3c7 100644 --- a/src/chord_sheet_serializer.ts +++ b/src/chord_sheet_serializer.ts @@ -102,6 +102,7 @@ class ChordSheetSerializer { type: TAG, name: tag.originalName, value: tag.value, + attributes: tag.attributes || {}, }; if (tag.chordDefinition) { @@ -216,8 +217,9 @@ class ChordSheetSerializer { value, location: { offset = null, line = null, column = null } = {}, chordDefinition, + attributes, } = astComponent; - const tag = new Tag(name, value, { line, column, offset }); + const tag = new Tag(name, value, { line, column, offset }, attributes); if (chordDefinition) { tag.chordDefinition = new ChordDefinition( diff --git a/src/formatter/chord_pro_formatter.ts b/src/formatter/chord_pro_formatter.ts index 052f73dde..b1205966b 100644 --- a/src/formatter/chord_pro_formatter.ts +++ b/src/formatter/chord_pro_formatter.ts @@ -122,6 +122,10 @@ class ChordProFormatter extends Formatter { } formatTag(tag: Tag): string { + if (tag.hasAttributes()) { + return `{${tag.originalName}: ${this.formatTagAttributes(tag)}}`; + } + if (tag.hasValue()) { return `{${tag.originalName}: ${tag.value}}`; } @@ -129,6 +133,16 @@ class ChordProFormatter extends Formatter { return `{${tag.originalName}}`; } + formatTagAttributes(tag: Tag) { + const keys = Object.keys(tag.attributes); + + if (keys.length === 0) { + return ''; + } + + return keys.map((key) => `${key}="${tag.attributes[key]}"`).join(' '); + } + formatChordLyricsPair(chordLyricsPair: ChordLyricsPair): string { return [ this.formatChordLyricsPairChords(chordLyricsPair), diff --git a/src/formatter/chords_over_words_formatter.ts b/src/formatter/chords_over_words_formatter.ts index dba9929c0..1a724e1b7 100644 --- a/src/formatter/chords_over_words_formatter.ts +++ b/src/formatter/chords_over_words_formatter.ts @@ -100,7 +100,7 @@ class ChordsOverWordsFormatter extends Formatter { formatItemTop(item: Item, _metadata: Metadata, line: Line): string { if (item instanceof Tag && item.isRenderable()) { - return item.value || ''; + return item.label; } if (item instanceof ChordLyricsPair) { @@ -148,7 +148,7 @@ class ChordsOverWordsFormatter extends Formatter { } if (item instanceof Tag && item.isRenderable()) { - return item.value || ''; + return item.label; } if (item instanceof ChordLyricsPair) { diff --git a/src/formatter/templates/html_div_formatter.ts b/src/formatter/templates/html_div_formatter.ts index 6b5134406..df2a746ce 100644 --- a/src/formatter/templates/html_div_formatter.ts +++ b/src/formatter/templates/html_div_formatter.ts @@ -77,7 +77,7 @@ export default ( `) } ${ when(item.hasRenderableLabel(), () => ` -

${ item.value }

+

${ item.label }

`) } `).elseWhen(isEvaluatable(item), () => `
diff --git a/src/formatter/templates/html_table_formatter.ts b/src/formatter/templates/html_table_formatter.ts index 08b9a8337..d04f3b1e5 100644 --- a/src/formatter/templates/html_table_formatter.ts +++ b/src/formatter/templates/html_table_formatter.ts @@ -90,7 +90,7 @@ export default ( `) } ${ when(item.hasRenderableLabel(), () => ` -

${ item.value }

+

${ item.label }

`) } `).elseWhen(isLiteral(item), () => ` ${ item.string } diff --git a/src/formatter/text_formatter.ts b/src/formatter/text_formatter.ts index fd95be798..467407bd8 100644 --- a/src/formatter/text_formatter.ts +++ b/src/formatter/text_formatter.ts @@ -128,7 +128,7 @@ class TextFormatter extends Formatter { formatItemTop(item: Item, _metadata: Metadata, line: Line): string { if (item instanceof Tag && item.isRenderable()) { - return item.value || ''; + return item.label; } if (item instanceof ChordLyricsPair) { @@ -160,7 +160,7 @@ class TextFormatter extends Formatter { formatItemBottom(item: Item, metadata: Metadata, line: Line): string { if (item instanceof Tag && item.isRenderable()) { - return item.value || ''; + return item.label; } if (item instanceof ChordLyricsPair) { diff --git a/src/parser/chord_pro/grammar.pegjs b/src/parser/chord_pro/grammar.pegjs index a60ca7e77..8b0671554 100644 --- a/src/parser/chord_pro/grammar.pegjs +++ b/src/parser/chord_pro/grammar.pegjs @@ -170,25 +170,51 @@ ChordDefinition } Tag - = "{" _ tagName:$(TagName) _ tagColonWithValue: TagColonWithValue? _ "}" { - return { - type: 'tag', - name: tagName, - value: tagColonWithValue, - location: location().start, - }; + = "{" _ tagName:$(TagName) _ tagColonWithValue:TagColonWithValue? "}" { + return helpers.buildTag(tagName, tagColonWithValue, location()); } TagColonWithValue - = ":" _ tagValue:TagValue { - return tagValue.map(c => c.char || c).join(''); + = ":" tagValue:TagValue { + return tagValue; + } + +TagValue + = attributes:TagAttributes { + return { attributes: attributes }; + } + / value:TagSimpleValue { + return { value: value }; + } + +TagAttributes + = attributes:TagAttributeWithLeadingSpace+ { + const obj = {}; + + attributes.forEach((pair) => { + obj[pair[0]] = pair[1]; + }); + + return obj; + } + +TagAttributeWithLeadingSpace + = __ attribute:TagAttribute { + return attribute; + } + +TagAttribute + = name:TagAttributeName _ "=" _ value:TagAttributeValue { + return [name, value]; } TagName = [a-zA-Z-_]+ -TagValue - = TagValueChar* +TagSimpleValue + = _ chars:TagValueChar* { + return chars.map(c => c.char || c).join(''); + } TagValueChar = [^}\\\r\n] @@ -200,3 +226,22 @@ TagValueChar ) { return sequence; } + +TagAttributeName + = $([a-zA-Z-_]+) + +TagAttributeValue + = "\"" value:$(TagAttributeValueChar*) "\"" { + return value; + } + +TagAttributeValueChar + = [^"}] + / Escape + sequence: ( + "\\" { return { type: 'char', char: '\\' }; } + / "}" { return { type: 'char', char: '\x7d' }; } + / "\"" { return { type: 'char', char: '"' }; } + ) { + return sequence; + } diff --git a/src/parser/chord_pro/helpers.ts b/src/parser/chord_pro/helpers.ts index 06236c6a8..7357ffa38 100644 --- a/src/parser/chord_pro/helpers.ts +++ b/src/parser/chord_pro/helpers.ts @@ -29,12 +29,17 @@ export function buildSection(startTag: SerializedTag, endTag: SerializedTag, con ]; } -export function buildTag(name: string, value: string | null, location: FileRange): SerializedTag { +export function buildTag( + name: string, + value: Partial<{ value: string | null, attributes: Record}> | null, + location: FileRange, +): SerializedTag { return { type: 'tag', name, - value: value || '', location: location.start, + value: value?.value || '', + attributes: value?.attributes || {}, }; } diff --git a/src/serialized_types.ts b/src/serialized_types.ts index cb9509a28..efe0080d1 100644 --- a/src/serialized_types.ts +++ b/src/serialized_types.ts @@ -38,6 +38,7 @@ export type SerializedTag = SerializedTraceInfo & { name: string, value: string, chordDefinition?: SerializedChordDefinition, + attributes?: Record, }; export interface SerializedComment { diff --git a/test/chord_sheet/song.test.ts b/test/chord_sheet/song.test.ts index 380632866..a9e60704c 100644 --- a/test/chord_sheet/song.test.ts +++ b/test/chord_sheet/song.test.ts @@ -153,7 +153,13 @@ describe('Song', () => { } if (item instanceof Tag) { - return item.set({ value: `${item.value} changed` }); + let changedTag = item.set({ value: `${item.value} changed` }); + + if (item.attributes.label) { + changedTag = changedTag.setAttribute('label', `${item.attributes.label} changed`); + } + + return changedTag; } return item; @@ -212,7 +218,7 @@ describe('Song', () => { describe('#mapItems', () => { it('changes the symbol song', () => { const song = exampleSongSymbol.clone(); - expect(song.paragraphs.map((p) => p.lines.length)).toEqual([0, 1, 3, 2, 3, 3, 3, 2, 3]); + expect(song.paragraphs.map((p) => p.lines.length)).toEqual([0, 1, 3, 2, 2, 3, 3, 3, 2, 3]); const changedSong = song.mapItems((item) => { if (item instanceof ChordLyricsPair) { @@ -223,7 +229,13 @@ describe('Song', () => { } if (item instanceof Tag) { - return item.set({ value: `${item.value} changed` }); + let changedTag = item.set({ value: `${item.value} changed` }); + + if (item.attributes.label) { + changedTag = changedTag.setAttribute('label', `${item.attributes.label} changed`); + } + + return changedTag; } return item; @@ -243,7 +255,7 @@ describe('Song', () => { it('changes the solfege song', () => { const song = exampleSongSolfege.clone(); - expect(song.paragraphs.map((p) => p.lines.length)).toEqual([0, 1, 3, 2, 3, 3, 3, 2, 3]); + expect(song.paragraphs.map((p) => p.lines.length)).toEqual([0, 1, 3, 2, 2, 3, 3, 3, 2, 3]); const changedSong = song.mapItems((item) => { if (item instanceof ChordLyricsPair) { diff --git a/test/fixtures/changed_song.ts b/test/fixtures/changed_song.ts index ad73515ba..d14290f6c 100644 --- a/test/fixtures/changed_song.ts +++ b/test/fixtures/changed_song.ts @@ -10,6 +10,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'title', value: 'Let it be changed', + attributes: {}, }, ], }, @@ -20,6 +21,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'subtitle', value: 'ChordSheetJS example version changed', + attributes: {}, }, ], }, @@ -30,6 +32,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'key', value: 'C changed', + attributes: {}, }, ], }, @@ -40,6 +43,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'x_some_setting', value: 'changed', + attributes: {}, }, ], }, @@ -50,6 +54,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'composer', value: 'John Lennon changed', + attributes: {}, }, ], }, @@ -60,6 +65,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'composer', value: 'Paul McCartney changed', + attributes: {}, }, ], }, @@ -133,6 +139,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'start_of_verse', value: 'Verse 1 changed', + attributes: {}, }, ], }, @@ -193,6 +200,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'transpose', value: '2 changed', + attributes: {}, }, ], }, @@ -264,6 +272,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'end_of_verse', value: 'changed', + attributes: {}, }, ], }, @@ -278,6 +287,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'start_of_chorus', value: 'changed', + attributes: {}, }, ], }, @@ -288,6 +298,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'comment', value: 'Breakdown changed', + attributes: {}, }, ], }, @@ -298,6 +309,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'transpose', value: 'G changed', + attributes: {}, }, ], }, @@ -341,6 +353,66 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'end_of_chorus', value: 'changed', + attributes: {}, + }, + ], + }, + { + type: 'line', + items: [], + }, + { + type: 'line', + items: [ + { + type: 'tag', + name: 'start_of_chorus', + value: 'changed', + attributes: { label: 'Chorus 2 changed' }, + }, + ], + }, + { + type: 'line', + items: [ + { + type: 'chordLyricsPair', + chords: 'D', + lyrics: 'WHISPER WORDS OF ', + chord: null, + annotation: '', + }, + { + type: 'chordLyricsPair', + chords: 'C', + lyrics: 'WISDOM, LET IT ', + chord: null, + annotation: '', + }, + { + type: 'chordLyricsPair', + chords: 'G', + lyrics: 'BE ', + chord: null, + annotation: '', + }, + { + type: 'chordLyricsPair', + chords: 'D', + lyrics: '', + chord: null, + annotation: '', + }, + ], + }, + { + type: 'line', + items: [ + { + type: 'tag', + name: 'end_of_chorus', + value: 'changed', + attributes: {}, }, ], }, @@ -354,7 +426,10 @@ export const changedSongSymbol: SerializedSong = { { type: 'tag', name: 'start_of_tab', - value: 'Tab 1 changed', + value: 'changed', + attributes: { + label: 'Tab 1 changed', + }, }, ], }, @@ -377,6 +452,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'end_of_tab', value: 'changed', + attributes: {}, }, ], }, @@ -391,6 +467,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'start_of_abc', value: 'ABC 1 changed', + attributes: {}, }, ], }, @@ -413,6 +490,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'end_of_abc', value: 'changed', + attributes: {}, }, ], }, @@ -427,6 +505,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'start_of_ly', value: 'LY 1 changed', + attributes: {}, }, ], }, @@ -449,6 +528,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'end_of_ly', value: 'changed', + attributes: {}, }, ], }, @@ -463,6 +543,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'start_of_bridge', value: 'Bridge 1 changed', + attributes: {}, }, ], }, @@ -485,6 +566,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'end_of_bridge', value: 'changed', + attributes: {}, }, ], }, @@ -499,6 +581,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'start_of_grid', value: 'Grid 1 changed', + attributes: {}, }, ], }, @@ -521,6 +604,7 @@ export const changedSongSymbol: SerializedSong = { type: 'tag', name: 'end_of_grid', value: 'changed', + attributes: {}, }, ], }, @@ -537,6 +621,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'title', value: 'Let it be changed', + attributes: {}, }, ], }, @@ -547,6 +632,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'subtitle', value: 'ChordSheetJS example version changed', + attributes: {}, }, ], }, @@ -557,6 +643,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'key', value: 'Do changed', + attributes: {}, }, ], }, @@ -567,6 +654,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'x_some_setting', value: 'changed', + attributes: {}, }, ], }, @@ -577,6 +665,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'composer', value: 'John Lennon changed', + attributes: {}, }, ], }, @@ -587,6 +676,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'composer', value: 'Paul McCartney changed', + attributes: {}, }, ], }, @@ -660,6 +750,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'start_of_verse', value: 'Verse 1 changed', + attributes: {}, }, ], }, @@ -710,6 +801,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'transpose', value: '2 changed', + attributes: {}, }, ], }, @@ -781,6 +873,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'end_of_verse', value: 'changed', + attributes: {}, }, ], }, @@ -795,6 +888,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'start_of_chorus', value: 'changed', + attributes: {}, }, ], }, @@ -805,6 +899,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'comment', value: 'Breakdown changed', + attributes: {}, }, ], }, @@ -815,6 +910,66 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'transpose', value: 'Sol changed', + attributes: {}, + }, + ], + }, + { + type: 'line', + items: [ + { + type: 'chordLyricsPair', + chords: 'Sim', + lyrics: 'WHISPER WORDS OF ', + chord: null, + annotation: '', + }, + { + type: 'chordLyricsPair', + chords: 'Do', + lyrics: 'WISDOM, LET IT ', + chord: null, + annotation: '', + }, + { + type: 'chordLyricsPair', + chords: 'Sol', + lyrics: 'BE ', + chord: null, + annotation: '', + }, + { + type: 'chordLyricsPair', + chords: 'Re', + lyrics: '', + chord: null, + annotation: '', + }, + ], + }, + { + type: 'line', + items: [ + { + type: 'tag', + name: 'end_of_chorus', + value: 'changed', + attributes: {}, + }, + ], + }, + { + type: 'line', + items: [], + }, + { + type: 'line', + items: [ + { + type: 'tag', + name: 'start_of_chorus', + value: 'changed', + attributes: { label: 'Chorus 2' }, }, ], }, @@ -858,6 +1013,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'end_of_chorus', value: 'changed', + attributes: {}, }, ], }, @@ -872,6 +1028,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'start_of_tab', value: 'Tab 1 changed', + attributes: {}, }, ], }, @@ -894,6 +1051,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'end_of_tab', value: 'changed', + attributes: {}, }, ], }, @@ -908,6 +1066,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'start_of_abc', value: 'ABC 1 changed', + attributes: {}, }, ], }, @@ -930,6 +1089,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'end_of_abc', value: 'changed', + attributes: {}, }, ], }, @@ -944,6 +1104,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'start_of_ly', value: 'LY 1 changed', + attributes: {}, }, ], }, @@ -966,6 +1127,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'end_of_ly', value: 'changed', + attributes: {}, }, ], }, @@ -980,6 +1142,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'start_of_bridge', value: 'Bridge 1 changed', + attributes: {}, }, ], }, @@ -1002,6 +1165,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'end_of_bridge', value: 'changed', + attributes: {}, }, ], }, @@ -1016,6 +1180,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'start_of_grid', value: 'Grid 1 changed', + attributes: {}, }, ], }, @@ -1038,6 +1203,7 @@ export const changedSongSolfege: SerializedSong = { type: 'tag', name: 'end_of_grid', value: 'changed', + attributes: {}, }, ], }, diff --git a/test/fixtures/chord_pro_sheet.ts b/test/fixtures/chord_pro_sheet.ts index e1bb8888a..60463b7ab 100644 --- a/test/fixtures/chord_pro_sheet.ts +++ b/test/fixtures/chord_pro_sheet.ts @@ -23,7 +23,11 @@ Let it [Am]be, \\ let it [C/G]be, let it [F]be, let it [C]be [Am]Whisper words of [Bb]wisdom, let it [F]be [C] {end_of_chorus} -{start_of_tab: Tab 1} +{start_of_chorus: label="Chorus 2"} +[C]Whisper words of [Bb]wisdom, let it [F]be [C] +{end_of_chorus} + +{start_of_tab: label="Tab 1"} Tab line 1 Tab line 2 {end_of_tab} @@ -70,6 +74,10 @@ Let it [Lam]be, let it [Do/Sol]be, let it [Fa]be, let it [Do]be [Lam]Whisper words of [Sib]wisdom, let it [Fa]be [Do] {end_of_chorus} +{start_of_chorus: label="Chorus 2"} +[Lam]Whisper words of [Sib]wisdom, let it [Fa]be [Do] +{end_of_chorus} + {start_of_tab: Tab 1} Tab line 1 Tab line 2 diff --git a/test/fixtures/serialized_song.ts b/test/fixtures/serialized_song.ts index b2e6cd200..313cf25bd 100644 --- a/test/fixtures/serialized_song.ts +++ b/test/fixtures/serialized_song.ts @@ -10,6 +10,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'title', value: 'Let it be', + attributes: {}, }, ], }, @@ -20,6 +21,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'subtitle', value: 'ChordSheetJS example version', + attributes: {}, }, ], }, @@ -30,6 +32,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'key', value: 'C', + attributes: {}, }, ], }, @@ -40,6 +43,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'x_some_setting', value: '', + attributes: {}, }, ], }, @@ -50,6 +54,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'composer', value: 'John Lennon', + attributes: {}, }, ], }, @@ -60,6 +65,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'composer', value: 'Paul McCartney', + attributes: {}, }, ], }, @@ -133,6 +139,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'start_of_verse', value: 'Verse 1', + attributes: {}, }, ], }, @@ -193,6 +200,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'transpose', value: '2', + attributes: {}, }, ], }, @@ -264,6 +272,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'end_of_verse', value: '', + attributes: {}, }, ], }, @@ -278,6 +287,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'start_of_chorus', value: '', + attributes: {}, }, ], }, @@ -288,6 +298,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'comment', value: 'Breakdown', + attributes: {}, }, ], }, @@ -298,6 +309,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'transpose', value: 'G', + attributes: {}, }, ], }, @@ -341,6 +353,66 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'end_of_chorus', value: '', + attributes: {}, + }, + ], + }, + { + type: 'line', + items: [], + }, + { + type: 'line', + items: [ + { + type: 'tag', + name: 'start_of_chorus', + value: '', + attributes: { label: 'Chorus 2' }, + }, + ], + }, + { + type: 'line', + items: [ + { + type: 'chordLyricsPair', + chords: 'C', + lyrics: 'Whisper words of ', + chord: null, + annotation: '', + }, + { + type: 'chordLyricsPair', + chords: 'Bb', + lyrics: 'wisdom, let it ', + chord: null, + annotation: '', + }, + { + type: 'chordLyricsPair', + chords: 'F', + lyrics: 'be ', + chord: null, + annotation: '', + }, + { + type: 'chordLyricsPair', + chords: 'C', + lyrics: '', + chord: null, + annotation: '', + }, + ], + }, + { + type: 'line', + items: [ + { + type: 'tag', + name: 'end_of_chorus', + value: '', + attributes: {}, }, ], }, @@ -354,7 +426,8 @@ export const serializedSongSymbol: SerializedSong = { { type: 'tag', name: 'start_of_tab', - value: 'Tab 1', + value: '', + attributes: { label: 'Tab 1' }, }, ], }, @@ -377,6 +450,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'end_of_tab', value: '', + attributes: {}, }, ], }, @@ -391,6 +465,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'start_of_abc', value: 'ABC 1', + attributes: {}, }, ], }, @@ -413,6 +488,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'end_of_abc', value: '', + attributes: {}, }, ], }, @@ -427,6 +503,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'start_of_ly', value: 'LY 1', + attributes: {}, }, ], }, @@ -449,6 +526,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'end_of_ly', value: '', + attributes: {}, }, ], }, @@ -463,6 +541,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'start_of_bridge', value: 'Bridge 1', + attributes: {}, }, ], }, @@ -485,6 +564,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'end_of_bridge', value: '', + attributes: {}, }, ], }, @@ -499,6 +579,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'start_of_grid', value: 'Grid 1', + attributes: {}, }, ], }, @@ -521,6 +602,7 @@ export const serializedSongSymbol: SerializedSong = { type: 'tag', name: 'end_of_grid', value: '', + attributes: {}, }, ], }, @@ -537,6 +619,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'title', value: 'Let it be', + attributes: {}, }, ], }, @@ -547,6 +630,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'subtitle', value: 'ChordSheetJS example version', + attributes: {}, }, ], }, @@ -557,6 +641,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'key', value: 'Do', + attributes: {}, }, ], }, @@ -567,6 +652,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'x_some_setting', value: '', + attributes: {}, }, ], }, @@ -577,6 +663,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'composer', value: 'John Lennon', + attributes: {}, }, ], }, @@ -587,6 +674,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'composer', value: 'Paul McCartney', + attributes: {}, }, ], }, @@ -660,6 +748,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'start_of_verse', value: 'Verse 1', + attributes: {}, }, ], }, @@ -710,6 +799,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'transpose', value: '2', + attributes: {}, }, ], }, @@ -781,6 +871,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'end_of_verse', value: '', + attributes: {}, }, ], }, @@ -795,6 +886,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'start_of_chorus', value: '', + attributes: {}, }, ], }, @@ -805,6 +897,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'comment', value: 'Breakdown', + attributes: {}, }, ], }, @@ -815,6 +908,66 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'transpose', value: 'Sol', + attributes: {}, + }, + ], + }, + { + type: 'line', + items: [ + { + type: 'chordLyricsPair', + chords: 'Lam', + lyrics: 'Whisper words of ', + chord: null, + annotation: '', + }, + { + type: 'chordLyricsPair', + chords: 'Sib', + lyrics: 'wisdom, let it ', + chord: null, + annotation: '', + }, + { + type: 'chordLyricsPair', + chords: 'Fa', + lyrics: 'be ', + chord: null, + annotation: '', + }, + { + type: 'chordLyricsPair', + chords: 'Do', + lyrics: '', + chord: null, + annotation: '', + }, + ], + }, + { + type: 'line', + items: [ + { + type: 'tag', + name: 'end_of_chorus', + value: '', + attributes: {}, + }, + ], + }, + { + type: 'line', + items: [], + }, + { + type: 'line', + items: [ + { + type: 'tag', + name: 'start_of_chorus', + value: '', + attributes: { label: 'Chorus 2' }, }, ], }, @@ -858,6 +1011,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'end_of_chorus', value: '', + attributes: {}, }, ], }, @@ -872,6 +1026,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'start_of_tab', value: 'Tab 1', + attributes: {}, }, ], }, @@ -894,6 +1049,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'end_of_tab', value: '', + attributes: {}, }, ], }, @@ -908,6 +1064,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'start_of_abc', value: 'ABC 1', + attributes: {}, }, ], }, @@ -930,6 +1087,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'end_of_abc', value: '', + attributes: {}, }, ], }, @@ -944,6 +1102,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'start_of_ly', value: 'LY 1', + attributes: {}, }, ], }, @@ -966,6 +1125,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'end_of_ly', value: '', + attributes: {}, }, ], }, @@ -980,6 +1140,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'start_of_bridge', value: 'Bridge 1', + attributes: {}, }, ], }, @@ -1002,6 +1163,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'end_of_bridge', value: '', + attributes: {}, }, ], }, @@ -1016,6 +1178,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'start_of_grid', value: 'Grid 1', + attributes: {}, }, ], }, @@ -1038,6 +1201,7 @@ export const serializedSongSolfege: SerializedSong = { type: 'tag', name: 'end_of_grid', value: '', + attributes: {}, }, ], }, diff --git a/test/fixtures/song.ts b/test/fixtures/song.ts index 53c5d4229..1348d039b 100644 --- a/test/fixtures/song.ts +++ b/test/fixtures/song.ts @@ -62,17 +62,26 @@ export const exampleSongSymbol = createSongFromAst([ ], [tag('end_of_chorus')], [], - ...section('tab', 'Tab 1', 'Tab line 1\nTab line 2'), + [tag('start_of_chorus', '', { label: 'Chorus 2' })], + [ + chordLyricsPair('C', 'Whisper words of '), + chordLyricsPair('Bb', 'wisdom, let it '), + chordLyricsPair('F', 'be '), + chordLyricsPair('C', ''), + ], + [tag('end_of_chorus')], [], - ...section('abc', 'ABC 1', 'ABC line 1\nABC line 2'), + ...section('tab', '', { label: 'Tab 1' }, 'Tab line 1\nTab line 2'), [], - ...section('ly', 'LY 1', 'LY line 1\nLY line 2'), + ...section('abc', 'ABC 1', {}, 'ABC line 1\nABC line 2'), + [], + ...section('ly', 'LY 1', {}, 'LY line 1\nLY line 2'), [], [tag('start_of_bridge', 'Bridge 1')], [chordLyricsPair('', 'Bridge line')], [tag('end_of_bridge')], [], - ...section('grid', 'Grid 1', 'Grid line 1\nGrid line 2'), + ...section('grid', 'Grid 1', {}, 'Grid line 1\nGrid line 2'), ]); export const exampleSongSolfege = createSongFromAst([ @@ -132,15 +141,24 @@ export const exampleSongSolfege = createSongFromAst([ ], [tag('end_of_chorus')], [], - ...section('tab', 'Tab 1', 'Tab line 1\nTab line 2'), + [tag('start_of_chorus', '', { label: 'Chorus 2' })], + [ + chordLyricsPair('Lam', 'Whisper words of '), + chordLyricsPair('Sib', 'wisdom, let it '), + chordLyricsPair('Fa', 'be '), + chordLyricsPair('Do', ''), + ], + [tag('end_of_chorus')], + [], + ...section('tab', 'Tab 1', {}, 'Tab line 1\nTab line 2'), [], - ...section('abc', 'ABC 1', 'ABC line 1\nABC line 2'), + ...section('abc', 'ABC 1', {}, 'ABC line 1\nABC line 2'), [], - ...section('ly', 'LY 1', 'LY line 1\nLY line 2'), + ...section('ly', 'LY 1', {}, 'LY line 1\nLY line 2'), [], [tag('start_of_bridge', 'Bridge 1')], [chordLyricsPair('', 'Bridge line')], [tag('end_of_bridge')], [], - ...section('grid', 'Grid 1', 'Grid line 1\nGrid line 2'), + ...section('grid', 'Grid 1', {}, 'Grid line 1\nGrid line 2'), ]); diff --git a/test/formatter/chords_over_words_formatter.test.ts b/test/formatter/chords_over_words_formatter.test.ts index dbaff8790..f1c7caab5 100644 --- a/test/formatter/chords_over_words_formatter.test.ts +++ b/test/formatter/chords_over_words_formatter.test.ts @@ -38,6 +38,10 @@ describe('ChordsOverWordsFormatter', () => { Em F C G Whisper words of wisdom, let it be + Chorus 2 + G F C G + Whisper words of wisdom, let it be + Tab 1 Tab line 1 Tab line 2 @@ -82,6 +86,10 @@ Breakdown Mim Fa Do Sol Whisper words of wisdom, let it be +Chorus 2 +Mim Fa Do Sol +Whisper words of wisdom, let it be + Tab 1 Tab line 1 Tab line 2 @@ -134,7 +142,7 @@ Grid line 2`; describe(`for ${type}`, () => { it('uses a configured delegate', () => { const song = createSongFromAst([ - ...section(type as ContentType, `${type} section`, `${type} line 1\n${type} line 2`), + ...section(type as ContentType, `${type} section`, {}, `${type} line 1\n${type} line 2`), ]); const configuration = new Configuration({ @@ -154,7 +162,7 @@ Grid line 2`; it('defaults to the default delegate', () => { const song = createSongFromAst([ - ...section(type as ContentType, `${type} section`, `${type} line 1\n${type} line 2`), + ...section(type as ContentType, `${type} section`, {}, `${type} line 1\n${type} line 2`), ]); const configuration = new Configuration(); diff --git a/test/formatter/html_div_formatter.test.ts b/test/formatter/html_div_formatter.test.ts index db93a3d3f..dba9d5eea 100644 --- a/test/formatter/html_div_formatter.test.ts +++ b/test/formatter/html_div_formatter.test.ts @@ -99,6 +99,7 @@ describe('HtmlDivFormatter', () => {
+
Breakdown
@@ -123,6 +124,30 @@ describe('HtmlDivFormatter', () => {
+
+
+

Chorus 2

+
+
+
+
G
+
Whisper words of
+
+
+
F
+
wisdom, let it
+
+
+
C
+
be
+
+
+
G
+
+
+
+
+

Tab 1

@@ -258,6 +283,7 @@ describe('HtmlDivFormatter', () => {
+
Breakdown
@@ -282,6 +308,30 @@ describe('HtmlDivFormatter', () => {
+
+
+

Chorus 2

+
+
+
+
Mim
+
Whisper words of
+
+
+
Fa
+
wisdom, let it
+
+
+
Do
+
be
+
+
+
Sol
+
+
+
+
+

Tab 1

@@ -597,6 +647,7 @@ describe('HtmlDivFormatter', () => {
+
Breakdown
@@ -621,6 +672,30 @@ describe('HtmlDivFormatter', () => {
+
+
+

Chorus 2

+
+
+
+
Bb
+
Whisper words of
+
+
+
Ab
+
wisdom, let it
+
+
+
Eb
+
be
+
+
+
Bb
+
+
+
+
+

Tab 1

@@ -757,6 +832,7 @@ describe('HtmlDivFormatter', () => {
+
Breakdown
@@ -781,6 +857,30 @@ describe('HtmlDivFormatter', () => {
+
+
+

Chorus 2

+
+
+
+
Solm
+
Whisper words of
+
+
+
Lab
+
wisdom, let it
+
+
+
Mib
+
be
+
+
+
Sib
+
+
+
+
+

Tab 1

@@ -904,7 +1004,7 @@ describe('HtmlDivFormatter', () => { it('does not render empty section labels', () => { const song = createSongFromAst([ - ...section('tab', '', 'Line 1\nLine 2'), + ...section('tab', '', {}, 'Line 1\nLine 2'), ]); const expectedOutput = html` @@ -928,7 +1028,7 @@ describe('HtmlDivFormatter', () => { describe(`for ${type}`, () => { it('uses a configured delegate', () => { const song = createSongFromAst([ - ...section(type as ContentType, `${type} section`, `${type} line 1\n${type} line 2`), + ...section(type as ContentType, `${type} section`, {}, `${type} line 1\n${type} line 2`), ]); const configuration = new Configuration({ @@ -956,7 +1056,7 @@ describe('HtmlDivFormatter', () => { it('defaults to the default delegate', () => { const song = createSongFromAst([ - ...section(type as ContentType, `${type} section`, `${type} line 1\n${type} line 2`), + ...section(type as ContentType, `${type} section`, {}, `${type} line 1\n${type} line 2`), ]); const configuration = new Configuration(); diff --git a/test/formatter/html_table_formatter.test.ts b/test/formatter/html_table_formatter.test.ts index ba689375e..2a06d06a7 100644 --- a/test/formatter/html_table_formatter.test.ts +++ b/test/formatter/html_table_formatter.test.ts @@ -77,6 +77,7 @@ describe('HtmlTableFormatter', () => {
+
@@ -99,6 +100,30 @@ describe('HtmlTableFormatter', () => {
+
+ + + + +
+

Chorus 2

+
+ + + + + + + + + + + + + +
GFCG
Whisper words of wisdom, let it be
+
+
@@ -224,6 +249,7 @@ describe('HtmlTableFormatter', () => {
+
@@ -246,6 +272,30 @@ describe('HtmlTableFormatter', () => {
+
+ + + + +
+

Chorus 2

+
+ + + + + + + + + + + + + +
MimFaDoSol
Whisper words of wisdom, let it be
+
+
@@ -565,6 +615,7 @@ describe('HtmlTableFormatter', () => {
+
@@ -587,6 +638,30 @@ describe('HtmlTableFormatter', () => {
+
+ + + + +
+

Chorus 2

+
+ + + + + + + + + + + + + +
BbAbEbBb
Whisper words of wisdom, let it be
+
+
@@ -686,7 +761,7 @@ describe('HtmlTableFormatter', () => { it('does not render empty section labels', () => { const song = createSongFromAst([ - ...section('tab', '', 'Line 1\nLine 2'), + ...section('tab', '', {}, 'Line 1\nLine 2'), ]); const expectedOutput = html` @@ -782,7 +857,7 @@ describe('HtmlTableFormatter', () => { describe(`for ${type}`, () => { it('uses a configured delegate', () => { const song = createSongFromAst([ - ...section(type as ContentType, `${type} section`, `${type} line 1\n${type} line 2`), + ...section(type as ContentType, `${type} section`, {}, `${type} line 1\n${type} line 2`), ]); const configuration = new Configuration({ @@ -812,7 +887,7 @@ describe('HtmlTableFormatter', () => { it('defaults to the default delegate', () => { const song = createSongFromAst([ - ...section(type as ContentType, `${type} section`, `${type} line 1\n${type} line 2`), + ...section(type as ContentType, `${type} section`, {}, `${type} line 1\n${type} line 2`), ]); const configuration = new Configuration(); diff --git a/test/formatter/text_formatter.test.ts b/test/formatter/text_formatter.test.ts index 31b0ec617..a969b358c 100644 --- a/test/formatter/text_formatter.test.ts +++ b/test/formatter/text_formatter.test.ts @@ -32,6 +32,10 @@ describe('TextFormatter', () => { Em F C G Whisper words of wisdom, let it be + Chorus 2 + G F C G + Whisper words of wisdom, let it be + Tab 1 Tab line 1 Tab line 2 @@ -71,6 +75,10 @@ Breakdown Mim Fa Do Sol Whisper words of wisdom, let it be +Chorus 2 +Mim Fa Do Sol +Whisper words of wisdom, let it be + Tab 1 Tab line 1 Tab line 2 @@ -139,6 +147,10 @@ Let it be, let it be, let it be, let it be`; Breakdown Gm Ab Eb Bb Whisper words of wisdom, let it be + + Chorus 2 + Bb Ab Eb Bb + Whisper words of wisdom, let it be Tab 1 Tab line 1 @@ -194,7 +206,7 @@ Let it be, let it be, let it be, let it be`; describe(`for ${type}`, () => { it('uses a configured delegate', () => { const song = createSongFromAst([ - ...section(type as ContentType, `${type} section`, `${type} line 1\n${type} line 2`), + ...section(type as ContentType, `${type} section`, {}, `${type} line 1\n${type} line 2`), ]); const configuration = new Configuration({ @@ -214,7 +226,7 @@ Let it be, let it be, let it be, let it be`; it('defaults to the default delegate', () => { const song = createSongFromAst([ - ...section(type as ContentType, `${type} section`, `${type} line 1\n${type} line 2`), + ...section(type as ContentType, `${type} section`, {}, `${type} line 1\n${type} line 2`), ]); const configuration = new Configuration(); diff --git a/test/matchers.ts b/test/matchers.ts index b7591d034..34df4b2ad 100644 --- a/test/matchers.ts +++ b/test/matchers.ts @@ -55,6 +55,10 @@ function getObjectType(object) { return `${object} (${typeof object})`; } +function property(value: any) { + return `${print(value)} (${typeof value})`; +} + function toBeClassInstanceWithProperties(received, klass, properties) { const propertyNames = Object.keys(properties); const pass = (!klass || received instanceof klass) && @@ -98,7 +102,9 @@ function toBeClassInstanceWithProperties(received, klass, properties) { but it was a ${actualRepr}`, ); } else if (!valuesEqual(expectedProperty, actualProperty)) { - errors.push(`its ${name} value was: ${print(actualProperty)} vs ${print(expectedProperty)}`); + errors.push( + `its ${name} value was: ${property(actualProperty)} vs ${property(expectedProperty)}`, + ); } }); } diff --git a/test/parser/chord_pro_parser.test.ts b/test/parser/chord_pro_parser.test.ts index 5da90b42b..9a602d668 100644 --- a/test/parser/chord_pro_parser.test.ts +++ b/test/parser/chord_pro_parser.test.ts @@ -128,6 +128,15 @@ describe('ChordProParser', () => { expect(song.metadata.get('x_other_directive')).toEqual('Bar'); }); + it('parses directives with attributes', () => { + const chordSheet = '{start_of_verse: label="Verse 1"}'; + const song = new ChordProParser().parse(chordSheet); + const tag = song.lines[0].items[0] as Tag; + + expect(tag.name).toEqual('start_of_verse'); + expect(tag.attributes).toEqual({ label: 'Verse 1' }); + }); + it('parses meta directives', () => { const chordSheetWithCustomMetaData = ` {meta: one_directive Foo} @@ -626,6 +635,7 @@ Let it [Am]be expect(paragraphs).toHaveLength(1); expect(paragraph.type).toEqual(LILYPOND); expect(lines).toHaveLength(3); + expect(lines[0].items[0]).toBeTag('start_of_ly', 'Intro'); expect(lines[1].items[0]).toBeLiteral('LY line 1'); expect(lines[2].items[0]).toBeLiteral('LY line 2'); diff --git a/test/utilities.ts b/test/utilities.ts index 989c9ea5a..605171390 100644 --- a/test/utilities.ts +++ b/test/utilities.ts @@ -103,8 +103,13 @@ export function createSongFromAst(lines: SerializedItem[][]): Song { return new ChordSheetSerializer().deserialize(serializedSong); } -export function tag(name: string, value = ''): SerializedTag { - return { type: 'tag', name, value }; +export function tag(name: string, value = '', attributes: Record = {}): SerializedTag { + return { + type: 'tag', + name, + value, + attributes, + }; } function splitContent(content: string | string[]): string[] { @@ -114,12 +119,13 @@ function splitContent(content: string | string[]): string[] { export function section( sectionType: ContentType, tagValue: string, + attributes: Record, content: string[] | string, startTag: SerializedTag | null = null, endTag: SerializedTag | null = null, ): SerializedItem[][] { return [ - [startTag || tag(`start_of_${sectionType}`, tagValue)], + [startTag || tag(`start_of_${sectionType}`, tagValue, attributes)], ...splitContent(content).map((line) => [line]), [endTag || tag(`end_of_${sectionType}`)], ];