Skip to content

Commit

Permalink
Add attribute support for tags (#1484)
Browse files Browse the repository at this point in the history
  • Loading branch information
martijnversluis authored Dec 3, 2024
1 parent 7cba110 commit 544893f
Show file tree
Hide file tree
Showing 24 changed files with 751 additions and 55 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/chord_sheet/paragraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
51 changes: 47 additions & 4 deletions src/chord_sheet/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>}
*/
attributes: Record<string, string> = {};

constructor(
name: string,
value: string | null = null,
traceInfo: TraceInfo | null = null,
attributes: Record<string, string> = {},
) {
super(traceInfo);
this.parseNameValue(name, value);
this.attributes = attributes;
}

private parseNameValue(name: string, value: string | null): void {
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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}
Expand All @@ -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();
}

/**
Expand All @@ -553,15 +584,27 @@ 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 {
return `Tag(name=${this.name}, value=${this.value})`;
}

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,
},
);
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/chord_sheet_serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ class ChordSheetSerializer {
type: TAG,
name: tag.originalName,
value: tag.value,
attributes: tag.attributes || {},
};

if (tag.chordDefinition) {
Expand Down Expand Up @@ -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(
Expand Down
14 changes: 14 additions & 0 deletions src/formatter/chord_pro_formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,27 @@ 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}}`;
}

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),
Expand Down
4 changes: 2 additions & 2 deletions src/formatter/chords_over_words_formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -148,7 +148,7 @@ class ChordsOverWordsFormatter extends Formatter {
}

if (item instanceof Tag && item.isRenderable()) {
return item.value || '';
return item.label;
}

if (item instanceof ChordLyricsPair) {
Expand Down
2 changes: 1 addition & 1 deletion src/formatter/templates/html_div_formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export default (
`) }
${ when(item.hasRenderableLabel(), () => `
<h3 class="label">${ item.value }</h3>
<h3 class="label">${ item.label }</h3>
`) }
`).elseWhen(isEvaluatable(item), () => `
<div class="column">
Expand Down
2 changes: 1 addition & 1 deletion src/formatter/templates/html_table_formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default (
`) }
${ when(item.hasRenderableLabel(), () => `
<td><h3 class="label"${ fontStyleTag(line.textFont) }>${ item.value }</h3></td>
<td><h3 class="label"${ fontStyleTag(line.textFont) }>${ item.label }</h3></td>
`) }
`).elseWhen(isLiteral(item), () => `
<td class="literal">${ item.string }</td>
Expand Down
4 changes: 2 additions & 2 deletions src/formatter/text_formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
67 changes: 56 additions & 11 deletions src/parser/chord_pro/grammar.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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;
}
9 changes: 7 additions & 2 deletions src/parser/chord_pro/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>}> | null,
location: FileRange,
): SerializedTag {
return {
type: 'tag',
name,
value: value || '',
location: location.start,
value: value?.value || '',
attributes: value?.attributes || {},
};
}

Expand Down
1 change: 1 addition & 0 deletions src/serialized_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type SerializedTag = SerializedTraceInfo & {
name: string,
value: string,
chordDefinition?: SerializedChordDefinition,
attributes?: Record<string, string>,
};

export interface SerializedComment {
Expand Down
20 changes: 16 additions & 4 deletions test/chord_sheet/song.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand All @@ -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) {
Expand Down
Loading

0 comments on commit 544893f

Please sign in to comment.