From a26cc1c5318e40a9ab2078d71788616aed7723ab Mon Sep 17 00:00:00 2001 From: Carlos Scheidegger Date: Thu, 16 May 2024 20:35:13 -0700 Subject: [PATCH] lua,callout - fix title when non-empty but no strings. refactor into modules to clean up globals --- src/resources/filters/customnodes/callout.lua | 1087 +++++++---------- src/resources/filters/main.lua | 2 + src/resources/filters/modules/authors.lua | 4 +- src/resources/filters/modules/callouts.lua | 199 +++ .../filters/modules/classpredicates.lua | 35 + src/resources/filters/modules/constants.lua | 45 +- src/resources/filters/modules/import_all.lua | 22 + src/resources/filters/modules/openxml.lua | 35 + src/resources/filters/quarto-post/docx.lua | 355 +++--- src/resources/filters/quarto-post/jats.lua | 48 +- src/resources/filters/quarto-post/latex.lua | 18 +- .../smoke-all/2024/01/31/8507.docx.snapshot | Bin 12266 -> 12841 bytes 12 files changed, 977 insertions(+), 873 deletions(-) create mode 100644 src/resources/filters/modules/callouts.lua create mode 100644 src/resources/filters/modules/classpredicates.lua create mode 100644 src/resources/filters/modules/import_all.lua create mode 100644 src/resources/filters/modules/openxml.lua diff --git a/src/resources/filters/customnodes/callout.lua b/src/resources/filters/customnodes/callout.lua index a8b9137f17..e109802ced 100644 --- a/src/resources/filters/customnodes/callout.lua +++ b/src/resources/filters/customnodes/callout.lua @@ -1,118 +1,466 @@ -- callout.lua -- Copyright (C) 2021-2022 Posit Software, PBC -local constants = require("modules/constants") - -function calloutType(div) - for _, class in ipairs(div.attr.classes) do - if isCallout(class) then - local type = class:match("^callout%-(.*)") - if type == nil then - type = "none" +function _callout_main() + local calloutidx = 1 + + local function calloutType(div) + for _, class in ipairs(div.attr.classes) do + if _quarto.modules.classpredicates.isCallout(class) then + local type = class:match("^callout%-(.*)") + if type == nil then + type = "none" + end + return type end - return type end + return nil end - return nil -end - -_quarto.ast.add_handler({ - -- use either string or array of strings - class_name = { "callout", "callout-note", "callout-warning", "callout-important", "callout-caution", "callout-tip" }, - -- the name of the ast node, used as a key in extended ast filter tables - ast_name = "Callout", + local function nameForCalloutStyle(calloutType) + if calloutType == nil then + return "default" + else + local name = pandoc.utils.stringify(calloutType) + + if name:lower() == "minimal" then + return "minimal" + elseif name:lower() == "simple" then + return "simple" + else + return "default" + end + end + end - -- callouts will be rendered as blocks - kind = "Block", + -- an HTML callout div + local function calloutDiv(node) + node = _quarto.modules.callouts.decorate_callout_title_with_crossref(node) - -- a function that takes the div node as supplied in user markdown - -- and returns the custom node - parse = function(div) - quarto_global_state.hasCallouts = true - local title = markdownToInlines(div.attr.attributes["title"]) - if not title or #title == 0 then - title = resolveHeadingCaption(div) + -- the first heading is the title + local div = pandoc.Div({}) + local c = quarto.utils.as_blocks(node.content) + if pandoc.utils.type(c) == "Blocks" then + div.content:extend(c) + else + div.content:insert(c) end - local old_attr = div.attr - local appearanceRaw = div.attr.attributes["appearance"] - local icon = div.attr.attributes["icon"] - local collapse = div.attr.attributes["collapse"] - div.attr.attributes["appearance"] = nil - div.attr.attributes["collapse"] = nil - div.attr.attributes["icon"] = nil - local callout_type = calloutType(div) - div.attr.classes = div.attr.classes:filter(function(class) return not isCallout(class) end) - return quarto.Callout({ - appearance = appearanceRaw, - title = title, - collapse = collapse, - content = div.content, - icon = icon, - type = callout_type, - attr = old_attr, + local title = quarto.utils.as_inlines(node.title) + local callout_type = node.type + local calloutAppearance = node.appearance + local icon = node.icon + local collapse = node.collapse + local found = false + + _quarto.ast.walk(title, { + RawInline = function(_) + found = true + end, + RawBlock = function(_) + found = true + end }) - end, - - -- These fields will be stored in the extended ast node - -- and available in the object passed to the custom filters - -- They must store Pandoc AST data. "Inline" custom nodes - -- can store Inlines in these fields, "Block" custom nodes - -- can store Blocks (and hence also Inlines implicitly). - slots = { "title", "content" }, - - constructor = function(tbl) - quarto_global_state.hasCallouts = true - - local t = tbl.type - local iconDefault = true - local appearanceDefault = nil - if t == "none" then - iconDefault = false - appearanceDefault = "simple" + + if calloutAppearance == _quarto.modules.constants.kCalloutAppearanceDefault and pandoc.utils.stringify(title) == "" and not found then + title = quarto.utils.as_inlines(pandoc.Plain(_quarto.modules.callouts.displayName(node.type))) end - local appearanceRaw = tbl.appearance - if appearanceRaw == nil then - appearanceRaw = option("callout-appearance", appearanceDefault) + + -- Make an outer card div and transfer classes and id + local calloutDiv = pandoc.Div({}) + calloutDiv.attr = node.attr:clone() + + local identifier = node.attr.identifier + if identifier ~= "" then + node.attr.identifier = "" + calloutDiv.attr.identifier = identifier + -- inject an anchor so callouts can be linked to + -- local attr = pandoc.Attr(identifier, {}, {}) + -- local anchor = pandoc.Link({}, "", "", attr) + -- title:insert(1, anchor) end - local icon = tbl.icon - if icon == nil then - icon = option("callout-icon", iconDefault) - elseif icon == "false" then - icon = false + div.attr.classes = pandoc.List() + div.attr.classes:insert("callout-body-container") + + -- add card attribute + calloutDiv.attr.classes:insert("callout") + calloutDiv.attr.classes:insert("callout-style-" .. calloutAppearance) + if node.type ~= nil then + calloutDiv.attr.classes:insert("callout-" .. node.type) end - local appearance = nameForCalloutStyle(appearanceRaw); - if appearance == "minimal" then - icon = false - appearance = "simple" + -- the image placeholder + local noicon = "" + + -- Check to see whether this is a recognized type + if icon == false or not _quarto.modules.callouts.isBuiltInType(callout_type) or type == nil then + noicon = " no-icon" + calloutDiv.attr.classes:insert("no-icon") end - local content = pandoc.Blocks({}) - content:extend(quarto.utils.as_blocks(tbl.content)) - local title = tbl.title - if type(title) == "string" then - title = pandoc.Str(title) + local imgPlaceholder = pandoc.Plain({pandoc.RawInline("html", "")}); + local imgDiv = pandoc.Div({imgPlaceholder}, pandoc.Attr("", {"callout-icon-container"})); + + -- show a titled callout + if title ~= nil and (pandoc.utils.type(title) == "string" or next(title) ~= nil) then + + -- mark the callout as being titleed + calloutDiv.attr.classes:insert("callout-titled") + + -- create a unique id for the callout + local calloutid = "callout-" .. calloutidx + calloutidx = calloutidx + 1 + + -- create the header to contain the title + -- title should expand to fill its space + local titleDiv = pandoc.Div(pandoc.Plain(title), pandoc.Attr("", {"callout-title-container", "flex-fill"})) + local headerDiv = pandoc.Div({imgDiv, titleDiv}, pandoc.Attr("", {"callout-header", "d-flex", "align-content-center"})) + local bodyDiv = div + bodyDiv.attr.classes:insert("callout-body") + + if collapse ~= nil then + + -- collapse default value + local expandedAttrVal = "true" + if collapse == "true" or collapse == true then + expandedAttrVal = "false" + end + + -- create the collapse button + local btnClasses = "callout-btn-toggle d-inline-block border-0 py-1 ps-1 pe-0 float-end" + local btnIcon = "" + local toggleButton = pandoc.RawInline("html", "
" .. btnIcon .. "
") + headerDiv.content:insert(pandoc.Plain(toggleButton)); + + -- configure the header div for collapse + local bsTargetClz = calloutid .. "-contents" + headerDiv.attr.attributes["bs-toggle"] = "collapse" + headerDiv.attr.attributes["bs-target"] = "." .. bsTargetClz + headerDiv.attr.attributes["aria-controls"] = calloutid + headerDiv.attr.attributes["aria-expanded"] = expandedAttrVal + headerDiv.attr.attributes["aria-label"] = 'Toggle callout' + + -- configure the body div for collapse + local collapseDiv = pandoc.Div({}) + collapseDiv.attr.identifier = calloutid + collapseDiv.attr.classes:insert(bsTargetClz) + collapseDiv.attr.classes:insert("callout-collapse") + collapseDiv.attr.classes:insert("collapse") + if expandedAttrVal == "true" then + collapseDiv.attr.classes:insert("show") + end + + -- add the current body to the collapse div and use the collapse div instead + collapseDiv.content:insert(bodyDiv) + bodyDiv = collapseDiv + end + + -- add the header and body to the div + calloutDiv.content:insert(headerDiv) + calloutDiv.content:insert(bodyDiv) + else + -- show an untitleed callout + + -- create a card body + local containerDiv = pandoc.Div({imgDiv, div}, pandoc.Attr("", {"callout-body"})) + containerDiv.attr.classes:insert("d-flex") + + -- add the container to the callout card + calloutDiv.content:insert(containerDiv) end - return { - title = title, - collapse = tbl.collapse, - content = content, - appearance = appearance, - icon = icon, - type = t, - attr = tbl.attr or pandoc.Attr(), - } + + return calloutDiv + end + + + + _quarto.ast.add_handler({ + -- use either string or array of strings + class_name = { "callout", "callout-note", "callout-warning", "callout-important", "callout-caution", "callout-tip" }, + + -- the name of the ast node, used as a key in extended ast filter tables + ast_name = "Callout", + + -- callouts will be rendered as blocks + kind = "Block", + + -- a function that takes the div node as supplied in user markdown + -- and returns the custom node + parse = function(div) + quarto_global_state.hasCallouts = true + local title = markdownToInlines(div.attr.attributes["title"]) + if not title or #title == 0 then + title = resolveHeadingCaption(div) + end + local old_attr = div.attr + local appearanceRaw = div.attr.attributes["appearance"] + local icon = div.attr.attributes["icon"] + local collapse = div.attr.attributes["collapse"] + div.attr.attributes["appearance"] = nil + div.attr.attributes["collapse"] = nil + div.attr.attributes["icon"] = nil + local callout_type = calloutType(div) + div.attr.classes = div.attr.classes:filter(function(class) return not _quarto.modules.classpredicates.isCallout(class) end) + return quarto.Callout({ + appearance = appearanceRaw, + title = title, + collapse = collapse, + content = div.content, + icon = icon, + type = callout_type, + attr = old_attr, + }) + end, + + -- These fields will be stored in the extended ast node + -- and available in the object passed to the custom filters + -- They must store Pandoc AST data. "Inline" custom nodes + -- can store Inlines in these fields, "Block" custom nodes + -- can store Blocks (and hence also Inlines implicitly). + slots = { "title", "content" }, + + constructor = function(tbl) + quarto_global_state.hasCallouts = true + + local t = tbl.type + local iconDefault = true + local appearanceDefault = nil + if t == "none" then + iconDefault = false + appearanceDefault = "simple" + end + local appearanceRaw = tbl.appearance + if appearanceRaw == nil then + appearanceRaw = option("callout-appearance", appearanceDefault) + end + + local icon = tbl.icon + if icon == nil then + icon = option("callout-icon", iconDefault) + elseif icon == "false" then + icon = false + end + + local appearance = nameForCalloutStyle(appearanceRaw); + if appearance == "minimal" then + icon = false + appearance = "simple" + end + local content = pandoc.Blocks({}) + content:extend(quarto.utils.as_blocks(tbl.content)) + local title = tbl.title + if type(title) == "string" then + title = pandoc.Str(title) + end + return { + title = title, + collapse = tbl.collapse, + content = content, + appearance = appearance, + icon = icon, + type = t, + attr = tbl.attr or pandoc.Attr(), + } + end + }) + + -- default renderer first + _quarto.ast.add_renderer("Callout", function(_) + return true + end, function(node) + node = _quarto.modules.callouts.decorate_callout_title_with_crossref(node) + local contents = _quarto.modules.callouts.resolveCalloutContents(node, true) + local callout = pandoc.BlockQuote(contents) + local result = pandoc.Div(callout, pandoc.Attr(node.attr.identifier or "")) + return result + end) + + _quarto.ast.add_renderer("Callout", function(_) + return _quarto.format.isHtmlOutput() and hasBootstrap() + end, calloutDiv) + + _quarto.ast.add_renderer("Callout", function(_) + return _quarto.format.isEpubOutput() or _quarto.format.isRevealJsOutput() + end, function (node) + local title = quarto.utils.as_inlines(node.title) + local type = node.type + local calloutAppearance = node.appearance + local hasIcon = node.icon + + if calloutAppearance == _quarto.modules.constants.kCalloutAppearanceDefault and pandoc.utils.stringify(title) == "" then + title = _quarto.modules.callouts.displayName(type) + end + + -- the body of the callout + local calloutBody = pandoc.Div({}, pandoc.Attr("", {"callout-body"})) + + local imgPlaceholder = pandoc.Plain({pandoc.RawInline("html", "")}); + local imgDiv = pandoc.Div({imgPlaceholder}, pandoc.Attr("", {"callout-icon-container"})); + + -- title + if title ~= nil and (pandoc.utils.type(title) == "string" or next(title) ~= nil) then + local callout_title = pandoc.Div({}, pandoc.Attr("", {"callout-title"})) + if hasIcon then + callout_title.content:insert(imgDiv) + end + callout_title.content:insert(pandoc.Para(pandoc.Strong(title))) + calloutBody.content:insert(callout_title) + else + if hasIcon then + calloutBody.content:insert(imgDiv) + end + end + + -- contents + local calloutContents = pandoc.Div(node.content, pandoc.Attr("", {"callout-content"})) + calloutBody.content:insert(calloutContents) + + -- set attributes (including hiding icon) + local attributes = pandoc.List({"callout"}) + if type ~= nil then + attributes:insert("callout-" .. type) + end + + if hasIcon == false then + attributes:insert("no-icon") + end + if title ~= nil and (pandoc.utils.type(title) == "string" or next(title) ~= nil) then + attributes:insert("callout-titled") + end + attributes:insert("callout-style-" .. calloutAppearance) + + local result = pandoc.Div({ calloutBody }, pandoc.Attr(node.attr.identifier or "", attributes)) + -- in revealjs or epub, if the leftover attr is non-trivial, + -- then we need to wrap the callout in a div (#5208, #6853) + if node.attr.identifier ~= "" or #node.attr.classes > 0 or #node.attr.attributes > 0 then + return pandoc.Div({ result }, node.attr) + else + return result + end + end) + + _quarto.ast.add_renderer("Callout", function(_) + return _quarto.format.isGithubMarkdownOutput() + end, function(callout) + local result = pandoc.Blocks({}) + local header = "[!" .. callout.type:upper() .. "]" + result:insert(pandoc.RawBlock("markdown", header)) + local tt = pandoc.utils.type(callout.title) + if tt ~= "nil" then + result:insert(pandoc.Header(3, quarto.utils.as_inlines(callout.title))) + end + local ct = pandoc.utils.type(callout.content) + if ct == "Block" then + result:insert(callout.content) + elseif ct == "Blocks" then + result:extend(callout.content) + else + internal_error() + end + return pandoc.BlockQuote(result) + end) + + local included_font_awesome = false + local function ensure_typst_font_awesome() + if included_font_awesome then + return + end + included_font_awesome = true + quarto.doc.include_text("in-header", "#import \"@preview/fontawesome:0.1.0\": *") end -}) -local calloutidx = 1 + _quarto.ast.add_renderer("Callout", function(_) + return _quarto.format.isTypstOutput() + end, function(callout) + ensure_typst_font_awesome() + + local attrs = _quarto.modules.callouts.callout_attrs[callout.type] + local background_color, icon_color, icon + if attrs == nil then + background_color = "white" + icon_color = "black" + icon = "fa-info" + else + background_color = "rgb(\"#" .. attrs.background_color .. "\")"; + icon_color = "rgb(\"#" .. attrs.color .. "\")"; + icon = attrs.fa_icon_typst + end + + local title = callout.title + if title == nil then + title = pandoc.Plain(_quarto.modules.callouts.displayName(callout.type)) + end + + local typst_callout = _quarto.format.typst.function_call("callout", { + { "body", _quarto.format.typst.as_typst_content(callout.content) }, + { "title", _quarto.format.typst.as_typst_content(title) }, + { "background_color", pandoc.RawInline("typst", background_color) }, + { "icon_color", pandoc.RawInline("typst", icon_color) }, + { "icon", pandoc.RawInline("typst", "" .. icon .. "()")} + }) + + if callout.attr.identifier == "" then + return typst_callout + end + + local category = crossref.categories.by_ref_type[refType(callout.attr.identifier)] + return make_typst_figure { + content = typst_callout, + caption_location = "top", + caption = pandoc.Plain(pandoc.Str("")), + kind = "quarto-callout-" .. callout.type, + supplement = category.name, + numbering = "1", + identifier = callout.attr.identifier + } + end) + + _quarto.ast.add_renderer("Callout", function(_) + return _quarto.format.isDocxOutput() + end, function(callout) + return calloutDocx(callout) + end) +end +_callout_main() function docx_callout_and_table_fixup() if not _quarto.format.isDocxOutput() then return {} end + -- Attempts to detect whether this element is a code cell + -- whose output is a table + local function isCodeCellTable(el) + local isTable = false + _quarto.ast.walk(el, { + Div = function(div) + if div.attr.classes:find_if(_quarto.modules.classpredicates.isCodeCellDisplay) then + _quarto.ast.walk(div, { + Table = function(tbl) + isTable = true + end + }) + end + end + }) + return isTable + end + + local function isCodeCellFigure(el) + local isFigure = false + _quarto.ast.walk(el, { + Div = function(div) + if div.attr.classes:find_if(_quarto.modules.classpredicates.isCodeCellDisplay) then + if (isFigureDiv(div)) then + isFigure = true + elseif div.content and #div.content > 0 then + isFigure = discoverFigure(div.content[1], true) ~= nil + end + end + end + }) + return isFigure + end + return { -- Insert paragraphs between consecutive callouts or tables for docx @@ -127,7 +475,7 @@ function docx_callout_and_table_fixup() local isCodeBlock = el.t == "CodeBlock" -- Determine whether this is a code cell that outputs a table - local isCodeCell = is_regular_node(el, "Div") and el.attr.classes:find_if(isCodeCell) + local isCodeCell = is_regular_node(el, "Div") and el.attr.classes:find_if(_quarto.modules.classpredicates.isCodeCell) if isCodeCell and (isCodeCellTable(el) or isCodeCellFigure(el)) then isTableOrFigure = true end @@ -166,565 +514,6 @@ function docx_callout_and_table_fixup() } end -function isCallout(class) - return class == 'callout' or class:match("^callout%-") -end - -function isDocxCallout(class) - return class == "docx-callout" -end - -function isCodeCell(class) - return class == "cell" -end - -function isCodeCellDisplay(class) - return class == "cell-output-display" -end - --- Attempts to detect whether this element is a code cell --- whose output is a table -function isCodeCellTable(el) - local isTable = false - _quarto.ast.walk(el, { - Div = function(div) - if div.attr.classes:find_if(isCodeCellDisplay) then - _quarto.ast.walk(div, { - Table = function(tbl) - isTable = true - end - }) - end - end - }) - return isTable -end - -function isCodeCellFigure(el) - local isFigure = false - _quarto.ast.walk(el, { - Div = function(div) - if div.attr.classes:find_if(isCodeCellDisplay) then - if (isFigureDiv(div)) then - isFigure = true - elseif div.content and #div.content > 0 then - isFigure = discoverFigure(div.content[1], true) ~= nil - end - end - end - }) - return isFigure -end - -local function callout_title_prefix(callout, withDelimiter) - local category = crossref.categories.by_ref_type[refType(callout.attr.identifier)] - if category == nil then - fail("unknown callout prefix '" .. refType(callout.attr.identifier) .. "'") - return - end - - return titlePrefix(category.ref_type, category.name, callout.order, withDelimiter) -end - -function decorate_callout_title_with_crossref(callout) - callout = ensure_custom(callout) - if not param("enable-crossref", true) then - -- don't decorate captions with crossrefs information if crossrefs are disabled - return callout - end - -- nil should never happen here, but the Lua analyzer doesn't know it - if callout == nil then - -- luacov: disable - internal_error() - -- luacov: enable - return callout - end - if not is_valid_ref_type(refType(callout.attr.identifier)) then - return callout - end - if callout.title == nil then - callout.title = pandoc.Plain({}) - end - local title = callout.title.content - - -- unlabeled callouts do not get a title prefix - local is_uncaptioned = not ((title ~= nil) and (#title > 0)) - -- this is a hack but we need it to control styling downstream - callout.is_uncaptioned = is_uncaptioned - local title_prefix = callout_title_prefix(callout, not is_uncaptioned) - tprepend(title, title_prefix) - - return callout -end - --- an HTML callout div -function calloutDiv(node) - node = decorate_callout_title_with_crossref(node) - - -- the first heading is the title - local div = pandoc.Div({}) - local c = quarto.utils.as_blocks(node.content) - if pandoc.utils.type(c) == "Blocks" then - div.content:extend(c) - else - div.content:insert(c) - end - local title = quarto.utils.as_inlines(node.title) - local callout_type = node.type - local calloutAppearance = node.appearance - local icon = node.icon - local collapse = node.collapse - - if calloutAppearance == constants.kCalloutAppearanceDefault and pandoc.utils.stringify(title) == "" then - title = quarto.utils.as_inlines(pandoc.Plain(displayName(node.type))) - end - - -- Make an outer card div and transfer classes and id - local calloutDiv = pandoc.Div({}) - calloutDiv.attr = node.attr:clone() - - local identifier = node.attr.identifier - if identifier ~= "" then - node.attr.identifier = "" - calloutDiv.attr.identifier = identifier - -- inject an anchor so callouts can be linked to - -- local attr = pandoc.Attr(identifier, {}, {}) - -- local anchor = pandoc.Link({}, "", "", attr) - -- title:insert(1, anchor) - end - - div.attr.classes = pandoc.List() - div.attr.classes:insert("callout-body-container") - - -- add card attribute - calloutDiv.attr.classes:insert("callout") - calloutDiv.attr.classes:insert("callout-style-" .. calloutAppearance) - if node.type ~= nil then - calloutDiv.attr.classes:insert("callout-" .. node.type) - end - - -- the image placeholder - local noicon = "" - - -- Check to see whether this is a recognized type - if icon == false or not isBuiltInType(callout_type) or type == nil then - noicon = " no-icon" - calloutDiv.attr.classes:insert("no-icon") - end - local imgPlaceholder = pandoc.Plain({pandoc.RawInline("html", "")}); - local imgDiv = pandoc.Div({imgPlaceholder}, pandoc.Attr("", {"callout-icon-container"})); - - -- show a titled callout - if title ~= nil and (pandoc.utils.type(title) == "string" or next(title) ~= nil) then - - -- mark the callout as being titleed - calloutDiv.attr.classes:insert("callout-titled") - - -- create a unique id for the callout - local calloutid = "callout-" .. calloutidx - calloutidx = calloutidx + 1 - - -- create the header to contain the title - -- title should expand to fill its space - local titleDiv = pandoc.Div(pandoc.Plain(title), pandoc.Attr("", {"callout-title-container", "flex-fill"})) - local headerDiv = pandoc.Div({imgDiv, titleDiv}, pandoc.Attr("", {"callout-header", "d-flex", "align-content-center"})) - local bodyDiv = div - bodyDiv.attr.classes:insert("callout-body") - - if collapse ~= nil then - - -- collapse default value - local expandedAttrVal = "true" - if collapse == "true" or collapse == true then - expandedAttrVal = "false" - end - - -- create the collapse button - local btnClasses = "callout-btn-toggle d-inline-block border-0 py-1 ps-1 pe-0 float-end" - local btnIcon = "" - local toggleButton = pandoc.RawInline("html", "
" .. btnIcon .. "
") - headerDiv.content:insert(pandoc.Plain(toggleButton)); - - -- configure the header div for collapse - local bsTargetClz = calloutid .. "-contents" - headerDiv.attr.attributes["bs-toggle"] = "collapse" - headerDiv.attr.attributes["bs-target"] = "." .. bsTargetClz - headerDiv.attr.attributes["aria-controls"] = calloutid - headerDiv.attr.attributes["aria-expanded"] = expandedAttrVal - headerDiv.attr.attributes["aria-label"] = 'Toggle callout' - - -- configure the body div for collapse - local collapseDiv = pandoc.Div({}) - collapseDiv.attr.identifier = calloutid - collapseDiv.attr.classes:insert(bsTargetClz) - collapseDiv.attr.classes:insert("callout-collapse") - collapseDiv.attr.classes:insert("collapse") - if expandedAttrVal == "true" then - collapseDiv.attr.classes:insert("show") - end - - -- add the current body to the collapse div and use the collapse div instead - collapseDiv.content:insert(bodyDiv) - bodyDiv = collapseDiv - end - - -- add the header and body to the div - calloutDiv.content:insert(headerDiv) - calloutDiv.content:insert(bodyDiv) - else - -- show an untitleed callout - - -- create a card body - local containerDiv = pandoc.Div({imgDiv, div}, pandoc.Attr("", {"callout-body"})) - containerDiv.attr.classes:insert("d-flex") - - -- add the container to the callout card - calloutDiv.content:insert(containerDiv) - end - - return calloutDiv -end - -function epubCallout(node) - local title = quarto.utils.as_inlines(node.title) - local type = node.type - local calloutAppearance = node.appearance - local hasIcon = node.icon - - if calloutAppearance == constants.kCalloutAppearanceDefault and pandoc.utils.stringify(title) == "" then - title = displayName(type) - end - - -- the body of the callout - local calloutBody = pandoc.Div({}, pandoc.Attr("", {"callout-body"})) - - local imgPlaceholder = pandoc.Plain({pandoc.RawInline("html", "")}); - local imgDiv = pandoc.Div({imgPlaceholder}, pandoc.Attr("", {"callout-icon-container"})); - - -- title - if title ~= nil and (pandoc.utils.type(title) == "string" or next(title) ~= nil) then - local callout_title = pandoc.Div({}, pandoc.Attr("", {"callout-title"})) - if hasIcon then - callout_title.content:insert(imgDiv) - end - callout_title.content:insert(pandoc.Para(pandoc.Strong(title))) - calloutBody.content:insert(callout_title) - else - if hasIcon then - calloutBody.content:insert(imgDiv) - end - end - - -- contents - local calloutContents = pandoc.Div(node.content, pandoc.Attr("", {"callout-content"})) - calloutBody.content:insert(calloutContents) - - -- set attributes (including hiding icon) - local attributes = pandoc.List({"callout"}) - if type ~= nil then - attributes:insert("callout-" .. type) - end - - if hasIcon == false then - attributes:insert("no-icon") - end - if title ~= nil and (pandoc.utils.type(title) == "string" or next(title) ~= nil) then - attributes:insert("callout-titled") - end - attributes:insert("callout-style-" .. calloutAppearance) - - local result = pandoc.Div({ calloutBody }, pandoc.Attr(node.attr.identifier or "", attributes)) - -- in revealjs or epub, if the leftover attr is non-trivial, - -- then we need to wrap the callout in a div (#5208, #6853) - if node.attr.identifier ~= "" or #node.attr.classes > 0 or #node.attr.attributes > 0 then - return pandoc.Div({ result }, node.attr) - else - return result - end - -end - -function simpleCallout(node) - node = decorate_callout_title_with_crossref(node) - local contents = resolveCalloutContents(node, true) - local callout = pandoc.BlockQuote(contents) - local result = pandoc.Div(callout, pandoc.Attr(node.attr.identifier or "")) - return result -end - -function resolveCalloutContents(node, require_title) - local title = quarto.utils.as_inlines(node.title) - local type = node.type - - local contents = pandoc.List({}) - - -- Add the titles and contents - -- class_name - if pandoc.utils.stringify(title) == "" and require_title then - ---@diagnostic disable-next-line: need-check-nil - title = stringToInlines(type:sub(1,1):upper()..type:sub(2)) - end - - -- raw paragraph with styles (left border, colored) - if title ~= nil then - contents:insert(pandoc.Para(pandoc.Strong(title))) - end - tappend(contents, quarto.utils.as_blocks(node.content)) - - return contents -end - -function removeParagraphPadding(contents) - if #contents > 0 then - - if #contents == 1 then - if contents[1].t == "Para" then - contents[1] = openXmlPara(contents[1], 'w:before="16" w:after="16"') - end - else - if contents[1].t == "Para" then - contents[1] = openXmlPara(contents[1], 'w:before="16"') - end - - if contents[#contents].t == "Para" then - contents[#contents] = openXmlPara(contents[#contents], 'w:after="16"') - end - end - end -end - -function openXmlPara(para, spacing) - local xmlPara = pandoc.Para({ - pandoc.RawInline("openxml", "\n\n") - }) - tappend(xmlPara.content, para.content) - return xmlPara -end - -function nameForCalloutStyle(calloutType) - if calloutType == nil then - return "default" - else - local name = pandoc.utils.stringify(calloutType); - - if name:lower() == "minimal" then - return "minimal" - elseif name:lower() == "simple" then - return "simple" - else - return "default" - end - end -end - -local kDefaultDpi = 96 -function docxCalloutImage(type) - - -- If the DPI has been changed, we need to scale the callout icon - local dpi = pandoc.WriterOptions(PANDOC_WRITER_OPTIONS)['dpi'] - local scaleFactor = 1 - if dpi ~= nil then - scaleFactor = dpi / kDefaultDpi - end - - -- try to form the svg name - local svg = nil - if type ~= nil then - svg = param("icon-" .. type, nil) - end - - -- lookup the image - if svg ~= nil then - local img = pandoc.Image({}, svg, '', {[constants.kProjectResolverIgnore]="true"}) - img.attr.attributes["width"] = tostring(16 * scaleFactor) - img.attr.attributes["height"] = tostring(16 * scaleFactor) - return img - else - return nil - end -end - -local callout_attrs = { - note = { - color = kColorNote, - background_color = kBackgroundColorNote, - latex_color = "quarto-callout-note-color", - latex_frame_color = "quarto-callout-note-color-frame", - fa_icon = "faInfo", - fa_icon_typst = "fa-info" - }, - warning = { - color = kColorWarning, - background_color = kBackgroundColorWarning, - latex_color = "quarto-callout-warning-color", - latex_frame_color = "quarto-callout-warning-color-frame", - fa_icon = "faExclamationTriangle", - fa_icon_typst = "fa-exclamation-triangle" - }, - important = { - color = kColorImportant, - background_color = kBackgroundColorImportant, - latex_color = "quarto-callout-important-color", - latex_frame_color = "quarto-callout-important-color-frame", - fa_icon = "faExclamation", - fa_icon_typst = "fa-exclamation" - }, - caution = { - color = kColorCaution, - background_color = kBackgroundColorCaution, - latex_color = "quarto-callout-caution-color", - latex_frame_color = "quarto-callout-caution-color-frame", - fa_icon = "faFire", - fa_icon_typst = "fa-fire" - }, - tip = { - color = kColorTip, - background_color = kBackgroundColorTip, - latex_color = "quarto-callout-tip-color", - latex_frame_color = "quarto-callout-tip-color-frame", - fa_icon = "faLightbulb", - fa_icon_typst = "fa-lightbulb" - }, - - __other = { - color = kColorUnknown, - background_color = kColorUnknown, - latex_color = "quarto-callout-color", - latex_frame_color = "quarto-callout-color-frame", - fa_icon = nil, - fa_icon_typst = nil - } -} - -setmetatable(callout_attrs, { - __index = function(tbl, key) - return tbl.__other - end -}) - -function htmlColorForType(type) - return callout_attrs[type].color -end - -function htmlBackgroundColorForType(type) - return callout_attrs[type].background_color -end - -function latexColorForType(type) - return callout_attrs[type].latex_color -end - -function latexFrameColorForType(type) - return callout_attrs[type].latex_frame_color -end - -function iconForType(type) - return callout_attrs[type].fa_icon -end - -function isBuiltInType(type) - local icon = iconForType(type) - return icon ~= nil -end - -function displayName(type) - local defaultName = type:sub(1,1):upper()..type:sub(2) - return param("callout-" .. type .. "-title", defaultName) -end - --- default renderer first -_quarto.ast.add_renderer("Callout", function(_) - return true -end, simpleCallout) -_quarto.ast.add_renderer("Callout", function(_) - return _quarto.format.isHtmlOutput() and hasBootstrap() -end, calloutDiv) -_quarto.ast.add_renderer("Callout", function(_) - return _quarto.format.isEpubOutput() or _quarto.format.isRevealJsOutput() -end, epubCallout) - -_quarto.ast.add_renderer("Callout", function(_) - return _quarto.format.isGithubMarkdownOutput() -end, function(callout) - local result = pandoc.Blocks({}) - local header = "[!" .. callout.type:upper() .. "]" - result:insert(pandoc.RawBlock("markdown", header)) - local tt = pandoc.utils.type(callout.title) - if tt ~= "nil" then - result:insert(pandoc.Header(3, quarto.utils.as_inlines(callout.title))) - end - local ct = pandoc.utils.type(callout.content) - if ct == "Block" then - result:insert(callout.content) - elseif ct == "Blocks" then - result:extend(callout.content) - else - internal_error() - end - return pandoc.BlockQuote(result) -end) - -local included_font_awesome = false -local function ensure_typst_font_awesome() - if included_font_awesome then - return - end - included_font_awesome = true - quarto.doc.include_text("in-header", "#import \"@preview/fontawesome:0.1.0\": *") -end - -_quarto.ast.add_renderer("Callout", function(_) - return _quarto.format.isTypstOutput() -end, function(callout) - ensure_typst_font_awesome() - - local attrs = callout_attrs[callout.type] - local background_color, icon_color, icon - if attrs == nil then - background_color = "white" - icon_color = "black" - icon = "fa-info" - else - background_color = "rgb(\"#" .. attrs.background_color .. "\")"; - icon_color = "rgb(\"#" .. attrs.color .. "\")"; - icon = attrs.fa_icon_typst - end - - local title = callout.title - if title == nil then - title = pandoc.Plain(displayName(callout.type)) - end - - local typst_callout = _quarto.format.typst.function_call("callout", { - { "body", _quarto.format.typst.as_typst_content(callout.content) }, - { "title", _quarto.format.typst.as_typst_content(title) }, - { "background_color", pandoc.RawInline("typst", background_color) }, - { "icon_color", pandoc.RawInline("typst", icon_color) }, - { "icon", pandoc.RawInline("typst", "" .. icon .. "()")} - }) - - if callout.attr.identifier == "" then - return typst_callout - end - - local category = crossref.categories.by_ref_type[refType(callout.attr.identifier)] - return make_typst_figure { - content = typst_callout, - caption_location = "top", - caption = pandoc.Plain(pandoc.Str("")), - kind = "quarto-callout-" .. callout.type, - supplement = category.name, - numbering = "1", - identifier = callout.attr.identifier - } -end) - -_quarto.ast.add_renderer("Callout", function(_) - return _quarto.format.isDocxOutput() -end, function(callout) - return calloutDocx(callout) -end) - function crossref_callouts() return { Callout = function(callout) diff --git a/src/resources/filters/main.lua b/src/resources/filters/main.lua index 6b57fda6af..848a2b0d3e 100644 --- a/src/resources/filters/main.lua +++ b/src/resources/filters/main.lua @@ -12,6 +12,8 @@ end import("./mainstateinit.lua") +import("./modules/import_all.lua") + import("./ast/scopedwalk.lua") import("./ast/customnodes.lua") import("./ast/emulatedfilter.lua") diff --git a/src/resources/filters/modules/authors.lua b/src/resources/filters/modules/authors.lua index 2c000f81c5..5da7e6a39b 100644 --- a/src/resources/filters/modules/authors.lua +++ b/src/resources/filters/modules/authors.lua @@ -1,5 +1,5 @@ --- meta.lua --- Copyright (C) 2020-2022 Posit Software, PBC +-- authors.lua +-- Copyright (C) 2020-2024 Posit Software, PBC -- read and replace the authors field -- without reshaped data that has been diff --git a/src/resources/filters/modules/callouts.lua b/src/resources/filters/modules/callouts.lua new file mode 100644 index 0000000000..4e228cfb49 --- /dev/null +++ b/src/resources/filters/modules/callouts.lua @@ -0,0 +1,199 @@ +-- callouts.lua +-- Copyright (C) 2024 Posit Software, PBC + +local constants = require("modules/constants") + +local function callout_title_prefix(callout, withDelimiter) + local category = crossref.categories.by_ref_type[refType(callout.attr.identifier)] + if category == nil then + fail("unknown callout prefix '" .. refType(callout.attr.identifier) .. "'") + return + end + + return titlePrefix(category.ref_type, category.name, callout.order, withDelimiter) +end + +local function decorate_callout_title_with_crossref(callout) + callout = ensure_custom(callout) + if not param("enable-crossref", true) then + -- don't decorate captions with crossrefs information if crossrefs are disabled + return callout + end + -- nil should never happen here, but the Lua analyzer doesn't know it + if callout == nil then + -- luacov: disable + internal_error() + -- luacov: enable + return callout + end + if not is_valid_ref_type(refType(callout.attr.identifier)) then + return callout + end + if callout.title == nil then + callout.title = pandoc.Plain({}) + end + local title = callout.title.content + + -- unlabeled callouts do not get a title prefix + local is_uncaptioned = not ((title ~= nil) and (#title > 0)) + -- this is a hack but we need it to control styling downstream + callout.is_uncaptioned = is_uncaptioned + local title_prefix = callout_title_prefix(callout, not is_uncaptioned) + tprepend(title, title_prefix) + + return callout +end + +local function resolveCalloutContents(node, require_title) + local title = quarto.utils.as_inlines(node.title) + local type = node.type + + local contents = pandoc.List({}) + + -- Add the titles and contents + -- class_name + if pandoc.utils.stringify(title) == "" and require_title then + ---@diagnostic disable-next-line: need-check-nil + title = stringToInlines(type:sub(1,1):upper()..type:sub(2)) + end + + -- raw paragraph with styles (left border, colored) + if title ~= nil then + contents:insert(pandoc.Para(pandoc.Strong(title))) + end + tappend(contents, quarto.utils.as_blocks(node.content)) + + return contents +end + +local kDefaultDpi = 96 +local function docxCalloutImage(type) + + -- If the DPI has been changed, we need to scale the callout icon + local dpi = pandoc.WriterOptions(PANDOC_WRITER_OPTIONS)['dpi'] + local scaleFactor = 1 + if dpi ~= nil then + scaleFactor = dpi / kDefaultDpi + end + + -- try to form the svg name + local svg = nil + if type ~= nil then + svg = param("icon-" .. type, nil) + end + + -- lookup the image + if svg ~= nil then + local img = pandoc.Image({}, svg, '', {[constants.kProjectResolverIgnore]="true"}) + img.attr.attributes["width"] = tostring(16 * scaleFactor) + img.attr.attributes["height"] = tostring(16 * scaleFactor) + return img + else + return nil + end +end + +local callout_attrs = { + note = { + color = constants.kColorNote, + background_color = constants.kBackgroundColorNote, + latex_color = "quarto-callout-note-color", + latex_frame_color = "quarto-callout-note-color-frame", + fa_icon = "faInfo", + fa_icon_typst = "fa-info" + }, + warning = { + color = constants.kColorWarning, + background_color = constants.kBackgroundColorWarning, + latex_color = "quarto-callout-warning-color", + latex_frame_color = "quarto-callout-warning-color-frame", + fa_icon = "faExclamationTriangle", + fa_icon_typst = "fa-exclamation-triangle" + }, + important = { + color = constants.kColorImportant, + background_color = constants.kBackgroundColorImportant, + latex_color = "quarto-callout-important-color", + latex_frame_color = "quarto-callout-important-color-frame", + fa_icon = "faExclamation", + fa_icon_typst = "fa-exclamation" + }, + caution = { + color = constants.kColorCaution, + background_color = constants.kBackgroundColorCaution, + latex_color = "quarto-callout-caution-color", + latex_frame_color = "quarto-callout-caution-color-frame", + fa_icon = "faFire", + fa_icon_typst = "fa-fire" + }, + tip = { + color = constants.kColorTip, + background_color = constants.kBackgroundColorTip, + latex_color = "quarto-callout-tip-color", + latex_frame_color = "quarto-callout-tip-color-frame", + fa_icon = "faLightbulb", + fa_icon_typst = "fa-lightbulb" + }, + + __other = { + color = constants.kColorUnknown, + background_color = constants.kColorUnknown, + latex_color = "quarto-callout-color", + latex_frame_color = "quarto-callout-color-frame", + fa_icon = nil, + fa_icon_typst = nil + } +} + +setmetatable(callout_attrs, { + __index = function(tbl, key) + return tbl.__other + end +}) + +local function htmlColorForType(type) + return callout_attrs[type].color +end + +local function htmlBackgroundColorForType(type) + return callout_attrs[type].background_color +end + +local function latexColorForType(type) + return callout_attrs[type].latex_color +end + +local function latexFrameColorForType(type) + return callout_attrs[type].latex_frame_color +end + +local function iconForType(type) + return callout_attrs[type].fa_icon +end + +local function isBuiltInType(type) + local icon = iconForType(type) + return icon ~= nil +end + +local function displayName(type) + local defaultName = type:sub(1,1):upper()..type:sub(2) + return param("callout-" .. type .. "-title", defaultName) +end + +return { + decorate_callout_title_with_crossref = decorate_callout_title_with_crossref, + callout_attrs = callout_attrs, + + -- TODO capitalization + resolveCalloutContents = resolveCalloutContents, + docxCalloutImage = docxCalloutImage, + + htmlColorForType = htmlColorForType, + htmlBackgroundColorForType = htmlBackgroundColorForType, + latexColorForType = latexColorForType, + latexFrameColorForType = latexFrameColorForType, + iconForType = iconForType, + isBuiltInType = isBuiltInType, + displayName = displayName, +} \ No newline at end of file diff --git a/src/resources/filters/modules/classpredicates.lua b/src/resources/filters/modules/classpredicates.lua new file mode 100644 index 0000000000..dbf16d91d7 --- /dev/null +++ b/src/resources/filters/modules/classpredicates.lua @@ -0,0 +1,35 @@ +-- classpredicates.lua +-- Copyright (C) 2024 Posit Software, PBC + +local function isCell(el) + return el.classes:includes("cell") +end + +local function isCodeCellOutput(el) + return el.classes:includes("cell-output") +end + +local function isCallout(class) + return class == 'callout' or class:match("^callout%-") +end + +local function isDocxCallout(class) + return class == "docx-callout" +end + +local function isCodeCell(class) + return class == "cell" +end + +local function isCodeCellDisplay(class) + return class == "cell-output-display" +end + +return { + isCallout = isCallout, + isCell = isCell, + isCodeCell = isCodeCell, + isCodeCellDisplay = isCodeCellDisplay, + isCodeCellOutput = isCodeCellOutput, + isDocxCallout = isDocxCallout +} \ No newline at end of file diff --git a/src/resources/filters/modules/constants.lua b/src/resources/filters/modules/constants.lua index 6b1694681f..90cf63f1a9 100644 --- a/src/resources/filters/modules/constants.lua +++ b/src/resources/filters/modules/constants.lua @@ -131,6 +131,30 @@ local kLangCommentChars = { } local kDefaultCodeAnnotationComment = {"#"} +-- These colors are used as background colors with an opacity of 0.75 +local kColorUnknown = "909090" +local kColorNote = "0758E5" +local kColorImportant = "CC1914" +local kColorWarning = "EB9113" +local kColorTip = "00A047" +local kColorCaution = "FC5300" + +-- these colors are used with no-opacity +local kColorUnknownFrame = "acacac" +local kColorNoteFrame = "4582ec" +local kColorImportantFrame = "d9534f" +local kColorWarningFrame = "f0ad4e" +local kColorTipFrame = "02b875" +local kColorCautionFrame = "fd7e14" + +local kBackgroundColorUnknown = "e6e6e6" +local kBackgroundColorNote = "dae6fb" +local kBackgroundColorImportant = "f7dddc" +local kBackgroundColorWarning = "fcefdc" +local kBackgroundColorTip = "ccf1e3" +local kBackgroundColorCaution = "ffe5d0" + + return { kCitation = kCitation, kContainerId = kContainerId, @@ -199,5 +223,24 @@ return { kLangCommentChars = kLangCommentChars, kDefaultCodeAnnotationComment = kDefaultCodeAnnotationComment, kHtmlTableProcessing = kHtmlTableProcessing, - kHtmlPreTagProcessing = kHtmlPreTagProcessing + kHtmlPreTagProcessing = kHtmlPreTagProcessing, + + kColorUnknown = kColorUnknown, + kColorNote = kColorNote, + kColorImportant = kColorImportant, + kColorWarning = kColorWarning, + kColorTip = kColorTip, + kColorCaution = kColorCaution, + kColorUnknownFrame = kColorUnknownFrame, + kColorNoteFrame = kColorNoteFrame, + kColorImportantFrame = kColorImportantFrame, + kColorWarningFrame = kColorWarningFrame, + kColorTipFrame = kColorTipFrame, + kColorCautionFrame = kColorCautionFrame, + kBackgroundColorUnknown = kBackgroundColorUnknown, + kBackgroundColorNote = kBackgroundColorNote, + kBackgroundColorImportant = kBackgroundColorImportant, + kBackgroundColorWarning = kBackgroundColorWarning, + kBackgroundColorTip = kBackgroundColorTip, + kBackgroundColorCaution = kBackgroundColorCaution, } diff --git a/src/resources/filters/modules/import_all.lua b/src/resources/filters/modules/import_all.lua new file mode 100644 index 0000000000..13f1360e12 --- /dev/null +++ b/src/resources/filters/modules/import_all.lua @@ -0,0 +1,22 @@ +-- import_all.lua +-- imports all modules into _quarto.modules + +_quarto.modules = { + astshortcode = require("modules/astshortcode"), + authors = require("modules/authors"), + callouts = require("modules/callouts"), + classpredicates = require("modules/classpredicates"), + constants = require("modules/constants"), + dashboard = require("modules/dashboard"), + filenames = require("modules/filenames"), + filters = require("modules/filters"), + license = require("modules/license"), + lightbox = require("modules/lightbox"), + mediabag = require("modules/mediabag"), + openxml = require("modules/openxml"), + patterns = require("modules/patterns"), + scope = require("modules/scope"), + string = require("modules/string"), + tablecolwidths = require("modules/tablecolwidths"), + typst = require("modules/typst") +} \ No newline at end of file diff --git a/src/resources/filters/modules/openxml.lua b/src/resources/filters/modules/openxml.lua new file mode 100644 index 0000000000..d14eda3be2 --- /dev/null +++ b/src/resources/filters/modules/openxml.lua @@ -0,0 +1,35 @@ +-- openxml.lua +-- Copyright (C) 2024 Posit Software, PBC + +local function openXmlPara(para, spacing) + local xmlPara = pandoc.Para({ + pandoc.RawInline("openxml", "\n\n") + }) + tappend(xmlPara.content, para.content) + return xmlPara +end + +local function removeParagraphPadding(contents) + if #contents > 0 then + + if #contents == 1 then + if contents[1].t == "Para" then + contents[1] = openXmlPara(contents[1], 'w:before="16" w:after="16"') + end + else + if contents[1].t == "Para" then + contents[1] = openXmlPara(contents[1], 'w:before="16"') + end + + if contents[#contents].t == "Para" then + contents[#contents] = openXmlPara(contents[#contents], 'w:after="16"') + end + end + end +end + +return { + -- TODO capitalization + openXmlPara = openXmlPara, + removeParagraphPadding = removeParagraphPadding +} \ No newline at end of file diff --git a/src/resources/filters/quarto-post/docx.lua b/src/resources/filters/quarto-post/docx.lua index ecd360d53b..d3fab3d020 100644 --- a/src/resources/filters/quarto-post/docx.lua +++ b/src/resources/filters/quarto-post/docx.lua @@ -3,201 +3,196 @@ -- -- renders AST nodes to docx -local constants = require("modules/constants") - -local function calloutDocxDefault(node, type, hasIcon) - local title = quarto.utils.as_inlines(node.title) - local color = htmlColorForType(type) - local backgroundColor = htmlBackgroundColorForType(type) - - local tablePrefix = [[ - - - - - - - - - - - - - - - - - - - - - - +function calloutDocx(node) + local function calloutDocxDefault(node, type, hasIcon) + local title = quarto.utils.as_inlines(node.title) + local color = _quarto.modules.callouts.htmlColorForType(type) + local backgroundColor = _quarto.modules.callouts.htmlBackgroundColorForType(type) + + local tablePrefix = [[ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ]] + local calloutContents = pandoc.List({ + pandoc.RawBlock("openxml", tablePrefix:gsub('$background', backgroundColor):gsub('$color', color)), + }) + + -- Create a title if there isn't already one + if pandoc.utils.stringify(title) == "" then + title = quarto.utils.as_inlines(pandoc.Plain(_quarto.modules.callouts.displayName(node.type))) + end + + -- add the image to the title, if needed + local calloutImage = _quarto.modules.callouts.docxCalloutImage(type); + if hasIcon and calloutImage ~= nil then + -- Create a paragraph with the icon, spaces, and text + local image_title = pandoc.List({ + pandoc.RawInline("openxml", '\n\n\n'), + calloutImage, + pandoc.Space(), + pandoc.Space()}) + tappend(image_title, title) + calloutContents:insert(pandoc.Para(image_title)) + else + local titleRaw = _quarto.modules.openxml.openXmlPara(pandoc.Para(title), 'w:before="16" w:after="16"') + calloutContents:insert(titleRaw) + end + + + -- end the title row and start the body row + local tableMiddle = [[ + + + + + + + - - - + + - ]] - local calloutContents = pandoc.List({ - pandoc.RawBlock("openxml", tablePrefix:gsub('$background', backgroundColor):gsub('$color', color)), - }) - - -- Create a title if there isn't already one - -- if title == nil then - -- title = pandoc.List({pandoc.Str(displayName(type))}) - -- end - if pandoc.utils.stringify(title) == "" then - title = quarto.utils.as_inlines(pandoc.Plain(displayName(node.type))) - end - - -- add the image to the title, if needed - local calloutImage = docxCalloutImage(type); - if hasIcon and calloutImage ~= nil then - -- Create a paragraph with the icon, spaces, and text - local image_title = pandoc.List({ - pandoc.RawInline("openxml", '\n\n\n'), - calloutImage, - pandoc.Space(), - pandoc.Space()}) - tappend(image_title, title) - calloutContents:insert(pandoc.Para(image_title)) - else - local titleRaw = openXmlPara(pandoc.Para(title), 'w:before="16" w:after="16"') - calloutContents:insert(titleRaw) - end - - -- end the title row and start the body row - local tableMiddle = [[ + ]] + calloutContents:insert(pandoc.Div(pandoc.RawBlock("openxml", tableMiddle))) + + -- the main contents of the callout + local contents = quarto.utils.as_blocks(node.content) + + -- ensure there are no nested callouts + if contents:find_if(function(el) + return is_regular_node(el, "Div") and el.attr.classes:find_if(_quarto.modules.classpredicates.isDocxCallout) ~= nil + end) ~= nil then + fail("Found a nested callout in the document. Please fix this issue and try again.") + end + + -- remove padding from existing content and add it + _quarto.modules.openxml.removeParagraphPadding(contents) + tappend(calloutContents, contents) + + -- close the table + local suffix = pandoc.List({pandoc.RawBlock("openxml", [[ - - - - - - - - - - - - - - ]] - calloutContents:insert(pandoc.Div(pandoc.RawBlock("openxml", tableMiddle))) - - -- the main contents of the callout - local contents = quarto.utils.as_blocks(node.content) - - -- ensure there are no nested callouts - if contents:find_if(function(el) - return is_regular_node(el, "Div") and el.attr.classes:find_if(isDocxCallout) ~= nil - end) ~= nil then - fail("Found a nested callout in the document. Please fix this issue and try again.") - end + + + ]])}) + tappend(calloutContents, suffix) - -- remove padding from existing content and add it - removeParagraphPadding(contents) - tappend(calloutContents, contents) - - -- close the table - local suffix = pandoc.List({pandoc.RawBlock("openxml", [[ - - - - ]])}) - tappend(calloutContents, suffix) - - -- return the callout - local callout = pandoc.Div(calloutContents, pandoc.Attr("", {"docx-callout"})) - return callout -end - - -local function calloutDocxSimple(node, type, hasIcon) - local color = htmlColorForType(type) - local title = quarto.utils.as_inlines(node.title) - - local tablePrefix = [[ - - - - - - - - - - - - - - - - - - - ]] - - local prefix = pandoc.List({ - pandoc.RawBlock("openxml", tablePrefix:gsub('$color', color)), - }) - - local calloutImage = docxCalloutImage(type) - if hasIcon and calloutImage ~= nil then - local imagePara = pandoc.Para({ - pandoc.RawInline("openxml", '\n\n\n'), calloutImage}) - prefix:insert(pandoc.RawBlock("openxml", '')) - prefix:insert(imagePara) - prefix:insert(pandoc.RawBlock("openxml", "\n")) - else - prefix:insert(pandoc.RawBlock("openxml", '')) - end - - local suffix = pandoc.List({pandoc.RawBlock("openxml", [[ - - - - ]])}) - - local calloutContents = pandoc.List({}) - tappend(calloutContents, prefix) - - -- deal with the title, if present - if title ~= nil then - local titlePara = pandoc.Para(pandoc.Strong(title)) - calloutContents:insert(openXmlPara(titlePara, 'w:before="16" w:after="64"')) + -- return the callout + local callout = pandoc.Div(calloutContents, pandoc.Attr("", {"docx-callout"})) + return callout end - -- convert to open xml paragraph - local contents = pandoc.List({}) -- use as pandoc.List() for find_if - contents:extend(quarto.utils.as_blocks(node.content)) - removeParagraphPadding(contents) - -- ensure there are no nested callouts - if contents:find_if(function(el) - return is_regular_node(el, "Div") and el.attr.classes:find_if(isDocxCallout) ~= nil - end) ~= nil then - fail("Found a nested callout in the document. Please fix this issue and try again.") + local function calloutDocxSimple(node, type, hasIcon) + local color = _quarto.modules.callouts.htmlColorForType(type) + local title = quarto.utils.as_inlines(node.title) + + local tablePrefix = [[ + + + + + + + + + + + + + + + + + + + ]] + + local prefix = pandoc.List({ + pandoc.RawBlock("openxml", tablePrefix:gsub('$color', color)), + }) + + local calloutImage = _quarto.modules.callouts.docxCalloutImage(type) + if hasIcon and calloutImage ~= nil then + local imagePara = pandoc.Para({ + pandoc.RawInline("openxml", '\n\n\n'), calloutImage}) + prefix:insert(pandoc.RawBlock("openxml", '')) + prefix:insert(imagePara) + prefix:insert(pandoc.RawBlock("openxml", "\n")) + else + prefix:insert(pandoc.RawBlock("openxml", '')) + end + + local suffix = pandoc.List({pandoc.RawBlock("openxml", [[ + + + + ]])}) + + local calloutContents = pandoc.List({}) + tappend(calloutContents, prefix) + + -- deal with the title, if present + if title ~= nil then + local titlePara = pandoc.Para(pandoc.Strong(title)) + calloutContents:insert(_quarto.modules.openxml.openXmlPara(titlePara, 'w:before="16" w:after="64"')) + end + + -- convert to open xml paragraph + local contents = pandoc.List({}) -- use as pandoc.List() for find_if + contents:extend(quarto.utils.as_blocks(node.content)) + _quarto.modules.openxml.removeParagraphPadding(contents) + + -- ensure there are no nested callouts + if contents:find_if(function(el) + return is_regular_node(el, "Div") and el.attr.classes:find_if(_quarto.modules.classpredicates.isDocxCallout) ~= nil + end) ~= nil then + fail("Found a nested callout in the document. Please fix this issue and try again.") + end + + tappend(calloutContents, contents) + tappend(calloutContents, suffix) + + local callout = pandoc.Div(calloutContents, pandoc.Attr("", {"docx-callout"})) + return callout end - - tappend(calloutContents, contents) - tappend(calloutContents, suffix) - - local callout = pandoc.Div(calloutContents, pandoc.Attr("", {"docx-callout"})) - return callout -end - -function calloutDocx(node) - node = decorate_callout_title_with_crossref(node) + + node = _quarto.modules.callouts.decorate_callout_title_with_crossref(node) local type = node.type local appearance = node.appearance local hasIcon = node.icon - if appearance == constants.kCalloutAppearanceDefault then + if appearance == _quarto.modules.constants.kCalloutAppearanceDefault then return calloutDocxDefault(node, type, hasIcon) else return calloutDocxSimple(node, type, hasIcon) diff --git a/src/resources/filters/quarto-post/jats.lua b/src/resources/filters/quarto-post/jats.lua index 0fd32fbf3b..8f267229df 100644 --- a/src/resources/filters/quarto-post/jats.lua +++ b/src/resources/filters/quarto-post/jats.lua @@ -3,12 +3,6 @@ local normalizeAuthors = require 'modules/authors' local normalizeLicense = require 'modules/license' -local constants = require("modules/constants") - -local function isCell(el) - return el.classes:includes("cell") -end - local function jatsMeta(meta) -- inspect the meta and set flags that will aide the rendering of -- the JATS template by providing some synthesize properties @@ -30,15 +24,15 @@ local function jatsMeta(meta) local hasLicense = meta[normalizeLicense.constants.license] ~= nil local hasPermissions = hasCopyright or hasLicense - if meta[constants.kQuartoInternal] == nil then - meta[constants.kQuartoInternal] = {} + if meta[_quarto.modules.constants.kQuartoInternal] == nil then + meta[_quarto.modules.constants.kQuartoInternal] = {} end - meta[constants.kQuartoInternal][constants.kHasAuthorNotes] = hasNotes; - meta[constants.kQuartoInternal][constants.kHasPermissions] = hasPermissions; + meta[_quarto.modules.constants.kQuartoInternal][_quarto.modules.constants.kHasAuthorNotes] = hasNotes; + meta[_quarto.modules.constants.kQuartoInternal][_quarto.modules.constants.kHasPermissions] = hasPermissions; -- normalize keywords into tags if they're present and tags aren't - if meta[constants.kTags] == nil and meta[constants.kKeywords] ~= nil and meta[constants.kKeywords].t == "Table" then - meta[constants.kKeywords] = meta[constants.kTags] + if meta[_quarto.modules.constants.kTags] == nil and meta[_quarto.modules.constants.kKeywords] ~= nil and meta[_quarto.modules.constants.kKeywords].t == "Table" then + meta[_quarto.modules.constants.kKeywords] = meta[_quarto.modules.constants.kTags] end return meta @@ -64,7 +58,7 @@ function unrollDiv(div, fnSkip) end function jatsCallout(node) - local contents = resolveCalloutContents(node, true) + local contents = _quarto.modules.callouts.resolveCalloutContents(node, true) local boxedStart = '' if node.id and node.id ~= "" then @@ -107,15 +101,7 @@ end function jatsSubarticle() if _quarto.format.isJatsOutput() then - - local isCodeCell = function(el) - return not el.classes:includes('markdown') - end - - local isCodeCellOutput = function(el) - return el.classes:includes("cell-output") - end - + local ensureValidIdentifier = function(identifier) -- Identifiers may not start with a digit, so add a prefix -- if necessary to ensure that they're valid @@ -160,8 +146,8 @@ function jatsSubarticle() Div = function(div) -- this is a notebook cell, handle it - if isCell(div) then - if isCodeCell(div) then + if _quarto.modules.classpredicates.isCell(div) then + if _quarto.modules.classpredicates.isCodeCell(div) then -- if this is an executable notebook cell, walk the contents and add identifiers -- to the outputs @@ -177,7 +163,7 @@ function jatsSubarticle() local outputEls = pandoc.List() local otherEls = pandoc.List() for i, v in ipairs(div.content) do - if is_regular_node(v, "Div") and isCodeCellOutput(v) then + if is_regular_node(v, "Div") and _quarto.modules.classpredicates.isCodeCellOutput(v) then outputEls:extend({v}) else otherEls:extend({v}) @@ -191,26 +177,26 @@ function jatsSubarticle() local count = 0 div = _quarto.ast.walk(div, { Div = function(childEl) - if (isCodeCellOutput(childEl)) then + if (_quarto.modules.classpredicates.isCodeCellOutput(childEl)) then childEl.identifier = parentId .. '-output-' .. count count = count + 1 - return renderCellOutput(childEl, constants.kNoteBookOutput) + return renderCellOutput(childEl, _quarto.modules.constants.kNoteBookOutput) end end }) -- render the cell - return renderCell(div, constants.kNoteBookCode) + return renderCell(div, _quarto.modules.constants.kNoteBookCode) else if #div.content == 0 then -- eat empty markdown cells return {} else -- the is a valid markdown cell, let it through - return renderCell(div, constants.kNoteBookContent) + return renderCell(div, _quarto.modules.constants.kNoteBookContent) end end - elseif isCodeCellOutput(div) then + elseif _quarto.modules.classpredicates.isCodeCellOutput(div) then -- do nothing else -- Forward the identifier from a table div onto the table itself and @@ -222,7 +208,7 @@ function jatsSubarticle() else -- otherwise, if this is a div, we can unroll its contents return unrollDiv(div, function(el) - return isCodeCellOutput(el) or isCell(el) + return _quarto.modules.classpredicates.isCodeCellOutput(el) or _quarto.modules.classpredicates.isCell(el) end) end diff --git a/src/resources/filters/quarto-post/latex.lua b/src/resources/filters/quarto-post/latex.lua index afe9159a71..a01d89f0c2 100644 --- a/src/resources/filters/quarto-post/latex.lua +++ b/src/resources/filters/quarto-post/latex.lua @@ -3,8 +3,6 @@ -- -- renders AST nodes to LaTeX -local constants = require("modules/constants") - local callout_counters = {} local function ensure_callout_counter(ref) @@ -32,10 +30,10 @@ function latexCalloutBoxDefault(title, callout_type, icon, callout) local borderWidth = '.15mm' local borderRadius = '.35mm' local leftPad = '2mm' - local color = latexColorForType(callout_type) - local frameColor = latexFrameColorForType(callout_type) + local color = _quarto.modules.callouts.latexColorForType(callout_type) + local frameColor = _quarto.modules.callouts.latexFrameColorForType(callout_type) - local iconForType = iconForType(callout_type) + local iconForType = _quarto.modules.callouts.iconForType(callout_type) local calloutContents = pandoc.List({}); @@ -101,8 +99,8 @@ function latexCalloutBoxSimple(title, type, icon, callout) local borderWidth = '.15mm' local borderRadius = '.35mm' local leftPad = '2mm' - local color = latexColorForType(type) - local colorFrame = latexFrameColorForType(type) + local color = _quarto.modules.callouts.latexColorForType(type) + local colorFrame = _quarto.modules.callouts.latexFrameColorForType(type) if title == nil then title = "" @@ -145,7 +143,7 @@ function latexCalloutBoxSimple(title, type, icon, callout) local endInlines = { pandoc.RawInline('latex', '\n\\end{tcolorbox}') } -- generate the icon and use a minipage to position it - local iconForCat = iconForType(type) + local iconForCat = _quarto.modules.callouts.iconForType(type) if icon ~= false and iconForCat ~= nil then local iconName = '\\' .. iconForCat local iconColSize = '5.5mm' @@ -360,9 +358,9 @@ function render_latex() -- generate the callout box local callout - if calloutAppearance == constants.kCalloutAppearanceDefault then + if calloutAppearance == _quarto.modules.constants.kCalloutAppearanceDefault then if title == nil then - title = displayName(type) + title = _quarto.modules.callouts.displayName(type) else title = pandoc.write(pandoc.Pandoc(title), 'latex') end diff --git a/tests/docs/smoke-all/2024/01/31/8507.docx.snapshot b/tests/docs/smoke-all/2024/01/31/8507.docx.snapshot index fb2e0bfc4a7492241d4b6f9667f7877021f3100a..231b42d5c08423fb76ff9988b5606bf3928f20bb 100644 GIT binary patch delta 8483 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 DZ>n-K