2019-10-08 05:43:49 +00:00
|
|
|
/**
|
|
|
|
* ChordProPipe
|
|
|
|
*
|
|
|
|
* A pipe for angular 2/4 that translate ChordPro-formatted text into an HTML representation, to be used in conjunction with a set of styles
|
|
|
|
* for proper display.
|
|
|
|
*
|
|
|
|
* If you make improvements, please send them to me for incorporation.
|
|
|
|
*
|
|
|
|
* @author David Quinn-Jacobs (dqj@authentrics.com)
|
|
|
|
* @licence Use this in any way you like, with no constraints.
|
|
|
|
*/
|
|
|
|
import { Pipe, PipeTransform } from '@angular/core';
|
2021-01-03 18:05:10 +00:00
|
|
|
import { MAT_AUTOCOMPLETE_SCROLL_STRATEGY_FACTORY } from '@angular/material/autocomplete';
|
2019-10-08 05:43:49 +00:00
|
|
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
|
|
|
|
|
|
|
@Pipe({ name: 'chordpro' })
|
|
|
|
|
|
|
|
export class ChordProPipe implements PipeTransform {
|
|
|
|
/**
|
|
|
|
* @var chordRegex Expression used to determine if given line contains a chord.
|
|
|
|
* @type {RegExp}
|
|
|
|
*/
|
|
|
|
private chordRegex = /\[([^\]]*)\]/;
|
|
|
|
private readonly MAX_HALF_STEPS = 11;
|
|
|
|
|
|
|
|
constructor(private sanitizer: DomSanitizer) {
|
|
|
|
this.notesSharpNotation['german'] = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'H'];
|
|
|
|
this.notesFlatNotation['german'] = ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'H'];
|
|
|
|
this.notesSharpNotation['english'] = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
|
|
|
this.notesFlatNotation['english'] = ['C', 'Db', 'D', 'Eb', 'Fb', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
|
|
|
|
}
|
|
|
|
|
|
|
|
private keys = [
|
|
|
|
{ name: 'Ab', value: 0 },
|
|
|
|
{ name: 'A', value: 1 },
|
|
|
|
{ name: 'Bb', value: 2 },
|
|
|
|
{ name: 'A#', value: 2 },
|
|
|
|
{ name: 'B', value: 3 },
|
|
|
|
{ name: 'C', value: 4 },
|
|
|
|
{ name: 'C#', value: 5 },
|
|
|
|
{ name: 'Db', value: 5 },
|
|
|
|
{ name: 'D', value: 6 },
|
|
|
|
{ name: 'Eb', value: 7 },
|
|
|
|
{ name: 'D#', value: 7 },
|
|
|
|
{ name: 'E', value: 8 },
|
|
|
|
{ name: 'F', value: 9 },
|
|
|
|
{ name: 'F#', value: 10 },
|
|
|
|
{ name: 'Gb', value: 10 },
|
|
|
|
{ name: 'G', value: 11 },
|
|
|
|
{ name: 'G#', value: 0 }
|
|
|
|
];
|
|
|
|
notesSharpNotation = {};
|
|
|
|
notesFlatNotation = {};
|
|
|
|
|
2022-12-17 07:09:43 +00:00
|
|
|
private fillHtml = `<span class="fill"><span class="fill-inner"></span></span>`;
|
|
|
|
|
2019-10-08 05:43:49 +00:00
|
|
|
decodeHTML(value: string) {
|
|
|
|
const tempElement = document.createElement('div');
|
|
|
|
tempElement.innerHTML = value;
|
|
|
|
return tempElement.innerText;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pipe transformation for ChordPro-formatted song texts.
|
2022-12-21 16:27:47 +00:00
|
|
|
*
|
2019-10-08 05:43:49 +00:00
|
|
|
* @param {string} song
|
|
|
|
* @param {number} nHalfSteps
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
2022-10-19 11:51:03 +00:00
|
|
|
transform(song: string/*, nHalfSteps: number*/): string|SafeHtml {
|
|
|
|
// we hardcode nHalfSteps to 0, since the transposing is not used in OpenLP web
|
|
|
|
const nHalfSteps = 0;
|
2019-10-08 05:43:49 +00:00
|
|
|
try {
|
|
|
|
if (song !== undefined && song) {
|
|
|
|
return this.sanitizer.bypassSecurityTrustHtml(this.parseToHTML(song, nHalfSteps));
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
return song;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (exception) {
|
|
|
|
console.warn('chordpro translation error', exception);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
chordRoot(chord) {
|
|
|
|
let root = '';
|
|
|
|
let ch2 = '';
|
|
|
|
if (chord && chord.length > 0) {
|
|
|
|
root = chord.substr(0, 1);
|
|
|
|
if (chord.length > 1) {
|
|
|
|
ch2 = chord.substr(1, 1);
|
|
|
|
if (ch2 === 'b' || ch2 === '#') {
|
|
|
|
root += ch2;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return root;
|
|
|
|
}
|
|
|
|
|
|
|
|
restOfChord(chord) {
|
|
|
|
let rest = '';
|
|
|
|
const root = this.chordRoot(chord);
|
|
|
|
if (chord.length > root.length) {
|
|
|
|
rest = chord.substr(root.length);
|
|
|
|
}
|
|
|
|
return rest;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Transpose the given chord the given (positive or negative) number of half steps.
|
2022-12-21 16:27:47 +00:00
|
|
|
*
|
2019-10-08 05:43:49 +00:00
|
|
|
* @param {string} chordRoot
|
|
|
|
* @param {number} nHalfSteps
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
transposeChord(chordRoot, nHalfSteps) {
|
|
|
|
let pos = -1;
|
2022-12-21 16:27:47 +00:00
|
|
|
for (const key of this.keys) {
|
|
|
|
if (key.name === chordRoot) {
|
|
|
|
pos = key.value;
|
2019-10-08 05:43:49 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (pos >= 0) {
|
|
|
|
pos += nHalfSteps;
|
|
|
|
if (pos < 0) {
|
|
|
|
pos += this.MAX_HALF_STEPS;
|
|
|
|
}
|
|
|
|
else if (pos > this.MAX_HALF_STEPS) {
|
|
|
|
pos -= this.MAX_HALF_STEPS + 1;
|
|
|
|
}
|
2022-12-21 16:27:47 +00:00
|
|
|
for (const key of this.keys) {
|
|
|
|
if (key.value === pos) {
|
|
|
|
return key.name;
|
2019-10-08 05:43:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return chordRoot;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse a string containing a ChordPro-formatted song, building an array of output HTML lines.
|
|
|
|
*
|
|
|
|
* @param {number} nHalfSteps
|
|
|
|
* @param {string} song
|
|
|
|
*/
|
|
|
|
private parseToHTML(song: string, nHalfSteps = 0): string {
|
|
|
|
// we are currently receiving html, we need to replace that stuff,
|
|
|
|
// becuase it gets messed up when a chord is placed on it..
|
|
|
|
// shouldn't be relevant if we actually get chordpro format
|
|
|
|
song = this.decodeHTML(song);
|
|
|
|
if (!song) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
let chordText = '';
|
2022-12-17 07:09:43 +00:00
|
|
|
if (!song.match(this.chordRegex)) {
|
2019-10-08 05:43:49 +00:00
|
|
|
return `<div class="no-chords">${song}</div>`;
|
|
|
|
}
|
2022-12-17 07:09:43 +00:00
|
|
|
// Processing backwards so we can better identify where chords should overlap lyric letters
|
|
|
|
// or insert a space in lyrics
|
|
|
|
song.split(/\n/).reverse().forEach((row, index) => {
|
|
|
|
|
|
|
|
chordText = `</div>${index > 0 ? '<br>' : ''}` + chordText;
|
|
|
|
const rowParts = row.split(this.chordRegex);
|
|
|
|
let lastPart;
|
|
|
|
|
|
|
|
for (let i = rowParts.length - 1, r = 0, isFirst = true; i >= -1; i--, r++ ) {
|
|
|
|
if (!isFirst) {
|
|
|
|
chordText = this._processChordRow(nHalfSteps, rowParts[i], i, lastPart, r) + chordText;
|
2019-10-08 05:43:49 +00:00
|
|
|
}
|
2022-12-17 07:09:43 +00:00
|
|
|
|
|
|
|
lastPart = rowParts[i];
|
|
|
|
isFirst = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
chordText = '<div class="song-row">' + chordText;
|
|
|
|
});
|
|
|
|
return `<div class="with-chords">${chordText}</div>`;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected _processChordRow(nHalfSteps, part, index, lastPart, reverseIndex) {
|
|
|
|
if (index % 2 !== 0) {
|
|
|
|
if (index > 0) {
|
2019-10-08 05:43:49 +00:00
|
|
|
// chord
|
2022-12-17 07:09:43 +00:00
|
|
|
let chord = part.replace(/[[]]/, '');
|
2019-10-08 05:43:49 +00:00
|
|
|
if (nHalfSteps !== 0) {
|
2022-12-17 07:09:43 +00:00
|
|
|
chord = chord.split('/').map(chordPart => {
|
|
|
|
const chordRoot = this.chordRoot(chordPart);
|
|
|
|
const newRoot = this.transposeChord(chordRoot, nHalfSteps);
|
|
|
|
return newRoot + this.restOfChord(chordPart);
|
2019-10-08 05:43:49 +00:00
|
|
|
}).join('/');
|
|
|
|
}
|
|
|
|
|
|
|
|
// use proper symbols
|
2022-12-17 07:09:43 +00:00
|
|
|
chord = chord.replace(/b/g, '♭');
|
|
|
|
chord = chord.replace(/#/g, '♯');
|
|
|
|
const textFirstLetter = `${lastPart.substring(0, 1)}`;
|
|
|
|
const textRest = lastPart.substring(1);
|
|
|
|
const isChordMusicKey = chord.startsWith('=');
|
|
|
|
const chordLength = chord.length;
|
|
|
|
const isFirstChordAfterRealWord = (lastPart.length && (reverseIndex === 1));
|
|
|
|
const shouldOverlapChord = (!isChordMusicKey) && (isFirstChordAfterRealWord || (lastPart.length > chord.length));
|
|
|
|
const isChordOnly = !lastPart.trim().length;
|
|
|
|
|
|
|
|
const chordClass = (shouldOverlapChord && 'overlap-chord' || '') + (isChordOnly && ' chord-only' || '');
|
|
|
|
|
|
|
|
const textLength = 1 + textRest.length;
|
|
|
|
const fillHtmlNeededLength = (!shouldOverlapChord && (textLength < chordLength)) ? chordLength - 1 : (chordLength - 1);
|
|
|
|
let fillHtml = !shouldOverlapChord && !isChordMusicKey ? this._makeFillHtml(fillHtmlNeededLength || 1) : '';
|
|
|
|
let fillHtmlLength = fillHtml?.length ?? 0;
|
|
|
|
const finalTextLength = 1 + textRest.length + fillHtmlLength;
|
|
|
|
const needExtraFill = (!shouldOverlapChord && (finalTextLength <= chordLength));
|
|
|
|
|
|
|
|
if (needExtraFill) {
|
|
|
|
fillHtml = this._makeFillHtml(fillHtmlLength + 1);
|
|
|
|
fillHtmlLength++;
|
|
|
|
chord += ' ';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!lastPart || (textFirstLetter === ' ')) {
|
|
|
|
fillHtml = ' '.repeat(fillHtmlLength);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((fillHtmlLength === 1 && chordLength < 2)) {
|
|
|
|
// To match text separator
|
|
|
|
chord += ' ';
|
|
|
|
}
|
|
|
|
|
2022-12-21 16:27:47 +00:00
|
|
|
return `<span data-chord="${chord}" class="${chordClass}">` +
|
|
|
|
`<span class="chord">${chord}</span>` +
|
|
|
|
`<span class="text ${fillHtml ? 'with-fill' : ''}">` +
|
|
|
|
`<span class="first-letter">${textFirstLetter}</span>` +
|
|
|
|
`${fillHtml}` +
|
|
|
|
`</span>` +
|
|
|
|
`</span>${textRest}`;
|
2022-12-17 07:09:43 +00:00
|
|
|
} else {
|
|
|
|
return `${lastPart}`;
|
2019-10-08 05:43:49 +00:00
|
|
|
}
|
2022-12-17 07:09:43 +00:00
|
|
|
}
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
protected _makeFillHtml(length) {
|
|
|
|
if (length >= 3) {
|
|
|
|
let text = '';
|
|
|
|
const middle = Math.floor(length / 2);
|
|
|
|
|
|
|
|
for (let i = 0; i < length; i++) {
|
|
|
|
text += (i === middle) ? '\u2014' : ' ';
|
|
|
|
}
|
|
|
|
|
|
|
|
return text;
|
|
|
|
} else {
|
|
|
|
return '\u00B7' + (length === 2 ? ' ' : '');
|
|
|
|
}
|
2019-10-08 05:43:49 +00:00
|
|
|
}
|
|
|
|
}
|