diff --git a/src/pco_chordspkg/ast.nim b/src/pco_chordspkg/ast.nim index b22446b..34d6a31 100644 --- a/src/pco_chordspkg/ast.nim +++ b/src/pco_chordspkg/ast.nim @@ -24,6 +24,11 @@ type ChordChartPitch* = enum Af, A, Bf, B, C, Df, D, Ef, E, F, Fs, G + ChordChartChord* = object + original*: string + rootPitch*: ChordChartPitch + flavor*: string + ChordChartNode* = ref object case kind: ChordChartNodeKind of ccnkSection: @@ -34,7 +39,7 @@ type of ccnkWord: spaceBefore*: bool spaceAfter*: bool - chord*: string + chord*: ChordChartChord word*: string of ccnkNote: note*: string @@ -81,6 +86,11 @@ func `$`*(pitch: ChordChartPitch): string = of Fs: "F#" of G: "G" +func `+`*(pitch: ChordChartPitch, steps: int): ChordChartPitch = + cast[ChordChartPitch]((ord(pitch) + steps) mod 12) + +func `-`*(a, b: ChordChartPitch): int = ord(a) - ord(b) + func dump*(m: ChordChartMetadata, indent = ""): string = return indent & "Metadata\p" & join(m.pairs --> map(indent & " " & it.key & ": " & it.value), "\p") @@ -111,7 +121,10 @@ func dump*(ccn: ChordChartNode, indent = ""): string = of ccnkWord: result = "" if ccn.spaceBefore: result &= " " - if ccn.chord.len > 0: result &= "[" & ccn.chord & "]" + if ccn.chord.flavor.len > 0: + result &= "[" & $ccn.chord.rootPitch & "_" & ccn.chord.flavor & "]" + elif ccn.chord.original.len > 0: result &= "[" & ccn.chord.original & "]" + if ccn.chord.original.len > 0: result &= "[" & ccn.chord.original & "]" result &= ccn.word if ccn.spaceAfter: result &= " " @@ -154,20 +167,46 @@ template addToCurSection(n: ChordChartNode): untyped = func parsePitch*(ctx: ParserContext, keyValue: string): ChordChartPitch = case keyValue.strip.toLower - of "gs", "gis", "g#", "ab", "af", "aes": return ChordChartPitch.Af - of "a": return ChordChartPitch.A - of "as", "ais", "a#", "bf", "bb", "bes": return ChordChartPitch.Bf - of "b", "ces", "cf": return ChordChartPitch.B - of "bs", "bis", "b#", "c": return ChordChartPitch.C - of "cs", "cis", "c#", "df", "db", "des": return ChordChartPitch.Df - of "d": return ChordChartPitch.D - of "ds", "dis", "d#", "ef", "eb", "ees": return ChordChartPitch.Ef - of "e", "fes", "ff": return ChordChartPitch.E - of "es", "eis", "e#", "f": return ChordChartPitch.F - of "fs", "fis", "f#", "gf", "gb", "ges": return ChordChartPitch.Fs - of "g": return ChordChartPitch.G + of "gs", "gis", "g#", "ab", "a♭", "af", "aes": return ChordChartPitch.Af + of "g𝄪", "a", "a♮", "b𝄫": return ChordChartPitch.A + of "as", "ais", "a#", "bf", "bb", "b♭", "bes", "c𝄫": return ChordChartPitch.Bf + of "a𝄪", "b", "ces", "cf": return ChordChartPitch.B + of "bs", "bis", "b#", "c", "d𝄫": return ChordChartPitch.C + of "b𝄪", "cs", "cis", "c#", "df", "db", "des": return ChordChartPitch.Df + of "c𝄪", "d", "e𝄫": return ChordChartPitch.D + of "ds", "dis", "d#", "ef", "eb", "ees", "f𝄫": return ChordChartPitch.Ef + of "d𝄪", "e", "fes", "ff": return ChordChartPitch.E + of "es", "eis", "e#", "f", "g𝄫": return ChordChartPitch.F + of "e𝄪", "fs", "fis", "f#", "gf", "gb", "ges": return ChordChartPitch.Fs + of "f𝄪", "g", "a𝄫": return ChordChartPitch.G else: raise ctx.makeError(keyValue.strip & " is not a recognized key.") +let CHORD_REGEX = + "([[:upper:]][b#♭♮𝄫𝄪]?)" & # chord root + "([mM1-9#b♭♮𝄫𝄪Δ+oøoø]|min|maj|aug|dim|sus|6\\/9)*" & # chord flavor/type + "(\\/[[:upper:]])?" # optional bass +let CHORD_PAT = re(CHORD_REGEX) + +proc parseChord*(ctx: ParserContext, chordValue: string): ChordChartChord = + let m = chordValue.match(CHORD_PAT) + if m.isNone: + return ChordChartChord( + original: chordValue, + flavor: "") + else: + let flavor = + if m.get.captures.contains(1): m.get.captures[1] + else: "" + + let bass = + if m.get.captures.contains(2): m.get.captures[2] + else: "" + + return ChordChartChord( + original: chordValue, + rootPitch: ctx.parsePitch(m.get.captures[0]), + flavor: flavor & bass) + let METADATA_LINE_PAT = re"^([^:]+):(.*)$" let METADATA_END_PAT = re"^-+$" proc parseMetadata(ctx: var ParserContext): ChordChartMetadata = @@ -203,11 +242,6 @@ proc parseMetadata(ctx: var ParserContext): ChordChartMetadata = key: ctx.parsePitch(songKey), optionalProps: optProps) -let NAKED_CHORD_REGEX = - "[[:upper:]][b#♭♮𝄫𝄪]?" & # chord root - "([mM1-9#b♭♮𝄫𝄪Δ+oøoø]|min|maj|aug|dim|sus|6\\/9)*" & # chord flavor/type - "(\\/[[:upper:]])?" # optional bass - const KNOWN_SECTION_NAMES = [ "chorus", "verse", "bridge", "breakdown", "vamp", "intstrumental", "interlude", "intro", "outtro", "ending", "end", "tag" @@ -233,19 +267,19 @@ let SPACE_PAT = re"\s" let CHORD_IN_LYRICS_PAT = re"(\w+)(\[.+)" let CHORD_AND_LYRICS_PAT = re"^\[([^\]]+)\]([^\s\[]+)(.*)$" let BRACED_CHORD_PAT = re"^\[([^\]]+)\]$" -let NAKED_CHORDS_ONLY_PAT = re("^(" & NAKED_CHORD_REGEX & "\\s*\\|*\\s*)+$") +let NAKED_CHORDS_ONLY_PAT = re("^(" & CHORD_REGEX & "\\s*\\|*\\s*)+$") -proc parseLineParts(parts: varargs[string]): seq[ChordChartNode] = +proc parseLineParts(ctx: ParserContext, parts: varargs[string]): seq[ChordChartNode] = result = @[] for p in parts: var m = p.match(SPACE_PAT) if m.isSome: - result &= parseLineParts(p.splitWhitespace) + result &= ctx.parseLineParts(p.splitWhitespace) continue m = p.match(CHORD_IN_LYRICS_PAT) if m.isSome: - result &= parseLineParts(m.get.captures[0], m.get.captures[1]) + result &= ctx.parseLineParts(m.get.captures[0], m.get.captures[1]) continue m = p.match(CHORD_AND_LYRICS_PAT) @@ -254,9 +288,9 @@ proc parseLineParts(parts: varargs[string]): seq[ChordChartNode] = kind: ccnkWord, spaceAfter: true, #FIXME spaceBefore: true, #FIXME - chord: m.get.captures[0], + chord: ctx.parseChord(m.get.captures[0]), word: m.get.captures[1])) - result &= parseLineParts(m.get.captures[2]) + result &= ctx.parseLineParts(m.get.captures[2]) continue m = p.match(BRACED_CHORD_PAT) @@ -265,7 +299,7 @@ proc parseLineParts(parts: varargs[string]): seq[ChordChartNode] = kind: ccnkWord, spaceAfter: false, #FIXME spaceBefore: false, #FIXME - chord: m.get.captures[0], + chord: ctx.parseChord(m.get.captures[0]), word: "")) continue @@ -274,10 +308,10 @@ proc parseLineParts(parts: varargs[string]): seq[ChordChartNode] = kind: ccnkWord, spaceAfter: true, #FIXME spaceBefore: true, #FIXME - chord: "", + chord: ChordChartChord(original: "", flavor: ""), word: p)) -proc parseLine(line: string): ChordChartNode = +proc parseLine(ctx: ParserContext, line: string): ChordChartNode = result = ChordChartNode(kind: ccnkLine) let m = line.match(NAKED_CHORDS_ONLY_PAT) @@ -287,10 +321,10 @@ proc parseLine(line: string): ChordChartNode = kind: ccnkWord, spaceAfter: false, #FIXME spaceBefore: false, #FIXME - chord: it.strip, + chord: ctx.parseChord(it.strip), word: "")) - else: result.line = parseLineParts(line.splitWhitespace) + else: result.line = ctx.parseLineParts(line.splitWhitespace) proc readNote(ctx: var ParserContext, endPat: Regex): string = let startLineNum = ctx.curLineNum @@ -331,31 +365,6 @@ proc parseBody(ctx: var ParserContext): seq[ChordChartNode] = inclInLyrics: m.get.match .len < 2, note: ctx.readNote(NOTE_END_PAT))) - m = line.match(SECTION_LINE_PAT) - if m.isSome: - let captures = m.get.captures.toSeq - ctx.curSection = ChordChartNode( - kind: ccnkSection, - sectionName: if captures[0].isSome: captures[0].get.strip - else: raise ctx.makeError("unknown error parsing section header: " & line), - sectionContents: @[]) - result.add(ctx.curSection) - if captures[3].isSome: - ctx.curSection.sectionContents &= parseLineParts(captures[3].get) - continue - - # FIXME: as implemented, this will not allow column breaks within a section - m = line.match(COL_BREAK_PAT) - if m.isSome: - result.add(ChordChartNode(kind: ccnkColBreak)) - continue - - # FIXME: as implemented, this will not allow page breaks within a section - m = line.match(PAGE_BREAK_PAT) - if m.isSome: - result.add(ChordChartNode(kind: ccnkPageBreak)) - continue - m = line.match(TRANSPOSE_PAT) if m.isSome: addToCurSection(ChordChartNode( @@ -375,8 +384,33 @@ proc parseBody(ctx: var ParserContext): seq[ChordChartNode] = newKey: cast[ChordChartPitch](newKeyInt))) continue + m = line.match(SECTION_LINE_PAT) + if m.isSome: + let captures = m.get.captures.toSeq + ctx.curSection = ChordChartNode( + kind: ccnkSection, + sectionName: if captures[0].isSome: captures[0].get.strip + else: raise ctx.makeError("unknown error parsing section header: " & line), + sectionContents: @[]) + result.add(ctx.curSection) + if captures[3].isSome: + ctx.curSection.sectionContents &= ctx.parseLineParts(captures[3].get) + continue + + # FIXME: as implemented, this will not allow column breaks within a section + m = line.match(COL_BREAK_PAT) + if m.isSome: + result.add(ChordChartNode(kind: ccnkColBreak)) + continue + + # FIXME: as implemented, this will not allow page breaks within a section + m = line.match(PAGE_BREAK_PAT) + if m.isSome: + result.add(ChordChartNode(kind: ccnkPageBreak)) + continue + else: - addToCurSection(parseLine(line)) + addToCurSection(ctx.parseLine(line)) continue diff --git a/src/pco_chordspkg/html.nim b/src/pco_chordspkg/html.nim index 9af4ed5..6a95fdf 100644 --- a/src/pco_chordspkg/html.nim +++ b/src/pco_chordspkg/html.nim @@ -3,6 +3,12 @@ import zero_functional import ./ast +type + FormatContext = ref object + chart: ChordChart + currentKey: ChordChartPitch + sourceKey: ChordChartPitch + const DEFAULT_STYLESHEET* = """ """ -proc toHtml(node: ChordChartNode, indent: string): string = +proc transpose(ctx: FormatContext, chord: ChordChartChord): string = + let distance = ctx.currentKey - ctx.sourceKey + if distance != 0: return $(chord.rootPitch + distance) & chord.flavor + else: return chord.original + +proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): string = result = "" case node.kind of ccnkSection: result &= indent & "
\p" & indent & " " & "

" & node.sectionName & "

\p" - result &= join( - node.sectionContents --> map(it.toHtml(indent & " ")), - "\p") + var contents = newSeq[string]() + for contentNode in node.sectionContents: + contents.add(ctx.toHtml(contentNode, indent & " ")) + result &= contents.join("\p") result &= indent & "
" of ccnkLine: result &= indent & "
\p" - result &= join(node.line --> map(it.toHtml(indent & " ")), "") + for linePart in node.line: result &= ctx.toHtml(linePart, indent & " ") result &= indent & "
" of ccnkWord: @@ -75,8 +87,8 @@ proc toHtml(node: ChordChartNode, indent: string): string = if node.spaceAfter: result&=" space-after" result &= "'>" - if node.chord.len > 0: - result &= "" & node.chord & "" + if node.chord.original.len > 0: + result &= "" & ctx.transpose(node.chord) & "" result &= "" if node.word.len > 0: result &= node.word @@ -90,14 +102,24 @@ proc toHtml(node: ChordChartNode, indent: string): string = of ccnkColBreak: discard #FIXME of ccnkPageBreak: discard #FIXME - of ccnkTransposeKey: discard #FIXME - of ccnkRedefineKey: discard #FIXME + + of ccnkTransposeKey: + ctx.currentKey = ctx.currentKey + node.transposeSteps + + of ccnkRedefineKey: + ctx.sourceKey = ctx.currentKey + node.transposeSteps + ctx.currentKey = ctx.sourceKey proc toHtml*( cc: ChordChart, stylesheets = @[DEFAULT_STYLESHEET], scripts: seq[string] = @[]): string = + var ctx = FormatContext( + chart: cc, + currentKey: cc.metadata.key, + sourceKey: cc.metadata.key) + result = """ @@ -124,6 +146,6 @@ proc toHtml*( if cc.metadata.contains("bpm"): result &= " " & cc.metadata["bpm"] & "bpm" result &= "\p" - result &= join(cc.nodes --> map(it.toHtml(indent & " ")), "\p") + result &= join(cc.nodes --> map(ctx.toHtml(it, indent & " ")), "\p") result &= " \p"