Compare commits

..

No commits in common. "main" and "0.3.1" have entirely different histories.
main ... 0.3.1

5 changed files with 69 additions and 238 deletions

View File

@ -1,6 +1,6 @@
# Package
version = "0.5.2"
version = "0.3.1"
author = "Jonathan Bernard"
description = "Chord chart formatter compatible with Planning Center Online"
license = "MIT"

View File

@ -34,16 +34,11 @@ when isMainModule:
$ast & "\p" &
"-".repeat(16) & "\p"
let transpose =
if args["--transpose"]: parseInt($args["--transpose"])
else: 0
let outputHtml = ast.toHtml(transpose, args["--number-chart"])
let outputHtml = ast.toHtml()
if args["--output"]: writeFile($args["--output"], outputHtml)
else: stdout.write(outputHtml)
except CatchableError:
except:
fatal getCurrentExceptionMsg()
debug getCurrentException().getStackTrace()
quit(QuitFailure)

View File

@ -1,10 +1,10 @@
import std/[nre, strtabs, strutils]
import std/logging, std/nre, std/strtabs, std/strutils
import zero_functional
type
ChordChartMetadata* = object
title*: string
key*: ChordChartChord
key*: ChordChartPitch
optionalProps: StringTableRef
ChordChart* = ref object
@ -23,10 +23,10 @@ type
ccnkRedefineKey,
ccnkNone
ChordChartPitch* = enum Af, A, Bf, B, C, Df, D, Ef, E, F, Gf, G
ChordChartPitch* = enum Af, A, Bf, B, C, Df, D, Ef, E, F, Fs, G
ChordChartChord* = object
original*: Option[string]
original*: string
rootPitch*: ChordChartPitch
flavor*: Option[string]
bassPitch*: Option[ChordChartPitch]
@ -35,7 +35,6 @@ type
case kind: ChordChartNodeKind
of ccnkSection:
sectionName*: string
remainingSectionLine*: Option[string]
sectionContents*: seq[ChordChartNode]
of ccnkLine:
line*: seq[ChordChartNode]
@ -77,60 +76,20 @@ iterator pairs*(ccmd: ChordChartMetadata): tuple[key, value: string] =
func kind*(ccn: ChordChartNode): ChordChartNodeKind = ccn.kind
func `$`(pitch: ChordChartPitch): string =
["A♭", "A", "B♭", "B", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"][ord(pitch)]
func renderPitchInKey*(
pitch: ChordChartPitch,
key: ChordChartPitch,
useSharps: Option[bool] = none[bool]()): string =
var scaleNames: array[(ord(high(ChordChartPitch)) + 1), string]
if useSharps.isNone:
scaleNames = case key
of A, B, D, E, G:
["G#", "A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G"]
of Af, Bf, C, Df, Ef, F:
["A♭", "A", "B♭", "B", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"]
of Gf:
["A♭", "A", "B♭", "C♭", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"]
elif useSharps.isSome and useSharps.get:
scaleNames = case key
of A, B, C, D, E, G:
["G#", "A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G"]
of Af:
["G#", "A", "A#", "B", "B#", "C#", "D", "D#", "E", "E#", "F#", "F𝄪"]
of Bf:
["G#", "G𝄪", "A#", "B", "B#", "C#", "C𝄪", "D#", "E", "E#", "F#", "F𝄪"]
of Df:
["G#", "A", "A#", "B", "B#", "C#", "D", "D#", "E", "E#", "F#", "G"]
of Ef:
["G#", "A", "A#", "B", "B#", "C#", "C𝄪", "D#", "E", "E#", "F#", "F𝄪"]
of Gf:
["G#", "A", "A#", "B", "C", "C#", "D", "D#", "E", "E#", "F#", "G"]
of F:
["G#", "G𝄪", "A#", "B", "B#", "C#", "C𝄪", "D#", "D𝄪", "E#", "F#", "F𝄪"]
else: # useSharps.isSome and not useSharps.get
scaleNames = case key
of C, Af, Bf, Df, Ef, F:
["A♭", "A", "B♭", "B", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"]
of A:
["A♭", "B𝄫", "B♭", "C♭", "C", "D♭", "E𝄫", "E♭", "F♭", "F", "G♭", "G"]
of B:
["A♭", "A", "B♭", "C♭", "C", "D♭", "D", "E♭", "F♭", "F", "G♭", "G"]
of D:
["A♭", "B𝄫", "B♭", "C♭", "C", "D♭", "E𝄫", "E♭", "F♭", "F", "G♭", "A𝄫"]
of E:
["A♭", "B𝄫", "B♭", "C♭", "C", "D♭", "D", "E♭", "F♭", "F", "G♭", "G"]
of G:
["A♭", "B𝄫", "B♭", "C♭", "D𝄫", "D♭", "E𝄫", "E♭", "F♭", "F", "G♭", "A𝄫"]
of Gf:
["A♭", "A", "B♭", "C♭", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"]
return scaleNames[ord(pitch)]
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 `+`*(pitch: ChordChartPitch, steps: int): ChordChartPitch =
cast[ChordChartPitch]((ord(pitch) + steps) mod 12)
@ -173,15 +132,13 @@ func dump*(ccn: ChordChartNode, indent = ""): string =
if ccn.chord.isSome:
let chord = ccn.chord.get
result &= "["
if chord.original.isSome and chord.flavor.isNone and
chord.bassPitch.isNone:
result &= chord.original.get
if chord.flavor.isNone and chord.bassPitch.isNone:
result &= chord.original
else:
result &= $chord.rootPitch
if chord.flavor.isSome:
result &= "_" & chord.flavor.get
result &= $chord.rootPitch & "_" & chord.flavor.get
if chord.bassPitch.isSome:
result &= "/" & $chord.bassPitch.get
result &= $chord.rootPitch & "/" & $chord.bassPitch.get
result &= "]"
if ccn.word.isSome: result &= ccn.word.get
@ -227,40 +184,27 @@ template addToCurSection(n: ChordChartNode): untyped =
if ctx.curSection.kind == ccnkSection: ctx.curSection.sectionContents.add(n)
else: result.add(n)
proc parsePitch*(ctx: ParserContext, keyValue: string): ChordChartPitch =
let normK = keyValue.strip.toLower
case normK
func parsePitch*(ctx: ParserContext, keyValue: string): ChordChartPitch =
case keyValue.strip.toLower
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", "c", "cb", "ces", "cf": return ChordChartPitch.B
of "a𝄪", "b", "ces", "cf": return ChordChartPitch.B
of "bs", "bis", "b#", "c", "d𝄫": return ChordChartPitch.C
of "b𝄪", "cs", "cis", "c#", "d", "df", "db", "des": return ChordChartPitch.Df
of "b𝄪", "cs", "cis", "c#", "df", "db", "des": return ChordChartPitch.Df
of "c𝄪", "d", "e𝄫": return ChordChartPitch.D
of "ds", "dis", "d#", "ef", "e", "eb", "ees", "f𝄫": return ChordChartPitch.Ef
of "d𝄪", "e", "f", "fes", "ff": return ChordChartPitch.E
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#", "g", "gf", "gb", "ges": return ChordChartPitch.Gf
of "e𝄪", "fs", "fis", "f#", "gf", "gb", "ges": return ChordChartPitch.Fs
of "f𝄪", "g", "a𝄫": return ChordChartPitch.G
of "#7", "1", "𝄫2": return ctx.curKeyCenter
of "𝄪7", "#1", "b2", "♭2": return ctx.curKeyCenter + 1
of "𝄪1", "2", "𝄫3": return ctx.curKeyCenter + 2
of "#2", "b3", "♭3", "𝄫4": return ctx.curKeyCenter + 3
of "𝄪2", "3", "b4", "♭4": return ctx.curKeyCenter + 4
of "#3", "4", "𝄫5": return ctx.curKeyCenter + 5
of "𝄪3", "#4", "b5", "♭5": return ctx.curKeyCenter + 6
of "𝄪4", "5", "𝄫6": return ctx.curKeyCenter + 7
of "#5", "b6", "♭6": return ctx.curKeyCenter + 8
of "𝄪5", "6", "𝄫7": return ctx.curKeyCenter + 9
of "#6", "b7", "♭7", "𝄫1": return ctx.curKeyCenter + 10
of "7", "b1", "♭1": return ctx.curKeyCenter + 11
else: raise ctx.makeError(keyValue.strip & " is not a recognized key.")
# see regexr.com/70nv1
let CHORD_REGEX =
"([b#♭♮𝄫𝄪]?[A-G1-7][b#♭♮𝄫𝄪]?)" & # chord root
"([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
"(\\/([b#♭♮𝄫𝄪]?[A-G1-7][b#♭♮𝄫𝄪]?))?" # optional bass
"(\\/([A-G1-7][b#♭♮𝄫𝄪]?))?" # optional bass
let CHORD_PAT = re(CHORD_REGEX)
proc parseChord*(
@ -281,7 +225,7 @@ proc parseChord*(
else: none[ChordChartPitch]()
return some(ChordChartChord(
original: some(chordValue),
original: chordValue,
rootPitch: ctx.parsePitch(m.get.captures[0]),
flavor: flavor,
bassPitch: bassPitch))
@ -291,8 +235,8 @@ let METADATA_END_PAT = re"^-+$"
proc parseMetadata(ctx: var ParserContext): ChordChartMetadata =
var title = "MISSING"
var songKey = "MISSING"
var optProps = newStringTable()
var songKey: Option[ChordChartChord]
while ctx.curLineNum < ctx.lines.len:
let line = ctx.nextLine
@ -307,32 +251,28 @@ proc parseMetadata(ctx: var ParserContext): ChordChartMetadata =
let key = m.get.captures[0].strip.tolower
let value = m.get.captures[1].strip
if key == "title": title = value
elif key == "key":
songKey = ctx.parseChord(value)
if songKey.isNone:
raise ctx.makeError("unrecognized key: " & value)
elif key == "key": songKey = value
else: optProps[key] = value
if title == "MISSING":
raise ctx.makeError("metadata is missing the 'title' property")
if songKey.isNone:
if songKey == "MISSING":
raise ctx.makeError("metadata is missing the 'key' property")
result = ChordChartMetadata(
title: title,
key: songKey.get,
key: ctx.parsePitch(songKey),
optionalProps: optProps)
const KNOWN_SECTION_NAMES = [
"chorus", "verse", "bridge", "breakdown", "vamp", "intstrumental",
"interlude", "intro", "outtro", "ending", "end", "tag", "prechorus",
"pre-chorus", "pre chorus"
"interlude", "intro", "outtro", "ending", "end", "tag"
]
let SECTION_LINE_PAT = re(
"^((" & "((?i)" & # case insensitive
KNOWN_SECTION_NAMES.join("|") & ")" & # known names
"^((" & # case insensitive
"((?i)" & KNOWN_SECTION_NAMES.join("|") & ")" & # known names
"|[[:upper:]]{3,}" & # all upper-case words
") ?\\d*)" & # numeric suffix (Verse 2)
@ -458,7 +398,7 @@ proc parseBody(ctx: var ParserContext): seq[ChordChartNode] =
m = line.match(REDEFINE_KEY_PAT)
if m.isSome:
let newKeyInt = (
ord(ctx.curKeyCenter) +
cast[int](ctx.curKeyCenter) +
parseInt(m.get.captures[0])
) mod 12
@ -482,14 +422,12 @@ proc parseBody(ctx: var ParserContext): seq[ChordChartNode] =
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),
remainingSectionLine:
if captures[3].isSome: some(captures[3].get.strip)
else: none[string](),
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
else:
@ -505,7 +443,7 @@ proc parseChordChart*(s: string): ChordChart =
unparsedLineParts: @[])
let metadata = parseMetadata(parserCtx)
parserCtx.curKeyCenter = metadata.key.rootPitch
parserCtx.curKeyCenter = metadata.key
result = ChordChart(
rawContent: s,

View File

@ -1,4 +1,4 @@
const PCO_CHORDS_VERSION* = "0.5.2"
const PCO_CHORDS_VERSION* = "0.3.1"
const USAGE* = """Usage:
pco_chords [options]
@ -6,20 +6,12 @@ const USAGE* = """Usage:
Options:
-i, --input <in-file> Read input from <in-file> (rather than from
-i, --input <in-file> Read input from <in-file> (rather than from
STDIN, which is the default).
-o, --output <out-file> Write output to <out-file> (rather than from
-o, --output <out-file> Write output to <out-file> (rather than from
STDOUT, which is the default).
-t, --transpose <distance> Transpose the chart by the given number of
semitones. Distance can be expressed as a
positive or negative number of semitones,
for example: 4, +3 or -5.
-n, --number-chart Write out a chart using the Nashville number
system rather than explicit pitches.
--help Print this usage information
--debug Enable debug logging.
--echo-args Echo the input parameters.

View File

@ -9,8 +9,6 @@ type
currentKey: ChordChartPitch
sourceKey: ChordChartPitch
currentSection: ChordChartNode
numberChart: bool
transposeSteps: int
const DEFAULT_STYLESHEET* = """
<style>
@ -42,13 +40,6 @@ h3 {
text-decoration: underline;
}
h3 .section-text {
font-style: italic;
font-size: 1em;
font-weight: normal;
margin: 0 0.5em;
}
.line {
display: flex;
flex-direction: row;
@ -71,20 +62,6 @@ h3 .section-text {
margin-right: 0.5em;
}
.chord .flavor {
font-variant-position: super;
}
.song-order h3 {
font-style: italic;
font-weight: normal;
}
.song-order li {
list-style: none;
margin-left: 1em;
}
@media screen {
body { margin: 1em; }
}
@ -95,70 +72,17 @@ h3 .section-text {
</style>
"""
func scaleDegree(root: ChordChartPitch, p: ChordChartPitch): string =
let distance = p - root
case distance:
of 0: "1"
of 1: "♭2"
of 2: "2"
of 3: "♭3"
of 4: "3"
of 5: "4"
of 6: "♭5"
of 7: "5"
of 8: "♭6"
of 9: "6"
of 10: "♭7"
of 11: "7"
else: raise newException(Exception, "Impossible")
func format(ctx: FormatContext, chord: ChordChartChord, useNumber = false): string =
##if not useNumber and chord.original.isSome: return chord.original.get
if useNumber:
result = "<span class=root>" &
ctx.currentKey.scaleDegree(chord.rootPitch) & "</span>"
if chord.flavor.isSome:
result &= "<span class=flavor>" & chord.flavor.get & "</span>"
if chord.bassPitch.isSome:
result &= "<span class=bass>/" &
ctx.currentKey.scaleDegree(chord.bassPitch.get) & "</span>"
else:
result = "<span class=root>" &
renderPitchInKey(chord.rootPitch, ctx.currentKey) & "</span>"
if chord.flavor.isSome:
result &= "<span class=flavor>" & chord.flavor.get & "</span>"
if chord.bassPitch.isSome:
result &= "<span class=bass>/" &
renderPitchInKey(chord.bassPitch.get, ctx.currentKey) & "</span>"
proc transpose(ctx: FormatContext, chord: ChordChartChord): ChordChartChord =
result = chord
proc transpose(ctx: FormatContext, chord: ChordChartChord): string =
let distance = ctx.currentKey - ctx.sourceKey
if distance != 0:
result = ChordChartChord(
original: none[string](),
rootPitch: chord.rootPitch + distance,
flavor:
if chord.flavor.isSome: some(chord.flavor.get)
else: none[string](),
bassPitch:
if chord.bassPitch.isSome: some(chord.bassPitch.get + distance)
else: none[ChordChartPitch]())
func makeSongOrder(songOrder, indent: string): string =
result = indent & "<section class=song-order>\p" &
indent & " <h3>Song Order</h3>\p" &
indent & " <ul>\p"
result &= join(songOrder.split(",") -->
map(indent & " <li>" & it.strip & "</li>\p"), "")
result &= indent & " </ul>\p" & indent & "</section>\p"
if distance != 0:
result = $(chord.rootPitch + distance)
if chord.flavor.isSome: result &= chord.flavor.get
if chord.bassPitch.isSome: result &= "/" & $(chord.bassPitch.get + distance)
#debug "transposed " & $ctx.sourceKey & " -> " & $ctx.currentKey &
# " (distance " & $distance & ")\p\tchord: " & $chord & " to " & result
else: result = chord.original
proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): string =
result = ""
@ -166,13 +90,7 @@ proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): strin
of ccnkSection:
ctx.currentSection = node
result &= indent & "<section>\p" &
indent & " " & "<h3>" & node.sectionName
if ctx.currentSection.remainingSectionLine.isSome:
result &= "<span class='section-text'>" &
ctx.currentSection.remainingSectionLine.get & "</span>"
result &= "</h3>\p"
indent & " " & "<h3>" & node.sectionName & "</h3>\p"
var contents = newSeq[string]()
for contentNode in node.sectionContents:
@ -194,8 +112,7 @@ proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): strin
result &= "'>"
if node.chord.isSome:
result &= "<span class=chord>" &
ctx.format(ctx.transpose(node.chord.get), ctx.numberChart) & "</span>"
result &= "<span class=chord>" & ctx.transpose(node.chord.get) & "</span>"
result &= "<span class=lyric>"
if node.word.isSome: result &= node.word.get
@ -221,8 +138,8 @@ proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): strin
result &= "</section><section>" & headingVal & "</section><section>"
of ccnkRedefineKey:
let oldKey = ctx.currentKey
ctx.sourceKey = node.newKey + ctx.transposeSteps
let oldKey = ctx.sourceKey
ctx.sourceKey = node.newKey
ctx.currentKey = ctx.sourceKey
if oldKey != ctx.currentKey:
@ -235,21 +152,14 @@ proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): strin
proc toHtml*(
cc: ChordChart,
transpose = 0,
numberChart = false,
stylesheets = @[DEFAULT_STYLESHEET],
scripts: seq[string] = @[]): string =
var ctx = FormatContext(
chart: cc,
currentKey: cc.metadata.key.rootPitch + transpose,
sourceKey: cc.metadata.key.rootPitch,
currentSection: EMPTY_CHORD_CHART_NODE,
numberChart: numberChart,
transposeSteps: transpose)
debug "Formatting:\p\tsource key: " & $ctx.sourceKey & "\p\ttransposing: " &
$transpose & "\p\ttarget key: " & $ctx.currentKey
currentKey: cc.metadata.key,
sourceKey: cc.metadata.key,
currentSection: EMPTY_CHORD_CHART_NODE)
result = """<!doctype html>
<html>
@ -269,11 +179,11 @@ proc toHtml*(
result &= " </head>\p <body>"
var indent = " "
#
# Title
result &= indent & "<h1>" & cc.metadata.title & "</h1>\p"
var metadataPieces = @["Key: " & ctx.format(ctx.transpose(cc.metadata.key))]
var metadataPieces = @["Key: " & $cc.metadata.key]
if cc.metadata.contains("time signature"):
metadataPieces.add(cc.metadata["time signature"])
if cc.metadata.contains("bpm"):
@ -285,10 +195,6 @@ proc toHtml*(
result &= "<div class=page-contents>"
result &= join(cc.nodes --> map(ctx.toHtml(it, indent & " ")), "\p")
if cc.metadata.contains("song order"):
result &= makeSongOrder(cc.metadata["song order"], indent)
result &= "</div>"
result &= " </body>\p</html>"