summaryrefslogtreecommitdiff
path: root/biblio/citation-style-language/citeproc-richtext.lua
diff options
context:
space:
mode:
Diffstat (limited to 'biblio/citation-style-language/citeproc-richtext.lua')
-rw-r--r--biblio/citation-style-language/citeproc-richtext.lua779
1 files changed, 0 insertions, 779 deletions
diff --git a/biblio/citation-style-language/citeproc-richtext.lua b/biblio/citation-style-language/citeproc-richtext.lua
deleted file mode 100644
index f9afd3acc6..0000000000
--- a/biblio/citation-style-language/citeproc-richtext.lua
+++ /dev/null
@@ -1,779 +0,0 @@
---
--- Copyright (c) 2021-2022 Zeping Lee
--- Released under the MIT license.
--- Repository: https://github.com/zepinglee/citeproc-lua
---
-
-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