-- -- Copyright (c) 2021-2024 Zeping Lee -- Released under the MIT license. -- Repository: https://github.com/zepinglee/citeproc-lua -- local context = {} local unicode local LocalizedQuotes local util if kpse then unicode = require("citeproc-unicode") LocalizedQuotes = require("citeproc-output").LocalizedQuotes util = require("citeproc-util") else unicode = require("citeproc.unicode") LocalizedQuotes = require("citeproc.output").LocalizedQuotes util = require("citeproc.util") end ---@class Context ---@field reference ItemData? ---@field format OutputFormat ---@field cite_id string? ---@field engine CiteProc ---@field style any Style ---@field lang LanguageCode ---@field locale Locale? ---@field name_citation any ---@field names_delimiter any ---@field position any ---@field disamb_pass any ---@field cite CitationItem? ---@field bib_number integer? ---@field in_bibliography boolean ---@field sort_key any ---@field year_suffix any local Context = { reference = nil, format = nil, cite_id = nil, style = nil, locale = nil, name_citation = nil, names_delimiter = nil, position = nil, disamb_pass = nil, cite = nil, bib_number = nil, in_bibliography = false, sort_key = nil, year_suffix = nil, } function Context:new() local o = { lang = "en-US", in_bibliography = false } setmetatable(o, self) self.__index = self return o end function Context:get_variable(name, form) local variable_type = util.variable_types[name] if variable_type == "number" then return self:get_number(name) -- elseif variable_type == "date" then -- return self:get_date(name) elseif variable_type == "name" then return self:get_name(name) else return self:get_ordinary(name, form) end end ---@param name string ---@return string | number? function Context:get_number(name) if name == "locator" then if self.cite then return self.cite.locator end elseif name == "citation-number" then -- return self.bib_number return self.reference["citation-number"] elseif name == "first-reference-note-number" then if self.cite then return self.cite["first-reference-note-number"] end elseif name == "page-first" then return self.page_first(self.reference.page) else return self.reference[name] end return nil end function Context:get_ordinary(name, form) local res = nil local variable_name = name if form and form ~= "long" then variable_name = variable_name .. "-" .. form end if variable_name == "locator" or variable_name == "label" and self.cite then res = self.cite[variable_name] elseif variable_name == "citation-label" then res = self.reference["citation-label"] if not res then res = util.get_citation_label(self.reference) self.reference["citation-label"] = res end else res = self.reference[variable_name] end if res then return res end if variable_name == "container-title-short" then res = self.reference["journalAbbreviation"] if res then return res end end if form then res = self.reference[name] if res then return res end end -- if name == "title-short" or name == "container-title-short" then -- variable_name = string.gsub(name, "%-short$", "") -- res = self.reference[variable_name] -- end return res end -- TODO: optimize: process only once -- TODO: organize the name parsing code function Context:get_name(variable_name) local names = self.reference[variable_name] if names then for _, name in ipairs(names) do if name.family == "" then name.family = nil end if name.given == "" then name.given = nil end if name.given and not name.family then name.family = name.given name.given = nil end self:parse_name_suffix(name) self:split_ndp_family(name) -- self:split_given_ndp(name) self:split_given_dp(name) end end return names end function Context:parse_name_suffix(name) if not name.suffix and name.family and string.match(name.family, ",") then local words = util.split(name.family, ",%s*") name.suffix = words[#words] name.family = table.concat(util.slice(words, 1, -2), ", ") end if not name.suffix and name.given and string.match(name.given, ",") then -- Split name suffix: magic_NameSuffixNoComma.txt -- "John, III" => given: "John", suffix: "III" local words = util.split(name.given, ",%s*") name.suffix = words[#words] name.given = table.concat(util.slice(words, 1, -2), ", ") end end function Context:split_ndp_family(name) if name["non-dropping-particle"] or not name.family then return end if util.startswith(name.family, '"') and util.endswith(name.family, '"') then -- Stop parsing family name if surrounded by quotation marks -- bugreports_parseName.txt name.family = string.gsub(name.family, '"', "") return end local ndp_parts = {} local family_parts = {} local parts = util.split(name.family) for i, part in ipairs(parts) do local ndp, family -- d'Aubignac ndp, family = string.match(part, "^(%l')(.+)$") if ndp and family then table.insert(ndp_parts, ndp) parts[i] = family else ndp, family = string.match(part, "^(%l’)(.+)$") if ndp and family then table.insert(ndp_parts, ndp) parts[i] = family else -- al-Aswānī ndp, family = string.match(part, "^(%l+%-)(.+)$") if ndp and family then table.insert(ndp_parts, ndp) parts[i] = family elseif i < #parts and unicode.islower(part) then table.insert(ndp_parts, part) end end end if ndp or i == #parts then for j = i, #parts do table.insert(family_parts, parts[j]) end break end if not unicode.islower(part) then for j = i, #parts do table.insert(family_parts, parts[j]) end break end end if #ndp_parts > 0 then name["non-dropping-particle"] = table.concat(ndp_parts, " ") name.family = table.concat(family_parts, " ") end end function Context:split_given_dp(name) if name["dropping-particle"] or not name.given then return end local dp_parts = {} local given_parts = {} local parts = util.split(name.given) for i = #parts, 1, -1 do local part = parts[i] if i == 1 or not unicode.islower(part) then for j = 1, i do table.insert(given_parts, parts[j]) end break end -- name_ParsedDroppingParticleWithApostrophe.txt -- given: "François Hédelin d'" => -- given: "François Hédelin", dropping-particle: "d'" if string.match(part, "^%l+'?$") or string.match(part, "^%l+’$") then table.insert(dp_parts, 1, part) end end if #dp_parts > 0 then name["dropping-particle"] = table.concat(dp_parts, " ") name.given = table.concat(given_parts, " ") end end -- function Context:split_given_ndp(name) -- if name["non-dropping-particle"] or not name.given then -- return -- end -- if not (string.match(name.given, "%l'$") or string.match(name.given, "%l’$")) then -- return -- end -- local words = util.split(name.given) -- if #words < 2 then -- return -- end -- local last_word = words[#words] -- if util.endswith(last_word, "'") or util.endswith(last_word, util.unicode["apostrophe"]) then -- name["non-dropping-particle"] = last_word -- name.given = table.concat(util.slice(words, 1, -2), " ") -- end -- util.debug(name) -- end function Context:get_localized_date(form) return self.locale.dates[form] end function Context:get_macro(name) local res = self.style.macros[name] if not res then util.error(string.format("Undefined macro '%s'", name)) end return res end function Context:get_simple_term(name, form, plural) -- assert(self.locale) return self.locale:get_simple_term(name, form, plural) end ---@return LocalizedQuotes function Context:get_localized_quotes() return LocalizedQuotes:new( self:get_simple_term("open-quote"), self:get_simple_term("close-quote"), self:get_simple_term("open-inner-quote"), self:get_simple_term("close-inner-quote"), self.locale.style_options.punctuation_in_quote ) end ---@param page string|number ---@return string? function Context.page_first(page) if not page then return nil end page = tostring(page) local page_first = util.split(page, "%s*[&,-]%s*")[1] return util.split(page_first, util.unicode["en dash"])[1] end -- https://docs.citationstyles.org/en/stable/specification.html#non-english-items function Context:is_english() local language = self:get_variable("language") if util.startswith(self.engine.lang, "en") then if not language or util.startswith(language, "en") then return true else return false end else if language and util.startswith(language, "en") then return true else return false end end end ---@class IrState ---@field macro_stack string[] ---@field suppressed {[string]: boolean} local IrState = {} function IrState:new(style, cite_id, cite, reference) local o = { macro_stack = {}, suppressed = {}, } setmetatable(o, self) self.__index = self return o end function IrState:push_macro(macro_name) for _, name in ipairs(macro_name) do if name == macro_name then util.error(string.format("Recursive macro '%s'.", macro_name)) end table.insert(self.macro_stack, macro_name) end end function IrState:pop_macro(macro_name) table.remove(self.macro_stack) end context.Context = Context context.IrState = IrState return context