Skip to content

Commit 641c36e

Browse files
committed
Add jog Lua module
1 parent 46cdcc3 commit 641c36e

File tree

2 files changed

+310
-1
lines changed

2 files changed

+310
-1
lines changed

src/resources/filters/modules/import_all.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ _quarto.modules = {
1111
dashboard = require("modules/dashboard"),
1212
filenames = require("modules/filenames"),
1313
filters = require("modules/filters"),
14+
jog = require("modules/jog"),
1415
license = require("modules/license"),
1516
lightbox = require("modules/lightbox"),
1617
mediabag = require("modules/mediabag"),
@@ -20,4 +21,4 @@ _quarto.modules = {
2021
string = require("modules/string"),
2122
tablecolwidths = require("modules/tablecolwidths"),
2223
typst = require("modules/typst")
23-
}
24+
}
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
--- jog.lua – walk the pandoc AST with context, and with inplace modification.
2+
---
3+
--- Copyright: © 2024 Albert Krewinkel, Carlos Scheidegger
4+
--- License: MIT – see LICENSE for details
5+
6+
local pandoc = require 'pandoc'
7+
local List = require 'pandoc.List'
8+
9+
local debug_getmetatable = debug.getmetatable
10+
11+
--- Get the element type; like pandoc.utils.type, but faster.
12+
local function ptype (x)
13+
local mt = debug_getmetatable(x)
14+
if mt then
15+
local name = mt.__name
16+
return name or type(x)
17+
else
18+
return type(x)
19+
end
20+
end
21+
22+
--- Checks whether the object is a list type.
23+
local listy_type = {
24+
Blocks = true,
25+
Inlines = true,
26+
List = true,
27+
}
28+
29+
local function run_filter_function (fn, element, context)
30+
if fn == nil then
31+
return element
32+
end
33+
34+
local result, continue = fn(element, context)
35+
if result == nil then
36+
return element, continue
37+
else
38+
return result, continue
39+
end
40+
end
41+
42+
--- Set of Block and Inline tags that are leaf nodes.
43+
local leaf_node_tags = {
44+
Code = true,
45+
CodeBlock = true,
46+
HorizontalRule = true,
47+
LineBreak = true,
48+
Math = true,
49+
RawBlock = true,
50+
RawInline = true,
51+
Space = true,
52+
SoftBreak = true,
53+
Str = true,
54+
}
55+
56+
--- Set of Block and Inline tags that have nested items in `.contents` only.
57+
local content_only_node_tags = {
58+
-- Blocks with Blocks content
59+
BlockQuote = true,
60+
Div = true,
61+
Header = true,
62+
-- Blocks with Inlines content
63+
Para = true,
64+
Plain = true,
65+
-- Blocks with List content
66+
LineBlock = true,
67+
BulletList = true,
68+
OrderedList = true,
69+
DefinitionList = true,
70+
-- Inlines with Inlines content
71+
Cite = true,
72+
Emph = true,
73+
Link = true,
74+
Quoted = true,
75+
SmallCaps = true,
76+
Span = true,
77+
Strikeout = true,
78+
Strong = true,
79+
Subscript = true,
80+
Superscript = true,
81+
Underline = true,
82+
-- Inline with Blocks content
83+
Note = true,
84+
}
85+
86+
--- Apply the filter on the nodes below the given element.
87+
local function recurse (element, tp, jogger)
88+
tp = tp or ptype(element)
89+
local tag = element.tag
90+
if leaf_node_tags[tag] then
91+
-- do nothing, cannot traverse any deeper
92+
elseif tp == 'table' then
93+
for key, value in pairs(element) do
94+
element[key] = jogger(value)
95+
end
96+
elseif content_only_node_tags[tag] or tp == 'pandoc Cell' then
97+
element.content = jogger(element.content)
98+
elseif tag == 'Image' then
99+
element.caption = jogger(element.caption)
100+
elseif tag == 'Table' then
101+
element.caption = jogger(element.caption)
102+
element.head = jogger(element.head)
103+
element.bodies = jogger(element.bodies)
104+
element.foot = jogger(element.foot)
105+
elseif tag == 'Figure' then
106+
element.caption = jogger(element.caption)
107+
element.content = jogger(element.content)
108+
elseif tp == 'Meta' then
109+
for key, value in pairs(element) do
110+
element[key] = jogger(value)
111+
end
112+
elseif tp == 'pandoc Row' then
113+
element.cells = jogger(element.cells)
114+
elseif tp == 'pandoc TableHead' or tp == 'pandoc TableFoot' then
115+
element.rows = jogger(element.rows)
116+
elseif tp == 'Blocks' or tp == 'Inlines' then
117+
local expected_itemtype = tp == 'Inlines' and 'Inline' or 'Block'
118+
local pos = 0
119+
local filtered_index = 1
120+
local filtered_items = element:map(function (x)
121+
return jogger(x)
122+
end)
123+
local item = filtered_items[filtered_index]
124+
local itemtype
125+
while item ~= nil do
126+
itemtype = ptype(item)
127+
if itemtype ~= tp and itemtype ~= expected_itemtype then
128+
-- neither the list type nor the list's item type. Try to convert.
129+
item = pandoc[tp](item)
130+
itemtype = tp
131+
end
132+
if itemtype == tp then
133+
local sublist_index = 1
134+
local sublistitem = item[sublist_index]
135+
while sublistitem ~= nil do
136+
pos = pos + 1
137+
element[pos] = sublistitem
138+
sublist_index = sublist_index + 1
139+
sublistitem = item[sublist_index]
140+
end
141+
else
142+
-- not actually a sublist, just an element
143+
pos = pos + 1
144+
element[pos] = item
145+
end
146+
filtered_index = filtered_index + 1
147+
item = filtered_items[filtered_index]
148+
end
149+
-- unset remaining indices if the new list is shorter than the old
150+
pos = pos + 1
151+
while element[pos] do
152+
element[pos] = nil
153+
pos = pos + 1
154+
end
155+
elseif tp == 'List' then
156+
local i, item = 1, element[1]
157+
while item do
158+
element[i] = jogger(item)
159+
i, item = i+1, element[i+1]
160+
end
161+
elseif tp == 'Pandoc' then
162+
element.meta = jogger(element.meta)
163+
element.blocks = jogger(element.blocks)
164+
else
165+
error("Don't know how to traverse " .. (element.t or tp))
166+
end
167+
return element
168+
end
169+
170+
local non_joggable_types = {
171+
['Attr'] = true,
172+
['boolean'] = true,
173+
['nil'] = true,
174+
['number'] = true,
175+
['string'] = true,
176+
}
177+
178+
local function get_filter_function(element, filter, tp)
179+
local result = nil
180+
if non_joggable_types[tp] or tp == 'table' then
181+
return nil
182+
elseif tp == 'Block' then
183+
return filter[element.tag] or filter.Block
184+
elseif tp == 'Inline' then
185+
return filter[element.tag] or filter.Inline
186+
else
187+
return filter[tp]
188+
end
189+
end
190+
191+
local function make_jogger (filter, context)
192+
local is_topdown = filter.traverse == 'topdown'
193+
local jogger
194+
195+
jogger = function (element)
196+
if context then
197+
context:insert(element)
198+
end
199+
local tp = ptype(element)
200+
local result, continue = nil, true
201+
if non_joggable_types[tp] then
202+
result = element
203+
elseif tp == 'table' then
204+
result = recurse(element, tp, jogger)
205+
else
206+
local fn = get_filter_function(element, filter, tp)
207+
if is_topdown then
208+
result, continue = run_filter_function(fn, element, context)
209+
if continue ~= false then
210+
result = recurse(result, tp, jogger)
211+
end
212+
else
213+
element = recurse(element, tp, jogger)
214+
result = run_filter_function(fn, element, context)
215+
end
216+
end
217+
218+
if context then
219+
context:remove() -- remove this element from the context
220+
end
221+
return result
222+
end
223+
return jogger
224+
end
225+
226+
local element_name_map = {
227+
Cell = 'pandoc Cell',
228+
Row = 'pandoc Row',
229+
TableHead = 'pandoc TableHead',
230+
TableFoot = 'pandoc TableFoot',
231+
}
232+
233+
--- Function to traverse the pandoc AST with context.
234+
local function jog(element, filter)
235+
local context = filter.context and List{} or nil
236+
237+
-- Table elements have a `pandoc ` prefix in the name
238+
for from, to in pairs(element_name_map) do
239+
filter[to] = filter[from]
240+
end
241+
242+
-- Check if we can just call Pandoc and Meta and be done
243+
if ptype(element) == 'Pandoc' then
244+
local must_recurse = false
245+
for name in pairs(filter) do
246+
if name:match'^[A-Z]' and name ~= 'Pandoc' and name ~= 'Meta' then
247+
must_recurse = true
248+
break
249+
end
250+
end
251+
if not must_recurse then
252+
element.meta = run_filter_function(filter.Meta, element.meta, context)
253+
element = run_filter_function(filter.Pandoc, element, context)
254+
return element
255+
end
256+
end
257+
258+
-- Create and call traversal function
259+
local jog_internal = make_jogger(filter, context)
260+
return jog_internal(element)
261+
end
262+
263+
--- Add `jog` as a method to all pandoc AST elements
264+
-- This uses undocumented features and might break!
265+
local function add_method(funname)
266+
funname = funname or 'jog'
267+
pandoc.Space() -- init metatable 'Inline'
268+
pandoc.HorizontalRule() -- init metatable 'Block'
269+
pandoc.Meta{} -- init metatable 'Pandoc'
270+
pandoc.Pandoc{} -- init metatable 'Pandoc'
271+
pandoc.Blocks{} -- init metatable 'Blocks'
272+
pandoc.Inlines{} -- init metatable 'Inlines'
273+
pandoc.Cell{} -- init metatable 'pandoc Cell'
274+
pandoc.Row{} -- init metatable 'pandoc Row'
275+
pandoc.TableHead{} -- init metatable 'pandoc TableHead'
276+
pandoc.TableFoot{} -- init metatable 'pandoc TableFoot'
277+
local reg = debug.getregistry()
278+
List{
279+
'Block', 'Inline', 'Pandoc',
280+
'pandoc Cell', 'pandoc Row', 'pandoc TableHead', 'pandoc TableFoot'
281+
}:map(
282+
function (name)
283+
if reg[name] then
284+
reg[name].methods[funname] = jog
285+
end
286+
end
287+
)
288+
for name in pairs(listy_type) do
289+
if reg[name] then
290+
reg[name][funname] = jog
291+
end
292+
end
293+
if reg['Meta'] then
294+
reg['Meta'][funname] = jog
295+
end
296+
end
297+
298+
local mt = {
299+
__call = function (_, ...)
300+
return jog(...)
301+
end
302+
}
303+
304+
local M = setmetatable({}, mt)
305+
M.jog = jog
306+
M.add_method = add_method
307+
308+
return M

0 commit comments

Comments
 (0)