diff options
Diffstat (limited to 'biblio/citation-style-language/citeproc-bibtex.lua')
-rw-r--r-- | biblio/citation-style-language/citeproc-bibtex.lua | 713 |
1 files changed, 0 insertions, 713 deletions
diff --git a/biblio/citation-style-language/citeproc-bibtex.lua b/biblio/citation-style-language/citeproc-bibtex.lua deleted file mode 100644 index 645fb5384b..0000000000 --- a/biblio/citation-style-language/citeproc-bibtex.lua +++ /dev/null @@ -1,713 +0,0 @@ --- --- Copyright (c) 2021-2022 Zeping Lee --- Released under the MIT license. --- Repository: https://github.com/zepinglee/citeproc-lua --- - ---[[ - A PEG-based implementation of a Bib(La)TeX dateabase (.bib) parser - References notes: tools/bibtex-parser-notes.md ---]] - -local bibtex = {} - -local lpeg = require("lpeg") -local unicode = require("unicode") -local bibtex_data = require("citeproc-bibtex-data") -local util = require("citeproc-util") - -local P = lpeg.P -local R = lpeg.R -local S = lpeg.S -local C = lpeg.C -local Cc = lpeg.Cc -local Cf = lpeg.Cf -local Cg = lpeg.Cg -local Cmt = lpeg.Cmt -local Cp = lpeg.Cp -local Ct = lpeg.Ct -local V = lpeg.V - -bibtex.strings = {} -- Stores all the @STRINGs -for key, value in pairs(bibtex_data.macros) do - bibtex.strings[key] = value.value -end - - --- Parse BibTeX content and convert to CSL-JSON -function bibtex.parse(str) - local bibliography = bibtex.parse_bibtex(str, bibtex.strings) - local csl_json_items = bibtex.convert_csl_json(bibliography) - return csl_json_items -end - - --- The case-insensitivity trick is taken from <http://boston.conman.org/2020/06/05.1>. -local function ignore_case(str) - local char = R("AZ", "az") / function (c) return P(c:lower()) + P(c:upper()) end - + P(1) / function (c) return P(c) end - return Cf(char^1, function (a, b) return a * b end):match(str) -end - - local function normalize_white_space(str) - local res = string.gsub(str, "%s+", " ") - return res - end - --- Based on the grammar described at <https://github.com/aclements/biblib>. -function bibtex.get_bibtex_grammar() - local comment = (1 - P"@")^0 - local space = S(" \t\r\n")^0 - local comment_cmd = ignore_case("comment") - local balanced = P{ "{" * V(1)^0 * "}" + (1 - S"{}") } - local ident = (- R"09") * (R"\x20\x7F" - S" \t\"#%'(),={}")^1 - - local piece = (P"{" * (balanced^0 / normalize_white_space) * P"}") - + (P'"' * ((balanced - P'"')^0 / normalize_white_space) * P'"') - + C(R("09")^1) - + C(ident) / function (name) - return { - category = "string", - name = string.lower(name), - } - end - local value = Ct(piece * (space * P"#" * space * piece)^0) - - local string_body = Cg(ident / string.lower, "name") * space * P"=" * space * Cg(value, "contents") - local string_cmd = Ct(Cg(Cc"string", "category") * ignore_case("string") * space * ( - P"{" * space * string_body * space * P"}" - + P"(" * space * string_body * space * P")" - )) - - local preamble_body = Cg(value, "contents") - local preamble_cmd = Ct(Cg(Cc"preamble", "category") * ignore_case("preamble") * space * ( - P"{" * space * preamble_body * space * P"}" - + P"(" * space * preamble_body * space * P")" - )) - - local key = (1 - S", \t}\r\n")^0 - local key_paren = (1 - S", \t\r\n")^0 - local field_value_pair = (ident / string.lower) * space * P"=" * space * value -- * record_success_position() - - local entry_body = Cf(Ct"" * (P"," * space * Cg(field_value_pair))^0 * (P",")^-1, rawset) - local entry = Ct(Cg(Cc"entry", "category") * Cg(ident / string.lower, "type") * space * ( - P"{" * space * Cg(key, "key") * space * Cg(entry_body, "fields")^-1 * space * (P"}") - + P"(" * space * Cg(key_paren, "key") * space * Cg(entry_body, "fields")^-1 * space * (P")") - )) - - local command_or_entry = P"@" * space * (comment_cmd + preamble_cmd + string_cmd + entry) - - -- The P(-1) causes nil parsing result in case of error. - local bibtex_grammar = Ct(comment * (command_or_entry * comment)^0) * P(-1) - - return bibtex_grammar -end - - -bibtex.bibtex_grammar = bibtex.get_bibtex_grammar() - - --- Return a list of entries, strings, preamble -function bibtex.parse_bibtex_objects(str) - return bibtex.bibtex_grammar:match(str) -end - - -local function concat_strings(pieces, strings) - local value = "" - for _, piece in ipairs(pieces) do - if type(piece) == "string" then - value = value .. piece - elseif type(piece) == "table" and piece.category == "string" then - local piece_str = strings[piece.name] - if piece_str then - value = value .. piece_str - else - util.warning(string.format('String name "%s" is undefined."', piece.name)) - end - end - end - value = normalize_white_space(value) - return value -end - - -function bibtex.parse_bibtex(content, strings) - if strings then - strings = setmetatable({}, {__index = strings}) - else - strings = setmetatable({}, {__index = bibtex.strings}) - end - local bib_objects = bibtex.parse_bibtex_objects(content) - - local res = { - entries = {}, - strings = {}, - preamble = "", - } - local exceptions = {} - for _, object in ipairs(bib_objects) do - if object.category == "entry" then - object.category = nil - for field, value in pairs(object.fields) do - local value_str = concat_strings(value, strings) - if field ~= "url" then - value_str = bibtex.to_unicode(value_str) - end - object.fields[field] = value_str - end - table.insert(res.entries, object) - - elseif object.category == "string" then - local string_value = concat_strings(object.contents, strings) - strings[object.name] = string_value - res.strings[object.name] = string_value - - elseif object.category == "preamble" then - res.preamble = res.preamble .. concat_strings(object.contents, strings) - - -- elseif object.category == "comment" then - -- Is this really needed? - - elseif object.category == "exception" then - -- TODO - - end - end - - return res -end - - -local function get_line_number(str, position) - local line_number = 1 - for i = 1, position do - if string.sub(str, i, i) == "\n" then - line_number = line_number + 1 - end - end - return line_number -end - - -function bibtex.get_latex_grammar() - local space = S(" \t\r\n")^0 - local specials = P"\\$" / "$" - + P"\\%" / "%" - + P"\\&" / "&" - + P"\\#" / "#" - + P"\\_" / "_" - + P"\\{" / "{" - + P"\\}" / "}" - + P"~" / util.unicode["no-break space"] - local control_sequence = C(P"\\" * (R("AZ", "az")^1 + 1) * space) / function (cs) - return { - type = "control_sequence", - name = util.rstrip(cs), - raw = cs, - } - end - local math = P"$" * C((P"\\$" + 1 - S"$")^0) * P"$" / function (math_text) - return { - type = "math", - contents = math_text, - } - end - local ligatures = P"``" / util.unicode["left double quotation mark"] - + P"`" / util.unicode["left single quotation mark"] - + P"''" / util.unicode["right double quotation mark"] - + P"'" / util.unicode["right single quotation mark"] - + P"---" / util.unicode["em dash"] - + P"--" / util.unicode["en dash"] - local plain_text = C(1 - S"{}$\\") - local latex_grammar = P{ - "latex_text"; - latex_text = Ct((specials + control_sequence + math + ligatures + specials + V"group" + plain_text)^0), - group = P"{" * V"latex_text" * P"}" / function (group_contents) - return { - type = "group", - contents = group_contents, - } - end, - } - return latex_grammar -end - -bibtex.latex_grammar = bibtex.get_latex_grammar() - - -function bibtex.to_unicode(str) - local tokens = bibtex.latex_grammar:match(str) - local res = bibtex.unescape_tokens(tokens, 0) - return res -end - -local format_commands_with_argment = { - ["\\textrm"] = {'<span class="nodecor">', '</span>'}, - ["\\textbf"] = {'<b>', '</b>'}, - ["\\textit"] = {'<i>', '</i>'}, - ["\\textsl"] = {'<i>', '</i>'}, - ["\\emph"] = {'<i>', '</i>'}, - ["\\textsc"] = {'<span style="font-variant: small-caps;">', '</span>'}, - ["\\textsuperscript"] = {'<sup>', '</sup>'}, - ["\\textsubscript"] = {'<sub>', '</sub>'}, -} - -local format_commands_without_argment = { - ["\\rm"] = {'<span class="nodecor">', '</span>'}, - ["\\upshape"] = {'<span class="nodecor">', '</span>'}, - ["\\bf"] = {'<b>', '</b>'}, - ["\\bfseries"] = {'<b>', '</b>'}, - ["\\it"] = {'<i>', '</i>'}, - ["\\itshape"] = {'<i>', '</i>'}, - ["\\sl"] = {'<i>', '</i>'}, - ["\\slshape"] = {'<i>', '</i>'}, - ["\\em"] = {'<i>', '</i>'}, - ["\\scshape"] = {'<span style="font-variant: small-caps;">', '</span>'}, - ["\\sc"] = {'<span style="font-variant: small-caps;">', '</span>'}, -} - - -function bibtex.unescape_tokens(tokens, brace_level) - local res = "" - local skip_token = false - for i, token in ipairs(tokens) do - if skip_token then - skip_token = false - else - if type(token) == "string" then - res = res .. token - elseif type(token) == "table" then - if token.type == "control_sequence" then - local code_point = bibtex_data.unicode_commands[token.name] - if code_point then - local unicode_char - if type(code_point) == "string" then - unicode_char = utf8.char(tonumber(code_point, 16)) - elseif type(code_point) == "table" then - -- The command takes an argument (\"{o}) - local arg - if i < #tokens then - local next_token = tokens[i + 1] - if type(next_token) == "string" then - arg = next_token - skip_token = true - elseif type(next_token) == "table" then - if next_token.type == "control_sequence" then - arg = next_token.name - skip_token = true - elseif next_token.type == "group" then - if #next_token.contents == 0 then - arg = "{}" - skip_token = true - elseif #next_token.contents == 1 then - next_token = next_token.contents[1] - if type(next_token) == "string" then - arg = next_token - skip_token = true - elseif type(next_token) == "table" then - arg = next_token.name - skip_token = true - end - end - end - end - end - if arg and code_point[arg] then - unicode_char = utf8.char(tonumber(code_point[arg], 16)) - end - end - if unicode_char then - res = res .. unicode_char - else - res = res .. '<span class="nocase">' .. token.raw .. '</span>' - end - - elseif format_commands_with_argment[token.name] then - -- Commands like `\textbf` - if i < #tokens then - local next_token = tokens[i + 1] - if type(next_token) == "table" and next_token.type == "group" then - local tags = format_commands_with_argment[token.name] - if brace_level == 0 then - res = res .. tags[1] .. '<span class="nocase">' .. bibtex.unescape_tokens(next_token.contents, brace_level + 1) .. '</span>' .. tags[2] - else - res = res .. tags[1] .. bibtex.unescape_tokens(next_token.contents, brace_level + 1) .. tags[2] - end - skip_token = true - else - res = res .. '<span class="nocase">' .. token.raw .. '</span>' - end - else - res = res .. '<span class="nocase">' .. token.raw .. '</span>' - end - - else - res = res .. '<span class="nocase">' .. token.raw .. '</span>' - - end - - elseif token.type == "math" then - -- res = res .. "$" .. token .. "$" - res = res .. '<span class="nocase">' .. token.raw .. '</span>' - - elseif token.type == "group" then - local first_token = token.contents[1] - if type(first_token) == "table" and first_token.type == "control_sequence" then - local tags = format_commands_without_argment[first_token.name] - if tags then - table.remove(token.contents, 1) - res = res .. tags[1] .. bibtex.unescape_tokens(token.contents, brace_level) .. tags[2] - else - res = res .. bibtex.unescape_tokens(token.contents, brace_level) - end - else - if brace_level == 0 then - res = res .. '<span class="nocase">' .. bibtex.unescape_tokens(token.contents, brace_level + 1) .. '</span>' - else - res = res .. '<span class="nocase">' .. bibtex.unescape_tokens(token.contents, brace_level + 1) .. '</span>' - end - end - - end - end - end - end - return res -end - --- function bibtex.format_syntanx_error(str, position, file_path) --- local line_number = 1 --- local line_start = 1 --- local column_number = 0 --- for i = 1, position do --- column_number = column_number + 1 --- if string.sub(str, i, i) == "\n" then --- line_number = line_number + 1 --- line_start = i + 1 --- column_number = 0 --- end --- end --- -- return line_number, column_number - --- local res --- if file_path then --- res = string.format('File "%s", line %d\n', file_path, line_number) --- else --- res = string.format('Line %d\n', line_number) --- end --- local line_end = position --- while line_end < #str + 1 and string.sub(str, line_end, line_end) ~= "\n" do --- line_end = line_end + 1 --- end --- res = res .. " " .. string.sub(str, line_start, line_end - 1) .. "\n" --- res = res .. "Syntax Error" --- return res --- end - - -function bibtex.convert_csl_json(bibliography) - local csl_json_data = {} - for _, entry in ipairs(bibliography.entries) do - local item = { -- CSL-JSON item - id = entry.key, - type = "document", - } - - -- CSL types - local type_data = bibtex_data.types[entry.type] - if type_data and type_data.csl then - item.type = type_data.csl - end - - for field, value in pairs(entry.fields) do - local csl_field, csl_value = bibtex.convert_field(field, value) - - if csl_field and csl_value and not item[csl_field] then - item[csl_field] = csl_value - end - end - - bibtex.process_special_fields(item, entry.fields) - - table.insert(csl_json_data, item) - end - - return csl_json_data -end - - -function bibtex.convert_field(bib_field, value) - local field_data = bibtex_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 - - local field_type = field_data.type - if field_type == "name" then - value = bibtex.parse_names(value) - - elseif field_type == "date" then - value = bibtex.parse_date(value) - - -- elseif bib_field == "title" or bib_field == "booktitle" then - -- value = bibtex.convert_sentence_case(value) - - elseif bib_field == "volume" or bib_field == "pages" then - value = string.gsub(value, util.unicode["en dash"], "-") - end - - return csl_field, value -end - - -function bibtex.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] = bibtex.parse_single_name(name) - end - return names -end - -function bibtex.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 bibtex.parse_revesed_name(name_parts) - else - return bibtex.parse_non_revesed_name(str) - end -end - -function bibtex.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 bibtex.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 bibtex.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_ = bibtex.parse_single_date(date_part) - if not date_ then - return literal - end - table.insert(date["date-parts"], date_) - end - return date -end - -function bibtex.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 - - --- -- TODO: check if the original title is in sentence case --- function bibtex.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 = string.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 bibtex.process_special_fields(item, bib_fields) - -- Default entry type `document` - if item.type == "document" then - if item.URL then - item.type = "webpage" - else - item.type = "article" - end - end - - -- event-title: for compatibility with CSL v1.0.1 and earlier versions - if item["event-title"] then - item.event = item["event-title"] - end - - -- issued date - if bib_fields.year and not item.issued then - item.issued = bibtex.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 - - -- language: convert `babel` language to ISO 639-1 language code - if not item.language and bib_fields.language then - item.language = bib_fields.language - end - if item.language then - local language_code = bibtex_data.language_code_map[item.language] - if language_code then - item.language = language_code - end - end - -- if not item.language then - -- if util.has_cjk_char(item.title) then - -- item.language = "zh" - -- end - -- end - - -- number - if item.number then - if item.type == "article-journal" or item.type == "article-magazine" or item.type == "article-newspaper" or item.type == "periodical" then - if not item.issue then - item.issue = item.number - item.number = nil - end - elseif item["collection-title"] and not item["collection-number"] then - item["collection-number"] = item.number - item.number = nil - end - end - - -- PMID - if bib_fields.eprint and string.lower(bib_fields.eprinttype) == "pubmed" and not item.PMID then - item.PMID = bib_fields.eprint - end - -end - - -return bibtex |