diff --git a/inputters/djot.lua b/inputters/djot.lua index 370cfbf..de6e8b1 100644 --- a/inputters/djot.lua +++ b/inputters/djot.lua @@ -6,6 +6,7 @@ -- @copyright License: MIT (c) 2023 Omikhleia -- @module inputters.djot -- +local utils = require("packages.markdown.utils") local ast = require("silex.ast") local createCommand, createStructuredCommand = ast.createCommand, ast.createStructuredCommand @@ -19,8 +20,8 @@ local Renderer = pl.class() function Renderer:_init(options) self.references = {} self.footnotes = {} - self.metadata = {} self.shift_headings = SU.cast("integer", options.shift_headings or 0) + self.metadata = {} for key, val in pairs(options) do local meta = key:match("^meta:(.*)") if meta then @@ -60,7 +61,19 @@ 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]) + local content = self[node.c[i].t](self, node.c[i]) + -- Simplify outputs by collating strings + if type(content) == "string" and type(out[#out]) == "string" then + out[#out] = out[#out] .. content + else + -- Simplify out by removing empty elements + if type(content) ~= "table" or content.command or #content > 1 then + out[#out+1] = content + elseif #content == 1 then + -- Simplify out by removing useless grouping + out[#out+1] = content[1] + end + end end if node.tight ~= nil then self.tight = oldtight @@ -161,6 +174,19 @@ end function Renderer:code_block (node) local options = node.attr or {} options.class = node.lang and ((options.class and (options.class.." ") or "") .. node.lang) or options.class + if utils.hasClass(options, "djot") or utils.hasClass(options, "markdown") then + options.shift_headings = self.shift_headings + self:resolveAllUserDefinedSymbols() + -- Parent metadata just have a label xxx + -- User-defined memoized metadata have the symbol (:xxx:) as key + -- So we sort the keys to get the user-defined metadata first as overrides. + for key, val in SU.sortedpairs(self.metadata) do + local name = key:match("^:([%w_+-]+):$") or key -- remove leading and trailing colons + if not options["meta:" .. name] then + options["meta:" .. name] = val + end + end + end return createCommand("markdown:internal:codeblock", options, node.s, self:render_pos(node)) end @@ -351,7 +377,7 @@ function Renderer.softbreak (_) end function Renderer.hardbreak (_) - return createCommand("cr") + return createCommand("markdown:internal:hardbreak") end function Renderer:nbsp (node) @@ -576,6 +602,31 @@ local predefinedSymbols = { }, } +function Renderer:getUserDefinedSymbol (label, node_fake_metadata) + local content + if self.metadata[label] then -- use memoized + content = self.metadata[label] + else + 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]) + else + content = self:render_children(node_fake_metadata) + end + self.metadata[label] = content -- memoize + end + return content +end + +function Renderer:resolveAllUserDefinedSymbols () + -- Ensure all fake footnotes are rendered and memoized, even unused ones. + for label, node_fake_metadata in pairs(self.footnotes) do + if label:match("^:([%w_+-]+):$") then + self:getUserDefinedSymbol(label, node_fake_metadata) + end + end +end + function Renderer:symbol (node) -- Let's first look at fake footnotes to resolve the symbol. -- We just added unforeseen templating and recursive variable substitution to Djot. @@ -586,19 +637,7 @@ function Renderer:symbol (node) if #node_fake_metadata.c > 1 and not node._standalone_ then SU.error("Cannot use multi-paragraph metatada "..label.." as inline content") end - - local content - if self.metadata[label] then -- use memoized - content = self.metadata[label] - else - 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]) - else - content = self:render_children(node_fake_metadata) - end - self.metadata[label] = content -- memoize - end + local content = self:getUserDefinedSymbol(label, node_fake_metadata) if node.attr then if not node._standalone_ then -- Add a span for attributes on the inline variant. diff --git a/inputters/markdown.lua b/inputters/markdown.lua index 3328bbd..ad49e3b 100644 --- a/inputters/markdown.lua +++ b/inputters/markdown.lua @@ -72,6 +72,19 @@ local function SileAstWriter (writerOps, renderOps) local generic = require("lunamark.writer.generic") local writer = generic.new(writerOps or {}) local shift_headings = SU.cast("integer", renderOps.shift_headings or 0) + local parentmetadata = {} + for key, val in pairs(renderOps) do + local meta = key:match("^meta:(.*)") + if meta then + if meta:match("[%w_+-]+") then + -- We don't use them in this renderer, but we can pass them through + -- to embedded djot documents. + parentmetadata[key] = val + else + SU.warn("Invalid metadata key is skipped: "..meta) + end + end + end -- Simple one-to-one mappings between lunamark AST and SILE @@ -86,7 +99,7 @@ local function SileAstWriter (writerOps, renderOps) writer.blockquote = simpleCommandWrapper("markdown:internal:blockquote") writer.verbatim = simpleCommandWrapper("verbatim") writer.listitem = simpleCommandWrapper("item") - writer.linebreak = simpleCommandWrapper("cr") + writer.linebreak = simpleCommandWrapper("markdown:internal:hardbreak") writer.singlequoted = simpleCommandWrapper("singlequoted") writer.doublequoted = simpleCommandWrapper("doublequoted") @@ -148,6 +161,10 @@ local function SileAstWriter (writerOps, renderOps) writer.fenced_code = function (content, infostring, attr) local opts = attr or { class = infostring } + if utils.hasClass(opts, "djot") or utils.hasClass(opts, "markdown") then + opts = pl.tablex.union(parentmetadata, opts) + opts.shift_headings = shift_headings + end return createCommand("markdown:internal:codeblock", opts, content) end diff --git a/inputters/pandocast.lua b/inputters/pandocast.lua index c195887..9d26b76 100644 --- a/inputters/pandocast.lua +++ b/inputters/pandocast.lua @@ -48,6 +48,19 @@ local Renderer = pl.class() function Renderer:_init(options) self.shift_headings = SU.cast("integer", options.shift_headings or 0) + self.parentmetadata = {} + for key, val in pairs(options) do + local meta = key:match("^meta:(.*)") + if meta then + if meta:match("[%w_+-]+") then + -- We don't use them in this renderer, but we can pass them through + -- to embedded djot documents. + self.parentmetadata[key] = val + else + SU.warn("Invalid metadata key is skipped: "..meta) + end + end + end end -- Allows unpacking tables on some Pandoc AST nodes so as to map them to methods @@ -77,8 +90,8 @@ local function addNodes(out, elements) out[#out] = out[#out] .. elements else -- Simplify out by removing empty elements - if type(elements) ~= "table" or elements.command or #elements > -1 then - out [#out+1] = elements + if type(elements) ~= "table" or elements.command or #elements > 0 then + out[#out+1] = elements end end end @@ -202,8 +215,12 @@ end -- CodeBlock Attr Text -- Code block (literal) with attributes -function Renderer.CodeBlock (_, attributes, text) +function Renderer:CodeBlock (attributes, text) local options = pandocAttributes(attributes) + if utils.hasClass(options, "djot") or utils.hasClass(options, "markdown") then + options = pl.tablex.union(self.parentmetadata, options) + options.shift_headings = self.shift_headings + end return createCommand("markdown:internal:codeblock", options, text) end @@ -481,7 +498,7 @@ end -- LineBreak -- Hard line break function Renderer.LineBreak (_) - return createCommand("cr") + return createCommand("markdown:internal:hardbreak") end -- Math MathType Text diff --git a/packages/markdown/commands.lua b/packages/markdown/commands.lua index 0c1a326..3c77213 100644 --- a/packages/markdown/commands.lua +++ b/packages/markdown/commands.lua @@ -9,6 +9,7 @@ -- require("silex.lang") local utils = require("packages.markdown.utils") +local hasClass = utils.hasClass local ast = require("silex.ast") local createCommand, createStructuredCommand, extractFromTree, subContent @@ -60,14 +61,6 @@ local function getSectioningCommand (level) return "markdown:fallback:header" end -local function hasClass (options, classname) - -- N.B. we want a true boolean here - if options.class and string.match(' ' .. options.class .. ' ',' '..classname..' ') then - return true - end - return false -end - local function hasLinkContent(tree) if type(tree) == "table" then return #tree > 1 or hasLinkContent(tree[1]) @@ -242,18 +235,18 @@ function package:registerCommands () elseif hasClass(options, "bigrule") then SILE.call("center", {}, function () SILE.call("raise", { height = "0.5ex" }, function () - SILE.call("hrule", { width = "33%lw" }) + SILE.call("hrule", { width = "33%lw", height = "0.4pt" }) end) end) elseif hasClass(options, "fullrule") and self:hasCouyards() then - SILE.call("fullrule") + SILE.call("fullrule", { thickness = "0.4pt" }) elseif hasClass(options, "pendant") and self:hasCouyards() then SILE.call("smallskip") SILE.call("couyard", { type = 6, width = "default" }) elseif not hasClass(options, "none") then SILE.call("center", {}, function () SILE.call("raise", { height = "0.5ex" }, function () - SILE.call("hrule", { width = "20%lw" }) + SILE.call("hrule", { width = "20%lw", height = "0.4pt" }) end) end) end @@ -271,13 +264,13 @@ function package:registerCommands () elseif options.separator == "---" then SILE.call("center", {}, function () SILE.call("raise", { height = "0.5ex" }, function () - SILE.call("hrule", { width = "20%lw" }) + SILE.call("hrule", { width = "20%lw", height = "0.4pt" }) end) end) elseif options.separator == "----" then SILE.call("center", {}, function () SILE.call("raise", { height = "0.5ex" }, function () - SILE.call("hrule", { width = "33%lw" }) + SILE.call("hrule", { width = "33%lw", height = "0.4pt" }) end) end) elseif options.separator == "- - - -" and self:hasCouyards() then @@ -286,7 +279,7 @@ function package:registerCommands () elseif options.separator == "--------------" then -- Page break SILE.call("eject") else - SILE.call("fullrule") + SILE.call("fullrule", { thickness = "0.4pt" }) end end, "Horizontal rule in Markdown (internal)") @@ -512,13 +505,30 @@ Please consider using a resilient-compatible class!]]) SILE.processString(rawtext, "lua") elseif format == "html" then if rawtext:match("^/%s]") then - SILE.call("cr") + SILE.call("markdown:internal:hardbreak") elseif rawtext:match("^/%s]") then SILE.call("penalty", { penalty = 100 }) end end end, "Raw native inline content in Markdown (internal)") + self:registerCommand("markdown:internal:hardbreak", function (_, _) + -- We don't want to use a cr here, because it would affect parindents, + -- insert a parskip, and maybe other things. + -- It's a bit tricky to handle a hardbreak depending on depending on the + -- alignment of the paragraph: + -- justified = we can't use a break, a cr (hfill+break) would work + -- ragged left = we can't use a cr + -- centered = we can't use a cr + -- ragged right = we don't care, a break is sufficient + -- Knowning the alignment is not obvious, neither guessing it from the skips. + -- Using a parfillskip seems to do the trick, but it's maybe a bit hacky. + -- This is nevertheless what would have occurred with a par in the same + -- situation. + SILE.typesetter:pushGlue(SILE.settings:get("typesetter.parfillskip")) + SILE.call("break") + end, "Hard break in Markdown (internal)") + self:registerCommand("markdown:internal:rawblock", function (options, content) local format = SU.required(options, "format", "rawcontent") if format == "sile" or format == "sile-lua" then diff --git a/packages/markdown/utils.lua b/packages/markdown/utils.lua index 3f1ca61..0959cea 100644 --- a/packages/markdown/utils.lua +++ b/packages/markdown/utils.lua @@ -32,8 +32,17 @@ local function nbspFilter (str) return #t == 1 and t[1] or t end +local function hasClass (options, classname) + -- N.B. we want a true boolean here + if options.class and string.match(' ' .. options.class .. ' ',' '..classname..' ') then + return true + end + return false +end + --- @export return { getFileExtension = getFileExtension, nbspFilter = nbspFilter, + hasClass = hasClass }