Index: cortav.ct
==================================================================
--- cortav.ct
+++ cortav.ct
@@ -137,11 +137,11 @@
* footnote {span ^|ref|[$styled-text]}: annotates the text with a defined footnote. in interactive output media [`\[^citations.qtheo Quantum Theosophy: A Neophyte's Catechism\]] will insert a link with the text [`Quantum Theosophy: A Neophyte's Catechism] that, when clicked, causes a footnote to pop up on the screen. for static output media, the text will simply have a superscript integer after it denoting where the footnote is to be found.
* superscript {obj '|[$styled-text]}
* subscript {obj ,|[$styled-text]}
* raw {obj \\ |[$raw-text]}: causes all characters within to be interpreted literally, without expansion. the only special characters are square brackets, which must have a matching closing bracket, and backslashes.
* raw literal [` \["[$raw-text]\]]: shorthand for a raw inside a literal, that is ["[`[\\ā¦]]]
-* macro [` \{[$name] [$arguments]}]: invokes a [>ex.mac macro], specified with a reference
+* macro [` \{[$name] [$arguments]}]: invokes a [>ex.mac macro] inline, specified with a reference. if the result of macro expansion contains newlines, they will be treated as line breaks, rather than paragraph breaks as they would be in a multiline context.
* argument {obj #|var}: in macros only, inserts the [$var]-th argument. otherwise, inserts a context variable provided by the renderer.
* raw argument {obj ##|var}: like above, but does not evaluate [$var].
* term {obj &|name}, {span &|name|[$expansion]}: quotes a defined term with a link to its definition, optionally with a custom expansion of the term (for instance, to expand the first use of an acronym)
* inline image {obj &@|name}: shows a small image or other object inline. the unicode character [`š¼] can also be used instead of [`&@].
* unicode codepoint {obj U+|hex-integer}: inserts an arbitrary UCS codepoint in the output, specified by [$hex-integer]. lowercase [`u] is also legal.
Index: cortav.lua
==================================================================
--- cortav.lua
+++ cortav.lua
@@ -131,10 +131,18 @@
return self.sec.refs[id], id, self.sec
else self:fail("no such ref %s in current section", id or '') end
else
local sec, ref = string.match(id, "(.-)%.(.+)")
local s = self.doc.sections[sec]
+ if not s then -- fall back on inheritance tree
+ for i, p in ipairs(self.doc.parents) do
+ if p.sections[sec] then
+ s = p.sections[sec]
+ break
+ end
+ end
+ end
if s then
if s.refs[ref] then
return s.refs[ref], ref, sec
else self:fail("no such ref %s in section %s", ref, sec) end
else self:fail("no such section %s", sec) end
@@ -151,11 +159,11 @@
depth = 0;
kind = 'ordinary';
} end;
construct = function(self, id, depth)
self.id = id
- self.depth = depth
+ self.depth = depth or self.depth
end;
fns = {
visible = function(self)
if self.kind == 'nonprinting' then return false end
local invisibles = {
@@ -266,10 +274,11 @@
-- are intentionally excluded here; subdocs can have their own vars
-- without losing access to parent vars
local nctx = ctx:clone()
nctx:init(newdoc, ctx.src)
nctx.line = ctx.line
+ nctx.docDepth = (ctx.docDepth or 0) + ctx.sec.depth - 1
return newdoc, nctx
end;
};
mk = function(...) return {
sections = {};
@@ -832,15 +841,17 @@
local function
blockwrap(fn)
return function(l,c,j,d)
local block = fn(l,c,j,d)
- block.origin = c:clone();
- table.insert(d, block);
- j:hook('block_insert', c, block, l)
- if block.spans then
- c.doc.docjob:hook('meddle_span', block.spans, block)
+ if block then
+ block.origin = c:clone();
+ table.insert(d, block);
+ j:hook('block_insert', c, block, l)
+ if block.spans then
+ c.doc.docjob:hook('meddle_span', block.spans, block)
+ end
end
end
end
local insert_paragraph = blockwrap(function(l,c)
@@ -1050,11 +1061,15 @@
local last = d[#d]
if (not last) or (last.kind ~= 'reference') then
c:fail('reference continuations must immediately follow a reference')
end
local str = l:match '^\t\t(.-)%s*$'
- last.val = last.val .. '\n' .. str
+ if last.val == '' then
+ last.val = str
+ else
+ last.val = last.val .. '\n' .. str
+ end
c.sec.refs[last.key] = last.val
end};
{seq = '\t', pred = function(l)
return (l:match '\t+([^:]+):%s*(.*)$')
end; fn = blockwrap(function(l,c,j,d)
@@ -1061,10 +1076,11 @@
local ref, val = l:match '\t+([^:]+):%s*(.*)$'
local last = d[#d]
local rsrc
if last and last.kind == 'resource' then
last.props[ref] = val
+ j:hook('rsrc_set_prop', c, last, ref, val, l)
rsrc = last
elseif last and last.kind == 'reference' and last.rsrc then
last.rsrc.props[ref] = val
rsrc = last.rsrc
else
@@ -1120,27 +1136,27 @@
end
elseif crit == '!' then
c:fail('critical directive %s not supported',cmd)
end
end;};
- {pred = function(s) return s:match '^(>+)([^%s]*)%s*(.*)$' end,
+ {pred = function(s) return s:match '^>[^>%s]*%s*.*$' end,
fn = function(l,c,j,d)
- local lvl,id,txt = l:match '^(>+)([^%s]*)%s*(.*)$'
- lvl = utf8.len(lvl)
+ local id,txt = l:match '^>([^>%s]*)%s*(.*)$'
+ if id == '' then id = nil end
local last = d[#d]
local node
local ctx
- if last and last.kind == 'quote' and (id == nil or id == '' or id == last.id) then
+ if last and last.kind == 'quote' and (id == nil or id == last.id) then
node = last
ctx = node.ctx
ctx.line = c.line -- is this enough??
else
local doc
doc, ctx = c.doc:sub(c)
- node = { kind = 'quote', doc = doc, ctx = ctx, id = id }
- j:hook('block_insert', c, node, l)
+ node = { kind = 'quote', doc = doc, ctx = ctx, id = id, origin = c }
table.insert(d, node)
+ j:hook('block_insert', c, node, l)
end
ct.parse_line(txt, ctx, ctx.sec.blocks)
end};
{seq = '~~~', fn = blockwrap(function(l,c,j)
@@ -1178,11 +1194,10 @@
c.mode = mode
if id then
if c.sec.refs[id] then c:fail('duplicate ID %s', id) end
c.sec.refs[id] = c.mode.listing
end
- j:hook('block_insert', c, mode.listing, l)
return c.mode.listing;
end)};
{pred = function(s,c)
if s:match '^[%-_][*_%-%s]+' then return true end
if startswith(s, 'ā') then
@@ -1195,33 +1210,118 @@
return true
end
end; fn = blockwrap(function()
return { kind = 'horiz-rule' }
end)};
- {seq='@', fn=blockwrap(function(s,c)
- local id = s:match '^@%s*(.-)%s*$'
+ {seq='@', fn=function(s,c,j,d)
+ local function mirror(b)
+ local ch = {}
+ local rev = {
+ ['['] = ']'; [']'] = '[';
+ ['{'] = '}'; ['}'] = '{';
+ ['('] = ')'; [')'] = '(';
+ ['<'] = '>'; ['>'] = '<';
+ }
+ for i = 1,#b do
+ local c = string.sub(b,-i,-i)
+ if rev[c] then
+ ch[i] = rev[c]
+ else
+ ch[i] = c
+ end
+ end
+ return table.concat(ch)
+ end
+
+ local id,rest = s:match '^@([^%s]*)%s*(.*)$'
+ local bs, brak = rest:match '()([{[(<][^%s]*)%s*$'
+ local src
+ if brak then
+ src = rest:sub(1,bs-1):gsub('%s+$','')
+ else src = rest end
+ if src == '' then src = nil end
+ if id == '' then id = nil end
local rsrc = {
kind = 'resource';
- props = {};
+ props = {src = src};
id = id;
+ origin = c;
}
- if c.sec.refs[id] then
- c:fail('an object with id ā%sā already exists in that section',id)
+ if brak then
+ rsrc.bracket = {
+ open = brak;
+ close = mirror(brak);
+ }
+ rsrc.raw = '';
+ if src == nil then
+ rsrc.props.src = 'text/x.cortav'
+ end
else
- c.sec.refs[id] = rsrc
+ -- load the raw body, where possible
+ end
+ if id then
+ if c.sec.refs[id] then
+ c:fail('an object with id ā%sā already exists in that section',id)
+ else
+ c.sec.refs[id] = rsrc
+ end
+ end
+ table.insert(d, rsrc)
+ j:hook('block_insert', c, rsrc, s)
+ if id == '' then --shorthand syntax
+ local embed = {
+ kind = 'embed';
+ rsrc = rsrc;
+ origin = c;
+ }
+ table.insert(d, embed)
+ j:hook('block_insert', c, embed, s)
+ end
+
+ if brak then
+ c.mode = {
+ kind = 'inline-rsrc';
+ rsrc = rsrc;
+ indent = nil;
+ depth = 0;
+ }
+ end
+ end};
+ {seq='&$', fn=blockwrap(function(s,c)
+ local id, args = s:match('^&$([^%s]+)%s?(.-)$')
+ if id == nil or id == '' then
+ c:fail 'malformed macro block'
+ end
+ local argv = ss.str.split(c.doc.enc, args, c.doc.enc.encodeUCS'|', {esc=true})
+ return {
+ kind = 'macro';
+ macro = id;
+ args = argv;
+ }
+ end)};
+ {seq='&', fn=blockwrap(function(s,c)
+ local id, cap = s:match('^&([^%s]+)%s*(.-)%s*$')
+ if id == nil or id == '' then
+ c:fail 'malformed embed block'
end
- return rsrc
+ if cap == '' then cap = nil end
+ return {
+ kind = 'embed';
+ ref = id;
+ cap = cap;
+ }
end)};
{fn = insert_paragraph};
}
-function ct.parse_line(l, ctx, dest)
+function ct.parse_line(rawline, ctx, dest)
local newspan
local job = ctx.doc.stage.job
- job:hook('line_read',ctx,l)
- if l then
- l = l:gsub("^ +","") -- trim leading spaces
+ job:hook('line_read',ctx,rawline)
+ local l
+ if rawline then
+ l = rawline:gsub("^ +","") -- trim leading spaces
end
if ctx.mode then
if ctx.mode.kind == 'code' then
if l and l:match '^~~~%s*$' then
job:hook('block_listing_end',ctx,ctx.mode.listing)
@@ -1235,11 +1335,34 @@
else newline = {l}
end
table.insert(ctx.mode.listing.lines, newline)
job:hook('block_listing_newline',ctx,ctx.mode.listing,newline)
end
- elseif ctx.mode.kind == 'quote' then
+ elseif ctx.mode.kind == 'inline-rsrc' then
+ local r = ctx.mode.rsrc
+ if rawline then
+ if rawline == r.bracket.close then
+ if ctx.mode.depth == 0 then
+ -- TODO how to handle depth?
+ ctx.mode = nil
+ end
+ else
+ if r.indent ~= nil then
+ r.raw = r.raw .. '\n'
+ else
+ r.indent = (rawline:sub(1,1) == '\t')
+ end
+
+ if r.indent == true then
+ if rawline:sub(1,1) == '\t' then
+ rawline = rawline:sub(2)
+ end
+ end
+
+ r.raw = r.raw .. rawline
+ end
+ end
else
local mf = job:proc('modes', ctx.mode.kind)
if not mf then
ctx:fail('unimplemented syntax mode %s', ctx.mode.kind)
end
@@ -1310,42 +1433,101 @@
return ctx.doc.enc.iswhitespace(cp)
end
if setup then setup(ctx) end
-
for full_line in file:lines() do ctx.line = ctx.line + 1
- local l
- for p, c in utf8.codes(full_line) do
- if not is_whitespace(c) then
- l = full_line:sub(p)
- break
- end
- end
- ct.parse_line(l, ctx, ctx.sec.blocks)
+ -- local l
+ -- for p, c in utf8.codes(full_line) do
+ -- if not is_whitespace(c) then
+ -- l = full_line:sub(p)
+ -- break
+ -- end
+ -- end
+ ct.parse_line(full_line, ctx, ctx.sec.blocks)
end
for i, sec in ipairs(ctx.doc.secorder) do
- for refid, r in ipairs(sec.refs) do
- if type(r) == 'table' and r.kind == 'resource' and r.props.src then
+ for n, r in pairs(sec.blocks) do
+ if r.kind == 'resource' and r.props.src then
local lines = ss.str.breaklines(ctx.doc.enc, r.props.src)
local srcs = {}
for i,l in ipairs(lines) do
local args = ss.str.breakwords(ctx.doc.enc, l, 2, {escape=true})
- if #args < 3 then
- r.origin:fail('invalid syntax for resource %s', t.ref)
+ if #args > 3 or (r.raw and #args > 2) then
+ r.origin:fail('invalid syntax for resource %s', r.id or '(anonymous)')
+ end
+ local p_mode, p_mime, p_uri
+ if r.raw then
+ p_mode = 'embed'
+ end
+ if #args == 1 then
+ if r.raw then -- inline content
+ p_mime = ss.mime(args[1])
+ else
+ p_uri = args[1]
+ end
+ elseif #args == 2 then
+ local ok, m = pcall(ss.mime, args[1])
+ if r.raw then
+ if not ok then
+ r.origin:fail('invalid mime-type ā%sā', args[1])
+ end
+ p_mode, p_mime = args[1], m
+ else
+ if ok then
+ p_mime, p_uri = m, args[2]
+ else
+ p_mode, p_uri = table.unpack(args)
+ end
+ end
+ else
+ p_mode, p_mime, p_uri = table.unpack(args)
+ p_mime = ss.mime(args[2])
+ end
+ local resource = {
+ mode = p_mode;
+ mime = p_mime or 'text/x.cortav';
+ uri = p_uri and ss.uri(p_uri) or nil;
+ }
+ if resource.mode == 'embed' or resource.mode == 'auto' then
+ -- the resource must be available for reading within this job
+ -- open it and read its source into memory
+ if resource.uri then
+ if resource.uri:canfetch() then
+ resource.raw = resource.uri:fetch()
+ elseif resource.mode == 'auto' then
+ -- resource cannot be accessed; force linking
+ resource.mode = 'link'
+ else
+ r.origin:fail('resource ā%sā wants to embed unfetchable URI ā%sā',
+ r.id or "(anonymous)", tostring(resource.uri))
+ end
+ elseif r.raw then
+ resource.raw = r.raw
+ else
+ r.origin:fail('resource ā%sā is not inline and supplies no URI',
+ r.id or "(anonymous)")
+ end
+
+ -- the resource has been cached. check the mime-type to see if
+ -- we need to parse it or if it is suitable as-is
+
+ if resource.mime.class == "text" then
+ if resource.mime.kind == "x.cortav" then
+ local sd, sc = r.origin.doc:sub(r.origin)
+ local lines = ss.str.breaklines(r.origin.doc.enc, resource.raw, {})
+ for i, ln in ipairs(lines) do
+ sc.line = sc.line + 1
+ ct.parse_line(ln, sc, sc.sec.blocks)
+ end
+ resource.doc = sd
+ end
+ end
end
- local mime = ss.mime(args[2]);
- local class = mimeclasses[mime]
- table.insert(srcs, {
- mode = args[1];
- mime = mime;
- uri = args[3];
- class = class or mime[1];
- })
+ table.insert(srcs, resource)
end
- --ideally move this into its own mimetype lib
r.srcs = srcs
-- note that resources do not themselves have kinds. when a
-- document requests to insert a resource, the renderer must
-- iterate through the sources and find the first source it
-- is capable of emitting. this allows constructions like
@@ -1352,10 +1534,41 @@
-- emitting a video for HTML outputs, a photo for printers,
-- and a screenplay for tty/plaintext outputs.
end
end
end
+
+ -- expand block macros
+ for i, sec in ipairs(ctx.doc.secorder) do
+ for n, r in pairs(sec.blocks) do
+ if r.kind == 'macro' then
+ local mc = r.origin:clone()
+ mc.invocation = r
+ local mac = r.origin:ref(r.macro)
+ if not mac then
+ r.origin:fail('no such reference or resource ā%sā', r.macro)
+ end
+ local subdoc, subctx = ctx.doc:sub(mc)
+ local rawbody
+
+ if type(mac) == 'string' then
+ rawbody = mac
+ elseif mac.raw then
+ rawbody = mac.raw
+ else
+ r.origin:fail('block macro ā%sā must be either a reference or an embedded text/x.cortav resource', r.macro)
+ end
+
+ local lines = ss.str.breaklines(ctx.doc.enc, rawbody)
+ for i, ln in ipairs(lines) do
+ ct.parse_line(ln, subctx, subctx.sec.blocks)
+ end
+ r.doc = subdoc
+ end
+ end
+ end
+
ctx.doc.stage = nil
ctx.doc.docjob:hook('meddle_ast')
return ctx.doc
end
Index: ext/toc.lua
==================================================================
--- ext/toc.lua
+++ ext/toc.lua
@@ -138,12 +138,13 @@
local _, op, val = words(2)
if op == nil then
local toc = {kind='toc'}
ctx:insert(toc)
-- same deal here -- directives are processed as part of
- -- the parse job, which is forked off the document job,
- -- so we need to climb the jobstack
+ -- the parse job, which is forked off the document job;
+ -- if we want state that can persist into the render job,
+ -- we need to climb the jobstack
job:unwind(1).state.toc_custom_position = true
job:hook('ext_toc_position', ctx, toc)
else
ctx:fail 'bad %toc directive'
end
@@ -190,24 +191,47 @@
local top = function() return stack[#stack] end
-- job.doc is the document the render job is bound to, and
-- its secorder field is a list of all the doc's sections in
-- the order they occur ("doc.sections" is a hashmap from name
-- to section object)
- local all = job.doc.secorder
+ local all = {}
+
+ local function blockHasSubdoc(b)
+ local subdocBlockKinds = {
+ quote = true;
+ embed = true;
+ macro = true;
+ }
+ return subdocBlockKinds[b.kind] and ct.doc.is(b.doc)
+ end
+
+ local function scandoc(doc, depth)
+ for i, sec in ipairs(doc.secorder) do
+ table.insert(all, {ref = sec, depth = sec.depth + depth})
+ for j, block in ipairs(sec.blocks) do
+ if blockHasSubdoc(block) then
+ scandoc(block.doc, depth + sec.depth-1)
+ end
+ end
+ end
+ end
+
+ scandoc(job.doc,0)
- for i, sec in ipairs(all) do
+ for i, secptr in ipairs(all) do
+ local sec = secptr.ref
if sec.heading_node then -- does this section have a label?
local ent = tag('li',nil,
catenate{tag('a', {href='#'..getSafeID(sec)},
sr.htmlSpan(sec.heading_node.spans, sec.heading_node, sec))})
- if sec.depth > #stack then
+ if secptr.depth > #stack then
local n = {tag = 'ol', attrs={}, nodes={ent}}
table.insert(top().nodes[#top().nodes].nodes, n)
table.insert(stack, n)
else
- if sec.depth < #stack then
- for j=#stack,sec.depth+1,-1 do stack[j] = nil end
+ if secptr.depth < #stack then
+ for j=#stack,secptr.depth+1,-1 do stack[j] = nil end
end
table.insert(top().nodes, ent)
end
-- now we need to assemble a list of items within the
@@ -236,11 +260,12 @@
attrs = {href = '#' .. l.id};
nodes = {sr.htmlSpan(l.label, l.block, sec)};
}
table.insert(n.nodes, {tag = 'li', attrs = {}, nodes={nn}})
end
- table.insert(ent.nodes, n)
+ table.insert(top().nodes, n)
+ table.insert(stack, n)
end
end
end
return lst
end;
Index: render/groff.lua
==================================================================
--- render/groff.lua
+++ render/groff.lua
@@ -21,14 +21,21 @@
for _, v in pairs{...} do s(v) end
return s
end
local function gsan(str)
- local tocodepoint = function(ch)
- return string.format('\\[u%04X]', utf8.codepoint(ch))
+ -- groff does not support UTF-8
+ local ascii = {}
+ for p,c in utf8.codes(str) do
+ if c > 0x7F or c == 0x27 or c == 0x22 or c == 0x5C then
+ table.insert(ascii, string.format('\\[u%04X]', c))
+ else
+ table.insert(ascii, utf8.char(c))
+ end
end
- str = str:gsub('(["\'\\])',tocodepoint)
+ str = table.concat(ascii)
+ str = str:gsub('\t','\\t') -- tabs are sometimes syntactically meaningful
return str
end
local gtxt = ss.declare {
ident = 'groff-text';
@@ -142,12 +149,14 @@
end;
blocks = {};
prop = {};
block = function(self)
local sub = self:clone()
+ sub.parent = self -- needed for blocks that contain blocks
sub.spans = {}
sub.blocks = nil
+ sub.block = nil
sub.span = function(me, ln)
local p = ss.clone(me.prop)
p.txt = ln
p.block = sub
p.origin = me.origin
@@ -402,34 +411,83 @@
rc.prop.margin = { top = 0.3 }
rc.prop.underline = 0.1
end
function blockRenderers.label(rc, b, sec)
if ct.sec.is(b.captions) then
+ local visDepth = b.captions.depth + (b.origin.docDepth or 0)
local sizes = {36,24,12,8,4,2}
local margins = {0,3}
local dedents = {2.5,1.3,0.8,0.4}
local uls = {3,1.5,0.5,0.25}
- rc.prop.dsz = sizes[b.captions.depth] or 10
- rc.prop.underline = uls[b.captions.depth]
- rc.prop.bold = b.captions.depth > 3
+ rc.prop.dsz = sizes[visDepth] or 10
+ rc.prop.underline = uls[visDepth]
+ rc.prop.bold = visDepth > 3
rc.prop.margin = {
- top = margins[b.captions.depth] or 1;
+ top = margins[visDepth] or 1;
bottom = 0.1;
}
rc.prop.vassure = rc.prop.dsz+70;
- rc.prop.indent = -(dedents[b.captions.depth] or 0)
+ rc.prop.indent = -(dedents[visDepth] or 0)
rc.prop.chtitle = collectText(rc, b.spans, b.spec):compile()
- if b.captions.depth == 1 then
+ if visDepth == 1 then
rc.prop.breakBefore = true
end
rs.renderSpans(rc, b.spans, b, sec)
else
ss.bug 'tried to render label for an unknown object type':throw()
end
end
function blockRenderers.paragraph(rc, b, sec)
rs.renderSpans(rc, b.spans, b, sec)
+ end
+ function blockRenderers.macro(rc, b, sec)
+ local rc = rc.parent:clone()
+ rs.renderDoc(rc, b.doc)
+ end
+ function blockRenderers.quote(rc, b, sec)
+ local rc = rc.parent:clone()
+ rc.prop.indent = (rc.prop.indent or 0) + 1
+ local added = rs.renderDoc(rc, b.doc)
+ -- select last block of last section and increase bottom margin
+ local ap = added[#added].blocks
+ ap = ap[#ap].prop
+ if ap.margin then
+ if ap.margin.bottom then
+ ap.margin.bottom = ap.margin.bottom + 1.1
+ else
+ ap.margin.bottom = 1.1
+ end
+ else
+ ap.margin = {bottom = 1.1}
+ end
+ end
+ function blockRenderers.table(rc, b, sec)
+ function rc:begin(g)
+ g:req 'TS'
+ local aligns = {}
+ for i, c in ipairs(b.rows[1]) do
+ aligns[i] = ({
+ left = 'l';
+ center = 'c';
+ right = 'r';
+ })[c.align] or 'l'
+ end
+ table.insert(aligns, '.')
+ g:txt(table.concat(aligns, ' ') .. '\n')
+
+ local rc_hdr = rc:clone()
+ rc_hdr.prop.bold = true
+ for ri, r in ipairs(b.rows) do
+ for ci, c in ipairs(r) do
+ local sp = collect(c.header and rc_hdr or rc, c.spans, b, sec)
+ for si, s in ipairs(sp) do rs.emitSpan(g,s) end
+ g:raw '\t'
+ end
+ if ri ~= #b.rows then g:raw '\n' end
+ end
+ g:req 'TE'
+ end
end
function rs.renderBlock(rc, b, sec, outerBlockRenderContext)
if blockRenderers[b.kind] then
local rcc = rc:block()
blockRenderers[b.kind](rcc, b, sec)
@@ -558,14 +616,17 @@
else
defer:req'ps'
end
end
- for i,s in pairs(b.spans) do
- rs.emitSpan(gtxt, s)
+ if b.begin then b:begin(gtxt) end
+ if b.spans then
+ for i,s in pairs(b.spans) do
+ rs.emitSpan(gtxt, s)
+ end
end
-
+ if b.complete then b:complete(gtxt) end
if ln.margin then
if ln.margin.bottom then
gtxt:req(string.format('sp %sm', ln.margin.bottom))
end
@@ -574,20 +635,23 @@
defer:flush()
if not ln.margin then gtxt:brk() end
end
- local ir = {}
- for i, sec in ipairs(doc.secorder) do
- if sec.kind == 'ordinary' then
- local rc = mkrc()
- for j, b in ipairs(sec.blocks) do
- rs.renderBlock(rc, b, sec)
+ function rs.renderDoc(gctx, doc, ir) ir = ir or {}
+ for i, sec in ipairs(doc.secorder) do
+ if sec.kind == 'ordinary' then
+ local rc = gctx and gctx:clone() or mkrc()
+ for j, b in ipairs(sec.blocks) do
+ rs.renderBlock(rc, b, sec)
+ end
+ table.insert(ir, {blocks = rc.blocks, src = sec})
end
- table.insert(ir, {blocks = rc.blocks, src = sec})
end
+ return ir
end
+ local ir = rs.renderDoc(nil, doc)
local gd = gtxt()
for i, s in ipairs(ir) do
for j, b in ipairs(s.blocks) do
rs.emitBlock(gd,b)
@@ -615,7 +679,9 @@
end
end
end
macs('.ds doctitle '..doctitle)
- return macs:compile'\n' .. '\n' .. gd:compile()
+ return macs:compile'\n' .. '\n' .. gd:compile() .. '\n'
+ -- if the document doesn't end with the character \n, groff will bitch
+ -- and moan in certain circumstances
end
Index: render/html.lua
==================================================================
--- render/html.lua
+++ render/html.lua
@@ -636,11 +636,11 @@
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)
+ b.origin:fail('%s is an object, not a reference', r.id)
end
local mctx = b.origin:clone()
mctx.invocation = m
return htmlSpan(ct.parse_span(r, mctx),b,s)
end
@@ -734,11 +734,13 @@
label = function(b,s)
if ct.sec.is(b.captions) then
if not (opts['fossil-uv'] or opts.snippet) then
addStyle 'header'
end
- local h = math.min(6,math.max(1,b.captions.depth))
+ -- use correct styling in subdocuments
+ local visDepth = b.captions.depth + (b.origin.docDepth or 0)
+ local h = math.min(6,math.max(1,visDepth))
return tag(f('h%u',h), nil, sr.htmlSpan(b.spans, b, s), b)
else
-- handle other uses of labels here
end
end;
@@ -788,35 +790,206 @@
-- lists need to be rewritten to work like asides
return '';
end;
}
- function block_renderers.quote(b,s)
+ local function renderSubdoc(doc)
local ir = {}
- local toIR = block_renderers
- for i, sec in ipairs(b.doc.secorder) do
+ for i, sec in ipairs(doc.secorder) do
local secnodes = {}
for i, bl in ipairs(sec.blocks) do
- if toIR[bl.kind] then
- table.insert(secnodes, toIR[bl.kind](bl,sec))
+ if block_renderers[bl.kind] then
+ table.insert(secnodes, block_renderers[bl.kind](bl,sec))
end
end
if next(secnodes) then
- if b.doc.secorder[2] then --#secs>1?
+ if doc.secorder[2] then --#secs>1?
-- only wrap in a section if >1 section
table.insert(ir, tag('section',
{id = getSafeID(sec)},
secnodes))
else
ir = secnodes
end
end
end
- return tag('blockquote', b.id and {id=getSafeID(b)} or {}, catenate(ir))
+ return ir
+ end
+
+ local function flatten(t)
+ if t == nil then
+ return ''
+ elseif type(t) == 'string' then
+ return t
+ elseif type(t) == 'table' then
+ if t[1] then
+ return catenate(ss.map(flatten, t))
+ elseif t.tag then
+ return tag(t.tag, t.attrs or {}, flatten(t.nodes))
+ elseif t.elt then
+ return tag(t.elt, t.attrs or {})
+ end
+ end
+ end
+
+ function block_renderers.embed(b,s)
+ local obj
+ if b.rsrc
+ then obj = b.rsrc
+ else obj = b.origin:ref(b.ref)
+ end
+ local function htmlURI(u)
+ local family = u:canfetch()
+ if family == 'file' or
+ (family == 'http' and u.namespace == nil) then
+ -- TODO asset:
+ return u.path
+ else
+ return tostring(u)
+ end
+ end
+ local function uriForSource(s)
+ if s.mode == 'link' or s.mode == 'auto' then
+ return htmlURI(s.uri)
+ elseif s.mode == 'embed' then
+ local mime = s.mime:clone()
+ mime.opts = {}
+ return string.format('data:%s;base64,%s', mime, ss.str.b64e(s.raw))
+ end
+ end
+ --figure out how to embed the given object
+ local embedActs = {
+ {ss.mime'image/*', function(s,ctr)
+ if s == nil then
+ return {tag = "picture", nodes = {}}
+ else
+ local uri = uriForSource(s)
+ local fbimg, idx
+ if next(ctr.nodes) == nil then
+ idx = 1
+ fbimg = {
+ elt = 'img'; --fallback
+ attrs = {
+ alt = '';
+ src = uri;
+ };
+ }
+ else idx = #ctr.nodes end
+ table.insert(ctr.nodes, idx, {
+ elt = 'source'; --fallback
+ attrs = { srcset = uri; };
+ })
+ if fbimg then
+ table.insert(ctr.nodes,fbimg)
+ else
+ -- fallback should be lowest-prio image
+ ctr.nodes[#ctr.nodes].attrs.src = uri;
+ end
+ end
+ end};
+ {ss.mime'text/x.cortav', function(s,ctr)
+ if s == nil then
+ return {}
+ elseif next(ctr) == nil then
+ if (s.mode == 'embed' or s.mode == 'auto') and s.doc then
+ ctr.tag = 'div'; -- kinda hacky, maybe fix
+ ctr.nodes = renderSubdoc(s.doc)
+ elseif s.mode == 'link' then
+ -- yeah this is not gonna work my dude
+ ctr.elt = 'embed';
+ ctr.attrs = {
+ type = 'text/x.cortav';
+ src = htmlURI(s.uri);
+ }
+ end
+ end
+ end};
+ {ss.mime'text/html', function(s,ctr)
+ if s == nil then
+ return {}
+ elseif next(ctr) == nil then
+ if (s.mode == 'embed' or s.mode == 'auto') and s.raw then
+ ctr.tag = 'div'
+ ctr.nodes = s.raw
+ elseif s.mode == 'link' then
+ ctr.elt = 'embed';
+ ctr.attrs = {
+ type = 'text/html';
+ src = htmlURI(s.uri);
+ }
+ end
+ end
+ end};
+ {ss.mime'text/*', function(s,ctr)
+ if s == nil then
+ return {}
+ elseif next(ctr) == nil then
+ local mime = s.mime:clone()
+ mime.opts={}
+ if (s.mode == 'embed' or s.mode == 'auto') and s.raw then
+ ctr.tag = 'pre';
+ ctr.nodes = s.raw
+ elseif s.mode == 'link' then
+ ctr.elt = 'embed';
+ ctr.attrs = {
+ type = tostring(mime);
+ src = htmlURI(s.uri);
+ }
+ end
+ end
+ end};
+ }
+
+ local rtype
+ local fallback
+ for n, src in ipairs(obj.srcs) do
+ if fallback == nil and (src.mode == 'link' or src.mode == 'auto') then
+ fallback = src
+ end
+
+ for i, ea in ipairs(embedActs) do
+ if ea[1] < src.mime then -- fits!
+ rtype = ea
+ goto compatFound
+ end
+ end
+ end
+ -- nothing found; install fallback link
+ if fallback then
+ local lnk = htmlURI(fallback.uri)
+ return tag('a', {href=lnk},
+ tag('div',{class=xref},
+ string.format("ā %s [%s]", b.cap or '', tostring(fallback.mime))))
+ else
+ addStyle 'docmeta'
+ return tag('div',{class="render-warn"},
+ 'could not embed object type ' .. tostring(obj.srcs.mime))
+ end
+
+ ::compatFound::
+ local top = rtype[2]() -- create container
+ for n, src in ipairs(obj.srcs) do
+ if rtype[1] < src.mime then
+ rtype[2](src, top)
+ end
+ end
+ local ft = flatten(top)
+ return ft
+ end
+
+ function block_renderers.macro(b,s)
+ local all = renderSubdoc(b.doc)
+ local cat = catenate(ss.map(flatten,all))
+ return tag('div', {}, cat)
+ end
+
+ function block_renderers.quote(b,s)
+ local ir = renderSubdoc(b.doc)
+ return tag('blockquote', b.id and {id=getSafeID(b)} or {}, catenate(ss.map(flatten,ir)))
end
- return block_renderers;
+ return block_renderers
end
local function getRenderers(procs)
local span_renderers = getSpanRenderers(procs)
local r = getBaseRenderers(procs,span_renderers)
Index: sirsem.lua
==================================================================
--- sirsem.lua
+++ sirsem.lua
@@ -1,11 +1,11 @@
-- [Ź] sirsem.lua
-- ~ lexi hale
-- glowpelt (hsl conversion)
-- ? utility library with functionality common to
-- cortav.lua and its extensions
--- from Ranuir "software utility"
+-- \ from Ranuir "software utility"
-- > local ss = require 'sirsem.lua'
local ss
do -- pull ourselves up by our own bootstraps
local package = _G.package
@@ -475,10 +475,62 @@
end
end
::skip::end
return tbl[bestmatch] or tbl[true], bestmatch
end
+
+function ss.str.b64e(str)
+ local bytes = {}
+ local n = 1
+ for i=1, #str, 3 do
+ local triple = {string.byte(str, i, i+2)}
+ local T = function(q)
+ return triple[q] or 0
+ end
+ local B = function(q)
+ print(q)
+ if q <= 25 then
+ return string.char(0x41 + q)
+ elseif q <= 51 then
+ return string.char(0x61 + (q-26))
+ elseif q <= 61 then
+ return string.char(0x30 + (q-52))
+ elseif q == 62 then
+ return '+'
+ elseif q == 63 then
+ return '/'
+ else error('base64 algorithm broken') end
+ end
+ local quads = {
+ ((T(1) & 0xFC) >> 2);
+ ((T(1) & 0x03) << 4) | ((T(2) & 0xF0) >> 4);
+ ((T(2) & 0x0F) << 2) | ((T(3) & 0xC0) >> 6);
+ ((T(3) & 0x3F));
+ }
+
+ bytes[n + 0] = B(quads[1])
+ bytes[n + 1] = B(quads[2])
+ if triple[2] then
+ bytes[n + 2] = B(quads[3])
+ if triple[3] then
+ bytes[n + 3] = B(quads[4])
+ else
+ bytes[n + 3] = '='
+ end
+ else
+ bytes[n + 2] = '='
+ bytes[n + 3] = '='
+ end
+
+ n = n + 4
+ end
+
+ return table.concat(bytes)
+end
+
+function ss.str.b64d(str)
+end
ss.math = {}
function ss.math.lerp(t, a, b)
return (1-t)*a + (t*b)
@@ -1112,10 +1164,215 @@
else
return s
end
end, ...))
end
+
+local fetchexn = ss.exnkind 'fetch'
+local fetchableProtocols = {
+ http = {
+ proto = {
+ {'http'};
+ {'https'};
+ {'http', 'tls'};
+ };
+ fetch = function(uri)
+ fetchexn('cortav must be compiled with the C shim and libcurl support to use http fetch'):throw()
+ end;
+ };
+ file = {
+ proto = {
+ {'file'};
+ {'file', 'txt'};
+ {'file', 'bin'};
+ {'asset'};
+ {'asset', 'txt'};
+ {'asset', 'bin'};
+ };
+ fetch = function(uri, env)
+ local assetDir = env.asset_base or '.'
+ if uri.namespace then
+ fetchexn('authority (hostname) segment is not supported in file: URIs'):throw()
+ end
+ if uri.svc then
+ fetchexn('service segment is not supported in file: URIs'):throw()
+ end
+ local mode = 'r'
+ local path = uri.path
+ if uri.class[1] == 'asset' then path = assetDir ..'/'.. path end
+ if uri.class[2] == 'bin' then mode = 'rb' end
+ local fd,e = io.open(path, mode)
+ if not fd then
+ fetchexn('IO error fetching URI ā%sā (%s)', tostring(uri), e):throw()
+ end
+ local data = fd:read '*a'
+ fd:close()
+ return data
+ end;
+ };
+}
+
+function ss.match(a,b, eq)
+ if #a ~= #b then return false end
+ eq = eq or function(p,q) return p == q end
+ for i = 1, #a do
+ if not eq(a[i],b[i]) then return false end
+ end
+ return true
+end
+
+ss.uri = ss.declare {
+ ident = 'uri';
+ mk = function() return {
+ class = nil;
+ namespace = nil;
+ path = nil;
+ query = nil;
+ frag = nil;
+ auth = nil;
+ } end;
+ construct = function(me, str)
+ local enc = ss.str.enc.utf8
+ -- URIs must be either ASCII or utf8, so we read and
+ -- store as UTF8. to use a URI in another encoding, it
+ -- must be manually converted to and fro using the
+ -- appropriate functions, such as encodeUCS
+ if not str then return end
+ me.raw = str
+ local rem = str
+ local s_class do
+ local s,r = rem:match '^([^:]+):(.*)$'
+ s_class, rem = s,r
+ end
+ if not rem then
+ ss.uri.exn('invalid URI ā%sā', str):throw()
+ end
+ local s_ns do
+ local s,r = rem:match '^//([^/]*)(.*)$'
+ if s then s_ns, rem = s,r end
+ end
+ local h_query
+ local s_frag
+ local s_path if rem ~= '' then
+ local s,q,r = rem:match '^([^?#]*)([?#]?)(.*)$'
+ if s == '' then s = nil end
+ s_path, rem = s,r
+
+ if q == '#' then
+ s_frag = rem
+ elseif q == '?' then
+ h_query = true
+ end
+ else s_path = '' end
+
+ local s_query if h_query then
+ local s,q,r = rem:match '^([^#]*)(#?)(.*)$'
+ s_query, rem = s,r
+ if q~='' then s_frag = rem end
+ end
+
+ local function dec(str)
+ if not str then return end
+ return str:gsub('%%([0-9A-Fa-f][0-9A-Fa-f])', function(hex)
+ return string.char(tonumber(hex,16))
+ end)
+ end
+
+ local s_auth if s_ns then
+ local s,r = s_ns:match('^([^@]*)@(.*)$')
+ if s then
+ s_ns = r
+ if s ~= '' then
+ s_auth = s
+ end
+ end
+ end
+
+ local s_svc if s_ns then
+ local r,s = s_ns:match('^(.*):(.-)$')
+ if r then
+ s_ns = r
+ if s and s ~= '' then
+ s_svc = s
+ end
+ end
+ end
+
+ me.class = ss.str.split(enc, s_class, '+', {keep_empties=true})
+ for i,v in ipairs(me.class) do me.class[i] = dec(v) end
+ me.auth = dec(s_auth)
+ me.svc = dec(s_svc)
+ me.namespace = dec(s_ns)
+ me.path = dec(s_path)
+ me.query = dec(s_query)
+ me.frag = dec(s_frag)
+ end;
+ cast = {
+ string = function(me)
+ local function san(str, chars)
+ -- TODO IRI support
+ chars = chars or ''
+ local ptn = '-a-zA-Z0-9_.,;'
+ ptn = ptn .. chars
+ return (str:gsub('[^'..ptn..']', function(c)
+ if c == ' ' then return '+' end
+ return string.format('%%%02X', string.byte(c))
+ end))
+ end
+ if me.class == nil or next(me.class) == nil then
+ return 'none:'
+ end
+ local parts = {
+ table.concat(ss.map(san,me.class), '+') .. ':';
+ }
+ if me.namespace or me.auth or me.svc then
+ table.insert(parts, '//')
+ if me.auth then
+ table.insert(parts, san(me.auth,':') .. '@')
+ end
+ if me.namespace then
+ table.insert(parts, san(me.namespace))
+ end
+ if me.svc then
+ table.insert(parts, ':' .. san(me.svc))
+ end
+ if me.path and not ss.str.begins(me.path, '/') then
+ table.insert(parts, '/')
+ end
+ end
+ if me.path then
+ table.insert(parts, san(me.path,'+/=&'))
+ end
+ if me.query then
+ table.insert(parts, '?' .. san(me.query,'?+/=&'))
+ end
+ if me.frag then
+ table.insert(parts, '#' .. san(me.frag,'+/=&'))
+ end
+ return table.concat(parts)
+ end;
+ };
+ fns = {
+ canfetch = function(me)
+ for id, pr in pairs(fetchableProtocols) do
+ for _, p in ipairs(pr.proto) do
+ if ss.match(me.class, p) then return id end
+ end
+ end
+ return false
+ end;
+ fetch = function(me, env)
+ local pid = me:canfetch()
+ if (not pid) or fetchableProtocols[pid].fetch == nil then
+ ss.uri.exn("URI ā%sā is unfetchable", tostring(me)):throw()
+ end
+ local proto = fetchableProtocols[pid]
+ return proto.fetch(me, env or {})
+ end;
+ };
+}
+ss.uri.exn = ss.exnkind 'URI'
ss.mime = ss.declare {
ident = 'mime-type';
mk = function() return {
class = nil;