Overview
Comment: | add extension mechanism, move toc to extensions, update docs, fix bugs |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
330e1ecfdb17218ca9fc8468b0f99ab7 |
User & Date: | lexi on 2021-12-22 10:19:13 |
Other Links: | manifest | tags |
Context
2021-12-22
| ||
10:19 | disable backtraces check-in: 2af5c4085a user: lexi tags: trunk | |
10:19 | add extension mechanism, move toc to extensions, update docs, fix bugs check-in: 330e1ecfdb user: lexi tags: trunk | |
2021-12-21
| ||
05:04 | add --version -V flag check-in: 0c6a784678 user: lexi tags: trunk | |
Changes
Modified cli.lua from [bdd85f20a4] to [497f5957f5].
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ... 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 ... 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 |
} local function main(input, output, log, mode, suggestions, vars) local doc = ct.parse(input.stream, input.src, mode) input.stream:close() if mode['parse:show-tree'] then log:write(dump(doc)) end -- the document has now had a chance to give its say; if it hasn't specified -- any modes of its own, we now merge in the 'weak modes' (suggestions) for k,v in pairs(suggestions) do if not mode[k] then mode[k] = v end end ................................................................................ if i + nargs > #arg then ct.exns.cli('not enough arguments for switch --%s (%s expected)', longopt, nargs):throw() end local nt = {} for j = i+1, i+nargs do table.insert(nt, arg[j]) end print('onsw') elseif nargs == 1 then onswitch[longopt](arg[i+1]) else onswitch[longopt]() end i = i + nargs end ................................................................................ input.stream = file input.src.file = args[1] end return main(input, outp, log, mode, suggestions, vars) end local ok, e = pcall(entry_cli) -- local ok, e = true, entry_cli() if not ok then local str = 'translation failure' if ss.exn.is(e) then str = e.kind.desc end local color = false if log:seek() == nil then |
| | | | |
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ... 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 ... 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 |
} local function main(input, output, log, mode, suggestions, vars) local doc = ct.parse(input.stream, input.src, mode) input.stream:close() if mode['parse:show-tree'] then log:write(ss.dump(doc)) end -- the document has now had a chance to give its say; if it hasn't specified -- any modes of its own, we now merge in the 'weak modes' (suggestions) for k,v in pairs(suggestions) do if not mode[k] then mode[k] = v end end ................................................................................ if i + nargs > #arg then ct.exns.cli('not enough arguments for switch --%s (%s expected)', longopt, nargs):throw() end local nt = {} for j = i+1, i+nargs do table.insert(nt, arg[j]) end onswitch[longopt](table.unpack(nt)) elseif nargs == 1 then onswitch[longopt](arg[i+1]) else onswitch[longopt]() end i = i + nargs end ................................................................................ input.stream = file input.src.file = args[1] end return main(input, outp, log, mode, suggestions, vars) end -- local ok, e = pcall(entry_cli) local ok, e = true, entry_cli() if not ok then local str = 'translation failure' if ss.exn.is(e) then str = e.kind.desc end local color = false if log:seek() == nil then |
Modified cortav.ct from [e9d2ad32df] to [c71fe3a9e8].
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 ... 199 200 201 202 203 204 205 206 207 208 209 210 211 212 ... 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 |
## styled text most blocks contain a sequence of spans. these spans are produced by interpreting a stream of [*styled-text] following the control sequence. styled-text is a sequence of codepoints potentially interspersed with escapes. an escape is formed by an open square bracket [$\[] followed by a [*span control sequence], and arguments for that sequence like more styled-text. escapes can be nested. * strong \[*[!styled-text]\]: causes its text to stand out from the narrative, generally rendered as bold or a brighter color. * emphatic \[![!styled-text]\]: indicates that its text should be spoken with emphasis, generally rendered as italics * literal \[$[!styled-text]\]: indicates that its text is a reference to a literal sequence of characters, variable name, or other discrete token. generally rendered in monospace * strikeout \[$[~styled-text]\]: indicates that its text should be struck through or otherwise indicated for deletion * insertion \[$[+styled-text]\]: indicates that its text should be indicated as a new addition to the text body. ** consider using a macro definition [$\edit: [~[#1]][+[#2]]] to save typing if you are doing editing work * 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. * footnote \[^[!ref] [!styled-text]\]: annotates the text with a defined footnote * raw \[\\[!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 * raw literal \[$\\[!raw-text]\]: shorthand for [\[$[\…]]] * macro \{[!name] [!arguments]\}: invokes a [>ex.mac macro], specified with a reference * argument \[#[!var]\]: in macros only, inserts the [$var]-th argument. otherwise, inserts a context variable provided by the renderer. ................................................................................ where possible, instead of [$needs x y z], the directive [$when has-ext x y z] should be used instead. this causes the next section to be rendered only if the named extensions are available. [$unless has-ext x y z] can be used to provide an alternative format. extensions are mainly interacted with through directives. all extension directives must be prefixed with the name of the extension. ### toc sections that have a title will be included in the table of contents. the table of contents is by default inserted at the break between the first level-1 section and the section immediately following it. you may instead place the directive [$toc] where you wish the TOC to be inserted, or suppress it entirely with [$inhibits toc]. note that some renderers may not display the TOC as part of the document itself. ### smart-quotes a cortav renderer may automatically translate punctuation marks to other punctuation marks depending on their context. ### hilite code can be highlighted according to the formal language it is written in. ### lua ................................................................................ -m html:hue-spread 35 \ -y html:dark-on-light # could also be written as: $ cortav readme.ct -ommmmy readme.html render:format html html:width 40em html:accent 80 html:hue-spread 35 html:dark-on-light ~~~ ## further directions ### LCH support right now, the use of color in the HTML renderer is very unsatisfactory. the accent mechanism operates on the basis of the CSS HSL function, which is not perceptually uniform; different hues will present different mixes of brightness and some (yellows?) may be ugly or unreadable. the ideal solution would be to simply switch to using LCH based colors. unfortunately, only Safari actually supports the LCH color function right now, and it's unlikely (unless Lea Verou and her husband manage to work a miracle) that Colors Level 4 is going to be implemented very widely any time soon. this leaves us in an awkward position. we can of course do the math ourselves, working in LCH to implement the internal [$@tone] macro, and then "converting" these colors to HSL. unfortunately, you can't actually convert from LCH to HSL; it's like converting from pounds to kilograms. LCH can represent any color the human visual system can perceive; sRGB can't, and CSS HSL is implemented in sRGB. however, we could at least approximate something that would allow for perceptually uniform brightness, which would be an improvement, and this is probably the direction to go in, unless a miracle occurs and [$lch()] or [$color()] pop up in Blink. it may be possible to do a more reasonable job of handling colors in the postscript and TeX outputs. unsure about SVG but i assume it suffers the same problems HTML/CSS do. does groff even support color?? ### intent files there's currently no standard way to describe the intent and desired formatting of a document besides placing pragmas in the source file itself. this is extremely suboptimal, as when generating collections of documents, it's ideal to be able to keep all formatting information in one place. users should also be able to specify their own styling overrides that describe the way they prefer to read [$cortav] files, especially for uses like gemini or gopher integration. at some point soon [$cortav] needs to address this by adding intent files that can be activated from outside the source file, such as with a command line flag or a configuration file setting. these will probably consist of lines that are interpreted as pragmata. in addition to the standard intent format however, individual implementations should feel free to provide their own ways to provide intent metadata; e.g. the reference implementation, which has a lua interpreter available, should be able to take a lua script that runs after the parse stage and generates . this will be particularly useful for the end-user who wishes to specify a particular format she likes reading her files in without forcing that format on everyone she sends the compiled document to, as it will be able to interrogate the document and make intelligent decisions about what pragmata to apply. |
| | > > > > > > > > > > > > > > > > > > > > > > |
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 ... 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 ... 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 |
## styled text most blocks contain a sequence of spans. these spans are produced by interpreting a stream of [*styled-text] following the control sequence. styled-text is a sequence of codepoints potentially interspersed with escapes. an escape is formed by an open square bracket [$\[] followed by a [*span control sequence], and arguments for that sequence like more styled-text. escapes can be nested. * strong \[*[!styled-text]\]: causes its text to stand out from the narrative, generally rendered as bold or a brighter color. * emphatic \[![!styled-text]\]: indicates that its text should be spoken with emphasis, generally rendered as italics * literal \[$[!styled-text]\]: indicates that its text is a reference to a literal sequence of characters, variable name, or other discrete token. generally rendered in monospace * strikeout \[~[!styled-text]\]: indicates that its text should be struck through or otherwise indicated for deletion * insertion \[+[!styled-text]\]: indicates that its text should be indicated as a new addition to the text body. ** consider using a macro definition [$\edit: [~[#1]][+[#2]]] to save typing if you are doing editing work * 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. * footnote \[^[!ref] [!styled-text]\]: annotates the text with a defined footnote * raw \[\\[!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 * raw literal \[$\\[!raw-text]\]: shorthand for [\[$[\…]]] * macro \{[!name] [!arguments]\}: invokes a [>ex.mac macro], specified with a reference * argument \[#[!var]\]: in macros only, inserts the [$var]-th argument. otherwise, inserts a context variable provided by the renderer. ................................................................................ where possible, instead of [$needs x y z], the directive [$when has-ext x y z] should be used instead. this causes the next section to be rendered only if the named extensions are available. [$unless has-ext x y z] can be used to provide an alternative format. extensions are mainly interacted with through directives. all extension directives must be prefixed with the name of the extension. ### toc sections that have a title will be included in the table of contents. the table of contents is by default inserted at the break between the first level-1 section and the section immediately following it. you may instead place the directive [$toc] where you wish the TOC to be inserted, or suppress it entirely with [$inhibits toc]. note that some renderers may not display the TOC as part of the document itself. toc provides the directives: * [$%[*toc]]: insert a table of contents in the specified position. this can be used more than once, but doing so may have confusing, incorrect, or nonsensical results under some renderers, and some may just ignore the directive entirely * [$%[*toc] mark [!styled-text]]: inserts a TOC entry with the label [!styled-text] pointing to the current location. this can be used to e.g. mark noteworthy images, instances of long quotes or literal blocks, or functions inside an expanded code block. * [$%[*toc] name [!id styled-text]]: like [$%[*toc] mark] but allows an additional [!id] parameter which specifies the ID the renderer will assign to an anchor element. this is not meaningful for all renderers and when it is, it is up to the renderer to decide what it means. ** the [*html] render backend interprets [!id] as the [$id] element for the anchor tag ** the [*groff] render backend ignores [!id] ### smart-quotes a cortav renderer may automatically translate punctuation marks to other punctuation marks depending on their context. ### hilite code can be highlighted according to the formal language it is written in. ### lua ................................................................................ -m html:hue-spread 35 \ -y html:dark-on-light # could also be written as: $ cortav readme.ct -ommmmy readme.html render:format html html:width 40em html:accent 80 html:hue-spread 35 html:dark-on-light ~~~ ## further directions ### additional backends it is eventually intended to support to following backends, if reasonably practicable. * [*html]: emit HTML and CSS code to typeset the document. [!in progress] * [*svg]: emit SVG, taking advantage of its precise layout features to produce a nicely formatted and paginated document. pagination can be accomplished through emitting multiple files or by assigning one layer to each page. [!long term] * [*groff]: the most important output backend, rivalling [*html]. will allow the document to be typeset in a wide variety of formats, including PDF and manpage. [!short term] * [*gemtext]: essentially a downrezzing of cortav to make it readable to Gemini clients some formats may eventually warrant their own renderer, but are not a priority: * [*text]: cortav source files are already plain text, but a certain amount of layout could be done using ascii art. * [*ansi]: emit sequences of ANSI escape codes to lay out a document in a terminal-friendly way * [*tex]: TeX is an unholy abomination and i neither like nor use it, but lots of people do and if cortav ever catches on, a TeX backend should probably be written eventually. PDF is not on either list because it's a nightmarish mess of a format and groff, which is installed on most linux systems already, can easily generate PDFs ### LCH support right now, the use of color in the HTML renderer is very unsatisfactory. the accent mechanism operates on the basis of the CSS HSL function, which is not perceptually uniform; different hues will present different mixes of brightness and some (yellows?) may be ugly or unreadable. the ideal solution would be to simply switch to using LCH based colors. unfortunately, only Safari actually supports the LCH color function right now, and it's unlikely (unless Lea Verou and her husband manage to work a miracle) that Colors Level 4 is going to be implemented very widely any time soon. this leaves us in an awkward position. we can of course do the math ourselves, working in LCH to implement the internal [$@tone] macro, and then "converting" these colors to HSL. unfortunately, you can't actually convert from LCH to HSL; it's like converting from pounds to kilograms. LCH can represent any color the human visual system can perceive; sRGB can't, and CSS HSL is implemented in sRGB. however, we could at least approximate something that would allow for perceptually uniform brightness, which would be an improvement, and this is probably the direction to go in, unless a miracle occurs and [$lch()] or [$color()] pop up in Blink. it may be possible to do a more reasonable job of handling colors in the postscript and TeX outputs. unsure about SVG but i assume it suffers the same problems HTML/CSS do. does groff even support color?? ### intent files there's currently no standard way to describe the intent and desired formatting of a document besides placing pragmas in the source file itself. this is extremely suboptimal, as when generating collections of documents, it's ideal to be able to keep all formatting information in one place. users should also be able to specify their own styling overrides that describe the way they prefer to read [$cortav] files, especially for uses like gemini or gopher integration. at some point soon [$cortav] needs to address this by adding intent files that can be activated from outside the source file, such as with a command line flag or a configuration file setting. these will probably consist of lines that are interpreted as pragmata. in addition to the standard intent format however, individual implementations should feel free to provide their own ways to provide intent metadata; e.g. the reference implementation, which has a lua interpreter available, should be able to take a lua script that runs after the parse stage and generates . this will be particularly useful for the end-user who wishes to specify a particular format she likes reading her files in without forcing that format on everyone she sends the compiled document to, as it will be able to interrogate the document and make intelligent decisions about what pragmata to apply. |
Modified cortav.lua from [7d896a453b] to [e7bf814ce9].
106 107 108 109 110 111 112 113 114 115 116 117 118 119 ... 149 150 151 152 153 154 155 156 157 158 159 160 161 162 ... 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 ... 221 222 223 224 225 226 227 228 229 230 231 232 233 234 ... 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 ... 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 ... 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 ... 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 ... 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 ... 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 ... 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 .... 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 .... 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 .... 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 .... 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 .... 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 .... 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 .... 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 |
fns = { fail = function(self, msg, ...) ct.exns.tx(msg, self.src.file, self.line or 0, ...):throw() end; insert = function(self, block) block.origin = self:clone() table.insert(self.sec.blocks,block) end; ref = function(self,id) if not id:find'%.' then local rid = self.sec.refs[id] if self.sec.refs[id] then return self.sec.refs[id] else self:fail("no such ref %s in current section", id or '') end ................................................................................ fns = { mksec = function(self, id, depth) local o = ct.sec(id, depth) if id then self.sections[id] = o end table.insert(self.secorder, o) return o end; context_var = function(self, var, ctx, test) local fail = function(...) if test then return false end ctx:fail(...) end if startswith(var, 'cortav.') then local v = var:sub(8) ................................................................................ elseif self.vars[var] then return self.vars[var] else if test then return false end return '' -- is this desirable behavior? end end; }; mk = function() return { sections = {}; secorder = {}; embed = {}; meta = {}; vars = {}; } end; } -- FP helper functions local function fmtfn(str) return function(...) return string.format(str, ...) ................................................................................ ct.exns.ext 'extension missing “id” field':throw() end if ct.ext.loaded[ext.id] then ct.exns.ext('there is already an extension with ID “%s” loaded', ext.id):throw() end ct.ext.loaded[ext.id] = ext end -- renderer engines function ct.render.html(doc, opts) local doctitle = opts['title'] local f = string.format local ids = {} local canonicalID = {} ................................................................................ } section > figure.listing > hr { border: none; margin: 0; height: 0.7em; counter-increment: line-number; } ]]; toc = [[ ]]; tocFixed = [[ @media (min-width: calc(@[width]:[100vw] + 20em)) { ol.toc { position: fixed; padding-top: 1em; padding-bottom: 1em; padding-right: 1em; margin-top: 0; margin-bottom: 0; right: 0; top: 0; bottom: 0; max-width: calc(50vw - ((@[width]:[0]) / 2) - 3.5em); overflow-y: auto; } @media (max-width: calc(@[width]:[100vw] + 30em)) { ol.toc { max-width: calc(100vw - ((@[width]:[0])) - 9.5em); } body { margin-left: 5em; } } } ]]; } local stylesNeeded = {} local function getSpanRenderers(tag,elt) local htmlDoc = function(title, head, body) return [[<!doctype html>]] .. tag('html',nil, tag('head', nil, elt('meta',{charset = 'utf-8'}) .. (title and tag('title', nil, title) or '') .. (head or '')) .. tag('body', nil, body or '')) ................................................................................ function span_renderers.var(v,b,s) local val if v.pos then if not v.origin.invocation then v.origin:fail 'positional arguments can only be used in a macro invocation' elseif not v.origin.invocation.args[v.pos] then v.origin:fail('macro invocation %s missing positional argument #%u', v.origin.invocation.macro, v.pos) end val = v.origin.invocation.args[v.pos] else val = v.origin.doc:context_var(v.var, v.origin) end if v.raw then return val ................................................................................ span_renderers = span_renderers; htmlSpan = htmlSpan; htmlDoc = htmlDoc; } end local function getBlockRenderers(tag,elt,sr,catenate) local function insert_toc(b,s) local lst = {tag = 'ol', attrs={class='toc'}, nodes={}} stylesNeeded.toc = true if opts['width'] then stylesNeeded.tocFixed = true end local stack = {lst} local top = function() return stack[#stack] end local all = s.origin.doc.secorder for i, sec in ipairs(all) do if sec.heading_node then local ent = tag('li',nil, catenate{tag('a', {href='#'..getSafeID(sec)}, sr.htmlSpan(sec.heading_node.spans))}) if sec.depth > #stack then local n = {tag = 'ol', attrs={}, nodes={ent}} table.insert(top().nodes[#top().nodes].nodes, n) table.insert(stack, n) else if sec.depth < #stack then for j=#stack,sec.depth+1,-1 do stack[j] = nil end end table.insert(top().nodes, ent) end end end return lst end local block_renderers = { paragraph = function(b,s) stylesNeeded.paragraph = true; return tag('p', nil, sr.htmlSpan(b.spans, b, s), b) end; directive = function(b,s) -- deal with renderer directives local _, cmd, args = b.words(2) ................................................................................ else -- handle other uses of labels here end end; ['list-item'] = function(b,s) return tag('li', nil, sr.htmlSpan(b.spans, b, s), b) end; toc = insert_toc; table = function(b,s) local tb = {} for i, r in ipairs(b.rows) do local row = {} for i, c in ipairs(r) do table.insert(row, tag(c.header and 'th' or 'td', {align=c.align}, sr.htmlSpan(c.spans, b))) ................................................................................ end, b.lines) if b.title then table.insert(nodes,1,tag('figcaption',nil,sr.htmlSpan(b.title))) end if b.lang then langsused[b.lang] = true end return tag('figure', {class='listing', lang=b.lang, id=b.id and getSafeID(b)}, catenate(nodes)) end; ['break'] = function() --[[nop]] end; } return block_renderers; end local pspan = getSpanRenderers(function(t,a,v) return v end, function(t,a) return '' end) local function getRenderers(tag,elt,catenate) local r = getSpanRenderers(tag,elt) r.block_renderers = getBlockRenderers(tag,elt,r,catenate) return r end local elt = function(t,attrs) return f('<%s%s>', t, attrs and ss.reduce(function(a,b) return a..b end, '', ss.map(function(v,k) if v == true then return ' '..k elseif v then return f(' %s="%s"', k, v) end end, attrs)) or '') end local tag = function(t,attrs,body) return f('%s%s</%s>', elt(t,attrs), body, t) end local ir = {} local toc local dr = getRenderers(tag,elt,table.concat) -- default renderers local plainr = getRenderers(function(t,a,v) return v end, function(t,a) return '' end, table.concat) local irBlockRdrs = getBlockRenderers( function(t,a,v,o) return {tag = t, attrs = a, nodes = type(v) == 'string' and {v} or v, src = o} end, function(t,a,o) return {tag = t, attrs = a, src = o} end, dr, function(...) return ... end) for i, sec in ipairs(doc.secorder) do if doctitle == nil and sec.depth == 1 and sec.heading_node then doctitle = plainr.htmlSpan(sec.heading_node.spans, sec.heading_node, sec) end local irs if sec.kind == 'ordinary' then if #(sec.blocks) > 0 then irs = {tag='section',attrs={id = getSafeID(sec)},nodes={}} for i, block in ipairs(sec.blocks) do local rd = irBlockRdrs[block.kind](block,sec) if rd then if opts['heading-anchors'] and block == sec.heading_node then stylesNeeded.headingAnchors = true table.insert(rd.nodes, ' ') table.insert(rd.nodes, { tag = 'a'; attrs = {href = '#' .. irs.attrs.id, class='anchor'}; nodes = {type(opts['heading-anchors'])=='string' and opts['heading-anchors'] or '§'}; }) end table.insert(irs.nodes, rd) end end end elseif sec.kind == 'blockquote' then elseif sec.kind == 'listing' then elseif sec.kind == 'embed' then end if irs then table.insert(ir, irs) end end -- restructure passes ---- list insertion pass local lists = {} for _, sec in pairs(ir) do if sec.tag == 'section' then local i = 1 while i <= #sec.nodes do local v = sec.nodes[i] if v.tag == 'li' then ................................................................................ sec.nodes[i] = struc end end end end -- collection pass local function collect_nodes(t) local ts = '' for i,v in ipairs(t) do if type(v) == 'string' then ts = ts .. v elseif v.nodes then ts = ts .. tag(v.tag, v.attrs, collect_nodes(v.nodes)) elseif v.text then ts = ts .. tag(v.tag,v.attrs,v.text) else ts = ts .. elt(v.tag,v.attrs) end end return ts end local body = collect_nodes(ir) for k in pairs(langsused) do ................................................................................ end local head = {} local styletag = '' if opts['link-css'] then local css = opts['link-css'] if type(css) ~= 'string' then ct.exns.mode('must be a string', 'html:link-css'):throw() end styletag = styletag .. elt('link',{rel='stylesheet',type='text/css',href=opts['link-css']}) end if next(styles) then if opts['gen-styles'] then styletag = styletag .. tag('style',{type='text/css'},table.concat(styles)) end table.insert(head, styletag) end if opts['fossil-uv'] then return tag('div',{class='fossil-doc',['data-title']=doctitle},styletag .. body) elseif opts.snippet then return styletag .. body else return dr.htmlDoc(doctitle, next(head) and table.concat(head), body) end end ................................................................................ end flush() return spans end local function blockwrap(fn) return function(l,c) local block = fn(l,c) block.origin = c:clone(); table.insert(c.sec.blocks, block); end end local insert_paragraph = blockwrap(function(l,c) if l:sub(1,1) == '.' then l = l:sub(2) end return { kind = "paragraph"; spans = ct.parse_span(l, c); } end) local insert_section = function(l,c) local depth, id, t = l:match '^([#§]+)([^%s]*)%s*(.-)$' if id and id ~= "" then if c.doc.sections[id] then c:fail('duplicate section name “%s”', id) end else id = nil end ................................................................................ origin = s.origin; captions = s; } table.insert(s.blocks, heading) s.heading_node = heading end c.sec = s end local dsetmeta = function(w,c) local key, val = w(1) c.doc.meta[key] = val end local dextctl = function(w,c) local mode, exts = w(1) for e in exts:gmatch '([^%s]+)' do if mode == 'uses' then elseif mode == 'needs' then elseif mode == 'inhibits' then ................................................................................ c.hide_next = mode == 'unless' end; ct.directives = { author = dsetmeta; license = dsetmeta; keywords = dsetmeta; desc = dsetmeta; toc = function(w,c) local toc, op, val = w(2) if op == nil then table.insert(c.sec.blocks, {kind='toc'}) end end; when = dcond; unless = dcond; expand = function(w,c) local _, m = w(1) if m ~= 'off' then c.expand_next = 1 else c.expand_next = 0 end end; } local function insert_table_row(l,c) local row = {} local buf local flush = function() if buf then buf.str = buf.str:gsub('%s+$','') table.insert(row, buf) end ................................................................................ if buf.str ~= '' then flush() end for _,v in pairs(row) do v.spans = ct.parse_span(v.str, c) end if #c.sec.blocks > 1 and c.sec.blocks[#c.sec.blocks].kind == 'table' then local tbl = c.sec.blocks[#c.sec.blocks] table.insert(tbl.rows, row) else table.insert(c.sec.blocks, { kind = 'table'; rows = {row}; origin = c:clone(); }) end end ct.ctlseqs = { {seq = '.', fn = insert_paragraph}; {seq = '¶', fn = insert_paragraph}; {seq = '❡', fn = insert_paragraph}; {seq = '#', fn = insert_section}; {seq = '§', fn = insert_section}; {seq = '+', fn = insert_table_row}; {seq = '|', fn = insert_table_row}; {seq = '│', fn = insert_table_row}; {pred = function(s,c) return s:match'^[*:]' end, fn = blockwrap(function(l,c) -- list local stars = l:match '^([*:]+)' local depth = utf8.len(stars) local id, txt = l:sub(#stars+1):match '^(.-)%s*(.-)$' local ordered = stars:sub(#stars) == ':' if id == '' then id = nil end return { kind = 'list-item'; depth = depth; ordered = ordered; spans = ct.parse_span(txt, c); } end)}; {seq = '\t', fn = function(l,c) local ref, val = l:match '\t+([^:]+):%s*(.*)$' c.sec.refs[ref] = val end}; {seq = '%', fn = function(l,c) -- directive local crit, cmdline = l:match '^%%([!%%]?)%s*(.*)$' local words = function(i) local wds = {} if i == 0 then return cmdline end for w,pos in cmdline:gmatch '([^%s]+)()' do table.insert(wds, w) i = i - 1 ................................................................................ return table.unpack(wds) end end end local cmd, rest = words(1) if ct.directives[cmd] then ct.directives[cmd](words,c) elseif cmd == c.doc.stage.mode['render:format'] then -- this is a directive for the renderer; insert it into the tree as is c:insert { kind = 'directive'; critical = crit == '!'; words = words; } elseif crit == '!' then c:fail('critical directive %s not supported',cmd) end end;}; {seq = '~~~', fn = blockwrap(function(l,c) local extract = function(ptn, str) local start, stop = str:find(ptn) if not start then return nil, str end local ex = str:sub(start,stop) local n = str:sub(1,start-1) .. str:sub(stop+1) return ex, n end ................................................................................ lang, s = extract('%b[]', s) if lang then lang = lang:sub(2,-2) end id, title = extract('#[^%s]+', s) if id then id = id:sub(2) end elseif l:match '^~~~' then -- MD shorthand style lang = l:match '^~~~%s*(.-)%s*$' end c.mode = { kind = 'code'; listing = { kind = 'listing'; lang = lang, id = id, title = title and ct.parse_span(title,c); lines = {}; } } if id then if c.sec.refs[id] then c:fail('duplicate ID %s', id) end c.sec.refs[id] = c.mode.listing end return c.mode.listing; end)}; {pred = function(s,c) if s:match '^[%-_][*_%-%s]+' then return true end if startswith(s, '—') then for c, p in eachcode(s) do if ({ ................................................................................ end)}; {fn = insert_paragraph}; } function ct.parse(file, src, mode) local function is_whitespace(cp) return cp == 0x20 end local ctx = ct.ctx.mk(src) ctx.line = 0 ctx.doc = ct.doc.mk() ctx.doc.src = src ctx.doc.stage = { kind = 'parse'; mode = mode; } ctx.sec = ctx.doc:mksec() -- toplevel section ctx.sec.origin = ctx:clone() for full_line in file:lines() do ctx.line = ctx.line + 1 local l for p, c in utf8.codes(full_line) do if not is_whitespace(c) then l = full_line:sub(p) break end end if ctx.mode then if ctx.mode.kind == 'code' then if l and l:match '^~~~%s*$' then ctx.mode = nil else -- TODO handle formatted code table.insert(ctx.mode.listing.lines, {l}) end else ctx:fail('unimplemented syntax mode %s', ctx.mode.kind) end else if l then local found = false for _, i in pairs(ct.ctlseqs) do if ((not i.seq ) or startswith(l, i.seq)) and ((not i.pred) or i.pred (l, ctx )) then found = true i.fn(l, ctx) break end end if not found then ctx:fail 'incomprehensible input line' end else if next(ctx.sec.blocks) and ctx.sec.blocks[#ctx.sec.blocks].kind ~= 'break' then table.insert(ctx.sec.blocks, {kind='break'}) end end end end return ctx.doc end |
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > < | < < < < < < < < < < < | < < < > | < < > > > > > > > > | | > | < > > > | < < < | > | | | < < < < < < | < < < < < < < < < < < < < < < < < < < < > > > < > > > > > > > < < < | | | > | | | | | | | | | | > > > > > > > > > > > > > > > > > > > > > | | | > > | > > > > > > > > > > > > > > > > > > > < < < < > > | < < < > > > | > > > > | > > > > > > > > > > > > > | | | | | | | | > | > > | > < < < < < < | > > < > < > > > > > > > > > > > > > > > > > > > > > > > | > | | | > > > > > > > > > > > > > > | | > > > | > > > > > > > > | > | | | | < | > | > > > > > > > > > | | > > > | > |
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 ... 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 ... 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 ... 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 ... 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 ... 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 ... 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 ... 746 747 748 749 750 751 752 753 754 755 756 757 758 759 ... 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 ... 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 .... 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 .... 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 .... 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 .... 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 .... 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 .... 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 .... 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 .... 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 |
fns = { fail = function(self, msg, ...) ct.exns.tx(msg, self.src.file, self.line or 0, ...):throw() end; insert = function(self, block) block.origin = self:clone() table.insert(self.sec.blocks,block) return block end; ref = function(self,id) if not id:find'%.' then local rid = self.sec.refs[id] if self.sec.refs[id] then return self.sec.refs[id] else self:fail("no such ref %s in current section", id or '') end ................................................................................ fns = { mksec = function(self, id, depth) local o = ct.sec(id, depth) if id then self.sections[id] = o end table.insert(self.secorder, o) return o end; allow_ext = function(self,name) if not ct.ext.loaded[name] then return false end if self.ext.inhibit[name] then return false end if self.ext.need[name] or self.ext.use[name] then return true end return ct.ext.loaded[name].default end; context_var = function(self, var, ctx, test) local fail = function(...) if test then return false end ctx:fail(...) end if startswith(var, 'cortav.') then local v = var:sub(8) ................................................................................ elseif self.vars[var] then return self.vars[var] else if test then return false end return '' -- is this desirable behavior? end end; job = function(self, name, pred, ...) -- convenience func return self.docjob:fork(name, pred, ...) end }; mk = function() return { sections = {}; secorder = {}; embed = {}; meta = {}; vars = {}; ext = { inhibit = {}; need = {}; use = {}; }; } end; construct = function(me) me.docjob = ct.ext.job('doc', me, nil) end; } -- FP helper functions local function fmtfn(str) return function(...) return string.format(str, ...) ................................................................................ ct.exns.ext 'extension missing “id” field':throw() end if ct.ext.loaded[ext.id] then ct.exns.ext('there is already an extension with ID “%s” loaded', ext.id):throw() end ct.ext.loaded[ext.id] = ext end function ct.ext.bind(doc) local fns = {} function fns.each(...) local cext local args = {...} return function() while true do cext = next(ct.ext.loaded, cext) if cext == nil then return nil end if doc == nil or doc:allow_ext(cext.id) then local v = ss.walk(ct.ext.loaded[cext.id], table.unpack(args)) if v ~= nil then return v, cext end end end end end function fns.hook(h, ...) -- this is the raw hook invocation function, used when hooks won't need -- private state to hold onto between invocation. if private state is -- necessary, construct a job instead local ret = {} -- for hooks that compile lists of responses from extensions for hook in fns.each('hook', h) do table.insert(ret,(hook(...))) end return ret end return fns end do local globalfns = ct.ext.bind() -- use these functions when document restrictions don't matter ct.ext.each, ct.ext.hook = globalfns.each, globalfns.hook end ct.ext.job = declare { ident = 'ext-job'; init = { states = {}; }; construct = function(me,name,doc,pred,...) print('constructing job',name,'for',doc) -- prepare contexts for relevant extensions me.name = name me.doc = doc -- for reqs + limiting for _, ext in pairs(ct.ext.loaded) do if pred == nil or pred(ext) then me.states[ext] = {} end end me:hook('init', ...) end; fns = { fork = function(me, name, pred, ...) -- generate a branch job linked to this job local branch = getmetatable(me)(name, me.doc, pred, ...) branch.parent = me return branch end; delegate = function(me, ext) -- creates a delegate for state access local submethods = { unwind = function(self, n) local function climb(dlg, job, n) if n == 0 then return job:delegate(dlg.extension) else return climb(dlg, job.parent, n-1) end end return climb(self._delegate_state, self._delegate_state.target, n) end; } local d = setmetatable({ _delegate_state = { target = (me._delegate_state and me._delegate_state.target) or me; extension = ext; }; }, { __name = 'job:delegate'; __index = function(self, key) local D = self._delegate_state if key == 'state' then return D.target.states[self._delegate_state.extension] elseif submethods[key] then return submethods[key] end return D.target[key] end; __newindex = function(self, key, value) local D = self._delegate_state if key == 'state' then D.target.states[D.extension] = value else D.target[D.extension] = value end end; }); return d; end; each = function(me, ...) local ek local path = {...} return function() while true do ek = next(me.states, ek) if not ek then return nil end if me.doc:allow_ext(ek.id) then local v = ss.walk(ek, table.unpack(path)) if v then return v, ek, me.states[ek] end end end end end; proc = function(me, ...) local p local owner local state for func, ext, s in me:each(...) do if p == nil then p = func owner = ext state = s else ct.exn.ext('extensions %s and %s define conflicting procedures for %s', owner.id, ext.id, table.concat({...},'.')):throw() end end if p == nil then return nil end if type(p) ~= 'function' then return p end return function(...) return p(me:delegate(owner), ...) end, owner, state end; hook = function(me, hook, ...) -- used when extensions may need to persist state across -- multiple functions or invocations local ret = {} local hook_id = me.name ..'_'.. hook for hookfn, ext, state in me:each('hook', hook_id) do print(' - running hook for ext',ext.id) table.insert(ret, (hookfn(me:delegate(ext),...))) end return ret end; }; } -- renderer engines function ct.render.html(doc, opts) local doctitle = opts['title'] local f = string.format local ids = {} local canonicalID = {} ................................................................................ } section > figure.listing > hr { border: none; margin: 0; height: 0.7em; counter-increment: line-number; } ]]; } local stylesNeeded = {} local render_state_handle = { doc = doc; opts = opts; style_rules = styles; -- use stylesneeded if at all possible stylesets = stylesets; stylesets_active = stylesNeeded; obj_htmlid = getSafeID; -- remaining fields added later } local renderJob = doc:job('render_html', nil, render_state_handle) local runhook = function(h, ...) return renderJob:hook(h, render_state_handle, ...) end local function getSpanRenderers(procs) local tag, elt, catenate = procs.tag, procs.elt, procs.catenate local htmlDoc = function(title, head, body) return [[<!doctype html>]] .. tag('html',nil, tag('head', nil, elt('meta',{charset = 'utf-8'}) .. (title and tag('title', nil, title) or '') .. (head or '')) .. tag('body', nil, body or '')) ................................................................................ function span_renderers.var(v,b,s) local val if v.pos then if not v.origin.invocation then v.origin:fail 'positional arguments can only be used in a macro invocation' elseif not v.origin.invocation.args[v.pos] then v.origin.invocation.origin:fail('macro invocation %s missing positional argument #%u', v.origin.invocation.macro, v.pos) end val = v.origin.invocation.args[v.pos] else val = v.origin.doc:context_var(v.var, v.origin) end if v.raw then return val ................................................................................ span_renderers = span_renderers; htmlSpan = htmlSpan; htmlDoc = htmlDoc; } end local function getBlockRenderers(procs, sr) local tag, elt, catenate = procs.tag, procs.elt, procs.catenate local null = function() return catenate{} end local block_renderers = { anchor = function(b,s) return tag('a',{id = getSafeID(b)},null()) end; paragraph = function(b,s) stylesNeeded.paragraph = true; return tag('p', nil, sr.htmlSpan(b.spans, b, s), b) end; directive = function(b,s) -- deal with renderer directives local _, cmd, args = b.words(2) ................................................................................ else -- handle other uses of labels here end end; ['list-item'] = function(b,s) return tag('li', nil, sr.htmlSpan(b.spans, b, s), b) end; table = function(b,s) local tb = {} for i, r in ipairs(b.rows) do local row = {} for i, c in ipairs(r) do table.insert(row, tag(c.header and 'th' or 'td', {align=c.align}, sr.htmlSpan(c.spans, b))) ................................................................................ end, b.lines) if b.title then table.insert(nodes,1,tag('figcaption',nil,sr.htmlSpan(b.title))) end if b.lang then langsused[b.lang] = true end return tag('figure', {class='listing', lang=b.lang, id=b.id and getSafeID(b)}, catenate(nodes)) end; aside = function(b,s) local bn = {} for _,v in pairs(b.lines) do table.insert(bn, tag('p', {}, sr.htmlSpan(v, b, s))) end return tag('aside', {}, bn) end; ['break'] = function() --[[nop]] end; } return block_renderers; end local function getRenderers(procs) local r = getSpanRenderers(procs) r.block_renderers = getBlockRenderers(procs, r) return r end local tagproc do local elt = function(t,attrs) return f('<%s%s>', t, attrs and ss.reduce(function(a,b) return a..b end, '', ss.map(function(v,k) if v == true then return ' '..k elseif v then return f(' %s="%s"', k, v) end end, attrs)) or '') end tagproc = { toTXT = { tag = function(t,a,v) return v end; elt = function(t,a) return '' end; catenate = table.concat; }; toIR = { tag = function(t,a,v,o) return { tag = t, attrs = a; nodes = type(v) == 'string' and {v} or v, src = o } end; elt = function(t,a,o) return { tag = t, attrs = a, src = o } end; catenate = function(...) return ... end; }; toHTML = { elt = elt; tag = function(t,attrs,body) return f('%s%s</%s>', elt(t,attrs), body, t) end; catenate = table.concat; }; } end local astproc = { toHTML = getRenderers(tagproc.toHTML); toTXT = getRenderers(tagproc.toTXT); toIR = { }; } astproc.toIR.span_renderers = ss.clone(astproc.toHTML); astproc.toIR.block_renderers = getBlockRenderers(tagproc.toIR,astproc.toHTML); -- note we use HTML here instead of IR span renderers, because as things -- currently stand we don't need that level of resolution. if we ever -- get to the point where we want to be able to twiddle spans around -- we'll need to introduce an IR span renderer render_state_handle.astproc = astproc; render_state_handle.tagproc = tagproc; -- bind to legacy names -- yikes this needs to be cleaned up so badly local ir = {} local dr = astproc.toHTML -- default renderers local plainr = astproc.toTXT local irBlockRdrs = astproc.toIR.block_renderers; render_state_handle.ir = ir; runhook('ir_assemble', ir) for i, sec in ipairs(doc.secorder) do if doctitle == nil and sec.depth == 1 and sec.heading_node then doctitle = astproc.toTXT.htmlSpan(sec.heading_node.spans, sec.heading_node, sec) end local irs if sec.kind == 'ordinary' then if #(sec.blocks) > 0 then irs = {tag='section',attrs={id = getSafeID(sec)},nodes={}} runhook('ir_section_build', irs, sec) for i, block in ipairs(sec.blocks) do local rd if irBlockRdrs[block.kind] then rd = irBlockRdrs[block.kind](block,sec) else local rdr = renderJob:proc('render',block.kind,'html') if rdr then rd = rdr({ state = render_state_handle; tagproc = tagproc.toIR; astproc = astproc.toIR; }, block, sec) end end if rd then if opts['heading-anchors'] and block == sec.heading_node then stylesNeeded.headingAnchors = true table.insert(rd.nodes, ' ') table.insert(rd.nodes, { tag = 'a'; attrs = {href = '#' .. irs.attrs.id, class='anchor'}; nodes = {type(opts['heading-anchors'])=='string' and opts['heading-anchors'] or '§'}; }) end table.insert(irs.nodes, rd) runhook('ir_section_node_insert', rd, irs, sec) end end end elseif sec.kind == 'blockquote' then elseif sec.kind == 'listing' then elseif sec.kind == 'embed' then end if irs then table.insert(ir, irs) end end -- restructure passes runhook('ir_restructure_pre', ir) ---- list insertion pass local lists = {} for _, sec in pairs(ir) do if sec.tag == 'section' then local i = 1 while i <= #sec.nodes do local v = sec.nodes[i] if v.tag == 'li' then ................................................................................ sec.nodes[i] = struc end end end end runhook('ir_restructure_post', ir) -- collection pass local function collect_nodes(t) local ts = '' for i,v in ipairs(t) do if type(v) == 'string' then ts = ts .. v elseif v.nodes then ts = ts .. tagproc.toHTML.tag(v.tag, v.attrs, collect_nodes(v.nodes)) elseif v.text then ts = ts .. tagproc.toHTML.tag(v.tag,v.attrs,v.text) else ts = ts .. tagproc.toHTML.elt(v.tag,v.attrs) end end return ts end local body = collect_nodes(ir) for k in pairs(langsused) do ................................................................................ end local head = {} local styletag = '' if opts['link-css'] then local css = opts['link-css'] if type(css) ~= 'string' then ct.exns.mode('must be a string', 'html:link-css'):throw() end styletag = styletag .. tagproc.toHTML.elt('link',{rel='stylesheet',type='text/css',href=opts['link-css']}) end if next(styles) then if opts['gen-styles'] then styletag = styletag .. tagproc.toHTML.tag('style',{type='text/css'},table.concat(styles)) end table.insert(head, styletag) end if opts['fossil-uv'] then return tagproc.toHTML.tag('div',{class='fossil-doc',['data-title']=doctitle},styletag .. body) elseif opts.snippet then return styletag .. body else return dr.htmlDoc(doctitle, next(head) and table.concat(head), body) end end ................................................................................ end flush() return spans end local function blockwrap(fn) return function(l,c,j) local block = fn(l,c,j) block.origin = c:clone(); table.insert(c.sec.blocks, block); j:hook('block_insert', c, block, l) end end local insert_paragraph = blockwrap(function(l,c) if l:sub(1,1) == '.' then l = l:sub(2) end return { kind = "paragraph"; spans = ct.parse_span(l, c); } end) local insert_section = function(l,c,j) local depth, id, t = l:match '^([#§]+)([^%s]*)%s*(.-)$' if id and id ~= "" then if c.doc.sections[id] then c:fail('duplicate section name “%s”', id) end else id = nil end ................................................................................ origin = s.origin; captions = s; } table.insert(s.blocks, heading) s.heading_node = heading end c.sec = s j:hook('section_attach', c, s) end local dsetmeta = function(w,c,j) local key, val = w(1) c.doc.meta[key] = val j:hook('metadata_set', key, val) end local dextctl = function(w,c) local mode, exts = w(1) for e in exts:gmatch '([^%s]+)' do if mode == 'uses' then elseif mode == 'needs' then elseif mode == 'inhibits' then ................................................................................ c.hide_next = mode == 'unless' end; ct.directives = { author = dsetmeta; license = dsetmeta; keywords = dsetmeta; desc = dsetmeta; when = dcond; unless = dcond; expand = function(w,c) local _, m = w(1) if m ~= 'off' then c.expand_next = 1 else c.expand_next = 0 end end; } local function insert_table_row(l,c,j) local row = {} local buf local flush = function() if buf then buf.str = buf.str:gsub('%s+$','') table.insert(row, buf) end ................................................................................ if buf.str ~= '' then flush() end for _,v in pairs(row) do v.spans = ct.parse_span(v.str, c) end if #c.sec.blocks > 1 and c.sec.blocks[#c.sec.blocks].kind == 'table' then local tbl = c.sec.blocks[#c.sec.blocks] table.insert(tbl.rows, row) j:hook('block_table_attach', c, tbl, row, l) j:hook('block_table_row_insert', c, tbl, row, l) else local tbl = { kind = 'table'; rows = {row}; origin = c:clone(); } table.insert(c.sec.blocks, tbl) j:hook('block_table_insert', c, tbl, l) j:hook('block_table_row_insert', c, tbl, tbl.rows[1], l) end end ct.ctlseqs = { {seq = '.', fn = insert_paragraph}; {seq = '¶', fn = insert_paragraph}; {seq = '❡', fn = insert_paragraph}; {seq = '#', fn = insert_section}; {seq = '§', fn = insert_section}; {seq = '+', fn = insert_table_row}; {seq = '|', fn = insert_table_row}; {seq = '│', fn = insert_table_row}; {seq = '!', fn = function(l,c,j) local last = c.sec.blocks[#c.sec.blocks] local txt = l:match '^%s*!%s*(.-)$' if (not last) or last.kind ~= 'aside' then local aside = { kind = 'aside'; lines = { ct.parse_span(txt, c) } } c:insert(aside) j:hook('block_aside_insert', c, aside, l) j:hook('block_aside_line_insert', c, aside, aside.lines[1], l) j:hook('block_insert', c, aside, l) else local sp = ct.parse_span(txt, c) table.insert(last.lines, sp) j:hook('block_aside_attach', c, last, sp, l) j:hook('block_aside_line_insert', c, last, sp, l) end end}; {pred = function(s,c) return s:match'^[*:]' end, fn = blockwrap(function(l,c) -- list local stars = l:match '^([*:]+)' local depth = utf8.len(stars) local id, txt = l:sub(#stars+1):match '^(.-)%s*(.-)$' local ordered = stars:sub(#stars) == ':' if id == '' then id = nil end return { kind = 'list-item'; depth = depth; ordered = ordered; spans = ct.parse_span(txt, c); } end)}; {seq = '\t', fn = function(l,c,j) local ref, val = l:match '\t+([^:]+):%s*(.*)$' c.sec.refs[ref] = val j:hook('section_ref_attach', c, ref, val, l) end}; {seq = '%', fn = function(l,c,j) -- directive local crit, cmdline = l:match '^%%([!%%]?)%s*(.*)$' local words = function(i) local wds = {} if i == 0 then return cmdline end for w,pos in cmdline:gmatch '([^%s]+)()' do table.insert(wds, w) i = i - 1 ................................................................................ return table.unpack(wds) end end end local cmd, rest = words(1) if ct.directives[cmd] then ct.directives[cmd](words,c,j) elseif cmd == c.doc.stage.mode['render:format'] then -- this is a directive for the renderer; insert it into the tree as is local dir = { kind = 'directive'; critical = crit == '!'; words = words; } c:insert(dir) j:hook('block_directive_render', j, c, dir) elseif c.doc:allow_ext(cmd) then -- extension directives begin with their id local ext = ct.ext.loaded[cmd] if ext.directives then local _, topcmd = words(2) if ext.directives[topcmd] then ext.directives[topcmd](j:delegate(ext), c, words) elseif ext.directives[true] then -- catch-all ext.directives[true](j:delegate(ext), c, words) elseif crit == '!' then c:fail('extension %s does not support critical directive %s', cmd, topcmd) end end elseif crit == '!' then c:fail('critical directive %s not supported',cmd) end end;}; {seq = '~~~', fn = blockwrap(function(l,c,j) local extract = function(ptn, str) local start, stop = str:find(ptn) if not start then return nil, str end local ex = str:sub(start,stop) local n = str:sub(1,start-1) .. str:sub(stop+1) return ex, n end ................................................................................ lang, s = extract('%b[]', s) if lang then lang = lang:sub(2,-2) end id, title = extract('#[^%s]+', s) if id then id = id:sub(2) end elseif l:match '^~~~' then -- MD shorthand style lang = l:match '^~~~%s*(.-)%s*$' end local mode = { kind = 'code'; listing = { kind = 'listing'; lang = lang, id = id, title = title and ct.parse_span(title,c); lines = {}; } } j:hook('mode_switch', c, mode) c.mode = mode if id then if c.sec.refs[id] then c:fail('duplicate ID %s', id) end c.sec.refs[id] = c.mode.listing end j:hook('block_insert', c, mode.listing, l) return c.mode.listing; end)}; {pred = function(s,c) if s:match '^[%-_][*_%-%s]+' then return true end if startswith(s, '—') then for c, p in eachcode(s) do if ({ ................................................................................ end)}; {fn = insert_paragraph}; } function ct.parse(file, src, mode) local function is_whitespace(cp) return cp == 0x20 or cp == 0xe390 end local ctx = ct.ctx.mk(src) ctx.line = 0 ctx.doc = ct.doc.mk() ctx.doc.src = src ctx.doc.stage = { kind = 'parse'; mode = mode; } ctx.sec = ctx.doc:mksec() -- toplevel section ctx.sec.origin = ctx:clone() -- create states for extension hooks local job = ctx.doc:job('parse',nil,ctx) for full_line in file:lines() do ctx.line = ctx.line + 1 local l for p, c in utf8.codes(full_line) do if not is_whitespace(c) then l = full_line:sub(p) break end end job:hook('line_read',ctx,l) if ctx.mode then if ctx.mode.kind == 'code' then if l and l:match '^~~~%s*$' then job:hook('block_listing_end',ctx,ctx.mode.listing) job:hook('mode_switch', c, nil) ctx.mode = nil else -- TODO handle formatted code local newline = {l} table.insert(ctx.mode.listing.lines, newline) job:hook('block_listing_newline',ctx,ctx.mode.listing,newline) end else ctx:fail('unimplemented syntax mode %s', ctx.mode.kind) end else if l then local function tryseqs(seqs, ...) for _, i in pairs(seqs) do if ((not i.seq ) or startswith(l, i.seq)) and ((not i.pred) or i.pred (l, ctx )) then i.fn(l, ctx, job, ...) return true end end return false end if not tryseqs(ct.ctlseqs) then local found = false for eb, ext, state in job:each('blocks') do if tryseqs(eb, state) then found = true break end end if not found then ctx:fail 'incomprehensible input line' end end else if next(ctx.sec.blocks) and ctx.sec.blocks[#ctx.sec.blocks].kind ~= 'break' then local brk = {kind='break'} job:hook('block_break', ctx, brk, l) table.insert(ctx.sec.blocks, brk) end end end job:hook('line_end',ctx,l) end return ctx.doc end |
Modified ext/toc.lua from [ed743c91e7] to [706a61f3d9].
1 2 3 4 5 6 7 8 9 10 11 12 |
local ct = require 'cortav' local ss = require 'sirsem' ct.ext.install { id = 'toc'; desc = 'provides a table of contents for HTML renderer plus generic fallback'; version = ss.version {0,1; 'devel'}; contributors = {{name='lexi hale', handle='velartrill', mail='lexi@hale.su', homepage='https://hale.su'}}; directive = function(words) end; } |
> > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > > > > > | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
local ct = require 'cortav' local ss = require 'sirsem' local css_toc = [[ ]] local css_toc_fixed = [[ @media (min-width: calc(@[width]:[100vw] + 20em)) { ol.toc { position: fixed; padding-top: 1em; padding-bottom: 1em; padding-right: 1em; margin-top: 0; margin-bottom: 0; right: 0; top: 0; bottom: 0; max-width: calc(50vw - ((@[width]:[0]) / 2) - 3.5em); overflow-y: auto; } @media (max-width: calc(@[width]:[100vw] + 30em)) { ol.toc { max-width: calc(100vw - ((@[width]:[0])) - 9.5em); } body { margin-left: 5em; } } } ]] ct.ext.install { id = 'toc'; desc = 'provides a table of contents for HTML renderer plus generic fallback'; version = ss.version {0,1; 'devel'}; contributors = {{name='lexi hale', handle='velartrill', mail='lexi@hale.su', homepage='https://hale.su'}}; default = true; -- on unless inhibited hook = { doc_init = function(job) print('initing doc:toc',job.doc) job.state.toc_custom_position = false end; render_html_init = function(job, render) render.stylesets.toc = css_toc render.stylesets.tocFixed = css_toc_fixed end; render_html_ir_assemble = function(job, render, ir) -- the custom position state is part of the document job, -- but rendering is a separate job, so we need to get the -- state of this extension in the parent job, which is -- done with the job:unwind(depth) call. unwind is a method -- of the delegate we access the job through which gives us -- direct access to the job state of this extension; unwind -- climbs the jobtree and constructs a similar delegate for -- the nth parent. note that this will only work if the -- current extension hasn't been excluded by predicate from -- the nth parent! if not job:unwind(1).state.toc_custom_position then -- TODO insert %toc end of first section end end; }; directives = { mark = function (job, ctx, words) local _, _, text = words(2) ctx:insert {kind = 'anchor', _toc_label = ct.parse_span(text,ctx)} end; name = function (job, ctx, words) local _, _, id, text = words(3) ctx:insert {kind = 'anchor', id=id, _toc_label = ct.parse_span(text,ctx)} end; [true] = function (job, ctx, words) local _, op, val = words(2) if op == nil then local toc = {kind='toc'} ctx:insert(toc) -- same deal here -- directives are processed as part of -- the parse job, which is forked off the document job, -- so we need to climb the jobstack job:unwind(1).state.toc_custom_position = true job:hook('ext_toc_position', ctx, toc) else ctx:fail 'bad %toc directive' end end; }; render = { toc = { html = function(job, renderer, block, section) -- “tagproc” contains the functions that determine what kind -- of data our abstract tags will be transformed into. this -- is needed to that plain text, HTML, and HTML IR can be -- produced from the same functions just by varying the -- proc set. -- -- “astproc” contains the functions that determine what form -- our span arrays (and blocks, but not relevant here) will -- be transformed into, and is analogous to “tagproc” local tag = renderer.tagproc.tag; local elt = renderer.tagproc.elt; local catenate = renderer.tagproc.catenate; local sr = renderer.astproc.span_renderers; local getSafeID = renderer.state.obj_htmlid; -- toplevel HTML IR local lst = {tag = 'ol', attrs={class='toc'}, nodes={}} -- "renderer.state" contains the stateglob of the renderer -- itself, not to be confused with the "state" parameter -- which contains this extension's share of the job state -- we use it to activate the stylesets we injected earlier renderer.state.stylesets_active.toc = true if renderer.state.opts['width'] then renderer.state.stylesets_active.tocFixed = true end -- assemble a tree of links from the document section -- structure. this is tricky, because we need a tree, but -- all we have is a flat list with depth values attached to -- each node. local stack = {lst} local top = function() return stack[#stack] end -- job.doc is the document the render job is bound to, and -- its secorder field is a list of all the doc's sections in -- the order they occur ("doc.sections" is a hashmap from name -- to section object) local all = job.doc.secorder for i, sec in ipairs(all) do if sec.heading_node then -- does this section have a label? local ent = tag('li',nil, catenate{tag('a', {href='#'..getSafeID(sec)}, sr.htmlSpan(sec.heading_node.spans, sec.heading_node, sec))}) if sec.depth > #stack then local n = {tag = 'ol', attrs={}, nodes={ent}} table.insert(top().nodes[#top().nodes].nodes, n) table.insert(stack, n) else if sec.depth < #stack then for j=#stack,sec.depth+1,-1 do stack[j] = nil end end table.insert(top().nodes, ent) end -- now we need to assemble a list of items within the -- section worthy of an entry on their own. currently -- this is only anchors created with %toc mark|name local innerlinks = {} local noteworthy = { anchor = true } for j, block in pairs(sec.blocks) do if noteworthy[block.kind] then local label = ss.coalesce(block._toc_label, block.label, block.spans) if label then table.insert(innerlinks, { id = renderer.state.obj_htmlid(block); label = label; block = block; }) end end end if next(innerlinks) then local n = {tag = 'ol', attrs = {}, nodes = {}} for i, l in ipairs(innerlinks) do local nn = { tag = 'a'; attrs = {href = '#' .. l.id}; nodes = {sr.htmlSpan(l.label, l.block, sec)}; } table.insert(n.nodes, {tag = 'li', attrs = {}, nodes={nn}}) end table.insert(ent.nodes, n) end print(ss.dump(ent)) end end return lst end; [true] = function() end; -- fallback // convert to different node types }; }; } |
Modified sirsem.lua from [8f6ee343ec] to [1f16b393f5].
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
...
364
365
366
367
368
369
370
|
exp = '<' .. state.tbls[p] ..'>'
done = true
else
state.tbls[p] = path and string.format('%s.%s', path, k) or k
end
end
if not done then
local function dodump() return dump(
p, state,
path and string.format("%s.%s", path, k) or k,
depth + 1
) end
-- boy this is ugly
if type(p) ~= 'table' or
getmetatable(p) == nil or
................................................................................
if mm.__name == 'class' then
return g
else
return nil
end
end
|
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
...
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
|
exp = '<' .. state.tbls[p] ..'>' done = true else state.tbls[p] = path and string.format('%s.%s', path, k) or k end end if not done then local function dodump() return ss.dump( p, state, path and string.format("%s.%s", path, k) or k, depth + 1 ) end -- boy this is ugly if type(p) ~= 'table' or getmetatable(p) == nil or ................................................................................ if mm.__name == 'class' then return g else return nil end end function ss.walk(o, key, ...) if o[key] then if select('#', ...) == 0 then return o[key] else return ss.walk(o[key], ...) end end return nil end function ss.coalesce(x, ...) if x ~= nil then return x elseif select('#', ...) == 0 then return nil else return ss.coalesce(...) end end |