From c301f36babdb9cc2bd02a90a647055cac6980192 Mon Sep 17 00:00:00 2001 From: Omikhleia Date: Sat, 19 Aug 2023 20:40:28 +0200 Subject: [PATCH] feat: Djot extension for symbol-based conditionals --- examples/sile-and-djot.dj | 32 +++++++++++- examples/sile-and-markdown-manual-styles.yml | 1 - examples/sile-and-markdown.md | 2 +- inputters/djot.lua | 51 +++++++++++++++++--- lua-libraries/djot/attributes.lua | 33 +++++++++++++ lua-libraries/djot/block.lua | 7 ++- 6 files changed, 115 insertions(+), 11 deletions(-) diff --git a/examples/sile-and-djot.dj b/examples/sile-and-djot.dj index 650ebbe..7837de9 100644 --- a/examples/sile-and-djot.dj +++ b/examples/sile-and-djot.dj @@ -403,6 +403,8 @@ Let's check how it expands: _:title:._ Again, the variable substitution mechanism also has precedence over these symbols. +## Syntax extensions + ### Attributed quotes (epigraphs) { rule="0.4pt" } @@ -427,8 +429,36 @@ Any option supported by the `\autodoc:package{resilient.epigraph}`{=sile} packag Be aware that this behavior is currently an extension. Other Djot converters will therefore likely skip the caption. +### Conditionals + +While the interpretation of symbols presented above is not a standard, the Djot specification leaves it to the rendering engine. +What we proposed there is thus perfectly acceptable so far. + +Since user-defined symbols and context metadata are now made available. +There's a trickier problem, however, if you want to only use a symbol if it is defined. +If you use a non-existent symbol, say `:non-existent:`, you just get :non-existent: in the output. +We cannot just ignore it, since something might have been expected there, but this is objectively not what you want. + +This converter therefore introduces an extension to the Djot attributes syntax, allowing you to conditionally render content based on the existence of a symbol. +The content is always parsed (so it has to remain valid Djot), but is only rendered if the symbol is defined. + +[This content has the `{?title}` condition, and since the `:title:` symbol is defined (_:title:_), it is rendered.]{?title} +Just below, however, a paragraph is annotated with the `{?non-existent}` condition, so you should not see it. + +{?non-existent} +You should NOT see this paragraph in the output. + +{!non-existent} +Now, this paragraph has the `{!non-existent}` condition, and since the `non-existent` symbol is NOT defined, it is rendered. + +Conditions work on blocks and inline elements alike. +They only apply to user-defined symbols (defined as pseudo-footnotes) and metadata symbols (set by the calling context). + +We can't say if future versions of the Djot specification will make use of `?` and `!` in attributes or bracketed content. +Therefore, we recommend that you call template files containing conditional content with the `.djt` extension, as a way to remember. + {#djot-configuration} -### Configuration +## Configuration You can pass additional options to the `\autodoc:command{\include}`{=sile} command or the `\autodoc:environment[check=false]{raw}`{=sile} environment to tune the behavior of the converter. diff --git a/examples/sile-and-markdown-manual-styles.yml b/examples/sile-and-markdown-manual-styles.yml index d3ca1f7..edc25c7 100644 --- a/examples/sile-and-markdown-manual-styles.yml +++ b/examples/sile-and-markdown-manual-styles.yml @@ -459,7 +459,6 @@ sectioning-chapter: level: 1 hook: "sectioning:chapter:hook" numberstyle: - header: "sectioning-chapter-head-number" main: "sectioning-chapter-main-number" reference: "sectioning-chapter-ref-number" settings: diff --git a/examples/sile-and-markdown.md b/examples/sile-and-markdown.md index 74ad4ca..f4cbc58 100644 --- a/examples/sile-and-markdown.md +++ b/examples/sile-and-markdown.md @@ -525,7 +525,7 @@ straight single and double quotes after digits to single and double primes. It can be useful for easily typesetting units (e.g. 6") or coordinates (e.g. Oxford is located at 51° 45' 7" N, 1° 15' 27" W). -### Configuration {#configuration} +## Configuration {#configuration} Most Markdown syntax extensions are enabled by default. You can pass additional options to diff --git a/inputters/djot.lua b/inputters/djot.lua index 370cfbf..7bb3c3a 100644 --- a/inputters/djot.lua +++ b/inputters/djot.lua @@ -16,7 +16,7 @@ local djotast = require("djot.ast") local Renderer = pl.class() -function Renderer:_init(options) +function Renderer:_init (options) self.references = {} self.footnotes = {} self.metadata = {} @@ -34,13 +34,13 @@ function Renderer:_init(options) self.tight = false -- We do not use it currently, though! end -function Renderer:render(doc) +function Renderer:render (doc) self.references = doc.references self.footnotes = doc.footnotes return self[doc.t](self, doc) end -function Renderer.render_pos(_, node) +function Renderer.render_pos (_, node) local p = node.pos and node.pos[1] if not p then return nil @@ -49,7 +49,41 @@ function Renderer.render_pos(_, node) return { lno = tonumber(lno), col = tonumber(col), pos = tonumber(pos) } end -function Renderer:render_children(node) +function Renderer:matchConditions(node) + -- Djot extension: conditional symbols + -- NOTE: We went for a quick hack in our modified Djot parser, + -- having the conditions in the class attribute. + -- Of course, it should be done in a more elegant way. + if node.attr and node.attr.class then + local conds_exist = {} + local conds_notexist = {} + local newclass = node.attr.class:gsub( "[?][%w_-]*", function(key) + table.insert(conds_exist, key:sub(2)) + return "" + end) + -- newclass = + newclass:gsub( "[!][%w_-]*", function(key) + table.insert(conds_notexist, key:sub(2)) + return "" + end) + for _, cond in ipairs(conds_exist) do + if not self.footnotes[":"..cond..":"] and not self.metadata[cond] then + return false + end + end + for _, cond in ipairs(conds_notexist) do + if self.footnotes[":"..cond..":"] or self.metadata[cond] then + return false + end + end + -- We could avoid passing the conditions further: + -- node.attr.class = newclass + -- Have to check we correctly memoize symbol substitution, though. + end + return true +end + +function Renderer:render_children (node) -- trap stack overflow local out = {} local ok, err = pcall(function () @@ -60,7 +94,9 @@ function Renderer:render_children(node) self.tight = node.tight end for i=1,#node.c do - out[#out+1] = self[node.c[i].t](self, node.c[i]) + if self:matchConditions(node.c[i]) then + out[#out+1] = self[node.c[i].t](self, node.c[i]) + end end if node.tight ~= nil then self.tight = oldtight @@ -81,7 +117,7 @@ function Renderer:render_children(node) return out end -function Renderer:doc(node) +function Renderer:doc (node) return self:render_children(node) end @@ -594,13 +630,14 @@ function Renderer:symbol (node) if #node_fake_metadata.c == 1 and node_fake_metadata.c[1].t == "para" then -- Skip a single para node. content = self:render_children(node_fake_metadata.c[1]) + if type(content) == "table" then content._single_ = true end else content = self:render_children(node_fake_metadata) end self.metadata[label] = content -- memoize end if node.attr then - if not node._standalone_ then + if type(content) ~= "table" or content._single_ or not node._standalone_ then -- Add a span for attributes on the inline variant. content = createCommand("markdown:internal:span", node.attr, content, self:render_pos(node)) else diff --git a/lua-libraries/djot/attributes.lua b/lua-libraries/djot/attributes.lua index 259cd65..91c63f4 100644 --- a/lua-libraries/djot/attributes.lua +++ b/lua-libraries/djot/attributes.lua @@ -31,6 +31,9 @@ local SCANNING_COMMENT = 10 local FAIL = 11 local DONE = 12 local START = 13 +-- BEGIN EXTENSION DIDIER 20230818 +local SCANNING_COND = 14 +-- END EXTENSION DIDIER 20230818 local AttributeParser = {} @@ -67,6 +70,11 @@ handlers[SCANNING] = function(self, pos) elseif c == '.' then self.begin = pos return SCANNING_CLASS + -- BEGIN EXTENSION DIDIER 20230818 + elseif c == '?' or c == "!" then + self.begin = pos + return SCANNING_COND + -- END EXTENSION DIDIER 20230818 elseif find(c, "^[%a%d_:-]") then self.begin = pos return SCANNING_KEY @@ -125,6 +133,31 @@ handlers[SCANNING_CLASS] = function(self, pos) end end +-- BEGIN EXTENSION DIDIER 20230818 +-- Of course hacking this in the class attribute is not a good idea, +-- but it is a quick way to get it working. +handlers[SCANNING_COND] = function(self, pos) + local c = sub(self.subject, pos, pos) + if find(c, "^[^%s%p]") or c == "_" or c == "-" then + return SCANNING_COND + elseif c == '}' then + if self.lastpos > self.begin then + self:add_match(self.begin, self.lastpos, "class") + end + self.begin = nil + return DONE + elseif find(c, "^%s") then + if self.lastpos > self.begin then + self:add_match(self.begin, self.lastpos, "class") + end + self.begin = nil + return SCANNING + else + return FAIL + end +end +-- END EXTENSION DIDIER 20230818 + handlers[SCANNING_KEY] = function(self, pos) local c = sub(self.subject, pos, pos) if c == "=" then diff --git a/lua-libraries/djot/block.lua b/lua-libraries/djot/block.lua index 72e3bc5..b451e2a 100644 --- a/lua-libraries/djot/block.lua +++ b/lua-libraries/djot/block.lua @@ -510,7 +510,12 @@ function Parser:specs() if not ep1 then return false end - local clsp, ep = find(self.subject, "^[%w_-]*", ep1 + 1) + -- BEGIN EXTENSION DIDIER 20230818 + -- Accept ? and ! as starting pseudo-class for conditionnal symbols + -- Of course hacking this in the class attribute is not a good idea, + -- but it is a quick way to get it working. + local clsp, ep = find(self.subject, "^[?!]?[%w_-]*", ep1 + 1) + -- END EXTENSION DIDIER 20230818 local _, eol = find(self.subject, "^[ \t]*[\r\n]", ep + 1) if eol then self:add_container(Container:new(spec, {equals = #equals}))