Skip to content

Commit dd7de57

Browse files
committed
Support multiple bibliographies per topic (etc.)
The full list of changes is: - Support multiple bibliographies per topic, i.e., in the 'bibliography' metadata, allow topic entries to be list-valued - Support the same topic structure for the 'references' metadata item, so can override references per topic - Ignore duplicate references, i.e. if the same reference exists in multiple topics (the first one encountered is used, but this is not satisfactory because the topic processing order is indeterminate). Note that something had to be done here, because these duplicates would have the same ids and therefore would be ambiguous - Take some changes from the diverged multiple-bibliographies repo, notably don't use utils.citeproc() because it doesn't have a 'quiet' option (which is needed for the second pass) - Fix an undefined 'orig_bib' bug (that probably caused no problems) - Add filters to renumber citations in the case where they're referenced by citation number. These filters won't do anything in other cases, but nevertheless probably shouldn't be here
1 parent 0c4fcb5 commit dd7de57

File tree

1 file changed

+126
-21
lines changed

1 file changed

+126
-21
lines changed

_extensions/multibib/multibib.lua

Lines changed: 126 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1515
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1616
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1717
]]
18+
19+
-- --citeproc was added in 2.11, so we never use the old pandoc-citeproc
1820
PANDOC_VERSION:must_be_at_least '2.11'
1921

2022
local List = require 'pandoc.List'
@@ -31,39 +33,66 @@ local metatype = pandoc.utils.type or
3133

3234
--- Collection of all cites in the document
3335
local all_cites = {}
36+
3437
--- Document meta value
3538
local doc_meta = pandoc.Meta{}
3639

3740
--- Div used by citeproc to insert the bibliography.
3841
local refs_div = pandoc.Div({}, pandoc.Attr('refs'))
3942

43+
--- 'references' metadata for each topic
44+
local topic_refs = {}
45+
4046
-- Div filled by citeproc with properties set according to
4147
-- the output format and the attributes of cs:bibliography
4248
local refs_div_with_properties
4349

50+
-- Whether utils.citeproc() supports a 'quiet' argument
51+
-- (it doesn't yet, but perhaps it will, in which case this
52+
-- will use the appropriate pandoc version check)
53+
local supports_quiet_arg = false
54+
4455
--- Run citeproc on a pandoc document
45-
local citeproc
46-
if utils.citeproc then
47-
-- Built-in Lua function
48-
citeproc = utils.citeproc
49-
else
50-
-- Use pandoc as a citeproc processor
51-
citeproc = function (doc)
52-
local opts = {'--from=json', '--to=json', '--citeproc', '--quiet'}
56+
local function citeproc(doc, quiet)
57+
-- utils.citeproc() was added in 2.19.1
58+
if utils.citeproc and supports_quiet_arg then
59+
-- Built-in Lua function
60+
return utils.citeproc(doc, quiet)
61+
else
62+
-- Use pandoc as a citeproc processor
63+
local path = PANDOC_STATE.resource_path
64+
local opts = {'--from=json', '--to=json', '--citeproc',
65+
'--resource-path=' .. table.concat(path, ':'),
66+
quiet and '--quiet' or nil}
5367
return run_json_filter(doc, 'pandoc', opts)
5468
end
5569
end
5670

5771
--- Resolve citations in the document by combining all bibliographies
58-
-- before running pandoc-citeproc on the full document.
72+
-- before running citeproc on the full document.
5973
local function resolve_doc_citations (doc)
60-
-- combine all bibliographies
74+
-- combine all bibliographies and references
6175
local meta = doc.meta
6276
local bibconf = meta.bibliography
6377
meta.bibliography = pandoc.MetaList{}
6478
if metatype(bibconf) == 'table' then
6579
for _, value in pairs(bibconf) do
66-
table.insert(meta.bibliography, stringify(value))
80+
-- support list-valued items
81+
if metatype(value) ~= 'List' then value = List{value} end
82+
for _, val in ipairs(value) do
83+
table.insert(meta.bibliography, stringify(val))
84+
end
85+
end
86+
end
87+
local refconf = meta.references
88+
meta.references = pandoc.MetaList{}
89+
if metatype(refconf) == 'table' then
90+
for topic, refs in pairs(refconf) do
91+
-- save topic references for meta_for_citeproc()
92+
topic_refs[topic] = refs
93+
for _, ref in ipairs(refs) do
94+
table.insert(meta.references, ref)
95+
end
6796
end
6897
end
6998
-- add refs div to catch the created bibliography
@@ -72,30 +101,53 @@ local function resolve_doc_citations (doc)
72101
doc = citeproc(doc)
73102
-- remove catch-all bibliography and keep it for future use
74103
refs_div_with_properties = table.remove(doc.blocks)
75-
-- restore bibliography to original value
76-
doc.meta.bibliography = orig_bib
104+
-- restore bibliography and references to original values
105+
doc.meta.bibliography = bibconf
106+
doc.meta.references = refconf
77107
return doc
78108
end
79109

80-
--- Explicitly create a new meta object with all fields relevant for
81-
--- pandoc-citeproc.
82-
local function meta_for_pandoc_citeproc (bibliography)
110+
--- Explicitly create a new meta object with all fields relevant for citeproc.
111+
local function meta_for_citeproc (bibliography, topic)
83112
-- We could just indiscriminately copy all meta fields, but let's be
84113
-- explicit about what's important.
85114
local fields = {
86115
'bibliography', 'references', 'csl', 'citation-style',
87116
'link-citations', 'citation-abbreviations', 'lang',
88117
'suppress-bibliography', 'reference-section-title',
89-
'notes-after-punctuation', 'nocite'
118+
'notes-after-punctuation', 'nocite', 'link-bibliography'
90119
}
91120
local new_meta = pandoc.Meta{}
92121
for _, field in ipairs(fields) do
93-
new_meta[field] = doc_meta[field]
122+
local value = doc_meta[field]
123+
-- replace 'references' with the topic references
124+
if field == 'references' and metatype(value) == 'table' and topic then
125+
value = topic_refs[topic]
126+
end
127+
new_meta[field] = value
94128
end
95129
new_meta.bibliography = bibliography
96130
return new_meta
97131
end
98132

133+
-- list of ref-xxx identifiers that have already been output
134+
local identifiers = List()
135+
136+
-- ignore duplicate references (the first definition will win)
137+
local function ignore_duplicates(blocks)
138+
local new_blocks = pandoc.Blocks{}
139+
for _, block in ipairs(blocks) do
140+
local identifier = block.attr.identifier
141+
if not identifiers:includes(identifier) then
142+
local new_block = pandoc.walk_block(block, {Span=_span})
143+
new_blocks:insert(new_block)
144+
identifiers:insert(identifier)
145+
end
146+
end
147+
148+
return new_blocks
149+
end
150+
99151
local function remove_duplicates(classes)
100152
local seen = {}
101153
return classes:filter(function(x)
@@ -118,18 +170,67 @@ local function create_topic_bibliography (div)
118170
return nil
119171
end
120172
local tmp_blocks = {pandoc.Para(all_cites), refs_div}
121-
local tmp_meta = meta_for_pandoc_citeproc(bibfile)
173+
local tmp_meta = meta_for_citeproc(bibfile, name)
122174
local tmp_doc = pandoc.Pandoc(tmp_blocks, tmp_meta)
123-
local res = citeproc(tmp_doc)
175+
local res = citeproc(tmp_doc, true)
124176
-- First block of the result contains the dummy paragraph, second is
125177
-- the refs Div filled by citeproc.
126-
div.content = res.blocks[2].content
178+
div.content = ignore_duplicates(res.blocks[2].content)
127179
-- Set the classes and attributes as citeproc did it on refs_div
128180
div.classes = remove_duplicates(refs_div_with_properties.classes)
129181
div.attributes = refs_div_with_properties.attributes
130182
return div
131183
end
132184

185+
-- renumber numbered references and their citations
186+
-- (this logic should probably be in a separate filter; it's too
187+
-- dependent on the CSL, although it should do no harm)
188+
189+
-- map from reference id to its new label
190+
local ref_map = List()
191+
192+
-- ref counter
193+
local ref_counter = 1
194+
195+
local function collect_numbered_refs(div)
196+
if div.attr.classes:includes('csl-entry') then
197+
local identifier = div.attr.identifier
198+
local content = div.content
199+
-- expect single Para with a Span (depending on style) possibly containing
200+
-- the citation number (only do anything if it does)
201+
if (#div.content > 0 and #div.content[1].content > 0 and
202+
div.content[1].content[1].tag == 'Span') then
203+
local span = div.content[1].content[1]
204+
local content = span.content
205+
if #content > 0 then
206+
local text = content[1].text
207+
local pre, num, post = content[1].text:match("^(%p*)(%d+)(%p*)$")
208+
if pre and num and post then
209+
local ident = identifier:gsub('^ref%-', '')
210+
local label = string.format('%s%d%s', pre, ref_counter, post)
211+
content[1] = pandoc.Str(label)
212+
ref_map[ident] = label
213+
ref_counter = ref_counter + 1
214+
return div
215+
end
216+
end
217+
end
218+
end
219+
end
220+
221+
local function renumber_cites(cite)
222+
-- only consider cites with single citations
223+
if #cite.citations == 1 then
224+
local id = cite.citations[1].id
225+
local label = ref_map[id]
226+
-- only change the content if the label is defined
227+
if label then
228+
cite.content = label
229+
return cite
230+
end
231+
end
232+
end
233+
133234
return {
134235
{
135236
-- Collect all citations and the doc's Meta value for other filters.
@@ -138,4 +239,8 @@ return {
138239
},
139240
{ Pandoc = resolve_doc_citations },
140241
{ Div = create_topic_bibliography },
242+
243+
-- These should probably be handled via a separate filter.
244+
{ Div = collect_numbered_refs },
245+
{ Cite = renumber_cites }
141246
}

0 commit comments

Comments
 (0)