Index: cortav.lua ================================================================== --- cortav.lua +++ cortav.lua @@ -396,17 +396,87 @@ return ret 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 local objid = pfx .. obj.id @@ -427,917 +497,13 @@ ids[cid] = obj 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) return { @@ -1476,20 +642,20 @@ end) local m = {s} --TODO return { kind = 'math'; original = s; - spans = m; + spans = {s}; origin = c:clone(); }; end}; {seq = '&', parse = function(s, c) 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) local fn, t = s:match '^([^%s]+)%s*(.-)$' @@ -1578,11 +744,11 @@ flush() 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 end Index: makefile ================================================================== --- makefile +++ makefile @@ -1,11 +1,35 @@ +# [สž] makefile +# ~ lexi hale +# ๐Ÿ„ฏ AGPLv3 +# ? this script performs the tasks necessary to produce a mostly +# standalone cortav executable from the source files in the +# repository. it assumes the presence of the following tools +# in $SHELL or in $PATH: +# +# * which * cat +# * mkdir * echo +# * install * lua +# * luac * sh +# +# if any are not present, the build will fail, although a missing +# `which` can be worked around by specifying the paths to lua, luac, +# and `sh` directly +# +# eventually you will be able to set a "standalone" variable to +# create a truly standalone binary, by embedding the binary in a +# C program and statically linking it to lua. + lua != which lua luac != which luac sh != which sh extens = $(wildcard ext/*.lua) extens-names ?= $(basename $(notdir $(extens))) +rendrs = $(wildcard render/*.lua) +rendrs-names ?= $(basename $(notdir $(rendrs))) + build = build executable = cortav default-format-flags = -m html:width 40em prefix = $(HOME)/.local @@ -20,12 +44,13 @@ # ($ make encoding-data=) encoding-data = ucstbls encoding-files = $(patsubst %,$(build)/%.lc,$(encoding-data)) encoding-data-ucs = https://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt -$(build)/$(executable): sirsem.lua $(encoding-files) cortav.lua $(extens) cli.lua | $(build)/ +$(build)/$(executable): sirsem.lua $(encoding-files) cortav.lua $(rendrs) $(extens) cli.lua | $(build)/ @echo ' ยป building with extensions $(extens-names)' + @echo ' ยป building with renderers $(rendrs-names)' echo '#!$(lua)' > $@ luac -o - $^ >> $@ chmod +x $@ $(build)/cortav.html: cortav.ct $(build)/$(executable) | $(build)/ @@ -54,11 +79,11 @@ %/: mkdir -p $@ $(build)/unicode.txt: | $(build)/ curl $(encoding-data-ucs) > $@ -$(build)/ucstbls.lc: $(build)/unicode.txt | $(build)/ +$(build)/ucstbls.lc: $(build)/unicode.txt tools/ucs.lua | $(build)/ $(lua) tools/ucs.lua $< | $(luac) -o $@ - .PHONY: install install: $(build)/cortav $(build)/cortav-view.sh $(build)/velartrill-cortav-view.desktop | $(bin-prefix)/ install $(build)/$(executable) $(bin-prefix) ADDED render/html.lua Index: render/html.lua ================================================================== --- render/html.lua +++ render/html.lua @@ -0,0 +1,937 @@ +local ct = require 'cortav' +local ss = require 'sirsem' + +-- install rendering function for html +function ct.render.html(doc, opts) + local doctitle = opts['title'] + local f = string.format + local getSafeID = ct.tool.namespace() + + 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 + local spans = {} + local function fmt(sp, target) + for i,v in ipairs(sp) do + if type(v) == 'string' then + local x = ct.tool.mathfmt(b.origin, v) + for _,v in ipairs(x) do + table.insert(target, v) + end + elseif type(v) == 'table' then + if v.spans then + local tbl = ss.delegate(v) + tbl.spans = {} + fmt(v.spans, tbl.spans) + table.insert(target, tbl) + else + table.insert(target, v) + end + end + end + end + fmt(m.spans,spans) + + return tag('span',{class='equation'},htmlSpan(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 Index: sirsem.lua ================================================================== --- sirsem.lua +++ sirsem.lua @@ -214,30 +214,30 @@ iswhitespace = function(c) return (c == ' ') or (c == '\t') or (c == '\n') end; ranges = { {0x00,0x1a, cc.ctl}; - {0x1b,0x1b, cc.ctl, cp.disallow}; + {0x1b,0x1b, cc.ctl | cp.disallow}; {0x1c,0x1f, cc.ctl}; {0x20,0x20, cc.space}; {0x21,0x22, cc.punct}; {0x23,0x26, cc.symbol}; {0x27,0x29, cc.punct}; {0x2a,0x2b, cc.symbol}; {0x2c,0x2f, cc.punct}; - {0x30,0x39, cc.numeral, cp.hexnumeral}; + {0x30,0x39, cc.numeral | cp.hexnumeral}; {0x3a,0x3b, cc.punct}; - {0x3c,0x3e, cc.symbol, cp.mathop}; + {0x3c,0x3e, cc.symbol | cp.mathop}; {0x3f,0x3f, cc.punct}; {0x40,0x40, cc.symbol}; - {0x41,0x46, cc.letter, cp.ucase, cp.hexnumeral}; - {0x47,0x5a, cc.letter, cp.ucase}; - {0x5b,0x5d, cc.symbol, cp.mathop}; - {0x5e,0x5e, cc.symbol, mathop}; + {0x41,0x46, cc.letter | cp.upper | cp.hexnumeral}; + {0x47,0x5a, cc.letter | cp.upper}; + {0x5b,0x5d, cc.symbol | cp.mathop}; + {0x5e,0x5e, cc.symbol | cp.mathop}; {0x5f,0x60, cc.symbol}; - {0x61,0x66, cc.letter, cp.lcase, cp.hexnumeral}; - {0x67,0x7a, cc.letter, cp.lcase}; + {0x61,0x66, cc.letter | cp.lower | cp.hexnumeral}; + {0x67,0x7a, cc.letter | cp.lower}; {0x7b,0x7e, cc.symbol}; {0x7f,0x7f, cc.ctl, cp.disallow}; } }; raw = {len = string.len; char = string.char; codepoint = string.byte; @@ -252,11 +252,11 @@ -- files and injected through a generated source file. if this -- part of the build process is disabled (e.g. due to lack of -- internet access, or to keep the size of the executable as -- small as possible), we still at least can make the ascii -- ranges available to UTF8 (UTF8 being a superset of ascii) -ss.str.enc.utf8.ranges = ss.delegate(ss.str.enc.ascii.ranges) +ss.str.enc.utf8.ranges = ss.str.enc.ascii.ranges function ss.str.enc.ascii.encodeUCS(str) local newstr = '' for c,p in ss.str.each(ss.str.enc.utf8, str, true) do if c > 0x7F then @@ -268,15 +268,39 @@ end for _, v in pairs{'utf8','ascii','raw'} do ss.str.enc[v].parse_escape = ss.str.enc_generics.pfxescape('\\',ss.str.enc[v]) end + +function ss.bitmask_expand(ty, v) + local bitrange = ty[true] + local fb + if bitrange[1] ~= 0 then + fb = v & ((1<= r[1] and ch <= r[2] then + local p,b = ss.bitmask_expand(ss.str.charprop, r[3]) + if b then p[ss.str.charclass[b]] = true end + return p + end + end + + return {} end function ss.str.each(enc, str, ascode) if enc.each then return enc.each(enc,str,ascode) end Index: tools/ucs.lua ================================================================== --- tools/ucs.lua +++ tools/ucs.lua @@ -23,50 +23,13 @@ if arg[1] then path = arg[1] file = io.open(path, 'rb') end -local bitmask_raw = function(n,ofs) - ofs = ofs or 0 - local function rec(i) - if i > n then return end - return 1<<(i+ofs), rec(i+1) - end - return 1< 0x7f then -- discard ASCII, we already have that +-- if v[1] > 0x7f then -- discard ASCII, we already have that local code = { codepoint = v[1]; name = v[2]; class = v[3]; kind = v[5]; @@ -142,11 +105,11 @@ end if code.cat ~= 0 then table.insert(recs,code) end - end +-- end end local ranges = {} local last = recs[1] @@ -181,29 +144,15 @@ end last = r end flush() --- expand bitmask - -- for k,v in pairs(ranges) do - -- local basic = v[3] & ((1<<3) - 1) -- first three bits - -- if basic ~= 0 then - -- v[4] = basictype[basic] - -- end - -- local bitrange = props[true] - -- for j=bitrange[1], bitrange[2] do - -- if (v[3] & (1<