3 Commits
0.5.1 ... 0.5.5

5 changed files with 191 additions and 59 deletions

View File

@ -1,6 +1,6 @@
# Package # Package
version = "0.5.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'"

View File

@ -38,7 +38,12 @@ when isMainModule:
if args["--transpose"]: parseInt($args["--transpose"]) if args["--transpose"]: parseInt($args["--transpose"])
else: 0 else: 0
let outputHtml = ast.toHtml(transpose, args["--number-chart"]) 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)

View File

@ -77,7 +77,7 @@ 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 =
["A♭", "A", "B♭", "B", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"][ord(pitch)] ["A♭", "A", "B♭", "B", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"][ord(pitch)]
func renderPitchInKey*( func renderPitchInKey*(
@ -88,9 +88,9 @@ func renderPitchInKey*(
var scaleNames: array[(ord(high(ChordChartPitch)) + 1), string] var scaleNames: array[(ord(high(ChordChartPitch)) + 1), string]
if useSharps.isNone: if useSharps.isNone:
scaleNames = case key scaleNames = case key
of A, B, C, D, E, G: of A, B, D, E, G:
["G#", "A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G"] ["G#", "A", "A#", "B", "C", "C#", "D", "D#", "E", "F", "F#", "G"]
of Af, Bf, Df, Ef, F: of Af, Bf, C, Df, Ef, F:
["A♭", "A", "B♭", "B", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"] ["A♭", "A", "B♭", "B", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"]
of Gf: of Gf:
["A♭", "A", "B♭", "C♭", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"] ["A♭", "A", "B♭", "C♭", "C", "D♭", "D", "E♭", "E", "F", "G♭", "G"]
@ -259,7 +259,7 @@ proc parsePitch*(ctx: ParserContext, keyValue: string): ChordChartPitch =
# see regexr.com/70nv1 # see regexr.com/70nv1
let CHORD_REGEX = let CHORD_REGEX =
"([b#♭♮𝄫𝄪]?[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
"(\\/([b#♭♮𝄫𝄪]?[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)
@ -344,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 = @[]
@ -394,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(
@ -406,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,
@ -475,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)

View File

@ -1,4 +1,4 @@
const PCO_CHORDS_VERSION* = "0.5.1" const PCO_CHORDS_VERSION* = "0.5.5"
const USAGE* = """Usage: const USAGE* = """Usage:
pco_chords [options] pco_chords [options]
@ -23,6 +23,7 @@ Options:
--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* = """

View File

@ -22,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; }
@ -49,10 +47,12 @@ h3 .section-text {
margin: 0 0.5em; 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;
@ -65,16 +65,25 @@ h3 .section-text {
} }
.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 { .chord .flavor {
font-variant-position: super; 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;
@ -88,9 +97,100 @@ h3 .section-text {
@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>
""" """
@ -191,6 +291,7 @@ 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:
@ -215,10 +316,7 @@ 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.currentKey let oldKey = ctx.currentKey
@ -254,7 +352,7 @@ proc toHtml*(
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
@ -266,7 +364,12 @@ 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 = " "
@ -283,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")