cortav  Diff

Differences From Artifact [70c60d1282]:

To Artifact [cd83b5f5a6]:


   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