Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: xref-able callouts #7161

Merged
merged 6 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/resources/filters/ast/customnodes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ function is_custom_node(node, name)
return false
end

function ensure_custom(node)
if pandoc.utils.type(node) == "Block" or pandoc.utils.type(node) == "Inline" then
local result = _quarto.ast.resolve_custom_data(node)
return result or node -- it'll never be nil or false, but the lua analyzer doesn't know that
end
return node
end

-- use this instead of node.t == "Div" so that custom nodes
-- are not considered Divs
function is_regular_node(node, name)
Expand Down
8 changes: 8 additions & 0 deletions src/resources/filters/common/crossref.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@
--
-- common crossref functions/data

function add_crossref(label, type, title)
if pandoc.utils.type(title) ~= "Blocks" then
title = quarto.utils.as_blocks(title)
end
local order = indexNextOrder(type)
indexAddEntry(label, nil, order, title)
return order
end
1 change: 1 addition & 0 deletions src/resources/filters/crossref/crossref.lua
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import("../common/base64.lua")
import("../common/citations.lua")
import("../common/colors.lua")
import("../common/collate.lua")
import("../common/crossref.lua")
import("../common/debug.lua")
import("../common/error.lua")
import("../common/figures.lua")
Expand Down
10 changes: 5 additions & 5 deletions src/resources/filters/crossref/refs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ function resolveRefs()
end

-- all valid ref types (so we can provide feedback when one doesn't match)
local refTypes = validRefTypes()
local refTypes = valid_ref_types()

-- scan citations for refs
local refs = pandoc.List()
for i, cite in ipairs (citeEl.citations) do
-- get the label and type, and note if the label is uppercase
local label = cite.id
local type = refType(label)
if type ~= nil and isValidRefType(type) then
if type ~= nil and is_valid_ref_type(type) then
local upper = not not string.match(cite.id, "^[A-Z]")

-- convert the first character of the label to lowercase for lookups
Expand Down Expand Up @@ -199,11 +199,11 @@ function refLabelPattern(type)
return "{#(" .. type .. "%-[^ }]+)}"
end

function isValidRefType(type)
return tcontains(validRefTypes(), type)
function is_valid_ref_type(type)
return tcontains(valid_ref_types(), type)
end

function validRefTypes()
function valid_ref_types()
local types = tkeys(theorem_types)
for k, _ in pairs(crossref.categories.by_ref_type) do
table.insert(types, k)
Expand Down
4 changes: 2 additions & 2 deletions src/resources/filters/crossref/theorems.lua
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ function crossref_theorems()
Theorem = function(thm)
local label = thm.identifier
local type = refType(label)
thm.order = indexNextOrder(type)
indexAddEntry(label, nil, thm.order, { thm.name })
local title = quarto.utils.as_blocks(thm.name)
thm.order = add_crossref(label, type, title)
return thm
end,
Div = function(el)
Expand Down
189 changes: 137 additions & 52 deletions src/resources/filters/customnodes/callout.lua
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ _quarto.ast.add_handler({
appearance = appearance,
icon = icon,
type = t,
attr = tbl.attr,
attr = tbl.attr or pandoc.Attr(),
}
end
})
Expand All @@ -117,51 +117,49 @@ function docx_callout_and_table_fixup()

-- Insert paragraphs between consecutive callouts or tables for docx
Blocks = function(blocks)
if _quarto.format.isDocxOutput() then
local lastWasCallout = false
local lastWasTableOrFigure = false
local newBlocks = pandoc.List()
for i,el in ipairs(blocks) do
-- determine what this block is
local isCallout = el.t == "Callout"
local isTableOrFigure = el.t == "Table" or isFigureDiv(el) or (discoverFigure(el, true) ~= nil)
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)
if isCodeCell and (isCodeCellTable(el) or isCodeCellFigure(el)) then
isTableOrFigure = true
end

-- insert spacer if appropriate
local insertSpacer = false
if isCallout and (lastWasCallout or lastWasTableOrFigure) then
insertSpacer = true
end
if isCodeBlock and lastWasCallout then
insertSpacer = true
end
if isTableOrFigure and lastWasTableOrFigure then
insertSpacer = true
end
local lastWasCallout = false
local lastWasTableOrFigure = false
local newBlocks = pandoc.List()
for i,el in ipairs(blocks) do
-- determine what this block is
local isCallout = is_custom_node(el, "Callout")
local isTableOrFigure = is_custom_node(el, "FloatRefTarget") or el.t == "Table" or isFigureDiv(el) or (discoverFigure(el, true) ~= nil)
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)
if isCodeCell and (isCodeCellTable(el) or isCodeCellFigure(el)) then
isTableOrFigure = true
end

-- insert spacer if appropriate
local insertSpacer = false
if isCallout and (lastWasCallout or lastWasTableOrFigure) then
insertSpacer = true
end
if isCodeBlock and lastWasCallout then
insertSpacer = true
end
if isTableOrFigure and lastWasTableOrFigure then
insertSpacer = true
end

if insertSpacer then
newBlocks:insert(pandoc.Para(stringToInlines(" ")))
end
if insertSpacer then
newBlocks:insert(pandoc.Para(stringToInlines(" ")))
end

-- always insert
newBlocks:insert(el)
-- always insert
newBlocks:insert(el)

-- record last state
lastWasCallout = isCallout
lastWasTableOrFigure = isTableOrFigure
end
-- record last state
lastWasCallout = isCallout
lastWasTableOrFigure = isTableOrFigure
end

if #newBlocks > #blocks then
return newBlocks
else
return nil
end
if #newBlocks > #blocks then
return newBlocks
else
return nil
end
end

Expand Down Expand Up @@ -218,8 +216,48 @@ function isCodeCellFigure(el)
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
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)
Expand All @@ -229,18 +267,27 @@ function calloutDiv(node)
div.content:insert(c)
end
local title = quarto.utils.as_inlines(node.title)
local type = node.type
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 = displayName(type)
title = quarto.utils.as_inlines(pandoc.Plain(displayName(node.type)))
end

local identifier = node.attr.identifier
if identifier ~= "" then
node.attr.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

-- Make an outer card div and transfer classes and id
local calloutDiv = pandoc.Div({})
calloutDiv.attr = (node.attr or pandoc.Attr()):clone()
calloutDiv.attr = node.attr:clone()
div.attr.classes = pandoc.List()
div.attr.classes:insert("callout-body-container")

Expand All @@ -255,7 +302,7 @@ function calloutDiv(node)
local noicon = ""

-- Check to see whether this is a recognized type
if icon == false or not isBuiltInType(type) or type == nil then
if icon == false or not isBuiltInType(callout_type) or type == nil then
noicon = " no-icon"
calloutDiv.attr.classes:insert("no-icon")
end
Expand All @@ -282,7 +329,7 @@ function calloutDiv(node)
if collapse ~= nil then

-- collapse default value
local expandedAttrVal= "true"
local expandedAttrVal = "true"
if collapse == "true" or collapse == true then
expandedAttrVal = "false"
end
Expand Down Expand Up @@ -381,7 +428,7 @@ function epubCallout(node)
end
attributes:insert("callout-style-" .. calloutAppearance)

local result = pandoc.Div({calloutBody}, pandoc.Attr(node.id or "", attributes))
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
Expand All @@ -392,10 +439,12 @@ function epubCallout(node)

end

function simpleCallout(node)
function simpleCallout(node)
node = decorate_callout_title_with_crossref(node)
local contents = resolveCalloutContents(node, true)
local callout = pandoc.BlockQuote(contents)
return pandoc.Div(callout, pandoc.Attr(node.id or ""))
local result = pandoc.Div(callout, pandoc.Attr(node.attr.identifier or ""))
return result
end

function resolveCalloutContents(node, require_title)
Expand Down Expand Up @@ -664,11 +713,47 @@ end, function(callout)
title = pandoc.Plain(displayName(callout.type))
end

return typst_function_call("callout", {
local typst_callout = typst_function_call("callout", {
{ "body", as_typst_content(callout.content) },
{ "title", as_typst_content(title) },
{ "background_color", pandoc.RawInline("typst", background_color) },
{ "icon_color", pandoc.RawInline("typst", icon_color) },
{ "icon", pandoc.RawInline("typst", "" .. icon .. "()")}
})
end)

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)
local type = refType(callout.attr.identifier)
if type == nil or not is_valid_ref_type(type) then
return nil
end
local label = callout.attr.identifier
local title = quarto.utils.as_blocks(callout.title)
callout.order = add_crossref(label, type, title)
return callout
end
}
end
7 changes: 0 additions & 7 deletions src/resources/filters/customnodes/floatreftarget.lua
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,6 @@ end, function(float)
return scaffold(float.content)
end)

local function ensure_custom(node)
if pandoc.utils.type(node) == "Block" or pandoc.utils.type(node) == "Inline" then
return _quarto.ast.resolve_custom_data(node)
end
return node
end

function is_unlabeled_float(float)
-- from src/resources/filters/common/refs.lua
return float.identifier:match("^%a+%-539a35d47e664c97a50115a146a7f1bd%-")
Expand Down
3 changes: 2 additions & 1 deletion src/resources/filters/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import("./common/base64.lua")
import("./common/citations.lua")
import("./common/colors.lua")
import("./common/collate.lua")
import("./common/crossref.lua")
import("./common/debug.lua")
import("./common/error.lua")
import("./common/figures.lua")
Expand Down Expand Up @@ -333,7 +334,6 @@ local quarto_post_filters = {
-- format-specific rendering
{ name = "post-render-asciidoc", filter = render_asciidoc() },
{ name = "post-render-latex", filter = render_latex() },
{ name = "post-render-docx", filter = render_docx() },
{ name = "post-render-typst", filter = render_typst() },
{ name = "post-render-dashboard", filters = render_dashboard()},

Expand Down Expand Up @@ -397,6 +397,7 @@ local quarto_crossref_filters = {
crossref_figures(),
equations(),
crossref_theorems(),
crossref_callouts(),
})},

{ name = "crossref-resolveRefs", filter = resolveRefs(),
Expand Down
Loading
Loading