Initial commit: initial implementation.

This commit is contained in:
2022-08-20 07:23:03 -05:00
commit 8515546c89
6 changed files with 533 additions and 0 deletions

322
src/pco_chordspkg/ast.nim Normal file
View File

@ -0,0 +1,322 @@
import std/nre, std/strtabs, std/strutils
import zero_functional
type
ChordChartMetadata* = object
title*: string
key*: ChordChartPitch
optionalProps: StringTableRef
ChordChart* = ref object
rawContent*: string
metadata*: ChordChartMetadata
nodes*: seq[ChordChartNode]
ChordChartNodeKind* = enum
ccnkSection,
ccnkLine,
ccnkWord,
ccnkNote,
ccnkColBreak,
ccnkPageBreak,
ccnkTransposeKey,
ccnkRedefineKey
ChordChartPitch* = enum Af, A, Bf, B, C, Df, D, Ef, E, F, Fs, G
ChordChartNode* = ref object
case kind: ChordChartNodeKind
of ccnkSection:
sectionName*: string
sectionContents*: seq[ChordChartNode]
of ccnkLine:
line*: seq[ChordChartNode]
of ccnkWord:
spaceBefore*: bool
spaceAfter*: bool
chord*: string
word*: string
of ccnkNote:
note*: string
inclInLyrics*: bool
of ccnkColBreak: discard
of ccnkPageBreak: discard
of ccnkTransposeKey:
transposeSteps*: int
of ccnkRedefineKey:
newKey*: ChordChartPitch
ParserContext = ref object
lines: seq[string]
curKeyCenter: ChordChartPitch
curLine: int
curNode: ChordChartNode
func `[]`*(ccmd: ChordChartMetadata, key: string): string =
ccmd.optionalProps[key]
func contains*(ccmd: ChordChartMetadata, key: string): bool =
return key == "title" or key == "key" or ccmd.optionalProps.contains(key)
iterator pairs*(ccmd: ChordChartMetadata): tuple[key, value: string] =
yield ("title", ccmd.title)
yield ("key", $ccmd.key)
for p in ccmd.optionalProps.pairs: yield p
func kind*(ccn: ChordChartNode): ChordChartNodeKind = ccn.kind
func `$`*(pitch: ChordChartPitch): string =
case pitch
of Af: "Ab"
of A: "A"
of Bf: "Bb"
of B: "B"
of C: "C"
of Df: "Db"
of D: "D"
of Ef: "Eb"
of E: "E"
of F: "F"
of Fs: "F#"
of G: "G"
func dump*(m: ChordChartMetadata, indent = ""): string =
return indent & "Metadata\p" & join(m.pairs --> map(indent & " " & it.key & ": " & it.value), "\p")
func dump*(ccn: ChordChartNode, indent = ""): string =
case ccn.kind
of ccnkSection:
let formattedChildren = ccn.sectionContents --> map(dump(it, indent & " "))
result = indent & "Section -- " & ccn.sectionName & "\p" &
formattedChildren.join("\p")
of ccnkColBreak: result = indent & "Column Break"
of ccnkPageBreak: result = indent & "Page Break"
of ccnkTransposeKey:
result = indent & "Transpose key by " & $ccn.transposeSteps
of ccnkRedefineKey:
result = indent & "Redefine key to " & $ccn.newKey
of ccnkLine:
result = indent & "Line\p"
for child in ccn.line:
let formattedChild = dump(child, indent)
if child.kind == ccnkWord: result &= formattedChild
else: result &= formattedChild & "\p"
of ccnkWord:
result = ""
if ccn.spaceBefore: result &= " "
if ccn.chord.len > 0: result &= "[" & ccn.chord & "]"
result &= ccn.word
if ccn.spaceAfter: result &= " "
of ccnkNote:
result = indent & "Note "
if not ccn.inclInLyrics: result &= "(chords only) "
result &= ccn.note
func `$`*(cc: ChordChart): string =
result = "ChordChart\p" &
dump(cc.metadata, " ") & "\p" &
join(cc.nodes --> map(dump(it, " ")), "\p")
# -----------------------------------------------------------------------------
# PARSER
# -----------------------------------------------------------------------------
template makeError(ctx: ParserContext, msg: string): untyped =
newException(ValueError,
"error parsing input at line " & $ctx.curLine & ":\p\t" & msg)
template addToCurSection(n: ChordChartNode): untyped =
if ctx.curNode.kind == ccnkSection: ctx.curNode.sectionContents.add(n)
else: result.add(n)
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
else: raise ctx.makeError(keyValue.strip & " is not a recognized key.")
let METADATA_LINE_PAT = re"^([^:]+):(.*)$"
let METADATA_END_PAT = re"^-+$"
proc parseMetadata(ctx: var ParserContext): ChordChartMetadata =
var title = "MISSING"
var songKey = "MISSING"
var optProps = newStringTable()
while ctx.curLine < ctx.lines.len:
let line = ctx.lines[ctx.curLine]
if line.match(METADATA_END_PAT).isSome:
ctx.curLine += 1
break
let m = line.match(METADATA_LINE_PAT)
if m.isNone:
raise ctx.makeError("expected metadata property or ending marker")
let key = m.get.captures[0].strip.tolower
let value = m.get.captures[1].strip
if key == "title": title = value
elif key == "key": songKey = value
else: optProps[key] = value
ctx.curLine += 1
if title == "MISSING":
raise ctx.makeError("metadata is missing the 'title' property")
if songKey == "MISSING":
raise ctx.makeError("metadata is missing the 'key' property")
result = ChordChartMetadata(
title: title,
key: ctx.parsePitch(songKey),
optionalProps: optProps)
let SECTION_LINE_PAT = re"^((?i)(chorus|verse|bridge|breakdown|vamp) ?\d*\s*)|([[:upper:]]+\s*)$"
let COL_BREAK_PAT = re"\s*COLUMN_BREAK\s*$"
let PAGE_BREAK_PAT = re"\s*PAGE_BREAK\s*$"
let TRANSPOSE_PAT = re"\s*TRANSPOSE KEY ([+-]\d+)\s*$"
let REDEFINE_KEY_PAT = re"\s*REDEFINE KEY ([+-]\d+)\s*$"
let NOTE_PAT = re"^(.*)({{[^}]+}}|{[^}]+})(.*)$"
let SPACE_PAT = re"\s"
let CHORD_IN_LYRICS_PAT = re"(\w+)(\[.+)"
let CHORD_AND_LYRICS_PAT = re"^\[([^\]]+)\]([^\s\[]+)(.*)$"
let CHORD_PAT = re"^\[([^\]]+)\]$"
proc parseLineParts(parts: varargs[string]): seq[ChordChartNode] =
result = @[]
for p in parts:
var m = p.match(NOTE_PAT)
if m.isSome:
if m.get.captures[0].len > 0: result &= parseLineParts(m.get.captures[0])
result.add(ChordChartNode(
kind: ccnkNote,
note: m.get.captures[1].strip(chars = {'{', '}', ' '}),
inclInLyrics: not m.get.captures[1].startsWith("{{")))
if m.get.captures[2].len > 0: result &= parseLineParts(m.get.captures[2])
continue
m = p.match(SPACE_PAT)
if m.isSome:
result &= parseLineParts(p.splitWhitespace)
continue
m = p.match(CHORD_IN_LYRICS_PAT)
if m.isSome:
result &= parseLineParts(m.get.captures[0], m.get.captures[1])
continue
m = p.match(CHORD_AND_LYRICS_PAT)
if m.isSome:
result.add(ChordChartNode(
kind: ccnkWord,
spaceAfter: true, #FIXME
spaceBefore: true, #FIXME
chord: m.get.captures[0],
word: m.get.captures[1]))
result &= parseLineParts(m.get.captures[2])
continue
m = p.match(CHORD_PAT)
if m.isSome:
result.add(ChordChartNode(
kind: ccnkWord,
spaceAfter: false, #FIXME
spaceBefore: false, #FIXME
chord: m.get.captures[0],
word: ""))
continue
if p.len > 0:
result.add(ChordChartNode(
kind: ccnkWord,
spaceAfter: true, #FIXME
spaceBefore: true, #FIXME
chord: "",
word: p))
proc parseLine(line: string): ChordChartNode =
result = ChordChartNode(
kind: ccnkLine,
line: parseLineParts(line.splitWhitespace))
proc parseBody(ctx: var ParserContext): seq[ChordChartNode] =
result = @[]
while ctx.curLine < ctx.lines.len:
let line = ctx.lines[ctx.curLine]
ctx.curLine += 1
var m = line.match(SECTION_LINE_PAT)
if m.isSome:
ctx.curNode = ChordChartNode(
kind: ccnkSection,
sectionName: line.strip,
sectionContents: @[])
result.add(ctx.curNode)
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(
kind: ccnkTransposeKey,
transposeSteps: parseInt(m.get.captures[0])))
continue
m = line.match(REDEFINE_KEY_PAT)
if m.isSome:
let newKeyInt = (
cast[int](ctx.curKeyCenter) +
parseInt(m.get.captures[0])
) mod 12
addToCurSection(ChordChartNode(
kind: ccnkRedefineKey,
newKey: cast[ChordChartPitch](newKeyInt)))
continue
else:
addToCurSection(parseLine(line))
continue
proc parseChordChart*(s: string): ChordChart =
var parserCtx = ParserContext(
lines: s.splitLines,
curLine: 0)
let metadata = parseMetadata(parserCtx)
parserCtx.curKeyCenter = metadata.key
result = ChordChart(
rawContent: s,
metadata: metadata,
nodes: parseBody(parserCtx))