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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
..
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
...
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
...
648
649
650
651
652
653
654
655
656


657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
...
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
...
863
864
865
866
867
868
869


870
871
872
873
874
875
876
877
878
879
880
....
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
* [*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.
* [*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).
* 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.
* [*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!
* [*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)
* [*images] are a bit more complicated, but much more versatile. see the section on [>rsrc resources] for an explanation.
* [*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 ([`→]).
* [*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);\]].

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.

## encoding
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.

## file type
................................................................................

##onblocks structure
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.

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.

* [*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
* newlines [` \\]: inserts a line break into previous paragraph and attaches the following text. mostly useful for poetry or lyrics
* [*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.:
** [`#] is a simple section break.
** [`#anchor] opens a new section with the ID [`anchor].
** [`# header] opens a new section with the title "header".
** [`#anchor header] opens a new section with both the ID [`anchor] and the title "header".
* [*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.
* [*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.
* [*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.
* [*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.
* [*comments] ([`%%]): a comment is a line of text that is simply ignored by the renderer.
* [*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:"
* [*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
** [`~~~]
** [`~~~ language] (markdown-style shorthand syntax)
................................................................................
##onspans styled text
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.

* strong {obj *|styled-text}: causes its text to stand out from the narrative, generally rendered as bold or a brighter color.
* emphatic {obj !|styled-text}: indicates that its text should be spoken with emphasis, generally rendered as italics
* 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.
* 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
* 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
* 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.
* strikeout {obj ~|styled-text}: indicates that its text should be struck through or otherwise indicated for deletion
* insertion {obj +|styled-text}: indicates that its text should be indicated as a new addition to the text body.
** consider using a macro definition [`\edit: [~[#1]][+[#2]]] to save typing if you are doing editing work
* 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.
* 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.
* superscript {obj '|[$styled-text]}
* subscript {obj ,|[$styled-text]}
* 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.
* raw literal \[$\\[!raw-text]\]: shorthand for [\[$[\…]]]
* macro [` \{[!name] [!arguments]}]: invokes a [>ex.mac macro], specified with a reference
* argument {obj #|var}: in macros only, inserts the [$var]-th argument. otherwise, inserts a context variable provided by the renderer.
* raw argument {obj ##|var}: like above, but does not evaluate [$var].
................................................................................
the interpreter should provide a [`cortav] table with the objects:
* [`ctx]: contains context variables

used files should return a table with the following members
* [`macros]: an array of functions that return strings or arrays of strings when invoked. these will be injected into the global macro namespace.

###ts ts
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.



ts enables the directives:
* [`%[*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.
* [`%[*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].
* [`%[*when] ts level [$level]]
* [`%[*when] ts word [$word]]

ts enables the spans:
* [`\[🔒#[!level] [$styled-text]\]]: redacts the span if the security level is below that specified.
* [`\[🔒.[!word] [$styled-text]\]]: redacts the span if the specified codeword clearance is not enabled.
(the padlock emoji is shorthand for [`%[*ts]].)

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.

~~~#ts-example example [cortav] ~~~
%ts word doc sorrowful-pines SORROWFUL PINES

# intercept R1440 TCT S3
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].

................................................................................

	corran: http://ʞ.cc/fic/spirals/society
	tengwar: https://en.wikipedia.org/wiki/Tengwar

###refimpl-switches switches
[`cortav.lua] offers various switches to control its behavior.
+ long                      + short + function                                    +
| [`\--out [!file]]              :|:[`-o]:| sets the output file (default stdout)       |
| [`\--log [!file]]              :|:[`-l]:| sets the log file (default stderr)          |
| [`\--define [!var] [!val]]     :|:[`-d]:| sets the context variable [$var] to [$val]  |
| [`\--mode-set [!mode]]         :|:[`-y]:| activates the [>refimpl-mode mode] with ID [!mode]
| [`\--mode-clear [!mode]]       :|:[`-n]:| disables the mode with ID [!mode]           |
| [`\--mode [!id] [!val]]        :|:[`-m]:| configures mode [!id] with the value [!val] |
| [`\--mode-set-weak [!mode]]    :|:[`-Y]:| activates the [>refimpl-mode mode] with ID [!mode] if the source file does not specify otherwise
| [`\--mode-clear-weak [!mode]]  :|:[`-N]:| disables the mode with ID [$mode] if the source file does not specify otherwise
| [`\--mode-weak [!id] [!val]]   :|:[`-M]:| configures mode [$id] with the value [$val] if the source file does not specify otherwise
| [`\--help]                     :|:[`-h]:| display online help                         |
| [`\--version]                  :|:[`-V]:| display the interpreter version             |

###refimpl-mode modes
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.

most modes are defined by the renderer backend. the following modes affect the behavior of the frontend:

+ ID                 + type   + effect
................................................................................
####refimpl-rend-html-modes modes
[`html] supports the following modes:

* string (css length) [`html:width] sets a maximum width for the body content in order to make the page more readable on large displays
* 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.
* flag [`html:dark-on-light] uses dark-on-light styling, instead of the default light-on-dark
* 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.


* 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.
* string [`html:link-css] generates a document linking to the named stylesheet
* flag [`html:gen-styles] embeds appropriate CSS styles in the document (default on)
* 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 😢)
* string [`html:title] specifies the webpage titlebar contents (normally autodetected from the document based on headings or directives)
* 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"])

~~~
$ cortav readme.ct --out readme.html \
	-m render:format html \
	-m html:width 40em \
................................................................................
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)

	sorcrep: https://c.hale.su/sorcery

### intent files
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.

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.

intent files should also be able to define [>rsrc resources], [>ctxvar context variables], and macros.







|







 







|






|







 







|





|







 







|

>
>
|
|




|
|
|


|







 







|
|
|
|
|
|
|
|
|
|
|







 







>
>



|







 







|


25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
..
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
...
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
...
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
...
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
...
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
....
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
* [*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.
* [*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).
* 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.
* [*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!
* [*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)
* [*images] are a bit more complicated, but much more versatile. see the section on [>rsrc resources] for an explanation.
* [*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 ([`→]).
* [*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);\]].

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.

## encoding
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.

## file type
................................................................................

##onblocks structure
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.

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.

* [*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
* [*newlines] [` \\]: inserts a line break into previous paragraph and attaches the following text. mostly useful for poetry or lyrics
* [*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.:
** [`#] is a simple section break.
** [`#anchor] opens a new section with the ID [`anchor].
** [`# header] opens a new section with the title "header".
** [`#anchor header] opens a new section with both the ID [`anchor] and the title "header".
* [*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.
* [*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.
* [*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.
* [*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.
* [*comments] ([`%%]): a comment is a line of text that is simply ignored by the renderer.
* [*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:"
* [*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
** [`~~~]
** [`~~~ language] (markdown-style shorthand syntax)
................................................................................
##onspans styled text
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.

* strong {obj *|styled-text}: causes its text to stand out from the narrative, generally rendered as bold or a brighter color.
* emphatic {obj !|styled-text}: indicates that its text should be spoken with emphasis, generally rendered as italics
* 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.
* 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
* 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
* 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.
* strikeout {obj ~|styled-text}: indicates that its text should be struck through or otherwise indicated for deletion
* insertion {obj +|styled-text}: indicates that its text should be indicated as a new addition to the text body.
** consider using a macro definition [`\edit: [~[#1]][+[#2]]] to save typing if you are doing editing work
* 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.
* 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.
* superscript {obj '|[$styled-text]}
* subscript {obj ,|[$styled-text]}
* 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.
* raw literal \[$\\[!raw-text]\]: shorthand for [\[$[\…]]]
* macro [` \{[!name] [!arguments]}]: invokes a [>ex.mac macro], specified with a reference
* argument {obj #|var}: in macros only, inserts the [$var]-th argument. otherwise, inserts a context variable provided by the renderer.
* raw argument {obj ##|var}: like above, but does not evaluate [$var].
................................................................................
the interpreter should provide a [`cortav] table with the objects:
* [`ctx]: contains context variables

used files should return a table with the following members
* [`macros]: an array of functions that return strings or arrays of strings when invoked. these will be injected into the global macro namespace.

###ts ts
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.

[`ts] currently has no support for misinformation.

[`ts] enables the directives:
* [`%[*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.
* [`%[*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].
* [`%[*when] ts level [$level]]
* [`%[*when] ts word [$word]]

[`ts] enables the spans:
* [` \[🔒#[$level] [$styled-text]\]]: redacts the span if the security level is below that specified.
* [` \[🔒.[$word] [$styled-text]\]]: redacts the span if the specified codeword clearance is not enabled.
(the padlock emoji is shorthand for [`%[*ts]].)

[`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.

~~~#ts-example example [cortav] ~~~
%ts word doc sorrowful-pines SORROWFUL PINES

# intercept R1440 TCT S3
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].

................................................................................

	corran: http://ʞ.cc/fic/spirals/society
	tengwar: https://en.wikipedia.org/wiki/Tengwar

###refimpl-switches switches
[`cortav.lua] offers various switches to control its behavior.
+ long                      + short + function                                    +
| [`--out [$file]]              :|:[`-o]:| sets the output file (default stdout)       |
| [`--log [$file]]              :|:[`-l]:| sets the log file (default stderr)          |
| [`--define [$var] [$val]]     :|:[`-d]:| sets the context variable [$var] to [$val]  |
| [`--mode-set [$mode]]         :|:[`-y]:| activates the [>refimpl-mode mode] with ID [!mode]
| [`--mode-clear [$mode]]       :|:[`-n]:| disables the mode with ID [!mode]           |
| [`--mode [$id] [$val]]        :|:[`-m]:| configures mode [$id] with the value [$val] |
| [`--mode-set-weak [$mode]]    :|:[`-Y]:| activates the [>refimpl-mode mode] with ID [$mode] if the source file does not specify otherwise
| [`--mode-clear-weak [$mode]]  :|:[`-N]:| disables the mode with ID [$mode] if the source file does not specify otherwise
| [`--mode-weak [$id] [$val]]   :|:[`-M]:| configures mode [$id] with the value [$val] if the source file does not specify otherwise
| [`--help]                     :|:[`-h]:| display online help                         |
| [`--version]                  :|:[`-V]:| display the interpreter version             |

###refimpl-mode modes
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.

most modes are defined by the renderer backend. the following modes affect the behavior of the frontend:

+ ID                 + type   + effect
................................................................................
####refimpl-rend-html-modes modes
[`html] supports the following modes:

* string (css length) [`html:width] sets a maximum width for the body content in order to make the page more readable on large displays
* 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.
* flag [`html:dark-on-light] uses dark-on-light styling, instead of the default light-on-dark
* 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.
* flag [`html:xhtml] generates syntactically-`valid' XHTML5
* flag [`html:epub] generates XHTML5 suitable for use in an EPUB3 archive
* 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.
* string [`html:link-css] generates a document linking to the named stylesheet
* flag [`html:gen-styles] embeds appropriate CSS styles in the document (default on)
* 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 😢)
* string [`html:title] specifies the webpage titlebar contents (normally autodetected from the document based on headings or directives)
* 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"])

~~~
$ cortav readme.ct --out readme.html \
	-m render:format html \
	-m html:width 40em \
................................................................................
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)

	sorcrep: https://c.hale.su/sorcery

### intent files
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.

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.

intent files should also be able to define [>rsrc resources], [>ctxvar context variables], and macros.

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

113
114
115
116
117
118
119







120
121
122
123
124
125
126
...
184
185
186
187
188
189
190






191
192
193
194
195
196
197
...
217
218
219
220
221
222
223







224
225
226


227
228
229
230
231
232
233

















234
235
236
237
238
239
240

241
242
243
244
245
246
247
...
594
595
596
597
598
599
600























601
602
603
604
605
606


607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
...
984
985
986
987
988
989
990
991

992
993
994
995
996
997
998
....
1073
1074
1075
1076
1077
1078
1079





















1080
1081
1082
1083
1084
1085
1086
....
1167
1168
1169
1170
1171
1172
1173

1174
1175
1176
1177
1178
1179
1180
1181
....
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
....
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
			ct.exns.tx(msg, self.src.file, self.line or 0, ...):throw()
		end;
		insert = function(self, block)
			block.origin = self:clone()
			table.insert(self.sec.blocks,block)
			return block
		end;







		ref = function(self,id)
			if not id:find'%.' then
				local rid = self.sec.refs[id]
				if self.sec.refs[id] then
					return self.sec.refs[id], id, self.sec
				else self:fail("no such ref %s in current section", id or '') end
			else
................................................................................
			end
			return ct.ext.loaded[name].default
		end;
		context_var = function(self, var, ctx, test)
			local fail = function(...)
				if test then return false end
				ctx:fail(...)






			end
			if startswith(var, 'cortav.') then
				local v = var:sub(8)
				if v == 'page' then
					if ctx.page then return tostring(ctx.page)
						else return '(unpaged)' end
				elseif v == 'renderer' then
................................................................................
				local val = os.getenv(v)
				if not val then
					return fail('undefined environment variable %s', v)
				end
			elseif self.stage.kind == 'render' and startswith(var, self.stage.format..'.') then
				-- TODO query the renderer somehow
				return fail('renderer %s does not implement variable %s', self.stage.format, var)







			elseif self.vars[var] then
				return self.vars[var]
			else


				if test then return false end
				return '' -- is this desirable behavior?
			end
		end;
		job = function(self, name, pred, ...) -- convenience func
			return self.docjob:fork(name, pred, ...)
		end

















	};
	mk = function() return {
		sections = {};
		secorder = {};
		embed = {};
		meta = {};
		vars = {};

		ext = {
			inhibit = {};
			need = {};
			use = {};
		};
		enc = ss.str.enc.utf8;
	} end;
................................................................................
				cmd = cmd;
				args = args;
				crit = crit;
				failthru = failthru;
				spans = spans;
			}
		end























	end
	ct.spanctls = {
		{seq = '!', parse = formatter 'emph'};
		{seq = '*', parse = formatter 'strong'};
		{seq = '~', parse = formatter 'strike'};
		{seq = '+', parse = formatter 'insert'};


		{seq = '\\', parse = function(s, c) -- raw
			return {
				kind = 'raw';
				spans = {s};
				origin = c:clone();
			}
		end};
		{seq = '`\\', parse = function(s, c) -- raw
			local o = c:clone();
			local str = ''
			for c, p in ss.str.each(c.doc.enc, s) do
				local q = p:esc()
				if q then
					str = str ..  q
					p.next.byte = p.next.byte + #q
				else
					str = str .. c
				end
			end
			return {
				kind = 'format';
				style = 'literal';
				spans = {{
					kind = 'raw';
					spans = {str};
					origin = o;
				}};
				origin = o;
			}
		end};
		{seq = '`', parse = formatter 'literal'};
		{seq = '$', parse = formatter 'variable'};
		{seq = '^', parse = function(s,c) --footnotes
			local r, t = s:match '^([^%s]+)%s*(.-)$'
			return {
				kind = 'footnote';
................................................................................
			local sp = ct.parse_span(txt, c)
			c.doc.docjob:hook('meddle_span', sp, last)
			table.insert(last.lines, sp)
			j:hook('block_aside_attach', c, last, sp, l)
			j:hook('block_aside_line_insert', c, last, sp, l)
		end
	end};
	{pred = function(s,c) return s:match'^[*:]' end, fn = blockwrap(function(l,c) -- list

		local stars = l:match '^([*:]+)'
		local depth = utf8.len(stars)
		local id, txt = l:sub(#stars+1):match '^(.-)%s*(.-)$'
		local ordered = stars:sub(#stars) == ':'
		if id == '' then id = nil end
		return {
			kind = 'list-item';
................................................................................
					c:fail('extension %s does not support critical directive %s', cmd, topcmd)
				end
			end
		elseif crit == '!' then
			c:fail('critical directive %s not supported',cmd)
		end
	end;};





















	{seq = '~~~', fn = blockwrap(function(l,c,j)
		local extract = function(ptn, str)
			local start, stop = str:find(ptn)
			if not start then return nil, str end
			local ex = str:sub(start,stop)
			local n = str:sub(1,start-1) .. str:sub(stop+1)
			return ex, n
................................................................................
				if ctx.mode.expand
					then newline = ct.parse_span(l, ctx)
					else newline = {l}
				end
				table.insert(ctx.mode.listing.lines, newline)
				job:hook('block_listing_newline',ctx,ctx.mode.listing,newline)
			end

	  else
			local mf = job:proc('modes', ctx.mode.kind)
			if not mf then
				ctx:fail('unimplemented syntax mode %s', ctx.mode.kind)
			end
			mf(job, ctx, l, dest) --NOTE: you are responsible for triggering the appropriate hooks if you insert anything!
		end
	else
................................................................................
				end
				return false
			end

			if not tryseqs(ct.ctlseqs) then
				local found = false

				for eb, ext, state in job:each('blocks') do
					if tryseqs(eb, state) then found = true break end
				end

				if not found then
					ctx:fail 'incomprehensible input line'
				end
			end
................................................................................
end

function ct.parse(file, src, mode, setup)
	-- this object is threaded down through the parse tree
	-- and copied to store information like the origin of the
	-- element in the source code
	local ctx = ct.ctx.mk(src)
	ctx.line = 0
	ctx.doc = ct.doc.mk()
	ctx.doc.src = src
	ctx.sec = ctx.doc:mksec() -- toplevel section
	ctx.sec.origin = ctx:clone()
	ctx.lang = mode['meta:lang']
	if mode['parse:enc'] then
		local e = ss.str.enc[mode['parse:enc']]
		if not e then
			ct.exns.enc('requested encoding not supported',mode['parse:enc']):throw()
		end
		ctx.doc.enc = e







>
>
>
>
>
>
>







 







>
>
>
>
>
>







 







>
>
>
>
>
>
>



>
>






|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

|





>







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>






>
>






<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







 







|
>







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







>
|







 







|







 







<
|
<
<
<







113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
...
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
...
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
...
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677























678
679
680
681
682
683
684
....
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
....
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
....
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
....
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
....
1279
1280
1281
1282
1283
1284
1285

1286



1287
1288
1289
1290
1291
1292
1293
			ct.exns.tx(msg, self.src.file, self.line or 0, ...):throw()
		end;
		insert = function(self, block)
			block.origin = self:clone()
			table.insert(self.sec.blocks,block)
			return block
		end;
		init = function(ctx, doc, src)
			ctx.line = 0
			ctx.doc = doc
			ctx.doc.src = src
			ctx.sec = doc:mksec() -- toplevel section
			ctx.sec.origin = ctx:clone()
		end;
		ref = function(self,id)
			if not id:find'%.' then
				local rid = self.sec.refs[id]
				if self.sec.refs[id] then
					return self.sec.refs[id], id, self.sec
				else self:fail("no such ref %s in current section", id or '') end
			else
................................................................................
			end
			return ct.ext.loaded[name].default
		end;
		context_var = function(self, var, ctx, test)
			local fail = function(...)
				if test then return false end
				ctx:fail(...)
			end
			local scanParents = function(k)
				for k,p in pairs(self.parents) do
					local v = p:context_var(k, ctx, true)
					if v ~= false then return v end
				end
			end
			if startswith(var, 'cortav.') then
				local v = var:sub(8)
				if v == 'page' then
					if ctx.page then return tostring(ctx.page)
						else return '(unpaged)' end
				elseif v == 'renderer' then
................................................................................
				local val = os.getenv(v)
				if not val then
					return fail('undefined environment variable %s', v)
				end
			elseif self.stage.kind == 'render' and startswith(var, self.stage.format..'.') then
				-- TODO query the renderer somehow
				return fail('renderer %s does not implement variable %s', self.stage.format, var)
			elseif startswith(var, 'super.') then
				local sp = scanParents(var:sub(8))
				if sp == nil then
					if test then return false else return '' end
				else
					return sp
				end
			elseif self.vars[var] then
				return self.vars[var]
			else
				local sp = scanParents(var)
				if sp then return sp end
				if test then return false end
				return '' -- is this desirable behavior?
			end
		end;
		job = function(self, name, pred, ...) -- convenience func
			return self.docjob:fork(name, pred, ...)
		end;
		sub = function(self, ctx)
			-- convenience function for single-inheritance structure
			-- sets up a doc/ctx pair for a subdocument embedded in the source
			-- of a gretaer document, pointing subdoc props to global tables/values
			local newdoc = ct.doc.mk(self)
			newdoc.meta = self.meta
			newdoc.ext = self.ext
			newdoc.enc = self.enc
			newdoc.stage = self.stage
			-- vars are handled through proper recursion across all parents and
			-- are intentionally excluded here; subdocs can have their own vars
			-- without losing access to parent vars
			local nctx = ctx:clone()
			nctx:init(newdoc, ctx.src)
			nctx.line = ctx.line
			return newdoc, nctx
		end;
	};
	mk = function(...) return {
		sections = {};
		secorder = {};
		embed = {};
		meta = {};
		vars = {};
		parents = {...};
		ext = {
			inhibit = {};
			need = {};
			use = {};
		};
		enc = ss.str.enc.utf8;
	} end;
................................................................................
				cmd = cmd;
				args = args;
				crit = crit;
				failthru = failthru;
				spans = spans;
			}
		end
	end
	local function rawcode(s, c) -- raw
		local o = c:clone();
		local str = ''
		for c, p in ss.str.each(c.doc.enc, s) do
			local q = p:esc()
			if q then
				str = str ..  q
				p.next.byte = p.next.byte + #q
			else
				str = str .. c
			end
		end
		return {
			kind = 'format';
			style = 'literal';
			spans = {{
				kind = 'raw';
				spans = {str};
				origin = o;
			}};
			origin = o;
		}
	end
	ct.spanctls = {
		{seq = '!', parse = formatter 'emph'};
		{seq = '*', parse = formatter 'strong'};
		{seq = '~', parse = formatter 'strike'};
		{seq = '+', parse = formatter 'insert'};
		{seq = '`\\', parse = rawcode};
		{seq = '\\\\', parse = rawcode};
		{seq = '\\', parse = function(s, c) -- raw
			return {
				kind = 'raw';
				spans = {s};
				origin = c:clone();
			}























		end};
		{seq = '`', parse = formatter 'literal'};
		{seq = '$', parse = formatter 'variable'};
		{seq = '^', parse = function(s,c) --footnotes
			local r, t = s:match '^([^%s]+)%s*(.-)$'
			return {
				kind = 'footnote';
................................................................................
			local sp = ct.parse_span(txt, c)
			c.doc.docjob:hook('meddle_span', sp, last)
			table.insert(last.lines, sp)
			j:hook('block_aside_attach', c, last, sp, l)
			j:hook('block_aside_line_insert', c, last, sp, l)
		end
	end};
	{pred = function(s,c) return s:match'^[*:]' end,
	 fn   = blockwrap(function(l,c) -- list
		local stars = l:match '^([*:]+)'
		local depth = utf8.len(stars)
		local id, txt = l:sub(#stars+1):match '^(.-)%s*(.-)$'
		local ordered = stars:sub(#stars) == ':'
		if id == '' then id = nil end
		return {
			kind = 'list-item';
................................................................................
					c:fail('extension %s does not support critical directive %s', cmd, topcmd)
				end
			end
		elseif crit == '!' then
			c:fail('critical directive %s not supported',cmd)
		end
	end;};
	{pred = function(s) return s:match '^(>+)([^%s]*)%s*(.*)$' end,
	 fn   = function(l,c,j,d)
		local lvl,id,txt = l:match '^(>+)([^%s]*)%s*(.*)$'
		lvl = utf8.len(lvl)
		local last = d[#d]
		local node
		local ctx
		if last and last.kind == 'quote' and (id == nil or id == '' or id == last.id) then
			node = last
			ctx = node.ctx
			ctx.line = c.line -- is this enough??
		else
			local doc
			doc, ctx = c.doc:sub(c)
			node = { kind = 'quote', doc = doc, ctx = ctx, id = id }
			j:hook('block_insert', c, node, l)
			table.insert(d, node)
		end

		ct.parse_line(txt, ctx, ctx.sec.blocks)
	end};
	{seq = '~~~', fn = blockwrap(function(l,c,j)
		local extract = function(ptn, str)
			local start, stop = str:find(ptn)
			if not start then return nil, str end
			local ex = str:sub(start,stop)
			local n = str:sub(1,start-1) .. str:sub(stop+1)
			return ex, n
................................................................................
				if ctx.mode.expand
					then newline = ct.parse_span(l, ctx)
					else newline = {l}
				end
				table.insert(ctx.mode.listing.lines, newline)
				job:hook('block_listing_newline',ctx,ctx.mode.listing,newline)
			end
		elseif ctx.mode.kind == 'quote' then
		else
			local mf = job:proc('modes', ctx.mode.kind)
			if not mf then
				ctx:fail('unimplemented syntax mode %s', ctx.mode.kind)
			end
			mf(job, ctx, l, dest) --NOTE: you are responsible for triggering the appropriate hooks if you insert anything!
		end
	else
................................................................................
				end
				return false
			end

			if not tryseqs(ct.ctlseqs) then
				local found = false

				for eb, ext, state in job:each 'blocks' do
					if tryseqs(eb, state) then found = true break end
				end

				if not found then
					ctx:fail 'incomprehensible input line'
				end
			end
................................................................................
end

function ct.parse(file, src, mode, setup)
	-- this object is threaded down through the parse tree
	-- and copied to store information like the origin of the
	-- element in the source code
	local ctx = ct.ctx.mk(src)

	ctx:init(ct.doc.mk(), src)



	ctx.lang = mode['meta:lang']
	if mode['parse:enc'] then
		local e = ss.str.enc[mode['parse:enc']]
		if not e then
			ct.exns.enc('requested encoding not supported',mode['parse:enc']):throw()
		end
		ctx.doc.enc = e

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

109
110
111
112
113
114
115

116
117
118
119
120
121
122
...
129
130
131
132
133
134
135

136
137
138
139
140
141
142

			<context name='span-del' attribute='Deleted Text' lineEndContext='#pop'>
				<IncludeRules context='span'/>
			</context>

			<context name='span-cue' attribute='Span Cue' lineEndContext='#pop' fallthroughContext="error">
				<StringDetect attribute='Span Cue' String='`\' context='#pop!flat-span' />


				<DetectChar   attribute='Span Cue' char='!' context='#pop!span-emph' />
				<DetectChar   attribute='Span Cue' char='*' context='#pop!span-strong' />
				<DetectChar   attribute='Span Cue' char='~' context='#pop!span-del' />

				<AnyChar      attribute='Span Cue' String='`$+🔒' context='#pop!span' />
				<StringDetect attribute='Span Cue' String='→' context='#pop!ref' />
................................................................................
				<DetectChar   attribute='Span Cue' char='\' context='#pop!flat-span' />
				<Detect2Chars attribute='Comment' char='%' char1='%' context='#pop!inline-comment' />
				<Detect2Chars attribute='Critical Directive Cue' char='%' char1='!' context='#pop!inline-directive' />
				<DetectChar   attribute='Directive Cue' char='%' context='#pop!inline-directive' />
			</context>

			<context name='flat-span' attribute='Unstyled Text' lineEndContext='#pop'>

				<Detect2Chars attribute='Escaped Char' context='#stay' char='\' char1=']'/>
				<DetectChar attribute='Span Delimiter' context='#pop' char=']'/>
			</context>

			<context name='inline-comment' attribute='Comment' lineEndContext='#pop'>
				<IncludeRules context='flat-span'/>
			</context>







>







 







>







109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
...
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144

			<context name='span-del' attribute='Deleted Text' lineEndContext='#pop'>
				<IncludeRules context='span'/>
			</context>

			<context name='span-cue' attribute='Span Cue' lineEndContext='#pop' fallthroughContext="error">
				<StringDetect attribute='Span Cue' String='`\' context='#pop!flat-span' />
				<StringDetect attribute='Span Cue' String='\\' context='#pop!flat-span' />

				<DetectChar   attribute='Span Cue' char='!' context='#pop!span-emph' />
				<DetectChar   attribute='Span Cue' char='*' context='#pop!span-strong' />
				<DetectChar   attribute='Span Cue' char='~' context='#pop!span-del' />

				<AnyChar      attribute='Span Cue' String='`$+🔒' context='#pop!span' />
				<StringDetect attribute='Span Cue' String='→' context='#pop!ref' />
................................................................................
				<DetectChar   attribute='Span Cue' char='\' context='#pop!flat-span' />
				<Detect2Chars attribute='Comment' char='%' char1='%' context='#pop!inline-comment' />
				<Detect2Chars attribute='Critical Directive Cue' char='%' char1='!' context='#pop!inline-directive' />
				<DetectChar   attribute='Directive Cue' char='%' context='#pop!inline-directive' />
			</context>

			<context name='flat-span' attribute='Unstyled Text' lineEndContext='#pop'>
				<DetectChar attribute='Unstyled Text' context='flat-span' char='['/>
				<Detect2Chars attribute='Escaped Char' context='#stay' char='\' char1=']'/>
				<DetectChar attribute='Span Delimiter' context='#pop' char=']'/>
			</context>

			<context name='inline-comment' attribute='Comment' lineEndContext='#pop'>
				<IncludeRules context='flat-span'/>
			</context>

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

37
38
39
40
41
42
43

44
45
46
47
48
49
50
51
52
53
54
55
56
...
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
			['(SM)'] = '℠';
		};
   };
}

local quotes = {
	[ss.str.enc.utf8] = {

		['en'] = {'“', '”'; '‘', '’'};
		['de'] = {'„', '“'; '‚', '‘'};
		['sp'] = {'«', '»'; '‹', '›'};
		['ja'] = {'「', '」'; '『', '』'};
		['fr'] = {'« ', ' »'; '‹ ', ' ›'};
		[true] = {'“', '”'; '‘', '’'};
	};
}

local function meddle(ctx, t)
	local pts = patterns[ctx.doc.enc]
	if not pts then return t end
	local str = ''
................................................................................
	version = ss.version {0,1; 'devel'};
	contributors = {{name='lexi hale', handle='velartrill', mail='lexi@hale.su', homepage='https://hale.su'}};
	default = true; -- on unless inhibited
	slow = true;
	hook = {
		doc_meddle_ast = function(job)
			for n, sec in pairs(job.doc.secorder) do
				if sec.kind=='ordinary' or sec.kind=='blockquote'
				or sec.kind=='footnote' then
					for i, block in pairs(sec.blocks) do
			         if type(block.spans) == 'table' then
							enterspan(block.origin, block.spans)
						elseif type(block.spans) == 'string' then
							block.spans = meddle(block.origin, block.spans)
						end
					end
				end
			end
		end;
	};
}







>
|
|
|
|
|
|







 







|













37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
...
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
			['(SM)'] = '℠';
		};
   };
}

local quotes = {
	[ss.str.enc.utf8] = {
		-- 5 = elision char
		['en'] = {'“', '”';  '‘', '’';  '’'};
		['de'] = {'„', '“';  '‚', '‘';  '’'};
		['sp'] = {'«', '»';  '‹', '›';  "’"};
		['ja'] = {'「', '」'; '『', '』'; "'"};
		['fr'] = {'« ',' »'; '‹ ',' ›';  "’"};
		[true] = {'“', '”';  '‘', '’';  '’'};
	};
}

local function meddle(ctx, t)
	local pts = patterns[ctx.doc.enc]
	if not pts then return t end
	local str = ''
................................................................................
	version = ss.version {0,1; 'devel'};
	contributors = {{name='lexi hale', handle='velartrill', mail='lexi@hale.su', homepage='https://hale.su'}};
	default = true; -- on unless inhibited
	slow = true;
	hook = {
		doc_meddle_ast = function(job)
			for n, sec in pairs(job.doc.secorder) do
				if sec.kind=='ordinary' or sec.kind=='quote'
				or sec.kind=='footnote' then
					for i, block in pairs(sec.blocks) do
			         if type(block.spans) == 'table' then
							enterspan(block.origin, block.spans)
						elseif type(block.spans) == 'string' then
							block.spans = meddle(block.origin, block.spans)
						end
					end
				end
			end
		end;
	};
}

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

13
14
15
16
17
18
19











20
21
22
23
24
25
26
..
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
..
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
...
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
...
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
...
251
252
253
254
255
256
257



258
259
260
261
262
263
264
...
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451








452
453
454
455
456
457
458
...
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484

485









486
487
488
489
490
491
492
493
494
...
645
646
647
648
649
650
651




652

653
654
655
656
657
658
659
660
661
662
663

664
665
666
667
668
669


670
671
672
673
674
675
676
...
746
747
748
749
750
751
752

























753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
...
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
...
814
815
816
817
818
819
820

821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839

840




841
842
843
844
845
846
847
848



























849
850
851
852
853
854

855
856
857
858
859
860
861
862
....
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
....
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
function ct.render.html(doc, opts, setup)
	local doctitle = opts['title']
	local f = string.format
	local getSafeID = ct.tool.namespace()

	local footnotes = {}
	local footnotecount = 0












	local langsused = {}
	local langpairs = {
		lua = { color = 0x9377ff };
		terra = { color = 0xff77c8 };
		c = { name = 'C', color = 0x77ffe8 };
		html = { color = 0xfff877 };
................................................................................
			li {
				padding: 0.1em 0;
			}
		]];
		list_ordered = [[]];
		list_unordered = [[]];
		footnote = [[
			div.footnote {
				font-family: 90%;
				grid-template-columns: 1em 1fr min-content;
				grid-template-rows: 1fr min-content;
				position: fixed;
				padding: 1em;
				background: @tone(0.03);
				margin:auto;
			}
			@media screen {
				div.footnote {
					display: grid;
					left: 10em;
					right: 10em;
					max-width: calc(@width + 2em);
					max-height: 30vw;
					bottom: 1em;
					border: 1px solid black;
					transform: translateY(200%);
					transition: 0.4s;
					z-index: 100;
				}
				div.footnote:target {
					transform: translateY(0%);
				}
				#cover {
					position: fixed;
					top: 0;
					left: 0;
					height: 100vh; width: 100vw;
................................................................................
						@tone/0.8(-0.07),
						@tone/0.4(-0.07));
					opacity: 0%;
					transition: 1s;
					pointer-events: none;
					backdrop-filter: blur(0px);
				}
				div.footnote:target ~ #cover {
					opacity: 100%;
					pointer-events: all;
					backdrop-filter: blur(5px);
				}
			}
			@media print {
				div.footnote {
					display: grid;
					position: relative;
				}
				div.footnote:first-of-type {
					border-top: 1px solid black;
				}
			}

			div.footnote > a[href="#0"]{
				grid-row: 2/3;
				grid-column: 3/4;
				display: block;
				text-align: center;
				padding: 0 0.3em;
				text-decoration: none;
				background: @tone(0.2);
................................................................................
				font-size: 150%;
				-webkit-user-select: none;
				-ms-user-select: none;
				user-select: none;
				-webkit-user-drag: none;
				user-drag: none;
			}
			div.footnote > a[href="#0"]:hover {
				background: @tone(0.3);
				color: @tone(2);
			}
			div.footnote > a[href="#0"]:active {
				background: @tone(0.05);
				color: @tone(0.4);
			}
			@media print {
				div.footnote > a[href="#0"]{
					display:none;
				}
			}
			div.footnote > div.number {
				text-align:right;
				grid-row: 1/2;
				grid-column: 1/2;
			}
			div.footnote > div.text {
				grid-row: 1/2;
				grid-column: 2/4;
				padding-left: 1em;
				overflow-y: auto;
			}
			div.footnote > div.text > p:first-child {
				margin-top: 0;
			}
		]];
		header = [[
			body { padding: 0 2.5em !important }
			h1,h2,h3,h4,h5,h6 { border-bottom: 1px solid @tone(0.7); }
			h1 { font-size: 200%; border-bottom-style: double !important; border-bottom-width: 3px !important; margin: 0em -1em; }
................................................................................
			h1,h2,h3,h4,h5,h6 {
				margin-top: 0;
				margin-bottom: 0;
			}
			:is(h1,h2,h3,h4,h5,h6) + p {
				margin-top: 0.4em;
			}

		]];
		headingAnchors = [[
			:is(h1,h2,h3,h4,h5,h6) > a[href].anchor {
				text-decoration: none;
				font-size: 1.2em;
				padding: 0.3em;
				opacity: 0%;
................................................................................
			}
			section > aside p {
				margin: 0;
				margin-top: 0.6em;
			}
			section > aside p:first-child {
				margin: 0;



			}
      ]];
		code = [[
			code {
				display: inline-block;
				background: @tone(-1);
				color: @tone(0.7);
................................................................................
	doc.stage.job = renderJob;

	local runhook = function(h, ...)
		return renderJob:hook(h, render_state_handle, ...)
	end

	local tagproc do
		local elt = function(t,attrs)
			return f('<%s%s>', t,
				attrs and ss.reduce(function(a,b) return a..b end, '',
					ss.map(function(v,k)
						if v == true
							then          return ' '..k
							elseif v then return f(' %s="%s"', k, v)
						end
					end, attrs)) or '')








		end

		tagproc = {
			toTXT = {
				tag = function(t,a,v) return v  end;
				elt = function(t,a)   return '' end;
				catenate = table.concat;
................................................................................
				} end;

				catenate = function(...) return ... end;
			};
			toHTML = {
				elt = elt;
				tag = function(t,attrs,body)
					return f('%s%s</%s>', elt(t,attrs), body, t)
				end;
				catenate = table.concat;
			};
		}
	end

	local function getBaseRenderers(procs, span_renderers)
		local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
		local htmlDoc = function(title, head, body)

			return [[<!doctype html>]] .. tag('html',nil,









				tag('head', nil,
					elt('meta',{charset = 'utf-8'}) ..
					(title and tag('title', nil, title) or '') ..
					(head or '')) ..
				tag('body', nil, body or ''))
		end

		local function htmlSpan(spans, block, sec)
			local text = {}
................................................................................
			elseif d.crit then
				b.origin:fail('critical extension %s unavailable', d.ext)
			elseif d.failthru then
				return htmlSpan(d.spans, b, s)
			end
		end
		function span_renderers.footnote(f,b,s)




			addStyle 'footnote'

			local source, sid, ssec = b.origin:ref(f.ref)
			local cnc = getSafeID(ssec) .. ' ' .. sid
			local fn
			if footnotes[cnc] then
				fn = footnotes[cnc]
			else
				footnotecount = footnotecount + 1
				fn = {num = footnotecount, origin = b.origin, fnid=cnc, source = source}
				fn.id = getSafeID(fn)
				footnotes[cnc] = fn
			end

			return tag('a', {href='#'..fn.id}, htmlSpan(f.spans) ..
						tag('sup',nil, fn.num))
		end

		return span_renderers
	end



	local function getBlockRenderers(procs, sr)
		local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
		local null = function() return catenate{} end

		local block_renderers = {
			anchor = function(b,s)
................................................................................
				return tag('aside', {}, bn)
			end;
			['break'] = function() -- HACK
				-- lists need to be rewritten to work like asides
				return '';
			end;
		}

























		return block_renderers;
	end

	local function getRenderers(procs)
		local span_renderers = getSpanRenderers(procs)
		local r = getBaseRenderers(procs,span_renderers)
		r.block_renderers = getBlockRenderers(procs, r)
		return r
	end

	local astproc = {
		toHTML = getRenderers(tagproc.toHTML);
		toTXT  = getRenderers(tagproc.toTXT);
		toIR   = { };
	}
	astproc.toIR.span_renderers = ss.clone(astproc.toHTML);
	astproc.toIR.block_renderers = getBlockRenderers(tagproc.toIR,astproc.toHTML);
		-- note we use HTML here instead of IR span renderers, because as things
................................................................................
	render_state_handle.tagproc = tagproc;

	-- bind to legacy names
	-- yikes this needs to be cleaned up so badly
	local ir = {}
	local dr = astproc.toHTML -- default renderers
	local plainr = astproc.toTXT
	local irBlockRdrs = astproc.toIR.block_renderers;

	render_state_handle.ir = ir;

	local function renderBlocks(blocks, irs)
		for i, block in ipairs(blocks) do
			local rd
			if irBlockRdrs[block.kind] then
				rd = irBlockRdrs[block.kind](block,sec)
			else
				local rdr = renderJob:proc('render',block.kind,'html')
				if rdr then
					rd = rdr({
						state = render_state_handle;
						tagproc = tagproc.toIR;
						astproc = astproc.toIR;
................................................................................
					rd.attrs.lang = rd.src.origin.lang
				end
				table.insert(irs.nodes, rd)
				runhook('ir_section_node_insert', rd, irs, sec)
			end
		end
	end

	runhook('ir_assemble', ir)
	for i, sec in ipairs(doc.secorder) do
		if doctitle == nil and sec.depth == 1 and sec.heading_node then
			doctitle = astproc.toTXT.htmlSpan(sec.heading_node.spans, sec.heading_node, sec)
		end
		local irs
		if sec.kind == 'ordinary' then
			if #(sec.blocks) > 0 then
				irs = {tag='section',attrs={id = getSafeID(sec)},nodes={}}
				runhook('ir_section_build', irs, sec)
				renderBlocks(sec.blocks, irs)
			end
		elseif sec.kind == 'blockquote' then
		elseif sec.kind == 'listing' then
		elseif sec.kind == 'embed' then
		end
		if irs then table.insert(ir, irs) end
	end


	for _, fn in pairs(footnotes) do




		local tag = tagproc.toIR.tag
		local body = {nodes={}}
		local ftir = {}
		for l in fn.source:gmatch('([^\n]*)') do
			ct.parse_line(l, fn.origin, ftir)
		end
		renderBlocks(ftir,body)
		local note = tag('div',{class='footnote',id=fn.id}, {



























			tag('div',{class='number'}, tostring(fn.num)),
			tag('div',{class='text'}, body.nodes),
			tag('a',{href='#0'},'⤫')
		})
		table.insert(ir, note)
	end

	if next(footnotes) then
		table.insert(ir, tagproc.toIR.tag('div',{id='cover'},''))
	end

	-- restructure passes
	runhook('ir_restructure_pre', ir)

	---- list insertion pass
................................................................................
	local styles = {}
	if opts.width then
		table.insert(styles, string.format([[body {padding:0 1em;margin:auto;max-width:%s}]], opts.width))
	end
	if opts.accent then
		table.insert(styles, string.format(':root {--accent:%s}', opts.accent))
	end
	if opts.accent or (not opts['dark-on-light']) and (not opts['fossil-uv']) then
		addStyle 'accent'
	end


	for _,k in pairs(stylesNeeded.order) do
		if not stylesets[k] then ct.exns.unimpl('styleset %s not implemented (!)',  k):throw() end
		table.insert(styles, prepcss(stylesets[k]))
................................................................................
	if opts['link-css'] then
		local css = opts['link-css']
		if type(css) ~= 'string' then ct.exns.mode('must be a string', 'html:link-css'):throw() end
		styletag = styletag .. tagproc.toHTML.elt('link',{rel='stylesheet',type='text/css',href=opts['link-css']})
	end
	if next(styles) then
		if opts['gen-styles'] then
			styletag = styletag .. tagproc.toHTML.tag('style',{type='text/css'},table.concat(styles))
		end
		table.insert(head, styletag)
	end

	if opts['fossil-uv'] then
		return tagproc.toHTML.tag('div',{class='fossil-doc',['data-title']=doctitle},styletag .. body)
	elseif opts.snippet then
		return styletag .. body
	else
		return dr.htmlDoc(doctitle, next(head) and table.concat(head), body)
	end
end







>
>
>
>
>
>
>
>
>
>
>







 







|









|











|







 







|






|



|




|







 







|



|




|



|




|





|







 







<







 







>
>
>







 







|
|
|





|
>
>
>
>
>
>
>
>







 







|









>
|
>
>
>
>
>
>
>
>
>

|







 







>
>
>
>
|
>











>
|





>
>







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>










|







 







<






|
|







 







>












|






>
|
>
>
>
>
|
|
|
|
|
|
|
<
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
|
|
|
>
|







 







|







 







|












13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
..
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
..
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
...
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
...
172
173
174
175
176
177
178

179
180
181
182
183
184
185
...
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
...
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
...
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
...
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
...
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
...
840
841
842
843
844
845
846

847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
...
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916

917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
....
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
....
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
function ct.render.html(doc, opts, setup)
	local doctitle = opts['title']
	local f = string.format
	local getSafeID = ct.tool.namespace()

	local footnotes = {}
	local footnotecount = 0

	local cdata = function(...) return ... end
	if opts.epub then
		opts.xhtml = true
	end

	if opts.xhtml then
		cdata = function(s)
			return '<![CDATA[' .. s .. ']]>'
		end
	end

	local langsused = {}
	local langpairs = {
		lua = { color = 0x9377ff };
		terra = { color = 0xff77c8 };
		c = { name = 'C', color = 0x77ffe8 };
		html = { color = 0xfff877 };
................................................................................
			li {
				padding: 0.1em 0;
			}
		]];
		list_ordered = [[]];
		list_unordered = [[]];
		footnote = [[
			aside.footnote {
				font-family: 90%;
				grid-template-columns: 1em 1fr min-content;
				grid-template-rows: 1fr min-content;
				position: fixed;
				padding: 1em;
				background: @tone(0.03);
				margin:auto;
			}
			@media screen {
				aside.footnote {
					display: grid;
					left: 10em;
					right: 10em;
					max-width: calc(@width + 2em);
					max-height: 30vw;
					bottom: 1em;
					border: 1px solid black;
					transform: translateY(200%);
					transition: 0.4s;
					z-index: 100;
				}
				aside.footnote:target {
					transform: translateY(0%);
				}
				#cover {
					position: fixed;
					top: 0;
					left: 0;
					height: 100vh; width: 100vw;
................................................................................
						@tone/0.8(-0.07),
						@tone/0.4(-0.07));
					opacity: 0%;
					transition: 1s;
					pointer-events: none;
					backdrop-filter: blur(0px);
				}
				aside.footnote:target ~ #cover {
					opacity: 100%;
					pointer-events: all;
					backdrop-filter: blur(5px);
				}
			}
			@media print {
				aside.footnote {
					display: grid;
					position: relative;
				}
				aside.footnote:first-of-type {
					border-top: 1px solid black;
				}
			}

			aside.footnote > a[href="#0"]{
				grid-row: 2/3;
				grid-column: 3/4;
				display: block;
				text-align: center;
				padding: 0 0.3em;
				text-decoration: none;
				background: @tone(0.2);
................................................................................
				font-size: 150%;
				-webkit-user-select: none;
				-ms-user-select: none;
				user-select: none;
				-webkit-user-drag: none;
				user-drag: none;
			}
			aside.footnote > a[href="#0"]:hover {
				background: @tone(0.3);
				color: @tone(2);
			}
			aside.footnote > a[href="#0"]:active {
				background: @tone(0.05);
				color: @tone(0.4);
			}
			@media print {
				aside.footnote > a[href="#0"]{
					display:none;
				}
			}
			aside.footnote > div.number {
				text-align:right;
				grid-row: 1/2;
				grid-column: 1/2;
			}
			aside.footnote > div.text {
				grid-row: 1/2;
				grid-column: 2/4;
				padding-left: 1em;
				overflow-y: auto;
			}
			aside.footnote > div.text > p:first-child {
				margin-top: 0;
			}
		]];
		header = [[
			body { padding: 0 2.5em !important }
			h1,h2,h3,h4,h5,h6 { border-bottom: 1px solid @tone(0.7); }
			h1 { font-size: 200%; border-bottom-style: double !important; border-bottom-width: 3px !important; margin: 0em -1em; }
................................................................................
			h1,h2,h3,h4,h5,h6 {
				margin-top: 0;
				margin-bottom: 0;
			}
			:is(h1,h2,h3,h4,h5,h6) + p {
				margin-top: 0.4em;
			}

		]];
		headingAnchors = [[
			:is(h1,h2,h3,h4,h5,h6) > a[href].anchor {
				text-decoration: none;
				font-size: 1.2em;
				padding: 0.3em;
				opacity: 0%;
................................................................................
			}
			section > aside p {
				margin: 0;
				margin-top: 0.6em;
			}
			section > aside p:first-child {
				margin: 0;
			}
         section aside + aside {
				margin-top: 0.5em;
			}
      ]];
		code = [[
			code {
				display: inline-block;
				background: @tone(-1);
				color: @tone(0.7);
................................................................................
	doc.stage.job = renderJob;

	local runhook = function(h, ...)
		return renderJob:hook(h, render_state_handle, ...)
	end

	local tagproc do
		local html_open = function(t,attrs)
			if attrs then
				return t .. ss.reduce(function(a,b) return a..b end, '',
					ss.map(function(v,k)
						if v == true
							then          return ' '..k
							elseif v then return f(' %s="%s"', k, v)
						end
					end, attrs))
			else return t end
		end

		local elt = function(t,attrs)
			if opts.xhtml then
				return f('<%s />', html_open(t,attrs))
			end
			return f('<%s>', html_open(t,attrs))
		end

		tagproc = {
			toTXT = {
				tag = function(t,a,v) return v  end;
				elt = function(t,a)   return '' end;
				catenate = table.concat;
................................................................................
				} end;

				catenate = function(...) return ... end;
			};
			toHTML = {
				elt = elt;
				tag = function(t,attrs,body)
					return f('<%s>%s</%s>', html_open(t,attrs), body, t)
				end;
				catenate = table.concat;
			};
		}
	end

	local function getBaseRenderers(procs, span_renderers)
		local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
		local htmlDoc = function(title, head, body)
			local attrs
			local header = [[<!doctype html>]]
			if opts['epub'] then
				-- so cursed
				attrs = {
					xmlns = "http://www.w3.org/1999/xhtml";
					['xmlns:epub'] = "http://www.idpf.org/2007/ops";
				}
				header = [[<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE html>]]
			end
			return header .. tag('html',attrs,
				tag('head', nil,
					(opts.epub and '' or elt('meta',{charset = 'utf-8'})) ..
					(title and tag('title', nil, title) or '') ..
					(head or '')) ..
				tag('body', nil, body or ''))
		end

		local function htmlSpan(spans, block, sec)
			local text = {}
................................................................................
			elseif d.crit then
				b.origin:fail('critical extension %s unavailable', d.ext)
			elseif d.failthru then
				return htmlSpan(d.spans, b, s)
			end
		end
		function span_renderers.footnote(f,b,s)
			local linkattr = {}
			if opts.epub then
				linkattr['epub:type'] = 'noteref'
			else
				addStyle 'footnote'
			end
			local source, sid, ssec = b.origin:ref(f.ref)
			local cnc = getSafeID(ssec) .. ' ' .. sid
			local fn
			if footnotes[cnc] then
				fn = footnotes[cnc]
			else
				footnotecount = footnotecount + 1
				fn = {num = footnotecount, origin = b.origin, fnid=cnc, source = source}
				fn.id = getSafeID(fn)
				footnotes[cnc] = fn
			end
			linkattr.href = '#'..fn.id
			return tag('a', linkattr, htmlSpan(f.spans) ..
						tag('sup',nil, fn.num))
		end

		return span_renderers
	end

	local astproc

	local function getBlockRenderers(procs, sr)
		local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
		local null = function() return catenate{} end

		local block_renderers = {
			anchor = function(b,s)
................................................................................
				return tag('aside', {}, bn)
			end;
			['break'] = function() -- HACK
				-- lists need to be rewritten to work like asides
				return '';
			end;
		}

		function block_renderers.quote(b,s)
			local ir = {}
			local toIR = block_renderers
			for i, sec in ipairs(b.doc.secorder) do
				local secnodes = {}
				for i, bl in ipairs(sec.blocks) do
					if toIR[bl.kind] then
						table.insert(secnodes, toIR[bl.kind](bl,sec))
					end
				end
				if next(secnodes) then
					if b.doc.secorder[2] then --#secs>1?
						-- only wrap in a section if >1 section
						table.insert(ir, tag('section',
													{id = getSafeID(sec)},
													secnodes))
					else
						ir = secnodes
					end
				end
			end
			return tag('blockquote', b.id and {id=getSafeID(b)} or {}, catenate(ir))
		end

		return block_renderers;
	end

	local function getRenderers(procs)
		local span_renderers = getSpanRenderers(procs)
		local r = getBaseRenderers(procs,span_renderers)
		r.block_renderers = getBlockRenderers(procs, r)
		return r
	end

	astproc = {
		toHTML = getRenderers(tagproc.toHTML);
		toTXT  = getRenderers(tagproc.toTXT);
		toIR   = { };
	}
	astproc.toIR.span_renderers = ss.clone(astproc.toHTML);
	astproc.toIR.block_renderers = getBlockRenderers(tagproc.toIR,astproc.toHTML);
		-- note we use HTML here instead of IR span renderers, because as things
................................................................................
	render_state_handle.tagproc = tagproc;

	-- bind to legacy names
	-- yikes this needs to be cleaned up so badly
	local ir = {}
	local dr = astproc.toHTML -- default renderers
	local plainr = astproc.toTXT


	render_state_handle.ir = ir;

	local function renderBlocks(blocks, irs)
		for i, block in ipairs(blocks) do
			local rd
			if astproc.toIR.block_renderers[block.kind] then
				rd = astproc.toIR.block_renderers[block.kind](block,sec)
			else
				local rdr = renderJob:proc('render',block.kind,'html')
				if rdr then
					rd = rdr({
						state = render_state_handle;
						tagproc = tagproc.toIR;
						astproc = astproc.toIR;
................................................................................
					rd.attrs.lang = rd.src.origin.lang
				end
				table.insert(irs.nodes, rd)
				runhook('ir_section_node_insert', rd, irs, sec)
			end
		end
	end

	runhook('ir_assemble', ir)
	for i, sec in ipairs(doc.secorder) do
		if doctitle == nil and sec.depth == 1 and sec.heading_node then
			doctitle = astproc.toTXT.htmlSpan(sec.heading_node.spans, sec.heading_node, sec)
		end
		local irs
		if sec.kind == 'ordinary' then
			if #(sec.blocks) > 0 then
				irs = {tag='section',attrs={id = getSafeID(sec)},nodes={}}
				runhook('ir_section_build', irs, sec)
				renderBlocks(sec.blocks, irs)
			end
		elseif sec.kind == 'quote' then
		elseif sec.kind == 'listing' then
		elseif sec.kind == 'embed' then
		end
		if irs then table.insert(ir, irs) end
	end

	do local fnsorted = {}
		for _, fn in pairs(footnotes) do
			fnsorted[fn.num] = fn
		end

		for _, fn in ipairs(fnsorted) do
			local tag = tagproc.toIR.tag
			local body = {nodes={}}
			local ftir = {}
			for l in fn.source:gmatch('([^\n]*)') do
				ct.parse_line(l, fn.origin, ftir)
			end
			renderBlocks(ftir,body)

			local fattr = {id=fn.id}
			if opts.epub then
				---UUUUUUGHHH
				local npfx = string.format('(%u) ', fn.num)
				if next(body.nodes) then
					local n = body.nodes[1]
					repeat
						if n.nodes[1] then
							if type(n.nodes[1]) == 'string' then
								n.nodes[1] = npfx .. n.nodes[1]
								break
							end
							n = n.nodes[1]
						else
							n.nodes[1] = {tag='p',attrs={},nodes={npfx}}
							break
						end
					until false

				else
					body.nodes[1] = {tag='p',attrs={},nodes={npfx}}
				end
				fattr['epub:type'] = 'footnote'
			else
				fattr.class = 'footnote'
			end
			local note = tag('aside', fattr, opts.epub and body.nodes or {
				tag('div',{class='number'}, tostring(fn.num)),
				tag('div',{class='text'}, body.nodes),
				tag('a',{href='#0'},'⤫')
			})
			table.insert(ir, note)
		end
	end
	if next(footnotes) and not opts.epub then
		table.insert(ir, tagproc.toIR.tag('div',{id='cover'},''))
	end

	-- restructure passes
	runhook('ir_restructure_pre', ir)

	---- list insertion pass
................................................................................
	local styles = {}
	if opts.width then
		table.insert(styles, string.format([[body {padding:0 1em;margin:auto;max-width:%s}]], opts.width))
	end
	if opts.accent then
		table.insert(styles, string.format(':root {--accent:%s}', opts.accent))
	end
	if not opts.epub and (opts.accent or (not opts['dark-on-light']) and (not opts['fossil-uv'])) then
		addStyle 'accent'
	end


	for _,k in pairs(stylesNeeded.order) do
		if not stylesets[k] then ct.exns.unimpl('styleset %s not implemented (!)',  k):throw() end
		table.insert(styles, prepcss(stylesets[k]))
................................................................................
	if opts['link-css'] then
		local css = opts['link-css']
		if type(css) ~= 'string' then ct.exns.mode('must be a string', 'html:link-css'):throw() end
		styletag = styletag .. tagproc.toHTML.elt('link',{rel='stylesheet',type='text/css',href=opts['link-css']})
	end
	if next(styles) then
		if opts['gen-styles'] then
			styletag = styletag .. tagproc.toHTML.tag('style',{type='text/css'},cdata(table.concat(styles)))
		end
		table.insert(head, styletag)
	end

	if opts['fossil-uv'] then
		return tagproc.toHTML.tag('div',{class='fossil-doc',['data-title']=doctitle},styletag .. body)
	elseif opts.snippet then
		return styletag .. body
	else
		return dr.htmlDoc(doctitle, next(head) and table.concat(head), body)
	end
end