Support one- and two-column layouts. Transposing keys doesn't start a new section.
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "0.5.4"
|
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"
|
||||||
|
@ -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*(
|
||||||
@ -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
|
||||||
"((min|maj|aug|dim|sus|6\\/9|[mM1-9#b♭♮𝄫𝄪Δ+oøoø][0-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)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const PCO_CHORDS_VERSION* = "0.5.4"
|
const PCO_CHORDS_VERSION* = "0.5.5"
|
||||||
|
|
||||||
const USAGE* = """Usage:
|
const USAGE* = """Usage:
|
||||||
pco_chords [options]
|
pco_chords [options]
|
||||||
|
@ -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,6 +47,8 @@ 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;
|
||||||
@ -73,10 +73,17 @@ h3 .section-text {
|
|||||||
white-space: nowrap;
|
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;
|
||||||
@ -90,10 +97,6 @@ h3 .section-text {
|
|||||||
@media screen {
|
@media screen {
|
||||||
body { margin: 1em; }
|
body { margin: 1em; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
|
||||||
.page-contents { column-count: 2; }
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -110,10 +113,8 @@ html {
|
|||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.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; }
|
||||||
|
|
||||||
@ -137,6 +138,8 @@ 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;
|
||||||
@ -165,10 +168,17 @@ h3 .section-text {
|
|||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
@ -182,10 +192,6 @@ h3 .section-text {
|
|||||||
@media screen {
|
@media screen {
|
||||||
body { margin: 1em; }
|
body { margin: 1em; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media print {
|
|
||||||
.page-contents { column-count: 2; }
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -310,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
|
||||||
@ -361,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 = " "
|
||||||
|
|
||||||
@ -378,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