cortav  Check-in [3ee2195d4f]

Overview
Comment:progress on resources + block macros; fix toc bug; add uri, fetch scaffolding, and b64e to sirsem
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 3ee2195d4f21f8e001e7821aed4b3ac1c65bac834dac2bc67125154e0585de39
User & Date: lexi on 2022-09-08 23:37:01
Other Links: manifest | tags
Context
2022-09-09
01:12
many fixes and updates to kate syntax check-in: 6c198bdce0 user: lexi tags: trunk
2022-09-08
23:37
progress on resources + block macros; fix toc bug; add uri, fetch scaffolding, and b64e to sirsem check-in: 3ee2195d4f user: lexi tags: trunk
2022-09-05
20:46
fix typo check-in: c30f235b93 user: lexi tags: trunk
Changes

Modified cortav.ct from [99aafd4711] to [3240d0f3f3].

   135    135   ** consider using a macro definition [`\edit: [~[#1]][+[#2]]] to save typing if you are doing editing work
   136    136   * link [` \[>[$ref] [$styled-text]\]]: produces a hyperlink or cross-reference denoted by [$ref], which may be either a URL specified with a reference or the name of an object like an image or section elsewhere in the document. the unicode characters [`โ†’] and [`๐Ÿ”—] can also be used instead of [`>] to denote a link.
   137    137   * 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.
   138    138   * superscript {obj '|[$styled-text]}
   139    139   * subscript {obj ,|[$styled-text]}
   140    140   * 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.
   141    141   * raw literal [` \["[$raw-text]\]]: shorthand for a raw inside a literal, that is ["[`[\\โ€ฆ]]]
   142         -* macro [` \{[$name] [$arguments]}]: invokes a [>ex.mac macro], specified with a reference
          142  +* 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.
   143    143   * argument {obj #|var}: in macros only, inserts the [$var]-th argument. otherwise, inserts a context variable provided by the renderer.
   144    144   * raw argument {obj ##|var}: like above, but does not evaluate [$var].
   145    145   * 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)
   146    146   * inline image {obj &@|name}: shows a small image or other object inline. the unicode character [`๐Ÿ–ผ] can also be used instead of [`&@].
   147    147   * unicode codepoint {obj U+|hex-integer}: inserts an arbitrary UCS codepoint in the output, specified by [$hex-integer]. lowercase [`u] is also legal.
   148    148   * math mode {obj =|equation}: activates additional transformations on the span to format it as a mathematical equation; e.g. [`*] becomes [`ร—] and [`/] --> [`รท].
   149    149   * extension {span %|ext|โ€ฆ}: invokes extension named in [$ext]. [$ext] will usually be an extension name followed by a symbol (often a period) and then an extension-specific directive, although for some simple extensions it may just be the plain extension name. further syntax and semantics depend on the extension. this syntax can also be used to apply formatting specific to certain renderers, such as assigning a CSS class in the [`html] renderer (["[%html.myclass my [!styled] text]]).

Modified cortav.lua from [70c60d1282] to [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

Modified ext/toc.lua from [e8a2cf6308] to [214c9479d0].

   136    136   		end;
   137    137   		[true] = function (job, ctx, words) 
   138    138   			local _, op, val = words(2)
   139    139   			if op == nil then
   140    140   				local toc = {kind='toc'}
   141    141   				ctx:insert(toc)
   142    142   				-- same deal here -- directives are processed as part of
   143         -				-- the parse job, which is forked off the document job,
   144         -				-- so we need to climb the jobstack
          143  +				-- the parse job, which is forked off the document job;
          144  +				-- if we want state that can persist into the render job,
          145  +            -- we need to climb the jobstack
   145    146   				job:unwind(1).state.toc_custom_position = true
   146    147   				job:hook('ext_toc_position', ctx, toc)
   147    148   			else
   148    149   				ctx:fail 'bad %toc directive'
   149    150   			end
   150    151   		end;
   151    152   	};
................................................................................
   188    189   				-- each node.
   189    190   				local stack = {lst}
   190    191   				local top = function() return stack[#stack] end
   191    192   				-- job.doc is the document the render job is bound to, and
   192    193   				-- its secorder field is a list of all the doc's sections in
   193    194   				-- the order they occur ("doc.sections" is a hashmap from name
   194    195   				-- to section object)
   195         -				local all = job.doc.secorder
          196  +				local all = {}
          197  +
          198  +				local function blockHasSubdoc(b)
          199  +					local subdocBlockKinds = {
          200  +						quote = true;
          201  +						embed = true;
          202  +						macro = true;
          203  +					}
          204  +					return subdocBlockKinds[b.kind] and ct.doc.is(b.doc)
          205  +				end
          206  +
          207  +            local function scandoc(doc, depth)
          208  +	            for i, sec in ipairs(doc.secorder) do
          209  +						table.insert(all, {ref = sec, depth = sec.depth + depth})
          210  +						for j, block in ipairs(sec.blocks) do
          211  +							if blockHasSubdoc(block) then
          212  +								scandoc(block.doc, depth + sec.depth-1)
          213  +							end
          214  +						end
          215  +	            end
          216  +            end
          217  +
          218  +				scandoc(job.doc,0)
   196    219   
   197         -				for i, sec in ipairs(all) do
          220  +				for i, secptr in ipairs(all) do
          221  +					local sec = secptr.ref
   198    222   					if sec.heading_node then -- does this section have a label?
   199    223   						local ent = tag('li',nil,
   200    224   							 catenate{tag('a', {href='#'..getSafeID(sec)},
   201    225   								sr.htmlSpan(sec.heading_node.spans, sec.heading_node, sec))})
   202         -						if sec.depth > #stack then
          226  +						if secptr.depth > #stack then
   203    227   							local n = {tag = 'ol', attrs={}, nodes={ent}}
   204    228   							table.insert(top().nodes[#top().nodes].nodes, n)
   205    229   							table.insert(stack, n)
   206    230   						else
   207         -							if sec.depth < #stack then
   208         -								for j=#stack,sec.depth+1,-1 do stack[j] = nil end
          231  +							if secptr.depth < #stack then
          232  +								for j=#stack,secptr.depth+1,-1 do stack[j] = nil end
   209    233   							end
   210    234   							table.insert(top().nodes, ent)
   211    235   						end
   212    236   
   213    237   						-- now we need to assemble a list of items within the
   214    238   						-- section worthy of an entry on their own. currently
   215    239   						-- this is only anchors created with %toc mark|name
................................................................................
   234    258   								local nn = {
   235    259   									tag = 'a';
   236    260   									attrs = {href = '#' .. l.id};
   237    261   									nodes = {sr.htmlSpan(l.label, l.block, sec)};
   238    262   								}
   239    263   								table.insert(n.nodes, {tag = 'li', attrs = {}, nodes={nn}})
   240    264   							end
   241         -							table.insert(ent.nodes, n)
          265  +							table.insert(top().nodes, n)
          266  +							table.insert(stack, n)
   242    267   						end
   243    268   					end
   244    269   				end
   245    270   				return lst
   246    271   			end;
   247    272   
   248    273   			[true] = function() end; -- fallback // convert to different node types
   249    274   		};
   250    275   	};
   251    276   }

Modified render/groff.lua from [62c90e9915] to [3a20caca9a].

    19     19   local lines = function(...)
    20     20   	local s = ss.strac()
    21     21   	for _, v in pairs{...} do s(v) end
    22     22   	return s
    23     23   end
    24     24   
    25     25   local function gsan(str)
    26         -	local tocodepoint = function(ch)
    27         -		return string.format('\\[u%04X]', utf8.codepoint(ch))
           26  +	-- groff does not support UTF-8
           27  +	local ascii = {}
           28  +	for p,c in utf8.codes(str) do
           29  +		if c > 0x7F or c == 0x27 or c == 0x22 or c == 0x5C then
           30  +			table.insert(ascii, string.format('\\[u%04X]', c))
           31  +		else
           32  +			table.insert(ascii, utf8.char(c))
           33  +		end
    28     34   	end
    29         -	str = str:gsub('(["\'\\])',tocodepoint)
           35  +	str = table.concat(ascii)
           36  +	str = str:gsub('\t','\\t') -- tabs are sometimes syntactically meaningful
    30     37   	return str
    31     38   end
    32     39   
    33     40   local gtxt = ss.declare {
    34     41   	ident = 'groff-text';
    35     42   	mk = function() return {
    36     43   		lines = {};
................................................................................
   140    147   				spans = self.spans;
   141    148   			}
   142    149   		end;
   143    150   		blocks = {};
   144    151   		prop = {};
   145    152   		block = function(self)
   146    153   			local sub = self:clone()
          154  +			sub.parent = self -- needed for blocks that contain blocks
   147    155   			sub.spans = {}
   148    156   			sub.blocks = nil
          157  +			sub.block = nil
   149    158   			sub.span = function(me, ln)
   150    159   				local p = ss.clone(me.prop)
   151    160   				p.txt = ln
   152    161   				p.block = sub
   153    162   				p.origin = me.origin
   154    163   				table.insert(me.spans, p)
   155    164   				return p
................................................................................
   400    409   	local blockRenderers = {}
   401    410   	blockRenderers['horiz-rule'] = function(rc, b, sec)
   402    411   		rc.prop.margin = { top = 0.3 }
   403    412   		rc.prop.underline = 0.1
   404    413   	end
   405    414   	function	blockRenderers.label(rc, b, sec)
   406    415   		if ct.sec.is(b.captions) then
          416  +			local visDepth = b.captions.depth + (b.origin.docDepth or 0)
   407    417   			local sizes = {36,24,12,8,4,2}
   408    418   			local margins = {0,3}
   409    419   			local dedents = {2.5,1.3,0.8,0.4}
   410    420   			local uls = {3,1.5,0.5,0.25}
   411         -			rc.prop.dsz = sizes[b.captions.depth] or 10
   412         -			rc.prop.underline = uls[b.captions.depth]
   413         -			rc.prop.bold = b.captions.depth > 3
          421  +			rc.prop.dsz = sizes[visDepth] or 10
          422  +			rc.prop.underline = uls[visDepth]
          423  +			rc.prop.bold = visDepth > 3
   414    424   			rc.prop.margin = {
   415         -				top = margins[b.captions.depth] or 1;
          425  +				top = margins[visDepth] or 1;
   416    426   				bottom = 0.1;
   417    427   			}
   418    428   			rc.prop.vassure = rc.prop.dsz+70;
   419         -			rc.prop.indent = -(dedents[b.captions.depth] or 0)
          429  +			rc.prop.indent = -(dedents[visDepth] or 0)
   420    430   			rc.prop.chtitle = collectText(rc, b.spans, b.spec):compile()
   421         -			if b.captions.depth == 1 then
          431  +			if visDepth == 1 then
   422    432   				rc.prop.breakBefore = true
   423    433   			end
   424    434   			rs.renderSpans(rc, b.spans, b, sec)
   425    435   		else
   426    436   			ss.bug 'tried to render label for an unknown object type':throw()
   427    437   		end
   428    438   	end
   429    439   	function	blockRenderers.paragraph(rc, b, sec)
   430    440   		rs.renderSpans(rc, b.spans, b, sec)
          441  +	end
          442  +	function	blockRenderers.macro(rc, b, sec)
          443  +		local rc = rc.parent:clone()
          444  +		rs.renderDoc(rc, b.doc)
          445  +	end
          446  +	function	blockRenderers.quote(rc, b, sec)
          447  +		local rc = rc.parent:clone()
          448  +		rc.prop.indent = (rc.prop.indent or 0) + 1
          449  +		local added = rs.renderDoc(rc, b.doc)
          450  +		 -- select last block of last section and increase bottom margin
          451  +		local ap = added[#added].blocks
          452  +		ap = ap[#ap].prop
          453  +		if ap.margin then
          454  +			if ap.margin.bottom then
          455  +				ap.margin.bottom = ap.margin.bottom + 1.1
          456  +			else
          457  +				ap.margin.bottom = 1.1
          458  +			end
          459  +		else
          460  +			ap.margin = {bottom = 1.1}
          461  +		end
          462  +	end
          463  +	function	blockRenderers.table(rc, b, sec)
          464  +		function rc:begin(g)
          465  +			g:req 'TS'
          466  +			local aligns = {}
          467  +			for i, c in ipairs(b.rows[1]) do
          468  +				aligns[i] = ({
          469  +					left = 'l';
          470  +					center = 'c';
          471  +					right = 'r';
          472  +				})[c.align] or 'l'
          473  +			end
          474  +			table.insert(aligns, '.')
          475  +			g:txt(table.concat(aligns, ' ') .. '\n')
          476  +
          477  +			local rc_hdr = rc:clone()
          478  +			rc_hdr.prop.bold = true
          479  +			for ri, r in ipairs(b.rows) do
          480  +				for ci, c in ipairs(r) do
          481  +					local sp = collect(c.header and rc_hdr or rc, c.spans, b, sec)
          482  +					for si, s in ipairs(sp) do rs.emitSpan(g,s) end
          483  +					g:raw '\t'
          484  +				end
          485  +				if ri ~= #b.rows then g:raw '\n' end
          486  +			end
          487  +			g:req 'TE'
          488  +		end
   431    489   	end
   432    490   	function rs.renderBlock(rc, b, sec, outerBlockRenderContext)
   433    491   		if blockRenderers[b.kind] then
   434    492   			local rcc = rc:block()
   435    493   			blockRenderers[b.kind](rcc, b, sec)
   436    494   		end
   437    495   	end
................................................................................
   556    614   			if ln.dsz then
   557    615   				defer:req('ps +' .. tostring(0 - ln.dsz) .. 'p')
   558    616   			else
   559    617   				defer:req'ps'
   560    618   			end
   561    619   		end
   562    620   
   563         -		for i,s in pairs(b.spans) do
   564         -			rs.emitSpan(gtxt, s)
          621  +		if b.begin then b:begin(gtxt) end
          622  +		if b.spans then
          623  +			for i,s in pairs(b.spans) do
          624  +				rs.emitSpan(gtxt, s)
          625  +			end
   565    626   		end
   566         -
          627  +		if b.complete then b:complete(gtxt) end
   567    628   
   568    629   		if ln.margin then
   569    630   			if ln.margin.bottom then
   570    631   				gtxt:req(string.format('sp %sm', ln.margin.bottom))
   571    632   			end
   572    633   		end
   573    634   
   574    635   		defer:flush()
   575    636   
   576    637   		if not ln.margin then gtxt:brk() end
   577    638   	end
   578    639   
   579         -	local ir = {}
   580         -	for i, sec in ipairs(doc.secorder) do
   581         -		if sec.kind == 'ordinary' then
   582         -			local rc = mkrc()
   583         -			for j, b in ipairs(sec.blocks) do
   584         -				rs.renderBlock(rc, b, sec)
          640  +	function rs.renderDoc(gctx, doc, ir) ir = ir or {}
          641  +		for i, sec in ipairs(doc.secorder) do
          642  +			if sec.kind == 'ordinary' then
          643  +				local rc = gctx and gctx:clone() or mkrc()
          644  +				for j, b in ipairs(sec.blocks) do
          645  +					rs.renderBlock(rc, b, sec)
          646  +				end
          647  +				table.insert(ir, {blocks = rc.blocks, src = sec})
   585    648   			end
   586         -			table.insert(ir, {blocks = rc.blocks, src = sec})
   587    649   		end
          650  +		return ir
   588    651   	end
          652  +	local ir = rs.renderDoc(nil, doc)
   589    653   
   590    654   	local gd = gtxt()
   591    655   	for i, s in ipairs(ir) do
   592    656   		for j, b in ipairs(s.blocks) do
   593    657   			rs.emitBlock(gd,b)
   594    658   		end
   595    659   	end
................................................................................
   613    677   				top = s.depth
   614    678   				doctitle = collectText(mkrc():block(), s.heading_node.spans, s.heading_node, s):compile()
   615    679   			end
   616    680   		end
   617    681   	end
   618    682   	macs('.ds doctitle '..doctitle)
   619    683   
   620         -	return macs:compile'\n' .. '\n' .. gd:compile()
          684  +	return macs:compile'\n' .. '\n' .. gd:compile() .. '\n'
          685  +	-- if the document doesn't end with the character \n, groff will bitch
          686  +	-- and moan in certain circumstances
   621    687   end

Modified render/html.lua from [5903337619] to [9686e0dc58].

   634    634   		end
   635    635   
   636    636   		function span_renderers.macro(m,b,s)
   637    637   			local macroname = plainrdr.htmlSpan(
   638    638   				ct.parse_span(m.macro, b.origin), b,s)
   639    639   			local r = b.origin:ref(macroname)
   640    640   			if type(r) ~= 'string' then
   641         -				b.origin:fail('%s is an object, not a reference', t.ref)
          641  +				b.origin:fail('%s is an object, not a reference', r.id)
   642    642   			end
   643    643   			local mctx = b.origin:clone()
   644    644   			mctx.invocation = m
   645    645   			return htmlSpan(ct.parse_span(r, mctx),b,s)
   646    646   		end
   647    647   		function span_renderers.math(m,b,s)
   648    648   			addStyle 'math'
................................................................................
   732    732   				end
   733    733   			end;
   734    734   			label = function(b,s)
   735    735   				if ct.sec.is(b.captions) then
   736    736   					if not (opts['fossil-uv'] or opts.snippet) then
   737    737   						addStyle 'header'
   738    738   					end
   739         -					local h = math.min(6,math.max(1,b.captions.depth))
          739  +					-- use correct styling in subdocuments
          740  +					local visDepth = b.captions.depth + (b.origin.docDepth or 0)
          741  +					local h = math.min(6,math.max(1,visDepth))
   740    742   					return tag(f('h%u',h), nil, sr.htmlSpan(b.spans, b, s), b)
   741    743   				else
   742    744   					-- handle other uses of labels here
   743    745   				end
   744    746   			end;
   745    747   			['list-item'] = function(b,s)
   746    748   				return tag('li', nil, sr.htmlSpan(b.spans, b, s), b)
................................................................................
   786    788   			end;
   787    789   			['break'] = function() -- HACK
   788    790   				-- lists need to be rewritten to work like asides
   789    791   				return '';
   790    792   			end;
   791    793   		}
   792    794   
   793         -		function block_renderers.quote(b,s)
          795  +		local function renderSubdoc(doc)
   794    796   			local ir = {}
   795         -			local toIR = block_renderers
   796         -			for i, sec in ipairs(b.doc.secorder) do
          797  +			for i, sec in ipairs(doc.secorder) do
   797    798   				local secnodes = {}
   798    799   				for i, bl in ipairs(sec.blocks) do
   799         -					if toIR[bl.kind] then
   800         -						table.insert(secnodes, toIR[bl.kind](bl,sec))
          800  +					if block_renderers[bl.kind] then
          801  +						table.insert(secnodes, block_renderers[bl.kind](bl,sec))
   801    802   					end
   802    803   				end
   803    804   				if next(secnodes) then
   804         -					if b.doc.secorder[2] then --#secs>1?
          805  +					if doc.secorder[2] then --#secs>1?
   805    806   						-- only wrap in a section if >1 section
   806    807   						table.insert(ir, tag('section',
   807    808   													{id = getSafeID(sec)},
   808    809   													secnodes))
   809    810   					else
   810    811   						ir = secnodes
   811    812   					end
   812    813   				end
   813    814   			end
   814         -			return tag('blockquote', b.id and {id=getSafeID(b)} or {}, catenate(ir))
          815  +			return ir
          816  +		end
          817  +
          818  +		local function flatten(t)
          819  +			if t == nil then
          820  +				return ''
          821  +			elseif type(t) == 'string' then
          822  +				return t
          823  +			elseif type(t) == 'table' then
          824  +				if t[1] then
          825  +					return catenate(ss.map(flatten, t))
          826  +				elseif t.tag then
          827  +					return tag(t.tag, t.attrs or {}, flatten(t.nodes))
          828  +				elseif t.elt then
          829  +					return tag(t.elt, t.attrs or {})
          830  +				end
          831  +			end
          832  +		end
          833  +
          834  +		function block_renderers.embed(b,s)
          835  +			local obj
          836  +			if b.rsrc
          837  +				then obj = b.rsrc
          838  +				else obj = b.origin:ref(b.ref)
          839  +			end
          840  +			local function htmlURI(u)
          841  +				local family = u:canfetch()
          842  +				if  family == 'file' or
          843  +					(family == 'http' and u.namespace == nil) then
          844  +					-- TODO asset:
          845  +					return u.path
          846  +				else
          847  +					return tostring(u)
          848  +				end
          849  +			end
          850  +			local function uriForSource(s)
          851  +				if s.mode == 'link' or s.mode == 'auto' then
          852  +					return htmlURI(s.uri)
          853  +				elseif s.mode == 'embed' then
          854  +					local mime = s.mime:clone()
          855  +					mime.opts = {}
          856  +					return string.format('data:%s;base64,%s', mime, ss.str.b64e(s.raw))
          857  +				end
          858  +			end
          859  +			--figure out how to embed the given object
          860  +			local embedActs = {
          861  +				{ss.mime'image/*',       function(s,ctr)
          862  +					if s == nil then
          863  +						return {tag = "picture", nodes = {}}
          864  +					else
          865  +						local uri = uriForSource(s)
          866  +						local fbimg, idx
          867  +						if next(ctr.nodes) == nil then
          868  +							idx = 1
          869  +							fbimg = {
          870  +								elt = 'img'; --fallback
          871  +								attrs = {
          872  +									alt = '';
          873  +									src = uri;
          874  +								};
          875  +							}
          876  +						else idx = #ctr.nodes end
          877  +						table.insert(ctr.nodes, idx, {
          878  +							elt = 'source'; --fallback
          879  +							attrs = { srcset = uri; };
          880  +						})
          881  +						if fbimg then
          882  +							table.insert(ctr.nodes,fbimg)
          883  +						else
          884  +							-- fallback <img> should be lowest-prio image
          885  +							ctr.nodes[#ctr.nodes].attrs.src = uri;
          886  +						end
          887  +					end
          888  +				end};
          889  +				{ss.mime'text/x.cortav', function(s,ctr)
          890  +					if s == nil then
          891  +						return {}
          892  +					elseif next(ctr) == nil then
          893  +						if (s.mode == 'embed' or s.mode == 'auto') and s.doc then
          894  +							ctr.tag = 'div'; -- kinda hacky, maybe fix
          895  +							ctr.nodes = renderSubdoc(s.doc)
          896  +						elseif s.mode == 'link' then
          897  +							-- yeah this is not gonna work my dude
          898  +							ctr.elt = 'embed';
          899  +							ctr.attrs = {
          900  +								type = 'text/x.cortav';
          901  +								src = htmlURI(s.uri);
          902  +							}
          903  +						end
          904  +					end
          905  +				end};
          906  +				{ss.mime'text/html',     function(s,ctr)
          907  +					if s == nil then
          908  +						return {}
          909  +					elseif next(ctr) == nil then
          910  +						if (s.mode == 'embed' or s.mode == 'auto') and s.raw then
          911  +							ctr.tag = 'div'
          912  +							ctr.nodes = s.raw
          913  +						elseif s.mode == 'link' then
          914  +							ctr.elt = 'embed';
          915  +							ctr.attrs = {
          916  +								type = 'text/html';
          917  +								src = htmlURI(s.uri);
          918  +							}
          919  +						end
          920  +					end
          921  +				end};
          922  +				{ss.mime'text/*',     function(s,ctr)
          923  +					if s == nil then
          924  +						return {}
          925  +					elseif next(ctr) == nil then
          926  +						local mime = s.mime:clone()
          927  +						mime.opts={}
          928  +						if (s.mode == 'embed' or s.mode == 'auto') and s.raw then
          929  +							ctr.tag = 'pre';
          930  +							ctr.nodes = s.raw
          931  +						elseif s.mode == 'link' then
          932  +							ctr.elt = 'embed';
          933  +							ctr.attrs = {
          934  +								type = tostring(mime);
          935  +								src = htmlURI(s.uri);
          936  +							}
          937  +						end
          938  +					end
          939  +				end};
          940  +			}
          941  +
          942  +			local rtype
          943  +			local fallback
          944  +			for n, src in ipairs(obj.srcs) do
          945  +				if fallback == nil and (src.mode == 'link' or src.mode == 'auto') then
          946  +					fallback = src
          947  +				end
          948  +
          949  +				for i, ea in ipairs(embedActs) do
          950  +					if ea[1] < src.mime then -- fits!
          951  +						rtype = ea
          952  +						goto compatFound
          953  +					end
          954  +				end
          955  +			end
          956  +			-- nothing found; install fallback link
          957  +				if fallback then
          958  +					local lnk = htmlURI(fallback.uri)
          959  +					return tag('a', {href=lnk},
          960  +								  tag('div',{class=xref},
          961  +										string.format("โ†’ %s [%s]", b.cap or '', tostring(fallback.mime))))
          962  +				else
          963  +					addStyle 'docmeta'
          964  +					return tag('div',{class="render-warn"},
          965  +								  'could not embed object type ' .. tostring(obj.srcs.mime))
          966  +				end
          967  +
          968  +			::compatFound::
          969  +			local top = rtype[2]() -- create container
          970  +			for n, src in ipairs(obj.srcs) do
          971  +				if rtype[1] < src.mime then
          972  +					rtype[2](src, top)
          973  +				end
          974  +			end
          975  +			local ft = flatten(top)
          976  +			return ft
          977  +		end
          978  +
          979  +		function block_renderers.macro(b,s)
          980  +			local all = renderSubdoc(b.doc)
          981  +			local cat = catenate(ss.map(flatten,all))
          982  +			return tag('div', {}, cat)
          983  +		end
          984  +
          985  +		function block_renderers.quote(b,s)
          986  +			local ir = renderSubdoc(b.doc)
          987  +			return tag('blockquote', b.id and {id=getSafeID(b)} or {}, catenate(ss.map(flatten,ir)))
   815    988   		end
   816    989   
   817         -		return block_renderers;
          990  +		return block_renderers
   818    991   	end
   819    992   
   820    993   	local function getRenderers(procs)
   821    994   		local span_renderers = getSpanRenderers(procs)
   822    995   		local r = getBaseRenderers(procs,span_renderers)
   823    996   		r.block_renderers = getBlockRenderers(procs, r)
   824    997   		return r

Modified sirsem.lua from [eb2c53bff4] to [aed49421db].

     1      1   -- [สž] sirsem.lua
     2      2   --  ~ lexi hale <lexi@hale.su>
     3      3   --    glowpelt (hsl conversion)
     4      4   --  ? utility library with functionality common to
     5      5   --    cortav.lua and its extensions
     6         ---    from Ranuir "software utility"
            6  +--  \ from Ranuir "software utility"
     7      7   --  > local ss = require 'sirsem.lua'
     8      8   
     9      9   local ss
    10     10   do -- pull ourselves up by our own bootstraps
    11     11   	local package = _G.package
    12     12   	-- prevent namespace from being broken by env shenanigans
    13     13   	local function namespace(name, tbl)
................................................................................
   473    473   				bestmatch = k
   474    474   				bestlen = #kt
   475    475   			end
   476    476   		end
   477    477   	::skip::end
   478    478   	return tbl[bestmatch] or tbl[true], bestmatch
   479    479   end
          480  +
          481  +function ss.str.b64e(str)
          482  +	local bytes = {}
          483  +	local n = 1
          484  +	for i=1, #str, 3 do
          485  +		local triple = {string.byte(str, i, i+2)}
          486  +		local T = function(q)
          487  +			return triple[q] or 0
          488  +		end
          489  +		local B = function(q)
          490  +		print(q)
          491  +			if q <= 25 then
          492  +				return string.char(0x41 + q)
          493  +			elseif q <= 51 then
          494  +				return string.char(0x61 + (q-26))
          495  +			elseif q <= 61 then
          496  +				return string.char(0x30 + (q-52))
          497  +			elseif q == 62 then
          498  +				return '+'
          499  +			elseif q == 63 then
          500  +				return '/'
          501  +			else error('base64 algorithm broken') end
          502  +		end
          503  +		local quads = {
          504  +			((T(1) & 0xFC) >> 2);
          505  +			((T(1) & 0x03) << 4) | ((T(2) & 0xF0) >> 4);
          506  +			((T(2) & 0x0F) << 2) | ((T(3) & 0xC0) >> 6);
          507  +			((T(3) & 0x3F));
          508  +		}
          509  +
          510  +		bytes[n + 0] = B(quads[1])
          511  +		bytes[n + 1] = B(quads[2])
          512  +		if triple[2] then
          513  +			bytes[n + 2] = B(quads[3])
          514  +			if triple[3] then
          515  +				bytes[n + 3] = B(quads[4])
          516  +			else
          517  +				bytes[n + 3] = '='
          518  +			end
          519  +		else
          520  +			bytes[n + 2] = '='
          521  +			bytes[n + 3] = '='
          522  +		end
          523  +
          524  +		n = n + 4
          525  +	end
          526  +
          527  +	return table.concat(bytes)
          528  +end
          529  +
          530  +function ss.str.b64d(str)
          531  +end
   480    532   
   481    533   ss.math = {}
   482    534   
   483    535   function ss.math.lerp(t, a, b)
   484    536   	return (1-t)*a + (t*b)
   485    537   end
   486    538   
................................................................................
  1110   1162   			-- versions at least can launch programs in a sane and secure
  1111   1163   			-- way.
  1112   1164   		else
  1113   1165   			return s
  1114   1166   		end
  1115   1167   	end, ...))
  1116   1168   end
         1169  +
         1170  +local fetchexn = ss.exnkind 'fetch'
         1171  +local fetchableProtocols = {
         1172  +	http = {
         1173  +		proto = {
         1174  +			{'http'};
         1175  +			{'https'};
         1176  +			{'http', 'tls'};
         1177  +		};
         1178  +		fetch = function(uri)
         1179  +			fetchexn('cortav must be compiled with the C shim and libcurl support to use http fetch'):throw()
         1180  +		end;
         1181  +	};
         1182  +	file = {
         1183  +		proto = {
         1184  +			{'file'};
         1185  +			{'file', 'txt'};
         1186  +			{'file', 'bin'};
         1187  +			{'asset'};
         1188  +			{'asset', 'txt'};
         1189  +			{'asset', 'bin'};
         1190  +		};
         1191  +		fetch = function(uri, env)
         1192  +			local assetDir = env.asset_base or '.'
         1193  +			if uri.namespace then
         1194  +				fetchexn('authority (hostname) segment is not supported in file: URIs'):throw()
         1195  +			end
         1196  +			if uri.svc then
         1197  +				fetchexn('service segment is not supported in file: URIs'):throw()
         1198  +			end
         1199  +			local mode = 'r'
         1200  +			local path = uri.path
         1201  +			if uri.class[1] == 'asset' then path = assetDir ..'/'.. path end
         1202  +			if uri.class[2] == 'bin'   then mode = 'rb' end
         1203  +			local fd,e = io.open(path, mode)
         1204  +			if not fd then
         1205  +				fetchexn('IO error fetching URI โ€œ%sโ€ (%s)', tostring(uri), e):throw()
         1206  +			end
         1207  +			local data = fd:read '*a'
         1208  +			fd:close()
         1209  +			return data
         1210  +		end;
         1211  +	};
         1212  +}
         1213  +
         1214  +function ss.match(a,b, eq)
         1215  +	if #a ~= #b then return false end
         1216  +	eq = eq or function(p,q) return p == q end
         1217  +	for i = 1, #a do
         1218  +		if not eq(a[i],b[i]) then return false end
         1219  +	end
         1220  +	return true
         1221  +end
         1222  +
         1223  +ss.uri = ss.declare {
         1224  +	ident = 'uri';
         1225  +	mk = function() return {
         1226  +		class = nil;
         1227  +		namespace = nil;
         1228  +		path = nil;
         1229  +		query = nil;
         1230  +		frag = nil;
         1231  +		auth = nil;
         1232  +	} end;
         1233  +	construct = function(me, str)
         1234  +		local enc = ss.str.enc.utf8
         1235  +		-- URIs must be either ASCII or utf8, so we  read and
         1236  +		-- store as UTF8. to use a URI in another encoding, it
         1237  +		-- must be manually converted to and fro using the
         1238  +		-- appropriate functions, such as encodeUCS
         1239  +		if not str then return end
         1240  +		me.raw = str
         1241  +		local rem = str
         1242  +		local s_class do
         1243  +			local s,r = rem:match '^([^:]+):(.*)$'
         1244  +			s_class, rem = s,r
         1245  +		end
         1246  +		if not rem then
         1247  +			ss.uri.exn('invalid URI โ€œ%sโ€', str):throw()
         1248  +		end
         1249  +		local s_ns do
         1250  +			local s,r = rem:match '^//([^/]*)(.*)$'
         1251  +			if s then s_ns, rem = s,r end
         1252  +		end
         1253  +		local h_query
         1254  +		local s_frag
         1255  +		local s_path if rem ~= '' then
         1256  +			local s,q,r = rem:match '^([^?#]*)([?#]?)(.*)$'
         1257  +			if s == '' then s = nil end
         1258  +			s_path, rem = s,r
         1259  +
         1260  +			if q == '#' then
         1261  +				s_frag = rem
         1262  +			elseif q == '?' then
         1263  +				h_query = true
         1264  +			end
         1265  +		else s_path = '' end
         1266  +
         1267  +		local s_query if h_query then
         1268  +			local s,q,r = rem:match '^([^#]*)(#?)(.*)$'
         1269  +			s_query, rem = s,r
         1270  +			if q~='' then s_frag = rem end
         1271  +		end
         1272  +
         1273  +		local function dec(str)
         1274  +			if not str then return end
         1275  +			return str:gsub('%%([0-9A-Fa-f][0-9A-Fa-f])', function(hex)
         1276  +				return string.char(tonumber(hex,16))
         1277  +			end)
         1278  +		end
         1279  +
         1280  +		local s_auth if s_ns then
         1281  +			local s,r = s_ns:match('^([^@]*)@(.*)$')
         1282  +			if s then
         1283  +				s_ns = r
         1284  +				if s ~= '' then
         1285  +					 s_auth = s
         1286  +				end
         1287  +			end
         1288  +		end
         1289  +
         1290  +		local s_svc if s_ns then
         1291  +			local r,s = s_ns:match('^(.*):(.-)$')
         1292  +			if r then
         1293  +				s_ns = r
         1294  +				if s and s ~= '' then
         1295  +					s_svc = s
         1296  +				end
         1297  +			end
         1298  +		end
         1299  +
         1300  +		me.class = ss.str.split(enc, s_class, '+', {keep_empties=true})
         1301  +		for i,v in ipairs(me.class) do me.class[i] = dec(v) end
         1302  +		me.auth = dec(s_auth)
         1303  +		me.svc = dec(s_svc)
         1304  +		me.namespace = dec(s_ns)
         1305  +		me.path = dec(s_path)
         1306  +		me.query = dec(s_query)
         1307  +		me.frag = dec(s_frag)
         1308  +	end;
         1309  +	cast = {
         1310  +		string = function(me)
         1311  +			local function san(str, chars)
         1312  +				-- TODO IRI support
         1313  +				chars = chars or ''
         1314  +				local ptn = '-a-zA-Z0-9_.,;'
         1315  +				ptn = ptn .. chars
         1316  +				return (str:gsub('[^'..ptn..']', function(c)
         1317  +					if c == ' ' then return '+' end
         1318  +					return string.format('%%%02X', string.byte(c))
         1319  +				end))
         1320  +			end
         1321  +			if me.class == nil or next(me.class) == nil then
         1322  +				return 'none:'
         1323  +			end
         1324  +			local parts = {
         1325  +				table.concat(ss.map(san,me.class), '+') .. ':';
         1326  +			}
         1327  +			if me.namespace or me.auth or me.svc then
         1328  +				table.insert(parts, '//')
         1329  +				if me.auth then
         1330  +					table.insert(parts, san(me.auth,':') .. '@')
         1331  +				end
         1332  +				if me.namespace then
         1333  +					table.insert(parts, san(me.namespace))
         1334  +				end
         1335  +				if me.svc then
         1336  +					table.insert(parts, ':' .. san(me.svc))
         1337  +				end
         1338  +				if me.path and not ss.str.begins(me.path, '/') then
         1339  +					table.insert(parts, '/')
         1340  +				end
         1341  +			end
         1342  +			if me.path then
         1343  +				table.insert(parts, san(me.path,'+/=&'))
         1344  +			end
         1345  +			if me.query then
         1346  +				table.insert(parts, '?' .. san(me.query,'?+/=&'))
         1347  +			end
         1348  +			if me.frag then
         1349  +				table.insert(parts, '#' .. san(me.frag,'+/=&'))
         1350  +			end
         1351  +			return table.concat(parts)
         1352  +		end;
         1353  +	};
         1354  +	fns = {
         1355  +		canfetch = function(me)
         1356  +			for id, pr in pairs(fetchableProtocols) do
         1357  +				for _, p in ipairs(pr.proto) do
         1358  +					if ss.match(me.class, p) then return id end
         1359  +				end
         1360  +			end
         1361  +			return false
         1362  +		end;
         1363  +		fetch = function(me, env)
         1364  +			local pid = me:canfetch()
         1365  +			if (not pid) or fetchableProtocols[pid].fetch == nil then
         1366  +				ss.uri.exn("URI โ€œ%sโ€ is unfetchable", tostring(me)):throw()
         1367  +			end
         1368  +			local proto = fetchableProtocols[pid]
         1369  +			return proto.fetch(me, env or {})
         1370  +		end;
         1371  +	};
         1372  +}
         1373  +ss.uri.exn = ss.exnkind 'URI'
  1117   1374   
  1118   1375   ss.mime = ss.declare {
  1119   1376   	ident = 'mime-type';
  1120   1377   	mk = function() return {
  1121   1378   		class = nil;
  1122   1379   		kind = nil;
  1123   1380   		opts = {};