summaryrefslogtreecommitdiff
path: root/biblio/citation-style-language/citeproc-unicode.lua
diff options
context:
space:
mode:
Diffstat (limited to 'biblio/citation-style-language/citeproc-unicode.lua')
-rw-r--r--biblio/citation-style-language/citeproc-unicode.lua272
1 files changed, 272 insertions, 0 deletions
diff --git a/biblio/citation-style-language/citeproc-unicode.lua b/biblio/citation-style-language/citeproc-unicode.lua
new file mode 100644
index 0000000000..530206ae3f
--- /dev/null
+++ b/biblio/citation-style-language/citeproc-unicode.lua
@@ -0,0 +1,272 @@
+--
+-- Copyright (c) 2021-2023 Zeping Lee
+-- Released under the MIT license.
+-- Repository: https://github.com/zepinglee/citeproc-lua
+--
+
+local unicode = {}
+
+local sln = require("unicode")
+local uni_case = require("lua-uni-case")
+local uni_words = nil
+if kpse.find_file("lua-uni-words", "lua") then
+ uni_words = require("lua-uni-words")
+end
+
+local util = require("citeproc-util")
+
+
+---Return a copy of the string with its first character capitalized and the rest lowercased.
+---@param str string
+---@param locale string?
+---@return string
+function unicode.capitalize(str, locale)
+ if type(str) ~= "string" then
+ error(string.format("bad argument #1 to 'capitalize' (string expected, got %s)", type(str)))
+ end
+ -- TODO: locale and unicode titlecase Lt
+ str = unicode.lower(str, locale)
+ str = sln.grapheme.gsub(str, ".", sln.grapheme.upper, 1)
+ return str
+end
+
+
+---Return a casefolded copy of the string. Casefolded strings may be used for caseless matching.
+---@param str string
+---@param locale string?
+---@return string
+function unicode.casefold(str, locale)
+ if type(str) ~= "string" then
+ error(string.format("bad argument #1 to 'casefold' (string expected, got %s)", type(str)))
+ end
+ local enable_special = false
+ if locale and string.match(locale, "^tr") then
+ enable_special = true
+ end
+ return uni_case.casefold(str, true, enable_special)
+end
+
+
+---Return True if all characters in the string are alphanumeric and there is at least one character, False otherwise.
+---@param str string
+---@return boolean
+function unicode.isalnum(str)
+ if type(str) ~= "string" then
+ error(string.format("bad argument #1 to 'isalnum' (string expected, got %s)", type(str)))
+ end
+ return sln.grapheme.match(str, "^%w+$") ~= nil
+end
+
+
+---Return True if all characters in the string are alphabetic and there is at least one character, False otherwise.
+---@param str string
+---@return boolean
+function unicode.isalpha(str)
+ if type(str) ~= "string" then
+ error(string.format("bad argument #1 to 'isalpha' (string expected, got %s)", type(str)))
+ end
+ return sln.grapheme.match(str, "^%a+$") ~= nil
+end
+
+
+---Return True if the string is empty or all characters in the string are ASCII, False otherwise.
+---@param str string
+---@return boolean
+function unicode.isascii(str)
+ if type(str) ~= "string" then
+ error(string.format("bad argument #1 to 'isascii' (string expected, got %s)", type(str)))
+ end
+ return string.match(str, "^[\0-\x7F]+$") ~= nil
+end
+
+
+---Return True if all characters in the string are digits and there is at least one character, False otherwise.
+---@param str string
+---@return boolean
+function unicode.isdigit(str)
+ if type(str) ~= "string" then
+ error(string.format("bad argument #1 to 'isdigit' (string expected, got %s)", type(str)))
+ end
+ return string.match(str, "^%d+$") ~= nil
+end
+
+
+---Return True if all cased characters in the string are lowercase and there is at least one cased character, False otherwise.
+---@param str string
+---@return boolean
+function unicode.islower(str)
+ if type(str) ~= "string" then
+ error(string.format("bad argument #1 to 'islower' (string expected, got %s)", type(str)))
+ end
+ --TODO: No titlecased letters
+ return sln.grapheme.find(str, "%l") and not sln.grapheme.find(str, "%u")
+end
+
+
+---Return True if all characters in the string are numeric characters, and there is at least one character, False otherwise.
+---@param str string
+---@return boolean
+function unicode.isnumeric(str)
+ if type(str) ~= "string" then
+ error(string.format("bad argument #1 to 'isnumeric' (string expected, got %s)", type(str)))
+ end
+ return string.match(str, "^%n+$") ~= nil
+end
+
+
+-- ---Return True if the string is a titlecased string and there is at least one character, for example uppercase characters may only follow uncased characters and lowercase characters only cased ones. Return False otherwise.
+-- ---@param str string
+-- ---@return boolean
+-- function unicode.istitle(str)
+-- if type(str) ~= "string" then
+-- error(string.format("bad argument #1 to 'istitle' (string expected, got %s)", type(str)))
+-- end
+-- error("Not implemented")
+-- return false
+-- end
+
+
+---Return True if all cased characters in the string are uppercase and there is at least one cased character, False otherwise.
+---@param str string
+---@return boolean
+function unicode.isupper(str)
+ if type(str) ~= "string" then
+ error(string.format("bad argument #1 to 'isupper' (string expected, got %s)", type(str)))
+ end
+ --TODO: No titlecased letters
+ return sln.grapheme.find(str, "%u") and not sln.grapheme.find(str, "%l")
+end
+
+
+---Return a copy of the string with all the cased characters converted to lowercase.
+---@param str string
+---@param locale string?
+---@return string
+function unicode.lower(str, locale)
+ if type(str) ~= "string" then
+ error(string.format("bad argument #1 to 'lower' (string expected, got %s)", type(str)))
+ end
+ -- TODO: locale
+ return sln.grapheme.lower(str)
+end
+
+
+-- ---Return a titlecased version of the string where words start with an uppercase character and the remaining characters are lowercase.
+-- ---@param str string
+-- ---@param locale string?
+-- ---@return string
+-- function unicode.title(str, locale)
+-- if type(str) ~= "string" then
+-- error(string.format("bad argument #1 to 'title' (string expected, got %s)", type(str)))
+-- end
+-- error("Not implemented")
+-- end
+
+
+---Return a copy of the string with all the cased characters converted to uppercase.
+---@param str string
+---@param locale string?
+---@return string
+function unicode.upper(str, locale)
+ if type(str) ~= "string" then
+ error(string.format("bad argument #1 to 'upper' (string expected, got %s)", type(str)))
+ end
+ -- TODO: locale
+ return sln.grapheme.lower(str)
+end
+
+
+---@enum CharState
+local CharState = {
+ Other = 0,
+ Word = 1,
+ Punctuation = 2,
+ Space = 3,
+}
+
+
+---@param str string
+---@return string[]
+function unicode.words(str)
+ local words = {}
+ if uni_words then
+ for _, _, segment in uni_words.word_boundaries(str) do
+ if unicode.isalnum(segment) then
+ table.insert(words, segment)
+ end
+ end
+
+ else
+ -- A naive implementation
+ local state = CharState.Other
+ local last_idx = 1
+ for idx, char in sln.grapheme.gmatch(str, "()(.)") do
+ local new_state = CharState.Other
+ if sln.grapheme.match(char, "%w") or char == "'" or char == "’" or char == "`" then
+ new_state = CharState.Word
+ else
+ new_state = CharState.Other
+ end
+ if new_state ~= state then
+ if idx > 1 and state == CharState.Word then
+ local segment = string.sub(str, last_idx, idx - 1)
+ table.insert(words, segment)
+ end
+ state = new_state
+ last_idx = idx
+ end
+ end
+
+ if state == CharState.Word then
+ local segment = string.sub(str, last_idx, #str)
+ table.insert(words, segment)
+ end
+ end
+
+ return words
+
+end
+
+
+---@param str string
+---@return string[]
+function unicode.split_word_bounds(str)
+ local segments = {}
+ if uni_words then
+ for _, _, segment in uni_words.word_boundaries(str) do
+ table.insert(segments, segment)
+ end
+
+ else
+ -- A naive implementation
+ local state = CharState.Other
+ local last_idx = 1
+ for idx, char in sln.grapheme.gmatch(str, "()(.)") do
+ local new_state = CharState.Other
+ if sln.grapheme.match(char, "%w") or char == "'" or char == "’" or char == "`" then
+ new_state = CharState.Word
+ elseif sln.grapheme.match(char, "%p") then
+ new_state = CharState.Punctuation
+ elseif sln.grapheme.match(char, "%s") then
+ new_state = CharState.Space
+ else
+ new_state = CharState.Other
+ end
+ if new_state ~= state then
+ if idx > 1 then
+ local segment = string.sub(str, last_idx, idx - 1)
+ table.insert(segments, segment)
+ end
+ state = new_state
+ last_idx = idx
+ end
+ end
+
+ local segment = string.sub(str, last_idx, #str)
+ table.insert(segments, segment)
+ end
+
+ return segments
+end
+
+return unicode