Skip to content

Commit

Permalink
feat: Djot extension for symbol-based conditionals
Browse files Browse the repository at this point in the history
  • Loading branch information
Omikhleia authored and Didier Willis committed Aug 19, 2023
1 parent f13ff0a commit c301f36
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 11 deletions.
32 changes: 31 additions & 1 deletion examples/sile-and-djot.dj
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand All @@ -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.

Expand Down
1 change: 0 additions & 1 deletion examples/sile-and-markdown-manual-styles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion examples/sile-and-markdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 44 additions & 7 deletions inputters/djot.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}
Expand All @@ -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
Expand All @@ -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 ()
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions lua-libraries/djot/attributes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion lua-libraries/djot/block.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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}))
Expand Down

0 comments on commit c301f36

Please sign in to comment.