Overview
Comment: | further develop html renderer and document it, many doc fixes, fix misc bugs |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
2e37b523b58606acc3a8d4d0f495e086 |
User & Date: | lexi on 2021-12-19 05:25:31 |
Other Links: | manifest | tags |
Context
2021-12-19
| ||
18:12 | add rudimentary syntax hiliting for kate/kwrite/kdepart check-in: 87fed4ec34 user: lexi tags: trunk | |
05:25 | further develop html renderer and document it, many doc fixes, fix misc bugs check-in: 2e37b523b5 user: lexi tags: trunk | |
2021-12-18
| ||
13:03 | enable command line control of process check-in: a651687c24 user: lexi tags: trunk | |
Changes
Modified cortav.ct from [ecd572122d] to [6a93030d29].
1 -# cortav 1 +# cortav specification 2 2 [*cortav] is a markup language designed to be a simpler, but more capable alternative to markdown. its name derives from the [>dict Ranuir words] [!cor] "writing" and [!tav] "document", translating to something like "(plain) text document". 3 3 4 4 dict: http://ʞ.cc/fic/spirals/glossary 5 5 6 +the cortav [!format] can be called [!cortavgil], or [!gil cortavi], to differentiate it from the reference implementation [!cortavsir] or [!sir cortavi]. 7 + 6 8 %toc 7 9 8 10 ## cortav vs. markdown 9 -the most important difference between cortav and markdown is that cortav is strictly line-oriented. this choice was made to ensure that cortav was relatively easy to parse. so while a simple [$.ct] file may look a bit like a [$.md] file, in reality it's a lot closer to Gemini structured text than any flavor of markdown. 11 +the most important difference between cortav and markdown is that cortav is strictly line-oriented. this choice was made to ensure that cortav was relatively easy to parse. so while a simple [$.ct] file may look a bit like a [$.md] file, in reality it's a lot closer to gemtext than any flavor of markdown. 10 12 11 13 ## encoding 12 14 a cortav document is made up of a sequence of codepoints. UTF-8 must be supported, but other encodings (such as UTF-32 or C6B) may be supported as well. lines will be derived by splitting the codepoints at the linefeed character or equivalent. note that unearthly encodings like C6B or EBCDIC will need to select their own control sequences. 13 15 14 16 ## structure 15 17 cortav is based on an HTML-like block model, where a document consists of sections, which are made up of blocks, which may contain a sequence of spans. flows of text are automatically conjoined into spans, and blocks are separated by one or more newlines. this means that, unlike in markdown, a single logical paragraph [*cannot] span multiple ASCII lines. the primary purpose of this was to ensure ease of parsing, but also, both markdown and cortav are supposed to be readable from within a plain text editor. this is the 21st century. every reasonable text editor supports soft word wrap, and if yours doesn't, that's entirely your own damn fault. 16 18 17 19 the first character(s) of every line (the "control sequence") indicates the role of that line. if no control sequence is recognized, the sequence [$.] is implied instead. the standard line classes and their associated control sequences are listed below. some control sequences have alternate forms, in order to support modern, readable unicode characters as well as plain ascii text. 18 20 19 -* paragraphs (. ¶ ❡): a paragraph is a simple block of text. the backslash control sequence is only necessary if the paragraph text begins with something that would otherwise be interpreted as a control sequence. 21 +* paragraphs (. ¶ ❡): a paragraph is a simple block of text. the period control sequence is only necessary if the paragraph text begins with something that would otherwise be interpreted as a control sequence. 20 22 * newlines (\): inserts a line break into previous paragraph and attaches the following text. mostly useful for poetry or lyrics. 21 23 * section starts (# §): starts a new section. all sections have an associated depth, determined by the number of sequence repetitions (e.g. "###" indicates depth-three"). sections may have headers and IDs; both are optional. IDs, if present, are a sequence of raw-text immediately following the hash marks. if the line has one or more space character followed by styled-text, a header will be attached. the character immediately following the hashes can specify a particular type of section. e.g.: 22 24 ** [$#] is a simple section break. 23 25 ** [$#anchor] opens a new section with the ID [$anchor]. 24 26 ** [$# header] opens a new section with the title "header". 25 27 ** [$#anchor header] opens a new section with both the ID [$anchor] and the title "header". 26 28 ** [$#>conversation] opens a blockquote section named [$conversation] without a header. ................................................................................ 52 54 53 55 ## styled text 54 56 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. 55 57 56 58 * strong \[*[!styled-text]\]: causes its text to stand out from the narrative, generally rendered as bold or a brighter color. 57 59 * emphatic \[![!styled-text]\]: indicates that its text should be spoken with emphasis, generally rendered as italics 58 60 * 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 59 -* 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. 61 +* 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. 60 62 * footnote \[^[!ref] [!styled-text]\]: annotates the text with a defined footnote 61 63 * 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 62 64 * raw literal \[$\\[!raw-text]\]: shorthand for [\[$[\…]]] 63 65 * macro \{[!name] [!arguments]}: invokes a [>ex.mac macro], specified with a reference 64 66 * argument \[#[!var]\]: in macros only, inserts the [$var]-th argument. otherwise, inserts a context variable provided by the renderer. 65 67 * raw argument \[##[!var]\]: like above, but does not evaluate [$var]. 66 68 * term \[&[!name] ([!label])\]: quotes a defined term with a link to its definition 67 -* inline image \[&@[!name]\]: shows a small image or other object inline 69 +* inline image \[&@[!name]\]: shows a small image or other object inline. the unicode character [$🖼] can also be used instead of [$&@]. 68 70 69 71 ## identifiers 70 72 any identifier (including a reference) that is defined within a named section must be referred to from outside that section as [$[!sec].[!obj]], where [$sec] is the ID of the containing section and [$obj] is the ID of the object one wishes to reference. 71 73 72 74 ## context variables 73 -context variables are provided so that cortav renderers can process templates. certain context variables are provided for by the standard. you can test for the presence of a context variable with the directive [$when ctx [!var]]. 75 +context variables are provided so that cortav renderers can process templates. certain context variables are provided for by the standard. you can test for the presence of a context variable with the directive [$%[*when] ctx [!var]]. 74 76 75 -* cortav.file: the name of the file currently being rendered 76 -* cortav.path: the absolute path of the file currently being rendered 77 -* cortav.time: the current system time 78 -* cortav.date: the current system date 79 -* cortav.page: the number of the page currently being rendered 80 -* cortav.id: the identifier of the renderer 81 -* cortav.hash: the SHA3 hash of the source file being rendered 77 +* {def cortav.file} the name of the file currently being rendered 78 +* {def cortav.path} the absolute path of the file currently being rendered 79 +* {def cortav.time} the current system time in the form [$[#cortav.time]] 80 +* {def cortav.date} the current system date in the form [$[#cortav.date]] 81 +* {def cortav.datetime} the current system date and time represented in the locale or system-standard manner (e.g. [$[#cortav.datetime]]) 82 +* {def cortav.page} the number of the page currently being rendered 83 +* {def cortav.id} the identifier of the renderer 84 +* {def cortav.hash} the SHA3 hash of the source file being rendered 85 + def: [*[#1]]: 82 86 83 87 on systems with environment variables, these may be accessed as context variables by prefixing their name with [$env.]. 84 88 85 89 different renderers may provide context in different ways, such as from command line options or a context file. any predefined variables should carry an appropriate prefix to prevent conflation. 86 90 87 91 ## directives 88 -* format: gives a hint on how the document should be formatted. the first hint that is understood will be applied; all others will be discarded. standard hints include 92 + d: [$%[*[##1]]] 93 +* {d format} gives a hint on how the document should be formatted. the first hint that is understood will be applied; all others will be discarded. standard hints include: 89 94 ** essay 90 95 ** narrative 91 96 ** screenplay: uses asides to denote actions, quotes for dialogue 92 97 ** stageplay: uses asides to denote actions, quotes for dialogue 93 98 ** manual 94 99 ** glossary 95 100 ** news 96 -* author: encodes document authorship 97 -* cols: specifies the number of columns the next object should be rendered with 98 -* include: transcludes another file 99 -* quote: transcludes another file, without expanding the text except for paragraphs 100 -* embed: where possible, embeds another file as an object within the current one. in HTML this could be accomplished with e.g. an iframe. 101 -* expand: causes the next object (usually a code block) to be fully expanded when it would otherwise not be 101 +* {d author} encodes document authorship 102 +* {d cols} specifies the number of columns the next object should be rendered with 103 +* {d include} transcludes another file 104 +* {d quote} transcludes another file, without expanding the text except for paragraphs 105 +* {d embed}, where possible, embeds another file as an object within the current one. in HTML this could be accomplished with e.g. an iframe. 106 +* {d expand} causes the next object (usually a code block) to be fully expanded when it would otherwise not be 102 107 103 108 ##ex examples 104 109 105 110 ~~~ blockquotes #bq [cortav] ~~~ 106 111 the following excerpts of text were recovered from a partially erased hard drive found in the Hawthorne manor in the weeks after the Incident. context is unknown. 107 112 108 113 #> ................................................................................ 173 178 the interpreter should provide a [$cortav] table with the objects: 174 179 * ctx: contains context variables 175 180 176 181 used files should return a table with the following members 177 182 * macros: an array of functions that return strings or arrays of strings when invoked. these will be injected into the global macro namespace. 178 183 179 184 ### ts 180 -the [*ts] extension allows documents to be marked up for basic classification constraints and automatically redacted. if you are seriously relying on ts for confidentiality, make damn sure you start the file with [$\[requires ts\]], so that rendering will fail with an error if the extension isn't supported. 185 +the [*ts] extension allows documents to be marked up for basic classification constraints and automatically redacted. if you are seriously relying on ts for confidentiality, make damn sure you start the file with [$%[*requires] ts], so that rendering will fail with an error if the extension isn't supported. 181 186 182 187 ts enables the directives: 183 188 * [$ts class [!scope] [!level] (styled-text)]: indicates a classification level for either the while document (scope [!doc]) or the next section (scope [!sec]). if the ts level is below [$level], the section will be redacted or rendering will fail with an error, as appropriate. if styled-text is included, this will be treated as the name of the classification level. 184 189 * [$ts word [!scope] [!word] (styled-text)]: indicates a codeword clearance that must be present for the text to render. if styled-text is present, this will be used to render the name of the codeword instead of [$word]. 185 190 * [$when ts level [!level]] 186 191 * [$when ts word [!word]] 187 192 ................................................................................ 210 215 <A> we may have a problem 211 216 <B> Hyacinth, I told you not to contact me without— 212 217 <A, shouting> god DAMMIT woman I am trying to SAVE your worthless skin 213 218 <B> Hyacinth! your Godforsaken scrambler! 214 219 <A> …oh, [!fuck]. 215 220 (signal lost) 216 221 ~~~ 222 + 223 +# reference implementation 224 +the cortav standard is implemented in [$cortav.lua], found in this repository. only the way [$cortav.lua] interprets the cortav language is defined as a reference implementation; other behaviors are simply how [$cortav.lua] implements the specification and may be copied, ignored, tweaked, violently assaulted, or used as inspiration by a compliant parser. 225 + 226 +## invocation 227 +[$cortav.lua] is operated from the command line, either with the command [$lua cortav.lua] or by first compiling it to bytecode; a makefile for producing a "bytecode binary" that can be executed like a normal executable is included in the repository. henceforth it will be assumed you are using the compiled form; if you are instead running [$cortav.lua] directly as an interpreted script, just replace [$$ cortav] with [$$ lua cortav.lua] in incantations. 228 + 229 +when run without commands, [$cortav.lua] will read input from standard input and write to standard output. alternately, a source file can be given as an argument. to write to a specific file instead of the standard output stream, use the [$-o [!file]] flag. 230 + 231 +~~~ 232 +$ cortav readme.ct -o readme.html 233 + # reads from readme.ct, writes to readme.html 234 +$ cortav -o readme.html 235 + # reads from standard input, writes to readme.html 236 +$ cortav readme.ct 237 + # reads from readme.ct, writes to standard output 238 +~~~ 239 + 240 +### switches 241 +[$cortav.lua] offers various switches to control its behavior. 242 ++ long + short + function + 243 +| [$--out [!file]] :|:[$-o]:| sets the output file (default stdout) | 244 +| [$--log [!file]] :|:[$-l]:| sets the log file (default stderr) | 245 +| [$--define [!var] [!val]]:|:[$-d]:| sets the context variable [$var] to [$val] | 246 +| [$--mode-set [!mode]] :|:[$-y]:| activates the [>refimpl-mode mode] with ID [!mode] 247 +| [$--mode-clear [!mode]] :|:[$-n]:| disables the mode with ID [!mode] | 248 +| [$--mode [!id] [!val]] :|:[$-m]:| configures mode [!id] with the value [!val] | 249 +| [$--help] :|:[$-h]:| display online help | 250 +| [$--version] :|:[$-V]:| display the interpreter version | 251 + 252 +###refimpl-mode modes 253 +most of [$cortav.lua]'s implementation-specific behavior is controlled by use of [!modes]. these are namespaced options which may have a boolean, string, or numeric value. boolean modes are set with the [$-y] [$-n] flags; other modes use the [$-m] flags. 254 + 255 +most modes are defined by the renderer backend. the following modes affect the behavior of the frontend: 256 + 257 ++ ID + type + effect 258 +| [$render:format]:| string | selects the [>refimpl-rend renderer] (default [$html]) 259 +| [$parse:show-tree]:| flag | dumps the parse tree to the log after parsing completes 260 + 261 +##refimpl-rend renderers 262 +[$cortav.lua] implements a frontend-backend architecture, separating the parsing stage from the rendering stage. this means new renderers can be added to [$cortav.lua] relatively easily. currently, only an [>refimpl-rend-html HTML renderer] is included; however, a [$groff] backend is planned at some point in the future, so that PDFs and manpages can be generated from cortav files. 263 + 264 +###refimpl-rend-html html 265 +the HTML renderer is activated with the incantation [$-m render:format html]. it is currently the default backend. it produces a single HTML file, optionally with CSS styling data, from a [$.ct] input file. 266 + 267 +it supports the following modes: 268 + 269 +* string (css length) [$html:width] sets a maximum width for the body content in order to make the page more readable on large displays 270 +* number [$html:accent] applies an accent hue to the generated webpage. the hue is specified in degrees, e.g. [$-m html:accent 0] applies a red accent. 271 +* flag [$html:dark-on-light] uses dark-on-light styling, instead of the default light-on-dark 272 +* flag [$html:fossil-uv] outputs an HTML snippet suitable for use with the Fossil VCS webserver. this is intended to be used with the unversioned content mechanism to host rendered versions of documentation written in cortav that's stored in a Fossil repository. 273 +* number [$html:hue-spread] generates a color palette based on the supplied accent hue. the larger the value, the more the other colors diverge from the accent hue. 274 +* string [$html:link-css] generates a document linking to the named stylesheet 275 +* flag [$html:gen-styles] embeds appropriate CSS styles in the document (default on) 276 +* flag [$html:snippet] produces a snippet of html instead of an entire web page. note that proper CSS scoping is not yet implemented (and can't be implemented hygienically since [$scoped] was removed 😢) 277 +* string [$html:title] specifies the webpage titlebar contents (normally autodetected from the document based on headings or directives) 278 + 279 +~~~ 280 +$ cortav readme.ct --out readme.html \ 281 + -m render:format html \ 282 + -m html:width 40em \ 283 + -m html:accent 80 \ 284 + -m html:hue-spread 35 \ 285 + -y html:dark-on-light # could also be written as: 286 +$ cortav readme.ct -ommmmy readme.html render:format html html:width 40em html:accent 80 html:hue-spread 35 html:dark-on-light 287 +~~~ 217 288
Modified cortav.lua from [f20a833e35] to [1d4d9e0a4b].
1 1 -- [ʞ] cortav.lua 2 2 -- ~ lexi hale <lexi@hale.su> 3 3 -- © AGPLv3 4 --- ? renderer 4 +-- ? reference implementation of the cortav document language 5 5 6 6 local ct = { render = {} } 7 7 8 8 local function hexdump(s) 9 9 local hexlines, charlines = {},{} 10 10 for i=1,#s do 11 11 local line = math.floor((i-1)/16) + 1 ................................................................................ 66 66 return str 67 67 elseif type(o) == "string" then 68 68 return string.format('“%s”', o) 69 69 else 70 70 return tostring(o) 71 71 end 72 72 end 73 + 74 +local function 75 +lerp(t, a, b) 76 + return (1-t)*a + (t*b) 77 +end 78 + 79 +local function 80 +startswith(str, pfx) 81 + return string.sub(str, 1, #pfx) == pfx 82 +end 73 83 74 84 local function declare(c) 75 85 local cls = setmetatable({ 76 86 __name = c.ident; 77 87 }, { 78 88 __name = 'class'; 79 89 __tostring = function() return c.ident or '(class)' end; ................................................................................ 173 183 io = ct.exnkind('IO error', function(msg, ...) 174 184 return string.format("<%s %s> "..msg, ...) 175 185 end); 176 186 cli = ct.exnkind 'command line parse error'; 177 187 mode = ct.exnkind('bad mode', function(msg, ...) 178 188 return string.format("mode “%s” "..msg, ...) 179 189 end); 190 + unimpl = ct.exnkind 'feature not implemented'; 180 191 } 181 192 182 193 ct.ctx = declare { 183 194 mk = function(src) return {src = src} end; 184 195 ident = 'context'; 185 196 cast = { 186 197 string = function(me) ................................................................................ 195 206 new.generation = 1 196 207 end 197 208 end; 198 209 fns = { 199 210 fail = function(self, msg, ...) 200 211 ct.exns.tx(msg, self.src.file, self.line or 0, ...):throw() 201 212 end; 213 + insert = function(self, block) 214 + block.origin = self:clone() 215 + table.insert(self.sec.blocks,block) 216 + end; 202 217 ref = function(self,id) 203 218 if not id:find'%.' then 204 219 local rid = self.sec.refs[id] 205 220 if self.sec.refs[id] then 206 221 return self.sec.refs[id] 207 222 else self:fail("no such ref %s in current section", id or '') end 208 223 else ................................................................................ 237 252 fns = { 238 253 mksec = function(self, id, depth) 239 254 local o = ct.sec(id, depth) 240 255 if id then self.sections[id] = o end 241 256 table.insert(self.secorder, o) 242 257 return o 243 258 end; 259 + context_var = function(self, var, ctx, test) 260 + local fail = function(...) 261 + if test then return false end 262 + ctx:fail(...) 263 + end 264 + if startswith(var, 'cortav.') then 265 + local v = var:sub(8) 266 + if v == 'page' then 267 + if ctx.page then return tostring(ctx.page) 268 + else return '(unpaged)' end 269 + elseif v == 'renderer' then 270 + if not self.stage then 271 + return fail 'document is not being rendererd' 272 + end 273 + return self.stage.format 274 + elseif v == 'datetime' then 275 + return os.date() 276 + elseif v == 'time' then 277 + return os.date '%H:%M:%S' 278 + elseif v == 'date' then 279 + return os.date '%A %d %B %Y' 280 + elseif v == 'id' then 281 + return 'cortav.lua (reference implementation)' 282 + elseif v == 'file' then 283 + return self.src.file 284 + else 285 + return fail('unimplemented predefined variable %s', var) 286 + end 287 + elseif startswith(var, 'env.') then 288 + local v = var:sub(5) 289 + local val = os.getenv(v) 290 + if not val then 291 + return fail('undefined environment variable %s', v) 292 + end 293 + elseif self.stage.kind == 'render' and startswith(var, self.stage.format..'.') then 294 + -- TODO query the renderer somehow 295 + return fail('renderer %s does not implement variable %s', self.stage.format, var) 296 + elseif self.vars[var] then 297 + return self.vars[var] 298 + else 299 + if test then return false end 300 + return '' -- is this desirable behavior? 301 + end 302 + end; 244 303 }; 245 304 mk = function() return { 246 305 sections = {}; 247 306 secorder = {}; 248 307 embed = {}; 249 308 meta = {}; 309 + vars = {}; 250 310 } end; 251 311 } 252 312 253 313 local function map(fn, lst) 254 314 local new = {} 255 315 for k,v in pairs(lst) do 256 316 table.insert(new, fn(v,k)) ................................................................................ 266 326 local function fmtfn(str) 267 327 return function(...) 268 328 return string.format(str, ...) 269 329 end 270 330 end 271 331 272 332 function ct.render.html(doc, opts) 333 + local doctitle = opts['title'] 273 334 local f = string.format 274 335 local ids = {} 275 336 local canonicalID = {} 276 337 local function getSafeID(obj) 277 338 if canonicalID[obj] then 278 339 return canonicalID[obj] 279 340 elseif obj.id and ids[obj.id] then ................................................................................ 307 368 lisp = { color = 0x77ff88 }; 308 369 fortran = { color = 0xff779a }; 309 370 python = { color = 0xffd277 }; 310 371 python = { color = 0xcdd6ff }; 311 372 } 312 373 313 374 local stylesets = { 375 + accent = [[ 376 + body { background: @bg; color: @fg } 377 + a[href] { 378 + color: @tone(0.7 30); 379 + text-decoration-color: @tone/0.4(0.7 30); 380 + } 381 + a[href]:hover { 382 + color: @tone(0.9 30); 383 + text-decoration-color: @tone/0.7(0.7 30); 384 + } 385 + h1,h2,h3,h4,h5,h6 { 386 + color: @tone(2); 387 + border-bottom: 1px solid @tone(0.7); 388 + } 389 + ]]; 314 390 code = [[ 315 391 code { 316 - background: #000; 317 - color: #fff; 392 + background: @fg; 393 + color: @bg; 318 394 font-family: monospace; 319 395 font-size: 90%; 320 396 padding: 3px 5px; 321 397 } 322 398 ]]; 323 399 abbr = [[ 324 400 abbr[title] { cursor: help; } 325 401 ]]; 326 402 editors_markup = [[]]; 327 403 block_code_listing = [[ 328 404 section > figure.listing { 329 405 font-family: monospace; 330 - background: #000; 331 - color: #fff; 406 + background: @tone(0.05); 407 + color: @fg; 332 408 padding: 0; 333 409 margin: 0.3em 0; 334 410 counter-reset: line-number; 335 411 position: relative; 412 + border: 1px solid @fg; 336 413 } 337 414 section > figure.listing>div { 338 415 white-space: pre-wrap; 339 416 counter-increment: line-number; 340 417 text-indent: -2.3em; 341 418 margin-left: 2.3em; 342 419 } 343 420 section > figure.listing>:is(div,hr)::before { 344 421 width: 1.0em; 345 422 padding: 0.2em 0.4em; 346 423 text-align: right; 347 424 display: inline-block; 348 - background-color: #333; 349 - border-right: 1px solid #fff; 425 + background-color: @tone(0.2); 426 + border-right: 1px solid @fg; 350 427 content: counter(line-number); 351 428 margin-right: 0.3em; 352 429 } 353 430 section > figure.listing>hr::before { 354 - color: #333; 431 + color: transparent; 355 432 padding-top: 0; 356 433 padding-bottom: 0; 357 434 } 358 435 section > figure.listing>div::before { 359 - color: #fff; 436 + color: @fg; 360 437 } 361 438 section > figure.listing>div:last-child::before { 362 439 padding-bottom: 0.5em; 363 440 } 364 441 section > figure.listing>figcaption:first-child { 365 442 border: none; 366 - border-bottom: 1px solid #fff; 443 + border-bottom: 1px solid @fg; 367 444 } 368 445 section > figure.listing>figcaption::after { 369 446 display: block; 370 447 float: right; 371 448 font-weight: normal; 372 449 font-style: italic; 373 450 font-size: 70%; 374 451 padding-top: 0.3em; 375 452 } 376 453 section > figure.listing>figcaption { 377 454 font-family: sans-serif; 378 - font-weight: bold; 379 - font-size: 130%; 455 + font-size: 120%; 380 456 padding: 0.2em 0.4em; 381 457 border: none; 458 + color: @tone(2); 382 459 } 383 460 section > figure.listing > hr { 384 461 border: none; 385 462 margin: 0; 386 463 height: 0.7em; 387 464 counter-increment: line-number; 388 465 } ................................................................................ 413 490 else 414 491 table.insert(text, span_renderers[v.kind](v, block, sec)) 415 492 end 416 493 end 417 494 return table.concat(text) 418 495 end 419 496 420 - function span_renderers.format(sp) 497 + function span_renderers.format(sp,...) 421 498 local tags = { strong = 'strong', emph = 'em', strike = 'del', insert = 'ins', literal = 'code' } 422 499 if sp.style == 'literal' and not opts['fossil-uv'] then 423 500 stylesNeeded.code = true 424 501 end 425 502 if sp.style == 'del' or sp.style == 'ins' then 426 503 stylesNeeded.editors_markup = true 427 504 end 428 - return tag(tags[sp.style],nil,htmlSpan(sp.spans)) 505 + return tag(tags[sp.style],nil,htmlSpan(sp.spans,...)) 429 506 end 430 507 431 - function span_renderers.term(t,b) 508 + function span_renderers.term(t,b,s) 432 509 local r = b.origin:ref(t.ref) 433 510 local name = t.ref 434 511 if name:find'%.' then name = name:match '^[^.]*%.(.+)$' end 435 512 if type(r) ~= 'string' then 436 513 b.origin:fail('%s is an object, not a reference', t.ref) 437 514 end 438 515 stylesNeeded.abbr = true 439 - return tag('abbr',{title=r},next(t.spans) and htmlSpan(t.spans) or name) 516 + return tag('abbr',{title=r},next(t.spans) and htmlSpan(t.spans,b,s) or name) 440 517 end 441 518 442 - function span_renderers.link(sp,b) 519 + function span_renderers.macro(m,b,s) 520 + local r = b.origin:ref(m.macro) 521 + if type(r) ~= 'string' then 522 + b.origin:fail('%s is an object, not a reference', t.ref) 523 + end 524 + local mctx = b.origin:clone() 525 + mctx.invocation = m 526 + return htmlSpan(ct.parse_span(r, mctx),b,s) 527 + end 528 + 529 + function span_renderers.var(v,b,s) 530 + local val 531 + if v.pos then 532 + if not v.origin.invocation then 533 + v.origin:fail 'positional arguments can only be used in a macro invocation' 534 + elseif not v.origin.invocation.args[v.pos] then 535 + v.origin:fail('macro invocation %s missing positional argument #%u', v.origin.invocation.macro, v.pos) 536 + end 537 + val = v.origin.invocation.args[v.pos] 538 + else 539 + val = v.origin.doc:context_var(v.var, v.origin) 540 + end 541 + if v.raw then 542 + return val 543 + else 544 + return htmlSpan(ct.parse_span(val, v.origin), b, s) 545 + end 546 + end 547 + 548 + function span_renderers.link(sp,b,s) 443 549 local href 444 550 if b.origin.doc.sections[sp.ref] then 445 551 href = '#' .. sp.ref 446 552 else 447 553 if sp.addr then href = sp.addr else 448 554 local r = b.origin:ref(sp.ref) 449 555 if type(r) == 'table' then 450 556 href = '#' .. getSafeID(r) 451 557 else href = r end 452 558 end 453 559 end 454 - return tag('a',{href=href},next(sp.spans) and htmlSpan(sp.spans) or href) 560 + return tag('a',{href=href},next(sp.spans) and htmlSpan(sp.spans,b,s) or href) 455 561 end 456 562 return { 457 563 span_renderers = span_renderers; 458 564 htmlSpan = htmlSpan; 459 565 htmlDoc = htmlDoc; 460 566 } 461 567 end ................................................................................ 486 592 end 487 593 return lst 488 594 end 489 595 490 596 local block_renderers = { 491 597 paragraph = function(b,s) 492 598 return tag('p', nil, sr.htmlSpan(b.spans, b, s), b) 599 + end; 600 + directive = function(b,s) 601 + -- deal with renderer directives 602 + local _, cmd, args = b.words(2) 603 + if cmd == 'page-title' then 604 + if not opts.title then doctitle = args end 605 + elseif b.critical then 606 + b.origin:fail('critical HTML renderer directive “%s” not supported', cmd) 607 + end 493 608 end; 494 609 label = function(b,s) 495 610 if ct.sec.is(b.captions) then 496 611 local h = math.min(6,math.max(1,b.captions.depth)) 497 612 return tag(f('h%u',h), nil, sr.htmlSpan(b.spans, b, s), b) 498 613 else 499 614 -- handle other uses of labels here ................................................................................ 523 638 else 524 639 return elt('hr') 525 640 end 526 641 end, b.lines) 527 642 if b.title then 528 643 table.insert(nodes,1,tag('figcaption',nil,sr.htmlSpan(b.title))) 529 644 end 530 - langsused[b.lang] = true 645 + if b.lang then langsused[b.lang] = true end 531 646 return tag('figure', {class='listing', lang=b.lang, id=b.id and getSafeID(b)}, catenate(nodes)) 532 647 end; 533 648 ['break'] = function() --[[nop]] end; 534 649 } 535 650 return block_renderers; 536 651 end 537 652 ................................................................................ 554 669 end 555 670 end, attrs)) or '') 556 671 end 557 672 local tag = function(t,attrs,body) 558 673 return f('%s%s</%s>', elt(t,attrs), body, t) 559 674 end 560 675 561 - local doctitle 562 676 local ir = {} 563 677 local toc 564 678 local dr = getRenderers(tag,elt,table.concat) -- default renderers 565 679 local plainr = getRenderers(function(t,a,v) return v end, 566 680 function(t,a) return '' end, table.concat) 567 681 local irBlockRdrs = getBlockRenderers( 568 682 function(t,a,v,o) return {tag = t, attrs = a, nodes = type(v) == 'string' and {v} or v, src = o} end, ................................................................................ 682 796 for k in pairs(langsused) do 683 797 local spec = langpairs[k] or {color=0xaaaaaa} 684 798 stylesets.block_code_listing = stylesets.block_code_listing .. string.format( 685 799 [[section > figure.listing[lang="%s"]>figcaption::after 686 800 { content: '%s'; color: #%06x }]], 687 801 k, spec.name or k, spec.color) 688 802 end 803 + 804 + local prepcss = function(css) 805 + local tone = function(fac, sat, sep, alpha) 806 + local hsl = function(h,s,l,a) 807 + local v = string.format('%s, %u%%, %u%%', h,s,l) 808 + if a then 809 + return string.format('hsla(%s, %s)', v,a) 810 + else 811 + return string.format('hsl(%s)', v) 812 + end 813 + end 814 + sat = sat or 1 815 + fac = math.max(math.min(fac, 1), 0) 816 + sat = math.max(math.min(sat, 1), 0) 817 + if opts.accent then 818 + local hue = 'var(--accent)' 819 + local hsep = tonumber(opts['hue-spread']) 820 + if hsep and sep and sep ~= 0 then 821 + hue = string.format('calc(%s - %s)', hue, sep * hsep) 822 + end 823 + return hsl(hue, math.floor(100*sat), math.floor(100*fac), alpha) 824 + else 825 + local g = math.floor(0xFF * fac) 826 + return string.format('#' .. string.rep('%02x',alpha and 4 or 3), g,g,g,alpha and math.floor(0xFF*alpha)) 827 + end 828 + end 829 + local replace = function(var,alpha,param) 830 + local tonespan = opts.accent and .1 or 0 831 + local tbg = opts['dark-on-light'] and 1.0 - tonespan or tonespan 832 + local tfg = opts['dark-on-light'] and tonespan or 1.0 - tonespan 833 + if var == 'bg' then 834 + return tone(tbg,nil,nil,tonumber(alpha)) 835 + elseif var == 'fg' then 836 + return tone(tfg,nil,nil,tonumber(alpha)) 837 + elseif var == 'tone' then 838 + local l, sep, sat 839 + for i=1,3 do -- 🙄 840 + l,sep,sat = param:match('^%('..string.rep('([^%s]*)%s*',i)..'%)$') 841 + if l then break end 842 + end 843 + l = lerp(tonumber(l), tbg, tfg) 844 + return tone(l, tonumber(sat), tonumber(sep), tonumber(alpha)) 845 + end 846 + end 847 + css = css:gsub('@(%w+)/([0-9.]+)(%b())', replace) 848 + css = css:gsub('@(%w+)(%b())', function(a,b) return replace(a,nil,b) end) 849 + css = css:gsub('@(%w+)/([0-9.]+)', replace) 850 + css = css:gsub('@(%w+)', function(a,b) return replace(a,nil,b) end) 851 + return (css:gsub('%s+',' ')) 852 + end 689 853 690 854 local styles = {} 855 + if opts.width then 856 + table.insert(styles, string.format([[body {padding:0 1em;margin:auto;max-width:%s}]], opts.width)) 857 + end 858 + if opts.accent then 859 + table.insert(styles, string.format(':root {--accent:%s}', opts.accent)) 860 + end 861 + if opts.accent or (not opts['dark-on-light']) then 862 + stylesNeeded.accent = true 863 + end 864 + 865 + 691 866 for k in pairs(stylesNeeded) do 692 - table.insert(styles, (stylesets[k]:gsub('%s+',' '))) 867 + if not stylesets[k] then ct.exns.unimpl('styleset %s not implemented (!)', k):throw() end 868 + table.insert(styles, prepcss(stylesets[k])) 693 869 end 694 870 695 871 local head = {} 696 872 local styletag = '' 697 873 if opts['link-css'] then 698 874 local css = opts['link-css'] 699 875 if type(css) ~= 'string' then ct.exns.mode('must be a string', 'html:link-css'):throw() end ................................................................................ 711 887 elseif opts.snippet then 712 888 return styletag .. body 713 889 else 714 890 return dr.htmlDoc(doctitle, next(head) and table.concat(head), body) 715 891 end 716 892 end 717 893 718 -local function 719 -startswith(str, pfx) 720 - return string.sub(str, 1, #pfx) == pfx 721 -end 722 - 723 894 local function eachcode(str, ascode) 724 895 local pos = { 725 896 code = 1; 726 897 byte = 1; 727 898 } 728 899 return function() 729 900 if pos.byte > #str then return nil end ................................................................................ 740 911 pos.byte = pos.byte + #utf8.char(thischar) 741 912 end 742 913 pos.code = pos.code + 1 743 914 return thischar, lastpos 744 915 end 745 916 end 746 917 747 -local function formatter(sty) 748 - return function(s,c) 749 - return { 750 - kind = 'format'; 751 - style = sty; 752 - spans = ct.parse_span(s, c); 753 - origin = c:clone(); 754 - } 755 - end 756 -end 757 -ct.spanctls = { 758 - {seq = '$', parse = formatter 'literal'}; 759 - {seq = '!', parse = formatter 'emph'}; 760 - {seq = '*', parse = formatter 'strong'}; 761 - {seq = '\\', parse = function(s, c) -- raw 762 - return s 763 - end}; 764 - {seq = '$\\', parse = function(s, c) -- raw 765 - return { 766 - kind = 'format'; 767 - style = 'literal'; 768 - spans = {s}; 769 - origin = c:clone(); 770 - } 771 - end}; 772 - {seq = '&', parse = function(s, c) 773 - local r, t = s:match '^([^%s]+)%s*(.-)$' 774 - return { 775 - kind = 'term'; 776 - spans = (t and t ~= "") and ct.parse_span(t, c) or {}; 777 - ref = r; 778 - origin = c:clone(); 779 - } 780 - end}; 781 - {seq = '^', parse = function(s, c) 782 - local fn, t = s:match '^([^%s]+)%s*(.-)$' 783 - return { 784 - kind = 'footnote'; 785 - spans = (t and t~='') and ct.parse_span(t, c) or {}; 786 - ref = fn; 787 - origin = c:clone(); 788 - } 789 - end}; 790 - {seq = '>', parse = function(s, c) 918 +do -- define span control sequences 919 + local function formatter(sty) 920 + return function(s,c) 921 + return { 922 + kind = 'format'; 923 + style = sty; 924 + spans = ct.parse_span(s, c); 925 + origin = c:clone(); 926 + } 927 + end 928 + end 929 + local function insert_link(s, c) 791 930 local to, t = s:match '^([^%s]+)%s*(.-)$' 792 931 if not to then c:fail('invalid link syntax >%s', s) end 793 932 if t == "" then t = nil end 794 933 return { 795 934 kind = 'link'; 796 935 spans = (t and t~='') and ct.parse_span(t, c) or {}; 797 936 ref = to; 798 937 origin = c:clone(); 799 938 } 800 - end}; 801 -} 939 + end 940 + local function insert_var_ref(raw) 941 + return function(s, c) 942 + local pos = tonumber(s) 943 + return { 944 + kind = 'var'; 945 + pos = pos; 946 + raw = raw; 947 + var = not pos and s or nil; 948 + origin = c:clone(); 949 + } 950 + end 951 + end 952 + ct.spanctls = { 953 + {seq = '$', parse = formatter 'literal'}; 954 + {seq = '!', parse = formatter 'emph'}; 955 + {seq = '*', parse = formatter 'strong'}; 956 + {seq = '\\', parse = function(s, c) -- raw 957 + return s 958 + end}; 959 + {seq = '$\\', parse = function(s, c) -- raw 960 + return { 961 + kind = 'format'; 962 + style = 'literal'; 963 + spans = {s}; 964 + origin = c:clone(); 965 + } 966 + end}; 967 + {seq = '&', parse = function(s, c) 968 + local r, t = s:match '^([^%s]+)%s*(.-)$' 969 + return { 970 + kind = 'term'; 971 + spans = (t and t ~= "") and ct.parse_span(t, c) or {}; 972 + ref = r; 973 + origin = c:clone(); 974 + } 975 + end}; 976 + {seq = '^', parse = function(s, c) 977 + local fn, t = s:match '^([^%s]+)%s*(.-)$' 978 + return { 979 + kind = 'footnote'; 980 + spans = (t and t~='') and ct.parse_span(t, c) or {}; 981 + ref = fn; 982 + origin = c:clone(); 983 + } 984 + end}; 985 + {seq = '>', parse = insert_link}; 986 + {seq = '→', parse = insert_link}; 987 + {seq = '🔗', parse = insert_link}; 988 + {seq = '##', parse = insert_var_ref(true)}; 989 + {seq = '#', parse = insert_var_ref(false)}; 990 + } 991 +end 802 992 803 993 function ct.parse_span(str,ctx) 804 994 local function delimited(start, stop, s) 805 995 local depth = 0 806 996 if not startswith(s, start) then return nil end 807 997 for c,p in eachcode(s) do 808 998 if c == '\\' then ................................................................................ 816 1006 return s:sub(1+#start, p.byte - #stop), p.byte -- FIXME 817 1007 elseif depth < 0 then 818 1008 ctx:fail('out of place %s', stop) 819 1009 end 820 1010 end 821 1011 end 822 1012 823 - ctx:fail('%s expected before end of line', stop) 1013 + ctx:fail('[%s] expected before end of line', stop) 824 1014 end 825 1015 local buf = "" 826 1016 local spans = {} 827 1017 local function flush() 828 1018 if buf ~= "" then 829 1019 table.insert(spans, buf) 830 1020 buf = "" ................................................................................ 833 1023 local skip = false 834 1024 for c,p in eachcode(str) do 835 1025 if skip == true then 836 1026 skip = false 837 1027 buf = buf .. c 838 1028 elseif c == '\\' then 839 1029 skip = true 1030 + elseif c == '{' then 1031 + flush() 1032 + local substr, following = delimited('{','}',str:sub(p.byte)) 1033 + local splitstart, splitstop = substr:find'%s+' 1034 + local id, argstr 1035 + if splitstart then 1036 + id, argstr = substr:sub(1,splitstart-1), substr:sub(splitstop+1) 1037 + else 1038 + id, argstr = substr, '' 1039 + end 1040 + local o = { 1041 + kind = 'macro'; 1042 + macro = id; 1043 + args = {}; 1044 + origin = ctx:clone(); 1045 + } 1046 + 1047 + do local start = 1 1048 + local i = 1 1049 + while i <= #argstr do 1050 + while i<=#argstr and (argstr:sub(i,i) ~= '|' or argstr:sub(i-1,i) == '\\|') do 1051 + i = i + 1 1052 + end 1053 + local arg = argstr:sub(start, i == #argstr and i or i-1) 1054 + start = i+1 1055 + table.insert(o.args, arg) 1056 + i = i + 1 1057 + end 1058 + end 1059 + 1060 + p.next.byte = p.next.byte + following - 1 1061 + table.insert(spans,o) 840 1062 elseif c == '[' then 841 1063 flush() 842 1064 local substr, following = delimited('[',']',str:sub(p.byte)) 843 1065 p.next.byte = following + p.byte 844 1066 local found = false 845 1067 for _,i in pairs(ct.spanctls) do 846 1068 if startswith(substr, i.seq) then ................................................................................ 942 1164 c.expand_next = 0 943 1165 end 944 1166 end; 945 1167 } 946 1168 947 1169 local function insert_table_row(l,c) 948 1170 local row = {} 949 - for kind, a1, text, a2 in l:gmatch '([+|])(:?)%s*([^:+|]*)%s*(:?)' do 950 - local header = kind == '+' 951 - local align 952 - if a1 == ':' and a2 ~= ':' then 953 - align = 'left' 954 - elseif a1 == ':' and a2 == ':' then 955 - align = 'center' 956 - elseif a1 ~= ':' and a2 == ':' then 957 - align = 'right' 958 - end 959 - text = text:match '^%s*(.-)%s*$' 960 - table.insert(row, { 961 - spans = ct.parse_span(text, c); 962 - align = align; 963 - header = header; 964 - }) 1171 + local buf 1172 + local flush = function() 1173 + if buf then table.insert(row, buf) end 1174 + buf = { str = '' } 1175 + end 1176 + for c,p in eachcode(l) do 1177 + if c == '|' or c == '+' and (p.code == 1 or l:sub(p.byte-1,p.byte-1)~='\\') then 1178 + flush() 1179 + buf.header = c == '+' 1180 + elseif c == ':' then 1181 + local lst = l:sub(p.byte-#c,p.byte-#c) 1182 + local nxt = l:sub(p.next.byte,p.next.byte) 1183 + if lst == '|' or lst == '+' and l:sub(p.byte-2,p.byte-2) ~= '\\' then 1184 + buf.align = 'left' 1185 + elseif nxt == '|' or nxt == '|' then 1186 + if buf.align == 'left' then 1187 + buf.align = 'center' 1188 + else 1189 + buf.align = 'right' 1190 + end 1191 + else 1192 + buf.str = buf.str .. c 1193 + end 1194 + elseif c:match '%s' then 1195 + if buf.str ~= '' then buf.str = buf.str .. c end 1196 + elseif c == '\\' then 1197 + local nxt = l:sub(p.next.byte,p.next.byte) 1198 + if nxt == '|' or nxt == '+' or nxt == ':' then 1199 + buf.str = buf.str .. nxt 1200 + p.next.byte = p.next.byte + #nxt 1201 + p.next.code = p.next.code + 1 1202 + else 1203 + buf.str = buf.str .. c 1204 + end 1205 + else 1206 + buf.str = buf.str .. c 1207 + end 1208 + end 1209 + if buf.str ~= '' then flush() end 1210 + for _,v in pairs(row) do 1211 + v.spans = ct.parse_span(v.str, c) 965 1212 end 966 1213 if #c.sec.blocks > 1 and c.sec.blocks[#c.sec.blocks].kind == 'table' then 967 1214 local tbl = c.sec.blocks[#c.sec.blocks] 968 1215 table.insert(tbl.rows, row) 969 1216 else 970 1217 table.insert(c.sec.blocks, { 971 1218 kind = 'table'; ................................................................................ 974 1221 }) 975 1222 end 976 1223 end 977 1224 978 1225 ct.ctlseqs = { 979 1226 {seq = '.', fn = insert_paragraph}; 980 1227 {seq = '¶', fn = insert_paragraph}; 1228 + {seq = '❡', fn = insert_paragraph}; 981 1229 {seq = '#', fn = insert_section}; 982 1230 {seq = '§', fn = insert_section}; 983 1231 {seq = '+', fn = insert_table_row}; 984 1232 {seq = '|', fn = insert_table_row}; 985 1233 {seq = '│', fn = insert_table_row}; 986 1234 {pred = function(s,c) return s:match'^[*:]' end, fn = blockwrap(function(l,c) -- list 987 1235 local stars = l:match '^([*:]+)' ................................................................................ 1005 1253 local words = function(i) 1006 1254 local wds = {} 1007 1255 if i == 0 then return cmdline end 1008 1256 for w,pos in cmdline:gmatch '([^%s]+)()' do 1009 1257 table.insert(wds, w) 1010 1258 i = i - 1 1011 1259 if i == 0 then 1012 - return table.unpack(wds), cmdline:sub(pos) 1260 + table.insert(wds,cmdline:sub(pos)) 1261 + return table.unpack(wds) 1013 1262 end 1014 1263 end 1015 1264 end 1016 1265 1017 1266 local cmd, rest = words(1) 1018 1267 if ct.directives[cmd] then 1019 1268 ct.directives[cmd](words,c) 1269 + elseif cmd == c.doc.stage.mode['render:format'] then 1270 + -- this is a directive for the renderer; insert it into the tree as is 1271 + c:insert { 1272 + kind = 'directive'; 1273 + critical = crit == '!'; 1274 + words = words; 1275 + } 1020 1276 elseif crit == '!' then 1021 1277 c:fail('critical directive %s not supported',cmd) 1022 1278 end 1023 1279 end;}; 1024 1280 {seq = '~~~', fn = blockwrap(function(l,c) 1025 1281 local extract = function(ptn, str) 1026 1282 local start, stop = str:find(ptn) ................................................................................ 1030 1286 return ex, n 1031 1287 end 1032 1288 local lang, id, title 1033 1289 if l:match '^~~~%s*$' then -- no args 1034 1290 elseif l:match '^~~~.*~~~%s*$' then -- CT style 1035 1291 local s = l:match '^~~~%s*(.-)%s*~~~%s*$' 1036 1292 lang, s = extract('%b[]', s) 1037 - lang = lang:sub(2,-2) 1293 + if lang then lang = lang:sub(2,-2) end 1038 1294 id, title = extract('#[^%s]+', s) 1039 1295 if id then id = id:sub(2) end 1040 1296 elseif l:match '^~~~' then -- MD shorthand style 1041 1297 lang = l:match '^~~~%s*(.-)%s*$' 1042 1298 end 1043 1299 c.mode = { 1044 1300 kind = 'code'; ................................................................................ 1067 1323 end 1068 1324 end; fn = blockwrap(function() 1069 1325 return { kind = 'horiz-rule' } 1070 1326 end)}; 1071 1327 {fn = insert_paragraph}; 1072 1328 } 1073 1329 1074 -function ct.parse(file, src) 1330 +function ct.parse(file, src, mode) 1075 1331 local function 1076 1332 is_whitespace(cp) 1077 1333 return cp == 0x20 1078 1334 end 1079 1335 1080 1336 local ctx = ct.ctx.mk(src) 1081 1337 ctx.line = 0 1082 1338 ctx.doc = ct.doc.mk() 1339 + ctx.doc.src = src 1340 + ctx.doc.stage = { 1341 + kind = 'parse'; 1342 + mode = mode; 1343 + } 1083 1344 ctx.sec = ctx.doc:mksec() -- toplevel section 1084 1345 ctx.sec.origin = ctx:clone() 1085 1346 1086 1347 for full_line in file:lines() do ctx.line = ctx.line + 1 1087 1348 local l 1088 1349 for p, c in utf8.codes(full_line) do 1089 1350 if not is_whitespace(c) then ................................................................................ 1153 1414 for k, v in pairs(list) do 1154 1415 if fn(k,v) then new[k] = v end 1155 1416 end 1156 1417 return new 1157 1418 end 1158 1419 1159 1420 local function main(input, output, log, mode, vars) 1160 - local doc = ct.parse(input.stream, input.src) 1421 + local doc = ct.parse(input.stream, input.src, mode) 1161 1422 input.stream:close() 1162 1423 if mode['parse:show-tree'] then 1163 1424 log:write(dump(doc)) 1164 1425 end 1165 1426 1166 1427 if not mode['render:format'] then 1167 1428 error 'what output format should i translate the input to?' 1168 1429 end 1430 + if mode['render:format'] == 'none' then return 0 end 1169 1431 if not ct.render[mode['render:format']] then 1170 - error(string.format('output format “%s” unsupported', mode['render:format'])) 1432 + ct.exns.unimpl('output format “%s” unsupported', mode['render:format']):throw() 1171 1433 end 1172 1434 1173 1435 local render_opts = kmap(function(k,v) 1174 1436 return k:sub(2+#mode['render:format']) 1175 1437 end, kfilter(mode, function(m) 1176 1438 return startswith(m, mode['render:format']..':') 1177 1439 end)) 1440 + 1441 + doc.vars = vars 1442 + 1443 + -- this is kind of gross but the context object belongs to the parser, 1444 + -- not the renderer, so that's not a suitable place for this information 1445 + doc.stage = { 1446 + kind = 'render'; 1447 + format = mode['render:format']; 1448 + mode = mode; 1449 + } 1178 1450 1179 1451 output:write(ct.render[mode['render:format']](doc, render_opts)) 1452 + return 0 1180 1453 end 1181 1454 1182 1455 local inp,outp,log = io.stdin, io.stdout, io.stderr 1183 1456 1184 1457 local function entry_cli() 1185 1458 local mode, vars, input = default_mode, {}, { 1186 1459 stream = inp; ................................................................................ 1227 1500 log = function(file) 1228 1501 local nf = io.open(file,'wb') 1229 1502 if nf then log:close() log = nf else 1230 1503 ct.exns.io('could not open log file for writing', 'open',file):throw() 1231 1504 end 1232 1505 end; 1233 1506 define = function(key,value) 1234 - -- set context key 1507 + if startswith(key, 'cortav.') or startswith(key, 'env.') then 1508 + ct.exns.cli 'cannot define variable in restricted namespace':throw() 1509 + end 1510 + vars[key] = value 1235 1511 end; 1236 1512 mode = function(key,value) mode[checkmodekey(key)] = value end; 1237 1513 ['mode-set'] = function(key) mode[checkmodekey(key)] = true end; 1238 1514 ['mode-clear'] = function(key) mode[checkmodekey(key)] = false end; 1239 1515 } 1240 1516 1241 1517 local args = {} ................................................................................ 1287 1563 if args[1] and args[1] ~= '' then 1288 1564 local file = io.open(arg[1], "rb") 1289 1565 if not file then error('unable to load file ' .. args[1]) end 1290 1566 input.stream = file 1291 1567 input.src.file = args[1] 1292 1568 end 1293 1569 1294 - main(input, outp, log, mode, vars) 1570 + return main(input, outp, log, mode, vars) 1295 1571 end 1296 1572 1297 --- local ok, e = pcall(entry_cli) 1298 -local ok, e = true, entry_cli() 1573 +local ok, e = pcall(entry_cli) 1574 +-- local ok, e = true, entry_cli() 1299 1575 if not ok then 1300 1576 local str = 'translation failure' 1577 + if ct.exn.is(e) then 1578 + str = e.kind.desc 1579 + end 1301 1580 local color = false 1302 1581 if log:seek() == nil then 1303 1582 -- this is not a very reliable heuristic for detecting 1304 1583 -- attachment to a tty but it's better than nothing 1305 1584 if os.getenv('COLORTERM') then 1306 1585 color = true 1307 1586 else ................................................................................ 1308 1587 local term = os.getenv('TERM') 1309 1588 if term:find 'color' then color = true end 1310 1589 end 1311 1590 end 1312 1591 if color then 1313 1592 str = string.format('\27[1;31m%s\27[m', str) 1314 1593 end 1315 - log:write(string.format('[%s] %s\n\t%s\n', os.date(), str, e)) 1594 + log:write(string.format('%s: %s\n', str, e)) 1316 1595 os.exit(1) 1317 1596 end 1597 +os.exit(e)
Added makefile version [35641b8f47].
1 +lua != which lua 2 +luac != which luac 3 + 4 +cortav: cortav.lua 5 + echo '#!$(lua)' > $@ 6 + luac -s -o - $< >> $@ 7 + chmod +x $@