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/.commitlintrc.yml b/.commitlintrc.yml index 7933d173f..50eba6a57 100644 --- a/.commitlintrc.yml +++ b/.commitlintrc.yml @@ -39,6 +39,7 @@ rules: - inputters - installation - languages + - api - manpage - manual - math 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/.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 \ 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..e1d4634e0 --- /dev/null +++ b/build-aux/config.ld @@ -0,0 +1,23 @@ +project = "SILE" +description = "The SILE Typesetter" +readme = "../README.md" +dir = "../lua-api-docs" +format = "discount" +file = { + "../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/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 diff --git a/classes/base.lua b/classes/base.lua index 38b97bd14..6462b3cd6 100644 --- a/classes/base.lua +++ b/classes/base.lua @@ -1,3 +1,6 @@ +--- SILE document class class. +-- @interfaces classes + local class = pl.class() class.type = "class" class._name = "base" @@ -277,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/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]) 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/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/globals.lua b/core/globals.lua new file mode 100644 index 000000000..041de291b --- /dev/null +++ b/core/globals.lua @@ -0,0 +1,26 @@ +--- Global library provisions. +-- @module globals +-- @alias _G + +--- 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 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 +if os.getenv("SILE_COVERAGE") then require("luacov") end diff --git a/core/languages.lua b/core/languages.lua index 180ff7104..67c93471e 100644 --- a/core/languages.lua +++ b/core/languages.lua @@ -1,3 +1,6 @@ +--- SILE language class. +-- @interfaces languages + local loadkit = require("loadkit") local cldr = require("cldr") diff --git a/core/settings.lua b/core/settings.lua index eae985a7a..7b1740535 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() @@ -98,12 +102,14 @@ 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 local previous = self.state @@ -118,6 +124,8 @@ function settings:popState () end end +--- Declare a new setting +--- @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 @@ -135,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 @@ -144,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 @@ -158,6 +165,9 @@ function settings:toplevelState () end end +--- Get the value of a setting +-- @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. @@ -175,6 +185,11 @@ function settings:get (parameter) end end +--- Set the value of a setting +-- @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 @@ -219,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 @@ -232,6 +253,9 @@ function settings:runHooks (parameter, value) 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()`) +-- @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() @@ -239,7 +263,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. +--- @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) return function(content) diff --git a/core/sile.lua b/core/sile.lua index 14d08ac10..431ce3396 100644 --- a/core/sile.lua +++ b/core/sile.lua @@ -1,49 +1,69 @@ --- Initialize SILE internals +--- The core SILE library +-- @module SILE + +-- Placeholder for 3rd party Lua libraries SILE always provides as globals +require("core.globals") + +-- Reserve scope placeholder for profiler (developer tooling) +local ProFi + +-- Placeholder for SILE internals table SILE = {} +--- 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. +-- @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] 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")() +--- Modules +-- @section modules --- Includes for _this_ scope -local lfs = require("lfs") - --- Developer tooling profiler -local ProFi +--- Utilities module, typically accessed via `SU` alias. +-- @see SU +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") -SILE.utilities = require("core.utilities") -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) @@ -54,27 +74,78 @@ 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 = {}, evaluateAfters = {}, uses = {}, options = {}, - preambles = {}, - postambles = {}, + preambles = {}, -- deprecated, undocumented + postambles = {}, -- deprecated, undocumented } -- Internal libraries that are idempotent and return classes that need instantiation @@ -111,7 +182,21 @@ local function runEvals (evals, arg) end end -SILE.init = function () +--- 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). +function SILE.init () if not SILE.backend then SILE.backend = "libtexpdf" end @@ -172,7 +257,14 @@ local function suggest_luarocks (module) ) end -SILE.use = function (module, options, reload) +--- 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. +function SILE.use (module, options, reload) local status, pack if type(module) == "string" then status, pack = pcall(require, module) @@ -218,7 +310,10 @@ SILE.use = function (module, options, reload) end end -SILE.require = function (dependency, pathprefix, deprecation_ack) +-- --- 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 +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 @@ -261,7 +356,11 @@ SILE.require = function (dependency, pathprefix, deprecation_ack) return lib end -SILE.process = function (ast) +--- 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). +function SILE.process (ast) if not ast then return end if SU.debugging("ast") then SU.debugAST(ast, 0) @@ -313,6 +412,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 @@ -344,7 +449,14 @@ 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 lfs = require("lfs") local doc if filename == "-" then filename = "STDIN" @@ -388,7 +500,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) @@ -400,7 +512,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 @@ -428,6 +543,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 {} @@ -444,6 +565,19 @@ 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. +-- @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 @@ -457,16 +591,22 @@ function SILE.registerCommand (name, func, help, pack, cheat) return class:registerCommand(name, func, help, pack) end -function SILE.setCommandDefaults (command, defaults) +--- 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, options) 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.types.unit[unit] then @@ -480,13 +620,23 @@ 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. 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. +-- 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 diff --git a/core/utilities/ast.lua b/core/utilities/ast.lua index b1c4e6d25..650d3469c 100644 --- a/core/utilities/ast.lua +++ b/core/utilities/ast.lua @@ -1,13 +1,58 @@ ---- SILE AST utilities --- +--- AST utilities. +-- Functions for working with SILE's Abstract Syntax Trees. +-- @module SU.ast + +-- @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.) ----@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 @@ -17,10 +62,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 @@ -30,12 +75,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 {} @@ -54,12 +99,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 {} @@ -78,9 +123,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 @@ -99,8 +144,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 @@ -121,10 +166,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 @@ -145,9 +190,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 @@ -159,12 +204,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 @@ -183,9 +228,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 @@ -204,8 +249,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 8e8d5a2e9..eb274cc47 100644 --- a/core/utilities/init.lua +++ b/core/utilities/init.lua @@ -1,3 +1,7 @@ +--- SILE.utilities (aliased as SU) +-- @module SU +-- @alias utilities + local bitshim = require("bitshim") local luautf8 = require("lua-utf8") local semver = require("semver") @@ -6,7 +10,36 @@ local utilities = {} local epsilon = 1E-12 -utilities.required = function (options, name, context, required_type) +--- 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. +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. +function utilities.map (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. +-- @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. +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]) @@ -14,87 +47,201 @@ 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 +function utilities.sortedpairs (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. +function utilities.splice (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? +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 + 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 -utilities.boolean = function (value, default) +--- 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 +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 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 - -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) +--- 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. +function utilities.cast (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.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") - end +--- 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 +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 + return value:prototype() + elseif type(value) == "table" and value.is_a then + return value.type else - io.stderr:write(" at " .. SILE.traceStack:locationHead()) + return type(value) end - io.stderr:write("\n") end -utilities.msg = function (message) +--- 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 +-- 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> +function utilities.debug (category, ...) if SILE.quiet then return end - io.stderr:write("\n! " .. message .. "\n") + if utilities.debugging(category) then + local inputs = pl.utils.pack(...) + for i, input in ipairs(inputs) do + if type(input) == "function" then + local status, output = pcall(input) + inputs[i] = status and output or SU.warn(("Output of %s debug function was an error: %s"):format(category, output)) + elseif type(input) ~= "string" then + inputs[i] = tostring(input) + end + end + local message = utilities.concat(inputs, " ") + if message then io.stderr:write(("\n[%s] %s"):format(category, message)) end + end end -utilities.debugging = function (category) +--- Determine if a specific debug flag is set. +-- @tparam string category Name of the flag status to check, e.g. "frames". +-- @treturn boolean +function utilities.debugging (category) return SILE.debugFlags.all and category ~= "profile" or SILE.debugFlags[category] end -utilities.feq = function (lhs, rhs) -- Float point equal - lhs = SU.cast("number", lhs) - rhs = SU.cast("number", rhs) - local abs = math.abs - return abs(lhs - rhs) <= epsilon * (abs(lhs) + abs(rhs)) -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 - -utilities.deprecated = function (old, new, warnat, errorat, extra) +--- 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. +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 @@ -112,128 +259,76 @@ utilities.deprecated = function (old, new, warnat, errorat, extra) end end -utilities.debug = function (category, ...) - if SILE.quiet then return end - if utilities.debugging(category) then - local inputs = pl.utils.pack(...) - for i, input in ipairs(inputs) do - if type(input) == "function" then - local status, output = pcall(input) - inputs[i] = status and output or SU.warn(("Output of %s debug function was an error: %s"):format(category, output)) - elseif type(input) ~= "string" then - inputs[i] = tostring(input) - end - end - local message = utilities.concat(inputs, " ") - if message then io.stderr:write(("\n[%s] %s"):format(category, message)) end - end -end - -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 - -utilities.dump = function (...) +--- 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 +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 -utilities.concat = function (array, separator) - return table.concat(utilities.map(tostring, array), separator) -end +local _skip_traceback_levels = 2 -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). +function utilities.error (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 -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 +function utilities.msg (message) + if SILE.quiet then return end + io.stderr:write("\n! " .. message .. "\n") 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). +function utilities.warn (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 -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 +function utilities.feq (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 -utilities.sum = function (array) +--- Add up all the values in a table. +-- @tparam table array Input list-like table. +-- @treturn number Sum of all values. +function utilities.sum (array) local total = 0 local last = #array for i = 1, last do @@ -242,8 +337,10 @@ utilities.sum = function (array) return total end --- Lua <= 5.2 can't handle objects in math functions -utilities.max = function (...) +--- 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. +function utilities.max (...) local input = pl.utils.pack(...) local max = table.remove(input, 1) for _, val in ipairs(input) do @@ -252,7 +349,10 @@ utilities.max = function (...) return max end -utilities.min = function (...) +--- 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. +function utilities.min (...) local input = pl.utils.pack(...) local min = input[1] for _, val in ipairs(input) do @@ -261,6 +361,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 @@ -269,20 +370,29 @@ 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. -utilities.debug_round = function (input) +-- @tparam number input Input value. +-- @treturn string Four-digit precision foating point. +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) end -utilities.compress = function (items) +--- 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. +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 return rv end -utilities.flip_in_place = function (tbl) +--- Reverse the order of a list-like table. +-- @tparam table tbl Input list-like table. +function utilities.flip_in_place (tbl) local tmp, j for i = 1, math.floor(#tbl / 2) do tmp = tbl[i] @@ -292,7 +402,8 @@ utilities.flip_in_place = function (tbl) end end -utilities.allCombinations = function (options) +-- TODO: Before documenting, consider whether this should be private to the one existing usage. +function utilities.allCombinations (options) local count = 1 for i=1,#options do count = count * options[i] end return coroutine.wrap(function() @@ -309,63 +420,14 @@ utilities.allCombinations = function (options) end) end -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 - -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) +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 @@ -374,13 +436,40 @@ utilities.rationWidth = function (target, width, ratio) return target end --- Unicode-related utilities -utilities.utf8char = function (c) - utilities.deprecated("SU.utf8char", "luautf8.char", "0.11.0", "0.12.0") - return luautf8.char(c) +--- 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 +function utilities.gtoke (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 -utilities.codepoint = function (uchar) +--- 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. +function utilities.codepoint (uchar) local seq = 0 local val = -1 for i = 1, #uchar do @@ -399,7 +488,10 @@ utilities.codepoint = function (uchar) return val end -utilities.utf8charfromcodepoint = function (codepoint) +--- 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. +function utilities.utf8charfromcodepoint (codepoint) local val = codepoint local cp = val local hex = (cp:match("[Uu]%+(%x+)") or cp:match("0[xX](%x+)")) @@ -415,12 +507,12 @@ 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) +--- 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. +function utilities.utf16codes (ustr, endian) local pos = 1 return function() if pos > #ustr then @@ -453,7 +545,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. +function utilities.splitUtf8 (str) local rv = {} for _, cp in luautf8.next, str do table.insert(rv, luautf8.char(cp)) @@ -461,19 +558,35 @@ utilities.splitUtf8 = function (str) -- Return an array of UTF8 strings each rep return rv end -utilities.lastChar = function (str) +--- 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. +function utilities.lastChar (str) local chars = utilities.splitUtf8(str) return chars[#chars] end -utilities.firstChar = function (str) +--- 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. +function utilities.firstChar (str) local chars = utilities.splitUtf8(str) return chars[1] end local byte, floor, reverse = string.byte, math.floor, string.reverse -utilities.utf8charat = function (str, index) +--- 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. +function utilities.utf8charat (str, index) return str:sub(index):match("([%z\1-\127\194-\244][\128-\191]*)") end @@ -481,7 +594,10 @@ local utf16bom = function(endianness) return endianness == "be" and "\254\255" or endianness == "le" and "\255\254" or SU.error("Unrecognized endianness") end -utilities.hexencoded = function (str) +--- Encode a string to a hexidecimal replesentation. +-- @tparam string str Input UTF-8 string +-- @treturn string Hexidecimal replesentation of str. +function utilities.hexencoded (str) local ustr = "" for i = 1, #str do ustr = ustr..string.format("%02x", byte(str, i, i+1)) @@ -489,7 +605,10 @@ utilities.hexencoded = function (str) return ustr end -utilities.hexdecoded = function (str) +--- Decode a hexidecimal replesentation into a string. +-- @tparam string str Input hexidecimal encoded string. +-- @treturn string UTF-8 string. +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 @@ -518,10 +637,25 @@ local utf8_to_utf16 = function(str, endianness) return ustr end -utilities.utf8_to_utf16be = function (str) return utf8_to_utf16(str, "be") end -utilities.utf8_to_utf16le = function (str) return utf8_to_utf16(str, "le") end -utilities.utf8_to_utf16be_hexencoded = function (str) return utilities.hexencoded(utilities.utf8_to_utf16be(str)) end -utilities.utf8_to_utf16le_hexencoded = function (str) return utilities.hexencoded(utilities.utf8_to_utf16le(str)) 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. +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. +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. +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. +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) @@ -534,10 +668,17 @@ local utf16_to_utf8 = function (str, endianness) return ustr end -utilities.utf16be_to_utf8 = function (str) return utf16_to_utf8(str, "be") end -utilities.utf16le_to_utf8 = function (str) return utf16_to_utf8(str, "le") 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. +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. +function utilities.utf16le_to_utf8 (str) return utf16_to_utf8(str, "le") end -utilities.breadcrumbs = function () +function utilities.breadcrumbs () local breadcrumbs = {} setmetatable (breadcrumbs, { @@ -577,29 +718,30 @@ 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) +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 diff --git a/core/utilities/numbers.lua b/core/utilities/numbers.lua index 6bb6dd024..6bd6f3552 100644 --- a/core/utilities/numbers.lua +++ b/core/utilities/numbers.lua @@ -1,9 +1,9 @@ --- --- Number formatting utilities --- MIT License (c) 2022 SILE organization --- +--- Number formatting utilities. +--- @module SU.numbers + 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/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/inputters/base.lua b/inputters/base.lua index 4674bdfde..cd609669c 100644 --- a/inputters/base.lua +++ b/inputters/base.lua @@ -1,3 +1,6 @@ +--- SILE inputter class. +-- @interfaces 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/languages/fr.lua b/languages/fr.lua index f42e92a2d..f9df5e967 100644 --- a/languages/fr.lua +++ b/languages/fr.lua @@ -1,4 +1,5 @@ --- French language rules +--- French language rules +-- @submodule languages local computeSpaces = function() -- Computes: diff --git a/outputters/base.lua b/outputters/base.lua index 2d5042d8d..51cc36e59 100644 --- a/outputters/base.lua +++ b/outputters/base.lua @@ -1,3 +1,6 @@ +--- SILE outputter class. +-- @interfaces outputters + local outputter = pl.class() outputter.type = "outputter" outputter._name = "base" diff --git a/packages/base.lua b/packages/base.lua index 4b86d30cf..26754d8f1 100644 --- a/packages/base.lua +++ b/packages/base.lua @@ -1,3 +1,6 @@ +--- SILE package class. +-- @interfaces packages + local package = pl.class() package.type = "package" package._name = "base" @@ -62,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 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..2eef602f5 100644 --- a/pagebuilders/base.lua +++ b/pagebuilders/base.lua @@ -1,3 +1,6 @@ +--- SILE pagebuilder class. +-- @interfaces pagebuilders + local pagebuilder = pl.class() pagebuilder.type = "pagebuilder" pagebuilder._name = "base" diff --git a/shapers/base.lua b/shapers/base.lua index 6c67694f3..ec4ddfb36 100644 --- a/shapers/base.lua +++ b/shapers/base.lua @@ -1,3 +1,6 @@ +--- SILE shaper class. +-- @interfaces 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..e436915b6 100644 --- a/types/color.lua +++ b/types/color.lua @@ -1,3 +1,6 @@ +--- SILE color type. +-- @types color + local colornames = { aliceblue = { 240, 248, 255 }, antiquewhite = { 250, 235, 215 }, diff --git a/types/length.lua b/types/length.lua index a1e6d21a0..3de74b519 100644 --- a/types/length.lua +++ b/types/length.lua @@ -1,3 +1,6 @@ +--- SILE length type. +-- @types length + 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..5a44d2ab8 100644 --- a/types/measurement.lua +++ b/types/measurement.lua @@ -1,3 +1,6 @@ +--- SILE measurement type. +-- @types measurement + local function _tonumber (amount) return SU.cast("number", amount) end diff --git a/types/node.lua b/types/node.lua index f34383b38..c8ddeadd7 100644 --- a/types/node.lua +++ b/types/node.lua @@ -1,3 +1,6 @@ +--- SILE node type. +-- @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..d5d43c255 100644 --- a/types/unit.lua +++ b/types/unit.lua @@ -1,3 +1,6 @@ +--- SILE unit type. +-- @types unit + local bits = require("core.parserbits") local unittypes = { diff --git a/typesetters/base.lua b/typesetters/base.lua index a2006b84b..6572ca4f0 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 class. +-- @interfaces typesetters +--- @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. @@ -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")