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:
@ -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)
|
||||
|
Reference in New Issue
Block a user