diff options
Diffstat (limited to 'biblio/citation-style-language/citeproc-util.lua')
-rw-r--r-- | biblio/citation-style-language/citeproc-util.lua | 794 |
1 files changed, 794 insertions, 0 deletions
diff --git a/biblio/citation-style-language/citeproc-util.lua b/biblio/citation-style-language/citeproc-util.lua new file mode 100644 index 0000000000..9aaa077874 --- /dev/null +++ b/biblio/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 |