chordsong
Version:
ChordSong is a simple text format for the notation of lyrics with guitar chords, and an application that renders them to portable HTML pages.
266 lines • 12.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = exports.chordsRegEx = void 0;
const Chord_1 = require("./Chord");
const Diagram_1 = require("./Diagram");
const SvgDiagram_1 = require("./SvgDiagram");
const regexChordName = '([A-G](?:[b#♭♯غ∆()+-]|[2-9]|1[0-3]|[Mm](?:aj)?|add|s(?:us)?|aug|d(?:im)?)*(?:\\/[A-G][b#♭♯]?)?)';
const regexChordShortName = regexChordName;
const regexChordLongName = regexChordName;
const chordsRegExFields = {
pattern: `(?:(?:\\b|^)(?:${regexChordShortName}\\|)?(?:${regexChordLongName}(?::(\\w+))?(?:\\[([x:/,\\-0-9]+)\\])?)|(?:\\[([x:/,\\-0-9]+)\\]))([\\t ]*)(\\/\\/.*)?`,
flags: 'g'
};
const chordLinesRegExFields = {
pattern: `^[\\t ]*(${chordsRegExFields.pattern})+[\\t ]*$`,
flags: 'gm'
};
exports.chordsRegEx = new RegExp(chordsRegExFields.pattern, chordsRegExFields.flags);
const chordLineRegEx = new RegExp(chordLinesRegExFields.pattern, chordLinesRegExFields.flags);
class Renderer {
constructor(text, defaultRenderMode = 'chordName') {
this.chords = {};
this.directives = [];
this.replacements = [];
this.text = '';
this.text = text;
if (this.text[this.text.length - 1] !== '\n\n')
this.text += '\n\n';
this.chordRenderMode = defaultRenderMode;
this.parseDirectives();
this.parseLyricsComments();
this.parseChords();
}
get title() {
return this.directive('title');
}
get artist() {
return this.directive('artist');
}
directive(name) {
const titleDirective = this.directives.filter(directive => directive.title === name);
return (titleDirective.length > 0) ? titleDirective[0].content : '';
}
parseChords() {
const chordLines = Array.from(this.text.matchAll(chordLineRegEx));
for (let i = 0; i < chordLines.length; i++) {
const chordLine = chordLines[i];
const lineIndex = chordLine.index ?? 0;
const nextLineIndex = lineIndex + chordLine[0].length + 1;
const nextLineLength = this.text.substr(nextLineIndex).indexOf('\n') + 1;
const nextLineIsLyrics = (() => {
if (i === chordLines.length - 1) {
return nextLineIndex < this.text.length;
}
const nextChordLine = chordLines[i + 1];
const nextChordLineIndex = nextChordLine.index ?? 0;
if (nextChordLineIndex !== nextLineIndex && this.text[nextLineIndex] !== '\n') {
return true;
}
return false;
})();
if (nextLineIsLyrics) {
const lineSectionStart = '<section class="chords-line">';
this.replacements.push({ index: lineIndex, length: 0, replacement: lineSectionStart });
const lineSectionStop = '</section>';
this.replacements.push({ index: nextLineIndex + nextLineLength - 1, length: 0, replacement: lineSectionStop });
}
const chordMatches = chordLine[0].matchAll(exports.chordsRegEx);
for (const match of chordMatches) {
const index = lineIndex + (match.index ?? 0);
const width = match[0].length - (match[7]?.length ?? 0);
const chordName = match[1] ?? match[2];
const chordRenderName = match[2];
const chordVariant = match[3];
const chordDiagram = match[4];
const diagramOnly = match[5];
let replacement;
if (diagramOnly !== undefined) {
const diagram = new Diagram_1.Diagram(match[5]);
replacement = this.renderDiagram(diagram, width);
this.replacements.push({ index, length: width, replacement });
}
else {
const chord = chordName in this.chords ? this.chords[chordName] : new Chord_1.Chord(chordName);
if (chordDiagram !== undefined) {
const diagram = new Diagram_1.Diagram(chordDiagram, chordRenderName, chordVariant);
chord.setDiagram(diagram, chordVariant);
this.replacements.push({ index, length: match[0].length, replacement: '' });
}
else {
replacement = this.renderChord(chord, width, chordVariant, this.chordRenderMode);
this.replacements.push({ index, length: width, replacement });
}
this.chords[chordName] = chord;
}
}
}
}
parseDirectives() {
const regEx = /{([\w-]+):\s*([\S\s]+?)}/g;
const matches = this.text.matchAll(regEx);
let index = 0;
let width = 0;
for (const match of matches) {
index = match.index ?? 0;
width = match[0].length;
const title = match[1];
const content = match[2];
let replacement = '';
switch (title) {
case 'columns':
break;
case 'render-mode':
if (content === 'diagram' || content === 'chordName')
this.chordRenderMode = content;
break;
case 'lyrics-font-size':
break;
case 'lyrics-font-color':
break;
case 'chords-font-size':
break;
case 'chords-font-color':
break;
case 'comments-font-size':
break;
case 'comments-font-color':
break;
case 'show-chord-diagrams':
break;
default:
replacement = this.renderDirective({ title, content });
break;
}
this.directives.push({ title, content });
this.replacements.push({ index, length: width, replacement });
}
while (this.text[index + width] === '\n') {
width++;
}
let divOpen = '<lyrics style="';
const columns = Number(this.directive('columns'));
if (isNaN(columns) || columns === 0) {
divOpen += `columns: ${longestLineLength(this.text.substr(index + width)) + 2}ch`;
}
else {
divOpen += `column-count: ${columns}`;
}
if (this.directive('lyrics-font-color') !== '')
divOpen += `; color: ${this.directive('lyrics-font-color')}`;
divOpen += '">';
const divClose = '</lyrics>';
this.replacements.push({ index: index + width, length: 0, replacement: divOpen });
this.replacements.push({ index: this.text.length - 1, length: 0, replacement: divClose });
return index + width;
}
parseLyricsComments() {
const regEx = /\s\/\/(.*)$/gm;
const matches = this.text.matchAll(regEx);
let commentsFontSize = this.directive('comments-font-size');
commentsFontSize = (commentsFontSize !== '') ? `font-size: ${commentsFontSize};` : '';
let commentsFontColor = this.directive('comments-font-color');
commentsFontColor = (commentsFontColor !== '') ? `color: ${commentsFontColor};` : '';
for (const match of matches) {
const index = match.index ?? 0;
const width = match[0].length;
const comment = match[1];
const replacement = `<span class="comment" style="${commentsFontSize} ${commentsFontColor}">${comment}</span>`;
this.replacements.push({ index, length: width, replacement });
}
}
renderDirective(directive) {
return `<div class="directive"><label for="directive-${directive.title}">${directive.title}</label><div id="directive-${directive.title}" class="content">${directive.content}</div></div>`;
}
renderChord(chord, width, chordVariant, renderMode) {
renderMode = renderMode ?? 'chordName';
const diagram = chord.getDiagram(chordVariant);
const svgDiagram = (diagram !== undefined) ? new SvgDiagram_1.SvgDiagram(diagram) : undefined;
if (renderMode === 'diagram' && svgDiagram !== undefined) {
return `<span class="chord-container" style="width: ${width}ch; height: 64px;">${svgDiagram.svg()}</span>`;
}
const text = (chordVariant !== undefined) ? `${chord.name}:${chordVariant}` : chord.name;
let rendered = '';
if (svgDiagram !== undefined) {
rendered += `<span class="diagram" style="left: 100%">${svgDiagram.svg()}</span>`;
}
let chordsFontSize = this.directive('chords-font-size');
chordsFontSize = (chordsFontSize !== '') ? `font-size: ${chordsFontSize};` : '';
let chordsFontColor = this.directive('chords-font-color');
chordsFontColor = (chordsFontColor !== '') ? `color: ${chordsFontColor};` : '';
rendered = `<span class="chord-name" style="${chordsFontSize} ${chordsFontColor}">${text}${rendered}</span>`;
return `<span class="chord-container" style="width: ${width}ch">${rendered}</span>`;
}
renderDiagram(diagram, width, unit = 'ch', showVariant) {
const svgDiagram = new SvgDiagram_1.SvgDiagram(diagram);
return `<span class="chord-container" style="width: ${width}${unit}; height: 64px">${svgDiagram.svg(showVariant)}</span>`;
}
renderDiagrams() {
function getChordRoot(chordName) {
let chordRoot = '';
if (chordName.length > 1 && (chordName[1] === '#' || chordName[1] === 'b')) {
chordRoot = `${chordName[0]}${chordName[1]}`;
}
else {
chordRoot = chordName[0];
}
return chordRoot;
}
let ret = '';
const sortedChordsNames = Object.keys(this.chords).sort((a, b) => {
const aRoot = getChordRoot(a);
const bRoot = getChordRoot(b);
return ((aRoot.length - bRoot.length) !== 0 && a[0] === b[0]) ? aRoot.length - bRoot.length : a.localeCompare(b);
});
let root = '';
for (const chordName of sortedChordsNames) {
const chord = this.chords[chordName];
const chordRoot = getChordRoot(chordName);
if (chordRoot !== root) {
ret += (root === '') ? '<p>' : '</p><p>';
root = chordRoot;
}
const defaultDiagram = chord.getDiagram();
if (defaultDiagram === undefined) {
console.warn(`A diagram for ${chordName} hasn't be provided`);
ret += chordName;
}
else {
ret += this.renderDiagram(chord.getDiagram(), 64, 'px', true);
}
for (const chordVariant in chord.diagrams) {
ret += this.renderDiagram(chord.getDiagram(chordVariant), 64, 'px', true);
}
}
ret += '</p>';
return `<chords><div class="title">Chords in this song:</div>${ret}</chords>`;
}
render() {
let ret = '';
let index = 0;
const replacements = this.replacements.sort((a, b) => a.index - b.index);
for (const replacement of replacements) {
ret += this.text.slice(index, replacement.index);
ret += replacement.replacement;
index = replacement.index + replacement.length;
}
ret += this.text.slice(index);
ret = ret.replace(/(<lyrics[\s\S]*?>)(\n*)/, '$1');
if (this.directive('show-chord-diagrams') === 'true')
ret += this.renderDiagrams();
let fontSizeStyle = '';
if (this.directive('lyrics-font-size') !== '')
fontSizeStyle += ` style="font-size: ${this.directive('lyrics-font-size')}"`;
return `${SvgDiagram_1.SvgDiagram.svgDefs}<chordsong${fontSizeStyle}>${ret}</chordsong>`;
}
}
exports.Renderer = Renderer;
function longestLineLength(str) {
const strArr = str.split('\n');
let max = strArr[0].length;
strArr.forEach(v => {
max = Math.max(max, v.length);
});
return max;
}
//# sourceMappingURL=Renderer.js.map