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 fb2e0bfc4a..231b42d5c0 100644 Binary files a/tests/docs/smoke-all/2024/01/31/8507.docx.snapshot and b/tests/docs/smoke-all/2024/01/31/8507.docx.snapshot differ