Overview
Comment: | initial commit |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
5e07b52c5728be3c548265b5a27f1ed7 |
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