From 0f5cbb7523bbb0a683c90a0aaa7f54aa2ba9c888 Mon Sep 17 00:00:00 2001 From: xmlatex Xake Date: Wed, 11 Dec 2024 14:28:43 +0100 Subject: [PATCH 01/21] ammended --- .vscode/tasks.json | 8 ++++---- aFirstXourse.tex | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1a03f29..4ce3818 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -12,6 +12,7 @@ { "label": "PDF", "args": [ + "-v", "compilePdf", "${relativeFile}" ], @@ -19,8 +20,7 @@ { "label": "HTML", "args": [ - "--", - "--skip-mathjax", + "-d", "compile", "${relativeFile}" ], @@ -29,14 +29,14 @@ "label": "Bake", "args": [ "--", - "--skip-mathjax", "bake" ], }, { "label": "All", "args": [ - "-d", "all" + "-v", + "all" ], }, { diff --git a/aFirstXourse.tex b/aFirstXourse.tex index ebca85d..aeabd87 100644 --- a/aFirstXourse.tex +++ b/aFirstXourse.tex @@ -2,7 +2,7 @@ % \input{preamble} %% No longer needed with ximeraLatex versions >= 2024-11-07 \author{Wim Obbels \and Bart Snapp} -\title{A First Ximera Xourse} +\title{A very First Ximera Xourse} \begin{document} \begin{abstract} From f7e9585138fcb0bd5f04891ef7503130ff50b835 Mon Sep 17 00:00:00 2001 From: xmlatex Xake Date: Wed, 11 Dec 2024 18:03:51 +0100 Subject: [PATCH 02/21] update devcontainer to v2.5-2a ('alpha') --- .devcontainer/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 5f38a05..603b19a 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,6 +1,6 @@ services: app: - image: ghcr.io/ximeraproject/xake2024:v2.4.2 + image: ghcr.io/ximeraproject/xake2024:v2.5-a2 # image: ghcr.io/ximeraproject/xake2024:v2.4.2-full volumes: # This is where VS Code should expect to find your project's source code and the value of "workspaceFolder" in .devcontainer/devcontainer.json From ae1791b2f4191086bc9175adecc69ff072a1970b Mon Sep 17 00:00:00 2001 From: xmlatex Xake Date: Thu, 12 Dec 2024 15:47:01 +0100 Subject: [PATCH 03/21] add labels --- aFirstFolder/aFirstActivity.tex | 6 ++++-- aFirstFolder/aGraphicsActivity.tex | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/aFirstFolder/aFirstActivity.tex b/aFirstFolder/aFirstActivity.tex index d1d1fd6..6210b97 100644 --- a/aFirstFolder/aFirstActivity.tex +++ b/aFirstFolder/aFirstActivity.tex @@ -9,6 +9,7 @@ A simple Ximera activity. \end{abstract} \maketitle +\label{xim:aFirstActivity} Perhaps the most natural setting for Ximera content is that of a \textit{worksheet}. This is some document that may contain discussion as well @@ -28,7 +29,7 @@ \section{A basic use case} questions. Since Ximera provides immediate feedback, we suggest following definitions like this one by a quick question. Here's an example: -\begin{definition} +\begin{definition}\label{def:absolute_value} The \textbf{absolute value} of a real number $a$, denoted by $|a|$, is \[ |a| = \begin{cases} @@ -75,7 +76,8 @@ \section{A paradox} \section{Basic exercises} -After that, you might want to have some exercises: +After that, you might want to have some exercises. +You will not find any inspiration in the above \hyperref[def:absolute_value]{definition of the absolute value}. \begin{exercise} Let $x$ be the number of people diff --git a/aFirstFolder/aGraphicsActivity.tex b/aFirstFolder/aGraphicsActivity.tex index b3c98d4..3e7a215 100644 --- a/aFirstFolder/aGraphicsActivity.tex +++ b/aFirstFolder/aGraphicsActivity.tex @@ -8,6 +8,7 @@ How to include graphics and other interactive content. \end{abstract} \maketitle +\label{xim:aGraphicsActivity} \section{Including images} @@ -126,4 +127,6 @@ \section{Desmos, Desmos3D, and Geogebra} \end{center} \end{verbatim} +And remember the \hyperref[def:absolute_value]{definition of the absolute value}. + \end{document} From 3c5cc0b4f4af9ee3c5119ea42f736813cac4f141 Mon Sep 17 00:00:00 2001 From: xmlatex Xake Date: Fri, 13 Dec 2024 11:47:09 +0100 Subject: [PATCH 04/21] dummy test --- aFirstFolder/aFirstActivity.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aFirstFolder/aFirstActivity.tex b/aFirstFolder/aFirstActivity.tex index 6210b97..68bf093 100644 --- a/aFirstFolder/aFirstActivity.tex +++ b/aFirstFolder/aFirstActivity.tex @@ -6,7 +6,7 @@ \begin{document} \begin{abstract} - A simple Ximera activity. + A very simple Ximera activity. \end{abstract} \maketitle \label{xim:aFirstActivity} From 15fba451b37612c261f24e963305b5f87e89ea0b Mon Sep 17 00:00:00 2001 From: xmlatex Xake Date: Fri, 13 Dec 2024 12:13:14 +0100 Subject: [PATCH 05/21] dummy test --- aFirstFolder/aFirstActivity.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aFirstFolder/aFirstActivity.tex b/aFirstFolder/aFirstActivity.tex index 68bf093..f43c74b 100644 --- a/aFirstFolder/aFirstActivity.tex +++ b/aFirstFolder/aFirstActivity.tex @@ -6,7 +6,7 @@ \begin{document} \begin{abstract} - A very simple Ximera activity. + Again a simple Ximera activity. \end{abstract} \maketitle \label{xim:aFirstActivity} From d1669a42ed2302e00c3b7019c8fd00df7019a14a Mon Sep 17 00:00:00 2001 From: xmlatex Xake Date: Fri, 13 Dec 2024 12:36:55 +0100 Subject: [PATCH 06/21] dummy test --- aFirstFolder/aFirstActivity.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aFirstFolder/aFirstActivity.tex b/aFirstFolder/aFirstActivity.tex index f43c74b..6210b97 100644 --- a/aFirstFolder/aFirstActivity.tex +++ b/aFirstFolder/aFirstActivity.tex @@ -6,7 +6,7 @@ \begin{document} \begin{abstract} - Again a simple Ximera activity. + A simple Ximera activity. \end{abstract} \maketitle \label{xim:aFirstActivity} From 12fa0d8fad8112b8ff1ee00bcba64c01bccabb9a Mon Sep 17 00:00:00 2001 From: xmlatex Xake Date: Fri, 13 Dec 2024 13:10:56 +0100 Subject: [PATCH 07/21] dummy test --- aFirstFolder/aFirstActivity.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aFirstFolder/aFirstActivity.tex b/aFirstFolder/aFirstActivity.tex index 6210b97..68bf093 100644 --- a/aFirstFolder/aFirstActivity.tex +++ b/aFirstFolder/aFirstActivity.tex @@ -6,7 +6,7 @@ \begin{document} \begin{abstract} - A simple Ximera activity. + A very simple Ximera activity. \end{abstract} \maketitle \label{xim:aFirstActivity} From 8aa4dfe6ce31b57e9ce4bc5e3dc3ccc7ec9a2c8d Mon Sep 17 00:00:00 2001 From: xmlatex Xake Date: Fri, 13 Dec 2024 18:17:43 +0100 Subject: [PATCH 08/21] dummy test --- aFirstFolder/aFirstActivity.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aFirstFolder/aFirstActivity.tex b/aFirstFolder/aFirstActivity.tex index 68bf093..41e90b6 100644 --- a/aFirstFolder/aFirstActivity.tex +++ b/aFirstFolder/aFirstActivity.tex @@ -6,7 +6,7 @@ \begin{document} \begin{abstract} - A very simple Ximera activity. + A rather simple Ximera activity. \end{abstract} \maketitle \label{xim:aFirstActivity} From 9b78bd0afdfb7cb232a648cebf01b49d7e7a07e2 Mon Sep 17 00:00:00 2001 From: xmlatex Xake Date: Fri, 13 Dec 2024 18:24:52 +0100 Subject: [PATCH 09/21] dummy test --- aFirstFolder/aFirstActivity.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aFirstFolder/aFirstActivity.tex b/aFirstFolder/aFirstActivity.tex index 41e90b6..818d4fa 100644 --- a/aFirstFolder/aFirstActivity.tex +++ b/aFirstFolder/aFirstActivity.tex @@ -1,6 +1,6 @@ \documentclass{ximera} -\title{A basic worksheet} +\title{A rather basic worksheet} \author{Wim Obbels \and Bart Snapp} \license{CC: 0} % replace with an appropriate license, or set it in xmPreamble From dedb0a686918df7bb1ecf4656e6edbfee29f5fda Mon Sep 17 00:00:00 2001 From: xmlatex Xake Date: Sat, 14 Dec 2024 23:21:42 +0100 Subject: [PATCH 10/21] dummy test --- aFirstFolder/aFirstActivity.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aFirstFolder/aFirstActivity.tex b/aFirstFolder/aFirstActivity.tex index 818d4fa..41e90b6 100644 --- a/aFirstFolder/aFirstActivity.tex +++ b/aFirstFolder/aFirstActivity.tex @@ -1,6 +1,6 @@ \documentclass{ximera} -\title{A rather basic worksheet} +\title{A basic worksheet} \author{Wim Obbels \and Bart Snapp} \license{CC: 0} % replace with an appropriate license, or set it in xmPreamble From f464a6173595639c521185e8355b1b85b9ad2fd1 Mon Sep 17 00:00:00 2001 From: xmlatex Xake Date: Thu, 19 Dec 2024 13:51:08 +0100 Subject: [PATCH 11/21] test --- .devcontainer/docker-compose.yml | 2 +- .gitignore | 3 + .texmf/luaxake/README.md | 106 ++ .texmf/luaxake/dkjson.lua | 752 +++++++++++++ .texmf/luaxake/luaxake | 452 ++++++++ .texmf/luaxake/luaxake-compile.lua | 310 ++++++ .texmf/luaxake/luaxake-config.lua | 49 + .texmf/luaxake/luaxake-files.lua | 447 ++++++++ .texmf/luaxake/luaxake-frost.lua | 377 +++++++ .texmf/luaxake/luaxake-graph.lua | 125 +++ .texmf/luaxake/luaxake-logging.lua | 154 +++ .texmf/luaxake/luaxake-transform-html.lua | 488 +++++++++ .vscode/tasks.json | 4 +- .ximera/ximera.4ht | 410 +++++++ .ximera/ximera.cfg | 183 ++++ .ximera/ximera.cls | 1174 +++++++++++++++++++++ 16 files changed, 5033 insertions(+), 3 deletions(-) create mode 100644 .texmf/luaxake/README.md create mode 100644 .texmf/luaxake/dkjson.lua create mode 100755 .texmf/luaxake/luaxake create mode 100644 .texmf/luaxake/luaxake-compile.lua create mode 100644 .texmf/luaxake/luaxake-config.lua create mode 100644 .texmf/luaxake/luaxake-files.lua create mode 100644 .texmf/luaxake/luaxake-frost.lua create mode 100644 .texmf/luaxake/luaxake-graph.lua create mode 100644 .texmf/luaxake/luaxake-logging.lua create mode 100644 .texmf/luaxake/luaxake-transform-html.lua create mode 100644 .ximera/ximera.4ht create mode 100644 .ximera/ximera.cfg create mode 100644 .ximera/ximera.cls diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 603b19a..4dd8b7a 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,6 +1,6 @@ services: app: - image: ghcr.io/ximeraproject/xake2024:v2.5-a2 + image: ghcr.io/ximeraproject/xake2024:v2.5-e-medium # image: ghcr.io/ximeraproject/xake2024:v2.4.2-full volumes: # This is where VS Code should expect to find your project's source code and the value of "workspaceFolder" in .devcontainer/devcontainer.json diff --git a/.gitignore b/.gitignore index c25e8ad..60f55ec 100644 --- a/.gitignore +++ b/.gitignore @@ -298,7 +298,10 @@ Thumbs.db # For exam-stuff ... ? ans[0-9]*.tex + + *.jax +*.xmjax *.ids *.oc diff --git a/.texmf/luaxake/README.md b/.texmf/luaxake/README.md new file mode 100644 index 0000000..ca59f57 --- /dev/null +++ b/.texmf/luaxake/README.md @@ -0,0 +1,106 @@ +# Luaxake + +This is a reimplementation of Xake using Lua. + +What it should do: + +- convert all standalone TeX files in a directory tree to PDF and HTML: + - we search for all files in subdirectories of the path + - standalone files are files that contain `\documentclass` command + - we detect included TeX files and recompile if any dependency is updated + +# Usage: + + $ texlua luaxake [options] path/to/directory + +# Options + +- `-c`,`--config` -- name of TeX4ht config file. It can be full path to the + config file, or just the name. If you pass just the filename, Luaxake will + search first in the directory with the current TeX file, to support different + config files for different projects, then in the current working directory, + project root and local TEXMF tree. + +- `-l`,`--loglevel` -- Set level of messages printed to the terminal. Possible + values: `debug`, `info`, `status`, `warning`, `error`, `fatal`. Default value is `status`, + which prints warnings, errors and status messages. + +- `-s`,`--script` -- Lua script that can change Luaxake configuration settings. + + +# Lua configuration + +You can set settings using a Lua script with the `-s` option. The script should +only set the configuration values. For example, to change the command for HTML +conversion, you can use the following script: + +```Lua +compilers.html.command = "make4ht -c @{config_file} @{filename} 'options'" +``` + +## Available configuration settings + +- `output_formats` -- list of extensions of output formats + +```Lua +output_formats = {"html", "pdf", "sagetex.sage"}, +``` + +- `documentclass_lines` -- number of lines in tex files where we should look for `\documentclass`. Luaxake compiles only files which contains the + `\documentclass` command on a line in this range. + +```Lua +documentclass_lines = 30, +``` + +- `compile_sequence` -- sequence of compilers to be called on each TeX file + +```Lua +compile_sequence = {"pdf", "sagetex.sage", "pdf", "html"}, +``` + +- `clean` -- list of extensions to be removed after compilation + +```Lua +clean = { "aux", "4ct", "4tc", "oc", "md5", "dpth", "out", "jax", "idv", "lg", "tmp", "xref", "log", "auxlock", "dvi", "scmd", "sout" } +``` + +## Compilers + +- `compilers` -- settings for compiler commands. Each compiler contains table with additional settings. + +There are three available compilers, but you can add more: + +- `pdf` -- command used for the PDF generation +- `html` -- command used for the HTML generation +- `sagetex.sage` -- command used for the `sagetex.sage` generation + +```Lua +compilers = { + html = { + command = "make4ht -f html5+dvisvgm_hashes -c @{config_file} -sm draft @{filename}", + check_log = true, -- check log + status = 0 -- check that the latex command return 0 + }, +} +``` + +### Settings available in the `compiler` table: + +- `check_log` -- should we check the log file for errors? +- `check_file` -- check if the file exists before compilation. It is used by `sage`, which must be executed only if `filename.sagetex.sage` exists. +- `status` -- expected status code from the command. +- `process_html` -- run HTML post-processing. +- `command` -- template for the command to be executed. `@{variable}` tag will be replaced with the content of variable. + +### Variables available in command templates: + + - `dir` -- relative directory path of the file + - `absolute_dir` -- absolute directory path of the file + - `filename` -- filename of the file + - `basename` -- filename without extension + - `extension` -- file extension + - `relative_path` -- relative path of the file + - `absolute_path` -- absolute path of the file + - `exists` -- boolean, true if file exists + - `config_file` -- TeX4ht config file diff --git a/.texmf/luaxake/dkjson.lua b/.texmf/luaxake/dkjson.lua new file mode 100644 index 0000000..3bfbec2 --- /dev/null +++ b/.texmf/luaxake/dkjson.lua @@ -0,0 +1,752 @@ +-- Module options: +local always_use_lpeg = false +local register_global_module_table = false +local global_module_name = 'json' + +--[==[ + +David Kolf's JSON module for Lua 5.1 - 5.4 + +Version 2.8 + + +For the documentation see the corresponding readme.txt or visit +. + +You can contact the author by sending an e-mail to 'david' at the +domain 'dkolf.de'. + + +Copyright (C) 2010-2024 David Heiko Kolf + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--]==] + +-- global dependencies: +local pairs, type, tostring, tonumber, getmetatable, setmetatable = + pairs, type, tostring, tonumber, getmetatable, setmetatable +local error, require, pcall, select = error, require, pcall, select +local floor, huge = math.floor, math.huge +local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat = + string.rep, string.gsub, string.sub, string.byte, string.char, + string.find, string.len, string.format +local strmatch = string.match +local concat = table.concat + +local json = { version = "dkjson 2.8" } + +local jsonlpeg = {} + +if register_global_module_table then + if always_use_lpeg then + _G[global_module_name] = jsonlpeg + else + _G[global_module_name] = json + end +end + +local _ENV = nil -- blocking globals in Lua 5.2 and later + +pcall (function() + -- Enable access to blocked metatables. + -- Don't worry, this module doesn't change anything in them. + local debmeta = require "debug".getmetatable + if debmeta then getmetatable = debmeta end +end) + +json.null = setmetatable ({}, { + __tojson = function () return "null" end +}) + +local function isarray (tbl) + local max, n, arraylen = 0, 0, 0 + for k,v in pairs (tbl) do + if k == 'n' and type(v) == 'number' then + arraylen = v + if v > max then + max = v + end + else + if type(k) ~= 'number' or k < 1 or floor(k) ~= k then + return false + end + if k > max then + max = k + end + n = n + 1 + end + end + if max > 10 and max > arraylen and max > n * 2 then + return false -- don't create an array with too many holes + end + return true, max +end + +local escapecodes = { + ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", + ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t" +} + +local function escapeutf8 (uchar) + local value = escapecodes[uchar] + if value then + return value + end + local a, b, c, d = strbyte (uchar, 1, 4) + a, b, c, d = a or 0, b or 0, c or 0, d or 0 + if a <= 0x7f then + value = a + elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then + value = (a - 0xc0) * 0x40 + b - 0x80 + elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then + value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80 + elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then + value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80 + else + return "" + end + if value <= 0xffff then + return strformat ("\\u%.4x", value) + elseif value <= 0x10ffff then + -- encode as UTF-16 surrogate pair + value = value - 0x10000 + local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400) + return strformat ("\\u%.4x\\u%.4x", highsur, lowsur) + else + return "" + end +end + +local function fsub (str, pattern, repl) + -- gsub always builds a new string in a buffer, even when no match + -- exists. First using find should be more efficient when most strings + -- don't contain the pattern. + if strfind (str, pattern) then + return gsub (str, pattern, repl) + else + return str + end +end + +local function quotestring (value) + -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js + value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8) + if strfind (value, "[\194\216\220\225\226\239]") then + value = fsub (value, "\194[\128-\159\173]", escapeutf8) + value = fsub (value, "\216[\128-\132]", escapeutf8) + value = fsub (value, "\220\143", escapeutf8) + value = fsub (value, "\225\158[\180\181]", escapeutf8) + value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8) + value = fsub (value, "\226\129[\160-\175]", escapeutf8) + value = fsub (value, "\239\187\191", escapeutf8) + value = fsub (value, "\239\191[\176-\191]", escapeutf8) + end + return "\"" .. value .. "\"" +end +json.quotestring = quotestring + +local function replace(str, o, n) + local i, j = strfind (str, o, 1, true) + if i then + return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1) + else + return str + end +end + +-- locale independent num2str and str2num functions +local decpoint, numfilter + +local function updatedecpoint () + decpoint = strmatch(tostring(0.5), "([^05+])") + -- build a filter that can be used to remove group separators + numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+" +end + +updatedecpoint() + +local function num2str (num) + return replace(fsub(tostring(num), numfilter, ""), decpoint, ".") +end + +local function str2num (str) + local num = tonumber(replace(str, ".", decpoint)) + if not num then + updatedecpoint() + num = tonumber(replace(str, ".", decpoint)) + end + return num +end + +local function addnewline2 (level, buffer, buflen) + buffer[buflen+1] = "\n" + buffer[buflen+2] = strrep (" ", level) + buflen = buflen + 2 + return buflen +end + +function json.addnewline (state) + if state.indent then + state.bufferlen = addnewline2 (state.level or 0, + state.buffer, state.bufferlen or #(state.buffer)) + end +end + +local encode2 -- forward declaration + +local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state) + local kt = type (key) + if kt ~= 'string' and kt ~= 'number' then + return nil, "type '" .. kt .. "' is not supported as a key by JSON." + end + if prev then + buflen = buflen + 1 + buffer[buflen] = "," + end + if indent then + buflen = addnewline2 (level, buffer, buflen) + end + -- When Lua is compiled with LUA_NOCVTN2S this will fail when + -- numbers are mixed into the keys of the table. JSON keys are always + -- strings, so this would be an implicit conversion too and the failure + -- is intentional. + buffer[buflen+1] = quotestring (key) + buffer[buflen+2] = ":" + return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state) +end + +local function appendcustom(res, buffer, state) + local buflen = state.bufferlen + if type (res) == 'string' then + buflen = buflen + 1 + buffer[buflen] = res + end + return buflen +end + +local function exception(reason, value, state, buffer, buflen, defaultmessage) + defaultmessage = defaultmessage or reason + local handler = state.exception + if not handler then + return nil, defaultmessage + else + state.bufferlen = buflen + local ret, msg = handler (reason, value, state, defaultmessage) + if not ret then return nil, msg or defaultmessage end + return appendcustom(ret, buffer, state) + end +end + +function json.encodeexception(reason, value, state, defaultmessage) + return quotestring("<" .. defaultmessage .. ">") +end + +encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state) + local valtype = type (value) + local valmeta = getmetatable (value) + valmeta = type (valmeta) == 'table' and valmeta -- only tables + local valtojson = valmeta and valmeta.__tojson + if valtojson then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + state.bufferlen = buflen + local ret, msg = valtojson (value, state) + if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end + tables[value] = nil + buflen = appendcustom(ret, buffer, state) + elseif value == nil then + buflen = buflen + 1 + buffer[buflen] = "null" + elseif valtype == 'number' then + local s + if value ~= value or value >= huge or -value >= huge then + -- This is the behaviour of the original JSON implementation. + s = "null" + else + s = num2str (value) + end + buflen = buflen + 1 + buffer[buflen] = s + elseif valtype == 'boolean' then + buflen = buflen + 1 + buffer[buflen] = value and "true" or "false" + elseif valtype == 'string' then + buflen = buflen + 1 + buffer[buflen] = quotestring (value) + elseif valtype == 'table' then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + level = level + 1 + local isa, n = isarray (value) + if n == 0 and valmeta and valmeta.__jsontype == 'object' then + isa = false + end + local msg + if isa then -- JSON array + buflen = buflen + 1 + buffer[buflen] = "[" + for i = 1, n do + buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + if i < n then + buflen = buflen + 1 + buffer[buflen] = "," + end + end + buflen = buflen + 1 + buffer[buflen] = "]" + else -- JSON object + local prev = false + buflen = buflen + 1 + buffer[buflen] = "{" + local order = valmeta and valmeta.__jsonorder or globalorder + if order then + local used = {} + n = #order + for i = 1, n do + local k = order[i] + local v = value[k] + if v ~= nil then + used[k] = true + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + for k,v in pairs (value) do + if not used[k] then + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + else -- unordered + for k,v in pairs (value) do + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + if indent then + buflen = addnewline2 (level - 1, buffer, buflen) + end + buflen = buflen + 1 + buffer[buflen] = "}" + end + tables[value] = nil + else + return exception ('unsupported type', value, state, buffer, buflen, + "type '" .. valtype .. "' is not supported by JSON.") + end + return buflen +end + +function json.encode (value, state) + state = state or {} + local oldbuffer = state.buffer + local buffer = oldbuffer or {} + state.buffer = buffer + updatedecpoint() + local ret, msg = encode2 (value, state.indent, state.level or 0, + buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state) + if not ret then + error (msg, 2) + elseif oldbuffer == buffer then + state.bufferlen = ret + return true + else + state.bufferlen = nil + state.buffer = nil + return concat (buffer) + end +end + +local function loc (str, where) + local line, pos, linepos = 1, 1, 0 + while true do + pos = strfind (str, "\n", pos, true) + if pos and pos < where then + line = line + 1 + linepos = pos + pos = pos + 1 + else + break + end + end + return strformat ("line %d, column %d", line, where - linepos) +end + +local function unterminated (str, what, where) + return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where) +end + +local function scanwhite (str, pos) + while true do + pos = strfind (str, "%S", pos) + if not pos then return nil end + local sub2 = strsub (str, pos, pos + 1) + if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then + -- UTF-8 Byte Order Mark + pos = pos + 3 + elseif sub2 == "//" then + pos = strfind (str, "[\n\r]", pos + 2) + if not pos then return nil end + elseif sub2 == "/*" then + pos = strfind (str, "*/", pos + 2) + if not pos then return nil end + pos = pos + 2 + else + return pos + end + end +end + +local escapechars = { + ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f", + ["n"] = "\n", ["r"] = "\r", ["t"] = "\t" +} + +local function unichar (value) + if value < 0 then + return nil + elseif value <= 0x007f then + return strchar (value) + elseif value <= 0x07ff then + return strchar (0xc0 + floor(value/0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0xffff then + return strchar (0xe0 + floor(value/0x1000), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0x10ffff then + return strchar (0xf0 + floor(value/0x40000), + 0x80 + (floor(value/0x1000) % 0x40), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + else + return nil + end +end + +local function scanstring (str, pos) + local lastpos = pos + 1 + local buffer, n = {}, 0 + while true do + local nextpos = strfind (str, "[\"\\]", lastpos) + if not nextpos then + return unterminated (str, "string", pos) + end + if nextpos > lastpos then + n = n + 1 + buffer[n] = strsub (str, lastpos, nextpos - 1) + end + if strsub (str, nextpos, nextpos) == "\"" then + lastpos = nextpos + 1 + break + else + local escchar = strsub (str, nextpos + 1, nextpos + 1) + local value + if escchar == "u" then + value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16) + if value then + local value2 + if 0xD800 <= value and value <= 0xDBff then + -- we have the high surrogate of UTF-16. Check if there is a + -- low surrogate escaped nearby to combine them. + if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then + value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16) + if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then + value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000 + else + value2 = nil -- in case it was out of range for a low surrogate + end + end + end + value = value and unichar (value) + if value then + if value2 then + lastpos = nextpos + 12 + else + lastpos = nextpos + 6 + end + end + end + end + if not value then + value = escapechars[escchar] or escchar + lastpos = nextpos + 2 + end + n = n + 1 + buffer[n] = value + end + end + if n == 1 then + return buffer[1], lastpos + elseif n > 1 then + return concat (buffer), lastpos + else + return "", lastpos + end +end + +local scanvalue -- forward declaration + +local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta) + local tbl, n = {}, 0 + local pos = startpos + 1 + if what == 'object' then + setmetatable (tbl, objectmeta) + else + setmetatable (tbl, arraymeta) + end + while true do + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + local char = strsub (str, pos, pos) + if char == closechar then + return tbl, pos + 1 + end + local val1, err + val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + if char == ":" then + if val1 == nil then + return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")" + end + pos = scanwhite (str, pos + 1) + if not pos then return unterminated (str, what, startpos) end + local val2 + val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + tbl[val1] = val2 + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + else + n = n + 1 + tbl[n] = val1 + end + if char == "," then + pos = pos + 1 + end + end +end + +scanvalue = function (str, pos, nullval, objectmeta, arraymeta) + pos = pos or 1 + pos = scanwhite (str, pos) + if not pos then + return nil, strlen (str) + 1, "no valid JSON value (reached the end)" + end + local char = strsub (str, pos, pos) + if char == "{" then + return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta) + elseif char == "[" then + return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta) + elseif char == "\"" then + return scanstring (str, pos) + else + local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos) + if pstart then + local number = str2num (strsub (str, pstart, pend)) + if number then + return number, pend + 1 + end + end + pstart, pend = strfind (str, "^%a%w*", pos) + if pstart then + local name = strsub (str, pstart, pend) + if name == "true" then + return true, pend + 1 + elseif name == "false" then + return false, pend + 1 + elseif name == "null" then + return nullval, pend + 1 + end + end + return nil, pos, "no valid JSON value at " .. loc (str, pos) + end +end + +local function optionalmetatables(...) + if select("#", ...) > 0 then + return ... + else + return {__jsontype = 'object'}, {__jsontype = 'array'} + end +end + +function json.decode (str, pos, nullval, ...) + local objectmeta, arraymeta = optionalmetatables(...) + return scanvalue (str, pos, nullval, objectmeta, arraymeta) +end + +function json.use_lpeg () + local g = require ("lpeg") + + if type(g.version) == 'function' and g.version() == "0.11" then + error "due to a bug in LPeg 0.11, it cannot be used for JSON matching" + end + + local pegmatch = g.match + local P, S, R = g.P, g.S, g.R + + local function ErrorCall (str, pos, msg, state) + if not state.msg then + state.msg = msg .. " at " .. loc (str, pos) + state.pos = pos + end + return false + end + + local function Err (msg) + return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall) + end + + local function ErrorUnterminatedCall (str, pos, what, state) + return ErrorCall (str, pos - 1, "unterminated " .. what, state) + end + + local SingleLineComment = P"//" * (1 - S"\n\r")^0 + local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/" + local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0 + + local function ErrUnterminated (what) + return g.Cmt (g.Cc (what) * g.Carg (2), ErrorUnterminatedCall) + end + + local PlainChar = 1 - S"\"\\\n\r" + local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars + local HexDigit = R("09", "af", "AF") + local function UTF16Surrogate (match, pos, high, low) + high, low = tonumber (high, 16), tonumber (low, 16) + if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then + return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000) + else + return false + end + end + local function UTF16BMP (hex) + return unichar (tonumber (hex, 16)) + end + local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit)) + local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP + local Char = UnicodeEscape + EscapeSequence + PlainChar + local String = P"\"" * (g.Cs (Char ^ 0) * P"\"" + ErrUnterminated "string") + local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0)) + local Fractal = P"." * R"09"^0 + local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1 + local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num + local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1) + local SimpleValue = Number + String + Constant + local ArrayContent, ObjectContent + + -- The functions parsearray and parseobject parse only a single value/pair + -- at a time and store them directly to avoid hitting the LPeg limits. + local function parsearray (str, pos, nullval, state) + local obj, cont + local start = pos + local npos + local t, nt = {}, 0 + repeat + obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state) + if cont == 'end' then + return ErrorUnterminatedCall (str, start, "array", state) + end + pos = npos + if cont == 'cont' or cont == 'last' then + nt = nt + 1 + t[nt] = obj + end + until cont ~= 'cont' + return pos, setmetatable (t, state.arraymeta) + end + + local function parseobject (str, pos, nullval, state) + local obj, key, cont + local start = pos + local npos + local t = {} + repeat + key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state) + if cont == 'end' then + return ErrorUnterminatedCall (str, start, "object", state) + end + pos = npos + if cont == 'cont' or cont == 'last' then + t[key] = obj + end + until cont ~= 'cont' + return pos, setmetatable (t, state.objectmeta) + end + + local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) + local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) + local Value = Space * (Array + Object + SimpleValue) + local ExpectedValue = Value + Space * Err "value expected" + local ExpectedKey = String + Err "key expected" + local End = P(-1) * g.Cc'end' + local ErrInvalid = Err "invalid JSON" + ArrayContent = (Value * Space * (P"," * g.Cc'cont' + P"]" * g.Cc'last'+ End + ErrInvalid) + g.Cc(nil) * (P"]" * g.Cc'empty' + End + ErrInvalid)) * g.Cp() + local Pair = g.Cg (Space * ExpectedKey * Space * (P":" + Err "colon expected") * ExpectedValue) + ObjectContent = (g.Cc(nil) * g.Cc(nil) * P"}" * g.Cc'empty' + End + (Pair * Space * (P"," * g.Cc'cont' + P"}" * g.Cc'last' + End + ErrInvalid) + ErrInvalid)) * g.Cp() + local DecodeValue = ExpectedValue * g.Cp () + + jsonlpeg.version = json.version + jsonlpeg.encode = json.encode + jsonlpeg.null = json.null + jsonlpeg.quotestring = json.quotestring + jsonlpeg.addnewline = json.addnewline + jsonlpeg.encodeexception = json.encodeexception + jsonlpeg.using_lpeg = true + + function jsonlpeg.decode (str, pos, nullval, ...) + local state = {} + state.objectmeta, state.arraymeta = optionalmetatables(...) + local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state) + if state.msg then + return nil, state.pos, state.msg + else + return obj, retpos + end + end + + -- cache result of this function: + json.use_lpeg = function () return jsonlpeg end + jsonlpeg.use_lpeg = json.use_lpeg + + return jsonlpeg +end + +if always_use_lpeg then + return json.use_lpeg() +end + +return json + diff --git a/.texmf/luaxake/luaxake b/.texmf/luaxake/luaxake new file mode 100755 index 0000000..163724a --- /dev/null +++ b/.texmf/luaxake/luaxake @@ -0,0 +1,452 @@ +#!/usr/bin/env texlua +kpse.set_program_name "luatex" + +local pl = require "penlight" +local utils = require "pl.utils" +local tablex = require("pl.tablex") +local path = pl.path +local lapp = require "pl.lapp" +-- local lapp = require "lapp-mk4" -- the above is 'better'? + +logging = require("luaxake-logging") +-- better make a logfile per day ... ? +-- NOTE: after a chdir (as in compile...), the logfile would change without the abspath!! +logging.set_outfile(path.abspath("luaxake.log")) + +local log = logging.new("luaxake") +local version = "{{version}}" + +local files = require "luaxake-files" +local compile = require "luaxake-compile" +local frost = require "luaxake-frost" +local html = require "luaxake-transform-html" + + +-- TODO: fix syntax with [command dirs]: should be 1 obligatory, then many optional args +local option_text = [[ +Luaxake: build system for Ximera documents +Usage: +$ luaxake [command dirs] + +Options: +-h,--help Print help message +-l,--loglevel (default info) Set log level: trace, debug, info, status, warning, error, fatal +-s,--settings (default none) Luaxake settings script +-v,--version Version info +-f,--force Recompile anyway +-C,--compile (default none) Compile sequence (default 'pdf,html', or as set in settings) +--check Check, no cleaning/compiling +--config (default ximera.cfg) TeX4ht config file + + +Possible commands: + bake + name -- NOT YET IMPLEMENTED HERE: see xmlatex !!! + frost + serve + clean / veryclean -- to be changed/improved + info -- not yet very useful.. + ]] + +-- REMOVED: (table) Document root directory +--- @class args +--- @field config string TeX4ht config file +--- @field help boolean Print help message +--- @field settings string Luaxake settings script +--- @field loglevel string Logging level +--- @field version boolean Print version +--- @field command string Command to execute +--- @field dir table Document root directory +local args = lapp(option_text) + +if args.version then + print("Luaxake version: " .. (version == "{{version}}" and "devel" or version)) + os.exit() +end + +logging.set_level(args.loglevel) + +-- first real argument is the command (bake/frost/serve/...) +local command = table.remove(args, 1) + +if not command then + log:error("Usage: script [command] ") + os.exit(1) +end + +log:debug("command: "..command) + + +-- all further arguments are considered dirs/files to be processed +local dirs = {} + +for i, value in ipairs(args) do + log:trace("Args: adding file/dir "..value) + table.insert(dirs, value) +end + +if #dirs == 0 then + log:debug("Using root folder . as default argument/target") + dirs = {"."} +end + +-- +-- FOR REFERENCE: (and to be checked ...?) +-- + +--- @class DOM_Object +--- Dummy type declaration for LuaXML DOM object, to prevent error messages from language server +--- @field query_selector function get all elements that match a given CSS selector +--- @field get_children function +--- @field get_text function +--- @field get_attribute function +--- @field remove_node function + +--- @class compiler +--- @field command string command template +--- @field check_log? boolean should we check the log file for errors? +--- @field check_file? boolean execute command only if the output file exists +--- @field status? number expected status code from the command +--- @field process_html? boolean run HTML post-processing + +--- @class config +--- @field output_formats [string] list of output format extensions +--- @field documentclass_lines number on how many lines in TeX files we should try to look for \documentclass +--- @field compilers {string: compiler} +--- @field compile_sequence [string] sequence of compiler names to be executed +--- @field clean [string] list of extensions of temp files to be removed after the compilation +--- @field config_file string TeX4ht config file + +config = { + -- list of outputs ( and extensions ) + output_formats = {"make4ht.html", "pdf", "handout.pdf"}, + -- output_formats = {"html", "pdf", "sagetex.sage"}, -- TODO: check/implement sage + compile_sequence = {"pdf", "make4ht.html", "handout.pdf"}, + -- compile_sequence = {"pdf", "sagetex.sage", "pdf", "html"}, + -- see infra -- default_dependencies = { "xmPreamble.tex" }, -- add here e.g. xmPreamble, ximera.cls, ... + compilers = { + -- just a dummy test: create .ddd files that contain the date .. + ddd = { + command = 'date >@{basename}.ddd', + status = 0, -- check that the latex command return 0 + }, + pdf = { + -- this doesn't work well + -- command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape "\\PassOptionsToClass{tikzexport}{ximera}\\PassOptionsToClass{xake}{ximera}\\PassOptionsToClass{xake}{xourse}\\nonstopmode\\input{@{filename}}"', + -- command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape "\\PassOptionsToClass{xake}{ximera}\\PassOptionsToClass{xake}{xourse}\\input{@{filename}}"', + command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape "\\input{@{filename}}"', -- mmm, this increases the .jax file !!! + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + infix = "" , -- used for .handout, and .make4k4 + extension = "pdf", -- not used ???? + post_command = 'move_to_downloads', + download_folder = 'ximera-downloads/with-answers', + }, + ["handout.pdf"] = { + command = 'pdflatex -interaction=nonstopmode -file-line-error -shell-escape -jobname @{basename}.handout "\\PassOptionsToClass{handout}{ximera}\\PassOptionsToClass{handout}{xourse}\\input{@{filename}}"', + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + extension = "handout.pdf", + infix = "handout" , + post_command = 'move_to_downloads', + download_folder = 'ximera-downloads/handouts', + }, + -- 20241217: use make4ht.html ! + html = { + -- command = "make4ht -f html5+dvisvgm_hashes -c @{config_file} -sm draft @{filename}", + -- command = "make4ht -c @{config_file} -f html5+dvisvgm_hashes -s @{make4ht_mode} -a debug @{filename} 'svg,htex4ht,mathjax,-css,info,tikz+' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + command = "make4ht -c @{config_file} -f html5+dvisvgm_hashes -j @{basename}.make4ht -s @{make4ht_extraoptions} @{filename} 'svg,htex4ht,mathjax,-css,info,tikz+' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + process_html = false, + post_command = 'process_html', + extension = "html", + infix = "make4ht" , + }, + ["make4ht.html"] = { + command = "make4ht -c @{config_file} -f html5+dvisvgm_hashes -j @{basename}.make4ht -s @{make4ht_extraoptions} @{filename} 'svg,htex4ht,mathjax,-css,info,tikz+' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + process_html = false, + post_command = 'process_html', + extension = "html", + infix = "make4ht" , + }, + -- do not use, only for testing + ["draft.html"] = { + command = "make4ht -c @{config_file} -f html5+dvisvgm_hashes -m draft -j @{basename}.make4ht -s @{make4ht_extraoptions} @{filename} 'svg,htex4ht,mathjax,-css,info,tikz+' '' '' '--interaction=nonstopmode -shell-escape -file-line-error'", + check_log = true, -- check log + status = 0, -- check that the latex command return 0 + post_command = 'process_html', + extension = "html", + infix = "make4ht" , + }, + -- sage not tested/implemented !!!! + ["sagetex.sage"] = { + command = "sage @{output_file}", + check_log = true, -- check log + check_file = true, -- check if the sagetex.sage file exists + status = 0, -- check that the latex command return 0 + extension = "sage", -- ? + }, + + }, + -- used for dependency-checking in .tex files + input_commands = { + input=true, + include=true, + includeonly=true, + activity=true, + activitychapter=true, + activitysection=true, + practicechapter=true, + practicesection=true, + }, + -- automatically clean files immediately after each compilation + -- the commented extensions might cause issues when automatically clean, to be verified + clean = { + -- "aux", + "4ct", + "4tc", + "oc", + "md5", + "dpth", + "out", + -- "jax", + "idv", + "lg", + "tmp", + -- "xref", + -- "log", + "auxlock", + "dvi", + "scmd", + "sout", + "ids", + "mw", + "cb", + "cb2", + }, + -- make4ht_loglevel = "", + make4ht_extraoptions= "", + -- number of lines in tex files where we should look for \documentclass + documentclass_lines = 30, +} + +-- set/add potential default dependencies +if not config.default_dependencies and path.exists("xmPreamble.tex") then + log:info("Adding default dependency xmPreamble.tex") + config.default_dependencies = { files.get_metadata("xmPreamble.tex") } +else + config.default_dependencies = {} +end + + +config.config_file = args.config +config.force_compilation = args.force +if config.config_file ~= "ximera.cfg" then + log:warning("Using non-default config file " .. config.config_file) +end + +if args.settings ~= "none" then + -- config file can be a Lua script, which should only set properties for the config table + local configlib = require "luaxake-config" + log:info("Using settings file: " .. args.settings) + configlib.update_config(args.settings, config) +end + +if args.compile ~= "none" then + config.compile_sequence = utils.split(args.compile,',') + config.output_formats = utils.split(args.compile,',') + log:info("Set compile_sequence and output_formats to " .. args.compile) +end + +config.check = args.check + +-- local function array_to_set(array) +-- local set = {} +-- for _, v in ipairs(array) do +-- set[v] = true +-- end +-- return set +-- end + +-- config.include_extensions = array_to_set({ +-- "tex", +-- "html", +-- "sty", +-- "pdf", +-- }) + + +config.dirs = dirs + + +-- collect all metadata / to_be_compiled etc + +local tex_files = {} + +for i,nextarg in ipairs(config.dirs) do + + log:info("Processing argument " .. nextarg) + + local more_tex_files = {} + + if path.isdir(nextarg) then + more_tex_files = files.get_tex_files_with_status(nextarg, config.output_formats, config.compilers) + -- an existing TeX-file, but without the extension + elseif not path.isfile(nextarg) and path.isfile(nextarg..".tex" ) then + log:trace("Adding .tex extension to file "..nextarg) + nextarg = nextarg..".tex" + elseif ( path.isfile(nextarg) and nextarg:match("%.tex$") ~= nil ) -- an existing TeX-file + then + local metadata = files.get_metadata(nextarg) + metadata.needs_compilation = true; + log:debug("Explicitly adding tex file "..metadata.filename) + more_tex_files = { metadata } + to_be_compiled = tex_files + elseif nextarg:match("%.tex$") == nil then + log:warning("File "..nextarg.." not a .tex file: SKIPPING") + else + log:warning("File "..nextarg.." not found: SKIPPING") + end + + table.move(more_tex_files, 1, #more_tex_files, #tex_files+1, tex_files) +end + + +log:debugf("Finding dependencies, and sorting files for %d tex_files", #tex_files) +local to_be_compiled = files.sort_dependencies(tex_files, config.force_compilation) +if #to_be_compiled > 0 then + log:status(#to_be_compiled.." files need compiling") +else + log:status("No files need compiling") +end + + -- + -- Start processing further commands + -- + + if command == "info" then + log:info("Got tex files:") + for _,file in ipairs(tex_files) do + print(file.relative_path) + + + end + end + + if command == "clean" or command == "veryclean" then + local n_files_deleted = 0 + local to_be_cleaned_extensions = tablex.copy(config.clean) + if command == "veryclean" then + log:debug("Appending extra extensions") + tablex.insertvalues(to_be_cleaned_extensions, config.output_formats) -- append arrays ... + tablex.insertvalues(to_be_cleaned_extensions, { "aux", "toc", "log", "xref", "jax" }) -- append arrays ... + log:debugf("Appended extra extensions %s", table.concat(to_be_cleaned_extensions, ', ')) + end + log:status("Cleaning files ", table.concat(to_be_cleaned_extensions, ', ')) + + for _,file in ipairs(tex_files) do + n_files_deleted = n_files_deleted + compile.clean(file, to_be_cleaned_extensions, config.check) + end + + -- require 'pl.pretty'.dump(config) + -- require 'pl.pretty'.dump(to_be_cleaned_extensions) + + log:infof("Cleaned %d files", n_files_deleted) + end + + + -- TODO: fix commands (cfr xmlatex/xmlatex/lua) + if command == "bake" or command == "compilePdf" or command == "compile" then + log:status("Start "..command) + + if command == "compilePdf" then + log:info("Compile only PDF") + config.compile_sequence = { "pdf" } + end + if command == "compile" then + log:info("Compile only HTML") + config.compile_sequence = { "make4ht.html" } + end + + local all_statuses = {} + if #to_be_compiled == 0 then + log:status("Nothing to be baked") + else + for i, file in ipairs(to_be_compiled) do + log:status(string.format("Compiling file %3d/%d: %s", i, #to_be_compiled, file.absolute_path)) + + local statuses = compile.compile(file, config.compilers, config.compile_sequence, config.check) + table.insert(all_statuses,statuses) + + compile.print_errors(statuses) + compile.clean(file, config.clean) + end + + -- filter errors out all statuses + local errors = {} + for _, entry in ipairs(all_statuses) do + for _, info in ipairs(entry) do + log:debug("File "..(info.output_file or "UNKNOWN??") .." got status " .. (info.status or 'NIL??') ) + -- require 'pl.pretty'.dump(info) + + if (info.status or 0) > 0 then + table.insert(errors, info) + -- errors is a list-per-compile-instance of a table with command/errors (with filename/line/error/context) and (FULL!!!) output ) + end + end + end + + if #errors == 0 then + log:info("Baked "..#to_be_compiled.." files without finding errors") + else + log:errorf("Baking failed with %d error%s detected", #errors, #errors == 1 and "" or "s") + + for _, errorinstance in ipairs(errors) do + for _, err in ipairs(errorinstance.errors) do + -- require 'pl.pretty'.dump(err) + log:errorf("Summary: %s: %s [%s]", err.filename, err.context,err.error) + end + end + os.exit(1) + end + end +end -- end baking + +-- process commands that do not depend on collecting metadata + +if command == "frost" then + log:status("Start " .. command) + local ret, msg = frost.frost(tex_files, to_be_compiled) + if ret > 0 then + log:error("Frost failed:", msg) + os.exit(ret) + end +end + +if command == "serve" then + log:status("Start " .. command) + local ret, msg = frost.serve() + if ret > 0 then + log:error("Serve failed:", msg) + os.exit(ret) + end +end + + +if command == "extrainfo" then + all_labels = {} + -- TEST: to be implemented ...? + for i, file in ipairs(tex_files) do + log:infof("Info for %s (%s)",file.filename,file.relative_path) + + local dom, msg = html.load_html(file.relative_path:gsub("tex$", "html")) + + file.labels = html.get_labels(dom) + for k,v in pairs(file.labels) do all_labels[k] = ( all_labels[k] or 0) + v end + end + require 'pl.pretty'.dump(all_labels) + +end \ No newline at end of file diff --git a/.texmf/luaxake/luaxake-compile.lua b/.texmf/luaxake/luaxake-compile.lua new file mode 100644 index 0000000..1e80e4a --- /dev/null +++ b/.texmf/luaxake/luaxake-compile.lua @@ -0,0 +1,310 @@ +local M = {} +local lfs = require "lfs" +local error_logparser = require("make4ht-errorlogparser") +local pl = require "penlight" +local path = pl.path +local pldir = pl.dir +local plfile = pl.file +local html = require "luaxake-transform-html" +local files = require "luaxake-files" -- for get_metadaa +local socket = require "socket" + +local log = logging.new("compile") + + + +-- --- fill command template with file information +-- --- @param file metadata file on which the command should be run +-- --- @param command string command template +-- --- @return string command +-- local function prepare_command(file, command_template) +-- -- replace placeholders like @{filename} with the corresponding keys from the metadata table +-- return command_template:gsub("@{(.-)}", file) +-- end + + +local function test_log_file(filename) + local f = io.open(filename, "r") + if not f then + log:error("Cannot open log file: " .. filename) + return nil + end + local content = f:read("*a") + f:close() + return error_logparser.parse(content) +end + +local function copy_table(tbl) + local t = {} + for k,v in pairs(tbl) do + if type(v) == "table" then + t[k] = copy_table(v) + else + t[k] = v + end + end + return t +end + +-- Function to find the first table with a given key/value using Penlight +local function find_entry(array, key, value) + for _, entry in ipairs(array) do + if entry[key] == value then + + return entry -- Return the first matching entry + end + end + return nil -- Return nil if no match is found +end + +-- +-- These next functions are/can be called by post_command in config.commands +-- HACK: these currently need to be global; TODO: fix! +-- +function process_html(file) + -- simple wrapper to make it work in post_command + -- + return html.process(file) +end + +function move_to_downloads(file, cmd_meta, root_dir) + -- move the pdf to a corresponding folder under root_dir (presumably ximera-downloads, with different path/name!) + -- + local folder = string.format("%s/%s/%s",root_dir, cmd_meta.download_folder, file.dir) + + -- require 'pl.pretty'.dump(cmd_meta) + -- require 'pl.pretty'.dump(file) + local src = find_entry(file.output_files, "extensionlong", cmd_meta.extension) + + if not src or src == "" then + log:errorf("No extensionlong = %s found for %s",cmd_meta.extension, file.relative_path) + require 'pl.pretty'.dump(file) + end + -- local src = file.output_files[1].absolute_path -- TODO: fix + + local tgt = string.format("%s/%s.%s", folder, file.basename, src.extension) + -- require 'pl.pretty'.dump(src) + if src and path.exists(src.absolute_path) then + log:infof("Moving %s to %s", src.absolute_path, tgt) + pldir.makepath(folder) + plfile.copy(src.absolute_path, tgt) + else + log:warningf("No output file found for ",file.relative_path) + end + return 1, 'NOK' +end + +-- +-- +-- + + +--- run a complete compile-cycle on a given file +--- +--- SIDE-EFFECT: adds output_files to the file argiument !!! +--- +--- @param file metadata file on which the command should be run +--- @param compilers [compiler] list of compilers +--- @param compile_sequence table sequence of keys from the compilers table to be executed +--- @return [compile_info] statuses information from the commands +local function compile(file, compilers, compile_sequence, only_check) + only_check = only_check or false + -- + -- WARNING: (tex-)compilation HAS TO START IN THE SUBFOLDER !!! + -- !!! CHDIR might confuse all relative paths !!!! + -- + local current_dir = lfs.currentdir() + log:tracef("Changing directory to %s (for actual compilations, from %s)",file.absolute_dir,current_dir) + lfs.chdir(file.absolute_dir) + + local statuses = {} + + -- Start ALL compilations for this file, in the correct order; stop as soon as one fails... + -- NOTE: extension is a bad name, it's rather 'compiler' + for _, extension in ipairs(compile_sequence) do + local command_metadata = compilers[extension] + + if not command_metadata then + log:errorf("No compiler defined for %s (%s); SKIPPING",extension,file.relative_path) + goto endofthiscompilation -- nice: a goto-statement !!! + end + if file.extension ~= "tex" then + log:errorf("Can't compile non-tex file %s; SKIPPING, SHOULD PROBABLY NOT HAVE HAPPENED",file.relative_path) + goto endofthiscompilation -- nice: a goto-statement !!! + end + + local infix = "" + if command_metadata.infix and command_metadata.infix ~= "" then + infix = command_metadata.infix.."." + end + local output_file = file.filename:gsub("tex$", extension) + local log_file = file.filename:gsub("tex$", infix.."log") + + -- sometimes compiler wants to check for the output file (like for sagetex.sage), + if command_metadata.check_file and not path.exists(output_file) then + log:debugf("Skipping compilation because 'check_file' and file %s does not exist",output_file) + goto endofthiscompilation -- nice: a goto-statement !!! + end + + -- if not output_file.needs_compilation then + -- log:debugf("Skipping compilation file %s is uptodate",output_file) + -- goto endofthiscompilation -- nice: a goto-statement !!! + -- end + + + -- NOT NEEDED ??? + -- local command_template = command_metadata.command + -- we need to make a copy of file metadata to insert some additional fields without modification of the original + -- log:debug("Command " .. command_template) + -- local tpl_table = copy_table(file) + -- tpl_table.output_file = output_file + -- tpl_table.make4ht_extraoptions = config.make4ht_extraoptions + -- tpl_table.make4ht_mode = config.make4ht_mode + -- local command = prepare_command(tpl_table, command_template) + + -- replace placeholders like @{filename} with the corresponding keys from the metadata table + file.output_file = output_file -- for potential substitution as @{output_file} in command: + local command = command_metadata.command:gsub("@{(.-)}", file) + command = command:gsub("@{(.-)}", config) + file.output_file = nil + + local start_time = socket.gettime() + local compilation_time = 0 + local status = 0 + local output = "" + + if only_check then + log:info("Running in check-modus: SKIPPING " .. command ) + else + log:info("Running " .. command ) + -- we reuse this file from make4ht's mkutils.lua + local f = io.popen(command, "r") + output = f:read("*all") + -- rc will contain return codes of the executed command + local rc = {f:close()} + -- the status code is on the third position + -- https://stackoverflow.com/a/14031974/2467963 + status = rc[3] + local end_time = socket.gettime() + compilation_time = end_time - start_time + + if status ~= command_metadata.status then + -- error will be handled and properly logged further down! + log:errorf("Compilation of %s for %s failed: returns %d (not %d) after %3f seconds", extension, file.relative_path, status, command_metadata.status,compilation_time) + end + end + + + --- @class compile_info + --- @field output_file string output file name + --- @field command string executed command + --- @field output string stdout from the command + --- @field status number status code returned by command + --- @field errors? table errors detected in the log file + --- @field post_status? boolean did HTML processing run without errors? + --- @field post_message? string possible error message from HTML post-processing + local compile_info = { + output_file = output_file, + command = command, + output = output, + status = status + } + if command_metadata.check_log then + compile_info.errors = test_log_file(log_file) -- gets errors the make4ht-way ! + for _, err in ipairs(compile_info.errors) do + log:errorf("%-20s: %s [[%s]]", err.filename or "?", err.error, err.context) + end + end + + if status == command_metadata.status then + + -- store outputfiles with metadata + -- log:infof("ADDING METADATA FOR %s : %s (from %s)",current_dir, output_file, file.absolute_dir) + local ofile = files.get_metadata(file.absolute_dir, output_file) + + log:debug("Adding outputfile "..ofile.relative_path.. " to "..file.relative_path) + -- require 'pl.pretty'.dump(ofile) + table.insert(file.output_files,ofile) + -- require 'pl.pretty'.dump(file) + + if command_metadata.post_command then + local cmd = command_metadata.post_command + log:infof("Postprocessing: %s", cmd) + + -- call the post_command + compile_info.post_status, compile_info.post_message = _G[cmd](file, command_metadata, current_dir) -- lua way of calling the function whose name is in 'cmd' + + if not compile_info.post_status then + log:error("Error in HTML post processing: " .. compile_info.post_message) + end + end + + else + if path.exists(output_file) then + -- prevent trailing non-correct files, as they prevent automatic re-compilation ! + log:debugf("Moving failed output file to %s",output_file..".failed") + pl.file.move(output_file,output_file..".failed") + end + end + table.insert(statuses, compile_info) + + log:info(string.format("Compilation of %s took %.1f seconds (%.20s)", output_file, compilation_time, file.title)) + + if status == command_metadata.fatal_status then + log:warning("Skipping further compilations for %s after error",file.relative_file) + break -- STOP FURTHER COMPILATION + end + --end + log:tracef("Ended compilation %s", extension) + ::endofthiscompilation:: + end + lfs.chdir(current_dir) + + -- if dump_metadata then + -- log:debug("Dumping new metadata for ".. relative_file ) + -- require 'pl.pretty'.dump(metadata) + -- end + + return statuses +end + + +--- print error messages parsed from the LaTeX log +---@param errors table +local function print_errors(statuses) + for _, status in ipairs(statuses) do + local errors = status.errors or {} + if #errors > 0 then + log:error("Errors from " .. status.command .. ":") + for _, err in ipairs(errors) do + log:errorf("%20s line %s: %s", err.filename or "?", err.line or "?", err.error) + log:error(err.context) + end + end + end +end + +--- remove temporary files +---@param basefile metadata +---@param extensions table list of extensions of files to be removed +---@return number nfiles number of files removed +local function clean(basefile, extensions, only_check) + only_check = only_check or false + local nfiles = 0 + local basename = path.splitext(basefile.absolute_path) + log:tracef("%s temp files for %s (%s)", (only_check and "Would remove" or "Removing"), basename, basefile.absolute_path) + for _, ext in ipairs(extensions) do + local filename = basename .. "." .. ext + if path.exists(filename) then + log:debugf("%s %s file %s", (only_check and "Would remove" or "Removing") ,ext, filename) + if not only_check then os.remove(filename); nfiles = nfiles + 1 end + end + end + return nfiles +end + +M.compile = compile +M.print_errors = print_errors +M.clean = clean + +return M diff --git a/.texmf/luaxake/luaxake-config.lua b/.texmf/luaxake/luaxake-config.lua new file mode 100644 index 0000000..8a3ae9c --- /dev/null +++ b/.texmf/luaxake/luaxake-config.lua @@ -0,0 +1,49 @@ +local M = {} +-- load and run a script in the provided environment +-- returns the modified environment table + + +-- https://stackoverflow.com/a/69910551 +--- run Lua file and return it's environment table +--- @param scriptfile string script file name +--- @param config config configuration table +--- @return table env script environment table +local function run_test_script(scriptfile, config) + local env = setmetatable({}, {__index=config}) + assert(pcall(loadfile(scriptfile,"run_test_script",env))) + setmetatable(env, nil) + return env +end + + +-- https://stackoverflow.com/a/7470789 +--- merge tables +---@param t1 table +---@param t2 table +---@return table merged +local function merge(t1, t2) + for k, v in pairs(t2) do + if (type(v) == "table") and (type(t1[k] or false) == "table") then + merge(t1[k], t2[k]) + else + t1[k] = v + end + end + return t1 +end + + + +--- update config table from script +--- @param scriptfile string filename +--- @param config config table +--- @return config configuration table +local function update_config(scriptfile, config) + local env = run_test_script(scriptfile, config) + return merge(config, env) +end + + +M.update_config = update_config + +return M diff --git a/.texmf/luaxake/luaxake-files.lua b/.texmf/luaxake/luaxake-files.lua new file mode 100644 index 0000000..5c72adb --- /dev/null +++ b/.texmf/luaxake/luaxake-files.lua @@ -0,0 +1,447 @@ +local M = {} +local pl = require "penlight" +local graph = require "luaxake-graph" +local log = logging.new("files") +local lfs = require "lfs" + +local path = pl.path +local abspath = pl.path.abspath + +--- identify, if the file should be ignored +--- @param entry string tested file path +--- @return boolean should_be_ignored if file should be ignored +local function ignore_entry(entry) + -- files that should be ignored + if entry:match("^%.") then -- ignore 'hidden' files/dirs (i. that start with a '.') + log:trace("Ignoring file "..entry) + return true + end + return false + + -- does not work properly, eg. with clean: just add everything... + -- local attr = lfs.attributes(entry) + -- if attr and attr.mode == "directory" then + -- log:trace("Keeping folder "..entry) + -- return false + -- end + -- local extension = entry:match(".%.([^%.]+)$") + -- local exts = config.include_extensions + -- if exts[extension] then + -- log:trace("Keeping file "..entry) + -- return false + -- end + -- log:tracef("Ignoring file %s (%s)",entry, extension) + + -- return true +end + + +--- get file extension +--- @param relative_path string file path +--- @return string extension +local function get_extension(relative_path) + return relative_path:match("%.([^%.]+)$") +end + + + +--- find TeX4ht config file +--- @param filename string name of the config file +--- @param directories [string] +--- @return string path of the config file +local function find_config(filename, directories) + -- the situation with the TeX4ht config file is a bit complicated + -- it can be placed in the current directory, in the document root directory, + -- or in the kpse path. if it cannot be found in any of these places, + -- we will set it to config.config_file (presumably ximera.cfg) + -- in any case, we must provide a full path to the config file, because it will + -- be used in different directories. + for _, dir in ipairs(directories) do + local lpath = dir .. "/" .. filename + if pl.path.exists(lpath) then + log:trace("find_config found "..filename.. " in ".. lpath.."( from "..table.concat(directories,', ')..")") + return lpath + end + end + -- if we cannot find the config file in any directory, try to find it using kpse + local lpath = kpse.find_file(filename, "texmfscripts") + if lpath then + log:trace("find_config found "..filename.. " in ".. lpath.."( from kpse texmfscripts)") + return lpath + end + -- lastly, test if it is a full path to the file + if pl.path.exists(filename) then + log:trace("find_config found "..filename.. " ( as this file happens to exist)") + return filename + end + -- xhtml is default TeX4th config file, use it if we cannot find a user config file + -- return "xhtml" + return config.config_file +end + +--- get absolute and relative file path, as well as other file metadata +--- @param file string retrieved file +--- @return metadata +local function get_metadata(relpath, entry) + local dir + -- Some hocus pocus to make get_metadata work as + -- get_metadata(filename_with_path) + -- or get_metadata(dir, filename) (where the path is explicitly split ...) + -- 202412: only the filename_with_path syntax should work fine ? + if not entry then + dir, entry = path.splitpath(relpath) + else + dir = relpath + relpath = string.format("%s/%s",dir,entry) + end + dir = dir or "" + + local relative_path = path.normpath(relpath) -- resolve potential ../ parts + dir, entry = path.splitpath(relative_path) + + + log:tracef("Getting metadata for file %s (from %s and %s)", relative_path, entry, dir) + -- We have dir, entry and relative path to fill lot's of variations/combinations... + -- needs_compilation is updated later + + if ignore_entry(entry) then + log:warningf("Collecting metadata for ignored file %s (%s).", relative_path, entry) + -- return + end + + --- @class metadata + --- @field dir string relative directory path of the file + --- @field absolute_dir string absolute directory path of the file + --- @field filename string filename of the file + --- @field basename string filename without extension + --- @field extension string file extension + --- @field relative_path string relative path of the file + --- @field absolute_path string absolute path of the file + --- @field modified number last modification time + --- @field dependecies metadata[] list of files the file depends on + --- @field needs_compilation boolean + --- @field exists boolean true if file exists + --- @field output_files output_file[] + --- @field config_file? string TeX4ht config file + local metadata = { + dir = dir, + absolute_dir = abspath(dir), + filename = entry, + relative_path = relative_path, + absolute_path = abspath(relative_path), + exists = path.exists(relative_path), + modified = path.getmtime(relative_path), + dependecies = {}, + needs_compilation = false, + output_files = {}, + config_file = config.config_file, -- always the same, unless overwritten somewhere ? + } + metadata.basename, metadata.extension = entry:match("(.*)%.([^%.]+)$") + metadata.basenameshort, metadata.extensionlong = entry:match("([^%.]*)%.(.+)$") + metadata.reldir, metadata.relfile = path.splitpath(metadata.relative_path) + + if config.dump_metadata then + log:debugf("Dumping new metadata for %s (%s)", relative_path, metadata.modified ) + require 'pl.pretty'.dump(metadata) + end + return metadata +end + +--- get metadata for all files in a directory and it's subdirectories +--- @param dir string path to the directory +--- @param files? table retrieved files +--- @return metadata[] +local function get_files(dir, files) + dir = dir:gsub("/$", "") -- remove potential trailing '/' + files = files or {} + local initial_nfiles = #files + for entry in path.dir(dir) do + if not ignore_entry(entry) then + local metadata = get_metadata(dir, entry) + local relative_path = metadata.relative_path + if path.isdir(relative_path) then + files = get_files(relative_path, files) + elseif path.isfile(relative_path) then + files[#files+1] = metadata + end + end + end + log:debugf("get_files returns %4d files in %s", #files - initial_nfiles, dir) + return files +end + + +--- filter TeX files from array of files +--- @param files metadata[] list of files to be checked +--- @return metadata[] list of TeX files +local function get_tex_files(files) + local tbl = {} + for _, file in ipairs(files) do + if file.extension == "tex" then + tbl[#tbl+1] = file + end + end + return tbl +end + + +-- OBSOLETE: see add_tex_metadata ! +-- --- test if the TeX file can be compiled standalone +-- --- @param filename string name of the tested TeX file +-- --- @param linecount number number of lines that should be tested +-- --- @return boolean is_main true if the file contains \documentclass +-- local function is_main_tex_file(filename, linecount) +-- -- we assume that the main TeX file contains \documentclass near beginning of the file +-- linecount = linecount or 30 -- number of lines that will be read +-- local line_no = 0 +-- for line in io.lines(filename) do +-- line_no = line_no + 1 +-- if line_no > linecount then break end +-- if line:match("^%s*\\documentclass") then return true end +-- end +-- return false +-- end + +--- add TeX metadata: can it be compiled standalone, is it a ximera or a xourse +--- @param filename string name of the tested TeX file +--- @param linecount number number of lines that should be tested +--- @return boolean is_main true if the file contains \documentclass +local function add_tex_metadata(file, linecount) + -- we assume that the main TeX file contains \documentclass near beginning of the file + linecount = linecount or 30 -- number of lines that will be read + local filename = file.absolute_path + local line_no = 0 + for line in io.lines(filename) do + line_no = line_no + 1 + if line_no > linecount then + file.tex_type='no-document' + break end + local class_name = line:match("\\documentclass%s*%[[^]]*%]%s*{([^}]+)}") + or line:match("\\documentclass%s*{([^}]+)}") + if class_name then + file.tex_type = class_name + -- log:debug("Document class: " .. class_name) + return true + end + end + return false +end + +--- get list of compilable TeX files +--- @param files metadata[] list of TeX files to be tested +--- @return metadata[] main_tex_files list of main TeX files +local function filter_main_tex_files(files) + local t = {} + for _, metadata in ipairs(files) do + -- if is_main_tex_file(metadata.absolute_path, config.documentclass_lines ) then + if add_tex_metadata(metadata, config.documentclass_lines ) then + log:debug("Found main TeX file: " .. metadata.absolute_path.. " ("..metadata.tex_type..")" ) + t[#t+1] = metadata + else + log:debug("Not a MAIN TeX file: " .. metadata.absolute_path) + end + end + return t +end + +--- Detect if the output file needs recompilation +---@param tex metadata metadata of the main TeX file to be compiled +---@param outfile metadata metadata of the output file +---@return boolean +local function needs_compiling(tex, outfile) + -- if the output file doesn't exist, it needs recompilation + log:tracef("Does %s need compilation? %s",outfile.relative_path, outfile.exists and "It exists" or "It doesn't exist") + if not outfile.exists then return true end + -- test if the output file is older if the main file or any dependency + local status = tex.modified > outfile.modified + if status then + log:tracef("TeX file %s has changed since compilation of %s",tex.relative_path, outfile.relative_path) + end + for _,subfile in ipairs(tex.dependecies or {}) do + -- log:tracef("Check modified of subfile %s", subfile.relative_path) + if not subfile or not subfile.relative_path or not subfile.modified then + log:warning("Incomplete data for dependency of %s",tex.relative_path) + pl.pretty.dump(subfile) + end + if subfile.modified > outfile.modified then + log:tracef("Dependent file %s has changed since compilation of %s",subfile.relative_path, outfile.relative_path) + status = status or subfile.modified > outfile.modified + end + end + log:tracef("%s %s", outfile.relative_path, status and "needs compilation" or "does not need compilation") + return status +end + +--- get list of files included in the given TeX file +--- @param metadata metadata TeX file metadata +--- @return metadata[] dependecies list of files included from the file +local function get_tex_dependencies(metadata) + local filename = metadata.absolute_path + local current_dir = metadata.absolute_dir + local dependecies = config.default_dependencies --- XXX + local dependecies = {} + table.move(config.default_dependencies, 1, #(config.default_dependencies), 1, dependecies) + -- local dependecies = {} + local f = io.open(filename, "r") + if f then + local content = f:read("*a") + f:close() + -- loop over all LaTeX commands with arguments + for command, argument in content:gmatch("\\(%w+)%s*{([^%}]+)}") do + -- add dependency if the current command is \input like + if config.input_commands[command] then + local metadata = get_metadata(current_dir, argument) + if not metadata or not metadata.exists then + -- the .tex extension may be missing, so try to read it again + metadata = get_metadata(current_dir, argument .. ".tex") + end + if metadata and metadata.exists then + log:debugf("File "..filename," depends on "..metadata.absolute_path) + dependecies[#dependecies+1] = metadata + else + log:warningf("No metadata found for %s/%s; not added to dependencies.", current_dir, argument) + end + end + end + end + log:debugf("tex_dependencies found %d dependencies for %s", #dependecies, filename) + return dependecies +end + + +--- check if any output file needs a compilation +--- @param metadata metadata metadata of the TeX file +--- @param extensions table list of extensions +--- @return boolean needs_compilation true if the file needs compilation +--- @return output_file[] list of output files +local function check_output_files(metadata, extensions, compilers) + local output_files = {} + local tex_file = metadata.filename + local needs_compilation = false + for _, extension in ipairs(extensions) do + local html_file = get_metadata(metadata.absolute_dir, tex_file:gsub("tex$", extension)) + -- detect if the HTML file needs recompilation + local status = needs_compiling(metadata, html_file) + -- for some extensions (like sagetex.sage), we need to check if the output file exists + -- and stop the compilation if it doesn't + local compiler = compilers[extension] or {} + if compiler.check_file and not path.exists(html_file.absolute_path) then + log:debug("Ignored output file doesn't exist: " .. html_file.absolute_path) + status = false + end + needs_compilation = needs_compilation or status + log:debugf("%-12s %8s: %s",extension, status and 'COMPILE' or 'OK', html_file.absolute_path) + --- @class output_file + --- @field needs_compilation boolean true if the file needs compilation + --- @field metadata metadata of the output file + --- @field extension string of the output file + -- output_files[#output_files+1] = { + -- needs_compilation = status, + -- metadata = html_file, + -- extension = extension + -- } + -- Mmm, use a 'flatter' structure for output_files ... + html_file.needs_compilation = status + html_file.extension = extension + output_files[#output_files+1] = html_file + end + return needs_compilation, output_files +end + +--- create sorted table of files that needs to be compiled +--- @param tex_files metadata[] list of TeX files metadata +--- @return metadata[] to_be_compiled list of files in order to be compiled +local function sort_dependencies(tex_files, force_compilation) + -- create a dependency graph for files that needs compilation + -- the files that include other courses needs to be compiled after changed courses + -- at least that is what the original Xake command did. I am not sure if it is really necessary. + log:tracef("Sorting dependencies (%s)", force_compilation) + + local Graph = graph:new() + local used = {} + local to_be_compiled = {} + -- first add all used files + for _, metadata in ipairs(tex_files) do + log:tracef("Consider %s", metadata.absolute_path) + + if force_compilation or metadata.needs_compilation then + Graph:add_edge("root", metadata.absolute_path) + used[metadata.absolute_path] = metadata + end + end + + -- now add edges to included files which needs to be recompiled + for _, metadata in pairs(used) do + local current_name = metadata.absolute_path + log:tracef("Get used = %s (%s)",current_name,metadata.dependecies) + for _, child in ipairs(metadata.dependecies or {}) do + local name = child.absolute_path + log:tracef("Get child = %s",name) + -- add edge only to files added in the first run, because only these needs compilation + if used[name] then + log:tracef("Added edge %s - %s", current_name, name) + Graph:add_edge(current_name, name) + end + end + end + log:tracef("Topographic sort") + + -- topographic sort of the graph to get dependency sequence + local sorted, msg = Graph:sort() + + if not sorted then + log:errorf("Could not sort dependency Graph: %s", msg) + log:errorf("RETURNING UNSORTED LIST") + return tex_files + else + -- we need to save files in the reversed order, because these needs to be compiled first + for i = #sorted, 1, -1 do + local name = sorted[i] + log:tracef("Adding to be compiled %2d: %s",i,name) + to_be_compiled[#to_be_compiled+1] = used[name] + end + end + return to_be_compiled +end + +--- find TeX files that needs to be compiled in the directory tree +--- @param dir string root directory where we should find TeX files +--- @return metadata[] tex_files list of all TeX files found in the directory tree +local function get_tex_files_with_status(dir, output_formats, compilers) + log:debugf("Getting tex files in %s (for %s and %s)", dir, table.concat(output_formats,', '), table.concat(compilers,', ')) + local files = get_files(dir) + local tex_files = filter_main_tex_files(get_tex_files(files)) + -- now check which output files needs a compilation + for _, metadata in ipairs(tex_files) do + -- get list of included TeX files + metadata.dependecies = get_tex_dependencies(metadata) + -- check for the need compilation + local status, output_files = check_output_files(metadata, output_formats, compilers) + metadata.needs_compilation = status + metadata.output_files = output_files + -- try to find the TeX4ht .cfg file + -- to speed things up, we will find it only for files that needs a compilation + if metadata.needs_compilation then + -- search in the current work dir first, then in the directory of the TeX file, and project root + -- TODO: check use of 'config.dir' !!! + metadata.config_file = find_config(config.config_file, {lfs.currentdir(), metadata.absolute_dir, abspath(dir)}) + if metadata.config_file ~= config.config_file then log:debug("Use config file: " .. metadata.config_file) end + end + if status then + log:infof("%-12s %8s: %s", metadata.extension, status and 'CHANGED' or 'OK', metadata.absolute_path) + else + log:debugf("%-12s %8s: %s", metadata.extension, status and 'CHANGED' or 'OK', metadata.absolute_path) + end + end + + -- SKIPPED: create ordered list of files that needs to be compiled + return tex_files +end + + +M.get_tex_files_with_status = get_tex_files_with_status +M.sort_dependencies = sort_dependencies +M.get_metadata = get_metadata + + +return M diff --git a/.texmf/luaxake/luaxake-frost.lua b/.texmf/luaxake/luaxake-frost.lua new file mode 100644 index 0000000..4bc2215 --- /dev/null +++ b/.texmf/luaxake/luaxake-frost.lua @@ -0,0 +1,377 @@ +local M = {} +local pl = require "penlight" +local path = require "pl.path" +local html = require "luaxake-transform-html" +local files = require "luaxake-files" +local log = logging.new("frost") + +local json = require("dkjson") + +--- save Ximera metadata.json file (with labels/xourses/...) +--- @param xmmetadata table ximera metadata table +--- @return boolean success +local function save_as_json(xmmetadata) + local file = io.open("metadata.json", "w") + + if file then + local contents = json.encode(xmmetadata) + file:write( contents ) + io.close( file ) + return true + else + return false + end + end + + + +local function osExecute(cmd) + log:debug("Exec: "..cmd) + local fileHandle = assert(io.popen(cmd .. " 2>&1", 'r')) + local commandOutput = assert(fileHandle:read('*a')) + local returnCode = fileHandle:close() and 0 or 1 + commandOutput = string.gsub(commandOutput, "\n$", "") + if returnCode > 0 then + log:warningf("Command %s returns %d: %s", cmd, returnCode, commandOutput) + end + log:trace("returns "..returnCode..": "..commandOutput..".") + return returnCode, commandOutput +end + +local function get_output_files(file, extension) + local result = {} + for _, entry in ipairs(file.output_files) do + if entry.extension == extension then --and entry.info.type == targetType then + if extension == "make4ht.html" then + local file = files.get_metadata(entry.reldir, entry.basenameshort..".html") + require 'pl.pretty'.dump(entry) + require 'pl.pretty'.dump(file) + table.insert(result, file) + log:debug(string.format("Hacking %-4s outputfile: %s ", file.extension, file.absolute_path)) + else + table.insert(result, entry) + log:debug(string.format("Adding %-4s outputfile: %s ", entry.extension, entry.absolute_path)) + end + else + log:tracef("Skipping %-4s outputfile: %s ", entry.extension, entry.absolute_path) + end + end + return result +end + +local function get_git_uncommitted_files() + local ret, out = osExecute("git ls-files --modified --other --exclude-standard") + if ret > 0 then + osError("Could not get git info: %s",out) + out = "GIT ERROR" + end + local utils = require "pl.utils" + return utils.split(out,"\n") +end + +-- -- Recursive function to list all files in a directory +-- function list_files(path, files) +-- files = files or {} +-- for file in lfs.dir(path) do +-- -- Skip "." and ".." (current and parent directory) +-- if file ~= "." and file ~= ".." then +-- local full_path = path .. "/" .. file +-- local attr = lfs.attributes(full_path) + +-- -- If it's a directory, recurse into it +-- if attr.mode == "directory" then +-- list_files(full_path,files) +-- --table.move(nfiles,1,#nfiles,#files+1,files) +-- else +-- -- If it's a file, print its path +-- -- f:write(full_path.."\n") +-- files[#files+1] = full_path +-- end +-- end +-- end +-- return files +-- end + + +-- -- Function to find the first table with a given key/value using Penlight +-- local function find_entry(array, key, value) +-- for _, entry in ipairs(array) do +-- if entry[key] == value then +-- return entry -- Return the first matching entry +-- end +-- end +-- return nil -- Return nil if no match is found +-- end + + +-- function move_to_downloads(file, cmd_meta, root_dir) +-- local folder = string.format("%s/%s/%s",root_dir, cmd_meta.download_folder, file.dir) + +-- require 'pl.pretty'.dump(cmd_meta) +-- require 'pl.pretty'.dump(file) +-- local src = find_entry(file.output_files, "extensionlong", cmd_meta.extension) +-- -- local src = file.output_files[1].absolute_path -- TODO: fix + +-- local tgt = string.format("%s/%s.%s", folder, file.basename, src.extension) +-- -- require 'pl.pretty'.dump(src) +-- if src and path.exists(src.absolute_path) then +-- log:infof("Moving %s to %s", src.absolute_path, tgt) +-- pldir.makepath(folder) +-- plfile.copy(src.absolute_path, tgt) +-- else +-- log:warningf("No output file found for ",file.relative_path) +-- end +-- return 1, 'NOK' +-- end + + +--- Frosting: create a 'publications' commit-and-tag +---@param file metadata -- presumably only root-folder really makes sense for 'frosting' +---@return boolean status +---@return string? msg +local function frost(tex_files, to_be_compiled_files, root) + log:debug("frost") + + local uncommitted_files = get_git_uncommitted_files() + + if uncommitted_files then + log:warningf("There are %d uncommitted files; serving only to localhost", #uncommitted_files) + end + + if #to_be_compiled_files > 0 then + log:warningf("There are %d file to be compiled; serving only to localhost", #to_be_compiled_files) + end + + -- local tex_files = files.get_tex_files_with_status(root, config.output_formats, config.compilers) + -- TODO: warn/error/compile if there are to_be_compiled files ? + + local needing_publication = {} + local all_labels = {} + local tex_xourses = {} + for i, tex_file in ipairs(tex_files) do + log:debug("Output for "..tex_file.absolute_path) + needing_publication[#needing_publication + 1] = tex_file.relative_path + + local html_files = get_output_files(tex_file, "make4ht.html") + + for i,html_file in ipairs(html_files) do + -- require 'pl.pretty'.dump(html_file) + + log:debug("Output for "..html_file.absolute_path) + needing_publication[#needing_publication + 1] = html_file.relative_path + + local html_name = html_file.absolute_path + local dom, msg = html.load_html(html_name) + if not dom then + log:errorf("No dom for %s (%s). SKIPPING", html_name, msg) + break + end + + -- get all anchors (from \label) + html_file.labels = html.get_labels(dom) + + -- merge them in a big table, to be added to metadata.json + for k,v in pairs(html_file.labels) do + if all_labels[k] then + log:warningf("Label %s already used in %s; ignoring for %s",k, all_labels[k], html_file.relative_path) + else + all_labels[k] = html_file.relative_path + log:tracef("Label %s added for %s",k,html_file.relative_path) + end + end + + local ass_files = html.get_associated_files(dom, html_file) + + table.move(ass_files, 1, #ass_files, #needing_publication + 1, needing_publication) + + + html_file.associated_files = ass_files + + log:debug(string.format("Added %4d files for new total of %4d for %s", #ass_files+2, #needing_publication, html_file.relative_path)) + -- require 'pl.pretty'.dump(to_be_compiled) + + -- Store xourses, they have to be added to metadata.json + if tex_file.tex_type == "xourse" then + log:info("Adding XOURSE "..tex_file.absolute_path.." ("..html_file.title..")") + tex_xourses[html_file.basename] = { title = html_file.title, abstract = html_file.abstract } + end + + end + end + + -- TODO: check/fix use of 'github'; check use of labels + local xmmetadata={ + xakeVersion = "2.1.3", + labels = all_labels, + githubexample = { + + owner = "XimeraProject", + repository = "ximeraExperimental" + }, + github = {}, + xourses = tex_xourses, + } + + + save_as_json(xmmetadata) + -- require 'pl.pretty'.dump(tex_xourses) + + needing_publication[#needing_publication + 1] = "metadata.json" + + -- + -- START FROSTING + -- + + local _, head_oid = osExecute("git rev-parse HEAD") + if not head_oid then + log:error("No headid returned by git rev-parse HEAD") + end + + + local publication_branch = "PUB_"..head_oid + + local ret, publication_oid = osExecute("git rev-parse --verify --quiet "..publication_branch) + + if ret > 0 then + osExecute("git branch "..publication_branch) + publication_oid = head_oid + end + log:debug("GOT publication_oid "..(publication_oid or "")) + + + if path.exists("ximera-downloads") then + -- needing_publication[#needing_publication + 1] = "ximera-downloads" + osExecute("git add -f ximera-downloads") + else + log:debug("No ximera-downloads folder, and thus no PDF files will be available for download") + end + -- require 'pl.pretty'.dump(needing_publication) + + -- 'git add' the files in batches of 10 (risks line-too-long!) + -- local files_string = table.concat(needing_publication,",") + -- Execute the git add command + + -- local downloads = list_files("ximera-downloads") + -- table.move(downloads, 1, #downloads, #needing_publication + 1, needing_publication) + +if false then + local f = io.open(".xmgitindexfiles", "w") + + for _, line in ipairs(needing_publication) do + log:trace("ADDING "..line) + f:write(line .. "\n") + end + f:close() + -- Close the process to flush stdin and complete execution + local proc = io.popen("cat .xmgitindexfiles | git update-index --add --stdin") + local output = proc:read("*a") + local success, reason, exit_code = proc:close() + + if not success then + log:errorf("git update-index fails with %s (%d)",reason, exit_code) + else + log:debugf("Added %d files (%s)", #needing_publication,output) + end + +else + for _, line in ipairs(needing_publication) do + log:trace("ADDING "..line) + osExecute("git add -f "..line) + end +end + + + local _, new_tree = osExecute("git write-tree") + if not new_tree then + log:error("No tree returned by git write-tree") + end + log:debug("Made new tree ", new_tree) + + + + -- local tagName = "publications/"..head_oid + + -- result, tag_oid = osExecute("git for-each-ref --sort=-creatordate --count=1 --format '%(refname:strip=2)' refs/tags/publications/*") + + local result, most_recent_publication = osExecute("git for-each-ref --sort=-creatordate --count=1 --format '%(tree) %(objectname) %(refname:strip=2)' refs/tags/publications/*") + + if not most_recent_publication or most_recent_publication == "" then + log:info("No publication found") + else + + log:debugf("Got publication: %s",most_recent_publication) + + + local tagtree_oid, tag_oid, tagName = most_recent_publication:match("([^%s]+) ([^%s]+) ([^%s]+)") + + log:infof("Found %s (tree:%s tag:%s) ", tagName, tagtree_oid, tag_oid) + + end + + if tagtree_oid and tagtree_oid == new_tree then + log:statusf("Tag "..tagName.." already exists (for %s)",tag_oid) + return 0, 'OK' + end + + -- -- Give a dummy account to push/commit if none is available + -- ret, output = osExecute("git config --get user.name || { echo Setting git user.name; git config --local user.name 'xmlatex Xake'; }") + -- ret, output = osExecute("git config --get user.email || { echo Setting git user.email; git config --local user.email 'xmlatex@xakecontainer'; }") + + local ret, commit_oid = osExecute("git commit-tree -m "..publication_branch.." -p "..publication_oid.." "..new_tree) + if ret > 0 then + return ret, commit_oid -- this is the errormessage in this case! + end + log:debug("GOT commit "..(commit_oid or "")) + + if logging.show_level <= logging.levels["trace"] then + log:tracef("Committed files for %s:", commit_oid) + osExecute("git ls-tree -r --name-only "..commit_oid) + end + + local ret, output = osExecute("git reset") + + -- TODO: check this, we might be creating too many commits/.. + if false and tagtree_oid then + log:statusf("Updating tag %s for %s (was %s)", tagName, commit_oid, tag_oid) + ret, output = osExecute("git update-ref refs/tags/"..tagName.." "..commit_oid) + else + --local tagName = "publications/"..os.date("%Y%m%d_%H%M%S") + local tagName = "publications/"..commit_oid + log:statusf("Creating tag %s for %s", tagName, commit_oid) + ret, output = osExecute("git tag "..tagName.." "..commit_oid) + -- if ret > 0 then + -- log:errorf("Created tag %s for %s: %s", tagName, commit_oid, output) + -- end + end + return ret, output +end + +local function serve() + + local result, most_recent_publication = osExecute("git for-each-ref --sort=-creatordate --count=1 --format '%(tree) %(objectname) %(refname:strip=2)' refs/tags/publications/*") + + if not most_recent_publication or most_recent_publication == "" then + log:warning("No publication tags found. Need 'frost' first?") + return 1, 'No publications found' + end + + log:debugf("Got publication: %s",most_recent_publication) + + + local tree_oid, tag_oid, tagName = most_recent_publication:match("([^%s]+) ([^%s]+) ([^%s]+)") + + log:infof("Publishing %s (tree:%s tag:%s) ", tagName, tree_oid, tag_oid) + + osExecute("git push -f ximera "..tagName) + osExecute("git push -f ximera "..tag_oid..":refs/heads/master") -- HACK ??? + + log:statusf("Published %s", tagName) + + return 0,'OK' +end + +M.get_output_files = get_output_files +M.frost = frost +M.serve = serve + +return M diff --git a/.texmf/luaxake/luaxake-graph.lua b/.texmf/luaxake/luaxake-graph.lua new file mode 100644 index 0000000..38a94e2 --- /dev/null +++ b/.texmf/luaxake/luaxake-graph.lua @@ -0,0 +1,125 @@ +local Graph = { nodes = {} } + +function Graph:new() + setmetatable(Graph, self) + self.__index = self + return self +end + +--- initialize graph node +---@param a string +---@return table +function Graph:add_node(a) + local node = self.nodes[a] or { edges = {}, edges_from = {}} + self.nodes[a] = node + return node +end + +--- add edge between nodes +---@param a string parent node +---@param b string child node +---@param value? any +function Graph:add_edge(a,b, value) + -- get the parent edge + local parent = self:add_node(a) + -- initialize also the second node + local child = self:add_node(b) + -- we can either add a value, or just simply use true + value = value or true + parent.edges[b] = value + child.edges_from[a] = value +end + +--- get node +---@param a string node name +---@return table node +function Graph:get_node(a) + return self.nodes[a] +end + +--- count number of items in a hash table +---@param tbl table hash table to be counted +---@return number count +local function count_table(tbl) + local count = 0 + for _, _ in pairs(tbl) do count = count + 1 end + return count +end + +--- count number of edges pointing to the given node +---@param a string node +---@return number count of edges +function Graph:count_incoming_edges(a) + return count_table(self.nodes[a].edges_from) +end + + + +function Graph:sort() + -- based on Kahn's algorithm + local L = {} + local S = {} + local all_edges = {} + -- prepare table with all edges that points to the given node + for name, node in pairs(self.nodes) do + local t = {} + for edge, _ in pairs(node.edges_from) do t[edge] = true end + all_edges[name] = t + end + -- get list of nodes with no incoming edges + for name, _ in pairs(self.nodes) do + if self:count_incoming_edges(name) == 0 then S[#S+1] = name end + end + while #S > 0 do + -- remove first entry from the list of nodes with no incoming edges + local n = table.remove(S, 1) + L[#L+1] = n + local node = self:get_node(n) + -- find all nodes that are children of n + for m, _ in pairs(node.edges) do + -- get edges that point to m + local edges = all_edges[m] + if edges[n] then + -- remove edge from n to m + edges[n] = nil + -- if there are no other edges, we need to process this node in the next run of this loop + if count_table(edges) == 0 then + S[#S+1] = m + end + end + end + end + -- test if we removed all edges + local count = 0 + for _, tbl in pairs(all_edges) do + count = count + count_table(tbl) + end + if count > 0 then + return nil, "Graph has "..count.." cycles" + end + return L +end + +--------------------- +-- Example of use: -- +-- ------------------ +-- local graph = Graph:new() +-- +-- +-- graph:add_edge("a", "b") +-- graph:add_edge("a", "c") +-- graph:add_edge("b", "d") +-- graph:add_edge("c", "d") +-- graph:add_edge("d", "e") +-- graph:add_edge("b", "e") +-- +-- for _, name in ipairs(graph:sort()) do +-- print(name) +-- end +-- + +return Graph + + + + diff --git a/.texmf/luaxake/luaxake-logging.lua b/.texmf/luaxake/luaxake-logging.lua new file mode 100644 index 0000000..7151838 --- /dev/null +++ b/.texmf/luaxake/luaxake-logging.lua @@ -0,0 +1,154 @@ +-- logging system for luaxake, minor adaptation from make4ht +-- inspired by https://github.com/rxi/log.lua +local logging = {} + +local levels = {} +-- level of bugs that should be shown +-- enable querying of current log level +logging.show_level = 1 +local max_width = 0 +local max_status = 0 + +logging.use_colors = true + +logging.modes = { + {name = "trace", color = 36}, + {name = "debug", color = 34}, + {name = "info", color = 32}, + {name = "status", color = 37}, + {name = "warning", color = 33}, + {name = "error", color = 31, status = 1}, + {name = "fatal", color = 35, status = 2} +} + +-- local posix = require("posix") -- not in tex ? +-- local ffi = require("ffi") +-- ffi.cdef[[ +-- int getpid(); +-- ]] +local pid = os.time() +-- Map the pid to a sting between AA - ZZ, eg as follows: +local firstLetter = string.char(65 + (pid % 26)) -- First letter (A-Z) +local secondLetter = string.char(65 + ((pid // 26) % 26)) -- Second letter (A-Z) + +local run_identifier = firstLetter .. secondLetter +logging.use_runidentifier = true +logging.dateformat = "%Y%m%d_".. run_identifier.." %H:%M:%S" + + + +-- prepare table with mapping between mode names and corresponding levels + +function logging.prepare_levels(modes) + local modes = modes or logging.modes + logging.modes = modes + for level, mode in ipairs(modes) do + levels[mode.name] = level + mode.level = level + max_width = math.max(string.len(mode.name), max_width) + end + logging.levels = levels + +end + +-- the logging level is set once +function logging.set_level(name) + local level = levels[name] or 1 + logging.show_level = level +end + +function logging.set_outfile(name) + logging.outfile = name +end + +function logging.print_msg(header, message, color) + local color = color or 0 + -- use format for colors depending on the use_colors option +-- local header = "[" .. header .. "]" + local color_format = logging.use_colors and string.format("\27[%im%%s\27[0m%%s", color) or "%s%s" + -- the padding is maximal mode name width + brackets + space + local padded_header = string.format("[%-".. max_width .. "s] ", header) + local output= string.format(color_format, padded_header, message) + print(output) + if logging.outfile then + local output= string.format("%s%s", padded_header, message) -- no color ! + local fp = io.open(logging.outfile, "a") + local str = string.format("%s: %s\n", os.date(logging.dateformat), output) + fp:write(str) + fp:close() + end +end + +-- +function logging.new(module) + local obj = { + module = module, + output = function(self, output) + -- used for printing of output of commands + if logging.show_level <= (levels["debug"] or 1) then + print(output) + end + end + } + obj.__index = obj + -- make a function for each mode + for _, mode in ipairs(logging.modes) do + local name = mode.name + local color = mode.color + local status = mode.status or 0 + obj[name] = function(self, ...) + -- set make4ht exit status + max_status = math.max(status, max_status) + -- max width is saved in logging.prepare_levels + if mode.level >= logging.show_level then + -- support variable number of parameters + local table_with_holes = table.pack(...) + local table_without_holes = {} + -- trick used to support the nil values in the varargs + -- https://stackoverflow.com/a/7186820/2467963 + for i= 1, table_with_holes.n do + table.insert(table_without_holes, tostring(table_with_holes[i]) or "") + end + local msg = table.concat(table_without_holes, "\t") + logging.print_msg(string.upper(name), string.format("%7s: %s", self.module, msg), color) + end + end + obj[name.."f"] = function(self, ...) + -- set make4ht exit status + max_status = math.max(status, max_status) + -- max width is saved in logging.prepare_levels + if mode.level >= logging.show_level then + local msg = string.format(...) + logging.print_msg(string.upper(name), string.format("%7s: %s", self.module, msg), color) + end + end + end + return setmetatable({}, obj) + +end + +-- exit make4ht with maximal error status +function logging.exit_status() + os.exit(max_status) +end + + +-- prepare default levels +logging.prepare_levels() + +-- for _, mode in ipairs(logging.modes) do +-- logging.print_msg(mode.name,"xxxx", mode.color) +-- end + +-- local cls = logging.new("sample") +-- cls:warning("hello") +-- cls:error("world") +-- cls:info("set new level") +-- logging.set_level("error") +-- cls:info("level set") +-- cls:error("just print the error") +-- + + +return logging + diff --git a/.texmf/luaxake/luaxake-transform-html.lua b/.texmf/luaxake/luaxake-transform-html.lua new file mode 100644 index 0000000..a4d113d --- /dev/null +++ b/.texmf/luaxake/luaxake-transform-html.lua @@ -0,0 +1,488 @@ +-- post-process HTML files created by TeX4ht to a form suitable for Ximera +local M = {} +local log = logging.new("html") +local domobject = require "luaxml-domobject" +local pl = require "penlight" +local path = require "pl.path" + +local url = require("socket.url") +-- local url = require "lualibs-url" + + +-- Function to create a backup copy of a file +local function backup_file(original_filename, extension) + -- Determine the backup filename (e.g., "file.txt" -> "file.txt.bak") + local backup_filename = original_filename .. "." .. (extension or "ORG") + + -- Open the original file for reading in binary mode + local original_file = io.open(original_filename, "rb") + if not original_file then + log:error("backup_file: Could not open the original file "..original_filename.." for reading.") + return false + end + + -- Read the entire contents of the original file + local content = original_file:read("*all") + original_file:close() + + -- Open the backup file for writing in binary mode + local backup_file = io.open(backup_filename, "wb") + if not backup_file then + log:error("backup_file: Could not open the backup file"..backup_filename.." for writing.") + return false + end + + -- Write the content to the backup file + backup_file:write(content) + backup_file:close() + + log:debug("Backup created: " .. backup_filename) + return true +end + + +--- find metadata for the HTML file +---@param file metadata +---@return metadata|nil html file +---@return string? error +local function find_html_file(file) + -- file metadata passed to the process function are for the TeX file + -- we need to find metadata for the output HTML file + log:trace("find_html_file for "..file.filename) + for _, output in ipairs(file.output_files) do + -- log:debug("Checking for 'html': "..(output.metadata.filename or "") ) + if output.extension == "html" then + log:trace("Returning: "..(output.filename or "")) + -- require 'pl.pretty'.dump(output) + return output + end + end + return nil, "Cannot find output HTML file for "..file.filename +end + + +local html_cache = {} + +--- load DOM from a HTML file +---@param filename string +---@return DOM_Object|nil dom +---@return string? error_message +local function load_html(filename) + -- cache DOM objects + if html_cache[filename] then + log:trace("returning cached dom ") + -- require 'pl.pretty'.dump(domobject.html_parse(content)) + return html_cache[filename] + else + log:debug("Loading and parsing html for "..filename) + local f = io.open(filename, "r") + if not f then return nil, "Cannot open HTML file: " .. (filename or "") end + -- log:debug("Opened html for "..filename) + local content = f:read("*a") + f:close() + -- log:debug("Dumping html for "..filename..": "..content) + html_cache[filename] = domobject.html_parse(content) + log:trace("returning non-cached dom ") + return domobject.html_parse(content) + end +end + +--- detect if the HTML file is xourse +---@param dom DOM_Object +---@return boolean +local function is_xourse(dom, html_file) + local metas = dom:query_selector("meta[name='description']") + if #metas == 0 then + -- log:warning("Cannot find any meta[description] tags in " .. html_file.absolute_path) + log:debug("No meta[description] tags in " .. html_file.absolute_path .. " (and thus not a xourse)") + end + for _, meta in ipairs(metas) do + if meta:get_attribute("content") == "xourse" then + log:debug("File "..html_file.relative_path.." is a xourse") + return true + else + log:debug("File "..html_file.relative_path.." has not-a-xourse description tag "..(meta.get_attribute("content") or "")) + end + end + -- log:debug("File "..html_file.relative_path.." is not a xourse ") + return false +end + +local function is_element_empty(element) + -- detect if element is empty or contains only blank spaces + local children = element:get_children() + if #children > 1 then return false + elseif #children == 1 then + if children[1]:is_text() then + if children[1]._text:match("^%s*$") then + return true + end + return false + end + return false + end + return true + +end + +--- Remove empty paragraphs +---@param dom DOM_Object +local function remove_empty_paragraphs(dom) + for _, par in ipairs(dom:query_selector("p")) do + if is_element_empty(par) then + log:trace("Removing empty par") + par:remove_node() + end + end +end + +local function read_title_and_abstract(activity_dom) + local title, abstract + local title_el = activity_dom:query_selector("title")[1] + if title_el then title = title_el:get_text() end + log:debug("Read title ", title) + local abstract_el = activity_dom:query_selector("div.abstract")[1] + if abstract_el then + return title, abstract_el:get_text() + end + return title, nil +end + +local function get_labels(activity_dom) + local labels = {} + for i, anchor in ipairs(activity_dom:query_selector("a.ximera-label")) do + -- require 'pl.pretty'.dump(anchor) + local label = anchor:get_attribute("id") + labels[label] = (labels[label] or 0) + 1 + log:tracef("Found label %s", label ) + if labels[label] > 1 then + log:warning("Duplicate label ",label) + end + end + return labels +end + +--- Transform Xourse files +---@param dom DOM_Object +---@param file metadata +---@return DOM_Object +local function transform_xourse(dom, file) + for _, activity in ipairs(dom:query_selector("a.activity")) do + local href = activity:get_attribute("href") + log:trace("activity", href) + if href then + -- some activity links don't have links to HTML files + -- remove the optional '.tex' + local newhref = href + if path.extension(href) == ".tex" then newhref, _ = path.splitext(href) end + -- add .html if no extension (anymore) + if path.extension(newhref) == "" then newhref = newhref .. ".html" end + + if newhref ~= href then + -- TODO: href has now added .html suffix. but maybe it was without suffix for some specific reason in the first place + log:debug("Resetting href to "..newhref .. "( from "..href..")") + activity:set_attribute("href",newhref) + end + + local htmlpath = file.absolute_dir .. "/" .. newhref + + if not path.exists(htmlpath) then + log:error("HTML file "..htmlpath.." for activity in "..file.filename.." not (yet?) found; SKIPPING add/update title and abstract") + else + -- add the title and abstract of the activity to the xourse file ... + log:debug("Updating title/abstract for activity ", htmlpath) + + local activity_dom, msg = load_html(htmlpath) + if not activity_dom then + log:error(msg) + else + + local title, abstract = read_title_and_abstract(activity_dom) + -- add titles and abstracts from linked activity HTML + -- local parent = activity:get_parent() + local parent = activity + -- local pos = activity:find_element_pos() + if title and title ~= "" then + local h2 = parent:create_element("h2") + local h2_text = h2:create_text_node(title ) + log:debug("Adding h2 for "..href..": "..title) + h2:add_child_node(h2_text) + parent:add_child_node(h2,1) + end + -- the problem with abstract is that Ximera redefines \maketitle in TeX4ht to produce nothing, + -- abstract in Ximera is part of \maketitle, so abstracts are missing in the generated HTML + if abstract then + --require 'pl.pretty'.dump(abstract) + local h3 = parent:create_element("h3") + local h3_text = h3:create_text_node(abstract) + log:debug("Adding abstract (h3) for "..href..": "..abstract) + h3:add_child_node(h3_text) + parent:add_child_node(h3,1) + end + end + end + end + end + + return dom +end + +--- return sha256 digest of a file +---@param filename string +---@return string|nil hash +---@return unknown? error +local function hash_file(filename) + -- Xake used sha1, but we don't have it in Texlua. On the other hand, sha256 is built-in + local f = io.open(filename, "r") + if not f then return nil, "Cannot open TeX dependency for hashing: " .. (filename or "") end + local content = f:read("*a") + f:close() + -- the digest return binary code, we need to convert it to hexa code + local bincode = sha2.digest256(content) + local hexs = {} + for char in bincode:gmatch(".") do + hexs[#hexs+1] = string.format("%X", string.byte(char)) + end + return table.concat(hexs) +end + + + +--- Add metadata with TeX file dependencies to the HTML DOM +---@param dom DOM_Object +---@param file metadata +---@return DOM_Object +local function add_dependencies(dom, file) + -- we will add also TeX file of the current HTML file + local t = {file} + -- copy dependencies, as we have an extra entry of the current file + for _, x in ipairs(file.dependecies) do t[#t+1] = x end + local head = dom:query_selector("head")[1] + if not head then log:error("Cannot find head element " .. file.absolute_path:gsub("tex$", "html")) end + for _, dependency in ipairs(file.dependecies) do + log:debug("dependency", dependency.relative_path, dependency.filename, dependency.basename) + local hash, msg = hash_file(dependency.absolute_path) + if not hash then + log:warning(msg) + else + local content = hash .. " " .. dependency.filename + local meta = head:create_element("meta", {name = "dependency", content = content}) + local newline = head:create_text_node("\n") + head:add_child_node(meta) + head:add_child_node(newline) + end + + end + return dom +end + + + +--- get file extension +--- @param relative_path string file path +--- @return string extension +local function get_extension(relative_path) + return relative_path:match("%.([^%.]+)$") +end + + +--- Get all files 'associated' with a given file (i.e. images) +---@param dom DOM_Object +---@param file metadata +---@return table +local function get_associated_files(dom, file) + log:debug("get_associated_files for "..file.filename) + -- pl.pretty.dump(file) + local ass_files = {} + local isXimeraFile = dom:query_selector("meta[name='ximera']")[1] + if not isXimeraFile then + log:warning(file.filename.." is not a ximera file (no meta[name='ximera' tag])") + end + + local title, abstract = read_title_and_abstract(dom) + file.title = title or "" + file.abstract = abstract or "" + + log:debug(string.format("Added title '%20.20s...' and abstract '%.10s...'", title, abstract)) + + + -- Add images + for _, img_el in ipairs(dom:query_selector("img") ) do + local src = img_el:get_attribute("src") + src = (file.dir or ".").."/"..src + log:debug("Found img "..src) + + if not path.exists(src) then + log:error("Image file "..src.." does not exist") + end + if path.getsize(src) == 0 then + log:error("Image file "..src.." has size zero") + end + + local u = url.parse(src) + + ass_files[#ass_files+1] = src + + if false and get_extension(u.path) == "svg" + then + local png = u.path:gsub(".svg$", ".png") + log:debug("also adding "..png) + ass_files[#ass_files+1] = png + end + -- sourceUrl, err := url.Parse(source) + + -- if err == nil { + -- if sourceUrl.Host == "" { + -- imgPath := filepath.Clean(filepath.Join(filepath.Dir(htmlFilename), sourceUrl.Path)) + -- results = append(results, imgPath) + + -- if filepath.Ext(imgPath) == ".svg" { + -- pngFilename := strings.TrimSuffix(imgPath, filepath.Ext(imgPath)) + ".png" + -- results = append(results, pngFilename) + -- } + -- } + -- } + + end + log:debug("get_associated_files done "..file.filename) + + return ass_files +end + +--- Save DOM to file +---@param dom DOM_Object +---@param filename string +local function save_html(dom, filename) + local f = io.open(filename, "w") + if not f then + return nil, "Cannot save updated HTML: " .. (filename or "") + end + f:write(dom:serialize()) + f:close() + return true +end + + +-- local function osExecute(cmd) +-- log:info("Exec: "..cmd) +-- local fileHandle = assert(io.popen(cmd .. " 2>&1", 'r')) +-- local commandOutput = assert(fileHandle:read('*a')) +-- local returnCode = fileHandle:close() and 0 or 1 +-- commandOutput = string.gsub(commandOutput, "\n$", "") +-- log:info("Gets: "..returnCode..": "..commandOutput) +-- return returnCode, commandOutput +-- end + +--- Post-process HTML files +---@param file metadata +---@return boolean status +---@return string? msg +local function process(file) + log:debug("process "..file.absolute_path) + + -- we must find metadata for the HTML file, because `file` is metadata of the TeX file + local html_file, msg = find_html_file(file) + if not html_file then + log:error("No HTML file found for "..file.relative_path) + return false, msg + end + -- log:debug("Found html_file "..(html_file.filename or "").." for file "..file.filename) + + local html_name = html_file.absolute_path + + -- only for debugging + -- local hash_orig = hash_file(html_name) + -- backup_file(html_name, hash_orig..".bak" or "ORIG.bak") + + local dom, msg = load_html(html_name) + if not dom then return false, msg end + remove_empty_paragraphs(dom) + add_dependencies(dom, file) + + + log:debug("Check if .jax file is present") + local jax_file = html_name:gsub(".html$", ".xmjax") + if not path.exists(jax_file) then + log:warning("Strange: no JAX file with extra Latex commands for MathJAX") + jax_file = nil + end + + if jax_file then + + local preambles = dom:query_selector("div.preamble") + + if #preambles == 0 then + log:error("No div.preamble in html : please add one") + end + + local preamble = preambles[1] + local scrpt = preamble:create_element("script") + scrpt:set_attribute("type", "math/tex") + + + local f = io.open(jax_file, "r") + local cmds = f:read("*a") + f:close() + local filtered_commands= cmds:gsub("[^\n]*[:*@].-\n", "") + + log:infof("Adding %d \newcommand (%d filtered)",#filtered_commands,#cmds) + local scrpt_text = scrpt:create_text_node(filtered_commands) + scrpt:add_child_node(scrpt_text) + preamble:add_child_node(scrpt) + + log:debug("No 'activity' card found ??? ") + + end + + local title, abstract = read_title_and_abstract(dom) + file.title = title or "" + file.abstract = abstract or "" + + if is_xourse(dom, html_file) then + transform_xourse(dom, file) + + + log:debug("Checking if a 'part' is present") + local part = dom:query_selector(".card.part") + + if #part == 0 then + log:debug("No parts: add one") + + local body = dom:query_selector("body")[1] + local first_activity = dom:query_selector(".card.activity")[1] + if first_activity then + log:info("Adding default card of type 'part' (HACK: needed by current preview server)") + local h1 = body:create_element("h1") + local h1_text = h1:create_text_node("Main Part") + h1:add_child_node(h1_text) + h1:set_attribute("class", "card part") + body:add_child_node(h1,6) -- the 3 is a guess + else + log:debug("No 'activity' card found ??? ") + end + end + + --

The First Topic of This Course

+ + + end + + -- Not needed here ...??? + -- local ass_files = get_associated_files(dom, html_file) + if string.match(html_name,".make4ht.") then + html_name = html_name:gsub(".make4ht","") + -- file.absolute_path = file.absolute_path:gsub(".make4ht","") + -- file.relative_path = file.relative_path:gsub(".make4ht","") + -- file.extension="html" + end + + log:infof("Adapted html being saved as %s", html_name ) + return save_html(dom, html_name) +end + +M.process = process +M.load_html = load_html +M.get_labels = get_labels +M.get_associated_files = get_associated_files + +return M diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 4ce3818..7b7277e 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -20,7 +20,7 @@ { "label": "HTML", "args": [ - "-d", + "-v", "compile", "${relativeFile}" ], @@ -28,7 +28,7 @@ { "label": "Bake", "args": [ - "--", + "-v", "bake" ], }, diff --git a/.ximera/ximera.4ht b/.ximera/ximera.4ht new file mode 100644 index 0000000..74ad6c3 --- /dev/null +++ b/.ximera/ximera.4ht @@ -0,0 +1,410 @@ +%% +%% This is file `ximera.4ht', +%% generated with the docstrip utility. +%% +%% The original source files were: +%% +%% ximera.dtx (with options: `htXimera') +%% src/pagesetup.dtx (with options: `htXimera') +%% src/abstract.dtx (with options: `htXimera') +%% src/title.dtx (with options: `htXimera') +%% src/problem.dtx (with options: `htXimera') +%% src/macros.dtx (with options: `htXimera') +%% src/theorems.dtx (with options: `htXimera') +%% src/proof.dtx (with options: `htXimera') +%% src/image.dtx (with options: `htXimera') +%% src/dialogue.dtx (with options: `htXimera') +%% src/foldable.dtx (with options: `htXimera') +%% src/interactives/video.dtx (with options: `htXimera') +%% src/xkcd.dtx (with options: `htXimera') +%% src/link.dtx (with options: `htXimera') +%% src/interactives/graph.dtx (with options: `htXimera') +%% src/answer.dtx (with options: `htXimera') +%% src/choice.dtx (with options: `htXimera') +%% src/freeresponse.dtx (with options: `htXimera') +%% src/interactives/javascript.dtx (with options: `htXimera') +%% src/interactives/include.dtx (with options: `htXimera') +%% src/interactives/geogebra.dtx (with options: `htXimera') +%% src/interactives/desmos.dtx (with options: `htXimera') +%% src/interactives/google.dtx (with options: `htXimera') +%% src/feedback.dtx (with options: `htXimera') +%% src/leash.dtx (with options: `htXimera') +%% src/labels.dtx (with options: `htXimera') +%% src/interactives/sagemath.dtx (with options: `htXimera') +%% src/ungraded.dtx (with options: `htXimera') +%% src/footnotes.dtx (with options: `htXimera') +%% src/accordion.dtx (with options: `htXimera') +%% src/ending.dtx (with options: `htXimera') +%% ------------:| ------------------------------------------------------------ +%% ximera:| Simultaneously writing print and online interactive materials +%% Author:| Jim Fowler and Oscar Levin and Jason Nowell and Wim Obbels and Hans Parshall and Bart Snapp +%% E-mail:| bart.snapp@gmail.com +%% License:| Released under the LaTeX Project Public License v1.3c or later +%% See:| http://www.latex-project.org/lppl.txt +%% + + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +\usepackage{microtype} +\DisableLigatures[f]{encoding=*} +\NewEnviron{html}{\HCode{\BODY}} +\RenewEnviron{abstract}{\BODY} +\Configure{@HEAD}{\HCode{\Hnewline}} +\def\and{and } + +\renewcommand{\maketitle}{} + + +\newcounter{identification} +\setcounter{identification}{0} + +\newcommand{\ConfigureQuestionEnv}[2]{% +\renewenvironment{#1}{\refstepcounter{problem}}{}% +\ConfigureEnv{#1}{\stepcounter{identification}\ifvmode \IgnorePar\fi \EndP\HCode{
}}{\ifvmode \IgnorePar\fi \EndP\HCode{
}\IgnoreIndent}{}{}% +} + +\ConfigureQuestionEnv{problem}{problem} +\ConfigureQuestionEnv{exercise}{exercise} +\ConfigureQuestionEnv{question}{question} +\ConfigureQuestionEnv{exploration}{exploration} + +\ifdefined\xmNotHintAsExpandable + \ConfigureQuestionEnv{hint}{hint} % 2024: hint is no longer a 'question-environment'. +\fi +%%%%\ConfigureQuestionEnv{shuffle}{shuffle} +\newcommand{\ConfigureTheoremEnv}[1]{% +\renewenvironment{#1}[1][]{\refstepcounter{problem}% +\ifthenelse{\equal{##1}{}}{}{% + \HCode{}% +}}{} +\ConfigureEnv{#1}{\stepcounter{identification}\ifvmode \IgnorePar\fi \EndP\HCode{}\IgnoreIndent}{}{}% +} + + + \ConfigureTheoremEnv{theorem} + + \ConfigureTheoremEnv{algorithm} + + \ConfigureTheoremEnv{axiom} + + \ConfigureTheoremEnv{claim} + + \ConfigureTheoremEnv{conclusion} + + \ConfigureTheoremEnv{condition} + + \ConfigureTheoremEnv{conjecture} + + \ConfigureTheoremEnv{corollary} + + \ConfigureTheoremEnv{criterion} + + \ConfigureTheoremEnv{definition} + + \ConfigureTheoremEnv{example} + + \ConfigureTheoremEnv{explanation} + + \ConfigureTheoremEnv{fact} + + \ConfigureTheoremEnv{lemma} + + \ConfigureTheoremEnv{formula} + + \ConfigureTheoremEnv{idea} + + \ConfigureTheoremEnv{notation} + + \ConfigureTheoremEnv{model} + + \ConfigureTheoremEnv{observation} + + \ConfigureTheoremEnv{proposition} + + \ConfigureTheoremEnv{paradox} + + \ConfigureTheoremEnv{procedure} + + \ConfigureTheoremEnv{remark} + + \ConfigureTheoremEnv{summary} + + \ConfigureTheoremEnv{template} + + \ConfigureTheoremEnv{warning} + % Mmm, (why) do we want/need this ...? + \ConfigureTheoremEnv{proof} +\ConfigureEnv{proof}{\ifvmode\IgnorePar\fi\EndP\HCode{
} +\ConfigureList{trivlist}{\ifvmode\IgnorePar\fi\EndP}{}{}{} +}{\ifvmode\IgnorePar\fi\EndP\HCode{
}}{}{} + +\newcounter{imagealt} +\setcounter{imagealt}{0} +\renewenvironment{image}[1][]{\stepcounter{imagealt}% + \ifvmode \IgnorePar\fi \EndP% + \HCode{}} +\renewcommand{\alt}[1]{\HCode{}} +\providecommand{\pgfsyspdfmark}[3]{} +\renewenvironment{dialogue}{\begin{description}}{\end{description}} + +\ConfigureList{dialogue}% + {\EndP\HCode{
}% + \PushMacro\end:itm +\global\let\end:itm=\empty} + {\PopMacro\end:itm \global\let\end:itm \end:itm +\EndP\HCode{
}\ShowPar} + {\end:itm \global\def\end:itm{\EndP\Tg}\HCode{
}\bgroup \bf} + {\egroup\EndP\HCode{
}} +\renewenvironment{foldable}{\stepcounter{identification}\ifvmode \IgnorePar\fi \EndP\HCode{
}}{\HCode{
}\IgnoreIndent} + +\ifdefined\xmNotExpandableAsAccordion +\renewenvironment{expandable}{\stepcounter{identification}\ifvmode \IgnorePar\fi \EndP\HCode{
} + +}{\HCode{
}\IgnoreIndent} +\fi + +\renewcommand{\unfoldable}[1]{\HCode{}#1\HCode{}} +%% \renewcommand{\youtube}[1]{\ifvmode \IgnorePar\fi \EndP\HCode{
_
}} +\renewcommand{\youtube}[1]{\ifvmode \IgnorePar\fi \EndP\HCode{}} + +\renewcommand{\xkcd}[1]{\ifvmode \IgnorePar\fi \EndP\HCode{}} + +\renewcommand*{\link}[2][]{% +\ifthenelse{\equal{#1}{}}% +{\url{#2}} +{\href{#2}{#1}}} + +\AtBeginDocument{\renewcommand{\ref}[1]{\HCode{#1}}} + + +\renewcommand{\graph}[2][]{\HCode{
}#2\HCode{
}} + +\renewcommand{\answer}[2][false]{\HCode{}#2\HCode{}} + +\def\validator[#1]{\stepcounter{identification}\HCode{
}} +\def\endvalidator{\HCode{
}} + +\newcounter{choiceId} +\renewcommand{\choice}[2][]{% +\setkeys{choice}{correct=false}% +\setkeys{choice}{#1}% +\stepcounter{choiceId}\IgnorePar% +\HCode{}% +#2\HCode{}} +\let\inlinechoice\choice + + +\renewenvironment{multipleChoice}[1][] +{\setkeys{multipleChoice}{#1}% +\stepcounter{identification}\ifvmode \IgnorePar\fi \EndP\HCode{
}% +}{\HCode{
}\IgnoreIndent} +\ConfigureEnv{multipleChoice}{}{}{}{} + +\renewenvironment{multipleChoice@}{\refstepcounter{problem}}{}% +\ConfigureEnv{multipleChoice@}{\stepcounter{identification}\IgnorePar\HCode{}}{\HCode{}\IgnoreIndent}{}{} + + + +\renewenvironment{selectAll}{\refstepcounter{problem}}{}% +\ConfigureEnv{selectAll}{\stepcounter{identification}\ifvmode \IgnorePar\fi \EndP\HCode{
}}{\HCode{
}\IgnoreIndent}{}{} + + + +\renewenvironment{freeResponse}{\refstepcounter{problem}}{}% +\ConfigureEnv{freeResponse}{\stepcounter{identification}\ifvmode \IgnorePar\fi \EndP\HCode{
}}{\HCode{
}\IgnoreIndent}{}{}% + + + + +\renewenvironment{javascript}{\NoFonts}{\EndNoFonts} +\ScriptEnv{javascript}{\stepcounter{identification}\ifvmode \IgnorePar\fi \EndP\HCode{
}} + + +\def\js#1{\stepcounter{identification}\HCode{}} + +\renewcommand{\includeinteractive}[2][]{\stepcounter{identification}\ifvmode \IgnorePar\fi \EndP\HCode{
}\HCode{}\IgnoreIndent} + +\define@key{geogebra}{rc}[true]{\def\geo@rc{#1}} +\define@key{geogebra}{sdz}[true]{\def\geo@sdz{#1}} +\define@key{geogebra}{smb}[true]{\def\geo@smb{#1}} +\define@key{geogebra}{stb}[true]{\def\geo@stb{#1}} +\define@key{geogebra}{stbh}[true]{\def\geo@stbh{#1}} +\define@key{geogebra}{ld}[true]{\def\geo@ld{#1}} +\define@key{geogebra}{sri}[true]{\def\geo@sri{#1}} +\setkeys{geogebra}{rc=false,sdz=false,smb=false,stb=false,stbh=false,ld=false,sri=false} +\renewcommand{\geogebra}[4][]{% + \setkeys{geogebra}{#1}% Set new keys + \HCode{}} +\catcode`\%=11 +\renewcommand{\desmos}[3]{\HCode{}} +\catcode`\%=14 +\renewcommand{\desmosThreeD}[3]{\HCode{}} + +\renewcommand{\googleSheet}[5]{% + \ifthenelse{\equal{#4}{}}% + {\HCode{}}% + {\ifthenelse{\equal{#5}{}}% + {\HCode{}}% + {\HCode{}}% + }% + }% + +\def\feedback{\@ifnextchar[{\@feedbackcode}{\@feedbackattempt}} +\def\@feedbackattempt{\@feedbackcode[attempt]} +\def\@feedbackcode[#1]{\stepcounter{identification}% +\ifvmode \IgnorePar\fi \EndP% +\ifthenelse{\equal{#1}{attempt}}{\HCode{