-- -- Copyright (c) 2021-2022 Zeping Lee -- Released under the MIT license. -- Repository: https://github.com/zepinglee/citeproc-lua -- local element = {} local unicode = require("unicode") local richtext = require("citeproc-richtext") local util = require("citeproc-util") local Element = { default_options = {}, } function Element:new () local o = {} setmetatable(o, self) self.__index = self return o end Element.option_type = { ["et-al-min"] = "integer", ["et-al-use-first"] = "integer", ["et-al-subsequent-min"] = "integer", ["et-al-subsequent-use-first"] = "integer", ["near-note-distance"] = "integer", ["line-spacing"] = "integer", ["entry-spacing"] = "integer", ["names-min"] = "integer", ["names-use-first"] = "integer", ["limit-day-ordinals-to-day-1"] = "boolean", ["punctuation-in-quote"] = "boolean", ["et-al-use-last"] = "boolean", ["initialize"] = "boolean", ["initialize-with-hyphen"] = "boolean", ["disambiguate-add-names"] = "boolean", ["disambiguate-add-givenname"] = "boolean", ["disambiguate-add-year-suffix"] = "boolean", ["hanging-indent"] = "boolean", ["names-use-last"] = "boolean", ["quotes"] = "boolean", ["strip-periods"] = "boolean", } Element.inheritable_options = { -- Style ["initialize-with-hyphen"] = true, ["page-range-format"] = true, ["demote-non-dropping-particle"] = true, -- Citation ["disambiguate-add-givenname"] = true, ["givenname-disambiguation-rule"] = true, ["disambiguate-add-names"] = true, ["disambiguate-add-year-suffix"] = true, ["cite-group-delimiter"] = true, ["collapse"] = true, ["year-suffix-delimiter"] = true, ["after-collapse-delimiter"] = true, ["near-note-distance"] = true, -- Bibliography ["second-field-align"] = true, -- for use in layout ["subsequent-author-substitute"] = true, ["subsequent-author-substitute-rule"] = true, -- Date ["date-parts"] = true, -- Names ["and"] = true, ["delimiter-precedes-et-al"] = true, ["delimiter-precedes-last"] = true, ["et-al-min"] = true, ["et-al-use-first"] = true, ["et-al-use-last"] = true, ["et-al-subsequent-min"] = true, ["et-al-subsequent-use-first"] = true, ["names-min"] = true, ["names-use-first"] = true, ["names-use-last"] = true, ["initialize-with"] = true, ["name-as-sort-order"] = true, ["sort-separator"] = true, ["name-form"] = true, ["name-delimiter"] = true, ["names-delimiter"] = true, } function Element:render (item, context) self:debug_info(context) context = self:process_context(context) return self:render_children(item, context) end function Element:render_children (item, context) local output = {} for i, child in ipairs(self:get_children()) do if child:is_element() then if child.render == nil then local element_name = child:get_element_name() util.warning("Unkown type \"" .. element_name .. "\"") end local str = child:render(item, context) table.insert(output, str) end end return self:concat(output, context) end function Element:set_base_class (node) if node:is_element() then local org_meta_table = getmetatable(node) setmetatable(node, {__index = function (_, key) if self[key] then return self[key] else return org_meta_table[key] end end}) end end function Element:debug_info (context, debug) -- debug = true if debug then local text = "" local level = 0 if context and context.level then level = context.level + 1 end text = text .. string.rep(" ", 2 * level) text = text .. self:get_element_name() local attrs = {} if self._attr then for attr, value in pairs(self._attr) do table.insert(attrs, attr .. "=\"" .. value .. "\"") end text = text .. "[" .. table.concat(attrs, " ") .. "]" end io.stderr:write(text .. "\n") end end function Element:get_child (type) for _, child in ipairs(self:get_children()) do if child:get_element_name() == type then return child end end return nil end function Element:get_style () local style = self:root_node().style assert(style ~= nil) return style end function Element:get_engine () local engine = self:root_node().engine assert(engine ~= nil) return engine end function Element:process_context (context) local state = { -- The `build` table is directly passed to new context. build = context.build or {}, -- The `option` table is copied. options = {}, -- Other items in `context` is copied. } for key, value in pairs(self.default_options) do state.options[key] = value end if context then local element_name = self:get_element_name() for key, value in pairs(context) do if key == "options" then for k, v in pairs(context.options) do if self.inheritable_options[k] then state.options[k] = v if element_name == "name" then if k == "name-form" then state.options["form"] = v end if k == "name-delimiter" then state.options["delimiter"] = v end elseif element_name == "names" then if k == "names-delimiter" then state.options["delimiter"] = v end end end end else state[key] = value end end if state.level then state.level = state.level + 1 else state.level = 0 end end if self._attr then for key, value in pairs(self._attr) do if self.option_type[key] == "integer" then value = tonumber(value) elseif self.option_type[key] == "boolean" then value = (value == "true") end state.options[key] = value end end return state end function Element:get_option (key, context) assert(context ~= nil) return context.options[key] end function Element:get_locale_option (key) local locales = self:get_style():get_locales() for i, locale in ipairs(locales) do local option = locale:get_option(key) if option ~= nil then return option end end return nil end function Element:get_variable (item, name, context) if context.suppressed_variables and context.suppressed_variables[name] then return nil else local res = item[name] if type(res) == "table" and res._type == "RichText" then -- TODO: should be deep copy res = res:shallow_copy() end if res and res ~= "" then if context.suppress_subsequent_variables then context.suppressed_variables[name] = true end end return res end end function Element:get_macro (name) local query = string.format("macro[name=\"%s\"]", name) local macro = self:root_node():query_selector(query)[1] if not macro then error(string.format("Failed to find %s.", query)) end return macro end function Element:get_term (name, form, number, gender) return self:get_style():get_term(name, form, number, gender) end -- Formatting function Element:escape (str, context) return str -- return self:get_engine().formatter.text_escape(str) end function Element:format(text, context) if not text or text == "" then return nil end if text._type ~= "RichText" then text = richtext.new(text) end local attributes = { "font-style", "font-variant", "font-weight", "text-decoration", "vertical-align", } for _, attribute in ipairs(attributes) do local value = context.options[attribute] if value then if text.formats[attribute] then local new = richtext.new() new.contents = {text} text = new end text:add_format(attribute, value) end end return text end -- Affixes function Element:wrap (str, context) if not str or str == "" then return nil end local prefix = context.options["prefix"] local suffix = context.options["suffix"] local res = str if prefix and prefix ~= "" then local linkable = false local variable_name = context.options["variable"] if variable_name == "DOI" or variable_name == "PMID" or variable_name == "PMCID" then linkable = true end if variable_name == "URL" or (linkable and not string.match(prefix, "^https?://")) then res:add_format(variable_name, "true") end res = richtext.concat(prefix, res) if linkable and string.match(prefix, "^https?://") then res:add_format("URL", "true") end end if suffix and suffix ~= "" then res = richtext.concat(res, suffix) end return res end -- Delimiters function Element:concat (strings, context) local delimiter = context.options["delimiter"] return richtext.concat_list(strings, delimiter) end -- Display function Element:display(text, context) if not text then return text end local value = context.options["display"] if not value then return text end if type(text) == "string" then text = richtext.new(text) end text:add_format("display", value) return text end -- Quotes function Element:quote (str, context) if not str then return nil end if context.sorting then return str end if not str._type == "RichText" then str = richtext.new(str) end local quotes = context.options["quotes"] or false if quotes then str:add_format("quotes", "true") end return str end -- Strip periods function Element:strip_periods (str, context) if not str then return nil end if str._type ~= "RichText" then str = richtext.new(str) end local strip_periods = context.options["strip-periods"] if strip_periods then str:strip_periods() end return str end -- Text-case function Element:case (text, context) if not text or text == "" then return nil end if text._type ~= "RichText" then text = richtext.new(text) end local text_case = context.options["text-case"] if not text_case then return text end if text_case == "title" then -- title case conversion only affects English-language items local language = context.item["language"] if not language then language = self:get_style():get_attribute("default-locale") or "en-US" end if not util.startswith(language, "en") then return text end end text:add_format("text-case", text_case) return text end element.Element = Element return element