Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
73bdb97881 | |||
a57aed0715 | |||
2712f9ff52 | |||
8271129b90 | |||
5b1238f038 | |||
9ca5a1b99c |
@ -1,6 +1,6 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "0.4.1"
|
version = "0.5.5"
|
||||||
author = "Jonathan Bernard"
|
author = "Jonathan Bernard"
|
||||||
description = "Chord chart formatter compatible with Planning Center Online"
|
description = "Chord chart formatter compatible with Planning Center Online"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@ -12,3 +12,9 @@ bin = @["pco_chords"]
|
|||||||
# Dependencies
|
# Dependencies
|
||||||
|
|
||||||
requires "nim >= 1.6.6", "docopt", "zero_functional"
|
requires "nim >= 1.6.6", "docopt", "zero_functional"
|
||||||
|
|
||||||
|
# Dependencies from git.jdbernard.com/jdb/nim-package
|
||||||
|
requires "update_nim_package_version"
|
||||||
|
|
||||||
|
task updateVersion, "Update the version of this tool.":
|
||||||
|
exec "update_nim_package_version pco_chords 'src/pco_chordspkg/cliconstants.nim'"
|
||||||
|
@ -34,11 +34,21 @@ when isMainModule:
|
|||||||
$ast & "\p" &
|
$ast & "\p" &
|
||||||
"-".repeat(16) & "\p"
|
"-".repeat(16) & "\p"
|
||||||
|
|
||||||
let outputHtml = ast.toHtml()
|
let transpose =
|
||||||
|
if args["--transpose"]: parseInt($args["--transpose"])
|
||||||
|
else: 0
|
||||||
|
|
||||||
|
let outputHtml =
|
||||||
|
if args["--large-print"]:
|
||||||
|
ast.toHtml(transpose, args["--number-chart"],
|
||||||
|
stylesheets = @[LARGE_PRINT_STYLESHEET])
|
||||||
|
else:
|
||||||
|
ast.toHtml(transpose, args["--number-chart"])
|
||||||
|
|
||||||
if args["--output"]: writeFile($args["--output"], outputHtml)
|
if args["--output"]: writeFile($args["--output"], outputHtml)
|
||||||
else: stdout.write(outputHtml)
|
else: stdout.write(outputHtml)
|
||||||
|
|
||||||
except:
|
except CatchableError:
|
||||||
fatal getCurrentExceptionMsg()
|
fatal getCurrentExceptionMsg()
|
||||||
debug getCurrentException().getStackTrace()
|
debug getCurrentException().getStackTrace()
|
||||||
quit(QuitFailure)
|
quit(QuitFailure)
|
||||||
|
@ -4,7 +4,7 @@ import zero_functional
|
|||||||
type
|
type
|
||||||
ChordChartMetadata* = object
|
ChordChartMetadata* = object
|
||||||
title*: string
|
title*: string
|
||||||
key*: ChordChartPitch
|
key*: ChordChartChord
|
||||||
optionalProps: StringTableRef
|
optionalProps: StringTableRef
|
||||||
|
|
||||||
ChordChart* = ref object
|
ChordChart* = ref object
|
||||||
@ -23,10 +23,10 @@ type
|
|||||||
ccnkRedefineKey,
|
ccnkRedefineKey,
|
||||||
ccnkNone
|
ccnkNone
|
||||||
|
|
||||||
ChordChartPitch* = enum Af, A, Bf, B, C, Df, D, Ef, E, F, Fs, G
|
ChordChartPitch* = enum Af, A, Bf, B, C, Df, D, Ef, E, F, Gf, G
|
||||||
|
|
||||||
ChordChartChord* = object
|
ChordChartChord* = object
|
||||||
original*: string
|
original*: Option[string]
|
||||||
rootPitch*: ChordChartPitch
|
rootPitch*: ChordChartPitch
|
||||||
flavor*: Option[string]
|
flavor*: Option[string]
|
||||||
bassPitch*: Option[ChordChartPitch]
|
bassPitch*: Option[ChordChartPitch]
|
||||||
@ -35,6 +35,7 @@ type
|
|||||||
case kind: ChordChartNodeKind
|
case kind: ChordChartNodeKind
|
||||||
of ccnkSection:
|
of ccnkSection:
|
||||||
sectionName*: string
|
sectionName*: string
|
||||||
|
remainingSectionLine*: Option[string]
|
||||||
sectionContents*: seq[ChordChartNode]
|
sectionContents*: seq[ChordChartNode]
|
||||||
of ccnkLine:
|
of ccnkLine:
|
||||||
line*: seq[ChordChartNode]
|
line*: seq[ChordChartNode]
|
||||||
@ -77,19 +78,59 @@ iterator pairs*(ccmd: ChordChartMetadata): tuple[key, value: string] =
|
|||||||
func kind*(ccn: ChordChartNode): ChordChartNodeKind = ccn.kind
|
func kind*(ccn: ChordChartNode): ChordChartNodeKind = ccn.kind
|
||||||
|
|
||||||
func `$`*(pitch: ChordChartPitch): string =
|
func `$`*(pitch: ChordChartPitch): string =
|
||||||
case pitch
|
["A♭", "A", "B♭", "B", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"][ord(pitch)]
|
||||||
of Af: "Ab"
|
|
||||||
of A: "A"
|
func renderPitchInKey*(
|
||||||
of Bf: "Bb"
|
pitch: ChordChartPitch,
|
||||||
of B: "B"
|
key: ChordChartPitch,
|
||||||
of C: "C"
|
useSharps: Option[bool] = none[bool]()): string =
|
||||||
of Df: "Db"
|
|
||||||
of D: "D"
|
var scaleNames: array[(ord(high(ChordChartPitch)) + 1), string]
|
||||||
of Ef: "Eb"
|
if useSharps.isNone:
|
||||||
of E: "E"
|
scaleNames = case key
|
||||||
of F: "F"
|
of A, B, D, E, G:
|
||||||
of Fs: "F#"
|
["G#", "A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G"]
|
||||||
of G: "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, steps: int): ChordChartPitch =
|
func `+`*(pitch: ChordChartPitch, steps: int): ChordChartPitch =
|
||||||
cast[ChordChartPitch]((ord(pitch) + steps) mod 12)
|
cast[ChordChartPitch]((ord(pitch) + steps) mod 12)
|
||||||
@ -132,13 +173,15 @@ func dump*(ccn: ChordChartNode, indent = ""): string =
|
|||||||
if ccn.chord.isSome:
|
if ccn.chord.isSome:
|
||||||
let chord = ccn.chord.get
|
let chord = ccn.chord.get
|
||||||
result &= "["
|
result &= "["
|
||||||
if chord.flavor.isNone and chord.bassPitch.isNone:
|
if chord.original.isSome and chord.flavor.isNone and
|
||||||
result &= chord.original
|
chord.bassPitch.isNone:
|
||||||
|
result &= chord.original.get
|
||||||
else:
|
else:
|
||||||
|
result &= $chord.rootPitch
|
||||||
if chord.flavor.isSome:
|
if chord.flavor.isSome:
|
||||||
result &= $chord.rootPitch & "_" & chord.flavor.get
|
result &= "_" & chord.flavor.get
|
||||||
if chord.bassPitch.isSome:
|
if chord.bassPitch.isSome:
|
||||||
result &= $chord.rootPitch & "/" & $chord.bassPitch.get
|
result &= "/" & $chord.bassPitch.get
|
||||||
result &= "]"
|
result &= "]"
|
||||||
|
|
||||||
if ccn.word.isSome: result &= ccn.word.get
|
if ccn.word.isSome: result &= ccn.word.get
|
||||||
@ -184,27 +227,40 @@ template addToCurSection(n: ChordChartNode): untyped =
|
|||||||
if ctx.curSection.kind == ccnkSection: ctx.curSection.sectionContents.add(n)
|
if ctx.curSection.kind == ccnkSection: ctx.curSection.sectionContents.add(n)
|
||||||
else: result.add(n)
|
else: result.add(n)
|
||||||
|
|
||||||
func parsePitch*(ctx: ParserContext, keyValue: string): ChordChartPitch =
|
proc parsePitch*(ctx: ParserContext, keyValue: string): ChordChartPitch =
|
||||||
case keyValue.strip.toLower
|
let normK = keyValue.strip.toLower
|
||||||
|
case normK
|
||||||
of "gs", "gis", "g#", "ab", "a♭", "af", "aes": return ChordChartPitch.Af
|
of "gs", "gis", "g#", "ab", "a♭", "af", "aes": return ChordChartPitch.Af
|
||||||
of "g𝄪", "a", "a♮", "b𝄫": return ChordChartPitch.A
|
of "g𝄪", "a", "a♮", "b𝄫": return ChordChartPitch.A
|
||||||
of "as", "ais", "a#", "bf", "bb", "b♭", "bes", "c𝄫": return ChordChartPitch.Bf
|
of "as", "ais", "a#", "bf", "bb", "b♭", "bes", "c𝄫": return ChordChartPitch.Bf
|
||||||
of "a𝄪", "b", "cb", "ces", "cf": return ChordChartPitch.B
|
of "a𝄪", "b", "c♭", "cb", "ces", "cf": return ChordChartPitch.B
|
||||||
of "bs", "bis", "b#", "c", "d𝄫": return ChordChartPitch.C
|
of "bs", "bis", "b#", "c", "d𝄫": return ChordChartPitch.C
|
||||||
of "b𝄪", "cs", "cis", "c#", "df", "db", "des": return ChordChartPitch.Df
|
of "b𝄪", "cs", "cis", "c#", "d♭", "df", "db", "des": return ChordChartPitch.Df
|
||||||
of "c𝄪", "d", "e𝄫": return ChordChartPitch.D
|
of "c𝄪", "d", "e𝄫": return ChordChartPitch.D
|
||||||
of "ds", "dis", "d#", "ef", "eb", "ees", "f𝄫": return ChordChartPitch.Ef
|
of "ds", "dis", "d#", "ef", "e♭", "eb", "ees", "f𝄫": return ChordChartPitch.Ef
|
||||||
of "d𝄪", "e", "fes", "ff": return ChordChartPitch.E
|
of "d𝄪", "e", "f♭", "fes", "ff": return ChordChartPitch.E
|
||||||
of "es", "eis", "e#", "f", "g𝄫": return ChordChartPitch.F
|
of "es", "eis", "e#", "f", "g𝄫": return ChordChartPitch.F
|
||||||
of "e𝄪", "fs", "fis", "f#", "gf", "gb", "ges": return ChordChartPitch.Fs
|
of "e𝄪", "fs", "fis", "f#", "g♭", "gf", "gb", "ges": return ChordChartPitch.Gf
|
||||||
of "f𝄪", "g", "a𝄫": return ChordChartPitch.G
|
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.")
|
else: raise ctx.makeError(keyValue.strip & " is not a recognized key.")
|
||||||
|
|
||||||
# see regexr.com/70nv1
|
# see regexr.com/70nv1
|
||||||
let CHORD_REGEX =
|
let CHORD_REGEX =
|
||||||
"([A-G1-7][b#♭♮𝄫𝄪]?)" & # chord root
|
"([b#♭♮𝄫𝄪]?[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
|
"((min|maj|aug|dim|sus|6\\/9|[mM1-9#b♭♮𝄫𝄪Δ+oøoø°𝆩][0-9]?|\\([1-9#b♭]+\\))*)" & # chord flavor/type
|
||||||
"(\\/([A-G1-7][b#♭♮𝄫𝄪]?))?" # optional bass
|
"(\\/([b#♭♮𝄫𝄪]?[A-G1-7][b#♭♮𝄫𝄪]?))?" # optional bass
|
||||||
let CHORD_PAT = re(CHORD_REGEX)
|
let CHORD_PAT = re(CHORD_REGEX)
|
||||||
|
|
||||||
proc parseChord*(
|
proc parseChord*(
|
||||||
@ -225,7 +281,7 @@ proc parseChord*(
|
|||||||
else: none[ChordChartPitch]()
|
else: none[ChordChartPitch]()
|
||||||
|
|
||||||
return some(ChordChartChord(
|
return some(ChordChartChord(
|
||||||
original: chordValue,
|
original: some(chordValue),
|
||||||
rootPitch: ctx.parsePitch(m.get.captures[0]),
|
rootPitch: ctx.parsePitch(m.get.captures[0]),
|
||||||
flavor: flavor,
|
flavor: flavor,
|
||||||
bassPitch: bassPitch))
|
bassPitch: bassPitch))
|
||||||
@ -235,8 +291,8 @@ let METADATA_END_PAT = re"^-+$"
|
|||||||
proc parseMetadata(ctx: var ParserContext): ChordChartMetadata =
|
proc parseMetadata(ctx: var ParserContext): ChordChartMetadata =
|
||||||
|
|
||||||
var title = "MISSING"
|
var title = "MISSING"
|
||||||
var songKey = "MISSING"
|
|
||||||
var optProps = newStringTable()
|
var optProps = newStringTable()
|
||||||
|
var songKey: Option[ChordChartChord]
|
||||||
|
|
||||||
while ctx.curLineNum < ctx.lines.len:
|
while ctx.curLineNum < ctx.lines.len:
|
||||||
let line = ctx.nextLine
|
let line = ctx.nextLine
|
||||||
@ -251,28 +307,32 @@ proc parseMetadata(ctx: var ParserContext): ChordChartMetadata =
|
|||||||
let key = m.get.captures[0].strip.tolower
|
let key = m.get.captures[0].strip.tolower
|
||||||
let value = m.get.captures[1].strip
|
let value = m.get.captures[1].strip
|
||||||
if key == "title": title = value
|
if key == "title": title = value
|
||||||
elif key == "key": songKey = value
|
elif key == "key":
|
||||||
|
songKey = ctx.parseChord(value)
|
||||||
|
if songKey.isNone:
|
||||||
|
raise ctx.makeError("unrecognized key: " & value)
|
||||||
else: optProps[key] = value
|
else: optProps[key] = value
|
||||||
|
|
||||||
if title == "MISSING":
|
if title == "MISSING":
|
||||||
raise ctx.makeError("metadata is missing the 'title' property")
|
raise ctx.makeError("metadata is missing the 'title' property")
|
||||||
|
|
||||||
if songKey == "MISSING":
|
if songKey.isNone:
|
||||||
raise ctx.makeError("metadata is missing the 'key' property")
|
raise ctx.makeError("metadata is missing the 'key' property")
|
||||||
|
|
||||||
result = ChordChartMetadata(
|
result = ChordChartMetadata(
|
||||||
title: title,
|
title: title,
|
||||||
key: ctx.parsePitch(songKey),
|
key: songKey.get,
|
||||||
optionalProps: optProps)
|
optionalProps: optProps)
|
||||||
|
|
||||||
const KNOWN_SECTION_NAMES = [
|
const KNOWN_SECTION_NAMES = [
|
||||||
"chorus", "verse", "bridge", "breakdown", "vamp", "intstrumental",
|
"chorus", "verse", "bridge", "breakdown", "vamp", "intstrumental",
|
||||||
"interlude", "intro", "outtro", "ending", "end", "tag"
|
"interlude", "intro", "outtro", "ending", "end", "tag", "prechorus",
|
||||||
|
"pre-chorus", "pre chorus"
|
||||||
]
|
]
|
||||||
|
|
||||||
let SECTION_LINE_PAT = re(
|
let SECTION_LINE_PAT = re(
|
||||||
"^((" & # case insensitive
|
"^((" & "((?i)" & # case insensitive
|
||||||
"((?i)" & KNOWN_SECTION_NAMES.join("|") & ")" & # known names
|
KNOWN_SECTION_NAMES.join("|") & ")" & # known names
|
||||||
"|[[:upper:]]{3,}" & # all upper-case words
|
"|[[:upper:]]{3,}" & # all upper-case words
|
||||||
") ?\\d*)" & # numeric suffix (Verse 2)
|
") ?\\d*)" & # numeric suffix (Verse 2)
|
||||||
|
|
||||||
@ -284,13 +344,46 @@ let PAGE_BREAK_PAT = re"\s*PAGE_BREAK\s*$"
|
|||||||
let TRANSPOSE_PAT = re"\s*TRANSPOSE KEY ([+-]\d+)\s*$"
|
let TRANSPOSE_PAT = re"\s*TRANSPOSE KEY ([+-]\d+)\s*$"
|
||||||
let REDEFINE_KEY_PAT = re"\s*REDEFINE KEY ([+-]\d+)\s*$"
|
let REDEFINE_KEY_PAT = re"\s*REDEFINE KEY ([+-]\d+)\s*$"
|
||||||
#let NOTE_PAT = re"^(.*)({{[^}]+}}|{[^}]+})(.*)$"
|
#let NOTE_PAT = re"^(.*)({{[^}]+}}|{[^}]+})(.*)$"
|
||||||
let NOTE_START_PAT = re"{{"
|
let NOTE_START_PAT = re"\{\{"
|
||||||
let NOTE_END_PAT = re"}}"
|
let NOTE_END_PAT = re"\}\}"
|
||||||
let SPACE_PAT = re"\s"
|
let SPACE_PAT = re"\s"
|
||||||
let CHORD_IN_LYRICS_PAT = re"(\w+)(\[.+)"
|
let CHORD_IN_LYRICS_PAT = re"(\w+)(\[.+)"
|
||||||
let CHORD_AND_LYRICS_PAT = re"^\[([^\]]+)\]([^\s\[]+)(.*)$"
|
let CHORD_AND_LYRICS_PAT = re"^\[([^\]]+)\]([^\s\[]+)(.*)$"
|
||||||
let BRACED_CHORD_PAT = re"^\[([^\]]+)\]$"
|
let BRACED_CHORD_PAT = re"^\[([^\]]+)\]$"
|
||||||
let NAKED_CHORDS_ONLY_PAT = re("^(" & CHORD_REGEX & "\\s*\\|*\\s*)+$")
|
let NAKED_CHORDS_ONLY_PAT = re("^\\s*(" & CHORD_REGEX & "\\s*\\|*\\s*)+$")
|
||||||
|
|
||||||
|
proc readNote(ctx: var ParserContext, firstLine: string): tuple[pre, note, post: string] =
|
||||||
|
let startLineNum = ctx.curLineNum
|
||||||
|
|
||||||
|
result = ("", "", "")
|
||||||
|
|
||||||
|
let startMatch = firstLine.find(NOTE_START_PAT)
|
||||||
|
var endMatch = firstLine.find(NOTE_END_PAT)
|
||||||
|
|
||||||
|
if startMatch.isNone: return
|
||||||
|
|
||||||
|
if startMatch.get.matchBounds.a > 0:
|
||||||
|
result.pre = firstLine[0..<startMatch.get.matchBounds.a]
|
||||||
|
|
||||||
|
if endMatch.isSome:
|
||||||
|
result.note = firstLine[(startMatch.get.matchBounds.b + 1)..<endMatch.get.matchBounds.a]
|
||||||
|
result.post = firstLine[(endMatch.get.matchBounds.b + 1)..^1]
|
||||||
|
else:
|
||||||
|
# if we don't find the end of the note, then we need to read more lines
|
||||||
|
# until we do
|
||||||
|
result.note = firstLine[(startMatch.get.matchBounds.b + 1)..^1]
|
||||||
|
|
||||||
|
while ctx.lines.len > ctx.curLineNum and ctx.hasNextLine:
|
||||||
|
let line = ctx.nextLine
|
||||||
|
endMatch = line.find(NOTE_END_PAT)
|
||||||
|
if endMatch.isSome:
|
||||||
|
result.note &= line[0..<endMatch.get.matchBounds.a]
|
||||||
|
result.post = line[(endMatch.get.matchBounds.b + 1)..^1]
|
||||||
|
return
|
||||||
|
else: result.note &= line
|
||||||
|
|
||||||
|
raise ctx.makeError("a note section was started on line " &
|
||||||
|
$startLineNum & " and never closed")
|
||||||
|
|
||||||
proc parseLineParts(ctx: ParserContext, parts: varargs[string]): seq[ChordChartNode] =
|
proc parseLineParts(ctx: ParserContext, parts: varargs[string]): seq[ChordChartNode] =
|
||||||
result = @[]
|
result = @[]
|
||||||
@ -334,10 +427,11 @@ proc parseLineParts(ctx: ParserContext, parts: varargs[string]): seq[ChordChartN
|
|||||||
chord: none[ChordChartChord](),
|
chord: none[ChordChartChord](),
|
||||||
word: some(p)))
|
word: some(p)))
|
||||||
|
|
||||||
proc parseLine(ctx: ParserContext, line: string): ChordChartNode =
|
proc parseLine(lentCtx: var ParserContext, line: string): ChordChartNode =
|
||||||
|
let ctx = lentCtx
|
||||||
result = ChordChartNode(kind: ccnkLine)
|
result = ChordChartNode(kind: ccnkLine)
|
||||||
|
|
||||||
let m = line.match(NAKED_CHORDS_ONLY_PAT)
|
var m = line.match(NAKED_CHORDS_ONLY_PAT)
|
||||||
if m.isSome:
|
if m.isSome:
|
||||||
result.line = line.splitWhitespace --> map(
|
result.line = line.splitWhitespace --> map(
|
||||||
ChordChartNode(
|
ChordChartNode(
|
||||||
@ -346,49 +440,28 @@ proc parseLine(ctx: ParserContext, line: string): ChordChartNode =
|
|||||||
spaceBefore: false, #FIXME
|
spaceBefore: false, #FIXME
|
||||||
chord: ctx.parseChord(it.strip),
|
chord: ctx.parseChord(it.strip),
|
||||||
word: none[string]()))
|
word: none[string]()))
|
||||||
|
return
|
||||||
|
|
||||||
|
m = line.match(NOTE_START_PAT)
|
||||||
|
if m.isSome:
|
||||||
|
let (pre, note, post) = lentCtx.readNote(line)
|
||||||
|
result.line = ctx.parseLineParts(pre) &
|
||||||
|
@[ChordChartNode(
|
||||||
|
kind: ccnkNote,
|
||||||
|
inclInLyrics: true,
|
||||||
|
note: note)] &
|
||||||
|
ctx.parseLineParts(post)
|
||||||
|
return
|
||||||
|
|
||||||
else: result.line = ctx.parseLineParts(line.splitWhitespace)
|
else: result.line = ctx.parseLineParts(line.splitWhitespace)
|
||||||
|
|
||||||
proc readNote(ctx: var ParserContext, endPat: Regex): string =
|
|
||||||
let startLineNum = ctx.curLineNum
|
|
||||||
result = ""
|
|
||||||
|
|
||||||
while ctx.lines.len > ctx.curLineNum and ctx.hasNextLine:
|
|
||||||
let line = ctx.nextLine
|
|
||||||
let m = line.find(endPat)
|
|
||||||
if m.isSome:
|
|
||||||
result &= line[0..<m.get.matchBounds.a]
|
|
||||||
ctx.pushPartialsToParse(line[m.get.matchBounds.b..^1])
|
|
||||||
break
|
|
||||||
else: result &= line
|
|
||||||
|
|
||||||
if not ctx.hasNextLine:
|
|
||||||
raise ctx.makeError("a note section was started on line " &
|
|
||||||
$startLineNum & " and never closed")
|
|
||||||
|
|
||||||
proc parseBody(ctx: var ParserContext): seq[ChordChartNode] =
|
proc parseBody(ctx: var ParserContext): seq[ChordChartNode] =
|
||||||
result = @[]
|
result = @[]
|
||||||
while ctx.hasNextLine:
|
while ctx.hasNextLine:
|
||||||
var line = ctx.nextLine
|
var line = ctx.nextLine
|
||||||
|
|
||||||
var m = line.find(NOTE_START_PAT)
|
var m = line.match(TRANSPOSE_PAT)
|
||||||
if m.isSome:
|
|
||||||
if m.get.matchBounds.a > 0:
|
|
||||||
# if this is not the first character of the line then let's split the
|
|
||||||
# line and continue to parse
|
|
||||||
ctx.pushPartialsToParse(
|
|
||||||
line[0..<m.get.matchBounds.a],
|
|
||||||
line[m.get.matchBounds.a..^1])
|
|
||||||
continue
|
|
||||||
|
|
||||||
else:
|
|
||||||
# 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,
|
|
||||||
note: ctx.readNote(NOTE_END_PAT)))
|
|
||||||
|
|
||||||
m = line.match(TRANSPOSE_PAT)
|
|
||||||
if m.isSome:
|
if m.isSome:
|
||||||
addToCurSection(ChordChartNode(
|
addToCurSection(ChordChartNode(
|
||||||
kind: ccnkTransposeKey,
|
kind: ccnkTransposeKey,
|
||||||
@ -398,7 +471,7 @@ proc parseBody(ctx: var ParserContext): seq[ChordChartNode] =
|
|||||||
m = line.match(REDEFINE_KEY_PAT)
|
m = line.match(REDEFINE_KEY_PAT)
|
||||||
if m.isSome:
|
if m.isSome:
|
||||||
let newKeyInt = (
|
let newKeyInt = (
|
||||||
cast[int](ctx.curKeyCenter) +
|
ord(ctx.curKeyCenter) +
|
||||||
parseInt(m.get.captures[0])
|
parseInt(m.get.captures[0])
|
||||||
) mod 12
|
) mod 12
|
||||||
|
|
||||||
@ -415,6 +488,7 @@ proc parseBody(ctx: var ParserContext): seq[ChordChartNode] =
|
|||||||
m = line.match(PAGE_BREAK_PAT)
|
m = line.match(PAGE_BREAK_PAT)
|
||||||
if m.isSome:
|
if m.isSome:
|
||||||
result.add(ChordChartNode(kind: ccnkPageBreak))
|
result.add(ChordChartNode(kind: ccnkPageBreak))
|
||||||
|
ctx.curSection = EMPTY_CHORD_CHART_NODE
|
||||||
continue
|
continue
|
||||||
|
|
||||||
m = line.match(SECTION_LINE_PAT)
|
m = line.match(SECTION_LINE_PAT)
|
||||||
@ -422,12 +496,14 @@ proc parseBody(ctx: var ParserContext): seq[ChordChartNode] =
|
|||||||
let captures = m.get.captures.toSeq
|
let captures = m.get.captures.toSeq
|
||||||
ctx.curSection = ChordChartNode(
|
ctx.curSection = ChordChartNode(
|
||||||
kind: ccnkSection,
|
kind: ccnkSection,
|
||||||
sectionName: if captures[0].isSome: captures[0].get.strip
|
sectionName:
|
||||||
else: raise ctx.makeError("unknown error parsing section header: " & line),
|
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](),
|
||||||
sectionContents: @[])
|
sectionContents: @[])
|
||||||
result.add(ctx.curSection)
|
result.add(ctx.curSection)
|
||||||
if captures[3].isSome:
|
|
||||||
ctx.curSection.sectionContents &= ctx.parseLineParts(captures[3].get)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -443,7 +519,7 @@ proc parseChordChart*(s: string): ChordChart =
|
|||||||
unparsedLineParts: @[])
|
unparsedLineParts: @[])
|
||||||
|
|
||||||
let metadata = parseMetadata(parserCtx)
|
let metadata = parseMetadata(parserCtx)
|
||||||
parserCtx.curKeyCenter = metadata.key
|
parserCtx.curKeyCenter = metadata.key.rootPitch
|
||||||
|
|
||||||
result = ChordChart(
|
result = ChordChart(
|
||||||
rawContent: s,
|
rawContent: s,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const PCO_CHORDS_VERSION* = "0.4.1"
|
const PCO_CHORDS_VERSION* = "0.5.5"
|
||||||
|
|
||||||
const USAGE* = """Usage:
|
const USAGE* = """Usage:
|
||||||
pco_chords [options]
|
pco_chords [options]
|
||||||
@ -6,15 +6,24 @@ const USAGE* = """Usage:
|
|||||||
|
|
||||||
Options:
|
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).
|
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).
|
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
|
--help Print this usage information
|
||||||
--debug Enable debug logging.
|
--debug Enable debug logging.
|
||||||
--echo-args Echo the input parameters.
|
--echo-args Echo the input parameters.
|
||||||
|
--large-print Use the large type styling.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
const ONLINE_HELP* = """
|
const ONLINE_HELP* = """
|
||||||
|
@ -9,6 +9,8 @@ type
|
|||||||
currentKey: ChordChartPitch
|
currentKey: ChordChartPitch
|
||||||
sourceKey: ChordChartPitch
|
sourceKey: ChordChartPitch
|
||||||
currentSection: ChordChartNode
|
currentSection: ChordChartNode
|
||||||
|
numberChart: bool
|
||||||
|
transposeSteps: int
|
||||||
|
|
||||||
const DEFAULT_STYLESHEET* = """
|
const DEFAULT_STYLESHEET* = """
|
||||||
<style>
|
<style>
|
||||||
@ -20,10 +22,8 @@ const DEFAULT_STYLESHEET* = """
|
|||||||
|
|
||||||
html { font-family: sans-serif; }
|
html { font-family: sans-serif; }
|
||||||
|
|
||||||
.page-contents {
|
.one-column .page-contents { column-count: 1; }
|
||||||
column-width: 336px;
|
.two-column .page-contents { column-count: 2; }
|
||||||
column-width: 3.5in;
|
|
||||||
}
|
|
||||||
|
|
||||||
.column-break { margin-bottom: auto; }
|
.column-break { margin-bottom: auto; }
|
||||||
|
|
||||||
@ -40,10 +40,19 @@ h3 {
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h3 .section-text {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist { font-style: italic; }
|
||||||
|
|
||||||
.line {
|
.line {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-end;
|
align-items: end;
|
||||||
|
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
@ -56,12 +65,25 @@ h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.word.space-after { margin-right: 0.5em; }
|
.word.space-after { margin-right: 0.5em; }
|
||||||
|
.word.no-chord { align-self: flex-end; }
|
||||||
|
|
||||||
.chord {
|
.chord {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chord > * {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chord .flavor {
|
||||||
|
font-variant-position: super;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note { margin-right: 1em; }
|
||||||
|
|
||||||
.song-order h3 {
|
.song-order h3 {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@ -75,24 +97,160 @@ h3 {
|
|||||||
@media screen {
|
@media screen {
|
||||||
body { margin: 1em; }
|
body { margin: 1em; }
|
||||||
}
|
}
|
||||||
|
</style>
|
||||||
|
"""
|
||||||
|
|
||||||
@media print {
|
const LARGE_PRINT_STYLESHEET* = """
|
||||||
.page-contents { column-count: 2; }
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.one-column .page-contents { column-count: 1; }
|
||||||
|
.two-column .page-contents { column-count: 2; }
|
||||||
|
|
||||||
|
.column-break { margin-bottom: auto; }
|
||||||
|
|
||||||
|
h2 { font-size: 1.25em; }
|
||||||
|
|
||||||
|
section {
|
||||||
|
break-inside: avoid;
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 .section-text {
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist { font-style: italic; }
|
||||||
|
|
||||||
|
.line {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
margin-left: 0.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word.space-after { margin-right: 0.5em; }
|
||||||
|
.word.no-chord { align-self: flex-end; }
|
||||||
|
|
||||||
|
.lyric {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chord {
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chord > * {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chord .flavor {
|
||||||
|
font-variant-position: super;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note { margin-right: 1em; }
|
||||||
|
|
||||||
|
.song-order h3 {
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.song-order li {
|
||||||
|
list-style: none;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen {
|
||||||
|
body { margin: 1em; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
proc transpose(ctx: FormatContext, chord: ChordChartChord): string =
|
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
|
||||||
|
|
||||||
let distance = ctx.currentKey - ctx.sourceKey
|
let distance = ctx.currentKey - ctx.sourceKey
|
||||||
|
|
||||||
if distance != 0:
|
if distance != 0:
|
||||||
result = $(chord.rootPitch + distance)
|
result = ChordChartChord(
|
||||||
if chord.flavor.isSome: result &= chord.flavor.get
|
original: none[string](),
|
||||||
if chord.bassPitch.isSome: result &= "/" & $(chord.bassPitch.get + distance)
|
rootPitch: chord.rootPitch + distance,
|
||||||
#debug "transposed " & $ctx.sourceKey & " -> " & $ctx.currentKey &
|
flavor:
|
||||||
# " (distance " & $distance & ")\p\tchord: " & $chord & " to " & result
|
if chord.flavor.isSome: some(chord.flavor.get)
|
||||||
|
else: none[string](),
|
||||||
else: result = chord.original
|
bassPitch:
|
||||||
|
if chord.bassPitch.isSome: some(chord.bassPitch.get + distance)
|
||||||
|
else: none[ChordChartPitch]())
|
||||||
|
|
||||||
func makeSongOrder(songOrder, indent: string): string =
|
func makeSongOrder(songOrder, indent: string): string =
|
||||||
result = indent & "<section class=song-order>\p" &
|
result = indent & "<section class=song-order>\p" &
|
||||||
@ -108,7 +266,13 @@ proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): strin
|
|||||||
of ccnkSection:
|
of ccnkSection:
|
||||||
ctx.currentSection = node
|
ctx.currentSection = node
|
||||||
result &= indent & "<section>\p" &
|
result &= indent & "<section>\p" &
|
||||||
indent & " " & "<h3>" & node.sectionName & "</h3>\p"
|
indent & " " & "<h3>" & node.sectionName
|
||||||
|
|
||||||
|
if ctx.currentSection.remainingSectionLine.isSome:
|
||||||
|
result &= "<span class='section-text'>" &
|
||||||
|
ctx.currentSection.remainingSectionLine.get & "</span>"
|
||||||
|
|
||||||
|
result &= "</h3>\p"
|
||||||
|
|
||||||
var contents = newSeq[string]()
|
var contents = newSeq[string]()
|
||||||
for contentNode in node.sectionContents:
|
for contentNode in node.sectionContents:
|
||||||
@ -127,10 +291,12 @@ proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): strin
|
|||||||
result &= "<span class='word"
|
result &= "<span class='word"
|
||||||
#if node.spaceBefore: result&=" space-before"
|
#if node.spaceBefore: result&=" space-before"
|
||||||
if node.spaceAfter: result&=" space-after"
|
if node.spaceAfter: result&=" space-after"
|
||||||
|
if node.chord.isNone: result&=" no-chord"
|
||||||
result &= "'>"
|
result &= "'>"
|
||||||
|
|
||||||
if node.chord.isSome:
|
if node.chord.isSome:
|
||||||
result &= "<span class=chord>" & ctx.transpose(node.chord.get) & "</span>"
|
result &= "<span class=chord>" &
|
||||||
|
ctx.format(ctx.transpose(node.chord.get), ctx.numberChart) & "</span>"
|
||||||
|
|
||||||
result &= "<span class=lyric>"
|
result &= "<span class=lyric>"
|
||||||
if node.word.isSome: result &= node.word.get
|
if node.word.isSome: result &= node.word.get
|
||||||
@ -150,14 +316,11 @@ proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): strin
|
|||||||
|
|
||||||
of ccnkTransposeKey:
|
of ccnkTransposeKey:
|
||||||
ctx.currentKey = ctx.currentKey + node.transposeSteps
|
ctx.currentKey = ctx.currentKey + node.transposeSteps
|
||||||
let headingVal = indent & "<h4 class=note>Key Change: " & $ctx.currentKey & "</h4>"
|
result &= indent & "<h4 class=note>Key Change: " & $ctx.currentKey & "</h4>"
|
||||||
if ctx.currentSection.kind == ccnkNone: result &= headingVal
|
|
||||||
else:
|
|
||||||
result &= "</section><section>" & headingVal & "</section><section>"
|
|
||||||
|
|
||||||
of ccnkRedefineKey:
|
of ccnkRedefineKey:
|
||||||
let oldKey = ctx.sourceKey
|
let oldKey = ctx.currentKey
|
||||||
ctx.sourceKey = node.newKey
|
ctx.sourceKey = node.newKey + ctx.transposeSteps
|
||||||
ctx.currentKey = ctx.sourceKey
|
ctx.currentKey = ctx.sourceKey
|
||||||
|
|
||||||
if oldKey != ctx.currentKey:
|
if oldKey != ctx.currentKey:
|
||||||
@ -170,19 +333,26 @@ proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): strin
|
|||||||
|
|
||||||
proc toHtml*(
|
proc toHtml*(
|
||||||
cc: ChordChart,
|
cc: ChordChart,
|
||||||
|
transpose = 0,
|
||||||
|
numberChart = false,
|
||||||
stylesheets = @[DEFAULT_STYLESHEET],
|
stylesheets = @[DEFAULT_STYLESHEET],
|
||||||
scripts: seq[string] = @[]): string =
|
scripts: seq[string] = @[]): string =
|
||||||
|
|
||||||
var ctx = FormatContext(
|
var ctx = FormatContext(
|
||||||
chart: cc,
|
chart: cc,
|
||||||
currentKey: cc.metadata.key,
|
currentKey: cc.metadata.key.rootPitch + transpose,
|
||||||
sourceKey: cc.metadata.key,
|
sourceKey: cc.metadata.key.rootPitch,
|
||||||
currentSection: EMPTY_CHORD_CHART_NODE)
|
currentSection: EMPTY_CHORD_CHART_NODE,
|
||||||
|
numberChart: numberChart,
|
||||||
|
transposeSteps: transpose)
|
||||||
|
|
||||||
|
debug "Formatting:\p\tsource key: " & $ctx.sourceKey & "\p\ttransposing: " &
|
||||||
|
$transpose & "\p\ttarget key: " & $ctx.currentKey
|
||||||
|
|
||||||
result = """<!doctype html>
|
result = """<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>""" & cc.metadata.title & "</title>\p"
|
<title>""" & cc.metadata.title & " — " & $ctx.currentKey & "</title>\p"
|
||||||
|
|
||||||
for ss in stylesheets:
|
for ss in stylesheets:
|
||||||
if ss.startsWith("<style>"): result &= ss
|
if ss.startsWith("<style>"): result &= ss
|
||||||
@ -194,14 +364,19 @@ proc toHtml*(
|
|||||||
elif fileExists(sc): result &= "<script>\p" & readFile(sc) & "\p</script>"
|
elif fileExists(sc): result &= "<script>\p" & readFile(sc) & "\p</script>"
|
||||||
else: warn "cannot read script file '" & sc & "'"
|
else: warn "cannot read script file '" & sc & "'"
|
||||||
|
|
||||||
result &= " </head>\p <body>"
|
result &= " </head>\p"
|
||||||
|
|
||||||
|
if cc.metadata.contains("columns") and cc.metadata["columns"] == "1":
|
||||||
|
result &= " <body class='one-column'>"
|
||||||
|
else:
|
||||||
|
result &= " <body class='two-column'>"
|
||||||
|
|
||||||
var indent = " "
|
var indent = " "
|
||||||
#
|
|
||||||
# Title
|
# Title
|
||||||
result &= indent & "<h1>" & cc.metadata.title & "</h1>\p"
|
result &= indent & "<h1>" & cc.metadata.title & "</h1>\p"
|
||||||
|
|
||||||
var metadataPieces = @["Key: " & $cc.metadata.key]
|
var metadataPieces = @["Key: " & ctx.format(ctx.transpose(cc.metadata.key))]
|
||||||
if cc.metadata.contains("time signature"):
|
if cc.metadata.contains("time signature"):
|
||||||
metadataPieces.add(cc.metadata["time signature"])
|
metadataPieces.add(cc.metadata["time signature"])
|
||||||
if cc.metadata.contains("bpm"):
|
if cc.metadata.contains("bpm"):
|
||||||
@ -211,6 +386,9 @@ proc toHtml*(
|
|||||||
|
|
||||||
result &= indent & "<h2>" & metadataPieces.join(" | ") & "</h2>\p"
|
result &= indent & "<h2>" & metadataPieces.join(" | ") & "</h2>\p"
|
||||||
|
|
||||||
|
if cc.metadata.contains("artist"):
|
||||||
|
result &= indent & "<div class=artist>" & cc.metadata["artist"] & "</div>\p"
|
||||||
|
|
||||||
result &= "<div class=page-contents>"
|
result &= "<div class=page-contents>"
|
||||||
result &= join(cc.nodes --> map(ctx.toHtml(it, indent & " ")), "\p")
|
result &= join(cc.nodes --> map(ctx.toHtml(it, indent & " ")), "\p")
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user