diff --git a/src/pco_chordspkg/ast.nim b/src/pco_chordspkg/ast.nim
index b22446b..34d6a31 100644
--- a/src/pco_chordspkg/ast.nim
+++ b/src/pco_chordspkg/ast.nim
@@ -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
diff --git a/src/pco_chordspkg/html.nim b/src/pco_chordspkg/html.nim
index 9af4ed5..6a95fdf 100644
--- a/src/pco_chordspkg/html.nim
+++ b/src/pco_chordspkg/html.nim
@@ -3,6 +3,12 @@ import zero_functional
import ./ast
+type
+ FormatContext = ref object
+ chart: ChordChart
+ currentKey: ChordChartPitch
+ sourceKey: ChordChartPitch
+
const DEFAULT_STYLESHEET* = """
"""
-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 & "" & node.sectionName & "
\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 & "