Skip to content

Commit 9db3e3e

Browse files
author
Allen Manning
authored
Merge pull request #4253 from quarto-dev/feature/footnotes
Confluence: Footnote Support
2 parents 00b8aa6 + 6fef2a3 commit 9db3e3e

File tree

4 files changed

+407
-1
lines changed

4 files changed

+407
-1
lines changed

src/publish/confluence/confluence-helper.ts

+29
Original file line numberDiff line numberDiff line change
@@ -745,3 +745,32 @@ export const findAttachments = (
745745

746746
return uniqueResult ?? [];
747747
};
748+
749+
export const footnoteTransform = (bodyValue: string): string => {
750+
const BACK_ANCHOR_FINDER: RegExp = /<a href="#fn(\d)"/g;
751+
const ANCHOR_FINDER: RegExp = /<a href="#fnref(\d)"/g;
752+
const CONFLUENCE_ANCHOR_FINDER: RegExp =
753+
/ac:macro-id="d2cb5be1217ae6e086bc60005e9d27b7"><ac:parameter ac:name="">fn/g;
754+
755+
if (bodyValue.search(CONFLUENCE_ANCHOR_FINDER) !== -1) {
756+
//the footnote transform has already happened
757+
return bodyValue;
758+
}
759+
760+
const buildConfluenceAnchor = (id: string) =>
761+
`<ac:structured-macro ac:name="anchor" ac:schema-version="1" ac:local-id="a6aa6f25-0bee-4a7f-929b-71fcb7eba592" ac:macro-id="d2cb5be1217ae6e086bc60005e9d27b7"><ac:parameter ac:name="">${id}</ac:parameter></ac:structured-macro>`;
762+
763+
const replacer =
764+
(prefix: string) =>
765+
(match: string, p1: string): string =>
766+
`${buildConfluenceAnchor(`${prefix}${p1}`)}${match}`;
767+
768+
let replacedBody: string = bodyValue.replaceAll(
769+
BACK_ANCHOR_FINDER,
770+
replacer("fnref")
771+
);
772+
773+
replacedBody = replacedBody.replaceAll(ANCHOR_FINDER, replacer("fn"));
774+
775+
return replacedBody;
776+
};

src/publish/confluence/confluence.ts

+4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
doWithSpinner,
5656
filterFilesForUpdate,
5757
findAttachments,
58+
footnoteTransform,
5859
getNextVersion,
5960
getTitle,
6061
isContentCreate,
@@ -386,6 +387,8 @@ async function publish(
386387
trace("attachmentsToUpload", attachmentsToUpload, LogPrefix.ATTACHMENT);
387388

388389
const updatedBody: ContentBody = updateImagePaths(body);
390+
updatedBody.storage.value = footnoteTransform(updatedBody.storage.value);
391+
389392
const toUpdate: ContentUpdate = {
390393
contentChangeType: ContentChangeType.update,
391394
id,
@@ -520,6 +523,7 @@ async function publish(
520523

521524
trace("attachmentsToUpload", attachmentsToUpload, LogPrefix.ATTACHMENT);
522525
const updatedBody: ContentBody = updateImagePaths(body);
526+
updatedBody.storage.value = footnoteTransform(updatedBody.storage.value);
523527

524528
const toCreate: ContentCreate = {
525529
contentChangeType: ContentChangeType.create,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
-- Confluence Storage Format for Pandoc
2+
-- https://confluence.atlassian.com/doc/confluence-storage-format-790796544.html
3+
-- https://pandoc.org/MANUAL.html#custom-readers-and-writers
4+
5+
local stringify = (require "pandoc.utils").stringify
6+
local confluence = require('overrides')
7+
8+
-- From https://stackoverflow.com/questions/9168058/how-to-dump-a-table-to-console
9+
function dumpObject(o)
10+
if type(o) == 'table' then
11+
local s = '{ '
12+
for k,v in pairs(o) do
13+
if type(k) ~= 'number' then k = '"'..k..'"' end
14+
s = s .. '['..k..'] = ' .. dumpObject(v) .. ','
15+
end
16+
return s .. '} '
17+
else
18+
return tostring(o)
19+
end
20+
end
21+
22+
23+
function dump(object, label)
24+
print(label or '' .. ': ', dumpObject(object))
25+
end
26+
27+
-- The global variable PANDOC_DOCUMENT contains the full AST of
28+
-- the document which is going to be written. It can be used to
29+
-- configure the writer.
30+
local meta = PANDOC_DOCUMENT.meta
31+
32+
-- Choose the image format based on the value of the
33+
-- `image_format` meta value.
34+
local image_format = meta.image_format
35+
and stringify(meta.image_format)
36+
or "png"
37+
local image_mime_type = ({
38+
jpeg = "image/jpeg",
39+
jpg = "image/jpeg",
40+
gif = "image/gif",
41+
png = "image/png",
42+
svg = "image/svg+xml",
43+
})[image_format]
44+
or error("unsupported image format `" .. image_format .. "`")
45+
46+
-- Character escaping
47+
local escape = confluence.escape
48+
49+
-- Helper function to convert an attributes table into
50+
-- a string that can be put into HTML tags.
51+
local function attributes(attr)
52+
local attr_table = {}
53+
for x,y in pairs(attr) do
54+
if y and y ~= "" then
55+
table.insert(attr_table, ' ' .. x .. '="' .. escape(y,true) .. '"')
56+
end
57+
end
58+
return table.concat(attr_table)
59+
end
60+
61+
-- Table to store footnotes, so they can be included at the end.
62+
local notes = {}
63+
64+
-- Blocksep is used to separate block elements.
65+
function Blocksep()
66+
return "\n\n"
67+
end
68+
69+
-- This function is called once for the whole document. Parameters:
70+
-- body is a string, metadata is a table, variables is a table.
71+
-- This gives you a fragment. You could use the metadata table to
72+
-- fill variables in a custom lua template. Or, pass `--template=...`
73+
-- to pandoc, and pandoc will do the template processing as usual.
74+
function Doc(body, metadata, variables)
75+
local buffer = {}
76+
local function add(s)
77+
table.insert(buffer, s)
78+
end
79+
add(body)
80+
if #notes > 0 then
81+
add('<ol class="footnotes">')
82+
for _,note in pairs(notes) do
83+
add(note)
84+
end
85+
add('</ol>')
86+
end
87+
return table.concat(buffer,'\n') .. '\n'
88+
end
89+
90+
-- The functions that follow render corresponding pandoc elements.
91+
-- s is always a string, attr is always a table of attributes, and
92+
-- items is always an array of strings (the items in a list).
93+
-- Comments indicate the types of other variables.
94+
95+
function Str(s)
96+
return escape(s)
97+
end
98+
99+
function Space()
100+
return " "
101+
end
102+
103+
function SoftBreak()
104+
return "\n"
105+
end
106+
107+
function LineBreak()
108+
return "<br/>"
109+
end
110+
111+
function Emph(s)
112+
return "<em>" .. s .. "</em>"
113+
end
114+
115+
function Strong(s)
116+
return "<strong>" .. s .. "</strong>"
117+
end
118+
119+
function Subscript(s)
120+
return "<sub>" .. s .. "</sub>"
121+
end
122+
123+
function Superscript(s)
124+
return "<sup>" .. s .. "</sup>"
125+
end
126+
127+
function SmallCaps(s)
128+
return '<span style="font-variant: small-caps;">' .. s .. '</span>'
129+
end
130+
131+
function Strikeout(s)
132+
return '<del>' .. s .. '</del>'
133+
end
134+
135+
function Link(s, tgt, tit, attr)
136+
return confluence.LinkConfluence(s, tgt, tit, attr)
137+
end
138+
139+
function Image(s, src, tit, attr)
140+
return confluence.CaptionedImageConfluence(src, tit, '', attr)
141+
end
142+
143+
function Code(s, attr)
144+
return "<code" .. attributes(attr) .. ">" .. escape(s) .. "</code>"
145+
end
146+
147+
function InlineMath(s)
148+
return "\\(" .. escape(s) .. "\\)"
149+
end
150+
151+
function DisplayMath(s)
152+
return "\\[" .. escape(s) .. "\\]"
153+
end
154+
155+
function SingleQuoted(s)
156+
return "&lsquo;" .. s .. "&rsquo;"
157+
end
158+
159+
function DoubleQuoted(s)
160+
return "&ldquo;" .. s .. "&rdquo;"
161+
end
162+
163+
function Note(s)
164+
local num = #notes + 1
165+
-- insert the back reference right before the final closing tag.
166+
s = string.gsub(s,
167+
'(.*)</', '%1 <a href="#fnref' .. num .. '">&#8617;</a></')
168+
-- add a list item with the note to the note table.
169+
table.insert(notes, '<li id="fn' .. num .. '">' .. s .. '</li>')
170+
-- return the footnote reference, linked to the note.
171+
return '<a id="fnref' .. num .. '" href="#fn' .. num ..
172+
'"><sup>' .. num .. '</sup></a>'
173+
end
174+
175+
function Span(s, attr)
176+
return "<span" .. attributes(attr) .. ">" .. s .. "</span>"
177+
end
178+
179+
function RawInline(format, str)
180+
if format == "html" then
181+
return str
182+
else
183+
return ''
184+
end
185+
end
186+
187+
function Cite(s, cs)
188+
local ids = {}
189+
for _,cit in ipairs(cs) do
190+
table.insert(ids, cit.citationId)
191+
end
192+
return "<span class=\"cite\" data-citation-ids=\"" .. table.concat(ids, ",") ..
193+
"\">" .. s .. "</span>"
194+
end
195+
196+
function Plain(s)
197+
return s
198+
end
199+
200+
function Para(s)
201+
return "<p>" .. s .. "</p>"
202+
end
203+
204+
-- lev is an integer, the header level.
205+
function Header(lev, s, attr)
206+
return "<h" .. lev .. attributes(attr) .. ">" .. s .. "</h" .. lev .. ">"
207+
end
208+
209+
function BlockQuote(s)
210+
return confluence.BlockQuoteConfluence(s)
211+
end
212+
213+
function HorizontalRule()
214+
return "<hr/>"
215+
end
216+
217+
function LineBlock(ls)
218+
return '<div style="white-space: pre-line;">' .. table.concat(ls, '\n') ..
219+
'</div>'
220+
end
221+
222+
function CodeBlock(s, attr)
223+
return confluence.CodeBlockConfluence(s, attr)
224+
end
225+
226+
function BulletList(items)
227+
local buffer = {}
228+
for _, item in pairs(items) do
229+
table.insert(buffer, "<li>" .. item .. "</li>")
230+
end
231+
return "<ul>\n" .. table.concat(buffer, "\n") .. "\n</ul>"
232+
end
233+
234+
function OrderedList(items)
235+
local buffer = {}
236+
for _, item in pairs(items) do
237+
table.insert(buffer, "<li>" .. item .. "</li>")
238+
end
239+
return "<ol>\n" .. table.concat(buffer, "\n") .. "\n</ol>"
240+
end
241+
242+
function DefinitionList(items)
243+
local buffer = {}
244+
for _,item in pairs(items) do
245+
local k, v = next(item)
246+
table.insert(buffer, "<dt>" .. k .. "</dt>\n<dd>" ..
247+
table.concat(v, "</dd>\n<dd>") .. "</dd>")
248+
end
249+
return "<dl>\n" .. table.concat(buffer, "\n") .. "\n</dl>"
250+
end
251+
252+
function CaptionedImage(src, tit, caption, attr)
253+
return confluence.CaptionedImageConfluence(src, tit, caption, attr)
254+
end
255+
256+
-- Caption is a string, aligns is an array of strings,
257+
-- widths is an array of floats, headers is an array of
258+
-- strings, rows is an array of arrays of strings.
259+
function Table(caption, aligns, widths, headers, rows)
260+
return TableConfluence(caption, aligns, widths, headers, rows)
261+
end
262+
263+
function RawBlock(format, str)
264+
if format == "html" then
265+
return str
266+
else
267+
return ''
268+
end
269+
end
270+
271+
function Div(s, attr)
272+
return "<div" .. attributes(attr) .. ">\n" .. s .. "</div>"
273+
end
274+
275+
-- The following code will produce runtime warnings when you haven't defined
276+
-- all of the functions you need for the custom writer, so it's useful
277+
-- to include when you're working on a writer.
278+
local meta = {}
279+
meta.__index =
280+
function(_, key)
281+
io.stderr:write(string.format("WARNING: Undefined function '%s'\n",key))
282+
return function() return "" end
283+
end
284+
setmetatable(_G, meta)

0 commit comments

Comments
 (0)