cortav  Check-in [2e37b523b5]

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: 2e37b523b58606acc3a8d4d0f495e0865935d97f9aeb49b0c71ff12aeec798b7
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 $@