martijnversluis/ChordSheetJS

View on GitHub
src/parser/chords_over_words/helpers.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { chopFirstWord } from '../parser_helpers';

import {
  SerializedChord,
  SerializedChordLyricsPair,
  SerializedLine,
  SerializedSoftLineBreak,
} from '../../serialized_types';

type CarriageReturn = '\r';
type LineFeed = '\n';
type CarriageReturnLineFeed = '\r\n';
type NewLine = CarriageReturn | LineFeed | CarriageReturnLineFeed;

type Lyrics = string;
type Chord = { column: number, value: string } & SerializedChord;

interface RhythmSymbol {
  type: 'symbol',
  value: '/' | '|' | '-' | 'x',
  column: number,
}

type DirectionLine = SerializedLine;
type InlineMetadata = SerializedLine;

interface ChordsLine {
  type: 'chordsLine',
  items: (Chord | RhythmSymbol)[]
}

interface LyricsLine {
  type: 'lyricsLine',
  content: Lyrics,
}

type ChordSheetLine = DirectionLine | InlineMetadata | ChordsLine | LyricsLine;

function combineChordSheetLines(
  newLine: NewLine | null,
  lines: ChordSheetLine[],
  trailingLine: ChordSheetLine | null,
): ChordSheetLine[] {
  const hasEmptyLine = newLine && newLine.length > 0;
  const emptyLines = (hasEmptyLine ? [{ type: 'line', items: [] }] : []) as ChordSheetLine[];
  return [...emptyLines, ...lines, trailingLine].filter((x) => x !== null);
}

function applySoftLineBreaks(line: string): (SerializedSoftLineBreak | SerializedChordLyricsPair | null)[] {
  return line
    .split(/\\\s+/)
    .flatMap((lyric, index) => ([
      index === 0 ? null : { type: 'softLineBreak' },
      lyric.length === 0 ? null : { type: 'chordLyricsPair', chords: '', lyrics: lyric },
    ]));
}

type ChordProperties = Omit<Chord, 'type'>;

function chordProperties(chord: Chord): ChordProperties {
  // Disable no-unused-vars until destructuredObjectIgnorePattern is available
  const { type: _type, ...properties } = chord;
  return properties;
}

function constructChordLyricsPairs(
  chords: Chord[],
  lyrics: string,
): (SerializedChordLyricsPair | SerializedSoftLineBreak)[] {
  return chords.map((chord, i) => {
    const nextChord = chords[i + 1];
    const start = chord.column - 1;
    const end = nextChord ? nextChord.column - 1 : lyrics.length;
    const pairLyrics = lyrics.substring(start, end);
    const [firstWord, rest] = chopFirstWord(pairLyrics);
    const chordData = (chord.type === 'chord') ? { chord: chordProperties(chord) } : { chords: chord.value };

    if (rest) {
      return [
        { ...chordData, type: 'chordLyricsPair', lyrics: `${firstWord} ` } as SerializedChordLyricsPair,
        ...applySoftLineBreaks(rest),
      ].filter((x) => x !== null);
    }

    return { ...chordData, type: 'chordLyricsPair', lyrics: firstWord } as SerializedChordLyricsPair;
  }).flat();
}

function pairChordsWithLyrics(chordsLine: ChordsLine, lyricsLine: LyricsLine): SerializedLine {
  const { content: lyrics } = lyricsLine;

  const chords = chordsLine.items as Chord[];
  const chordLyricsPairs = constructChordLyricsPairs(chords, lyrics);
  const firstChord = chords[0];

  if (firstChord && firstChord.column > 1) {
    const firstChordPosition = firstChord.column;

    if (firstChordPosition > 0) {
      chordLyricsPairs.unshift({
        type: 'chordLyricsPair',
        chords: '',
        lyrics: lyrics.substring(0, firstChordPosition - 1),
      });
    }
  }

  return { type: 'line', items: chordLyricsPairs };
}

function lyricsStringToLine(lyrics: string): SerializedLine {
  return {
    type: 'line',
    items: [
      {
        type: 'chordLyricsPair',
        chords: '',
        lyrics,
      },
    ],
  };
}

function chordsLineItemToChordLyricsPair(item: Chord | RhythmSymbol): SerializedChordLyricsPair {
  switch (item.type) {
    case 'chord':
      return {
        type: 'chordLyricsPair', chord: item, chords: '', lyrics: null,
      };
    case 'symbol':
      return { type: 'chordLyricsPair', chords: item.value, lyrics: null };
    default:
      throw new Error(`Unexpected chordsLine item ${item}`);
  }
}

function chordsToLine(chordsLine: ChordsLine): SerializedLine {
  return {
    type: 'line',
    items: chordsLine.items.map((item) => chordsLineItemToChordLyricsPair(item)),
  };
}

function lyricsToLine(lyricsLine: LyricsLine): SerializedLine {
  const { content } = lyricsLine;

  if (content && content.length > 0) {
    return lyricsStringToLine(content);
  }

  return { type: 'line', items: [] };
}

function buildLine(chordSheetLine: ChordSheetLine, nextLine: ChordSheetLine): [SerializedLine, boolean] {
  const { type } = chordSheetLine;

  if (type === 'lyricsLine') {
    return [lyricsToLine(chordSheetLine), false];
  } if (type === 'chordsLine') {
    if (nextLine && nextLine.type === 'lyricsLine' && nextLine.content && nextLine.content.length > 0) {
      return [pairChordsWithLyrics(chordSheetLine, nextLine), true];
    }
    return [chordsToLine(chordSheetLine), false];
  }
  return [chordSheetLine, false];
}

function arrangeChordSheetLines(chordSheetLines: ChordSheetLine[]): SerializedLine[] {
  const arrangedLines: SerializedLine[] = [];
  let lineIndex = 0;
  const lastLineIndex = chordSheetLines.length - 1;

  while (lineIndex <= lastLineIndex) {
    const chordSheetLine = chordSheetLines[lineIndex];
    const nextLine = chordSheetLines[lineIndex + 1];
    const [arrangedLine, skipNextLine] = buildLine(chordSheetLine, nextLine);
    arrangedLines.push(arrangedLine);
    lineIndex += (skipNextLine ? 2 : 1);
  }

  return arrangedLines;
}

export function composeChordSheetContents(
  newLine: NewLine | null,
  lines: ChordSheetLine[],
  trailingLine: ChordSheetLine | null,
) {
  const allLines = combineChordSheetLines(newLine, lines, trailingLine);
  return arrangeChordSheetLines(allLines);
}