@@ -132,8 +132,16 @@ 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 @@ -152,9 +160,9 @@ 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 @@ -267,8 +275,9 @@ -- 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 { @@ -833,13 +842,15 @@ 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 @@ -1051,9 +1062,13 @@ 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*(.*)$') @@ -1062,8 +1077,9 @@ 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 @@ -1121,25 +1137,25 @@ 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}; @@ -1179,9 +1195,8 @@ 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 @@ -1196,31 +1211,116 @@ 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 @@ -1236,9 +1336,32 @@ 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) @@ -1311,40 +1434,99 @@ 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 @@ -1353,8 +1535,39 @@ -- 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