diff options
author | Karl Berry <karl@freefriends.org> | 2022-01-21 22:58:35 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2022-01-21 22:58:35 +0000 |
commit | 39207a02179eab0fca34dfb7bb8dbbb6215fdc7a (patch) | |
tree | 14f6a970d4a28da5523838a10debde16683f3fb9 /Master/texmf-dist/scripts | |
parent | 59ab26e4b8b11acdd61b232207d7a9ae578c63e4 (diff) |
citation-style-language (21jan22)
git-svn-id: svn://tug.org/texlive/trunk@61687 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/scripts')
22 files changed, 6051 insertions, 0 deletions
diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc b/Master/texmf-dist/scripts/citation-style-language/citeproc new file mode 100755 index 00000000000..3a72892ec7c --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc @@ -0,0 +1,188 @@ +#!/usr/bin/env texlua + +kpse.set_program_name("luatex") + +require("lualibs") +local citeproc = require("citeproc") +local util = require("citeproc-util") +local core = require("csl-core") + +local function getopt( arg, options ) + local tab = {} + for k, v in ipairs(arg) do + if string.sub( v, 1, 2) == "--" then + local x = string.find( v, "=", 1, true ) + if x then tab[ string.sub( v, 3, x-1 ) ] = string.sub( v, x+1 ) + else tab[ string.sub( v, 3 ) ] = true + end + elseif string.sub( v, 1, 1 ) == "-" then + local y = 2 + local l = string.len(v) + local jopt + while ( y <= l ) do + jopt = string.sub( v, y, y ) + if string.find( options, jopt, 1, true ) then + if y < l then + tab[ jopt ] = string.sub( v, y+1 ) + y = l + else + tab[ jopt ] = arg[ k + 1 ] + end + else + tab[ jopt ] = true + end + y = y + 1 + end + else + if tab.file then + error(string.format('Invalid argument "%s"', v)) + end + tab.file = v + end + + end + return tab +end + + +local function print_version() + io.write(string.format("CiteProc-Lua %s\n", citeproc.__VERSION__)) +end + + +local function print_help() + io.write("Usage: citeproc [options] auxname[.aux]\n") + io.write("Options:\n") + io.write(" -h, --help Print this message and exit.\n") + io.write(" -V, --version Print the version number and exit.\n") +end + + +local function convert_bib(path, output_path) + local contents = util.read_file(path) + local bib = citeproc.parse_bib(contents) + if not output_path then + output_path = string.gsub(path, "%.bib$", ".json") + end + local file = io.open(output_path, "w") + file:write(utilities.json.tojson(bib)) + file:write('\n') + file:close() +end + + + +local function read_aux_file(aux_file) + local bib_style = nil + local bib_files = {} + local citations = {} + local csl_options = {} + + local file = io.open(aux_file, "r") + if not file then + error(string.format('Failed to open "%s"', aux_file)) + return + end + for line in file:lines() do + local match + match = string.match(line, "^\\bibstyle%s*(%b{})") + if match then + bib_style = string.sub(match, 2, -2) + else + match = string.match(line, "^\\bibdata%s*(%b{})") + if match then + for _, bib in ipairs(util.split(string.sub(match, 2, -2), "%s*,%s*")) do + table.insert(bib_files, bib) + end + else + match = string.match(line, "^\\citation%s*(%b{})") + if match then + local citation = core.make_citation(string.sub(match, 2, -2)) + table.insert(citations, citation) + else + match = string.match(line, "^\\csloptions%s*(%b{})") + if match then + for key, value in string.gmatch(match, "([%w-]+)=(%w+)") do + csl_options[key] = value + end + end + end + end + end + end + file:close() + + return bib_style, bib_files, citations, csl_options +end + + +local function process_aux_file(aux_file) + if not util.endswith(aux_file, ".aux") then + aux_file = aux_file .. ".aux" + end + + local style_name, bib_files, citations, csl_options = read_aux_file(aux_file) + + local lang = csl_options.locale + + local engine = core.init(style_name, bib_files, lang) + if csl_options.linking == "true" then + engine:enable_linking() + end + local style_class = engine:get_style_class() + + local citation_strings = core.process_citations(engine, citations) + + local output_string = "" + + for _, citation in ipairs(citations) do + local citation_id = citation.citationID + if citation_id ~= "nocite" then + local citation_str = citation_strings[citation_id] + output_string = output_string .. string.format("\\cslcite{%s}{{%s}{%s}}\n", citation_id, style_class, citation_str) + end + end + + output_string = output_string .. "\n" + + local result = core.make_bibliography(engine) + output_string = output_string .. result + + local output_path = string.gsub(aux_file, "%.aux$", ".bbl") + local bbl_file = io.open(output_path, "w") + bbl_file:write(output_string) + bbl_file:close() +end + + +local function main() + local args = getopt(arg, "o") + + -- for k, v in pairs(args) do + -- print( k, v ) + -- end + + if args.V or args.version then + print_version() + return + elseif args.h or args.help then + print_help() + return + end + + if not args.file then + error("citeproc: Need exactly one file argument.\n") + end + + local path = args.file + + local output_path = args.o or args.output + if util.endswith(path, ".bib") then + convert_bib(path, output_path) + else + process_aux_file(path) + end + +end + +main() diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-bib.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-bib.lua new file mode 100644 index 00000000000..d3532c098be --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-bib.lua @@ -0,0 +1,395 @@ +--[[ + A naive implementation of a Bib(La)TeX dateabase (.bib) parser + References: + - http://mirrors.ctan.org/biblio/bibtex/base/btxdoc.pdf + - http://mirrors.ctan.org/info/bibtex/tamethebeast/ttb_en.pdf + - https://github.com/brechtm/citeproc-py/blob/master/citeproc/source/bibtex/bibparse.py + - http://maverick.inria.fr/~Xavier.Decoret/resources/xdkbibtex/bibtex_summary.html + - https://github.com/pcooksey/bibtex-js/blob/master/src/bibtex_js.js +--]] + +local bib = {} + +require("lualibs") +local unicode = require("unicode") + +local util = require("citeproc-util") + + +local path = "citeproc-bib-data.json" +if kpse then + path = kpse.find_file(path) +end +if path then + local contents = util.read_file(path) + if not contents then + error(string.format('Failed to find "%s"', path)) + end + bib.bib_data = utilities.json.tolua(contents) +end + +function bib.parse(contents) + local items = {} + for item_contents in string.gmatch(contents, "(@%w+%b{})") do + local item = bib.parse_item(item_contents) + table.insert(items, item) + end + return items +end + +function bib.parse_item(contents) + contents = string.gsub(contents, "%s*\r?\n%s*", " ") + local bib_type, id + bib_type, id, contents = string.match(contents, "^@(%w+){([^%s,]+),%s*(.*)}$") + if not id then + return nil + end + + local item = {id = id} + + bib_type = string.lower(bib_type) + local type_data = bib.bib_data.types[bib_type] + if type_data then + if type_data.csl then + item.type = type_data.csl + else + item.type = "document" + end + else + item.type = "document" + end + + local bib_fields = bib.parse_fields(contents) + -- util.debug(bib_fields) + + for bib_field, value in pairs(bib_fields) do + local csl_field, csl_value = bib.convert_field(bib_field, value) + + if csl_field and not item[csl_field] then + item[csl_field] = csl_value + end + end + + bib.process_special_fields(item, bib_fields) + + return item +end + +function bib.parse_fields(contents) + local fields = {} + local field_patterns = { + "^(%w+)%s*=%s*(%b{}),?%s*(.-)$", + '^(%w+)%s*=%s*"([^"]*)",?%s*(.-)$', + "^(%w+)%s*=%s*(%w+),?%s*(.-)$", + } + + while #contents > 0 do + local field, value, rest + -- This pattern may fail in the case of `title = {foo\}bar}`. + for pattern_index, pattern in ipairs(field_patterns) do + field, value, rest = string.match(contents, pattern) + if value then + if pattern_index == 1 then + -- Strip braces "{}" + value = string.sub(value, 2, -2) + elseif pattern_index == 3 then + if not string.match(value, "^%d+$") then + local string_name = value + local macro = bib.bib_data.macros[string_name] + if macro then + value = macro.value + else + util.warning(string.format('String name "%s" is undefined', string_name)) + end + end + end + fields[field] = value + contents = rest + break + end + end + end + return fields +end + +function bib.convert_field(bib_field, value) + local field_data = bib.bib_data.fields[bib_field] + if not field_data then + return nil, nil + end + local csl_field = field_data.csl + if not csl_field then + return nil, nil + end + + value = bib.unescape(bib_field, value) + + local field_type = field_data.type + if field_type == "name" then + value = bib.parse_names(value) + elseif field_type == "date" then + value = bib.parse_date(value) + end + + if bib_field == "title" or bib_field == "booktitle" then + -- TODO: check if the original title is in sentence case + value = bib.convert_sentence_case(value) + end + + if bib_field == "volume" or bib_field == "pages" then + value = string.gsub(value, util.unicode["en dash"], "-") + end + + return csl_field, value +end + +function bib.unescape(field, str) + str = string.gsub(str, "%-%-%-", util.unicode["em dash"]) + str = string.gsub(str, "%-%-", util.unicode["en dash"]) + str = string.gsub(str, "``", util.unicode["left double quotation mark"]) + str = string.gsub(str, "''", util.unicode["right double quotation mark"]) + str = string.gsub(str, "`", util.unicode["left single quotation mark"]) + str = string.gsub(str, "'", util.unicode["right single quotation mark"]) + -- TODO: unicode chars like \"{o} + str = string.gsub(str, "\\#", "#") + str = string.gsub(str, "\\%$", "$") + str = string.gsub(str, "\\%%", "%") + str = string.gsub(str, "\\&", "&") + str = string.gsub(str, "\\{", "{") + str = string.gsub(str, "\\}", "}") + str = string.gsub(str, "\\_", "_") + if field ~= "url" then + str = string.gsub(str, "~", util.unicode["no-break space"]) + end + str = string.gsub(str, "\\quad%s+", util.unicode["em space"]) + return str +end + +function bib.convert_sentence_case(str) + local res = "" + local to_lower = false + local brace_level = 0 + for _, code_point in utf8.codes(str) do + local char = utf8.char(code_point) + if to_lower and brace_level == 0 then + char = unicode.utf8.lower(char) + end + if string.match(char, "%S") then + to_lower = true + end + if char == "{" then + brace_level = brace_level + 1 + char = "" + elseif char == "}" then + brace_level = brace_level - 1 + char = "" + elseif char == ":" then + to_lower = false + end + res = res .. char + end + return res +end + +function bib.parse_names(str) + -- "{International Federation of Library Association and Institutions}" + local names = {} + local brace_level = 0 + local name = "" + local last_word = "" + for i = 1, #str do + local char = string.sub(str, i, i) + if char == " " then + if brace_level == 0 and last_word == "and" then + table.insert(names, name) + name = "" + else + if name ~= "" then + name = name .. " " + end + name = name .. last_word + end + last_word = "" + else + last_word = last_word .. char + if char == "{" then + brace_level = brace_level + 1 + elseif char == "}" then + brace_level = brace_level - 1 + end + end + end + + if name ~= "" then + name = name .. " " + end + name = name .. last_word + table.insert(names, name) + + for i, name in ipairs(names) do + names[i] = bib.parse_single_name(name) + end + return names +end + +function bib.parse_single_name(str) + local literal = string.match(str, "^{(.*)}$") + if literal then + return { + literal = literal, + } + end + + local name_parts = util.split(str, ",%s*") + if #name_parts > 1 then + return bib.parse_revesed_name(name_parts) + else + return bib.parse_non_revesed_name(str) + end +end + +function bib.parse_revesed_name(name_parts) + local name = {} + local von, last, jr, first + if #name_parts == 2 then + first = name_parts[2] + elseif #name_parts >= 3 then + jr = name_parts[2] + first = name_parts[3] + end + if first and first ~= "" then + name.given = first + end + if jr and jr ~= "" then + name.suffix = jr + end + + last = name_parts[1] + local words = util.split(last) + local index = #words - 1 + while index > 0 and string.match(words[index], "^%L") do + index = index - 1 + end + name.family = util.concat(util.slice(words, index + 1), " ") + if index >= 1 then + von = util.concat(util.slice(words, 1, index), " ") + name["non-dropping-particle"] = von + end + return name +end + +function bib.parse_non_revesed_name(str) + local name = {} + local words = util.split(str) + + local index = 1 + -- TODO: case determination for pseudo-characters (e.g., "\bb{BB}") + while index < #words and string.match(words[index], "^%L") do + index = index + 1 + end + if index > 1 then + name.given = util.concat(util.slice(words, 1, index - 1), " ") + end + + local particle_start_index = index + index = #words - 1 + while index >= particle_start_index and string.match(words[index], "^%L") do + index = index - 1 + end + if index >= particle_start_index then + local particles = util.slice(words, particle_start_index, index) + -- TODO: distiguish dropping and non-dropping particles + name["non-dropping-particle"] = util.concat(particles, " ") + end + name.family = util.concat(util.slice(words, index + 1), " ") + + return name +end + +function bib.parse_date(str) + local date_range = util.split(str, "/") + if #date_range == 1 then + date_range = util.split(str, util.unicode["en dash"]) + end + + local literal = { literal = str } + + if #date_range > 2 then + return literal + end + + local date = {} + date["date-parts"] = {} + for _, date_part in ipairs(date_range) do + local date_ = bib.parse_single_date(date_part) + if not date_ then + return literal + end + table.insert(date["date-parts"], date_) + end + return date +end + +function bib.parse_single_date(str) + local date = {} + for _, date_part in ipairs(util.split(str, "%-")) do + if not string.match(date_part, "^%d+$") then + return nil + end + table.insert(date, tonumber(date_part)) + end + return date +end + +function bib.process_special_fields(item, bib_fields) + if item.type == "document" then + if item.URL then + item.type = "webpage" + else + item.type = "article" + end + end + + if item.type == "article-journal" then + if not item["container-title"] then + item.type = "article" + end + end + + if bib_fields.year and not item.issued then + item.issued = bib.parse_date(bib_fields.year) + end + local month = bib_fields.month + if month and string.match(month, "^%d+$") then + if item.issued and item.issued["date-parts"] and + item.issued["date-parts"][1] and + item.issued["date-parts"][1][2] == nil then + item.issued["date-parts"][1][2] = tonumber(month) + end + end + + if item.number then + if not item.issue and item.type == "article-journal" or item.type == "article-magazine" or item.type == "article-newspaper" or item.type == "periodical" then + item.issue = item.number + item.number = nil + elseif item.type == "patent" or item.type == "report" or item.type == "standard" then + else + item["collection-number"] = item.number + item.number = nil + end + end + + if not item.PMID and bib_fields.eprint and string.lower(bib_fields.eprinttype) == "pubmed" then + item.PMID = bib_fields.eprint + end + + -- if not item.language then + -- if util.has_cjk_char(item.title) then + -- item.language = "zh" + -- else + -- item.language = "en" + -- end + -- end +end + +return bib diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-element.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-element.lua new file mode 100644 index 00000000000..58d3402d2c5 --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-element.lua @@ -0,0 +1,410 @@ +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 diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-engine.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-engine.lua new file mode 100644 index 00000000000..ac8cc9c44e3 --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-engine.lua @@ -0,0 +1,419 @@ +--[[ + Copyright (C) 2021 Zeping Lee +--]] + +local engine = {} + +local dom = require("luaxml-domobject") + +local richtext = require("citeproc-richtext") +local element = require("citeproc-element") +local nodes = require("citeproc-nodes") +local formats = require("citeproc-formats") +local util = require("citeproc-util") + + +local CiteProc = {} + +function CiteProc.new (sys, style, lang, force_lang) + if sys == nil 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 + local o = {} + o.registry = { + citations = {}, -- A map + citation_strings = {}, -- A list + registry = {}, -- A map + reflist = {}, -- A list + previous_citation = nil, + requires_sorting = false, + } + + o.sys = sys + o.system_locales = {} + + if type(style) == "string" then + o.csl = dom.parse(style) + else + o.csl = style + end + o.csl:traverse_elements(CiteProc.set_base_class) + o.csl:root_node().engine = o + o.style = o.csl:get_path("style")[1] + o.style.lang = lang + o.csl:root_node().style = o.style + + o.style:set_lang(lang, force_lang) + + o.formatter = formats.latex + o.linking_enabled = false + + setmetatable(o, { __index = CiteProc }) + return o +end + +function CiteProc:updateItems (ids) + self.registry.reflist = {} + self.registry.registry = {} + for _, id in ipairs(ids) do + self:get_item(id) + end +end + +function CiteProc:updateUncitedItems(ids) + for _, id in ipairs(ids) do + if not self.registry.registry[id] then + self:get_item(id) + end + end + -- TODO: disambiguation +end + +function CiteProc:processCitationCluster(citation, citationsPre, citationsPost) + -- citation = { + -- citationID = "CITATION-3", + -- citationItems = { + -- { id = "ITEM-1" }, + -- { id = "ITEM-2" }, + -- }, + -- properties = { + -- noteIndex = 3, + -- }, + -- } + -- citationsPre = { + -- {"CITATION-1", 1}, + -- {"CITATION-2", 2}, + -- } + -- citationsPost = { + -- {"CITATION-4", 4}, + -- } + -- returns = { + -- { + -- bibchange = true, + -- citation_errors = {}, + -- }, + -- { + -- { 2, "[1,2]", "CITATION-3" } + -- } + -- } + self.registry.citations[citation.citationID] = citation + + local items = {} + + for _, cite_item in ipairs(citation.citationItems) do + cite_item.id = tostring(cite_item.id) + local position_first = (self.registry.registry[cite_item.id] == nil) + local item_data = self:get_item(cite_item.id) + + if item_data then + -- Create a wrapper of the orignal item from registry so that + -- it may hold different `locator` or `position` values for cites. + local item = setmetatable({}, {__index = function (_, key) + if cite_item[key] then + return cite_item[key] + else + return item_data[key] + end + end}) + + if not item.position and position_first then + item.position = util.position_map["first"] + end + + local first_reference_note_number = nil + for _, pre_citation in ipairs(citationsPre) do + pre_citation = self.registry.citations[pre_citation[1]] + for _, pre_cite_item in ipairs(pre_citation.citationItems) do + if pre_cite_item.id == cite_item.id then + first_reference_note_number = pre_citation.properties.noteIndex + end + break + end + if first_reference_note_number then + break + end + end + item["first-reference-note-number"] = first_reference_note_number + + table.insert(items, item) + end + end + + if #citationsPre > 0 then + local previous_citation_id = citationsPre[#citationsPre][1] + local previous_citation = self.registry.citations[previous_citation_id] + self.registry.previous_citation = previous_citation + end + + if self.registry.requires_sorting then + self:sort_bibliography() + end + + local params = { + bibchange = false, + citation_errors = {}, + } + + local citation_id_note_list = {} + for _, citation_id_note in ipairs(citationsPre) do + table.insert(citation_id_note_list, citation_id_note) + end + local note_index = 0 + if citation.properties and citation.properties.noteIndex then + note_index = citation.properties.noteIndex + end + table.insert(citation_id_note_list, {citation.citationID, note_index}) + for _, citation_id_note in ipairs(citationsPost) do + table.insert(citation_id_note_list, citation_id_note) + end + + local citation_id_cited = {} + for _, citation_id_note in ipairs(citation_id_note_list) do + citation_id_cited[citation_id_note[1]] = true + end + for citation_id, _ in pairs(self.registry.citations) do + if not citation_id_cited[citation_id] then + self.registry.citations[citation_id] = nil + self.registry.citation_strings[citation_id] = nil + end + end + + local output = {} + + for i, citation_id_note in ipairs(citation_id_note_list) do + local citation_id = citation_id_note[1] + -- local note_index = citation_id_note[2] + if citation_id == citation.citationID then + local context = { + build = {}, + engine = self, + } + local citation_str = self.style:render_citation(items, context) + + self.registry.citation_strings[citation_id] = citation_str + table.insert(output, {i - 1, citation_str, citation_id}) + else + -- TODO: correct note_index + -- TODO: update other citations after disambiguation + local citation_str = self.registry.citation_strings[citation_id] + if self.registry.citation_strings[citation_id] ~= citation_str then + params.bibchange = true + self.registry.citation_strings[citation_id] = citation_str + table.insert(output, {i - 1, citation_str, citation_id}) + end + end + end + + return {params, output} +end + +function CiteProc:makeCitationCluster (citation_items) + local items = {} + for _, cite_item in ipairs(citation_items) do + cite_item.id = tostring(cite_item.id) + local position_first = (self.registry.registry[cite_item.id] == nil) + 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 item = setmetatable({}, {__index = function (_, key) + if cite_item[key] then + return cite_item[key] + else + return item_data[key] + end + end}) + + if not item.position and position_first then + item.position = util.position_map["first"] + end + table.insert(items, item) + end + + if self.registry.requires_sorting then + self:sort_bibliography() + end + + local context = { + build = {}, + engine=self, + } + local res = self.style:render_citation(items, context) + self.registry.previous_citation = { + citationID = "pseudo-citation", + citationItems = items, + properties = { + noteIndex = 1, + } + } + return res +end + +function CiteProc:makeBibliography() + local items = {} + + if self.registry.requires_sorting then + self:sort_bibliography() + end + + for _, id in ipairs(self.registry.reflist) do + local item = self.registry.registry[id] + table.insert(items, item) + end + + local context = { + build = {}, + engine=self, + } + local res = self.style:render_biblography(items, context) + return res +end + +function CiteProc:set_formatter(format) + self.formatter = formats[format] +end + +function CiteProc:enable_linking() + self.linking_enabled = true +end + +function CiteProc:disable_linking() + self.linking_enabled = false +end + +function CiteProc.set_base_class (node) + if node:is_element() then + local name = node:get_element_name() + local element_class = nodes[name] + if element_class then + element_class:set_base_class(node) + else + element.Element:set_base_class(node) + end + end +end + +function CiteProc:get_style_class() + return self.style:get_attribute("class") or "in-text" +end + +function CiteProc:get_item (id) + local item = self.registry.registry[id] + if not item then + item = self:_retrieve_item(id) + if not item then + return nil + end + 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 +end + +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('Failed to retrieve item "%s"', id)) + return nil + end + + item.id = tostring(item.id) + + for key, value in pairs(item) do + if key == "title" then + value = self.normalize_string(value) + end + 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 + +function CiteProc.normalize_string (str) + if not str or str == "" then + return str + end + -- French punctuation spacing + if type(str) == "string" then + str = string.gsub(str, " ;", util.unicode["narrow no-break space"] .. ";") + str = string.gsub(str, " %?", util.unicode["narrow no-break space"] .. "?") + str = string.gsub(str, " !", util.unicode["narrow no-break space"] .. "!") + str = string.gsub(str, " »", util.unicode["narrow no-break space"] .. "»") + str = string.gsub(str, "« ", "«" .. util.unicode["narrow no-break space"]) + end + -- local text = str + local text = richtext.new(str) + return text +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 = self.style:get_path("style bibliography sort")[1] + 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 context = { + engine = self, + style = self.style, + mode = "bibliography", + } + context = self.style:process_context(context) + context = self.style:get_path("style bibliography")[1]:process_context(context) + + bibliography_sort:sort(items, context) + self.registry.reflist = {} + for i, item in ipairs(items) do + item["citation-number"] = i + table.insert(self.registry.reflist, item.id) + end + self.registry.requires_sorting = false +end + +function CiteProc:get_system_locale (lang) + local locale = self.system_locales[lang] + if not locale then + locale = self.sys.retrieveLocale(lang) + if not locale then + util.warning(string.format("Failed to retrieve locale \"%s\"", lang)) + return nil + end + if type(locale) == "string" then + locale = dom.parse(locale) + end + locale:traverse_elements(self.set_base_class) + locale = locale:get_path("locale")[1] + locale:root_node().engine = self + locale:root_node().style = self.style + self.system_locales[lang] = locale + end + return locale +end + + +engine.CiteProc = CiteProc + +return engine diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-formats.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-formats.lua new file mode 100644 index 00000000000..854a528b95d --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-formats.lua @@ -0,0 +1,209 @@ +--[[ + Copyright (C) 2021 Zeping Lee +--]] + +local util = require("citeproc-util") + + +local formats = {} + +formats.html = { + ["text_escape"] = function (str) + str = string.gsub(str, "%&", "&") + str = string.gsub(str, "<", "<") + str = string.gsub(str, ">", ">") + for char, sub in pairs(util.superscripts) do + str = string.gsub(str, char, "<sup>" .. sub .. "</sup>") + end + return str + end, + ["bibstart"] = "<div class=\"csl-bib-body\">\n", + ["bibend"] = "</div>", + ["@font-style/italic"] = "<i>%s</i>", + ["@font-style/oblique"] = "<em>%s</em>", + ["@font-style/normal"] = '<span style="font-style:normal;">%s</span>', + ["@font-variant/small-caps"] = '<span style="font-variant:small-caps;">%s</span>', + ["@font-variant/normal"] = '<span style="font-variant:normal;">%s</span>', + ["@font-weight/bold"] = "<b>%s</b>", + ["@font-weight/normal"] = '<span style="font-weight:normal;">%s</span>', + ["@font-weight/light"] = false, + ["@text-decoration/none"] = '<span style="text-decoration:none;">%s</span>', + ["@text-decoration/underline"] = '<span style="text-decoration:underline;">%s</span>', + ["@vertical-align/sup"] = "<sup>%s</sup>", + ["@vertical-align/sub"] = "<sub>%s</sub>", + ["@vertical-align/baseline"] = '<span style="baseline">%s</span>', + ["@quotes/true"] = function (str, context) + local open_quote = context.style:get_term("open-quote"):render(context) + local close_quote = context.style:get_term("close-quote"):render(context) + return open_quote .. str .. close_quote + end, + ["@quotes/inner"] = function (str, context) + local open_quote = context.style:get_term("open-inner-quote"):render(context) + local close_quote = context.style:get_term("close-inner-quote"):render(context) + return open_quote .. str .. close_quote + end, + ["@bibliography/entry"] = function (str, context) + return '<div class="csl-entry">' .. str .. "</div>\n" + end, + ["@display/block"] = function (str, state) + return '\n\n <div class="csl-block">' .. str .. "</div>\n" + end, + ["@display/left-margin"] = function (str, state) + return '\n <div class="csl-left-margin">' .. str .. "</div>" + end, + ["@display/right-inline"] = function (str, state) + str = util.rstrip(str) + return '<div class="csl-right-inline">' .. str .. "</div>\n " + end, + ["@display/indent"] = function (str, state) + return '<div class="csl-indent">' .. str .. "</div>\n " + end, + ["@URL/true"] = function (str, state) + if state.engine.linking_enabled then + return string.format('<a href="%s">%s</a>', str, str) + else + return str + end + end, + ["@DOI/true"] = function (str, state) + if state.engine.linking_enabled then + local href = str + if not string.match(href, "^https?://") then + href = "https://doi.org/" .. str; + end + return string.format('<a href="%s">%s</a>', href, str) + else + return str + end + end, + ["@PMID/true"] = function (str, state) + if state.engine.linking_enabled then + local href = str + if not string.match(href, "^https?://") then + href = "https://www.ncbi.nlm.nih.gov/pubmed/" .. str; + end + return string.format('<a href="%s">%s</a>', href, str) + else + return str + end + end, + ["@PMCID/true"] = function (str, state) + if state.engine.linking_enabled then + local href = str + if not string.match(href, "^https?://") then + href = "https://www.ncbi.nlm.nih.gov/pmc/articles/" .. str; + end + return string.format('<a href="%s">%s</a>', href, str) + else + return str + end + end, +} + +formats.latex = { + ["text_escape"] = function (str) + str = str:gsub("\\", "\\textbackslash") + str = str:gsub("#", "\\#") + str = str:gsub("%$", "\\$") + str = str:gsub("%%", "\\%%") + str = str:gsub("&", "\\&") + str = str:gsub("{", "\\{") + str = str:gsub("}", "\\}") + str = str:gsub("_", "\\_") + str = str:gsub(util.unicode["no-break space"], "~") + for char, sub in pairs(util.superscripts) do + str = string.gsub(str, char, "\\textsuperscript{" .. sub .. "}") + end + return str + end, + ["bibstart"] = function (context) + return string.format("\\begin{thebibliography}{%s}\n", context.build.longest_label) + end, + ["bibend"] = "\\end{thebibliography}", + ["@font-style/normal"] = "{\\normalshape %s}", + ["@font-style/italic"] = "\\emph{%s}", + ["@font-style/oblique"] = "\\textsl{%s}", + ["@font-variant/normal"] = "{\\normalshape %s}", + ["@font-variant/small-caps"] = "\\textsc{%s}", + ["@font-weight/normal"] = "\\fontseries{m}\\selectfont %s", + ["@font-weight/bold"] = "\\textbf{%s}", + ["@font-weight/light"] = "\\fontseries{l}\\selectfont %s", + ["@text-decoration/none"] = false, + ["@text-decoration/underline"] = "\\underline{%s}", + ["@vertical-align/sup"] = "\\textsuperscript{%s}", + ["@vertical-align/sub"] = "\\textsubscript{%s}", + ["@vertical-align/baseline"] = false, + ["@quotes/true"] = function (str, context) + local open_quote = context.style:get_term("open-quote"):render(context) + local close_quote = context.style:get_term("close-quote"):render(context) + return open_quote .. str .. close_quote + end, + ["@quotes/inner"] = function (str, context) + local open_quote = context.style:get_term("open-inner-quote"):render(context) + local close_quote = context.style:get_term("close-inner-quote"):render(context) + return open_quote .. str .. close_quote + end, + ["@bibliography/entry"] = function (str, context) + if not string.match(str, "\\bibitem") then + str = "\\bibitem{".. context.item.id .. "}\n" .. str + end + return str .. "\n" + end, + ["@display/block"] = function (str, state) + return str + end, + ["@display/left-margin"] = function (str, state) + if #str > #state.build.longest_label then + state.build.longest_label = str + end + if string.match(str, "%]") then + str = "{" .. str .. "}" + end + return string.format("\\bibitem[%s]{%s}\n", str, state.item.id) + end, + ["@display/right-inline"] = function (str, state) + return str + end, + ["@display/indent"] = function (str, state) + return str + end, + ["@URL/true"] = function (str, state) + return "\\url{" .. str .. "}" + end, + ["@DOI/true"] = function (str, state) + if state.engine.linking_enabled then + local href = str + if not string.match(href, "^https?://") then + href = "https://doi.org/" .. str; + end + return string.format("\\href{%s}{%s}", href, str) + else + return str + end + end, + ["@PMID/true"] = function (str, state) + if state.engine.linking_enabled then + local href = str + if not string.match(href, "^https?://") then + href = "https://www.ncbi.nlm.nih.gov/pubmed/" .. str; + end + return string.format("\\href{%s}{%s}", href, str) + else + return str + end + end, + ["@PMCID/true"] = function (str, state) + if state.engine.linking_enabled then + local href = str + if not string.match(href, "^https?://") then + href = "https://www.ncbi.nlm.nih.gov/pmc/articles/" .. str; + end + return string.format("\\href{%s}{%s}", href, str) + else + return str + end + end, +} + + +return formats diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-node-choose.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-choose.lua new file mode 100644 index 00000000000..aa36e2c8c37 --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-choose.lua @@ -0,0 +1,126 @@ +local choose = {} + +local element = require("citeproc-element") +local util = require("citeproc-util") + + +local Choose = element.Element:new() + +function Choose:render (item, context) + self:debug_info(context) + context = self:process_context(context) + for i, child in ipairs(self:get_children()) do + if child:is_element() then + local result, status = child:render(item, context) + if status then + return result + end + end + end + return nil +end + + +local If = element.Element:new() + +If.render = function (self, item, context) + self:debug_info(context) + context = self:process_context(context) + local results = {} + + local variable_names = context.options["is-numeric"] + if variable_names then + for _, variable_name in ipairs(util.split(variable_names)) do + local variable = self:get_variable(item, variable_name, context) + table.insert(results, util.is_numeric(variable)) + end + end + + variable_names = context.options["is-uncertain-date"] + if variable_names then + for _, variable_name in ipairs(util.split(variable_names)) do + local variable = self:get_variable(item, variable_name, context) + table.insert(results, util.is_uncertain_date(variable)) + end + end + + local locator_types = context.options["locator"] + if locator_types then + for _, locator_type in ipairs(util.split(locator_types)) do + local locator_label = item.label or "page" + local res = locator_label == locator_type + if locator_type == "sub-verbo" then + res = locator_label == "sub verbo" + end + table.insert(results, res) + end + end + + local positions = context.options["position"] + if positions then + for _, position in ipairs(util.split(positions)) do + local res = false + if context.mode == "citation" then + if position == "first" then + res = (item.position == util.position_map["first"]) + elseif position == "near-note" then + res = item["near-note"] ~= nil and item["near-note"] ~= false + else + res = (item.position >= util.position_map[position]) + end + end + table.insert(results, res) + end + end + + local type_names = context.options["type"] + if type_names then + for _, type_name in ipairs(util.split(type_names)) do + table.insert(results, item["type"] == type_name) + end + end + + variable_names = context.options["variable"] + if variable_names then + for _, variable_name in ipairs(util.split(variable_names)) do + local variable = self:get_variable(item, variable_name, context) + local res = (variable ~= nil and variable ~= "") + table.insert(results, res) + end + end + + local match = context.options["match"] or "all" + local status = false + if match == "any" then + status = util.any(results) + elseif match == "none" then + status = not util.any(results) + else + status = util.all(results) + end + if status then + return self:render_children(item, context), status + else + return nil, false + end +end + + +local ElseIf = If:new() + + +local Else = element.Element:new() + +Else.render = function (self, item, context) + self:debug_info(context) + context = self:process_context(context) + return self:render_children(item, context), true +end + + +choose.Choose = Choose +choose.If = If +choose.ElseIf = ElseIf +choose.Else = Else + +return choose diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-node-date.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-date.lua new file mode 100644 index 00000000000..9c712a8135b --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-date.lua @@ -0,0 +1,379 @@ +local date_module = {} + +local element = require("citeproc-element") +local util = require("citeproc-util") + + +local Date = element.Element:new() + +function Date:render (item, context) + self:debug_info(context) + context = self:process_context(context) + + if context.sorting then + return self:render_sort_key(item, context) + end + + local variable_name = context.options["variable"] + + local is_locale_date + if variable_name then + context.variable = variable_name + is_locale_date = false + else + variable_name = context.variable + is_locale_date = true + end + + local date = self:get_variable(item, variable_name, context) + if not date then + return nil + end + + local res = nil + local form = context.options["form"] + if form and not is_locale_date then + for _, date_part in ipairs(self:query_selector("date-part")) do + local name = date_part:get_attribute("name") + if not context.date_part_attributes then + context.date_part_attributes = {} + end + if not context.date_part_attributes[name] then + context.date_part_attributes[name] = {} + end + + for attr, value in pairs(date_part._attr) do + if attr ~= name then + context.date_part_attributes[name][attr] = value + end + end + end + res = self:get_locale_date(context, form):render(item, context) + else + if not date["date-parts"] or #date["date-parts"] == 0 then + local literal = date["literal"] + if literal then + res = literal + else + local raw = date["raw"] + if raw then + res = raw + end + end + + else + if #date["date-parts"] == 1 then + res = self:_render_single_date(date, context) + elseif #date["date-parts"] == 2 then + res = self:_render_date_range(date, context) + end + end + end + + table.insert(context.variable_attempt, res ~= nil) + + res = self:format(res, context) + res = self:wrap(res, context) + return res +end + +function Date:get_locale_date(context, form) + local date = nil + local style = context.style + local query = string.format("date[form=\"%s\"]", form) + for _, locale in ipairs(style:get_locales()) do + date = locale:query_selector(query)[1] + if date then + break + end + end + if not date then + error(string.format("Failed to find '%s'", query)) + end + return date +end + +function Date:render_sort_key (item, context) + local variable_name = context.options["variable"] + local date = self:get_variable(item, variable_name, context) + if not date or not date["date-parts"] then + return nil + end + local show_parts = { + year = false, + month = false, + day = false, + } + if self:get_attribute("form") then + local date_parts = self:get_attribute("date-parts") or "year-month-day" + for _, dp_name in ipairs(util.split(date_parts, "%-")) do + show_parts[dp_name] = true + end + else + for _, child in ipairs(self:query_selector("date-part")) do + show_parts[child:get_attribute("name")] = true + end + end + local res = "" + for _, date_parts in ipairs(date["date-parts"]) do + for i, dp_name in ipairs({"year", "month", "day"}) do + local value = date_parts[i] + if not value or not show_parts[dp_name] then + value = 0 + end + if i == 1 then + res = res .. string.format("%05d", value + 10000) + else + res = res .. string.format("%02d", value) + end + end + end + return res +end + +function Date:_render_single_date (date, context) + local show_parts = self:_get_show_parts(context) + + local output = {} + for _, child in ipairs(self:query_selector("date-part")) do + if show_parts[child:get_attribute("name")] then + table.insert(output, child:render(date, context)) + end + end + return self:concat(output, context) +end + +function Date:_render_date_range (date, context) + local show_parts = self:_get_show_parts(context) + local part_index = {} + + local largest_diff_part = nil + for i, name in ipairs({"year", "month", "day"}) do + part_index[name] = i + local part_value1 = date["date-parts"][1][i] + if show_parts[name] and part_value1 then + if not largest_diff_part then + largest_diff_part = name + end + end + end + + local date_parts = {} + for _, date_part in ipairs(self:query_selector("date-part")) do + if show_parts[date_part:get_attribute("name")] then + table.insert(date_parts, date_part) + end + end + + local diff_begin = 0 + local diff_end = #date_parts + local range_delimiter = nil + + for i, date_part in ipairs(date_parts) do + local name = date_part:get_attribute("name") + if name == largest_diff_part then + range_delimiter = date_part:get_attribute("range-delimiter") + if not range_delimiter then + range_delimiter = util.unicode["en dash"] + end + end + + local index = part_index[name] + local part_value1 = date["date-parts"][1][index] + local part_value2 = date["date-parts"][2][index] + if part_value1 and part_value1 ~= part_value2 then + if diff_begin == 0 then + diff_begin = i + end + diff_end = i + end + end + + local same_prefix = {} + local range_begin = {} + local range_end = {} + local same_suffix = {} + + local no_suffix_context = self:process_context(context) + no_suffix_context.options["suffix"] = nil + + for i, date_part in ipairs(date_parts) do + local res = nil + if i == diff_end then + res = date_part:render(date, no_suffix_context, true) + else + res = date_part:render(date, context) + end + if i < diff_begin then + table.insert(same_prefix, res) + elseif i <= diff_end then + table.insert(range_begin, res) + table.insert(range_end, date_part:render(date, context, false, true)) + else + table.insert(same_suffix, res) + end + end + + local prefix_output = self:concat(same_prefix, context) or "" + local range_begin_output = self:concat(range_begin, context) or "" + local range_end_output = self:concat(range_end, context) or "" + local suffix_output = self:concat(same_suffix, context) + local range_output = range_begin_output .. range_delimiter .. range_end_output + + local res = self:concat({prefix_output, range_output, suffix_output}, context) + + return res +end + +function Date:_get_show_parts (context) + local show_parts = {} + local date_parts = context.options["date-parts"] or "year-month-day" + for _, date_part in ipairs(util.split(date_parts, "%-")) do + show_parts[date_part] = true + end + return show_parts +end + + +local DatePart = element.Element:new() + +DatePart.render = function (self, date, context, last_range_begin, range_end) + self:debug_info(context) + context = self:process_context(context) + local name = context.options["name"] + local range_delimiter = context.options["range-delimiter"] or false + + -- The attributes set on cs:date-part elements of a cs:date with form + -- attribute override those specified for the localized date formats + if context.date_part_attributes then + local context_attributes = context.date_part_attributes[name] + if context_attributes then + for attr, value in pairs(context_attributes) do + context.options[attr] = value + end + end + end + + if last_range_begin then + context.options["suffix"] = "" + end + + local date_parts_index = 1 + if range_end then + date_parts_index = 2 + end + + local res = nil + if name == "day" then + local day = date["date-parts"][date_parts_index][3] + if not day then + return nil + end + day = tonumber(day) + -- range open + if day == 0 then + return nil + end + local form = context.options["form"] or "numeric" + + if form == "ordinal" then + local option = self:get_locale_option("limit-day-ordinals-to-day-1") + if option and option ~= "false" and day > 1 then + form = "numeric" + end + end + if form == "numeric" then + res = tostring(day) + elseif form == "numeric-leading-zeros" then + -- TODO: day == nil? + if not day then + return nil + end + res = string.format("%02d", day) + elseif form == "ordinal" then + res = util.to_ordinal(day) + end + + elseif name == "month" then + local form = context.options["form"] or "long" + + local month = date["date-parts"][date_parts_index][2] + if month then + month = tonumber(month) + -- range open + if month == 0 then + return nil + end + end + + if form == "long" or form == "short" then + local term_name = nil + if month then + if month >= 1 and month <= 12 then + term_name = string.format("month-%02d", month) + elseif month >= 13 and month <= 24 then + local season = month % 4 + if season == 0 then + season = 4 + end + term_name = string.format("season-%02d", season) + else + util.warning("Invalid month value") + return nil + end + else + local season = date["season"] + if season then + season = tonumber(season) + term_name = string.format("season-%02d", season) + else + return nil + end + end + res = self:get_term(term_name, form):render(context) + elseif form == "numeric" then + res = tostring(month) + elseif form == "numeric-leading-zeros" then + -- TODO: month == nil? + if not month then + return nil + end + res = string.format("%02d", month) + end + res = self:strip_periods(res, context) + + elseif name == "year" then + local year = date["date-parts"][date_parts_index][1] + if year then + year = tonumber(year) + -- range open + if year == 0 then + return nil + end + local form = context.options["form"] or "long" + if form == "long" then + year = tonumber(year) + if year < 0 then + res = tostring(-year) .. self:get_term("bc"):render(context) + elseif year < 1000 then + res = tostring(year) .. self:get_term("ad"):render(context) + else + res = tostring(year) + end + elseif form == "short" then + res = string.sub(tostring(year), -2) + end + end + end + res = self:case(res, context) + res = self:format(res, context) + res = self:wrap(res, context) + res = self:display(res, context) + return res +end + + +date_module.Date = Date +date_module.DatePart = DatePart + +return date_module diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-node-group.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-group.lua new file mode 100644 index 00000000000..3148e961486 --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-group.lua @@ -0,0 +1,32 @@ +local group = {} + +local element = require("citeproc-element") +local util = require("citeproc-util") + + +local Group = element.Element:new() + +function Group:render (item, context) + self:debug_info(context) + context = self:process_context(context) + + local num_variable_attempt = #context.variable_attempt + + local res = self:render_children(item, context) + + if #context.variable_attempt > num_variable_attempt then + if not util.any(util.slice(context.variable_attempt, num_variable_attempt + 1)) then + res = nil + end + end + + res = self:format(res, context) + res = self:wrap(res, context) + res = self:display(res, context) + return res +end + + +group.Group = Group + +return group diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-node-label.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-label.lua new file mode 100644 index 00000000000..d8bfe2ba3d4 --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-label.lua @@ -0,0 +1,106 @@ +local label = {} + +local element = require("citeproc-element") +local util = require("citeproc-util") + + +local Label = element.Element:new() + +function Label:render (item, context) + self:debug_info(context) + context = self:process_context(context) + + local variable_name + if context.names_element then + -- The `variable` attribute of names may hold multiple roles. + -- Each of them may call `Label:render()` to render the term. + -- When used in `names` element, the role name is the first argument + -- and the item is accessed via `context.item`. + -- Bad design + -- TODO: Redesign the arguments of render() + variable_name = item + else + variable_name = context.options["variable"] + end + + local form = context.options["form"] + local plural = context.options["plural"] or "contextual" + + if not context.names_element then + local variable_type = util.variable_types[variable_name] + -- variable must be or one of the number variables. + if variable_type ~= "number" then + return nil + end + -- The term is only rendered if the selected variable is non-empty + local variable = item[variable_name] + if not variable then + return nil + end + if type(variable) == "string" then + if not (string.match(variable, "^%d") or util.is_numeric(variable)) then + return nil + end + end + end + + local term + if variable_name == "locator" then + local locator_type = item.label or "page" + term = self:get_term(locator_type, form) + else + term = self:get_term(variable_name, form) + end + + local res = nil + if term then + if plural == "contextual" and self:_is_plural(variable_name, context) or plural == "always" then + res = term:render(context, true) + else + res = term:render(context, false) + end + + res = self:strip_periods(res, context) + res = self:case(res, context) + res = self:format(res, context) + res = self:wrap(res, context) + end + return res +end + +function Label:_is_plural (variable_name, context) + local variable_type = util.variable_types[variable_name] + -- Don't use self:get_variable here + local variable = context.item[variable_name] + local res = false + if variable_type == "name" then + -- Label inside `names` + res = #variable > 1 + + elseif variable_type == "number" then + if util.startswith(variable_name, "number-of-") then + res = tonumber(variable) > 1 + else + variable = tostring(variable) + variable = string.gsub(variable, "\\%-", "") + if #util.split(variable, "%s*[,&-]%s*") > 1 then + -- check if contains multiple numbers + -- "i–ix": true + -- res = string.match(tostring(variable), "%d+%D+%d+") ~= nil + res = true + elseif string.match(variable, "%Aand%A") or string.match(variable, "%Aet%A") then + res = true + else + res = false + end + end + else + util.warning("Invalid attribute \"variable\".") + end + return res +end + + +label.Label = Label + +return label diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-node-layout.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-layout.lua new file mode 100644 index 00000000000..40ce358629b --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-layout.lua @@ -0,0 +1,231 @@ +local layout = {} + +local richtext = require("citeproc-richtext") +local element = require("citeproc-element") +local util = require("citeproc-util") + + +local Layout = element.Element:new() + +function Layout:render (items, context) + self:debug_info(context) + + context.items = items + + -- When used within cs:citation, the delimiter attribute may be used to specify a delimiter for cites within a citation. + -- Thus the processing of context is put after render_children(). + if context.mode == "citation" then + if context.options["collapse"] == "citation-number" then + context.build.item_citation_numbers = {} + context.build.item_citation_number_text = {} + end + elseif context.mode == "bibliography" then + context.build.longest_label = "" + context.build.preceding_first_rendered_names = nil + context = self:process_context(context) + end + + local output = {} + local previous_cite = nil + for _, item in ipairs(items) do + + context.item = item + context.variable_attempt = {} + context.suppressed_variables = {} + context.suppress_subsequent_variables = false + if context.mode == "bibliography" then + context.build.first_rendered_names = {} + end + + if not item.position then + item.position = self:_get_position(item, previous_cite, context) + end + + local first = nil + local second = {} + local element_index = 0 + for _, child in ipairs(self:get_children()) do + if child:is_element() then + element_index = element_index + 1 + local text = child:render(item, context) + if element_index == 1 then + first = text + else + table.insert(second, text) + end + end + end + second = self:concat(second, context) + + if context.mode == "bibliography" then + if first and context.options["prefix"] then + first = richtext.new(context.options["prefix"]) .. first + end + if second and context.options["suffix"] then + second = second .. richtext.new(context.options["suffix"]) + end + end + + local res = nil + if context.options["second-field-align"] == "flush" then + if first then + first:add_format("display", "left-margin") + res = first + end + if second then + second:add_format("display", "right-inline") + if res then + res = richtext.concat(res, second) + else + res = second + end + end + else + res = self:concat({first, second}, context) + end + + if context.mode == "citation" then + if res and item["prefix"] then + res = richtext.new(item["prefix"]) .. res + end + if res and item["suffix"] then + res = res .. richtext.new(item["suffix"]) + end + elseif context.mode == "bibliography" then + if not res then + res = richtext.new("[CSL STYLE ERROR: reference with no printed form.]") + end + res = self:wrap(res, context) + -- util.debug(text) + res = res:render(context.engine.formatter, context) + res = context.engine.formatter["@bibliography/entry"](res, context) + end + table.insert(output, res) + previous_cite = item + end + + if context.mode == "citation" then + if next(output) == nil then + return "[CSL STYLE ERROR: reference with no printed form.]" + end + + context = self:process_context(context) + local res + if context.options["collapse"] then + res = self:_collapse_citations(output, context) + else + res = self:concat(output, context) + end + res = self:wrap(res, context) + res = self:format(res, context) + if res then + -- util.debug(res) + res = res:render(context.engine.formatter, context) + end + return res + + else + local params = { + maxoffset = #context.build.longest_label, + } + + return {params, output} + end +end + +function Layout:_get_position (item, previous_cite, context) + local engine = context.engine + if not engine.registry.registry[item.id] then + return util.position_map["first"] + end + + local position = util.position_map["subsequent"] + -- Find the preceding cite referencing the same item + local preceding_cite = nil + if previous_cite then + -- a. the current cite immediately follows on another cite + if item.id == previous_cite.id then + preceding_cite = previous_cite + end + elseif engine.registry.previous_citation then + -- b. first cite in the citation and previous citation exists + for _, cite in ipairs(engine.registry.previous_citation.citationItems) do + if item.id == cite.id then + preceding_cite = cite + break + end + end + end + + if preceding_cite then + if preceding_cite.locator then + -- Preceding cite does have a locator + if item.locator then + if item.locator == preceding_cite.locator then + position = util.position_map["ibid"] + else + position = util.position_map["ibid-with-locator"] + end + else + -- the current cite lacks a locator + position = util.position_map["subsequent"] + end + else + -- Preceding cite does not have a locator + if item.locator then + position = util.position_map["ibid-with-locator"] + else + position = util.position_map["ibid"] + end + end + end + return position +end + + +function Layout:_collapse_citations(output, context) + if context.options["collapse"] == "citation-number" then + assert(#output == #context.items) + local citation_numbers = {} + for i, item in ipairs(context.items) do + citation_numbers[i] = context.build.item_citation_numbers[item.id] or 0 + end + + local collapsed_output = {} + local citation_number_range_delimiter = util.unicode["en dash"] + local index = 1 + while index <= #citation_numbers do + local stop_index = index + 1 + if output[index] == context.build.item_citation_number_text[index] then + while stop_index <= #citation_numbers do + if output[stop_index] ~= context.build.item_citation_number_text[stop_index] then + break + end + if citation_numbers[stop_index - 1] + 1 ~= citation_numbers[stop_index] then + break + end + stop_index = stop_index + 1 + end + end + + if stop_index >= index + 3 then + local range_text = output[index] .. citation_number_range_delimiter .. output[stop_index - 1] + table.insert(collapsed_output, range_text) + else + for i = index, stop_index - 1 do + table.insert(collapsed_output, output[i]) + end + end + + index = stop_index + end + + return self:concat(collapsed_output, context) + end + return self:concat(output, context) +end + + +layout.Layout = Layout + +return layout diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-node-locale.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-locale.lua new file mode 100644 index 00000000000..e324c3502eb --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-locale.lua @@ -0,0 +1,130 @@ +local locale = {} + +local element = require("citeproc-element") + + +local Locale = element.Element:new() + +function Locale:get_option (key) + local query = string.format("style-options[%s]", key) + local option = self:query_selector(query)[1] + if option then + local value = option:get_attribute(key) + if self.option_type[key] == "integer" then + value = tonumber(value) + elseif self.option_type[key] == "boolean" then + value = (value == "true") + end + return value + else + return nil + end +end + +function Locale:get_term (name, form, number, gender) + + if form == "long" then + form = nil + end + + local match_last + local match_last_two + local match_whole + if number then + assert(type(number) == "number") + match_last = string.format("%s-%02d", name, number % 10) + match_last_two = string.format("%s-%02d", name, number % 100) + match_whole = string.format("%s-%02s", name, number) + end + + local res = nil + for _, term in ipairs(self:query_selector("term")) do + -- Use get_path? + local match_name = name + + if number then + local term_match = term:get_attribute("last-two-digits") + if term_match == "whole-number" then + match_name = match_whole + elseif term_match == "last-two-digits" then + match_name = match_last_two + elseif number < 10 then + -- "13" can match only "ordinal-13" not "ordinal-03" + -- It is sliced to "3" in a later checking pass. + match_name = match_last_two + else + match_name = match_last + end + end + + local term_name = term:get_attribute("name") + local term_form = term:get_attribute("form") + if term_form == "long" then + term_form = nil + end + local term_gender = term:get_attribute("gender-form") + + if term_name == match_name and term_form == form and term_gender == gender then + return term + end + + end + + -- Fallback + if form == "verb-sort" then + return self:get_term(name, "verb") + elseif form == "symbol" then + return self:get_term(name, "short") + elseif form == "verb" then + return self:get_term(name, "long") + elseif form == "short" then + return self:get_term(name, "long") + end + + if number and number > 10 then + return self:get_term(name, nil, number % 10, gender) + end + + if gender then + return self:get_term(name, nil, number, nil) + end + + if number then + return self:get_term(name, nil, nil, nil) + end + + return nil +end + + +local Term = element.Element:new() + +function Term:render (context, is_plural) + self:debug_info(context) + context = self:process_context(context) + + local output = { + single = self:get_text(), + } + for _, child in ipairs(self:get_children()) do + if child:is_element() then + output[child:get_element_name()] = self:escape(child:get_text()) + end + end + local res = output.single + if is_plural then + if output.multiple then + res = output.multiple + end + end + if res == "" then + return nil + end + return res +end + + +locale.Locale = Locale +locale.Term = Term + +return locale diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-node-names.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-names.lua new file mode 100644 index 00000000000..9958e28999e --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-names.lua @@ -0,0 +1,696 @@ +local names_module = {} + +local unicode = require("unicode") + +local richtext = require("citeproc-richtext") +local element = require("citeproc-element") +local util = require("citeproc-util") + + +local Name = element.Element:new() + +Name.default_options = { + ["delimiter"] = ", ", + ["delimiter-precedes-et-al"] = "contextual", + ["delimiter-precedes-last"] = "contextual", + ["et-al-min"] = nil, + ["et-al-use-first"] = nil, + ["et-al-subsequent-min"] = nil, + ["et-al-subsequent-use-first "] = nil, + ["et-al-use-last"] = false, + ["form"] = "long", + ["initialize"] = true, + ["initialize-with"] = false, + ["name-as-sort-order"] = false, + ["sort-separator"] = ", ", + ["prefix"] = "", + ["suffix"] = "", +} + +function Name:render (names, context) + self:debug_info(context) + context = self:process_context(context) + + local and_ = context.options["and"] + local delimiter = context.options["delimiter"] + local delimiter_precedes_et_al = context.options["delimiter-precedes-et-al"] + local delimiter_precedes_last = context.options["delimiter-precedes-last"] + local et_al_min = context.options["et-al-min"] + local et_al_use_first = context.options["et-al-use-first"] + local et_al_subsequent_min = context.options["et-al-subsequent-min"] + local et_al_subsequent_use_first = context.options["et-al-subsequent-use-first "] + local et_al_use_last = context.options["et-al-use-last"] + + -- sorting + if context.options["names-min"] then + et_al_min = context.options["names-min"] + end + if context.options["names-use-first"] then + et_al_use_first = context.options["names-use-first"] + end + if context.options["names-use-last"] ~= nil then + et_al_use_last = context.options["names-use-last"] + end + + local form = context.options["form"] + + local et_al_truncate = et_al_min and et_al_use_first and #names >= et_al_min + local et_al_last = et_al_use_last and et_al_use_first <= et_al_min - 2 + + if form == "count" then + if et_al_truncate then + return et_al_use_first + else + return #names + end + end + + local output = nil + + local res = nil + local inverted = false + + for i, name in ipairs(names) do + if et_al_truncate and i > et_al_use_first then + if et_al_last then + if i == #names then + output = richtext.concat(output, delimiter) + output = output .. util.unicode["horizontal ellipsis"] + output = output .. " " + res = self:render_single_name(name, i, context) + output = output .. res + end + else + if not self:_check_delimiter(delimiter_precedes_et_al, i, inverted) then + delimiter = " " + end + if output then + output = richtext.concat_list({output, context.et_al:render(context)}, delimiter) + end + break + end + else + if i > 1 then + if i == #names and context.options["and"] then + if self:_check_delimiter(delimiter_precedes_last, i, inverted) then + output = richtext.concat(output, delimiter) + else + output = output .. " " + end + local and_term = "" + if context.options["and"] == "text" then + and_term = self:get_term("and"):render(context) + elseif context.options["and"] == "symbol" then + and_term = self:escape("&") + end + output = output .. and_term .. " " + else + output = richtext.concat(output, delimiter) + end + end + res, inverted = self:render_single_name(name, i, context) + + if res and res ~= "" then + res = richtext.new(res) + if context.build.first_rendered_names then + table.insert(context.build.first_rendered_names, res) + end + + if output then + output = richtext.concat(output, res) + else + output = res + end + end + end + end + + local ret = self:format(output, context) + ret = self:wrap(ret, context) + return ret +end + +function Name:_check_delimiter (delimiter_attribute, index, inverted) + -- `delimiter-precedes-et-al` and `delimiter-precedes-last` + if delimiter_attribute == "always" then + return true + elseif delimiter_attribute == "never" then + return false + elseif delimiter_attribute == "contextual" then + if index > 2 then + return true + else + return false + end + elseif delimiter_attribute == "after-inverted-name" then + if inverted then + return true + else + return false + end + end + return false +end + +function Name:render_single_name (name, index, context) + local form = context.options["form"] + local initialize = context.options["initialize"] + local initialize_with = context.options["initialize-with"] + local name_as_sort_order = context.options["name-as-sort-order"] + if context.sorting then + name_as_sort_order = "all" + end + local sort_separator = context.options["sort-separator"] + + local demote_non_dropping_particle = context.options["demote-non-dropping-particle"] + + -- TODO: make it a module + local function _strip_quotes(str) + if str then + str = string.gsub(str, '"', "") + str = string.gsub(str, "'", util.unicode["apostrophe"]) + end + return str + end + + local family = _strip_quotes(name["family"]) or "" + local given = _strip_quotes(name["given"]) or "" + local dp = _strip_quotes(name["dropping-particle"]) or "" + local ndp = _strip_quotes(name["non-dropping-particle"]) or "" + local suffix = _strip_quotes(name["suffix"]) or "" + local literal = _strip_quotes(name["literal"]) or "" + + if family == "" then + family = literal + if family == "" then + family = given + given = "" + end + if family ~= "" then + return family + else + error("Name not avaliable") + end + end + + if initialize_with then + given = self:initialize(given, initialize_with, context) + end + + local demote_ndp = false -- only active when form == "long" + if demote_non_dropping_particle == "display-and-sort" or + demote_non_dropping_particle == "sort-only" and context.sorting then + demote_ndp = true + else -- demote_non_dropping_particle == "never" + demote_ndp = false + end + + local family_name_part = nil + local given_name_part = nil + for _, child in ipairs(self:get_children()) do + if child:is_element() and child:get_element_name() == "name-part" then + local name_part = child:get_attribute("name") + if name_part == "family" then + family_name_part = child + elseif name_part == "given" then + given_name_part = child + end + end + end + + local res = nil + local inverted = false + if form == "long" then + local order + local suffix_separator = sort_separator + if not util.has_romanesque_char(name["family"]) then + order = {family, given} + inverted = true + sort_separator = "" + elseif name_as_sort_order == "all" or (name_as_sort_order == "first" and index == 1) then + + -- "Alan al-One" + local hyphen_parts = util.split(family, "%-", 1) + if #hyphen_parts > 1 then + local particle + particle, family = table.unpack(hyphen_parts) + particle = particle .. "-" + ndp = richtext.concat(ndp, particle) + end + + if family_name_part then + family = family_name_part:format_name_part(family, context) + ndp = family_name_part:format_name_part(ndp, context) + end + if given_name_part then + given = given_name_part:format_name_part(given, context) + dp = family_name_part:format_name_part(dp, context) + end + + if demote_ndp then + given = richtext.concat_list({given, dp, ndp}, " ") + else + family = richtext.concat_list({ndp, family}, " ") + given = richtext.concat_list({given, dp}, " ") + end + + if family_name_part then + family = family_name_part:wrap_name_part(family, context) + end + if given_name_part then + given = given_name_part:wrap_name_part(given, context) + end + + order = {family, given, suffix} + inverted = true + else + if family_name_part then + family = family_name_part:format_name_part(family, context) + ndp = family_name_part:format_name_part(ndp, context) + end + if given_name_part then + given = given_name_part:format_name_part(given, context) + dp = family_name_part:format_name_part(dp, context) + end + + family = richtext.concat_list({dp, ndp, family}, " ") + if name["comma-suffix"] then + suffix_separator = ", " + else + suffix_separator = " " + end + family = richtext.concat_list({family, suffix}, suffix_separator) + + if family_name_part then + family = family_name_part:wrap_name_part(family, context) + end + if given_name_part then + given = given_name_part:wrap_name_part(given, context) + end + + order = {given, family} + sort_separator = " " + end + res = richtext.concat_list(order, sort_separator) + + elseif form == "short" then + if family_name_part then + family = family_name_part:format_name_part(family, context) + ndp = family_name_part:format_name_part(ndp, context) + end + family = util.concat({ndp, family}, " ") + if family_name_part then + family = family_name_part:wrap_name_part(family, context) + end + res = family + else + error(string.format('Invalid attribute form="%s" of "name".', form)) + end + return res, inverted +end + +function Name:initialize (given, terminator, context) + if not given or given == "" then + return "" + end + + local initialize = context.options["initialize"] + if context.options["initialize-with-hyphen"] == false then + given = string.gsub(given, "-", " ") + end + + -- Split the given name to name_list (e.g., {"John", "M." "E"}) + -- Compound names are splitted too but are marked in punc_list. + local name_list = {} + local punct_list = {} + local last_position = 1 + for name, pos in string.gmatch(given, "([^-.%s]+[-.%s]+)()") do + table.insert(name_list, string.match(name, "^[^-%s]+")) + if string.match(name, "%-") then + table.insert(punct_list, "-") + else + table.insert(punct_list, "") + end + last_position = pos + end + if last_position <= #given then + table.insert(name_list, util.strip(string.sub(given, last_position))) + table.insert(punct_list, "") + end + + for i, name in ipairs(name_list) do + local is_particle = false + local is_abbreviation = false + + local first_letter = utf8.char(utf8.codepoint(name)) + if util.is_lower(first_letter) then + is_particle = true + elseif #name == 1 then + is_abbreviation = true + else + local abbreviation = string.match(name, "^([^.]+)%.$") + if abbreviation then + is_abbreviation = true + name = abbreviation + end + end + + if is_particle then + name_list[i] = name .. " " + if i > 1 and not string.match(name_list[i-1], "%s$") then + name_list[i-1] = name_list[i-1] .. " " + end + elseif is_abbreviation then + name_list[i] = name .. terminator + else + if initialize then + if util.is_upper(name) then + name = first_letter + else + -- Long abbreviation: "TSerendorjiin" -> "Ts." + local abbreviation = "" + for _, c in utf8.codes(name) do + local char = utf8.char(c) + local lower = unicode.utf8.lower(char) + if lower == char then + break + end + if abbreviation == "" then + abbreviation = char + else + abbreviation = abbreviation .. lower + end + end + name = abbreviation + end + name_list[i] = name .. terminator + else + name_list[i] = name .. " " + end + end + + -- Handle the compound names + if i > 1 and punct_list[i-1] == "-" then + if is_particle then -- special case "Guo-ping" + name_list[i] = "" + else + name_list[i-1] = util.rstrip(name_list[i-1]) + name_list[i] = "-" .. name_list[i] + end + end + end + + local res = util.concat(name_list, "") + res = util.strip(res) + return res + +end + +local NamePart = element.Element:new() + +function NamePart:format_name_part(name_part, context) + context = self:process_context(context) + local res = self:case(name_part, context) + res = self:format(res, context) + return res +end + +function NamePart:wrap_name_part(name_part, context) + context = self:process_context(context) + local res = self:wrap(name_part, context) + return res +end + + +local EtAl = element.Element:new() + +EtAl.default_options = { + term = "et-al", +} + +EtAl.render = function (self, context) + self:debug_info(context) + context = self:process_context(context) + local res = self:get_term(context.options["term"]):render(context) + res = self:format(res, context) + return res +end + + +local Substitute = element.Element:new() + +function Substitute:render (item, context) + self:debug_info(context) + + if context.suppressed_variables then + -- true in layout, not in sort + context.suppress_subsequent_variables = true + end + + for i, child in ipairs(self:get_children()) do + if child:is_element() then + local result = child:render(item, context) + if result and result ~= "" then + return result + end + end + end + return nil +end + + +local Names = element.Element:new() + +function Names:render (item, context) + self:debug_info(context) + context = self:process_context(context) + + local names_delimiter = context.options["names-delimiter"] + if names_delimiter then + context.options["delimiter"] = names_delimiter + end + + -- Inherit attributes of parent `names` element + local names_element = context.names_element + if names_element then + for key, value in pairs(names_element._attr) do + context.options[key] = value + end + for key, value in pairs(self._attr) do + context.options[key] = value + end + else + context.names_element = self + context.variable = context.options["variable"] + end + + local name, et_al, label + -- The position of cs:label relative to cs:name determines the order of + -- the name and label in the rendered text. + local label_position = nil + for _, child in ipairs(self:get_children()) do + if child:is_element() then + local element_name = child:get_element_name() + if element_name == "name" then + name = child + if label then + label_position = "before" + end + elseif element_name == "et-al" then + et_al = child + elseif element_name == "label" then + label = child + if name then + label_position = "after" + end + end + end + end + if label_position then + context.label_position = label_position + else + label_position = context.label_position or "after" + end + + -- local name = self:get_child("name") + if not name then + name = context.name_element + end + if not name then + name = self:create_element("name", {}, self) + Name:set_base_class(name) + end + context.name_element = name + + -- local et_al = self:get_child("et-al") + if not et_al then + et_al = context.et_al + end + if not et_al then + et_al = self:create_element("et-al", {}, self) + EtAl:set_base_class(et_al) + end + context.et_al = et_al + + -- local label = self:get_child("label") + if label then + context.label = label + else + label = context.label + end + + local sub_str = nil + if context.mode == "bibliography" and not context.sorting then + sub_str = context.options["subsequent-author-substitute"] + -- if sub_str and #context.build.preceding_first_rendered_names == 0 then + -- context.rendered_names = {} + -- else + -- sub_str = nil + -- context.rendered_names = nil + -- end + end + + local variable_names = context.options["variable"] or context.variable + local ret = nil + + if variable_names then + local output = {} + local num_names = 0 + for _, role in ipairs(util.split(variable_names)) do + local names = self:get_variable(item, role, context) + + table.insert(context.variable_attempt, names ~= nil) + + if names then + local res = name:render(names, context) + if res then + if type(res) == "number" then -- name[form="count"] + num_names = num_names + res + elseif label and not context.sorting then + -- drop name label in sorting + local label_result = label:render(role, context) + if label_result then + if label_position == "before" then + res = richtext.concat(label_result, res) + else + res = richtext.concat(res, label_result) + end + end + end + end + table.insert(output, res) + end + end + + if num_names > 0 then + ret = tostring(num_names) + else + ret = self:concat(output, context) + if ret and sub_str and context.build.first_rendered_names then + ret = self:substitute_names(ret, context) + end + end + end + + if ret then + ret = self:format(ret, context) + ret = self:wrap(ret, context) + ret = self:display(ret, context) + return ret + else + local substitute = self:get_child("substitute") + if substitute then + ret = substitute:render(item, context) + end + if ret and sub_str then + ret = self:substitute_single_field(ret, context) + end + return ret + end +end + +function Names:substitute_single_field(result, context) + if not result then + return nil + end + if context.build.first_rendered_names and #context.build.first_rendered_names == 0 then + context.build.first_rendered_names[1] = result + end + result = self:substitute_names(result, context) + return result +end + +function Names:substitute_names(result, context) + if not context.build.first_rendered_names then + return result + end + local name_strings = {} + local match_all + + if #context.build.first_rendered_names > 0 then + match_all = true + else + match_all = false + end + for i, text in ipairs(context.build.first_rendered_names) do + local str = text:render(context.engine.formatter, context) + name_strings[i] = str + if context.build.preceding_first_rendered_names and str ~= context.build.preceding_first_rendered_names[i] then + match_all = false + end + end + + if context.build.preceding_first_rendered_names then + local sub_str = context.options["subsequent-author-substitute"] + local sub_rule = context.options["subsequent-author-substitute-rule"] + + if sub_rule == "complete-all" then + if match_all then + if sub_str == "" then + result = nil + else + result.contents = {sub_str} + end + end + + elseif sub_rule == "complete-each" then + -- In-place substitution + if match_all then + for _, text in ipairs(context.build.first_rendered_names) do + text.contents = {sub_str} + end + result = self:concat(context.build.first_rendered_names, context) + end + + elseif sub_rule == "partial-each" then + for i, text in ipairs(context.build.first_rendered_names) do + if name_strings[i] == context.build.preceding_first_rendered_names[i] then + text.contents = {sub_str} + else + break + end + end + result = self:concat(context.build.first_rendered_names, context) + + elseif sub_rule == "partial-first" then + if name_strings[1] == context.build.preceding_first_rendered_names[1] then + context.build.first_rendered_names[1].contents = {sub_str} + end + result = self:concat(context.build.first_rendered_names, context) + end + end + + if #context.build.first_rendered_names > 0 then + context.build.first_rendered_names = nil + end + context.build.preceding_first_rendered_names = name_strings + return result +end + +names_module.Names = Names +names_module.Name = Name +names_module.NamePart = NamePart +names_module.EtAl = EtAl +names_module.Substitute = Substitute + +return names_module diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-node-number.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-number.lua new file mode 100644 index 00000000000..e4fe710fd00 --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-number.lua @@ -0,0 +1,99 @@ +local number_module = {} + +local element = require("citeproc-element") +local util = require("citeproc-util") + + +local Number = element.Element:new() + +function Number:render (item, context) + self:debug_info(context) + context = self:process_context(context) + local variable = context.options["variable"] + local content = self:get_variable(item, variable, context) + + table.insert(context.variable_attempt, content ~= nil) + + if not content then + return nil + end + + local numbers = {} + local punct_list = {} + local last_position = 1 + for number, punct, pos in string.gmatch(content, "(.-)%s*([-,&])%s*()") do + table.insert(numbers, number) + table.insert(punct_list, punct) + last_position = pos + end + table.insert(numbers, string.sub(content, last_position)) + + local res = "" + for i, number in ipairs(numbers) do + local punct = punct_list[i] + number = self:_format_single_number(number, context) + res = res .. number + + if punct == "-" then + res = res .. punct + elseif punct == "," then + res = res .. punct .. " " + elseif punct == "&" then + res = res .. " " .. punct .. " " + end + end + + res = self:case(res, context) + res = self:wrap(res, context) + res = self:display(res, context) + + return res +end + +function Number:_format_single_number(number, context) + local form = context.options["form"] or "numeric" + if form == "numeric" or not string.match(number, "^%d+$") then + return number + end + number = tonumber(number) + if form == "ordinal" or form == "long-ordinal" then + return self:_format_oridinal(number, form, context) + elseif form == "roman" then + return util.convert_roman(number) + end +end + +function Number:_format_oridinal(number, form, context) + assert(type(number) == "number") + local variable = context.options["variable"] + + if form == "long-ordinal" then + if number < 1 or number > 10 then + form = "ordinal" + end + end + + local gender = nil + local term = self:get_term(variable) + if term then + gender = term:get_attribute("gender") + end + + term = self:get_term(form, nil, number, gender) + local res = term:render(context) + if form == "ordinal" then + if res then + return tostring(number) .. res + else + res = tostring(number) + end + else + return res + end + return res +end + + +number_module.Number = Number + +return number_module diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-node-sort.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-sort.lua new file mode 100644 index 00000000000..f36baccf00a --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-sort.lua @@ -0,0 +1,183 @@ +local sort = {} + +local unicode = require("unicode") + +local element = require("citeproc-element") +local names = require("citeproc-node-names") +local date = require("citeproc-node-date") +local util = require("citeproc-util") + + +local Sort = element.Element:new() + +function Sort:sort (items, context) + -- key_map = { + -- id1 = {key1, key2, ...}, + -- id2 = {key1, key2, ...}, + -- ... + -- } + context.variable_attempt = {} + + local key_map = {} + local sort_directions = {} + -- true: ascending + -- false: descending + + if not Sort.collator_obj then + local lang = context.style.lang + local language = string.sub(lang, 1, 2) + -- It's 6 seconds slower to run the whole test-suite if these package + -- loading statements are put in the header. + local ducet = require("lua-uca.lua-uca-ducet") + local collator = require("lua-uca.lua-uca-collator") + local languages = require("lua-uca.lua-uca-languages") + local collator_obj = collator.new(ducet) + if languages[language] then + Sort.collator_obj = languages[language](collator_obj) + else + util.warning(string.format('Lcoale "%s" is not supported.', lang)) + end + end + + for _, item in ipairs(items) do + if not key_map[item.id] then + key_map[item.id] = {} + + context.item = item + for i, key in ipairs(self:query_selector("key")) do + if sort_directions[i] == nil then + local direction = (key:get_attribute("sort") ~= "descending") + sort_directions[i] = direction + end + local value = key:render(item, context) + table.insert(key_map[item.id], value) + end + end + end + + -- util.debug(key_map) + + local function compare_entry(item1, item2) + return self.compare_entry(key_map, sort_directions, item1, item2) + end + table.sort(items, compare_entry) + + return items +end + +function Sort.compare(value1, value2) + if type(value1) == "string" then + return Sort.compare_strings(value1, value2) + else + return value1 < value2 + end +end + +function Sort.compare_strings(str1, str2) + if Sort.collator_obj then + return Sort.collator_obj:compare_strings(str1, str2) + else + return str1 < str2 + end +end + +function Sort.compare_entry(key_map, sort_directions, item1, item2) + for i, value1 in ipairs(key_map[item1.id]) do + local ascending = sort_directions[i] + local value2 = key_map[item2.id][i] + if value1 and value2 then + local res + if ascending then + res = Sort.compare(value1, value2) + else + res = Sort.compare(value2, value1) + end + if res or value1 ~= value2 then + return res + end + elseif value1 then + return true + elseif value2 then + return false + end + end +end + +local Key = element.Element:new() + +function Key:render (item, context) + context = self:process_context(context) + context.options["name-as-sort-order"] = "all" + context.sorting = true + local variable = self:get_attribute("variable") + local res = nil + if variable then + context.variable = variable + local variable_type = util.variable_types[variable] + if variable_type == "name" then + res = self:_render_name(item, context) + elseif variable_type == "date" then + res = self:_render_date(item, context) + elseif variable_type == "number" then + res = item[variable] + else + res = item[variable] + end + else + local macro = self:get_attribute("macro") + if macro then + res = self:get_macro(macro):render(item, context) + end + end + if res == nil then + res = false + elseif type(res) == "table" and res._type == "RichText" then + res = res:render(nil, context) + end + if type(res) == "string" then + res = self._normalize_string(res) + end + return res +end + +function Key:_render_name (item, context) + if not self.names then + self.names = self:create_element("names", {}, self) + names.Names:set_base_class(self.names) + self.names:set_attribute("variable", context.options["variable"]) + self.names:set_attribute("form", "long") + end + local res = self.names:render(item, context) + return res +end + +function Key:_render_date (item, context) + if not self.date then + self.date = self:create_element("date", {}, self) + date.Date:set_base_class(self.date) + self.date:set_attribute("variable", context.options["variable"]) + self.date:set_attribute("form", "numeric") + end + local res = self.date:render(item, context) + return res +end +function Key._normalize_string(str) + str = unicode.utf8.lower(str) + str = string.gsub(str, "[%[%]]", "") + local words = {} + for _, word in ipairs(util.split(str, " ")) do + -- TODO: strip leading prepositions + -- remove leading apostrophe on name particle + word = string.gsub(word, "^" .. util.unicode["apostrophe"], "") + table.insert(words, word) + end + str = table.concat(words, " ") + str = string.gsub(str, util.unicode["apostrophe"], "'") + return str +end + + +sort.Sort = Sort +sort.Key = Key + +return sort diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-node-style.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-style.lua new file mode 100644 index 00000000000..b8386c47e7c --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-style.lua @@ -0,0 +1,193 @@ +local style = {} + +local element = require("citeproc-element") +local util = require("citeproc-util") + + +local Style = element.Element:new() + +Style.default_options = { + ["initialize-with-hyphen"] = true, + ["page-range-format"] = nil, + ["demote-non-dropping-particle"] = "display-and-sort", +} + +function Style:set_lang(lang, force_lang) + local default_locale = self:get_attribute("default-locale") + if lang then + if default_locale and not force_lang then + self.lang = default_locale + end + else + self.lang = default_locale or "en-US" + end +end + +function Style:render_citation (items, context) + self:debug_info(context) + context = self:process_context(context) + context.style = self + local citation = self:get_child("citation") + return citation:render(items, context) +end + +function Style:render_biblography (items, context) + self:debug_info(context) + context = self:process_context(context) + context.style = self + local bibliography = self:get_child("bibliography") + return bibliography:render(items, context) +end + +function Style:get_version () + return self:get_attribute("version") +end + +function Style:get_locales() + if not self.locale_dict then + self.locale_dict = {} + end + local locales = self.locale_dict[self.lang] + if not locales then + locales = self:get_locale_list(self.lang) + self.locale_dict[self.lang] = locales + end + return locales +end + +function Style:get_locale_list (lang) + assert(lang ~= nil) + local language = string.sub(lang, 1, 2) + local primary_dialect = util.primary_dialects[language] + if not primary_dialect then + -- util.warning(string.format("Failed to find primary dialect of \"%s\"", language)) + end + local locale_list = {} + + -- 1. In-style cs:locale elements + -- i. `xml:lang` set to chosen dialect, “de-AT” + if lang == language then + lang = primary_dialect + end + table.insert(locale_list, self:get_in_style_locale(lang)) + + -- ii. `xml:lang` set to matching language, “de” (German) + if language and language ~= lang then + table.insert(locale_list, self:get_in_style_locale(language)) + end + + -- iii. `xml:lang` not set + table.insert(locale_list, self:get_in_style_locale(nil)) + + -- 2. Locale files + -- iv. `xml:lang` set to chosen dialect, “de-AT” + if lang then + table.insert(locale_list, self:get_engine():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(locale_list, self:get_engine():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(locale_list, self:get_engine():get_system_locale("en-US")) + end + + return locale_list +end + +function Style:get_in_style_locale (lang) + for _, locale in ipairs(self:query_selector("locale")) do + if locale:get_attribute("xml:lang") == lang then + return locale + end + end + return nil +end + +function Style:get_term (...) + for _, locale in ipairs(self:get_locales()) do + local res = locale:get_term(...) + if res then + return res + end + end + return nil +end + + +local Citation = element.Element:new() + +function Citation:render (items, context) + self:debug_info(context) + context = self:process_context(context) + + context.mode = "citation" + context.citation = self + + local sort = self:get_child("sort") + if sort then + sort:sort(items, context) + end + + local layout = self:get_child("layout") + return layout:render(items, context) +end + + +local Bibliography = element.Element:new() + +Bibliography.default_options = { + ["hanging-indent"] = false, + ["second-field-align"] = nil, + ["line-spacing"] = 1, + ["entry-spacing"] = 1, + ["subsequent-author-substitute"] = nil, + ["subsequent-author-substitute-rule"] = "complete-all", +} + +function Bibliography:render (items, context) + self:debug_info(context) + context = self:process_context(context) + -- util.debug(context) + + context.mode = "bibliography" + context.bibliography = self + + -- Already sorted in CiteProc:sort_bibliography() + + local layout = self:get_child("layout") + local res = layout:render(items, context) + + local params = res[1] + + params.entryspacing = context.options["entry-spacing"] + params.linespacing = context.options["line-spacing"] + params.hangingindent = context.options["hanging-indent"] + params["second-field-align"] = context.options["second-field-align"] + for _, key in ipairs({"bibstart", "bibend"}) do + local value = context.engine.formatter[key] + if type(value) == "function" then + value = value(context) + end + params[key] = value + end + + params.bibliography_errors = {} + params.entry_ids = {} + for _, item in ipairs(items) do + table.insert(params.entry_ids, item.id) + end + + return res +end + +style.Style = Style +style.Citation = Citation +style.Bibliography = Bibliography + + +return style diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-node-text.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-text.lua new file mode 100644 index 00000000000..55dd13e295c --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-text.lua @@ -0,0 +1,217 @@ +local text = {} + +local element = require("citeproc-element") +local richtext = require("citeproc-richtext") +local util = require("citeproc-util") + + +local Text = element.Element:new() + +function Text:render (item, context) + self:debug_info(context) + context = self:process_context(context) + + local res = nil + + local variable = nil + local variable_name = self:get_attribute("variable") + if variable_name then + local form = self:get_attribute("form") + if form == "short" then + variable = self:get_variable(item, variable_name .. "-" .. form, context) + end + if not variable then + variable = self:get_variable(item, variable_name, context) + end + if variable then + res = variable + if type(res) == "number" then + res = tostring(res) + end + if variable_name == "page" or variable_name == "locator" then + res = util.lstrip(res) + res = self:_format_page(res, context) + end + end + + table.insert(context.variable_attempt, res ~= nil) + end + + local macro_name = self:get_attribute("macro") + if macro_name then + local macro = self:get_macro(macro_name) + res = macro:render(item, context) + end + + local term_name = self:get_attribute("term") + if term_name then + local form = self:get_attribute("form") + + local term = self:get_term(term_name, form) + if term then + res = term:render(context) + end + end + + local value = self:get_attribute("value") + if value then + res = value + res = self:escape(res) + end + + if type(res) == "string" and res ~= "" then + res = richtext.new(res) + end + + res = self:strip_periods(res, context) + res = self:case(res, context) + res = self:format(res, context) + res = self:quote(res, context) + res = self:wrap(res, context) + res = self:display(res, context) + + if variable_name == "citation-number" then + res = self:_process_citation_number(variable, res, context) + end + + return res +end + + +function Text:_process_citation_number(citation_number, res, context) + if context.mode == "citation" and not context.sorting and context.options["collapse"] == "citation-number" then + context.build.item_citation_numbers[context.item.id] = citation_number + if type(res) == "string" then + res = richtext.new(res) + end + table.insert(context.build.item_citation_number_text, res) + end + return res +end + + +function Text:_format_page (page, context) + local res = nil + + local page_range_delimiter = self:get_term("page-range-delimiter"):render(context) or util.unicode["en dash"] + local page_range_format = context.options["page-range-format"] + if page_range_format == "chicago" then + if self:get_style():get_version() >= "1.1" then + page_range_format = "chicago-16" + else + page_range_format = "chicago-15" + end + end + + local last_position = 1 + local page_parts = {} + local punct_list = {} + for part, punct, pos in string.gmatch(page, "(.-)%s*([,&])%s*()") do + table.insert(page_parts, part) + table.insert(punct_list, punct) + last_position = pos + end + table.insert(page_parts, string.sub(page, last_position)) + + res = "" + for i, part in ipairs(page_parts) do + res = res .. self:_format_range(part, page_range_format, page_range_delimiter) + local punct = punct_list[i] + if punct then + if punct == "&" then + res = res .. " " .. punct .. " " + else + res = res .. punct .. " " + end + end + end + res = self:escape(res) + return res +end + +function Text:_format_range (str, format, range_delimiter) + local start, delimiter, stop = string.match(str, "(%w+)%s*(%-+)%s*(%S*)") + if not stop or stop == "" then + return string.gsub(str, "\\%-", "-") + end + + + local start_prefix, start_num = string.match(start, "(.-)(%d*)$") + local stop_prefix, stop_num = string.match(stop, "(.-)(%d*)$") + + if start_prefix ~= stop_prefix then + -- Not valid range: "n11564-1568" -> "n11564-1568" + -- 110-N6 + -- N110-P5 + return start .. delimiter .. stop + end + + if format == "chicago-16" then + stop = self:_format_range_chicago_16(start_num, stop_num) + elseif format == "chicago-15" then + stop = self:_format_range_chicago_15(start_num, stop_num) + elseif format == "expanded" then + stop = stop_prefix .. self:_format_range_expanded(start_num, stop_num) + elseif format == "minimal" then + stop = self:_format_range_minimal(start_num, stop_num) + elseif format == "minimal-two" then + stop = self:_format_range_minimal(start_num, stop_num, 2) + end + + return start .. range_delimiter .. stop +end + +function Text:_format_range_chicago_16(start, stop) + if #start < 3 or string.sub(start, -2) == "00" then + return self:_format_range_expanded(start, stop) + elseif string.sub(start, -2, -2) == "0" then + return self:_format_range_minimal(start, stop) + else + return self:_format_range_minimal(start, stop, 2) + end + return stop +end + +function Text:_format_range_chicago_15(start, stop) + if #start < 3 or string.sub(start, -2) == "00" then + return self:_format_range_expanded(start, stop) + else + local changed_digits = self:_format_range_minimal(start, stop) + if string.sub(start, -2, -2) == "0" then + return changed_digits + elseif #start == 4 and #changed_digits == 3 then + return self:_format_range_expanded(start, stop) + else + return self:_format_range_minimal(start, stop, 2) + end + end + return stop +end + +function Text:_format_range_expanded(start, stop) + -- Expand "1234–56" -> "1234–1256" + if #start <= #stop then + return stop + end + return string.sub(start, 1, #start - #stop) .. stop +end + +function Text:_format_range_minimal(start, stop, threshold) + threshold = threshold or 1 + if #start < #stop then + return stop + end + local offset = #start - #stop + for i = 1, #stop - threshold do + local j = i + offset + if string.sub(stop, i, i) ~= string.sub(start, j, j) then + return string.sub(stop, i) + end + end + return string.sub(stop, -threshold) +end + + +text.Text = Text + +return text diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-nodes.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-nodes.lua new file mode 100644 index 00000000000..255d170bc9d --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-nodes.lua @@ -0,0 +1,44 @@ +--[[ + Copyright (C) 2021 Zeping Lee +--]] + + +local style = require("citeproc-node-style") +local locale = require("citeproc-node-locale") +local layout = require("citeproc-node-layout") +local text = require("citeproc-node-text") +local date = require("citeproc-node-date") +local number = require("citeproc-node-number") +local names = require("citeproc-node-names") +local label = require("citeproc-node-label") +local group = require("citeproc-node-group") +local choose = require("citeproc-node-choose") +local sort = require("citeproc-node-sort") + +local nodes = { + ["style"] = style.Style, + ["citation"] = style.Citation, + ["bibliography"] = style.Bibliography, + ["locale"] = locale.Locale, + ["term"] = locale.Term, + ["layout"] = layout.Layout, + ["text"] = text.Text, + ["date"] = date.Date, + ["date-part"] = date.DatePart, + ["number"] = number.Number, + ["names"] = names.Names, + ["name"] = names.Name, + ["name-part"] = names.NamePart, + ["et-al"] = names.EtAl, + ["substitute"] = names.Substitute, + ["label"] = label.Label, + ["group"] = group.Group, + ["choose"] = choose.Choose, + ["if"] = choose.If, + ["else"] = choose.Else, + ["else-if"] = choose.ElseIf, + ["sort"] = sort.Sort, + ["key"] = sort.Key, +} + +return nodes diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-richtext.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-richtext.lua new file mode 100644 index 00000000000..50f78a4466d --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-richtext.lua @@ -0,0 +1,777 @@ +--[[ + Copyright (C) 2021 Zeping Lee +--]] + +local richtext = {} + +local unicode = require("unicode") + +local util = require("citeproc-util") + + +local RichText = { + contents = nil, + formats = nil, + _type = "RichText", +} + +function RichText:shallow_copy() + local res = richtext.new() + for _, text in ipairs(self.contents) do + table.insert(res.contents, text) + end + for key, value in pairs(self.formats) do + res.formats[key] = value + end + return res +end + +function RichText:render(formatter, context, punctuation_in_quote) + self:merge_punctuations() + + if punctuation_in_quote == nil and context then + punctuation_in_quote = context.style:get_locale_option("punctuation-in-quote") + end + if punctuation_in_quote then + self:move_punctuation_in_quote() + end + + self:change_case() + + self:flip_flop() + + self:clean_formats() + + return self:_render(formatter, context) +end + +function RichText:_render(formatter, context) + local res = "" + for _, text in ipairs(self.contents) do + local str + if type(text) == "string" then + if formatter and formatter.text_escape then + str = formatter.text_escape(text) + else + str = text + end + else -- RichText + str = text:_render(formatter, context) + end + -- Remove leading spaces + if string.sub(res, -1) == " " and string.sub(str, 1, 1) == " " then + str = string.gsub(str, "^%s+", "") + end + res = res .. str + end + for _, attr in ipairs(richtext.format_sequence) do + local value = self.formats[attr] + if value then + local key = string.format("@%s/%s", attr, value) + if formatter then + local format = formatter[key] + if type(format) == "string" then + res = string.format(format, res) + elseif type(format) == "function" then + res = format(res, context) + end + end + end + end + return res +end + +function RichText:merge_punctuations(contents, index) + for i, text in ipairs(self.contents) do + if text._type == "RichText" then + contents, index = text:merge_punctuations(contents, index) + elseif type(text) == "string" then + if contents and index then + local previous_string = contents[index] + local last_char = string.sub(previous_string, -1) + local right_punct_map = richtext.punctuation_map[last_char] + if right_punct_map then + local first_char = string.sub(text, 1, 1) + local new_punctuations = nil + if first_char == last_char then + new_punctuations = last_char + elseif contents == self.contents then + new_punctuations = right_punct_map[first_char] + end + if new_punctuations then + if #text == 1 then + table.remove(self.contents, i) + else + self.contents[i] = string.sub(text, 2) + end + contents[index] = string.sub(previous_string, 1, -2) .. new_punctuations + end + end + end + contents = self.contents + index = i + end + end + return contents, index +end + +function RichText:move_punctuation_in_quote() + local i = 1 + while i <= #self.contents do + local text = self.contents[i] + if type(text) == "table" and text._type == "RichText" then + text:move_punctuation_in_quote() + + if text.formats["quotes"] then + local contents = self.contents + local last_string = text + while type(last_string) == "table" and last_string._type == "RichText" do + contents = last_string.contents + last_string = contents[#contents] + end + + local done = false + while not done do + done = true + last_string = contents[#contents] + local last_char = string.sub(last_string, -1) + if i < #self.contents then + local next_text = self.contents[i + 1] + if type(next_text) == "string" then + local first_char = string.sub(next_text, 1, 1) + if richtext.in_quote_punctuations[first_char] then + done = false + local right_punct_map = richtext.punctuation_map[last_char] + if right_punct_map then + first_char = right_punct_map[first_char] + last_string = string.sub(last_string, 1, -2) + end + contents[#contents] = last_string .. first_char + if #next_text == 1 then + table.remove(self.contents, i + 1) + else + self.contents[i + 1] = string.sub(next_text, 2) + end + end + end + end + end + end + end + i = i + 1 + end +end + +function RichText:change_case() + for _, text in ipairs(self.contents) do + if type(text) == "table" and text._type == "RichText" then + text:change_case() + end + end + local text_case = self.formats["text-case"] + if text_case then + if text_case == "lowercase" then + self:lowercase() + elseif text_case == "uppercase" then + self:uppercase() + elseif text_case == "capitalize-first" then + self:capitalize_first() + elseif text_case == "capitalize-all" then + self:capitalize_all() + elseif text_case == "sentence" then + self:sentence() + elseif text_case == "title" then + self:title() + end + end +end + +function RichText:_change_word_case(state, word_transform, first_tranform, is_phrase) + if self.formats["text-case"] == "nocase" then + return + end + if is_phrase and (self.formats["vertical-align"] == "sup" or + self.formats["vertical-align"] == "sub" or + self.formats["font-variant"] == "small-caps") then + return + end + state = state or "after-sentence" + word_transform = word_transform or function (x) return x end + first_tranform = first_tranform or word_transform + for i, text in ipairs(self.contents) do + if type(text) == "string" then + + local res = "" + local word_seps = { + " ", + "%-", + "/", + util.unicode["no-break space"], + util.unicode["en dash"], + util.unicode["em dash"], + } + for _, tuple in ipairs(util.split(text, word_seps, nil, true)) do + local word, punctuation = table.unpack(tuple) + if state == "after-sentence" then + res = res .. first_tranform(word) + if string.match(word, "%w") then + state = "after-word" + end + else + res = res .. word_transform(word, punctuation) + end + res = res .. punctuation + if string.match(word, "[.!?:]%s*$") then + state = "after-sentence" + end + end + + -- local word_index = 0 + -- local res = string.gsub(text, "%w+", function (word) + -- word_index = word_index + 1 + -- if word_index == 1 then + -- return first_tranform(word) + -- else + -- return word_transform(word) + -- end + -- end) + -- if string.match(res, "[.!?:]%s*$") then + -- state = "after-sentence" + -- end + + self.contents[i] = res + else + state = text:_change_word_case(state, word_transform, first_tranform, true) + end + end + return state +end + +function RichText:lowercase() + local word_transform = unicode.utf8.lower + self:_change_word_case("after-sentence", word_transform) +end + +function RichText:uppercase() + local word_transform = unicode.utf8.upper + self:_change_word_case("after-sentence", word_transform) +end + +local function capitalize(str) + local res = string.gsub(str, utf8.charpattern, unicode.utf8.upper, 1) + return res +end + +local function capitalize_if_lower(word) + if util.is_lower(word) then + return capitalize(word) + else + return word + end +end + +function RichText:capitalize_first(state) + local first_tranform = capitalize_if_lower + self:_change_word_case("after-sentence", nil, first_tranform) +end + +function RichText:capitalize_all() + local word_transform = capitalize_if_lower + self:_change_word_case("after-sentence", word_transform) +end + +function RichText:is_upper() + for _, text in ipairs(self.contents) do + if type(text) == "string" then + if not util.is_upper(text) then + return false + end + else + local res = text:is_upper() + if not res then + return false + end + end + end + return true +end + +function RichText:sentence() + if self:is_upper() then + local first_tranform = function(word) + return capitalize(unicode.utf8.lower(word)) + end + local word_transform = unicode.utf8.lower + self:_change_word_case("after-sentence", word_transform, first_tranform) + else + local first_tranform = capitalize_if_lower + self:_change_word_case("after-sentence", nil, first_tranform) + end +end + +function RichText:title() + if self:is_upper() then + local first_tranform = function(word) + return capitalize(unicode.utf8.lower(word)) + end + local word_transform = function(word, sep) + local res = unicode.utf8.lower(word) + if not util.stop_words[res] then + res = capitalize(res) + end + return res + end + self:_change_word_case("after-sentence", word_transform, first_tranform) + else + local first_tranform = capitalize_if_lower + local word_transform = function(word, sep) + local lower = unicode.utf8.lower(word) + -- Stop word before hyphen is treated as a normal word. + if util.stop_words[lower] and sep ~= "-" then + return lower + elseif word == lower then + return capitalize(word) + else + return word + end + end + self:_change_word_case("after-sentence", word_transform, first_tranform) + end +end + +function richtext.concat(str1, str2) + assert(str1 and str2) + + if type(str1) == "string" then + str1 = richtext.new(str1) + end + + local res + if next(str1.formats) == nil or str2 == "" then + -- shallow copy + res = str1 + else + res = richtext.new() + res.contents = {str1} + end + + if str2._type == "RichText" then + if next(str2.formats) == nil then + for _, text in ipairs(str2.contents) do + table.insert(res.contents, text) + end + else + table.insert(res.contents, str2) + end + elseif str2 ~= "" then + table.insert(res.contents, str2) + end + return res +end + +function richtext.concat_list(list, delimiter) + -- Strings in the list may be nil thus ipairs() should be avoided. + -- The delimiter may be nil. + local res = nil + for i = 1, #list do + local text = list[i] + if text and text ~= "" then + if res then + if delimiter and delimiter ~= "" then + res = richtext.concat(res, delimiter) + end + res = richtext.concat(res, text) + else + if type(text) == "string" then + text = richtext.new(text) + end + res = text + end + end + end + return res +end + +function RichText:strip_periods() + local last_string = self + local contents = self.contents + while last_string._type == "RichText" do + contents = last_string.contents + last_string = contents[#contents] + end + if string.sub(last_string, -1) == "." then + contents[#contents] = string.sub(last_string, 1, -2) + end +end + +function RichText:add_format(attr, value) + self.formats[attr] = value +end + +function RichText:flip_flop(attr, value) + if not attr then + for attr, _ in pairs(richtext.flip_flop_formats) do + self:flip_flop(attr) + end + return + end + + local default_value = richtext.default_formats[attr] + + if value and value ~= default_value and self.formats[attr] == value then + self.formats[attr] = richtext.flip_flop_values[attr][value] + end + if self.formats[attr] then + value = self.formats[attr] + end + + for _, text in ipairs(self.contents) do + if type(text) == "table" and text._type == "RichText" then + text:flip_flop(attr, value) + end + end +end + +function RichText:clean_formats(format) + -- Remove the formats that are default values + if not format then + for format, _ in pairs(richtext.default_formats) do + self:clean_formats(format) + end + return + end + if self.formats[format] then + if self.formats[format] == richtext.default_formats[format] then + self.formats[format] = nil + else + return + end + end + for _, text in ipairs(self.contents) do + if type(text) == "table" and text._type == "RichText" then + text:clean_formats(format) + end + end +end + +local RichText_mt = { + __index = RichText, + __concat = richtext.concat, +} + +local function table_update(t, new_t) + for key, value in pairs(new_t) do + t[key] = value + end + return t +end + +function RichText._split_tags(str) + -- Normalize markup + str = string.gsub(str, '<span%s+style="font%-variant:%s*small%-caps;?">', '<span style="font-variant:small-caps;">') + str = string.gsub(str, '<span%s+class="nocase">', '<span class="nocase">') + str = string.gsub(str, '<span%s+class="nodecor">', '<span class="nodecor">') + + local strings = {} + + local start_index = 1 + local i = 1 + + while i <= #str do + local substr = string.sub(str, i) + local starts_with_tag = false + for tag, _ in pairs(richtext.tags) do + if util.startswith(substr, tag) then + if start_index <= i - 1 then + table.insert(strings, string.sub(str, start_index, i-1)) + end + table.insert(strings, tag) + i = i + #tag + start_index = i + starts_with_tag = true + break + end + end + if not starts_with_tag then + i = i + 1 + end + end + if start_index <= #str then + table.insert(strings, string.sub(str, start_index)) + end + + for i = 1, #strings do + str = strings[i] + if str == "'" or str == util.unicode["apostrophe"] then + local previous_str = strings[i - 1] + local next_str = strings[i + 1] + if previous_str and next_str then + local previous_code_point = nil + for _, code_point in utf8.codes(previous_str) do + previous_code_point = code_point + end + local next_code_point = utf8.codepoint(next_str) + if util.is_romanesque(previous_code_point) and util.is_romanesque(next_code_point) then + -- An apostrophe + strings[i-1] = strings[i-1] .. util.unicode["apostrophe"] .. strings[i+1] + table.remove(strings, i+1) + table.remove(strings, i) + end + end + end + end + + return strings +end + +function richtext.new(text, formats) + local res = { + contents = {}, + formats = formats or {}, + } + + setmetatable(res, RichText_mt) + + if not text then + return res + end + + if type(text) == "string" then + + local strings = RichText._split_tags(text) + local contents = {} + for _, str in ipairs(strings) do + table.insert(contents, str) + + local end_tag = nil + if str == '"' then + local last_text = contents[#contents - 1] + if last_text and type(last_text) == "string" and string.match(last_text, "%s$") then + end_tag = nil + else + end_tag = str + end + elseif richtext.end_tags[str] then + end_tag = str + end + + if end_tag then + for i = #contents - 1, 1, -1 do + local start_tag = contents[i] + if type(start_tag) == "string" and richtext.tag_pairs[start_tag] == end_tag then + local subtext = richtext.new() + -- subtext.contents = util.slice(contents, i + 1, #contents - 1) + if start_tag == "'" and end_tag == "'" and i == #contents - 1 then + contents[i] = util.unicode["apostrophe"] + contents[#contents] = util.unicode["apostrophe"] + break + end + + for j = i + 1, #contents - 1 do + local substr = contents[j] + if substr == "'" then + substr = util.unicode["apostrophe"] + end + local last_text = subtext.contents[#subtext.contents] + if type(substr) == "string" and type(last_text) == "string" then + subtext.contents[#subtext.contents] = last_text .. substr + else + table.insert(subtext.contents, substr) + end + end + + if start_tag == '<span class="nodecor">' then + for attr, value in pairs(richtext.default_formats) do + subtext.formats[attr] = value + end + subtext.formats["text-case"] = "nocase" + else + for attr, value in pairs(richtext.tag_formats[start_tag]) do + subtext.formats[attr] = value + end + end + + for j = #contents, i, -1 do + table.remove(contents, j) + end + table.insert(contents, subtext) + break + end + end + end + end + + for i = #contents, 1, -1 do + if contents[i] == "'" then + contents[i] = util.unicode["apostrophe"] + end + if type(contents[i]) == "string" and type(contents[i+1]) == "string" then + contents[i] = contents[i] .. contents[i+1] + table.remove(contents, i+1) + end + end + + if #contents == 1 and type(contents[1]) == "table" then + res = contents[1] + else + res.contents = contents + end + + return res + + elseif type(text) == "table" and text._type == "RichText" then + return text + + elseif type(text) == "table" then + return text + end + return nil +end + +richtext.tag_formats = { + ["<i>"] = {["font-style"] = "italic"}, + ["<b>"] = {["font-weight"] = "bold"}, + ["<sup>"] = {["vertical-align"] = "sup"}, + ["<sub>"] = {["vertical-align"] = "sub"}, + ["<sc>"] = {["font-variant"] = "small-caps"}, + ['<span style="font-variant:small-caps;">'] = {["font-variant"] = "small-caps"}, + ['<span class="nocase">'] = {["text-case"] = "nocase"}, + ['"'] = {["quotes"] = "true"}, + [util.unicode['left double quotation mark']] = {["quotes"] = "true"}, + ["'"] = {["quotes"] = "true"}, + [util.unicode['left single quotation mark']] = {["quotes"] = "true"}, +} + +richtext.default_formats = { + ["URL"] = "false", + ["DOI"] = "false", + ["PMID"] = "false", + ["PMCID"] = "false", + ["font-style"] = "normal", + ["font-variant"] = "normal", + ["font-weight"] = "normal", + ["text-decoration"] = "none", + ["vertical-align"] = "baseline", + ["quotes"] = "false", +} + +richtext.format_sequence = { + "URL", + "DOI", + "PMID", + "PMCID", + "font-style", + "font-variant", + "font-weight", + "text-decoration", + "vertical-align", + "quotes", + "display", +} + +richtext.flip_flop_formats = { + ["font-style"] = true, + ["font-weight"] = true, + ["font-variant"] = true, + ["quotes"] = true, +} + +richtext.flip_flop_values = { + ["font-style"] = { + italic = "normal", + normal = "italic", + }, + ["font-weight"] = { + bold = "normal", + normal = "bold", + }, + ["font-variant"] = { + ["small-caps"] = "normal", + normal = "small-caps", + }, + ["quotes"] = { + ["true"] = "inner", + inner = "true", + }, +} + +-- https://github.com/Juris-M/citeproc-js/blob/aa2683f48fe23be459f4ed3be3960e2bb56203f0/src/queue.js#L724 +-- Also merge duplicate punctuations. +richtext.punctuation_map = { + ["!"] = { + ["."] = "!", + ["?"] = "!?", + [":"] = "!", + [","] = "!,", + [";"] = "!;", + }, + ["?"] = { + ["!"] = "?!", + ["."] = "?", + [":"] = "?", + [","] = "?,", + [";"] = "?;", + }, + ["."] = { + ["!"] = ".!", + ["?"] = ".?", + [":"] = ".:", + [","] = ".,", + [";"] = ".;", + }, + [":"] = { + ["!"] = "!", + ["?"] = "?", + ["."] = ":", + [","] = ":,", + [";"] = ":;", + }, + [","] = { + ["!"] = ",!", + ["?"] = ",?", + [":"] = ",:", + ["."] = ",.", + [";"] = ",;", + }, + [";"] = { + ["!"] = "!", + ["?"] = "?", + [":"] = ";", + [","] = ";,", + ["."] = ";", + } +} + +richtext.in_quote_punctuations = { + [","] = true, + ["."] = true, + ["?"] = true, + ["!"] = true, +} + +richtext.tag_pairs = { + ["<i>"] = "</i>", + ["<b>"] = "</b>", + ["<sup>"] = "</sup>", + ["<sub>"] = "</sub>", + ["<sc>"] = "</sc>", + ['<span style="font-variant:small-caps;">'] = "</span>", + ['<span class="nocase">'] = "</span>", + ['<span class="nodecor">'] = "</span>", + ['"'] = '"', + [util.unicode['left double quotation mark']] = util.unicode['right double quotation mark'], + ["'"] = "'", + [util.unicode['left single quotation mark']] = util.unicode['right single quotation mark'], +} + +richtext.tags = {} + +richtext.end_tags = {} + +for start_tag, end_tag in pairs(richtext.tag_pairs) do + richtext.tags[start_tag] = true + richtext.tags[end_tag] = true + richtext.end_tags[end_tag] = true +end + +return richtext diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-util.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-util.lua new file mode 100644 index 00000000000..9aaa0778746 --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-util.lua @@ -0,0 +1,794 @@ +--[[ + Copyright (C) 2021 Zeping Lee +--]] + +-- load `slnunicode` from LuaTeX +local unicode = require("unicode") +local inspect = require("inspect") + + +local util = {} + +function util.to_ordinal (n) + assert(type(n) == "number") + local last_digit = n % 10 + if last_digit == 1 and n ~= 11 + then return tostring(n) .. "st" + elseif last_digit == 2 and n ~= 12 + then return tostring(n) .. "nd" + elseif last_digit == 3 and n ~= 13 + then return tostring(n) .. "rd" + else + return tostring(n) .. "th" + end +end + + +function util.error(message) + if luatexbase then + luatexbase.module_error("citeproc", message) + else + error(message, 2) + end +end + +util.warning_enabled = true + +function util.warning(message) + if luatexbase then + luatexbase.module_warning("citeproc", message) + elseif util.warning_enabled then + io.stderr:write(message, "\n") + end +end + +local function remove_all_metatables(item, path) + if path[#path] ~= inspect.METATABLE then return item end +end + +function util.debug(...) + -- io.stderr:write(inspect(..., {process = remove_all_metatables})) + io.stderr:write(inspect(...)) + io.stderr:write("\n") +end + +-- Similar to re.split() in Python +function util.split(str, seps, maxsplit, include_sep) + if not str then + error("Invalid string.") + end + seps = seps or "%s+" + if seps == "" then + error("Empty separator") + end + if type(seps) == "string" then + seps = {seps} + end + + local splits = {} + for _, sep_pattern in ipairs(seps) do + for start, sep, stop in string.gmatch(str, "()(" .. sep_pattern .. ")()") do + table.insert(splits, {start, sep, stop}) + end + end + + if #seps > 1 then + table.sort(splits, function(a, b) return a[1] < b[1] end) + end + + local res = {} + local previous = 1 + for _, sep_tuple in ipairs(splits) do + local start, sep, stop = table.unpack(sep_tuple) + local item = string.sub(str, previous, start - 1) + if include_sep then + item = {item, sep} + end + table.insert(res, item) + previous = stop + end + local item = string.sub(str, previous, #str) + if include_sep then + item = {item, ""} + end + table.insert(res, item) + return res +end + +function util.slice (t, start, stop) + start = start or 1 + stop = stop or #t + if start < 0 then + start = start + #t + 1 + end + if stop < 0 then + stop = stop + #t + 1 + end + local new = {} + for i, item in ipairs(t) do + if i >= start and i <= stop then + table.insert(new, item) + end + end + return new +end + +function util.concat (list, sep) + -- This helper function omits empty strings in list, which is different from table.concat + -- This function always returns a string, even empty. + local res = "" + for i = 1, #list do + local s = list[i] + if s and s~= "" then + if res == "" then + res = s + else + res = res .. sep .. s + end + end + end + return res +end + +function util.lstrip (str) + if not str then + return nil + end + local res = string.gsub(str, "^%s+", "") + return res +end + +function util.rstrip (str) + if not str then + return nil + end + local res = string.gsub(str, "%s+$", "") + return res +end + +function util.strip (str) + return util.lstrip(util.rstrip(str)) +end + +function util.startswith (str, prefix) + return string.sub(str, 1, #prefix) == prefix +end + +function util.endswith (str, suffix) + return string.sub(str, -#suffix) == suffix +end + +function util.is_numeric (str) + if str == nil or str == "" then + return false + end + local res = true + for w in string.gmatch(str, "%w+") do + if string.match(w, "^[a-zA-Z]*%d+[a-zA-Z]*$") == nil then + res = false + break + end + end + for w in string.gmatch(str, "%W+") do + if string.match(w, "^%s*[,&-]+%s*$") == nil then + res = false + break + end + end + return res +end + +function util.is_uncertain_date (variable) + if variable == nil then + return false + end + local value = variable["circa"] + return value ~= nil and value ~= "" +end + +util.variable_types = {} + +-- schema/schemas/styles/csl-variables.rnc +util.variables = {} + +-- Date variables +util.variables.date = { + "accessed", + "available-date", + "event-date", + "issued", + "original-date", + "submitted", +} + +-- Name variables +util.variables.name = { + "author", + "chair", + "collection-editor", + "compiler", + "composer", + "container-author", + "contributor", + "curator", + "director", + "editor", + "editor-translator", + "editorial-director", + "executive-producer", + "guest", + "host", + "illustrator", + "interviewer", + "narrator", + "organizer", + "original-author", + "performer", + "producer", + "recipient", + "reviewed-author", + "script-writer", + "series-creator", + "translator", +} + +-- Number variables +util.variables.number = { + "chapter-number", + "citation-number", + "collection-number", + "edition", + "first-reference-note-number", + "issue", + "locator", + "number", + "number-of-pages", + "number-of-volumes", + "page", + "page-first", + "part-number", + "printing-number", + "section", + "supplement-number", + "version", + "volume", +} + +util.variable_types = {} + +for type, variables in pairs(util.variables) do + for _, variable in ipairs(variables) do + util.variable_types[variable] = type + end +end + +util.primary_dialects = { + af= "af-ZA", + ar= "ar", + bg= "bg-BG", + ca= "ca-AD", + cs= "cs-CZ", + cy= "cy-GB", + da= "da-DK", + de= "de-DE", + el= "el-GR", + en= "en-US", + es= "es-ES", + et= "et-EE", + eu= "eu", + fa= "fa-IR", + fi= "fi-FI", + fr= "fr-FR", + he= "he-IL", + hi= "hi-IN", + hr= "hr-HR", + hu= "hu-HU", + id= "id-ID", + is= "is-IS", + it= "it-IT", + ja= "ja-JP", + km= "km-KH", + ko= "ko-KR", + la= "la", + lt= "lt-LT", + lv= "lv-LV", + mn= "mn-MN", + nb= "nb-NO", + nl= "nl-NL", + nn= "nn-NO", + pl= "pl-PL", + pt= "pt-PT", + ro= "ro-RO", + ru= "ru-RU", + sk= "sk-SK", + sl= "sl-SI", + sr= "sr-RS", + sv= "sv-SE", + th= "th-TH", + tr= "tr-TR", + uk= "uk-UA", + vi= "vi-VN", + zh= "zh-CN" +} + + + +-- Range delimiter + +util.unicode = { + ["no-break space"] = "\u{00A0}", + ["em space"] = "\u{2003}", + ["en dash"] = "\u{2013}", + ["em dash"] = "\u{2014}", + ["left single quotation mark"] = "\u{2018}", + ["right single quotation mark"] = "\u{2019}", + ["apostrophe"] = "\u{2019}", + ["left double quotation mark"] = "\u{201C}", + ["right double quotation mark"] = "\u{201D}", + ["horizontal ellipsis"] = "\u{2026}", + ["narrow no-break space"] = "\u{202F}", +} + + +-- Text-case + +function util.is_lower (str) + return unicode.utf8.lower(str) == str +end + +function util.is_upper (str) + return unicode.utf8.upper(str) == str +end + +function util.capitalize (str) + str = unicode.utf8.lower(str) + local res = string.gsub(str, "%w", unicode.utf8.upper, 1) + return res +end + +function util.sentence (str) + if util.is_upper(str) then + return util.capitalize(str) + else + local output = {} + for i, word in ipairs(util.split(str)) do + if i == 1 and util.is_lower(word) then + table.insert(output, util.capitalize(word)) + else + table.insert(output, word) + end + end + return table.concat(output, " ") + end +end + +-- TODO: process multiple words +util.stop_words = { + ["a"] = true, + ["according to"] = true, + ["across"] = true, + ["afore"] = true, + ["after"] = true, + ["against"] = true, + ["ahead of"] = true, + ["along"] = true, + ["alongside"] = true, + ["amid"] = true, + ["amidst"] = true, + ["among"] = true, + ["amongst"] = true, + ["an"] = true, + ["and"] = true, + ["anenst"] = true, + ["apart from"] = true, + ["apropos"] = true, + ["apud"] = true, + ["around"] = true, + ["as"] = true, + ["as regards"] = true, + ["aside"] = true, + ["astride"] = true, + ["at"] = true, + ["athwart"] = true, + ["atop"] = true, + ["back to"] = true, + ["barring"] = true, + ["because of"] = true, + ["before"] = true, + ["behind"] = true, + ["below"] = true, + ["beneath"] = true, + ["beside"] = true, + ["besides"] = true, + ["between"] = true, + ["beyond"] = true, + ["but"] = true, + ["by"] = true, + ["c"] = true, + ["ca"] = true, + ["circa"] = true, + ["close to"] = true, + ["d'"] = true, + ["de"] = true, + ["despite"] = true, + ["down"] = true, + ["due to"] = true, + ["during"] = true, + ["et"] = true, + ["except"] = true, + ["far from"] = true, + ["for"] = true, + ["forenenst"] = true, + ["from"] = true, + ["given"] = true, + ["in"] = true, + ["inside"] = true, + ["instead of"] = true, + ["into"] = true, + ["lest"] = true, + ["like"] = true, + ["modulo"] = true, + ["near"] = true, + ["next"] = true, + ["nor"] = true, + ["notwithstanding"] = true, + ["of"] = true, + ["off"] = true, + ["on"] = true, + ["onto"] = true, + ["or"] = true, + ["out"] = true, + ["outside of"] = true, + ["over"] = true, + ["per"] = true, + ["plus"] = true, + ["prior to"] = true, + ["pro"] = true, + ["pursuant to"] = true, + ["qua"] = true, + ["rather than"] = true, + ["regardless of"] = true, + ["sans"] = true, + ["since"] = true, + ["so"] = true, + ["such as"] = true, + ["than"] = true, + ["that of"] = true, + ["the"] = true, + ["through"] = true, + ["throughout"] = true, + ["thru"] = true, + ["thruout"] = true, + ["till"] = true, + ["to"] = true, + ["toward"] = true, + ["towards"] = true, + ["under"] = true, + ["underneath"] = true, + ["until"] = true, + ["unto"] = true, + ["up"] = true, + ["upon"] = true, + ["v."] = true, + ["van"] = true, + ["versus"] = true, + ["via"] = true, + ["vis-à-vis"] = true, + ["von"] = true, + ["vs."] = true, + ["where as"] = true, + ["with"] = true, + ["within"] = true, + ["without"] = true, + ["yet"] = true, +} + +function util.title (str) + local output = {} + local previous = ":" + for i, word in ipairs(util.split(str)) do + local lower = unicode.utf8.lower(word) + if previous ~= ":" and util.stop_words[string.match(lower, "%w+")] then + table.insert(output, lower) + elseif util.is_lower(word) or util.is_upper(word) then + table.insert(output, util.capitalize(word)) + else + table.insert(output, word) + end + end + local res = table.concat(output, " ") + return res +end + +function util.all (t) + for _, item in ipairs(t) do + if not item then + return false + end + end + return true +end + +function util.any (t) + for _, item in ipairs(t) do + if item then + return true + end + end + return false +end + +-- ROMANESQUE_REGEXP = "-0-9a-zA-Z\u0e01-\u0e5b\u00c0-\u017f\u0370-\u03ff\u0400-\u052f\u0590-\u05d4\u05d6-\u05ff\u1f00-\u1fff\u0600-\u06ff\u200c\u200d\u200e\u0218\u0219\u021a\u021b\u202a-\u202e" + +util.romanesque_ranges = { + {0x0030, 0x0039}, -- 0-9 + {0x0041, 0x005A}, -- A-Z + {0x0061, 0x007A}, -- a-z + {0x0E01, 0x0E5B}, -- Thai + {0x0E01, 0x0E5B}, -- Thai + {0x00C0, 0x017F}, -- Latin-1 Supplement + {0x0370, 0x03FF}, -- Greek and Coptic + {0x0400, 0x052F}, -- Cyrillic + {0x0590, 0x05D4}, -- Hebrew + {0x05D6, 0x05FF}, -- Hebrew + {0x1F00, 0x1FFF}, -- Greek Extended + {0x0600, 0x06FF}, -- Arabic + {0x202A, 0x202E}, -- Writing directions in General Punctuation +} + +util.romanesque_chars = { + 0x200c, + 0x200d, + 0x200e, + 0x0218, + 0x0219, + 0x021a, + 0x021b, +} + +util.CJK_ranges = { + {0x4E00, 0x9FFF}, -- CJK Unified Ideographs + {0x3400, 0x4DBF}, -- CJK Unified Ideographs Extension A + {0x3040, 0x309F}, -- Hiragana + {0x30A0, 0x30FF}, -- Katakana + {0xF900, 0xFAFF}, -- CJK Compatibility Ideographs + {0x20000, 0x2A6DF}, -- CJK Unified Ideographs Extension B + {0x2A700, 0x2B73F}, -- CJK Unified Ideographs Extension C + {0x2B740, 0x2B81F}, -- CJK Unified Ideographs Extension D + {0x2B820, 0x2CEAF}, -- CJK Unified Ideographs Extension E + {0x2CEB0, 0x2EBEF}, -- CJK Unified Ideographs Extension F + {0x30000, 0x3134F}, -- CJK Unified Ideographs Extension G + {0x2F800, 0x2FA1F}, -- CJK Compatibility Ideographs Supplement +} + +function util.in_list (value, list) + for _, v in ipairs(list) do + if value == v then + return true + end + end + return false +end + +function util.in_ranges (value, ranges) + for _, range in ipairs(ranges) do + if value >= range[1] and value <= range[2] then + return true + end + end + return false +end + +function util.is_romanesque(code_point) + if not code_point then + return false + end + if util.in_ranges(code_point, util.romanesque_ranges) then + return true + end + if util.in_list(code_point, util.romanesque_chars) then + return true + end + return false +end + +function util.has_romanesque_char(s) + -- has romanesque char but not necessarily pure romanesque + if not s then + return false + end + for _, code_point in utf8.codes(s) do + if util.is_romanesque(code_point) then + return true + end + end + return false +end + +function util.is_cjk_char(code_point) + if not code_point then + return false + end + if util.in_ranges(code_point, util.CJK_ranges) then + return true + end + return false +end + +function util.has_cjk_char(s) + -- has romanesque char but not necessarily pure romanesque + if not s then + return false + end + for _, code_point in utf8.codes(s) do + if util.is_cjk_char(code_point) then + return true + end + end + return false +end + +function util.convert_roman (number) + assert(type(number) == "number") + local output = {} + for _, tuple in ipairs(util.roman_numerals) do + local letter, value = table.unpack(tuple) + table.insert(output, string.rep(letter, number // value)) + number = number % value + end + return table.concat(output, "") +end + +util.roman_numerals = { + {"m", 1000}, + {"cm", 900}, + {"d", 500}, + {"cd", 400}, + {"c", 100}, + {"xc", 90}, + {"l", 50}, + {"xl", 40}, + {"x", 10}, + {"ix", 9}, + {"v", 5}, + {"iv", 4}, + {"i", 1}, +}; + + +-- Choose + +util.position_map = { + ["first"] = 0, + ["subsequent"] = 1, + ["ibid"] = 2, + ["ibid-with-locator"] = 3, + ["container-subsequent"] = 4, +} + + +-- Output + +util.superscripts = { + ["\u{00AA}"] = "\u{0061}", + ["\u{00B2}"] = "\u{0032}", + ["\u{00B3}"] = "\u{0033}", + ["\u{00B9}"] = "\u{0031}", + ["\u{00BA}"] = "\u{006F}", + ["\u{02B0}"] = "\u{0068}", + ["\u{02B1}"] = "\u{0266}", + ["\u{02B2}"] = "\u{006A}", + ["\u{02B3}"] = "\u{0072}", + ["\u{02B4}"] = "\u{0279}", + ["\u{02B5}"] = "\u{027B}", + ["\u{02B6}"] = "\u{0281}", + ["\u{02B7}"] = "\u{0077}", + ["\u{02B8}"] = "\u{0079}", + ["\u{02E0}"] = "\u{0263}", + ["\u{02E1}"] = "\u{006C}", + ["\u{02E2}"] = "\u{0073}", + ["\u{02E3}"] = "\u{0078}", + ["\u{02E4}"] = "\u{0295}", + ["\u{1D2C}"] = "\u{0041}", + ["\u{1D2D}"] = "\u{00C6}", + ["\u{1D2E}"] = "\u{0042}", + ["\u{1D30}"] = "\u{0044}", + ["\u{1D31}"] = "\u{0045}", + ["\u{1D32}"] = "\u{018E}", + ["\u{1D33}"] = "\u{0047}", + ["\u{1D34}"] = "\u{0048}", + ["\u{1D35}"] = "\u{0049}", + ["\u{1D36}"] = "\u{004A}", + ["\u{1D37}"] = "\u{004B}", + ["\u{1D38}"] = "\u{004C}", + ["\u{1D39}"] = "\u{004D}", + ["\u{1D3A}"] = "\u{004E}", + ["\u{1D3C}"] = "\u{004F}", + ["\u{1D3D}"] = "\u{0222}", + ["\u{1D3E}"] = "\u{0050}", + ["\u{1D3F}"] = "\u{0052}", + ["\u{1D40}"] = "\u{0054}", + ["\u{1D41}"] = "\u{0055}", + ["\u{1D42}"] = "\u{0057}", + ["\u{1D43}"] = "\u{0061}", + ["\u{1D44}"] = "\u{0250}", + ["\u{1D45}"] = "\u{0251}", + ["\u{1D46}"] = "\u{1D02}", + ["\u{1D47}"] = "\u{0062}", + ["\u{1D48}"] = "\u{0064}", + ["\u{1D49}"] = "\u{0065}", + ["\u{1D4A}"] = "\u{0259}", + ["\u{1D4B}"] = "\u{025B}", + ["\u{1D4C}"] = "\u{025C}", + ["\u{1D4D}"] = "\u{0067}", + ["\u{1D4F}"] = "\u{006B}", + ["\u{1D50}"] = "\u{006D}", + ["\u{1D51}"] = "\u{014B}", + ["\u{1D52}"] = "\u{006F}", + ["\u{1D53}"] = "\u{0254}", + ["\u{1D54}"] = "\u{1D16}", + ["\u{1D55}"] = "\u{1D17}", + ["\u{1D56}"] = "\u{0070}", + ["\u{1D57}"] = "\u{0074}", + ["\u{1D58}"] = "\u{0075}", + ["\u{1D59}"] = "\u{1D1D}", + ["\u{1D5A}"] = "\u{026F}", + ["\u{1D5B}"] = "\u{0076}", + ["\u{1D5C}"] = "\u{1D25}", + ["\u{1D5D}"] = "\u{03B2}", + ["\u{1D5E}"] = "\u{03B3}", + ["\u{1D5F}"] = "\u{03B4}", + ["\u{1D60}"] = "\u{03C6}", + ["\u{1D61}"] = "\u{03C7}", + ["\u{2070}"] = "\u{0030}", + ["\u{2071}"] = "\u{0069}", + ["\u{2074}"] = "\u{0034}", + ["\u{2075}"] = "\u{0035}", + ["\u{2076}"] = "\u{0036}", + ["\u{2077}"] = "\u{0037}", + ["\u{2078}"] = "\u{0038}", + ["\u{2079}"] = "\u{0039}", + ["\u{207A}"] = "\u{002B}", + ["\u{207B}"] = "\u{2212}", + ["\u{207C}"] = "\u{003D}", + ["\u{207D}"] = "\u{0028}", + ["\u{207E}"] = "\u{0029}", + ["\u{207F}"] = "\u{006E}", + ["\u{2120}"] = "\u{0053}\u{004D}", + ["\u{2122}"] = "\u{0054}\u{004D}", + ["\u{3192}"] = "\u{4E00}", + ["\u{3193}"] = "\u{4E8C}", + ["\u{3194}"] = "\u{4E09}", + ["\u{3195}"] = "\u{56DB}", + ["\u{3196}"] = "\u{4E0A}", + ["\u{3197}"] = "\u{4E2D}", + ["\u{3198}"] = "\u{4E0B}", + ["\u{3199}"] = "\u{7532}", + ["\u{319A}"] = "\u{4E59}", + ["\u{319B}"] = "\u{4E19}", + ["\u{319C}"] = "\u{4E01}", + ["\u{319D}"] = "\u{5929}", + ["\u{319E}"] = "\u{5730}", + ["\u{319F}"] = "\u{4EBA}", + ["\u{02C0}"] = "\u{0294}", + ["\u{02C1}"] = "\u{0295}", + ["\u{06E5}"] = "\u{0648}", + ["\u{06E6}"] = "\u{064A}", +} + + +-- File IO + +function util.read_file(path) + if not path then + print(debug.traceback()) + end + local file = io.open(path, "r") + if not file then return nil end + local content = file:read("*a") + file:close() + return content +end + + +return util diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc.lua new file mode 100644 index 00000000000..19f217535ed --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc.lua @@ -0,0 +1,18 @@ +--[[ + Copyright (C) 2021 Zeping Lee +--]] + + +local citeproc = {} + +local engine = require("citeproc-engine") +local bib = require("citeproc-bib") +local util = require("citeproc-util") + +citeproc.__VERSION__ = "0.1.0" + +citeproc.new = engine.CiteProc.new +citeproc.parse_bib = bib.parse +citeproc.util = util + +return citeproc diff --git a/Master/texmf-dist/scripts/citation-style-language/csl-core.lua b/Master/texmf-dist/scripts/citation-style-language/csl-core.lua new file mode 100644 index 00000000000..8e0448b993e --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/csl-core.lua @@ -0,0 +1,264 @@ +local core = {} + +local citeproc = require("citeproc") +local util = citeproc.util +require("lualibs") + + +core.locale_file_format = "csl-locales-%s.xml" +core.ids = {} +core.loaded_ids = {} +core.uncite_all_items = false + +function core.error(message) + if luatexbase then + luatexbase.module_error("csl", message) + else + util.error(message) + end +end + +function core.warning(message) + if luatexbase then + luatexbase.module_warning("csl", message) + else + util.warning(message) + end +end + +function core.info(message) + if luatexbase then + luatexbase.module_info("csl", message) + else + util.info(message) + end +end + + +function core.read_file(file_name, ftype, file_info) + file_info = file_info or "file" + local path = kpse.find_file(file_name, ftype) + if not path then + if ftype and not util.endswith(file_name, ftype) then + file_name = file_name .. ftype + end + core.error(string.format('Failed to find %s "%s"', file_info, file_name)) + return nil + end + local file = io.open(path, "r") + if not file then + core.error(string.format('Failed to open %s "%s"', file_info, path)) + return nil + end + local contents = file:read("*a") + file:close() + return contents +end + +local function load_bib(bib_files) + local bib = {} + for _, bib_file in ipairs(bib_files) do + -- TODO: try to load `<bibname>.json` first? + local bib_contents = core.read_file(bib_file, "bib", "database file") + local file_name = bib_file + if not util.endswith(file_name, ".bib") then + file_name = file_name .. ".bib" + end + -- TODO: parse bib entries on demand + local csl_items = citeproc.parse_bib(bib_contents) + for _, item in ipairs(csl_items) do + local id = item.id + if bib[id] then + core.error(string.format('Duplicate entry key "%s" in "%s".', id, file_name)) + end + bib[id] = item + end + end + return bib +end + +function core.make_citeproc_sys(bib_files) + core.bib = load_bib(bib_files) + local citeproc_sys = { + retrieveLocale = function (lang) + local locale_file_format = core.locale_file_format or "locales-%s.xml" + local filename = string.format(locale_file_format, lang) + return core.read_file(filename) + end, + retrieveItem = function (id) + local res = core.bib[id] + -- if not res then + -- core.warning(string.format('Failed to find entry "%s".', id)) + -- end + return res + end + } + + return citeproc_sys +end + +function core.init(style_name, bib_files, lang) + if style_name == "" or #bib_files == 0 then + return nil + end + local style = core.read_file(style_name .. ".csl", nil, "style file") + if not style then + core.error(string.format('Failed to load style "%s.csl"', style_name)) + return nil + end + + local force_lang = nil + if lang and lang ~= "" then + force_lang = true + else + lang = nil + end + + local citeproc_sys = core.make_citeproc_sys(bib_files) + local engine = citeproc.new(citeproc_sys, style, lang, force_lang) + return engine +end + + +function core.make_citation(citation_info) + -- `citation_info`: "{ITEM-1@2}{{id={ITEM-1},label={page},locator={6}}}{3}" + local arguments = {} + for argument in string.gmatch(citation_info, "(%b{})") do + table.insert(arguments, string.sub(argument, 2, -2)) + end + if #arguments ~= 3 then + error(string.format('Invalid citation "%s"', citation_info)) + return nil + end + local citation_id, cite_items_str, note_index = table.unpack(arguments) + + local cite_items = {} + if citation_id == "nocite" then + for _, cite_id in ipairs(util.split(cite_items_str, "%s*,%s*")) do + table.insert(cite_items, {id = cite_id}) + end + + else + for item_str in string.gmatch(cite_items_str, "(%b{})") do + item_str = string.sub(item_str, 2, -2) + local cite_item = {} + for key, value in string.gmatch(item_str, "([%w%-]+)=(%b{})") do + if key == "sub-verbo" then + key = "sub verbo" + end + value = string.sub(value, 2, -2) + cite_item[key] = value + end + table.insert(cite_items, cite_item) + end + end + + local citation = { + citationID = citation_id, + citationItems = cite_items, + properties = { + noteIndex = tonumber(note_index), + }, + } + + return citation +end + + +function core.process_citations(engine, citations) + local citations_pre = {} + + -- Save the time of bibliography sorting by update all ids at one time. + core.update_item_ids(engine, citations) + local citation_strings = {} + + for _, citation in ipairs(citations) do + if citation.citationID ~= "nocite" then + local res = engine:processCitationCluster(citation, citations_pre, {}) + + for _, citation_res in ipairs(res[2]) do + local citation_str = citation_res[2] + local citation_id = citation_res[3] + citation_strings[citation_id] = citation_str + end + + table.insert(citations_pre, {citation.citationID, citation.properties.noteIndex}) + end + end + + return citation_strings +end + + +function core.update_item_ids(engine, citations) + if core.uncite_all_items then + for item_id, _ in pairs(core.bib) do + if not core.loaded_ids[item_id] then + table.insert(core.ids, item_id) + core.loaded_ids[item_id] = true + end + end + end + for _, citation in ipairs(citations) do + for _, cite_item in ipairs(citation.citationItems) do + local id = cite_item.id + if id == "*" then + for item_id, _ in pairs(core.bib) do + if not core.loaded_ids[item_id] then + table.insert(core.ids, item_id) + core.loaded_ids[item_id] = true + end + end + else + if not core.loaded_ids[id] then + table.insert(core.ids, id) + core.loaded_ids[id] = true + end + end + end + end + engine:updateItems(core.ids) + -- TODO: engine:updateUncitedItems(ids) +end + + +function core.make_bibliography(engine) + local result = engine:makeBibliography() + + local params = result[1] + local bib_items = result[2] + + local res = "" + + local bib_options = "" + if params["hangingindent"] then + bib_options = bib_options .. "\n hanging-indent = true," + end + if params["linespacing"] then + bib_options = bib_options .. string.format("\n line-spacing = %d,", params["linespacing"]) + end + if params["entryspacing"] then + bib_options = bib_options .. string.format("\n entry-spacing = %d,", params["entryspacing"]) + end + + if bib_options ~= "" then + bib_options = "\\cslsetup{" .. bib_options .. "\n}\n\n" + res = res .. bib_options + end + + if params.bibstart then + res = res .. params.bibstart + end + + for _, bib_item in ipairs(bib_items) do + res = res .. "\n" .. bib_item + end + + if params.bibend then + res = res .. "\n" .. params.bibend + end + return res +end + + +return core diff --git a/Master/texmf-dist/scripts/citation-style-language/csl.lua b/Master/texmf-dist/scripts/citation-style-language/csl.lua new file mode 100644 index 00000000000..701102319be --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/csl.lua @@ -0,0 +1,141 @@ +--[[ + Copyright (C) 2021 Zeping Lee +--]] + +local csl = {} + +local citeproc = require("citeproc") +local util = citeproc.util +require("lualibs") +local core = require("csl-core") + + +csl.initialized = "false" +csl.citations = {} +csl.citations_pre = {} + + +function csl.error(str) + luatexbase.module_error("csl", str) +end +function csl.warning(str) + luatexbase.module_warning("csl", str) +end +function csl.info(str) + luatexbase.module_info("csl", str) +end + + +function csl.init(style_name, bib_files, lang) + bib_files = util.split(util.strip(bib_files), "%s*,%s*") + + csl.engine = core.init(style_name, bib_files, lang) + + if csl.engine then + csl.initialized = "true" + else + return + end + + -- csl.init is called via \AtBeginDocument and it's executed after + -- loading .aux file. The csl.ids are already registered. + csl.citation_strings = core.process_citations(csl.engine, csl.citations) + csl.style_class = csl.engine:get_style_class() + + for _, citation in ipairs(csl.citations) do + local citation_id = citation.citationID + local citation_str = csl.citation_strings[citation_id] + local bibcite_command = string.format("\\bibcite{%s}{{%s}{%s}}", citation.citationID, csl.style_class, citation_str) + tex.sprint(bibcite_command) + end + +end + + +function csl.register_citation_info(citation_info) + local citation = core.make_citation(citation_info) + table.insert(csl.citations, citation) +end + + +function csl.enable_linking() + csl.engine:enable_linking() +end + + +function csl.cite(citation_info) + if not csl.engine then + csl.error("CSL engine is not initialized.") + end + + local citation = core.make_citation(citation_info) + + local res = csl.engine:processCitationCluster(citation, csl.citations_pre, {}) + + local citation_str + for _, citation_res in ipairs(res[2]) do + local citation_id = citation_res[3] + -- csl.citation_strings[citation_id] = citation_res[2] + if citation_id == citation.citationID then + citation_str = citation_res[2] + end + end + tex.sprint(citation_str) + + table.insert(csl.citations_pre, {citation.citationID, citation.properties.noteIndex}) +end + + +function csl.nocite(ids_string) + local cite_ids = util.split(ids_string, "%s*,%s*") + if csl.engine then + local ids = {} + for _, cite_id in ipairs(cite_ids) do + if cite_id == "*" then + for item_id, _ in pairs(core.bib) do + table.insert(ids, item_id) + end + else + table.insert(ids, cite_id) + end + end + csl.engine:updateUncitedItems(ids) + else + -- `\nocite` in preamble, where csl.engine is not initialized yet + for _, cite_id in ipairs(cite_ids) do + if cite_id == "*" then + core.uncite_all_items = true + else + if not core.loaded_ids[cite_id] then + table.insert(core.ids, cite_id) + core.loaded_ids[cite_id] = true + end + end + end + end +end + + +function csl.bibliography() + if not csl.engine then + csl.error("CSL engine is not initialized.") + return + end + + -- if csl.include_all_items then + -- for id, _ in pairs(csl.bib) do + -- if not csl.loaded_ids[id] then + -- table.insert(csl.ids, id) + -- csl.loaded_ids[id] = true + -- end + -- end + -- end + -- csl.engine:updateItems(csl.ids) + + local result = core.make_bibliography(csl.engine) + + tex.print(util.split(result, "\n")) +end + + +return csl |