-- -- Copyright (c) 2021-2024 Zeping Lee -- Released under the MIT license. -- Repository: https://github.com/zepinglee/citeproc-lua -- local engine = {} local dom local context local element local nodes local node_locale local node_style local output local util if kpse then dom = require("luaxml-domobject") context = require("citeproc-context") element = require("citeproc-element") nodes = require("citeproc-nodes") node_locale = require("citeproc-node-locale") node_style = require("citeproc-node-style") output = require("citeproc-output") util = require("citeproc-util") else dom = require("citeproc.luaxml.domobject") context = require("citeproc.context") element = require("citeproc.element") nodes = require("citeproc.nodes") node_locale = require("citeproc.node-locale") node_style = require("citeproc.node-style") output = require("citeproc.output") util = require("citeproc.util") end local Element = element.Element local Style = node_style.Style local Locale = node_locale.Locale local Context = context.Context local IrState = context.IrState local InlineElement = output.InlineElement -- local OutputFormat = output.OutputFormat local LatexWriter = output.LatexWriter local HtmlWriter = output.HtmlWriter local SortStringFormat = output.SortStringFormat local Position = util.Position ---@alias ItemId string | number ---@alias NoteIndex integer ---@alias CitationId string ---@alias CitationData {citationID: CitationId, citationItems: CitationItem[], properties: CitationProperties, citation_index: integer} ---@alias CitationProperties { noteIndex: integer, mode: string? } ---@class NameVariable ---@field family string? ---@field given string? ---@field dropping-particle string? ---@field non-dropping-particle string? ---@field suffix string? ---@field comma-suffix string | number | boolean? ---@field static-ordering string | number | boolean? ---@field literal string | number | boolean? ---@field parse-names string | number | boolean? ---@class DateVariable ---@field date-parts (string | number)[][] ---@field season (string | number) ---@field circa (string | number | boolean) ---@field literal string ---@field raw string ---@alias ItemData { id: ItemId, type: string, language: string?, [string]: string | number | NameVariable[] | DateVariable } ---@class Registry ---@field citations_by_id table ---@field citation_list CitationData[] ---@field citations_by_item_id table ---@field registry table ---@field reflist ItemId[] ---@field uncited_list ItemId[] ---@field previous_citation CitationData? ---@field requires_sorting boolean ---@field widest_label string ---@field maxoffset integer local Registry = {} ---@class CiteProc ---@field style Style ---@field sys any ---@field locales Locale[] ---@field system_locales Locale[] ---@field lang string ---@field output_format OutputFormat ---@field opt table ---@field registry Registry ---@field cite_first_note_numbers table local CiteProc = {} ---comment ---@param sys table ---@param style string ---@param lang string? ---@param force_lang boolean? ---@return CiteProc function CiteProc.new(sys, style, lang, force_lang) if not sys then error("\"citeprocSys\" required") end if sys.retrieveLocale == nil then error("\"citeprocSys.retrieveLocale\" required") end if sys.retrieveItem == nil then error("\"citeprocSys.retrieveItem\" required") end ---@type CiteProc local o = {} o.style = Style:parse(style) o.sys = sys o.locales = {} o.system_locales = {} o.lang = o.style.default_locale if not o.lang or force_lang then o.lang = lang or "en-US" end o.output_format = LatexWriter:new() o.opt = { -- Similar to citeproc-js's development_extensions.wrap_url_and_doi wrap_url_and_doi = false, citation_link = false, title_link = false, } o.registry = { citations_by_id = {}, -- A map citation_list = {}, -- A list citations_by_item_id = {}, -- A map from item id to a map of citations registry = {}, -- A map of bibliographic meta data reflist = {}, -- list of cited ids uncited_list = {}, previous_citation = nil, requires_sorting = false, widest_label = "", maxoffset = 0, } o.cite_first_note_numbers = {} o.cite_last_note_numbers = {} o.note_citations_map = {} o.tainted_item_ids = {} o.disam_irs = {} -- { , , ... } o.cite_irs_by_output = {} -- { -- ["Roe, J"] = {}, -- ["Doe, J"] = {, }, -- ["Doe, John"] = {}, -- ["Doe, Jack"] = {}, -- } o.person_names = {} o.person_names_by_output = {} setmetatable(o, { __index = CiteProc }) return o end ---@return boolean function CiteProc:is_dependent_style() return self.style.info.independent_parent ~= nil end ---@return string? function CiteProc:get_independent_parent() return self.style.info.independent_parent end function CiteProc:check_valid_citation_element() if not self.style.citation then if self.style.info and self.style.info.independent_parent then util.error(string.format('This is a dependent style linked to "%s".', self.style.info.independent_parent)) else util.error('No in style.') end end end ---@param ids CiteId[] function CiteProc:updateItems(ids) -- util.debug(string.format('updateItems(%s)', table.concat(ids, ", "))) self.registry.reflist = {} self.registry.registry = {} self.person_names = {} self.person_names_by_output = {} self.disam_irs = {} self.cite_irs_by_output = {} local cite_items = {} local loaded_ids = {} for _, id in ipairs(ids) do table.insert(cite_items, {id = id}) loaded_ids[id] = true end for _, id in ipairs(self.registry.uncited_list) do if not loaded_ids[id] then table.insert(cite_items, {id = id}) loaded_ids[id] = true end end -- Clean the first note number to reset all the positions self.cite_first_note_numbers = {} -- TODO: optimize this self:makeCitationCluster(cite_items) self.registry.previous_citation = nil self.cite_first_note_numbers = {} self.cite_last_note_numbers = {} self.note_citations_map = {} for _, item in ipairs(self.registry.registry) do item.year_suffix_number = nil item["year-suffix"] = nil end end function CiteProc:updateUncitedItems(uncited_ids) -- self.registry.reflist = {} self.registry.registry = {} self.registry.uncited_list = {} self.person_names = {} self.person_names_by_output = {} self.disam_irs = {} self.cite_irs_by_output = {} local cite_items = {} local loaded_ids = {} for _, id in ipairs(self.registry.reflist) do if not loaded_ids[id] then table.insert(cite_items, {id = id}) loaded_ids[id] = true end end self.registry.reflist = {} for _, id in ipairs(uncited_ids) do if not loaded_ids[id] then table.insert(cite_items, {id = id}) loaded_ids[id] = true end end loaded_ids = {} for _, id in ipairs(uncited_ids) do if not loaded_ids[id] then table.insert(self.registry.uncited_list, id) loaded_ids[id] = true end end -- TODO: optimize this self:makeCitationCluster(cite_items) self.registry.previous_citation = nil self.cite_first_note_numbers = {} self.cite_last_note_numbers = {} self.note_citations_map = {} end ---@param citation CitationData ---@param citationsPre (CitationId | NoteIndex)[] ---@param citationsPost (CitationId | NoteIndex)[] ---@return (table | (integer | string | CitationId)[])[] function CiteProc:processCitationCluster(citation, citationsPre, citationsPost) -- util.debug(string.format('processCitationCluster(%s)', citation.citationID)) self:check_valid_citation_element() citation = self:normalize_citation_input(citation) -- Registor citation self.registry.citations_by_id[citation.citationID] = citation local citation_note_pairs = {} util.extend(citation_note_pairs, citationsPre) table.insert(citation_note_pairs, {citation.citationID, citation.properties.noteIndex}) util.extend(citation_note_pairs, citationsPost) -- util.debug(citation_note_pairs) local citations_by_id = {} local citation_list = {} for _, pair in ipairs(citation_note_pairs) do local citation_id, note_number = table.unpack(pair) local citation_ = self.registry.citations_by_id[citation_id] if not citation_ then util.error("Citation not in registry.") end citations_by_id[citation_.citationID] = citation_ table.insert(citation_list, citation_) end self.registry.citations_by_id = citations_by_id self.registry.citation_list = citation_list -- update self.registry.citations_by_item_id local item_ids = {} self.registry.citations_by_item_id = {} for _, citation_ in ipairs(citation_list) do for _, cite_item in ipairs(citation_.citationItems) do if not self.registry.citations_by_item_id[cite_item.id] then self.registry.citations_by_item_id[cite_item.id] = {} table.insert(item_ids, cite_item.id) end table.insert(self.registry.citations_by_item_id[cite_item.id], citation_) end end self:updateItems(item_ids) local params = { bibchange = false, citation_errors = {}, } local output = {} local tainted_citation_ids = self:get_tainted_citation_ids(citation_note_pairs) tainted_citation_ids[citation.citationID] = true -- Citeproc-js marks all related citations as tainted but I don't think it's -- necessary. if self.style.class == "note" and self.style.has_disambiguate then for _, cite_item in ipairs(citation.citationItems) do for _, citation_ in ipairs(self.registry.citations_by_item_id[cite_item.id]) do tainted_citation_ids[citation_.citationID] = true end end end -- util.debug(tainted_citation_ids) -- params.bibchange = #tainted_citation_ids > 0 for citation_id, _ in pairs(tainted_citation_ids) do local citation_ = self.registry.citations_by_id[citation_id] local citation_index = citation_.citation_index local mode = citation_.properties.mode if mode == "suppress-author" and self.style.class == "note" then mode = nil end local citation_elemement = self.style.citation if mode == "author-only" and self.style.intext then citation_elemement = self.style.intext end local citation_str = citation_elemement:build_citation_str(citation_, self) table.insert(output, {citation_index, citation_str, citation_id}) end return {params, output} end function CiteProc:normalize_citation_input(citation) citation = util.deep_copy(citation) if not citation.citationID then citation.citationID = "CITATION-" .. tostring(#self.registry.citation_list) end if not citation.citationItems then citation.citationItems = {} end for i, cite_item in ipairs(citation.citationItems) do citation.citationItems[i] = self:normalize_cite_item(cite_item) end -- Fix missing noteIndex: sort_CitationNumberPrimaryAscendingViaMacroCitation.txt if not citation.properties then citation.properties = {} end if not citation.properties.noteIndex then citation.properties.noteIndex = 0 end return citation end ---@param cite_item CitationItem ---@return CitationItem function CiteProc:normalize_cite_item(cite_item) -- Shallow copy cite_item = util.clone(cite_item) cite_item.id = tostring(cite_item.id) -- Use "page" as locator label if missing -- label_PluralWithAmpersand.txt if cite_item.locator and not cite_item.label then cite_item.label = "page" end if cite_item.prefix then -- Assert CSL rich-text or HTML-like tagged string if cite_item.prefix == "" then cite_item.prefix = nil else cite_item.prefix_inlines = InlineElement:parse(cite_item.prefix, nil, true) end end if cite_item.suffix then if cite_item.suffix == "" then cite_item.suffix = nil else cite_item.suffix_inlines = InlineElement:parse(cite_item.suffix, nil, true) end end return cite_item end -- A variant of processCitationCluster() for easy use with LaTeX. -- It should be run after refreshing the registry (updateItems()) with all items function CiteProc:process_citation(citation) -- util.debug(citation) -- util.debug(citationsPre) -- Fix missing noteIndex: sort_CitationNumberPrimaryAscendingViaMacroCitation.txt citation = self:normalize_citation_input(citation) -- Registor citation self.registry.citations_by_id[citation.citationID] = citation table.insert(self.registry.citation_list, citation) local citation_note_pairs = {} for _, citation_ in ipairs(self.registry.citation_list) do table.insert(citation_note_pairs, {citation_.citationID, citation_.properties.noteIndex}) end -- update self.registry.citations_by_item_id for _, cite_item in ipairs(citation.citationItems) do if not self.registry.citations_by_item_id[cite_item.id] then self.registry.citations_by_item_id[cite_item.id] = {} end table.insert(self.registry.citations_by_item_id[cite_item.id], citation) end -- self:updateItems(item_ids) for i, cite_item in ipairs(citation.citationItems) do self:get_item(cite_item.id) end local tainted_citation_ids = self:get_tainted_citation_ids(citation_note_pairs) local mode = citation.properties.mode if mode == "suppress-author" and self.style.class == "note" then mode = nil end local citation_elemement = self.style.citation if mode == "author-only" and self.style.intext then citation_elemement = self.style.intext end local citation_str = citation_elemement:build_citation_str(citation, self) return citation_str end function CiteProc:get_tainted_citation_ids(citation_note_pairs) local tainted_citation_ids = {} self.cite_first_note_numbers = {} self.cite_last_note_numbers = {} self.note_citations_map = {} -- { -- 1 = {"citation-1", "citation-2"}, -- 2 = {"citation-2"}, -- } -- Citations with noteIndex == 0 are in-text citations and they may also -- have position properties. local in_text_citations = {} local previous_citation for citation_index, pair in ipairs(citation_note_pairs) do local citation_id, note_number = table.unpack(pair) -- util.debug(citation_id) local citation = self.registry.citations_by_id[citation_id] citation.properties.noteIndex = note_number citation.citation_index = citation_index local tainted = false local prev_citation = previous_citation if note_number == 0 then -- Find the previous in-text citation. prev_citation = in_text_citations[#in_text_citations] end local previous_cite for _, cite_item in ipairs(citation.citationItems) do tainted = self:set_cite_item_position(cite_item, note_number, previous_cite, prev_citation, citation) -- https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html#citations -- Citations within the main text of the document have a noteIndex of zero. if (self.style.class == "note" and note_number > 0) or citation.properties.mode ~= "author-only" then self.cite_last_note_numbers[cite_item.id] = note_number previous_cite = cite_item end end if tainted then tainted_citation_ids[citation.citationID] = true end if not self.note_citations_map[note_number] then self.note_citations_map[note_number] = {} end table.insert(self.note_citations_map[note_number], citation.citationID) if citation.properties.mode ~= "author-only" then if note_number == 0 then table.insert(in_text_citations, citation) else previous_citation = citation end end end -- Update tainted citation ids because of citation-number's change -- The self.tainted_item_ids were added in the sort_bibliography() procedure. for item_id, _ in pairs(self.tainted_item_ids) do if self.registry.citations_by_item_id[item_id] then for _, citation in ipairs(self.registry.citations_by_item_id[item_id]) do tainted_citation_ids[citation.citationID] = true end end end return tainted_citation_ids end function CiteProc:set_cite_item_position(cite_item, note_number, previous_cite, previous_citation, citation) local position = Position.First -- https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html#citations -- Citations within the main text of the document have a noteIndex of zero. if citation.properties.mode == "author-only" then -- discretionary_IbidInAuthorDateStyleWithoutIntext.txt cite_item.position_level = position return false end local first_reference_note_number = self.cite_first_note_numbers[cite_item.id] if first_reference_note_number then position = Position.Subsequent elseif note_number > 0 then -- note_number == 0 implied an in-text citation self.cite_first_note_numbers[cite_item.id] = note_number end local preceding_cite_item = self:get_preceding_cite_item(cite_item, previous_cite, previous_citation, note_number) if preceding_cite_item then position = self:_get_cite_position(cite_item, preceding_cite_item) end local near_note = false local last_note_number = self.cite_last_note_numbers[cite_item.id] if last_note_number then local note_distance = note_number - last_note_number if note_distance <= self.style.citation.near_note_distance then near_note = true end end local tainted = false if cite_item.position_level ~= position then tainted = true cite_item.position_level = position end if cite_item["first-reference-note-number"] ~= first_reference_note_number then tainted = true cite_item["first-reference-note-number"] = first_reference_note_number end if cite_item.near_note ~= near_note then tainted = true cite_item.near_note = near_note end return tainted end -- Find the preceding cite referencing the same item function CiteProc:get_preceding_cite_item(cite_item, previous_cite, previous_citation, note_number) if previous_cite then -- a. the current cite immediately follows on another cite, within the same -- citation, that references the same item if cite_item.id == previous_cite.id then return previous_cite end elseif previous_citation then -- (hidden) The previous citation is the only one in the previous note. -- See also -- https://github.com/citation-style-language/documentation/issues/121 -- position_IbidWithMultipleSoloCitesInBackref.txt -- b. the current cite is the first cite in the citation, and the previous -- citation consists of a single cite referencing the same item local previous_note_number = previous_citation.properties.noteIndex local num_previous_note_citations = #self.note_citations_map[previous_note_number] if (previous_note_number == note_number - 1 and num_previous_note_citations == 1) or previous_note_number == note_number then if #previous_citation.citationItems == 1 then previous_cite = previous_citation.citationItems[1] if previous_cite.id == cite_item.id then return previous_cite end end end end return nil end function CiteProc:_get_cite_position(item, preceding_cite) if preceding_cite.locator then if item.locator then if item.locator == preceding_cite.locator and item.label == preceding_cite.label then return Position.Ibid else return Position.IbidWithLocator end else return Position.Subsequent end else if item.locator then return Position.IbidWithLocator else return Position.Ibid end end end function CiteProc:makeCitationCluster(citation_items) local special_form = nil local items = {} for i, cite_item in ipairs(citation_items) do cite_item = self:normalize_cite_item(cite_item) local item_data = self:get_item(cite_item.id) -- Create a wrapper of the orignal item from registry so that -- it may hold different `locator` or `position` values for cites. local cite_item = setmetatable(cite_item, {__index = item_data}) if not special_form then for _, form in ipairs({"author-only", "suppress-author", "coposite"}) do if cite_item[form] then special_form = form end end end -- Set "first-reference-note-number" variable when called from -- processCitationCluster() > updateItems() local citations = self.registry.citations_by_item_id[cite_item.id] if citations and #citations > 0 then item_data["first-reference-note-number"] = citations[1].properties.noteIndex end cite_item.position_level = Position.First if self.cite_first_note_numbers[cite_item.id] then cite_item.position_level = Position.Subsequent else self.cite_first_note_numbers[cite_item.id] = 0 end local preceding_cite if i == 1 then local previous_citation = self.registry.previous_citation if previous_citation then if #previous_citation.citationItems == 1 and previous_citation.citationItems[1].id == cite_item.id then preceding_cite = previous_citation.citationItems[1] end end elseif citation_items[i - 1].id == cite_item.id then preceding_cite = citation_items[i - 1] end if preceding_cite then cite_item.position_level = self:_get_cite_position(cite_item, preceding_cite) end table.insert(items, cite_item) end if self.registry.requires_sorting then self:sort_bibliography() end self:check_valid_citation_element() local citation_element = self.style.citation if special_form == "author-only" and self.style.intext then citation_element = self.style.intext end local res = citation_element:build_cluster(items, self) -- local context = { -- build = {}, -- engine=self, -- } -- local res = self.style:render_citation(items, context) self.registry.previous_citation = { citationID = "pseudo-citation", citationItems = items, properties = { noteIndex = 0, } } return res end function CiteProc:makeBibliography(bibsection) -- The bibsection works as a filter described in -- . if not self.style.bibliography then return {{}, {}} end local res = {} self.registry.widest_label = "" self.registry.maxoffset = 0 local ids = self:get_sorted_refs() if bibsection then ids = self:filter_with_bibsection(ids, bibsection) end for _, id in ipairs(ids) do local str = self.style.bibliography:build_bibliography_str(id, self) table.insert(res, str) end local bib_start = self.output_format.markups["bibstart"] local bib_end = self.output_format.markups["bibend"] if type(bib_start) == "function" then bib_start = bib_start(self) end if type(bib_end) == "function" then bib_end = bib_end(self) end local params = { hangingindent = self.style.bibliography.hanging_indent, ["second-field-align"] = self.style.bibliography.second_field_align ~= nil, linespacing = self.style.bibliography.line_spacing, entryspacing = self.style.bibliography.entry_spacing, maxoffset = self.registry.maxoffset, widest_label = self.registry.widest_label, bibstart = bib_start, bibend = bib_end, entry_ids = util.clone(self.registry.reflist), } return {params, res} end function CiteProc:get_sorted_refs() if self.registry.requires_sorting then self:sort_bibliography() end return self.registry.reflist end function CiteProc:filter_with_bibsection(ids, bibsection) if bibsection.quash then return self:filter_quash(ids, bibsection) elseif bibsection.select then return self:filter_select(ids, bibsection) elseif bibsection.include then return self:filter_include(ids, bibsection) elseif bibsection.exclude then return self:filter_exclude(ids, bibsection) else return ids end end function CiteProc:match_bibsection_object(item, bibsection_object) local field = bibsection_object.field local value = bibsection_object.value -- util.debug(item.id) -- util.debug(field) -- util.debug(value) -- util.debug(item[field]) local match = false if value == "" then if not item[field] or item[field] == "" then match = true end else if type(item[field]) == "table" then if util.in_list(value, item[field]) then match = true end elseif field == "keyword" then if item.keyword and util.in_list(value, util.split(item.keyword, "%s*[;,]%s*")) then match = true end elseif item[field] == value then match = true end end if bibsection_object.negative then match = not match end -- util.debug(match) return match end function CiteProc:filter_select(ids, bibsection) -- Include the item if, and only if, all of the objects match. local res = {} for _, id in ipairs(ids) do local item = self.registry.registry[id] local match = true for _, bibsection_object in ipairs(bibsection.select) do if not self:match_bibsection_object(item, bibsection_object) then match = false break end end if match then table.insert(res, id) end end return res end function CiteProc:filter_include(ids, bibsection) -- Include the item if any of the objects match. local res = {} for _, id in ipairs(ids) do local item = self.registry.registry[id] local match = false for _, bibsection_object in ipairs(bibsection.include) do if self:match_bibsection_object(item, bibsection_object) then match = true break end end if match then table.insert(res, id) end end return res end function CiteProc:filter_exclude(ids, bibsection) -- Include the item if none of the objects match. local res = {} for _, id in ipairs(ids) do local item = self.registry.registry[id] local match = false for _, bibsection_object in ipairs(bibsection.exclude) do if self:match_bibsection_object(item, bibsection_object) then match = true break end end if not match then table.insert(res, id) end end return res end function CiteProc:filter_quash(ids, bibsection) -- Skip the item if all of the objects match. local res = {} for _, id in ipairs(ids) do local item = self.registry.registry[id] local match = true for _, bibsection_object in ipairs(bibsection.quash) do if not self:match_bibsection_object(item, bibsection_object) then match = false break end end if not match then table.insert(res, id) end end return res end function CiteProc:set_output_format(format) if format == "latex" then self.output_format = LatexWriter:new() elseif format == "html" then self.output_format = HtmlWriter:new() end end function CiteProc:enable_linking() self.opt.wrap_url_and_doi = true self.opt.citation_link = true end function CiteProc:disable_linking() self.opt.wrap_url_and_doi = false self.opt.citation_link = false end function CiteProc.create_element_tree(node) local element_name = node:get_element_name() local element_class = nodes[element_name] local el = nil if element_class then el = element_class:from_node(node) end if el then for i, child in ipairs(node:get_children()) do if child:is_element() then local child_element = CiteProc.create_element_tree(child) if child_element then if not el.children then el.children = {} end table.insert(el.children, child_element) end end end end return el end ---@param id ItemId ---@return ItemData? function CiteProc:get_item(id) ---@type ItemData? local item = self.registry.registry[id] if not item then item = self:_retrieve_item(id) if not item then return nil end item = self:process_extra_note(item) table.insert(self.registry.reflist, id) item["citation-number"] = #self.registry.reflist self.registry.registry[id] = item self.registry.requires_sorting = true end -- local res = {} -- setmetatable(res, {__index = item}) -- return res return item end ---@param id ItemId ---@return ItemData? function CiteProc:_retrieve_item(id) -- Retrieve, copy, and normalize local res = {} local item = self.sys.retrieveItem(id) if not item then util.warning(string.format("Didn't find a database entry for '%s'", id)) return nil end -- TODO: normalize data input item.id = tostring(item.id) for key, value in pairs(item) do res[key] = value end -- if res["page"] and not res["page-first"] then -- local page_first = util.split(res["page"], "%s*[&,-]%s*")[1] -- page_first = util.split(page_first, util.unicode["en dash"])[1] -- res["page-first"] = page_first -- end return res end -- TODO: Nomalize all inputs function CiteProc:process_extra_note(item) if item.note then local note_fields = {} for _, line in ipairs(util.split(item.note, "%s*\r?\n%s*")) do -- util.debug(line) local splits = util.split(line, ":%s+", 1) -- util.debug(splits) if #splits == 2 then local field, value = table.unpack(splits) -- util.debug(field) local variable_type = util.variable_types[field] if not item[field] or field == "type" or variable_type == "date" then if variable_type == "number" then item[field] = value elseif variable_type == "date" then item[field] = util.parse_edtf(value) elseif variable_type == "name" then if not note_fields[field] then note_fields[field] = {} end table.insert(note_fields[field], util.parse_extra_name(value)) else item[field] = value end end end end for field, value in pairs(note_fields) do item[field] = value end end return item end function CiteProc:sort_bibliography() -- Sort the items in registry according to the `sort` in `bibliography.` -- This will update the `citation-number` of each item. local bibliography_sort = nil if self.style.bibliography and self.style.bibliography.sort then bibliography_sort = self.style.bibliography.sort end if not bibliography_sort then return end local items = {} for _, id in ipairs(self.registry.reflist) do table.insert(items, self.registry.registry[id]) end local state = IrState:new() local context = Context:new() context.engine = self context.style = self.style context.area = self.style.bibliography context.in_bibliography = true context.locale = self:get_locale(self.lang) context.name_inheritance = self.style.bibliography.name_inheritance context.format = SortStringFormat:new() -- context.id = id context.cite = nil -- context.reference = self:get_item(id) bibliography_sort:sort(items, state, context) self.registry.reflist = {} self.tainted_item_ids = {} for i, item in ipairs(items) do if item["citation-number"] ~= i then self.tainted_item_ids[item.id] = true end item["citation-number"] = i self.registry.reflist[i] = item.id end self.registry.requires_sorting = false end function CiteProc:get_locale(lang) lang = util.primary_dialects[lang] or lang local locale = self.locales[lang] or self:get_merged_locales(lang) return locale end function CiteProc:get_merged_locales(lang) local fall_back_locales = {} local language = string.sub(lang, 1, 2) local primary_dialect = util.primary_dialects[language] -- 1. In-style cs:locale elements -- i. `xml:lang` set to chosen dialect, “de-AT” table.insert(fall_back_locales, self.style.locales[lang]) -- ii. `xml:lang` set to matching language, “de” (German) if language and language ~= lang then table.insert(fall_back_locales, self.style.locales[language]) end -- iii. `xml:lang` not set table.insert(fall_back_locales, self.style.locales["@generic"]) -- 2. Locale files -- iv. `xml:lang` set to chosen dialect, “de-AT” if lang then table.insert(fall_back_locales, self:get_system_locale(lang)) end -- v. `xml:lang` set to matching primary dialect, “de-DE” (Standard German) -- (only applicable when the chosen locale is a secondary dialect) if primary_dialect and primary_dialect ~= lang then table.insert(fall_back_locales, self:get_system_locale(primary_dialect)) end -- vi. `xml:lang` set to “en-US” (American English) if lang ~= "en-US" and primary_dialect ~= "en-US" then table.insert(fall_back_locales, self:get_system_locale("en-US")) end -- Merge locales local locale = Locale:new() for i = #fall_back_locales, 1, -1 do local fall_back_locale = fall_back_locales[i] locale:merge(fall_back_locale) end self.locales[lang] = locale return locale end function CiteProc:get_system_locale(lang) local locale = self.system_locales[lang] if locale then return locale end local locale_str = self.sys.retrieveLocale(lang) if not locale_str then util.warning(string.format("Failed to retrieve locale \"%s\"", lang)) return nil end local locale_xml = dom.parse(locale_str) local root_element = locale_xml:get_path("locale")[1] locale = Locale:from_node(root_element) self.system_locales[lang] = locale return locale end function CiteProc:get_style_class() if self.style and self.style.class then return self.style.class else return nil end end ---@class Macro: Element local Macro = Element:derive("macro") function Macro:from_node(node) local o = Macro:new() o.children = {} o:set_attribute(node, "name") o:process_children_nodes(node) return o end function Macro:build_ir(engine, state, context) local ir = self:build_group_ir(engine, state, context) return ir end engine.CiteProc = CiteProc return engine