Refactors to support key transposition.
This commit is contained in:
@ -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
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user