@@ -397,15 +397,85 @@ end; }; } --- renderer engines -function ct.render.html(doc, opts) - local doctitle = opts['title'] - local f = string.format +-- common renderer utility functions +ct.tool = {} + +function ct.tool.mathfmt(ctx, eqn) + local buf = '' + local m = ss.enum {'num','var','op'} + local lsc = 0 + local spans = {} + + local flush = function() + local o + if buf ~= '' then + if lsc == 0 then + o = buf + elseif lsc == m.num then + o = { + kind = 'format'; + style = 'literal'; + spans = {buf}; + } + elseif lsc == m.var then + o = { + kind = 'format'; + style = 'variable'; + spans = {buf}; + } + elseif lsc == m.op then + o = { + kind = 'format'; + style = 'strong'; + spans = {buf}; + } + end + if o then + table.insert(spans, o) + end + end + buf = '' + lsc = 0 + end + + for c, p in ss.str.each(ctx.doc.enc, eqn) do + local cl = ss.str.classify(ctx.doc.enc, c) + local nc = 0 + if not cl.space then + if cl.numeral then + nc = m.num + elseif cl.mathop or cl.symbol then + nc = m.op + elseif cl.letter then + nc = m.var + end + if nc ~= lsc then + flush() + lsc = nc + end + buf = buf .. c + end + end + flush() + return spans +end + +function ct.tool.namespace() +-- some renderers need to be able to generate unique IDs for +-- objects, including ones that users have not assigned IDs +-- to, and objects with the same name in different unlabeled +-- sections. to handle this, we provide a "namespace" mechanism, +-- where some lua table (really its address in memory) is used +-- as a handle for the object and a unique ID is attached to it. +-- if the object has an ID of its own, it is guaranteed to be +-- unique and returned; otherwise, a generic id of the form +-- `x-%u` is generated, where %u is an integer that increments +-- for every new object local ids = {} local canonicalID = {} - local function getSafeID(obj,pfx) + return function(obj,pfx) pfx = pfx or '' if canonicalID[obj] then return canonicalID[obj] elseif obj.id and ids[pfx .. obj.id] then @@ -428,915 +498,11 @@ canonicalID[obj] = cid return cid end end - - local footnotes = {} - local footnotecount = 0 - - local langsused = {} - local langpairs = { - lua = { color = 0x9377ff }; - terra = { color = 0xff77c8 }; - c = { name = 'C', color = 0x77ffe8 }; - html = { color = 0xfff877 }; - scheme = { color = 0x77ff88 }; - lisp = { color = 0x77ff88 }; - fortran = { color = 0xff779a }; - python = { color = 0xffd277 }; - ruby = { color = 0xcdd6ff }; - } - - local stylesets = { - footnote = [[ - div.footnote { - font-family: 90%; - display: none; - grid-template-columns: 1em 1fr min-content; - grid-template-rows: 1fr min-content; - position: fixed; - padding: 1em; - background: @tone(0.05); - border: black; - margin:auto; - } - div.footnote:target { display:grid; } - @media screen { - div.footnote { - left: 10em; - right: 10em; - max-width: calc(@width + 2em); - max-height: 30vw; - bottom: 1em; - } - } - @media print { - div.footnote { - position: relative; - } - div.footnote:first-of-type { - border-top: 1px solid black; - } - } - - div.footnote > a[href="#0"]{ - grid-row: 2/3; - grid-column: 3/4; - display: block; - padding: 0.2em 0.7em; - text-align: center; - text-decoration: none; - background: @tone(0.2); - color: @tone(1); - border: 1px solid black; - margin-top: 0.6em; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; - -webkit-user-drag: none; - user-drag: none; - } - div.footnote > a[href="#0"]:hover { - background: @tone(0.3); - color: @tone(2); - } - div.footnote > a[href="#0"]:active { - background: @tone(0.05); - color: @tone(0.4); - } - @media print { - div.footnote > a[href="#0"]{ - display:none; - } - } - div.footnote > div.number { - text-align:right; - grid-row: 1/2; - grid-column: 1/2; - } - div.footnote > div.text { - grid-row: 1/2; - grid-column: 2/4; - padding-left: 1em; - overflow-y: scroll; - } - ]]; - header = [[ - body { padding: 0 2.5em !important } - h1,h2,h3,h4,h5,h6 { border-bottom: 1px solid @tone(0.7); } - h1 { font-size: 200%; border-bottom-style: double !important; border-bottom-width: 3px !important; margin: 0em -1em; } - h2 { font-size: 130%; margin: 0em -0.7em; } - h3 { font-size: 110%; margin: 0em -0.5em; } - h4 { font-size: 100%; font-weight: normal; margin: 0em -0.2em; } - h5 { font-size: 90%; font-weight: normal; } - h6 { font-size: 80%; font-weight: normal; } - h3, h4, h5, h6 { border-bottom-style: dotted !important; } - h1,h2,h3,h4,h5,h6 { - margin-top: 0; - margin-bottom: 0; - } - :is(h1,h2,h3,h4,h5,h6) + p { - margin-top: 0.4em; - } - - ]]; - headingAnchors = [[ - :is(h1,h2,h3,h4,h5,h6) > a[href].anchor { - text-decoration: none; - font-size: 1.2em; - padding: 0.3em; - opacity: 0%; - transition: 0.3s; - font-weight: 100; - } - :is(h1,h2,h3,h4,h5,h6):hover > a[href].anchor { - opacity: 50%; - } - :is(h1,h2,h3,h4,h5,h6) > a[href].anchor:hover { - opacity: 100%; - } - - ]] .. -- this is necessary to avoid the sections jumping around - -- when focus changes from one to another - [[ section { - border: 1px solid transparent; - } - - section:target { - margin-left: -2em; - margin-right: -2em; - padding: 0 2em; - background: @tone(0.04); - border: 1px dotted @tone(0.3); - } - - section:target > :is(h1,h2,h3,h4,h5,h6) { - - } - ]]; - paragraph = [[ - p { - margin: 0.7em 0; - text-align: justify; - } - section { - margin: 1.2em 0; - } - section:first-child { margin-top: 0; } - ]]; - accent = [[ - @media screen { - body { background: @bg; color: @fg } - a[href] { - color: @tone(0.7 30); - text-decoration-color: @tone/0.4(0.7 30); - } - a[href]:hover { - color: @tone(0.9 30); - text-decoration-color: @tone/0.7(0.7 30); - } - h1 { color: @tone(2); } - h2 { color: @tone(1.5); } - h3 { color: @tone(1.2); } - h4 { color: @tone(1); } - h5,h6 { color: @tone(0.8); } - } - @media print { - a[href] { - text-decoration: none; - color: black; - font-weight: bold; - } - h1,h2,h3,h4,h5,h6 { - border-bottom: 1px black; - } - } - ]]; - aside = [[ - section > aside { - text-align: justify; - margin: 0 1.5em; - padding: 0.5em 0.8em; - background: @tone(0.05); - font-size: 90%; - border-left: 5px solid @tone(0.2 15); - border-right: 5px solid @tone(0.2 15); - } - section > aside p { - margin: 0; - margin-top: 0.6em; - } - section > aside p:first-child { - margin: 0; - } - ]]; - code = [[ - code { - display: inline-block; - background: @tone(0.9); - color: @bg; - font-family: monospace; - font-size: 90%; - padding: 3px 5px; - } - ]]; - var = [[ - var { - font-style: italic; - font-family: monospace; - color: @tone(0.7); - } - code var { - color: @tone(0.25); - } - ]]; - math = [[ - span.equation { - display: inline-block; - background: @tone(0.08); - color: @tone(2); - padding: 0.1em 0.3em; - border: 1px solid @tone(0.5); - } - ]]; - abbr = [[ - abbr[title] { cursor: help; } - ]]; - editors_markup = [[]]; - block_code_listing = [[ - figure.listing { - font-family: monospace; - background: @tone(0.05); - color: @fg; - padding: 0; - margin: 0.3em 0; - counter-reset: line-number; - position: relative; - border: 1px solid @fg; - } - figure.listing>div { - white-space: pre-wrap; - tab-size: 3; - -moz-tab-size: 3; - counter-increment: line-number; - text-indent: -2.3em; - margin-left: 2.3em; - } - figure.listing>:is(div,hr)::before { - width: 1.0em; - padding: 0.2em 0.4em; - text-align: right; - display: inline-block; - background-color: @tone(0.2); - border-right: 1px solid @fg; - content: counter(line-number); - margin-right: 0.3em; - } - figure.listing>hr::before { - color: transparent; - padding-top: 0; - padding-bottom: 0; - } - figure.listing>div::before { - color: @fg; - } - figure.listing>div:last-child::before { - padding-bottom: 0.5em; - } - figure.listing>figcaption:first-child { - border: none; - border-bottom: 1px solid @fg; - } - figure.listing>figcaption::after { - display: block; - float: right; - font-weight: normal; - font-style: italic; - font-size: 70%; - padding-top: 0.3em; - } - figure.listing>figcaption { - font-family: sans-serif; - font-size: 120%; - padding: 0.2em 0.4em; - border: none; - color: @tone(2); - } - figure.listing > hr { - border: none; - margin: 0; - height: 0.7em; - counter-increment: line-number; - } - ]]; - } - - local stylesNeeded = {} - - local render_state_handle = { - doc = doc; - opts = opts; - style_rules = styles; -- use stylesneeded if at all possible - stylesets = stylesets; - stylesets_active = stylesNeeded; - obj_htmlid = getSafeID; - -- remaining fields added later - } - - local renderJob = doc:job('render_html', nil, render_state_handle) - doc.stage.job = renderJob; - - local runhook = function(h, ...) - return renderJob:hook(h, render_state_handle, ...) - end - - local tagproc do - local elt = function(t,attrs) - return f('<%s%s>', t, - attrs and ss.reduce(function(a,b) return a..b end, '', - ss.map(function(v,k) - if v == true - then return ' '..k - elseif v then return f(' %s="%s"', k, v) - end - end, attrs)) or '') - end - - tagproc = { - toTXT = { - tag = function(t,a,v) return v end; - elt = function(t,a) return '' end; - catenate = table.concat; - }; - toIR = { - tag = function(t,a,v,o) return { - tag = t, attrs = a; - nodes = type(v) == 'string' and {v} or v, src = o - } end; - - elt = function(t,a,o) return { - tag = t, attrs = a, src = o - } end; - - catenate = function(...) return ... end; - }; - toHTML = { - elt = elt; - tag = function(t,attrs,body) - return f('%s%s', elt(t,attrs), body, t) - end; - catenate = table.concat; - }; - } - end - - local function getBaseRenderers(procs, span_renderers) - local tag, elt, catenate = procs.tag, procs.elt, procs.catenate - local htmlDoc = function(title, head, body) - return [[]] .. tag('html',nil, - tag('head', nil, - elt('meta',{charset = 'utf-8'}) .. - (title and tag('title', nil, title) or '') .. - (head or '')) .. - tag('body', nil, body or '')) - end - - local function htmlSpan(spans, block, sec) - local text = {} - for k,v in pairs(spans) do - if type(v) == 'string' then - v=v:gsub('[<>&"]', function(x) - return string.format('&#%02u;', string.byte(x)) - end) - for fn, ext in renderJob:each('hook','render_html_sanitize') do - v = fn(renderJob:delegate(ext), v) - end - table.insert(text,v) - else - table.insert(text, (span_renderers[v.kind](v, block, sec))) - end - end - return table.concat(text) - end - return {htmlDoc=htmlDoc, htmlSpan=htmlSpan} - end - - local spanparse = function(...) - local s = ct.parse_span(...) - doc.docjob:hook('meddle_span', s) - return s - end - - local cssRulesFor = {} - local function getSpanRenderers(procs) - local tag, elt, catenate = procs.tag, procs.elt, procs.catenate - local span_renderers = {} - local plainrdr = getBaseRenderers(tagproc.toTXT, span_renderers) - local htmlSpan = getBaseRenderers(procs, span_renderers).htmlSpan - - function span_renderers.format(sp,...) - local tags = { strong = 'strong', emph = 'em', strike = 'del', insert = 'ins', literal = 'code', variable = 'var'} - if sp.style == 'literal' and not opts['fossil-uv'] then - stylesNeeded.code = true - elseif sp.style == 'strike' or sp.style == 'insert' then - stylesNeeded.editors_markup = true - elseif sp.style == 'variable' then - stylesNeeded.var = true - end - return tag(tags[sp.style],nil,htmlSpan(sp.spans,...)) - end - - function span_renderers.deref(t,b,s) - local r = b.origin:ref(t.ref) - local name = t.ref - if name:find'%.' then name = name:match '^[^.]*%.(.+)$' end - if type(r) == 'string' then - stylesNeeded.abbr = true - return tag('abbr',{title=r},next(t.spans) and htmlSpan(t.spans,b,s) or name) - end - if r.kind == 'resource' then - local rid = getSafeID(r, 'res-') - if r.class == 'image' then - if not cssRulesFor[r] then - local css = prepcss(string.format([[ - section p > .%s { - } - ]], rid)) - stylesets[r] = css - cssRulesFor[r] = css - stylesNeeded[r] = true - end - return tag('div',{class=rid},catenate{'blaah'}) - elseif r.class == 'video' then - local vid = {} - return tag('video',nil,vid) - elseif r.class == 'font' then - b.origin:fail('fonts cannot be instantiated, use %font directive instead') - end - else - b.origin:fail('%s is not an object that can be embedded', t.ref) - end - end - - function span_renderers.var(v,b,s) - local val - if v.pos then - if not v.origin.invocation then - v.origin:fail 'positional arguments can only be used in a macro invocation' - elseif not v.origin.invocation.args[v.pos] then - v.origin.invocation.origin:fail('macro invocation %s missing positional argument #%u', v.origin.invocation.macro, v.pos) - end - val = v.origin.invocation.args[v.pos] - else - val = v.origin.doc:context_var(v.var, v.origin) - end - if v.raw then - return val - else - return htmlSpan(ct.parse_span(val, v.origin), b, s) - end - end - - function span_renderers.raw(v,b,s) - return htmlSpan(v.spans, b, s) - end - - function span_renderers.link(sp,b,s) - local href - if b.origin.doc.sections[sp.ref] then - href = '#' .. sp.ref - else - if sp.addr then href = sp.addr else - local r = b.origin:ref(sp.ref) - if type(r) == 'table' then - href = '#' .. getSafeID(r) - else href = r end - end - end - return tag('a',{href=href},next(sp.spans) and htmlSpan(sp.spans,b,s) or href) - end - - span_renderers['line-break'] = function(sp,b,s) - return elt('br') - end - - function span_renderers.macro(m,b,s) - local macroname = plainrdr.htmlSpan( - ct.parse_span(m.macro, b.origin), b,s) - local r = b.origin:ref(macroname) - if type(r) ~= 'string' then - b.origin:fail('%s is an object, not a reference', t.ref) - end - local mctx = b.origin:clone() - mctx.invocation = m - return htmlSpan(ct.parse_span(r, mctx),b,s) - end - function span_renderers.math(m,b,s) - stylesNeeded.math = true - return tag('span',{class='equation'},htmlSpan(m.spans, b, s)) - end; - function span_renderers.directive(d,b,s) - if d.ext == 'html' then - elseif b.origin.doc:allow_ext(d.ext) then - elseif d.crit then - b.origin:fail('critical extension %s unavailable', d.ext) - elseif d.failthru then - return htmlSpan(d.spans, b, s) - end - end - function span_renderers.footnote(f,b,s) - stylesNeeded.footnote = true - local source, sid, ssec = b.origin:ref(f.ref) - local cnc = getSafeID(ssec) .. ' ' .. sid - local fn - if footnotes[cnc] then - fn = footnotes[cnc] - else - footnotecount = footnotecount + 1 - fn = {num = footnotecount, origin = b.origin, fnid=cnc, source = source} - fn.id = getSafeID(fn) - footnotes[cnc] = fn - end - return tag('a', {href='#'..fn.id}, htmlSpan(f.spans) .. - tag('sup',nil, fn.num)) - end - - return span_renderers - end - - local function getBlockRenderers(procs, sr) - local tag, elt, catenate = procs.tag, procs.elt, procs.catenate - local null = function() return catenate{} end - - local block_renderers = { - anchor = function(b,s) - return tag('a',{id = getSafeID(b)},null()) - end; - paragraph = function(b,s) - stylesNeeded.paragraph = true; - return tag('p', nil, sr.htmlSpan(b.spans, b, s), b) - end; - directive = function(b,s) - -- deal with renderer directives - local _, cmd, args = b.words(2) - if cmd == 'page-title' then - if not opts.title then doctitle = args end - elseif b.critical then - b.origin:fail('critical HTML renderer directive ā€œ%sā€ not supported', cmd) - end - end; - label = function(b,s) - if ct.sec.is(b.captions) then - if not (opts['fossil-uv'] or opts.snippet) then - stylesNeeded.header = true - end - local h = math.min(6,math.max(1,b.captions.depth)) - return tag(f('h%u',h), nil, sr.htmlSpan(b.spans, b, s), b) - else - -- handle other uses of labels here - end - end; - ['list-item'] = function(b,s) - return tag('li', nil, sr.htmlSpan(b.spans, b, s), b) - end; - table = function(b,s) - local tb = {} - for i, r in ipairs(b.rows) do - local row = {} - for i, c in ipairs(r) do - table.insert(row, tag(c.header and 'th' or 'td', - {align=c.align}, sr.htmlSpan(c.spans, b))) - end - table.insert(tb, tag('tr',nil,catenate(row))) - end - return tag('table',nil,catenate(tb)) - end; - listing = function(b,s) - stylesNeeded.block_code_listing = true - local nodes = ss.map(function(l) - if #l > 0 then - return tag('div',nil,sr.htmlSpan(l, b, s)) - else - return elt('hr') - end - end, b.lines) - if b.title then - table.insert(nodes,1, tag('figcaption',nil,sr.htmlSpan(b.title))) - end - if b.lang then langsused[b.lang] = true end - return tag('figure', {class='listing', lang=b.lang, id=b.id and getSafeID(b)}, catenate(nodes)) - end; - aside = function(b,s) - local bn = {} - stylesNeeded.aside = true - if #b.lines == 1 then - bn[1] = sr.htmlSpan(b.lines[1], b, s) - else - for _,v in pairs(b.lines) do - table.insert(bn, tag('p', {}, sr.htmlSpan(v, b, s))) - end - end - return tag('aside', {}, bn) - end; - ['break'] = function() -- HACK - -- lists need to be rewritten to work like asides - return ''; - end; - } - return block_renderers; - end - - local function getRenderers(procs) - local span_renderers = getSpanRenderers(procs) - local r = getBaseRenderers(procs,span_renderers) - r.block_renderers = getBlockRenderers(procs, r) - return r - end - - local astproc = { - toHTML = getRenderers(tagproc.toHTML); - toTXT = getRenderers(tagproc.toTXT); - toIR = { }; - } - astproc.toIR.span_renderers = ss.clone(astproc.toHTML); - astproc.toIR.block_renderers = getBlockRenderers(tagproc.toIR,astproc.toHTML); - -- note we use HTML here instead of IR span renderers, because as things - -- currently stand we don't need that level of resolution. if we ever - -- get to the point where we want to be able to twiddle spans around - -- we'll need to introduce an IR span renderer - - render_state_handle.astproc = astproc; - render_state_handle.tagproc = tagproc; - - -- bind to legacy names - -- yikes this needs to be cleaned up so badly - local ir = {} - local dr = astproc.toHTML -- default renderers - local plainr = astproc.toTXT - local irBlockRdrs = astproc.toIR.block_renderers; - - render_state_handle.ir = ir; - - local function renderBlocks(blocks, irs) - for i, block in ipairs(blocks) do - local rd - if irBlockRdrs[block.kind] then - rd = irBlockRdrs[block.kind](block,sec) - else - local rdr = renderJob:proc('render',block.kind,'html') - if rdr then - rd = rdr({ - state = render_state_handle; - tagproc = tagproc.toIR; - astproc = astproc.toIR; - }, block, sec) - end - end - if rd then - if opts['heading-anchors'] and block == sec.heading_node then - stylesNeeded.headingAnchors = true - table.insert(rd.nodes, ' ') - table.insert(rd.nodes, { - tag = 'a'; - attrs = {href = '#' .. irs.attrs.id, class='anchor'}; - nodes = {type(opts['heading-anchors'])=='string' and opts['heading-anchors'] or '§'}; - }) - end - if rd.src and rd.src.origin.lang then - if not rd.attrs then rd.attrs = {} end - rd.attrs.lang = rd.src.origin.lang - end - table.insert(irs.nodes, rd) - runhook('ir_section_node_insert', rd, irs, sec) - end - end - end - runhook('ir_assemble', ir) - for i, sec in ipairs(doc.secorder) do - if doctitle == nil and sec.depth == 1 and sec.heading_node then - doctitle = astproc.toTXT.htmlSpan(sec.heading_node.spans, sec.heading_node, sec) - end - local irs - if sec.kind == 'ordinary' then - if #(sec.blocks) > 0 then - irs = {tag='section',attrs={id = getSafeID(sec)},nodes={}} - runhook('ir_section_build', irs, sec) - renderBlocks(sec.blocks, irs) - end - elseif sec.kind == 'blockquote' then - elseif sec.kind == 'listing' then - elseif sec.kind == 'embed' then - end - if irs then table.insert(ir, irs) end - end - - for _, fn in pairs(footnotes) do - local tag = tagproc.toIR.tag - local body = {nodes={}} - local ftir = {} - for l in fn.source:gmatch('([^\n]*)') do - ct.parse_line(l, fn.origin, ftir) - end - renderBlocks(ftir,body) - local note = tag('div',{class='footnote',id=fn.id}, { - tag('div',{class='number'}, tostring(fn.num)), - tag('div',{class='text'}, body.nodes), - tag('a',{href='#0'},'close') - }) - table.insert(ir, note) - end - - -- restructure passes - runhook('ir_restructure_pre', ir) - - ---- list insertion pass - local lists = {} - for _, sec in pairs(ir) do - if sec.tag == 'section' then - local i = 1 while i <= #sec.nodes do local v = sec.nodes[i] - if v.tag == 'li' then - local ltag - if v.src.ordered - then ltag = 'ol' - else ltag = 'ul' - end - local last = i>1 and sec.nodes[i-1] - if last and last.embed == 'list' and not ( - last.ref[#last.ref].src.depth == v.src.depth and - last.ref[#last.ref].src.ordered ~= v.src.ordered - ) then - -- add to existing list - table.insert(last.ref, v) - table.remove(sec.nodes, i) i = i - 1 - else - -- wrap in list - local newls = {v} - sec.nodes[i] = {embed = 'list', ref = newls} - table.insert(lists,newls) - end - end - i = i + 1 end - end - end - - for _, sec in pairs(ir) do - if sec.tag == 'section' then - for i, elt in pairs(sec.nodes) do - if elt.embed == 'list' then - local function fail_nest() - elt.ref[1].src.origin:fail('improper list nesting') - end - local struc = {attrs={}, nodes={}} - if elt.ref[1].src.ordered then struc.tag = 'ol' else struc.tag = 'ul' end - if elt.ref[1].src.depth ~= 1 then fail_nest() end - - local stack = {struc} - local copyNodes = function(old,new) - for i,v in ipairs(old) do new[#new + i] = v end - end - for i,e in ipairs(elt.ref) do - if e.src.depth > #stack then - if e.src.depth - #stack > 1 then fail_nest() end - local newls = {attrs={}, nodes={e}} - copyNodes(e.nodes,newls) - if e.src.ordered then newls.tag = 'ol' else newls.tag='ul' end - table.insert(stack[#stack].nodes[#stack[#stack].nodes].nodes, newls) - table.insert(stack, newls) - else - if e.src.depth < #stack then - -- pop entries off the stack - for i=#stack, e.src.depth+1, -1 do stack[i] = nil end - end - table.insert(stack[#stack].nodes, e) - end - end - - sec.nodes[i] = struc - end - end - end - end - - runhook('ir_restructure_post', ir) - - -- collection pass - local function collect_nodes(t) - local ts = '' - for i,v in ipairs(t) do - if type(v) == 'string' then - ts = ts .. v - elseif v.nodes then - ts = ts .. tagproc.toHTML.tag(v.tag, v.attrs, collect_nodes(v.nodes)) - elseif v.text then - ts = ts .. tagproc.toHTML.tag(v.tag,v.attrs,v.text) - else - ts = ts .. tagproc.toHTML.elt(v.tag,v.attrs) - end - end - return ts - end - local body = collect_nodes(ir) - - for k in pairs(langsused) do - local spec = langpairs[k] or {color=0xaaaaaa} - stylesets.block_code_listing = stylesets.block_code_listing .. string.format( - [[section > figure.listing[lang="%s"]>figcaption::after - { content: '%s'; color: #%06x }]], - k, spec.name or k, spec.color) - end - - local prepcss = function(css) - local tone = function(fac, sat, sep, alpha) - local hsl = function(h,s,l,a) - local v = string.format('%s, %u%%, %u%%', h,s,l) - if a then - return string.format('hsla(%s, %s)', v,a) - else - return string.format('hsl(%s)', v) - end - end - sat = sat or 1 - fac = math.max(math.min(fac, 1), 0) - sat = math.max(math.min(sat, 1), 0) - if opts.accent then - local hue = 'var(--accent)' - local hsep = tonumber(opts['hue-spread']) - if hsep and sep and sep ~= 0 then - hue = string.format('calc(%s - %s)', hue, sep * hsep) - end - return hsl(hue, math.floor(100*sat), math.floor(100*fac), alpha) - else - local g = math.floor(0xFF * fac) - return string.format('#' .. string.rep('%02x',alpha and 4 or 3), g,g,g,alpha and math.floor(0xFF*alpha)) - end - end - local replace = function(var,alpha,param) - local tonespan = opts.accent and .1 or 0 - local tbg = opts['dark-on-light'] and 1.0 - tonespan or tonespan - local tfg = opts['dark-on-light'] and tonespan or 1.0 - tonespan - if var == 'bg' then - return tone(tbg,nil,nil,tonumber(alpha)) - elseif var == 'fg' then - return tone(tfg,nil,nil,tonumber(alpha)) - elseif var == 'width' then - return opts['width'] or '100vw' - elseif var == 'tone' then - local l, sep, sat - for i=1,3 do -- šŸ™„ - l,sep,sat = param:match('^%('..string.rep('([^%s]*)%s*',i)..'%)$') - if l then break end - end - l = ss.math.lerp(tonumber(l), tbg, tfg) - return tone(l, tonumber(sat), tonumber(sep), tonumber(alpha)) - end - end - css = css:gsub('@(%b[]):(%b[])', function(v,d) return opts[v:sub(2,-2)] or v:sub(2,-2) end) - css = css:gsub('@(%w+)/([0-9.]+)(%b())', replace) - css = css:gsub('@(%w+)(%b())', function(a,b) return replace(a,nil,b) end) - css = css:gsub('@(%w+)/([0-9.]+)', replace) - css = css:gsub('@(%w+)', function(a,b) return replace(a,nil,b) end) - return (css:gsub('%s+',' ')) - end - - local styles = {} - if opts.width then - table.insert(styles, string.format([[body {padding:0 1em;margin:auto;max-width:%s}]], opts.width)) - end - if opts.accent then - table.insert(styles, string.format(':root {--accent:%s}', opts.accent)) - end - if opts.accent or (not opts['dark-on-light']) and (not opts['fossil-uv']) then - stylesNeeded.accent = true - end - - - for k in pairs(stylesNeeded) do - if not stylesets[k] then ct.exns.unimpl('styleset %s not implemented (!)', k):throw() end - table.insert(styles, prepcss(stylesets[k])) - end - - local head = {} - local styletag = '' - if opts['link-css'] then - local css = opts['link-css'] - if type(css) ~= 'string' then ct.exns.mode('must be a string', 'html:link-css'):throw() end - styletag = styletag .. tagproc.toHTML.elt('link',{rel='stylesheet',type='text/css',href=opts['link-css']}) - end - if next(styles) then - if opts['gen-styles'] then - styletag = styletag .. tagproc.toHTML.tag('style',{type='text/css'},table.concat(styles)) - end - table.insert(head, styletag) - end - - if opts['fossil-uv'] then - return tagproc.toHTML.tag('div',{class='fossil-doc',['data-title']=doctitle},styletag .. body) - elseif opts.snippet then - return styletag .. body - else - return dr.htmlDoc(doctitle, next(head) and table.concat(head), body) - end end + +-- renderer engines do -- define span control sequences local function formatter(sty) return function(s,c) @@ -1477,9 +643,9 @@ local m = {s} --TODO return { kind = 'math'; original = s; - spans = m; + spans = {s}; origin = c:clone(); }; end}; {seq = '&', parse = function(s, c) @@ -1486,9 +652,9 @@ local r, t = s:match '^([^%s]+)%s*(.-)$' return { kind = 'deref'; spans = (t and t ~= "") and ct.parse_span(t, c) or {}; - ref = r; + ref = r; origin = c:clone(); } end}; {seq = '^', parse = function(s, c) @@ -1579,9 +745,9 @@ local substr, following = delimited('[',']',str:sub(p.byte)) p.next.byte = following + p.byte local found = false for _,i in pairs(ct.spanctls) do - if startswith(substr, i.seq) then + if ss.str.begins(substr, i.seq) then found = true table.insert(spans, i.parse(substr:sub(1+#i.seq), ctx)) break end