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: |
3ee2195d4f21f8e001e7821aed4b3ac1 |
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 = {};