cortav  Check-in [5e07b52c57]

Overview
Comment:initial commit
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 5e07b52c5728be3c548265b5a27f1ed7bdf45db4884296a9415ef298694fb169
User & Date: lexi on 2021-12-18 05:24:39
Other Links: manifest | tags
Context
2021-12-18
13:03
enable command line control of process check-in: a651687c24 user: lexi tags: trunk
05:24
initial commit check-in: 5e07b52c57 user: lexi tags: trunk
05:23
initial empty check-in check-in: b9c12d090c user: lexi tags: trunk
Changes

Added cortav.ct version [ecd572122d].

            1  +# cortav
            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  +
            4  +	dict: http://ʞ.cc/fic/spirals/glossary
            5  +
            6  +%toc
            7  +
            8  +## 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.
           10  +
           11  +## encoding
           12  +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  +
           14  +## structure
           15  +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  +
           17  +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  +
           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.
           20  +* newlines (\): inserts a line break into previous paragraph and attaches the following text. mostly useful for poetry or lyrics.
           21  +* 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  +** [$#] is a simple section break.
           23  +** [$#anchor] opens a new section with the ID [$anchor].
           24  +** [$# header] opens a new section with the title "header".
           25  +** [$#anchor header] opens a new section with both the ID [$anchor] and the title "header".
           26  +** [$#>conversation] opens a blockquote section named [$conversation] without a header.
           27  +** [$#^id] opens a footnote section for the multiline footnote [$id]. the ID must be specified.
           28  +** [$#$id] opens the multiline macro [$id]. the ID must be specified.
           29  +** [$#&id mime] opens a new inline object [$id] of type [$mime]. useful for embedding SVGs. the ID and mime type must be specified.
           30  +* lists (* :): these are like paragraph nodes, but list nodes that occur next to each other will be arranged so as to show they compose a sequence. depth is determined by the number of stars/colons. like headers, a list entry may have an ID that can be used to refer back to it; it is indicated in the same way. if colons are used, this indicates that the order of the items is signifiant. :-lists and *-lists may be intermixed; however, note than only the last character in the sequence actually controls the depth type.
           31  +* directives (%): a directive issues a hint to the renderer in the form of an arbitrary string. directives are normally ignored if they are not supported, but you may cause a warning to be emitted where the directive is not supported with [$%!] or mark a directive critical with [$%!!] so that rendering will entirely fail if it cannot be parsed.
           32  +* comments (%%): a comment is a line of text that is simply ignored by the renderer. 
           33  +* asides (!): indicates text that diverges from the narrative, and can be skipped without interrupting it. think of it like block-level parentheses. asides which follow one another are merged as paragraphs of the same aside, usually represented as a sort of box. if the first line of an aside contains a colon, the stretch of styled-text from the beginning to the aside to the colon will be treated as a "type heading," e.g. "Warning:"
           34  +* code (~~~): a line beginning with ~~~ begins or terminates a block of code. the opening line should look like one of the below
           35  +** [$~~~]
           36  +** [$~~~ language] (markdown-style shorthand syntax)
           37  +** [$~~~ \[language\] ~~~] (cortav syntax)
           38  +** [$~~~ \[language\] #id ~~~]
           39  +** [$~~~ title ~~~]
           40  +** [$~~~ title \[language\] ~~~]
           41  +** [$~~~ \[language\] title ~~~]
           42  +** [$~~~ title \[language\] #id ~~~]
           43  +* reference (tab): a line beginning with a tab is treated as a "reference." references hold out-of-line metadata for preceding text like links and footnotes. a reference consists of an identifier followed by a colon and an arbitrary number of spaces or tabs, followed by text. whether this text is interpreted as raw-text or styled-text depends on the context in which the reference is used.
           44  +* quotation (<): a line of the form [$<[!name]> [!quote]] denotes an utterance by [$name].
           45  +* blockquote (>): alternate blockquote syntax. can be nested by repeating the 
           46  +* subtitle (--): attaches a subtitle to the previous header
           47  +* embed (&): embeds a referenced object. can be used to show images or repeat previously defined objects like lists or tables, optionally with a caption.
           48  +** &myimg All that remained of the unfortunate blood magic pageant contestants and audience (police photo)
           49  +** &$mymacro arg 1|arg 2|arg 3
           50  +* break (---): inserts a horizontal rule or other context break; does not end the section. must be followed by newline.
           51  +* table cells (+ |): see [>ex.tab table examples].
           52  +
           53  +## styled text
           54  +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  +
           56  +* strong \[*[!styled-text]\]: causes its text to stand out from the narrative, generally rendered as bold or a brighter color.
           57  +* emphatic \[![!styled-text]\]: indicates that its text should be spoken with emphasis, generally rendered as italics
           58  +* 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.
           60  +* footnote \[^[!ref] [!styled-text]\]: annotates the text with a defined footnote
           61  +* 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  +* raw literal \[$\\[!raw-text]\]: shorthand for [\[$[\…]]]
           63  +* macro \{[!name] [!arguments]}: invokes a [>ex.mac macro], specified with a reference
           64  +* argument \[#[!var]\]: in macros only, inserts the [$var]-th argument. otherwise, inserts a context variable provided by the renderer.
           65  +* raw argument \[##[!var]\]: like above, but does not evaluate [$var].
           66  +* 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
           68  +
           69  +## identifiers
           70  +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  +
           72  +## 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]].
           74  +
           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
           82  +
           83  +on systems with environment variables, these may be accessed as context variables by prefixing their name with [$env.].
           84  +
           85  +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  +
           87  +## 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
           89  +** essay
           90  +** narrative
           91  +** screenplay: uses asides to denote actions, quotes for dialogue
           92  +** stageplay: uses asides to denote actions, quotes for dialogue
           93  +** manual
           94  +** glossary
           95  +** 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
          102  +
          103  +##ex examples
          104  +
          105  +~~~ blockquotes #bq [cortav] ~~~
          106  +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  +
          108  +#>
          109  +—spoke to the man under the bridge again, the one who likes to bite the heads off the fish, and he suggested i take a brief sabbatical and journey to the Wandering Oak (where all paths meet) in search of inspiration and the forsaken sword of Pirate Queen Granuaile. a capital idea! i shall depart upon the morrow, having honored the Lord Odin and poisoned my accursed minstrels as is tradition—
          110  +—can't smell my soul anymore, but that's beside the point entirely—
          111  +—that second moon (always have wondered why nobody else seems to notice the damn fool thing except on Michaelmas day). alas, my luck did not endure, and i was soon to find myself knee-deep in—
          112  +—just have to see about that, won't we!—
          113  +#
          114  +
          115  +the nearest surviving relative of Lord Hawthorne is believed to be a wandering beggar with a small pet meerkat who sells cursed wooden trinkets to unwary children. she will not be contacted, as the officers of the Yard fear her.
          116  +~~~
          117  +
          118  +~~~links & notes #lnr [cortav] ~~~
          119  +this sentence contains a [>zombo link] to zombo com. you can do anything[^any] at zombo com.
          120  +	zombo: https://zombo.com
          121  +	any: anything you want
          122  +~~~
          123  +
          124  +~~~ macros #mac [cortav] ~~~
          125  +the ranuir word {gloss cor|writing}…
          126  +	gloss: [*[#1]] “[#2]”
          127  +~~~
          128  +
          129  +~~~ tables #tab [cortav] ~~~
          130  +here is a glossary table.
          131  +
          132  ++ english :+ ranuir + zia ţai  + thaliste        +
          133  +| honor   :| tef    | pang     | mbecheve        |
          134  +| rakewym :| hirvag | hi phang | nache umwelinde |
          135  +| eat     :| fese   | dzia     | rotechqa        |
          136  +
          137  +and now the other way around!
          138  +
          139  ++:english  :| honor |
          140  ++:ranuir   :| tef   |
          141  ++:zia ţai  :| pang  |
          142  ++:thalishte:| mbecheve |
          143  +~~~
          144  +
          145  +## extensions
          146  +the cortav specification also specifies a number of extensions that do not have to be supported for a renderer to be compliant. the extension mechanism supports the following directives.
          147  +
          148  +* inhibits: prevents an extension from being used even where available
          149  +* uses: turns on an extension that is not specified by the user operating the renderer (e.g. on the command line)
          150  +* needs: causes rendering to fail with an error if the extensions are not available
          151  +
          152  +where possible, instead of [$needs x y z], the directive [$when has-ext x y z] should be used instead. this causes the next section to be rendered only if the named extensions are available. [$unless has-ext x y z] can be used to provide an alternative format.
          153  +
          154  +extensions are mainly interacted with through directives. all extension directives must be prefixed with the name of the extension.
          155  +
          156  +### toc
          157  +sections that have a title will be included in the table of contents. the table of contents is by default inserted at the break between the first level-1 section and the section immediately following it. you may instead place the directive [$toc] where you wish the TOC to be inserted, or suppress it entirely with [$inhibits toc]. note that some renderers may not display the TOC as part of the document itself.
          158  +
          159  +### smart-quotes
          160  +a cortav renderer may automatically translate punctuation marks to other punctuation marks depending on their context. 
          161  +
          162  +### hilite
          163  +code can be highlighted according to the formal language it is written in.
          164  +
          165  +### lua
          166  +renderers with a lua interpreter available can evaluate lua code:
          167  +* [$%lua use [!file]]: evaluates [$file] and makes its definitions available
          168  +* [$\[%lua raw [!script]\]]: evaluates [$script] and emits the string it returns (if any) in raw span context.
          169  +* [$\[%lua exp [!script]\]]: evaluates [$script] and emits the string it returns (if any) in expanded span context.
          170  +* [$%lua raw [!script]]: evaluates [$script] and emits the string array it returns (if any) in raw block context.
          171  +* [$%lua exp [!script]]: evaluates [$script] and emits the string array it returns (if any) in expanded block context.
          172  +
          173  +the interpreter should provide a [$cortav] table with the objects:
          174  +* ctx: contains context variables
          175  +
          176  +used files should return a table with the following members
          177  +* macros: an array of functions that return strings or arrays of strings when invoked. these will be injected into the global macro namespace.
          178  +
          179  +### 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.
          181  +
          182  +ts enables the directives:
          183  +* [$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  +* [$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  +* [$when ts level [!level]]
          186  +* [$when ts word [!word]]
          187  +
          188  +ts enables the spans:
          189  +* [$\[🔒#[!level] [!styled-text]\]]: redacts the span if the security level is below that specified.
          190  +* [$\[🔒.[!word] [!styled-text]\]]: redacts the span if the specified codeword clearance is not enabled.
          191  +(the padlock emoji is shorthand for [$%ts].)
          192  +
          193  +ts redacts spans securely; that is, they are simply replaced with an indicator that they have been redacted, without visually leaking the length of the redacted text.
          194  +
          195  +~~~ts-example example ~~~ cortav
          196  +%ts word doc sorrowful-pines SORROWFUL PINES
          197  +
          198  +# intercept R1440 TCT S3
          199  +this communication between the ambassador of [*POLITY DOORMAT CRIMSON] "Socialist League world Glory" and an unknown noble of [*POLITY ROSE] "the Empire of a Thousand Suns" was intercepted by [*SYSTEM SUPINE WARBLE].
          200  +
          201  +## involved individuals
          202  +* (A) [*DOORMAT CRIMSON] Ambassador [🔒.morose-frenzy Hyacinth Autumn-Lotus] (confidence 1.0)
          203  +* (B) [*ROSE] Duchess [!UNKNOWN] (confidence 0.4)
          204  +
          205  +## provenance
          206  +this communication was retrieved by [🔒#3 automated buoy downlink] from [*SYSTEM SUPINE WARBLE].
          207  +
          208  +%ts level sec 9 ULTRAVIOLET
          209  +##> transcript
          210  +<A> we may have a problem
          211  +<B> Hyacinth, I told you not to contact me without—
          212  +<A, shouting> god DAMMIT woman I am trying to SAVE your worthless skin
          213  +<B> Hyacinth! your Godforsaken scrambler!
          214  +<A> …oh, [!fuck].
          215  +(signal lost)
          216  +~~~
          217  +

Added cortav.lua version [fc129ed31b].

            1  +-- [ʞ] cortav.lua
            2  +--  ~ lexi hale <lexi@hale.su>
            3  +--  © AGPLv3
            4  +--  ? renderer
            5  +
            6  +local ct = { render = {} }
            7  +
            8  +local function hexdump(s)
            9  +	local hexlines, charlines = {},{}
           10  +	for i=1,#s do
           11  +		local line = math.floor((i-1)/16) + 1
           12  +		hexlines[line] = (hexlines[line] or '') .. string.format("%02x ",string.byte(s, i))
           13  +		charlines[line] = (charlines[line] or '') .. ' ' .. string.gsub(string.sub(s, i, i), '[^%g ]', '\x1b[;35m·\x1b[36;1m') .. ' '
           14  +	end
           15  +	local str = ''
           16  +	for i=1,#hexlines do
           17  +		str = str .. '\x1b[1;36m' .. charlines[i] .. '\x1b[m\n' .. hexlines[i] .. '\n'
           18  +	end
           19  +	return str
           20  +end
           21  +
           22  +local function dump(o, state, path, depth)
           23  +	state = state or {tbls = {}}
           24  +	depth = depth or 0
           25  +	local pfx = string.rep('   ', depth)
           26  +	if type(o) == "table" then
           27  +		local str = ''
           28  +		for k,p in pairs(o) do
           29  +			local done = false
           30  +			local exp
           31  +			if type(p) == 'table' then
           32  +				if state.tbls[p] then
           33  +					exp = '<' .. state.tbls[p] ..'>'
           34  +					done = true
           35  +				else
           36  +					state.tbls[p] = path and string.format('%s.%s', path, k) or k
           37  +				end
           38  +			end
           39  +			if not done then
           40  +				local function dodump() return dump(
           41  +					p, state,
           42  +					path and string.format("%s.%s", path, k) or k,
           43  +					depth + 1
           44  +				) end
           45  +				-- boy this is ugly
           46  +				if type(p) ~= 'table' or
           47  +					getmetatable(p) == nil or
           48  +					getmetatable(p).__tostring == nil then
           49  +					exp = dodump()
           50  +				end
           51  +				if type(p) == 'table' then
           52  +					exp = string.format('{\n%s%s}', exp, pfx)
           53  +					local meta = getmetatable(p)
           54  +					if meta then
           55  +						if meta.__tostring then
           56  +							exp = tostring(p)
           57  +						end
           58  +						if meta.__name then
           59  +							exp = meta.__name .. ' ' .. exp
           60  +						end
           61  +					end
           62  +				end
           63  +			end
           64  +			str = str .. pfx .. string.format("%s = %s\n", k, exp)
           65  +		end
           66  +		return str
           67  +	elseif type(o) == "string" then
           68  +		return string.format('“%s”', o)
           69  +	else
           70  +		return tostring(o)
           71  +	end
           72  +end
           73  +
           74  +local function declare(c)
           75  +	local cls = setmetatable({
           76  +		__name = c.ident;
           77  +	}, {
           78  +		__name = 'class';
           79  +		__tostring = function() return c.ident or '(class)' end;
           80  +	})
           81  +
           82  +	cls.__call = c.call
           83  +	cls.__index = function(self, k)
           84  +		if c.default and c.default[k] then
           85  +			return c.default[k]
           86  +		end
           87  +		if k == 'clone' then
           88  +			return function(self)
           89  +				local new = cls.mk()
           90  +				for k,v in pairs(self) do
           91  +					new[k] = v
           92  +				end
           93  +				if c.clonesetup then
           94  +					c.clonesetup(new, self)
           95  +				end
           96  +				return new
           97  +			end
           98  +		elseif k == 'to' then
           99  +			return function(self, to, ...)
          100  +				if to == 'string' then return tostring(self)
          101  +				elseif to == 'number' then return tonumber(self)
          102  +				elseif to == 'int' then return math.floor(tonumber(self))
          103  +				elseif c.cast and c.cast[to] then
          104  +					return c.cast[to](self, ...)
          105  +				elseif type(to) == 'table' and getmetatable(to) and getmetatable(to).cvt and getmetatable(to).cvt[cls] then
          106  +				else error((c.ident or 'class') .. ' is not convertible to ' .. (type(to) == 'string' and to or tostring(to))) end
          107  +			end
          108  +		end
          109  +		if c.fns then return c.fns[k] end
          110  +	end
          111  +
          112  +	if c.cast then
          113  +		if c.cast.string then
          114  +			cls.__tostring = c.cast.string
          115  +		end
          116  +		if c.cast.number then
          117  +			cls.__tonumber = c.cast.number
          118  +		end
          119  +	end
          120  +
          121  +	cls.mk = function(...)
          122  +		local val = setmetatable(c.mk and c.mk(...) or {}, cls)
          123  +		if c.init then
          124  +			for k,v in pairs(c.init) do
          125  +				val[k] = v
          126  +			end
          127  +		end
          128  +		if c.construct then
          129  +			c.construct(val, ...)
          130  +		end
          131  +		return val
          132  +	end
          133  +	getmetatable(cls).__call = function(_, ...) return cls.mk(...) end
          134  +	cls.is = function(o) return getmetatable(o) == cls end
          135  +	return cls
          136  +end
          137  +ct.exn = declare {
          138  +	ident = 'exn';
          139  +	mk = function(kind, ...)
          140  +		return {
          141  +			vars = {...};
          142  +			kind = kind;
          143  +		}
          144  +	end;
          145  +	cast = {
          146  +		string = function(me)
          147  +			return me.kind.report(table.unpack(me.vars))
          148  +		end;
          149  +	};
          150  +	fns = {
          151  +		throw = function(me) error(me) end;
          152  +	}
          153  +}
          154  +ct.exnkind = declare {
          155  +	ident = 'exn-kind';
          156  +	mk = function(desc, report)
          157  +		return { desc = desc, report = report }
          158  +	end;
          159  +	call = function(me, ...)
          160  +		return ct.exn(me, ...)
          161  +	end;
          162  +}
          163  +
          164  +ct.exns = {
          165  +	tx = ct.exnkind('translation error', function(msg,...)
          166  +		return string.format("(%s:%u) "..msg, ...)
          167  +	end)
          168  +}
          169  +
          170  +ct.ctx = declare {
          171  +	mk = function(src) return {src = src} end;
          172  +	ident = 'context';
          173  +	cast = {
          174  +		string = function(me)
          175  +			return string.format("%s:%s [%u]", me.src.file, me.line, me.generation or 0)
          176  +		end;
          177  +	};
          178  +	clonesetup = function(new, old)
          179  +		for k,v in pairs(old) do new[k] = v end
          180  +		if old.generation then
          181  +			new.generation = old.generation + 1
          182  +		else
          183  +			new.generation = 1
          184  +		end
          185  +	end;
          186  +	fns = {
          187  +		fail = function(self, msg, ...)
          188  +			ct.exns.tx(msg, self.src.file, self.line or 0, ...):throw()
          189  +		end;
          190  +		ref = function(self,id)
          191  +			if not id:find'%.' then
          192  +				local rid = self.sec.refs[id]
          193  +				if self.sec.refs[id] then
          194  +					return self.sec.refs[id]
          195  +				else self:fail("no such ref %s in current section", id or '') end
          196  +			else
          197  +				local sec, ref = string.match(id, "(.-)%.(.+)")
          198  +				local s = self.doc.sections[sec]
          199  +				if s then
          200  +					if s.refs[ref] then
          201  +						return s.refs[ref]
          202  +					else self:fail("no such ref %s in section %s", ref, sec) end
          203  +				else self:fail("no such section %s", sec) end
          204  +			end
          205  +		end
          206  +	};
          207  +}
          208  +
          209  +ct.sec = declare {
          210  +	ident = 'section';
          211  +	mk = function() return {
          212  +		blocks = {};
          213  +		refs = {};
          214  +		depth = 0;
          215  +		kind = 'ordinary';
          216  +	} end;
          217  +	construct = function(self, id, depth)
          218  +		self.id = id
          219  +		self.depth = depth
          220  +	end;
          221  +}
          222  +
          223  +ct.doc = declare {
          224  +	ident = 'doc';
          225  +	fns = {
          226  +		mksec = function(self, id, depth)
          227  +			local o = ct.sec(id, depth)
          228  +			if id then self.sections[id] = o end
          229  +			table.insert(self.secorder, o)
          230  +			return o
          231  +		end;
          232  +	};
          233  +	mk = function() return {
          234  +		sections = {};
          235  +		secorder = {};
          236  +		embed = {};
          237  +		meta = {};
          238  +	} end;
          239  +}
          240  +
          241  +local function map(fn, lst)
          242  +	local new = {}
          243  +	for k,v in pairs(lst) do
          244  +		table.insert(new, fn(v,k))
          245  +	end
          246  +	return new
          247  +end
          248  +local function reduce(fn, acc, lst)
          249  +	for i,v in ipairs(lst) do
          250  +		acc = fn(acc, v, i)
          251  +	end
          252  +	return acc
          253  +end
          254  +local function fmtfn(str)
          255  +	return function(...)
          256  +		return string.format(str, ...)
          257  +	end
          258  +end
          259  +
          260  +function ct.render.html(doc, opts)
          261  +	local f = string.format
          262  +	local ids = {}
          263  +	local canonicalID = {}
          264  +	local function getSafeID(obj)
          265  +		if canonicalID[obj] then
          266  +			return canonicalID[obj]
          267  +		elseif obj.id and ids[obj.id] then
          268  +			local newid
          269  +			local i = 1
          270  +			repeat newid = obj.id .. string.format('-%x', i)
          271  +				i = i + 1 until not ids[newid]
          272  +			ids[newid] = obj
          273  +			canonicalID[obj] = newid
          274  +			return newid
          275  +		else
          276  +			local cid = obj.id
          277  +			if not cid then
          278  +				local i = 1
          279  +				repeat cid = string.format('x-%x', i)
          280  +					i = i + 1 until not ids[cid]
          281  +			end
          282  +			ids[cid] = obj
          283  +			canonicalID[obj] = cid
          284  +			return cid
          285  +		end
          286  +	end
          287  +
          288  +	local langsused = {}
          289  +	local langpairs = {
          290  +		lua = { color = 0x9377ff };
          291  +		terra = { color = 0xff77c8 };
          292  +		c = { name = 'C', color = 0x77ffe8 };
          293  +		html = { color = 0xfff877 };
          294  +		scheme = { color = 0x77ff88 };
          295  +		lisp = { color = 0x77ff88 };
          296  +		fortran = { color = 0xff779a };
          297  +		python = { color = 0xffd277 };
          298  +		python = { color = 0xcdd6ff };
          299  +	}
          300  +
          301  +	local stylesets = {
          302  +		code = [[
          303  +			code {
          304  +				background: #000;
          305  +				color: #fff;
          306  +				font-family: monospace;
          307  +				font-size: 90%;
          308  +				padding: 3px 5px;
          309  +			}
          310  +		]];
          311  +		abbr = [[
          312  +			abbr[title] { cursor: help; }
          313  +		]];
          314  +		editors_markup = [[]];
          315  +		block_code_listing = [[
          316  +			section > figure.listing {
          317  +				font-family: monospace;
          318  +				background: #000;
          319  +				color: #fff;
          320  +				padding: 0;
          321  +				margin: 0.3em 0;
          322  +				counter-reset: line-number;
          323  +				position: relative;
          324  +			}
          325  +			section > figure.listing>div {
          326  +				white-space: pre-wrap;
          327  +				counter-increment: line-number;
          328  +				text-indent: -2.3em;
          329  +				margin-left: 2.3em;
          330  +			}
          331  +			section > figure.listing>:is(div,hr)::before {
          332  +				width: 1.0em;
          333  +				padding: 0.2em 0.4em;
          334  +				text-align: right;
          335  +				display: inline-block;
          336  +				background-color: #333;
          337  +				border-right: 1px solid #fff;
          338  +				content: counter(line-number);
          339  +				margin-right: 0.3em;
          340  +			}
          341  +			section > figure.listing>hr::before {
          342  +				color: #333;
          343  +				padding-top: 0;
          344  +				padding-bottom: 0;
          345  +			}
          346  +			section > figure.listing>div::before {
          347  +				color: #fff;
          348  +			}
          349  +			section > figure.listing>div:last-child::before {
          350  +				padding-bottom: 0.5em;
          351  +			}
          352  +			section > figure.listing>figcaption:first-child {
          353  +				border: none;
          354  +				border-bottom: 1px solid #fff;
          355  +			}
          356  +			section > figure.listing>figcaption::after {
          357  +				display: block;
          358  +				float: right;
          359  +				font-weight: normal;
          360  +				font-style: italic;
          361  +				font-size: 70%;
          362  +				padding-top: 0.3em;
          363  +			}
          364  +			section > figure.listing>figcaption {
          365  +				font-family: sans-serif;
          366  +				font-weight: bold;
          367  +				font-size: 130%;
          368  +				padding: 0.2em 0.4em;
          369  +				border: none;
          370  +			}
          371  +			section > figure.listing > hr {
          372  +				border: none;
          373  +				margin: 0;
          374  +				height: 0.7em;
          375  +				counter-increment: line-number;
          376  +			}
          377  +		]];
          378  +	}
          379  +
          380  +	local stylesNeeded = {}
          381  +
          382  +	local function getSpanRenderers(tag,elt)
          383  +		local htmlDoc = function(title, head, body)
          384  +			return [[<!doctype html>]] .. tag('html',nil,
          385  +				tag('head', nil,
          386  +					elt('meta',{charset = 'utf-8'}) ..
          387  +					(title and tag('title', nil, title) or '') ..
          388  +					(head or '')) ..
          389  +				tag('body', nil, body or ''))
          390  +		end
          391  +
          392  +		local span_renderers = {}
          393  +		local function htmlSpan(spans, block, sec)
          394  +			local text = {}
          395  +			for k,v in pairs(spans) do
          396  +				if type(v) == 'string' then
          397  +					table.insert(text,(v:gsub('[<>&]',
          398  +						function(x)
          399  +							return string.format('&#%02u', string.byte(x))
          400  +						end)))
          401  +				else
          402  +					table.insert(text, span_renderers[v.kind](v, block, sec))
          403  +				end
          404  +			end
          405  +			return table.concat(text)
          406  +		end
          407  +
          408  +		function span_renderers.format(sp)
          409  +			local tags = { strong = 'strong', emph = 'em', strike = 'del', insert = 'ins', literal = 'code' }
          410  +			if sp.style == 'literal' then
          411  +				stylesNeeded.code = true
          412  +			end
          413  +			if sp.style == 'del' or sp.style == 'ins' then
          414  +				stylesNeeded.editors_markup = true
          415  +			end
          416  +			return tag(tags[sp.style],nil,htmlSpan(sp.spans))
          417  +		end
          418  +
          419  +		function span_renderers.term(t,b)
          420  +			local r = b.origin:ref(t.ref)
          421  +			local name = t.ref
          422  +			if name:find'%.' then name = name:match '^[^.]*%.(.+)$' end
          423  +			if type(r) ~= 'string' then
          424  +				b.origin:fail('%s is an object, not a reference', t.ref)
          425  +			end
          426  +			stylesNeeded.abbr = true
          427  +			return tag('abbr',{title=r},next(t.spans) and htmlSpan(t.spans) or name)
          428  +		end
          429  +
          430  +		function span_renderers.link(sp,b)
          431  +			local href
          432  +			if b.origin.doc.sections[sp.ref] then
          433  +				href = '#' .. sp.ref
          434  +			else
          435  +				if sp.addr then href = sp.addr else
          436  +					local r = b.origin:ref(sp.ref)
          437  +					if type(r) == 'table' then
          438  +						href = '#' .. getSafeID(r)
          439  +					else href = r end
          440  +				end
          441  +			end
          442  +			return tag('a',{href=href},next(sp.spans) and htmlSpan(sp.spans) or href)
          443  +		end
          444  +		return {
          445  +			span_renderers = span_renderers;
          446  +			htmlSpan = htmlSpan;
          447  +			htmlDoc = htmlDoc;
          448  +		}
          449  +	end
          450  +
          451  +
          452  +	local function getBlockRenderers(tag,elt,sr,catenate)
          453  +		local function insert_toc(b,s)
          454  +			local lst = {tag = 'ol', attrs={}, nodes={}}
          455  +			local stack = {lst}
          456  +			local top = function() return stack[#stack] end
          457  +			local all = s.origin.doc.secorder
          458  +			for i, sec in ipairs(all) do
          459  +				if sec.heading_node then
          460  +					local ent = tag('li',nil,
          461  +						 catenate{tag('a', {href='#'..getSafeID(sec)},
          462  +							sr.htmlSpan(sec.heading_node.spans))})
          463  +					if sec.depth > #stack then
          464  +						local n = {tag = 'ol', attrs={}, nodes={ent}}
          465  +						table.insert(top().nodes[#top().nodes].nodes, n)
          466  +						table.insert(stack, n)
          467  +					else
          468  +						if sec.depth < #stack then
          469  +							for j=#stack,sec.depth+1,-1 do stack[j] = nil end
          470  +						end
          471  +						table.insert(top().nodes, ent)
          472  +					end
          473  +				end
          474  +			end
          475  +			return lst
          476  +		end
          477  +
          478  +		local block_renderers = {
          479  +			paragraph = function(b,s)
          480  +				return tag('p', nil, sr.htmlSpan(b.spans, b, s), b)
          481  +			end;
          482  +			label = function(b,s)
          483  +				if ct.sec.is(b.captions) then
          484  +					local h = math.min(6,math.max(1,b.captions.depth))
          485  +					return tag(f('h%u',h), nil, sr.htmlSpan(b.spans, b, s), b)
          486  +				else
          487  +					-- handle other uses of labels here
          488  +				end
          489  +			end;
          490  +			['list-item'] = function(b,s)
          491  +				return tag('li', nil, sr.htmlSpan(b.spans, b, s), b)
          492  +			end;
          493  +			toc = insert_toc;
          494  +			table = function(b,s)
          495  +				local tb = {}
          496  +				for i, r in ipairs(b.rows) do
          497  +					local row = {}
          498  +					for i, c in ipairs(r) do
          499  +						table.insert(row, tag(c.header and 'th' or 'td',
          500  +						{align=c.align}, sr.htmlSpan(c.spans, b)))
          501  +					end
          502  +					table.insert(tb, tag('tr',nil,catenate(row)))
          503  +				end
          504  +				return tag('table',nil,catenate(tb))
          505  +			end;
          506  +			listing = function(b,s)
          507  +				stylesNeeded.block_code_listing = true
          508  +				local nodes = map(function(l)
          509  +					if #l > 0 then
          510  +						return tag('div',nil,sr.htmlSpan(l, b, s))
          511  +					else
          512  +						return elt('hr')
          513  +					end
          514  +				end, b.lines)
          515  +				if b.title then
          516  +					table.insert(nodes,1,tag('figcaption',nil,sr.htmlSpan(b.title)))
          517  +				end
          518  +				langsused[b.lang] = true
          519  +				return tag('figure', {class='listing', lang=b.lang, id=b.id and getSafeID(b)}, catenate(nodes))
          520  +			end;
          521  +			['break'] = function() --[[nop]] end;
          522  +		}
          523  +		return block_renderers;
          524  +	end
          525  +
          526  +	local pspan = getSpanRenderers(function(t,a,v) return v  end,
          527  +	                               function(t,a)   return '' end)
          528  +	 
          529  +	local function getRenderers(tag,elt,catenate)
          530  +		local r = getSpanRenderers(tag,elt)
          531  +		r.block_renderers = getBlockRenderers(tag,elt,r,catenate)
          532  +		return r
          533  +	end
          534  +
          535  +	local elt = function(t,attrs)
          536  +		return f('<%s%s>', t,
          537  +			attrs and reduce(function(a,b) return a..b end, '', 
          538  +				map(function(v,k)
          539  +					if v == true
          540  +						then          return ' '..k
          541  +						elseif v then return f(' %s="%s"', k, v)
          542  +					end
          543  +				end, attrs)) or '')
          544  +	end
          545  +	local tag = function(t,attrs,body)
          546  +		return f('%s%s</%s>', elt(t,attrs), body, t)
          547  +	end
          548  +
          549  +	local doctitle
          550  +	local ir = {}
          551  +	local toc
          552  +	local dr = getRenderers(tag,elt,table.concat) -- default renderers
          553  +	local plainr = getRenderers(function(t,a,v) return v  end,
          554  +	                            function(t,a)   return '' end, table.concat)
          555  +	local irBlockRdrs = getBlockRenderers(
          556  +		function(t,a,v,o) return {tag = t, attrs = a, nodes = type(v) == 'string' and {v} or v, src = o} end,
          557  +		function(t,a,o) return {tag = t, attrs = a, src = o} end,
          558  +		dr, function(...) return ... end)
          559  +
          560  +	for i, sec in ipairs(doc.secorder) do
          561  +		if doctitle == nil and sec.depth == 1 and sec.heading_node then
          562  +			doctitle = plainr.htmlSpan(sec.heading_node.spans, sec.heading_node, sec)
          563  +		end
          564  +		local irs
          565  +		if sec.kind == 'ordinary' then
          566  +			if #(sec.blocks) > 0 then
          567  +				irs = {tag='section',attrs={id = getSafeID(sec)},nodes={}}
          568  +
          569  +				for i, block in ipairs(sec.blocks) do
          570  +					local rd = irBlockRdrs[block.kind](block,sec)
          571  +					if rd then table.insert(irs.nodes, rd) end
          572  +				end
          573  +			end
          574  +		elseif sec.kind == 'blockquote' then
          575  +		elseif sec.kind == 'listing' then
          576  +		elseif sec.kind == 'embed' then
          577  +		end
          578  +		if irs then table.insert(ir, irs) end
          579  +	end
          580  +
          581  +	-- restructure passes
          582  +	
          583  +	---- list insertion pass
          584  +	local lists = {}
          585  +	for _, sec in pairs(ir) do
          586  +		if sec.tag == 'section' then
          587  +			local i = 1 while i <= #sec.nodes do local v = sec.nodes[i]
          588  +				if v.tag == 'li' then
          589  +					local ltag
          590  +					if v.src.ordered
          591  +						then ltag = 'ol'
          592  +						else ltag = 'ul'
          593  +					end
          594  +					local last = i>1 and sec.nodes[i-1]
          595  +					if last and last.embed == 'list' and not (
          596  +						last.ref[#last.ref].src.depth   == v.src.depth and
          597  +						last.ref[#last.ref].src.ordered ~= v.src.ordered
          598  +					) then
          599  +						-- add to existing list
          600  +						table.insert(last.ref, v)
          601  +						table.remove(sec.nodes, i) i = i - 1
          602  +					else
          603  +						-- wrap in list
          604  +						local newls = {v}
          605  +						sec.nodes[i] = {embed = 'list', ref = newls}
          606  +						table.insert(lists,newls)
          607  +					end
          608  +				end
          609  +			i = i + 1 end
          610  +		end
          611  +	end
          612  +
          613  +	for _, sec in pairs(ir) do
          614  +		if sec.tag == 'section' then
          615  +			for i, elt in pairs(sec.nodes) do
          616  +				if elt.embed == 'list' then
          617  +					local function fail_nest()
          618  +						elt.ref[1].src.origin:fail('improper list nesting')
          619  +					end
          620  +					local struc = {attrs={}, nodes={}}
          621  +					if elt.ref[1].src.ordered then struc.tag = 'ol' else struc.tag = 'ul' end
          622  +					if elt.ref[1].src.depth ~= 1 then fail_nest() end
          623  +
          624  +					local stack = {struc}
          625  +					local copyNodes = function(old,new)
          626  +						for i,v in ipairs(old) do new[#new + i] = v end
          627  +					end
          628  +					for i,e in ipairs(elt.ref) do
          629  +						if e.src.depth > #stack then
          630  +							if e.src.depth - #stack > 1 then fail_nest() end
          631  +							local newls = {attrs={}, nodes={e}}
          632  +							copyNodes(e.nodes,newls)
          633  +							if e.src.ordered then newls.tag = 'ol' else newls.tag='ul' end
          634  +							table.insert(stack[#stack].nodes[#stack[#stack].nodes].nodes, newls)
          635  +							table.insert(stack, newls)
          636  +						else
          637  +							if e.src.depth < #stack then
          638  +								-- pop entries off the stack
          639  +								for i=#stack, e.src.depth+1, -1 do stack[i] = nil end
          640  +							end
          641  +							table.insert(stack[#stack].nodes, e)
          642  +						end
          643  +					end
          644  +
          645  +					sec.nodes[i] = struc
          646  +				end
          647  +			end
          648  +		end
          649  +	end
          650  +	
          651  +
          652  +	-- collection pass
          653  +	local function collect_nodes(t)
          654  +		local ts = ''
          655  +		for i,v in ipairs(t) do
          656  +			if type(v) == 'string' then
          657  +				ts = ts .. v
          658  +			elseif v.nodes then
          659  +				ts = ts .. tag(v.tag, v.attrs, collect_nodes(v.nodes))
          660  +			elseif v.text then
          661  +				ts = ts .. tag(v.tag,v.attrs,v.text)
          662  +			else
          663  +				ts = ts .. elt(v.tag,v.attrs)
          664  +			end
          665  +		end
          666  +		return ts
          667  +	end
          668  +	local body = collect_nodes(ir)
          669  +
          670  +	for k in pairs(langsused) do
          671  +		local spec = langpairs[k] or {color=0xaaaaaa}
          672  +		stylesets.block_code_listing = stylesets.block_code_listing .. string.format(
          673  +			[[section > figure.listing[lang="%s"]>figcaption::after
          674  +				{ content: '%s'; color: #%06x }]],
          675  +			k, spec.name or k, spec.color)
          676  +	end
          677  +
          678  +	local styles = {}
          679  +	for k in pairs(stylesNeeded) do
          680  +		table.insert(styles, (stylesets[k]:gsub('%s+',' ')))
          681  +	end
          682  +
          683  +	local head = {}
          684  +	if next(styles) then
          685  +		table.insert(head, tag('style',{type='text/css'},table.concat(styles)))
          686  +	end
          687  +
          688  +	if opts.snippet then
          689  +		return body
          690  +	else
          691  +		return dr.htmlDoc(doctitle, next(head) and table.concat(head), body)
          692  +	end
          693  +end
          694  +
          695  +local function
          696  +startswith(str, pfx)
          697  +	return string.sub(str, 1, #pfx) == pfx
          698  +end
          699  +
          700  +local function eachcode(str, ascode)
          701  +	local pos = {
          702  +		code = 1;
          703  +		byte = 1;
          704  +	}
          705  +	return function()
          706  +		if pos.byte > #str then return nil end
          707  +		local thischar = utf8.codepoint(str, pos.byte)
          708  +		local lastpos = {
          709  +			code = pos.code;
          710  +			byte = pos.byte;
          711  +			next = pos;
          712  +		}
          713  +		if not ascode then
          714  +			thischar = utf8.char(thischar)
          715  +			pos.byte = pos.byte + #thischar
          716  +		else
          717  +			pos.byte = pos.byte + #utf8.char(thischar)
          718  +		end
          719  +		pos.code = pos.code + 1
          720  +		return thischar, lastpos
          721  +	end
          722  +end
          723  +
          724  +local function formatter(sty)
          725  +	return function(s,c)
          726  +		return {
          727  +			kind = 'format';
          728  +			style = sty;
          729  +			spans = ct.parse_span(s, c);
          730  +			origin = c:clone();
          731  +		}
          732  +	end
          733  +end
          734  +ct.spanctls = {
          735  +	{seq = '$', parse = formatter 'literal'};
          736  +	{seq = '!', parse = formatter 'emph'};
          737  +	{seq = '*', parse = formatter 'strong'};
          738  +	{seq = '\\', parse = function(s, c) -- raw
          739  +		return s
          740  +	end};
          741  +	{seq = '$\\', parse = function(s, c) -- raw
          742  +		return {
          743  +			kind = 'format';
          744  +			style = 'literal';
          745  +			spans = {s};
          746  +			origin = c:clone();
          747  +		}
          748  +	end};
          749  +	{seq = '&', parse = function(s, c)
          750  +		local r, t = s:match '^([^%s]+)%s*(.-)$'
          751  +		return {
          752  +			kind = 'term';
          753  +			spans = (t and t ~= "") and ct.parse_span(t, c) or {};
          754  +			ref = r; 
          755  +			origin = c:clone();
          756  +		}
          757  +	end};
          758  +	{seq = '^', parse = function(s, c)
          759  +		local fn, t = s:match '^([^%s]+)%s*(.-)$'
          760  +		return {
          761  +			kind = 'footnote';
          762  +			spans = (t and t~='') and ct.parse_span(t, c) or {};
          763  +			ref = fn;
          764  +			origin = c:clone();
          765  +		}
          766  +	end};
          767  +	{seq = '>', parse = function(s, c)
          768  +		local to, t = s:match '^([^%s]+)%s*(.-)$'
          769  +		if not to then c:fail('invalid link syntax >%s', s) end
          770  +		if t == "" then t = nil end
          771  +		return {
          772  +			kind = 'link';
          773  +			spans = (t and t~='') and ct.parse_span(t, c) or {};
          774  +			ref = to;
          775  +			origin = c:clone();
          776  +		}
          777  +	end};
          778  +}
          779  +
          780  +function ct.parse_span(str,ctx)
          781  +	local function delimited(start, stop, s)
          782  +		local depth = 0
          783  +		if not startswith(s, start) then return nil end
          784  +		for c,p in eachcode(s) do
          785  +			if c == '\\' then
          786  +				p.next.byte = p.next.byte + #utf8.char(utf8.codepoint(s, p.next.byte))
          787  +				p.next.code = p.next.code + 1
          788  +			elseif c == start then
          789  +				depth = depth + 1
          790  +			elseif c == stop then
          791  +				depth = depth - 1
          792  +				if depth == 0 then
          793  +					return s:sub(1+#start, p.byte - #stop), p.byte -- FIXME
          794  +				elseif depth < 0 then
          795  +					ctx:fail('out of place %s', stop)
          796  +				end
          797  +			end
          798  +		end
          799  +
          800  +		ctx:fail('%s expected before end of line', stop)
          801  +	end
          802  +	local buf = ""
          803  +	local spans = {}
          804  +	local function flush()
          805  +		if buf ~= "" then
          806  +			table.insert(spans, buf)
          807  +			buf = ""
          808  +		end
          809  +	end
          810  +	local skip = false
          811  +	for c,p in eachcode(str) do
          812  +		if skip == true then
          813  +			skip = false
          814  +			buf = buf .. c
          815  +		elseif c == '\\' then
          816  +			skip = true
          817  +		elseif c == '[' then
          818  +			flush()
          819  +			local substr, following = delimited('[',']',str:sub(p.byte))
          820  +			p.next.byte = following + p.byte
          821  +			local found = false
          822  +			for _,i in pairs(ct.spanctls) do
          823  +				if startswith(substr, i.seq) then
          824  +					found = true
          825  +					table.insert(spans, i.parse(substr:sub(1+#i.seq), ctx))
          826  +					break
          827  +				end
          828  +			end
          829  +			if not found then
          830  +				ctx:fail('no recognized control sequence in [%s]', substr)
          831  +			end
          832  +		else
          833  +			buf = buf .. c
          834  +		end
          835  +	end
          836  +	flush()
          837  +	return spans
          838  +end
          839  +
          840  +
          841  +local function
          842  +blockwrap(fn)
          843  +	return function(l,c)
          844  +		local block = fn(l,c)
          845  +		block.origin = c:clone();
          846  +		table.insert(c.sec.blocks, block);
          847  +	end
          848  +end
          849  +
          850  +local insert_paragraph = blockwrap(function(l,c)
          851  +	if l:sub(1,1) == '.' then l = l:sub(2) end
          852  +	return {
          853  +		kind = "paragraph";
          854  +		spans = ct.parse_span(l, c);
          855  +	}
          856  +end)
          857  +
          858  +local insert_section = function(l,c)
          859  +	local depth, id, t = l:match '^([#§]+)([^%s]*)%s*(.-)$'
          860  +	if id and id ~= "" then
          861  +		if c.doc.sections[id] then
          862  +			c:fail('duplicate section name “%s”', id)
          863  +		end
          864  +	else id = nil end
          865  +
          866  +	local s = c.doc:mksec(id, utf8.len(depth))
          867  +	s.depth = utf8.len(depth)
          868  +	s.origin = c:clone()
          869  +	s.blocks={}
          870  +
          871  +	if t and t ~= "" then
          872  +		local heading = {
          873  +			kind = "label";
          874  +			spans = ct.parse_span(t,c);
          875  +			origin = s.origin;
          876  +			captions = s;
          877  +		}
          878  +		table.insert(s.blocks, heading)
          879  +		s.heading_node = heading
          880  +	end
          881  +	c.sec = s
          882  +end
          883  +
          884  +local dsetmeta = function(w,c)
          885  +	local key, val = w(1)
          886  +	c.doc.meta[key] = val
          887  +end
          888  +local dextctl = function(w,c)
          889  +	local mode, exts = w(1)
          890  +	for e in exts:gmatch '([^%s]+)' do
          891  +		if mode == 'uses' then
          892  +		elseif mode == 'needs' then
          893  +		elseif mode == 'inhibits' then
          894  +		end
          895  +	end
          896  +end
          897  +local dcond = function(w,c)
          898  +	local mode, cond, exp = w(2)
          899  +	c.hide_next = mode == 'unless'
          900  +end;
          901  +ct.directives = {
          902  +	author = dsetmeta;
          903  +	license = dsetmeta;
          904  +	keywords = dsetmeta;
          905  +	desc = dsetmeta;
          906  +	toc = function(w,c)
          907  +		local toc, op, val = w(2)
          908  +		if op == nil then
          909  +			table.insert(c.sec.blocks, {kind='toc'})
          910  +		end
          911  +	end;
          912  +	when = dcond;
          913  +	unless = dcond;
          914  +	expand = function(w,c)
          915  +		local _, m = w(1)
          916  +		if m ~= 'off' then
          917  +			c.expand_next = 1
          918  +		else
          919  +			c.expand_next = 0
          920  +		end
          921  +	end;
          922  +}
          923  +
          924  +local function insert_table_row(l,c)
          925  +	local row = {}
          926  +	for kind, a1, text, a2 in l:gmatch '([+|])(:?)%s*([^:+|]*)%s*(:?)' do
          927  +		local header = kind == '+'
          928  +		local align
          929  +		if     a1 == ':' and a2 ~= ':' then
          930  +			align = 'left'
          931  +		elseif a1 == ':' and a2 == ':' then
          932  +			align = 'center'
          933  +		elseif a1 ~= ':' and a2 == ':' then
          934  +			align = 'right'
          935  +		end
          936  +		table.insert(row, {
          937  +			spans = ct.parse_span(text, c);
          938  +			align = align;
          939  +			header = header;
          940  +		})
          941  +	end
          942  +	if #c.sec.blocks > 1 and c.sec.blocks[#c.sec.blocks].kind == 'table' then
          943  +		local tbl = c.sec.blocks[#c.sec.blocks]
          944  +		table.insert(tbl.rows, row)
          945  +	else
          946  +		table.insert(c.sec.blocks, {
          947  +			kind = 'table';
          948  +			rows = {row};
          949  +			origin = c:clone();
          950  +		})
          951  +	end
          952  +end
          953  +
          954  +ct.ctlseqs = {
          955  +	{seq = '.', fn = insert_paragraph};
          956  +	{seq = '¶', fn = insert_paragraph};
          957  +	{seq = '#', fn = insert_section};
          958  +	{seq = '§', fn = insert_section};
          959  +	{seq = '+', fn = insert_table_row};
          960  +	{seq = '|', fn = insert_table_row};
          961  +	{seq = '│', fn = insert_table_row};
          962  +	{pred = function(s,c) return s:match'^[*:]' end, fn = blockwrap(function(l,c) -- list
          963  +		local stars = l:match '^([*:]+)'
          964  +		local depth = utf8.len(stars)
          965  +		local id, txt = l:sub(#stars+1):match '^(.-)%s*(.-)$'
          966  +		local ordered = stars:sub(#stars) == ':'
          967  +		if id == '' then id = nil end
          968  +		return {
          969  +			kind = 'list-item';
          970  +			depth = depth;
          971  +			ordered = ordered;
          972  +			spans = ct.parse_span(txt, c);
          973  +		}
          974  +	end)};
          975  +	{seq = '\t', fn = function(l,c)
          976  +		local ref, val = l:match '\t+([^:]+):%s*(.*)$'
          977  +		c.sec.refs[ref] = val
          978  +	end};
          979  +	{seq = '%', fn = function(l,c) -- directive
          980  +		local crit, cmdline = l:match '^%%([!%%]?)%s*(.*)$'
          981  +		local words = function(i)
          982  +			local wds = {}
          983  +			if i == 0 then return cmdline end
          984  +			for w,pos in cmdline:gmatch '([^%s]+)()' do
          985  +				table.insert(wds, w)
          986  +				i = i - 1
          987  +				if i == 0 then
          988  +					return table.unpack(wds), cmdline:sub(pos)
          989  +				end
          990  +			end
          991  +		end
          992  +
          993  +		local cmd, rest = words(1)
          994  +		if ct.directives[cmd] then
          995  +			ct.directives[cmd](words,c)
          996  +		elseif crit == '!' then
          997  +			c:fail('critical directive %s not supported',cmd)
          998  +		end
          999  +	end;};
         1000  +	{seq = '~~~', fn = blockwrap(function(l,c)
         1001  +		local extract = function(ptn, str)
         1002  +			local start, stop = str:find(ptn)
         1003  +			if not start then return nil, str end
         1004  +			local ex = str:sub(start,stop)
         1005  +			local n = str:sub(1,start-1) .. str:sub(stop+1)
         1006  +			return ex, n
         1007  +		end
         1008  +		local lang, id, title
         1009  +		if l:match '^~~~%s*$' then -- no args
         1010  +		elseif l:match '^~~~.*~~~%s*$' then -- CT style
         1011  +			local s = l:match '^~~~%s*(.-)%s*~~~%s*$'
         1012  +			lang, s = extract('%b[]', s)
         1013  +			lang = lang:sub(2,-2)
         1014  +			id, title = extract('#[^%s]+', s)
         1015  +			if id then id = id:sub(2) end
         1016  +		elseif l:match '^~~~' then -- MD shorthand style
         1017  +			lang = l:match '^~~~%s*(.-)%s*$'
         1018  +		end
         1019  +		c.mode = {
         1020  +			kind = 'code';
         1021  +			listing = {
         1022  +				kind = 'listing';
         1023  +				lang = lang, id = id, title = title and ct.parse_span(title,c);
         1024  +				lines = {};
         1025  +			}
         1026  +		}
         1027  +		if id then
         1028  +			if c.sec.refs[id] then c:fail('duplicate ID %s', id) end
         1029  +			c.sec.refs[id] = c.mode.listing
         1030  +		end
         1031  +		return c.mode.listing;
         1032  +	end)};
         1033  +	{pred = function(s,c)
         1034  +		if s:match '^[%-_][*_%-%s]+' then return true end
         1035  +		if startswith(s, '—') then
         1036  +			for c, p in eachcode(s) do
         1037  +				if ({
         1038  +					['—'] = true, ['-'] = true, [' '] = true;
         1039  +					['*'] = true, ['_'] = true, ['\t'] = true;
         1040  +				})[c] ~= true then return false end
         1041  +			end
         1042  +			return true
         1043  +		end
         1044  +	end; fn = blockwrap(function()
         1045  +		return { kind = 'horiz-rule' }
         1046  +	end)};
         1047  +	{fn = insert_paragraph};
         1048  +}
         1049  +
         1050  +function ct.parse(file, src)
         1051  +	local function
         1052  +	is_whitespace(cp)
         1053  +		return cp == 0x20
         1054  +	end
         1055  +
         1056  +	local ctx = ct.ctx.mk(src)
         1057  +	ctx.line = 0
         1058  +	ctx.doc = ct.doc.mk()
         1059  +	ctx.sec = ctx.doc:mksec() -- toplevel section
         1060  +	ctx.sec.origin = ctx:clone()
         1061  +
         1062  +	for full_line in file:lines() do ctx.line = ctx.line + 1
         1063  +		local l
         1064  +		for p, c in utf8.codes(full_line) do
         1065  +			if not is_whitespace(c) then
         1066  +				l = full_line:sub(p)
         1067  +				break
         1068  +			end
         1069  +		end
         1070  +		if ctx.mode then
         1071  +			if ctx.mode.kind == 'code' then
         1072  +				if l and l:match '^~~~%s*$' then
         1073  +					ctx.mode = nil
         1074  +				else
         1075  +					-- TODO handle formatted code
         1076  +					table.insert(ctx.mode.listing.lines, {l})
         1077  +				end
         1078  +			else
         1079  +				ctx:fail('unimplemented syntax mode %s', ctx.mode.kind)
         1080  +			end
         1081  +		else
         1082  +			if l then
         1083  +				local found = false
         1084  +				for _, i in pairs(ct.ctlseqs) do
         1085  +					if  ((not i.seq ) or startswith(l, i.seq)) and
         1086  +						((not i.pred) or i.pred    (l, ctx  )) then
         1087  +						found = true
         1088  +						i.fn(l, ctx)
         1089  +						break
         1090  +					end
         1091  +				end
         1092  +				if not found then
         1093  +					ctx:fail 'incomprehensible input line'
         1094  +				end
         1095  +			else
         1096  +				if next(ctx.sec.blocks) and ctx.sec.blocks[#ctx.sec.blocks].kind ~= 'break' then
         1097  +					table.insert(ctx.sec.blocks, {kind='break'})
         1098  +				end
         1099  +			end
         1100  +		end
         1101  +	end
         1102  +
         1103  +	return ctx.doc
         1104  +end
         1105  +
         1106  +local default_mode = {
         1107  +	format = 'html';
         1108  +}
         1109  +
         1110  +local function main(input, output, log, mode, vars)
         1111  +	local doc = ct.parse(input.stream, input.src)
         1112  +	input.stream:close()
         1113  +	if mode['show-tree'] then
         1114  +		log:write(dump(doc))
         1115  +	end
         1116  +
         1117  +	if not mode.format then
         1118  +		error 'what output format should i translate the input to?'
         1119  +	end
         1120  +	if not ct.render[mode.format] then
         1121  +		error(string.format('output format “%s” unsupported', mode.format))
         1122  +	end
         1123  +	
         1124  +	output:write(ct.render[mode.format](doc, {}))
         1125  +end
         1126  +
         1127  +local inp,outp,log = io.stdin, io.stdout, io.stderr
         1128  +
         1129  +local function entry_cli()
         1130  +	local mode, vars, input = default_mode, {}, {
         1131  +		stream = inp;
         1132  +		src = {
         1133  +			file = '(stdin)';
         1134  +		}
         1135  +	}
         1136  +
         1137  +	if arg[1] and arg[1] ~= '' then
         1138  +		local file = io.open(arg[1], "rb")
         1139  +		if not file then error('unable to load file ' .. arg[1]) end
         1140  +		input.stream = file
         1141  +		input.src.file = arg[1]
         1142  +	end
         1143  +
         1144  +	main(input, outp, log, mode, vars)
         1145  +end
         1146  +
         1147  +-- local ok, e = pcall(entry_cli)
         1148  +local ok, e = true, entry_cli()
         1149  +if not ok then
         1150  +	local str = 'translation failure'
         1151  +	local color = false
         1152  +	if log:seek() == nil then
         1153  +		-- this is not a very reliable heuristic for detecting
         1154  +		-- attachment to a tty but it's better than nothing
         1155  +		if os.getenv('COLORTERM') then
         1156  +			color = true
         1157  +		else
         1158  +			local term = os.getenv('TERM')
         1159  +			if term:find 'color' then color = true end
         1160  +		end
         1161  +	end
         1162  +	if color then
         1163  +		str = string.format('\27[1;31m%s\27[m', str)
         1164  +	end
         1165  +	log:write(string.format('[%s] %s\n\t%s\n', os.date(), str, e))
         1166  +	os.exit(1)
         1167  +end