Better column support, redefine keys.

- Fix transposition bug (wrap around past G->A)
- Better chord parsing and handling (bass transposed correctly)
- Add column and page break support.
This commit is contained in:
2022-10-29 10:56:56 -05:00
parent 4a6519b1a7
commit 4833cc4f37
5 changed files with 132 additions and 61 deletions

View File

@ -20,14 +20,16 @@ type
ccnkColBreak,
ccnkPageBreak,
ccnkTransposeKey,
ccnkRedefineKey
ccnkRedefineKey,
ccnkNone
ChordChartPitch* = enum Af, A, Bf, B, C, Df, D, Ef, E, F, Fs, G
ChordChartChord* = object
original*: string
rootPitch*: ChordChartPitch
flavor*: string
flavor*: Option[string]
bassPitch*: Option[ChordChartPitch]
ChordChartNode* = ref object
case kind: ChordChartNodeKind
@ -39,8 +41,8 @@ type
of ccnkWord:
spaceBefore*: bool
spaceAfter*: bool
chord*: ChordChartChord
word*: string
chord*: Option[ChordChartChord]
word*: Option[string]
of ccnkNote:
note*: string
inclInLyrics*: bool
@ -50,6 +52,7 @@ type
transposeSteps*: int
of ccnkRedefineKey:
newKey*: ChordChartPitch
of ccnkNone: discard
ParserContext = ref object
lines: seq[string]
@ -58,6 +61,8 @@ type
curSection: ChordChartNode
unparsedLineParts: seq[string]
let EMPTY_CHORD_CHART_NODE* = ChordChartNode(kind: ccnkNone)
func `[]`*(ccmd: ChordChartMetadata, key: string): string =
ccmd.optionalProps[key]
@ -89,7 +94,9 @@ func `$`*(pitch: ChordChartPitch): string =
func `+`*(pitch: ChordChartPitch, steps: int): ChordChartPitch =
cast[ChordChartPitch]((ord(pitch) + steps) mod 12)
func `-`*(a, b: ChordChartPitch): int = ord(a) - ord(b)
func `-`*(a, b: ChordChartPitch): int =
result = ord(a) - ord(b)
if result < 0: result += 12
func dump*(m: ChordChartMetadata, indent = ""): string =
return indent & "Metadata\p" & join(m.pairs --> map(indent & " " & it.key & ": " & it.value), "\p")
@ -121,11 +128,20 @@ func dump*(ccn: ChordChartNode, indent = ""): string =
of ccnkWord:
result = ""
if ccn.spaceBefore: result &= " "
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.chord.isSome:
let chord = ccn.chord.get
result &= "["
if chord.flavor.isNone and chord.bassPitch.isNone:
result &= chord.original
else:
if chord.flavor.isSome:
result &= $chord.rootPitch & "_" & chord.flavor.get
if chord.bassPitch.isSome:
result &= $chord.rootPitch & "/" & $chord.bassPitch.get
result &= "]"
if ccn.word.isSome: result &= ccn.word.get
if ccn.spaceAfter: result &= " "
of ccnkNote:
@ -133,6 +149,9 @@ func dump*(ccn: ChordChartNode, indent = ""): string =
if not ccn.inclInLyrics: result &= "(chords only) "
result &= ccn.note
of ccnkNone:
result &= indent & "NONE"
func `$`*(cc: ChordChart): string =
result = "ChordChart\p" &
dump(cc.metadata, " ") & "\p" &
@ -181,31 +200,35 @@ func parsePitch*(ctx: ParserContext, keyValue: string): ChordChartPitch =
of "f𝄪", "g", "a𝄫": return ChordChartPitch.G
else: raise ctx.makeError(keyValue.strip & " is not a recognized key.")
# see regexr.com/70nv1
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
"([A-G1-7][b#♭♮𝄫𝄪]?)" & # chord root
"(([mM1-9#b♭♮𝄫𝄪Δ+oøoø][0-9]?|min|maj|aug|dim|sus|6\\/9|\\([1-9#b♭]+\\))*)" & # chord flavor/type
"(\\/([A-G1-7][b#♭♮𝄫𝄪]?))?" # optional bass
let CHORD_PAT = re(CHORD_REGEX)
proc parseChord*(ctx: ParserContext, chordValue: string): ChordChartChord =
proc parseChord*(
ctx: ParserContext, chordValue: string
): Option[ChordChartChord] =
let m = chordValue.match(CHORD_PAT)
if m.isNone:
return ChordChartChord(
original: chordValue,
flavor: "")
if m.isNone: none[ChordChartChord]()
else:
let flavor =
if m.get.captures.contains(1): m.get.captures[1]
else: ""
if m.get.captures.contains(1) and m.get.captures[1].len > 0:
some(m.get.captures[1])
else: none[string]()
let bass =
if m.get.captures.contains(2): m.get.captures[2]
else: ""
let bassPitch =
if m.get.captures.contains(4) and m.get.captures[4].len > 0:
some(ctx.parsePitch(m.get.captures[4]))
else: none[ChordChartPitch]()
return ChordChartChord(
return some(ChordChartChord(
original: chordValue,
rootPitch: ctx.parsePitch(m.get.captures[0]),
flavor: flavor & bass)
flavor: flavor,
bassPitch: bassPitch))
let METADATA_LINE_PAT = re"^([^:]+):(.*)$"
let METADATA_END_PAT = re"^-+$"
@ -289,7 +312,7 @@ proc parseLineParts(ctx: ParserContext, parts: varargs[string]): seq[ChordChartN
spaceAfter: true, #FIXME
spaceBefore: true, #FIXME
chord: ctx.parseChord(m.get.captures[0]),
word: m.get.captures[1]))
word: some(m.get.captures[1])))
result &= ctx.parseLineParts(m.get.captures[2])
continue
@ -300,7 +323,7 @@ proc parseLineParts(ctx: ParserContext, parts: varargs[string]): seq[ChordChartN
spaceAfter: false, #FIXME
spaceBefore: false, #FIXME
chord: ctx.parseChord(m.get.captures[0]),
word: ""))
word: none[string]()))
continue
if p.len > 0:
@ -308,8 +331,8 @@ proc parseLineParts(ctx: ParserContext, parts: varargs[string]): seq[ChordChartN
kind: ccnkWord,
spaceAfter: true, #FIXME
spaceBefore: true, #FIXME
chord: ChordChartChord(original: "", flavor: ""),
word: p))
chord: none[ChordChartChord](),
word: some(p)))
proc parseLine(ctx: ParserContext, line: string): ChordChartNode =
result = ChordChartNode(kind: ccnkLine)
@ -322,7 +345,7 @@ proc parseLine(ctx: ParserContext, line: string): ChordChartNode =
spaceAfter: false, #FIXME
spaceBefore: false, #FIXME
chord: ctx.parseChord(it.strip),
word: ""))
word: none[string]()))
else: result.line = ctx.parseLineParts(line.splitWhitespace)
@ -330,7 +353,7 @@ proc readNote(ctx: var ParserContext, endPat: Regex): string =
let startLineNum = ctx.curLineNum
result = ""
while ctx.lines.len > ctx.curLineNum:
while ctx.lines.len > ctx.curLineNum and ctx.hasNextLine:
let line = ctx.nextLine
let m = line.find(endPat)
if m.isSome:
@ -362,7 +385,7 @@ proc parseBody(ctx: var ParserContext): seq[ChordChartNode] =
# if this is the first character of the line, then let's parse the note
result.add(ChordChartNode(
kind: ccnkNote,
inclInLyrics: m.get.match .len < 2,
inclInLyrics: m.get.match.len < 2,
note: ctx.readNote(NOTE_END_PAT)))
m = line.match(TRANSPOSE_PAT)
@ -384,6 +407,16 @@ proc parseBody(ctx: var ParserContext): seq[ChordChartNode] =
newKey: cast[ChordChartPitch](newKeyInt)))
continue
m = line.match(COL_BREAK_PAT)
if m.isSome:
result.add(ChordChartNode(kind: ccnkColBreak))
continue
m = line.match(PAGE_BREAK_PAT)
if m.isSome:
result.add(ChordChartNode(kind: ccnkPageBreak))
continue
m = line.match(SECTION_LINE_PAT)
if m.isSome:
let captures = m.get.captures.toSeq
@ -397,18 +430,6 @@ proc parseBody(ctx: var ParserContext): seq[ChordChartNode] =
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(ctx.parseLine(line))
continue
@ -418,6 +439,7 @@ proc parseChordChart*(s: string): ChordChart =
var parserCtx = ParserContext(
lines: s.splitLines,
curLineNum: -1,
curSection: EMPTY_CHORD_CHART_NODE,
unparsedLineParts: @[])
let metadata = parseMetadata(parserCtx)