From 3995e1498745055a2b9e71a6c2b027e66fe7fc9e Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Sat, 3 Feb 2024 18:24:48 +0300 Subject: [PATCH 01/20] feat(tooling): Add tooling to generate Lua API documentation from sources --- Makefile.am | 10 ++++++++-- build-aux/config.ld | 11 +++++++++++ configure.ac | 1 + 3 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 build-aux/config.ld diff --git a/Makefile.am b/Makefile.am index 44377e867..f4df7e413 100644 --- a/Makefile.am +++ b/Makefile.am @@ -70,7 +70,7 @@ dist_pdf_DATA = $(_MANUAL) dist_license_DATA = LICENSE.md EXTRA_DIST = spec tests documentation sile-dev-1.rockspec fontconfig.conf EXTRA_DIST += Makefile-distfiles -EXTRA_DIST += build-aux/action-updater.js build-aux/cargo-updater.js build-aux/decore-automake.sh build-aux/git-version-gen build-aux/list-dist-files.sh +EXTRA_DIST += build-aux/action-updater.js build-aux/cargo-updater.js build-aux/config.ld build-aux/decore-automake.sh build-aux/git-version-gen build-aux/list-dist-files.sh EXTRA_DIST += Dockerfile build-aux/docker-bootstrap.sh build-aux/docker-fontconfig.conf hooks/build EXTRA_DIST += default.nix flake.nix flake.lock shell.nix build-aux/pkg.nix EXTRA_DIST += package.json # imported by both Nix and Docker @@ -173,7 +173,13 @@ selfcheck: | $(bin_PROGRAMS) $(_BUILT_SUBDIRS) $(PDFINFO) $$output | $(GREP) "SILE v$(VERSION)" .PHONY: docs -docs: $(_MANUAL) +docs: $(_MANUAL) lua-api-docs + +.PHONY: lua-api-docs +lua-api-docs: lua-api-docs/index.html + +lua-api-docs/index.html: build-aux/config.ld + $(LDOC) -c build-aux/config.ld . .PHONY: docs-figures docs-figures: $(FIGURES) diff --git a/build-aux/config.ld b/build-aux/config.ld new file mode 100644 index 000000000..bce1eb056 --- /dev/null +++ b/build-aux/config.ld @@ -0,0 +1,11 @@ +project = "SILE" +description = "The SILE Typesetter" +dir = "../lua-api-docs" +format = "discount" +file = { + "../sile-lua", + "../core", + "../typesetters", +} +multimodule = true +merge = true diff --git a/configure.ac b/configure.ac index 5d221d056..e61906f5d 100644 --- a/configure.ac +++ b/configure.ac @@ -220,6 +220,7 @@ AM_COND_IF([DEPENDENCY_CHECKS], [ AX_PROGVAR([git]) AX_PROGVAR([grep]) AX_PROGVAR([head]) + AX_PROGVAR([ldoc]) AX_PROGVAR([luacheck]) AX_PROGVAR([luarocks]) AX_PROGVAR([nix]) From c0507d205f90bd018943f632943a7fceabe57b54 Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Thu, 8 Feb 2024 18:45:54 +0300 Subject: [PATCH 02/20] chore(tooling): Add commit scope for API docs --- .commitlintrc.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.commitlintrc.yml b/.commitlintrc.yml index 12a25a8fb..8285b630e 100644 --- a/.commitlintrc.yml +++ b/.commitlintrc.yml @@ -39,6 +39,7 @@ rules: - inputters - installation - languages + - api - manpage - manual - math From cf7e26a6a74e31d373a03704983e580e3fcf482f Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Thu, 8 Feb 2024 18:45:15 +0300 Subject: [PATCH 03/20] docs(api): Start organizing modules and documenting interfaces --- core/settings.lua | 27 ++++++++++++++++++++++++--- core/sile.lua | 13 +++++++++++++ core/utilities/ast.lua | 4 +++- core/utilities/init.lua | 11 +++++++++++ core/utilities/numbers.lua | 9 +++++---- typesetters/base.lua | 14 +++++++------- 6 files changed, 63 insertions(+), 15 deletions(-) diff --git a/core/settings.lua b/core/settings.lua index c63d5f411..0d9e03df6 100644 --- a/core/settings.lua +++ b/core/settings.lua @@ -1,7 +1,11 @@ +--- core settings instance +--- @module SILE.settings + local deprecator = function () SU.deprecated("SILE.settings.*", "SILE.settings:*", "0.13.0", "0.15.0") end +--- @type settings local settings = pl.class() function settings:_init() @@ -97,17 +101,21 @@ function settings:_init() end +--- Stash the current values of all settings in a stack to be returned to later function settings:pushState () if not self then return deprecator() end table.insert(self.stateQueue, self.state) self.state = pl.tablex.copy(self.state) end +--- Return the most recently pushed set of values in the setting stack function settings:popState () if not self then return deprecator() end self.state = table.remove(self.stateQueue) end +--- Declare a new setting +--- @param specs table: { parameter, ... } declaration specification function settings:declare (spec) if not spec then return deprecator() end if spec.name then @@ -130,8 +138,8 @@ function settings:reset () end --- Restore all settings to the value they had in the top-level state, --- that is at the head of the settings stack (normally the document --- level). +--- that is at the head of the settings stack (normally the document +--- level). function settings:toplevelState () if not self then return deprecator() end if #self.stateQueue ~= 0 then @@ -144,6 +152,8 @@ function settings:toplevelState () end end +--- Get the value of a setting +--- @param parameter The full name of the setting to fetch. function settings:get (parameter) -- HACK FIXME https://github.com/sile-typesetter/sile/issues/1699 -- See comment on set() below. @@ -161,6 +171,11 @@ function settings:get (parameter) end end +--- Set the value of a setting +--- @param parameter The full name of the setting to change. +--- @param value The new value to change it to. +--- @param makedefault boolean Whether to make this the new default value (default false). +--- @param reset Whether to reset the value to the current default value (default false). function settings:set (parameter, value, makedefault, reset) -- HACK FIXME https://github.com/sile-typesetter/sile/issues/1699 -- Anything dubbed current.xxx should likely NOT be a "setting" (subject @@ -204,6 +219,9 @@ function settings:set (parameter, value, makedefault, reset) end end +--- Isolate a block of processing so that setting changes made during the block don't last past the block. +--- (Under the hood this just uses `:pushState()`, the processes the function, then runs `:popState()`) +--- @param function A function wrapping the actions to take without affecting settings for future use. function settings:temporarily (func) if not func then return deprecator() end self:pushState() @@ -211,7 +229,10 @@ function settings:temporarily (func) self:popState() end -function settings:wrap () -- Returns a closure which applies the current state, later +--- Create a settings wrapper function that applies current settings to later content processing. +--- @return a closure fuction accepting one argument (content) to process using +--- typesetter settings as they are at the time of closure creation. +function settings:wrap () if not self then return deprecator() end local clSettings = pl.tablex.copy(self.state) return function(content) diff --git a/core/sile.lua b/core/sile.lua index 698a43340..d7a17eda9 100644 --- a/core/sile.lua +++ b/core/sile.lua @@ -1,7 +1,19 @@ +--- The core SILE library +--- @module SILE + -- Initialize SILE internals SILE = {} +--- Current version of SILE, prefixed with v SILE.version = require("core.version") + +--- Status information about what options SILE was compiled with +--- @field appkit boolean +--- @field font_variations boolean +--- @field fontconfig boolean +--- @field harfbuzz boolean +--- @field icu boolean +--- @table SILE.features SILE.features = require("core.features") -- Initialize Lua environment and global utilities @@ -113,6 +125,7 @@ local function runEvals (evals, arg) end end +--- initialize sile SILE.init = function () if not SILE.backend then SILE.backend = "libtexpdf" diff --git a/core/utilities/ast.lua b/core/utilities/ast.lua index b1c4e6d25..a2f0c9bb2 100644 --- a/core/utilities/ast.lua +++ b/core/utilities/ast.lua @@ -1,5 +1,7 @@ --- SILE AST utilities --- +--- @module SU.ast + +--- @type ast local ast = {} --- Find a command node in a SILE AST tree, diff --git a/core/utilities/init.lua b/core/utilities/init.lua index 73ab0e299..bf8769648 100644 --- a/core/utilities/init.lua +++ b/core/utilities/init.lua @@ -1,11 +1,22 @@ +--- SILE.Utilities (aliased as SU) +--- @module SU +--- alias SU.utilities + local bitshim = require("bitshim") local luautf8 = require("lua-utf8") local semver = require("semver") +--- @type utilities local utilities = {} local epsilon = 1E-12 +--- Require that an option table contains a specific value, otherwise raise an error. +--- @param options Input table of options. +--- @param name Name of the required option. +--- @param context User friendly name of the function or calling context. +--- @param required_type The name of a data type that the option must sucessfully cast to. +-- SU.required utilities.required = function (options, name, context, required_type) if not options[name] then utilities.error(context.." needs a "..name.." parameter") end if required_type then diff --git a/core/utilities/numbers.lua b/core/utilities/numbers.lua index 6bb6dd024..b53cb2541 100644 --- a/core/utilities/numbers.lua +++ b/core/utilities/numbers.lua @@ -1,9 +1,10 @@ --- --- Number formatting utilities --- MIT License (c) 2022 SILE organization --- +--- Number formatting utilities +--- @submodule SU +--- submodule SU + local icu = require("justenoughicu") +--- @type formatNumber -- Language-specific number formatters add functions to this table, -- see e.g. languages/eo.lua local formatNumber = { diff --git a/typesetters/base.lua b/typesetters/base.lua index c92cb3116..ba2b7019c 100644 --- a/typesetters/base.lua +++ b/typesetters/base.lua @@ -1,12 +1,9 @@ ---- SILE typesetter (default/base) class. --- --- @copyright License: MIT --- @module typesetters.base --- - --- Typesetter base class +--- SILE typesetter base class. +--- @module SILE.typesetters.base +--- @type typesetter local typesetter = pl.class() + typesetter.type = "typesetter" typesetter._name = "base" @@ -46,6 +43,8 @@ function typesetter:init (frame) self:_init(frame) end +--- Constructor +-- @param frame A initial frame to attach the typesetter to. function typesetter:_init (frame) self:declareSettings() self.hooks = {} @@ -60,6 +59,7 @@ function typesetter:_init (frame) return self end +--- Declare new setting types function typesetter.declareSettings(_) -- Settings common to any typesetter instance. From 47fd2538867428c1d6421665c9c8b07398f7e046 Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Sat, 10 Feb 2024 15:35:19 +0300 Subject: [PATCH 04/20] docs(api): Add doc-strings for some main SILE instance fields --- core/sile.lua | 179 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 163 insertions(+), 16 deletions(-) diff --git a/core/sile.lua b/core/sile.lua index d7a17eda9..4ea859855 100644 --- a/core/sile.lua +++ b/core/sile.lua @@ -1,25 +1,42 @@ --- The core SILE library ---- @module SILE +-- @module SILE --- Initialize SILE internals +-- Placeholder for SILE internals SILE = {} ---- Current version of SILE, prefixed with v +--- Fields +-- @section fields + +--- Machine friendly short-form version. +-- Semver, prefixed with "v", possible postfixed with ".r" followed by VCS version information. +-- @string version SILE.version = require("core.version") ---- Status information about what options SILE was compiled with ---- @field appkit boolean ---- @field font_variations boolean ---- @field fontconfig boolean ---- @field harfbuzz boolean ---- @field icu boolean ---- @table SILE.features +--- Status information about what options SILE was compiled with. +-- @table SILE.features +-- @tfield boolean appkit +-- @tfield boolean font_variations +-- @tfield boolean fontconfig +-- @tfield boolean harfbuzz +-- @tfield boolean icu SILE.features = require("core.features") -- Initialize Lua environment and global utilities + +--- ABI version of Lua VM. +-- For example may be `"5.1"` or `"5.4"` or others. Note that the ABI version for most LuaJIT implementations is 5.1. +-- @string lua_version SILE.lua_version = _VERSION:sub(-3) + + +--- Whether or not Lua VM is a JIT compiler. +-- @boolean lua_isjit -- luacheck: ignore jit SILE.lua_isjit = type(jit) == "table" + +--- User friendly long-form version string. +-- For example may be "SILE v0.14.17 (Lua 5.4)". +-- @string full_version SILE.full_version = string.format("SILE %s (%s)", SILE.version, SILE.lua_isjit and jit.version or _VERSION) -- Backport of lots of Lua 5.3 features to Lua 5.[12] @@ -45,6 +62,10 @@ local lfs = require("lfs") -- Developer tooling profiler local ProFi +--- Modules +-- @section modules + +--- Utilities module, aliased as `SU`. SILE.utilities = require("core.utilities") SU = SILE.utilities -- regrettable global alias @@ -61,19 +82,70 @@ local core_loader = function (scope) }) end +--- Data tables +--- @section data + +--- Stash of all Lua functions used to power typesetter commands. +-- @table Commands SILE.Commands = {} + +--- Short usage messages corresponding to typesetter commands. +-- @table Help SILE.Help = {} + +--- List of currently enabled debug flags. +-- E.g. `{ typesetter = true, frames, true }`. +-- @table debugFlags SILE.debugFlags = {} + SILE.nodeMakers = {} SILE.tokenizers = {} SILE.status = {} + +--- The wild-west of stash stuff. +-- No rules, just right (or usually wrong). Everything in here *should* be somewhere else, but lots of early SILE code +-- relied on this as a global key/value store for various class, document, and package values. Since v0.14.0 with many +-- core SILE components being instances of classes –and especially with each package having it's own variable namespace– +-- there are almost always better places for things. This scratch space will eventually be completely deprecated, so +-- don't put anything new in here and only use things in it if there are no other current alternatives. +-- @table scratch SILE.scratch = {} + +--- Data storage for typesetter, frame, and class information. +-- Current document class instances, node queues, and other "hot" data can be found here. As with `SILE.scratch` +-- everything in here probably belongs elsewhere, but for now it is what it is. +-- @table documentState +-- @tfield table documentClass The instantiated document processing class. +-- @tfield table thisPageTemplate The frameset used for the current page. +-- @tfield table paperSize The current paper size. +-- @tfield table orgPaperSize The original paper size if the current one is modified via packages. SILE.documentState = {} + +--- Callback functions for handling types of raw content. +-- All registered handlers for raw content blocks have an entry in this table with the type as the key and the +-- processing function as the value. +-- @ table rawHandlers SILE.rawHandlers = {} +--- User input +-- @section input + +--- All user-provided input collected before beginning document processing. -- User input values, currently from CLI options, potentially all the inuts -- needed for a user to use a SILE-as-a-library version to produce documents -- programmatically. +-- @table input +-- @tfield table filenames Path names of file(s) intended for processing. Files are processed in the order provided. +-- File types may be mixed of any formaat for which SILE has an inputter module. +-- @tfield table evaluates List of strings to be evaluated as Lua code snippets *before* processing inputs. +-- @tfield table evaluteAfters List of strings to be evaluated as Lua code snippets *after* processing inputs. +-- @tfield table uses List of strings specifying module names (and optionally optionns) for modules to load *before* +-- processing inputs. For example this accomodates loading inputter modules before any input of that type is encountered. +-- Additionally it can be used to process a document using a document class *other than* the one specified in the +-- document itself. Document class modules loaded here are instantiated after load, meaning the document will not be +-- queried for a class at all. +-- @tfield table options Extra document class options to set or override in addition to ones found in the first input +-- document. SILE.input = { filenames = {}, evaluates = {}, @@ -125,7 +197,20 @@ local function runEvals (evals, arg) end end ---- initialize sile +--- Core functions +-- @section functions + +--- Initialize a SILE instance. +-- Presumes CLI args have already been processed and/or library inputs are set. +-- +-- 1. If no backend has been loaded already (e.g. via `--use`) then assumes *libtexpdf*. +-- 2. Loads and instantiates a shaper and outputter module appropriate for the chosen backend. +-- 3. Instantiates a pagebuilder. +-- 4. Starts a Lua profiler if the profile debug flag is set. +-- 5. Instantiates a dependency tracker if we've been asked to write make dependencies. +-- 6. Runs any code snippents passed with `--eval`. +-- +-- Does not move on to processing input document(s). SILE.init = function () if not SILE.backend then SILE.backend = "libtexpdf" @@ -187,6 +272,13 @@ local function suggest_luarocks (module) ) end +--- Multi-purpose loader to load and initialize modules. +-- This is used to load and intitialize core parts of SILE and also 3rd party modules. +-- Module types supported bay be an *inputter*, *outputer*, *shaper*, *typesetter*, *pagebuilder*, or *package*. +-- @tparam string|table module The module spec name to load (dot-separated, e.g. `"packages.lorem"`) or a table with +-- a module that has already been loaded. +-- @tparam[opt] table options Startup options as key/value pairs passed to the module when initialized. +-- @tparam[opt=false] boolean reload whether or not to reload a module that has been loaded and initialized before. SILE.use = function (module, options, reload) local status, pack if type(module) == "string" then @@ -233,6 +325,9 @@ SILE.use = function (module, options, reload) end end +-- --- Content loader like Lua's `require()` but whith special path handling for loading SILE resource files. +-- -- Used for example by commands that load data via a `src=file.name` option. +-- -- @tparam string dependency Lua spec SILE.require = function (dependency, pathprefix, deprecation_ack) if pathprefix and not deprecation_ack then local notice = string.format([[ @@ -276,6 +371,10 @@ SILE.require = function (dependency, pathprefix, deprecation_ack) return lib end +--- Process content. +-- This is the main 'action' SILE does. Once input files are parsed into an abstract syntax tree, then we recursively +-- iterate through the tree handling each item in the order encountered. +-- @tparam table ast SILE content in abstract syntax tree format (a table of strings, functions, or more AST trees). SILE.process = function (ast) if not ast then return end if SU.debugging("ast") then @@ -328,6 +427,12 @@ local function detectFormat (doc, filename) SU.error(("Unable to pick inputter to process input from '%s'"):format(filename)) end +--- Process an input string. +-- First converts the string to an AST, then runs `process` on it. +-- @tparam string doc Input string to be coverted to SILE content. +-- @tparam[opt] nil|string format The name of the formatter. If nil, defaults to using each intputter's auto detection. +-- @tparam[opt] nil|string filename Pseudo filename to identify the content with, useful for error messages stack traces. +-- @tparam[opt] nil|table options Options to pass to the inputter instance when instantiated. function SILE.processString (doc, format, filename, options) local cpf if not filename then @@ -359,6 +464,12 @@ function SILE.processString (doc, format, filename, options) if cpf then SILE.currentlyProcessingFile = cpf end end +--- Process an input file +-- Opens a file, converts the contents to an AST, then runs `process` on it. +-- Roughly equivalent to listing the file as an input, but easier to embed in code. +-- @tparam string filename Path of file to open string to be coverted to SILE content. +-- @tparam[opt] nil|string format The name of the formatter. If nil, defaults to using each intputter's auto detection. +-- @tparam[opt] nil|table options Options to pass to the inputter instance when instantiated. function SILE.processFile (filename, format, options) local doc if filename == "-" then @@ -415,7 +526,10 @@ SILE.typesetNaturally = function (frame, func) if SILE.typesetter.frame then SILE.typesetter.frame:enter(SILE.typesetter) end end --- Sort through possible places files could be +--- Resolve relative file paths to identify absolute resources locations. +-- Makes it possible to load resources from relative paths, relative to a document or project or SILE itself. +-- @tparam string filename Name of file to find using the same order of precidence logic in `require()`. +-- @tparam[opt] nil|string pathprefix Optional prefix in which to look for if the file isn't found otherwise. function SILE.resolveFile (filename, pathprefix) local candidates = {} -- Start with the raw file name as given prefixed with a path if requested @@ -443,6 +557,12 @@ function SILE.resolveFile (filename, pathprefix) return resolved end +--- Execute a registered SILE command. +-- Uses a function previously registered by any modules explicitly loaded by the user at runtime via `--use`, the SILE +-- core, the document class, or any loaded package. +-- @tparam string command Command name. +-- @tparam[opt={}] nil|table options Options to pass to the command. +-- @tparam[opt] nil|table content Any valid AST node to be processed by the command. function SILE.call (command, options, content) options = options or {} content = content or {} @@ -459,6 +579,17 @@ function SILE.call (command, options, content) return result end +--- (Deprecated) Register a function as a SILE command. +-- Takes any Lua function and registers it for use as a SILE command (which will in turn be used to process any content +-- nodes identified with the command name. +-- +-- Note that alternative versions of this action are available as methods on document classes and packages. Those +-- interfaces should be prefered to this global one. +-- @tparam string name Name of cammand to register. +-- @tparam function func Callback function to use as command handler. +-- @tparam[opt] nil|string help User friendly short usage string for use in error messages, documentation, etc. +-- @tparam[opt] nil|string pack Information identifying the module registering the command for use in error and usage +-- messages. Usually auto-detected. function SILE.registerCommand (name, func, help, pack, cheat) local class = SILE.documentState.documentClass if not cheat then @@ -472,16 +603,22 @@ function SILE.registerCommand (name, func, help, pack, cheat) return class:registerCommand(name, func, help, pack) end +--- Wrap an existing command with new default options. +-- Modifies an already registered SILE command with a new table of options to be used as default values any time it is +-- called. Calling options still take precidence. +-- @tparam string command Name of command to overwride. +-- @tparam table options Options to set as updated defaults. function SILE.setCommandDefaults (command, defaults) local oldCommand = SILE.Commands[command] - SILE.Commands[command] = function (options, content) - for k, v in pairs(defaults) do - options[k] = options[k] or v + SILE.Commands[command] = function (defaults, content) + for k, v in pairs(options) do + defaults[k] = defaults[k] or v end - return oldCommand(options, content) + return oldCommand(defaults, content) end end +-- TODO: Move to new table entry handler in types.unit function SILE.registerUnit (unit, spec) -- If a unit exists already, clear it first so we get fresh meta table entries, see #1607 if SILE.units[unit] then @@ -495,6 +632,16 @@ function SILE.paperSizeParser (size) return SILE.papersize(size) end +--- Finalize document processing +-- Signals that all the `SILE.process()` calls have been made and SILE should move on to finish up the output +-- +-- 1. Stops logging dependecies and writes them to a makedepends file if requested. +-- 2. Tells the document class to run its `:finish()` method. This method is typically responsible for calling the +-- `:finish()` method of the outputter module in the appropriate sequence. +-- 3. Closes out anything in active memory we don't need like font instances. +-- 4. Evaluate any snippets in SILE.input.evalAfter table. +-- 5. Close out the Lua profiler if it was running. +-- 6. Output version information if versions debug flag is set. function SILE.finish () if SILE.makeDeps then SILE.makeDeps:write() From c0d28b366786689b72977aabe6677aaeefadcd0d Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Sat, 10 Feb 2024 15:35:19 +0300 Subject: [PATCH 05/20] style(core): Normalize Lua coding style for function definitions --- core/sile.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/sile.lua b/core/sile.lua index 4ea859855..3c3d2f2f6 100644 --- a/core/sile.lua +++ b/core/sile.lua @@ -71,7 +71,7 @@ SU = SILE.utilities -- regrettable global alias -- On demand loader, allows modules to be loaded into a specific scope but -- only when/if accessed. -local core_loader = function (scope) +local function core_loader (scope) return setmetatable({}, { __index = function (self, key) -- local var = rawget(self, key) @@ -211,7 +211,7 @@ end -- 6. Runs any code snippents passed with `--eval`. -- -- Does not move on to processing input document(s). -SILE.init = function () +function SILE.init () if not SILE.backend then SILE.backend = "libtexpdf" end @@ -279,7 +279,7 @@ end -- a module that has already been loaded. -- @tparam[opt] table options Startup options as key/value pairs passed to the module when initialized. -- @tparam[opt=false] boolean reload whether or not to reload a module that has been loaded and initialized before. -SILE.use = function (module, options, reload) +function SILE.use (module, options, reload) local status, pack if type(module) == "string" then status, pack = pcall(require, module) @@ -328,7 +328,7 @@ end -- --- Content loader like Lua's `require()` but whith special path handling for loading SILE resource files. -- -- Used for example by commands that load data via a `src=file.name` option. -- -- @tparam string dependency Lua spec -SILE.require = function (dependency, pathprefix, deprecation_ack) +local SILE.require (dependency, pathprefix, deprecation_ack) if pathprefix and not deprecation_ack then local notice = string.format([[ Please don't use the path prefix mechanism; it was intended to provide @@ -375,7 +375,7 @@ end -- This is the main 'action' SILE does. Once input files are parsed into an abstract syntax tree, then we recursively -- iterate through the tree handling each item in the order encountered. -- @tparam table ast SILE content in abstract syntax tree format (a table of strings, functions, or more AST trees). -SILE.process = function (ast) +function SILE.process (ast) if not ast then return end if SU.debugging("ast") then SU.debugAST(ast, 0) @@ -514,7 +514,7 @@ end -- TODO: this probably needs deprecating, moved here just to get out of the way so -- typesetters classing works as expected -SILE.typesetNaturally = function (frame, func) +function SILE.typesetNaturally (frame, func) local saveTypesetter = SILE.typesetter if SILE.typesetter.frame then SILE.typesetter.frame:leave(SILE.typesetter) end SILE.typesetter = SILE.typesetters.base(frame) @@ -608,7 +608,7 @@ end -- called. Calling options still take precidence. -- @tparam string command Name of command to overwride. -- @tparam table options Options to set as updated defaults. -function SILE.setCommandDefaults (command, defaults) +function SILE.setCommandDefaults (command, options) local oldCommand = SILE.Commands[command] SILE.Commands[command] = function (defaults, content) for k, v in pairs(options) do From f59670c95f31cb404ce6bfcd99e4ab750f5387ea Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Sat, 10 Feb 2024 22:50:59 +0300 Subject: [PATCH 06/20] fix(core): Output makedepends file after class finish and snippets Catches any new dependencies introduced in these late running code locations. --- core/sile.lua | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/core/sile.lua b/core/sile.lua index 3c3d2f2f6..e868f740d 100644 --- a/core/sile.lua +++ b/core/sile.lua @@ -328,7 +328,7 @@ end -- --- Content loader like Lua's `require()` but whith special path handling for loading SILE resource files. -- -- Used for example by commands that load data via a `src=file.name` option. -- -- @tparam string dependency Lua spec -local SILE.require (dependency, pathprefix, deprecation_ack) +function SILE.require (dependency, pathprefix, deprecation_ack) if pathprefix and not deprecation_ack then local notice = string.format([[ Please don't use the path prefix mechanism; it was intended to provide @@ -635,20 +635,20 @@ end --- Finalize document processing -- Signals that all the `SILE.process()` calls have been made and SILE should move on to finish up the output -- --- 1. Stops logging dependecies and writes them to a makedepends file if requested. --- 2. Tells the document class to run its `:finish()` method. This method is typically responsible for calling the +-- 1. Tells the document class to run its `:finish()` method. This method is typically responsible for calling the -- `:finish()` method of the outputter module in the appropriate sequence. --- 3. Closes out anything in active memory we don't need like font instances. --- 4. Evaluate any snippets in SILE.input.evalAfter table. +-- 2. Closes out anything in active memory we don't need like font instances. +-- 3. Evaluate any snippets in SILE.input.evalAfter table. +-- 4. Stops logging dependecies and writes them to a makedepends file if requested. -- 5. Close out the Lua profiler if it was running. -- 6. Output version information if versions debug flag is set. function SILE.finish () - if SILE.makeDeps then - SILE.makeDeps:write() - end SILE.documentState.documentClass:finish() SILE.font.finish() runEvals(SILE.input.evaluateAfters, "evaluate-after") + if SILE.makeDeps then + SILE.makeDeps:write() + end if not SILE.quiet then io.stderr:write("\n") end From 01a0ef5f4400e8d41761acf9b8ab04517a141407 Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Sat, 10 Feb 2024 23:36:19 +0300 Subject: [PATCH 07/20] chore(tooling): Setup Nix develop environment to build Lua API docs --- build-aux/config.ld | 1 - build-aux/pkg.nix | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/build-aux/config.ld b/build-aux/config.ld index bce1eb056..ffa4c80f1 100644 --- a/build-aux/config.ld +++ b/build-aux/config.ld @@ -7,5 +7,4 @@ file = { "../core", "../typesetters", } -multimodule = true merge = true diff --git a/build-aux/pkg.nix b/build-aux/pkg.nix index 0be09f536..3046eb852 100644 --- a/build-aux/pkg.nix +++ b/build-aux/pkg.nix @@ -47,6 +47,8 @@ let # lua packages needed for testing busted luacheck + # packages needed for building api docs + ldoc # NOTE: Add lua packages here, to change the luaEnv also read by `flake.nix` ] ++ lib.optionals (lib.versionOlder lua.luaversion "5.2") [ bit32 From 96a5d3e490b1483497c4cf4c61e41cc8037b03da Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Sun, 11 Feb 2024 14:43:43 +0300 Subject: [PATCH 08/20] docs(api): Add all module directories we want to generate docs for --- build-aux/config.ld | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/build-aux/config.ld b/build-aux/config.ld index ffa4c80f1..2907a3da4 100644 --- a/build-aux/config.ld +++ b/build-aux/config.ld @@ -4,7 +4,15 @@ dir = "../lua-api-docs" format = "discount" file = { "../sile-lua", - "../core", - "../typesetters", + "../core/", + "../classes/", + "../inputters/", + "../languages/", + "../outputters/", + "../packages/", + "../pagebuilders/", + "../shapers/", + "../types/", + "../typesetters/", } merge = true From ffb505b8f88b9431c1ccd24b56864eb02a84ee09 Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Sun, 11 Feb 2024 15:36:57 +0300 Subject: [PATCH 09/20] docs(api): Add README to API docs --- build-aux/config.ld | 1 + 1 file changed, 1 insertion(+) diff --git a/build-aux/config.ld b/build-aux/config.ld index 2907a3da4..ce5c320ef 100644 --- a/build-aux/config.ld +++ b/build-aux/config.ld @@ -1,5 +1,6 @@ project = "SILE" description = "The SILE Typesetter" +readme = "../README.md" dir = "../lua-api-docs" format = "discount" file = { From dfc22835ab56879b8da3ce44c4040ee6e5b384e7 Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Sun, 11 Feb 2024 16:05:00 +0300 Subject: [PATCH 10/20] docs(api): Work on navigation sanity in Lua docs --- classes/base.lua | 3 ++ core/languages.lua | 3 ++ core/sile.lua | 52 ++++++++++++++++++-------------- inputters/base.lua | 3 ++ outputters/base.lua | 3 ++ packages/base.lua | 3 ++ packages/bibtex/bibliography.lua | 14 ++++----- pagebuilders/base.lua | 3 ++ shapers/base.lua | 3 ++ types/color.lua | 3 ++ types/length.lua | 4 +++ types/measurement.lua | 4 +++ types/node.lua | 3 ++ types/unit.lua | 3 ++ typesetters/base.lua | 4 +-- 15 files changed, 76 insertions(+), 32 deletions(-) diff --git a/classes/base.lua b/classes/base.lua index 38b97bd14..fc4ed7195 100644 --- a/classes/base.lua +++ b/classes/base.lua @@ -1,3 +1,6 @@ +--- SILE document class class. +-- @classmod SILE.classes + local class = pl.class() class.type = "class" class._name = "base" diff --git a/core/languages.lua b/core/languages.lua index 180ff7104..dca7e65ef 100644 --- a/core/languages.lua +++ b/core/languages.lua @@ -1,3 +1,6 @@ +--- SILE language class. +-- @classmod SILE.languages + local loadkit = require("loadkit") local cldr = require("cldr") diff --git a/core/sile.lua b/core/sile.lua index 5a3ce368e..22c9b8275 100644 --- a/core/sile.lua +++ b/core/sile.lua @@ -1,7 +1,30 @@ --- The core SILE library -- @module SILE --- Placeholder for SILE internals +--- Global library provisions. +-- @section globals +-- Loading SILE foo + +--- Penlight. +-- On-demand module loader, provided for SILE and document usage +pl = require("pl.import_into")() + +-- For developer testing only, usually in CI +if os.getenv("SILE_COVERAGE") then require("luacov") end + +--- UTF-8 String handler. +-- Lua 5.3+ has a UTF-8 safe string function module but it is somewhat +-- underwhelming. This module includes more functions and supports older Lua +-- versions. Docs: https://github.com/starwing/luautf8 +luautf8 = require("lua-utf8") + +--- Fluent localization library. +fluent = require("fluent")() + +-- Reserve global scope placeholder for profiler (developer tooling) +local ProFi + +-- Placeholder for SILE internals table SILE = {} --- Fields @@ -42,26 +65,6 @@ SILE.full_version = string.format("SILE %s (%s)", SILE.version, SILE.lua_isjit a -- Backport of lots of Lua 5.3 features to Lua 5.[12] if not SILE.lua_isjit and SILE.lua_version < "5.3" then require("compat53") end --- Penlight on-demand module loader, provided for SILE and document usage -pl = require("pl.import_into")() - --- For developer testing only, usually in CI -if os.getenv("SILE_COVERAGE") then require("luacov") end - --- Lua 5.3+ has a UTF-8 safe string function module but it is somewhat --- underwhelming. This module includes more functions and supports older Lua --- versions. Docs: https://github.com/starwing/luautf8 -luautf8 = require("lua-utf8") - --- Localization library, provided as global -fluent = require("fluent")() - --- Includes for _this_ scope -local lfs = require("lfs") - --- Developer tooling profiler -local ProFi - -- For warnings and shims scheduled for removal that are easier to keep track -- of when they are not spread across so many locations... -- Loaded early to make it easier to manage migrations in core code. @@ -157,8 +160,8 @@ SILE.input = { evaluateAfters = {}, uses = {}, options = {}, - preambles = {}, - postambles = {}, + preambles = {}, -- deprecated, undocumented + postambles = {}, -- deprecated, undocumented } -- Internal libraries that are idempotent and return classes that need instantiation @@ -469,6 +472,7 @@ end -- @tparam[opt] nil|string format The name of the formatter. If nil, defaults to using each intputter's auto detection. -- @tparam[opt] nil|table options Options to pass to the inputter instance when instantiated. function SILE.processFile (filename, format, options) + local lfs = require("lfs") local doc if filename == "-" then filename = "STDIN" @@ -588,6 +592,8 @@ end -- @tparam[opt] nil|string help User friendly short usage string for use in error messages, documentation, etc. -- @tparam[opt] nil|string pack Information identifying the module registering the command for use in error and usage -- messages. Usually auto-detected. +-- @see SILE.classes +-- @see SILE.packages function SILE.registerCommand (name, func, help, pack, cheat) local class = SILE.documentState.documentClass if not cheat then diff --git a/inputters/base.lua b/inputters/base.lua index 4674bdfde..9a476a00e 100644 --- a/inputters/base.lua +++ b/inputters/base.lua @@ -1,3 +1,6 @@ +--- SILE inputter class. +-- @classmod SILE.inputters + local _deprecated = [[ You appear to be using a document class '%s' programmed for SILE <= v0.12.5. This system was refactored in v0.13.0 and the shims trying to make it diff --git a/outputters/base.lua b/outputters/base.lua index 2d5042d8d..e337524fe 100644 --- a/outputters/base.lua +++ b/outputters/base.lua @@ -1,3 +1,6 @@ +--- SILE outputter class. +-- @classmod SILE.outputters + local outputter = pl.class() outputter.type = "outputter" outputter._name = "base" diff --git a/packages/base.lua b/packages/base.lua index 4b86d30cf..b55a1b2c6 100644 --- a/packages/base.lua +++ b/packages/base.lua @@ -1,3 +1,6 @@ +--- SILE package class. +-- @classmod SILE.packages + local package = pl.class() package.type = "package" package._name = "base" diff --git a/packages/bibtex/bibliography.lua b/packages/bibtex/bibliography.lua index b55104d63..0572390d1 100644 --- a/packages/bibtex/bibliography.lua +++ b/packages/bibtex/bibliography.lua @@ -9,18 +9,18 @@ local function find_outside_braces(str, pat, i) local j, k = string.find(str, pat, i) if not j then return j, k end local jb, kb = string.find(str, '%b{}', i) - while jb and jb < j do --- scan past braces - --- braces come first, so we search again after close brace + while jb and jb < j do -- scan past braces + -- braces come first, so we search again after close brace local i2 = kb + 1 j, k = string.find(str, pat, i2) if not j then return j, k end jb, kb = string.find(str, '%b{}', i2) end -- either pat precedes braces or there are no braces - return string.find(str, pat, j) --- 2nd call needed to get captures + return string.find(str, pat, j) -- 2nd call needed to get captures end -local function split(str, pat, find) --- return list of substrings separated by pat +local function split(str, pat, find) -- return list of substrings separated by pat find = find or string.find -- could be find_outside_braces -- @Omikhelia: I added this check here to avoid breaking on error, -- but probably in could have been done earlier... @@ -43,7 +43,7 @@ local function split(str, pat, find) --- return list of substrings separated by return t end -local function splitters(str, pat, find) --- return list of separators +local function splitters(str, pat, find) -- return list of separators find = find or string.find -- could be find_outside_braces local t = { } local insert = table.insert @@ -130,7 +130,7 @@ do trailers[i] = string.sub(trailer, 1, 1) end end - local commas = { } --- maps each comma to index of token the follows it + local commas = { } -- maps each comma to index of token the follows it for i, t in ipairs(trailers) do string.gsub(t, ',', function() table.insert(commas, i+1) end) end @@ -263,7 +263,7 @@ do end end ---- Thanks, Norman, for the above functions! +-- Thanks, Norman, for the above functions! local Bibliography Bibliography = { diff --git a/pagebuilders/base.lua b/pagebuilders/base.lua index 1c2da15c2..94063d337 100644 --- a/pagebuilders/base.lua +++ b/pagebuilders/base.lua @@ -1,3 +1,6 @@ +--- SILE pagebuilder class. +-- @classmod SILE.pagebuilders + local pagebuilder = pl.class() pagebuilder.type = "pagebuilder" pagebuilder._name = "base" diff --git a/shapers/base.lua b/shapers/base.lua index 6c67694f3..ed1e00448 100644 --- a/shapers/base.lua +++ b/shapers/base.lua @@ -1,3 +1,6 @@ +--- SILE shaper class. +-- @classmod SILE.shapers + -- local smallTokenSize = 20 -- Small words will be cached -- local shapeCache = {} -- local _key = function (options) diff --git a/types/color.lua b/types/color.lua index c6b112dba..a95ea6692 100644 --- a/types/color.lua +++ b/types/color.lua @@ -1,3 +1,6 @@ +--- SILE color type. +-- @classmod SILE.types.color + local colornames = { aliceblue = { 240, 248, 255 }, antiquewhite = { 250, 235, 215 }, diff --git a/types/length.lua b/types/length.lua index a1e6d21a0..8f5d7d6d7 100644 --- a/types/length.lua +++ b/types/length.lua @@ -1,3 +1,7 @@ +--- SILE length type. +-- @classmod SILE.types.length +-- @within Types + local function _error_if_not_number (a) if type(a) ~= "number" then SU.error("We tried to do impossible arithmetic on a " .. SU.type(a) .. ". (That's a bug)", true) diff --git a/types/measurement.lua b/types/measurement.lua index 36d1af0d0..c9cd56438 100644 --- a/types/measurement.lua +++ b/types/measurement.lua @@ -1,3 +1,7 @@ +--- SILE measurement type. +-- @classmod SILE.types.measurement +-- + local function _tonumber (amount) return SU.cast("number", amount) end diff --git a/types/node.lua b/types/node.lua index f34383b38..5592b2b8c 100644 --- a/types/node.lua +++ b/types/node.lua @@ -1,3 +1,6 @@ +--- SILE node type. +-- @classmod SILE.types.node + local nodetypes = {} -- This infinity needs to be smaller than an actual infinity but bigger than the infinite stretch diff --git a/types/unit.lua b/types/unit.lua index 452ecab9f..f99fc4878 100644 --- a/types/unit.lua +++ b/types/unit.lua @@ -1,3 +1,6 @@ +--- SILE unit type. +-- @classmod SILE.types.unit + local bits = require("core.parserbits") local unittypes = { diff --git a/typesetters/base.lua b/typesetters/base.lua index 75b41ddd1..0d7373cd0 100644 --- a/typesetters/base.lua +++ b/typesetters/base.lua @@ -1,5 +1,5 @@ ---- SILE typesetter base class. ---- @module SILE.typesetters.base +--- SILE typesetter class. +-- @classmod SILE.typesetters --- @type typesetter local typesetter = pl.class() From ba2e37a9959cebf1e0a334351a4041ee026bac4c Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Mon, 12 Feb 2024 14:20:20 +0300 Subject: [PATCH 11/20] fix(utilities): Cast empty to default and only ever return a bool from SU.boolean() --- core/utilities/init.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/utilities/init.lua b/core/utilities/init.lua index cf991daa2..5119f306b 100644 --- a/core/utilities/init.lua +++ b/core/utilities/init.lua @@ -36,9 +36,10 @@ utilities.boolean = function (value, default) if value == "true" then return true end if value == "no" then preferbool(); return false end if value == "yes" then preferbool(); return true end - if value == nil then return default end + if value == nil then return default == true end + if value == "" then return default == true end SU.error("Expecting a boolean value but got '" .. value .. "'") - return default + return default == true end local _skip_traceback_levels = 2 From ea38243abb42dfb5cd1173c4bd68e9c390d7aa73 Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Mon, 12 Feb 2024 14:39:33 +0300 Subject: [PATCH 12/20] docs(api): Keep working on organizing and documenting, cleanup types --- core/font.lua | 2 + core/settings.lua | 32 +++++---- core/sile.lua | 3 +- core/utilities/ast.lua | 95 ++++++++++++------------ core/utilities/init.lua | 144 +++++++++++++++++++++++++++++++++---- core/utilities/numbers.lua | 5 +- core/utilities/sorting.lua | 7 +- languages/fr.lua | 3 +- typesetters/base.lua | 21 +++--- 9 files changed, 220 insertions(+), 92 deletions(-) diff --git a/core/font.lua b/core/font.lua index 9da62ba07..f7704fd07 100644 --- a/core/font.lua +++ b/core/font.lua @@ -1,3 +1,5 @@ +--- font +-- @module SILE.font local icu = require("justenoughicu") local lastshaper diff --git a/core/settings.lua b/core/settings.lua index 559aa44e8..7b1740535 100644 --- a/core/settings.lua +++ b/core/settings.lua @@ -125,7 +125,7 @@ function settings:popState () end --- Declare a new setting ---- @param specs table: { parameter, ... } declaration specification +--- @tparam table specs { parameter, type, default, help, hook, ... } declaration specification function settings:declare (spec) if not spec then return deprecator() end if spec.name then @@ -143,7 +143,7 @@ function settings:declare (spec) self:set(spec.parameter, spec.default, true) end ---- Reset all settings to their default value. +--- Reset all settings to their registered default values. function settings:reset () if not self then return deprecator() end for k,_ in pairs(self.state) do @@ -152,8 +152,7 @@ function settings:reset () end --- Restore all settings to the value they had in the top-level state, ---- that is at the head of the settings stack (normally the document ---- level). +-- that is at the tap of the settings stack (normally the document level). function settings:toplevelState () if not self then return deprecator() end if #self.stateQueue ~= 0 then @@ -167,7 +166,8 @@ function settings:toplevelState () end --- Get the value of a setting ---- @param parameter The full name of the setting to fetch. +-- @tparam string parameter The full name of the setting to fetch. +-- @return Value of setting function settings:get (parameter) -- HACK FIXME https://github.com/sile-typesetter/sile/issues/1699 -- See comment on set() below. @@ -186,10 +186,10 @@ function settings:get (parameter) end --- Set the value of a setting ---- @param parameter The full name of the setting to change. ---- @param value The new value to change it to. ---- @param makedefault boolean Whether to make this the new default value (default false). ---- @param reset Whether to reset the value to the current default value (default false). +-- @tparam string parameter The full name of the setting to change. +-- @param value The new value to change it to. +-- @tparam[opt=false] boolean makedefault Whether to make this the new default value. +-- @tparam[opt=false] boolean reset Whether to reset the value to the current default value. function settings:set (parameter, value, makedefault, reset) -- HACK FIXME https://github.com/sile-typesetter/sile/issues/1699 -- Anything dubbed current.xxx should likely NOT be a "setting" (subject @@ -234,10 +234,16 @@ function settings:set (parameter, value, makedefault, reset) self:runHooks(parameter, value) end +--- Register a callback hook to be run when a setting changes. +-- @tparam string parameter Name of the setting to add a hook to. +-- @tparam function func Callback function accepting one argument (the new value). function settings:registerHook (parameter, func) table.insert(self.hooks[parameter], func) end +--- Trigger execution of callback hooks for a given setting. +-- @tparam string parameter The name of the parameter changes. +-- @param value The new value of the setting, passed as the first argument to the hook function. function settings:runHooks (parameter, value) if self.hooks[parameter] then for _, func in ipairs(self.hooks[parameter]) do @@ -248,8 +254,8 @@ function settings:runHooks (parameter, value) end --- Isolate a block of processing so that setting changes made during the block don't last past the block. ---- (Under the hood this just uses `:pushState()`, the processes the function, then runs `:popState()`) ---- @param function A function wrapping the actions to take without affecting settings for future use. +-- (Under the hood this just uses `:pushState()`, the processes the function, then runs `:popState()`) +-- @tparam function func A function wrapping the actions to take without affecting settings for future use. function settings:temporarily (func) if not func then return deprecator() end self:pushState() @@ -258,8 +264,8 @@ function settings:temporarily (func) end --- Create a settings wrapper function that applies current settings to later content processing. ---- @return a closure fuction accepting one argument (content) to process using ---- typesetter settings as they are at the time of closure creation. +--- @treturn function a closure fuction accepting one argument (content) to process using +--- typesetter settings as they are at the time of closure creation. function settings:wrap () if not self then return deprecator() end local clSettings = pl.tablex.copy(self.state) diff --git a/core/sile.lua b/core/sile.lua index 22c9b8275..d54bcd736 100644 --- a/core/sile.lua +++ b/core/sile.lua @@ -73,7 +73,8 @@ require("core/deprecations") --- Modules -- @section modules ---- Utilities module, aliased as `SU`. +--- Utilities module, typically accessed via `SU` alias. +-- @see SU SILE.utilities = require("core.utilities") SU = SILE.utilities -- regrettable global alias diff --git a/core/utilities/ast.lua b/core/utilities/ast.lua index a2f0c9bb2..3e5b27c6f 100644 --- a/core/utilities/ast.lua +++ b/core/utilities/ast.lua @@ -1,15 +1,16 @@ ---- SILE AST utilities ---- @module SU.ast +--- AST utilities. +-- Functions for working with SILE's Abstract Syntax Trees. +-- @module SU.ast ---- @type ast +-- @type SU.ast local ast = {} --- Find a command node in a SILE AST tree, ---- looking only at the first level. ---- (We're not reimplementing XPath here.) ----@param tree table AST tree ----@param command string command name ----@return table|nil AST command node +-- looking only at the first level. +-- (We're not reimplementing XPath here.) +-- @tparam table tree AST tree +-- @tparam string command command name +-- @treturn table|nil AST command node function ast.findInTree (tree, command) for i=1, #tree do if type(tree[i]) == "table" and tree[i].command == command then @@ -19,10 +20,10 @@ function ast.findInTree (tree, command) end --- Find and extract (remove) a command node in a SILE AST tree, ---- looking only at the first level. ----@param tree table AST tree ----@param command string command name ----@return table|nil AST command node +-- looking only at the first level. +-- @tparam table tree AST tree +-- @tparam string command command name +-- @treturn table|nil AST command node function ast.removeFromTree (tree, command) for i=1, #tree do if type(tree[i]) == "table" and tree[i].command == command then @@ -32,12 +33,12 @@ function ast.removeFromTree (tree, command) end --- Create a command from a simple content tree. ---- It encapsulates the content in a command node. ----@param command string command name ----@param options table command options ----@param content table child AST tree ----@param position table position in source (or parent AST command node) ----@return table AST command node +-- It encapsulates the content in a command node. +-- @tparam string command command name +-- @tparam table options command options +-- @tparam table content child AST tree +-- @tparam table position position in source (or parent AST command node) +-- @treturn table AST command node function ast.createCommand (command, options, content, position) local result = { content } result.options = options or {} @@ -56,12 +57,12 @@ function ast.createCommand (command, options, content, position) end --- Create a command from a structured content tree. ---- The content is normally a table of an already prepared content list. ----@param command string command name ----@param options table command options ----@param content table child AST tree ----@param position table position in source (or parent AST command node) ----@return table AST command node +-- The content is normally a table of an already prepared content list. +-- @tparam string command command name +-- @tparam table options command options +-- @tparam table content child AST tree +-- @tparam table position position in source (or parent AST command node) +-- @treturn table AST command node function ast.createStructuredCommand (command, options, content, position) local result = type(content) == "table" and content or { content } result.options = options or {} @@ -80,9 +81,9 @@ function ast.createStructuredCommand (command, options, content, position) end --- Extract the sub-content tree from a (command) node, ---- that is the child nodes of the (command) node. ----@param content table AST tree ----@return table AST tree +-- that is the child nodes of the (command) node. +-- @tparam table content AST tree +-- @treturn table AST tree function ast.subContent (content) local out = {} for _, val in ipairs(content) do @@ -101,8 +102,8 @@ end --- Content tree trimming: remove leading and trailing spaces, but from --- a content tree i.e. possibly containing several elements. ----@param content table AST tree ----@return table AST tree +-- @tparam table content AST tree +-- @treturn table AST tree function ast.trimSubContent (content) if #content == 0 then return @@ -123,10 +124,10 @@ function ast.trimSubContent (content) end --- Process the AST walking through content nodes as a "structure": ---- Text nodes are ignored (e.g. usually just spaces due to indentation) ---- Command options are enriched with their "true" node position, so we can later ---- refer to it (as with an XPath pos()). ----@param content table AST tree +-- Text nodes are ignored (e.g. usually just spaces due to indentation) +-- Command options are enriched with their "true" node position, so we can later +-- refer to it (as with an XPath pos()). +-- @tparam table content AST tree function ast.processAsStructure (content) local iElem = 0 local nElem = 0 @@ -147,9 +148,9 @@ function ast.processAsStructure (content) end --- Call `action` on each content AST node, recursively, including `content` itself. ---- Not called on leaves, i.e. strings. ----@param content table AST tree ----@param action function function to call on each node +-- Not called on leaves, i.e. strings. +-- @tparam table content AST tree +-- @tparam function action A function to call on each node function ast.walkContent (content, action) if type(content) ~= "table" then return @@ -161,12 +162,12 @@ function ast.walkContent (content, action) end --- Strip position, line and column recursively from a content tree. ---- This can be used to remove position details where we do not want them, ---- e.g. in table of contents entries (referring to the original content, ---- regardless where it was exactly, for the purpose of checking whether ---- the table of contents changed.) ----@param content table AST tree ----@return table AST tree +-- This can be used to remove position details where we do not want them, +-- e.g. in table of contents entries (referring to the original content, +-- regardless where it was exactly, for the purpose of checking whether +-- the table of contents changed.) +-- @param table content AST tree +-- @treturn table AST tree function ast.stripContentPos (content) if type(content) ~= "table" then return content @@ -185,9 +186,9 @@ function ast.stripContentPos (content) end --- Flatten content trees into just the string components (allows passing ---- objects with complex structures to functions that need plain strings) ---- @param content table AST tree ---- @return string string representation of content +-- objects with complex structures to functions that need plain strings) +-- @tparam table content AST tree +-- @treturn string A string representation of content function ast.contentToString (content) local string = "" for i = 1, #content do @@ -206,8 +207,8 @@ function ast.contentToString (content) end --- Check whether a content AST tree is empty. ----@param content table AST tree ----@return boolean true if content is not empty +-- @tparam table content AST tree +-- @treturn boolean true if content is not empty function ast.hasContent (content) return type(content) == "function" or type(content) == "table" and #content > 0 end diff --git a/core/utilities/init.lua b/core/utilities/init.lua index 5119f306b..af5948c1a 100644 --- a/core/utilities/init.lua +++ b/core/utilities/init.lua @@ -1,22 +1,23 @@ ---- SILE.Utilities (aliased as SU) ---- @module SU ---- alias SU.utilities +--- SILE.utilities (aliased as SU) +-- @module SU +-- @alias utilities local bitshim = require("bitshim") local luautf8 = require("lua-utf8") local semver = require("semver") ---- @type utilities local utilities = {} local epsilon = 1E-12 +--- Generic +-- @section generic + --- Require that an option table contains a specific value, otherwise raise an error. ---- @param options Input table of options. ---- @param name Name of the required option. ---- @param context User friendly name of the function or calling context. ---- @param required_type The name of a data type that the option must sucessfully cast to. --- SU.required +-- @param options Input table of options. +-- @param name Name of the required option. +-- @param context User friendly name of the function or calling context. +-- @param required_type The name of a data type that the option must sucessfully cast to. utilities.required = function (options, name, context, required_type) if not options[name] then utilities.error(context.." needs a "..name.." parameter") end if required_type then @@ -29,6 +30,13 @@ local function preferbool () utilities.warn("Please use boolean values or strings such as 'true' and 'false' instead of 'yes' and 'no'.") end +--- Cast user intput into a boolean type. +-- User input content such as options typed into documents will return string values such as "true" or "false rather +-- than true or false types. This evaluates those strings or other inputs ane returns a consistent boolean type in +-- return. +-- @tparam nil|bool|string value Input value such as a string to evaluate for thruthyness. +-- @tparam[opt=false] boolean default Whether to assume inputs that don't specifically evaluate to something should be true or false. +-- @treturn boolean utilities.boolean = function (value, default) if value == false then return false end if value == true then return true end @@ -44,6 +52,10 @@ end local _skip_traceback_levels = 2 +--- Raise an error and exit. +-- Outputs a warning message via `warn`, then finishes up anything it can without processing more content, then exits. +-- @tparam string message The error message to give. +-- @tparam boolean isbug Whether or not hitting this error is expected to be a code bug (as opposed to misakes in user input). utilities.error = function (message, isbug) _skip_traceback_levels = 3 utilities.warn(message, isbug) @@ -54,6 +66,10 @@ utilities.error = function (message, isbug) error("", 2) end +--- Output a warning. +-- Outputs a warning message including identifying where in the processing SILE is at when the warning is given. +-- @tparam string message The error message to give. +-- @tparam boolean isbug Whether or not hitting this warning is expected to be a code bug (as opposed to misakes in user input). utilities.warn = function (message, isbug) if SILE.quiet then return end io.stderr:write("\n! " .. message) @@ -68,22 +84,42 @@ utilities.warn = function (message, isbug) io.stderr:write("\n") end +--- Output an information message. +-- @tparam string message utilities.msg = function (message) if SILE.quiet then return end io.stderr:write("\n! " .. message .. "\n") end +--- Determine if a specific debug flag is set. +-- @tparam string category Name of the flag status to check, e.g. "frames". +-- @treturn boolean utilities.debugging = function (category) return SILE.debugFlags.all and category ~= "profile" or SILE.debugFlags[category] end -utilities.feq = function (lhs, rhs) -- Float point equal +--- Math +-- @section math + +--- Check equality of floating point values. +-- Comparing floating point numbers using math functions in Lua may give different and unexpected answers depending on +-- the Lua VM and other environmental factors. This normalizes them using our standard internal epsilon value and +-- compares the absolute intereger value to avoid floating point number wierdness. +-- @tparam float lhs +-- @tparam float rhs +-- @treturn boolean +utilities.feq = function (lhs, rhs) lhs = SU.cast("number", lhs) rhs = SU.cast("number", rhs) local abs = math.abs return abs(lhs - rhs) <= epsilon * (abs(lhs) + abs(rhs)) end +--- Iterate over a string split into tokens via a pattern. +-- @tparam string string Input string. +-- @tparam string pattern Pattern on which to split the input. +-- @treturn function An iterator function +-- @usage for str in SU.gtoke("foo-bar-baz", "-") do print(str) end utilities.gtoke = function (string, pattern) string = string and tostring(string) or '' pattern = pattern and tostring(pattern) or "%s+" @@ -106,6 +142,15 @@ utilities.gtoke = function (string, pattern) end) end +--- Warn about use of a deprecated feature. +-- Checks the current version and decides whether to warn or error, then oatputs a message with as much useful +-- information as possible to make it easy for end users to update their usage. +-- @tparam string old The name of the deprecated interface. +-- @tparam string new A name of a suggested replacement interface. +-- @tparam string warnat The first release where the interface is considered deprecated, at which point their might be +-- a shim. +-- @tparam string errorat The first release where the interface is no longer functional even with a shim. +-- @tparam string extra Longer-form help to include in output separate from the expected one-liner of warning messages. utilities.deprecated = function (old, new, warnat, errorat, extra) warnat, errorat = semver(warnat or 0), semver(errorat or 0) local current = SILE.version and semver(SILE.version:match("v([0-9]*.[0-9]*.[0-9]*)")) or warnat @@ -124,6 +169,17 @@ utilities.deprecated = function (old, new, warnat, errorat, extra) end end +--- Output a debug message only if debugging for a specific category is enabled. +-- Importantly passing siries of strings, functions, or tables is more effecient than trying to formulate a full message +-- using concatentation and tostring() methods in the original code because it doesn't have to even run if the relevant +-- debug flag is not enabled. +-- @tparam text category Category flag for which this message should be output. +-- @tparam string|function|table ... Each argument will be returned separated by spaces, strings directly, functions by +-- evaluating them and assuming the return value is a string, and tables by using their internal :__tostring() methods. +-- @usage +-- > glue = SILE.types.node.glue("6em") +-- > SU.debug("foo", "A glue node", glue) +-- [foo] A glue node G<6em> utilities.debug = function (category, ...) if SILE.quiet then return end if utilities.debugging(category) then @@ -141,6 +197,9 @@ utilities.debug = function (category, ...) end end +--- Output developer friendly debugging view of an AST. +-- @tparam table ast Abstract Syntax Tree. +-- @tparam integer level Starting level to review. utilities.debugAST = function (ast, level) if not ast then SU.error("debugAST called with nil", true) @@ -180,15 +239,24 @@ utilities.debugAST = function (ast, level) if level == 0 then SU.debug("ast", "]") end end +--- Dump the contents of a any Lua type. +-- For quick debugging, can be used on any number of any type of Lua value. Pretty-prints tables. +-- @tparam any ... Any number of values utilities.dump = function (...) local arg = { ... } -- Avoid things that Lua stuffs in arg like args to self() pl.pretty.dump(#arg == 1 and arg[1] or arg, "/dev/stderr") end +--- Concatenate values from a table using a given separator. +-- Differs from `table.concat` in that all values are explicitly cast to strings, allowing debugging of tables that +-- include functions, other tables, data types, etc. +-- @tparam table array Input. +-- @tparam[opt=" "] string separator Separator. utilities.concat = function (array, separator) return table.concat(utilities.map(tostring, array), separator) end +-- TODO: Unused, now deprecated? utilities.inherit = function (orig, spec) local new = pl.tablex.deepcopy(orig) if spec then @@ -198,6 +266,9 @@ utilities.inherit = function (orig, spec) return new end +--- Execute a callback function on each value in a table. +-- @tparam function func Function to run on each value. +-- @tparam table array Input list-like table. utilities.map = function (func, array) local new_array = {} local last = #array @@ -207,6 +278,11 @@ utilities.map = function (func, array) return new_array end +--- Iterate over key/value pairs in sequence of the sorted keys. +-- Table iteration order with `pairs` is non-deterministic. This function returns an iterator that can be used in plais +-- of `pairs` that will iterate through the values in the order of their *sorted* keys. +-- @tparam table input Input table. +-- @usage for val in SU.sortedpairs({ b: "runs second", a: "runs first" ) do print(val) end utilities.sortedpairs = function (input) local keys = {} for k, _ in pairs(input) do @@ -225,6 +301,12 @@ utilities.sortedpairs = function (input) end) end +--- Substitute a range of value(s) in one table with values from another. +-- @tparam table array Table to modify. +-- @tparam integer start First key to replace. +-- @tparam integer stop Last key to replace. +-- @tparam table replacement Table from which to pull key/values plairs to inject in array. +-- @treturn table array First input array modified with values from replacement. utilities.splice = function (array, start, stop, replacement) local ptr = start local room = stop - start + 1 @@ -245,6 +327,9 @@ utilities.splice = function (array, start, stop, replacement) return array end +--- Add up all the values in a table. +-- @tparam table array Input list-like table. +-- @treturn number Sum of all values. utilities.sum = function (array) local total = 0 local last = #array @@ -254,7 +339,9 @@ utilities.sum = function (array) return total end --- Lua <= 5.2 can't handle objects in math functions +--- Return maximum value of inputs. +-- `math.max`, but works on SILE types such as SILE.types.measurement. +-- Lua <= 5.2 can't handle math operators on objects. utilities.max = function (...) local input = pl.utils.pack(...) local max = table.remove(input, 1) @@ -264,6 +351,9 @@ utilities.max = function (...) return max end +--- Return minimum value of inputs. +-- `math.min`, but works on SILE types such as SILE.types.measurement. +-- Lua <= 5.2 can't handle math operators on objects. utilities.min = function (...) local input = pl.utils.pack(...) local min = input[1] @@ -273,6 +363,7 @@ utilities.min = function (...) return min end +--- Round and normalize a number for debugging. -- LuaJIT 2.1 betas (and inheritors such as OpenResty and Moonjit) are biased -- towards rounding 0.5 up to 1, all other Lua interpreters are biased -- towards rounding such floating point numbers down. This hack shaves off @@ -281,12 +372,19 @@ end -- inherent to the floating point type. Also note we are erroring in favor of -- the *less* common option because the LuaJIT VMS are hopelessly broken -- whereas normal LUA VMs can be cooerced. +-- @tparam number input Input value. +-- @treturn string Four-digit precision foating point. utilities.debug_round = function (input) if input > 0 then input = input + .00000000000001 end if input < 0 then input = input - .00000000000001 end return string.format("%.4f", input) end +--- Remove empty spaces from list-like tables +-- Iterating list-like tables is hard if some values have been removed. This converts { 1 = "a", 3 = "b" } into +-- { 1 = "a", 2 = "b" } which can be iterated using `ipairs()` without stopping after 1. +-- @tparam table items List-like table potentially with holes. +-- @treturn table List like table without holes. utilities.compress = function (items) local rv = {} local max = math.max(pl.utils.unpack(pl.tablex.keys(items))) @@ -294,6 +392,8 @@ utilities.compress = function (items) return rv end +--- Reverse the order of a list-like table. +-- @tparam table tbl Input list-like table. utilities.flip_in_place = function (tbl) local tmp, j for i = 1, math.floor(#tbl / 2) do @@ -304,6 +404,7 @@ utilities.flip_in_place = function (tbl) end end +-- TODO: Before documenting, consider whether this should be private to the one existing usage. utilities.allCombinations = function (options) local count = 1 for i=1,#options do count = count * options[i] end @@ -321,6 +422,11 @@ utilities.allCombinations = function (options) end) end +--- Return the type of an object +-- Like `type`, but also handles various SILE user data types. +-- @tparam any value Any input value. If a table is one of SILE's classes or types, report on it's internal type. +-- Otherwise use the output of `type`. +-- @treturn string utilities.type = function(value) if type(value) == "number" then return math.floor(value) == value and "integer" or "number" @@ -333,6 +439,12 @@ utilities.type = function(value) end end +--- Cast user intput to an expected type. +-- If possible, converts input from one type to another. Not all types can be cast. For example "four" can't be cast to +-- a number, but "4" or 4 can. Likewise "6pt" or 6 can be cast to a SILE.types.measurement, SILE.types.length, or even +-- a SILE.types.node.glue, but not a SILE.types.color. +-- @tparam string wantedType Expected type. +-- @return A value of the type wantedType. utilities.cast = function (wantedType, value) local actualType = SU.type(value) wantedType = string.lower(wantedType) @@ -386,12 +498,17 @@ utilities.rationWidth = function (target, width, ratio) return target end --- Unicode-related utilities +--- Unicode +-- @section utf8 + utilities.utf8char = function (c) utilities.deprecated("SU.utf8char", "luautf8.char", "0.11.0", "0.12.0") return luautf8.char(c) end +--- Convert a Unicode character to its corresponding codepoint. +-- @tparam string uchar A single inicode character. +-- @return number The Unicode code point where uchar is encoded. utilities.codepoint = function (uchar) local seq = 0 local val = -1 @@ -411,6 +528,9 @@ utilities.codepoint = function (uchar) return val end +--- Covert a code point to a Unicode character. +-- @tparam number|string codepoint Input code point value, either as a number or a string representing the decimal value "U+NNNN" or hex value "0xFFFF". +-- @treturn string The character replestened by a codepoint descriptions. utilities.utf8charfromcodepoint = function (codepoint) local val = codepoint local cp = val diff --git a/core/utilities/numbers.lua b/core/utilities/numbers.lua index b53cb2541..6bd6f3552 100644 --- a/core/utilities/numbers.lua +++ b/core/utilities/numbers.lua @@ -1,6 +1,5 @@ ---- Number formatting utilities ---- @submodule SU ---- submodule SU +--- Number formatting utilities. +--- @module SU.numbers local icu = require("justenoughicu") diff --git a/core/utilities/sorting.lua b/core/utilities/sorting.lua index 38a350e62..88a0402d2 100644 --- a/core/utilities/sorting.lua +++ b/core/utilities/sorting.lua @@ -1,7 +1,6 @@ --- --- Table sorting with language-dependent collation --- MIT License (c) 2022 SILE organization --- +--- Table sorting with language-dependent collation. +-- @module SU.sorting + local icu = require("justenoughicu") local collatedSort = { diff --git a/languages/fr.lua b/languages/fr.lua index f42e92a2d..919e75e7d 100644 --- a/languages/fr.lua +++ b/languages/fr.lua @@ -1,4 +1,5 @@ --- French language rules +--- French language rules +-- @submodule SILE.languages local computeSpaces = function() -- Computes: diff --git a/typesetters/base.lua b/typesetters/base.lua index 0d7373cd0..729038135 100644 --- a/typesetters/base.lua +++ b/typesetters/base.lua @@ -991,12 +991,11 @@ function linerBox:__tostring () return "*L[" .. self.name .. "]H<" .. tostring(self.width) .. ">^" .. tostring(self.height) .. "-" .. tostring(self.depth) .. "v" end ---- Any unclosed liner is reopened on the current line, so we clone and repeat --- it. +--- Any unclosed liner is reopened on the current line, so we clone and repeat it. -- An assumption is that the inserts are done after the current slice content, -- supposed to be just before meaningful (visible) content. ----@param slice table Current line nodes ----@return boolean Whether a liner was reopened +-- @tparam slice slice +-- @treturn boolean Whether a liner was reopened function typesetter:_repeatEnterLiners (slice) local m = self.state.liners if #m > 0 then @@ -1012,8 +1011,8 @@ end --- All pairs of liners are rebuilt as hboxes wrapping their content. -- Migrating content, however, must be kept outside the hboxes at top slice level. ----@param slice table Flat nodes from current line ----@return table New reboxed slice +-- @tparam table slice Flat nodes from current line +-- @treturn table New reboxed slice function typesetter._reboxLiners (_, slice) local outSlice = {} local migratingList = {} @@ -1061,8 +1060,8 @@ function typesetter._reboxLiners (_, slice) end --- Check if a node is a liner, and process it if so, in a stack. ----@param node any Current node ----@return boolean Whether a liner was opened +-- @tparam table node Current node (any type) +-- @treturn boolean Whether a liner was opened function typesetter:_processIfLiner(node) local entered = false if node.is_enter then @@ -1384,9 +1383,9 @@ end -- effects. -- If we are already in horizontal-restricted mode, the liner is processed -- immediately, since line breaking won't occur then. ----@param name string Name of the liner (usefull for debugging) ----@param content table SILE AST to process ----@param outputYourself function Output method for wrapped boxes +-- @tparam string name Name of the liner (usefull for debugging) +-- @tparam table content SILE AST to process +-- @tparam function outputYourself Output method for wrapped boxes function typesetter:liner (name, content, outputYourself) if self.state.hmodeOnly then SU.debug("typesetter.liner", "Applying liner in horizontal-restricted mode") From 79f6aad35d3f146e0c04716569ac772c03790c0c Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Mon, 12 Feb 2024 16:31:22 +0300 Subject: [PATCH 13/20] chore(utilities): Move deprecated functions to deprecations file --- core/deprecations.lua | 8 ++++++++ core/sile.lua | 10 +++++----- core/utilities/init.lua | 10 ---------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/core/deprecations.lua b/core/deprecations.lua index 5875bb627..4bddc87ca 100644 --- a/core/deprecations.lua +++ b/core/deprecations.lua @@ -138,6 +138,14 @@ setmetatable(SILE.PackageManager, { __index = nopackagemanager }) +SU.utf8char = function () + SU.deprecated("SU.utf8char", "luautf8.char", "0.11.0", "0.12.0") +end + +SU.utf8codes = function () + SU.deprecated("SU.utf8codes", "luautf8.codes", "0.11.0", "0.12.0") +end + -- luacheck: ignore updatePackage -- luacheck: ignore installPackage updatePackage = nopackagemanager diff --git a/core/sile.lua b/core/sile.lua index d54bcd736..c41270f93 100644 --- a/core/sile.lua +++ b/core/sile.lua @@ -65,11 +65,6 @@ SILE.full_version = string.format("SILE %s (%s)", SILE.version, SILE.lua_isjit a -- Backport of lots of Lua 5.3 features to Lua 5.[12] if not SILE.lua_isjit and SILE.lua_version < "5.3" then require("compat53") end --- For warnings and shims scheduled for removal that are easier to keep track --- of when they are not spread across so many locations... --- Loaded early to make it easier to manage migrations in core code. -require("core/deprecations") - --- Modules -- @section modules @@ -78,6 +73,11 @@ require("core/deprecations") SILE.utilities = require("core.utilities") SU = SILE.utilities -- regrettable global alias +-- For warnings and shims scheduled for removal that are easier to keep track +-- of when they are not spread across so many locations... +-- Loaded early to make it easier to manage migrations in core code. +require("core/deprecations") + -- On demand loader, allows modules to be loaded into a specific scope but -- only when/if accessed. local function core_loader (scope) diff --git a/core/utilities/init.lua b/core/utilities/init.lua index af5948c1a..3493ade09 100644 --- a/core/utilities/init.lua +++ b/core/utilities/init.lua @@ -501,11 +501,6 @@ end --- Unicode -- @section utf8 -utilities.utf8char = function (c) - utilities.deprecated("SU.utf8char", "luautf8.char", "0.11.0", "0.12.0") - return luautf8.char(c) -end - --- Convert a Unicode character to its corresponding codepoint. -- @tparam string uchar A single inicode character. -- @return number The Unicode code point where uchar is encoded. @@ -547,11 +542,6 @@ utilities.utf8charfromcodepoint = function (codepoint) return val end -utilities.utf8codes = function (ustr) - utilities.deprecated("SU.utf8codes", "luautf8.codes", "0.11.0", "0.12.0") - return luautf8.codes(ustr) -end - utilities.utf16codes = function (ustr, endian) local pos = 1 return function() From eea28def07b81d6de97673cf65a337e6261a8b3d Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Mon, 12 Feb 2024 16:51:10 +0300 Subject: [PATCH 14/20] docs(api): Expand utilities docs and re-order for some sectioning --- core/utilities/init.lua | 551 ++++++++++++++++++++++------------------ 1 file changed, 306 insertions(+), 245 deletions(-) diff --git a/core/utilities/init.lua b/core/utilities/init.lua index 3493ade09..8dc00de0b 100644 --- a/core/utilities/init.lua +++ b/core/utilities/init.lua @@ -13,6 +13,27 @@ local epsilon = 1E-12 --- Generic -- @section generic +--- Concatenate values from a table using a given separator. +-- Differs from `table.concat` in that all values are explicitly cast to strings, allowing debugging of tables that +-- include functions, other tables, data types, etc. +-- @tparam table array Input. +-- @tparam[opt=" "] string separator Separator. +utilities.concat = function (array, separator) + return table.concat(utilities.map(tostring, array), separator) +end + +--- Execute a callback function on each value in a table. +-- @tparam function func Function to run on each value. +-- @tparam table array Input list-like table. +utilities.map = function (func, array) + local new_array = {} + local last = #array + for i = 1, last do + new_array[i] = func(array[i]) + end + return new_array +end + --- Require that an option table contains a specific value, otherwise raise an error. -- @param options Input table of options. -- @param name Name of the required option. @@ -26,6 +47,68 @@ utilities.required = function (options, name, context, required_type) return options[name] end +--- Iterate over key/value pairs in sequence of the sorted keys. +-- Table iteration order with `pairs` is non-deterministic. This function returns an iterator that can be used in plais +-- of `pairs` that will iterate through the values in the order of their *sorted* keys. +-- @tparam table input Input table. +-- @usage for val in SU.sortedpairs({ b: "runs second", a: "runs first" ) do print(val) end +utilities.sortedpairs = function (input) + local keys = {} + for k, _ in pairs(input) do + keys[#keys+1] = k + end + table.sort(keys, function(a, b) + if type(a) == type(b) then return a < b + elseif type(a) == "number" then return true + else return false + end + end) + return coroutine.wrap(function() + for i = 1, #keys do + coroutine.yield(keys[i], input[keys[i]]) + end + end) +end + +--- Substitute a range of value(s) in one table with values from another. +-- @tparam table array Table to modify. +-- @tparam integer start First key to replace. +-- @tparam integer stop Last key to replace. +-- @tparam table replacement Table from which to pull key/values plairs to inject in array. +-- @treturn table array First input array modified with values from replacement. +utilities.splice = function (array, start, stop, replacement) + local ptr = start + local room = stop - start + 1 + local last = replacement and #replacement or 0 + for i = 1, last do + if room > 0 then + room = room - 1 + array[ptr] = replacement[i] + else + table.insert(array, ptr, replacement[i]) + end + ptr = ptr + 1 + end + + for _ = 1, room do + table.remove(array, ptr) + end + return array +end + +-- TODO: Unused, now deprecated? +utilities.inherit = function (orig, spec) + local new = pl.tablex.deepcopy(orig) + if spec then + for k,v in pairs(spec) do new[k] = v end + end + if new.init then new:init() end + return new +end + +--- Type handling +-- @section types + local function preferbool () utilities.warn("Please use boolean values or strings such as 'true' and 'false' instead of 'yes' and 'no'.") end @@ -50,125 +133,70 @@ utilities.boolean = function (value, default) return default == true end -local _skip_traceback_levels = 2 - ---- Raise an error and exit. --- Outputs a warning message via `warn`, then finishes up anything it can without processing more content, then exits. --- @tparam string message The error message to give. --- @tparam boolean isbug Whether or not hitting this error is expected to be a code bug (as opposed to misakes in user input). -utilities.error = function (message, isbug) - _skip_traceback_levels = 3 - utilities.warn(message, isbug) - _skip_traceback_levels = 2 - io.stderr:flush() - SILE.outputter:finish() -- Only really useful from the REPL but no harm in trying - SILE.scratch.caughterror = true - error("", 2) -end - ---- Output a warning. --- Outputs a warning message including identifying where in the processing SILE is at when the warning is given. --- @tparam string message The error message to give. --- @tparam boolean isbug Whether or not hitting this warning is expected to be a code bug (as opposed to misakes in user input). -utilities.warn = function (message, isbug) - if SILE.quiet then return end - io.stderr:write("\n! " .. message) - if SILE.traceback or isbug then - io.stderr:write(" at:\n" .. SILE.traceStack:locationTrace()) - if _skip_traceback_levels == 2 then - io.stderr:write(debug.traceback("", _skip_traceback_levels) or "\t! debug.traceback() did not identify code location") +--- Cast user intput to an expected type. +-- If possible, converts input from one type to another. Not all types can be cast. For example "four" can't be cast to +-- a number, but "4" or 4 can. Likewise "6pt" or 6 can be cast to a SILE.types.measurement, SILE.types.length, or even +-- a SILE.types.node.glue, but not a SILE.types.color. +-- @tparam string wantedType Expected type. +-- @return A value of the type wantedType. +utilities.cast = function (wantedType, value) + local actualType = SU.type(value) + wantedType = string.lower(wantedType) + if wantedType:match(actualType) then return value + elseif actualType == "nil" and wantedType:match("nil") then return nil + elseif wantedType:match("length") then return SILE.types.length(value) + elseif wantedType:match("measurement") then return SILE.types.measurement(value) + elseif wantedType:match("vglue") then return SILE.types.node.vglue(value) + elseif wantedType:match("glue") then return SILE.types.node.glue(value) + elseif wantedType:match("kern") then return SILE.types.node.kern(value) + elseif actualType == "nil" then SU.error("Cannot cast nil to " .. wantedType) + elseif wantedType:match("boolean") then return SU.boolean(value) + elseif wantedType:match("string") then return tostring(value) + elseif wantedType:match("number") then + if type(value) == "table" and type(value.tonumber) == "function" then + return value:tonumber() end - else - io.stderr:write(" at " .. SILE.traceStack:locationHead()) + local num = tonumber(value) + if not num then SU.error("Cannot cast '" .. value .. "'' to " .. wantedType) end + return num + elseif wantedType:match("integer") then + local num + if type(value) == "table" and type(value.tonumber) == "function" then + num = value:tonumber() + else + num = tonumber(value) + end + if not num then SU.error("Cannot cast '" .. value .. "'' to " .. wantedType) end + if not wantedType:match("number") and num % 1 ~= 0 then + -- Could be an error but since it wasn't checked before, let's just warn: + -- Some packages might have wrongly typed settings, for instance. + SU.warn("Casting an integer but got a float number " .. num) + end + return num + else SU.error("Cannot cast to unrecognized type " .. wantedType) end - io.stderr:write("\n") end ---- Output an information message. --- @tparam string message -utilities.msg = function (message) - if SILE.quiet then return end - io.stderr:write("\n! " .. message .. "\n") -end - ---- Determine if a specific debug flag is set. --- @tparam string category Name of the flag status to check, e.g. "frames". --- @treturn boolean -utilities.debugging = function (category) - return SILE.debugFlags.all and category ~= "profile" or SILE.debugFlags[category] -end - ---- Math --- @section math - ---- Check equality of floating point values. --- Comparing floating point numbers using math functions in Lua may give different and unexpected answers depending on --- the Lua VM and other environmental factors. This normalizes them using our standard internal epsilon value and --- compares the absolute intereger value to avoid floating point number wierdness. --- @tparam float lhs --- @tparam float rhs --- @treturn boolean -utilities.feq = function (lhs, rhs) - lhs = SU.cast("number", lhs) - rhs = SU.cast("number", rhs) - local abs = math.abs - return abs(lhs - rhs) <= epsilon * (abs(lhs) + abs(rhs)) -end - ---- Iterate over a string split into tokens via a pattern. --- @tparam string string Input string. --- @tparam string pattern Pattern on which to split the input. --- @treturn function An iterator function --- @usage for str in SU.gtoke("foo-bar-baz", "-") do print(str) end -utilities.gtoke = function (string, pattern) - string = string and tostring(string) or '' - pattern = pattern and tostring(pattern) or "%s+" - local length = #string - return coroutine.wrap(function() - local index = 1 - repeat - local first, last = string:find(pattern, index) - if last then - if index < first then coroutine.yield({ string = string:sub(index, first - 1) }) end - coroutine.yield({ separator = string:sub(first, last) }) - index = last + 1 - else - if index <= length then - coroutine.yield({ string = string:sub(index) }) - end - break - end - until index > length - end) -end - ---- Warn about use of a deprecated feature. --- Checks the current version and decides whether to warn or error, then oatputs a message with as much useful --- information as possible to make it easy for end users to update their usage. --- @tparam string old The name of the deprecated interface. --- @tparam string new A name of a suggested replacement interface. --- @tparam string warnat The first release where the interface is considered deprecated, at which point their might be --- a shim. --- @tparam string errorat The first release where the interface is no longer functional even with a shim. --- @tparam string extra Longer-form help to include in output separate from the expected one-liner of warning messages. -utilities.deprecated = function (old, new, warnat, errorat, extra) - warnat, errorat = semver(warnat or 0), semver(errorat or 0) - local current = SILE.version and semver(SILE.version:match("v([0-9]*.[0-9]*.[0-9]*)")) or warnat - -- SILE.version is defined *after* most of SILE loads. It’s available at - -- runtime but not useful if we encounter deprecated code in core code. Users - -- will never encounter this failure, but as a developer it’s hard to test a - -- deprecation when core code refactoring is an all-or-nothing proposition. - -- Hence we fake it ‘till we make it, all deprecations internally are warnings. - local brackets = old:sub(1,1) == '\\' and "" or "()" - local _new = new and "Please use " .. (new .. brackets) .. " instead." or "Plase don't use it." - local msg = (old .. brackets) .. " was deprecated in SILE v" .. tostring(warnat) .. ". " .. _new .. (extra and ("\n\n" .. extra .. "\n") or "") - if errorat and current >= errorat then - SU.error(msg) - elseif warnat and current >= warnat then - SU.warn(msg) +--- Return the type of an object +-- Like Lua's `type`, but also handles various SILE user data types. +-- @tparam any value Any input value. If a table is one of SILE's classes or types, report on it's internal type. +-- Otherwise use the output of `type`. +-- @treturn string +utilities.type = function(value) + if type(value) == "number" then + return math.floor(value) == value and "integer" or "number" + elseif type(value) == "table" and value.prototype then + return value:prototype() + elseif type(value) == "table" and value.is_a then + return value.type + else + return type(value) end end +--- Errors and debugging +-- @section errors + --- Output a debug message only if debugging for a specific category is enabled. -- Importantly passing siries of strings, functions, or tables is more effecient than trying to formulate a full message -- using concatentation and tostring() methods in the original code because it doesn't have to even run if the relevant @@ -197,6 +225,13 @@ utilities.debug = function (category, ...) end end +--- Determine if a specific debug flag is set. +-- @tparam string category Name of the flag status to check, e.g. "frames". +-- @treturn boolean +utilities.debugging = function (category) + return SILE.debugFlags.all and category ~= "profile" or SILE.debugFlags[category] +end + --- Output developer friendly debugging view of an AST. -- @tparam table ast Abstract Syntax Tree. -- @tparam integer level Starting level to review. @@ -239,6 +274,33 @@ utilities.debugAST = function (ast, level) if level == 0 then SU.debug("ast", "]") end end +--- Warn about use of a deprecated feature. +-- Checks the current version and decides whether to warn or error, then oatputs a message with as much useful +-- information as possible to make it easy for end users to update their usage. +-- @tparam string old The name of the deprecated interface. +-- @tparam string new A name of a suggested replacement interface. +-- @tparam string warnat The first release where the interface is considered deprecated, at which point their might be +-- a shim. +-- @tparam string errorat The first release where the interface is no longer functional even with a shim. +-- @tparam string extra Longer-form help to include in output separate from the expected one-liner of warning messages. +utilities.deprecated = function (old, new, warnat, errorat, extra) + warnat, errorat = semver(warnat or 0), semver(errorat or 0) + local current = SILE.version and semver(SILE.version:match("v([0-9]*.[0-9]*.[0-9]*)")) or warnat + -- SILE.version is defined *after* most of SILE loads. It’s available at + -- runtime but not useful if we encounter deprecated code in core code. Users + -- will never encounter this failure, but as a developer it’s hard to test a + -- deprecation when core code refactoring is an all-or-nothing proposition. + -- Hence we fake it ‘till we make it, all deprecations internally are warnings. + local brackets = old:sub(1,1) == '\\' and "" or "()" + local _new = new and "Please use " .. (new .. brackets) .. " instead." or "Plase don't use it." + local msg = (old .. brackets) .. " was deprecated in SILE v" .. tostring(warnat) .. ". " .. _new .. (extra and ("\n\n" .. extra .. "\n") or "") + if errorat and current >= errorat then + SU.error(msg) + elseif warnat and current >= warnat then + SU.warn(msg) + end +end + --- Dump the contents of a any Lua type. -- For quick debugging, can be used on any number of any type of Lua value. Pretty-prints tables. -- @tparam any ... Any number of values @@ -247,84 +309,62 @@ utilities.dump = function (...) pl.pretty.dump(#arg == 1 and arg[1] or arg, "/dev/stderr") end ---- Concatenate values from a table using a given separator. --- Differs from `table.concat` in that all values are explicitly cast to strings, allowing debugging of tables that --- include functions, other tables, data types, etc. --- @tparam table array Input. --- @tparam[opt=" "] string separator Separator. -utilities.concat = function (array, separator) - return table.concat(utilities.map(tostring, array), separator) -end +local _skip_traceback_levels = 2 --- TODO: Unused, now deprecated? -utilities.inherit = function (orig, spec) - local new = pl.tablex.deepcopy(orig) - if spec then - for k,v in pairs(spec) do new[k] = v end - end - if new.init then new:init() end - return new +--- Raise an error and exit. +-- Outputs a warning message via `warn`, then finishes up anything it can without processing more content, then exits. +-- @tparam string message The error message to give. +-- @tparam boolean isbug Whether or not hitting this error is expected to be a code bug (as opposed to misakes in user input). +utilities.error = function (message, isbug) + _skip_traceback_levels = 3 + utilities.warn(message, isbug) + _skip_traceback_levels = 2 + io.stderr:flush() + SILE.outputter:finish() -- Only really useful from the REPL but no harm in trying + SILE.scratch.caughterror = true + error("", 2) end ---- Execute a callback function on each value in a table. --- @tparam function func Function to run on each value. --- @tparam table array Input list-like table. -utilities.map = function (func, array) - local new_array = {} - local last = #array - for i = 1, last do - new_array[i] = func(array[i]) - end - return new_array +--- Output an information message. +-- Basically like `warn`, except to source tracing information is added. +-- @tparam string message +utilities.msg = function (message) + if SILE.quiet then return end + io.stderr:write("\n! " .. message .. "\n") end ---- Iterate over key/value pairs in sequence of the sorted keys. --- Table iteration order with `pairs` is non-deterministic. This function returns an iterator that can be used in plais --- of `pairs` that will iterate through the values in the order of their *sorted* keys. --- @tparam table input Input table. --- @usage for val in SU.sortedpairs({ b: "runs second", a: "runs first" ) do print(val) end -utilities.sortedpairs = function (input) - local keys = {} - for k, _ in pairs(input) do - keys[#keys+1] = k - end - table.sort(keys, function(a, b) - if type(a) == type(b) then return a < b - elseif type(a) == "number" then return true - else return false - end - end) - return coroutine.wrap(function() - for i = 1, #keys do - coroutine.yield(keys[i], input[keys[i]]) +--- Output a warning. +-- Outputs a warning message including identifying where in the processing SILE is at when the warning is given. +-- @tparam string message The error message to give. +-- @tparam boolean isbug Whether or not hitting this warning is expected to be a code bug (as opposed to misakes in user input). +utilities.warn = function (message, isbug) + utilities.msg(message) + if SILE.traceback or isbug then + io.stderr:write(" at:\n" .. SILE.traceStack:locationTrace()) + if _skip_traceback_levels == 2 then + io.stderr:write(debug.traceback("", _skip_traceback_levels) or "\t! debug.traceback() did not identify code location") end - end) + else + io.stderr:write(" at " .. SILE.traceStack:locationHead()) + end + io.stderr:write("\n") end ---- Substitute a range of value(s) in one table with values from another. --- @tparam table array Table to modify. --- @tparam integer start First key to replace. --- @tparam integer stop Last key to replace. --- @tparam table replacement Table from which to pull key/values plairs to inject in array. --- @treturn table array First input array modified with values from replacement. -utilities.splice = function (array, start, stop, replacement) - local ptr = start - local room = stop - start + 1 - local last = replacement and #replacement or 0 - for i = 1, last do - if room > 0 then - room = room - 1 - array[ptr] = replacement[i] - else - table.insert(array, ptr, replacement[i]) - end - ptr = ptr + 1 - end +--- Math +-- @section math - for _ = 1, room do - table.remove(array, ptr) - end - return array +--- Check equality of floating point values. +-- Comparing floating point numbers using math functions in Lua may give different and unexpected answers depending on +-- the Lua VM and other environmental factors. This normalizes them using our standard internal epsilon value and +-- compares the absolute intereger value to avoid floating point number wierdness. +-- @tparam float lhs +-- @tparam float rhs +-- @treturn boolean +utilities.feq = function (lhs, rhs) + lhs = SU.cast("number", lhs) + rhs = SU.cast("number", rhs) + local abs = math.abs + return abs(lhs - rhs) <= epsilon * (abs(lhs) + abs(rhs)) end --- Add up all the values in a table. @@ -382,7 +422,7 @@ end --- Remove empty spaces from list-like tables -- Iterating list-like tables is hard if some values have been removed. This converts { 1 = "a", 3 = "b" } into --- { 1 = "a", 2 = "b" } which can be iterated using `ipairs()` without stopping after 1. +-- { 1 = "a", 2 = "b" } which can be iterated using `ipairs` without stopping after 1. -- @tparam table items List-like table potentially with holes. -- @treturn table List like table without holes. utilities.compress = function (items) @@ -422,66 +462,6 @@ utilities.allCombinations = function (options) end) end ---- Return the type of an object --- Like `type`, but also handles various SILE user data types. --- @tparam any value Any input value. If a table is one of SILE's classes or types, report on it's internal type. --- Otherwise use the output of `type`. --- @treturn string -utilities.type = function(value) - if type(value) == "number" then - return math.floor(value) == value and "integer" or "number" - elseif type(value) == "table" and value.prototype then - return value:prototype() - elseif type(value) == "table" and value.is_a then - return value.type - else - return type(value) - end -end - ---- Cast user intput to an expected type. --- If possible, converts input from one type to another. Not all types can be cast. For example "four" can't be cast to --- a number, but "4" or 4 can. Likewise "6pt" or 6 can be cast to a SILE.types.measurement, SILE.types.length, or even --- a SILE.types.node.glue, but not a SILE.types.color. --- @tparam string wantedType Expected type. --- @return A value of the type wantedType. -utilities.cast = function (wantedType, value) - local actualType = SU.type(value) - wantedType = string.lower(wantedType) - if wantedType:match(actualType) then return value - elseif actualType == "nil" and wantedType:match("nil") then return nil - elseif wantedType:match("length") then return SILE.types.length(value) - elseif wantedType:match("measurement") then return SILE.types.measurement(value) - elseif wantedType:match("vglue") then return SILE.types.node.vglue(value) - elseif wantedType:match("glue") then return SILE.types.node.glue(value) - elseif wantedType:match("kern") then return SILE.types.node.kern(value) - elseif actualType == "nil" then SU.error("Cannot cast nil to " .. wantedType) - elseif wantedType:match("boolean") then return SU.boolean(value) - elseif wantedType:match("string") then return tostring(value) - elseif wantedType:match("number") then - if type(value) == "table" and type(value.tonumber) == "function" then - return value:tonumber() - end - local num = tonumber(value) - if not num then SU.error("Cannot cast '" .. value .. "'' to " .. wantedType) end - return num - elseif wantedType:match("integer") then - local num - if type(value) == "table" and type(value.tonumber) == "function" then - num = value:tonumber() - else - num = tonumber(value) - end - if not num then SU.error("Cannot cast '" .. value .. "'' to " .. wantedType) end - if not wantedType:match("number") and num % 1 ~= 0 then - -- Could be an error but since it wasn't checked before, let's just warn: - -- Some packages might have wrongly typed settings, for instance. - SU.warn("Casting an integer but got a float number " .. num) - end - return num - else SU.error("Cannot cast to unrecognized type " .. wantedType) - end -end utilities.rateBadness = function(inf_bad, shortfall, spring) if spring == 0 then return inf_bad end @@ -498,8 +478,35 @@ utilities.rationWidth = function (target, width, ratio) return target end ---- Unicode --- @section utf8 +--- Text handling +-- @section text + +--- Iterate over a string split into tokens via a pattern. +-- @tparam string string Input string. +-- @tparam string pattern Pattern on which to split the input. +-- @treturn function An iterator function +-- @usage for str in SU.gtoke("foo-bar-baz", "-") do print(str) end +utilities.gtoke = function (string, pattern) + string = string and tostring(string) or '' + pattern = pattern and tostring(pattern) or "%s+" + local length = #string + return coroutine.wrap(function() + local index = 1 + repeat + local first, last = string:find(pattern, index) + if last then + if index < first then coroutine.yield({ string = string:sub(index, first - 1) }) end + coroutine.yield({ separator = string:sub(first, last) }) + index = last + 1 + else + if index <= length then + coroutine.yield({ string = string:sub(index) }) + end + break + end + until index > length + end) +end --- Convert a Unicode character to its corresponding codepoint. -- @tparam string uchar A single inicode character. @@ -542,6 +549,11 @@ utilities.utf8charfromcodepoint = function (codepoint) return val end +--- Convert a UTF-16 encoded string to a series of code points. +-- Like `luautf8.codes`, but for UTF-16 strings. +-- @tparam string ustr Input string. +-- @tparam string endian Either "le" or "be" depending on the enconding endedness. +-- @treturn string Serious of hex encoded code points. utilities.utf16codes = function (ustr, endian) local pos = 1 return function() @@ -575,7 +587,12 @@ utilities.utf16codes = function (ustr, endian) end end -utilities.splitUtf8 = function (str) -- Return an array of UTF8 strings each representing a Unicode char +--- Split a UTF-8 string into characters. +-- Lua's `string.split` will only explode a string by bytes. For text processing purposes it is usually more desirable +-- to split it into 1, 2, 3, or 4 byte grups matching the UTF-8 encoding. +-- @tparam string str Input UTF-8 encoded string. +-- @treturn table A list-like table of UTF8 strings each representing a Unicode char from the input string. +utilities.splitUtf8 = function (str) local rv = {} for _, cp in luautf8.next, str do table.insert(rv, luautf8.char(cp)) @@ -583,11 +600,21 @@ utilities.splitUtf8 = function (str) -- Return an array of UTF8 strings each rep return rv end +--- The last Unicode character in a UTF-8 encoded string. +-- Uses `SU.splitUtf8` to break an string into segments represtenting encoded characters, returns the last one. May be +-- more than one byte. +-- @tparam string str Input string. +-- @treturn string A single Unicode character. utilities.lastChar = function (str) local chars = utilities.splitUtf8(str) return chars[#chars] end +--- The first Unicode character in a UTF-8 encoded string. +-- Uses `SU.splitUtf8` to break an string into segments represtenting encoded characters, returns the first one. May be +-- more than one byte. +-- @tparam string str Input string. +-- @treturn string A single Unicode character. utilities.firstChar = function (str) local chars = utilities.splitUtf8(str) return chars[1] @@ -595,6 +622,12 @@ end local byte, floor, reverse = string.byte, math.floor, string.reverse +--- The Unicode character in a UTF-8 encoded string at a specifi position +-- Uses `SU.splitUtf8` to break an string into segments represtenting encoded characters, returns the Nth one. May be +-- more than one byte. +-- @tparam string str Input string. +-- @tparam number index Index of character to return. +-- @treturn string A single Unicode character. utilities.utf8charat = function (str, index) return str:sub(index):match("([%z\1-\127\194-\244][\128-\191]*)") end @@ -603,6 +636,9 @@ local utf16bom = function(endianness) return endianness == "be" and "\254\255" or endianness == "le" and "\255\254" or SU.error("Unrecognized endianness") end +--- Encode a string to a hexidecimal replesentation. +-- @tparam string str Input UTF-8 string +-- @treturn string Hexidecimal replesentation of str. utilities.hexencoded = function (str) local ustr = "" for i = 1, #str do @@ -611,6 +647,9 @@ utilities.hexencoded = function (str) return ustr end +--- Decode a hexidecimal replesentation into a string. +-- @tparam string str Input hexidecimal encoded string. +-- @treturn string UTF-8 string. utilities.hexdecoded = function (str) if #str % 2 == 1 then SU.error("Cannot decode hex string with odd len") end local ustr = "" @@ -640,9 +679,24 @@ local utf8_to_utf16 = function(str, endianness) return ustr end +--- Convert a UTF-8 string to big-endian UTF-16. +-- @tparam string str UTF-8 encoded string. +-- @treturn string Big-endian UTF-16 encoded string. utilities.utf8_to_utf16be = function (str) return utf8_to_utf16(str, "be") end + +--- Convert a UTF-8 string to little-endian UTF-16. +-- @tparam string str UTF-8 encoded string. +-- @treturn string Little-endian UTF-16 encoded string. utilities.utf8_to_utf16le = function (str) return utf8_to_utf16(str, "le") end + +--- Convert a UTF-8 string to big-endian UTF-16, then encode in hex. +-- @tparam string str UTF-8 encoded string. +-- @treturn string Hexidecimal representation of a big-endian UTF-16 encoded string. utilities.utf8_to_utf16be_hexencoded = function (str) return utilities.hexencoded(utilities.utf8_to_utf16be(str)) end + +--- Convert a UTF-8 string to little-endian UTF-16, then encode in hex. +-- @tparam string str UTF-8 encoded string. +-- @treturn string Hexidecimal representation of a little-endian UTF-16 encoded string. utilities.utf8_to_utf16le_hexencoded = function (str) return utilities.hexencoded(utilities.utf8_to_utf16le(str)) end local utf16_to_utf8 = function (str, endianness) @@ -656,7 +710,14 @@ local utf16_to_utf8 = function (str, endianness) return ustr end +--- Convert a big-endian UTF-16 string to UTF-8. +-- @tparam string str Big-endian UTF-16 encoded string. +-- @treturn string UTF-8 encoded string. utilities.utf16be_to_utf8 = function (str) return utf16_to_utf8(str, "be") end + +--- Convert a little-endian UTF-16 string to UTF-8. +-- @tparam string str Little-endian UTF-16 encoded string. +-- @treturn string UTF-8 encoded string. utilities.utf16le_to_utf8 = function (str) return utf16_to_utf8(str, "le") end utilities.breadcrumbs = function () From 78d90084c4cffbe9a7c06ae117f8b2f216c07430 Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Mon, 12 Feb 2024 17:28:25 +0300 Subject: [PATCH 15/20] refactor(utilities): Move AST debug function into related module --- core/utilities/ast.lua | 42 ++++++++++++++++++++++++++++++++++++++++ core/utilities/init.lua | 43 +---------------------------------------- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/core/utilities/ast.lua b/core/utilities/ast.lua index 3e5b27c6f..650d3469c 100644 --- a/core/utilities/ast.lua +++ b/core/utilities/ast.lua @@ -5,6 +5,48 @@ -- @type SU.ast local ast = {} +--- Output developer friendly debugging view of an AST. +-- @tparam table tree Abstract Syntax Tree. +-- @tparam integer level Starting level to review. +function ast.debug (tree, level) + if not tree then + SU.error("debugAST called with nil", true) + end + local out = string.rep(" ", 1+level) + if level == 0 then + SU.debug("ast", function () + return "[" .. SILE.currentlyProcessingFile + end) + end + if type(tree) == "function" then + SU.debug("ast", function () + return out .. tostring(tree) + end) + elseif type(tree) == "table" then + for _, content in ipairs(tree) do + if type(content) == "string" then + SU.debug("ast", function () + return out .. "[" .. content .. "]" + end) + elseif type(content) == "table" then + if SILE.Commands[content.command] then + SU.debug("ast", function () + return out .. "\\" .. content.command .. " " .. pl.pretty.write(content.options, "") + end) + if (#content>=1) then ast.debug(content, level+1) end + elseif content.id == "content" or (not content.command and not content.id) then + ast.debug(content, level+1) + else + SU.debug("ast", function () + return out .. "?\\" .. (content.command or content.id) + end) + end + end + end + end + if level == 0 then SU.debug("ast", "]") end +end + --- Find a command node in a SILE AST tree, -- looking only at the first level. -- (We're not reimplementing XPath here.) diff --git a/core/utilities/init.lua b/core/utilities/init.lua index 8dc00de0b..72749c318 100644 --- a/core/utilities/init.lua +++ b/core/utilities/init.lua @@ -232,48 +232,6 @@ utilities.debugging = function (category) return SILE.debugFlags.all and category ~= "profile" or SILE.debugFlags[category] end ---- Output developer friendly debugging view of an AST. --- @tparam table ast Abstract Syntax Tree. --- @tparam integer level Starting level to review. -utilities.debugAST = function (ast, level) - if not ast then - SU.error("debugAST called with nil", true) - end - local out = string.rep(" ", 1+level) - if level == 0 then - SU.debug("ast", function () - return "[" .. SILE.currentlyProcessingFile - end) - end - if type(ast) == "function" then - SU.debug("ast", function () - return out .. tostring(ast) - end) - elseif type(ast) == "table" then - for _, content in ipairs(ast) do - if type(content) == "string" then - SU.debug("ast", function () - return out .. "[" .. content .. "]" - end) - elseif type(content) == "table" then - if SILE.Commands[content.command] then - SU.debug("ast", function () - return out .. "\\" .. content.command .. " " .. pl.pretty.write(content.options, "") - end) - if (#content>=1) then utilities.debugAST(content, level+1) end - elseif content.id == "content" or (not content.command and not content.id) then - utilities.debugAST(content, level+1) - else - SU.debug("ast", function () - return out .. "?\\" .. (content.command or content.id) - end) - end - end - end - end - if level == 0 then SU.debug("ast", "]") end -end - --- Warn about use of a deprecated feature. -- Checks the current version and decides whether to warn or error, then oatputs a message with as much useful -- information as possible to make it easy for end users to update their usage. @@ -760,6 +718,7 @@ utilities.formatNumber = require("core.utilities.numbers") utilities.collatedSort = require("core.utilities.sorting") utilities.ast = require("core.utilities.ast") +utilities.debugAST = utilities.ast.debug utilities.subContent = function (content) SU.deprecated("SU.subContent", "SU.ast.subContent", "0.15.0", "0.17.0", [[ From d2701154a5ae711242878bda39a88bf1113a1c6b Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Mon, 12 Feb 2024 18:01:48 +0300 Subject: [PATCH 16/20] style(utilities): Normalize coding style of function definitions --- core/utilities/init.lua | 96 ++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/core/utilities/init.lua b/core/utilities/init.lua index 72749c318..eb274cc47 100644 --- a/core/utilities/init.lua +++ b/core/utilities/init.lua @@ -18,14 +18,14 @@ local epsilon = 1E-12 -- include functions, other tables, data types, etc. -- @tparam table array Input. -- @tparam[opt=" "] string separator Separator. -utilities.concat = function (array, separator) +function utilities.concat (array, separator) return table.concat(utilities.map(tostring, array), separator) end --- Execute a callback function on each value in a table. -- @tparam function func Function to run on each value. -- @tparam table array Input list-like table. -utilities.map = function (func, array) +function utilities.map (func, array) local new_array = {} local last = #array for i = 1, last do @@ -39,7 +39,7 @@ end -- @param name Name of the required option. -- @param context User friendly name of the function or calling context. -- @param required_type The name of a data type that the option must sucessfully cast to. -utilities.required = function (options, name, context, required_type) +function utilities.required (options, name, context, required_type) if not options[name] then utilities.error(context.." needs a "..name.." parameter") end if required_type then return utilities.cast(required_type, options[name]) @@ -52,7 +52,7 @@ end -- of `pairs` that will iterate through the values in the order of their *sorted* keys. -- @tparam table input Input table. -- @usage for val in SU.sortedpairs({ b: "runs second", a: "runs first" ) do print(val) end -utilities.sortedpairs = function (input) +function utilities.sortedpairs (input) local keys = {} for k, _ in pairs(input) do keys[#keys+1] = k @@ -76,7 +76,7 @@ end -- @tparam integer stop Last key to replace. -- @tparam table replacement Table from which to pull key/values plairs to inject in array. -- @treturn table array First input array modified with values from replacement. -utilities.splice = function (array, start, stop, replacement) +function utilities.splice (array, start, stop, replacement) local ptr = start local room = stop - start + 1 local last = replacement and #replacement or 0 @@ -97,7 +97,7 @@ utilities.splice = function (array, start, stop, replacement) end -- TODO: Unused, now deprecated? -utilities.inherit = function (orig, spec) +function utilities.inherit (orig, spec) local new = pl.tablex.deepcopy(orig) if spec then for k,v in pairs(spec) do new[k] = v end @@ -120,7 +120,7 @@ end -- @tparam nil|bool|string value Input value such as a string to evaluate for thruthyness. -- @tparam[opt=false] boolean default Whether to assume inputs that don't specifically evaluate to something should be true or false. -- @treturn boolean -utilities.boolean = function (value, default) +function utilities.boolean (value, default) if value == false then return false end if value == true then return true end if value == "false" then return false end @@ -139,7 +139,7 @@ end -- a SILE.types.node.glue, but not a SILE.types.color. -- @tparam string wantedType Expected type. -- @return A value of the type wantedType. -utilities.cast = function (wantedType, value) +function utilities.cast (wantedType, value) local actualType = SU.type(value) wantedType = string.lower(wantedType) if wantedType:match(actualType) then return value @@ -182,7 +182,7 @@ end -- @tparam any value Any input value. If a table is one of SILE's classes or types, report on it's internal type. -- Otherwise use the output of `type`. -- @treturn string -utilities.type = function(value) +function utilities.type (value) if type(value) == "number" then return math.floor(value) == value and "integer" or "number" elseif type(value) == "table" and value.prototype then @@ -208,7 +208,7 @@ end -- > glue = SILE.types.node.glue("6em") -- > SU.debug("foo", "A glue node", glue) -- [foo] A glue node G<6em> -utilities.debug = function (category, ...) +function utilities.debug (category, ...) if SILE.quiet then return end if utilities.debugging(category) then local inputs = pl.utils.pack(...) @@ -228,7 +228,7 @@ end --- Determine if a specific debug flag is set. -- @tparam string category Name of the flag status to check, e.g. "frames". -- @treturn boolean -utilities.debugging = function (category) +function utilities.debugging (category) return SILE.debugFlags.all and category ~= "profile" or SILE.debugFlags[category] end @@ -241,7 +241,7 @@ end -- a shim. -- @tparam string errorat The first release where the interface is no longer functional even with a shim. -- @tparam string extra Longer-form help to include in output separate from the expected one-liner of warning messages. -utilities.deprecated = function (old, new, warnat, errorat, extra) +function utilities.deprecated (old, new, warnat, errorat, extra) warnat, errorat = semver(warnat or 0), semver(errorat or 0) local current = SILE.version and semver(SILE.version:match("v([0-9]*.[0-9]*.[0-9]*)")) or warnat -- SILE.version is defined *after* most of SILE loads. It’s available at @@ -262,7 +262,7 @@ end --- Dump the contents of a any Lua type. -- For quick debugging, can be used on any number of any type of Lua value. Pretty-prints tables. -- @tparam any ... Any number of values -utilities.dump = function (...) +function utilities.dump (...) local arg = { ... } -- Avoid things that Lua stuffs in arg like args to self() pl.pretty.dump(#arg == 1 and arg[1] or arg, "/dev/stderr") end @@ -273,7 +273,7 @@ local _skip_traceback_levels = 2 -- Outputs a warning message via `warn`, then finishes up anything it can without processing more content, then exits. -- @tparam string message The error message to give. -- @tparam boolean isbug Whether or not hitting this error is expected to be a code bug (as opposed to misakes in user input). -utilities.error = function (message, isbug) +function utilities.error (message, isbug) _skip_traceback_levels = 3 utilities.warn(message, isbug) _skip_traceback_levels = 2 @@ -286,7 +286,7 @@ end --- Output an information message. -- Basically like `warn`, except to source tracing information is added. -- @tparam string message -utilities.msg = function (message) +function utilities.msg (message) if SILE.quiet then return end io.stderr:write("\n! " .. message .. "\n") end @@ -295,7 +295,7 @@ end -- Outputs a warning message including identifying where in the processing SILE is at when the warning is given. -- @tparam string message The error message to give. -- @tparam boolean isbug Whether or not hitting this warning is expected to be a code bug (as opposed to misakes in user input). -utilities.warn = function (message, isbug) +function utilities.warn (message, isbug) utilities.msg(message) if SILE.traceback or isbug then io.stderr:write(" at:\n" .. SILE.traceStack:locationTrace()) @@ -318,7 +318,7 @@ end -- @tparam float lhs -- @tparam float rhs -- @treturn boolean -utilities.feq = function (lhs, rhs) +function utilities.feq (lhs, rhs) lhs = SU.cast("number", lhs) rhs = SU.cast("number", rhs) local abs = math.abs @@ -328,7 +328,7 @@ end --- Add up all the values in a table. -- @tparam table array Input list-like table. -- @treturn number Sum of all values. -utilities.sum = function (array) +function utilities.sum (array) local total = 0 local last = #array for i = 1, last do @@ -340,7 +340,7 @@ end --- Return maximum value of inputs. -- `math.max`, but works on SILE types such as SILE.types.measurement. -- Lua <= 5.2 can't handle math operators on objects. -utilities.max = function (...) +function utilities.max (...) local input = pl.utils.pack(...) local max = table.remove(input, 1) for _, val in ipairs(input) do @@ -352,7 +352,7 @@ end --- Return minimum value of inputs. -- `math.min`, but works on SILE types such as SILE.types.measurement. -- Lua <= 5.2 can't handle math operators on objects. -utilities.min = function (...) +function utilities.min (...) local input = pl.utils.pack(...) local min = input[1] for _, val in ipairs(input) do @@ -372,7 +372,7 @@ end -- whereas normal LUA VMs can be cooerced. -- @tparam number input Input value. -- @treturn string Four-digit precision foating point. -utilities.debug_round = function (input) +function utilities.debug_round (input) if input > 0 then input = input + .00000000000001 end if input < 0 then input = input - .00000000000001 end return string.format("%.4f", input) @@ -383,7 +383,7 @@ end -- { 1 = "a", 2 = "b" } which can be iterated using `ipairs` without stopping after 1. -- @tparam table items List-like table potentially with holes. -- @treturn table List like table without holes. -utilities.compress = function (items) +function utilities.compress (items) local rv = {} local max = math.max(pl.utils.unpack(pl.tablex.keys(items))) for i = 1, max do if items[i] then rv[#rv+1] = items[i] end end @@ -392,7 +392,7 @@ end --- Reverse the order of a list-like table. -- @tparam table tbl Input list-like table. -utilities.flip_in_place = function (tbl) +function utilities.flip_in_place (tbl) local tmp, j for i = 1, math.floor(#tbl / 2) do tmp = tbl[i] @@ -403,7 +403,7 @@ utilities.flip_in_place = function (tbl) end -- TODO: Before documenting, consider whether this should be private to the one existing usage. -utilities.allCombinations = function (options) +function utilities.allCombinations (options) local count = 1 for i=1,#options do count = count * options[i] end return coroutine.wrap(function() @@ -421,13 +421,13 @@ utilities.allCombinations = function (options) end -utilities.rateBadness = function(inf_bad, shortfall, spring) +function utilities.rateBadness (inf_bad, shortfall, spring) if spring == 0 then return inf_bad end local bad = math.floor(100 * math.abs(shortfall / spring) ^ 3) return math.min(inf_bad, bad) end -utilities.rationWidth = function (target, width, ratio) +function utilities.rationWidth (target, width, ratio) if ratio < 0 and width.shrink:tonumber() > 0 then target:___add(width.shrink:tonumber() * ratio) elseif ratio > 0 and width.stretch:tonumber() > 0 then @@ -444,7 +444,7 @@ end -- @tparam string pattern Pattern on which to split the input. -- @treturn function An iterator function -- @usage for str in SU.gtoke("foo-bar-baz", "-") do print(str) end -utilities.gtoke = function (string, pattern) +function utilities.gtoke (string, pattern) string = string and tostring(string) or '' pattern = pattern and tostring(pattern) or "%s+" local length = #string @@ -469,7 +469,7 @@ end --- Convert a Unicode character to its corresponding codepoint. -- @tparam string uchar A single inicode character. -- @return number The Unicode code point where uchar is encoded. -utilities.codepoint = function (uchar) +function utilities.codepoint (uchar) local seq = 0 local val = -1 for i = 1, #uchar do @@ -491,7 +491,7 @@ end --- Covert a code point to a Unicode character. -- @tparam number|string codepoint Input code point value, either as a number or a string representing the decimal value "U+NNNN" or hex value "0xFFFF". -- @treturn string The character replestened by a codepoint descriptions. -utilities.utf8charfromcodepoint = function (codepoint) +function utilities.utf8charfromcodepoint (codepoint) local val = codepoint local cp = val local hex = (cp:match("[Uu]%+(%x+)") or cp:match("0[xX](%x+)")) @@ -512,7 +512,7 @@ end -- @tparam string ustr Input string. -- @tparam string endian Either "le" or "be" depending on the enconding endedness. -- @treturn string Serious of hex encoded code points. -utilities.utf16codes = function (ustr, endian) +function utilities.utf16codes (ustr, endian) local pos = 1 return function() if pos > #ustr then @@ -550,7 +550,7 @@ end -- to split it into 1, 2, 3, or 4 byte grups matching the UTF-8 encoding. -- @tparam string str Input UTF-8 encoded string. -- @treturn table A list-like table of UTF8 strings each representing a Unicode char from the input string. -utilities.splitUtf8 = function (str) +function utilities.splitUtf8 (str) local rv = {} for _, cp in luautf8.next, str do table.insert(rv, luautf8.char(cp)) @@ -563,7 +563,7 @@ end -- more than one byte. -- @tparam string str Input string. -- @treturn string A single Unicode character. -utilities.lastChar = function (str) +function utilities.lastChar (str) local chars = utilities.splitUtf8(str) return chars[#chars] end @@ -573,7 +573,7 @@ end -- more than one byte. -- @tparam string str Input string. -- @treturn string A single Unicode character. -utilities.firstChar = function (str) +function utilities.firstChar (str) local chars = utilities.splitUtf8(str) return chars[1] end @@ -586,7 +586,7 @@ local byte, floor, reverse = string.byte, math.floor, string.reverse -- @tparam string str Input string. -- @tparam number index Index of character to return. -- @treturn string A single Unicode character. -utilities.utf8charat = function (str, index) +function utilities.utf8charat (str, index) return str:sub(index):match("([%z\1-\127\194-\244][\128-\191]*)") end @@ -597,7 +597,7 @@ end --- Encode a string to a hexidecimal replesentation. -- @tparam string str Input UTF-8 string -- @treturn string Hexidecimal replesentation of str. -utilities.hexencoded = function (str) +function utilities.hexencoded (str) local ustr = "" for i = 1, #str do ustr = ustr..string.format("%02x", byte(str, i, i+1)) @@ -608,7 +608,7 @@ end --- Decode a hexidecimal replesentation into a string. -- @tparam string str Input hexidecimal encoded string. -- @treturn string UTF-8 string. -utilities.hexdecoded = function (str) +function utilities.hexdecoded (str) if #str % 2 == 1 then SU.error("Cannot decode hex string with odd len") end local ustr = "" for i = 1, #str, 2 do @@ -640,22 +640,22 @@ end --- Convert a UTF-8 string to big-endian UTF-16. -- @tparam string str UTF-8 encoded string. -- @treturn string Big-endian UTF-16 encoded string. -utilities.utf8_to_utf16be = function (str) return utf8_to_utf16(str, "be") end +function utilities.utf8_to_utf16be (str) return utf8_to_utf16(str, "be") end --- Convert a UTF-8 string to little-endian UTF-16. -- @tparam string str UTF-8 encoded string. -- @treturn string Little-endian UTF-16 encoded string. -utilities.utf8_to_utf16le = function (str) return utf8_to_utf16(str, "le") end +function utilities.utf8_to_utf16le (str) return utf8_to_utf16(str, "le") end --- Convert a UTF-8 string to big-endian UTF-16, then encode in hex. -- @tparam string str UTF-8 encoded string. -- @treturn string Hexidecimal representation of a big-endian UTF-16 encoded string. -utilities.utf8_to_utf16be_hexencoded = function (str) return utilities.hexencoded(utilities.utf8_to_utf16be(str)) end +function utilities.utf8_to_utf16be_hexencoded (str) return utilities.hexencoded(utilities.utf8_to_utf16be(str)) end --- Convert a UTF-8 string to little-endian UTF-16, then encode in hex. -- @tparam string str UTF-8 encoded string. -- @treturn string Hexidecimal representation of a little-endian UTF-16 encoded string. -utilities.utf8_to_utf16le_hexencoded = function (str) return utilities.hexencoded(utilities.utf8_to_utf16le(str)) end +function utilities.utf8_to_utf16le_hexencoded (str) return utilities.hexencoded(utilities.utf8_to_utf16le(str)) end local utf16_to_utf8 = function (str, endianness) local bom = utf16bom(endianness) @@ -671,14 +671,14 @@ end --- Convert a big-endian UTF-16 string to UTF-8. -- @tparam string str Big-endian UTF-16 encoded string. -- @treturn string UTF-8 encoded string. -utilities.utf16be_to_utf8 = function (str) return utf16_to_utf8(str, "be") end +function utilities.utf16be_to_utf8 (str) return utf16_to_utf8(str, "be") end --- Convert a little-endian UTF-16 string to UTF-8. -- @tparam string str Little-endian UTF-16 encoded string. -- @treturn string UTF-8 encoded string. -utilities.utf16le_to_utf8 = function (str) return utf16_to_utf8(str, "le") end +function utilities.utf16le_to_utf8 (str) return utf16_to_utf8(str, "le") end -utilities.breadcrumbs = function () +function utilities.breadcrumbs () local breadcrumbs = {} setmetatable (breadcrumbs, { @@ -720,28 +720,28 @@ utilities.collatedSort = require("core.utilities.sorting") utilities.ast = require("core.utilities.ast") utilities.debugAST = utilities.ast.debug -utilities.subContent = function (content) +function utilities.subContent (content) SU.deprecated("SU.subContent", "SU.ast.subContent", "0.15.0", "0.17.0", [[ Note that the new implementation no longer introduces an id="stuff" key.]]) return utilities.ast.subContent(content) end -utilities.hasContent = function(content) +function utilities.hasContent (content) SU.deprecated("SU.hasContent", "SU.ast.hasContent", "0.15.0", "0.17.0") return SU.ast.hasContent(content) end -utilities.contentToString = function (content) +function utilities.contentToString (content) SU.deprecated("SU.contentToString", "SU.ast.contentToString", "0.15.0", "0.17.0") return SU.ast.contentToString(content) end -utilities.walkContent = function (content, action) +function utilities.walkContent (content, action) SU.deprecated("SU.walkContent", "SU.ast.walkContent", "0.15.0", "0.17.0") SU.ast.walkContent(content, action) end -utilities.stripContentPos = function (content) +function utilities.stripContentPos (content) SU.deprecated("SU.stripContentPos", "SU.ast.stripContentPos", "0.15.0", "0.17.0") return SU.ast.stripContentPos(content) end From 8ec9c0316c8e85587cd561daa3843cfa6794c026 Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Tue, 13 Feb 2024 21:08:38 +0300 Subject: [PATCH 17/20] ci(tooling): Avoid needing API doc tooling for testing runs --- .cirrus.yml | 2 +- .github/workflows/coverage.yml | 2 +- .github/workflows/test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index c50746cdc..f9d02041e 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -42,7 +42,7 @@ task: - ./bootstrap.sh configure_script: | ./configure MAKE=gmake \ - --enable-developer LUAROCKS=false LUACHECK=false BUSTED=false DELTA=cat PDFINFO=false NIX=false NPM=false DOCKER=false \ + --enable-developer LDOC=false LUAROCKS=false LUACHECK=false BUSTED=false DELTA=cat PDFINFO=false NIX=false NPM=false DOCKER=false \ --disable-font-variations \ --with-system-lua-sources \ --with-system-luarocks \ diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 4aa41a092..975828a86 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -60,7 +60,7 @@ jobs: run: | ./bootstrap.sh ./configure \ - --enable-developer LUACHECK=false NIX=false DELTA=cat \ + --enable-developer LDOC=false LUACHECK=false NIX=false DELTA=cat \ --disable-font-variations \ --without-manual - name: Make diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d3e7f9de..abad02550 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -71,7 +71,7 @@ jobs: ./bootstrap.sh ./configure \ ${{ matrix.luaVersion[1] }} \ - --enable-developer LUACHECK=false NIX=false DELTA=cat \ + --enable-developer LDOC=false LUACHECK=false NIX=false DELTA=cat \ --disable-font-variations \ --with${{ !startsWith(matrix.luaVersion[0], 'luajit') && 'out' || '' }}-luajit \ --without-system-luarocks \ From e8ec8628617dca68c2f10f47edf926ad41fd0ed8 Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Tue, 13 Feb 2024 22:11:16 +0300 Subject: [PATCH 18/20] refactor(core): Move global library handling to its own module --- core/globals.lua | 19 +++++++++++++++++++ core/sile.lua | 23 +++-------------------- 2 files changed, 22 insertions(+), 20 deletions(-) create mode 100644 core/globals.lua diff --git a/core/globals.lua b/core/globals.lua new file mode 100644 index 000000000..d202b039b --- /dev/null +++ b/core/globals.lua @@ -0,0 +1,19 @@ +--- Global library provisions. +-- @module globals +-- @alias _G + +--- Penlight. +-- On-demand module loader, provided for SILE and document usage +_G.pl = require("pl.import_into")() + +--- UTF-8 String handler. +-- Lua 5.3+ has a UTF-8 safe string function module but it is somewhat +-- underwhelming. This module includes more functions and supports older Lua +-- versions. Docs: https://github.com/starwing/luautf8 +_G.luautf8 = require("lua-utf8") + +--- Fluent localization library. +_G.fluent = require("fluent")() + +-- For developer testing only, usually in CI +if os.getenv("SILE_COVERAGE") then require("luacov") end diff --git a/core/sile.lua b/core/sile.lua index c41270f93..d42d40177 100644 --- a/core/sile.lua +++ b/core/sile.lua @@ -1,27 +1,10 @@ --- The core SILE library -- @module SILE ---- Global library provisions. --- @section globals --- Loading SILE foo +-- Placeholder for 3rd party Lua libraries SILE always provides as globals +require("core.globals") ---- Penlight. --- On-demand module loader, provided for SILE and document usage -pl = require("pl.import_into")() - --- For developer testing only, usually in CI -if os.getenv("SILE_COVERAGE") then require("luacov") end - ---- UTF-8 String handler. --- Lua 5.3+ has a UTF-8 safe string function module but it is somewhat --- underwhelming. This module includes more functions and supports older Lua --- versions. Docs: https://github.com/starwing/luautf8 -luautf8 = require("lua-utf8") - ---- Fluent localization library. -fluent = require("fluent")() - --- Reserve global scope placeholder for profiler (developer tooling) +-- Reserve scope placeholder for profiler (developer tooling) local ProFi -- Placeholder for SILE internals table From 296b808ecbd71292a03c3638e74b9653fbcd89b9 Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Tue, 13 Feb 2024 22:11:39 +0300 Subject: [PATCH 19/20] docs(api): Expand API docs, cover globals and some class and package stuff --- classes/base.lua | 12 ++++++++++++ core/globals.lua | 19 +++++++++++++------ core/sile.lua | 4 ++-- packages/base.lua | 12 ++++++++++++ 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/classes/base.lua b/classes/base.lua index fc4ed7195..05be90033 100644 --- a/classes/base.lua +++ b/classes/base.lua @@ -280,6 +280,18 @@ function class:runHooks (category, options) end end +--- Register a function as a SILE command. +-- Takes any Lua function and registers it for use as a SILE command (which will in turn be used to process any content +-- nodes identified with the command name. +-- +-- Note that this should only be used to register commands supplied directly by a document class. A similar method is +-- available for packages, `packages:registerCommand`. +-- @tparam string name Name of cammand to register. +-- @tparam function func Callback function to use as command handler. +-- @tparam[opt] nil|string help User friendly short usage string for use in error messages, documentation, etc. +-- @tparam[opt] nil|string pack Information identifying the module registering the command for use in error and usage +-- messages. Usually auto-detected. +-- @see SILE.packages:registerCommand function class.registerCommand (_, name, func, help, pack) SILE.Commands[name] = func if not pack then diff --git a/core/globals.lua b/core/globals.lua index d202b039b..041de291b 100644 --- a/core/globals.lua +++ b/core/globals.lua @@ -2,17 +2,24 @@ -- @module globals -- @alias _G ---- Penlight. --- On-demand module loader, provided for SILE and document usage +--- Penlight od-demand loader. +-- The Lua language adopts a "no batteries included" philosophy by providing a minimal standard library. Penlight is +-- a widely used set libraries for making it easier to work with common tasks. Loading SILE implies that the PEnlight +-- on-demand module loader is available, allowing any Penlight functions to be accessed using the `pl` prefix. Consult +-- the [Penlight documentation](https://lunarmodules.github.io/Penlight/) for specifics of the utilities available. _G.pl = require("pl.import_into")() ---- UTF-8 String handler. --- Lua 5.3+ has a UTF-8 safe string function module but it is somewhat --- underwhelming. This module includes more functions and supports older Lua --- versions. Docs: https://github.com/starwing/luautf8 +--- UTF-8 string library. +-- LuaJIT 5.1 and 5.2's `string` module only handle strings as bytes. Lua 5.3+ has a UTF-8 safe `string` module, but its +-- feature set is somewhat underwhelming. This module includes more functions and levels the playing field no matter +-- which Lua VM is being used. See [luautf8 docs](https://github.com/starwing/luautf8) for more details. _G.luautf8 = require("lua-utf8") --- Fluent localization library. +-- For handling messages in various languages SILE provides an implementation of [Project +-- Fluent](https://projectfluent.org/)'s localization system (originally developed by Mozilla for use in Firefox). This +-- global is an instantiated interface to [fluent-lua](https://github.com/alerque/fluent-lua) pre-loaded with resources +-- for all the langugaes and regions SILE has support for. _G.fluent = require("fluent")() -- For developer testing only, usually in CI diff --git a/core/sile.lua b/core/sile.lua index d42d40177..431ce3396 100644 --- a/core/sile.lua +++ b/core/sile.lua @@ -576,8 +576,8 @@ end -- @tparam[opt] nil|string help User friendly short usage string for use in error messages, documentation, etc. -- @tparam[opt] nil|string pack Information identifying the module registering the command for use in error and usage -- messages. Usually auto-detected. --- @see SILE.classes --- @see SILE.packages +-- @see SILE.classes:registerCommand +-- @see SILE.packages:registerCommand function SILE.registerCommand (name, func, help, pack, cheat) local class = SILE.documentState.documentClass if not cheat then diff --git a/packages/base.lua b/packages/base.lua index b55a1b2c6..6fae09fec 100644 --- a/packages/base.lua +++ b/packages/base.lua @@ -65,6 +65,18 @@ function package.registerCommands (_) end -- This gives us a hook to match commands with the packages that registered -- them as opposed to core commands or class-provided commands + +--- Register a function as a SILE command. +-- Takes any Lua function and registers it for use as a SILE command (which will in turn be used to process any content +-- nodes identified with the command name. +-- +-- A similar method is available for classes, `classes:registerCommand`. +-- @tparam string name Name of cammand to register. +-- @tparam function func Callback function to use as command handler. +-- @tparam[opt] nil|string help User friendly short usage string for use in error messages, documentation, etc. +-- @tparam[opt] nil|string pack Information identifying the module registering the command for use in error and usage +-- messages. Usually auto-detected. +-- @see SILE.classes:registerCommand function package:registerCommand (name, func, help, pack) self.class:registerCommand(name, func, help, pack) end From ba879807d1986f83633c3df73819fe9ef0e69b2e Mon Sep 17 00:00:00 2001 From: Caleb Maclennan Date: Tue, 13 Feb 2024 23:23:19 +0300 Subject: [PATCH 20/20] docs(api): Introduce new top level types to LDoc to organize stuff --- .editorconfig | 2 +- build-aux/config.ld | 26 +++++++++++++++----------- classes/base.lua | 2 +- core/languages.lua | 2 +- inputters/base.lua | 2 +- languages/fr.lua | 2 +- outputters/base.lua | 2 +- packages/base.lua | 2 +- pagebuilders/base.lua | 2 +- shapers/base.lua | 2 +- types/color.lua | 2 +- types/length.lua | 3 +-- types/measurement.lua | 3 +-- types/node.lua | 2 +- types/unit.lua | 2 +- typesetters/base.lua | 2 +- 16 files changed, 30 insertions(+), 28 deletions(-) diff --git a/.editorconfig b/.editorconfig index c8de8f6b5..b877137e0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,7 +17,7 @@ indent_size = 8 [*.md] trim_trailing_whitespace = false -[{*.lua,*.lua.in,sile.in,*rockspec,.busted,.luacheckrc}] +[{*.lua,*.lua.in,sile.in,*rockspec,.busted,.luacheckrc,config.ld}] indent_style = space indent_size = 3 max_line_length = 120 diff --git a/build-aux/config.ld b/build-aux/config.ld index ce5c320ef..e1d4634e0 100644 --- a/build-aux/config.ld +++ b/build-aux/config.ld @@ -4,16 +4,20 @@ readme = "../README.md" dir = "../lua-api-docs" format = "discount" file = { - "../sile-lua", - "../core/", - "../classes/", - "../inputters/", - "../languages/", - "../outputters/", - "../packages/", - "../pagebuilders/", - "../shapers/", - "../types/", - "../typesetters/", + "../sile-lua", + "../core/", + "../classes/", + "../inputters/", + "../languages/", + "../outputters/", + "../packages/", + "../pagebuilders/", + "../shapers/", + "../types/", + "../typesetters/", } merge = true +no_space_before_args = true +sort_modules = true +new_type("interfaces", "Interfaces", true) +new_type("types", "Types", true) diff --git a/classes/base.lua b/classes/base.lua index 05be90033..6462b3cd6 100644 --- a/classes/base.lua +++ b/classes/base.lua @@ -1,5 +1,5 @@ --- SILE document class class. --- @classmod SILE.classes +-- @interfaces classes local class = pl.class() class.type = "class" diff --git a/core/languages.lua b/core/languages.lua index dca7e65ef..67c93471e 100644 --- a/core/languages.lua +++ b/core/languages.lua @@ -1,5 +1,5 @@ --- SILE language class. --- @classmod SILE.languages +-- @interfaces languages local loadkit = require("loadkit") local cldr = require("cldr") diff --git a/inputters/base.lua b/inputters/base.lua index 9a476a00e..cd609669c 100644 --- a/inputters/base.lua +++ b/inputters/base.lua @@ -1,5 +1,5 @@ --- SILE inputter class. --- @classmod SILE.inputters +-- @interfaces inputters local _deprecated = [[ You appear to be using a document class '%s' programmed for SILE <= v0.12.5. diff --git a/languages/fr.lua b/languages/fr.lua index 919e75e7d..f9df5e967 100644 --- a/languages/fr.lua +++ b/languages/fr.lua @@ -1,5 +1,5 @@ --- French language rules --- @submodule SILE.languages +-- @submodule languages local computeSpaces = function() -- Computes: diff --git a/outputters/base.lua b/outputters/base.lua index e337524fe..51cc36e59 100644 --- a/outputters/base.lua +++ b/outputters/base.lua @@ -1,5 +1,5 @@ --- SILE outputter class. --- @classmod SILE.outputters +-- @interfaces outputters local outputter = pl.class() outputter.type = "outputter" diff --git a/packages/base.lua b/packages/base.lua index 6fae09fec..26754d8f1 100644 --- a/packages/base.lua +++ b/packages/base.lua @@ -1,5 +1,5 @@ --- SILE package class. --- @classmod SILE.packages +-- @interfaces packages local package = pl.class() package.type = "package" diff --git a/pagebuilders/base.lua b/pagebuilders/base.lua index 94063d337..2eef602f5 100644 --- a/pagebuilders/base.lua +++ b/pagebuilders/base.lua @@ -1,5 +1,5 @@ --- SILE pagebuilder class. --- @classmod SILE.pagebuilders +-- @interfaces pagebuilders local pagebuilder = pl.class() pagebuilder.type = "pagebuilder" diff --git a/shapers/base.lua b/shapers/base.lua index ed1e00448..ec4ddfb36 100644 --- a/shapers/base.lua +++ b/shapers/base.lua @@ -1,5 +1,5 @@ --- SILE shaper class. --- @classmod SILE.shapers +-- @interfaces shapers -- local smallTokenSize = 20 -- Small words will be cached -- local shapeCache = {} diff --git a/types/color.lua b/types/color.lua index a95ea6692..e436915b6 100644 --- a/types/color.lua +++ b/types/color.lua @@ -1,5 +1,5 @@ --- SILE color type. --- @classmod SILE.types.color +-- @types color local colornames = { aliceblue = { 240, 248, 255 }, diff --git a/types/length.lua b/types/length.lua index 8f5d7d6d7..3de74b519 100644 --- a/types/length.lua +++ b/types/length.lua @@ -1,6 +1,5 @@ --- SILE length type. --- @classmod SILE.types.length --- @within Types +-- @types length local function _error_if_not_number (a) if type(a) ~= "number" then diff --git a/types/measurement.lua b/types/measurement.lua index c9cd56438..5a44d2ab8 100644 --- a/types/measurement.lua +++ b/types/measurement.lua @@ -1,6 +1,5 @@ --- SILE measurement type. --- @classmod SILE.types.measurement --- +-- @types measurement local function _tonumber (amount) return SU.cast("number", amount) diff --git a/types/node.lua b/types/node.lua index 5592b2b8c..c8ddeadd7 100644 --- a/types/node.lua +++ b/types/node.lua @@ -1,5 +1,5 @@ --- SILE node type. --- @classmod SILE.types.node +-- @types node local nodetypes = {} diff --git a/types/unit.lua b/types/unit.lua index f99fc4878..d5d43c255 100644 --- a/types/unit.lua +++ b/types/unit.lua @@ -1,5 +1,5 @@ --- SILE unit type. --- @classmod SILE.types.unit +-- @types unit local bits = require("core.parserbits") diff --git a/typesetters/base.lua b/typesetters/base.lua index 729038135..6572ca4f0 100644 --- a/typesetters/base.lua +++ b/typesetters/base.lua @@ -1,5 +1,5 @@ --- SILE typesetter class. --- @classmod SILE.typesetters +-- @interfaces typesetters --- @type typesetter local typesetter = pl.class()