UNPKG

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
"use strict"; 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