summaryrefslogtreecommitdiff
path: root/support/make4ht
diff options
context:
space:
mode:
Diffstat (limited to 'support/make4ht')
-rw-r--r--support/make4ht/README961
-rw-r--r--support/make4ht/changelog.tex802
-rw-r--r--support/make4ht/domfilters/make4ht-aeneas.lua144
-rw-r--r--support/make4ht/domfilters/make4ht-fixinlines.lua57
-rw-r--r--support/make4ht/domfilters/make4ht-idcolons.lua20
-rw-r--r--support/make4ht/domfilters/make4ht-joincharacters.lua85
-rw-r--r--support/make4ht/domfilters/make4ht-joincolors.lua75
-rw-r--r--support/make4ht/domfilters/make4ht-odtimagesize.lua16
-rw-r--r--support/make4ht/domfilters/make4ht-t4htlinks.lua27
-rw-r--r--support/make4ht/extensions/common_domfilters.lua27
-rw-r--r--support/make4ht/extensions/common_filters.lua26
-rw-r--r--support/make4ht/extensions/dvisvgm_hashes.lua268
-rw-r--r--support/make4ht/extensions/join_colors.lua16
-rw-r--r--support/make4ht/extensions/latexmk_build.lua31
-rw-r--r--support/make4ht/extensions/mathjaxnode.lua16
-rw-r--r--support/make4ht/extensions/odttemplate.lua16
-rw-r--r--support/make4ht/extensions/staticsite.lua135
-rw-r--r--support/make4ht/extensions/tidy.lua57
-rw-r--r--support/make4ht/filters/make4ht-cleanspan-nat.lua12
-rw-r--r--support/make4ht/filters/make4ht-cleanspan.lua33
-rw-r--r--support/make4ht/filters/make4ht-domfilter.lua63
-rw-r--r--support/make4ht/filters/make4ht-entities-to-unicode.lua15
-rw-r--r--support/make4ht/filters/make4ht-entities.lua14
-rw-r--r--support/make4ht/filters/make4ht-filter.lua19
-rw-r--r--support/make4ht/filters/make4ht-fix-links.lua19
-rw-r--r--support/make4ht/filters/make4ht-fixligatures.lua16
-rw-r--r--support/make4ht/filters/make4ht-hruletohr.lua11
-rw-r--r--support/make4ht/filters/make4ht-mathjaxnode.lua94
-rw-r--r--support/make4ht/filters/make4ht-odttemplate.lua60
-rw-r--r--support/make4ht/filters/make4ht-staticsite.lua98
-rw-r--r--support/make4ht/filters/make4ht-svg-height.lua43
-rw-r--r--support/make4ht/formats/docbook.lua38
-rw-r--r--support/make4ht/formats/html5.lua17
-rw-r--r--support/make4ht/formats/odt.lua223
-rw-r--r--support/make4ht/formats/tei.lua14
-rw-r--r--support/make4ht/formats/xhtml.lua16
-rw-r--r--support/make4ht/lapp-mk4.lua329
-rwxr-xr-xsupport/make4ht/make4ht110
-rw-r--r--support/make4ht/make4ht-aeneas-config.lua215
-rw-r--r--support/make4ht/make4ht-config.lua92
-rw-r--r--support/make4ht/make4ht-doc.pdfbin0 -> 110638 bytes
-rw-r--r--support/make4ht/make4ht-doc.tex26
-rw-r--r--support/make4ht/make4ht-dvireader.lua182
-rw-r--r--support/make4ht/make4ht-filterlib.lua42
-rw-r--r--support/make4ht/make4ht-lib.lua226
-rw-r--r--support/make4ht/make4ht-xtpipes.lua82
-rw-r--r--support/make4ht/mathnode.lua87
-rw-r--r--support/make4ht/mkparams.lua238
-rw-r--r--support/make4ht/mkutils.lua557
-rw-r--r--support/make4ht/readme.tex1085
50 files changed, 6855 insertions, 0 deletions
diff --git a/support/make4ht/README b/support/make4ht/README
new file mode 100644
index 0000000000..a99a108d69
--- /dev/null
+++ b/support/make4ht/README
@@ -0,0 +1,961 @@
+% [![Build Status](https://travis-ci.org/michal-h21/make4ht.svg?branch=master)](https://travis-ci.org/michal-h21/make4ht)
+
+# Introduction
+
+`make4ht` is a simple build system for `tex4ht`, \TeX\ to XML converter. It provides a command line tool
+that drive the conversion process. It also provides a library which can be used to create
+customized conversion tools. An example of such conversion tool is
+[tex4ebook](https://github.com/michal-h21/tex4ebook) for conversion of \TeX\ to
+ePub and other e-book formats.
+
+
+## How it works
+
+### The issues with default `tex4ht` conversion commands
+
+
+`tex4ht` system supports several output formats, most notably `XHTML`, `HTML 5` and `ODT`.
+The conversion can be invoked using several commands. These commands invoke LaTeX\ or Plain TeX
+with special instructions to load `tex4ht.sty` package. The \TeX\ run produces special `DVI` file
+which contains the code for desired output format. Produced `DVI` file is then processed and
+desired output files are created.
+
+The basic command provided by `tex4ht` is named `htlatex`. It compiles \LaTeX\
+files to `HTML` with this command sequence:
+
+ latex $latex_options 'code for loading tex4ht.sty \input{filename}'
+ latex $latex_options 'code for loading tex4ht.sty \input{filename}'
+ latex $latex_options 'code for loading tex4ht.sty \input{filename}'
+ tex4ht $tex4ht_options filename
+ t4ht $t4ht_options filename
+
+The options for various parts of the system can be passed on the command line:
+
+ htlatex filename "tex4ht.sty options" "tex4ht_options" "t4ht_options" "latex_options"
+
+For basic `HTML` conversion it is possible to use the most basic invocation:
+
+ htlatex filename.tex
+
+It can be much more involved for `HTML 5` output in `UTF-8` encoding:
+
+ htlatex filename.tex "xhtml,html5,charset=utf-8" "-cmozhtf -utf8"
+
+`make4ht` can simplify it:
+
+ make4ht -uf html5 filename.tex
+
+Another issue is the fixed compilation order and hard-coded number of LaTeX invocations.
+
+When you need to run a program which interact with LaTeX, such as `Makeindex`
+or `Bibtex`, you need to create a new script based on `htlatex`, or run
+`htlatex` twice, which means that LaTeX will be invoked six times.
+This can lead to significantly long compilation times. `make4ht` provides build files and
+extensions, which can be used for interaction with external tools.
+
+It is also possible to have several compilation modes. When you just add new text to a document,
+which doesn't contain cross-references, don't add new stuff to the table of contents, etc.,
+it is possible to use the `draft` mode which will invoke LaTeX only once. It
+can save quite a lot of the compilation time:
+
+ make4ht -um draft -f html5 filename.tex
+
+There are also issues with a behaviour of the `t4ht` application. It reads file
+`filename.lg`, generated by `tex4ht`, where are instructions about generated
+files, `CSS` instructions, calls to external applications, instructions for
+image conversions etc. It can be instructed to copy generated files to some
+output directory, but it doesn't preserve directory structure, so when you
+have images in a subdirectory, they will be copied to the output directory.
+Links will be pointing to a non-existing subdirectory. The following command
+should copy all output files to the correct destinations.
+
+ make4ht -d outputdir filename.tex
+
+The image conversion is configured in the
+[env file](http://www.tug.org/applications/tex4ht/mn35.html#index35-73001),
+which has really strange syntax based and the rules are
+[os dependent](http://www.tug.org/applications/tex4ht/mn-unix.html#index27-69005).
+`make4ht` provides simpler means for the image conversion in the build files.
+
+With `make4ht` build files, we have simple mean to fix these issues. We can
+change image conversion parameters without the need to modify the `env file`,
+or execute actions on the output files. These actions can be either external
+programs such as `xslt` processors or `HTML tidy` or `Lua` functions.
+
+The idea is to make system controlled by a build file. Because `Lua`
+interpreter is included in modern TeX distributions and `Lua` is ideal language
+for such task, it was chosen as language in which the build scripts are written.
+
+# Output file formats and extensions
+
+The default output format used by `make4ht` is `html5`. Different
+format can be requested using the `--format` option. Supported formats are:
+
+ - `xhtml`
+ - `html5`
+ - `odt`
+ - `tei`
+ - `docbook`
+
+The `--format` option can be also used for extension loading.
+
+## Extensions
+
+Extensions can be used to modify the build process without the need to use a build file. They
+may post-process the output files or request additional commands for the compilation.
+
+The extensions can be enabled or disabled by appending `+EXTENSION` or `-EXTENSION` after
+the output format name:
+
+ make4ht -uf html5+tidy filename.tex
+
+Available extensions:
+
+latexmk_build
+
+: use `Latexmk` for \LaTeX\ compilation.
+
+tidy
+
+: clean the `HTML` files using the `tidy` command.
+
+dvisvgm_hashes
+
+: efficient generation of SVG pictures using Dvisvgm. It can utilize
+multiple processor cores and generates only changed images.
+
+common\_filters
+
+: clean the output HTML files using filters.
+
+common\_domfilters
+
+: clean the HTML file using DOM filters. It is more powerful than
+`common_filters`. Used DOM filters are `fixinlines`, `idcolons` and
+`joincharacters`.
+
+join\_colors
+
+: load the `joincolors` domfilter for all HTML files.
+
+mathjaxnode
+
+: use [mathjax-node-page](https://github.com/pkra/mathjax-node-page/) to
+ convert from MathML code to HTML + CSS or SVG. See [the available
+ settings](#mathjaxsettings).
+
+odttemplate
+
+: automatically load the `odttemplate` filter.
+
+staticsite
+
+: build the document in form suitable for static site generators like [Jekyll](https://jekyllrb.com/).
+
+# Build files
+
+`make4ht` supports build files. These are `Lua` scripts which can adjust
+the build process. You can request external applications like `bibtex` or `makeindex`,
+pass options to the commands, modify the image conversion process, or post-process the
+generated files.
+
+`make4ht` tries to load default build file named as `filename + .mk4 extension`.
+You can choose different build file with `-e` or `--build-file` command line
+option.
+
+Sample build file:
+
+ Make:htlatex()
+ Make:match("html$", "tidy -m -xml -utf8 -q -i ${filename}")
+
+`Make:htlatex()` is preconfigured command for calling LaTeX with `tex4ht`
+loaded on the input file. In this case, it will be called one time. After
+compilation, the `tidy` command is executed on the output `HTML` file.
+
+Note that you don't have to call `tex4ht` and `t4ht` commands explicitly in the
+build file, they are called automatically.
+
+## User commands
+
+You can add more commands like `Make:htlatex` using `Make:add` command:
+
+ Make:add("name", "command", {parameters}, repetition)
+
+The `name` and `command` parameters are required, rest of the parameters are optional.
+
+This defines `name` command, which can be then executed as `Make:name()` command.
+
+### Provided commands
+
+`Make:htlatex`
+
+: One call to TeX engine with special configuration for `tex4ht` loading.
+
+`Make:latexmk`
+
+: Use `Latexmk` for the document compilation. `tex4ht` will be loaded automatically.
+
+`Make:tex4ht`
+
+: Process the `DVI` file and creates the output files.
+
+`Make:t4ht`
+
+: Creates the CSS file.
+
+
+### Command function
+
+The `command` parameter can be either string template or function:
+
+ Make:add("text", "echo hello, input file: ${input}")
+ Make:add("function", function(params)
+ for k, v in pairs(params) do
+ print(k..": "..v)
+ end, {custom="Hello world"}
+ )
+
+The template can get variable value from the parameters table using a
+`${var_name}` placeholder. Templates are executed using operating system, so
+they should invoke existing OS commands. Function commands may execute system
+commands using `os.execute` function.
+
+
+### Parameters table
+
+`parameters` parameter is optional, it can be table or `nil` value, which
+should be used if you want to use the `repetition` parameter, but don't want to
+modify the parameters table.
+
+The table with default parameters is passed to all commands, they can be accessed from command functions
+or templates. When you specify your own parameters in the command definition, these additional
+parameters are added to the default parameters table for this particular
+command. You can override the default parameters in the parameters table.
+
+
+
+The default parameters are following:
+
+`htlatex`
+
+: used compiler
+
+`input`
+
+: it is output file name in fact
+
+`tex_file`
+
+: input TeX file
+
+`latex_par`
+
+: parameters to `latex`
+
+`packages`
+
+: insert additional LaTeX code which is inserted before `\documentclass`.
+ Useful for passing options to packages or additional packages loading
+
+`tex4ht_sty_par`
+
+: parameters to `tex4ht.sty`
+
+`tex4ht_par`
+
+: parameters to the `tex4ht` application
+
+`t4ht_par`
+
+: parameters to the `t4ht` application
+
+`outdir`
+
+: the output directory
+
+`repetition`
+
+: limit number of command execution.
+
+`correct_exit`
+
+: expected `exit code` from the command. The compilation will be terminated
+ if the command `exit code` is different.
+
+
+### Repetition
+
+Repetition is number which specifies a maximal number of executions of the
+particular command. This is used for instance for `tex4ht` and `t4ht`
+commands, as they should be executed only once in the compilation. They would
+be executed multiple times if you include them in the build file because they
+are called by `make4ht` by default. Because these commands allow only one
+`repetition`, the second execution will be blocked.
+
+### Expected exit code
+
+You can set the expected exit code from a command with a `correct_exit` key in the
+parameters table. The compilation will be stopped when the command returns a
+different exit code.
+
+This mechanism isn't used for LaTeX (for all TeX engines and formats, in
+fact), because it doesn't differentiate between fatal and non-fatal errors, and
+it returns the same exit code in all cases. Log parsing is used because of
+that, error code `1` is returned in the case of fatal error, `0` is used
+otherwise. The `Make.testlogfile` function can be used in the build file to
+detect compilation errors in the TeX log file.
+
+## File matches
+
+Another type of action which can be specified in the build file is
+`match`. It can be called on the generated files:
+
+ Make:match("html$", "tidy -m -xml -utf8 -q -i ${filename}")
+
+It tests output file names with `Lua` pattern matching and on matched items will
+execute a command or a function, specified in the second argument. Commands may be
+specified as strings, the templates will be expanded, `${var_name}` placeholders
+will be replaced with corresponding variables from the `parameters` table,
+described in the previous subsection. One additional variable is available:
+`filename`. It contains the name of the current output file.
+
+The above example will clean all output `HTML` files using the `tidy` command.
+
+If function is used instead, it will get two parameters.
+The first one is a current filename, the second one
+table with parameters.
+
+
+
+### Filters
+
+Some default `match` actions which can be used are available from the
+`make4ht-filter` module. It contains some functions which are useful for
+fixing some `tex4ht` bugs or shortcomings.
+
+Example:
+
+ local filter = require "make4ht-filter"
+ local process = filter{"cleanspan", "fixligatures", "hruletohr"}
+ Make:htlatex()
+ Make:htlatex()
+ Make:match("html$",process)
+
+The `make4ht-filter` module return a function which can be used for the filter
+chain building. Multiple filters can be chained, each of them can modify the string
+which was modified by the previous filters. The changes are then saved to the
+processed file.
+
+Built-in filters are:
+
+cleanspan
+
+: clean spurious span elements when accented characters are used
+
+cleanspan-nat
+
+: alternative clean span filter, provided by Nat Kuhn
+
+fixligatures
+
+: decompose ligatures to base characters
+
+hruletohr
+
+: `\hrule` commands are translated to series of underscore characters
+ by `tex4ht`, this filter translate these underscores to `<hr>` elements
+
+entites
+
+: convert prohibited named entities to numeric entities (currently, only
+ `&nbsp;`, as it causes validation errors, and `tex4ht` is producing it
+ sometimes)
+
+fix-links
+
+: replace colons in local links and `id` attributes with underscores. Some
+ cross-reference commands may produce colons in internal links, which results in
+ validation error.
+
+mathjaxnode
+
+: use [mathjax-node-page](https://github.com/pkra/mathjax-node-page/) to
+ convert from MathML code to HTML + CSS or SVG. See [the available
+ settings](#mathjaxsettings).
+
+odttemplate
+
+: use styles from another `ODT` file serving as a template in the current
+ document. It works for the `styles.xml` file in the `ODT` file. During
+ the compilation, this file is named as `\jobname.4oy`.
+
+staticsite
+
+: create HTML files in format suitable for static site generators such as [Jekyll](https://jekyllrb.com/)
+
+svg-height
+
+: some SVG images produced by `dvisvgm` seem to have wrong dimensions. This filter
+ tries to set the correct image size.
+
+Function `filter` accepts also function arguments, in this case this function
+takes file contents as a parameter and modified contents are returned.
+
+Example:
+
+ local filter = require "make4ht-filter"
+ local changea = function(s) return s:gsub("a","z") end
+ local process = filter{"cleanspan", "fixligatures", changea}
+ Make:htlatex()
+ Make:htlatex()
+ Make:match("html$",process)
+
+In this example, spurious span elements are joined, ligatures are decomposed,
+and then all letters 'a' are replaced with 'z' letters.
+
+### DOM filters
+
+DOM filters use the [`LuaXML`](https://ctan.org/pkg/luaxml) library to modify
+directly the XML object. This enables more powerful
+operations than the regex based filters from the previous section.
+
+Example:
+
+ local domfilter = require "make4ht-domfilter"
+ local process = domfilter {"joincharacters"}
+ Make:match("html$", process)
+
+
+Available DOM filters:
+
+aeneas
+
+: [Aeneas](https://www.readbeyond.it/aeneas/) is a tool for automagical synchronization of text and audio.
+ This filter modifies the HTML code to support the synchronization.
+
+fixinlines
+
+: put all inline elements which are direct children of the `<body>` elements to a paragraph.
+
+idcolons
+
+: replace the colon (`:`) character in internal links and `id` attributes. They cause validation issues.
+
+joincharacters
+
+: join consecutive `<span>` or `<mn>` elements.
+
+joincolors
+
+: many `<span>` elements with unique `id` attribute are created when \LaTeX\ colors are being used in the document.
+ CSS rule is added for each of these elements, which may result in
+ substantial grow of the CSS file. This filter replace these rules with a
+ common one for elements with the same color value.
+
+odtimagesize
+
+: set correct dimensions for images in the ODT format. It is loaded by default for the ODT output.
+
+t4htlinks
+
+: fix hyperlinks in the ODT format.
+
+### make4ht-aeneas-config package
+
+Companion for the `aeneas` DOM filter is the `make4ht-aeneas-config` plugin. It
+can be used to write Aeneas configuration file or execute Aeneas on the
+generated HTML files.
+
+Available functions:
+
+write\_job(parameters)
+
+: write Aenas job configuration to `config.xml` file. See the [Aeneas
+ documentation](https://www.readbeyond.it/aeneas/docs/clitutorial.html#processing-jobs)
+ for more information about jobs.
+
+execute(parameters)
+
+: execute Aeneas.
+
+process\_files(parameters)
+
+: process the audio and generated subtitle files.
+
+
+By default, the `smil` file is created. It is assumed that there is audio file
+in `mp3` format named as the TeX file. It is possible to use different formats
+and file names using mapping.
+
+The configuration options can be passed directly to the functions or set using
+`filter_settings "aeneas-config" {parameters}` function.
+
+
+Available parameters:
+
+
+lang
+
+: document language. It is interfered from the HTML file, so it is not necessary to set it.
+
+map
+
+: mapping between HTML, audio and subtitle files. More info bellow.
+
+text\_type
+
+: type of the input. The `aeneas` DOM filter produces `unparsed` text type.
+
+id\_sort
+
+: sorting of id attributes. Default value is `numeric`.
+
+id\_regex
+
+: regular expression to parse the id attributes.
+
+sub\_format
+
+: generated subtitle format. Default `smil`.
+
+
+Additional parameters for the job configuration file:
+
+- description
+- prefix
+- config\_name
+- keep\_config
+
+
+
+It is possible to generate multiple HTML files from the LaTeX source. For
+example, `tex4ebook` generates separate file for each chapter or section. It is
+possible to set options for each HTML file, in particular names of the
+corresponding audio files. This mapping is done using `map` parameter.
+
+Example:
+
+ filter_settings "aeneas-config" {
+ map = {
+ ["sampleli1.html"] = {audio_file="sample.mp3"},
+ ["sample.html"] = false
+ }
+ }
+
+Table keys are the configured file names. It is necessary to insert them as
+`["filename.html"]`, because of Lua syntax rules.
+
+This example maps audio file `sample.mp3` to a section subpage. The main HTML
+file, which may contain title and table of contents doesn't have a
+corresponding audio file.
+
+Filenames of the sub files corresponds to the chapter numbers, so they are not
+stable when a new chapter is added. It is possible to request file names
+interfered from the chapter titles using the `sec-filename` option or `tex4ht`.
+
+Available `map` options:
+
+
+audio\_file
+
+: the corresponding audio file
+
+sub\_file
+
+: name of the generated subtitle file
+
+The following options are same as their counter-parts from the main parameters table and generally don't need to be set:
+
+- prefix
+- file\_desc
+- file\_id
+- text\_type
+- id\_sort
+- id\_prefix
+- sub\_format
+
+
+Full example:
+
+
+ local domfilter = require "make4ht-domfilter"
+ local aeneas_config = require "make4ht-aeneas-config"
+
+ filter_settings "aeneas-config" {
+ map = {
+ ["krecekli1.xhtml"] = {audio_file="krecek.mp3"},
+ ["krecek.xhtml"] = false
+ }
+ }
+
+ local process = domfilter {"aeneas"}
+ Make:match("html$", process)
+
+ if mode == "draft" then
+ aeneas_config.process_files {}
+ else
+ aeneas_config.execute {}
+ end
+
+
+
+
+## Image conversion
+
+It is possible to convert parts of LaTeX input to pictures, it is used
+for example for math or diagrams in `tex4ht`.
+
+These pictures are stored in a special `dvi` file, which can be processed by
+the `dvi to image` commands.
+
+This conversion is normally configured in the `env file`,
+which is system dependent and which has a bit unintuitive syntax.
+This configuration is processed by the `t4ht` application and conversion
+commands are called for all pictures.
+
+It is possible to disable `t4ht` image processing and configure image
+conversion in the build file:
+
+ Make:image("png$",
+ "dvipng -bg Transparent -T tight -o ${output} -pp ${page} ${source}")
+
+
+`Make:image` takes two parameters, pattern to match image name and action.
+Action can be either string template with conversion command,
+or function which takes a table with parameters as an argument.
+
+There are three parameters:
+
+ - `output` - output image file name
+ - `source` - `dvi` file with the pictures
+ - `page` - page number of the converted image
+
+## The `mode` variable
+
+There is global `mode` variable available in the build file. It contains
+contents of the `--mode` command line option. It can be used to run some commands
+conditionally. For example:
+
+ if mode == "draft" then
+ Make:htlatex{}
+ else
+ Make:htlatex{}
+ Make:htlatex{}
+ Make:htlatex{}
+ end
+
+In this example (which is the default configuration used by `make4ht`),
+LaTeX is called only once when `make4ht` is called with `draft` mode:
+
+ make4ht -m draft filename
+
+## The `settings` table
+
+You may want to access to the parameters also outside commands, file matches
+and image conversion functions. For example, if you want to convert your file to
+the `OpenDocument Format (ODT)`, you can use the following settings, based on the `oolatex`
+command:
+
+ settings.tex4ht_sty_par = settings.tex4ht_sty_par ..",ooffice"
+ settings.tex4ht_par = settings.tex4ht_par .. " ooffice/! -cmozhtf"
+ settings.t4ht_par = settings.t4ht_par .. " -cooxtpipes -coo "
+
+There are some functions to ease access to the settings:
+
+`set_settings{parameters}`
+
+: overwrite settings with values from a passed table
+
+`settings_add{parameters}`
+
+: add values to the current settings
+
+`filter_settings "filter name" {parameters}`
+
+: set settings for a filter
+
+`get_filter_settings(name)`
+
+: get settings for a filter
+
+
+Using these functions, it is possible to simplify the settings for the `ODT` format:
+
+ settings_add {
+ tex4ht_sty_par =",ooffice",
+ tex4ht_par = " ooffice/! -cmozhtf",
+ t4ht_par = " -cooxtpipes -coo "
+ }
+
+Settings for filters and extensions can be set using `filter_settings`:
+
+
+ filter_settings "test" {
+ hello = "world"
+ }
+
+These settings can be read in the extensions and filters using `get_filter_settings`:
+
+ function test(input)
+ local options = get_filter_settings("test")
+ print(options.hello)
+ return input
+ end
+
+## List of available settings for filters and extensions.
+
+These settings may be set using `filter_settings` function.
+
+### The `tidy` extension
+
+options
+
+: command line options for the `tidy` command. Default value is `-m -utf8 -w 512 -q`.
+
+### The `fixinlines` dom filter
+
+inline\_elements
+
+: table of inline elements which shouldn't be direct descendants of the `body` element. The element names should be table keys, the values should be true.
+
+Example
+
+ filter_settings "fixinlines" {inline_elements = {a = true, b = true}}
+
+### The `joincharacters` dom filter
+
+charelements
+
+: table of elements which should be joined if several instances with the same value of `class` attribute are side by side.
+
+Example
+
+ filter_settings "joincharacters" { charclases = { span=true, mn = true}}
+
+### The `mathjaxnode` filter {#mathjaxsettings}
+
+options
+
+: command line options for the `mjpage` command. Default value is `--output CommonHTML`
+
+Example
+
+ filter_settings "mathjaxnode" {
+ options="--output SVG --font Neo-Euler"
+ }
+
+cssfilename
+
+: `mjpage` puts some CSS code into the HTML pages. `mathjaxnode` extracts this information and saves it to a standalone CSS file. Default CSS filename is `mathjax-chtml.css`
+
+fontdir
+
+: directory with MathJax font files. This option enables use of local fonts, which
+ is usefull in Epub conversion, for example. The font directory should be
+ sub-directory of the current directory. Only TeX font is supported at the moment.
+
+Example
+
+
+ filter_settings "mathjaxnode" {
+ fontdir="fonts/TeX/woff/"
+ }
+
+### The `aeneas` filter
+
+skip\_elements
+
+: List of CSS selectors that match elements which shouldn't be processed. Default value: `{ "math", "svg"}`.
+
+id\_prefix
+
+: prefix used in the ID attribute forming.
+
+sentence\_match
+
+: Lua pattern used to match a sentence. Default value: `"([^%.^%?^!]*)([%.%?!]?)"`.
+
+### The `staticsite` filter and extension
+
+site\_root
+
+: directory where generated files should be copied.
+
+map
+
+: table where keys are patterns that match filenames, value contains destination directoryfor matched files, relative to the `site_root` (it is possible to use `..` to swich to parent directory).
+
+file\_pattern
+
+: pattern used for filename generation. It is possible to use string templates and format strings for `os.date` function. Default value of `%Y-%m-%d-${input}` creates names in the form of `YYYY-MM-DD-file_name`.
+
+header
+
+: table with variables to be set in the YAML header in HTML files. If the table value is a function, it is executed with current parameters and HTML page DOM object as arguments.
+
+Example:
+
+
+ local outdir = os.getenv "blog_root"
+
+ filter_settings "staticsite" {
+ site_root = outdir,
+ map = {
+ [".css$"] = "../css/"
+ },
+ header = {
+ layout="post",
+ date = function(parameters, dom)
+ return os.date("!%Y-%m-%d %T", parameters.time)
+ end
+ }
+ }
+
+### The `dvisvgm_hashes` extension
+
+options
+
+: command line options for Dvisvgm. The default value is `-n --exact -c 1.15,1.15`.
+
+cpu_cnt
+
+: number of processor cores used for conversion. The extension tries to detect the available cores automatically by default.
+
+parallel_size
+
+: number of pages used in each Dvisvgm call. The extension detects changed pages in the DVI file and construct multiple calls to Dvisvgm with only changed pages.
+
+scale
+
+: SVG scaling.
+
+### The `odttemplate` filter and extension
+
+template
+
+: filename of the template `ODT` file
+
+
+`odttemplate` can also get the template filename from the `odttemplate` option from `tex4ht_sty_par` parameter. It can be set useing following command line call, for example:
+
+ make4ht -f odt+odttemplate filename.tex "odttemplate=template.odt"
+
+
+
+# Configuration file {#configfile}
+
+It is possible to globally modify the build settings using the configuration
+file. New compilation commands can be added, extensions can be loaded or
+disabled and settings can be set.
+
+## Location
+
+The configuration file can be saved either in
+`$HOME/.config/make4ht/config.lua` or in `.make4ht` in the current directory or
+it's parents (up to `$HOME`).
+
+## Additional commands
+
+There are two additional commands:
+
+`Make:enable_extension(name)`
+
+: require extension
+
+`Make:disable_extension(name)`
+
+: disable extension
+
+## Example
+
+The following configuration add support for the `biber` command, requires
+`common_domfilters` extension and requires MathML
+output for math.
+
+ Make:add("biber", "biber ${input}")
+ Make:enable_extension "common_domfilters"
+ settings_add {
+ tex4ht_sty_par =",mathml"
+ }
+
+# Command line options
+
+ make4ht - build system for tex4ht
+ Usage:
+ make4ht [options] filename ["tex4ht.sty op." "tex4ht op."
+ "t4ht op" "latex op"]
+ -b,--backend (default tex4ht) Backend used for xml generation.
+ possible values: tex4ht or lua4ht
+ -c,--config (default xhtml) Custom config file
+ -d,--output-dir (default "") Output directory
+ -e,--build-file (default nil) If build file name is different
+ than `filename`.mk4
+ -f,--format (default nil) Output file format
+ -l,--lua Use lualatex for document compilation
+ -m,--mode (default default) Switch which can be used in the makefile
+ -n,--no-tex4ht Disable dvi file processing with tex4ht command
+ -s,--shell-escape Enables running external programs from LaTeX
+ -u,--utf8 For output documents in utf8 encoding
+ -v,--version Print version number
+ -x,--xetex Use xelatex for document compilation
+ <filename> (string) Input file name
+
+
+You can still invoke `make4ht` in the same way as `htlatex`:
+
+ make4ht filename "customcfg, charset=utf-8" "-cunihtf -utf8" "-dfoo"
+
+Note that this will not use `make4ht` routines for output directory making and
+copying. If you want to use them, change the line above to:
+
+ make4ht -d foo filename "customcfg, charset=utf-8" "-cunihtf -utf8"
+
+This call has the same effect as the following:
+
+ make4ht -u -c customcfg -d foo filename
+
+Output directory doesn't have to exist, it will be created automatically.
+Specified path can be relative to current directory, or absolute:
+
+ make4ht -d use/current/dir/ filename
+ make4ht -d ../gotoparrentdir filename
+ make4ht -d ~/gotohomedir filename
+ make4ht -d c:\documents\windowspathsareworkingtoo filename
+
+# Troubleshooting
+
+## Incorrect handling of command line arguments for `tex4ht`, `t4ht` or `latex`
+
+Sometimes, you may get a similar error:
+
+ make4ht:unrecognized parameter: i
+
+It may be caused by a following `make4ht` invocation:
+
+ make4ht hello.tex "customcfg,charset=utf-8" "-cunihtf -utf8" -d foo
+
+The command line option parser is confused by mixing options for `make4ht` and
+`tex4ht` in this case and tries to interpret the `-cunihtf -utf8`, which are
+options for `tex4ht` command as `make4ht` options. To fix that, you can either
+move the `-d foo` directly after `make4ht` command:
+
+ make4ht -d foo hello.tex "customcfg,charset=utf-8" "-cunihtf -utf8"
+
+Another option is to add a space before `tex4ht` options:
+
+ make4ht hello.tex "customcfg,charset=utf-8" " -cunihtf -utf8" -d foo
+
+The former way is preferable, though.
+
+## Filenames containing spaces
+
+`tex4ht` cannot handle filenames containing spaces. `make4ht` thus replaces spaces in input file names with underscores, so generated XML files use underscores instead of spaces as well.
+
+## Filenames containing non-ASCII characters
+
+The `odt` output doesn't support accented filenames, it is best to stick to ASCII characters in filenames.
+
+# License
+
+Permission is granted to copy, distribute and/or modify this software
+under the terms of the LaTeX Project Public License, version 1.3.
diff --git a/support/make4ht/changelog.tex b/support/make4ht/changelog.tex
new file mode 100644
index 0000000000..8d46424dec
--- /dev/null
+++ b/support/make4ht/changelog.tex
@@ -0,0 +1,802 @@
+\hypertarget{changelog}{%
+\section{Changelog}\label{changelog}}
+
+\begin{itemize}
+\item
+ 2019/05/24
+
+ \begin{itemize}
+ \tightlist
+ \item
+ version 0.2g released
+ \item
+ fixed failing \texttt{dvisvgm\_hashes} extension on Windows.
+ \end{itemize}
+\item
+ 2019/05/02
+
+ \begin{itemize}
+ \tightlist
+ \item
+ fixed infinite loop bug in the \texttt{dvisvgm\_hashes} extension
+ \end{itemize}
+\item
+ 2019/04/09
+
+ \begin{itemize}
+ \tightlist
+ \item
+ \texttt{make4ht-joincolors} fix: remove the hash character from the
+ color name. This caused issues with colors specified in the
+ hexadecimal format.
+ \end{itemize}
+\item
+ 2019/04/02
+
+ \begin{itemize}
+ \tightlist
+ \item
+ \texttt{dvisvgm\_hashes} fix: update also the lgfile.images table
+ with generated filenames, in order to support tex4ebook
+ \end{itemize}
+\item
+ 2019/04/01
+
+ \begin{itemize}
+ \tightlist
+ \item
+ fixed bug in \texttt{dvisvgm\_hashes} extension: didn't check for
+ table index existence in string concenation
+ \end{itemize}
+\item
+ 2019/03/21
+
+ \begin{itemize}
+ \tightlist
+ \item
+ version 0.2f released
+ \end{itemize}
+\item
+ 2019/03/15
+
+ \begin{itemize}
+ \tightlist
+ \item
+ check for the image dimensions existence in the
+ \texttt{odtimagesize} domfilter.
+ \end{itemize}
+\item
+ 2019/03/13
+
+ \begin{itemize}
+ \tightlist
+ \item
+ don't use \texttt{odtimagesize} domfilter in the \texttt{ODT}
+ format, the issue it fixes had been resolved in \texttt{tex4ht}.
+ \end{itemize}
+\item
+ 2019/03/08
+
+ \begin{itemize}
+ \tightlist
+ \item
+ use \texttt{\%USERPROFILE} for home dir search on Windows.
+ \end{itemize}
+\item
+ 2019/01/28
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added \texttt{joincolors} domfilter and \texttt{join\_colors}
+ extension. It can join CSS rules created for the LaTeX colors and
+ update the HTML file.
+ \end{itemize}
+\item
+ 2019/01/22
+
+ \begin{itemize}
+ \tightlist
+ \item
+ version 0.2e released
+ \item
+ updated the \texttt{odttemplate} filter. It will use styles from the
+ generated ODT file that haven't been present in the template file.
+ \end{itemize}
+\item
+ 2019/01/10
+
+ \begin{itemize}
+ \tightlist
+ \item
+ version 0.2d released
+ \end{itemize}
+\item
+ 2019/01/05
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added \texttt{docbook} and \texttt{tei} output formats.
+ \end{itemize}
+\item
+ 2018/12/19
+
+ \begin{itemize}
+ \tightlist
+ \item
+ new library: \texttt{make4ht-xtpipes.lua}. It contains code for
+ xtpipes handling.
+ \item
+ moved Xtpipes handling code from \texttt{formats/odt.lua}.
+ \end{itemize}
+\item
+ 2018/12/18
+
+ \begin{itemize}
+ \tightlist
+ \item
+ new filter: \texttt{odttemplate}. It can be used for replacing style
+ in a generated \texttt{ODT} file by a style from another existing
+ \texttt{ODT} file.
+ \item
+ new extension: \texttt{odttemplate}. Companioning extension for
+ filter with the same name.
+ \item
+ fixed bug in \texttt{make4ht-filters.lua}: the parameters table
+ haven't been passed to filters.
+ \end{itemize}
+\item
+ 2018/12/17
+
+ \begin{itemize}
+ \tightlist
+ \item
+ fixed extension handling. The disabling from the command line didn't
+ take precedence over extensions enabled in the config file.
+ Extensions also could be executed multiple times.
+ \end{itemize}
+\item
+ 2018/11/08
+
+ \begin{itemize}
+ \tightlist
+ \item
+ removed replacing newlines by blank strings in the
+ \texttt{joincharacters} domfilter. The issue it fixed doesn't seem
+ to exist anymore, and it ate spaces sometimes.
+ \end{itemize}
+\item
+ 2018/11/01
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added \texttt{t4htlinks} domfilter
+ \item
+ fixed the \texttt{xtpipes} and \texttt{filters} execution order in
+ the \texttt{ODT} format
+ \end{itemize}
+\item
+ 2018/10/26
+
+ \begin{itemize}
+ \tightlist
+ \item
+ fixed ODT generation for files that contains special characters for
+ Lua string patterns
+ \item
+ replace non-breaking spaces with entities. It caused issues in LO
+ \end{itemize}
+\item
+ 2018/10/18
+
+ \begin{itemize}
+ \tightlist
+ \item
+ fixed the executable installation
+ \end{itemize}
+\item
+ 2018/09/16
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added the \texttt{scale} option for \texttt{dvisvgm\_hashes}
+ extension
+ \end{itemize}
+\item
+ 2018/09/14
+
+ \begin{itemize}
+ \tightlist
+ \item
+ require the \texttt{-dvi} option with \texttt{latexmk\_build}
+ extension
+ \end{itemize}
+\item
+ 2018/09/12
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added \texttt{xindy} command for the build file
+ \end{itemize}
+\item
+ 2018/09/03
+
+ \begin{itemize}
+ \tightlist
+ \item
+ expanded the \texttt{-\/-help} option
+ \end{itemize}
+\item
+ 2018/08/27
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added \texttt{odtimagesize} domfilter
+ \item
+ load \texttt{odtimagesize} by default in the ODT format
+ \end{itemize}
+\item
+ 2018/08/23
+
+ \begin{itemize}
+ \tightlist
+ \item
+ released version 0.2c
+ \end{itemize}
+\item
+ 2018/08/21
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added processor core detection on Windows
+ \item
+ make processor number configurable
+ \item
+ updated the documentation.
+ \end{itemize}
+\item
+ 2018/08/20
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added \texttt{dvisvgm\_hashes} extension
+ \end{itemize}
+\item
+ 2018/07/03
+
+ \begin{itemize}
+ \tightlist
+ \item
+ create the \texttt{mimetype} file to achieve the ODT file validity
+ \end{itemize}
+\item
+ 2018/07/02
+
+ \begin{itemize}
+ \tightlist
+ \item
+ disabled conversion of XML entities for \&, \textless{} and
+ \textgreater{} characters back to Unicode, because it breaks XML
+ validity
+ \end{itemize}
+\item
+ 2018/06/27
+
+ \begin{itemize}
+ \tightlist
+ \item
+ fixed root dir detection
+ \end{itemize}
+\item
+ 2018/06/26
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added code for detection of TeX distribution root for Miktex and TL
+ \end{itemize}
+\item
+ 2018/06/25
+
+ \begin{itemize}
+ \tightlist
+ \item
+ moved call to \texttt{xtpipes} from \texttt{t4ht} to the
+ \texttt{ODT} format drives. This should fix issues with path
+ expansion in \texttt{tex4ht.env} in TeX distributions.
+ \end{itemize}
+\item
+ 2018/06/22
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added \texttt{mkutils.find\_zip} function. It detects \texttt{zip}
+ or \texttt{miktex-zip} executables
+ \end{itemize}
+\item
+ 2018/06/19
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added new filter: \texttt{entities-to-unicode}. It converts XML
+ entites for Unicode characters back to Unicode.
+ \item
+ execute \texttt{entities-to-unicode} filter on text and math files
+ in the ODT output.
+ \end{itemize}
+\item
+ 2018/06/12
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added support for direct \texttt{ODT} file packing
+ \end{itemize}
+\item
+ 2018/06/11
+
+ \begin{itemize}
+ \tightlist
+ \item
+ new function available for formats, \texttt{format.modify\_build}
+ \item
+ function \texttt{mkutils.delete\_dir} for directory removal
+ \item
+ function \texttt{mkutils.mv} for file moving
+ \item
+ started on packing of the \texttt{ODT} files directly by the format,
+ instead of \texttt{t4ht}
+ \end{itemize}
+\item
+ 2018/06/08
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added support for filenames containing spaces
+ \item
+ added support for filenames containing non-ascii characters
+ \item
+ don't require sudo for the installation, let the user to install
+ symbolic links to \texttt{\$PATH}
+ \end{itemize}
+\item
+ 2018/05/03
+
+ \begin{itemize}
+ \tightlist
+ \item
+ released version \texttt{0.2b}
+ \item
+ bug fix: use only \texttt{load} function in \texttt{Make:run}, in
+ order to support a local environment.
+ \end{itemize}
+\item
+ 2018/05/03
+
+ \begin{itemize}
+ \tightlist
+ \item
+ released version \texttt{0.2a}
+ \item
+ renamed \texttt{latexmk} extension to \texttt{latexmk\_build}, due
+ to clash in TL
+ \end{itemize}
+\item
+ 2018/04/18
+
+ \begin{itemize}
+ \tightlist
+ \item
+ \texttt{staticsite} extension:
+
+ \begin{itemize}
+ \tightlist
+ \item
+ make YAML header configurable
+ \item
+ set the \texttt{time} and \texttt{updated} headers
+ \end{itemize}
+ \item
+ don't override existing tables in \texttt{filter\_settings}
+ \end{itemize}
+\item
+ 2018/04/17
+
+ \begin{itemize}
+ \tightlist
+ \item
+ done first version of \texttt{staticsite} extension
+ \end{itemize}
+\item
+ 2018/04/16
+
+ \begin{itemize}
+ \tightlist
+ \item
+ check for Git repo in the Makefile, don't run Git commands outside
+ of repo
+ \end{itemize}
+\item
+ 2018/04/15
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added \texttt{staticsite} filter
+ \item
+ working on \texttt{staticsite} extension
+ \end{itemize}
+\item
+ 2018/04/13
+
+ \begin{itemize}
+ \tightlist
+ \item
+ use \texttt{ipairs} instead of \texttt{pairs} to traverse lists of
+ images and image match functions
+ \item
+ load extensions in the correct order
+ \end{itemize}
+\item
+ 2018/04/09
+
+ \begin{itemize}
+ \tightlist
+ \item
+ released version \texttt{0.2}
+ \item
+ disabled default loading of \texttt{common\_domfilters} extension
+ \end{itemize}
+\item
+ 2018/04/06
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added \texttt{Make:enable\_extension} and
+ \texttt{Make:disable\_extension} functions
+ \item
+ documented the configuration file
+ \end{itemize}
+\item
+ 2018/03/09
+
+ \begin{itemize}
+ \tightlist
+ \item
+ load the configuration file before extensions
+ \end{itemize}
+\item
+ 2018/03/02
+
+ \begin{itemize}
+ \tightlist
+ \item
+ Aeneas execution works
+ \item
+ Aeneas documentation
+ \item
+ added support for \texttt{.make4ht} configuration file
+ \end{itemize}
+\item
+ 2018/02/28
+
+ \begin{itemize}
+ \tightlist
+ \item
+ Aeneas configuration file creation works
+ \end{itemize}
+\item
+ 2018/02/22
+
+ \begin{itemize}
+ \tightlist
+ \item
+ fixed bug in \texttt{fixinlines} DOM filter
+ \end{itemize}
+\item
+ 2018/02/21
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added Aeneas domfilter
+ \item
+ fixed bugs in \texttt{joincharacters} DOM filter
+ \end{itemize}
+\item
+ 2018/02/20
+
+ \begin{itemize}
+ \tightlist
+ \item
+ fixed bug in \texttt{joincharacters} DOM filter
+ \item
+ make \texttt{woff} default font format for \texttt{mathjaxnode}
+ \item
+ added documentation for \texttt{mathjaxnode} settings
+ \end{itemize}
+\item
+ 2018/02/19
+
+ \begin{itemize}
+ \tightlist
+ \item
+ fixed bug in filter loading
+ \item
+ added \texttt{mathjaxnode} extension
+ \end{itemize}
+\item
+ 2018/02/15
+
+ \begin{itemize}
+ \tightlist
+ \item
+ use HTML5 as a default format
+ \item
+ use \texttt{common\_domfilters} implicitly for the XHTML and HTML5
+ formats
+ \end{itemize}
+\item
+ 2018/02/12
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added \texttt{common\_domfilters} extension
+ \item
+ documented DOM filters
+ \end{itemize}
+\item
+ 2018/02/12
+
+ \begin{itemize}
+ \tightlist
+ \item
+ handle XML parsing errors in the DOM handler
+ \item
+ enable extension loading in Formatters
+ \end{itemize}
+\item
+ 2018/02/11
+
+ \begin{itemize}
+ \tightlist
+ \item
+ fixed Tidy extension output to support LuaXML
+ \item
+ fixed white space issues with \texttt{joincharacters} DOM filter
+ \end{itemize}
+\item
+ 2018/02/09
+
+ \begin{itemize}
+ \tightlist
+ \item
+ fixed issues with the Mathjax filter
+ \item
+ documented basic info about thd DOM filters
+ \item
+ DOM filter optimalizations
+ \end{itemize}
+\item
+ 2018/02/08
+
+ \begin{itemize}
+ \tightlist
+ \item
+ make Tidy extension configurable
+ \item
+ documented filter settings
+ \end{itemize}
+\item
+ 2018/02/07
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added filter for Mathjax-node
+ \end{itemize}
+\item
+ 2018/02/06
+
+ \begin{itemize}
+ \tightlist
+ \item
+ created DOM filter function
+ \item
+ added DOM filter for spurious inlinine elements
+ \end{itemize}
+\item
+ 2018/02/03
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added settings handling functions
+ \item
+ settings made available for extensions and filters
+ \end{itemize}
+\item
+ 2017/12/08
+
+ \begin{itemize}
+ \tightlist
+ \item
+ fixed the \texttt{mk4} build file loading when it is placed in the
+ current working dir and another one with same filename somewhere in
+ the TEXMF tree.
+ \end{itemize}
+\item
+ 2017/11/10
+
+ \begin{itemize}
+ \tightlist
+ \item
+ Added new filter: \texttt{svg-height}. It tries to fix height of
+ some of the images produced by \texttt{dvisvgm}
+ \end{itemize}
+\item
+ 2017/10/06
+
+ \begin{itemize}
+ \tightlist
+ \item
+ Added support for output format selection. Supported formats are
+ \texttt{xhtml}, \texttt{html5} and \texttt{odt}
+ \item
+ Added support for extensions
+ \end{itemize}
+\item
+ 2017/09/10
+
+ \begin{itemize}
+ \tightlist
+ \item
+ Added support for Latexmk
+ \item
+ Added support of \texttt{math} library and \texttt{tonumber}
+ function in the build files
+ \end{itemize}
+\item
+ 2017/09/04
+
+ \begin{itemize}
+ \tightlist
+ \item
+ fixed bug caused by the previous change -- the --help and --version
+ didn't work
+ \end{itemize}
+\item
+ 2017/08/22
+
+ \begin{itemize}
+ \tightlist
+ \item
+ fixed the command line option parsing for \texttt{tex4ht},
+ \texttt{t4ht} and \texttt{latex} commands
+ \item
+ various grammar and factual fixes in the documentation
+ \end{itemize}
+\item
+ 2017/04/26
+
+ \begin{itemize}
+ \tightlist
+ \item
+ Released version \texttt{v0.1c}
+ \end{itemize}
+\item
+ 2017/03/16
+
+ \begin{itemize}
+ \tightlist
+ \item
+ check for \texttt{TeX\ capacity\ exceeded} error in the \LaTeX~run.
+ \end{itemize}
+\item
+ 2016/12/19
+
+ \begin{itemize}
+ \tightlist
+ \item
+ use full input name in \texttt{tex\_file} variable. This should
+ enable use of files without \texttt{.tex} extension.
+ \end{itemize}
+\item
+ 2016/10/22
+
+ \begin{itemize}
+ \tightlist
+ \item
+ new command available in the build file:
+ \texttt{Make:add\_file(filename)}. This enables filters and commands
+ to register files to the output.
+ \item
+ use ipairs instead of pairs for traversing files and executing
+ filters. This should ensure correct order of executions.
+ \end{itemize}
+\item
+ 2016/10/18
+
+ \begin{itemize}
+ \tightlist
+ \item
+ new filter: replace colons in \texttt{id} and \texttt{href}
+ attributes with underscores
+ \end{itemize}
+\item
+ 2016/01/11
+
+ \begin{itemize}
+ \tightlist
+ \item
+ fixed bug in loading documents with full path specified
+ \end{itemize}
+\item
+ 2015/12/06 version 0.1b
+
+ \begin{itemize}
+ \tightlist
+ \item
+ modifed lapp library to recognize \texttt{-\/-version} and
+ \item
+ added \texttt{-\/-help} and \texttt{-\/-version} command line
+ options
+ \end{itemize}
+\item
+ 2015/11/30
+
+ \begin{itemize}
+ \tightlist
+ \item
+ use \texttt{kpse} library for build file locating
+ \end{itemize}
+\item
+ 2015/11/17
+
+ \begin{itemize}
+ \tightlist
+ \item
+ better \texttt{-jobname} handling
+ \end{itemize}
+\item
+ 2015/09/23 version 0.1a
+
+ \begin{itemize}
+ \tightlist
+ \item
+ various documentation updates
+ \item
+ \texttt{mozhtf} profile for unicode output is used, this should
+ prevent ligatures in the output files
+ \end{itemize}
+\item
+ 2015/06/29 version 0.1
+
+ \begin{itemize}
+ \tightlist
+ \item
+ major README file update
+ \end{itemize}
+\item
+ 2015/06/26
+
+ \begin{itemize}
+ \tightlist
+ \item
+ added Makefile
+ \item
+ moved INSTALL instructions from README to INSTALL
+ \end{itemize}
+\end{itemize}
diff --git a/support/make4ht/domfilters/make4ht-aeneas.lua b/support/make4ht/domfilters/make4ht-aeneas.lua
new file mode 100644
index 0000000000..0b93c95840
--- /dev/null
+++ b/support/make4ht/domfilters/make4ht-aeneas.lua
@@ -0,0 +1,144 @@
+-- DOM filter for Aeneas, tool for automatical text and audio synchronization
+-- https://github.com/readbeyond/aeneas
+-- It adds elements with id attributes for text chunks, in sentence length.
+--
+--
+local cssquery = require "luaxml-cssquery"
+local mkutils = require "mkutils"
+
+-- Table of CSS selectors to be skipped.
+local skip_elements = { "math", "svg"}
+
+-- The id attribute format is configurable
+-- Aeneas must be told to search for the ID pattern using is_text_unparsed_id_regex
+-- option in Aneas configuration file
+local id_prefix = "ast"
+
+-- Pattern to mach a sentence. It should match two groups, first is actual
+-- sentence, the second optional interpunction mark.
+local sentence_match = "([^%.^%?^!]*)([%.%?!]?)"
+
+-- convert table with selectors to a query list
+local function prepare_selectors(skips)
+ local css = cssquery()
+ for _, selector in ipairs(skips) do
+ css:add_selector(selector)
+ end
+ return css
+end
+
+-- save the HTML language
+local function save_config(dom, saves)
+ local get_lang = function(d)
+ local html = d:query_selector("html")[1] or {}
+ return html:get_attribute("lang")
+ end
+ local saves = saves or {}
+ local config = get_filter_settings "aeneas_config"
+ if config.language then return end
+ saves.lang = get_lang(dom)
+ filter_settings "aeneas-config" (saves)
+end
+-- make span element with unique id for a sentence
+local function make_span(id,parent, text)
+ local newobj = parent:create_element("span", {id=id })
+ newobj.processed = true -- to disable multiple processing of the node
+ local text_node = newobj:create_text_node(text)
+ newobj:add_child_node(text_node)
+ return newobj
+end
+
+-- make the id attribute and update the id value
+local function make_id(lastid, id_prefix)
+ local id = id_prefix .. lastid
+ lastid = lastid + 1
+ return id, lastid
+end
+
+-- parse text for sentences and add spans
+local function make_ids(parent, text, lastid, id_prefix)
+ local t = {}
+ local id
+ for chunk, punct in text:gmatch(sentence_match) do
+ id, lastid = make_id(lastid, id_prefix)
+ local newtext = chunk..punct
+ -- the newtext is empty string sometimes. we can skipt it then.
+ if newtext~="" then
+ table.insert(t, make_span(id, parent, newtext))
+ end
+ end
+ return t, lastid
+end
+
+
+
+-- test if the DOM element is in list of skipped CSS selectors
+local function is_skipped(el, css)
+ local matched = css:match_querylist(el)
+ return #matched > 0
+end
+
+local function aeneas(dom, par)
+ local par = par or {}
+ local id = 1
+ local options = get_filter_settings "aeneas"
+ local skip_elements = options.skip_elements or par.skip_elements or skip_elements
+ local id_prefix = options.id_prefix or par.id_prefix or id_prefix
+ local skip_object = prepare_selectors(skip_elements)
+ sentence_match = options.sentence_match or par.sentence_match or sentence_match
+
+ local body = dom:query_selector("body")[1]
+ -- process only the document body
+ if not body then return dom end
+ -- save information for aeneas_config
+ save_config(dom, {id_prefix = id_prefix})
+ body:traverse_elements(function(el)
+ -- skip disabled elements
+ if(is_skipped(el, skip_object)) then return false end
+ -- skip already processed elements
+ if el.processed then return false end
+ local newchildren = {} -- this will contain the new elements
+ local children = el:get_children()
+ local first_child = children[1]
+
+ -- if the element contains only text, doesn't already have an id attribute and the text is short,
+ -- the id is set directly on that element.
+ if #children == 1
+ and first_child:is_text()
+ and not el:get_attribute("id")
+ and string.len(first_child._text) < 20
+ and el._attr
+ then
+ local idtitle
+ idtitle, id = make_id(id, id_prefix)
+ print(el._name, first_child._text)
+ el:set_attribute("id", idtitle)
+ return el
+ end
+
+ for _, child in ipairs(children) do
+ -- process only non-empty text
+ if child:is_text() and child._text:match("%a+") then
+ local newnodes
+ newnodes, id = make_ids(child, child._text, id, id_prefix)
+ for _, node in ipairs(newnodes) do
+ table.insert(newchildren, node or {})
+ end
+ else
+ -- insert the current processing element to the new element list
+ -- if it isn't only text
+ table.insert(newchildren, child or {})
+ end
+ end
+ -- replace element children with the new ones
+ if #newchildren > 0 then
+ el._children = {}
+ for _, c in ipairs(newchildren) do
+ el:add_child_node(c)
+ end
+ end
+ end)
+ return dom
+end
+
+return aeneas
diff --git a/support/make4ht/domfilters/make4ht-fixinlines.lua b/support/make4ht/domfilters/make4ht-fixinlines.lua
new file mode 100644
index 0000000000..25489f59ca
--- /dev/null
+++ b/support/make4ht/domfilters/make4ht-fixinlines.lua
@@ -0,0 +1,57 @@
+local inline_elements = {
+ b=true,
+ big=true,
+ i=true,
+ small=true,
+ tt=true,
+ abbr=true,
+ acronym=true,
+ cite=true,
+ code=true,
+ dfn=true,
+ em=true,
+ kbd=true,
+ strong=true,
+ samp=true,
+ time=true,
+ var=true,
+ a=true,
+ bdo=true,
+ br=true,
+ img=true,
+ map=true,
+ object=true,
+ q=true,
+ script=true,
+ span=true,
+ sub=true,
+ sup=true,
+ button=true,
+ input=true,
+ label=true,
+ select=true,
+ textarea=true,
+ mn=true,
+ mi=true
+}
+
+
+local function fix_inlines(obj)
+ local settings = get_filter_settings "fixinlines"
+ local inline_elements = settings.inline_elements or inline_elements
+ local nodes = obj:get_path("html body")
+ obj:traverse_node_list(nodes, function(jej)
+ if jej._type == "ELEMENT" then
+ local name = string.lower(jej._name)
+ -- local parent = jej:get_parent_node()
+ if inline_elements[name] then
+ local new = obj:create_element("p" )
+ new:add_child_node(obj:copy_node(jej))
+ jej:replace_node(new)
+ end
+ end
+ end)
+ return obj
+end
+
+return fix_inlines
diff --git a/support/make4ht/domfilters/make4ht-idcolons.lua b/support/make4ht/domfilters/make4ht-idcolons.lua
new file mode 100644
index 0000000000..ccdafbd6d4
--- /dev/null
+++ b/support/make4ht/domfilters/make4ht-idcolons.lua
@@ -0,0 +1,20 @@
+local function id_colons(obj)
+ -- replace : characters in links and ids with unserscores
+ obj:traverse_elements(function(el)
+ local name = string.lower(obj:get_element_name(el))
+ if name == "a" then
+ local href = obj:get_attribute(el, "href")
+ if href and not href:match("[a-z]%://") then
+ obj:set_attribute(el, "href", href:gsub(":", "_"))
+ end
+ end
+ local id = obj:get_attribute( el , "id")
+ if id then
+ obj:set_attribute(el, "id", id:gsub(":", "_"))
+ end
+ -- local id = obj:get_attribute(el, "id")
+ end)
+ return obj
+end
+
+return id_colons
diff --git a/support/make4ht/domfilters/make4ht-joincharacters.lua b/support/make4ht/domfilters/make4ht-joincharacters.lua
new file mode 100644
index 0000000000..99d3058d7d
--- /dev/null
+++ b/support/make4ht/domfilters/make4ht-joincharacters.lua
@@ -0,0 +1,85 @@
+local charclases = {
+ span=true,
+ mn = true,
+}
+
+local function join_characters(obj,par)
+ -- join adjanced span and similar elements inserted by
+ -- tex4ht to just one object.
+ local par = par or {}
+ local options = get_filter_settings "joincharacters"
+ local charclases = options.charclases or par.charclases or charclases
+
+ obj:traverse_elements(function(el)
+ local get_name = function(curr)
+ return string.lower(curr:get_element_name())
+ end
+ local get_class = function(next_el)
+ return next_el:get_attribute("class")
+ end
+ local is_span = function(next_el)
+ return charclases[get_name(next_el)]
+ end
+
+ local function get_next(curr, class)
+ local next_el = curr:get_next_node()
+ if next_el and next_el:is_element() and is_span(next_el) then
+ return next_el
+ -- if the next node is space followed by a matching element, we should add this space
+ elseif next_el and next_el:is_text() and get_next(next_el, class) then
+ local text = next_el._text
+ -- match only text containing just whitespace
+ if text:match("^%s+$") then return next_el end
+ end
+ end
+ -- loop over all elements and test if the current element is in a list of
+ -- processed elements (charclases)
+ if is_span(el) then
+ local next_el = get_next(el)
+ -- loop over the following elements and test whether they are of the same type
+ -- as the current one
+ while next_el do
+ -- save the next element because we will remove it later
+ local real_next = get_next(next_el)
+ if get_name(el) == get_name(next_el) and get_class(el) == get_class(next_el) and not el:get_attribute("id") then
+ -- it the following element match, copy it's children to the current element
+ for _, child in ipairs(next_el:get_children()) do
+ el:add_child_node(child)
+ end
+ -- remove the next element
+ next_el:remove_node()
+ -- add the whitespace
+ elseif next_el:is_text() then
+ local s = next_el._text
+ -- we must create a new node
+ el:add_child_node(el:create_text_node(s))
+ next_el:remove_node()
+ real_next = nil
+ else
+ real_next = nil
+ end
+ -- use the saved element as a next object
+ next_el = real_next
+ end
+ end
+
+ end)
+ -- join text nodes in an element into one
+ obj:traverse_elements(function(el)
+ -- save the text
+ local t = {}
+ local children = el:get_children()
+ for _, x in ipairs(children) do
+ if x:is_text() then
+ t[#t+1] = x._text
+ else
+ return nil
+ end
+ end
+ el._text = table.concat(t)
+ return el
+ end)
+ return obj
+end
+
+return join_characters
diff --git a/support/make4ht/domfilters/make4ht-joincolors.lua b/support/make4ht/domfilters/make4ht-joincolors.lua
new file mode 100644
index 0000000000..94a3a054e6
--- /dev/null
+++ b/support/make4ht/domfilters/make4ht-joincolors.lua
@@ -0,0 +1,75 @@
+local cssfiles = {}
+
+
+-- keep mapping between span ids and colors
+local colors = {}
+
+local function extract_colors(csscontent)
+ local used_colors = {}
+ -- delete the color ids and save the used colors
+ csscontent = csscontent:gsub("[%a]*%#(textcolor.-)%s*{%s*color%s*%:%s*(.-)%s*%}%s", function(id, color)
+ -- convert rgb() function to hex value and generate the span name
+ local converted = "textcolor-" .. color:gsub("rgb%((.-),(.-),(.-)%)", function(r,g,b)
+ return string.format("%02x%02x%02x", tonumber(r), tonumber(g), tonumber(b))
+ end)
+ -- remove the # characters from the converted color name
+ converted = converted:gsub("%#", "")
+ -- save the id and used color
+ colors[id] = converted
+ used_colors[converted] = color
+ return ""
+ end)
+ -- add the used colors to css
+ local t = {}
+ for class, color in pairs(used_colors) do
+ t[#t+1] = string.format(".%s{color:%s;}", class, color)
+ end
+ return csscontent .. table.concat(t, "\n")
+end
+
+local function process_css(cssfile)
+ local f = io.open(cssfile,"r")
+ if not f then return nil, "Cannot open the CSS file: ".. cssfile end
+ local content = f:read("*all")
+ f:close()
+ -- delete color ids and replace them with joined spans
+ local newcontent = extract_colors(content)
+ -- save the updated css file
+ local f=io.open(cssfile, "w")
+ f:write(newcontent)
+ f:close()
+end
+
+
+local function process_css_files(dom)
+ for _, el in ipairs(dom:query_selector("link")) do
+ local href = el:get_attribute("href") or ""
+ if not cssfiles[href] and href:match("css$") then
+ print("Load CSS file ", href)
+ cssfiles[href] = true
+ process_css(href)
+ end
+ end
+
+end
+
+local function join_colors(dom)
+ -- find css files in the current HTML file and join the colors
+ process_css_files(dom)
+ for _, span in ipairs(dom:query_selector("span")) do
+ local id = span:get_attribute("id")
+ if id then
+ -- test if the id is in the saved colors
+ local class = colors[id]
+ if class then
+ -- remove the id
+ span:set_attribute("id", nil)
+ span:set_attribute("class", class)
+ end
+ end
+ end
+
+ return dom
+end
+
+return join_colors
diff --git a/support/make4ht/domfilters/make4ht-odtimagesize.lua b/support/make4ht/domfilters/make4ht-odtimagesize.lua
new file mode 100644
index 0000000000..4ce8f0d92e
--- /dev/null
+++ b/support/make4ht/domfilters/make4ht-odtimagesize.lua
@@ -0,0 +1,16 @@
+-- set correct dimensions to frames around images
+return function(dom)
+ local frames = dom:query_selector("draw|frame")
+ for _, frame in ipairs(frames) do
+ local images = frame:query_selector("draw|image")
+ if #images > 0 then
+ local image = images[1]
+ local width = image:get_attribute("svg:width")
+ local height = image:get_attribute("svg:height")
+ if widht then frame:set_attribute("svg:width", width) end
+ if height then frame:set_attribute("svg:height", height) end
+ print("image dimensions", width, height)
+ end
+ end
+ return dom
+end
diff --git a/support/make4ht/domfilters/make4ht-t4htlinks.lua b/support/make4ht/domfilters/make4ht-t4htlinks.lua
new file mode 100644
index 0000000000..9a4efb9739
--- /dev/null
+++ b/support/make4ht/domfilters/make4ht-t4htlinks.lua
@@ -0,0 +1,27 @@
+-- This filter is used by the ODT output format to fix links
+return function(dom)
+ for _, link in ipairs(dom:query_selector("t4htlink")) do
+ local name = link:get_attribute("name")
+ local href = link:get_attribute("href")
+ local children = link:get_children()
+ -- print("link", name, href, #link._children, link:get_text())
+ -- add a link if it contains any subnodes and has href attribute
+ if #children > 0 and href then
+ link._name = "text:a"
+ link._attr = {["xlink:type"]="simple", ["xlink:href"]=href}
+ -- if the link is named, add a bookmark
+ if name then
+ local bookmark = link:create_element("text:bookmark", {["text:name"] = name})
+ link:add_child_node(bookmark)
+ end
+ -- add bookmark if element has name
+ elseif name then
+ link._name = "text:bookmark"
+ link._attr = {["text:name"] = name}
+ else
+ -- just remove the link in other cases
+ link:remove_node()
+ end
+ end
+ return dom
+end
diff --git a/support/make4ht/extensions/common_domfilters.lua b/support/make4ht/extensions/common_domfilters.lua
new file mode 100644
index 0000000000..7c9a2dfaea
--- /dev/null
+++ b/support/make4ht/extensions/common_domfilters.lua
@@ -0,0 +1,27 @@
+local M = {}
+
+
+local filter = require "make4ht-domfilter"
+-- local process = filter {"fixinlines", "idcolons", "joincharacters" }
+
+-- filters support only html formats
+function M.test(format)
+ if format == "odt" then return false end
+ return true
+end
+
+function M.modify_build(make)
+ local process = filter {"fixinlines", "idcolons", "joincharacters"}
+ make:match("html$", process)
+ local matches = make.matches
+ -- the filters should be first match to be executed, especially if tidy
+ -- should be executed as well
+ if #matches > 1 then
+ local last = matches[#matches]
+ table.insert(matches, 1, last)
+ matches[#matches] = nil
+ end
+ return make
+end
+
+return M
diff --git a/support/make4ht/extensions/common_filters.lua b/support/make4ht/extensions/common_filters.lua
new file mode 100644
index 0000000000..513fbed34e
--- /dev/null
+++ b/support/make4ht/extensions/common_filters.lua
@@ -0,0 +1,26 @@
+local M = {}
+
+
+local filter = require "make4ht-filter"
+local process = filter {"cleanspan-nat", "fixligatures", "hruletohr", "entities", "fix-links"}
+
+-- filters support only html formats
+function M.test(format)
+ if format == "odt" then return false end
+ return true
+end
+
+function M.modify_build(make)
+ make:match("html$", process)
+ local matches = make.matches
+ -- the filters should be first match to be executed, especially if tidy
+ -- should be executed as well
+ if #matches > 1 then
+ local last = matches[#matches]
+ table.insert(matches, 1, last)
+ matches[#matches] = nil
+ end
+ return make
+end
+
+return M
diff --git a/support/make4ht/extensions/dvisvgm_hashes.lua b/support/make4ht/extensions/dvisvgm_hashes.lua
new file mode 100644
index 0000000000..83010ac388
--- /dev/null
+++ b/support/make4ht/extensions/dvisvgm_hashes.lua
@@ -0,0 +1,268 @@
+local dvireader = require "make4ht-dvireader"
+local mkutils = require "mkutils"
+local filter = require "make4ht-filter"
+
+
+local dvisvgm_par = {}
+
+local M = {}
+-- mapping between tex4ht image names and hashed image names
+local output_map = {}
+local dvisvgm_options = "-n --exact -c ${scale},${scale}"
+local parallel_size = 64
+-- local parallel_size = 3
+
+local function make_hashed_name(base, hash)
+ return base .. "-" ..hash..".svg"
+end
+
+-- detect the number of available processors
+local cpu_cnt = 3 -- set a reasonable default for non-Linux systems
+
+if os.name == 'linux' then
+ cpu_cnt = 0
+ local cpuinfo=assert(io.open('/proc/cpuinfo', 'r'))
+ for line in cpuinfo:lines() do
+ if line:match('^processor') then
+ cpu_cnt = cpu_cnt + 1
+ end
+ end
+ -- set default number of threds if no CPU core have been found
+ if cpu_cnt == 0 then cpu_cnt = 1 end
+ cpuinfo:close()
+elseif os.name == 'cygwin' or os.type == 'windows' then
+ -- windows has NUMBER_OF_PROCESSORS environmental value
+ local nop = os.getenv('NUMBER_OF_PROCESSORS')
+ if tonumber(nop) then
+ cpu_cnt = nop
+ end
+end
+
+
+
+-- process output of dvisvgm and find output page numbers and corresponding files
+local function get_generated_pages(output, pages)
+ local pages = pages or {}
+ local pos = 1
+ local pos, finish, page = string.find(output, "processing page (%d+)", pos)
+ while(pos) do
+ pos, finish, file = string.find(output, "output written to ([^\n]+)", finish)
+ pages[tonumber(page)] = file
+ if not finish then break end
+ pos, finish, page = string.find(output, "processing page (%d+)", finish)
+ end
+ return pages
+end
+
+local function make_ranges(pages)
+ local newpages = {}
+ local start, stop
+ for i=1,#pages do
+ local current = pages[i]
+ local next_el = pages[i+1] or current + 100 -- just select a big number
+ local diff = next_el - current
+ if diff == 1 then
+ if not start then start = current end
+ else
+ local element
+ if start then
+ element = start .. "-" .. current
+ else
+ element = current
+ end
+ newpages[#newpages+1] = element
+ start = nil
+ end
+ end
+ return newpages
+end
+
+local function read_log(dvisvgmlog)
+ local f = io.open(dvisvgmlog, "rb")
+ if not f then return nil, "Cannot read dvisvgm log" end
+ local output = f:read("*all")
+ f:close()
+ return output
+end
+
+-- test the existence of GNU Make, which can execute tasks in parallel
+local function test_make()
+ local make = io.popen("make -v", "r")
+ if not make then return false end
+ local content = make:read("*all")
+ make:close()
+ return true
+end
+
+local function save_file(filename, text)
+ local f = io.open(filename, "w")
+ f:write(text)
+ f:close()
+end
+
+
+local function make_makefile_command(idvfile, page_sequences)
+ local logs = {}
+ local all = {} -- list of targets in the "all:" makefile target
+ local targets = {}
+ local basename = idvfile:gsub(".idv$", "")
+ local makefilename = basename .. "-images" .. ".mk"
+ -- build make targets
+ for i, ranges in ipairs(page_sequences) do
+ local target = basename .. "-" .. i
+ local logfile = target .. ".dlog"
+ logs[#logs + 1] = logfile
+ all[#all+1] = target
+ local chunk = target .. ":\n\tdvisvgm -v4 " .. dvisvgm_options .. " -p " .. ranges .. " " .. idvfile .. " 2> " .. logfile .. "\n"
+ targets[#targets + 1] = chunk
+ end
+ -- construct makefile and save it
+ local makefile = "all: " .. table.concat(all, " ") .. "\n\n" .. table.concat(targets, "\n")
+ save_file(makefilename, makefile)
+ local command = "make -j" .. cpu_cnt .." -f " .. makefilename
+ return command, logs
+end
+
+local function prepare_command(idvfile, pages)
+ local logs = {}
+ if #pages > parallel_size and test_make() then
+ local page_sequences = {}
+ for i=1, #pages, parallel_size do
+ local current_pages = {}
+ for x = i, i+parallel_size -1 do
+ current_pages[#current_pages + 1] = pages[x]
+ end
+ table.insert(page_sequences,table.concat(make_ranges(current_pages), ","))
+ end
+ return make_makefile_command(idvfile, page_sequences)
+ end
+ -- else
+ local pagesequence = table.concat(make_ranges(pages), ",")
+ -- the stderr from dvisvgm must be redirected and postprocessed
+ local dvisvgmlog = idvfile:gsub("idv$", "dlog")
+ -- local dvisvgm = io.popen("dvisvgm -v4 -n --exact -c 1.15,1.15 -p " .. pagesequence .. " " .. idvfile, "r")
+ local command = "dvisvgm -v4 " .. dvisvgm_options .. " -p " .. pagesequence .. " " .. idvfile .. " 2> " .. dvisvgmlog
+ return command, {dvisvgmlog}
+ -- end
+end
+
+local function execute_dvisvgm(idvfile, pages)
+ if #pages < 1 then return nil, "No pages to convert" end
+ local command, logs = prepare_command(idvfile, pages)
+ print(command)
+ os.execute(command)
+ local generated_pages = {}
+ for _, dvisvgmlog in ipairs(logs) do
+ local output = read_log(dvisvgmlog)
+ generated_pages = get_generated_pages(output, generated_pages)
+ end
+ return generated_pages
+end
+
+local function get_dvi_pages(arg)
+ -- list of pages to convert in this run
+ local to_convert = {}
+ local idv_file = arg.input .. ".idv"
+ -- set extension options
+ local extoptions = mkutils.get_filter_settings "dvisvgm_hashes" or {}
+ dvisvgm_options = arg.options or extoptions.options or dvisvgm_options
+ parallel_size = arg.parallel_size or extoptions.parallel_size or parallel_size
+ cpu_cnt = arg.cpu_cnt or extoptions.cpu_cnt or cpu_cnt
+ dvisvgm_par.scale = arg.scale or extoptions.scale or 1.15
+ dvisvgm_options = dvisvgm_options % dvisvgm_par
+ local f = io.open(idv_file, "rb")
+ if not f then return nil, "Cannot open idv file: " .. idv_file end
+ local content = f:read("*all")
+ f:close()
+ local dvi_pages = dvireader.get_pages(content)
+ -- we must find page numbers and output name sfor the generated images
+ local lg = mkutils.parse_lg(arg.input ..".lg")
+ for _, name in ipairs(lg.images) do
+ local page = tonumber(name.page)
+ local hash = dvi_pages[page]
+ local tex4ht_name = name.output
+ local output_name = make_hashed_name(arg.input, hash)
+ output_map[tex4ht_name] = output_name
+ if not mkutils.file_exists(output_name) then
+ print(output_name)
+ to_convert[#to_convert+1] = page
+ end
+ end
+ local generated_files, msg = execute_dvisvgm(idv_file, to_convert)
+ if not generated_files then
+ return nil, msg
+ end
+
+ -- rename the generated files to the hashed filenames
+ for page, file in pairs(generated_files) do
+ os.rename(file, make_hashed_name(arg.input, dvi_pages[page]))
+ end
+
+end
+
+function M.test(format)
+ -- ODT format doesn't support SVG
+ if format == "odt" then return false end
+ return true
+end
+
+function M.modify_build(make)
+ -- this must be used in the .mk4 file as
+ -- Make:dvisvgm_hashes {}
+ make:add("dvisvgm_hashes", function(arg)
+ get_dvi_pages(arg)
+ end,
+ {
+ })
+
+ -- insert dvisvgm_hashes command at the end of the build sequence -- it needs to be called after t4ht
+ make:dvisvgm_hashes {}
+
+ -- replace original image names with hashed names
+ local executed = false
+ make:match(".*", function(arg)
+ if not executed then
+ executed = true
+ local lgfiles = make.lgfile.files
+ for i, filename in ipairs(lgfiles) do
+ local replace = output_map[filename]
+ if replace then
+ lgfiles[i] = replace
+ end
+ end
+ -- tex4ebook process also the images table, so we need to replace generated filenames here as well
+ local lgimages = make.lgfile.images
+ for _, image in ipairs(lgimages) do
+ local replace = output_map[image.output]
+ if replace then
+ image.output = replace
+ end
+ end
+ end
+ end)
+
+ -- fix src attributes
+ local process = filter {
+ function(str)
+ return str:gsub('src="([^"]+)', function(filename)
+ local newname = output_map[filename] or filename
+ print("newname", newname)
+ return 'src="'.. newname
+ end)
+ end
+ }
+
+ make:match("htm.?$", process)
+
+ -- disable the image processing
+ for _,v in ipairs(make.build_seq) do
+ if v.name == "t4ht" then
+ local t4ht_par = v.params.t4ht_par or make.params.t4ht_par or ""
+ v.params.t4ht_par = t4ht_par .. " -p"
+ end
+ end
+ make:image(".", function() return "" end)
+ return make
+end
+
+return M
diff --git a/support/make4ht/extensions/join_colors.lua b/support/make4ht/extensions/join_colors.lua
new file mode 100644
index 0000000000..182661fbdb
--- /dev/null
+++ b/support/make4ht/extensions/join_colors.lua
@@ -0,0 +1,16 @@
+local M = {}
+
+local filter = require "make4ht-domfilter"
+
+-- filters support only html formats
+function M.test(format)
+ if format == "odt" then return false end
+ return true
+end
+
+function M.modify_build(make)
+ local process = filter {"joincolors"}
+ make:match("html$", process)
+ return make
+end
+return M
diff --git a/support/make4ht/extensions/latexmk_build.lua b/support/make4ht/extensions/latexmk_build.lua
new file mode 100644
index 0000000000..bf4dcdde5a
--- /dev/null
+++ b/support/make4ht/extensions/latexmk_build.lua
@@ -0,0 +1,31 @@
+-- use Latexmk in first LaTeX call
+-- only in the first call, because we don't need to execute biber, etc. in the subsequent
+-- LaTeX calls, these are only for resolving the cross-references
+local M = {}
+function M.modify_build(make)
+ local used = false
+ local first
+ local build_seq = make.build_seq
+ -- find first htlatex call in the build sequence
+ for pos,v in ipairs(build_seq) do
+ if v.name == "htlatex" and not first then
+ first = pos
+ end
+ end
+ -- if htlatex was found
+ if first then
+ -- add dummy latexmk call to the build sequence
+ make:latexmk {}
+ -- replace name, command and type in the first htlatex
+ -- call with values from the dummy latexmk call
+ local replaced = build_seq[first]
+ local latexmk = build_seq[#build_seq]
+ replaced.name = latexmk.name
+ replaced.command = latexmk.command
+ replaced.type = latexmk.type
+ -- remove the dummy latexmk
+ table.remove(build_seq)
+ end
+ return make
+end
+return M
diff --git a/support/make4ht/extensions/mathjaxnode.lua b/support/make4ht/extensions/mathjaxnode.lua
new file mode 100644
index 0000000000..cbad897d4e
--- /dev/null
+++ b/support/make4ht/extensions/mathjaxnode.lua
@@ -0,0 +1,16 @@
+local M = {}
+
+
+local filter = require "make4ht-filter"
+function M.test(format)
+ if format == "odt" then return false end
+ return true
+end
+
+function M.modify_build(make)
+ local mathjax = filter { "mathjaxnode"}
+ make:match("html$",mathjax)
+ return make
+end
+
+return M
diff --git a/support/make4ht/extensions/odttemplate.lua b/support/make4ht/extensions/odttemplate.lua
new file mode 100644
index 0000000000..3b0af2a699
--- /dev/null
+++ b/support/make4ht/extensions/odttemplate.lua
@@ -0,0 +1,16 @@
+local M = {}
+
+local filter = require "make4ht-filter"
+
+-- this extension only works for the ODT format
+M.test = function(format)
+ return format=="odt"
+end
+
+M.modify_build = function(make)
+ local process = filter {"odttemplate"}
+ make:match("4oy$", process)
+ return make
+end
+
+return M
diff --git a/support/make4ht/extensions/staticsite.lua b/support/make4ht/extensions/staticsite.lua
new file mode 100644
index 0000000000..fcb6c09e17
--- /dev/null
+++ b/support/make4ht/extensions/staticsite.lua
@@ -0,0 +1,135 @@
+local M = {}
+local filter = require "make4ht-filter"
+local mkutils = require "mkutils"
+
+-- get the published file name
+local function get_slug(settings)
+ local published_name = mkutils.remove_extension(settings.tex_file) .. ".published"
+ local config = get_filter_settings "staticsite"
+ local file_pattern = config.file_pattern or "%Y-%m-%d-${input}"
+ local time = os.time()
+
+ -- we must save the published date, so the subsequent compilations at different days
+ -- use the same name
+ if mkutils.file_exists(published_name) then
+ local f = io.open(published_name, "r")
+ local readtime = f:read("*line")
+ time = tonumber(readtime)
+ print("Already pubslished", slug)
+ f:close()
+ else
+ -- escape
+ -- slug must contain the unescaped input name
+ local f = io.open(published_name, "w")
+ f:write(time)
+ f:close()
+ end
+ -- set the updated and publishing times
+ local updated
+ -- the updated time will be set only when it is more than one day from the published time
+ local newtime = os.time()
+ if (newtime - time) > (24 * 3600) then updated = newtime end
+ filter_settings "staticsite" {
+ header = {
+ time = time,
+ updated = updated
+ }
+ }
+
+ -- make the output file name in the format YYYY-MM-DD-old-filename.html
+ local slug = os.date(file_pattern,time) % settings
+ return slug
+end
+
+
+-- it is necessary to set correct -jobname in latex_par parameters field
+-- in order to the get correct HTML file name
+local function update_jobname(slug, latex_par)
+ local latex_par = latex_par or ""
+ if latex_par:match("%-jobname") then
+ local firstchar=latex_par:match("%-jobname=.")
+ local replace_pattern="%-jobname=[^%s]+"
+ if firstchar == "'" or firstchar=='"' then
+ replace_pattern = "%-jobname=".. firstchar .."[^%"..firstchar.."]+"
+ end
+
+ return latex_par:gsub(replace_pattern, "-jobname=".. slug)
+ else
+ return latex_par .. "-jobname="..slug
+ end
+end
+
+-- execute the function passed as parameter only once, when the file matching
+-- starts
+local function insert_filter(make, pattern, fn)
+ local insert_executed = false
+ table.insert(make.matches, 1, {
+ pattern=pattern,
+ params = {},
+ command = function()
+ if not insert_executed then
+ fn()
+ end
+ insert_executed = true
+ end
+ })
+end
+
+
+local function copy_files(filename, par)
+ local function prepare_path(dir, subdir)
+ local path = dir .. "/" .. subdir .. "/" .. filename
+ return path:gsub("//", "/")
+ end
+ -- get extension settings
+ local site_settings = get_filter_settings "staticsite"
+ local site_root = site_settings.site_root
+ local map = site_settings.map or {}
+ -- default path without subdir, will be used if the file is not matched
+ -- by any pattern in the map
+ local path = prepare_path(site_root, "")
+ for pattern, destination in pairs(map) do
+ if filename:match(pattern) then
+ path = prepare_path(site_root, destination)
+ break
+ end
+ end
+ -- it is possible to use string extrapolation in path, for example for slug
+ mkutils.copy(filename, path % par)
+end
+
+function M.modify_build(make)
+ -- it is necessary to insert the filters for YAML header and file copying as last matches
+ -- we use an bogus match which will be executed only once as the very first one to insert
+ -- the filters
+ -- I should make filter from this
+ local process = filter {
+ "staticsite"
+ }
+ local settings = make.params
+ -- get the published file name
+ local slug = get_slug(settings)
+ for _, cmd in ipairs(make.build_seq) do
+ -- all commands must use the published file name
+ cmd.params.input = slug
+ cmd.params.latex_par = update_jobname(slug, cmd.params.latex_par)
+ end
+
+ local quotepattern = '(['..("%^$().[]*+-?"):gsub("(.)", "%%%1")..'])'
+ local mainfile = string.gsub(slug, quotepattern, "%%%1")
+
+ -- run the following code once in the first match on the first file
+ insert_filter(make, ".*", function()
+ -- for _, match in ipairs(make.matches) do
+ -- match.params.outdir = outdir
+ -- print(match.pattern, match.params.outdir)
+ -- end
+ -- make the YAML header only for the main HTML file
+ make:match(mainfile .. ".html", process)
+ make:match(".*", copy_files, {slug=slug})
+ end)
+
+ return make
+end
+
+return M
diff --git a/support/make4ht/extensions/tidy.lua b/support/make4ht/extensions/tidy.lua
new file mode 100644
index 0000000000..cf8ea9785a
--- /dev/null
+++ b/support/make4ht/extensions/tidy.lua
@@ -0,0 +1,57 @@
+local M = {}
+
+function M.test(format)
+ if format == "odt" then return false end
+ return true
+end
+
+local empty_elements = {
+ area=true,
+ base=true,
+ br=true,
+ col=true,
+ embed=true,
+ hr=true,
+ img=true,
+ input=true,
+ keygen=true,
+ link=true,
+ meta=true,
+ param=true,
+ source=true,
+ track=true,
+ wbr=true,
+}
+
+-- LuaXML cannot read HTML with unclosed tags (like <meta name="hello" content="world">)
+-- Tidy removes end slashes in the HTML output, so
+-- this function will add them back
+local function close_tags(s)
+ return s:gsub("<(%w+)([^>]-)>", function(tag, rest)
+ local endslash = ""
+ if empty_elements[tag] then endslash = " /" end
+ return string.format("<%s%s%s>", tag, rest, endslash)
+ end)
+end
+
+
+
+function M.modify_build(make)
+ make:match("html$", function(filename, par)
+ local settings = get_filter_settings "tidy" or {}
+ par.options = par.options or settings.options or "-utf8 -w 512 -ashtml -q"
+ local command = "tidy ${options} ${filename}" % par
+ print("execute: ".. command)
+ -- os.execute(command)
+ local run = io.popen(command, "r")
+ local result = run:read("*all")
+ run:close()
+ result = close_tags(result)
+ local f = io.open(filename, "w")
+ f:write(result)
+ f:close()
+ end)
+ return make
+end
+
+return M
diff --git a/support/make4ht/filters/make4ht-cleanspan-nat.lua b/support/make4ht/filters/make4ht-cleanspan-nat.lua
new file mode 100644
index 0000000000..896132160c
--- /dev/null
+++ b/support/make4ht/filters/make4ht-cleanspan-nat.lua
@@ -0,0 +1,12 @@
+-- cleanspan function submitted by Nat Kuhn
+-- http://www.natkuhn.com/
+
+local function filter(s)
+ local pattern = "(<span%s+([^>]+)>[^<]*)</span>(%s*)<span%s+%2>"
+ repeat
+ s, n = s:gsub(pattern, "%1%3")
+ until n == 0
+ return s
+end
+
+return filter
diff --git a/support/make4ht/filters/make4ht-cleanspan.lua b/support/make4ht/filters/make4ht-cleanspan.lua
new file mode 100644
index 0000000000..ede5c6c74e
--- /dev/null
+++ b/support/make4ht/filters/make4ht-cleanspan.lua
@@ -0,0 +1,33 @@
+-- make4ht-cleanspan4ht.lua
+-- fixes spurious <span> elements in tex4ht output
+
+
+function filter(input)
+ local parse_args = function(s)
+ local at = {}
+ s:gsub("(%w+)%s*=%s*\"([^\"]-)\"", function(k,w)
+ at[k]=w
+ end)
+ return at
+ end
+ -- local pattern = "(<?/?[%w]*>?)<span[%s]*class=\"([^\"]+)\"[%s]*>"
+ local pattern = "(<?/?[%w]*>?)([%s]*)<span[%s]*([^>]-)>"
+ local last_class = ""
+ local depth = 0
+ return input:gsub(pattern, function(tag,space, args)
+ local attr = parse_args(args) or {}
+ local class = attr["class"] or ""
+ if tag == "</span>" then
+ if class == last_class and class~= "" then
+ last_class = class
+ return space .. ""
+ end
+ elseif tag == "" then
+ class=""
+ end
+ last_class = class
+ return tag ..space .. '<span '..args ..'>'
+ end)
+end
+
+return filter
diff --git a/support/make4ht/filters/make4ht-domfilter.lua b/support/make4ht/filters/make4ht-domfilter.lua
new file mode 100644
index 0000000000..27d9ed871d
--- /dev/null
+++ b/support/make4ht/filters/make4ht-domfilter.lua
@@ -0,0 +1,63 @@
+local filter_lib = require "make4ht-filterlib"
+local dom = require "luaxml-domobject"
+local mkutils = require "mkutils"
+
+local function load_filter(filtername)
+ return require("domfilters.make4ht-"..filtername)
+end
+
+-- save processed names, in order to block multiple executions of the filter
+-- sequence on a same file
+local processed = {}
+
+local function filter(filters, name)
+ -- because XML parsing to DOM is potentially expensive operation
+ -- this filter will use cache for it's sequence
+ -- all requests to the domfilter will add new filters to the
+ -- one sequence, which will be executed on one DOM object.
+ -- it is possible to request a different sequence using
+ -- unique name parameter
+ local name = name or "domfilter"
+ local settings = mkutils.get_filter_settings(name) or {}
+ local sequence = settings.sequence or {}
+ local local_sequence = filter_lib.load_filters(filters, load_filter)
+ for _, filter in ipairs(local_sequence) do
+ table.insert(sequence, filter)
+ end
+ settings.sequence = sequence
+ mkutils.filter_settings (name) (settings)
+
+ return function(filename, parameters)
+ -- load processed files for the current filter name
+ local processed_files = processed[name] or {}
+ -- don't process the file again
+ if processed_files[filename] then
+ return nil
+ end
+ local input = filter_lib.load_input_file(filename)
+ if not input then return nil, "Cannot load the input file" end
+ -- we need to use pcall, because XML error would break the whole build process
+ -- domobject will be error object if DOM parsing failed
+ local status, domobject = pcall(function()
+ return dom.parse(input)
+ end)
+ if not status then
+ print("DOM parsing of " .. filename .. " failed:")
+ print(domobject)
+ return nil, "DOM parsing failed"
+ end
+ for _,f in pairs(sequence) do
+ domobject = f(domobject,parameters)
+ end
+ local output = domobject:serialize()
+ if output then
+ filter_lib.save_input_file(filename, output)
+ else
+ print("DOM filter failed on ".. filename)
+ end
+ -- mark the filename as processed
+ processed_files[filename] = true
+ processed[name] = processed_files
+ end
+end
+return filter
diff --git a/support/make4ht/filters/make4ht-entities-to-unicode.lua b/support/make4ht/filters/make4ht-entities-to-unicode.lua
new file mode 100644
index 0000000000..fb921a631a
--- /dev/null
+++ b/support/make4ht/filters/make4ht-entities-to-unicode.lua
@@ -0,0 +1,15 @@
+-- convert Unicode characters encoded as XML entities back to Unicode
+
+local utfchar = unicode.utf8.char
+-- list of disabled characters
+local disabled = { ["&"] = "&amp;", ["<"] = "&lt;", [">"] = "&gt;"}
+return function(content)
+ local content = content:gsub("%&%#x([A-Fa-f0-9]+);", function(entity)
+ -- convert hexadecimal entity to Unicode
+ local newchar = utfchar(tonumber(entity, 16))
+ -- we don't want to break XML validity with forbidden characters
+ return disabled[newchar] or newchar
+ end)
+ -- the non-breaking space character cause issues in the ODT opening
+ return content:gsub(string.char(160), "&#xA0;")
+end
diff --git a/support/make4ht/filters/make4ht-entities.lua b/support/make4ht/filters/make4ht-entities.lua
new file mode 100644
index 0000000000..f9f6c883d1
--- /dev/null
+++ b/support/make4ht/filters/make4ht-entities.lua
@@ -0,0 +1,14 @@
+-- Fix bad entities
+-- Sometimes, tex4ht produce named xml entities, which are prohobited in epub
+-- &nbsp;, for example
+function filter(s)
+ local replaces = {
+ nbsp = "#160"
+ }
+ return s:gsub("&(%w+);",function(x)
+ local m = replaces[x] or x
+ return "&"..m..";"
+ end)
+end
+
+return filter
diff --git a/support/make4ht/filters/make4ht-filter.lua b/support/make4ht/filters/make4ht-filter.lua
new file mode 100644
index 0000000000..41804aea39
--- /dev/null
+++ b/support/make4ht/filters/make4ht-filter.lua
@@ -0,0 +1,19 @@
+local filter_lib = require "make4ht-filterlib"
+
+local function load_filter(filtername)
+ return require("filters.make4ht-"..filtername)
+end
+
+function filter(filters)
+ local sequence = filter_lib.load_filters(filters, load_filter)
+ return function(filename, parameters)
+ if not filename then return false, "filters: no filename" end
+ local input = filter_lib.load_input_file(filename)
+ if not input then return nil, "Cannot load the input file" end
+ for _,f in pairs(sequence) do
+ input = f(input,parameters)
+ end
+ filter_lib.save_input_file(filename, input)
+ end
+end
+return filter
diff --git a/support/make4ht/filters/make4ht-fix-links.lua b/support/make4ht/filters/make4ht-fix-links.lua
new file mode 100644
index 0000000000..4dbc26824f
--- /dev/null
+++ b/support/make4ht/filters/make4ht-fix-links.lua
@@ -0,0 +1,19 @@
+-- replace colons in `id` or `href` attributes for local links with underscores
+--
+
+local function fix_href_colons(s)
+ return s:gsub('(href=".-")', function(a)
+ if a:match("[a-z]%://") then return a end
+ return a:gsub(":","_")
+ end)
+end
+
+local function fix_id_colons(s)
+ return s:gsub('(id=".-")', function(a)
+ return a:gsub(":", "_")
+ end)
+end
+
+return function(s)
+ return fix_id_colons(fix_href_colons(s))
+end
diff --git a/support/make4ht/filters/make4ht-fixligatures.lua b/support/make4ht/filters/make4ht-fixligatures.lua
new file mode 100644
index 0000000000..01b31ed306
--- /dev/null
+++ b/support/make4ht/filters/make4ht-fixligatures.lua
@@ -0,0 +1,16 @@
+-- fix ligatures
+-- replace ligatures produced by tex4ht with their components
+-- this prevents problems with some readers
+local gsub = unicode.utf8.gsub
+function filter(s)
+ local replaces = {
+ ["fi"] = "fi",
+ ["ffi"] = "ffi",
+ ["fl"] = "fl",
+ ["ffl"] = "ffl",
+ ["ff"] = "ff"
+ }
+ return gsub(s, "([fiffiflfflff])",function (x) return replaces[x] or x end)
+end
+
+return filter
diff --git a/support/make4ht/filters/make4ht-hruletohr.lua b/support/make4ht/filters/make4ht-hruletohr.lua
new file mode 100644
index 0000000000..a9c0c5e9df
--- /dev/null
+++ b/support/make4ht/filters/make4ht-hruletohr.lua
@@ -0,0 +1,11 @@
+-- hruletohr
+-- \hrule primitive is impossible to redefine catching all possible arguments
+-- with tex4ht, it is converted as series of underscores
+-- it seems that these underscores are always part of previous paragraph
+-- this assumption may be wrong, needs more real world testing
+
+local hruletohr = function(s)
+ return s:gsub("___+(.-)</p>","%1</p>\n<hr class=\"hrule\" />")
+end
+
+return hruletohr
diff --git a/support/make4ht/filters/make4ht-mathjaxnode.lua b/support/make4ht/filters/make4ht-mathjaxnode.lua
new file mode 100644
index 0000000000..2b453e4255
--- /dev/null
+++ b/support/make4ht/filters/make4ht-mathjaxnode.lua
@@ -0,0 +1,94 @@
+local mkutils = require "mkutils"
+-- other possible value is page2svg
+local mathnodepath = "mjpage"
+-- options for MathJax command
+local options = "--output CommonHTML"
+-- math fonts position
+-- don't alter fonts if not set
+local fontdir = nil
+-- if we copy fonts
+local fontdest = nil
+local fontformat = "woff"
+local cssfilename = "mathjax-chtml.css"
+
+local function compile(text)
+ local tmpfile = os.tmpname()
+ print("Compile using MathJax")
+ local command = mathnodepath .. " ".. options .. " > " .. tmpfile
+ print(command)
+ local commandhandle = io.popen(command,"w")
+ commandhandle:write(text)
+ commandhandle:close()
+ print("Result written to: ".. tmpfile)
+ local f = io.open(tmpfile)
+ local content = f:read("*all")
+ f:close()
+ os.remove(tmpfile)
+ return content
+end
+
+-- save the css code from the html page generated by MathJax
+local function extract_css(contents)
+ local css = ""
+ local filename = cssfilename
+ contents = contents:gsub('<style [^>]+>(.-)</style>', function(style)
+ -- replace only the style for mathjax
+ if style:match "%.mjx%-math" then
+ css = style
+ return '<link rel="stylesheet" type="text/css" href="'..filename ..'" />'
+ end
+ end)
+ -- local x = assert(io.open(file, "w"))
+ -- x:write(contents)
+ -- x:close()
+ return filename, contents, css
+end
+
+-- Update the paths to fonts to use the local versions
+local function use_fonts(css)
+ local family_pattern = "font%-family:%s*(.-);.-%/([^%/]+)%.".. fontformat
+ local family_build = "@font-face {font-family: %s; src: url('%s/%s.%s') format('%s')}"
+ local fontdir = fontdir:gsub("/$","")
+ css = css:gsub("(@font%-face%s*{.-})", function(face)
+ if not face:match("url%(") then return face end
+ -- print(face)
+ local family, filename = face:match(family_pattern)
+ print(family, filename)
+ local newfile = string.format("%s/%s.%s", fontdir, filename, fontformat)
+ Make:add_file(newfile)
+ return family_build:format(family, fontdir, filename, fontformat, fontformat)
+ -- return face
+ end)
+ return css
+end
+
+
+local function save_css(filename, css)
+ local f = io.open(filename, "w")
+ f:write(css)
+ f:close()
+end
+
+return function(text, arguments)
+ -- if arguments.prg then mathnodepath = arguments.prg end
+ local extoptions = mkutils.get_filter_settings "mathjaxnode" or {}
+ local arguments = arguments or {}
+ mathnodepath = arguments.prg or extoptions.prg or mathnodepath
+ options = arguments.options or extoptions.options or options
+ fontdir = arguments.fontdir or extoptions.fontdir or fontdir
+ -- the following ne is unused ATM
+ fontdest = arguments.fontdest or extoptions.fontdest or fontdest
+ fontformat = arguments.fontformat or extoptions.fontformat or fontformat
+ cssfilename = arguments.cssfilename or extoptions.cssfilename or cssfilename
+ local newtext = compile(text)
+ local cssfile, newtext, css = extract_css(newtext)
+ -- use local font files if fontdir is present
+ if fontdir then
+ css = use_fonts(css)
+ end
+ save_css(cssfile, css)
+ Make:add_file(cssfile)
+ -- print(css)
+ print(cssfile)
+ return newtext
+end
diff --git a/support/make4ht/filters/make4ht-odttemplate.lua b/support/make4ht/filters/make4ht-odttemplate.lua
new file mode 100644
index 0000000000..c323d206c1
--- /dev/null
+++ b/support/make4ht/filters/make4ht-odttemplate.lua
@@ -0,0 +1,60 @@
+local mkutils = require "mkutils"
+local zip = require "zip"
+local domobject = require "luaxml-domobject"
+
+
+local function get_template_filename(settings)
+ -- either get the template odt filename from tex4ht.sty options (make4ht filename.tex "odttemplate=test.odt")
+ local tex4ht_settings = settings.tex4ht_sty_par
+ local templatefile = tex4ht_settings:match("odttemplate=([^%,]+)")
+ if templatefile then return templatefile end
+ -- read the template odt filename from settings
+ local filtersettings = get_filter_settings "odttemplate"
+ return settings.template or filtersettings.template
+end
+
+local function join_styles(old, new)
+ local old_dom = domobject.parse(old)
+ local new_dom = domobject.parse(new)
+
+ local template_styles = {}
+ local template_obj -- <office:styles> element, we will add new styles from the generated ODT here
+
+ -- detect style names in the template file and save them in a table for easy accesss
+ for _, style in ipairs(new_dom:query_selector("office|styles *")) do
+ template_obj = template_obj or style:get_parent()
+ local name = style:get_attribute("style:name") -- get the <office:styles> element
+ if name then
+ template_styles[name] = true
+ end
+ end
+
+ -- process the generated styles and add ones not used in the template
+ for _, style in ipairs(old_dom:query_selector("office|styles *")) do
+ local name = style:get_attribute("style:name")
+ if name and not template_styles[name] then
+ template_obj:add_child_node(style)
+ end
+ end
+
+ -- return template with additional styles from the generated file
+ return new_dom:serialize()
+end
+
+return function(content, settings)
+ -- use settings added from the Make:match, or default settings saved in Make object
+ local templatefile = get_template_filename(settings)
+ -- don't do anything if the template file doesn't exist
+ if not templatefile or not mkutils.file_exists(templatefile) then return content end
+ local odtfile = zip.open(templatefile)
+ if odtfile then
+ local stylesfile = odtfile:open("styles.xml")
+ -- just break if the styles cannot be found
+ if not stylesfile then return content end
+ local styles = stylesfile:read("*all")
+ local newstyle = join_styles(content, styles)
+ return newstyle
+ end
+ -- just return content in the case of problems
+ return content
+end
diff --git a/support/make4ht/filters/make4ht-staticsite.lua b/support/make4ht/filters/make4ht-staticsite.lua
new file mode 100644
index 0000000000..6378f0440a
--- /dev/null
+++ b/support/make4ht/filters/make4ht-staticsite.lua
@@ -0,0 +1,98 @@
+local domobj = require "luaxml-domobject"
+-- save the header settings in YAML format
+local function make_yaml(tbl, level)
+ local t = {}
+ local level = level or 0
+ local indent = string.rep(" ", level)
+ -- indentation for multilen strings
+ local str_indent = string.rep(" ", level + 1)
+ for k,v in pairs(tbl) do
+ if type(v)=="string" then
+ -- detect multiline strings
+ if v:match("\n") then
+ table.insert(t, string.format(indent .. "%s: |", k))
+ table.insert(t, str_indent .. (v:gsub("\n", "\n".. str_indent)))
+ else
+ v = v:gsub("'", "''")
+ table.insert(t, string.format(indent .. "%s: '%s'", k,v))
+ end
+ elseif type(v) == "table" then
+ table.insert(t,string.format(indent .. "%s:", k))
+ -- we need to differently process array and hash table
+ -- we don't support mixing types
+ if #v > 0 then
+ for x,y in ipairs(v) do
+ if type(y) == "string" then
+ -- each string can be printed on it's own line
+ table.insert(t, indent .. string.format("- '%s'", y))
+ else
+ -- subtables need to be indented
+ -- table.insert(t, indent .. "-")
+ local subtable = make_yaml(y, level + 1)
+ -- we must insert dash at a correct place
+ local insert_dash = subtable:gsub("^(%s*)%s%s", "%1- ")
+ table.insert(t, insert_dash)
+ end
+ end
+ else
+ -- print indented table
+ table.insert(t, make_yaml(v,level + 1))
+ end
+ else
+ -- convert numbers and other values to string
+ table.insert(t, string.format(indent .. "%s: %s", k,tostring(v)))
+ end
+
+ end
+ return table.concat(t, "\n")
+end
+
+local function update_properties(properties, dom)
+ -- enable properties update from the config or build file
+ local settings = get_filter_settings "staticsite" or {}
+ local header = settings.header or {}
+ for field, rule in pairs(header) do
+ -- it is possible to pass function as a rule, it will be executed with properties as a parameter
+ if type(rule) == "function" then
+ properties[field] = rule(properties, dom)
+ else
+ -- otherwise set properties
+ properties[field] = rule
+ end
+ end
+ return properties
+end
+
+local function get_header(tbl)
+ local yaml = make_yaml(tbl)
+ return "---\n".. yaml.. "\n---\n"
+end
+
+return function(s,par)
+ print(os.getenv "blog_home")
+ local dom = domobj.parse(s)
+ local properties = {}
+ local head = dom:query_selector("head")[1]
+ properties.title = head:query_selector("title")[1]:get_text()
+ local styles = {}
+ for _, link in ipairs(head:query_selector("link")) do
+ local typ = link:get_attribute("type")
+ if typ == "text/css" then
+ table.insert(styles, link:get_attribute("href"))
+ end
+ end
+ properties.styles = styles
+ local metas = {}
+ for _, meta in ipairs(head:query_selector("meta")) do
+ print(meta:serialize())
+ table.insert(metas, {charset= meta:get_attribute("charset"), content = meta:get_attribute("content"), property = meta:get_attribute("property"), name = meta:get_attribute("name")})
+ end
+ properties.meta = metas
+ properties = update_properties(properties, dom)
+
+
+ local body = dom:query_selector("body")[1]
+ print(get_header(properties))
+ -- return s
+ return get_header(properties) .. body:serialize():gsub("<body.->", ""):gsub("</body>", "")
+end
diff --git a/support/make4ht/filters/make4ht-svg-height.lua b/support/make4ht/filters/make4ht-svg-height.lua
new file mode 100644
index 0000000000..44e98748ea
--- /dev/null
+++ b/support/make4ht/filters/make4ht-svg-height.lua
@@ -0,0 +1,43 @@
+
+-- Make:image("svg$", "dvisvgm -n -a -p ${page} -b preview -c 1.4,1.4 -s ${source} > ${output}")
+
+local max = function(a,b)
+ return a > b and a or b
+end
+
+local function get_height(svg)
+ local height = svg:match("height='([0-9%.]+)pt'")
+ return tonumber(height)
+end
+
+local function get_max_height(path,max_number)
+ local coordinates = {}
+ for number in path:gmatch("(%-?[0-9%.]+)") do
+ table.insert(coordinates, tonumber(number))
+ end
+ for i = 2, #coordinates, 2 do
+ max_number = max(max_number, coordinates[i])
+ end
+ return max_number
+end
+
+local function update_height(svg, height)
+ return svg:gsub("height='.-pt'", "height='"..height .."pt'")
+end
+
+-- we need to fix the svg height
+return function(svg)
+ local max_height = 0
+ local height = get_height(svg)
+ for path in svg:gmatch("path d='([^']+)'") do
+ -- find highest height in all paths in the svg file
+ max_height = get_max_height(path, max_height)
+ end
+ -- update the height only if the max_height is larger than height set in the SVG file
+ print(max_height, height)
+ if max_height > height then
+ svg = update_height(svg, max_height)
+ end
+ return svg
+end
+
diff --git a/support/make4ht/formats/docbook.lua b/support/make4ht/formats/docbook.lua
new file mode 100644
index 0000000000..9e5ffe9488
--- /dev/null
+++ b/support/make4ht/formats/docbook.lua
@@ -0,0 +1,38 @@
+local M = {}
+local mkutils = require "mkutils"
+local lfs = require "lfs"
+local os = require "os"
+local kpse = require "kpse"
+local filter = require "make4ht-filter"
+local domfilter = require "make4ht-domfilter"
+local xtpipeslib = require "make4ht-xtpipes"
+
+function M.prepare_parameters(settings, extensions)
+ settings.tex4ht_sty_par = settings.tex4ht_sty_par ..",docbook"
+ settings = mkutils.extensions_prepare_parameters(extensions, settings)
+ return settings
+end
+
+local move_matches = xtpipeslib.move_matches
+
+-- call xtpipes from Lua
+local function call_xtpipes(make)
+ -- we must find root of the TeX distribution
+ local selfautoparent = xtpipeslib.get_selfautoparent()
+
+ if selfautoparent then
+ local matchfunction = xtpipeslib.get_xtpipes(selfautoparent)
+ make:match("xml$", matchfunction)
+ move_matches(make)
+ else
+ print "Cannot locate xtpipes. Try to set TEXMFROOT variable to a root directory of your TeX distribution"
+ end
+end
+
+function M.modify_build(make)
+ -- use xtpipes to fix some common docbook issues
+ call_xtpipes(make)
+ return make
+end
+
+return M
diff --git a/support/make4ht/formats/html5.lua b/support/make4ht/formats/html5.lua
new file mode 100644
index 0000000000..633a45e7f3
--- /dev/null
+++ b/support/make4ht/formats/html5.lua
@@ -0,0 +1,17 @@
+local M = {}
+
+local mkutils = require "mkutils"
+
+function M.prepare_extensions(extensions)
+ -- return mkutils.add_extensions("+common_domfilters", extensions)
+ return extensions --mkutils.add_extensions("+tidy", extensions)
+end
+
+function M.prepare_parameters(parameters,extensions)
+ parameters.tex4ht_sty_par = parameters.tex4ht_sty_par .. ",html5"
+ parameters = mkutils.extensions_prepare_parameters(extensions,parameters)
+ return parameters
+end
+
+
+return M
diff --git a/support/make4ht/formats/odt.lua b/support/make4ht/formats/odt.lua
new file mode 100644
index 0000000000..64b8793e2a
--- /dev/null
+++ b/support/make4ht/formats/odt.lua
@@ -0,0 +1,223 @@
+local M = {}
+local mkutils = require "mkutils"
+local lfs = require "lfs"
+local os = require "os"
+local kpse = require "kpse"
+local filter = require "make4ht-filter"
+local domfilter = require "make4ht-domfilter"
+local xtpipeslib = require "make4ht-xtpipes"
+
+
+function M.prepare_parameters(settings, extensions)
+ settings.tex4ht_sty_par = settings.tex4ht_sty_par ..",ooffice"
+ settings.tex4ht_par = settings.tex4ht_par .. " ooffice/! -cmozhtf"
+ -- settings.t4ht_par = settings.t4ht_par .. " -cooxtpipes -coo "
+ -- settings.t4ht_par = settings.t4ht_par .. " -cooxtpipes "
+ settings = mkutils.extensions_prepare_parameters(extensions, settings)
+ return settings
+end
+
+-- object for working with the ODT file
+local Odtfile = {}
+Odtfile.__index = Odtfile
+
+Odtfile.new = function(archivename)
+ local self = setmetatable({}, Odtfile)
+ -- create temporary directory
+ local tmpname = os.tmpname()
+ tmpname = tmpname:match("([a-zA-Z0-9_%-]+)$")
+ local status, msg = lfs.mkdir(tmpname)
+ if not status then return nil, msg end
+ -- make picture dir
+ lfs.mkdir(tmpname .. "/Pictures")
+ self.archivelocation = tmpname
+ self.name = archivename
+ return self
+end
+
+function Odtfile:copy(src, dest)
+ mkutils.cp(src, self.archivelocation .. "/" .. dest)
+end
+
+function Odtfile:move(src, dest)
+ mkutils.mv(src, self.archivelocation .. "/" .. dest)
+end
+
+function Odtfile:create_dir(dir)
+ local currentdir = lfs.currentdir()
+ lfs.chdir(self.archivelocation)
+ lfs.mkdir(dir)
+ lfs.chdir(currentdir)
+end
+
+function Odtfile:make_mimetype()
+ self.mimetypename = "mimetype"
+ local m = io.open(self.mimetypename, "w")
+ m:write("application/vnd.oasis.opendocument.text")
+ m:close()
+end
+
+function Odtfile:remove_mimetype()
+ os.remove(self.mimetypename)
+end
+
+
+function Odtfile:pack()
+ local currentdir = lfs.currentdir()
+ local zip_command = mkutils.find_zip()
+ lfs.chdir(self.archivelocation)
+ -- make temporary mime type file
+ self:make_mimetype()
+ os.execute(zip_command .. " -q0X " .. self.name .. " " .. self.mimetypename)
+ -- remove it, so the next command doesn't overwrite it
+ self:remove_mimetype()
+ os.execute(zip_command .." -r " .. self.name .. " *")
+ lfs.chdir(currentdir)
+ mkutils.cp(self.archivelocation .. "/" .. self.name, self.name)
+ mkutils.delete_dir(self.archivelocation)
+end
+
+-- escape string to be used in the gsub search
+local function escape_file(filename)
+ local quotepattern = '(['..("%^$().[]*+-?"):gsub("(.)", "%%%1")..'])'
+ return filename:gsub(quotepattern, "%%%1")
+end
+
+local move_matches = xtpipeslib.move_matches
+
+-- call xtpipes from Lua
+local function call_xtpipes(make)
+ -- we must find root of the TeX distribution
+ local selfautoparent = xtpipeslib.get_selfautoparent()
+
+ if selfautoparent then
+ local matchfunction = xtpipeslib.get_xtpipes(selfautoparent)
+ make:match("4oo", matchfunction)
+ make:match("4om", matchfunction)
+ -- move last match to a first place
+ -- we need to move last two matches, for 4oo and 4om files
+ move_matches(make)
+ move_matches(make)
+ else
+ print "Cannot locate xtpipes. Try to set TEXMFROOT variable to a root directory of your TeX distribution"
+ end
+end
+
+-- sort output files according to their extensions
+local function prepare_output_files(lgfiles)
+ local groups = {}
+ for _, name in ipairs(lgfiles) do
+ local basename, extension = name:match("(.-)%.([^%.]+)$")
+ local group = groups[extension] or {}
+ table.insert(group, basename)
+ groups[extension] = group
+ print(basename, extension)
+ end
+ return groups
+end
+
+-- execute function on all files in the group
+-- function fn takes current filename and table with various attributes
+local function exec_group(groups, name, fn)
+ for _, basename in ipairs(groups[name] or {}) do
+ fn{basename = basename, extension=name, filename = basename .. "." .. name}
+ end
+end
+
+function M.modify_build(make)
+ local executed = false
+ -- execute xtpipes from the build file, instead of t4ht. this fixes issues with wrong paths
+ -- expanded in tex4ht.env in Miktex or Debian
+ call_xtpipes(make)
+ -- fix the image dimensions wrongly set by xtpipes
+ local domfilters = domfilter {"t4htlinks"}
+ make:match("4oo$", domfilters)
+ -- execute it before xtpipes, because we don't want xtpipes to mess with t4htlink elements
+ move_matches(make)
+ -- convert XML entities for Unicode characters produced by Xtpipes to characters
+ local fixentities = filter {"entities-to-unicode"}
+ make:match("4oo", fixentities)
+ make:match("4om", fixentities)
+ -- execute fixentities as first
+ move_matches(make)
+ move_matches(make)
+
+ -- build the ODT file. This match must be executed as a last one
+ -- this will be executed as a first match, just to find the last filename
+ -- in the lgfile
+ make:match(".*", function()
+ -- execute it only once
+ if not executed then
+ -- this is list of processed files
+ local lgfiles = make.lgfile.files
+ -- find the last file and escape it so it can be used
+ -- in filename match
+ local lastfile = escape_file(lgfiles[#lgfiles]) .."$"
+ -- make match for the last file
+ -- odt packing will be done here
+ make:match(lastfile, function()
+ local groups = prepare_output_files(make.lgfile.files)
+ local basename = groups.odt[1]
+ local odtname = basename .. ".odt"
+ local odt,msg = Odtfile.new(odtname)
+ if not odt then
+ print("Cannot create ODT file: " .. msg)
+ end
+ -- helper function for simple file moving
+ local function move_file(group, dest)
+ exec_group(groups, group, function(par)
+ odt:move("${filename}" % par, dest)
+ end)
+ end
+
+ -- the document text
+ exec_group(groups, "4oo", function(par)
+ odt:move("${filename}" % par, "content.xml")
+ odt:create_dir("Pictures")
+ end)
+
+ -- manifest
+ exec_group(groups, "4of", function(par)
+ odt:create_dir("META-INF")
+ odt:move("${filename}" % par, "META-INF/manifest.xml")
+ end)
+
+ -- math
+ exec_group(groups, "4om", function(par)
+ odt:create_dir(par.basename)
+ odt:move("${filename}" % par, "${basename}/content.xml" % par)
+ -- copy the settings file to math subdir
+ local settings = groups["4os"][1]
+ odt:copy(settings .. ".4os", "${basename}/settings.xml" % par)
+ end)
+
+ -- these files are created only once, so it doesn't matter that they are
+ -- copied to one file
+ move_file("4os", "settings.xml")
+ move_file("4ot", "meta.xml")
+ move_file("4oy", "styles.xml")
+
+ -- pictures
+ exec_group(groups, "4og", function(par)
+ -- add support for images in the TEXMF tree
+ if not mkutils.file_exists(par.basename) then
+ par.basename = kpse.find_file(par.basename, "graphic/figure")
+ if not par.basename then return nil, "Cannot find picture" end
+ end
+ -- the Pictues dir is flat, without subdirs
+ odt:copy("${basename}" % par, "Pictures")
+ end)
+
+ -- remove some spurious file
+ exec_group(groups, "4od", function(par)
+ os.remove(par.filename)
+ end)
+
+ odt:pack()
+ end)
+ end
+ executed = true
+ end)
+ return make
+end
+return M
diff --git a/support/make4ht/formats/tei.lua b/support/make4ht/formats/tei.lua
new file mode 100644
index 0000000000..f81953a8b3
--- /dev/null
+++ b/support/make4ht/formats/tei.lua
@@ -0,0 +1,14 @@
+local M = {}
+local xtpipeslib = require "make4ht-xtpipes"
+
+function M.prepare_parameters(settings, extensions)
+ settings.tex4ht_sty_par = settings.tex4ht_sty_par ..",tei"
+ settings = mkutils.extensions_prepare_parameters(extensions, settings)
+ return settings
+end
+
+function M.prepare_extensions(extensions)
+ return extensions
+end
+
+return M
diff --git a/support/make4ht/formats/xhtml.lua b/support/make4ht/formats/xhtml.lua
new file mode 100644
index 0000000000..4a4ac6689c
--- /dev/null
+++ b/support/make4ht/formats/xhtml.lua
@@ -0,0 +1,16 @@
+local M = {}
+
+local mkutils = require "mkutils"
+
+function M.prepare_extensions(extensions)
+ -- return mkutils.add_extensions("+common_domfilters", extensions)
+ return extensions
+end
+
+function M.prepare_parameters(parameters,extensions)
+ parameters = mkutils.extensions_prepare_parameters(extensions,parameters)
+ return parameters
+end
+
+
+return M
diff --git a/support/make4ht/lapp-mk4.lua b/support/make4ht/lapp-mk4.lua
new file mode 100644
index 0000000000..89d5c7061a
--- /dev/null
+++ b/support/make4ht/lapp-mk4.lua
@@ -0,0 +1,329 @@
+-- lapp.lua
+-- Simple command-line parsing using human-readable specification
+-----------------------------
+--~ -- args.lua
+--~ local args = require ('lapp') [[
+--~ Testing parameter handling
+--~ -p Plain flag (defaults to false)
+--~ -q,--quiet Plain flag with GNU-style optional long name
+--~ -o (string) Required string option
+--~ -n (number) Required number option
+--~ -s (default 1.0) Option that takes a number, but will default
+--~ <start> (number) Required number argument
+--~ <input> (default stdin) A parameter which is an input file
+--~ <output> (default stdout) One that is an output file
+--~ ]]
+--~ for k,v in pairs(args) do
+--~ print(k,v)
+--~ end
+-------------------------------
+--~ > args -pq -o help -n 2 2.3
+--~ input file (781C1B78)
+--~ p true
+--~ s 1
+--~ output file (781C1B98)
+--~ quiet true
+--~ start 2.3
+--~ o help
+--~ n 2
+--------------------------------
+
+lapp = {}
+
+local append = table.insert
+local usage
+local open_files = {}
+local parms = {}
+local aliases = {}
+local parmlist = {}
+
+local filetypes = {
+ stdin = {io.stdin,'file-in'}, stdout = {io.stdout,'file-out'},
+ stderr = {io.stderr,'file-out'}
+}
+
+local function quit(msg,no_usage)
+ if msg then
+ io.stderr:write(msg..'\n\n')
+ end
+ if not no_usage then
+ io.stderr:write(usage)
+ end
+ os.exit(1);
+end
+
+local function help()
+ print(usage)
+ os.exit()
+end
+
+local function version()
+ return {version = true}
+end
+
+local function error(msg,no_usage)
+ quit(arg[0]:gsub('.+[\\/]','')..':'..msg,no_usage)
+end
+
+local function ltrim(line)
+ return line:gsub('^%s*','')
+end
+
+local function rtrim(line)
+ return line:gsub('%s*$','')
+end
+
+local function trim(s)
+ return ltrim(rtrim(s))
+end
+
+local function open (file,opt)
+ local val,err = io.open(file,opt)
+ if not val then error(err,true) end
+ append(open_files,val)
+ return val
+end
+
+local function xassert(condn,msg)
+ if not condn then
+ error(msg)
+ end
+end
+
+local function range_check(x,min,max,parm)
+ xassert(min <= x and max >= x,parm..' out of range')
+end
+
+local function xtonumber(s)
+ local val = tonumber(s)
+ if not val then error("unable to convert to number: "..s) end
+ return val
+end
+
+local function is_filetype(type)
+ return type == 'file-in' or type == 'file-out'
+end
+
+local types = {}
+
+local function convert_parameter(ps,val)
+ if ps.converter then
+ val = ps.converter(val)
+ end
+ if ps.type == 'number' then
+ val = xtonumber(val)
+ elseif is_filetype(ps.type) then
+ val = open(val,(ps.type == 'file-in' and 'r') or 'w' )
+ elseif ps.type == 'boolean' then
+ val = true
+ end
+ if ps.constraint then
+ ps.constraint(val)
+ end
+ return val
+end
+
+function lapp.add_type (name,converter,constraint)
+ types[name] = {converter=converter,constraint=constraint}
+end
+
+local function force_short(short)
+ xassert(#short==1,short..": short parameters should be one character")
+end
+
+function process_options_string(str)
+ local res = {}
+ local varargs
+
+ local function check_varargs(s)
+ local res,cnt = s:gsub('%.%.%.$','')
+ varargs = cnt > 0
+ return res
+ end
+
+ local function set_result(ps,parm,val)
+ if not ps.varargs then
+ res[parm] = val
+ else
+ if not res[parm] then
+ res[parm] = { val }
+ else
+ append(res[parm],val)
+ end
+ end
+ end
+
+ usage = str
+
+ for line in str:gmatch('([^\n]*)\n') do
+ local optspec,optparm,i1,i2,defval,vtype,constraint
+ line = ltrim(line)
+ -- flags: either -<short> or -<short>,<long>
+ i1,i2,optspec = line:find('^%-(%S+)')
+ if i1 then
+ optspec = check_varargs(optspec)
+ local short,long = optspec:match('([^,]+),(.+)')
+ if short then
+ optparm = long:sub(3)
+ aliases[short] = optparm
+ force_short(short)
+ else
+ optparm = optspec
+ force_short(optparm)
+ end
+ else -- is it <parameter_name>?
+ i1,i2,optparm = line:find('(%b<>)')
+ if i1 then
+ -- so <input file...> becomes input_file ...
+ optparm = check_varargs(optparm:sub(2,-2)):gsub('%A','_')
+ append(parmlist,optparm)
+ end
+ end
+ if i1 then -- this is not a pure doc line
+ local last_i2 = i2
+ local sval
+ line = ltrim(line:sub(i2+1))
+ -- do we have (default <val>) or (<type>)?
+ i1,i2,typespec = line:find('^%s*(%b())')
+ if i1 then
+ typespec = trim(typespec:sub(2,-2)) -- trim the parens and any space
+ sval = typespec:match('default%s+(.+)')
+ if sval then
+ local val = tonumber(sval)
+ if val then -- we have a number!
+ defval = val
+ vtype = 'number'
+ elseif filetypes[sval] then
+ local ft = filetypes[sval]
+ defval = ft[1]
+ vtype = ft[2]
+ else
+ defval = sval
+ vtype = 'string'
+ end
+ else
+ local min,max = typespec:match '([^%.]+)%.%.(.+)'
+ if min then -- it's (min..max)
+ vtype = 'number'
+ min = xtonumber(min)
+ max = xtonumber(max)
+ constraint = function(x)
+ range_check(x,min,max,optparm)
+ end
+ else -- () just contains type of required parameter
+ vtype = typespec
+ end
+ end
+ else -- must be a plain flag, no extra parameter required
+ defval = false
+ vtype = 'boolean'
+ end
+ local ps = {
+ type = vtype,
+ defval = defval,
+ required = defval == nil,
+ comment = line:sub((i2 or last_i2)+1) or optparm,
+ constraint = constraint,
+ varargs = varargs
+ }
+ if types[vtype] then
+ local converter = types[vtype].converter
+ if type(converter) == 'string' then
+ ps.type = converter
+ else
+ ps.converter = converter
+ end
+ ps.constraint = types[vtype].constraint
+ end
+ parms[optparm] = ps
+ end
+ end
+ -- cool, we have our parms, let's parse the command line args
+ local iparm = 1
+ local iextra = 1
+ local i = 1
+ local parm,ps,val
+ while i <= #arg do
+ -- look for a flag, -<short flags> or --<long flag>
+ local i1,i2,dash,parmstr = arg[i]:find('^(%-+)(%a.*)')
+ if i1 then -- we have a flag
+ if #dash == 2 then -- long option
+ parm = parmstr
+ else -- short option
+ if #parmstr == 1 then
+ parm = parmstr
+ else -- multiple flags after a '-',?
+ parm = parmstr:sub(1,1)
+ if parmstr:find('^%a%d+') then
+ -- a short option followed by a digit? (exception for AW ;))
+ -- push ahead into the arg array
+ table.insert(arg,i+1,parmstr:sub(2))
+ else
+ -- push multiple flags into the arg array!
+ for k = 2,#parmstr do
+ table.insert(arg,i+k-1,'-'..parmstr:sub(k,k))
+ end
+ end
+ end
+ end
+ if parm == 'h' or parm == 'help' then
+ help()
+ end
+ if parm == "v" or parm == "version" then
+ return version()
+ end
+ if aliases[parm] then parm = aliases[parm] end
+ ps = parms[parm]
+ if not ps then error("unrecognized parameter: "..parm) end
+ if ps.type ~= 'boolean' then -- we need a value! This should follow
+ val = arg[i+1]
+ i = i + 1
+ xassert(val,parm.." was expecting a value")
+ end
+ else -- a parameter
+ parm = parmlist[iparm]
+ if not parm then
+ -- extra unnamed parameters are indexed starting at 1
+ parm = iextra
+ iextra = iextra + 1
+ ps = { type = 'string' }
+ else
+ ps = parms[parm]
+ end
+ if not ps.varargs then
+ iparm = iparm + 1
+ end
+ val = arg[i]
+ end
+ ps.used = true
+ val = convert_parameter(ps,val)
+ set_result(ps,parm,val)
+ if is_filetype(ps.type) then
+ set_result(ps,parm..'_name',arg[i])
+ end
+ if lapp.callback then
+ lapp.callback(parm,arg[i],res)
+ end
+ i = i + 1
+ end
+ -- check unused parms, set defaults and check if any required parameters were missed
+ for parm,ps in pairs(parms) do
+ if not ps.used then
+ if ps.required then error("missing required parameter: "..parm) end
+ set_result(ps,parm,ps.defval)
+ end
+ end
+ return res
+end
+
+setmetatable(lapp, {
+ __call = function(tbl,str) return process_options_string(str) end,
+ __index = {
+ open = open,
+ quit = quit,
+ error = error,
+ assert = xassert,
+ }
+})
+
+return lapp
diff --git a/support/make4ht/make4ht b/support/make4ht/make4ht
new file mode 100755
index 0000000000..564bc6e7a0
--- /dev/null
+++ b/support/make4ht/make4ht
@@ -0,0 +1,110 @@
+#!/usr/bin/env texlua
+-- Package make4ht. Author Michal Hoftich <michal.h21@gmail.com>
+-- This package is subject of LPPL license, version 1.3
+kpse.set_program_name("luatex")
+
+
+
+local make4ht = require("make4ht-lib")
+local lapp = require("lapp-mk4")
+local mkutils = require("mkutils")
+local mkparams = require("mkparams")
+local mk_config = require("make4ht-config")
+-- args string is here just as sample, we dont pass it it to
+-- mkparams.get_args() so default args string is used
+local args = [[
+make4ht - build system for tex4ht
+Usage:
+make4ht [options] filename ["tex4ht.sty op." "tex4ht op." "t4ht op" "latex op"]
+-c,--config (default xhtml) Custom config file
+-d,--output-dir (default nil) Output directory
+-l,--lua Use lualatex for document compilation
+-s,--shell-escape Enables running external programs from LaTeX
+-u,--utf8 For output documents in utf8 encoding
+-x,--xetex Use xelatex for document compilation
+<filename> (string) Input file name
+]]
+
+-- set version number. the template should be replaced by the
+-- actual version number by the build script
+local version = "v0.2g"
+mkparams.version_number = version
+
+local args = mkparams.get_args()
+
+local parameters = mkparams.process_args(args)
+
+local mode = parameters.mode
+local build_file = parameters.build_file
+
+-- handle output formats
+local allowed_output_formats = {xhtml = true, html5=true, odt = true, docbook=true, tei=true}
+-- formatter is Lua library which must provide at least prepare_parameters
+-- and process_build_sequence functions
+local formatter
+local output_format = parameters.output_format
+if allowed_output_formats[ output_format ] then
+ formatter = mkutils.load_output_format(output_format)
+else
+ -- load html5 as default output format
+ if output_format then
+ print("Cannot load output format: ".. output_format)
+ end
+ formatter = mkutils.load_output_format("html5")
+end
+-- find make4ht configuration file
+local configname = "make4ht"
+local conffile = mk_config.find_config(configname) or mk_config.find_xdg_config(configname)
+if conffile then
+ print("Using configuration file: " .. conffile)
+ mkutils.load_config(parameters, conffile)
+end
+local extensions = formatter.prepare_extensions(parameters.extensions)
+extensions = mkutils.load_extensions(extensions, output_format)
+
+
+
+-- run extensions with prepare_parameters function
+parameters = formatter.prepare_parameters(parameters,extensions)
+local make = mkutils.load_config(parameters, build_file)["Make"]
+make.params = parameters
+if make:length() < 1 then
+ if mode == "draft" then
+ make:htlatex()
+ else
+ make:htlatex()
+ make:htlatex()
+ make:htlatex()
+ end
+end
+
+
+if not args["no-tex4ht"] then
+ make:tex4ht()
+end
+
+local ext = args.xetex and "xdv" or "dvi"
+if #make.image_patterns > 0 then
+ make.params.t4ht_par = make.params.t4ht_par .. " -p"
+end
+make:t4ht {ext = ext}
+-- run extensions which modify the build sequence
+if #extensions > 0 then
+ make = mkutils.extensions_modify_build(extensions, make)
+end
+
+-- allow output formats to modify the build process at the end
+make = formatter.modify_build(make) or make
+
+
+make:match("tmp$", function() return false,"tmp file" end)
+make:match(".*",function(filename,par)
+ local outdir = '' --par["outdir"] and par["outdir"] .."/" or ''
+ if par['outdir'] ~= "" then outdir = par['outdir'] .. '/' end
+ print("outdir: "..outdir)
+ local outfilename = outdir .. filename
+ mkutils.copy(filename,outfilename)
+ return true
+end)
+
+make:run()
diff --git a/support/make4ht/make4ht-aeneas-config.lua b/support/make4ht/make4ht-aeneas-config.lua
new file mode 100644
index 0000000000..d0b4a1360d
--- /dev/null
+++ b/support/make4ht/make4ht-aeneas-config.lua
@@ -0,0 +1,215 @@
+local M = {}
+
+local mkutils = require "mkutils"
+
+local task_template = [[
+<task>
+ <task_language>${lang}</task_language>
+ <task_description>${file_desc}</task_description>
+ <task_custom_id>${file_id}</task_custom_id>
+ <is_text_file>${prefix}${html_file}</is_text_file>
+ <is_text_type>${text_type}</is_text_type>
+ <is_audio_file>${prefix}${audio_file}</is_audio_file>
+ <is_text_unparsed_id_sort>${id_sort}</is_text_unparsed_id_sort>
+ <is_text_unparsed_id_regex>${id_regex}</is_text_unparsed_id_regex>
+ <os_task_file_name>${sub_file}</os_task_file_name>
+ <os_task_file_format>${sub_format}</os_task_file_format>
+ <os_task_file_smil_page_ref>${html_file}</os_task_file_smil_page_ref>
+ <os_task_file_smil_audio_ref>${audio_file}</os_task_file_smil_audio_ref>
+</task>
+]]
+
+-- get html files
+local function get_html_files(config)
+ local config = config or {}
+ local files = {}
+ local filematch = config.file_match or "html$"
+ -- this is a trick to get list of files from the LG file
+ for _, file in ipairs(Make.lgfile.files) do
+ if file:match(filematch) then table.insert(files, file) end
+ end
+ return files
+end
+
+-- prepare filename for the audio
+local function get_audio_file(filename, config)
+ local extension = config.audio_extension or "mp3"
+ local base = mkutils.remove_extension(filename)
+ return base .. "." .. extension
+end
+
+local function get_sub_file(filename, config)
+ local extension = config.sub_format or "smil"
+ local base = mkutils.remove_extension(filename)
+ return base .. "." .. extension
+end
+
+
+-- create task record for each HTML file
+local function prepare_tasks(files, configuration)
+ local tasks = {}
+ -- the map can contain info for particular files, otherwise we will interfere default values
+ local map = configuration.map or {}
+ -- task_template should be configurable
+ local task_template = configuration.task_template or task_template
+ for i, filename in ipairs(files) do
+ local filemap = map[filename]
+ if filemap ~= false then
+ filemap = filemap or {}
+ local taskconfig = configuration
+ taskconfig.html_file = filename
+ taskconfig.prefix = filemap.prefix or configuration.prefix
+ taskconfig.file_desc = filemap.description or configuration.description .. " " .. i
+ taskconfig.file_id = filemap.id or filename:gsub("[%/%.]", "_")
+ taskconfig.text_type = filemap.text_type or configuration.text_type
+ taskconfig.audio_file = filemap.audio_file or get_audio_file(filename, configuration)
+ taskconfig.sub_file = filemap.sub_file or get_sub_file(filename, configuration)
+ taskconfig.id_sort= filemap.id_sort or configuration.id_sort
+ taskconfig.id_prefix = filemap.id_regex or configuration.id_regex
+ taskconfig.sub_format = filemap.sub_format or configuration.sub_format
+ tasks[#tasks+1] = task_template % taskconfig
+ Make:add_file(taskconfig.audio_file)
+ Make:add_file(taskconfig.sub_file)
+ end
+ end
+ return tasks --table.concat(tasks, "\n")
+end
+-- from https://www.readbeyond.it/aeneas/docs/clitutorial.html#xml-config-file-config-xml
+local config_template = [[
+<job>
+ <job_language>${lang}</job_language>
+ <job_description>${description}</job_description>
+ <tasks>
+ ${tasks}
+ </tasks>
+ <os_job_file_name>output_example4</os_job_file_name>
+ <os_job_file_container>zip</os_job_file_container>
+ <os_job_file_hierarchy_type>flat</os_job_file_hierarchy_type>
+ <os_job_file_hierarchy_prefix>${prefix}</os_job_file_hierarchy_prefix>
+</job>
+]]
+
+-- check if the config file exists
+local function is_config(filename)
+ return mkutils.file_exists(filename)
+end
+
+-- prepare Aeneas configuration
+local function prepare_configuration(parameters)
+ local config = parameters or {}
+ config.lang = parameters.lang
+ config.tasks = table.concat(prepare_tasks(parameters.files, config), "\n")
+ return config
+end
+
+-- write Aeneeas configuration file in the XML format
+local function write_config(filename, configuration)
+ local cfg = config_template % configuration
+ print(cfg)
+ local f = io.open(filename, "w")
+ f:write(cfg)
+ f:close()
+end
+
+
+local function make_default_options(options)
+ local configuration = {}
+ local par = get_filter_settings "aeneas-config"
+ configuration.lang = options.lang or par.lang or "en"
+ configuration.description = options.description or par.description or "Aeneas job"
+ configuration.map = options.map or par.map or {}
+ configuration.text_type = options.text_type or par.text_type or "unparsed"
+ configuration.id_sort = options.id_sort or par.id_sort or "numeric"
+ configuration.id_regex = options.id_regex or par.id_regex or par.id_prefix .. "[0-9]+"
+ configuration.sub_format = options.sub_format or par.sub_format or "smil"
+ configuration.prefix = options.prefix or par.prefix or "./"
+ configuration.config_name = options.config_name or par.config_name or "config.xml"
+ configuration.keep_config = options.keep_config or par.keep_config
+ return configuration
+end
+
+
+local function configure_job(options)
+ local configuration = make_default_options(options)
+ local config_name = configuration.config_name
+ -- prepare the configuration in every case
+ configuration.files = get_html_files()
+ local configuration = prepare_configuration(configuration)
+ -- write the configuration only if the config file doesn't exist
+ -- and keep_config option is set to true
+ if is_config(config_name) and configuration.keep_config==true then
+ else
+ write_config(config_name, configuration)
+ end
+end
+
+local function execute_job(options)
+ local par = get_filter_settings "aeneas-config"
+ local configuration = make_default_options(options)
+ configuration.files = get_html_files()
+ -- we need to configure prepare_tasks to return calls to aeneas task convertor
+ configuration.python = options.python or par.python or "python3"
+ configuration.module = options.module or par.module or "aeneas.tools.execute_task"
+ configuration.task_template = '${python} -m "${module}" "${audio_file}" "${html_file}" "is_text_type=${text_type}|os_task_file_smil_audio_ref=${audio_file}|os_task_file_smil_page_ref=${html_file}|task_language=${lang}|is_text_unparsed_id_sort=${id_sort}|is_text_unparsed_id_regex=${id_regex}|os_task_file_format=${sub_format}" "${sub_file}"'
+ local tasks = prepare_tasks(configuration.files, configuration)
+ -- execute the tasks
+ for _, v in ipairs(tasks) do
+ print("task", v)
+ local proc = io.popen(v, "r")
+ local result = proc:read("*all")
+ proc:close()
+ print(result)
+ end
+end
+
+-- the aeneas configuration must be executed at last processed file, after all filters
+-- have been executed
+local function get_last_lg_file()
+ local t = Make.lgfile.files
+ for i = #t, 1, -1 do
+ -- find last html file or the tmp file
+ local x = t[i]
+ if x:match "html$" or x:match "tmp$" then
+ return x
+ end
+ end
+ return t[#t]
+end
+
+-- write Aeneas job configuration file
+-- it doesn't execute Aeneas
+function M.write_job(par)
+ -- configuration table for Aeneas job
+ Make:match("tmp$", function()
+ configure_job(par)
+ end)
+end
+
+-- execute Aeneas directly
+function M.execute(par)
+ Make:match("tmp$", function(current_name)
+ -- there may be html files after the .tmp file
+ -- the aeneas must be executed after the Aeneas filter inserts the id
+ -- attributes, so it is necessary to execute this code as very last one
+ local last = get_last_lg_file()
+ -- execute the job if there are no HTML files after the tmp file
+ if current_name == last then
+ execute_job(par)
+ end
+ Make:match(last, function()
+ execute_job(par)
+ end)
+ end)
+end
+
+-- only register the audio and smil files as processed files
+function M.process_files(par)
+ Make:match("tmp$", function()
+ local configuration = make_default_options(par)
+ local files = get_html_files()
+ prepare_tasks(files, configuration)
+ end)
+end
+
+
+return M
diff --git a/support/make4ht/make4ht-config.lua b/support/make4ht/make4ht-config.lua
new file mode 100644
index 0000000000..310f5afd7b
--- /dev/null
+++ b/support/make4ht/make4ht-config.lua
@@ -0,0 +1,92 @@
+local m = {}
+
+local mkutils = require "mkutils"
+
+local file_exists = mkutils.file_exists
+-- function file_exists(name)
+-- local f=io.open(name,"r")
+-- if f~=nil then io.close(f) return true else return false end
+-- end
+
+
+local make_name = function(name)
+ return table.concat(name, "/")
+ -- return name:gsub("//","/")
+end
+
+-- find the config file in XDG_CONFIG_HOME or in the HOME directry
+-- the XDG tree is looked up first, the $HOME is used only when it cannot be
+-- find in the former
+local xdg_config = function(filename, xdg_config_name)
+ local dotfilename = "." .. filename
+ local xdg_config_name = xdg_config_name or "config.lua"
+ local xdg = os.getenv("XDG_CONFIG_HOME") or ((os.getenv("HOME") or "") .. "/.config")
+ local home = os.getenv("HOME") or os.getenv("USERPROFILE")
+ if xdg then
+ -- filename like ~/.config/make4ht/config.lua
+ local fn = make_name{ xdg ,filename , xdg_config_name }
+ if file_exists(fn) then
+ return fn
+ end
+ end
+ if home then
+ -- ~/.make4ht
+ local fn = make_name{ home, dotfilename }
+ if file_exists(fn) then
+ return fn
+ end
+ end
+end
+
+local find_config = function(filename)
+ local filename = "." .. filename
+ local current = lfs.currentdir()
+ local path = {}
+ current:gsub("([^/]+)", function(s) table.insert(path,s) end)
+ local begin = os.type == "windows" and "" or "/"
+ for i = #path, 1, -1 do
+ local fn =begin .. table.concat(path,"/") .. "/".. filename
+ -- print("testing",fn)
+ if file_exists(fn) then return fn end
+ table.remove(path)
+ end
+ return false
+end
+
+local function load_config(filename, default)
+ local default = default or {}
+ default.table = table
+ default.string = string
+ default.io = io
+ default.os = os
+ default.math = math
+ default.print = print
+ default.ipairs = ipairs
+ default.pairs = pairs
+ local f = io.open(filename, "r")
+ local contents = f:read("*all")
+ f:close()
+ load(contents,"sandbox config","bt", default)()
+ return default
+end
+
+--[[
+local function load_config(filename, default)
+ local default = default or {}
+ if ~file_exists(filename) then
+ return nil, "Cannot load config file "..filename
+ end
+ local section = "default"
+ local file = io.open(filename, "r")
+ if ~file then return nil, "Error opening config file"..filename end
+ for line in file:lines() do
+ local ts = line:match("")
+ end
+ file:close()
+end
+--]]
+
+m.find_config = find_config
+m.find_xdg_config = xdg_config
+m.load_config = load_config
+return m
diff --git a/support/make4ht/make4ht-doc.pdf b/support/make4ht/make4ht-doc.pdf
new file mode 100644
index 0000000000..0b7dd091ea
--- /dev/null
+++ b/support/make4ht/make4ht-doc.pdf
Binary files differ
diff --git a/support/make4ht/make4ht-doc.tex b/support/make4ht/make4ht-doc.tex
new file mode 100644
index 0000000000..6815e50606
--- /dev/null
+++ b/support/make4ht/make4ht-doc.tex
@@ -0,0 +1,26 @@
+\documentclass{ltxdoc}
+
+
+\usepackage[english]{babel}
+\usepackage{hyperref}
+\ifdefined\HCode
+\usepackage[T1]{fontenc}
+\usepackage[utf8]{inputenc}
+\else
+\usepackage{fontspec}
+\setmainfont{TeX Gyre Schola}
+\setmonofont[Scale=MatchLowercase]{Inconsolatazi4}
+\fi
+\usepackage{microtype}
+\providecommand\tightlist{\relax}
+
+\title{The \texttt{make4ht} build system}
+\author{Michal Hoftich\footnote{\url{michal.h21@gmail.com}}}
+\date{Version \version\\\gitdate}
+\begin{document}
+\maketitle
+\tableofcontents
+\input{readme}
+
+\input{changelog}
+\end{document}
diff --git a/support/make4ht/make4ht-dvireader.lua b/support/make4ht/make4ht-dvireader.lua
new file mode 100644
index 0000000000..6355650ef4
--- /dev/null
+++ b/support/make4ht/make4ht-dvireader.lua
@@ -0,0 +1,182 @@
+-- This is not actually full DVI reader. It just calculates hash for each page,
+-- so it can be detected if it changed between compilations and needs to be
+-- converted to image using Dvisvgm or Dvipng
+--
+-- information about DVI format is from here: https://web.archive.org/web/20070403030353/http://www.math.umd.edu/~asnowden/comp-cont/dvi.html
+--
+local M
+
+-- the file after post_post is filled with bytes 223
+local endfill = 223
+
+-- numbers of bytes for each data type in DVI file
+local int = 4
+local byte = 1
+local sixteen = 2
+
+local function read_char(str, pos)
+ if pos and pos > string.len(str) then return nil end
+ return string.sub(str, pos, pos + 1)
+end
+
+local function read_byte(str, pos)
+ return string.byte(read_char(str, pos))
+end
+
+-- DVI file format uses signed big endian integers. This code doesn't take into account
+-- the sign, so it will return incorrect result for negative numbers. It doesn't matter
+-- for the original purpose of this library, but it should be fixed for general use.
+local function read_integer(str, pos)
+ local first = read_byte(str, pos)
+ local num = first * (256 ^ 3)
+ num = read_byte(str, pos + 1) * (256 ^ 2) + num
+ num = read_byte(str, pos + 2) * 256 + num
+ num = read_byte(str, pos + 3) + num
+ return num
+end
+
+local function read_sixteen(str, pos)
+ local num = read_byte(str, pos) * 256
+ num = read_byte(str, pos + 1) + num
+ return num
+end
+
+-- select reader function with number of bytes of an argument
+local readers = {
+ [byte] = read_byte,
+ [int] = read_integer,
+ [sixteen] = read_sixteen
+}
+
+
+local opcodes = {
+ post_post = {
+ opcode = 249, args = {
+ {name="q", type = int}, -- postamble address
+ {name="i", type = byte}
+ }
+ },
+ post = {
+ opcode = 248,
+ args = {
+ {name="p", type = int}, -- address of the last page
+ {name="num", type = int},
+ {name="den", type = int},
+ {name="mag", type = int},
+ {name="l", type = int},
+ {name="u", type = int},
+ {name="s", type = sixteen},
+ {name="t", type = sixteen},
+ }
+ },
+ bop = {
+ opcode = 139,
+ args = {
+ {name="c0", type=int},
+ {name="c1", type=int},
+ {name="c2", type=int},
+ {name="c3", type=int},
+ {name="c4", type=int},
+ {name="c5", type=int},
+ {name="c6", type=int},
+ {name="c7", type=int},
+ {name="c8", type=int},
+ {name="c9", type=int},
+ {name="p", type=int}, -- previous page
+ }
+ }
+}
+
+local function read_arguments(str, pos, args)
+ local t = {}
+ for _, v in ipairs(args) do
+ local fn = readers[v.type]
+ t[v.name] = fn(str, pos)
+ -- seek the position. v.type contains size of the current data type in bytes
+ pos = pos + v.type
+ end
+ return t
+end
+
+local function read_opcode(opcode, str, pos)
+ local format = opcodes[opcode]
+ if not format then return nil, "Cannot find opcode format: " .. opcode end
+ -- check that opcode byte in the current position is the same as required opcode
+ local op = read_byte(str, pos)
+ if op ~= format.opcode then return nil, "Wrong opcode " .. op .. " at position " .. pos end
+ return read_arguments(str, pos+1, format.args)
+end
+
+-- find the postamble address
+local function get_postamble_addr(dvicontent)
+ local pos = string.len(dvicontent)
+ local last = read_char(dvicontent, pos)
+ -- skip endfill bytes at the end of file
+ while string.byte(last) == endfill do
+ pos = pos - 1
+ last = read_char(dvicontent, pos)
+ end
+ -- first read post_post to get address of the postamble
+ local post_postamble, msg = read_opcode("post_post", dvicontent, pos-5)
+ if not post_postamble then return nil, msg end
+ -- return the postamble address
+ return post_postamble.q + 1
+ -- return read_opcode("post", dvicontent, post_postamble.q + 1)
+
+end
+
+local function read_page(str, start, stop)
+ local function get_end_of_page(str, pos)
+ if read_byte(str, pos) == 140 then -- end of page
+ return pos
+ end
+ return get_end_of_page(str, pos - 1)
+ end
+ -- we reached the end of file
+ if start == 2^32-1 then return nil end
+ local current_page = read_opcode("bop", str, start + 1)
+ if not current_page then return nil end
+ local endofpage = get_end_of_page(str, stop)
+ -- get the page contents, but skip all parameters, because they can change
+ -- (especially pointer to the previous page)
+ local page = str:sub(start + 46, endofpage)
+ local page_obj = {
+ number = current_page.c0, -- the page number
+ hash = md5.sumhexa(page) -- hash the page contents
+ }
+ return page_obj, current_page.p, start
+end
+
+local function get_pages(dvicontent)
+ local pages = {}
+ local postamble_pos = get_postamble_addr(dvicontent)
+ local postamble = read_opcode("post", dvicontent, postamble_pos)
+ local next_page_pos = postamble.p
+ local page, previous_page = nil, postamble_pos
+ local page_sequence = {}
+ while next_page_pos do
+ page, next_page_pos, previous_page = read_page(dvicontent, next_page_pos, previous_page)
+ page_sequence[#page_sequence+1] = page
+ end
+
+ -- reorder pages
+ for _, v in ipairs(page_sequence) do
+ pages[v.number] = v.hash
+ end
+ return pages
+
+end
+
+-- if arg[1] then
+-- local f = io.open(arg[1], "r")
+-- local dvicontent = f:read("*all")
+-- f:close()
+-- local pages = get_pages(dvicontent)
+-- for k,v in pairs(pages) do
+-- print(k,v)
+-- end
+-- end
+
+return {
+ get_pages = get_pages
+}
diff --git a/support/make4ht/make4ht-filterlib.lua b/support/make4ht/make4ht-filterlib.lua
new file mode 100644
index 0000000000..3c2e63cf69
--- /dev/null
+++ b/support/make4ht/make4ht-filterlib.lua
@@ -0,0 +1,42 @@
+local M = {}
+
+-- the filter module must implement the load_filter function
+function M.load_filters(filters, load_filter)
+ local sequence = {}
+ if type(filters) == "string" then
+ table.insert(sequence,load_filter(filters))
+ elseif type(filters) == "table" then
+ for _,n in ipairs(filters) do
+ if type(n) == "string" then
+ table.insert(sequence,load_filter(n))
+ elseif type(n) == "function" then
+ table.insert(sequence, n)
+ end
+ end
+ elseif type(filters) == "function" then
+ table.insert(sequence, filters)
+ else
+ return false, "Argument to filter must be either\ntable with filter names, or single filter name"
+ end
+ return sequence
+end
+
+function M.load_input_file(filename)
+ if not filename then return false, "filters: no filename" end
+ local input = nil
+
+ if filename then
+ local file = io.open(filename,"r")
+ input = file:read("*all")
+ file:close()
+ end
+ return input
+end
+
+function M.save_input_file(filename, input)
+ local file = io.open(filename,"w")
+ file:write(input)
+ file:close()
+end
+
+return M
diff --git a/support/make4ht/make4ht-lib.lua b/support/make4ht/make4ht-lib.lua
new file mode 100644
index 0000000000..2ada398f86
--- /dev/null
+++ b/support/make4ht/make4ht-lib.lua
@@ -0,0 +1,226 @@
+-- Simple make system for tex4ht
+--kpse.set_program_name("luatex")
+-- module(...,package.seeall)
+local m = {}
+
+Make = {}
+--Make.params = {}
+Make.build_seq = {}
+-- Patterns for matching output filenames
+Make.matches = {}
+Make.image_patterns = {}
+Make.run_count = {}
+
+Make.add = function(self,name,fn,par,rep)
+ local par = par or {}
+ self.params = self.params or {}
+ Make[name] = function(self,p,typ)
+ local params = {}
+ for k,v in pairs(self.params) do params[k] = v end
+ for k,v in pairs(par) do params[k] = v; print("setting param "..k) end
+ local typ = typ or "make"
+ local p = p or {}
+ local fn = fn
+ for k,v in pairs(p) do
+ params[k]=v
+ print("Adding: ",k,v)
+ end
+ -- print( fn % params)
+ local command = {
+ name=name,
+ type=typ,
+ command = fn,
+ params = params,
+ repetition = rep
+ }
+ table.insert(self.build_seq,command)
+ end
+end
+
+Make.length = function(self)
+ return #self.build_seq
+end
+
+Make.match = function(self, pattern, command, params)
+ local params = params or {}
+ table.insert(self.matches,{pattern = pattern, command = command, params = params})
+end
+
+Make.run_command = function(self,filename,s)
+ local command = s.command
+ local params = s.params
+ params["filename"] = filename
+ print("parse_lg process file: "..filename)
+ --for k,v in pairs(params) do print(k..": "..v) end
+ if type(command) == "function" then
+ return command(filename,params)
+ elseif type(command) == "string" then
+ local run = command % params
+ print("Execute: " .. run)
+ return os.execute(run)
+ end
+ return false, "parse_lg: Command is not string or function"
+end
+
+Make.image = function(self, pattern, command, params)
+ local tab = {
+ pattern = pattern,
+ command = command,
+ params = params
+ }
+ table.insert(self.image_patterns, tab)
+end
+
+Make.image_convert = function(self, images)
+ local image_patterns = self.image_patterns or {}
+ for i, r in pairs(image_patterns) do
+ local p = self.params or {}
+ local v = r.params or {}
+ for k,v in pairs(v) do
+ p[k]= v
+ end
+ image_patterns[i].params = p
+ end
+ for _,i in ipairs(images) do
+ local output = i.output
+ for _, x in ipairs(image_patterns) do
+ local pattern = x.pattern
+ if output:match(pattern) then
+ local command = x.command
+ local p = x.params or {}
+ p.output = output
+ p.page= i.page
+ p.source = i.source
+ if type(command) == "function" then
+ command(p)
+ elseif type(command) == "string" then
+ local c = command % p
+ print("Make4ht convert: "..c)
+ os.execute(c)
+ end
+ break
+ end
+ end
+ end
+end
+
+Make.file_matches = function(self, files)
+ local statuses = {}
+ -- First make params for all matchers
+ for k,v in ipairs(self.matches) do
+ local v = self.matches[k].params or {}
+ local p = self.params or {}
+ for i,j in pairs(p) do
+ v[i] = j
+ end
+ self.matches[k].params = v
+ end
+ -- Loop over files, run command on matched
+ for _, file in ipairs(files)do
+ statuses[file] = {}
+ for _, s in ipairs(self.matches) do
+ local pattern= s.pattern
+ if file:match(pattern) then
+ local status, msg = self:run_command(file,s)
+ msg = msg or "No message given"
+ table.insert(statuses[file],status)
+ if status == false then
+ print(msg)
+ break
+ end
+ end
+ end
+ end
+ return statuses
+end
+
+-- add files from the mk4 file
+-- we must add them to the table generated from the lg file, so they can be processed later
+--
+Make.add_file = function(self, filename)
+ -- self.lgfile should be present, as it is created once the lg_file was parsed for the first time
+ local lg = self.lgfile or {}
+ local files = lg.files or {}
+ -- run filters on the file
+ local filtertable = {filename}
+ -- should we care about return status?
+ self:file_matches(filtertable)
+ -- break if the file is present already
+ -- start at the end, it it was added by a build file, the file will be likely at the end
+ for i = #files,1,-1 do
+ if files[i] == filename then return false, "File was already added" end
+ end
+ -- save the added file to the lg_file
+ table.insert(lg.files, filename)
+ self.lg = lg
+end
+
+Make.run = function(self)
+ local return_codes = {}
+ local params = self.params or {}
+ for _,v in ipairs(self.build_seq) do
+ --print("sekvence: "..v.name)
+ for p,n in pairs(v.params) do params[p] = n end
+ --for c,_ in pairs(params) do print("build param: "..c) end
+ if type(v.command)=="function" then
+ table.insert(return_codes,{name=v.name,status = v.command(params)})
+ elseif type(v.command) =="string" then
+ local command = v.command % params
+ -- Some commands should be executed only limited times, typicaly once
+ -- tex4ht or t4ht for example
+ local run_count = self.run_count[v.command] or 0
+ run_count = run_count + 1
+ self.run_count[v.command] = run_count
+ local repetition = v.repetition
+ if repetition and run_count > repetition then
+ print ("Make4ht: ".. command .." can be executed only "..repetition .."x")
+ else
+ print("Make4ht: " .. command)
+ local status = os.execute(command)
+ table.insert(return_codes,{name=v.name,status=status})
+ end
+ else
+ print("Unknown command type, must be string or function - " ..v.name..": "..type(v.command))
+ end
+ local correct_exit = params.correct_exit or nil
+ if correct_exit then
+ local last_return = return_codes[#return_codes] or {}
+ local current_status = last_return.status or 0
+ if current_status ~= correct_exit then
+ local last_name = last_return.name or "unknown"
+ print("Make4ht: Fatal error. Command "..last_name .." returned exit code "..current_status)
+ os.exit(1)
+ end
+ end
+ end
+ local lgfile = params.input and params.input .. ".lg" or nil
+ if lgfile then
+ self.lgfile = self.lgfile or mkutils.parse_lg(lgfile)
+ local lg = self.lgfile
+ -- First convert images from lg files
+ self:image_convert(lg["images"])
+ -- Then run file matchers on lg files and converted images
+ local files = lg["files"]
+ for _,v in ipairs(lg["images"]) do
+ local v = v.output
+ -- print(v)
+ table.insert(files,v)
+ end
+ self:file_matches(files)
+ else
+ print("No lg file. tex4ht run failed?")
+ end
+ return return_codes
+end
+
+m.Make = Make
+
+return m
+
+--[[Make:add("hello", "hello ${world}", {world = "world"})
+Make:add("ajaj", "ajaj")
+Make:hello()
+Make:hello{world="světe"}
+Make:hello()
+Make:run()
+--]]
diff --git a/support/make4ht/make4ht-xtpipes.lua b/support/make4ht/make4ht-xtpipes.lua
new file mode 100644
index 0000000000..78014baff1
--- /dev/null
+++ b/support/make4ht/make4ht-xtpipes.lua
@@ -0,0 +1,82 @@
+local M = {}
+
+-- find if tex4ht.jar exists in a path
+local function find_tex4ht_jar(path)
+ local jar_file = path .. "/tex4ht/bin/tex4ht.jar"
+ return mkutils.file_exists(jar_file)
+end
+
+-- return value of TEXMFROOT variable if it exists and if tex4ht.jar can be located inside
+local function get_texmfroot()
+ -- user can set TEXMFROOT environmental variable as the last resort
+ local root_directories = {kpse.var_value("TEXMFROOT"), kpse.var_value("TEXMFDIST"), os.getenv("TEXMFROOT")}
+ for _, root in ipairs(root_directories) do
+ if root then
+ if find_tex4ht_jar(root) then return root end
+ -- TeX live locates files in texmf-dist subdirectory, but Miktex doesn't
+ local path = root .. "/texmf-dist"
+ if find_tex4ht_jar(path) then return path end
+ end
+ end
+end
+
+-- Miktex doesn't seem to set TeX variables such as TEXMFROOT
+-- we will try to find the TeX root using trick with locating package in TeX root
+-- there is a danger that this file is located in TEXMFHOME, the location will fail then
+local function find_texmfroot()
+ local tex4ht_path = kpse.find_file("tex4ht.sty")
+ if tex4ht_path then
+ local path = tex4ht_path:gsub("/tex/generic/tex4ht/tex4ht.sty$","")
+ if find_tex4ht_jar(path) then return path end
+ end
+ return nil
+end
+
+function M.get_selfautoparent()
+ return get_texmfroot() or find_texmfroot()
+end
+
+function M.get_xtpipes(selfautoparent)
+ -- make pattern using TeX distro path
+ local pattern = string.format("java -classpath %s/tex4ht/bin/tex4ht.jar xtpipes -i %s/tex4ht/xtpipes/ -o ${outputfile} ${filename}", selfautoparent, selfautoparent)
+ -- call xtpipes on a temporary file
+ local matchfunction = function(filename)
+ -- move the matched file to a temporary file, xtpipes will write it back to the original file
+ local basename = mkutils.remove_extension(filename)
+ local tmpfile = basename ..".tmp"
+ mkutils.mv(filename, tmpfile)
+ local command = pattern % {filename = tmpfile, outputfile = filename}
+ print(command)
+ local status = os.execute(command)
+ if status > 0 then
+ -- if xtpipes failed to process the file, it may mean that it was bad-formed xml
+ -- we can try to make it well-formed using Tidy
+ local tidy_command = "tidy -utf8 -xml -asxml -q -o ${filename} ${tmpfile}" % {tmpfile = tmpfile, filename = filename}
+ print("xtpipes failed trying tidy")
+ print(tidy_command)
+ local status = os.execute(tidy_command)
+ if status > 0 then
+ -- if tidy failed as well, just use the original file
+ -- it will probably produce corrupted ODT file though
+ print("Tidy failed as well")
+ mkutils.mv(tmpfile, filename)
+ end
+ end
+ end
+ return matchfunction
+end
+
+-- This function moves the last added file matching function to the first place
+-- in the execution order. This ensures that filters are executed in the
+-- correct order.
+function M.move_matches(make)
+ local matches = make.matches
+ local last = matches[#matches]
+ table.insert(matches, 1, last)
+ matches[#matches] = nil
+end
+
+M.get_texmfroot = get_texmfroot
+M.find_texmfroot = find_texmfroot
+M.find_tex4ht_jar = find_tex4ht_jar
+return M
diff --git a/support/make4ht/mathnode.lua b/support/make4ht/mathnode.lua
new file mode 100644
index 0000000000..6d054ad7b8
--- /dev/null
+++ b/support/make4ht/mathnode.lua
@@ -0,0 +1,87 @@
+-- local mathnodepath = os.getenv "mathjaxnodepath"
+--
+-- print("mathnode", mathnodepath)
+local mkutils = require "mkutils"
+-- other possible value is page2svg
+local mathnodepath = "page2html"
+-- options for MathJax command
+local options = "--format MathML"
+-- math fonts position
+-- don't alter fonts if not set
+local fontdir = nil
+-- if we copy fonts
+local fontdest = nil
+local fontformat = "otf"
+
+local function compile(src)
+ local tmpfile = os.tmpname()
+ local filename = src
+ print("Compile using MathJax")
+ local command = mathnodepath .. " ".. options .. " < " .. filename .. " > " .. tmpfile
+ print(command)
+ local status = os.execute(command)
+ print("Result written to: ".. tmpfile)
+ mkutils.cp(tmpfile, src)
+ os.remove(tmpfile)
+end
+
+local function extract_css(file)
+ local f = io.open(file, "r")
+ local contents = f:read("*all")
+ f:close()
+ local css = ""
+ local filename = ""
+ contents = contents:gsub('<style id="(MathJax.-)">(.+)</style>', function(name, style)
+ css = style
+ filename = (name or "") .. ".css"
+ return '<link rel="stylesheet" type="text/css" href="'..filename ..'" />'
+ end)
+ local x = assert(io.open(file, "w"))
+ x:write(contents)
+ x:close()
+ return filename, css
+end
+
+--
+local function use_fonts(css)
+ local family_pattern = "font%-family:%s*(.-);.-%/([^%/]+)%.".. fontformat
+ local family_build = "@font-face {font-family: %s; src: url('%s/%s.%s') format('%s')}"
+ local fontdir = fontdir:gsub("/$","")
+ css = css:gsub("(@font%-face%s*{.-})", function(face)
+ -- if not face:match("url%(") then return face end
+ if not face:match("url%(") then return "" end
+ -- print(face)
+ local family, filename = face:match(family_pattern)
+ print(family, filename)
+ local newfile = string.format("%s/%s.%s", fontdir, filename, fontformat)
+ Make:add_file(newfile)
+ return family_build:format(family, fontdir, filename, fontformat, fontformat)
+ -- return face
+ end)
+ return css
+end
+
+
+local function save_css(filename, css)
+ local f = io.open(filename, "w")
+ f:write(css)
+ f:close()
+end
+
+return function(text, arguments)
+ -- if arguments.prg then mathnodepath = arguments.prg end
+ mathnodepath = arguments.prg or mathnodepath
+ options = arguments.options or options
+ fontdir = arguments.fontdir or fontdir
+ fontdest = arguments.fontdest or fontdest
+ fontformat = arguments.fontformat or fontformat
+ compile(text)
+ filename, css = extract_css(text)
+ if fontdir then
+ css = use_fonts(css)
+ end
+ save_css(filename, css)
+ Make:add_file(filename)
+ -- print(css)
+ print(filename)
+end
diff --git a/support/make4ht/mkparams.lua b/support/make4ht/mkparams.lua
new file mode 100644
index 0000000000..4db967f270
--- /dev/null
+++ b/support/make4ht/mkparams.lua
@@ -0,0 +1,238 @@
+local lapp = require "lapp-mk4"
+local mkutils = require "mkutils"
+local m = {} -- use ugly module system for new lua versions support
+
+-- these two variables will be used in the version number
+-- progname will be set in get_args
+m.progname = "make4ht"
+-- set the version number before call to process_args()
+m.version_number = "v0.1"
+
+m.optiontext = [[
+${progname} - build system for tex4ht
+Usage:
+${progname} [options] filename ["tex4ht.sty op."] ["tex4ht op."] ["t4ht op"] ["latex op"]
+
+Available options:
+ -b,--backend (default tex4ht) Backend used for xml generation.
+ possible values: tex4ht or lua4ht
+ -c,--config (default xhtml) Custom config file
+ -d,--output-dir (default nil) Output directory
+ -e,--build-file (default nil) If build file is different than `filename`.mk4
+ -f,--format (default html5) Output file format
+ -h,-- help Display this message
+ -l,--lua Use lualatex for document compilation
+ -m,--mode (default default) Switch which can be used in the makefile
+ -n,--no-tex4ht Disable dvi file processing with tex4ht command
+ -s,--shell-escape Enables running external programs from LaTeX
+ -u,--utf8 For output documents in utf8 encoding
+ -x,--xetex Use xelatex for document compilation
+ -v,--version Display version number
+]]
+
+-- test if the current command line argument should be passed to tex4ht, t4ht or latex
+local function is_escapedargument(arg)
+ -- we need to ignore make4ht options which can be used without filename, ie --version and --help
+ local ignored_options = {["-h"]=true, ["--help"]=true, ["-v"] = true, ["--version"]=true}
+ if ignored_options[arg] then return false end
+ -- in other cases, match if the argument starts with "-" character
+ return arg:match("^%-")
+end
+local function get_args(parameters, optiontext)
+ local parameters = parameters or {}
+ parameters.progname = parameters.progname or "make4ht"
+ parameters.postparams = parameters.postparams or ""
+ local optiontext = optiontext or m.optiontext
+ parameters.postfile = parameters.postfile or ""
+ optiontext = optiontext .. parameters.postparams ..[[ <filename> (string) Input file name
+
+Positional optional argumens:
+ ["tex4ht.sty op."] Additional parameters for tex4ht.sty
+ ["tex4ht op."] Options for tex4ht command
+ ["t4ht op"] Options for t4ht command
+ ["latex op"] Additional options for LaTeX
+
+Documentation: https://tug.org/applications/tex4ht/mn.html
+Issue tracker for tex4ht bugs: https://puszcza.gnu.org.ua/bugs/?group=tex4ht
+Issue tracker for make4ht bugs: https://github.com/michal-h21/make4ht/issues
+ ]] .. parameters.postfile
+ -- we can pass arguments for tex4ht and t4ht after filename, but it will confuse lapp, thinking that these
+ -- options are for make4ht. this may result in execution error or wrong option parsing
+ -- as fix, add a space before options at the end (we need to stop to add spaces as soon as we find
+ -- nonempty string which doesn't start with - it will be filename or tex4ht.sty options
+ if #arg > 1 then -- do this only if more than one argument is used
+ for i=#arg,1,-1 do
+ local current = arg[i]
+ if is_escapedargument(arg[i]) then
+ arg[i] = " ".. arg[i]
+ -- empty parameter
+ elseif current == "" then
+ else
+ break
+ end
+ end
+ end
+ --print("--------------\n" .. optiontext .."--------------\n")
+ return lapp(optiontext % parameters)
+end
+
+--- get outptut file format and list of extensions from --format option string
+local function get_format_extensions(format_string)
+ local format, rest = format_string:match("^([a-zA-Z0-9]+)(.*)")
+ local extensions = {}
+ -- it is possible to pass only the extensions
+ rest = rest or format_string
+ rest:gsub("([%+%-])([^%+^%-]+)",function(typ, name)
+ table.insert(extensions, {type = typ, name = name})
+ end)
+ return format, extensions
+end
+
+local function process_args(args)
+ local function get_inserter(args,tb)
+ return function(key, value)
+ --local v = args[key] and value or ""
+ local v = ""
+ if args[key] then v = value end
+ table.insert(tb,v)
+ end
+ end
+
+ if args.version ==true then
+ print(string.format("%s version %s", m.progname, m.version_number))
+ os.exit()
+ end
+
+ local outdir = ""
+ local packages = ""
+
+ if args["output-dir"] ~= "nil" then
+ outdir = args["output-dir"] or ""
+ outdir = outdir:gsub('\\','/')
+ outdir = outdir:gsub('/$','')
+ end
+
+ if args.backend == "lua4ht" then
+ args.lua = true
+ args.xetex = nil
+ args.utf8 = true
+ args["no-tex4ht"] = true
+ packages = packages .."\\RequirePackage{lua4ht}"
+ end
+
+
+ local compiler = args.lua and "dvilualatex" or args.xetex and "xelatex --no-pdf" or "latex"
+ local tex_file = args.filename
+ local input = mkutils.remove_extension(args.filename)
+ local latex_params = {}
+ local insert_latex = get_inserter(args,latex_params)
+ insert_latex("shell-escape","-shell-escape")
+ local latex_cli_params = args[4] or ""
+ if not latex_cli_params:match("%-jobname") then
+ -- we must strip out directories from jobname when full path to document is given
+ input = input:match("([^%/^%\\]+)$")
+ -- input also cannot contain spaces, replace them with underscores
+ input = input:gsub("%s", "_")
+ table.insert(latex_params,"-jobname="..input)
+ else
+ -- when user specifies -jobname, we must change name of the input file,
+ -- in order to be able to process correct dvi file with tex4ht and t4ht
+ local newinput
+ local first, rest = latex_cli_params:match("%-jobname=(.)(.*)")
+ if first=='"' then
+ newinput=rest:match('([^"]+)')
+ elseif first=="'" then
+ newinput=rest:match("([^']+)")
+ elseif type(first)== "string" then
+ rest = first.. rest
+ newinput = rest:match("([^ ]+)")
+ end
+ if newinput then
+ input = newinput
+ end
+ end
+ table.insert(latex_params, latex_cli_params)
+ --table.insert(latex_params,args["shell-escape"] and "-shell-escape")
+
+
+ local t4sty = args[1] or ""
+ -- test if first option is custom config file
+ local cfg_tmp = t4sty:match("([^,^ ]+)")
+ if cfg_tmp and cfg_tmp ~= args.config then
+ local fn = cfg_tmp..".cfg"
+ local f = io.open(fn,"r")
+ if f then
+ args.config = cfg_tmp
+ f:close()
+ end
+ end
+ --[[if args[1] and args[1] ~= "" then
+ t4sty = args[1]
+ else
+ --]]
+ -- Different behaviour from htlatex
+ local utf = args.utf8 and ",charset=utf-8" or ""
+ t4sty = args.config .. "," .. t4sty .. utf
+ --end
+
+ local tex4ht = ""
+ local dvi= args.xetex and "xdv" or "dvi"
+ if args[2] and args[2] ~="" then
+ tex4ht = args[2]
+ else
+ tex4ht = args.utf8 and " -cmozhtf -utf8" or ""
+ if args.xetex then tex4ht = tex4ht .. " -.xdv" end
+ -- tex4ht = tex4ht .. xdv
+ end
+
+ local t4ht = args[3] or ""
+
+ local mode = args.mode or "default"
+
+ local build_file = input.. ".mk4"
+
+ if args["build-file"] and args["build-file"] ~= "nil" then
+ build_file = args["build-file"]
+ end
+
+ local outformat, extensions
+ if args["format"] and arg["format"] ~= "nil" then
+ outformat, extensions = get_format_extensions(args["format"])
+ end
+
+ local parameters = {
+ htlatex = compiler
+ ,input=input
+ ,tex_file=tex_file
+ ,packages=packages
+ ,latex_par=table.concat(latex_params," ")
+ --,config=ebookutils.remove_extension(args.config)
+ ,tex4ht_sty_par=t4sty
+ ,tex4ht_par=tex4ht
+ ,t4ht_par=t4ht
+ ,mode = mode
+ ,dvi = dvi
+ ,build_file = build_file
+ ,output_format = outformat
+ ,extensions = extensions
+ --,t4ht_dir_format=t4ht_dir_format
+ }
+ if outdir then parameters.outdir = outdir end
+ print("Output dir: ",outdir)
+ print("Compiler: ", compiler)
+ print("Latex options: ", table.concat(latex_params," "))
+ print("tex4ht.sty :",t4sty)
+ print("tex4ht",tex4ht)
+ print("build_file", build_file)
+ if outformat~="nil" then
+ print("Output format", outformat)
+ for _, ex in ipairs(extensions) do
+ print("Extension", ex.type .. ex.name)
+ end
+ end
+ return parameters
+end
+m.get_args = get_args
+m.get_format_extensions = get_format_extensions
+m.process_args = process_args
+return m
diff --git a/support/make4ht/mkutils.lua b/support/make4ht/mkutils.lua
new file mode 100644
index 0000000000..fd32c91e08
--- /dev/null
+++ b/support/make4ht/mkutils.lua
@@ -0,0 +1,557 @@
+module(...,package.seeall)
+
+local make4ht = require("make4ht-lib")
+local mkparams = require("mkparams")
+--template engine
+function interp(s, tab)
+ local tab = tab or {}
+ return (s:gsub('($%b{})', function(w) return tab[w:sub(3, -2)] or w end))
+end
+--print( interp("${name} is ${value}", {name = "foo", value = "bar"}) )
+
+function addProperty(s,prop)
+ if prop ~=nil then
+ return s .." "..prop
+ else
+ return s
+ end
+end
+getmetatable("").__mod = interp
+getmetatable("").__add = addProperty
+
+--print( "${name} is ${value}" % {name = "foo", value = "bar"} )
+-- Outputs "foo is bar"
+
+
+-- merge two tables recursively
+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
+
+function string:split(sep)
+ local sep, fields = sep or ":", {}
+ local pattern = string.format("([^%s]+)", sep)
+ self:gsub(pattern, function(c) fields[#fields+1] = c end)
+ return fields
+end
+
+function remove_extension(path)
+ local found, len, remainder = string.find(path, "^(.*)%.[^%.]*$")
+ if found then
+ return remainder
+ else
+ return path
+ end
+end
+
+--
+
+function file_exists(file)
+ local f = io.open(file, "rb")
+ if f then f:close() end
+ return f ~= nil
+end
+
+-- searching for converted images
+function parse_lg(filename)
+ print("Parse LG")
+ local outputimages,outputfiles,status={},{},nil
+ local fonts, used_fonts = {},{}
+ if not file_exists(filename) then
+ print("Cannot read log file: "..filename)
+ else
+ local usedfiles={}
+ for line in io.lines(filename) do
+ --- needs --- pokus.idv[1] ==> pokus0x.png ---
+ -- line:gsub("needs --- (.+?)[([0-9]+) ==> ([%a%d%p%.%-%_]*)",function(name,page,k) table.insert(outputimages,k)end)
+ line:gsub("needs %-%-%- (.+)%[([0-9]+)%] ==> ([%a%d%p%.%-%_]*)",
+ function(file,page,output)
+ local rec = {
+ source = file,
+ page=page,
+ output = output
+ }
+ table.insert(outputimages,rec)
+ end
+ )
+ line:gsub("File: (.*)", function(k)
+ if not usedfiles[k] then
+ table.insert(outputfiles,k)
+ usedfiles[k] = true
+ end
+ end)
+ line:gsub("htfcss: ([^%s]+)(.*)",function(k,r)
+ local fields = {}
+ r:gsub("[%s]*([^%:]+):[%s]*([^;]+);",function(c,v)
+ fields[c] = v
+ end)
+ fonts[k] = fields
+ end)
+
+ line:gsub('Font("([^"]+)","([%d]+)","([%d]+)","([%d]+)"',function(n,s1,s2,s3)
+ table.insert(used_fonts,{n,s1,s2,s3})
+ end)
+
+ end
+ status=true
+ end
+ return {files = outputfiles, images = outputimages},status
+end
+
+
+--
+local cp_func = os.type == "unix" and "cp" or "copy"
+-- maybe it would be better to actually move the files
+-- in reality it isn't.
+-- local cp_func = os.type == "unix" and "mv" or "move"
+function cp(src,dest)
+ local command = string.format('%s "%s" "%s"', cp_func, src, dest)
+ if cp_func == "copy" then command = command:gsub("/",'\\') end
+ print("Copy: "..command)
+ os.execute(command)
+end
+
+function mv(src, dest)
+ local mv_func = os.type == "unix" and "mv " or "move "
+ local command = string.format('%s "%s" "%s"', mv_func, src, dest)
+ -- fix windows paths
+ if mv_func == "move" then command = command:gsub("/",'\\') end
+ print("Move: ".. command)
+ os.execute(command)
+end
+
+function delete_dir(path)
+ local cmd = os.type == "unix" and "rm -rd " or "rd /s/q "
+ os.execute(cmd .. path)
+end
+
+local used_dir = {}
+
+function prepare_path(path)
+ --local dirs = path:split("/")
+ local dirs = {}
+ if path:match("^/") then dirs = {""}
+ elseif path:match("^~") then
+ local home = os.getenv "HOME"
+ dirs = home:split "/"
+ path = path:gsub("^~/","")
+ table.insert(dirs,1,"")
+ end
+ if path:match("/$")then path = path .. " " end
+ for _,d in pairs(path:split "/") do
+ table.insert(dirs,d)
+ end
+ table.remove(dirs,#dirs)
+ return dirs,table.concat(dirs,"/")
+end
+
+-- Find which part of path already exists
+-- and which directories have to be created
+function find_directories(dirs, pos)
+ local pos = pos or #dirs
+ -- we tried whole path and no dir exist
+ if pos < 1 then return dirs end
+ local path = ""
+ -- in the case of unix absolute path, empty string is inserted in dirs
+ if pos == 1 and dirs[pos] == "" then
+ path = "/"
+ else
+ path = table.concat(dirs,"/", 1,pos) .. "/"
+ end
+ if not lfs.chdir(path) then -- recursion until we succesfully changed dir
+ -- or there are no elements in the dir table
+ return find_directories(dirs,pos - 1)
+elseif pos ~= #dirs then -- if we succesfully changed dir
+ -- and we have dirs to create
+ local p = {}
+ for i = pos+1, #dirs do
+ table.insert(p, dirs[i])
+ end
+ return p
+else -- whole path exists
+ return {}
+end
+end
+
+function mkdirectories(dirs)
+ if type(dirs) ~="table" then
+ return false, "mkdirectories: dirs is not table"
+ end
+ for _,d in ipairs(dirs) do
+ local stat,msg = lfs.mkdir(d)
+ if not stat then return false, "makedirectories error: "..msg end
+ lfs.chdir(d)
+ end
+ return true
+end
+
+function copy_filter(src,dest, filter)
+ local src_f=io.open(src,"rb")
+ local dst_f=io.open(dest,"w")
+ local contents = src_f:read("*all")
+ local filter = filter or function(s) return s end
+ src_f:close()
+ dst_f:write(filter(contents))
+ dst_f:close()
+end
+
+
+
+function copy(filename,outfilename)
+ local currdir = lfs.currentdir()
+ if filename == outfilename then return true end
+ local parts, path = prepare_path(outfilename)
+ if not used_dir[path] then
+ local to_create, msg = find_directories(parts)
+ if not to_create then
+ print(msg)
+ return false
+ end
+ used_dir[path] = true
+ local stat, msg = mkdirectories(to_create)
+ if not stat then print(msg) end
+ end
+ lfs.chdir(currdir)
+ cp(filename, path)
+ return true
+end
+
+-- find the zip command
+function find_zip()
+ if io.popen("zip -v","r"):close() then
+ return "zip"
+ elseif io.popen("miktex-zip -v","r"):close() then
+ return "miktex-zip"
+ end
+ -- we cannot find the zip command
+ return "zip"
+end
+
+-- Config loading
+local function run(untrusted_code, env)
+ if untrusted_code:byte(1) == 27 then return nil, "binary bytecode prohibited" end
+ local untrusted_function = nil
+ untrusted_function, message = load(untrusted_code, nil, "t",env)
+ if not untrusted_function then return nil, message end
+ if not setfenv then setfenv = function(a,b) return true end end
+ setfenv(untrusted_function, env)
+ return pcall(untrusted_function)
+end
+
+local main_settings = {}
+main_settings.fonts = {}
+local env = {}
+
+-- We make sandbox for make script, all functions must be explicitely declared
+-- Function declarations:
+env.pairs = pairs
+env.ipairs = ipairs
+env.print = print
+env.split = split
+env.string = string
+env.table = table
+env.copy = copy
+env.tonumber = tonumber
+env.tostring = tostring
+env.mkdirectories = mkdirectories
+env.require = require
+env.texio = texio
+env.type = type
+env.lfs = lfs
+env.os = os
+env.io = io
+env.math = math
+env.unicode = unicode
+
+
+-- it is necessary to use the settings table
+-- set in the Make environment by mkutils
+function env.set_settings(par)
+ local settings = env.settings
+ for k,v in pairs(par) do
+ settings[k] = v
+ end
+end
+
+-- Add a value to the current settings
+function env.settings_add(par)
+ local settings = env.settings
+ for k,v in pairs(par) do
+ local oldval = settings[k] or ""
+ settings[k] = oldval .. v
+ end
+end
+
+function env.get_filter_settings(name)
+ local settings = env.settings
+ -- local settings = self.params
+ local filters = settings.filter or {}
+ local filter_options = filters[name] or {}
+ return filter_options
+end
+
+function env.filter_settings(name)
+ -- local settings = Make.params
+ local settings = env.settings
+ local filters = settings.filter or {}
+ local filter_options = filters[name] or {}
+ return function(par)
+ filters[name] = merge(filter_options, par)
+ settings.filter = filters
+ end
+end
+env.Font = function(s)
+ local font_name = s["name"]
+ if not font_name then return nil, "Cannot find font name" end
+ env.settings.fonts[font_name] = s
+end
+
+env.Make = make4ht.Make
+env.Make.params = env.settings
+env.Make:add("test","test the variables: ${tex4ht_sty_par} ${htlatex} ${input} ${config}")
+
+-- this function reads the LaTeX log file and tries to detect fatal errors in the compilation
+local function testlogfile(par)
+ local logfile = par.input .. ".log"
+ local f = io.open(logfile,"r")
+ if not f then
+ print("Make4ht: cannot open log file "..logfile)
+ return 1
+ end
+ local len = f:seek("end")
+ -- test only the end of the log file, no need to run search functions on everything
+ local newlen = len - 1256
+ -- but the value to seek must be greater than 0
+ newlen = (newlen > 0) and newlen or 0
+ f:seek("set", newlen)
+ local text = f:read("*a")
+ f:close()
+ if text:match("No pages of output") or text:match("TeX capacity exceeded, sorry") then return 1 end
+ return 0
+end
+
+
+-- Make this function available in the build files
+Make.testlogfile = testlogfile
+--env.Make:add("htlatex", "${htlatex} ${latex_par} '\\\makeatletter\\def\\HCode{\\futurelet\\HCode\\HChar}\\def\\HChar{\\ifx\"\\HCode\\def\\HCode\"##1\"{\\Link##1}\\expandafter\\HCode\\else\\expandafter\\Link\\fi}\\def\\Link#1.a.b.c.{\\g@addto@macro\\@documentclasshook{\\RequirePackage[#1,html]{tex4ht}\\let\\HCode\\documentstyle\\def\\documentstyle{\\let\\documentstyle\\HCode\\expandafter\\def\\csname tex4ht\\endcsname{#1,html}\\def\\HCode####1{\\documentstyle[tex4ht,}\\@ifnextchar[{\\HCode}{\\documentstyle[tex4ht]}}}\\makeatother\\HCode '${config}${tex4ht_sty_par}'.a.b.c.\\input ' ${input}")
+
+-- template for calling LaTeX with tex4ht loaded
+Make.latex_command = "${htlatex} ${latex_par} '\\makeatletter"..
+"\\def\\HCode{\\futurelet\\HCode\\HChar}\\def\\HChar{\\ifx\"\\HCode"..
+"\\def\\HCode\"##1\"{\\Link##1}\\expandafter\\HCode\\else"..
+"\\expandafter\\Link\\fi}\\def\\Link#1.a.b.c.{\\g@addto@macro"..
+"\\@documentclasshook{\\RequirePackage[#1,html]{tex4ht}${packages}}"..
+"\\let\\HCode\\documentstyle\\def\\documentstyle{\\let\\documentstyle"..
+"\\HCode\\expandafter\\def\\csname tex4ht\\endcsname{#1,html}\\def"..
+"\\HCode####1{\\documentstyle[tex4ht,}\\@ifnextchar[{\\HCode}{"..
+"\\documentstyle[tex4ht]}}}\\makeatother\\HCode ${tex4ht_sty_par}.a.b.c."..
+"\\input \"\\detokenize{${tex_file}}\"'"
+
+env.Make:add("htlatex",function(par)
+ local command = Make.latex_command
+ if os.type == "windows" then
+ command = command:gsub("'",'')
+ end
+ command = command % par
+ print("LaTeX call: "..command)
+ os.execute(command)
+ return testlogfile(par)
+end
+,{correct_exit=0})
+
+
+env.Make:add("latexmk", function(par)
+ local command = Make.latex_command
+ par.expanded = command % par
+ -- quotes in latex_command must be escaped, they cause Latexmk error
+ par.expanded = par.expanded:gsub('"', '\\"')
+ local newcommand = 'latexmk -latex="${expanded}" -dvi ${tex_file}' % par
+ os.execute(newcommand)
+ return Make.testlogfile(par)
+end, {correct_exit= 0})
+
+
+
+env.Make:add("tex4ht","tex4ht ${tex4ht_par} \"${input}.${dvi}\"", nil, 1)
+env.Make:add("t4ht","t4ht ${t4ht_par} \"${input}.${ext}\"",{ext="dvi"},1)
+
+-- enable extension in the config file
+-- the following two functions must be here and not in make4ht-lib.lua
+-- because of the access to env.settings
+env.Make.enable_extension = function(self,name)
+ table.insert(env.settings.extensions, {type="+", name=name})
+end
+
+-- disable extension in the config file
+env.Make.disable_extension = function(self,name)
+ table.insert(env.settings.extensions, {type="-", name=name})
+end
+
+function load_config(settings, config_name)
+ local settings = settings or main_settings
+ -- the extensions requested from the command line should take precedence over
+ -- extensions enabled in the config file
+ local saved_extensions = settings.extensions
+ settings.extensions = {}
+ env.settings = settings
+ env.mode = settings.mode
+ if config_name and not file_exists(config_name) then
+ config_name = kpse.find_file(config_name, 'texmfscripts') or config_name
+ end
+ local f = io.open(config_name,"r")
+ if not f then
+ print("Cannot open config file", config_name)
+ return env
+ end
+ print("Using build file", config_name)
+ local code = f:read("*all")
+ local fn, msg = run(code,env)
+ if not fn then print(msg) end
+ assert(fn)
+ -- reload extensions from command line arguments for the "format" parameter
+ for _,v in ipairs(saved_extensions) do
+ table.insert(settings.extensions, v)
+ end
+ return env
+end
+
+env.Make:add("xindy", function(par)
+ -- par.encoding = par.encoding or "utf8"
+ -- par.language = par.language or "english"
+ par.idxfile = par.idxfile or par.input .. ".idx"
+ local modules = par.modules or {par.input}
+ local t = {}
+ for k,v in ipairs(modules) do
+ t[#t+1] = "-M ".. v
+ end
+ par.moduleopt = table.concat(t, " ")
+ local xindy_call = "xindy -L ${language} -C ${encoding} ${moduleopt} ${idxfile}" % par
+ print(xindy_call)
+ return os.execute(xindy_call)
+end, { language = "english", encoding = "utf8"})
+
+local function find_lua_file(name)
+ local extension_path = name:gsub("%.", "/") .. ".lua"
+ return kpse.find_file(extension_path, "lua")
+end
+
+--- load the output format plugins
+function load_output_format(format_name)
+ local format_library = "make4ht.formats."..format_name
+ local is_format_file = find_lua_file(format_library)
+ if is_format_file then
+ local format = assert(require(format_library))
+ if format then
+ format.prepare_extensions = format.prepare_extensions or function(extensions) return extensions end
+ format.modify_build = format.modify_build or function(make) return make end
+ end
+ return format
+ end
+end
+
+--- Execute the prepare_parameters function in list of extensions
+function extensions_prepare_parameters(extensions, parameters)
+ for _, ext in ipairs(extensions) do
+ -- execute the extension only if it contains prepare_parameters function
+ local fn = ext.prepare_parameters
+ if fn then
+ parameters = fn(parameters)
+ end
+ end
+ return parameters
+end
+
+--- Modify the build sequence using extensions
+-- @param extensions list of extensions
+-- @make Make object
+function extensions_modify_build(extensions, make)
+ for _, ext in ipairs(extensions) do
+ local fn = ext.modify_build
+ if fn then
+ make = fn(make)
+ end
+ end
+ return make
+end
+
+
+--- load one extension
+-- @param name extension name
+-- @param format current output format
+function load_extension(name,format)
+ -- first test if the extension exists
+ local extension_library = "make4ht.extensions." .. name
+ local is_extension_file = find_lua_file(extension_library)
+ -- don't try to load the extension if it doesn't exist
+ if not is_extension_file then return nil end
+ local extension = require("make4ht.extensions.".. name)
+ -- extensions can test if the current output format is supported
+ local test = extension.test
+ if test then
+ if test(format) then
+ return extension
+ end
+ -- if the test fail return nil
+ return nil
+ end
+ -- if the extension doesn't provide the test function, we will assume that
+ -- it supports every output format
+ return extension
+end
+
+--- load extensions
+-- @param extensions table created by mkparams.get_format_extensions function
+-- @param format output type format. extensions may support only certain file
+-- formats
+function load_extensions(extensions, format)
+ local module_names = {}
+ local extension_table = {}
+ local extension_sequence = {}
+ -- process the extension table. it contains type field, which can enable or
+ -- diable the extension
+ for _, v in ipairs(extensions) do
+ local enable = v.type == "+" and true or nil
+ -- load extenisons in a correct order
+ -- don't load extensions multiple times
+ if enable and not module_names[v.name] then
+ table.insert(extension_sequence, v.name)
+ end
+ -- the last extension request can disable it
+ module_names[v.name] = enable
+ end
+ for _, name in ipairs(extension_sequence) do
+ -- the extension can be inserted into the extension_sequence, but disabled
+ -- later.
+ if module_names[name] == true then
+ local extension = load_extension(name,format)
+ if extension then
+ print("Load extension", name)
+ table.insert(extension_table, extension)
+ else
+ print("Cannot load extension: ".. name)
+ end
+ end
+ end
+ return extension_table
+end
+
+--- add new extensions to a list of loaded extensions
+-- @param added string with extensions to be added in the form +ext1+ext2
+function add_extensions(added, extensions)
+ local _, newextensions = mkparams.get_format_extensions("dummyfmt" .. added)
+ -- insert new extension at the beginning, in order to support disabling using
+ -- the -f option
+ for _, x in ipairs(extensions or {}) do table.insert(newextensions, x) end
+ return newextensions
+end
+
+-- I don't know if this is clean, but settings functions won't be available
+-- for filters and extensions otherwise
+for k,v in pairs(env) do _G[k] = v end
diff --git a/support/make4ht/readme.tex b/support/make4ht/readme.tex
new file mode 100644
index 0000000000..c9fc0a2f3e
--- /dev/null
+++ b/support/make4ht/readme.tex
@@ -0,0 +1,1085 @@
+\hypertarget{introduction}{%
+\section{Introduction}\label{introduction}}
+
+\texttt{make4ht} is a simple build system for \texttt{tex4ht}, \TeX~to
+XML converter. It provides a command line tool that drive the conversion
+process. It also provides a library which can be used to create
+customized conversion tools. An example of such conversion tool is
+\href{https://github.com/michal-h21/tex4ebook}{tex4ebook} for conversion
+of \TeX~to ePub and other e-book formats.
+
+\hypertarget{how-it-works}{%
+\subsection{How it works}\label{how-it-works}}
+
+\hypertarget{the-issues-with-default-tex4ht-conversion-commands}{%
+\subsubsection{\texorpdfstring{The issues with default \texttt{tex4ht}
+conversion
+commands}{The issues with default tex4ht conversion commands}}\label{the-issues-with-default-tex4ht-conversion-commands}}
+
+\texttt{tex4ht} system supports several output formats, most notably
+\texttt{XHTML}, \texttt{HTML\ 5} and \texttt{ODT}. The conversion can be
+invoked using several commands. These commands invoke LaTeX~or Plain TeX
+with special instructions to load \texttt{tex4ht.sty} package. The
+\TeX~run produces special \texttt{DVI} file which contains the code for
+desired output format. Produced \texttt{DVI} file is then processed and
+desired output files are created.
+
+The basic command provided by \texttt{tex4ht} is named \texttt{htlatex}.
+It compiles \LaTeX~ files to \texttt{HTML} with this command sequence:
+
+\begin{verbatim}
+latex $latex_options 'code for loading tex4ht.sty \input{filename}'
+latex $latex_options 'code for loading tex4ht.sty \input{filename}'
+latex $latex_options 'code for loading tex4ht.sty \input{filename}'
+tex4ht $tex4ht_options filename
+t4ht $t4ht_options filename
+\end{verbatim}
+
+The options for various parts of the system can be passed on the command
+line:
+
+\begin{verbatim}
+htlatex filename "tex4ht.sty options" "tex4ht_options" "t4ht_options" "latex_options"
+\end{verbatim}
+
+For basic \texttt{HTML} conversion it is possible to use the most basic
+invocation:
+
+\begin{verbatim}
+htlatex filename.tex
+\end{verbatim}
+
+It can be much more involved for \texttt{HTML\ 5} output in
+\texttt{UTF-8} encoding:
+
+\begin{verbatim}
+htlatex filename.tex "xhtml,html5,charset=utf-8" "-cmozhtf -utf8"
+\end{verbatim}
+
+\texttt{make4ht} can simplify it:
+
+\begin{verbatim}
+make4ht -uf html5 filename.tex
+\end{verbatim}
+
+Another issue is the fixed compilation order and hard-coded number of
+LaTeX invocations.
+
+When you need to run a program which interact with LaTeX, such as
+\texttt{Makeindex} or \texttt{Bibtex}, you need to create a new script
+based on \texttt{htlatex}, or run \texttt{htlatex} twice, which means
+that LaTeX will be invoked six times. This can lead to significantly
+long compilation times. \texttt{make4ht} provides build files and
+extensions, which can be used for interaction with external tools.
+
+It is also possible to have several compilation modes. When you just add
+new text to a document, which doesn't contain cross-references, don't
+add new stuff to the table of contents, etc., it is possible to use the
+\texttt{draft} mode which will invoke LaTeX only once. It can save quite
+a lot of the compilation time:
+
+\begin{verbatim}
+make4ht -um draft -f html5 filename.tex
+\end{verbatim}
+
+There are also issues with a behaviour of the \texttt{t4ht} application.
+It reads file \texttt{filename.lg}, generated by \texttt{tex4ht}, where
+are instructions about generated files, \texttt{CSS} instructions, calls
+to external applications, instructions for image conversions etc. It can
+be instructed to copy generated files to some output directory, but it
+doesn't preserve directory structure, so when you have images in a
+subdirectory, they will be copied to the output directory. Links will be
+pointing to a non-existing subdirectory. The following command should
+copy all output files to the correct destinations.
+
+\begin{verbatim}
+make4ht -d outputdir filename.tex
+\end{verbatim}
+
+The image conversion is configured in the
+\href{http://www.tug.org/applications/tex4ht/mn35.html\#index35-73001}{env
+file}, which has really strange syntax based and the rules are
+\href{http://www.tug.org/applications/tex4ht/mn-unix.html\#index27-69005}{os
+dependent}. \texttt{make4ht} provides simpler means for the image
+conversion in the build files.
+
+With \texttt{make4ht} build files, we have simple mean to fix these
+issues. We can change image conversion parameters without the need to
+modify the \texttt{env\ file}, or execute actions on the output files.
+These actions can be either external programs such as \texttt{xslt}
+processors or \texttt{HTML\ tidy} or \texttt{Lua} functions.
+
+The idea is to make system controlled by a build file. Because
+\texttt{Lua} interpreter is included in modern TeX distributions and
+\texttt{Lua} is ideal language for such task, it was chosen as language
+in which the build scripts are written.
+
+\hypertarget{output-file-formats-and-extensions}{%
+\section{Output file formats and
+extensions}\label{output-file-formats-and-extensions}}
+
+The default output format used by \texttt{make4ht} is \texttt{html5}.
+Different format can be requested using the \texttt{-\/-format} option.
+Supported formats are:
+
+\begin{itemize}
+\tightlist
+\item
+ \texttt{xhtml}
+\item
+ \texttt{html5}
+\item
+ \texttt{odt}
+\item
+ \texttt{tei}
+\item
+ \texttt{docbook}
+\end{itemize}
+
+The \texttt{-\/-format} option can be also used for extension loading.
+
+\hypertarget{extensions}{%
+\subsection{Extensions}\label{extensions}}
+
+Extensions can be used to modify the build process without the need to
+use a build file. They may post-process the output files or request
+additional commands for the compilation.
+
+The extensions can be enabled or disabled by appending
+\texttt{+EXTENSION} or \texttt{-EXTENSION} after the output format name:
+
+\begin{verbatim}
+ make4ht -uf html5+tidy filename.tex
+\end{verbatim}
+
+Available extensions:
+
+\begin{description}
+\item[latexmk\_build]
+use \texttt{Latexmk} for \LaTeX~compilation.
+\item[tidy]
+clean the \texttt{HTML} files using the \texttt{tidy} command.
+\item[dvisvgm\_hashes]
+efficient generation of SVG pictures using Dvisvgm. It can utilize
+multiple processor cores and generates only changed images.
+\item[common\_filters]
+clean the output HTML files using filters.
+\item[common\_domfilters]
+clean the HTML file using DOM filters. It is more powerful than
+\texttt{common\_filters}. Used DOM filters are \texttt{fixinlines},
+\texttt{idcolons} and \texttt{joincharacters}.
+\item[join\_colors]
+load the \texttt{joincolors} domfilter for all HTML files.
+\item[mathjaxnode]
+use \href{https://github.com/pkra/mathjax-node-page/}{mathjax-node-page}
+to convert from MathML code to HTML + CSS or SVG. See
+\protect\hyperlink{mathjaxsettings}{the available settings}.
+\item[odttemplate]
+automatically load the \texttt{odttemplate} filter.
+\item[staticsite]
+build the document in form suitable for static site generators like
+\href{https://jekyllrb.com/}{Jekyll}.
+\end{description}
+
+\hypertarget{build-files}{%
+\section{Build files}\label{build-files}}
+
+\texttt{make4ht} supports build files. These are \texttt{Lua} scripts
+which can adjust the build process. You can request external
+applications like \texttt{bibtex} or \texttt{makeindex}, pass options to
+the commands, modify the image conversion process, or post-process the
+generated files.
+
+\texttt{make4ht} tries to load default build file named as
+\texttt{filename\ +\ .mk4\ extension}. You can choose different build
+file with \texttt{-e} or \texttt{-\/-build-file} command line option.
+
+Sample build file:
+
+\begin{verbatim}
+Make:htlatex()
+Make:match("html$", "tidy -m -xml -utf8 -q -i ${filename}")
+\end{verbatim}
+
+\texttt{Make:htlatex()} is preconfigured command for calling LaTeX with
+\texttt{tex4ht} loaded on the input file. In this case, it will be
+called one time. After compilation, the \texttt{tidy} command is
+executed on the output \texttt{HTML} file.
+
+Note that you don't have to call \texttt{tex4ht} and \texttt{t4ht}
+commands explicitly in the build file, they are called automatically.
+
+\hypertarget{user-commands}{%
+\subsection{User commands}\label{user-commands}}
+
+You can add more commands like \texttt{Make:htlatex} using
+\texttt{Make:add} command:
+
+\begin{verbatim}
+Make:add("name", "command", {parameters}, repetition)
+\end{verbatim}
+
+The \texttt{name} and \texttt{command} parameters are required, rest of
+the parameters are optional.
+
+This defines \texttt{name} command, which can be then executed as
+\texttt{Make:name()} command.
+
+\hypertarget{provided-commands}{%
+\subsubsection{Provided commands}\label{provided-commands}}
+
+\begin{description}
+\item[\texttt{Make:htlatex}]
+One call to TeX engine with special configuration for \texttt{tex4ht}
+loading.
+\item[\texttt{Make:latexmk}]
+Use \texttt{Latexmk} for the document compilation. \texttt{tex4ht} will
+be loaded automatically.
+\item[\texttt{Make:tex4ht}]
+Process the \texttt{DVI} file and creates the output files.
+\item[\texttt{Make:t4ht}]
+Creates the CSS file.
+\end{description}
+
+\hypertarget{command-function}{%
+\subsubsection{Command function}\label{command-function}}
+
+The \texttt{command} parameter can be either string template or
+function:
+
+\begin{verbatim}
+Make:add("text", "echo hello, input file: ${input}")
+Make:add("function", function(params)
+ for k, v in pairs(params) do
+ print(k..": "..v)
+ end, {custom="Hello world"}
+)
+\end{verbatim}
+
+The template can get variable value from the parameters table using a
+\texttt{\$\{var\_name\}} placeholder. Templates are executed using
+operating system, so they should invoke existing OS commands. Function
+commands may execute system commands using \texttt{os.execute} function.
+
+\hypertarget{parameters-table}{%
+\subsubsection{Parameters table}\label{parameters-table}}
+
+\texttt{parameters} parameter is optional, it can be table or
+\texttt{nil} value, which should be used if you want to use the
+\texttt{repetition} parameter, but don't want to modify the parameters
+table.
+
+The table with default parameters is passed to all commands, they can be
+accessed from command functions or templates. When you specify your own
+parameters in the command definition, these additional parameters are
+added to the default parameters table for this particular command. You
+can override the default parameters in the parameters table.
+
+The default parameters are following:
+
+\begin{description}
+\item[\texttt{htlatex}]
+used compiler
+\item[\texttt{input}]
+it is output file name in fact
+\item[\texttt{tex\_file}]
+input TeX file
+\item[\texttt{latex\_par}]
+parameters to \texttt{latex}
+\item[\texttt{packages}]
+insert additional LaTeX code which is inserted before
+\texttt{\textbackslash{}documentclass}. Useful for passing options to
+packages or additional packages loading
+\item[\texttt{tex4ht\_sty\_par}]
+parameters to \texttt{tex4ht.sty}
+\item[\texttt{tex4ht\_par}]
+parameters to the \texttt{tex4ht} application
+\item[\texttt{t4ht\_par}]
+parameters to the \texttt{t4ht} application
+\item[\texttt{outdir}]
+the output directory
+\item[\texttt{repetition}]
+limit number of command execution.
+\item[\texttt{correct\_exit}]
+expected \texttt{exit\ code} from the command. The compilation will be
+terminated if the command \texttt{exit\ code} is different.
+\end{description}
+
+\hypertarget{repetition}{%
+\subsubsection{Repetition}\label{repetition}}
+
+Repetition is number which specifies a maximal number of executions of
+the particular command. This is used for instance for \texttt{tex4ht}
+and \texttt{t4ht} commands, as they should be executed only once in the
+compilation. They would be executed multiple times if you include them
+in the build file because they are called by \texttt{make4ht} by
+default. Because these commands allow only one \texttt{repetition}, the
+second execution will be blocked.
+
+\hypertarget{expected-exit-code}{%
+\subsubsection{Expected exit code}\label{expected-exit-code}}
+
+You can set the expected exit code from a command with a
+\texttt{correct\_exit} key in the parameters table. The compilation will
+be stopped when the command returns a different exit code.
+
+This mechanism isn't used for LaTeX (for all TeX engines and formats, in
+fact), because it doesn't differentiate between fatal and non-fatal
+errors, and it returns the same exit code in all cases. Log parsing is
+used because of that, error code \texttt{1} is returned in the case of
+fatal error, \texttt{0} is used otherwise. The \texttt{Make.testlogfile}
+function can be used in the build file to detect compilation errors in
+the TeX log file.
+
+\hypertarget{file-matches}{%
+\subsection{File matches}\label{file-matches}}
+
+Another type of action which can be specified in the build file is
+\texttt{match}. It can be called on the generated files:
+
+\begin{verbatim}
+Make:match("html$", "tidy -m -xml -utf8 -q -i ${filename}")
+\end{verbatim}
+
+It tests output file names with \texttt{Lua} pattern matching and on
+matched items will execute a command or a function, specified in the
+second argument. Commands may be specified as strings, the templates
+will be expanded, \texttt{\$\{var\_name\}} placeholders will be replaced
+with corresponding variables from the \texttt{parameters} table,
+described in the previous subsection. One additional variable is
+available: \texttt{filename}. It contains the name of the current output
+file.
+
+The above example will clean all output \texttt{HTML} files using the
+\texttt{tidy} command.
+
+If function is used instead, it will get two parameters. The first one
+is a current filename, the second one table with parameters.
+
+\hypertarget{filters}{%
+\subsubsection{Filters}\label{filters}}
+
+Some default \texttt{match} actions which can be used are available from
+the \texttt{make4ht-filter} module. It contains some functions which are
+useful for fixing some \texttt{tex4ht} bugs or shortcomings.
+
+Example:
+
+\begin{verbatim}
+local filter = require "make4ht-filter"
+local process = filter{"cleanspan", "fixligatures", "hruletohr"}
+Make:htlatex()
+Make:htlatex()
+Make:match("html$",process)
+\end{verbatim}
+
+The \texttt{make4ht-filter} module return a function which can be used
+for the filter chain building. Multiple filters can be chained, each of
+them can modify the string which was modified by the previous filters.
+The changes are then saved to the processed file.
+
+Built-in filters are:
+
+\begin{description}
+\item[cleanspan]
+clean spurious span elements when accented characters are used
+\item[cleanspan-nat]
+alternative clean span filter, provided by Nat Kuhn
+\item[fixligatures]
+decompose ligatures to base characters
+\item[hruletohr]
+\texttt{\textbackslash{}hrule} commands are translated to series of
+underscore characters by \texttt{tex4ht}, this filter translate these
+underscores to \texttt{\textless{}hr\textgreater{}} elements
+\item[entites]
+convert prohibited named entities to numeric entities (currently, only
+\texttt{\&nbsp;}, as it causes validation errors, and \texttt{tex4ht} is
+producing it sometimes)
+\item[fix-links]
+replace colons in local links and \texttt{id} attributes with
+underscores. Some cross-reference commands may produce colons in
+internal links, which results in validation error.
+\item[mathjaxnode]
+use \href{https://github.com/pkra/mathjax-node-page/}{mathjax-node-page}
+to convert from MathML code to HTML + CSS or SVG. See
+\protect\hyperlink{mathjaxsettings}{the available settings}.
+\item[odttemplate]
+use styles from another \texttt{ODT} file serving as a template in the
+current document. It works for the \texttt{styles.xml} file in the
+\texttt{ODT} file. During the compilation, this file is named as
+\texttt{\textbackslash{}jobname.4oy}.
+\item[staticsite]
+create HTML files in format suitable for static site generators such as
+\href{https://jekyllrb.com/}{Jekyll}
+\item[svg-height]
+some SVG images produced by \texttt{dvisvgm} seem to have wrong
+dimensions. This filter tries to set the correct image size.
+\end{description}
+
+Function \texttt{filter} accepts also function arguments, in this case
+this function takes file contents as a parameter and modified contents
+are returned.
+
+Example:
+
+\begin{verbatim}
+local filter = require "make4ht-filter"
+local changea = function(s) return s:gsub("a","z") end
+local process = filter{"cleanspan", "fixligatures", changea}
+Make:htlatex()
+Make:htlatex()
+Make:match("html$",process)
+\end{verbatim}
+
+In this example, spurious span elements are joined, ligatures are
+decomposed, and then all letters `a' are replaced with `z' letters.
+
+\hypertarget{dom-filters}{%
+\subsubsection{DOM filters}\label{dom-filters}}
+
+DOM filters use the \href{https://ctan.org/pkg/luaxml}{\texttt{LuaXML}}
+library to modify directly the XML object. This enables more powerful
+operations than the regex based filters from the previous section.
+
+Example:
+
+\begin{verbatim}
+local domfilter = require "make4ht-domfilter"
+local process = domfilter {"joincharacters"}
+Make:match("html$", process)
+\end{verbatim}
+
+Available DOM filters:
+
+\begin{description}
+\item[aeneas]
+\href{https://www.readbeyond.it/aeneas/}{Aeneas} is a tool for
+automagical synchronization of text and audio. This filter modifies the
+HTML code to support the synchronization.
+\item[fixinlines]
+put all inline elements which are direct children of the
+\texttt{\textless{}body\textgreater{}} elements to a paragraph.
+\item[idcolons]
+replace the colon (\texttt{:}) character in internal links and
+\texttt{id} attributes. They cause validation issues.
+\item[joincharacters]
+join consecutive \texttt{\textless{}span\textgreater{}} or
+\texttt{\textless{}mn\textgreater{}} elements.
+\item[joincolors]
+many \texttt{\textless{}span\textgreater{}} elements with unique
+\texttt{id} attribute are created when \LaTeX~colors are being used in
+the document. CSS rule is added for each of these elements, which may
+result in substantial grow of the CSS file. This filter replace these
+rules with a common one for elements with the same color value.
+\item[odtimagesize]
+set correct dimensions for images in the ODT format. It is loaded by
+default for the ODT output.
+\item[t4htlinks]
+fix hyperlinks in the ODT format.
+\end{description}
+
+\hypertarget{make4ht-aeneas-config-package}{%
+\subsubsection{make4ht-aeneas-config
+package}\label{make4ht-aeneas-config-package}}
+
+Companion for the \texttt{aeneas} DOM filter is the
+\texttt{make4ht-aeneas-config} plugin. It can be used to write Aeneas
+configuration file or execute Aeneas on the generated HTML files.
+
+Available functions:
+
+\begin{description}
+\item[write\_job(parameters)]
+write Aenas job configuration to \texttt{config.xml} file. See the
+\href{https://www.readbeyond.it/aeneas/docs/clitutorial.html\#processing-jobs}{Aeneas
+documentation} for more information about jobs.
+\item[execute(parameters)]
+execute Aeneas.
+\item[process\_files(parameters)]
+process the audio and generated subtitle files.
+\end{description}
+
+By default, the \texttt{smil} file is created. It is assumed that there
+is audio file in \texttt{mp3} format named as the TeX file. It is
+possible to use different formats and file names using mapping.
+
+The configuration options can be passed directly to the functions or set
+using \texttt{filter\_settings\ "aeneas-config"\ \{parameters\}}
+function.
+
+Available parameters:
+
+\begin{description}
+\item[lang]
+document language. It is interfered from the HTML file, so it is not
+necessary to set it.
+\item[map]
+mapping between HTML, audio and subtitle files. More info bellow.
+\item[text\_type]
+type of the input. The \texttt{aeneas} DOM filter produces
+\texttt{unparsed} text type.
+\item[id\_sort]
+sorting of id attributes. Default value is \texttt{numeric}.
+\item[id\_regex]
+regular expression to parse the id attributes.
+\item[sub\_format]
+generated subtitle format. Default \texttt{smil}.
+\end{description}
+
+Additional parameters for the job configuration file:
+
+\begin{itemize}
+\tightlist
+\item
+ description
+\item
+ prefix
+\item
+ config\_name
+\item
+ keep\_config
+\end{itemize}
+
+It is possible to generate multiple HTML files from the LaTeX source.
+For example, \texttt{tex4ebook} generates separate file for each chapter
+or section. It is possible to set options for each HTML file, in
+particular names of the corresponding audio files. This mapping is done
+using \texttt{map} parameter.
+
+Example:
+
+\begin{verbatim}
+filter_settings "aeneas-config" {
+ map = {
+ ["sampleli1.html"] = {audio_file="sample.mp3"},
+ ["sample.html"] = false
+ }
+}
+\end{verbatim}
+
+Table keys are the configured file names. It is necessary to insert them
+as \texttt{{[}"filename.html"{]}}, because of Lua syntax rules.
+
+This example maps audio file \texttt{sample.mp3} to a section subpage.
+The main HTML file, which may contain title and table of contents
+doesn't have a corresponding audio file.
+
+Filenames of the sub files corresponds to the chapter numbers, so they
+are not stable when a new chapter is added. It is possible to request
+file names interfered from the chapter titles using the
+\texttt{sec-filename} option or \texttt{tex4ht}.
+
+Available \texttt{map} options:
+
+\begin{description}
+\item[audio\_file]
+the corresponding audio file
+\item[sub\_file]
+name of the generated subtitle file
+\end{description}
+
+The following options are same as their counter-parts from the main
+parameters table and generally don't need to be set:
+
+\begin{itemize}
+\tightlist
+\item
+ prefix
+\item
+ file\_desc
+\item
+ file\_id
+\item
+ text\_type
+\item
+ id\_sort
+\item
+ id\_prefix
+\item
+ sub\_format
+\end{itemize}
+
+Full example:
+
+\begin{verbatim}
+local domfilter = require "make4ht-domfilter"
+local aeneas_config = require "make4ht-aeneas-config"
+
+filter_settings "aeneas-config" {
+ map = {
+ ["krecekli1.xhtml"] = {audio_file="krecek.mp3"},
+ ["krecek.xhtml"] = false
+ }
+}
+
+local process = domfilter {"aeneas"}
+Make:match("html$", process)
+
+if mode == "draft" then
+ aeneas_config.process_files {}
+else
+ aeneas_config.execute {}
+end
+\end{verbatim}
+
+\hypertarget{image-conversion}{%
+\subsection{Image conversion}\label{image-conversion}}
+
+It is possible to convert parts of LaTeX input to pictures, it is used
+for example for math or diagrams in \texttt{tex4ht}.
+
+These pictures are stored in a special \texttt{dvi} file, which can be
+processed by the \texttt{dvi\ to\ image} commands.
+
+This conversion is normally configured in the \texttt{env\ file}, which
+is system dependent and which has a bit unintuitive syntax. This
+configuration is processed by the \texttt{t4ht} application and
+conversion commands are called for all pictures.
+
+It is possible to disable \texttt{t4ht} image processing and configure
+image conversion in the build file:
+
+\begin{verbatim}
+Make:image("png$",
+"dvipng -bg Transparent -T tight -o ${output} -pp ${page} ${source}")
+\end{verbatim}
+
+\texttt{Make:image} takes two parameters, pattern to match image name
+and action. Action can be either string template with conversion
+command, or function which takes a table with parameters as an argument.
+
+There are three parameters:
+
+\begin{itemize}
+\tightlist
+\item
+ \texttt{output} - output image file name
+\item
+ \texttt{source} - \texttt{dvi} file with the pictures
+\item
+ \texttt{page} - page number of the converted image
+\end{itemize}
+
+\hypertarget{the-mode-variable}{%
+\subsection{\texorpdfstring{The \texttt{mode}
+variable}{The mode variable}}\label{the-mode-variable}}
+
+There is global \texttt{mode} variable available in the build file. It
+contains contents of the \texttt{-\/-mode} command line option. It can
+be used to run some commands conditionally. For example:
+
+\begin{verbatim}
+ if mode == "draft" then
+ Make:htlatex{}
+ else
+ Make:htlatex{}
+ Make:htlatex{}
+ Make:htlatex{}
+ end
+\end{verbatim}
+
+In this example (which is the default configuration used by
+\texttt{make4ht}), LaTeX is called only once when \texttt{make4ht} is
+called with \texttt{draft} mode:
+
+\begin{verbatim}
+make4ht -m draft filename
+\end{verbatim}
+
+\hypertarget{the-settings-table}{%
+\subsection{\texorpdfstring{The \texttt{settings}
+table}{The settings table}}\label{the-settings-table}}
+
+You may want to access to the parameters also outside commands, file
+matches and image conversion functions. For example, if you want to
+convert your file to the \texttt{OpenDocument\ Format\ (ODT)}, you can
+use the following settings, based on the \texttt{oolatex} command:
+
+\begin{verbatim}
+settings.tex4ht_sty_par = settings.tex4ht_sty_par ..",ooffice"
+settings.tex4ht_par = settings.tex4ht_par .. " ooffice/! -cmozhtf"
+settings.t4ht_par = settings.t4ht_par .. " -cooxtpipes -coo "
+\end{verbatim}
+
+There are some functions to ease access to the settings:
+
+\begin{description}
+\item[\texttt{set\_settings\{parameters\}}]
+overwrite settings with values from a passed table
+\item[\texttt{settings\_add\{parameters\}}]
+add values to the current settings
+\item[\texttt{filter\_settings\ "filter\ name"\ \{parameters\}}]
+set settings for a filter
+\item[\texttt{get\_filter\_settings(name)}]
+get settings for a filter
+\end{description}
+
+Using these functions, it is possible to simplify the settings for the
+\texttt{ODT} format:
+
+\begin{verbatim}
+settings_add {
+ tex4ht_sty_par =",ooffice",
+ tex4ht_par = " ooffice/! -cmozhtf",
+ t4ht_par = " -cooxtpipes -coo "
+}
+\end{verbatim}
+
+Settings for filters and extensions can be set using
+\texttt{filter\_settings}:
+
+\begin{verbatim}
+filter_settings "test" {
+ hello = "world"
+}
+\end{verbatim}
+
+These settings can be read in the extensions and filters using
+\texttt{get\_filter\_settings}:
+
+\begin{verbatim}
+function test(input)
+ local options = get_filter_settings("test")
+ print(options.hello)
+ return input
+end
+
+\end{verbatim}
+
+\hypertarget{list-of-available-settings-for-filters-and-extensions.}{%
+\subsection{List of available settings for filters and
+extensions.}\label{list-of-available-settings-for-filters-and-extensions.}}
+
+These settings may be set using \texttt{filter\_settings} function.
+
+\hypertarget{the-tidy-extension}{%
+\subsubsection{\texorpdfstring{The \texttt{tidy}
+extension}{The tidy extension}}\label{the-tidy-extension}}
+
+\begin{description}
+\item[options]
+command line options for the \texttt{tidy} command. Default value is
+\texttt{-m\ -utf8\ -w\ 512\ -q}.
+\end{description}
+
+\hypertarget{the-fixinlines-dom-filter}{%
+\subsubsection{\texorpdfstring{The \texttt{fixinlines} dom
+filter}{The fixinlines dom filter}}\label{the-fixinlines-dom-filter}}
+
+\begin{description}
+\item[inline\_elements]
+table of inline elements which shouldn't be direct descendants of the
+\texttt{body} element. The element names should be table keys, the
+values should be true.
+\end{description}
+
+Example
+
+\begin{verbatim}
+filter_settings "fixinlines" {inline_elements = {a = true, b = true}}
+\end{verbatim}
+
+\hypertarget{the-joincharacters-dom-filter}{%
+\subsubsection{\texorpdfstring{The \texttt{joincharacters} dom
+filter}{The joincharacters dom filter}}\label{the-joincharacters-dom-filter}}
+
+\begin{description}
+\item[charelements]
+table of elements which should be joined if several instances with the
+same value of \texttt{class} attribute are side by side.
+\end{description}
+
+Example
+
+\begin{verbatim}
+filter_settings "joincharacters" { charclases = { span=true, mn = true}}
+\end{verbatim}
+
+\hypertarget{mathjaxsettings}{%
+\subsubsection{\texorpdfstring{The \texttt{mathjaxnode}
+filter}{The mathjaxnode filter}}\label{mathjaxsettings}}
+
+\begin{description}
+\item[options]
+command line options for the \texttt{mjpage} command. Default value is
+\texttt{-\/-output\ CommonHTML}
+\end{description}
+
+Example
+
+\begin{verbatim}
+filter_settings "mathjaxnode" {
+ options="--output SVG --font Neo-Euler"
+}
+\end{verbatim}
+
+\begin{description}
+\item[cssfilename]
+\texttt{mjpage} puts some CSS code into the HTML pages.
+\texttt{mathjaxnode} extracts this information and saves it to a
+standalone CSS file. Default CSS filename is \texttt{mathjax-chtml.css}
+\item[fontdir]
+directory with MathJax font files. This option enables use of local
+fonts, which is usefull in Epub conversion, for example. The font
+directory should be sub-directory of the current directory. Only TeX
+font is supported at the moment.
+\end{description}
+
+Example
+
+\begin{verbatim}
+filter_settings "mathjaxnode" {
+ fontdir="fonts/TeX/woff/"
+}
+\end{verbatim}
+
+\hypertarget{the-aeneas-filter}{%
+\subsubsection{\texorpdfstring{The \texttt{aeneas}
+filter}{The aeneas filter}}\label{the-aeneas-filter}}
+
+\begin{description}
+\item[skip\_elements]
+List of CSS selectors that match elements which shouldn't be processed.
+Default value: \texttt{\{\ "math",\ "svg"\}}.
+\item[id\_prefix]
+prefix used in the ID attribute forming.
+\item[sentence\_match]
+Lua pattern used to match a sentence. Default value:
+\texttt{"({[}\^{}\%.\^{}\%?\^{}!{]}*)({[}\%.\%?!{]}?)"}.
+\end{description}
+
+\hypertarget{the-staticsite-filter-and-extension}{%
+\subsubsection{\texorpdfstring{The \texttt{staticsite} filter and
+extension}{The staticsite filter and extension}}\label{the-staticsite-filter-and-extension}}
+
+\begin{description}
+\item[site\_root]
+directory where generated files should be copied.
+\item[map]
+table where keys are patterns that match filenames, value contains
+destination directoryfor matched files, relative to the
+\texttt{site\_root} (it is possible to use \texttt{..} to swich to
+parent directory).
+\item[file\_pattern]
+pattern used for filename generation. It is possible to use string
+templates and format strings for \texttt{os.date} function. Default
+value of \texttt{\%Y-\%m-\%d-\$\{input\}} creates names in the form of
+\texttt{YYYY-MM-DD-file\_name}.
+\item[header]
+table with variables to be set in the YAML header in HTML files. If the
+table value is a function, it is executed with current parameters and
+HTML page DOM object as arguments.
+\end{description}
+
+Example:
+
+\begin{verbatim}
+local outdir = os.getenv "blog_root"
+
+filter_settings "staticsite" {
+ site_root = outdir,
+ map = {
+ [".css$"] = "../css/"
+ },
+ header = {
+ layout="post",
+ date = function(parameters, dom)
+ return os.date("!%Y-%m-%d %T", parameters.time)
+ end
+ }
+}
+\end{verbatim}
+
+\hypertarget{the-dvisvgm_hashes-extension}{%
+\subsubsection{\texorpdfstring{The \texttt{dvisvgm\_hashes}
+extension}{The dvisvgm\_hashes extension}}\label{the-dvisvgm_hashes-extension}}
+
+\begin{description}
+\item[options]
+command line options for Dvisvgm. The default value is
+\texttt{-n\ -\/-exact\ -c\ 1.15,1.15}.
+\item[cpu\_cnt]
+number of processor cores used for conversion. The extension tries to
+detect the available cores automatically by default.
+\item[parallel\_size]
+number of pages used in each Dvisvgm call. The extension detects changed
+pages in the DVI file and construct multiple calls to Dvisvgm with only
+changed pages.
+\item[scale]
+SVG scaling.
+\end{description}
+
+\hypertarget{the-odttemplate-filter-and-extension}{%
+\subsubsection{\texorpdfstring{The \texttt{odttemplate} filter and
+extension}{The odttemplate filter and extension}}\label{the-odttemplate-filter-and-extension}}
+
+\begin{description}
+\item[template]
+filename of the template \texttt{ODT} file
+\end{description}
+
+\texttt{odttemplate} can also get the template filename from the
+\texttt{odttemplate} option from \texttt{tex4ht\_sty\_par} parameter. It
+can be set useing following command line call, for example:
+
+\begin{verbatim}
+ make4ht -f odt+odttemplate filename.tex "odttemplate=template.odt"
+\end{verbatim}
+
+\hypertarget{configfile}{%
+\section{Configuration file}\label{configfile}}
+
+It is possible to globally modify the build settings using the
+configuration file. New compilation commands can be added, extensions
+can be loaded or disabled and settings can be set.
+
+\hypertarget{location}{%
+\subsection{Location}\label{location}}
+
+The configuration file can be saved either in
+\texttt{\$HOME/.config/make4ht/config.lua} or in \texttt{.make4ht} in
+the current directory or it's parents (up to \texttt{\$HOME}).
+
+\hypertarget{additional-commands}{%
+\subsection{Additional commands}\label{additional-commands}}
+
+There are two additional commands:
+
+\begin{description}
+\item[\texttt{Make:enable\_extension(name)}]
+require extension
+\item[\texttt{Make:disable\_extension(name)}]
+disable extension
+\end{description}
+
+\hypertarget{example}{%
+\subsection{Example}\label{example}}
+
+The following configuration add support for the \texttt{biber} command,
+requires \texttt{common\_domfilters} extension and requires MathML
+output for math.
+
+\begin{verbatim}
+Make:add("biber", "biber ${input}")
+Make:enable_extension "common_domfilters"
+settings_add {
+ tex4ht_sty_par =",mathml"
+}
+\end{verbatim}
+
+\hypertarget{command-line-options}{%
+\section{Command line options}\label{command-line-options}}
+
+\begin{verbatim}
+make4ht - build system for tex4ht
+Usage:
+make4ht [options] filename ["tex4ht.sty op." "tex4ht op."
+ "t4ht op" "latex op"]
+-b,--backend (default tex4ht) Backend used for xml generation.
+ possible values: tex4ht or lua4ht
+-c,--config (default xhtml) Custom config file
+-d,--output-dir (default "") Output directory
+-e,--build-file (default nil) If build file name is different
+ than `filename`.mk4
+-f,--format (default nil) Output file format
+-l,--lua Use lualatex for document compilation
+-m,--mode (default default) Switch which can be used in the makefile
+-n,--no-tex4ht Disable dvi file processing with tex4ht command
+-s,--shell-escape Enables running external programs from LaTeX
+-u,--utf8 For output documents in utf8 encoding
+-v,--version Print version number
+-x,--xetex Use xelatex for document compilation
+<filename> (string) Input file name
+\end{verbatim}
+
+You can still invoke \texttt{make4ht} in the same way as
+\texttt{htlatex}:
+
+\begin{verbatim}
+make4ht filename "customcfg, charset=utf-8" "-cunihtf -utf8" "-dfoo"
+\end{verbatim}
+
+Note that this will not use \texttt{make4ht} routines for output
+directory making and copying. If you want to use them, change the line
+above to:
+
+\begin{verbatim}
+make4ht -d foo filename "customcfg, charset=utf-8" "-cunihtf -utf8"
+\end{verbatim}
+
+This call has the same effect as the following:
+
+\begin{verbatim}
+make4ht -u -c customcfg -d foo filename
+\end{verbatim}
+
+Output directory doesn't have to exist, it will be created
+automatically. Specified path can be relative to current directory, or
+absolute:
+
+\begin{verbatim}
+make4ht -d use/current/dir/ filename
+make4ht -d ../gotoparrentdir filename
+make4ht -d ~/gotohomedir filename
+make4ht -d c:\documents\windowspathsareworkingtoo filename
+\end{verbatim}
+
+\hypertarget{troubleshooting}{%
+\section{Troubleshooting}\label{troubleshooting}}
+
+\hypertarget{incorrect-handling-of-command-line-arguments-for-tex4ht-t4ht-or-latex}{%
+\subsection{\texorpdfstring{Incorrect handling of command line arguments
+for \texttt{tex4ht}, \texttt{t4ht} or
+\texttt{latex}}{Incorrect handling of command line arguments for tex4ht, t4ht or latex}}\label{incorrect-handling-of-command-line-arguments-for-tex4ht-t4ht-or-latex}}
+
+Sometimes, you may get a similar error:
+
+\begin{verbatim}
+make4ht:unrecognized parameter: i
+\end{verbatim}
+
+It may be caused by a following \texttt{make4ht} invocation:
+
+\begin{verbatim}
+make4ht hello.tex "customcfg,charset=utf-8" "-cunihtf -utf8" -d foo
+\end{verbatim}
+
+The command line option parser is confused by mixing options for
+\texttt{make4ht} and \texttt{tex4ht} in this case and tries to interpret
+the \texttt{-cunihtf\ -utf8}, which are options for \texttt{tex4ht}
+command as \texttt{make4ht} options. To fix that, you can either move
+the \texttt{-d\ foo} directly after \texttt{make4ht} command:
+
+\begin{verbatim}
+make4ht -d foo hello.tex "customcfg,charset=utf-8" "-cunihtf -utf8"
+\end{verbatim}
+
+Another option is to add a space before \texttt{tex4ht} options:
+
+\begin{verbatim}
+make4ht hello.tex "customcfg,charset=utf-8" " -cunihtf -utf8" -d foo
+\end{verbatim}
+
+The former way is preferable, though.
+
+\hypertarget{filenames-containing-spaces}{%
+\subsection{Filenames containing
+spaces}\label{filenames-containing-spaces}}
+
+\texttt{tex4ht} cannot handle filenames containing spaces.
+\texttt{make4ht} thus replaces spaces in input file names with
+underscores, so generated XML files use underscores instead of spaces as
+well.
+
+\hypertarget{filenames-containing-non-ascii-characters}{%
+\subsection{Filenames containing non-ASCII
+characters}\label{filenames-containing-non-ascii-characters}}
+
+The \texttt{odt} output doesn't support accented filenames, it is best
+to stick to ASCII characters in filenames.
+
+\hypertarget{license}{%
+\section{License}\label{license}}
+
+Permission is granted to copy, distribute and/or modify this software
+under the terms of the LaTeX Project Public License, version 1.3.