Skip to content

Commit

Permalink
✨ Add latex formatter (for songs package)
Browse files Browse the repository at this point in the history
- Only supports a very limited subset of tags so far (artist, title, subtitle, comment, capo)
- Other tags are simply rendered as `\textcomment`
- The result has additional newlines, because some tags are filtered out to construct the first `\beginsong` line. If they were the sole item on their previous line, that line remains as an empty line.
  • Loading branch information
ftes committed Oct 4, 2019
1 parent d41b1b3 commit 95301aa
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/chordsheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import TextFormatter from './formatter/text_formatter';
import HtmlTableFormatter from './formatter/html_table_formatter';
import HtmlDivFormatter from './formatter/html_div_formatter';
import ChordProFormatter from './formatter/chord_pro_formatter';
import LatexFormatter from './formatter/latex_formatter';
import ChordLyricsPair from './chord_sheet/chord_lyrics_pair';
import Line from './chord_sheet/line';
import Song from './chord_sheet/song';
Expand All @@ -16,6 +17,7 @@ export default {
HtmlTableFormatter,
HtmlDivFormatter,
ChordProFormatter,
LatexFormatter,
ChordLyricsPair,
Line,
Song,
Expand Down
114 changes: 114 additions & 0 deletions src/formatter/latex_formatter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import Tag, {
ARTIST,
TITLE,
SUBTITLE,
COMMENT,
CAPO,
START_OF_CHORUS,
END_OF_CHORUS,
START_OF_VERSE,
END_OF_VERSE,
} from '../chord_sheet/tag';
import ChordLyricsPair from '../chord_sheet/chord_lyrics_pair';

const NEW_LINE = '\n';
const flatMap = (arr, fn) => arr.reduce((acc, x) => [...acc, ...fn(x)], []);

/**
* Formats a song into a .tex file for the Latex songs package.
* http://songs.sourceforge.net/docs.html
*/
class LatexFormatter {
isHeaderTag(item) {
return item instanceof Tag && [TITLE, SUBTITLE, ARTIST].indexOf(item.name) !== -1;
}

/**
* Formats a song into .tex file.
* @param {Song} song The song to be formatted
* @returns {string} The .tex file contents as string
*/
format(song) {
return [
this.formatHeaderTags(song),
this.formatOther(song),
'\\endsong',
].join(NEW_LINE);
}

formatHeaderTags(song) {
const headerTags = flatMap(song.lines, line => line.items)
.filter(this.isHeaderTag)
.reduce((tmp, tag) => ({
...tmp,
[tag.name]: tag,
}), {});
const title = headerTags.title && headerTags.title.value;
const subtitle = headerTags.subtitle && headerTags.subtitle.value;
const artist = headerTags.artist ? headerTags.artist.value : '';
const titleString = title + (subtitle ? ` \\\\ ${subtitle}` : '');
return `\\beginsong{${titleString}}[by={${artist}}]`;
}

formatOther(song) {
return song.lines.map(line => this.formatLine(line)).join(NEW_LINE);
}

formatLine(line) {
return line.items.map(item => this.formatItem(item)).join('');
}

formatItem(item) {
if (this.isHeaderTag(item)) {
return '';
} else if (item instanceof Tag) {
return this.formatTag(item);
} else if (item instanceof ChordLyricsPair) {
return this.formatChordLyricsPair(item);
}

return '';
}

formatTag(tag) {
switch (tag.name) {
case COMMENT:
return `\\textcomment{${tag.value}}`;
case CAPO:
return `\\capo{${tag.value}}`;
case START_OF_CHORUS:
return '\\beginchorus';
case END_OF_CHORUS:
return '\\endchorus';
case START_OF_VERSE:
return '\\beginverse';
case END_OF_VERSE:
return '\\endverse';
default:
return tag.hasValue()
? `\\textcomment{${tag.originalName}: ${tag.value}}`
: `\\textcomment{${tag.originalName}}`;
}
}

formatChordLyricsPair(chordLyricsPair) {
return [
this.formatChordLyricsPairChords(chordLyricsPair),
this.formatChordLyricsPairLyrics(chordLyricsPair),
].join('');
}

formatChordLyricsPairChords(chordLyricsPair) {
if (chordLyricsPair.chords) {
return `\\[${chordLyricsPair.chords}]`;
}

return '';
}

formatChordLyricsPairLyrics(chordLyricsPair) {
return chordLyricsPair.lyrics || '';
}
}

export default LatexFormatter;
29 changes: 29 additions & 0 deletions test/formatter/latex_formatter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { expect } from 'chai';

import LatexFormatter from '../../src/formatter/latex_formatter';
import song from '../fixtures/song';

describe('LatexFormatter', () => {
it('formats a song to a .tex file correctly', () => {
const formatter = new LatexFormatter();

const expectedChordSheet = `
\\beginsong{Let it be \\\\ ChordSheetJS example version}[by={}]
\\textcomment{x_some_setting}
\\textcomment{Bridge}
\\beginverse
Let it \\[Am]be, let it \\[C/G]be, let it \\[F]be, let it \\[C]be
\\[C]Whisper words of \\[F]wis\\[G]dom, let it \\[F]be \\[C/E] \\[Dm] \\[C]
\\endverse
\\beginchorus
\\[Am]Whisper words of \\[Bb]wisdom, let it \\[F]be \\[C]
\\endchorus
\\endsong`.substring(1);

expect(formatter.format(song)).to.equal(expectedChordSheet);
});
});

0 comments on commit 95301aa

Please sign in to comment.