diff options
author | Karl Berry <karl@freefriends.org> | 2022-01-21 22:58:35 +0000 |
---|---|---|
committer | Karl Berry <karl@freefriends.org> | 2022-01-21 22:58:35 +0000 |
commit | 39207a02179eab0fca34dfb7bb8dbbb6215fdc7a (patch) | |
tree | 14f6a970d4a28da5523838a10debde16683f3fb9 /Master/texmf-dist/scripts/citation-style-language/citeproc-bib.lua | |
parent | 59ab26e4b8b11acdd61b232207d7a9ae578c63e4 (diff) |
citation-style-language (21jan22)
git-svn-id: svn://tug.org/texlive/trunk@61687 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/scripts/citation-style-language/citeproc-bib.lua')
-rw-r--r-- | Master/texmf-dist/scripts/citation-style-language/citeproc-bib.lua | 395 |
1 files changed, 395 insertions, 0 deletions
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 |