summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/luaotfload/luaotfload-case.lua
diff options
context:
space:
mode:
Diffstat (limited to 'macros/luatex/generic/luaotfload/luaotfload-case.lua')
-rw-r--r--macros/luatex/generic/luaotfload/luaotfload-case.lua312
1 files changed, 265 insertions, 47 deletions
diff --git a/macros/luatex/generic/luaotfload/luaotfload-case.lua b/macros/luatex/generic/luaotfload/luaotfload-case.lua
index 33bb1bc924..f5a147334d 100644
--- a/macros/luatex/generic/luaotfload/luaotfload-case.lua
+++ b/macros/luatex/generic/luaotfload/luaotfload-case.lua
@@ -21,6 +21,7 @@ local setdisc = direct.setdisc
local getdisc = direct.getdisc
local getfield = direct.getfield
local remove = direct.remove
+local free = direct.free
local copy = direct.copy
local insert_after = direct.insert_after
local traverse = direct.traverse
@@ -34,41 +35,133 @@ local disc = node.id'disc'
-- * Final_Sigma is never language dependent
-- * Other contexts are always language dependent
-- * The only languages with special mappings are Lithuanian (lt/"LTH "/lit), Turkish (tr/"TRK "/tur), and Azeri/Azerbaijani (az/"AZE "/aze)
+-- (Additionally we add special mappings for de-x-eszett, el, el-x-iota, hy-x-yiwn which are not present in SpecialCasing.txt)
]]
-local font_lang = setmetatable({}, {__index = function(t, fid)
- local f = font.getfont(fid)
- local lang = f.specification.features.normal.language
- lang = lang == 'lth' and 'lt' or lang == 'trk' and 'tr' or lang == 'aze' and 'az' or false
- t[fid] = lang
- return lang
-end})
+local UPPER_MASK = 0x3FF
+local HAS_VOWEL = 0x200000
+local HAS_YPOGEGRAMMENI = 0x400000
+local HAS_ACCENT = 0x800000
+local HAS_DIALYTIKA = 0x1000000
+local HAS_OTHER_GREEK_DIACRITIC = 0x2000000
-local function is_Final_Sigma(font, mapping, n, after)
- mapping = mapping.Final_Sigma
- if not mapping then return false end
- mapping = mapping._
- if not mapping then return false end
+local greek_data
+local greek_diacritic = {
+ [0x0300] = HAS_ACCENT,
+ [0x0301] = HAS_ACCENT,
+ [0x0342] = HAS_ACCENT,
+ [0x0302] = HAS_ACCENT,
+ [0x0303] = HAS_ACCENT,
+ [0x0311] = HAS_ACCENT,
+ [0x0308] = HAS_DIALYTIKA,
+ [0x0344] = HAS_DIALYTIKA | HAS_ACCENT,
+ [0x0345] = HAS_YPOGEGRAMMENI,
+ [0x0304] = HAS_OTHER_GREEK_DIACRITIC,
+ [0x0306] = HAS_OTHER_GREEK_DIACRITIC,
+ [0x0313] = HAS_OTHER_GREEK_DIACRITIC,
+ [0x0314] = HAS_OTHER_GREEK_DIACRITIC,
+ [0x0343] = HAS_OTHER_GREEK_DIACRITIC,
+}
+
+local greek_precombined_iota = {
+ [0x0391] = 0x1FBC,
+ [0x0397] = 0x1FCC,
+ [0x03A9] = 0x1FFC,
+}
+
+-- Greek handling based on https://icu.unicode.org/design/case/greek-upper
+-- with smaller variations since we ant to preserve nodes whenever possible.
+local function init_greek_data()
+ local NFD = require'lua-uni-normalize'.NFD
+ local data = {}
+ greek_data = data
+
+ local vowels = {
+ [utf8.codepoint'Α'] = true, [utf8.codepoint'Ε'] = true,
+ [utf8.codepoint'Η'] = true, [utf8.codepoint'Ι'] = true,
+ [utf8.codepoint'Ο'] = true, [utf8.codepoint'Ω'] = true,
+ [utf8.codepoint'Υ'] = true,
+ }
+ local function handle_char(c)
+ local decomp = NFD(utf8.char(c))
+ local first = utf8.codepoint(decomp)
+ local upper = uppercase[first]
+ if upper then
+ if not tonumber(upper) then
+ upper = upper._
+ assert(#upper == 1)
+ upper = upper[1]
+ end
+ else
+ upper = first
+ end
+ if upper > UPPER_MASK then return end -- Only happens for unassigned codepoints
+ local datum = upper
+ if vowels[upper] then
+ datum = datum | HAS_VOWEL
+ end
+ if utf8.len(decomp) > 1 then
+ for _, c in utf8.codes(decomp) do
+ local dia = greek_diacritic[c]
+ if dia and dia ~= HAS_OTHER_GREEK_DIACRITIC then datum = datum | dia end
+ end
+ end
+ data[c] = datum
+ end
+ for c = 0x0370, 0x03ff do handle_char(c) end
+ for c = 0x1f00, 0x1fff do handle_char(c) end
+ for c = 0x2126, 0x2126 do handle_char(c) end
+end
+
+local function font_lang(feature)
+ return setmetatable({}, {__index = function(t, fid)
+ local f = font.getfont(fid)
+ local features = f.specification.features.normal
+ local lang = features[feature]
+ if type(lang) ~= 'string' or lang == 'auto' then
+ lang = features.language
+ lang = lang == 'lth' and 'lt'
+ or lang == 'trk' and 'tr'
+ or lang == 'aze' and 'az'
+ or lang == 'hye' and 'hy'
+ or (lang == 'ell' or lang == 'pgr') and 'el'
+ or false
+ end
+ t[fid] = lang
+ return lang
+ end})
+end
+
+local function is_followed_by_cased(font, n, after)
n = getnext(n)
repeat
while n do
local char, id = is_char(n, font)
- if id == disc then
+ if not char and id == disc then
after = getnext(n)
n = getfield(n, 'replace')
char, id = is_char(n, font)
- elseif char then
+ end
+ if char then
if not case_ignorable[char] then
- return not cased[char] and mapping
+ return cased[char]
end
n = getnext(n)
else
- return mapping
+ return false
end
end
n, after = after
until not n
- return mapping
+ return false
+end
+
+local function is_Final_Sigma(font, mapping, n, after)
+ mapping = mapping.Final_Sigma
+ if not mapping then return false end
+ mapping = mapping._
+ if not mapping then return false end
+ return not is_followed_by_cased(font, n, after) and mapping
end
local function is_More_Above(font, mapping, n, after)
@@ -150,56 +243,181 @@ local function is_Language_Mapping(font, mapping, n, after, seen_soft_dotted, se
return is_More_Above(font, mapping, n, after) or is_Not_Before_Dot(font, mapping, n, after) or mapping._ -- Might be nil
end
-local function process(table)
- local function processor(head, font, after, seen_cased, seen_soft_dotted, seen_I)
+local function process(table, feature)
+ local font_lang = font_lang(feature)
+ -- The other seen_... are booleans, while seen_greek has more states:
+ -- - nil: Not greek
+ -- - true: Greek. Last was not a vowel with accent and without dialytika
+ -- - node: Greek. Last vowel with accent and without dialytika
+ local function processor(head, font, after, seen_cased, seen_soft_dotted, seen_I, seen_greek)
local lang = font_lang[font]
+ local greek, greek_iota
+ if lang == 'el' or lang == 'el-x-iota' then
+ if table == uppercase then
+ if not greek_data then
+ init_greek_data()
+ end
+ greek, greek_iota = greek_data, lang == 'el-x-iota'
+ end
+ lang = false
+ end
local n = head
while n do
do
local new = has_glyph(n)
if n ~= new then
- seen_cased, seen_soft_dotted, seen_I = nil
+ seen_cased, seen_soft_dotted, seen_I, seen_greek = nil
end
n = new
end
if not n then break end
local char, id = is_char(n, font)
if char then
- local mapping = table[char]
- if mapping then
- if tonumber(mapping) then
- setchar(n, mapping)
+ if greek and (char >= 0x0370 and char <= 0x03ff or char >= 0x1f00 and char <= 0x1fff or char == 0x2126) then
+ -- In the greek uppercase situation we want to remove diacritics except under some exceptions.
+ local first_datum = greek[char] or 0
+ local datum = first_datum
+ local upper = datum & UPPER_MASK
+ -- When a vowel ges an accent removed and does not have a dialytika and is followed by a Ι or Υ,
+ -- then this iota or ypsilon gets a dialytika.
+ if datum & HAS_VOWEL ~= 0 and seen_greek and seen_greek ~= true and (upper == 0x0399 or upper == 0x03a5) then
+ datum = datum | HAS_DIALYTIKA;
+ end
+ local has_ypogegrammeni = datum & HAS_YPOGEGRAMMENI ~= 0
+ local add_ypogegrammeni = has_ypogegrammeni
+ local post = getnext(n)
+ local last
+ local saved_tonos, saved_dialytika
+ while post do
+ local char = is_char(post, font)
+ if not char then break end
+ local diacritic_data = greek_diacritic[char]
+ if not diacritic_data then break end
+ -- Preserve flags to be aware if a dialytika has to be reinserted
+ -- TODO: Keep dialytika node around
+ datum = datum | diacritic_data
+ -- Preserve ypogegrammeni (iota subscript) but convert them into capital iotas.
+ -- If el-x-iota is active keep the combining character instead.
+ if diacritic_data & HAS_YPOGEGRAMMENI ~= 0 then
+ has_ypogegrammeni = true
+ if not greek_iota then
+ setchar(post, 0x0399)
+ end
+ last = post
+ post = getnext(post)
+ else
+ -- Otherwise they get removed
+ local old = post
+ head, post = remove(head, post)
+ if char == 0x0301 and not saved_tonos then
+ -- But if we have a tonos we might want to reinsert it later
+ saved_tonos = old
+ elseif diacritic_data & HAS_DIALYTIKA ~= 0 and not saved_dialytika then
+ -- Similar for dilytika
+ saved_dialytika = old
+ else
+ free(old)
+ end
+ end
+ end
+ -- Special case: An isolated Ή preserves the tonos.
+ if upper == 0x0397
+ and not has_ypogegrammeni
+ and not seen_cased
+ and not is_followed_by_cased(font, n, after)
+ then
+ if first_datum & HAS_ACCENT ~= 0 then
+ upper = 0x0389
+ -- If it's precomposed we don't have to keep any combining accents
+ if saved_tonos then
+ free(saved_tonos)
+ saved_tonos = nil
+ end
+ end
else
- mapping = seen_cased and is_Final_Sigma(font, mapping, n, after)
- or lang and is_Language_Mapping(font, mapping[lang], n, after, seen_soft_dotted, seen_I)
- or mapping._
- if #mapping == 0 then
- head, n = remove(head, n)
- goto continue
+ -- Not the special case so we don't have to keep the tonos node
+ if saved_tonos then
+ free(saved_tonos)
+ saved_tonos = nil
+ end
+ -- Handle precomposed dialytika. If both a combining ans a precomposed
+ -- dialyika are present (typically because the precomposed one is
+ -- automatically added at the beginning) prefer the combining one to
+ -- preserve attributes.
+ if datum & HAS_DIALYTIKA ~= 0 and not saved_dialytika then
+ if upper == 0x0399 then -- upper == 'Ι'
+ upper = 0x03AA
+ elseif upper == 0x03A5 then -- upper == 'Υ'
+ upper = 0x03AB
+ else
+ assert(false) -- Should not be possible
+ end
+ end
+ end
+ if greek_iota and add_ypogegrammeni then
+ local mapped = greek_precombined_iota[upper]
+ if mapped then -- AFAICT always true
+ upper = mapped
+ add_ypogegrammeni = false
+ end
+ end
+ setchar(n, upper)
+ -- Potentially reinsert accents
+ if saved_dialytika then
+ head, n = insert_after(head, n, saved_dialytika)
+ setchar(n, 0x0308) -- Needed since we might have a U+0344 (COMBINING GREEK DIALYTIKA TONOS)
+ end
+ if saved_tonos then
+ head, n = insert_after(head, n, saved_tonos)
+ end
+ if add_ypogegrammeni then
+ head, n = insert_after(head, n, copy(n))
+ setchar(n, 0x0399)
+ end
+ -- If we preserved any combining ypogegrammeni nodes, skip them now
+ n = last or n
+ seen_greek = datum & (HAS_VOWEL | HAS_ACCENT | HAS_DIALYTIKA) == HAS_VOWEL | HAS_ACCENT and n or true
+ seen_I, seen_soft_dotted = nil
+ else
+ local mapping = table[char]
+ if mapping then
+ if tonumber(mapping) then
+ setchar(n, mapping)
else
- setchar(n, mapping[1])
- for i=2, #mapping do
- head, n = insert_after(head, n, copy(n))
- setchar(n, mapping[i])
+ mapping = seen_cased and is_Final_Sigma(font, mapping, n, after)
+ or lang and is_Language_Mapping(font, mapping[lang], n, after, seen_soft_dotted, seen_I)
+ or mapping._
+ if #mapping == 0 then
+ local old = n
+ head, n = remove(head, n)
+ free(old)
+ goto continue
+ else
+ setchar(n, mapping[1])
+ for i=2, #mapping do
+ head, n = insert_after(head, n, copy(n))
+ setchar(n, mapping[i])
+ end
end
end
end
+ local char_ccc = ccc[char]
+ if not char_ccc or char_ccc == 230 then
+ seen_I = char == 0x49 or nil
+ seen_soft_dotted = soft_dotted[char]
+ end
+ seen_greek = nil
end
if not case_ignorable[char] then
seen_cased = cased[char] or nil
end
- local char_ccc = ccc[char]
- if not char_ccc or char_ccc == 230 then
- seen_I = char == 0x49 or nil
- seen_soft_dotted = soft_dotted[char]
- end
elseif id == disc and uses_font(n, font) then
local pre, post, rep = getdisc(n)
local after = getnext(n)
- pre, post, rep, seen_cased, seen_soft_dotted, seen_I =
- processor(pre, font, nil, seen_cased, seen_soft_dotted, seen_I),
- processor(post, font, after),
- processor(rep, font, after, seen_cased, seen_soft_dotted, seen_I)
+ pre, post, rep, seen_cased, seen_soft_dotted, seen_I, seen_greek =
+ processor(pre, font, nil, seen_cased, seen_soft_dotted, seen_I, seen_greek),
+ processor(post, font, after, seen_greek),
+ processor(rep, font, after, seen_cased, seen_soft_dotted, seen_I, seen_greek)
setdisc(n, pre, post, rep)
else
seen_cased, seen_soft_dotted, seen_I = nil
@@ -207,12 +425,12 @@ local function process(table)
n = getnext(n)
::continue::
end
- return head, seen_cased, seen_soft_dotted, seen_I
+ return head, seen_cased, seen_soft_dotted, seen_I, seen_greek
end
- return function(head, font) return (processor(head, font)) end
+ return function(head, font, ...) return (processor(head, font)) end
end
-local upper_process = process(uppercase)
+local upper_process = process(uppercase, 'upper')
otfregister {
name = 'upper',
description = 'Map to uppercase',
@@ -225,7 +443,7 @@ otfregister {
},
}
-local lower_process = process(lowercase)
+local lower_process = process(lowercase, 'lower')
otfregister {
name = 'lower',
description = 'Map to lowercase',