Files
pco_chords/src/pco_chordspkg/html.nim

369 lines
8.3 KiB
Nim

import std/[logging, options, os, strutils]
import zero_functional
import ./[ast, notation]
type
FormatContext = ref object
chart: ChordChart
currentKey: Key
sourceKey: Key
currentSection: ChordChartNode
numberChart: bool
transposeSteps: int
const DEFAULT_STYLESHEET* = """
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html { font-family: sans-serif; }
.page-contents { line-height: 1.1; }
.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.125em;
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: end;
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; }
.chord {
font-weight: 600;
margin-bottom: -0.25em;
margin-right: 0.5em;
white-space: nowrap;
}
.chord > * {
display: inline-block;
height: 1.2em;
}
.number-chart .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>
"""
const LARGE_PRINT_STYLESHEET* = """
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-family: sans-serif;
font-size: 1.3em;
font-variant: small-caps;
}
.page-contents { line-height: 1.1; }
.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.3em; }
.word.no-chord { align-self: flex-end; }
.lyric {
font-weight: 600;
}
.chord {
font-size: 125%;
font-style: italic;
font-weight: 700;
margin-right: 0.5em;
}
.chord > * {
display: inline-block;
height: 1.2em;
}
.number-chart .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>
"""
func format(ctx: FormatContext, chord: ChordChartChord, useNumber = false): string =
##if not useNumber and chord.original.isSome: return chord.original.get
result = "<span class=root>"
if useNumber: result &= $chord.root
else: result &= $ctx.currentKey.spellPitch(chord.root)
result &= "</span>"
if chord.flavor.isSome:
result &= "<span class=flavor>" & chord.flavor.get & "</span>"
if chord.bass.isSome:
result &= "<span class=bass>/"
if useNumber: result &= $chord.bass.get
else: result &= $ctx.currentKey.spellPitch(chord.bass.get)
result &= "</span>"
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"
proc toHtml(ctx: var FormatContext, node: ChordChartNode, indent: string): string =
result = ""
case node.kind
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"
var contents = newSeq[string]()
for contentNode in node.sectionContents:
contents.add(ctx.toHtml(contentNode, indent & " "))
result &= contents.join("\p")
result &= indent & "</section>"
ctx.currentSection = EMPTY_CHORD_CHART_NODE
of ccnkLine:
result &= indent & "<div class=line>\p"
for linePart in node.line: result &= ctx.toHtml(linePart, indent & " ")
result &= indent & "</div>"
of ccnkWord:
result &= "<span class='word"
#if node.spaceBefore: result&=" space-before"
if node.spaceAfter: result&=" space-after"
if node.chord.isNone: result&=" no-chord"
result &= "'>"
if node.chord.isSome:
result &= "<span class=chord>" &
ctx.format(node.chord.get, ctx.numberChart) & "</span>"
result &= "<span class=lyric>"
if node.word.isSome: result &= node.word.get
else: result &= "&nbsp;"
result &= "</span>"
result &= "</span>"
of ccnkNote:
result &= indent & "<div class=note>" & node.note & "</div>"
of ccnkColBreak:
result &= "<div class=column-break />"
of ccnkPageBreak:
result &= "</div><div class=page-contents>"
of ccnkTransposeKey:
ctx.currentKey = ctx.currentKey.transpose(node.transposeSteps)
result &= indent & "<h4 class=note>Key Change: " & $ctx.currentKey & "</h4>"
of ccnkRedefineKey:
let oldKey = ctx.currentKey
ctx.sourceKey = node.newKey
ctx.currentKey = ctx.sourceKey.transpose(ctx.transposeSteps)
if oldKey != ctx.currentKey:
let headingVal = indent & "<h4 class=note>Key Change: " & $ctx.currentKey & "</h4>"
if ctx.currentSection.kind == ccnkNone: result &= headingVal
else:
result &= "</section><section>" & headingVal & "</section><section>"
of ccnkNone: discard
proc toHtml*(
cc: ChordChart,
transpose = 0,
numberChart = false,
stylesheets = @[DEFAULT_STYLESHEET],
scripts: seq[string] = @[]): string =
var ctx = FormatContext(
chart: cc,
currentKey: cc.metadata.key.transpose(transpose),
sourceKey: cc.metadata.key,
currentSection: EMPTY_CHORD_CHART_NODE,
numberChart: numberChart,
transposeSteps: transpose)
debug "Formatting:\p\tsource key: $#\p\ttransposing: $#\p\ttarget key: $#" %
[$ctx.sourceKey, $transpose, $ctx.currentKey]
result = """<!doctype html>
<html>
<head>
<title>""" & cc.metadata.title & "" & $ctx.currentKey & "</title>\p"
for ss in stylesheets:
if ss.startsWith("<style>"): result &= ss
elif fileExists(ss): result &= "<style>\p" & readFile(ss) & "\p</style>"
else: warn "cannot read stylesheet file '" & ss & "'"
for sc in scripts:
if sc.startsWith("<script"): result &= sc
elif fileExists(sc): result &= "<script>\p" & readFile(sc) & "\p</script>"
else: warn "cannot read script file '" & sc & "'"
result &= " </head>\p"
var bodyClasses = newSeq[string]()
if numberChart: bodyClasses.add("number-chart")
if cc.metadata.contains("columns") and cc.metadata["columns"] == "1":
bodyClasses.add("one-column")
else: bodyClasses.add("two-column")
result &= " <body class='" & bodyClasses.join(" ") & "'>"
var indent = " "
# Title
result &= indent & "<h1>" & cc.metadata.title & "</h1>\p"
var metadataPieces = @["Key: " & $ctx.currentKey]
if cc.metadata.contains("time signature"):
metadataPieces.add(cc.metadata["time signature"])
if cc.metadata.contains("bpm"):
metadataPieces.add(cc.metadata["bpm"] & "bpm")
if cc.metadata.contains("tempo"):
metadataPieces.add(cc.metadata["tempo"])
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 &= 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>"