#!/usr/bin/env texlua ------------------------------------------------------------------------------- -- FILE: luaotfload-configuration.lua -- DESCRIPTION: config file reader -- REQUIREMENTS: Luaotfload 2.7 or above -- AUTHOR: Philipp Gesang, -- AUTHOR: Dohyun Kim ------------------------------------------------------------------------------- -- if not modules then modules = { } end modules ["luaotfload-configuration"] = { version = "2.7", comment = "part of Luaotfload", author = "Philipp Gesang, Dohyun Kim", copyright = "Luaotfload Development Team", license = "GNU GPL v2.0" } local status_file = "luaotfload-status" local luaotfloadstatus = require (status_file) local string = string local stringfind = string.find local stringformat = string.format local stringstrip = string.strip local stringsub = string.sub local tableappend = table.append local tableconcat = table.concat local tablecopy = table.copy local table = table local tabletohash = table.tohash local math = math local mathfloor = math.floor local io = io local ioloaddata = io.loaddata local iopopen = io.popen local iowrite = io.write local os = os local osdate = os.date local osgetenv = os.getenv local lpeg = require "lpeg" local lpegmatch = lpeg.match local commasplitter = lpeg.splitat "," local equalssplitter = lpeg.splitat "=" local kpseexpand_path = kpse.expand_path local lfs = lfs local lfsisfile = lfs.isfile local lfsisdir = lfs.isdir local file = file local filejoin = file.join local filereplacesuffix = file.replacesuffix local logreport = print -- overloaded later local getwritablepath = caches.getwritablepath local config_parser -- set later during init local stripslashes -- set later during init ------------------------------------------------------------------------------- --- SETTINGS ------------------------------------------------------------------------------- local path_t = 0 local kpse_t = 1 local val_home = kpseexpand_path "~" local val_xdg_config_home = kpseexpand_path "$XDG_CONFIG_HOME" if val_xdg_config_home == "" then val_xdg_config_home = "~/.config" end local config_paths = { --- needs adapting for those other OS { path_t, "./luaotfload.conf" }, { path_t, "./luaotfloadrc" }, { path_t, filejoin (val_xdg_config_home, "luaotfload/luaotfload.conf") }, { path_t, filejoin (val_xdg_config_home, "luaotfload/luaotfloadrc") }, { path_t, filejoin (val_home, ".luaotfloadrc") }, { kpse_t, "luaotfloadrc" }, { kpse_t, "luaotfload.conf" }, } local valid_formats = tabletohash { "otf", "ttc", "ttf", "afm", "pfb" } local default_anon_sequence = { "tex", "path", "name" } local valid_resolvers = tabletohash { "tex", "path", "name", "file", "my" } local feature_presets = { arab = tabletohash { "ccmp", "locl", "isol", "fina", "fin2", "fin3", "medi", "med2", "init", "rlig", "calt", "liga", "cswh", "mset", "curs", "kern", "mark", "mkmk", }, deva = tabletohash { "ccmp", "locl", "init", "nukt", "akhn", "rphf", "blwf", "half", "pstf", "vatu", "pres", "blws", "abvs", "psts", "haln", "calt", "blwm", "abvm", "dist", "kern", "mark", "mkmk", }, khmr = tabletohash { "ccmp", "locl", "pref", "blwf", "abvf", "pstf", "pres", "blws", "abvs", "psts", "clig", "calt", "blwm", "abvm", "dist", "kern", "mark", "mkmk", }, thai = tabletohash { "ccmp", "locl", "liga", "kern", "mark", "mkmk", }, } --[[doc-- We allow loading of arbitrary fontloaders. Nevertheless we maintain a list of the “official” ones shipped with Luaotfload so we can emit a different log message. --doc]]-- local default_fontloader = function () return luaotfloadstatus and luaotfloadstatus.notes.loader or "reference" end local registered_loaders = { default = default_fontloader (), reference = "reference", unpackaged = "unpackaged", context = "context", } local pick_fontloader = function (s) local ldr = registered_loaders[s] if ldr ~= nil and type (ldr) == "string" then logreport ("log", 2, "conf", "Using predefined fontloader \"%s\".", ldr) return ldr end local idx = stringfind (s, ":") if idx and idx > 2 then if stringsub (s, 1, idx - 1) == "context" then local pth = stringsub (s, idx + 1) pth = stringstrip (pth) logreport ("log", 2, "conf", "Context base path specified at \"%s\".", pth) if lfsisdir (pth) then logreport ("log", 5, "conf", "Context base path exists at \"%s\".", pth) return pth end pth = kpseexpand_path (pth) if lfsisdir (pth) then logreport ("log", 5, "conf", "Context base path exists at \"%s\".", pth) return pth end logreport ("both", 0, "conf", "Context base path not found at \"%s\".", pth) end end return nil end --[[doc-- The ``post_linebreak_filter`` has been made the default callback for hooking the colorizer into. This helps with the linebreaking whose inserted hyphens would remain unaffected by the coloring otherwise. http://tex.stackexchange.com/q/238539/14066 --doc]]-- local permissible_color_callbacks = { default = "post_linebreak_filter", pre_linebreak_filter = "pre_linebreak_filter", post_linebreak_filter = "post_linebreak_filter", pre_output_filter = "pre_output_filter", } ------------------------------------------------------------------------------- --- DEFAULTS ------------------------------------------------------------------------------- local default_config = { db = { formats = "otf,ttf,ttc", scan_local = false, skip_read = false, strip = true, update_live = true, compress = true, max_fonts = 2^51, use_fontforge = false, }, run = { anon_sequence = default_anon_sequence, resolver = "cached", definer = "patch", log_level = 0, color_callback = "post_linebreak_filter", fontloader = default_fontloader (), }, misc = { bisect = false, version = luaotfload.version, statistics = false, termwidth = nil, }, paths = { names_dir = "names", cache_dir = "fonts", index_file = "luaotfload-names.lua", lookups_file = "luaotfload-lookup-cache.lua", lookup_path_lua = nil, lookup_path_luc = nil, index_path_lua = nil, index_path_luc = nil, }, default_features = { global = { mode = "node" }, dflt = tabletohash { "ccmp", "locl", "rlig", "liga", "clig", "kern", "mark", "mkmk", 'itlc', }, arab = feature_presets.arab, syrc = feature_presets.arab, mong = feature_presets.arab, nko = feature_presets.arab, deva = feature_presets.deva, beng = feature_presets.deva, guru = feature_presets.deva, gujr = feature_presets.deva, orya = feature_presets.deva, taml = feature_presets.deva, telu = feature_presets.deva, knda = feature_presets.deva, mlym = feature_presets.deva, sinh = feature_presets.deva, khmr = feature_presets.khmr, tibt = feature_presets.khmr, thai = feature_presets.thai, lao = feature_presets.thai, hang = tabletohash { "ccmp", "ljmo", "vjmo", "tjmo", }, }, } ------------------------------------------------------------------------------- --- RECONFIGURATION TASKS ------------------------------------------------------------------------------- --[[doc-- Procedures to be executed in order to put the new configuration into effect. --doc]]-- local reconf_tasks = nil local min_terminal_width = 40 --- The “termwidth” value is only considered when printing --- short status messages, e.g. when building the database --- online. local check_termwidth = function () if config.luaotfload.misc.termwidth == nil then local tw = 79 if not ( os.type == "windows" --- Assume broken terminal. or osgetenv "TERM" == "dumb") then local p = iopopen "tput cols" if p then result = tonumber (p:read "*all") p:close () if result then tw = result else logreport ("log", 2, "db", "tput returned non-number.") end else logreport ("log", 2, "db", "Shell escape disabled or tput executable missing.") logreport ("log", 2, "db", "Assuming 79 cols terminal width.") end end config.luaotfload.misc.termwidth = tw end return true end local set_font_filter = function () local names = fonts.names if names and names.set_font_filter then local formats = config.luaotfload.db.formats if not formats or formats == "" then formats = default_config.db.formats end names.set_font_filter (formats) end return true end local set_name_resolver = function () local names = fonts.names if names and names.resolve_cached then --- replace the resolver from luatex-fonts if config.luaotfload.db.resolver == "cached" then logreport ("both", 2, "cache", "Caching of name: lookups active.") names.resolvespec = fonts.names.lookup_font_name_cached else names.resolvespec = fonts.names.lookup_font_name end end return true end local set_loglevel = function () if luaotfload then luaotfload.log.set_loglevel (config.luaotfload.run.log_level) return true end return false end local build_cache_paths = function () local paths = config.luaotfload.paths local prefix = getwritablepath (paths.names_dir, "") if not prefix then luaotfload.error ("Impossible to find a suitable writeable cache...") return false end prefix = lpegmatch (stripslashes, prefix) logreport ("log", 0, "conf", "Root cache directory is %s.", prefix) local index_file = filejoin (prefix, paths.index_file) local lookups_file = filejoin (prefix, paths.lookups_file) paths.prefix = prefix paths.index_path_lua = filereplacesuffix (index_file, "lua") paths.index_path_luc = filereplacesuffix (index_file, "luc") paths.lookup_path_lua = filereplacesuffix (lookups_file, "lua") paths.lookup_path_luc = filereplacesuffix (lookups_file, "luc") return true end local set_default_features = function () local default_features = config.luaotfload.default_features luaotfload.features = luaotfload.features or { global = { }, defaults = { }, } current_features = luaotfload.features for var, val in next, default_features do if var == "global" then current_features.global = val else current_features.defaults[var] = val end end return true end local set_fontforge = function () local names = fonts.names if not names or not names.use_fontforge then --- happens normally on the first run logreport ("log", 4, "db", "Database not present.") return true end local use_ff = config.luaotfload.db.use_fontforge if use_ff == true then logreport ("both", 0, "db", "Fontforge loader was requested but not supported anymore.") end return true end reconf_tasks = { { "Set the log level" , set_loglevel }, { "Build cache paths" , build_cache_paths }, { "Check terminal dimensions" , check_termwidth }, { "Set the font filter" , set_font_filter }, { "Install font name resolver", set_name_resolver }, { "Set default features" , set_default_features }, { "Set fontforge" , set_fontforge }, } ------------------------------------------------------------------------------- --- OPTION SPECIFICATION ------------------------------------------------------------------------------- local string_t = "string" local table_t = "table" local number_t = "number" local boolean_t = "boolean" local function_t = "function" local tointeger = function (n) n = tonumber (n) if n then return mathfloor (n + 0.5) end end local toarray = function (s) local fields = { lpegmatch (commasplitter, s) } local ret = { } for i = 1, #fields do local field = stringstrip (fields[i]) if field and field ~= "" then ret[#ret + 1] = field end end return ret end local tohash = function (s) local result = { } local fields = toarray (s) for _, field in next, fields do local var, val if stringfind (field, "=") then local tmp var, tmp = lpegmatch (equalssplitter, field) if tmp == "true" or tmp == "yes" then val = true else val = tmp end else var, val = field, true end result[var] = val end return result end local option_spec = { db = { formats = { in_t = string_t, out_t = string_t, transform = function (f) local fields = toarray (f) --- check validity if not fields then logreport ("both", 0, "conf", "Expected list of identifiers, got %q.", f) return nil end --- strip dupes local known = { } local result = { } for i = 1, #fields do local field = fields[i] if known[field] ~= true then --- yet unknown, tag as seen known[field] = true --- include in output if valid if valid_formats[field] == true then result[#result + 1] = field else logreport ("both", 4, "conf", "Invalid font format identifier %q, ignoring.", field) end end end if #result == 0 then --- force defaults return nil end return tableconcat (result, ",") end }, scan_local = { in_t = boolean_t, }, skip_read = { in_t = boolean_t, }, strip = { in_t = boolean_t, }, update_live = { in_t = boolean_t, }, compress = { in_t = boolean_t, }, max_fonts = { in_t = number_t, out_t = number_t, --- TODO int_t from 5.3.x on transform = tointeger, }, use_fontforge = { in_t = boolean_t, }, }, run = { anon_sequence = { in_t = string_t, out_t = table_t, transform = function (s) if s ~= "default" then local bits = { lpegmatch (commasplitter, s) } if next (bits) then local seq = { } local done = { } for i = 1, #bits do local bit = bits [i] if valid_resolvers [bit] then if not done [bit] then done [bit] = true seq [#seq + 1] = bit else logreport ("both", 0, "conf", "ignoring duplicate resolver %s at position %d \z in lookup sequence", bit, i) end else logreport ("both", 0, "conf", "lookup sequence contains invalid item %s \z at position %d.", bit, i) end end if next (seq) then logreport ("both", 2, "conf", "overriding anon lookup sequence %s.", tableconcat (seq, ",")) return seq end end end return default_anon_sequence end }, live = { in_t = boolean_t, }, --- false for the tool, true for TeX run resolver = { in_t = string_t, out_t = string_t, transform = function (r) return r == "normal" and r or "cached" end, }, definer = { in_t = string_t, out_t = string_t, transform = function (d) if d == "generic" or d == "patch" or d == "info_generic" or d == "info_patch" then return d end return "patch" end, }, fontloader = { in_t = string_t, out_t = string_t, transform = function (id) local ldr = pick_fontloader (id) if ldr ~= nil then return ldr end logreport ("log", 0, "conf", "Requested fontloader \"%s\" not defined, " .. "use at your own risk.", id) return id end, }, log_level = { in_t = number_t, out_t = number_t, --- TODO int_t from 5.3.x on transform = tointeger, }, color_callback = { in_t = string_t, out_t = string_t, transform = function (cb_spec) --- These are the two that make sense. local cb = permissible_color_callbacks[cb_spec] if cb then logreport ("log", 3, "conf", "Using callback \"%s\" for font colorization.", cb) return cb end logreport ("log", 0, "conf", "Requested callback identifier \"%s\" invalid, " .. "falling back to default.", cb_spec) return permissible_color_callbacks.default end, }, }, misc = { bisect = { in_t = boolean_t, }, --- doesn’t make sense in a config file version = { in_t = string_t, }, statistics = { in_t = boolean_t, }, termwidth = { in_t = number_t, out_t = number_t, transform = function (w) w = tointeger (w) if w < min_terminal_width then return min_terminal_width end return w end, }, }, paths = { names_dir = { in_t = string_t, }, cache_dir = { in_t = string_t, }, index_file = { in_t = string_t, }, lookups_file = { in_t = string_t, }, lookup_path_lua = { in_t = string_t, }, lookup_path_luc = { in_t = string_t, }, index_path_lua = { in_t = string_t, }, index_path_luc = { in_t = string_t, }, }, default_features = { __default = { in_t = string_t, out_t = table_t, transform = tohash, }, }, } ------------------------------------------------------------------------------- --- FORMATTERS ------------------------------------------------------------------------------- local commented = function (str) return ";" .. str end local underscore_replacer = lpeg.replacer ("_", "-", true) local dashed = function (var) --- INI spec dictates that dashes are valid in variable names, not --- underscores. return underscore_replacer (var) or var end local indent = " " local format_string = function (var, val) return stringformat (indent .. "%s = %s", var, val) end local format_integer = function (var, val) return stringformat (indent .. "%s = %d", var, val) end local format_boolean = function (var, val) return stringformat (indent .. "%s = %s", var, val == true and "true" or "false") end local format_keyval = function (var, val) local list = { } local keys = table.sortedkeys (val) for i = 1, #keys do local key = keys[i] local subval = val[key] if subval == true then list[#list + 1] = stringformat ("%s", key) else list[#list + 1] = stringformat ("%s=%s", key, val[key]) end end if next (list) then return stringformat (indent .. "%s = %s", var, tableconcat (list, ",")) end end local format_list = function (var, val) local elts = { } for i = 1, #val do elts [i] = val [i] end if next (elts) then return stringformat (indent .. "%s = %s", var, tableconcat (elts, ",")) end end local format_section = function (title) return stringformat ("[%s]", dashed (title)) end local conf_header = [==[ ;;----------------------------------------------------------------------------- ;; Luaotfload Configuration ;;----------------------------------------------------------------------------- ;; ;; This file was generated by luaotfload-tool ;; on %s. Configuration variables ;; are documented in the manual to luaotfload.conf(5). ;; ;;----------------------------------------------------------------------------- ]==] local conf_footer = [==[ ;; vim:filetype=dosini:expandtab:shiftwidth=2 ]==] --- Each dumpable variable (the ones mentioned in the man page) receives a --- formatter that will be used in dumping the variable. Each value receives a --- “commented” flag that indicates whether or not the line should be printed --- as a comment. local formatters = { db = { compress = { false, format_boolean }, formats = { false, format_string }, max_fonts = { false, format_integer }, scan_local = { false, format_boolean }, skip_read = { false, format_boolean }, strip = { false, format_boolean }, update_live = { false, format_boolean }, use_fontforge = { false, format_boolean }, }, default_features = { __default = { true, format_keyval }, }, misc = { bisect = { false, format_boolean }, statistics = { false, format_boolean }, termwidth = { true , format_integer }, version = { true , format_string }, }, paths = { cache_dir = { false, format_string }, names_dir = { false, format_string }, index_file = { false, format_string }, lookups_file = { false, format_string }, }, run = { anon_sequence = { false, format_list }, color_callback = { false, format_string }, definer = { false, format_string }, fontloader = { false, format_string }, log_level = { false, format_integer }, resolver = { false, format_string }, }, } ------------------------------------------------------------------------------- --- MAIN FUNCTIONALITY ------------------------------------------------------------------------------- --[[doc-- tilde_expand -- Rudimentary tilde expansion; covers just the “substitute ‘~’ by the current users’s $HOME” part. --doc]]-- local tilde_expand = function (p) if #p > 2 then if stringsub (p, 1, 2) == "~/" then local homedir = osgetenv "HOME" if homedir and lfsisdir (homedir) then p = filejoin (homedir, stringsub (p, 3)) end end end return p end local resolve_config_path = function () for i = 1, #config_paths do local t, p = unpack (config_paths[i]) local fullname if t == kpse_t then fullname = kpse.lookup (p) logreport ("both", 6, "conf", "kpse lookup: %s -> %s.", p, fullname) elseif t == path_t then local expanded = tilde_expand (p) if lfsisfile (expanded) then fullname = expanded end logreport ("both", 6, "conf", "path lookup: %s -> %s.", p, fullname) end if fullname then logreport ("both", 3, "conf", "Reading configuration file at %q.", fullname) return fullname end end logreport ("both", 2, "conf", "No configuration file found.") return false end local add_config_paths = function (t) if not next (t) then return end local result = { } for i = 1, #t do local path = t[i] result[#result + 1] = { path_t, path } end config_paths = tableappend (result, config_paths) end local process_options = function (opts) local new = { } for i = 1, #opts do local section = opts[i] local title = section.section.title local vars = section.variables if not title then --- trigger warning: arrow code ahead logreport ("both", 2, "conf", "Section %d lacks a title; skipping.", i) elseif not vars then logreport ("both", 2, "conf", "Section %d (%s) lacks a variable section; skipping.", i, title) else local spec = option_spec[title] if not spec then logreport ("both", 2, "conf", "Section %d (%s) unknown; skipping.", i, title) else local newsection = new[title] if not newsection then newsection = { } new[title] = newsection end for var, val in next, vars do local vspec = spec[var] or spec.__default local t_val = type (val) if not vspec then logreport ("both", 2, "conf", "Section %d (%s): invalid configuration variable %q (%q); ignoring.", i, title, var, tostring (val)) elseif t_val ~= vspec.in_t then logreport ("both", 2, "conf", "Section %d (%s): type mismatch of input value %q (%q, %s != %s); ignoring.", i, title, var, tostring (val), t_val, vspec.in_t) else --- type matches local transform = vspec.transform if transform then local dval local t_transform = type (transform) if t_transform == function_t then dval = transform (val) elseif t_transform == table_t then dval = transform[val] end if dval then local out_t = vspec.out_t if out_t then local t_dval = type (dval) if t_dval == out_t then newsection[var] = dval else logreport ("both", 2, "conf", "Section %d (%s): type mismatch of derived value of %q (%q, %s != %s); ignoring.", i, title, var, tostring (dval), t_dval, out_t) end else newsection[var] = dval end else logreport ("both", 2, "conf", "Section %d (%s): value of %q could not be derived via %s from input %q; ignoring.", i, title, var, t_transform, tostring (val)) end else --- insert as is newsection[var] = val end end end end end end return new end local apply apply = function (old, new) if not new then if not old then return false end return tablecopy (old) elseif not old then return tablecopy (new) end local result = tablecopy (old) for name, section in next, new do local t_section = type (section) if t_section ~= table_t then logreport ("both", 1, "conf", "Error applying configuration: entry %s is %s, expected table.", section, t_section) --- ignore else local currentsection = result[name] for var, val in next, section do currentsection[var] = val end end end result.status = luaotfloadstatus return result end local reconfigure = function () for i = 1, #reconf_tasks do local name, task = unpack (reconf_tasks[i]) logreport ("both", 3, "conf", "Launch post-configuration task %q.", name) if not task () then logreport ("both", 0, "conf", "Post-configuration task %q failed.", name) return false end end return true end local read = function (extra) if extra then add_config_paths (extra) end local readme = resolve_config_path () if readme == false then logreport ("both", 2, "conf", "No configuration file.") return false end local raw = ioloaddata (readme) if not raw then logreport ("both", 2, "conf", "Error reading the configuration file %q.", readme) return false end local parsed = lpegmatch (config_parser, raw) if not parsed then logreport ("both", 2, "conf", "Error parsing configuration file %q.", readme) return false end local ret, msg = process_options (parsed) if not ret then logreport ("both", 2, "conf", "File %q is not a valid configuration file.", readme) logreport ("both", 2, "conf", "Error: %s", msg) return false end return ret end local apply_defaults = function () local defaults = default_config local vars = read () --- Side-effects galore ... config.luaotfload = apply (defaults, vars) return reconfigure () end local dump = function () local sections = table.sortedkeys (config.luaotfload) local confdata = { } for i = 1, #sections do local section = sections[i] local vars = config.luaotfload[section] local varnames = table.sortedkeys (vars) local sformats = formatters[section] if sformats then confdata[#confdata + 1] = format_section (section) for j = 1, #varnames do local var = varnames[j] local val = vars[var] local comment, sformat = unpack (sformats[var] or { }) if not sformat then comment, sformat = unpack (sformats.__default or { }) end if sformat then local dashedvar = dashed (var) if comment then confdata[#confdata + 1] = commented (sformat (dashedvar, val)) else confdata[#confdata + 1] = sformat (dashedvar, val) end end end confdata[#confdata + 1] = "" end end if next(confdata) then iowrite (stringformat (conf_header, osdate ("%Y-%m-d %H:%M:%S", os.time ()))) iowrite (tableconcat (confdata, "\n")) iowrite (conf_footer) end end ------------------------------------------------------------------------------- --- EXPORTS ------------------------------------------------------------------------------- return { init = function () config.luaotfload = { } logreport = luaotfload.log.report local parsers = luaotfload.parsers config_parser = parsers.config stripslashes = parsers.stripslashes luaotfload.default_config = default_config config.actions = { read = read, apply = apply, apply_defaults = apply_defaults, reconfigure = reconfigure, dump = dump, } if not apply_defaults () then logreport ("log", 0, "load", "Configuration unsuccessful: error loading default settings.") end return true end }