summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/scripts/citation-style-language
diff options
context:
space:
mode:
authorKarl Berry <karl@freefriends.org>2022-01-21 22:58:35 +0000
committerKarl Berry <karl@freefriends.org>2022-01-21 22:58:35 +0000
commit39207a02179eab0fca34dfb7bb8dbbb6215fdc7a (patch)
tree14f6a970d4a28da5523838a10debde16683f3fb9 /Master/texmf-dist/scripts/citation-style-language
parent59ab26e4b8b11acdd61b232207d7a9ae578c63e4 (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/citation-style-language')
-rwxr-xr-xMaster/texmf-dist/scripts/citation-style-language/citeproc188
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-bib.lua395
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-element.lua410
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-engine.lua419
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-formats.lua209
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-node-choose.lua126
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-node-date.lua379
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-node-group.lua32
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-node-label.lua106
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-node-layout.lua231
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-node-locale.lua130
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-node-names.lua696
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-node-number.lua99
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-node-sort.lua183
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-node-style.lua193
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-node-text.lua217
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-nodes.lua44
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-richtext.lua777
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc-util.lua794
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/citeproc.lua18
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/csl-core.lua264
-rw-r--r--Master/texmf-dist/scripts/citation-style-language/csl.lua141
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, "%&", "&#38;")
+ str = string.gsub(str, "<", "&#60;")
+ str = string.gsub(str, ">", "&#62;")
+ 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