cortav  Check-in [35ea3c5797]

Overview
Comment:add blockquote support for html, subdocument mechanisms, mode to generate epub-compatible XHTML5; various fixes and improvements
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 35ea3c5797d672b430bd3a8ce9ee99133f40a37a206bf488a82b61795a8b0c77
User & Date: lexi on 2022-09-05 18:49:58
Other Links: manifest | tags
Context
2022-09-05
20:15
cleanup check-in: e551f71321 user: lexi tags: trunk
18:49
add blockquote support for html, subdocument mechanisms, mode to generate epub-compatible XHTML5; various fixes and improvements check-in: 35ea3c5797 user: lexi tags: trunk
2022-04-17
21:16
various updates fuck man idr what all i did check-in: c0bdfa46df user: lexi tags: trunk
Changes

Modified cortav.ct from [2cc3a528df] to [03a705bddd].

    25     25   * [*lists] use a different syntax from markdown. you can start a line with a [`*] to create an unordered list, or [`:] to create an ordered list; indentation doesn't matter. if you want to nest list items, instead of putting two spaces before the child item, you just add another star or colon. and of course, you can nest lists of different kinds within one another.
    26     26   * [*horizontal rules] use roughly the same syntax: three or more hyphens on a line of their own ([`\---]). underlines also work ([`___], [`-_-], [`__-__-__] etc), as do horizontal unicode box drawing characters ([`─ ━ ┈] etc).
    27     27   * some markdown implementations support [*tables]. cortav does too, using a very simple notation similar to the usual notation used in markdown. a key difference, however, is that cortav table cells can contain any formatting a paragraph can.
    28     28   * [*underlines] are supported by some markdown implementations. in cortav, you can apply them with the notation [`\[_my underlined text\]] -- please just use them sparingly when you render to HTML!
    29     29   * [*strikethrough] is supported by some extended versions of markdown. cortav uses the notation [`\[~my deleted text\]], with the intended semantics of text that is being removed by some revision of a document. (you can also denote text that is being [!added] by using a plus sign instead of a tilde)
    30     30   * [*images] are a bit more complicated, but much more versatile. see the section on [>rsrc resources] for an explanation.
    31     31   * [*smart quotes] and [*em dashes] are inserted automatically, just as in markdown, provided you have the [>tsmog transmogrify] extension available. (it is part of the reference implementation and defined by the spec, but not required.) in fact, you can insert longer dashes than em dashes just by increasing the number of hyphens. the reference implementation's transmogrifier also translates ascii arrows like [`\-->] into their unicode equivalents ([`→]).
    32         -* [*literals] (also known as [*code text]) can be inserted with the [`\[`int main(void);] syntax. note however that literals are not protected from the transmogrifier, and are parsed like any other span, which may cause problems if the source code you're quoting makes use of such forbidden runes. in this case, you'll want to wrap the code span in a raw span. the syntax for this is [`\[`[\\int main(void);\]]], but since this is a bit unwieldy it can also be abbreviated as [`\[`\\int main(void);\]].
           32  +* [*literals] (also known as [*code text]) can be inserted with the [`\[`int main(void);\]] syntax. note however that literals are not protected from the transmogrifier, and are parsed like any other span, which may cause problems if the source code you're quoting makes use of such forbidden runes. in this case, you'll want to wrap the code span in a raw span. the syntax for this is [`\[`[\\int main(void);\]]], but since this is a bit unwieldy it can also be abbreviated as [`\[`\\int main(void);\]].
    33     33   
    34     34   of course, this is only a small taste of what cortav can do, not even touching on key features like macros, footnotes, or equation formatting. read the sections on [>onblocks blocks] and [>onspans spans] for all the gory details.
    35     35   
    36     36   ## encoding
    37     37   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.
    38     38   
    39     39   ## file type
................................................................................
    76     76   
    77     77   ##onblocks structure
    78     78   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. hard-wrapping lines is incredibly user-hostile, especially to users on mobile devices with small screens. cortav does not allow it.
    79     79   
    80     80   the first character(s) of every line (the "control sequence") indicates the role of that line. if no control sequence is recognized, the line is treated as a paragraph. the currently supported 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.
    81     81   
    82     82   * [*paragraphs] ([`.] [` ¶] [`❡]): a paragraph is a simple block of text. the period control sequence is only necessary if the paragraph text starts with text that would be interpreted as a control sequence otherwise
    83         -* newlines [` \\]: inserts a line break into previous paragraph and attaches the following text. mostly useful for poetry or lyrics
           83  +* [*newlines] [` \\]: inserts a line break into previous paragraph and attaches the following text. mostly useful for poetry or lyrics
    84     84   * [*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.:
    85     85   ** [`#] is a simple section break.
    86     86   ** [`#anchor] opens a new section with the ID [`anchor].
    87     87   ** [`# header] opens a new section with the title "header".
    88     88   ** [`#anchor header] opens a new section with both the ID [`anchor] and the title "header".
    89     89   * [*nonprinting sections] ([`^]): sometimes, you'll want to create a namespace without actually adding a visible new section to the document. you can achieve this by creating a [!nonprinting section] and defining resources within it. nonprinting sections can also be used to store comments, notes, to-dos, or other meta-information that is useful to have in the source file without it becoming a part of the output. nonprinting sections can be used for a sort of "literate markup," where resource and reference definitions can intermingle with human-readable narrative about those definitions.
    90         -* [*resource] ([`@]): defines a [!resource]. a resource is a file or object that exists outside of the document but which will are to be included in the document somehow. common examples of resources include images, videos, iframes, or headers/footers. see [>rsrc resources] for more information.
           90  +* [*resource] ([`@]): defines a [!resource]. a resource is a file or object that is to be embedded in the document somehow. common examples of resources include images, videos, iframes, or headers/footers. resources can be defined inline, or reference external objects. see [>rsrc resources] for more information.
    91     91   * [*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 type. a blank line terminates the current list.
    92     92   * [*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 obeyed.
    93     93   * [*comments] ([`%%]): a comment is a line of text that is simply ignored by the renderer.
    94     94   * [*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:"
    95     95   * [*code] ([`~~~]): a line beginning with ~~~ begins or terminates a block of code. code blocks are by default not parsed, but parsing can be activated by preceding the code block with an [`%[*expand]] directive. the opening line should look like one of the below
    96     96   ** [`~~~]
    97     97   ** [`~~~ language] (markdown-style shorthand syntax)
................................................................................
   124    124   ##onspans styled text
   125    125   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.
   126    126   
   127    127   * strong {obj *|styled-text}: causes its text to stand out from the narrative, generally rendered as bold or a brighter color.
   128    128   * emphatic {obj !|styled-text}: indicates that its text should be spoken with emphasis, generally rendered as italics
   129    129   * custom style {span .|id|[$styled-text]}: applies a specially defined font style. for example, if you have defined [`caution] to mean "demibold italic underline", cortav will try to apply the proper weight and styling within the constraints of the current font to the span [$styled-text]. see the [>fonts-sty fonts section] for more information about this mechanism.
   130    130   * literal {obj `|styled-text}: indicates that its text is a reference to a literal sequence of characters or other discrete token. generally rendered in monospace
   131         -* variable {obj $|styled-text}: indicates that its text is a stand-in that will be replaced with what it names. generally rendered in italic monospace, ideally of a different color
          131  +* variable {obj $|styled-text}: indicates to the reader that its text is a placeholder, rather than a literal representation. generally rendered in italic monospace, ideally of a different color
   132    132   * underline {obj _|styled-text}: underlines the text. use sparingly on text intended for webpages -- underlined text  [!is] distinct from links, but underlining non-links is still a violation of convention.
   133    133   * strikeout {obj ~|styled-text}: indicates that its text should be struck through or otherwise indicated for deletion
   134    134   * insertion {obj +|styled-text}: indicates that its text should be indicated as a new addition to the text body.
   135    135   ** consider using a macro definition [`\edit: [~[#1]][+[#2]]] to save typing if you are doing editing work
   136    136   * link \[>[!ref] [!styled-text]\]: produces a hyperlink or cross-reference denoted by [$ref], which may be either a URL specified with a reference or the name of an object like an image or section elsewhere in the document. the unicode characters [`→] and [`🔗] can also be used instead of [`>] to denote a link.
   137         -* footnote {span ^|ref|[$styled-text]}: annotates the text with a defined footnote. in interactive output media [`\[^citations.qtheo Quantum Theosophy: A Neophyte's Catechism]] will insert a link with the text [`Quantum Theosophy: A Neophyte's Catechism] that, when clicked, causes a footnote to pop up on the screen. for static output media, the text will simply have a superscript integer after it denoting where the footnote is to be found.
          137  +* footnote {span ^|ref|[$styled-text]}: annotates the text with a defined footnote. in interactive output media [`\[^citations.qtheo Quantum Theosophy: A Neophyte's Catechism\]] will insert a link with the text [`Quantum Theosophy: A Neophyte's Catechism] that, when clicked, causes a footnote to pop up on the screen. for static output media, the text will simply have a superscript integer after it denoting where the footnote is to be found.
   138    138   * superscript {obj '|[$styled-text]}
   139    139   * subscript {obj ,|[$styled-text]}
   140    140   * raw {obj \\ |[$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, and backslashes.
   141    141   * raw literal \[$\\[!raw-text]\]: shorthand for [\[$[\…]]]
   142    142   * macro [` \{[!name] [!arguments]}]: invokes a [>ex.mac macro], specified with a reference
   143    143   * argument {obj #|var}: in macros only, inserts the [$var]-th argument. otherwise, inserts a context variable provided by the renderer.
   144    144   * raw argument {obj ##|var}: like above, but does not evaluate [$var].
................................................................................
   648    648   the interpreter should provide a [`cortav] table with the objects:
   649    649   * [`ctx]: contains context variables
   650    650   
   651    651   used files should return a table with the following members
   652    652   * [`macros]: an array of functions that return strings or arrays of strings when invoked. these will be injected into the global macro namespace.
   653    653   
   654    654   ###ts ts
   655         -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.
          655  +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.
   656    656   
   657         -ts enables the directives:
   658         -* [`%[*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.
          657  +[`ts] currently has no support for misinformation.
          658  +
          659  +[`ts] enables the directives:
          660  +* [`%[*ts] class [$scope level] ([$styled-text])]: indicates a classification level for either the whole 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.
   659    661   * [`%[*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].
   660    662   * [`%[*when] ts level [$level]]
   661    663   * [`%[*when] ts word [$word]]
   662    664   
   663         -ts enables the spans:
   664         -* [`\[🔒#[!level] [$styled-text]\]]: redacts the span if the security level is below that specified.
   665         -* [`\[🔒.[!word] [$styled-text]\]]: redacts the span if the specified codeword clearance is not enabled.
          665  +[`ts] enables the spans:
          666  +* [` \[🔒#[$level] [$styled-text]\]]: redacts the span if the security level is below that specified.
          667  +* [` \[🔒.[$word] [$styled-text]\]]: redacts the span if the specified codeword clearance is not enabled.
   666    668   (the padlock emoji is shorthand for [`%[*ts]].)
   667    669   
   668         -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.
          670  +[`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. redacted sections are simply omitted.
   669    671   
   670    672   ~~~#ts-example example [cortav] ~~~
   671    673   %ts word doc sorrowful-pines SORROWFUL PINES
   672    674   
   673    675   # intercept R1440 TCT S3
   674    676   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].
   675    677   
................................................................................
   829    831   
   830    832   	corran: http://ʞ.cc/fic/spirals/society
   831    833   	tengwar: https://en.wikipedia.org/wiki/Tengwar
   832    834   
   833    835   ###refimpl-switches switches
   834    836   [`cortav.lua] offers various switches to control its behavior.
   835    837   + long                      + short + function                                    +
   836         -| [`\--out [!file]]              :|:[`-o]:| sets the output file (default stdout)       |
   837         -| [`\--log [!file]]              :|:[`-l]:| sets the log file (default stderr)          |
   838         -| [`\--define [!var] [!val]]     :|:[`-d]:| sets the context variable [$var] to [$val]  |
   839         -| [`\--mode-set [!mode]]         :|:[`-y]:| activates the [>refimpl-mode mode] with ID [!mode]
   840         -| [`\--mode-clear [!mode]]       :|:[`-n]:| disables the mode with ID [!mode]           |
   841         -| [`\--mode [!id] [!val]]        :|:[`-m]:| configures mode [!id] with the value [!val] |
   842         -| [`\--mode-set-weak [!mode]]    :|:[`-Y]:| activates the [>refimpl-mode mode] with ID [!mode] if the source file does not specify otherwise
   843         -| [`\--mode-clear-weak [!mode]]  :|:[`-N]:| disables the mode with ID [$mode] if the source file does not specify otherwise
   844         -| [`\--mode-weak [!id] [!val]]   :|:[`-M]:| configures mode [$id] with the value [$val] if the source file does not specify otherwise
   845         -| [`\--help]                     :|:[`-h]:| display online help                         |
   846         -| [`\--version]                  :|:[`-V]:| display the interpreter version             |
          838  +| [`--out [$file]]              :|:[`-o]:| sets the output file (default stdout)       |
          839  +| [`--log [$file]]              :|:[`-l]:| sets the log file (default stderr)          |
          840  +| [`--define [$var] [$val]]     :|:[`-d]:| sets the context variable [$var] to [$val]  |
          841  +| [`--mode-set [$mode]]         :|:[`-y]:| activates the [>refimpl-mode mode] with ID [!mode]
          842  +| [`--mode-clear [$mode]]       :|:[`-n]:| disables the mode with ID [!mode]           |
          843  +| [`--mode [$id] [$val]]        :|:[`-m]:| configures mode [$id] with the value [$val] |
          844  +| [`--mode-set-weak [$mode]]    :|:[`-Y]:| activates the [>refimpl-mode mode] with ID [$mode] if the source file does not specify otherwise
          845  +| [`--mode-clear-weak [$mode]]  :|:[`-N]:| disables the mode with ID [$mode] if the source file does not specify otherwise
          846  +| [`--mode-weak [$id] [$val]]   :|:[`-M]:| configures mode [$id] with the value [$val] if the source file does not specify otherwise
          847  +| [`--help]                     :|:[`-h]:| display online help                         |
          848  +| [`--version]                  :|:[`-V]:| display the interpreter version             |
   847    849   
   848    850   ###refimpl-mode modes
   849    851   most of [`cortav.lua]'s implementation-specific behavior is controlled by use of [!modes]. these are namespaced options which may have a boolean, string, or numeric value. boolean modes are set with the [`-y] [`-n] flags; other modes use the [`-m] flags.
   850    852   
   851    853   most modes are defined by the renderer backend. the following modes affect the behavior of the frontend:
   852    854   
   853    855   + ID                 + type   + effect
................................................................................
   863    865   ####refimpl-rend-html-modes modes
   864    866   [`html] supports the following modes:
   865    867   
   866    868   * string (css length) [`html:width] sets a maximum width for the body content in order to make the page more readable on large displays
   867    869   * number [`html:accent] applies an accent hue to the generated webpage. the hue is specified in degrees, e.g. [$-m html:accent 0] applies a red accent.
   868    870   * flag [`html:dark-on-light] uses dark-on-light styling, instead of the default light-on-dark
   869    871   * flag [`html:fossil-uv] outputs an HTML snippet suitable for use with the Fossil VCS webserver. this is intended to be used with the unversioned content mechanism to host rendered versions of documentation written in cortav that's stored in a Fossil repository.
          872  +* flag [`html:xhtml] generates syntactically-`valid' XHTML5
          873  +* flag [`html:epub] generates XHTML5 suitable for use in an EPUB3 archive
   870    874   * number [`html:hue-spread] generates a color palette based on the supplied accent hue. the larger the value, the more the other colors diverge from the accent hue.
   871    875   * string [`html:link-css] generates a document linking to the named stylesheet
   872    876   * flag [`html:gen-styles] embeds appropriate CSS styles in the document (default on)
   873         -* flag [`html:snippet] produces a snippet of html instead of an entire web page. note that proper CSS scoping is not yet implemented (and can't be implemented hygienically since [$scoped] was removed 😢)
          877  +* flag [`html:snippet] produces a snippet of html instead of an entire web page. note that proper CSS scoping is not yet implemented (and can't be implemented hygienically since [`scoped] was removed 😢)
   874    878   * string [`html:title] specifies the webpage titlebar contents (normally autodetected from the document based on headings or directives)
   875    879   * string [`html:font] specifies the default font to use when rendering as a CSS font specification (e.g. [`-m html:font 'Alegreya, Junicode, Georgia, "Times New Roman"])
   876    880   
   877    881   ~~~
   878    882   $ cortav readme.ct --out readme.html \
   879    883   	-m render:format html \
   880    884   	-m html:width 40em \
................................................................................
  1018   1022   currently all internal colors are expressed and stored as HSL, and we're using code borrowed from my Minetest mod [`[>sorcrep sorcery]] & written by glowpelt for converting that into RGB for non-HTML outputs like groff. probably what should be done is to switch to LCH internally, accept both HSL and LCH input, and "downres" to the best representation each format can support. that's probably going to have to wait until someone who knows a bit more about color theory and math than me can do it (paging Lea Verou)
  1019   1023   
  1020   1024   	sorcrep: https://c.hale.su/sorcery
  1021   1025   
  1022   1026   ### intent files
  1023   1027   there's currently no standard way to describe the intent and desired formatting of a document besides placing pragmata in the source file itself. this is extremely suboptimal, as when generating collections of documents, it's ideal to be able to keep all formatting information in one place. users should also be able to specify their own styling overrides that describe the way they prefer to read [`cortav] files, especially for uses like gemini or gopher integration.
  1024   1028   
  1025         -at some point soon [`cortav] needs to address this by adding intent files that can be activated from outside the source file, such as with a command line flag or a configuration file setting. these will probably consist of lines that are interpreted as pragmata. in addition to the standard intent format however, individual implementations should feel free to provide their own ways to provide intent metadata; e.g. the reference implementation, which has a lua interpreter available, should be able to take a lua script that runs after the parse stage and generates . this will be particularly useful for the end-user who wishes to specify a particular format she likes reading her files in without forcing that format on everyone she sends the compiled document to, as it will be able to interrogate the document and make intelligent decisions about what pragmata to apply.
         1029  +at some point soon [`cortav] needs to address this by adding intent files that can be activated from outside the source file, such as with a command line flag or a configuration file setting. these will probably consist of lines that are interpreted as pragmata. in addition to the standard intent format however, individual implementations should feel free to provide their own ways to provide intent metadata; e.g. the reference implementation, which has a lua interpreter available, should be able to take a lua script that runs after the parse stage and makes arbitrary alterations to the AST. this will be particularly useful for the end-user who wishes to specify a particular format she likes reading her files in without forcing that format on everyone she sends the compiled document to, as it will be able to interrogate the document and make intelligent decisions about what pragmata to apply.
  1026   1030   
  1027   1031   intent files should also be able to define [>rsrc resources], [>ctxvar context variables], and macros.

Modified cortav.lua from [c52f4282f5] to [18c311a386].

   113    113   			ct.exns.tx(msg, self.src.file, self.line or 0, ...):throw()
   114    114   		end;
   115    115   		insert = function(self, block)
   116    116   			block.origin = self:clone()
   117    117   			table.insert(self.sec.blocks,block)
   118    118   			return block
   119    119   		end;
          120  +		init = function(ctx, doc, src)
          121  +			ctx.line = 0
          122  +			ctx.doc = doc
          123  +			ctx.doc.src = src
          124  +			ctx.sec = doc:mksec() -- toplevel section
          125  +			ctx.sec.origin = ctx:clone()
          126  +		end;
   120    127   		ref = function(self,id)
   121    128   			if not id:find'%.' then
   122    129   				local rid = self.sec.refs[id]
   123    130   				if self.sec.refs[id] then
   124    131   					return self.sec.refs[id], id, self.sec
   125    132   				else self:fail("no such ref %s in current section", id or '') end
   126    133   			else
................................................................................
   184    191   			end
   185    192   			return ct.ext.loaded[name].default
   186    193   		end;
   187    194   		context_var = function(self, var, ctx, test)
   188    195   			local fail = function(...)
   189    196   				if test then return false end
   190    197   				ctx:fail(...)
          198  +			end
          199  +			local scanParents = function(k)
          200  +				for k,p in pairs(self.parents) do
          201  +					local v = p:context_var(k, ctx, true)
          202  +					if v ~= false then return v end
          203  +				end
   191    204   			end
   192    205   			if startswith(var, 'cortav.') then
   193    206   				local v = var:sub(8)
   194    207   				if v == 'page' then
   195    208   					if ctx.page then return tostring(ctx.page)
   196    209   						else return '(unpaged)' end
   197    210   				elseif v == 'renderer' then
................................................................................
   217    230   				local val = os.getenv(v)
   218    231   				if not val then
   219    232   					return fail('undefined environment variable %s', v)
   220    233   				end
   221    234   			elseif self.stage.kind == 'render' and startswith(var, self.stage.format..'.') then
   222    235   				-- TODO query the renderer somehow
   223    236   				return fail('renderer %s does not implement variable %s', self.stage.format, var)
          237  +			elseif startswith(var, 'super.') then
          238  +				local sp = scanParents(var:sub(8))
          239  +				if sp == nil then
          240  +					if test then return false else return '' end
          241  +				else
          242  +					return sp
          243  +				end
   224    244   			elseif self.vars[var] then
   225    245   				return self.vars[var]
   226    246   			else
          247  +				local sp = scanParents(var)
          248  +				if sp then return sp end
   227    249   				if test then return false end
   228    250   				return '' -- is this desirable behavior?
   229    251   			end
   230    252   		end;
   231    253   		job = function(self, name, pred, ...) -- convenience func
   232    254   			return self.docjob:fork(name, pred, ...)
   233         -		end
          255  +		end;
          256  +		sub = function(self, ctx)
          257  +			-- convenience function for single-inheritance structure
          258  +			-- sets up a doc/ctx pair for a subdocument embedded in the source
          259  +			-- of a gretaer document, pointing subdoc props to global tables/values
          260  +			local newdoc = ct.doc.mk(self)
          261  +			newdoc.meta = self.meta
          262  +			newdoc.ext = self.ext
          263  +			newdoc.enc = self.enc
          264  +			newdoc.stage = self.stage
          265  +			-- vars are handled through proper recursion across all parents and
          266  +			-- are intentionally excluded here; subdocs can have their own vars
          267  +			-- without losing access to parent vars
          268  +			local nctx = ctx:clone()
          269  +			nctx:init(newdoc, ctx.src)
          270  +			nctx.line = ctx.line
          271  +			return newdoc, nctx
          272  +		end;
   234    273   	};
   235         -	mk = function() return {
          274  +	mk = function(...) return {
   236    275   		sections = {};
   237    276   		secorder = {};
   238    277   		embed = {};
   239    278   		meta = {};
   240    279   		vars = {};
          280  +		parents = {...};
   241    281   		ext = {
   242    282   			inhibit = {};
   243    283   			need = {};
   244    284   			use = {};
   245    285   		};
   246    286   		enc = ss.str.enc.utf8;
   247    287   	} end;
................................................................................
   594    634   				cmd = cmd;
   595    635   				args = args;
   596    636   				crit = crit;
   597    637   				failthru = failthru;
   598    638   				spans = spans;
   599    639   			}
   600    640   		end
          641  +	end
          642  +	local function rawcode(s, c) -- raw
          643  +		local o = c:clone();
          644  +		local str = ''
          645  +		for c, p in ss.str.each(c.doc.enc, s) do
          646  +			local q = p:esc()
          647  +			if q then
          648  +				str = str ..  q
          649  +				p.next.byte = p.next.byte + #q
          650  +			else
          651  +				str = str .. c
          652  +			end
          653  +		end
          654  +		return {
          655  +			kind = 'format';
          656  +			style = 'literal';
          657  +			spans = {{
          658  +				kind = 'raw';
          659  +				spans = {str};
          660  +				origin = o;
          661  +			}};
          662  +			origin = o;
          663  +		}
   601    664   	end
   602    665   	ct.spanctls = {
   603    666   		{seq = '!', parse = formatter 'emph'};
   604    667   		{seq = '*', parse = formatter 'strong'};
   605    668   		{seq = '~', parse = formatter 'strike'};
   606    669   		{seq = '+', parse = formatter 'insert'};
          670  +		{seq = '`\\', parse = rawcode};
          671  +		{seq = '\\\\', parse = rawcode};
   607    672   		{seq = '\\', parse = function(s, c) -- raw
   608    673   			return {
   609    674   				kind = 'raw';
   610    675   				spans = {s};
   611    676   				origin = c:clone();
   612    677   			}
   613         -		end};
   614         -		{seq = '`\\', parse = function(s, c) -- raw
   615         -			local o = c:clone();
   616         -			local str = ''
   617         -			for c, p in ss.str.each(c.doc.enc, s) do
   618         -				local q = p:esc()
   619         -				if q then
   620         -					str = str ..  q
   621         -					p.next.byte = p.next.byte + #q
   622         -				else
   623         -					str = str .. c
   624         -				end
   625         -			end
   626         -			return {
   627         -				kind = 'format';
   628         -				style = 'literal';
   629         -				spans = {{
   630         -					kind = 'raw';
   631         -					spans = {str};
   632         -					origin = o;
   633         -				}};
   634         -				origin = o;
   635         -			}
   636    678   		end};
   637    679   		{seq = '`', parse = formatter 'literal'};
   638    680   		{seq = '$', parse = formatter 'variable'};
   639    681   		{seq = '^', parse = function(s,c) --footnotes
   640    682   			local r, t = s:match '^([^%s]+)%s*(.-)$'
   641    683   			return {
   642    684   				kind = 'footnote';
................................................................................
   984   1026   			local sp = ct.parse_span(txt, c)
   985   1027   			c.doc.docjob:hook('meddle_span', sp, last)
   986   1028   			table.insert(last.lines, sp)
   987   1029   			j:hook('block_aside_attach', c, last, sp, l)
   988   1030   			j:hook('block_aside_line_insert', c, last, sp, l)
   989   1031   		end
   990   1032   	end};
   991         -	{pred = function(s,c) return s:match'^[*:]' end, fn = blockwrap(function(l,c) -- list
         1033  +	{pred = function(s,c) return s:match'^[*:]' end,
         1034  +	 fn   = blockwrap(function(l,c) -- list
   992   1035   		local stars = l:match '^([*:]+)'
   993   1036   		local depth = utf8.len(stars)
   994   1037   		local id, txt = l:sub(#stars+1):match '^(.-)%s*(.-)$'
   995   1038   		local ordered = stars:sub(#stars) == ':'
   996   1039   		if id == '' then id = nil end
   997   1040   		return {
   998   1041   			kind = 'list-item';
................................................................................
  1073   1116   					c:fail('extension %s does not support critical directive %s', cmd, topcmd)
  1074   1117   				end
  1075   1118   			end
  1076   1119   		elseif crit == '!' then
  1077   1120   			c:fail('critical directive %s not supported',cmd)
  1078   1121   		end
  1079   1122   	end;};
         1123  +	{pred = function(s) return s:match '^(>+)([^%s]*)%s*(.*)$' end,
         1124  +	 fn   = function(l,c,j,d)
         1125  +		local lvl,id,txt = l:match '^(>+)([^%s]*)%s*(.*)$'
         1126  +		lvl = utf8.len(lvl)
         1127  +		local last = d[#d]
         1128  +		local node
         1129  +		local ctx
         1130  +		if last and last.kind == 'quote' and (id == nil or id == '' or id == last.id) then
         1131  +			node = last
         1132  +			ctx = node.ctx
         1133  +			ctx.line = c.line -- is this enough??
         1134  +		else
         1135  +			local doc
         1136  +			doc, ctx = c.doc:sub(c)
         1137  +			node = { kind = 'quote', doc = doc, ctx = ctx, id = id }
         1138  +			j:hook('block_insert', c, node, l)
         1139  +			table.insert(d, node)
         1140  +		end
         1141  +
         1142  +		ct.parse_line(txt, ctx, ctx.sec.blocks)
         1143  +	end};
  1080   1144   	{seq = '~~~', fn = blockwrap(function(l,c,j)
  1081   1145   		local extract = function(ptn, str)
  1082   1146   			local start, stop = str:find(ptn)
  1083   1147   			if not start then return nil, str end
  1084   1148   			local ex = str:sub(start,stop)
  1085   1149   			local n = str:sub(1,start-1) .. str:sub(stop+1)
  1086   1150   			return ex, n
................................................................................
  1167   1231   				if ctx.mode.expand
  1168   1232   					then newline = ct.parse_span(l, ctx)
  1169   1233   					else newline = {l}
  1170   1234   				end
  1171   1235   				table.insert(ctx.mode.listing.lines, newline)
  1172   1236   				job:hook('block_listing_newline',ctx,ctx.mode.listing,newline)
  1173   1237   			end
  1174         -	  else
         1238  +		elseif ctx.mode.kind == 'quote' then
         1239  +		else
  1175   1240   			local mf = job:proc('modes', ctx.mode.kind)
  1176   1241   			if not mf then
  1177   1242   				ctx:fail('unimplemented syntax mode %s', ctx.mode.kind)
  1178   1243   			end
  1179   1244   			mf(job, ctx, l, dest) --NOTE: you are responsible for triggering the appropriate hooks if you insert anything!
  1180   1245   		end
  1181   1246   	else
................................................................................
  1190   1255   				end
  1191   1256   				return false
  1192   1257   			end
  1193   1258   
  1194   1259   			if not tryseqs(ct.ctlseqs) then
  1195   1260   				local found = false
  1196   1261   
  1197         -				for eb, ext, state in job:each('blocks') do
         1262  +				for eb, ext, state in job:each 'blocks' do
  1198   1263   					if tryseqs(eb, state) then found = true break end
  1199   1264   				end
  1200   1265   
  1201   1266   				if not found then
  1202   1267   					ctx:fail 'incomprehensible input line'
  1203   1268   				end
  1204   1269   			end
................................................................................
  1214   1279   end
  1215   1280   
  1216   1281   function ct.parse(file, src, mode, setup)
  1217   1282   	-- this object is threaded down through the parse tree
  1218   1283   	-- and copied to store information like the origin of the
  1219   1284   	-- element in the source code
  1220   1285   	local ctx = ct.ctx.mk(src)
  1221         -	ctx.line = 0
  1222         -	ctx.doc = ct.doc.mk()
  1223         -	ctx.doc.src = src
  1224         -	ctx.sec = ctx.doc:mksec() -- toplevel section
  1225         -	ctx.sec.origin = ctx:clone()
         1286  +	ctx:init(ct.doc.mk(), src)
  1226   1287   	ctx.lang = mode['meta:lang']
  1227   1288   	if mode['parse:enc'] then
  1228   1289   		local e = ss.str.enc[mode['parse:enc']]
  1229   1290   		if not e then
  1230   1291   			ct.exns.enc('requested encoding not supported',mode['parse:enc']):throw()
  1231   1292   		end
  1232   1293   		ctx.doc.enc = e

Modified desk/cortav.xml from [d0eba8f9ef] to [bda20d41cd].

   109    109   
   110    110   			<context name='span-del' attribute='Deleted Text' lineEndContext='#pop'>
   111    111   				<IncludeRules context='span'/>
   112    112   			</context>
   113    113   
   114    114   			<context name='span-cue' attribute='Span Cue' lineEndContext='#pop' fallthroughContext="error">
   115    115   				<StringDetect attribute='Span Cue' String='`\' context='#pop!flat-span' />
          116  +				<StringDetect attribute='Span Cue' String='\\' context='#pop!flat-span' />
   116    117   
   117    118   				<DetectChar   attribute='Span Cue' char='!' context='#pop!span-emph' />
   118    119   				<DetectChar   attribute='Span Cue' char='*' context='#pop!span-strong' />
   119    120   				<DetectChar   attribute='Span Cue' char='~' context='#pop!span-del' />
   120    121   
   121    122   				<AnyChar      attribute='Span Cue' String='`$+🔒' context='#pop!span' />
   122    123   				<StringDetect attribute='Span Cue' String='→' context='#pop!ref' />
................................................................................
   129    130   				<DetectChar   attribute='Span Cue' char='\' context='#pop!flat-span' />
   130    131   				<Detect2Chars attribute='Comment' char='%' char1='%' context='#pop!inline-comment' />
   131    132   				<Detect2Chars attribute='Critical Directive Cue' char='%' char1='!' context='#pop!inline-directive' />
   132    133   				<DetectChar   attribute='Directive Cue' char='%' context='#pop!inline-directive' />
   133    134   			</context>
   134    135   
   135    136   			<context name='flat-span' attribute='Unstyled Text' lineEndContext='#pop'>
          137  +				<DetectChar attribute='Unstyled Text' context='flat-span' char='['/>
   136    138   				<Detect2Chars attribute='Escaped Char' context='#stay' char='\' char1=']'/>
   137    139   				<DetectChar attribute='Span Delimiter' context='#pop' char=']'/>
   138    140   			</context>
   139    141   
   140    142   			<context name='inline-comment' attribute='Comment' lineEndContext='#pop'>
   141    143   				<IncludeRules context='flat-span'/>
   142    144   			</context>

Modified ext/transmogrify.lua from [ffa0ca0a64] to [ad59e4c740].

    37     37   			['(SM)'] = '℠';
    38     38   		};
    39     39      };
    40     40   }
    41     41   
    42     42   local quotes = {
    43     43   	[ss.str.enc.utf8] = {
    44         -		['en'] = {'“', '”'; '‘', '’'};
    45         -		['de'] = {'„', '“'; '‚', '‘'};
    46         -		['sp'] = {'«', '»'; '‹', '›'};
    47         -		['ja'] = {'「', '」'; '『', '』'};
    48         -		['fr'] = {'« ', ' »'; '‹ ', ' ›'};
    49         -		[true] = {'“', '”'; '‘', '’'};
           44  +		-- 5 = elision char
           45  +		['en'] = {'“', '”';  '‘', '’';  '’'};
           46  +		['de'] = {'„', '“';  '‚', '‘';  '’'};
           47  +		['sp'] = {'«', '»';  '‹', '›';  "’"};
           48  +		['ja'] = {'「', '」'; '『', '』'; "'"};
           49  +		['fr'] = {'« ',' »'; '‹ ',' ›';  "’"};
           50  +		[true] = {'“', '”';  '‘', '’';  '’'};
    50     51   	};
    51     52   }
    52     53   
    53     54   local function meddle(ctx, t)
    54     55   	local pts = patterns[ctx.doc.enc]
    55     56   	if not pts then return t end
    56     57   	local str = ''
................................................................................
   139    140   	version = ss.version {0,1; 'devel'};
   140    141   	contributors = {{name='lexi hale', handle='velartrill', mail='lexi@hale.su', homepage='https://hale.su'}};
   141    142   	default = true; -- on unless inhibited
   142    143   	slow = true;
   143    144   	hook = {
   144    145   		doc_meddle_ast = function(job)
   145    146   			for n, sec in pairs(job.doc.secorder) do
   146         -				if sec.kind=='ordinary' or sec.kind=='blockquote'
          147  +				if sec.kind=='ordinary' or sec.kind=='quote'
   147    148   				or sec.kind=='footnote' then
   148    149   					for i, block in pairs(sec.blocks) do
   149    150   			         if type(block.spans) == 'table' then
   150    151   							enterspan(block.origin, block.spans)
   151    152   						elseif type(block.spans) == 'string' then
   152    153   							block.spans = meddle(block.origin, block.spans)
   153    154   						end
   154    155   					end
   155    156   				end
   156    157   			end
   157    158   		end;
   158    159   	};
   159    160   }

Modified render/html.lua from [39c7338664] to [5903337619].

    13     13   function ct.render.html(doc, opts, setup)
    14     14   	local doctitle = opts['title']
    15     15   	local f = string.format
    16     16   	local getSafeID = ct.tool.namespace()
    17     17   
    18     18   	local footnotes = {}
    19     19   	local footnotecount = 0
           20  +
           21  +	local cdata = function(...) return ... end
           22  +	if opts.epub then
           23  +		opts.xhtml = true
           24  +	end
           25  +
           26  +	if opts.xhtml then
           27  +		cdata = function(s)
           28  +			return '<![CDATA[' .. s .. ']]>'
           29  +		end
           30  +	end
    20     31   
    21     32   	local langsused = {}
    22     33   	local langpairs = {
    23     34   		lua = { color = 0x9377ff };
    24     35   		terra = { color = 0xff77c8 };
    25     36   		c = { name = 'C', color = 0x77ffe8 };
    26     37   		html = { color = 0xfff877 };
................................................................................
    44     55   			li {
    45     56   				padding: 0.1em 0;
    46     57   			}
    47     58   		]];
    48     59   		list_ordered = [[]];
    49     60   		list_unordered = [[]];
    50     61   		footnote = [[
    51         -			div.footnote {
           62  +			aside.footnote {
    52     63   				font-family: 90%;
    53     64   				grid-template-columns: 1em 1fr min-content;
    54     65   				grid-template-rows: 1fr min-content;
    55     66   				position: fixed;
    56     67   				padding: 1em;
    57     68   				background: @tone(0.03);
    58     69   				margin:auto;
    59     70   			}
    60     71   			@media screen {
    61         -				div.footnote {
           72  +				aside.footnote {
    62     73   					display: grid;
    63     74   					left: 10em;
    64     75   					right: 10em;
    65     76   					max-width: calc(@width + 2em);
    66     77   					max-height: 30vw;
    67     78   					bottom: 1em;
    68     79   					border: 1px solid black;
    69     80   					transform: translateY(200%);
    70     81   					transition: 0.4s;
    71     82   					z-index: 100;
    72     83   				}
    73         -				div.footnote:target {
           84  +				aside.footnote:target {
    74     85   					transform: translateY(0%);
    75     86   				}
    76     87   				#cover {
    77     88   					position: fixed;
    78     89   					top: 0;
    79     90   					left: 0;
    80     91   					height: 100vh; width: 100vw;
................................................................................
    82     93   						@tone/0.8(-0.07),
    83     94   						@tone/0.4(-0.07));
    84     95   					opacity: 0%;
    85     96   					transition: 1s;
    86     97   					pointer-events: none;
    87     98   					backdrop-filter: blur(0px);
    88     99   				}
    89         -				div.footnote:target ~ #cover {
          100  +				aside.footnote:target ~ #cover {
    90    101   					opacity: 100%;
    91    102   					pointer-events: all;
    92    103   					backdrop-filter: blur(5px);
    93    104   				}
    94    105   			}
    95    106   			@media print {
    96         -				div.footnote {
          107  +				aside.footnote {
    97    108   					display: grid;
    98    109   					position: relative;
    99    110   				}
   100         -				div.footnote:first-of-type {
          111  +				aside.footnote:first-of-type {
   101    112   					border-top: 1px solid black;
   102    113   				}
   103    114   			}
   104    115   
   105         -			div.footnote > a[href="#0"]{
          116  +			aside.footnote > a[href="#0"]{
   106    117   				grid-row: 2/3;
   107    118   				grid-column: 3/4;
   108    119   				display: block;
   109    120   				text-align: center;
   110    121   				padding: 0 0.3em;
   111    122   				text-decoration: none;
   112    123   				background: @tone(0.2);
................................................................................
   116    127   				font-size: 150%;
   117    128   				-webkit-user-select: none;
   118    129   				-ms-user-select: none;
   119    130   				user-select: none;
   120    131   				-webkit-user-drag: none;
   121    132   				user-drag: none;
   122    133   			}
   123         -			div.footnote > a[href="#0"]:hover {
          134  +			aside.footnote > a[href="#0"]:hover {
   124    135   				background: @tone(0.3);
   125    136   				color: @tone(2);
   126    137   			}
   127         -			div.footnote > a[href="#0"]:active {
          138  +			aside.footnote > a[href="#0"]:active {
   128    139   				background: @tone(0.05);
   129    140   				color: @tone(0.4);
   130    141   			}
   131    142   			@media print {
   132         -				div.footnote > a[href="#0"]{
          143  +				aside.footnote > a[href="#0"]{
   133    144   					display:none;
   134    145   				}
   135    146   			}
   136         -			div.footnote > div.number {
          147  +			aside.footnote > div.number {
   137    148   				text-align:right;
   138    149   				grid-row: 1/2;
   139    150   				grid-column: 1/2;
   140    151   			}
   141         -			div.footnote > div.text {
          152  +			aside.footnote > div.text {
   142    153   				grid-row: 1/2;
   143    154   				grid-column: 2/4;
   144    155   				padding-left: 1em;
   145    156   				overflow-y: auto;
   146    157   			}
   147         -			div.footnote > div.text > p:first-child {
          158  +			aside.footnote > div.text > p:first-child {
   148    159   				margin-top: 0;
   149    160   			}
   150    161   		]];
   151    162   		header = [[
   152    163   			body { padding: 0 2.5em !important }
   153    164   			h1,h2,h3,h4,h5,h6 { border-bottom: 1px solid @tone(0.7); }
   154    165   			h1 { font-size: 200%; border-bottom-style: double !important; border-bottom-width: 3px !important; margin: 0em -1em; }
................................................................................
   161    172   			h1,h2,h3,h4,h5,h6 {
   162    173   				margin-top: 0;
   163    174   				margin-bottom: 0;
   164    175   			}
   165    176   			:is(h1,h2,h3,h4,h5,h6) + p {
   166    177   				margin-top: 0.4em;
   167    178   			}
   168         -
   169    179   		]];
   170    180   		headingAnchors = [[
   171    181   			:is(h1,h2,h3,h4,h5,h6) > a[href].anchor {
   172    182   				text-decoration: none;
   173    183   				font-size: 1.2em;
   174    184   				padding: 0.3em;
   175    185   				opacity: 0%;
................................................................................
   251    261   			}
   252    262   			section > aside p {
   253    263   				margin: 0;
   254    264   				margin-top: 0.6em;
   255    265   			}
   256    266   			section > aside p:first-child {
   257    267   				margin: 0;
          268  +			}
          269  +         section aside + aside {
          270  +				margin-top: 0.5em;
   258    271   			}
   259    272         ]];
   260    273   		code = [[
   261    274   			code {
   262    275   				display: inline-block;
   263    276   				background: @tone(-1);
   264    277   				color: @tone(0.7);
................................................................................
   436    449   	doc.stage.job = renderJob;
   437    450   
   438    451   	local runhook = function(h, ...)
   439    452   		return renderJob:hook(h, render_state_handle, ...)
   440    453   	end
   441    454   
   442    455   	local tagproc do
   443         -		local elt = function(t,attrs)
   444         -			return f('<%s%s>', t,
   445         -				attrs and ss.reduce(function(a,b) return a..b end, '',
          456  +		local html_open = function(t,attrs)
          457  +			if attrs then
          458  +				return t .. ss.reduce(function(a,b) return a..b end, '',
   446    459   					ss.map(function(v,k)
   447    460   						if v == true
   448    461   							then          return ' '..k
   449    462   							elseif v then return f(' %s="%s"', k, v)
   450    463   						end
   451         -					end, attrs)) or '')
          464  +					end, attrs))
          465  +			else return t end
          466  +		end
          467  +
          468  +		local elt = function(t,attrs)
          469  +			if opts.xhtml then
          470  +				return f('<%s />', html_open(t,attrs))
          471  +			end
          472  +			return f('<%s>', html_open(t,attrs))
   452    473   		end
   453    474   
   454    475   		tagproc = {
   455    476   			toTXT = {
   456    477   				tag = function(t,a,v) return v  end;
   457    478   				elt = function(t,a)   return '' end;
   458    479   				catenate = table.concat;
................................................................................
   468    489   				} end;
   469    490   
   470    491   				catenate = function(...) return ... end;
   471    492   			};
   472    493   			toHTML = {
   473    494   				elt = elt;
   474    495   				tag = function(t,attrs,body)
   475         -					return f('%s%s</%s>', elt(t,attrs), body, t)
          496  +					return f('<%s>%s</%s>', html_open(t,attrs), body, t)
   476    497   				end;
   477    498   				catenate = table.concat;
   478    499   			};
   479    500   		}
   480    501   	end
   481    502   
   482    503   	local function getBaseRenderers(procs, span_renderers)
   483    504   		local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
   484    505   		local htmlDoc = function(title, head, body)
   485         -			return [[<!doctype html>]] .. tag('html',nil,
          506  +			local attrs
          507  +			local header = [[<!doctype html>]]
          508  +			if opts['epub'] then
          509  +				-- so cursed
          510  +				attrs = {
          511  +					xmlns = "http://www.w3.org/1999/xhtml";
          512  +					['xmlns:epub'] = "http://www.idpf.org/2007/ops";
          513  +				}
          514  +				header = [[<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE html>]]
          515  +			end
          516  +			return header .. tag('html',attrs,
   486    517   				tag('head', nil,
   487         -					elt('meta',{charset = 'utf-8'}) ..
          518  +					(opts.epub and '' or elt('meta',{charset = 'utf-8'})) ..
   488    519   					(title and tag('title', nil, title) or '') ..
   489    520   					(head or '')) ..
   490    521   				tag('body', nil, body or ''))
   491    522   		end
   492    523   
   493    524   		local function htmlSpan(spans, block, sec)
   494    525   			local text = {}
................................................................................
   645    676   			elseif d.crit then
   646    677   				b.origin:fail('critical extension %s unavailable', d.ext)
   647    678   			elseif d.failthru then
   648    679   				return htmlSpan(d.spans, b, s)
   649    680   			end
   650    681   		end
   651    682   		function span_renderers.footnote(f,b,s)
   652         -			addStyle 'footnote'
          683  +			local linkattr = {}
          684  +			if opts.epub then
          685  +				linkattr['epub:type'] = 'noteref'
          686  +			else
          687  +				addStyle 'footnote'
          688  +			end
   653    689   			local source, sid, ssec = b.origin:ref(f.ref)
   654    690   			local cnc = getSafeID(ssec) .. ' ' .. sid
   655    691   			local fn
   656    692   			if footnotes[cnc] then
   657    693   				fn = footnotes[cnc]
   658    694   			else
   659    695   				footnotecount = footnotecount + 1
   660    696   				fn = {num = footnotecount, origin = b.origin, fnid=cnc, source = source}
   661    697   				fn.id = getSafeID(fn)
   662    698   				footnotes[cnc] = fn
   663    699   			end
   664         -			return tag('a', {href='#'..fn.id}, htmlSpan(f.spans) ..
          700  +			linkattr.href = '#'..fn.id
          701  +			return tag('a', linkattr, htmlSpan(f.spans) ..
   665    702   						tag('sup',nil, fn.num))
   666    703   		end
   667    704   
   668    705   		return span_renderers
   669    706   	end
          707  +
          708  +	local astproc
   670    709   
   671    710   	local function getBlockRenderers(procs, sr)
   672    711   		local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
   673    712   		local null = function() return catenate{} end
   674    713   
   675    714   		local block_renderers = {
   676    715   			anchor = function(b,s)
................................................................................
   746    785   				return tag('aside', {}, bn)
   747    786   			end;
   748    787   			['break'] = function() -- HACK
   749    788   				-- lists need to be rewritten to work like asides
   750    789   				return '';
   751    790   			end;
   752    791   		}
          792  +
          793  +		function block_renderers.quote(b,s)
          794  +			local ir = {}
          795  +			local toIR = block_renderers
          796  +			for i, sec in ipairs(b.doc.secorder) do
          797  +				local secnodes = {}
          798  +				for i, bl in ipairs(sec.blocks) do
          799  +					if toIR[bl.kind] then
          800  +						table.insert(secnodes, toIR[bl.kind](bl,sec))
          801  +					end
          802  +				end
          803  +				if next(secnodes) then
          804  +					if b.doc.secorder[2] then --#secs>1?
          805  +						-- only wrap in a section if >1 section
          806  +						table.insert(ir, tag('section',
          807  +													{id = getSafeID(sec)},
          808  +													secnodes))
          809  +					else
          810  +						ir = secnodes
          811  +					end
          812  +				end
          813  +			end
          814  +			return tag('blockquote', b.id and {id=getSafeID(b)} or {}, catenate(ir))
          815  +		end
          816  +
   753    817   		return block_renderers;
   754    818   	end
   755    819   
   756    820   	local function getRenderers(procs)
   757    821   		local span_renderers = getSpanRenderers(procs)
   758    822   		local r = getBaseRenderers(procs,span_renderers)
   759    823   		r.block_renderers = getBlockRenderers(procs, r)
   760    824   		return r
   761    825   	end
   762    826   
   763         -	local astproc = {
          827  +	astproc = {
   764    828   		toHTML = getRenderers(tagproc.toHTML);
   765    829   		toTXT  = getRenderers(tagproc.toTXT);
   766    830   		toIR   = { };
   767    831   	}
   768    832   	astproc.toIR.span_renderers = ss.clone(astproc.toHTML);
   769    833   	astproc.toIR.block_renderers = getBlockRenderers(tagproc.toIR,astproc.toHTML);
   770    834   		-- note we use HTML here instead of IR span renderers, because as things
................................................................................
   776    840   	render_state_handle.tagproc = tagproc;
   777    841   
   778    842   	-- bind to legacy names
   779    843   	-- yikes this needs to be cleaned up so badly
   780    844   	local ir = {}
   781    845   	local dr = astproc.toHTML -- default renderers
   782    846   	local plainr = astproc.toTXT
   783         -	local irBlockRdrs = astproc.toIR.block_renderers;
   784    847   
   785    848   	render_state_handle.ir = ir;
   786    849   
   787    850   	local function renderBlocks(blocks, irs)
   788    851   		for i, block in ipairs(blocks) do
   789    852   			local rd
   790         -			if irBlockRdrs[block.kind] then
   791         -				rd = irBlockRdrs[block.kind](block,sec)
          853  +			if astproc.toIR.block_renderers[block.kind] then
          854  +				rd = astproc.toIR.block_renderers[block.kind](block,sec)
   792    855   			else
   793    856   				local rdr = renderJob:proc('render',block.kind,'html')
   794    857   				if rdr then
   795    858   					rd = rdr({
   796    859   						state = render_state_handle;
   797    860   						tagproc = tagproc.toIR;
   798    861   						astproc = astproc.toIR;
................................................................................
   814    877   					rd.attrs.lang = rd.src.origin.lang
   815    878   				end
   816    879   				table.insert(irs.nodes, rd)
   817    880   				runhook('ir_section_node_insert', rd, irs, sec)
   818    881   			end
   819    882   		end
   820    883   	end
          884  +
   821    885   	runhook('ir_assemble', ir)
   822    886   	for i, sec in ipairs(doc.secorder) do
   823    887   		if doctitle == nil and sec.depth == 1 and sec.heading_node then
   824    888   			doctitle = astproc.toTXT.htmlSpan(sec.heading_node.spans, sec.heading_node, sec)
   825    889   		end
   826    890   		local irs
   827    891   		if sec.kind == 'ordinary' then
   828    892   			if #(sec.blocks) > 0 then
   829    893   				irs = {tag='section',attrs={id = getSafeID(sec)},nodes={}}
   830    894   				runhook('ir_section_build', irs, sec)
   831    895   				renderBlocks(sec.blocks, irs)
   832    896   			end
   833         -		elseif sec.kind == 'blockquote' then
          897  +		elseif sec.kind == 'quote' then
   834    898   		elseif sec.kind == 'listing' then
   835    899   		elseif sec.kind == 'embed' then
   836    900   		end
   837    901   		if irs then table.insert(ir, irs) end
   838    902   	end
   839    903   
   840         -	for _, fn in pairs(footnotes) do
   841         -		local tag = tagproc.toIR.tag
   842         -		local body = {nodes={}}
   843         -		local ftir = {}
   844         -		for l in fn.source:gmatch('([^\n]*)') do
   845         -			ct.parse_line(l, fn.origin, ftir)
          904  +	do local fnsorted = {}
          905  +		for _, fn in pairs(footnotes) do
          906  +			fnsorted[fn.num] = fn
          907  +		end
          908  +
          909  +		for _, fn in ipairs(fnsorted) do
          910  +			local tag = tagproc.toIR.tag
          911  +			local body = {nodes={}}
          912  +			local ftir = {}
          913  +			for l in fn.source:gmatch('([^\n]*)') do
          914  +				ct.parse_line(l, fn.origin, ftir)
          915  +			end
          916  +			renderBlocks(ftir,body)
          917  +			local fattr = {id=fn.id}
          918  +			if opts.epub then
          919  +				---UUUUUUGHHH
          920  +				local npfx = string.format('(%u) ', fn.num)
          921  +				if next(body.nodes) then
          922  +					local n = body.nodes[1]
          923  +					repeat
          924  +						if n.nodes[1] then
          925  +							if type(n.nodes[1]) == 'string' then
          926  +								n.nodes[1] = npfx .. n.nodes[1]
          927  +								break
          928  +							end
          929  +							n = n.nodes[1]
          930  +						else
          931  +							n.nodes[1] = {tag='p',attrs={},nodes={npfx}}
          932  +							break
          933  +						end
          934  +					until false
          935  +
          936  +				else
          937  +					body.nodes[1] = {tag='p',attrs={},nodes={npfx}}
          938  +				end
          939  +				fattr['epub:type'] = 'footnote'
          940  +			else
          941  +				fattr.class = 'footnote'
          942  +			end
          943  +			local note = tag('aside', fattr, opts.epub and body.nodes or {
          944  +				tag('div',{class='number'}, tostring(fn.num)),
          945  +				tag('div',{class='text'}, body.nodes),
          946  +				tag('a',{href='#0'},'⤫')
          947  +			})
          948  +			table.insert(ir, note)
   846    949   		end
   847         -		renderBlocks(ftir,body)
   848         -		local note = tag('div',{class='footnote',id=fn.id}, {
   849         -			tag('div',{class='number'}, tostring(fn.num)),
   850         -			tag('div',{class='text'}, body.nodes),
   851         -			tag('a',{href='#0'},'⤫')
   852         -		})
   853         -		table.insert(ir, note)
   854    950   	end
   855         -	if next(footnotes) then
          951  +	if next(footnotes) and not opts.epub then
   856    952   		table.insert(ir, tagproc.toIR.tag('div',{id='cover'},''))
   857    953   	end
   858    954   
   859    955   	-- restructure passes
   860    956   	runhook('ir_restructure_pre', ir)
   861    957   
   862    958   	---- list insertion pass
................................................................................
  1012   1108   	local styles = {}
  1013   1109   	if opts.width then
  1014   1110   		table.insert(styles, string.format([[body {padding:0 1em;margin:auto;max-width:%s}]], opts.width))
  1015   1111   	end
  1016   1112   	if opts.accent then
  1017   1113   		table.insert(styles, string.format(':root {--accent:%s}', opts.accent))
  1018   1114   	end
  1019         -	if opts.accent or (not opts['dark-on-light']) and (not opts['fossil-uv']) then
         1115  +	if not opts.epub and (opts.accent or (not opts['dark-on-light']) and (not opts['fossil-uv'])) then
  1020   1116   		addStyle 'accent'
  1021   1117   	end
  1022   1118   
  1023   1119   
  1024   1120   	for _,k in pairs(stylesNeeded.order) do
  1025   1121   		if not stylesets[k] then ct.exns.unimpl('styleset %s not implemented (!)',  k):throw() end
  1026   1122   		table.insert(styles, prepcss(stylesets[k]))
................................................................................
  1031   1127   	if opts['link-css'] then
  1032   1128   		local css = opts['link-css']
  1033   1129   		if type(css) ~= 'string' then ct.exns.mode('must be a string', 'html:link-css'):throw() end
  1034   1130   		styletag = styletag .. tagproc.toHTML.elt('link',{rel='stylesheet',type='text/css',href=opts['link-css']})
  1035   1131   	end
  1036   1132   	if next(styles) then
  1037   1133   		if opts['gen-styles'] then
  1038         -			styletag = styletag .. tagproc.toHTML.tag('style',{type='text/css'},table.concat(styles))
         1134  +			styletag = styletag .. tagproc.toHTML.tag('style',{type='text/css'},cdata(table.concat(styles)))
  1039   1135   		end
  1040   1136   		table.insert(head, styletag)
  1041   1137   	end
  1042   1138   
  1043   1139   	if opts['fossil-uv'] then
  1044   1140   		return tagproc.toHTML.tag('div',{class='fossil-doc',['data-title']=doctitle},styletag .. body)
  1045   1141   	elseif opts.snippet then
  1046   1142   		return styletag .. body
  1047   1143   	else
  1048   1144   		return dr.htmlDoc(doctitle, next(head) and table.concat(head), body)
  1049   1145   	end
  1050   1146   end