Refactors to support key transposition.
This commit is contained in:
parent
6a3793cb0a
commit
4a6519b1a7
@ -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
|
||||
|
||||
|
||||
|
@ -3,6 +3,12 @@ import zero_functional
|
||||
|
||||
import ./ast
|
||||
|
||||
type
|
||||
FormatContext = ref object
|
||||
chart: ChordChart
|
||||
currentKey: ChordChartPitch
|
||||
sourceKey: ChordChartPitch
|
||||
|
||||
const DEFAULT_STYLESHEET* = """
|
||||
<style>
|
||||
* {
|
||||
@ -51,22 +57,28 @@ h3 {
|
||||
</style>
|
||||
"""
|
||||
|
||||
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 & "<section>\p" &
|
||||
indent & " " & "<h3>" & node.sectionName & "</h3>\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 & "</section>"
|
||||
|
||||
of ccnkLine:
|
||||
result &= indent & "<div class=line>\p"
|
||||
result &= join(node.line --> map(it.toHtml(indent & " ")), "")
|
||||
for linePart in node.line: result &= ctx.toHtml(linePart, indent & " ")
|
||||
result &= indent & "</div>"
|
||||
|
||||
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 &= "<span class=chord>" & node.chord & "</span>"
|
||||
if node.chord.original.len > 0:
|
||||
result &= "<span class=chord>" & ctx.transpose(node.chord) & "</span>"
|
||||
|
||||
result &= "<span class=lyric>"
|
||||
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 = """<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
@ -124,6 +146,6 @@ proc toHtml*(
|
||||
if cc.metadata.contains("bpm"): result &= " " & cc.metadata["bpm"] & "bpm"
|
||||
result &= "</h2>\p"
|
||||
|
||||
result &= join(cc.nodes --> map(it.toHtml(indent & " ")), "\p")
|
||||
result &= join(cc.nodes --> map(ctx.toHtml(it, indent & " ")), "\p")
|
||||
|
||||
result &= " </body>\p</html>"
|
||||
|
Loading…
x
Reference in New Issue
Block a user