Differences From
Artifact [70c60d1282]:
129 129 local rid = self.sec.refs[id]
130 130 if self.sec.refs[id] then
131 131 return self.sec.refs[id], id, self.sec
132 132 else self:fail("no such ref %s in current section", id or '') end
133 133 else
134 134 local sec, ref = string.match(id, "(.-)%.(.+)")
135 135 local s = self.doc.sections[sec]
136 + if not s then -- fall back on inheritance tree
137 + for i, p in ipairs(self.doc.parents) do
138 + if p.sections[sec] then
139 + s = p.sections[sec]
140 + break
141 + end
142 + end
143 + end
136 144 if s then
137 145 if s.refs[ref] then
138 146 return s.refs[ref], ref, sec
139 147 else self:fail("no such ref %s in section %s", ref, sec) end
140 148 else self:fail("no such section %s", sec) end
141 149 end
142 150 end
................................................................................
149 157 blocks = {};
150 158 refs = {};
151 159 depth = 0;
152 160 kind = 'ordinary';
153 161 } end;
154 162 construct = function(self, id, depth)
155 163 self.id = id
156 - self.depth = depth
164 + self.depth = depth or self.depth
157 165 end;
158 166 fns = {
159 167 visible = function(self)
160 168 if self.kind == 'nonprinting' then return false end
161 169 local invisibles = {
162 170 ['break'] = true;
163 171 reference = true;
................................................................................
264 272 newdoc.stage = self.stage
265 273 -- vars are handled through proper recursion across all parents and
266 274 -- are intentionally excluded here; subdocs can have their own vars
267 275 -- without losing access to parent vars
268 276 local nctx = ctx:clone()
269 277 nctx:init(newdoc, ctx.src)
270 278 nctx.line = ctx.line
279 + nctx.docDepth = (ctx.docDepth or 0) + ctx.sec.depth - 1
271 280 return newdoc, nctx
272 281 end;
273 282 };
274 283 mk = function(...) return {
275 284 sections = {};
276 285 secorder = {};
277 286 embed = {};
................................................................................
830 839 return spans
831 840 end
832 841
833 842 local function
834 843 blockwrap(fn)
835 844 return function(l,c,j,d)
836 845 local block = fn(l,c,j,d)
837 - block.origin = c:clone();
838 - table.insert(d, block);
839 - j:hook('block_insert', c, block, l)
840 - if block.spans then
841 - c.doc.docjob:hook('meddle_span', block.spans, block)
846 + if block then
847 + block.origin = c:clone();
848 + table.insert(d, block);
849 + j:hook('block_insert', c, block, l)
850 + if block.spans then
851 + c.doc.docjob:hook('meddle_span', block.spans, block)
852 + end
842 853 end
843 854 end
844 855 end
845 856
846 857 local insert_paragraph = blockwrap(function(l,c)
847 858 if l:sub(1,1) == '.' then l = l:sub(2) end
848 859 return {
................................................................................
1048 1059 end)};
1049 1060 {seq = '\t\t', fn = function(l,c,j,d)
1050 1061 local last = d[#d]
1051 1062 if (not last) or (last.kind ~= 'reference') then
1052 1063 c:fail('reference continuations must immediately follow a reference')
1053 1064 end
1054 1065 local str = l:match '^\t\t(.-)%s*$'
1055 - last.val = last.val .. '\n' .. str
1066 + if last.val == '' then
1067 + last.val = str
1068 + else
1069 + last.val = last.val .. '\n' .. str
1070 + end
1056 1071 c.sec.refs[last.key] = last.val
1057 1072 end};
1058 1073 {seq = '\t', pred = function(l)
1059 1074 return (l:match '\t+([^:]+):%s*(.*)$')
1060 1075 end; fn = blockwrap(function(l,c,j,d)
1061 1076 local ref, val = l:match '\t+([^:]+):%s*(.*)$'
1062 1077 local last = d[#d]
1063 1078 local rsrc
1064 1079 if last and last.kind == 'resource' then
1065 1080 last.props[ref] = val
1081 + j:hook('rsrc_set_prop', c, last, ref, val, l)
1066 1082 rsrc = last
1067 1083 elseif last and last.kind == 'reference' and last.rsrc then
1068 1084 last.rsrc.props[ref] = val
1069 1085 rsrc = last.rsrc
1070 1086 else
1071 1087 c.sec.refs[ref] = val
1072 1088 end
................................................................................
1118 1134 c:fail('extension %s does not support critical directive %s', cmd, topcmd)
1119 1135 end
1120 1136 end
1121 1137 elseif crit == '!' then
1122 1138 c:fail('critical directive %s not supported',cmd)
1123 1139 end
1124 1140 end;};
1125 - {pred = function(s) return s:match '^(>+)([^%s]*)%s*(.*)$' end,
1141 + {pred = function(s) return s:match '^>[^>%s]*%s*.*$' end,
1126 1142 fn = function(l,c,j,d)
1127 - local lvl,id,txt = l:match '^(>+)([^%s]*)%s*(.*)$'
1128 - lvl = utf8.len(lvl)
1143 + local id,txt = l:match '^>([^>%s]*)%s*(.*)$'
1144 + if id == '' then id = nil end
1129 1145 local last = d[#d]
1130 1146 local node
1131 1147 local ctx
1132 - if last and last.kind == 'quote' and (id == nil or id == '' or id == last.id) then
1148 + if last and last.kind == 'quote' and (id == nil or id == last.id) then
1133 1149 node = last
1134 1150 ctx = node.ctx
1135 1151 ctx.line = c.line -- is this enough??
1136 1152 else
1137 1153 local doc
1138 1154 doc, ctx = c.doc:sub(c)
1139 - node = { kind = 'quote', doc = doc, ctx = ctx, id = id }
1140 - j:hook('block_insert', c, node, l)
1155 + node = { kind = 'quote', doc = doc, ctx = ctx, id = id, origin = c }
1141 1156 table.insert(d, node)
1157 + j:hook('block_insert', c, node, l)
1142 1158 end
1143 1159
1144 1160 ct.parse_line(txt, ctx, ctx.sec.blocks)
1145 1161 end};
1146 1162 {seq = '~~~', fn = blockwrap(function(l,c,j)
1147 1163 local extract = function(ptn, str)
1148 1164 local start, stop = str:find(ptn)
................................................................................
1176 1192 end
1177 1193 j:hook('mode_switch', c, mode)
1178 1194 c.mode = mode
1179 1195 if id then
1180 1196 if c.sec.refs[id] then c:fail('duplicate ID %s', id) end
1181 1197 c.sec.refs[id] = c.mode.listing
1182 1198 end
1183 - j:hook('block_insert', c, mode.listing, l)
1184 1199 return c.mode.listing;
1185 1200 end)};
1186 1201 {pred = function(s,c)
1187 1202 if s:match '^[%-_][*_%-%s]+' then return true end
1188 1203 if startswith(s, '—') then
1189 1204 for c, p in ss.str.each(c.doc.enc,s) do
1190 1205 if ({
................................................................................
1193 1208 })[c] ~= true then return false end
1194 1209 end
1195 1210 return true
1196 1211 end
1197 1212 end; fn = blockwrap(function()
1198 1213 return { kind = 'horiz-rule' }
1199 1214 end)};
1200 - {seq='@', fn=blockwrap(function(s,c)
1201 - local id = s:match '^@%s*(.-)%s*$'
1215 + {seq='@', fn=function(s,c,j,d)
1216 + local function mirror(b)
1217 + local ch = {}
1218 + local rev = {
1219 + ['['] = ']'; [']'] = '[';
1220 + ['{'] = '}'; ['}'] = '{';
1221 + ['('] = ')'; [')'] = '(';
1222 + ['<'] = '>'; ['>'] = '<';
1223 + }
1224 + for i = 1,#b do
1225 + local c = string.sub(b,-i,-i)
1226 + if rev[c] then
1227 + ch[i] = rev[c]
1228 + else
1229 + ch[i] = c
1230 + end
1231 + end
1232 + return table.concat(ch)
1233 + end
1234 +
1235 + local id,rest = s:match '^@([^%s]*)%s*(.*)$'
1236 + local bs, brak = rest:match '()([{[(<][^%s]*)%s*$'
1237 + local src
1238 + if brak then
1239 + src = rest:sub(1,bs-1):gsub('%s+$','')
1240 + else src = rest end
1241 + if src == '' then src = nil end
1242 + if id == '' then id = nil end
1202 1243 local rsrc = {
1203 1244 kind = 'resource';
1204 - props = {};
1245 + props = {src = src};
1205 1246 id = id;
1247 + origin = c;
1206 1248 }
1207 - if c.sec.refs[id] then
1208 - c:fail('an object with id “%s” already exists in that section',id)
1249 + if brak then
1250 + rsrc.bracket = {
1251 + open = brak;
1252 + close = mirror(brak);
1253 + }
1254 + rsrc.raw = '';
1255 + if src == nil then
1256 + rsrc.props.src = 'text/x.cortav'
1257 + end
1209 1258 else
1210 - c.sec.refs[id] = rsrc
1259 + -- load the raw body, where possible
1260 + end
1261 + if id then
1262 + if c.sec.refs[id] then
1263 + c:fail('an object with id “%s” already exists in that section',id)
1264 + else
1265 + c.sec.refs[id] = rsrc
1266 + end
1267 + end
1268 + table.insert(d, rsrc)
1269 + j:hook('block_insert', c, rsrc, s)
1270 + if id == '' then --shorthand syntax
1271 + local embed = {
1272 + kind = 'embed';
1273 + rsrc = rsrc;
1274 + origin = c;
1275 + }
1276 + table.insert(d, embed)
1277 + j:hook('block_insert', c, embed, s)
1278 + end
1279 +
1280 + if brak then
1281 + c.mode = {
1282 + kind = 'inline-rsrc';
1283 + rsrc = rsrc;
1284 + indent = nil;
1285 + depth = 0;
1286 + }
1287 + end
1288 + end};
1289 + {seq='&$', fn=blockwrap(function(s,c)
1290 + local id, args = s:match('^&$([^%s]+)%s?(.-)$')
1291 + if id == nil or id == '' then
1292 + c:fail 'malformed macro block'
1293 + end
1294 + local argv = ss.str.split(c.doc.enc, args, c.doc.enc.encodeUCS'|', {esc=true})
1295 + return {
1296 + kind = 'macro';
1297 + macro = id;
1298 + args = argv;
1299 + }
1300 + end)};
1301 + {seq='&', fn=blockwrap(function(s,c)
1302 + local id, cap = s:match('^&([^%s]+)%s*(.-)%s*$')
1303 + if id == nil or id == '' then
1304 + c:fail 'malformed embed block'
1211 1305 end
1212 - return rsrc
1306 + if cap == '' then cap = nil end
1307 + return {
1308 + kind = 'embed';
1309 + ref = id;
1310 + cap = cap;
1311 + }
1213 1312 end)};
1214 1313 {fn = insert_paragraph};
1215 1314 }
1216 1315
1217 -function ct.parse_line(l, ctx, dest)
1316 +function ct.parse_line(rawline, ctx, dest)
1218 1317 local newspan
1219 1318 local job = ctx.doc.stage.job
1220 - job:hook('line_read',ctx,l)
1221 - if l then
1222 - l = l:gsub("^ +","") -- trim leading spaces
1319 + job:hook('line_read',ctx,rawline)
1320 + local l
1321 + if rawline then
1322 + l = rawline:gsub("^ +","") -- trim leading spaces
1223 1323 end
1224 1324 if ctx.mode then
1225 1325 if ctx.mode.kind == 'code' then
1226 1326 if l and l:match '^~~~%s*$' then
1227 1327 job:hook('block_listing_end',ctx,ctx.mode.listing)
1228 1328 job:hook('mode_switch', c, nil)
1229 1329 ctx.mode = nil
................................................................................
1233 1333 if ctx.mode.expand
1234 1334 then newline = ct.parse_span(l, ctx)
1235 1335 else newline = {l}
1236 1336 end
1237 1337 table.insert(ctx.mode.listing.lines, newline)
1238 1338 job:hook('block_listing_newline',ctx,ctx.mode.listing,newline)
1239 1339 end
1240 - elseif ctx.mode.kind == 'quote' then
1340 + elseif ctx.mode.kind == 'inline-rsrc' then
1341 + local r = ctx.mode.rsrc
1342 + if rawline then
1343 + if rawline == r.bracket.close then
1344 + if ctx.mode.depth == 0 then
1345 + -- TODO how to handle depth?
1346 + ctx.mode = nil
1347 + end
1348 + else
1349 + if r.indent ~= nil then
1350 + r.raw = r.raw .. '\n'
1351 + else
1352 + r.indent = (rawline:sub(1,1) == '\t')
1353 + end
1354 +
1355 + if r.indent == true then
1356 + if rawline:sub(1,1) == '\t' then
1357 + rawline = rawline:sub(2)
1358 + end
1359 + end
1360 +
1361 + r.raw = r.raw .. rawline
1362 + end
1363 + end
1241 1364 else
1242 1365 local mf = job:proc('modes', ctx.mode.kind)
1243 1366 if not mf then
1244 1367 ctx:fail('unimplemented syntax mode %s', ctx.mode.kind)
1245 1368 end
1246 1369 mf(job, ctx, l, dest) --NOTE: you are responsible for triggering the appropriate hooks if you insert anything!
1247 1370 end
................................................................................
1308 1431 local function
1309 1432 is_whitespace(cp)
1310 1433 return ctx.doc.enc.iswhitespace(cp)
1311 1434 end
1312 1435
1313 1436 if setup then setup(ctx) end
1314 1437
1315 -
1316 1438 for full_line in file:lines() do ctx.line = ctx.line + 1
1317 - local l
1318 - for p, c in utf8.codes(full_line) do
1319 - if not is_whitespace(c) then
1320 - l = full_line:sub(p)
1321 - break
1322 - end
1323 - end
1324 - ct.parse_line(l, ctx, ctx.sec.blocks)
1439 + -- local l
1440 + -- for p, c in utf8.codes(full_line) do
1441 + -- if not is_whitespace(c) then
1442 + -- l = full_line:sub(p)
1443 + -- break
1444 + -- end
1445 + -- end
1446 + ct.parse_line(full_line, ctx, ctx.sec.blocks)
1325 1447 end
1326 1448
1327 1449 for i, sec in ipairs(ctx.doc.secorder) do
1328 - for refid, r in ipairs(sec.refs) do
1329 - if type(r) == 'table' and r.kind == 'resource' and r.props.src then
1450 + for n, r in pairs(sec.blocks) do
1451 + if r.kind == 'resource' and r.props.src then
1330 1452 local lines = ss.str.breaklines(ctx.doc.enc, r.props.src)
1331 1453 local srcs = {}
1332 1454 for i,l in ipairs(lines) do
1333 1455 local args = ss.str.breakwords(ctx.doc.enc, l, 2, {escape=true})
1334 - if #args < 3 then
1335 - r.origin:fail('invalid syntax for resource %s', t.ref)
1456 + if #args > 3 or (r.raw and #args > 2) then
1457 + r.origin:fail('invalid syntax for resource %s', r.id or '(anonymous)')
1458 + end
1459 + local p_mode, p_mime, p_uri
1460 + if r.raw then
1461 + p_mode = 'embed'
1462 + end
1463 + if #args == 1 then
1464 + if r.raw then -- inline content
1465 + p_mime = ss.mime(args[1])
1466 + else
1467 + p_uri = args[1]
1468 + end
1469 + elseif #args == 2 then
1470 + local ok, m = pcall(ss.mime, args[1])
1471 + if r.raw then
1472 + if not ok then
1473 + r.origin:fail('invalid mime-type “%s”', args[1])
1474 + end
1475 + p_mode, p_mime = args[1], m
1476 + else
1477 + if ok then
1478 + p_mime, p_uri = m, args[2]
1479 + else
1480 + p_mode, p_uri = table.unpack(args)
1481 + end
1482 + end
1483 + else
1484 + p_mode, p_mime, p_uri = table.unpack(args)
1485 + p_mime = ss.mime(args[2])
1486 + end
1487 + local resource = {
1488 + mode = p_mode;
1489 + mime = p_mime or 'text/x.cortav';
1490 + uri = p_uri and ss.uri(p_uri) or nil;
1491 + }
1492 + if resource.mode == 'embed' or resource.mode == 'auto' then
1493 + -- the resource must be available for reading within this job
1494 + -- open it and read its source into memory
1495 + if resource.uri then
1496 + if resource.uri:canfetch() then
1497 + resource.raw = resource.uri:fetch()
1498 + elseif resource.mode == 'auto' then
1499 + -- resource cannot be accessed; force linking
1500 + resource.mode = 'link'
1501 + else
1502 + r.origin:fail('resource “%s” wants to embed unfetchable URI “%s”',
1503 + r.id or "(anonymous)", tostring(resource.uri))
1504 + end
1505 + elseif r.raw then
1506 + resource.raw = r.raw
1507 + else
1508 + r.origin:fail('resource “%s” is not inline and supplies no URI',
1509 + r.id or "(anonymous)")
1510 + end
1511 +
1512 + -- the resource has been cached. check the mime-type to see if
1513 + -- we need to parse it or if it is suitable as-is
1514 +
1515 + if resource.mime.class == "text" then
1516 + if resource.mime.kind == "x.cortav" then
1517 + local sd, sc = r.origin.doc:sub(r.origin)
1518 + local lines = ss.str.breaklines(r.origin.doc.enc, resource.raw, {})
1519 + for i, ln in ipairs(lines) do
1520 + sc.line = sc.line + 1
1521 + ct.parse_line(ln, sc, sc.sec.blocks)
1522 + end
1523 + resource.doc = sd
1524 + end
1525 + end
1336 1526 end
1337 - local mime = ss.mime(args[2]);
1338 - local class = mimeclasses[mime]
1339 - table.insert(srcs, {
1340 - mode = args[1];
1341 - mime = mime;
1342 - uri = args[3];
1343 - class = class or mime[1];
1344 - })
1527 + table.insert(srcs, resource)
1345 1528 end
1346 - --ideally move this into its own mimetype lib
1347 1529 r.srcs = srcs
1348 1530 -- note that resources do not themselves have kinds. when a
1349 1531 -- document requests to insert a resource, the renderer must
1350 1532 -- iterate through the sources and find the first source it
1351 1533 -- is capable of emitting. this allows constructions like
1352 1534 -- emitting a video for HTML outputs, a photo for printers,
1353 1535 -- and a screenplay for tty/plaintext outputs.
1354 1536 end
1355 1537 end
1356 1538 end
1539 +
1540 + -- expand block macros
1541 + for i, sec in ipairs(ctx.doc.secorder) do
1542 + for n, r in pairs(sec.blocks) do
1543 + if r.kind == 'macro' then
1544 + local mc = r.origin:clone()
1545 + mc.invocation = r
1546 + local mac = r.origin:ref(r.macro)
1547 + if not mac then
1548 + r.origin:fail('no such reference or resource “%s”', r.macro)
1549 + end
1550 + local subdoc, subctx = ctx.doc:sub(mc)
1551 + local rawbody
1552 +
1553 + if type(mac) == 'string' then
1554 + rawbody = mac
1555 + elseif mac.raw then
1556 + rawbody = mac.raw
1557 + else
1558 + r.origin:fail('block macro “%s” must be either a reference or an embedded text/x.cortav resource', r.macro)
1559 + end
1560 +
1561 + local lines = ss.str.breaklines(ctx.doc.enc, rawbody)
1562 + for i, ln in ipairs(lines) do
1563 + ct.parse_line(ln, subctx, subctx.sec.blocks)
1564 + end
1565 + r.doc = subdoc
1566 + end
1567 + end
1568 + end
1569 +
1357 1570 ctx.doc.stage = nil
1358 1571 ctx.doc.docjob:hook('meddle_ast')
1359 1572 return ctx.doc
1360 1573 end
1361 1574
1362 1575 function ct.expand_var(v)
1363 1576 local val