Skip to content

Commit

Permalink
Merge pull request #7161 from quarto-dev/feature/callout-xref
Browse files Browse the repository at this point in the history
feature: xref-able callouts
  • Loading branch information
cscheid authored Oct 30, 2023
2 parents a555e73 + 4f3854d commit ff1a8c4
Show file tree
Hide file tree
Showing 16 changed files with 420 additions and 104 deletions.
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

0 comments on commit ff1a8c4

Please sign in to comment.