diff options
author | Norbert Preining <norbert@preining.info> | 2019-09-02 13:46:59 +0900 |
---|---|---|
committer | Norbert Preining <norbert@preining.info> | 2019-09-02 13:46:59 +0900 |
commit | e0c6872cf40896c7be36b11dcc744620f10adf1d (patch) | |
tree | 60335e10d2f4354b0674ec22d7b53f0f8abee672 /support/make4ht |
Initial commit
Diffstat (limited to 'support/make4ht')
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 + ` `, 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 = { ["&"] = "&", ["<"] = "<", [">"] = ">"} +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), " ") +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 +-- , 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 Binary files differnew file mode 100644 index 0000000000..0b7dd091ea --- /dev/null +++ b/support/make4ht/make4ht-doc.pdf 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{\ }, 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. |