[
  {
    "path": ".github/actions/make4ht-build/Dockerfile",
    "content": "FROM debian:unstable-slim\n\nLABEL \"maintainer\"=\"Michal Hoftich <michal.h21@gmail.com>\"\nLABEL \"repository\"=\"https://github.com/michal-h21/make4ht\"\nLABEL \"homepage\"=\"https://github.com/michal-h21/make4ht\"\n\nLABEL \"com.github.actions.name\"=\"LaTeX to XML\"\nLABEL \"com.github.actions.description\"=\"Convert LaTeX documents to XML with make4ht.\"\nLABEL \"com.github.actions.icon\"=\"code\"\nLABEL \"com.github.actions.color\"=\"blue\"\n\nENV DEBIAN_FRONTEND noninteractive\n\n# Install all TeX and LaTeX dependencies\nRUN apt-get update && \\\n    apt-get install --yes --no-install-recommends \\\n    make luatex texlive-base texlive-luatex texlive-latex-extra context \\\n    tidy \\\n    # texlive-fonts-recommended \\\n    fonts-noto-mono \\\n    texlive-plain-generic \\\n    texlive-latex-recommended \\\n    pandoc latexmk texlive lmodern fonts-lmodern tex-gyre fonts-texgyre \\\n    texlive-lang-english && \\\n    apt-get autoclean && apt-get --purge --yes autoremove && \\\n    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*\n\nADD entrypoint.sh /entrypoint.sh\n\nRUN chmod +x /entrypoint.sh\n\nENTRYPOINT [\"/entrypoint.sh\"]\n"
  },
  {
    "path": ".github/actions/make4ht-build/action.yml",
    "content": "name: \"LaTeX to XML\"\ndescription: \"Convert LaTeX documents to XML with make4ht\"\nruns:\n  using: \"docker\"\n  image: \"Dockerfile\"\n\n\n\n"
  },
  {
    "path": ".github/actions/make4ht-build/entrypoint.sh",
    "content": "#!/bin/bash\n\n# make4ht -um draft \nmake install SUDO=\"\"\nmake htmldoc\ncat htmldoc/make4ht-doc.html\n\n\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Build documentation\n\non: \n  push:\n    paths: \n    - README.md\n    - CHANGELOG.md\n    - make4ht-doc.tex\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n    \n    steps:\n    - uses: actions/checkout@v1\n    - name: Run a one-line script\n      run: echo Hello, world!\n    - name: Run a multi-line script\n      run: |\n        echo Add other actions to build,\n        echo test, and deploy your project.\n    - name: Generate HTML docs\n      uses: ./.github/actions/make4ht-build\n    - name: Publish the web pages\n      uses: peaceiris/actions-gh-pages@v2.5.0\n      env:\n        ACTIONS_DEPLOY_KEY: ${{ secrets.ACTIONS_DEPLOY_KEY }}\n        PUBLISH_BRANCH: gh-pages\n        PUBLISH_DIR: ./htmldoc\n"
  },
  {
    "path": ".gitignore",
    "content": "*.aux\n*.lg\n*.log\n*.tmp\n*.xref\n*.4tc\n*.4ct\n*.swp\n*.idv\n*.dvi\n*.fls\n*.html\n*.out\n*.pdf\nbackup\nbuild\n*.toc\n*.fdb_latexmk\ndoc\ntags\n*.css\nreadme.tex\nchangelog.tex\nmake4ht-char-def.lua\n"
  },
  {
    "path": ".travis.yml",
    "content": "dist: bionic\n\ninstall:\n  # Local\n  - sudo apt-get install -qq luatex texlive-base texlive-luatex luarocks\n  # Global \n  - sudo apt-get install -qq pandoc latexmk texlive texlive-xetex  texlive-fonts-recommended fonts-lmodern tex-gyre fonts-texgyre fonts-noto\n\nscript:\n  # Already runs locally\n  - ./make4ht -v\n  # - luarocks --local install busted\n  - git fetch --tags\n  - make\n  - make justinstall\n  # - sudo ln -s /home/travis/texmf/scripts/lua/make4ht/make4ht /usr/local/bin/make4ht\n  # - make test\n  # Now runs globally\n  - make4ht -v\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n- 2026/05/11\n\n  - added new `mkutils` function, `escape_pattern`\n  - use `escape_pattern` to sanitize output file names\n    https://tex.stackexchange.com/q/762690/2891\n  - look for images outside of the build dir in the ODT output.\n    https://tex.stackexchange.com/q/762690/2891\n\n- 2026/05/10\n\n  - set anchor id in lists to the `<li>` element in the `itemparagraphs` DOM filter.\n\n- 2026/02/24\n\n  - version `0.4e` released.\n\n- 2026/02/18\n\n  - consecutive `<mn>` elements in MathML are now merged only within parent elements whose meaning does not depend on the order of their child elements.\n\n- 2026/01/31\n\n  - remove `#` character from internal links in the JATS output (it needs to be here to distinguish internal and external links).\n\n- 2026/01/29\n\n  - removed the `<script>` element from list of inline elements in the `fixinlines` DOM filter.\n  - use `article` instead of `ltxdoc` class, because the latter results in infinite loop with some verbatims.\n\n- 2025/10/01\n\n  - added the `tablecaption` DOM filter. It moves `<caption>` elements as the first child of `<table>`, to create a valid HTML code.\n  https://puszcza.gnu.org.ua/bugs/index.php?315\n\n- 2025/08/16\n\n  - added support for the MathML `intent` and `arg` attributes in the `mathml-fixes` DOM filter.\n\n- 2025/08/14\n\n  - produce fatal error if `Make:autohtlatex` fails.\n\n- 2025/06/27\n\n  - fixed `latexmk_build` extension to work with the current build process.\n\n- 2025/06/02\n\n  - use `--embed-bitmaps` option for `dvisvgm` in the `dvisvgm_hashes` extension.\n\n- 2025/04/16 \n\n  - added support for numbers in `id` attributes created by the `sectionid` DOM filter.\n\n- 2025/04/14 \n\n  - version `0.4d` released.\n\n- 2025/04/11\n\n  - support `--build-dir` in `autohtlatex`\n\n- 2025/03/12\n\n  - added `Make:autohtlatex` function to automate \\LaTeX\\ compilation, ensuring\n    repeated runs until temporary file checksums stabilize or a max limit is reached.\n    This function is used by default instead of `Make:htlatex`.\n\n- 2025/03/02 \n\n  - fixed bug in the `make4ht-indexing` library. If subentries generated by\n    Xindex keywords were only numbers, they were mistaken for the page numbers.\n    https://tug.org/pipermail/tex4ht/2025q1/003699.html\n\n- 2025/02/19 \n\n  - version `0.4c` released.\n\n- 2025/02/03 \n\n  - added the `nodynamicodt` extension. It removes dynamic content from ODT files.\n    https://puszcza.gnu.org.ua/bugs/?505#discussion\n\n- 2025/01/15\n\n  - fixed handling of commands with number as optional arguments in the `make4ht-indexing` library.\n  https://fosstodon.org/@juergen_hubert@thefolklore.cafe/113809011903088411\n\n- 2025/01/09\n\n  - convert horizontal rules in MathML arrays to the `rowlines` attribute.\n    https://tex.stackexchange.com/a/734616/2891\n\n- 2025/01/06\n\n  - use Unicode characters for MathML font styles.\n    https://tex.stackexchange.com/a/734331/2891\n\n- 2024/12/23 \n\n  - fixed bug in the `make4ht-indexing` library. It didn't handle index entries\n    with the duplicate locator number that were broken over several lines correctly.\n    https://tex.stackexchange.com/a/733106/2891\n\n- 2024/12/20 \n\n  - remove duplicate index locators in the `make4ht-indexing` library.\n    https://tex.stackexchange.com/a/733106/2891\n\n- 2024/12/19\n\n  - fixed fatal error caused by handling comments in the `fixinlines` DOM filter.\n\n- 2024/12/17\n\n  - keep track of braces around index entries page numbers.\n    https://tex.stackexchange.com/a/733106/2891\n\n- 2024/12/08\n\n  - support other index locators than index command counters.\n    https://tex.stackexchange.com/a/732446/2891\n\n- 2024/12/05 \n\n  - recognize inline math and comments as inline content in the `fixinlines` DOM filter.\n\n- 2024/11/09 \n\n  - fixed `tablerows` for longtables longer than 200 rows.\n    https://tex.stackexchange.com/a/730466/2891\n\n- 2024/10/22 \n\n  - version `0.4b` released.\n  - test for the existence of the class atribute of tables before performing string matches in the `tablerows` DOM filter.\n\n- 2024/10/21 \n\n  - version `0.4a` released.\n\n- 2024/09/30\n\n  - remove last empty row in `longtable` environments.\n\n- 2024/09/25 \n\n  - added `make4ht-char-def` library to remove dependency on `char-def` from\n    ConTeXt. It is used by the `sectionid` DOM filter. The library is\n    automatically created from UnicodeData.txt by `tools/make_chardata.lua`.\n    https://tex.stackexchange.com/q/727202/2891\n\n- 2024/09/18 \n\n  - print error messages from the `log` file.\n\n- 2024/08/22 \n\n  - print debug message with XML parsing error context\n\n- 2024/08/21 \n\n  - try the HTML parser for DOM filters if XML parser fails.\n\n- 2024/06/18 \n\n  - changed default scaling in `dvisvgm_hashes` to 1.4.\n\n- 2024/06/17\n\n  - fixed support for index entries that are broken over several lines.\n\n- 2024/05/29\n\n  - create temporary Lg file for Xtpipes.\n    https://github.com/michal-h21/make4ht/issues/148#issuecomment-2136160818\n  - fixed handling of the generated ODT file, don't copy it unnecessarily and\n    handle build and output directories.\n    https://github.com/michal-h21/make4ht/issues/148#issuecomment-2136160818\n\n- 2024/04/23\n\n  - test if referenced files are contained in the `build-dir` directory, use the current dir otherwise.\n    https://github.com/michal-h21/tex4ebook/issues/126\n\n- 2024/04/16\n\n  - fixed bug in `dvisvgm_hashes` log parsing. On Windows, it didn't expect `\\r` characters at the line ends, they then ended in new SVG filenames. \n\n- 2024/04/15\n\n  - made the `make` command used in the `dvisvgm_hashes` extension configurable.\n    https://tex.stackexchange.com/q/715633/2891\n\n- 2024/03/29\n\n  - ignore numbers in braces in `make4ht-indexing` library.\n    https://tex.stackexchange.com/q/714247/2891\n\n- 2024/02/22\n\n  - version `0.4` released\n  - fixed name of the `<title-group>` element in JATS output.\n\n- 2024/02/21\n\n  - add standalone minus sign to the `<mn>` element in MathML.\n\n- 2024/01/26\n\n  - fixed passing of the params table in the `staticsite` extension.\n\n- 2024/01/22\n\n  - don't enable parsing of void elements in DOM filter with XML output formats.\n\n- 2023/12/15\n\n  - call `fix_rel_mo` MathML fix only in the ODT output.\n\n- 2023/12/11\n\n  - fixed handling of the `--build-dir` argument in `dvisvgm_hashes` extension.\n\n- 2023/11/03\n\n  - remove leading dashes in ids created by the `sectionid` DOM filter.\n\n- 2023/10/26\n\n  - check that removed elements in the `sectionid` DOM filter are `<a>` elements.\n\n- 2023/10/25\n\n  - fixed addition of named IDs to figures.\n\n- 2023/10/19\n\n  - print info about packages with no corresponding `.4ht` file.\n\n- 2023/10/05\n\n  - added fix for LibreOffice's bug regarding relation type math operators.\n\n- 2023/10/03\n\n  - fixed most features that didn't work with the `--build-dir` argument.\n\n- 2023/10/02\n\n  - working on new command line argument `--build-dir`. It should allow moving of temporary files to a temporary directory. The idea and most of the code comes from Robin Seth Ekman.\n\n- 2023/09/07\n\n  - fix spacing for the `dcases*` environment in the `mathml_fixes` DOM filter.\n\n- 2023/08/24\n\n  - support non-numerical values in index entry destinations (for example roman numerals).\n\n- 2023/08/22\n\n  - updated list of DOM filters used by the `common_domfilters` extension, and documented that it is used automatically in the HTML output.\n\n- 2023/08/12\n\n  - remove unnecessary `<a>` elements with `id` attributes and set the `id` on the parent.\n\n- 2023/07/06\n\n  - add prefix to section IDs if the section name is empty.\n\n- 2023/06/20\n\n  - added the `copy_images` extension.\n\n- 2023/06/14\n\n  - fixed bug in the `mathmlfixes` DOM filter -- non-empty last rows were\n    removed if they contained only one element.\n\n- 2023/05/26\n\n  - load `tex4ht.sty` before input file processing starts.\n\n- 2023/04/19\n\n  - handle additional characters in the `idcolons` DOM filter.\n\n- 2023/04/06\n\n  - fixed handling of ID attributes in the `idcolons` DOM filter.\n\n- 2023/03/07\n\n  - remove empty rows in `longtable`.\n\n- 2023/02/24\n\n  - version `0.3k` released.\n\n- 2023/01/09\n\n  - fixed detection of image file names in `mkutils.parse_lg()`\n\n- 2022/11/25\n\n  - reverted change of index page numbers, it was buggy\n  - test if the `.idx` file exists.\n\n- 2022/11/24\n\n  - `make4ht-indexing`: fixed handling of numbers in index entries text.\n\n- 2022/11/01\n\n  - remove empty last rows in MathML tables.\n\n- 2022/10/21\n\n  - added the `inlinecss` DOM filter and extension with the same name.\n\n- 2022/09/29\n\n  - the `join_characters` DOM filter now shouldn't produce extra `<span>` elements after white space.\n\n- 2022/09/16\n\n  - use the `no^` option to compile the `make4ht` HTML docs, to prevent clash with the Doc package.\n\n- 2022/07/22\n\n  - `mathmlfixes` DOM filter: \n    - don't change `<mo>` to `<mtext>` if the element contain the `stretchy` attribute.\n    - add `<mtext>` to `<mstyle>` if it contains only plain text\n\n- 2022/07/08\n\n  - configure elements used in `join_characters` DOM filter.\n  - added support for the `mml:` prefix in `mathml_fixes` DOM filter.\n\n- 2022/06/28\n\n  - handle `\\maketitle` in JATS.\n\n- 2022/06/24\n  \n  - handle internal and external links in the JATS output.\n  - better detection of empty paragraphs.\n\n- 2022/06/16\n\n  - use DOM filters to fix JATS output.\n\n- 2022/04/22\n\n  - use more explicit options for `latexmk`.\n\n- 2022/04/19\n\n  - remove all `htlatex` calls from the build sequence when the `latexmk_build` extension is used.\n  - fixed other issues that caused spurious executions of `latexmk`.\n\n- 2022/04/01\n\n  - don't copy files to the output dir if it wasn't requested\n  - fixed copying of the ODT file to the output dir.\n\n- 2022/03/29\n\n  - check if tidy return non-empty string in the `tidy` extension.\n\n- 2022/03/24\n\n  - don't use totally random names in the `preprocess_input` extension, in order to support images correctly.\n\n- 2022/03/22\n\n  - version `0.3l` released.\n  - fixed issues with filenames on Windows.\n\n- 2022/03/01\n\n  - use `rmarkdown` package to process `.rmd` files in the `preprocess_input`\n    extension (thanks to James Clawson).\n\n- 2022/02/18\n\n  - version `0.3k` released.\n\n- 2022/02/07\n\n  - fixed support for some fonts in the ODT format.\n  - added `odtfonts` DOM filter.\n\n- 2022/01/30\n\n  - fix `mathvariant` attribue of `<mi>` elements if they are children of `<mstyle>`.\n\n- 2021/12/17\n\n  - quote jobname in order to support filenames like `(xxx).tex`.\n\n- 2021/12/13\n\n  - fixed setting of properties in the `staticsite` filter.\n\n- 2021/12/06\n\n  - in the end, use `<mtext>` even for one `<mo>` in the `fix_operators` function. LO \n    had issues with `<mi>`. \n\n- 2021/12/03\n\n  - don't add additional `<mrow>` elements in the `mathmlfixes` DOM filter. It caused \n    various issues.\n\n- 2021/12/01\n\n  - transform `<mn>x</mn><mo>.</mo><mn>x</mn>` to `<mn>x.x</mn>` in MathML.\n  - transform `<mo>` elements that are single childs to `<mi>` in MathML, and\n    list of consecutive `<mo>` elements to `<mtext>`. This should fix rendering\n    issues of superscripts in LibreOffice.\n  - added filter names in extensions to prevent multiple execution of filters.\n\n- 2021/11/29\n \n  - make current logging level available outside of the Logging module.\n  - print Xtpipes and Tidy output if these command fail in the Xtpipes module.\n\n- 2021/11/18\n\n  - don't put `<mrow>` as children of `<mrow>` in the `mathmlfixes` DOM filter.\n\n- 2021/11/04\n\n  - more intelligent handling of text and inline elements outside of paragraphs\n    in the `fixinlines` DOM filter.\n\n- 2021/10/11\n\n  - version `0.3j` released.\n\n- 2021/10/09\n\n  - fixed wrong DOM object name in the ODT format.\n  - add addtional `<mrow>` elements when necessary.\n\n- 2021/09/30\n\n  - version `0.3i` released.\n\n- 2021/09/21\n\n    - run DOM parse in sandbox in the ODT format picture size function.\n\n- 2021/09/20\n\n    - remove LaTeX commands from TOC entries in `sectionid` DOM filter.\n\n- 2021/09/09\n\n    - corrected SVG dimension setting in the ODT output. Dimensions are set also for PNG and JPG pictures.\n\n- 2021/09/05\n\n    - corrected detection of closing brace in CSS style in `mjcli` filter.\n\n- 2021/08/13\n\n    - use LaTeX new hook mechanism to load `tex4ht.sty` before document class.\n      It fixes some issues with packages required in classes.\n\n- 2021/08/12\n\n    - correctly set dimensions for `SVG` images in the `ODT` format.\n\n- 2021/07/29\n\n    - sort YAML header in the `staticsite` filter.\n\n- 2021/07/25\n\n    - version `0.3h` released.\n\n- 2021/07/25\n\n    - use current directory as default output dir in `staticsite` extension.\n\n- 2021/07/23\n\n    - fixed detection of single paragraphs inside `<li>` in the `itemparagraphs` DOM filter.\n\n- 2021/07/18\n \n    - remove elements produced by `\\maketitle` in the `staticsite` extension.\n\n- 2021/07/05\n\n    - sort colors alphabetically in the `joincolors` DOM filter to enable reproducible builds.\n\n- 2021/06/26\n\n    - rewrote the `collapsetoc` DOM filter.\n\n- 2021/06/20\n\n    - test for the `svg` picture mode in the `tex4ht` command. Use the `-g.svg`\n      option if it is detected. This is necessary for correct support of\n      pictorial characters.\n\n- 2021/06/16\n\n    - better handling of duplicate ID attributes in `sectionid` DOM filter.\n    - support `notoc` option in `sectionid`.\n\n- 2021/06/13\n\n    - added `itemparagraphs` DOM filter. It removes unnecessary paragraphs from `<li>` elements.\n\n- 2021/05/06\n\n    - remove `<hr>` elements in `.hline` rows in `tablerows` DOM filter.\n\n- 2021/05/01\n\n    - added function `mkutils.isModuleAvailable`. It checks if Lua library is available.\n    - check for `char-def` library in `sectionid` DOM filter.\n\n- 2021/04/08\n\n    - removed `build_changed`. New script, [siterebuild](https://github.com/michal-h21/siterebuild), should be used instead.\n    - new DOM filter, `sectionid`. It uses sanitized titles instead of automatically generated numbers as section IDs.\n    - added `sectionid` to `common_domfilters`.\n    - use `context` in the Docker file, because it contains the `char-def.lua` file.\n\n- 2021/03/20\n\n    - use `kpse` library when files are copied to the output directory.\n    - added `clean` mode. It removes all generated, temporary and auxilary files.\n\n- 2021/03/19 \n\n    - version `0.3g` released.\n\n- 2021/02/08\n\n    - remove `<?xtpipes ?>` processing instructions from the generated ODT file.\n\n- 2021/02/01\n\n    - better error messages when extension cannot be loaded.\n    - added `mjcli` extension.\n    - `mjcli` filter supports \\LaTeX\\ syntax.\n    - updated documentation.\n\n- 2021/01/31\n\n    - added new MathJax Node filter, `mjcli`.\n\n- 2020/12/19\n\n    - build web documentation only when documentation sources change.\n\n- 2020/11/22\n\n    - set exit status for the `make4ht` command.\n\n- 2020/11/22\n\n    - new extension, `build_changed`. \n\n- 2020/11/01\n\n    - fix deprecated `<mfenced>` element in MathML \n    - convert `<mo fence>` elements to `<mfenced>` in `ODT` format.\n\n- 2020/10/28\n\n    - fixed handling of nested `<span>` elements in `joincharacters` DOM filter.\n\n- 2020/10/25\n\n    - fixed command name for `Make:httex`, it was `Make:htttex`.\n\n- 2020/10/17\n\n    - generate YAML header for all generated files with the `staticsite` extension.\n\n- 2020/09/17\n\n    - require `mathml` option when `mathjaxnode` extension is used.\n\n- 2020/09/07\n\n    - version `0.3f` released.\n\n- 2020/08/26\n\n    - `fixinlines` DOM filter: added `<a>` element into list of inline elements.\n\n- 2020/08/24\n\n    - initialize attributes in new element in `mathmlfixes` DOM extension.\n\n- 2020/07/18\n\n    - changed CSS for the HTML documentation.\n\n- 2020/07/17\n\n    - fixed bug in index parsing.\n\n- 2020/07/10\n\n    - use the `joincharacters` DOM filter for TEI output.\n\n- 2020/07/08\n\n    - don't fail when filename cannot be detected in `make4ht-errorlogparser.lua`.\n\n- 2020/05/27\n\n    - test if copied file exists in `mkutils.cp`.\n    \n- 2020/05/19\n\n    - fixed image filename replace in `dvisvgm_hashes` extension.\n\n- 2020/05/16\n\n    - fixed HTML filename matching in extensions.\n\n- 2020/05/08\n\n    - use global environment in the build files.\n\n- 2020/03/03\n\n    - added `jats` format.\n\n- 2020/02/28\n\n    - version `0.3e released`.\n\n- 2020/02/24\n\n    - `t4htlinks` DOM filter: cleanup file names from internal links.\n    - `make4ht-indexing`: added support for splitindex.\n\n- 2020/02/19\n\n    - use `UTF-8` output by default. `8-bit` output is broken and non fixable.\n\n- 2020/02/07\n\n    - use `lualatex-dev` instead of `harflatex`\n\n- 2020/02/06\n\n    - added support for `harflatex` and `harftex` in the `detect_engine` extension.\n\n- 2020/01/22\n\n    - version `0.3d` released.\n    - added `Make:httex` command for Plain TeX support.\n    - added `detect_engine` extension. It supports detection of the used engine\n      and format from TeX Shop or TeXWorks magic comments. These comments can\n      look like: `%!TEX TS-program = xelatex`.\n\n- 2020/01/22\n\n    - fixed support for multiple indices in `make4ht-indexing.lua`.\n\n- 2019/12/29\n\n    - use the `mathvariant=\"italic\"` attribute for joined `<mi>` elements.\n    - fixed comparison of element attributes in `joincharacters` DOM filter.\n\n- 2019/12/28\n\n    - print warning if the input file doesn't exist.\n\n- 2019/12/17\n\n    - added `booktabs` DOM filter.\n    - load the `booktabs` in `common_domfilters` by default. \n\n- 2019/12/14\n\n    - fixed bug in the `tablerows` DOM filter -- it could remove table rows if\n      they contained only one column with elements that contained no text\n      content.\n\n- 2019/11/28\n\n    - version `0.3c` released.\n    - updated `mathmlfixes` DOM filter. It handles `<mstyle>` element inside token elements now.\n    - use `mathmlfixes` and `joincharacters` DOM filters for math XML files in the ODT output.\n\n- 2019/11/25\n\n    - added `pythontex` command.\n    - added `mathmlfixes` DOM filter.\n    - use the `mathmlfixes` DOM filter in `common_domfilters` extension.\n\n- 2019/11/22\n\n    - `make4ht-joincharacters` dom filter: added support for the  `<mi>`\n      element. Test all attributes for match when joining characters.\n    - `html5` format: use the `common_domfilters` by default.\n\n- 2019/11/03\n\n    - version `0.3b`\n    - use `make4ht-ext-` prefix for extensions to prevent filename clashes with corresponding filters.\n\n- 2019/11/01\n\n    - version `0.3a` released.\n    - added `make4ht-` prefix to all extensions and formats\n    - removed the unused `mathjaxnode.lua` file.\n\n- 2019/11/01\n\n    - version `0.3` released.\n    - added `Make:makeindex`, `Make:xindex` and `Make:bibtex` commands.\n\n- 2019/10/25\n\n    - modified the `Make:xindy` command to use the indexing mechanism.\n\n- 2019/10/24\n\n    - added functions for preparing and cleaning of the index files in `make4ht-indexing.lua`.\n\n- 2019/10/23\n\n    - replaced `os.execute` function with `mkutils.execute`. It uses the logging mechanism for the output.\n    - finished transforming of filters, extensions and formats to the logging system.\n\n- 2019/10/22\n\n    - added `tablerows` domfilter.\n    - added the `tablerows` domfilter to the `common_domfilters` extension.\n    - converted most of the filters to use the logging mechanism.\n\n- 2019/10/20\n\n    - added `status` log level.\n\n- 2019/10/18\n\n    - converted most print commands to use the logging mechanism.\n    - added `output` log level used for printing of the commands output.\n\n- 2019/10/17\n\n    - added `--loglevel` CLI parameter.\n    - added logging mechanism.\n    - moved `htlatex` related code to `make4ht-htlatex.lua` from `mkutils.lua`\n\n- 2019/10/11\n\n    - added `xindy` settings.\n    - added simple regular expression to detect errors in the log file, because log parsing can be slow.\n\n- 2019/10/09\n\n    - added the `interaction` parameter for the `htlatex` command. The default\n      value is `batchmode` to suppress the user input on errors, and to\n      suppress  full log output to the terminal.\n    - added the `make4ht-errorlogparser` module. It is used to parse errors in\n      the `htlatex` run unless `interaction` is set to `errorstopmode`.\n\n- 2019/10/08\n\n    - set up Github Actions pipeline to compile the documentation to HTML and publish it at https://www.kodymirus.cz/make4ht/make4ht-doc.html.\n\n- 2019/10/07\n\n    - don't move the `common_domfilters` extension to the first place in the\n      file matches pipeline. We may want to run `tidy` or regex filters first,\n      to fix XML validation errors.\n\n- 2019/10/04\n\n    - added HTML documentation.\n\n- 2019/09/27\n\n    - don't convert Latin 1 entities to Unicode in the `entities_to_unicode` extension.\n\n- 2019/09/20\n\n    - fixed bugs in the temporary directory handling for the ODT output.\n\n- 2019/09/13\n\n    - added `preprocess_input` extension. It enables compilation of formats\n      supported by [Knitr](https://yihui.name/knitr/) (`.Rnw`, `.Rtex`, `.Rmd`, `.Rrst`) \n      and also Markdown and reStructuredText formats.\n\n- 2019/09/12\n\n    - added support for the ODT files in `common_domfilters` extension.\n    - renamed `charclases` option for the `joincharacters` DOM filter to `charclasses`.\n    - don't execute the `fixentities` filter before Xtpipes, it makes no sense.\n\n- 2019/09/11\n\n    - added support for Biber in the build files.\n\n- 2019/08/28\n\n    - added support for input from `stdin`.\n\n- 2019/08/27\n\n    - fixed `-jobname` detection regex.\n    - added function `handle_jobname`.\n    - added the `--jobname` command line option.\n\n- 2019/08/26\n\n    - quote file names and paths in `xtpipes` and `tidy` invocation.\n\n- 2019/08/25\n\n    - the issue tracker link in the help message is now configurable.\n    - fixed bug in the XeTeX handling: the `.xdv` argument for `tex4ht` wasn't\n      used if command line arguments for `tex4ht` were present.\n\n- 2019/07/03\n\n    - new DOM filter: `odtpartable`. It fixes tables nested in paragraphs in the ODT format.\n\n- 2019/06/13\n\n    - new DOM extension: `collapsetoc`.\n\n- 2019/05/29\n\n    - new module: `make4ht-indexing` for working  with index files.\n\n- 2019/05/24\n\n    - version 0.2g released\n    - fixed failing `dvisvgm_hashes` extension on Windows.\n\n- 2019/05/02\n\n    - fixed infinite loop bug in the `dvisvgm_hashes` extension.\n\n- 2019/04/09\n\n    - `make4ht-joincolors` fix: remove the hash character from the color name.\n      This caused issues with colors specified in the hexadecimal format.\n\n- 2019/04/02\n\n    - `dvisvgm_hashes` fix:  update also the lgfile.images table with generated filenames, in order to support tex4ebook\n\n- 2019/04/01\n  \n    - fixed bug in `dvisvgm_hashes` extension: didn't check for table index existence in string concenation\n\n- 2019/03/21\n\n    - version 0.2f released \n\n- 2019/03/15\n\n    - check for the image dimensions existence in the `odtimagesize` domfilter.\n\n- 2019/03/13\n\n    - don't use `odtimagesize` domfilter in the `ODT` format, the issue it fixes had been resolved in `tex4ht`.\n\n- 2019/03/08\n\n    - use `%USERPROFILE` for home dir search on Windows.\n\n- 2019/01/28\n\n    - added `joincolors` domfilter and `join_colors` extension. It can join CSS rules created for the LaTeX colors and update the HTML file.\n\n- 2019/01/22\n\n    - version 0.2e released\n    - updated the `odttemplate` filter. It will use styles from the generated ODT file that haven't been present in the template file.\n\n- 2019/01/10\n\n    - version 0.2d released\n\n- 2019/01/05\n\n    - added `docbook` and `tei` output formats.\n\n- 2018/12/19\n\n    - new library: `make4ht-xtpipes.lua`. It contains code for xtpipes handling.\n    - moved Xtpipes handling code from `formats/odt.lua`.\n\n- 2018/12/18\n\n    - new filter: `odttemplate`. It can be used for replacing style in a generated `ODT` file by a style from another existing `ODT` file.\n    - new extension: `odttemplate`. Companioning extension for filter with the same name.\n    - fixed bug in `make4ht-filters.lua`: the parameters table haven't been passed to filters.\n\n- 2018/12/17\n\n    - fixed extension handling. The disabling from the command line didn't take\n      precedence over extensions enabled in the config file. Extensions also\n      could be executed multiple times.\n\n- 2018/11/08\n\n    - removed replacing newlines by blank strings in the `joincharacters` domfilter. The issue it fixed doesn't seem to exist anymore, and it ate spaces sometimes.\n\n- 2018/11/01\n\n    - added `t4htlinks` domfilter\n    - fixed the `xtpipes` and `filters` execution order in the `ODT` format\n\n- 2018/10/26\n\n    - fixed ODT generation for files that contains special characters for Lua string patterns\n    - replace non-breaking spaces with entities. It caused issues in LO \n\n- 2018/10/18\n\n    - fixed the executable installation\n\n- 2018/09/16\n\n    - added the `scale` option for `dvisvgm_hashes` extension\n\n- 2018/09/14\n\n    - require the `-dvi` option with `latexmk_build` extension\n\n- 2018/09/12\n\n    - added `xindy` command for the build file\n\n- 2018/09/03\n\n    - expanded the `--help` option\n\n- 2018/08/27\n\n    - added `odtimagesize` domfilter\n    - load `odtimagesize` by default in the ODT format\n\n- 2018/08/23\n\n    - released version 0.2c\n\n- 2018/08/21\n\n    - added processor core detection on Windows\n    - make processor number configurable\n    - updated the documentation.\n\n- 2018/08/20\n\n    - added `dvisvgm_hashes` extension\n\n- 2018/07/03\n\n    - create the `mimetype` file to achieve the ODT file validity\n\n- 2018/07/02\n\n    - disabled conversion of XML entities for &, < and > characters back to Unicode, because it breaks XML validity\n\n- 2018/06/27\n\n    - fixed root dir detection\n\n- 2018/06/26\n\n    - added code for detection of TeX distribution root for Miktex and TL\n\n- 2018/06/25\n\n    - moved call to `xtpipes` from `t4ht` to the `ODT` format drives. This should fix issues with path expansion in `tex4ht.env` in TeX distributions.\n\n- 2018/06/22\n\n    - added `mkutils.find_zip` function. It detects `zip` or `miktex-zip` executables\n\n- 2018/06/19\n\n    - added new filter: `entities-to-unicode`. It converts XML entites for Unicode characters back to Unicode.\n    - execute `entities-to-unicode` filter on text and math files in the ODT output.\n\n- 2018/06/12\n\n    - added support for direct `ODT` file packing\n\n- 2018/06/11\n\n    - new function available for formats, `format.modify_build`\n    - function `mkutils.delete_dir` for directory removal\n    - function `mkutils.mv` for file moving\n    - started on packing of the `ODT` files directly by the format, instead of `t4ht`\n\n- 2018/06/08\n\n    - added support for filenames containing spaces\n    - added support for filenames containing non-ascii characters\n    - don't require sudo for the installation, let the user to install symbolic links to `$PATH`\n\n- 2018/05/03\n\n    - released version `0.2b`\n    - bug fix: use only `load` function in `Make:run`, in order to support a local environment.\n\n- 2018/05/03\n\n    - released version `0.2a`\n    - renamed `latexmk` extension to `latexmk_build`, due to clash in TL\n\n- 2018/04/18\n\n    - `staticsite` extension:\n      - make YAML header configurable\n      - set the `time` and `updated` headers\n    - don't override existing tables in `filter_settings`\n\n- 2018/04/17\n\n    - done first version of `staticsite` extension\n\n- 2018/04/16\n\n    - check for Git repo in the Makefile, don't run Git commands outside of repo\n\n- 2018/04/15\n\n    - added `staticsite` filter\n    - working on `staticsite` extension\n\n- 2018/04/13\n\n    - use `ipairs` instead of `pairs` to traverse lists of images and image match functions\n    - load extensions in the correct order\n\n- 2018/04/09\n\n    - released version `0.2`\n    - disabled default loading of `common_domfilters` extension\n    \n\n- 2018/04/06\n\n    - added `Make:enable_extension` and `Make:disable_extension` functions\n    - documented the configuration file\n\n- 2018/03/09\n\n    - load the configuration file before extensions\n\n- 2018/03/02\n\n    - Aeneas execution works\n    - Aeneas documentation\n    - added support for `.make4ht` configuration file\n\n- 2018/02/28\n\n    - Aeneas configuration file creation works\n\n- 2018/02/22\n\n    - fixed bug in `fixinlines` DOM filter\n\n- 2018/02/21\n\n    - added Aeneas domfilter\n    - fixed bugs in `joincharacters` DOM filter\n\n- 2018/02/20\n\n    - fixed bug in `joincharacters` DOM filter\n    - make `woff` default font format for `mathjaxnode`\n    - added documentation for `mathjaxnode` settings\n\n- 2018/02/19\n\n    - fixed bug in filter loading\n    - added `mathjaxnode` extension\n\n- 2018/02/15\n\n    - use HTML5 as a default format\n    - use `common_domfilters` implicitly for the XHTML and HTML5 formats\n\n- 2018/02/12\n\n    - added `common_domfilters` extension\n    - documented DOM filters\n\n- 2018/02/12\n\n    - handle XML parsing errors in the DOM handler\n    - enable extension loading in Formatters\n\n- 2018/02/11\n\n    - fixed Tidy extension output to support LuaXML\n    - fixed white space issues with `joincharacters` DOM filter\n \n- 2018/02/09\n\n    - fixed issues with the Mathjax filter\n    - documented basic info about thd DOM filters\n    - DOM filter optimalizations\n\n- 2018/02/08\n\n    - make Tidy extension configurable\n    - documented filter settings\n\n- 2018/02/07\n\n    - added filter for Mathjax-node\n\n- 2018/02/06\n\n    - created DOM filter function\n    - added DOM filter for spurious inlinine elements\n\n- 2018/02/03\n\n    - added settings handling functions\n    - settings made available for extensions and filters\n\n- 2017/12/08\n\n    - fixed the `mk4` build file loading when it is placed in the current working dir and another one with same filename somewhere in the TEXMF tree.\n\n- 2017/11/10\n\n    - Added new filter: `svg-height`. It tries to fix height of some of the images produced by `dvisvgm`\n\n- 2017/10/06\n\n    - Added support for output format selection. Supported formats are `xhtml`, `html5` and `odt`\n    - Added support for extensions\n\n- 2017/09/10\n\n    - Added support for Latexmk\n    - Added support of `math` library and `tonumber` function in the build files\n\n- 2017/09/04\n\n    - fixed bug caused by the previous change -- the --help and --version didn't work\n\n- 2017/08/22\n\n    - fixed the command line option parsing for `tex4ht`, `t4ht` and `latex` commands\n    - various grammar and factual fixes in the documentation\n\n- 2017/04/26\n\n    - Released version `v0.1c`\n\n- 2017/03/16\n\n    - check for `TeX capacity exceeded` error in the \\LaTeX\\ run.\n\n- 2016/12/19\n\n    - use full input name in `tex_file` variable. This should enable use of files without `.tex` extension.\n\n- 2016/10/22\n\n    - new command available in the build file: `Make:add_file(filename)`. This enables filters and commands to register files to the output.\n    - use ipairs instead of pairs for traversing files and executing filters. This should ensure correct order of executions.\n\n- 2016/10/18\n\n    - new filter: replace colons in `id` and `href` attributes with underscores\n\n- 2016/01/11\n\n    - fixed bug in loading documents with full path specified\n\n- 2015/12/06 version 0.1b\n\n    - modifed lapp library to recognize `--version` and \n    - added `--help` and `--version` command line options\n\n- 2015/11/30\n\n    - use `kpse` library for build file locating\n\n- 2015/11/17\n\n    - better `-jobname` handling\n\n- 2015/09/23 version 0.1a\n\n    - various documentation updates\n   - `mozhtf` profile for unicode output is used, this should prevent ligatures in the output files\n\n- 2015/06/29 version 0.1\n\n    - major README file update\n\n\n- 2015/06/26\n\n    - added Makefile\n    - moved INSTALL instructions from README to INSTALL\n \n"
  },
  {
    "path": "INSTALL.md",
    "content": "Installation\n------------\n\nIf you use TeX Live 2015 or up-to date Miktex distributions, `make4ht` should be installed already on your system. \nYou need to install it only if you use older distribution or try new features which aren't accessible in the version\nincluded in the distributions.\n\n## Prerequisites  \n\nYou need a teX distribution such as TeX Live or Miktex. It must include `tex4ht` system, Noto fonts and `texlua` script. All modern\ndistributions include it. You also need [Pandoc](http://pandoc.org/) in order to make the documentation and \n`latexmk`, which should be included in your TeX distro.\n\n## Unix systems\n\nRun these commands:\n\n     make\n     make install\n\n`make4ht` is installed to `/usr/local/bin` directory by default. The\ndirectory can be changed by passing it's location to the `BIN_DIR` variable:\n\n    make install BIN_DIR=~/.local/bin/\n\n## Windows\n\nSee a [guide by Volker Gottwald](https://d800fotos.wordpress.com/2015/01/19/create-e-books-from-latex-tex-files-ebook-aus-latex-tex-dateien-erstellen/) on how\nto install `make4ht` and `tex4ebook`. \n\nCreate a batch file for `make4ht` somewhere in the `path`:\n\n    texlua \"C:\\full\\path\\to\\make4ht\" %*\n\nyou can find directories in the path with \n\n    path\n\ncommand, or you can create new directory and [add it to the path](http://stackoverflow.com/questions/9546324/adding-directory-to-path-environment-variable-in-windows).\n\nNote for `Miktex` users: you may need to create `texmf` directory first. See \n[this answer on TeX.sx](http://tex.stackexchange.com/questions/69483/create-a-local-texmf-tree-in-miktex).\n\n\n## Troubleshooting\n\n### Missing support for LuaLaTeX in `latexmk`\n\nIf you get the following error message:\n\n    latexmk -lualatex make4ht-doc.tex\n    Latexmk: -lualatex bad option\n    Latexmk: Bad options specified\n    Use\n      latexmk -help\n    to get usage information\n    make: *** [make4ht-doc.pdf] Error 10\n\nthen you have old version of `latexmk`. Try to replace line:\n\n    latexmk -lualatex make4ht-doc.tex\n    \nin the `Makefile` with\n\n    lualatex make4ht-doc.tex\n    lualatex make4ht-doc.tex\n    \n`latexmk` takes care of correct number of compilations needed to produce the correct document.\n\n### Need to update TeX database\n\nIf you get following error message:\n\n    /usr/local/bin/make4ht:5: module 'make4ht-lib' not found:\n    no field package.preload['make4ht-lib']\n    [kpse lua searcher] file not found: 'make4ht-lib'\n    [kpse C searcher] file not found: 'make4ht-lib'\n\nthen try to run command \n\n    texhash\n    \nthis will update the TeX file database and newly installed files should be usable.\n"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: build tags\nlua_content = make4ht $(wildcard *.lua) \nfilters = $(wildcard filters/*.lua)\ndomfilters = $(wildcard domfilters/*.lua)\nextensions = $(wildcard extensions/*.lua)\nformats = $(wildcard formats/*.lua)\ntex_content = $(wildcard *.tex)\ndoc_root = make4ht-doc\ndoc_tex = $(doc_root).tex\ndoc_file = $(doc_root).pdf\nhtmldoc = $(HTML_DOC_DIR)/$(doc_root).html\ndoc_sources = $(doc_tex) readme.tex changelog.tex tags\n\nTEXMFHOME = $(shell kpsewhich -var-value=TEXMFHOME)\nINSTALL_DIR = $(TEXMFHOME)/scripts/lua/make4ht\nMANUAL_DIR = $(TEXMFHOME)/doc/latex/make4ht\nFILTERS_DIR = $(INSTALL_DIR)/filters\nDOMFILTERS_DIR = $(INSTALL_DIR)/domfilters\nFORMATS_DIR = $(INSTALL_DIR)/formats\nEXTENSION_DIR = $(INSTALL_DIR)/extensions\nBIN_DIR = /usr/local/bin\n# expand the bin directory\nSYSTEM_DIR = $(realpath $(BIN_DIR))\nEXECUTABLE = $(SYSTEM_DIR)/make4ht\nBUILD_DIR = build\nBUILD_MAKE4HT = $(BUILD_DIR)/make4ht\nHTML_DOC_DIR = htmldoc\nVERSION:= undefined\nDATE:= undefined\n\nifeq ($(strip $(shell git rev-parse --is-inside-work-tree 2>/dev/null)),true)\n\tVERSION:= $(shell git --no-pager describe --abbrev=0 --tags --always )\n\tDATE:= $(firstword $(shell git --no-pager show --date=short --format=\"%ad\" --name-only))\nendif\n\n# use sudo for install to destination directory outise home\nifeq ($(findstring home,$(SYSTEM_DIR)),home)\n\tSUDO:=\nelse\n\tSUDO:=sudo\nendif\n\n# install the executable only if the symlink doesn't exist yet\nifeq (\"$(wildcard $(EXECUTABLE))\",\"\")\n\tINSTALL_COMMAND:=$(SUDO) ln -s $(INSTALL_DIR)/make4ht $(EXECUTABLE)\nelse\n\tINSTALL_COMMAND:=\nendif\n\nall: doc\n\ntags:\nifeq ($(strip $(shell git rev-parse --is-inside-work-tree 2>/dev/null)),true)\n\tgit fetch --tags\nendif \n\ndoc: chardef $(doc_file) readme.tex \n\nhtmldoc: chardef ${htmldoc}\n \nmake4ht-doc.pdf: $(doc_sources)\n\tlatexmk -pdf -pdflatex='lualatex \"\\def\\version{${VERSION}}\\def\\gitdate{${DATE}}\\input{%S}\"' make4ht-doc.tex\n\n$(htmldoc): $(doc_sources)\n\tmake4ht -ulm draft -c config.cfg -f html5+tidy+common_domfilters+latexmk_build -d ${HTML_DOC_DIR} ${doc_tex} \"no^\" \"\" \"\" \"\\\"\\def\\version{${VERSION}}\\def\\gitdate{${DATE}}\\\"\"\n\nreadme.tex: README.md\n\tpandoc -f markdown+definition_lists -t LaTeX README.md > readme.tex\n\nchangelog.tex: CHANGELOG.md\n\tpandoc -f markdown+definition_lists -t LaTeX CHANGELOG.md > changelog.tex\n\nbuild: chardef doc $(lua_content) $(filters) $(domfilters)\n\t@rm -rf build\n\t@mkdir -p $(BUILD_MAKE4HT)\n\t@mkdir -p $(BUILD_MAKE4HT)/filters\n\t@mkdir -p $(BUILD_MAKE4HT)/domfilters\n\t@mkdir -p $(BUILD_MAKE4HT)/extensions\n\t@mkdir -p $(BUILD_MAKE4HT)/formats\n\t@cp $(lua_content) $(tex_content)  make4ht-doc.pdf $(BUILD_MAKE4HT)\n\t@cat make4ht | sed -e \"s/{{version}}/${VERSION}/\" >  $(BUILD_MAKE4HT)/make4ht\n\t@cp $(filters) $(BUILD_MAKE4HT)/filters\n\t@cp $(domfilters) $(BUILD_MAKE4HT)/domfilters\n\t@cp $(formats)  $(BUILD_MAKE4HT)/formats\n\t@cp $(extensions)  $(BUILD_MAKE4HT)/extensions\n\t@cp README.md $(BUILD_MAKE4HT)/README\n\t@cd $(BUILD_DIR) && zip -r make4ht.zip make4ht\n\ninstall: chardef doc $(lua_content) $(filters) $(domfilters) justinstall \n\tcp  $(doc_file) $(MANUAL_DIR)\n\njustinstall: chardef\n\tmkdir -p $(INSTALL_DIR)\n\tmkdir -p $(MANUAL_DIR)\n\tmkdir -p $(FILTERS_DIR)\n\tmkdir -p $(DOMFILTERS_DIR)\n\tmkdir -p $(FORMATS_DIR)\n\tmkdir -p $(EXTENSION_DIR)\n\tcp $(lua_content) $(INSTALL_DIR)\n\t@cat make4ht | sed -e \"s/{{version}}/${VERSION}/\" >  $(INSTALL_DIR)/make4ht\n\tcp $(filters) $(FILTERS_DIR)\n\tcp $(domfilters) $(DOMFILTERS_DIR)\n\tcp $(extensions) $(EXTENSION_DIR)\n\tcp $(formats)  $(FORMATS_DIR)\n\tchmod +x $(INSTALL_DIR)/make4ht\n\techo $(wildcard $(EXECUTABLE))\n\t$(INSTALL_COMMAND)\n\nchardef:\n\ttexlua tools/make_chardata.lua > make4ht-char-def.lua\n\ttexlua tools/make_mathmlchardata.lua > make4ht-mathml-char-def.lua\n\nversion:\n\techo $(VERSION), $(DATE)\n\n.PHONY: test\ntest:\n\ttexlua test/test-mkparams.lua\n\n"
  },
  {
    "path": "README.md",
    "content": "% [![Build Status](https://travis-ci.org/michal-h21/make4ht.svg?branch=master)](https://travis-ci.org/michal-h21/make4ht)\n% HTML version of the documentation can be found [here](https://www.kodymirus.cz/make4ht/make4ht-doc.html)\n\n# Introduction\n\n`make4ht` is a build system for [\\TeX4ht](https://tug.org/tex4ht/), \\TeX\\ to XML converter. It provides a command line tool\nthat drives the conversion process. It also provides a library that can be used to create\ncustomized conversion tools. An example of such a tool is\n[tex4ebook](https://github.com/michal-h21/tex4ebook), a tool for conversion from \\TeX\\ to\nePub and other e-book formats.\n\nSee section \\ref{sec:htlatex} for some reasons why you should consider to use `make4ht` instead \nof `htlatex`, section \\ref{sec:output} talks about supported output formats and extensions and section \\ref{sec:buildfiles} \ndescribes build files, which can be used to execute additional commands or post-process the generated files.\n\n\n\n# Usage\n\nThe basic conversion from \\LaTeX\\ to `HTML` using `make4ht` can be executed using the following command:\n\n    $ make4ht filename.tex\n\nIt will produce a file named `filename.html` if the compilation goes without fatal errors.\n\n## Command line options {#clioptions}\n\\label{sec:clioptions}\n\n    make4ht - build system for TeX4ht\n    Usage:\n    make4ht [options] filename [\"tex4ht.sty op.\" \"tex4ht op.\" \n         \"t4ht op\" \"latex op\"]\n    -a,--loglevel (default status) Set log level.\n                possible values: debug, info, status, warning, error, fatal\n    -b,--backend (default tex4ht) Backend used for xml generation.\n         possible values: tex4ht or lua4ht\n    -c,--config (default xhtml) Custom config file\n    -d,--output-dir (default \"\")  Output directory\n    -B,--build-dir (default nil)  Build directory\n    -e,--build-file (default nil)  If the build filename is different \n         than `filename`.mk4\n    -f,--format  (default nil)  Output file format\n    -j,--jobname (default nil)  Set the jobname\n    -l,--lua  Use lualatex for document compilation\n    -m,--mode (default default) Switch which can be used in the makefile\n    -n,--no-tex4ht  Disable DVI file processing with tex4ht command\n    -s,--shell-escape Enables running external programs from LaTeX\n    -u,--utf8  For output documents in utf8 encoding\n    -x,--xetex Use xelatex for document compilation\n    -v,--version  Print version number\n    <filename> (string) Input filename\n\n## Option handling\n\nIt is possible to invoke `make4ht` in the same way as `htlatex`:\n\n    $ make4ht filename \"customcfg, charset=utf-8\" \"-cunihtf -utf8\" \"-dfoo\"\n\nNote that this will not use `make4ht` routines for the output directory handling. \nSee section \\ref{sec:output-dir} for more information about this issue.\nTo use these routines, change the previous listing to:\n\n    $ make4ht -d foo filename \"customcfg, charset=utf-8\" \"-cunihtf -utf8\"\n\nThis call has the same effect as the following:\n\n    $ make4ht -u -c customcfg -d foo filename\n\n\nOutput directory does not have to exist, it `make4ht` creates it automatically. \nSpecified path can be relative to the current directory, or absolute:\n\n    $ make4ht -d use/current/dir/ filename\n    $ make4ht -d ../gotoparrentdir filename\n    $ make4ht -d ~/gotohomedir filename\n    $ make4ht -d c:\\documents\\windowspathsareworkingtoo filename\n\nThe short options that do not take parameters can be collapsed:\n\n\n    $ make4ht -ulc customcfg -d foo filename\n\n## Input from the standard input\n\n\nTo pass the output from other commands to `make4ht`, use the `-` character as a\nfilename. It is best to use this feature together with the `--jobname` or `-j`\noption.\n\n    $ cat hello.tex | make4ht -j world -\n\n## Change amount of information printed on the command line\n\nBy default, `make4ht` tries to be quiet, so it hides most of the command line\nmessages and output from the executed commands. It displays status\nmessages, warnings, and errors. The logging level can be selected using the\n`--loglevel` or `-a` options. If the compilation fails, it may be useful to display more \ninformation using the `info` or `debug` levels. \n\n\n    $ make4ht -a debug faulty.tex\n\n\n\n# Difference of `make4ht` from  `htlatex` \n\\label{sec:htlatex}\n\n\n\\TeX4ht\\ system supports several output formats, most notably `XHTML`, `HTML 5`\nand `ODT`, but it also supports `TEI` or `Docbook`.\n\nThe conversion can be invoked using several scripts, which are distributed with \\TeX4ht.\nThey differ in parameters passed to the underlying commands.\n\nThese scripts invoke \\LaTeX\\ or Plain \\TeX\\ with special instructions to load\nthe `tex4ht.sty` package. The \\TeX\\ run produces a special `DVI` file \nthat contains the code for the desired output format. The produced `DVI` file\nis then processed using the `tex4ht` command, which in conjunction with the\n`t4ht` command produces the desired output files.\n\n## Passing of command line arguments to low-level commands used in the conversion\n\nThe basic conversion script provided by \\TeX4ht\\ system is named `htlatex`. It  compiles \\LaTeX\\  \nfiles to `HTML` with this command sequence:\n\n    $ latex $latex_options 'code for loading tex4ht.sty \\input{filename}'\n    $ latex $latex_options 'code for loading tex4ht.sty \\input{filename}'\n    $ latex $latex_options 'code for loading tex4ht.sty \\input{filename}'\n    $ tex4ht $tex4ht_options filename\n    $ t4ht $t4ht_options filename\n\nThe options for various parts of the system can be passed on the command line:\n\n    $ htlatex filename \"tex4ht.sty options\" \"tex4ht_options\" \"t4ht_options\" \"latex_options\"\n\nFor basic `HTML` conversion it is possible to use the most basic invocation:\n\n    $ htlatex filename.tex\n\nIt can be much more involved for the `HTML 5` output in `UTF-8` encoding:\n\n    $ htlatex filename.tex \"xhtml,html5,charset=utf-8\" \" -cmozhtf -utf8\"\n\n`make4ht` can simplify it:\n\n    $ make4ht -u filename.tex\n\nThe `-u` option requires the `UTF-8` encoding. `HTML 5` is used as the default\noutput format by `make4ht`.\n\nMore information about the command line arguments can be found in section\n\\ref{sec:clioptions}.\n\n\n## Compilation sequence\n\n`htlatex` has a fixed compilation order and a hard-coded number of \\LaTeX\\ invocations. \n\nIt is not possible to execute additional commands during the compilation.\nWhen we want to run a program that interacts with \\LaTeX, such as `Makeindex`\nor `Bibtex`, we have two options. The first option is to create a new script based on\n`htlatex` and add the wanted commands to the modified script. The second option\nis to execute `htlatex`, then the additional and then `htlatex` again. The\nsecond option means that \\LaTeX\\ will be invoked six times, as each call to\n`htlatex` executes three calls to \\LaTeX. This can lead to significantly long\ncompilation times. \n\n`make4ht` provides a solution for this issue using a build file, or extensions.\nThese can be used for interaction with external tools.\n\n`make4ht`  also provides compilation modes, which enables to select commands that\nshould be executed using a command line option.\n\nThere is a built-in `draft` mode, which invokes \\LaTeX\\ only once, instead of\nthe default three invocations.  It is useful for the compilations of the\ndocument before its final stage, when it is not important that  all\ncross-references work. It can save quite a lot of the compilation time:\n\n    $ make4ht -um draft filename.tex\n\nAnother buil-in mode is `clean`. It executes the `Make:clean()` command to\nremove all generated and temporary files from the current directory. \nNo \\LaTeX\\ compilation happens in this mode. \n\nIt should be used in this way:\n    \n    # copy generated files to a direcory\n    $ make4ht -d outdir filename.tex \n    # remove all generated files in the current dir\n    # the -a info option will print files that are removed\n    $ make4ht -m clean -a info filename.tex\n    \n\nMore information about the build files can be found in section \\ref{sec:buildfiles}.\n\n## Handling of the generated files\n\\label{sec:output-dir}\n\nThere are also issues with the behavior of the `t4ht` application. It reads the \n`.lg` file generated by the `tex4ht` command. This file contains\ninformation about the generated files, `CSS` instructions, calls to the external\napplications, instructions for image conversions, etc. \n\n\n`t4ht` can be instructed to copy the generated files to an output directory, but\nit doesn't preserve the directory structure. When the images are placed in a  \nsubdirectory, they will be copied to the output directory, losing the directory structure.\nLinks will be pointing to a non-existing subdirectory. The following command\nshould copy all output files to the correct destinations.\n\n    $ make4ht -d outputdir filename.tex\n\n`make4ht` can also output temporary files to a build directory, thanks to the `--build-dir` (or `-B`)\noption. The following command with put `.aux`, `.4tc` and other auxiliary files to the\n`build` dir, and the generated `.html` and `.css` files to the `outputdir` directory.\n\n    $ make4ht -B build -d outputdir filename.tex\n\n## Image conversion and postprocessing of the generated files\n\n\\TeX4ht\\ can convert parts of the document to images. This is useful \nfor diagrams or complicated math, for example.\n\nBy default, the image conversion is configured in a\n[`.env` file](https://www.tug.org/applications/tex4ht/mn34.html#mn35.html).\nIt has a bit of strange syntax,  with \noperating system dependent rules.\n`make4ht` provides simpler means for the image conversion in the build files.\nIt is possible to change the image conversion parameters without a need to modify the `.env` file.\nThe process is described in section \\ref{sec:imageconversion}.\n\nIt is also possible to post-process the generated output files. The post-processing can be done\neither using external programs such as `XSLT` processors and `HTML Tidy` or\nusing `Lua` functions. More information can be found in section \\ref{sec:postprocessing}.\n\n\n\n# Output file formats and extensions\n\\label{sec:output}\n\nThe default output format used by `make4ht` is `html5`. A different\nformat can be requested using the `--format` option. Supported formats are:\n\n - `xhtml`\n - `html5`\n - `odt`\n - `tei`\n - `docbook`\n\nThe `--format` option can be also used for extension loading.\n\n## Extensions\n\nExtensions can be used to modify the build process without the need to use a build file. They\nmay post-process the output files or request additional commands for the compilation.\n\nThe extensions can be enabled or disabled by appending `+EXTENSION` or `-EXTENSION` after\nthe output format name:\n\n     $ make4ht -f html5+tidy filename.tex\n\nIn `xhtml` and `html5` output formats, the `common_domfilters` extension is triggered automatically, but\nit can still be disabled using:\n\n     $ make4ht -f html5-common_domfilters filename.tex\n\n\nAvailable extensions:\n\n\ncommon\\_filters\n\n:    clean the output HTML files using filters.\n\ncommon\\_domfilters\n\n:    clean the HTML file using DOM filters. It is more powerful than\n     `common_filters`. It used following DOM filters: `fixinlines`, `idcolons`,\n     `joincharacters`, `mathmlfixes`, `tablerows`,`booktabs`, `sectionid`\n     and`itemparagraphs`\n\ncopy\\_images\n\n:    Copies the images to the output directory. This is useful if the original\n     images are stored in directories above the document directory.\n\ndetect\\_engine\n\n:    detect engine and format necessary for the document compilation from the\n     magic comments supported by \\LaTeX\\ editors such as TeXShop or TeXWorks. \n     Add something like the following line at the beginning of the main \\TeX\\ file:\n\n     `%!TEX TS-program = xelatex`\n\n     It supports also Plain \\TeX, use for example `tex` or `luatex` as the program name.\n\ndvisvgm\\_hashes\n\n:    efficient generation of SVG pictures using Dvisvgm. It can utilize\nmultiple processor cores and generates only changed images.\n\ninlinecss\n\n:    load the `inlinecss` DOM filter.\n\njoin\\_colors\n\n:    load the `joincolors` DOM filter for all HTML files.\n\nlatexmk\\_build\n\n:    use [Latexmk](https://ctan.org/pkg/latexmk?lang=en) for the \\LaTeX\\ compilation.\n\nmathjaxnode\n\n:    (**deprecated**, use `mjcli` extension instead) Old information: use [mathjax-node-page](https://github.com/pkra/mathjax-node-page/) to\n     convert from MathML code to HTML + CSS or SVG. See [the available\n     settings](#mathjaxsettings).\n\nmjcli\n\n:    use [mjcli](https://github.com/michal-h21/mjcli) to convert math in MathML or \\LaTeX\\ \n     format to plain HTML + CSS. MathML is used by default. If you want to use \\LaTeX\\ math,\n     add \"mathjax\" option on the command line (like `make4ht -f html5+mjcli filename.tex \"mathjax\"`).\n     See [the available settings](#mathjaxsettings).\n\nnodynamicodt \n\n:    change dynamic content in ODT files (such as tables of contents or bibliographies) to text.\n\nodttemplate\n\n:    it automatically loads the `odttemplate` filter (page \\pageref{sec:odttemplate}).\n\npreprocess\\_input \n\n:     compilation of the formats\n      supported by [Knitr](https://yihui.name/knitr/) (`.Rnw`, `.Rtex`, `.Rmd`, `.Rrst`) \n      and also Markdown and reStructuredText formats. It requires\n[R](https://www.r-project.org/) + [Knitr](https://yihui.name/knitr/)\ninstallation, it requires also [Pandoc](https://pandoc.org/) for formats based on Markdown or\nreStructuredText.\n\nstaticsite\n\n:    build the document in a form suitable for static site generators like [Jekyll](https://jekyllrb.com/).\n\ntidy\n\n:    clean the `HTML` files using the `tidy` command.\n\n# Build files\n\\label{sec:buildfiles}\n\n`make4ht` supports build files. These are `Lua` scripts that can adjust\nthe build process. They can request external applications like `BibTeX` or `Makeindex`,\npass options to the commands, modify the image conversion process, or post-process the\ngenerated files.\n\n`make4ht` tries to load default build file named as `filename + .mk4 extension`.\nIt is possible to select a different build file with `-e` or `--build-file` command line\noption.\n\nSample build file:\n\n    Make:htlatex()\n    Make:match(\"html$\", \"tidy -m -xml -utf8 -q -i ${filename}\")\n\n`Make:htlatex()` is preconfigured command for calling \\LaTeX\\ with the `tex4ht.sty` package\nloaded. In this example, it will be executed  only once. After the \ncompilation, the `tidy` command is executed on the output `HTML` files.\n\nNote that it is not necessary to call `tex4ht` and `t4ht` commands explicitly in the\nbuild file, they are called automatically. \n\n## User commands\n\nIt is possible to add more commands like `Make:htlatex` using the `Make:add` command:\n\n    Make:add(\"name\", \"command\", {settings table}, repetition)\n\nThis defines the `name` command, which can be then executed using `Make:name()`\ncommand in the build file. \n\nThe `name` and `command` parameters are required, the rest of the parameters are optional.\n\nThe defined command receives a table with settings as a parameter at the call time. \nThe default settings are provided by `make4ht`. Additional settings can be\ndeclared in the `Make:add` commands, user can also override the default settings\nwhen the command is executed in the build file:\n\n    Make:name({hello=\"world\"})\n\nMore information about settings, including the default settings provided by\n`make4ht`,  can be found in section \\ref{sec:settings} on page\n\\pageref{sec:settings}.\n\n\n### The `command` function\n\\label{sec:commandfunction}\n\nThe `command` parameter can be either a string template or function:\n\n    Make:add(\"text\", \"echo hello, input file: ${input}\")\n\nThe template can get a variable value from the parameters table using a\n`${var_name}` placeholder. Templates are executed using the operating system, so\nthey should invoke existing OS commands. \n\n\n\n\n\n### The `settings table` table\n\n\nThe `settings table` parameter is optional. If it is present, it should be\na table with new settings available in the command. It can also override the default\n`make4ht` settings for the defined command.\n\n    Make:add(\"sample_function\", function(params) \n      for k, v in pairs(params) do \n        print(k..\": \"..v) \n      end, {custom=\"Hello world\"}\n    )\n\n\n### Repetition\n\nThe `repetition` parameter  specifies the maximum number of executions of the\nparticular command.  This is used for instance for `tex4ht` and `t4ht`\ncommands, as they should be executed only once in the compilation. They would\nbe executed multiple times when they are included in the build file, as they\nare called by `make4ht` by default. Because these commands allow only one\n`repetition`, the second execution is blocked.\n\n### Expected exit code\n\nYou can set the expected exit code from a command with a `correct_exit` key in the\nsettings table. The compilation will be terminated when the command returns a\ndifferent exit code. \n\n    Make:add(\"biber\", \"biber ${input}\", {correct_exit=0})\n\nCommands that execute lua functions can return the numerical values using the `return` statement.\n\n\nThis mechanism isn't used for \\TeX, because it doesn't differentiate between fatal and non-fatal errors. \nIt returns the same exit code in all cases. Because of this, log parsing is used for a fatal error detection instead.\nError code value `1` is returned in the case of a fatal error, `0` is used\notherwise. The `Make.testlogfile` function can be used in the build file to\ndetect compilation errors in the TeX log file.\n\n\n## Provided commands\n\n`Make:htlatex`\n\n:    One call to the TeX engine with special configuration for loading of the `tex4ht.sty` package.\n\n`Make:autohtlatex`\n\n:    Variant of `Make:htlatex` that automates the compilation of \\LaTeX\\ documents, \n     ensuring that the process is repeated until the output stabilizes or an error occurs.\n\n`Make:clean`\n\n:    This command removes all generated files, including images, HTML files and\n     various auxilary files, from the current directory. It keeps files whose\n     file names don't match the input file name. It is preferable to use `make4ht -m clean filename.tex`\n     to clean output files.\n\n`Make:httex`\n\n:    Variant of `Make:htlatex` suitable for Plain \\TeX.\n\n`Make:latexmk`\n\n:    Use `Latexmk` for the document compilation. `tex4ht.sty` will be loaded automatically.\n\n`Make:tex4ht`\n\n:    Process the `DVI` file and create output files.\n\n`Make:t4ht`\n\n:    Create the CSS file and generate images.\n\n`Make:biber`\n\n:    Process bibliography using the `biber` command.\n\n`Make:pythontex`\n\n:    Process the input file using `pythontex`.\n\n`Make:bibtex`\n\n:    Process bibliography using the `bibtex` command.\n\n`Make:xindy`\n\n:    Generate index using Xindy index processor.\n\n`Make:makeindex`\n\n:    Generate index using the Makeindex command.\n\n`Make:xindex`\n\n:    Generate index using the Xindex command.\n\n\n## File matches\n\\label{sec:postprocessing}\n\nAnother type of action that can be specified in the build file is\n`Make:match`.  It can be used to post-process  the generated files:\n\n    Make:match(\"html$\", \"tidy -m -xml -utf8 -q -i ${filename}\")\n\nThe above example will clean all output `HTML` files using the `tidy` command.\n\nThe `Make:match` action tests output filenames using a `Lua` pattern matching function.  \nIt executes a command or a function, specified in the second argument, on files\nwhose filenames match the pattern. \n\nThe commands to be executed can be specified as strings. They can contain\n`${var_name}` placeholders, which are replaced with corresponding variables\nfrom the `settings` table. The templating system was described in \nsubsection \\ref{sec:commandfunction}. There is an additional variable\navailable in this table, called `filename`. It contains the name of the current\noutput file.\n\n\nIf a function is used instead, it will get two parameters.  The first one is the\ncurrent filename, the second one is the `settings` table. \n\n    Make:match(\"html$\", function(filename, settings)\n      print(\"Post-processing file: \".. filename)\n      print(\"Available settings\")\n      for k,v in pairs(settings)\n        print(k,v)\n      end\n      return true\n   end)\n\nMultiple post-processing actions can be executed on each filename. The Lua\naction functions can return an exit code. If the exit code is false, the execution\nof the post-processing chain for the current file will be terminated.\n\n### Filters\n\\label{sec:filters}\n\nTo make it easier to post-process the generated files using the `match`\nactions, `make4ht` provides a filtering mechanism thanks to the\n`make4ht-filter` module. \n\nThe `make4ht-filter` module returns a function that can be used for the filter\nchain building. Multiple filters can be chained into a pipeline. Each filter\ncan modify the string that is passed to it from the previous filters. The\nchanges are then saved to the processed file. \n\nSeveral built-in filters are available, it is also possible to create new ones.\n\nExample that use only the built-in filters:\n\n    local filter = require \"make4ht-filter\"\n    local process = filter{\"cleanspan\", \"fixligatures\", \"hruletohr\"}\n    Make:htlatex()\n    Make:match(\"html$\",process)\n\nFunction `filter` accepts also function arguments, in this case this function\ntakes file contents as a parameter and modified contents are returned.\n\nExample with custom filter:\n\n    local filter  = require \"make4ht-filter\"\n    local changea = function(s) return s:gsub(\"a\",\"z\") end\n    local process = filter{\"cleanspan\", \"fixligatures\", changea}\n    Make:htlatex()\n    Make:match(\"html$\",process)\n\nIn this example, spurious span elements are joined, ligatures are decomposed,\nand then all letters \"a\" are replaced with \"z\" letters.\n\nBuilt-in filters are the following:\n\ncleanspan\n\n:    clean spurious span elements when accented characters are used\n\ncleanspan-nat\n\n:    alternative clean span filter, provided by Nat Kuhn\n\nfixligatures\n\n:    decompose ligatures to base characters\n\nhruletohr\n\n:   `\\hrule` commands are translated to series of underscore characters\n    by \\TeX4ht, this filter translates these underscores to `<hr>` elements\n\nentites\n\n:    convert prohibited named entities to numeric entities (only\n     `&nbsp;` currently).\n\nfix-links\n\n:    replace colons in local links and `id` attributes with underscores. Some\n     cross-reference commands may produce colons in internal links, which results in\n     a validation error.\n\nmathjaxnode\n\n:    (**deprecated**, use `mjcli` extension instead) Old information: use [mathjax-node-page](https://github.com/pkra/mathjax-node-page/) to\n     convert from MathML code to HTML + CSS or SVG. See [the available\n     settings](#mathjaxsettings).\n\nmjcli\n\n:    use [mjcli](https://github.com/michal-h21/mjcli) to convert math in MathML or \\LaTeX\\ \n     format to plain HTML + CSS.  See [the available settings](#mathjaxsettings).\n\n\nodttemplate\n\n:    use styles from another `ODT` file serving as a template in the current\n     document. It works for the `styles.xml` file in the `ODT` file. During\n     the compilation, this file is named as `\\jobname.4oy`.\n     \\label{sec:odttemplate}\n\nstaticsite\n\n:    create HTML files in a format suitable for static site generators such as [Jekyll](https://jekyllrb.com/)\n\nsvg-height\n\n:    some  SVG images produced by `dvisvgm` seem to have wrong dimensions. This filter\n     tries to set the correct image size.\n\n\n### DOM filters\n\nDOM filters are variants of filters that use the\n[`LuaXML`](https://ctan.org/pkg/luaxml) library to modify\ndirectly the XML object. This enables more powerful\noperations than the regex-based filters from the previous section. \n\nExample:\n\n    local domfilter = require \"make4ht-domfilter\"\n    local process = domfilter {\"joincharacters\"}\n    Make:match(\"html$\", process)\n\n\nAvailable DOM filters:\n\naeneas\n\n:  [Aeneas](https://www.readbeyond.it/aeneas/) is a tool for automagical synchronization of text and audio.\n   This filter modifies the HTML code to support synchronization.\n\nbooktabs\n\n:  fix lines produced by the `\\cmidrule` command provided by the Booktabs package.\n\ncollapsetoc\n\n:  collapse table of contents to contain only top-level sectioning level and sections on the current page.\n\nfixinlines\n\n:  put all inline elements which are direct children of the `<body>` elements to a paragraph.\n\nidcolons\n\n:  replace the colon (`:`) character in internal links and `id` attributes. They cause validation issues.\n\ninlinecss\n\n:  remove CSS rules that target elements with unique attributes, such as color boxes, table rules, or inline math pictures,\n   and insert their properties as a inline `style` attribute in the HTML document.\n\njoincharacters\n\n:  join consecutive `<span>` or `<mn>` elements. This DOM filter supersedes the `cleanspan` filter.\n\njoincolors\n\n:  many `<span>` elements with unique `id` attributes are created when \\LaTeX\\ colors are being used in the document.\n   A CSS rule is added for each of these elements, which may result in\n   substantial growth of the CSS file. This filter replaces these rules with a\n   common one for elements with the same color value. See also the `inlinecss` DOM filter and extension, which provides an\n   alternative using inline styles.\n\nodtfonts\n\n:  fix styles for fonts that were wrongly converted by `Xtpipes` in the ODT format.\n\nodtimagesize\n\n:  set correct dimensions for images in the ODT format. It is no longer used, as the dimensions are set by TeX4ht itself.\n\n\nodtpartable\n\n:  resolve tables nested inside paragraphs, which is invalid in the ODT format.\n\ntablerows\n\n:  remove spurious rows from HTML tables.\n\nmathmlfixes\n\n:  fix common issues for MathML.\n\nsectionid\n\n:  create `id` attribute for HTML sectioning elements derived from the section\n   title. It also updates links to these sections. Use the `notoc` command line\n   option to prevent that.\n\nt4htlinks\n\n:  fix hyperlinks in the ODT format.\n\n\n\n## Image conversion\n\\label{sec:imageconversion}\n\nIt is possible to convert parts of the \\LaTeX\\ input as pictures. It can be used\nfor preserving the appearance of  math or diagrams, for example. \n\nThese pictures are stored in a special `DVI` file, which can be processed by\na `DVI` to image commands, such as `dvipng` or `dvisvgm`. \n\nThis conversion is normally configured in the `tex4ht.env` file. This file\nis system dependent and it has quite an unintuitive syntax.\nThe configuration is processed by the `t4ht` application and the conversion\ncommand is called for all pictures.\n\nIt is possible to disable `t4ht` image processing and configure image\nconversion in the build file using the `image` action:\n\n    Make:image(\"png$\",\n    \"dvipng -bg Transparent -T tight -o ${output}  -pp ${page} ${source}\")\n\n\n`Make:image` takes two parameters, a `Lua` pattern to match the image name, and\nthe action.\n\nAction can be either a string template with the conversion command\nor a function that takes a table with parameters as an argument.\n\nThere are three parameters:\n\n  - `output` - output image filename\n  - `source` - `DVI` file with the pictures\n  - `page`   - page number of the converted image\n\n## The `mode` variable\n\nThe `mode` variable available in the build process contains \ncontents of the `--mode` command line option.  It can be used to run some commands\nconditionally. For example:\n\n     if mode == \"draft\" then\n       Make:htlatex{} \n     else\n       Make:htlatex{}\n       Make:htlatex{}\n       Make:htlatex{}\n     end\n\nIn this example (which is the default configuration used by `make4ht`),\n\\LaTeX\\ is called only once when `make4ht` is called with the `draft` mode:\n    \n    make4ht -m draft filename\n\n## The `settings` table\n\\label{sec:settings}\n\nIt is possible to access the parameters outside commands, file matches\nand image conversion functions. For example, to convert the document to\nthe `OpenDocument Format (ODT)`, the following settings can be used. They are\nbased on the `oolatex` command:\n\n    settings.tex4ht_sty_par = settings.tex4ht_sty_par ..\",ooffice\"\n    settings.tex4ht_par = settings.tex4ht_par .. \" ooffice/! -cmozhtf\"\n    settings.t4ht_par = settings.t4ht_par .. \" -cooxtpipes -coo \"\n\n(Note that it is possible to use the `--format odt` option\nwhich is superior to the previous code. This example is intended just as an\nillustration)\n\nThere are some functions to simplify access to the settings:\n\n`set_settings{parameters}`\n\n:   overwrite settings with values from a passed table\n\n`settings_add{parameters}`\n\n:   add values to the current settings \n\n`filter_settings \"filter name\" {parameters}`\n\n:   set settings for a filter\n\n`get_filter_settings(name)`\n\n:   get settings for a filter\n\n\nFor example, it is possible to simplify the sample from the previous code listings:\n\n    settings_add {\n      tex4ht_sty_par =\",ooffice\",\n      tex4ht_par = \" ooffice/! -cmozhtf\",\n      t4ht_par = \" -cooxtpipes -coo \"\n    }\n\nSettings for filters and extensions can be set using `filter_settings`:\n\n    \n    filter_settings \"test\" {\n      hello = \"world\"\n    }\n\nThese settings can be retrieved in the extensions and filters using the `get_filter_settings` function:\n\n    function test(input)\n       local options = get_filter_settings(\"test\")\n       print(options.hello)\n       return input\n    end\n       \n### Default settings\n\nThe default parameters are the following:\n\n`htlatex`\n\n:     used \\TeX\\ engine\n\n`input`\n\n:    content of `\\jobname`, see also the `tex_file` parameter.\n\n`interaction`\n\n:    interaction mode for the \\TeX\\ engine. The default value is `batchmode` to\n     suppress user input on compilation errors. It also suppresses most of the \\TeX\\ \n     compilation log output. Use the `errorstopmode` for the default behavior.\n\n`tex_file`\n\n:    input \\TeX\\ filename\n\n`latex_par`\n\n:    command line parameters to the \\TeX\\ engine\n\n`packages`\n\n:    additional \\LaTeX\\ code  inserted before `\\documentclass`.\n     Useful for passing options to packages used in the document or to load additional packages.\n\n`tex4ht_sty_par`\n\n:    options for `tex4ht.sty`\n\n`tex4ht_par`\n\n:     command line options for the `tex4ht` command\n\n`t4ht_par`\n\n:    command line options for the `t4ht` command\n\n`outdir`\n\n:    the output directory\n\n`correct_exit`\n\n:    expected `exit code` from the command. The compilation will be terminated\n     if the exit code of the executed command has a different value.\n\n`auto_extensions`\n\n:    table with extensions of auxiliary files that should be watched by the `Make:autohtlatex` command.\n\n`max_compilations`\n\n:    maximum number of \\LaTeX\\ runs by the `Make:autohtlatex` command.\n\n\n# `make4ht` configuration file {#configfile}\n\nIt is possible to globally modify the build settings using the configuration\nfile. It is a special version of a build file where the global settings can be set.\n\nCommon tasks for the configuration file can be a declaration of the new commands,\nloading of the default filters or specification of a default build sequence. \n\nOne additional functionality not available in the build files are commands for\nenabling and disabling of extensions.\n\n\n## Location \n\nThe configuration file can be saved either in the\n`$HOME/.config/make4ht/config.lua` file, or in the `.make4ht` file placed in\nthe current directory or it's parent directories (up to the `$HOME` directory). \n\n## Additional commands\n\nThere are two additional commands:\n\n`Make:enable_extension(name)`\n\n:  require extension\n\n`Make:disable_extension(name)`\n\n:  disable extension\n\n## Example\n\nThe following example of the configuration file adds support for the `biber` command, requires\n`common_domfilters` extension and requires MathML\noutput for math.\n\n    Make:add(\"biber\", \"biber ${input}\")\n    Make:enable_extension \"common_domfilters\"\n    settings_add {\n      tex4ht_sty_par =\",mathml\"\n    }\n\n<!--\n# Development\n\n## Custom filters\n\n## New extensions\n\n## How to add a new output format\n\n-->\n\n# List of available settings for filters and extensions.\n\nThese settings may be set using `filter_settings` function in a build file or in the `make4ht` configuration file.\n\n## Compilation commands\n\n## The `autohtlatex` command\n\nauto_extensions\n\n:    table with extensions of auxiliary files that should be watched by the `Make:autohtlatex` command.\n\nmax_compilations\n\n:    maximum number of \\LaTeX\\ runs by the `Make:autohtlatex` command.\n\n\n## Indexing commands\n\nThe indexing commands (like `xindy` or `makeindex`) use some common settings.\n\nidxfile\n\n:    name of the `.idx` file. Default value is `\\jobname.idx`.\n\nindfile\n\n:    name of the `.ind` file. Default value is the same as `idxfile` with the file extension changed to `.ind`.\n\nEach indexing command can have some additional settings.\n\n### The `xindy` command\n\nencoding\n\n:    text encoding of the `.idx` file. Default value is `utf8`.\n\nlanguage\n\n:    index language. Default language is English.\n\nmodules\n\n:    table with names of additional `Xindy` modules to be used.\n\n### The `makeindex` command\n\noptions\n\n:    additional command line options for the Makeindex command.\n\n### The `xindex` command\n\noptions\n\n:    additional command line options for the Xindex command.\n\nlanguage\n\n:    document language\n\n## The `tidy` extension\n\noptions\n\n:  command line options for the `tidy` command. Default value is `-m -utf8 -w 512 -q`.\n\n## The `collapsetoc` dom filter\n\n`toc_query` \n\n:  CSS selector for selection of element that contains the table of contents. \n\n`title_query`\n\n:  CSS selector for selecting all elements that contain the section ID attribute.\n\n`toc_levels` \n\n:  table containing a hierarchy of classes used in TOC\n\n`max_depth`\n\n:  set detph of displayed children TOC levels\n\nDefault values:\n\n    filter_settings \"collapsetoc\" {\n      toc_query = \".tableofcontents\",\n      title_query = \"h1 a, h2 a, h3 a, h4 a, h5 a, h6 a\",\n      max_depth = 1,\n      toc_levels = {\n        tocpart = 1,\n        toclikepart = 1,\n        tocappendix = 1,\n        toclikechapter = 2,\n        tocchapter = 2,\n        tocsection = 3,\n        toclikesection = 3,\n        tocsubsection = 4,\n        toclikesubsection = 4,\n        tocsubsubsection = 5,\n        toclikesubsubsection = 5,\n        tocparagraph = 6,\n        toclikeparagraph = 6,\n        tocsubparagraph = 7,\n        toclikesubparagraph = 7,\n      }\n    }\n\n## The `copy_images` extension\n\nextensions\n\n:  table with list of image extensions that should be processed. \n\n\nimg\\_dir\n\n:  name of the output directory where images should be stored\n\nDefault values:\n\n     filter_settings \"copy_images\" {\n        extensions = {\"png\", \"jpg\", \"jpeg\", \"svg\"},\n        img_dir = \"\"\n     }\n\n## The `fixinlines` dom filter \n\ninline\\_elements\n\n:  table of inline elements that shouldn't be direct descendants of the `body` element. The element names should be table keys, the values should be true.\n\nExample\n\n    filter_settings \"fixinlines\" {inline_elements = {a = true, b = true}}\n\n## The `joincharacters` dom filter\n\ncharclasses \n\n:  table of elements that should be concatenated when two or more of such elements with the same value of the `class` attribute are placed one after another.\n\nExample\n\n    filter_settings \"joincharacters\" { charclasses = { span=true, mn = true}}\n\n## The `mjcli` filter and extension {#mathjaxsettings}\n\n`mjcli` detects whether to use MathML or \\LaTeX\\ input by use of the `mathjax` option for `make4ht`. By default, it uses MathML. \\LaTeX\\ input can be required using:\n\n    make4ht -f html5+mjcli filename.tex \"mathjax\"\n\n### Available settings\n\noptions\n\n:  command line options for the `mjcli` command. \n\nExample\n\n    filter_settings \"mjcli\" {\n      options=\"--svg\"\n    }\n\ncssfilename  \n\n:  the `mjcli` command puts some CSS code into the HTML pages. The `mjcli` filter extracts this information and saves it to a standalone CSS file. Default name of this CSS file is `${input}-mathjax.css`\n\nfontdir\n\n:  directory with MathJax font files. This option enables the use of local fonts, which\n   is useful in the conversion to ePub, for example. The font directory should be\n   sub-directory of the current directory. Only \\TeX\\ font is supported at the moment.\n\nExample\n\n\n    filter_settings \"mjcli\" {\n      fontdir=\"fonts/TeX/woff/\" \n    }\n\n\n## The `staticsite` filter and extension\n\nsite\\_root \n\n:  directory where generated files should be copied.\n\nmap\n\n:  a hash table where keys contain patterns that match filenames and values contain\ndestination directory for the matched files. The destination directories are\nrelative to the `site_root` (it is possible to use `..` to switch to a parent\ndirectory).\n\nfile\\_pattern \n\n:  a pattern used for filename generation. It is possible to use string templates\nand format strings for `os.date` function. The default pattern `%Y-%m-%d-${input}`\ncreates names in the form of `YYYY-MM-DD-file_name`.\n\nheader\n\n:  table with variables to be set in the YAML header in HTML files. If the\ntable value is a function, it is executed with current parameters and HTML page\nDOM object as arguments.\n\nremove\\_maketitle\n\n:  the `staticsite` extension removes text produced by the `\\maketitle` command by default. Set this \noption to `false` to disable the removal.\n\nExample:\n\n\n    -- set the environmental variable 'blog_root' with path to \n    -- the directory that should hold the generated HTML files\n    local outdir = os.getenv \"blog_root\" \n    \n    filter_settings \"staticsite\" {\n      site_root = outdir, \n      map = {\n        [\".css$\"] = \"/css/\"\n      },\n      header = {\n         layout=\"post\",\n         date = function(parameters, dom)\n           return os.date(\"!%Y-%m-%d %T\", parameters.time)\n         end\n      }\n    }\n\n## The `dvisvgm_hashes` extension\n\noptions\n\n:  command line options for Dvisvgm. The default value is `-n --exact -c ${scale},${scale}`.\n\ncpu_cnt\n\n:  the number of processor cores used for the conversion. The extension tries to detect the available cores automatically by default.\n\nmake_command\n\n:  variant of the `make` command used for the parallel conversion of large\nnumber of pages. It receives tvo variables, `process_count` and `make_file`.\nDefault value is \"make -j ${process_count} -f ${make_file}\".\n\ntest_make_command\n\n:  command that tests if the selected variant of the `make` command exists. Default value is `make -v`.\n\n\nparallel_size\n\n:  the number of pages used in each Dvisvgm call. The extension detects changed\npages in the DVI file and constructs multiple calls to Dvisvgm with only changed\npages.\n\nscale\n\n:  amount of SVG scaling. The default value is 1.4.\n\n## The `odttemplate` filter and extension\n\ntemplate\n\n:  filename of the template `ODT` file \n\n\n`odttemplate` can also get the template filename from the `odttemplate` option from `tex4ht_sty_par` parameter. It can be set using the following command line call:\n\n     make4ht -f odt+odttemplate filename.tex \"odttemplate=template.odt\"\n\n## The `aeneas` filter\n\nskip\\_elements\n\n:  List of CSS selectors that match elements that shouldn't be processed. Default value: `{ \"math\", \"svg\"}`.\n\nid\\_prefix \n\n:  prefix used in the ID attribute forming.\n\nsentence\\_match \n\n:  Lua pattern used to match a sentence. Default value: `\"([^%.^%?^!]*)([%.%?!]?)\"`.\n\n## The  `make4ht-aeneas-config` package\n\nCompanion for the `aeneas` DOM filter is the `make4ht-aeneas-config` plugin. It\ncan be used to write the Aeneas configuration file or execute Aeneas on the\ngenerated HTML files.\n\nAvailable functions:\n\nwrite\\_job(parameters)\n\n:  write Aenas job configuration to `config.xml` file. See the [Aeneas\n   documentation](https://www.readbeyond.it/aeneas/docs/clitutorial.html#processing-jobs)\n   for more information about jobs.\n\nexecute(parameters)\n\n:  execute Aeneas.\n\nprocess\\_files(parameters)\n\n:  process the audio and generated subtitle files.\n\n\nBy default, a `SMIL` file is created. It is assumed that there is an audio file\nin the `mp3` format, named as the \\TeX\\ file. It is possible to use different formats\nand filenames using mapping.\n\nThe configuration options can be passed directly to the functions or set using\n`filter_settings \"aeneas-config\" {parameters}` function.\n\n\n### Available parameters\n\n\nlang \n\n:  document language. It is interfered from the HTML file, so it is not necessary to set it. \n\nmap \n\n:  mapping between HTML, audio and subtitle files. More info below. \n\ntext\\_type \n\n:  type of input. The `aeneas` DOM filter produces an `unparsed` text type.\n\nid\\_sort \n\n:  sorting of id attributes. The default value is `numeric`.\n\nid\\_regex \n\n:  regular expression to parse the id attributes.\n\nsub\\_format \n\n:  generated subtitle format. The default value is `smil`.\n\n\n### Additional parameters for the job configuration file\n\n- description \n- prefix \n- config\\_name \n- keep\\_config \n\n\n\nIt is possible to generate multiple HTML files from the \\LaTeX\\ source. For\nexample, `tex4ebook` generates a separate file for each chapter or section. It is\npossible to set options for each HTML file, in particular names of the\ncorresponding audio files. This mapping is done using the `map` parameter. \n\nExample:\n\n    filter_settings \"aeneas-config\" {\n      map = {\n        [\"sampleli1.html\"] = {audio_file=\"sample.mp3\"}, \n        [\"sample.html\"] = false\n      }\n    }\n\nTable keys are the configured filenames. It is necessary to insert them as\n`[\"filename.html\"]`, because of Lua syntax rules.\n\nThis example maps audio file `sample.mp3` to a section subpage. The main HTML\nfile, which may contain title and table of contents doesn't have a\ncorresponding audio file.\n\nFilenames of the subfiles correspond to the chapter numbers, so they are not\nstable when a new chapter is added. It is possible to request filenames\nderived from the chapter titles using the `sec-filename` option for `tex4ht.sty`.\n\n### Available `map` options\n\n\naudio\\_file \n\n:  the corresponding audio file \n\nsub\\_file \n\n:  name of the generated subtitle file\n\nThe following options are the same as their counterparts from the main parameters table and generally, don't need to be set:\n\n- prefix \n- file\\_desc \n- file\\_id \n- text\\_type \n- id\\_sort\n- id\\_prefix \n- sub\\_format \n\n\n### Full example\n\n\n    local domfilter = require \"make4ht-domfilter\"\n    local aeneas_config = require \"make4ht-aeneas-config\"\n    \n    filter_settings \"aeneas-config\" {\n      map = {\n        [\"krecekli1.xhtml\"] = {audio_file=\"krecek.mp3\"}, \n        [\"krecek.xhtml\"] = false\n      }\n    }\n    \n    local process = domfilter {\"aeneas\"}\n    Make:match(\"html$\", process)\n\n    if mode == \"draft\" then\n      aeneas_config.process_files {}\n    else\n      aeneas_config.execute {}\n    end\n\n\n\n\n# Troubleshooting \n\n## Incorrect handling of command line arguments for `tex4ht`, `t4ht` or `latex`\n\nSometimes, you may get a similar error:\n\n    make4ht:unrecognized parameter: i\n\nIt may be caused by a following `make4ht` invocation:\n\n    $ make4ht hello.tex \"customcfg,charset=utf-8\" \"-cunihtf -utf8\" -d foo\n\nThe command line option parser is confused by mixing options for `make4ht` and\n\\TeX4ht\\ in this case. It tries to interpret the `-cunihtf -utf8`, which are\noptions for the `tex4ht` command, as `make4ht` options. To fix that, try to\nmove the `-d foo` directly after the `make4ht` command:\n\n    $ make4ht -d foo hello.tex \"customcfg,charset=utf-8\" \"-cunihtf -utf8\"\n\nAnother option is to add a space before the `tex4ht` options:\n\n    $ make4ht hello.tex \"customcfg,charset=utf-8\" \" -cunihtf -utf8\" -d foo\n\nThe former way is preferable, though.\n\n## Table of Contents points to a wrong destination\n\nThe `sectionid` DOM filter creates better link destinations for sectioning commands.\nIn some cases, for example if you use Pandoc, the document may already contain the\nlink destination with the same name. In such cases the original destination is preserved \nin the file. In this case links to the section will point to that place, instead of\ncorrect destination in the section. This may happen for example if you use Pandoc for\nthe Markdown to \\LaTeX\\ conversion. It creates `\\hypertarget` commands that are placed \njust before section. The links points to that place, instead of the actual section. \n\nIn this case you don't want to update links. Use the `notoc` option to prevent that.\n\n\n\n## Filenames containing spaces\n\n`tex4ht` command cannot handle filenames containing spaces. to fix this issue, `make4ht` \nreplaces spaces in the input filenames with underscores. The generated\nXML filenames use underscores instead of spaces as well.\n\n## Filenames containing non-ASCII characters\n\nThe `odt` output doesn't support accented filenames, it is best to stick to ASCII characters in filenames.\n\n# License\n\nPermission is granted to copy, distribute and/or modify this software\nunder the terms of the LaTeX Project Public License, version 1.3.\n"
  },
  {
    "path": "config.cfg",
    "content": "\\Preamble{xhtml}\n% this was fixed in the upstream, but Debian used in the Docker container\n% doesn't contain it yet\n\\def\\hypertarget#1#2{\\Link{}{#1}\\EndLink#2}\n\\Configure{@HEAD}{\\HCode{\\Hnewline<script src=\"https://hypothes.is/embed.js\" async></script>}}\n\\Configure{@HEAD}{\\HCode{\\Hnewline<meta property=\"og:title\" content=\"\\LikeRef{TITLE+}\" />}}\n\\Configure{@HEAD}{\\HCode{\\Hnewline<meta property=\"og:author\" content=\"Michal Hoftich\" />}}\n\\Configure{@HEAD}{\\HCode{\\Hnewline<meta property=\"og:type\" content=\"website\" />}}\n\\Configure{@HEAD}{\\HCode{\\Hnewline<meta property=\"og:url\" content=\"https://www.kodymirus.cz/make4ht/make4ht-doc.html\" />}}\n\\Configure{@HEAD}{\\HCode{\\Hnewline<link rel=\"canonical\" href=\"https://www.kodymirus.cz/make4ht/make4ht-doc.html\" /> }}\n\\Css{body{\n  font-size:18px;\n  font-size-adjust: 0.5;\n  width:65ch;\n  max-width:100\\%;\n  margin: 1em auto;\n}}\n\n\\Css{p,li,dt{\n    line-height: calc(1ex / 0.32);\n    text-align: justify;\n    hyphens: auto;\n}}\n\n\\Css{div.center p {text-align:center;}}\n\n% save the \\@title command\n\\begin{document}\n\\makeatletter\n\\Tag{TITLE+}{The make4ht build system}\n\\makeatother\n\\def\\TeX{TeX}\n\\def\\LaTeX{LaTeX}\n\\EndPreamble\n"
  },
  {
    "path": "domfilters/make4ht-aeneas.lua",
    "content": "-- DOM filter for Aeneas, tool for automatical text and audio synchronization\n-- https://github.com/readbeyond/aeneas\n-- It adds elements with id attributes for text chunks, in sentence length.\n--\n--\nlocal cssquery = require \"luaxml-cssquery\"\nlocal mkutils  = require \"mkutils\"\nlocal log = logging.new \"aeneas\"\n\n-- Table of CSS selectors to be skipped.\nlocal skip_elements = { \"math\", \"svg\"}\n\n-- The id attribute format is configurable\n-- Aeneas must be told to search for the ID pattern using is_text_unparsed_id_regex\n-- option in Aneas configuration file\nlocal id_prefix = \"ast\"\n\n-- Pattern to mach a sentence. It should match two groups, first is actual\n-- sentence, the second optional interpunction mark.\nlocal sentence_match = \"([^%.^%?^!]*)([%.%?!]?)\"\n\n-- convert table with selectors to a query list\nlocal function prepare_selectors(skips)\n  local css = cssquery()\n  for _, selector in ipairs(skips) do\n    css:add_selector(selector)\n  end\n  return css\nend\n\n-- save the HTML language \nlocal function save_config(dom, saves)\n  local get_lang = function(d)\n    local html = d:query_selector(\"html\")[1] or {}\n    return html:get_attribute(\"lang\")\n  end\n  local saves = saves or {}\n  local config = get_filter_settings \"aeneas_config\"\n  if config.language then return end\n  saves.lang = get_lang(dom)\n  filter_settings \"aeneas-config\" (saves)\nend\n-- make span element with unique id for a sentence\nlocal function make_span(id,parent, text)\n  local newobj = parent:create_element(\"span\", {id=id }) \n  newobj.processed = true -- to disable multiple processing of the node\n  local text_node = newobj:create_text_node(text)\n  newobj:add_child_node(text_node)\n  return newobj\nend\n\n-- make the id attribute and update the id value\nlocal function make_id(lastid, id_prefix)\n  local id = id_prefix .. lastid\n  lastid = lastid + 1\n  return id, lastid\nend\n\n-- parse text for sentences and add spans \nlocal function make_ids(parent, text, lastid, id_prefix)\n  local t = {}\n  local id\n  for chunk, punct in text:gmatch(sentence_match) do\n    id, lastid = make_id(lastid, id_prefix)\n    local newtext = chunk..punct\n    -- the newtext is empty string sometimes. we can skipt it then.\n    if newtext~=\"\" then\n      table.insert(t, make_span(id, parent, newtext))\n    end\n  end\n  return t, lastid\nend\n\n\n\n-- test if the DOM element is in list of skipped CSS selectors\nlocal function is_skipped(el, css)\n  local matched = css:match_querylist(el)\n  return #matched > 0\nend\n\nlocal function aeneas(dom, par)\n  local par = par or {}\n  local id = 1\n  local options = get_filter_settings \"aeneas\"\n  local skip_elements = options.skip_elements or par.skip_elements or skip_elements\n  local id_prefix = options.id_prefix or par.id_prefix or id_prefix\n  local skip_object = prepare_selectors(skip_elements)\n  sentence_match = options.sentence_match or par.sentence_match or sentence_match\n\n  local body = dom:query_selector(\"body\")[1]\n  -- process only the document body\n  if not body then return dom end\n  -- save information for aeneas_config\n  save_config(dom, {id_prefix = id_prefix})\n  body:traverse_elements(function(el)\n    -- skip disabled elements\n    if(is_skipped(el, skip_object)) then return false end\n    -- skip already processed elements\n    if el.processed then return false end\n    local newchildren = {} -- this will contain the new elements\n    local children = el:get_children()\n    local first_child = children[1]\n\n    -- if the element contains only text, doesn't already have an id attribute and the text is short,\n    -- the id is set directly on that element.\n    if #children == 1\n      and first_child:is_text()\n      and not el:get_attribute(\"id\")\n      and string.len(first_child._text) < 20\n      and el._attr\n    then\n      local idtitle\n      idtitle, id = make_id(id, id_prefix)\n      log:debug(el._name, first_child._text)\n      el:set_attribute(\"id\", idtitle)\n      return el\n    end\n\n    for _, child in ipairs(children) do\n      -- process only non-empty text\n      if child:is_text() and child._text:match(\"%a+\") then\n        local newnodes\n        newnodes, id = make_ids(child, child._text, id, id_prefix)\n        for _, node in ipairs(newnodes) do\n          table.insert(newchildren, node or {})\n        end\n      else\n        -- insert the current processing element to the new element list\n        -- if it isn't only text\n        table.insert(newchildren, child or {})\n      end\n    end\n    -- replace element children with the new ones\n    if #newchildren > 0 then\n      el._children = {}\n      for _, c in ipairs(newchildren) do\n        el:add_child_node(c)\n      end\n    end\n  end)\n  return dom\nend\n\nreturn aeneas\n"
  },
  {
    "path": "domfilters/make4ht-booktabs.lua",
    "content": "\nlocal function find_cmidrules(current_rows)\n  -- save rows with cmidrules here\n  local matched_rows = {}\n  local continue = false\n  for row_no, row in ipairs(current_rows) do\n    local columnposition = 1\n    local matched_cmidrule = false\n\n    for _, col in ipairs(row:query_selector(\"td\")) do\n      -- keep track of culumns\n      local span = tonumber(col:get_attribute(\"colspan\")) or 1\n      local cmidrule = col:query_selector(\".cmidrule\")\n      -- column contain cmidrule\n      if #cmidrule > 0 then\n        -- remove any child elements, we don't need them anymore\n        col._children = {}\n        -- only one cmidrule can be on each row, save the position, column span and all attributes\n        matched_rows[row_no] = {attributes = col._attr, column = columnposition, span = span, continue = continue}\n        matched_cmidrule = true\n      end\n      columnposition = columnposition + span\n    end\n    if matched_cmidrule then\n      -- save the row number of the first cmidrule on the current row\n      continue = continue or row_no\n    else\n      continue = false\n    end\n\n  end\n  -- save the table rows count, so we can loop over them sequentially later\n  matched_rows.length = #current_rows\n  return matched_rows\nend\n\nlocal function update_row(current_rows, match, newspan, i)\n  local row_to_update = current_rows[match.continue]\n  -- insert spanning column if necessary\n  if newspan > 0 then\n    local td = row_to_update:create_element(\"td\", {colspan=tostring(newspan), span=\"nazdar\"})\n    row_to_update:add_child_node(td)\n  end\n  -- insert the rule column\n  local td = row_to_update:create_element(\"td\", match.attributes)\n  row_to_update:add_child_node(td)\n  -- remove unnecessary row\n  current_rows[i]:remove_node()\nend\n\nlocal function join_rows(matched_rows,current_rows)\n  for i = 1, matched_rows.length do\n    local match = matched_rows[i]\n    if match then\n      -- we only need to process rows that place subsequent cmidrules on the same row\n      local continue = match.continue\n      if continue then\n        local prev_row = matched_rows[continue]\n        -- find column where the previous cmidrule ends\n        local prev_end = prev_row.column + prev_row.span\n        local newspan = match.column - prev_end \n        update_row(current_rows, match, newspan, i)\n        -- update the current row position\n        prev_row.column = match.column\n        prev_row.span = match.span\n      end\n    end\n  end\nend\n\nlocal function process_booktabs(dom)\n  local tables = dom:query_selector(\"table\")\n  for _, tbl in ipairs(tables) do\n    local current_rows = tbl:query_selector(\"tr\")\n    local matched_rows = find_cmidrules(current_rows)\n    join_rows(matched_rows, current_rows)\n  end\n  return dom\nend\n\nreturn process_booktabs\n\n"
  },
  {
    "path": "domfilters/make4ht-collapsetoc.lua",
    "content": "-- mini TOC support for make4ht\nlocal domobject = require \"luaxml-domobject\"\n\nlocal filter = require \"make4ht-filter\"\nlocal log = logging.new \"collapsetoc\"\nlocal mktuils = require \"mkutils\"\n\n\n-- assign levels to entries in the .4tc file\nlocal toc_levels = {\n  tocpart = 1,\n  toclikepart = 1,\n  tocappendix = 2,\n  toclikechapter = 2,\n  tocchapter = 2,\n  tocsection = 3,\n  toclikesection = 3,\n  tocsubsection = 4,\n  toclikesubsection = 4,\n  tocsubsubsection = 5,\n  toclikesubsubsection = 5,\n  tocparagraph = 6,\n  toclikeparagraph = 6,\n  tocsubparagraph = 7,\n  toclikesubparagraph = 7,\n}\n\n-- number of child levels to be kept\n-- the depth of 1 ensures that only direct children of the current sectioning command \n-- will be kept in TOC\nlocal max_depth = 1\n\n\n-- debugging function to test correct structure of the TOC tree\nlocal function print_tree(tree, level) \n  local level = level or 0\n  log:debug(string.rep(\" \", level) .. (tree.type or \"root\"), tree.id)\n  for k, v in pairs(tree.children) do\n    print_tree(v, level + 2)\n  end\nend\n\n-- convert the parsed toc entries to a tree structure\nlocal function make_toc_tree(tocentries, lowestlevel, position, tree)\n  local position = position or 1\n  local tree = tree or {\n    level = lowestlevel - 1,\n    children = {}\n  }\n  local stack = {tree}\n  if position > #tocentries then return tree, position end\n  -- loop over TOC entries and make a tree\n  for i = 1, #tocentries do\n    -- initialize new child\n    local element = tocentries[i]\n    element.children = element.children or {}\n    local parent = stack[#stack]\n    local level_diff = element.level - parent.level\n    if level_diff == 0 then -- entry is sibling of parent\n      -- current parent is sibling of the current elemetn, true parent is \n      -- sibling's parent\n      parent = parent.parent\n      -- we must replace sibling element with the current element in stact\n      -- so the child elements get correct parent\n      table.remove(stack)\n      table.insert(stack, element)\n    elseif level_diff > 0 then -- entry is child of parent\n      for x = 1, level_diff do\n        table.insert(stack, element)\n      end\n    else\n      -- we must remove levels from the stack to get the correct parent\n      for x =1 , level_diff, -1 do\n        if #stack > 0 then\n          parent = table.remove(stack)\n        end\n      end\n      -- we must reinsert parent back to stack, place the current element to stact too\n      table.insert(stack, parent)\n      table.insert(stack, element)\n    end\n    table.insert(parent.children, element)\n    element.parent = parent\n  end\n  print_tree(tree)\n  return tree\nend\n\n-- find first sectioning element in the current page\nlocal function find_headers(dom, header_levels)\n  -- we need to find id attributes in <a> elements that are children of sectioning elements\n  local ids = {}\n  for _, header in ipairs(dom:query_selector(header_levels)) do\n    local id = header:get_attribute \"id\"\n    if id then ids[#ids+1] = id end\n  end\n  return ids\nend\n\n\n-- process list of ids and find those that should be kept:\n-- siblings, children, parents and top level\nlocal function find_toc_entries_to_keep(ids, tree)\n  local tree = tree or {}\n  -- all id in TOC tree that we want to kepp are saved in this table\n  local ids_to_keep = {}\n  -- find current id in the TOC tree\n  local function find_id(id, tree)\n    if tree.id == id then return tree end\n    if not tree.children or #tree.children == 0 then return false end\n    for k,v in pairs(tree.children) do\n      local found_id = find_id(id, v)\n      if found_id then return found_id end\n    end\n    return false\n  end\n  -- always keep top level of the hiearchy\n  local function keep_toplevel(tree)\n    for _, el in ipairs(tree.children) do\n      ids_to_keep[el.id] = true\n    end\n  end\n  -- we want to keep all children in TOC hiearchy\n  local function keep_children(element, depth)\n    local depth = depth or 1\n    local max_depth = max_depth or 1\n    -- stop processing when there are no children\n    for _, el in pairs(element.children or {}) do\n      if el.id then ids_to_keep[el.id] = true end\n      -- by default, we keep just direct children of the current sectioning element\n      if depth < max_depth then\n        keep_children(el, depth + 1)\n      end\n    end\n  end\n  -- also keep all siblings\n  local function keep_siblings(element)\n    local parent = element.parent\n    for k, v in pairs(parent.children or {}) do\n      ids_to_keep[v.id] = true\n    end\n  end\n  -- and of course, keep all parents\n  local function keep_parents(element)\n    local parent = element.parent\n    if parent and parent.id then\n      ids_to_keep[parent.id] = true\n      -- we should keep siblings of all parents as well\n      keep_siblings(parent)\n      keep_parents(parent)\n    end\n  end\n  -- always keep the top-level TOC hiearchy, even if we cannot find any sectioning element on the page\n  keep_toplevel(tree)\n  for _, id in ipairs(ids) do\n    -- keep the current id\n    ids_to_keep[id] = true\n    local found_element = find_id(id, tree)\n    if found_element then\n      keep_children(found_element)\n      keep_siblings(found_element)\n      keep_parents(found_element)\n    end\n  end\n  return ids_to_keep\nend\n\n-- process the .4tc file and convert entries to a tree structure\n-- based on the sectioning level\nlocal function parse_4tc(parameters, toc_levels)\n  local tcfilename = mkutils.file_in_builddir(parameters.input .. \".4tc\", parameters)\n  if not mkutils.file_exists(tcfilename) then \n    log:warning(\"Cannot find TOC: \" .. tcfilename)\n    return {}\n  end\n  local tocentries = {}\n  local f = io.open(tcfilename, \"r\")\n  -- we need to find the lowest level used in the TOC\n  local lowestlevel = 999\n  for line in f:lines() do\n    -- entries looks like: \\doTocEntry\\tocsubsection{1.2.2}{\\csname a:TocLink\\endcsname{5}{x5-60001.2.2}{QQ2-5-6}{aaaa}}{7}\\relax \n    -- we want do extract tocsubsection and x5-60001.2.2\n    local toctype, id = line:match(\"\\\\doTocEntry\\\\(.-){.-}{.-{.-}{(.-)}\")\n    if toctype then\n      local level = toc_levels[toctype]\n      if not level then \n        log:warning(\"Cannot find TOC level for: \" .. toctype)\n      else\n        lowestlevel = level < lowestlevel and level or lowestlevel\n        table.insert(tocentries, {type = toctype, id = id, level = level})\n      end\n    end\n  end\n  f:close()\n  local toc =  make_toc_tree(tocentries, lowestlevel)\n  return toc\nend\n\nlocal function remove_levels(toc, matched_ids)\n  -- remove links that aren't in the TOC hiearchy that should be kept\n  for _, link in ipairs(toc:query_selector(\"a\")) do\n    local href = link:get_attribute(\"href\")\n    -- find id in the href\n    local id = href:match(\"#(.+)\")\n    if id and not matched_ids[id] then\n      -- toc links are in <span> elements that can contain the section number\n      -- we must remove them too\n      local parent = link:get_parent()\n      if parent:get_element_name() == \"span\" then\n        parent:remove_node()\n      else\n        -- if the parent node isn't <span>, remove at least the link itself\n        link:remove_node()\n      end\n    end\n  end\nend\n\n\nlocal function collapsetoc(dom, parameters)\n  -- set options\n  local par = parameters\n  local options = get_filter_settings \"collapsetoc\"\n  -- query to find the TOC element in DOM\n  local toc_query = par.toc_query or options.toc_query or \".tableofcontents\"\n  -- query to select sectioning elements with id's\n  local title_query = par.title_query or options.title_query or \"h1 a, h2 a, h3 a, h4 a, h5 a, h6 a\" \n  -- level of child levels to be kept in TOC\n  max_depth = par.max_depth or options.max_depth or max_depth\n  -- set level numbers for particular TOC entry types\n  local user_toc_levels = par.toc_levels or options.toc_levels or {}\n  -- join user's levels with default\n  for k,v in pairs(user_toc_levels) do toc_levels[k] = v end\n  -- parse the .4tc file to get TOC tree\n  toc = toc or parse_4tc(parameters, toc_levels)\n  -- find sections in the current html file\n  local ids = find_headers(dom, title_query)\n  log:debug(\"Ids\", table.concat(ids, \",\"))\n  local ids_to_keep = find_toc_entries_to_keep(ids, toc)\n  local toc_dom = dom:query_selector(toc_query)[1]\n  if toc_dom then\n    remove_levels(toc_dom, ids_to_keep)\n  else\n    log:warning(\"Cannot find TOC element using query: \" .. toc_query)\n  end\n  return dom\nend\n\nreturn collapsetoc\n"
  },
  {
    "path": "domfilters/make4ht-fixinlines.lua",
    "content": "local inline_elements = {\n  a=true,\n  b=true,\n  big=true,\n  i=true,\n  small=true,\n  tt=true,\n  abbr=true,\n  acronym=true,\n  cite=true,\n  code=true,\n  dfn=true,\n  em=true,\n  kbd=true,\n  strong=true,\n  samp=true,\n  time=true,\n  var=true,\n  a=true,\n  bdo=true,\n  br=true,\n  img=true,\n  map=true,\n  object=true,\n  q=true,\n  span=true,\n  sub=true,\n  sup=true,\n  button=true,\n  input=true,\n  label=true,\n  select=true,\n  textarea=true,\n}\n\n\nlocal function fix_inlines(obj)\n  local settings = get_filter_settings \"fixinlines\"\n  local inline_elements = settings.inline_elements or inline_elements\n  local nodes = obj:get_path(\"html body\")\n  local new = nil\n  obj:traverse_node_list(nodes, function(jej)\n    if jej._type == \"ELEMENT\" or jej._type == \"TEXT\" or jej._type == \"COMMENT\" then\n      local name = string.lower(jej._name or \"\")\n      -- local parent = jej:get_parent_node()\n      if inline_elements[name] or jej._type == \"TEXT\" or jej._type == \"COMMENT\" or (name:match(\":?math\") and  jej:get_attribute(\"display\") == \"inline\") then\n        if not new then\n          -- start new paragraph\n          if (jej._type == \"TEXT\" and jej._text:match(\"^%s+$\")) or jej._type == \"COMMENT\" then\n            -- ignore parts that contain only whitespace or comments and are placed before \n            -- paragraph start\n          else\n            new = obj:create_element(\"p\" )\n            new:add_child_node(obj:copy_node(jej))\n            jej:replace_node(new)\n          end\n        else\n          -- paragraph already exists\n          new:add_child_node(obj:copy_node(jej))\n          jej:remove_node()\n        end\n      else\n        -- close the current paragraph before new block element\n        new = nil\n      end\n    else\n      new = nil\n    end\n  end)\n  return obj\nend\n\nreturn fix_inlines\n"
  },
  {
    "path": "domfilters/make4ht-idcolons.lua",
    "content": "local allowed_chars = {\n  [\"-\"] = true,\n  [\".\"] = true\n}\nlocal function fix_colons(id)\n  -- match every non alphanum character\n  return id:gsub(\"[%W]\", function(s)\n    -- some characters are allowed, we don't need to replace them\n    if allowed_chars[s] then return s end\n    -- in other cases, replace with underscore\n    return \"_\"\n  end)\nend\n\nlocal function id_colons(obj)\n  -- replace non-valid characters in links and ids with underscores\n  obj:traverse_elements(function(el) \n    local name = string.lower(obj:get_element_name(el))\n    if name == \"a\" then\n      local href = el:get_attribute(\"href\")\n      -- don't replace colons in external links\n      if href and not href:match(\"[a-z]%://\") then\n        local base, id = href:match(\"(.*)%#(.*)\")\n        if base and id then\n          id = fix_colons(id)\n          el:set_attribute(\"href\", base .. \"#\" .. id)\n        end\n      end\n    end\n    local id  = el:get_attribute(\"id\")\n    if id then\n      el:set_attribute(\"id\", fix_colons(id))\n    end\n  end)\n  return obj\nend\n\nreturn id_colons\n"
  },
  {
    "path": "domfilters/make4ht-inlinecss.lua",
    "content": "local cssquery  = require \"luaxml-cssquery\"\n\nlocal log = logging.new(\"inlinecss\")\n\nlocal cssrules = {}\nlocal cssobj   = cssquery()\n\nlocal function parse_rule(line)\n  -- parse CSS selector and attributes\n  -- they are always on one line in the CSS file produced by TeX4ht\n  local selector, values = line:match(\"%s*(.-)%s*(%b{})\")\n  if values then\n    values = values:sub(2,-2)\n  end\n  return selector, values\nend\n\nlocal function join_values(old, new)\n  -- correctly joins two attribute lists, depending on the ending\n  local separator = \";\"\n  if not old then return new end\n  -- if old already ends with ;, then don't use semicolon as a separator\n  if old:match(\";%s*$\") then separator = \"\" end\n  return old .. separator .. new\nend\n\nlocal function parse_css(filename)\n  local css_file = io.open(filename, \"r\")\n  if not css_file then return nil, \"cannot load css file: \" .. (filename or \"\") end\n  local newlines = {}\n  for line in css_file:lines() do\n    -- match lines that contain # or =, as these can be id or attribute selectors\n    if line:match(\"[%#%=].-{\") then\n      -- update attributes for the current selector\n      local selector, value = parse_rule(line)\n      local oldvalue = cssrules[selector] \n      cssrules[selector] = join_values(oldvalue, value)\n    else\n      newlines[#newlines+1] = line\n    end\n  end\n  -- we need to add css rules\n  for selector, value in pairs(cssrules) do\n    cssobj:add_selector(selector, function(dom) end, {value=value})\n  end\n  css_file:close()\n  -- write new version of the CSS file, without rules for ids and attributes\n  local css_file = io.open(filename, \"w\")\n  css_file:write(table.concat(newlines, \"\\n\"))\n  css_file:close()\n  return true\nend\n\nlocal processed = false\n\n-- process the HTML file and insert inline CSS for id and attribute selectors\nreturn function(dom, par)\n  if not processed then \n    -- process the CSS file before everything else, but only once\n    processed = true\n    local css_file = mkutils.file_in_builddir(par.input .. \".css\", par)\n    local status, msg = parse_css(css_file)\n    if not status then log:warning(msg) end\n  end\n  -- loop over all elements in the current page\n  dom:traverse_elements(function(curr)\n    -- use CSS object to match if the current element\n    -- is matched by id attribute selector\n    local matched = cssobj:match_querylist(curr)\n    if #matched > 0 then\n      -- join possible already existing style attribute with values from the CSS file\n      local values = curr:get_attribute(\"style\")\n      -- join values of all matched rules\n      for _,rule in ipairs(matched) do\n        values = join_values(values, rule.params.value)\n      end\n      curr:set_attribute(\"style\", values)\n    end\n\n  end)\n  return dom\nend\n\n"
  },
  {
    "path": "domfilters/make4ht-itemparagraphs.lua",
    "content": "-- TeX4ht puts contents of all \\item commands into paragraphs. We are not\n-- able to detect if it contain only one paragraph, or more. If just one,\n-- we can remove the paragraph and put the contents directly to <li> element.\nreturn function(dom)\n  for _, li in ipairs(dom:query_selector(\"li\")) do\n    local is_single_par = false\n    -- count elements and paragraphs that are direct children of <li>\n    -- remove the paragraph only if it is the only child element\n    local el_count, par_count = 0, 0\n    local par = {}\n    for pos, el in ipairs(li._children) do\n      if el:is_element() then\n        el_count = el_count + 1\n        local name = el:get_element_name()\n        if name == \"p\" then\n          par[#par+1] = el\n        elseif name == \"a\" and el_count == 1 and el:get_attribute(\"id\") then\n          -- if the first element is <a> with id, we can move it to <li> and remove it from the list of children, this is needed for nested lists\n          el_count = el_count - 1\n          local id = el:get_attribute(\"id\")\n          if not li:get_attribute(\"id\") then\n            li:set_attribute(\"id\", id)\n            el:remove_node()\n          end\n        end\n      end\n    end\n    if #par == 1 and el_count == 1 then\n      -- place paragraph children as direct children of <li>, this\n      -- efectivelly removes <p>\n      li._children = par[1]._children\n    end\n  end\n  return dom\nend\n"
  },
  {
    "path": "domfilters/make4ht-joincharacters.lua",
    "content": "local log = logging.new(\"joincharacters\")\n\nlocal charclasses = {\n  span=true,\n  mn = true,\n}\n\nlocal safe_mathml_elements = {\n  math = true,\n  mrow = true,\n  mstyle = true,\n  mtext = true,\n  mtd = true,\n}\n\nlocal function update_mathvariant(curr)\n  -- when we join several <mi> elements, they will be rendered incorrectly\n  -- we must set the mathvariant attribute\n  local parent = curr:get_parent()\n  -- set mathvariant only if it haven't been set by the parent element\n  if not parent:get_attribute(\"mathvariant\") then\n    -- curr._attr = curr._attr or {}\n    local mathvariant = \"italic\"\n    -- the joined elements don't have attributes\n    curr._attr = curr._attr or {}\n    curr:set_attribute(\"mathvariant\", mathvariant)\n  end\nend\n\nlocal table_count = function(tbl)\n  local tbl = tbl or {}\n  local i = 0\n  for k,v in pairs(tbl) do i = i + 1 end\n  return i\nend\n\n\nlocal has_matching_attributes = function (el, next_el)\n  local el_attr = el._attr or {}\n  local next_attr = next_el._attr or {}\n  -- if the number of attributes doesn't match, elements don't match\n  if table_count(next_attr) ~= table_count(el_attr) then return false end\n  for k, v in pairs(el_attr) do\n    -- if any attribute doesn't match, elements don't match\n    if v~=next_attr[k] then return false end\n  end\n  return true\nend\n\n\nlocal function join_characters(obj,par)\n  -- join adjanced span and similar elements inserted by \n  -- tex4ht to just one object.\n  local par = par or {}\n  local options = get_filter_settings \"joincharacters\"\n  local charclasses = options.charclasses or par.charclasses or charclasses\n\n  local get_name = function(curr) \n    return string.lower(curr:get_element_name())\n  end\n  local get_class = function(next_el)\n    return next_el:get_attribute(\"class\") or next_el:get_attribute(\"mathvariant\")\n  end\n  local is_span = function(next_el)\n    return charclasses[get_name(next_el)]\n  end\n\n  local is_safe_mathml = function(el)\n    -- we want to join the <mn> element only when it is safe. for example <mfrac><mn>1</mn><mn>2</mn></mfrac> should be left\n    local current_name = get_name(el)\n    if current_name == \"mn\" then\n      local parent_name = get_name(el:get_parent())\n      return safe_mathml_elements[parent_name]\n    end\n    return true\n  end\n  local has_children = function(curr)\n    -- don't process spans that have child elements\n    local children = curr:get_children() or {}\n    -- if there is more than one child, we can be sure that it has child elements\n    if #children > 1 then \n      return true \n    elseif #children == 1 then\n      -- test if the child is an element\n      return children[1]:is_element()\n    end\n    return false\n  end\n  local join_elements = function(el, next_el)\n    -- it the following element match, copy it's children to the current element\n    for _, child in ipairs(next_el:get_children()) do\n      el:add_child_node(child)\n    end\n    -- remove the next element\n    next_el:remove_node()\n  end\n\n  local function get_next(curr, class)\n    local next_el = curr:get_next_node()\n    if next_el and next_el:is_element() and is_span(next_el) then\n      return next_el\n      -- if the next node is space followed by a matching element, we should add this space\n    elseif next_el and next_el:is_text() and get_next(next_el, class) then\n      local text = next_el._text \n      -- match only text containing just whitespace\n      if text:match(\"^%s+$\") then return next_el end\n    end\n  end\n\n  obj:traverse_elements(function(el)\n    -- loop over all elements and test if the current element is in a list of\n    -- processed elements (charclasses) and if it doesn't contain children\n    if is_span(el) and not has_children(el) and is_safe_mathml(el) then\n      local next_el = get_next(el)\n      -- loop over the following elements and test whether they are of the same type\n      -- as the current one\n      while next_el do\n        -- save the next element because we will remove it later\n        local real_next = get_next(next_el)\n        if get_name(el) == get_name(next_el) and has_matching_attributes(el,next_el) and not el:get_attribute(\"id\") then\n          join_elements(el, next_el)\n          -- add the whitespace\n        elseif next_el:is_text() then\n          local s = next_el._text\n          -- we must create a new node\n          el:add_child_node(el:create_text_node(s))\n          next_el:remove_node()\n          -- real_next = nil\n        else\n          real_next = nil\n        end\n        -- use the saved element as a next object\n        next_el = real_next\n      end\n    end\n\n  end)\n  -- process <mi> elements\n  obj:traverse_elements(function(el)\n    local function get_next_mi(curr)\n      local next_el = curr:get_next_node()\n      if next_el and next_el:is_element() then\n        return next_el\n      end\n    end\n    local function has_no_attributes(x)\n      return table_count(x._attr) == 0\n    end\n\n    -- join only subsequential <mi> elements with no attributes\n    if get_name(el) == \"mi\" and has_no_attributes(el) then\n      local next_el = get_next_mi(el)\n      while next_el do\n        local real_next = get_next_mi(next_el)\n        if get_name(next_el) == \"mi\" and has_no_attributes(next_el) then\n          join_elements(el, next_el)\n          -- set math variant to italic \n          -- (if the parent <mstyle> element doesn't set it to something else)\n          update_mathvariant(el)\n        else\n          -- break the loop otherwise\n          real_next = nil\n        end\n        next_el = real_next\n      end\n    end\n  end)\n  -- join text nodes in an element into one\n  obj:traverse_elements(function(el)\n    -- save the text\n    local t = {}\n    local children = el:get_children()\n    for _, x in ipairs(children) do\n      if x:is_text() then\n        t[#t+1] = x._text\n      else\n        return nil\n      end\n    end\n    el._text = table.concat(t)\n    return el\n  end)\n  return obj\nend\n\nreturn join_characters\n"
  },
  {
    "path": "domfilters/make4ht-joincolors.lua",
    "content": "local cssfiles = {}\nlocal log = logging.new \"joincolors\"\n\n\n-- keep mapping between span ids and colors\nlocal colors = {}\n\nlocal function extract_colors(csscontent)\n  local used_colors = {}\n  -- delete the color ids and save the used colors\n  csscontent = csscontent:gsub(\"[%a]*%#(textcolor.-)%s*{%s*color%s*%:%s*(.-)%s*%}%s\", function(id, color)\n    -- convert rgb() function to hex value and generate the span name\n    local converted = \"textcolor-\" .. color:gsub(\"rgb%((.-),(.-),(.-)%)\", function(r,g,b)\n      return string.format(\"%02x%02x%02x\", tonumber(r), tonumber(g), tonumber(b))\n    end)\n    -- remove the # characters from the converted color name\n    converted = converted:gsub(\"%#\", \"\")\n    -- save the id and used color\n    colors[id] = converted\n    used_colors[converted] = color\n    return \"\"\n  end)\n  -- add the used colors to css\n  local t = {}\n  for class, color in pairs(used_colors) do\n    t[#t+1] = string.format(\".%s{color:%s;}\", class, color)\n  end\n  table.sort(t)\n  return csscontent .. table.concat(t, \"\\n\")\nend\n\nlocal function process_css(cssfile)\n  local f = io.open(cssfile,\"r\")\n  if not f then return nil, \"Cannot open the CSS file: \".. cssfile end\n  local content = f:read(\"*all\")\n  f:close()\n  -- delete color ids and replace them with joined spans\n  local newcontent = extract_colors(content)\n  -- save the updated css file\n  local f=io.open(cssfile, \"w\")\n  f:write(newcontent)\n  f:close()\nend\n\n\nlocal function process_css_files(dom)\n  for _, el in ipairs(dom:query_selector(\"link\")) do\n    local href = el:get_attribute(\"href\") or \"\"\n    if not cssfiles[href] and href:match(\"css$\") then\n      log:debug(\"Load CSS file \", href)\n      cssfiles[href] = true\n      process_css(href)\n    end\n  end\n\nend\n\nlocal function join_colors(dom)\n  -- find css files in the current HTML file and join the colors\n  process_css_files(dom)\n  for _, span in ipairs(dom:query_selector(\"span\")) do\n    local id = span:get_attribute(\"id\")\n    if id then\n      -- test if the id is in the saved colors\n      local class = colors[id]\n      if class then\n        -- remove the id\n        span:set_attribute(\"id\", nil)\n        span:set_attribute(\"class\", class)\n      end\n    end\n  end\n\n  return dom\nend\n\nreturn join_colors\n"
  },
  {
    "path": "domfilters/make4ht-mathmlfixes.lua",
    "content": "local log = logging.new(\"mathmlfixes\")\n\nlocal mathml_chardata = require \"make4ht-mathml-char-def\"\n\n-- <mglyph> should be inside <mi>, so we don't process it \n-- even though it is  a token element\nlocal token = {\"mi\", \"mn\", \"mo\", \"mtext\", \"mspace\", \"ms\"}\nlocal token_elements = {}\nfor _, tok in ipairs(token) do token_elements[tok] = true end\n\n-- helper functions to support MathML elements with prefixes (<mml:mi> etc).\n--\nlocal function get_element_name(el)\n  -- return element name and xmlns prefix\n  local name = el:get_element_name()\n  if name:match(\":\") then\n    local prefix, real_name =  name:match(\"([^%:]+):?(.+)\")\n    return real_name, prefix\n  else\n    return name\n  end\nend\n\nlocal function get_attribute(el, attr_name)\n  -- attributes can have the prefix, but sometimes they don't have it\n  -- so we need to catch both cases\n  local _, prefix = get_element_name(el)\n  prefix = prefix or \"\"\n  return el:get_attribute(attr_name) or el:get_attribute(prefix .. \":\" .. attr_name)\nend\n\nlocal function get_new_element_name(name, prefix)\n  return prefix and prefix .. \":\" .. name or name\nend\n\nlocal function update_element_name(el, name, prefix)\n  local newname = get_new_element_name(name, prefix)\n  el._name = newname\nend\n\nlocal function create_element(el, name, prefix, attributes)\n  local attributes = attributes or {}\n  local newname = get_new_element_name(name, prefix)\n  return el:create_element(newname, attributes)\nend\n\nlocal function element_pos(el)\n  local pos, count = 0, 0\n  for _, node in ipairs(el:get_siblings()) do\n    if node:is_element() then\n      count = count + 1\n      if node == el then\n        pos = count\n      end\n    end\n  end\n  return pos, count\nend\n\n-- test if element is the first element in the current element list\nlocal function is_first_element(el)\n  local pos, count = element_pos(el)\n  return pos == 1 \nend\n\n-- test if element is the last element in the current element list\nlocal function is_last_element(el)\n  local pos, count = element_pos(el)\n  return pos == count\nend\n\n\n\nlocal function is_token_element(el)\n  local name, prefix = get_element_name(el)\n  return token_elements[name], prefix\nend\n\nlocal function fix_token_elements(el)\n  -- find token elements that are children of other token elements\n  if is_token_element(el) then\n    local parent = el:get_parent()\n    local is_parent_token, prefix = is_token_element(parent)\n    if is_parent_token then\n      -- change top element in nested token elements to mstyle\n      update_element_name(parent, \"mstyle\", prefix)\n    end\n  end\nend\n\nlocal function fix_nested_mstyle(el)\n  -- the <mstyle> element can be child of token elements\n  -- we must exterminate it\n  local el_name = get_element_name(el)\n  if el_name == \"mstyle\" then\n    local parent = el:get_parent()\n    if is_token_element(parent) then\n      -- if parent doesn't have the mathvariant attribute copy it from <mstyle>\n      if not parent:get_attribute(\"mathvariant\") then\n        local mathvariant = el:get_attribute(\"mathvariant\") \n        parent._attr = parent._attr or {}\n        parent:set_attribute(\"mathvariant\", mathvariant)\n      end\n      -- copy the contents of <mstyle> to the parent element\n      parent._children = el._children\n    end\n  end\nend\n\nlocal function fix_mathvariant(el)\n  -- set mathvariant of <mi> that is child of <mstyle> to have the same value\n  local function find_mstyle(x)\n    -- find if element has <mstyle> parent, and its value of mathvariant\n    if not x:is_element() then\n      return nil\n    elseif get_element_name(x) == \"mstyle\" then \n      return x:get_attribute(\"mathvariant\")\n    else\n      return find_mstyle(x:get_parent())\n    end\n  end\n  if get_element_name(el) == \"mi\" then\n    -- process only <mi> that have mathvariant set\n    local oldmathvariant = el:get_attribute(\"mathvariant\")\n    if oldmathvariant then\n      local mathvariant = find_mstyle(el:get_parent())\n      if mathvariant then\n        el:set_attribute(\"mathvariant\", mathvariant)\n      end\n    end\n  end\nend\n\nlocal function contains_only_text(el)\n  -- detect if element contains only text\n  local elements = 0\n  local text     = 0\n  local children = el:get_children() or {}\n  for _ , child in ipairs(children) do\n    if child:is_text() then text = text + 1\n    elseif child:is_element() then elements = elements + 1\n    end\n  end\n  return text > 0 and elements == 0\nend\n\n-- check if <mstyle> element contains direct text. in that case, add\n-- <mtext>\nlocal function fix_missing_mtext(el)\n  if el:get_element_name() == \"mstyle\" and contains_only_text(el) then\n    -- add child <mtext>\n    log:debug(\"mstyle contains only text: \" .. el:get_text())\n    -- copy the current mode, change it's element name to mtext and add it as a child of <mstyle>\n    local copy = el:copy_node()\n    copy._name = \"mtext\"\n    copy._parent = el\n    el._children = {copy}\n  end\nend\n\nlocal function is_radical(el)\n  local radicals = {msup=true, msub=true, msubsup=true}\n  return radicals[el:get_element_name()]\nend\n\nlocal function get_mrow_child(el)\n  local get_first = function(x) \n    local children = x:get_children() \n    return children[1]\n  end\n  local first = get_first(el)\n  -- either return first child, and if the child is <mrow>, return it's first child\n  if first and first:is_element() then\n    if first:get_element_name() == \"mrow\" then\n      return get_first(first), first\n    else\n      return first\n    end\n  end\nend\n\nlocal function fix_radicals(el)\n  if is_radical(el) then\n    local first_child, mrow = get_mrow_child(el)\n    -- if the first child is only one character long, it is possible that there is a problem\n    if first_child and string.len(first_child:get_text()) == 1 then\n      local name = first_child:get_element_name() \n      local siblings = el:get_siblings()\n      local pos = el:find_element_pos()\n      -- it doesn't make sense to do any further processing if the element is at the beginning\n      if pos == 1 then return end\n      if name == \"mo\" then\n        for i = pos, 1,-1 do\n        end\n\n      end\n    end\n\n  end\nend\n\n-- put <mrow> as child of <math> if it already isn't here\nlocal allowed_top_mrow = {\n  math=true\n}\nlocal function top_mrow(math)\n  local children = math:get_children()\n  local put_mrow = false\n  -- don't process elements with one or zero children\n  -- don't process elements that already are mrow\n  local parent = math:get_parent()\n  local parent_name\n  if parent then parent_name = get_element_name(parent) end\n  local current_name, prefix = get_element_name(math)\n  if #children < 2 or not allowed_top_mrow[current_name] or current_name == \"mrow\" or parent_name == \"mrow\" then return nil end\n  local mrow_count = 0\n  for _,v in ipairs(children) do\n    if v:is_element() and is_token_element(v) then\n      put_mrow = true\n      -- break\n    elseif v:is_element() and get_element_name(v) == \"mrow\" then\n      mrow_count = mrow_count + 1\n    end\n  end\n  if not put_mrow and get_element_name(math) == \"math\" and mrow_count == 0 then\n    -- put at least one <mrow> to each <math>\n    put_mrow = true\n  end\n  if put_mrow then\n    local newname = get_new_element_name(\"mrow\", prefix)\n    local mrow = math:create_element(newname)\n    for _, el in ipairs(children) do\n      mrow:add_child_node(el)\n    end\n    math._children = {mrow}\n  end\n\nend\n\nlocal function get_fence(el, attr, form)\n  -- convert fence attribute to <mo> element\n  -- attr: open | close\n  -- form: prefix | postfix\n  local char = el:get_attribute(attr)\n  local mo \n  if char then\n    local name, prefix = get_element_name(el)\n    local newname = get_new_element_name(\"mo\", prefix)\n    mo = el:create_element(newname, {fence=\"true\", form = form})\n    mo:add_child_node(mo:create_text_node(char))\n  end\n  return mo\nend\n\n\nlocal function fix_mfenced(el)\n  -- TeX4ht uses in some cases <mfenced> element which is deprecated in MathML.\n  -- Firefox doesn't support it already.\n  local name, prefix = get_element_name(el)\n  if name == \"mfenced\" then\n    -- we must replace it by <mrow><mo>start</mo><mfenced children...><mo>end</mo></mrow>\n    local open = get_fence(el, \"open\", \"prefix\")\n    local close = get_fence(el, \"close\", \"postfix\")\n    -- there can be also separator attribute, but it is not used in TeX4ht\n    -- change <mfenced> to <mrow> and remove all attributes\n    local newname = get_new_element_name(\"mrow\", prefix)\n    el._name = newname\n    el._attr = {}\n    -- open must be first child, close needs to be last\n    if open then el:add_child_node(open, 1) end\n    if close then el:add_child_node(close) end\n  end\nend\n\nlocal function is_fence(el)\n  return get_element_name(el) == \"mo\" and el:get_attribute(\"fence\") == \"true\"\nend\n\nlocal function fix_mo_to_mfenced(el)\n  -- LibreOffice NEEDS <mfenced> element. so we need to convert <mrow><mo fence=\"true\">\n  -- to <mfenced>. ouch.\n  if is_fence(el) then\n    local parent = el:get_parent()\n    local open = el:get_text():gsub(\"%s*\", \"\") -- convert mo content to text, so it can be used in \n    -- close needs to be the last element in the sibling list of the current element\n    local siblings = el:get_siblings()\n    el:remove_node() -- we don't need this element anymore\n    local close\n    for i = #siblings, 1, -1 do\n      last = siblings[i]\n      if last:is_element() then\n        if is_fence(last) then -- set close attribute only if the last element is fence\n          close = last:get_text():gsub(\"%s*\", \"\")\n          last:remove_node() -- remove <mo>\n        end\n        break -- break looping over elements once we find last element\n      end \n    end\n    -- convert parent <mrow> to <mfenced>\n    local _, prefix = get_element_name(parent)\n    local newname = get_new_element_name(\"mfenced\", prefix)\n    parent._name = newname\n    parent._attr = {open = open, close = close}\n  end\nend\n\nlocal function fix_numbers(el)\n  -- convert <mn>1</mn><mo>.</mo><mn>3</mn> to <mn>1.3</mn>\n  if get_element_name(el) == \"mn\" then\n    -- sometimes minus sign can be outside <mn>\n    local x = el:get_sibling_node(-1)\n    if x and x:is_text()\n         and x:get_text() == \"−\" \n    then\n      el:add_child_node(x:copy_node(), 1)\n      x:remove_node()\n    end\n    local n = el:get_sibling_node(1)\n    -- test if next  element is <mo class=\"MathClass-punc\">.</mo>\n    if n and n:is_element() \n         and get_element_name(n) == \"mo\" \n         and get_attribute(n, \"class\") == \"MathClass-punc\" \n         and n:get_text() == \".\" \n    then\n      -- get next element and test if it is <mn>\n      local x = el:get_sibling_node(2)\n      if x and x:is_element() \n           and get_element_name(x) == \"mn\" \n      then\n        -- join numbers and set it as text content of the current element\n        local newnumber = el:get_text() .. \".\" .. x:get_text()\n        log:debug(\"Joining numbers: \" .. newnumber)\n        el._children = {}\n        local newchild = el:create_text_node(newnumber)\n        el:add_child_node(newchild)\n        -- remove elements that hold dot and decimal part\n        n:remove_node()\n        x:remove_node()\n      end\n    end\n  end\nend\n\n\nlocal function just_operators(list)\n  -- count <mo> and return true if list contains just them\n  local mo = 0\n  for _, x in ipairs(list) do\n    if get_element_name(x) == \"mo\" then mo = mo + 1 end\n  end\n  return mo\nend\n\n\nlocal function fix_operators(x)\n  -- change <mo> elements that are only children of any element to <mi>\n  -- this fixes issues in LibreOffice with a^{*}\n  -- I hope it doesn't introduce different issues\n  -- process only <mo>\n  local el_name, prefix = get_element_name(x)\n  if el_name ~= \"mo\" then return nil end\n\tlocal siblings = x:get_siblings()\n\t-- test if current element list contains only <mo>\n\tif just_operators(siblings) == #siblings then\n\t\tif #siblings == 1 then\n      if not x:get_attribute(\"stretchy\") then\n        -- one <mo> translates to <mtext>\n        local newname = get_new_element_name(\"mtext\", prefix)\n        x._name = newname\n        log:debug(\"changing one <mo> to <mtext>: \" .. x:get_text())\n        -- I think we should use <mi>, but LO incorrectly renders it in <msubsup>,\n        -- even if we use the mathvariant=\"normal\" attribute. <mtext> works, so\n        -- we use that instead.\n        -- x:set_attribute(\"mathvariant\", \"normal\")\n      end\n\t\telse\n\t\t\t-- multiple <mo> translate to <mtext>\n\t\t\tlocal text = {}\n\t\t\tfor _, el in ipairs(siblings) do\n\t\t\t\ttext[#text+1] = el:get_text()\n\t\t\tend\n\t\t\t-- replace first <mo> text with concetanated text content\n\t\t\t-- of all <mo> elements\n\t\t\tx._children = {}\n      local newtext = table.concat(text)\n\t\t\tlocal text_el = x:create_text_node(newtext)\n      log:debug(\"changing <mo> to <mtext>: \" .. newtext)\n      x:add_child_node(text_el)\n      -- change <mo> to <mtext>\n      local newname = get_new_element_name(\"mtext\", prefix)\n      x._name = newname\n      -- remove subsequent <mo>\n      for i = 2, #siblings do\n        siblings[i]:remove_node()\n      end\n    end\n  end\nend\n\nlocal function get_third_parent(el)\n  local first = el:get_parent()\n  if not first then return nil end\n  local second = first:get_parent()\n  if not second then return nil end\n  return second:get_parent()\nend\n\nlocal function add_space(el, pos)\n  local parent = el:get_parent()\n  local name, prefix = get_element_name(el)\n  local space = create_element(parent, \"mspace\", prefix)\n  space:set_attribute(\"width\", \"0.3em\")\n  parent:add_child_node(space, pos)\nend\n\nlocal function fix_dcases(el)\n\t-- we need to fix spacing in dcases* environments\n\t-- when you use something like:\n\t-- \\begin{dcases*}\n\t-- 1 & if $a=b$ then\n\t-- \\end{dcases*}\n\t-- the spaces around $a=b$ will be missing\n\t-- we detect if the <mtext> elements contains spaces that are collapsed by the browser, and add explicit <mspace>\n\t-- elements when necessary\n\tif el:get_element_name() == \"mtext\" then\n\t\tlocal parent = get_third_parent(el)\n\t\tif parent and parent:get_element_name() == \"mtable\" and parent:get_attribute(\"class\") == \"dcases-star\" then\n\t\t\tlocal text = el:get_text()\n\t\t\tlocal pos = el:find_element_pos()\n\t\t\tif pos == 1 and text:match(\"%s$\") then \n\t\t\t\tadd_space(el, 2)\n\t\t\telseif text:match(\"^%s\") and not el._used then\n\t\t\t\tadd_space(el, pos)\n\t\t\t\t-- this is necessary to avoid infinite loop, we mark this element as processed\n\t\t\t\tel._used = true\n\t\t\tend\n\t\tend\n\tend\nend\n\nlocal function is_empty_row(el)\n  -- empty row should contain only one <mtd>\n  local count = 0\n  if el:get_text():match(\"^%s*$\") then\n    for _, child in ipairs(el:get_children()) do\n      if child:is_element() then count = count + 1 end\n    end\n  else\n    -- row is not empty if it contains any text\n    return false\n  end\n  -- if there is one or zero childrens, then it is empty row\n  return count < 2\nend\n\n\nlocal function delete_last_empty_mtr(el)\n  -- arrays sometimes contain last empty row, which causes rendering issues,\n  -- so we should remove them\n  local el_name, prefix = get_element_name(el)\n  if el_name == \"mtr\" \n    and get_attribute(el, \"class\") == \"array-row\" \n    and is_last_element(el)\n    and is_empty_row(el)\n  then\n    el:remove_node()\n  end\n\nend\n\n\nlocal function fix_mtable_hlines(mtable)\n  -- TeX4ht adds <mtr class=\"hline\"> for hlines. we need to remove these <mtr> elements and construct \n  -- correct \"rowlines\" attribute for horizontal lines\n  local hlines = {}\n  local rowlines = {}\n  local styles = {}\n  local el_name, prefix = get_element_name(mtable)\n  -- process only <mtable> elements\n  if el_name ~= \"mtable\" or  mtable:get_attribute(\"rowlines\") then\n    -- if rowlines attribute is already set, we don't need to do anything\n    return\n  end\n  local mtrs = mtable:query_selector(\"mtr\")\n  for count, mtr in ipairs(mtrs) do\n    local hline = mtr:get_attribute(\"class\")\n    if hline and hline == \"array-hline\" then\n      table.insert(hlines, \"hline\")\n      -- we need to remove <mtr> elements that represent hlines, hlines will be displayed using the rowlines attribute\n      mtr:remove_node()\n    elseif count == #mtrs and hline == \"array-row\" and is_empty_row(mtr) then\n      -- ignore empty row that is inserted if \\hline is at the end of the array\n      mtr:remove_node()\n    else\n      -- just keep the track of normal lines\n      table.insert(hlines, \"\")\n    end\n  end\n  -- now we need to construct rowlines attribute\n  for i, el in ipairs(hlines) do\n    if el == \"hline\" then\n      -- rowlines are used only inside the array. at the start and at the end, we need to use CSS\n      if i == 1 then\n        table.insert(styles, \"border-top: 1px solid black;\")\n      elseif i == #hlines then\n        table.insert(styles, \"border-bottom: 1px solid black;\")\n      else\n        table.insert(rowlines, \"solid\")\n      end\n    else\n      -- we need to detect rows that weren't separated by hlines. in that case, we need to insert none to rowlines\n      if i > 1 and i ~= #hlines then\n        if hlines[i-1] ~= \"hline\" then table.insert(rowlines, \"none\") end\n      end\n    end\n  end\n  mtable:set_attribute(\"rowlines\", table.concat(rowlines, \" \"))\n  local style = mtable:get_attribute(\"style\") or \"\"\n  mtable:set_attribute(\"style\", style .. table.concat(styles, \" \"))\nend\n\n\nlocal function fix_rel_mo(el)\n  -- this is necessary for LibreOffice. It has a problem with relative <mo> that are\n  -- first childs in an element list. This often happens in equations, where first\n  -- element in a table column is an operator, like non-equal-, less-than etc.\n  local el_name, prefix = get_element_name(el)\n  if el_name == \"mo\" \n     and not get_attribute(el, \"fence\") -- ignore fences\n     and not get_attribute(el, \"form\")  -- these should be also ignored\n     and not get_attribute(el, \"accent\") -- and accents too\n  then\n    local parent = el:get_parent()\n    if is_first_element(el) then\n      local mrow = create_element(parent, \"mrow\", prefix)\n      parent:add_child_node(mrow, 1)\n    elseif is_last_element(el) then\n      local mrow = create_element(parent, \"mrow\", prefix)\n      parent:add_child_node(mrow)\n    end\n  end\n\nend\n\nlocal uchar = utf8.char\nlocal ucodes = utf8.codes\n\n-- current version of MathML doesn't support the mathvariant attribute, so we need to replace unicode characters with the corresponding base code for the current font style\nlocal function replace_characters(math, current_style)\n  -- recursively loop over all the children of the math element and replace the unicode characters with the corresponding base code for the current font style\n  for _, child in ipairs(math:get_children()) do\n    if child:is_text() then\n      local text = child:get_text()\n      local new_text = {}\n      for _ ,char in ucodes(text) do\n        -- replace the unicode characters with the corresponding base code for the current font style\n        local code = mathml_chardata[char]\n        if code then\n          local new_char = code[current_style] or char\n          table.insert(new_text, uchar(new_char))\n        else\n          table.insert(new_text, uchar(char))\n        end\n      end\n      child._text = table.concat(new_text)\n    elseif child:is_element() then\n      local current_style = child:get_attribute(\"mathvariant\") or current_style\n      replace_characters(child, current_style)\n    end\n  end\nend\n\nlocal function fix_mathml_chars(el)\n  local el_name, _ = get_element_name(el)\n  if el_name == \"math\" then\n    replace_characters(el, \"normal\")\n  end\nend\n\n\nlocal function fix_intent(mrow)\n  -- put the intent or arg attribute on a child element if mrow with these attributes contain only single child node \n  local element_name, _ = get_element_name(mrow)\n  if element_name ~= \"mrow\" then\n    return nil\n  end\n  local intent = get_attribute(mrow,\"intent\")\n  local arg = get_attribute(mrow, \"arg\")\n  if intent or arg then\n    local children = mrow:get_children()\n    local first_child = children[1]\n    -- if there is only one child, we can set the attributes on it and remove mrow\n    if #children == 1 and first_child:is_element() then\n      local parent = mrow:get_parent()\n      -- replace the mrow with its single child\n      local pos = mrow:find_element_pos()\n      parent._children[pos] = first_child\n      -- now set the attributes on the child element\n      first_child:set_attribute(\"arg\", arg)\n      first_child:set_attribute(\"intent\", intent)\n    end\n  end\nend\n\n\nreturn function(dom)\n  dom:traverse_elements(function(el)\n    if settings.output_format ~= \"odt\" then\n      -- LibreOffice needs <mfenced>, but Firefox doesn't\n      fix_mfenced(el)\n    else\n      fix_mo_to_mfenced(el)\n      fix_rel_mo(el)\n    end\n    fix_mtable_hlines(el)\n    fix_radicals(el)\n    fix_token_elements(el)\n    fix_nested_mstyle(el)\n    fix_missing_mtext(el)\n    fix_numbers(el)\n    fix_operators(el)\n    fix_mathvariant(el)\n    if settings.output_format ~= \"odt\" then\n      -- ODT needs older MathML version\n      fix_mathml_chars(el)\n    end\n    fix_dcases(el)\n    fix_intent(el)\n    top_mrow(el)\n    delete_last_empty_mtr(el)\n  end)\n  return dom\nend\n\n"
  },
  {
    "path": "domfilters/make4ht-odtfonts.lua",
    "content": "return function(dom, params)\n  -- fix ODT style for fonts \n  -- sometimes, fonts have missing size, we need to patch styles\n  local properties = get_filter_settings \"odtfonts\" or {}\n  local fix_lgfile_fonts = params.patched_lg_fonts or properties.patched_lg_fonts or {}\n  for _, style in ipairs(dom:query_selector \"style|style\") do\n    local typ  = style:get_attribute(\"style:family\")\n    if typ == \"text\" then\n      -- detect if the style is for font\n      local style_name = style:get_attribute(\"style:name\")\n      local name, size, size2, size3 = style_name:match(\"(.-)%-(%d*)x%-(%d*)x%-(%d+)\")\n      if name then\n        -- find if the style corresponds to a problematic font (it is set in formats/make4ht-odt.lua)\n        local used_name = name .. \"-\" .. size\n        if fix_lgfile_fonts[used_name] then\n          -- copy current style and fix the name\n          local new = style:copy_node()\n          new:set_attribute(\"style:name\", string.format(\"%s-x-%sx-%s\", name, size2, size3))\n          local parent = style:get_parent()\n          parent:add_child_node(new)\n        end\n      end\n    end\n  end\n  return dom\nend\n"
  },
  {
    "path": "domfilters/make4ht-odtimagesize.lua",
    "content": "local log = logging.new \"odtimagesize\"\n-- set correct dimensions to frames around images\nreturn  function(dom)\n  local frames  = dom:query_selector(\"draw|frame\")\n  for _, frame in ipairs(frames) do\n    local images = frame:query_selector(\"draw|image\")\n    if #images > 0 then\n      local image = images[1]\n      local width = image:get_attribute(\"svg:width\")\n      local height = image:get_attribute(\"svg:height\")\n      if widht then frame:set_attribute(\"svg:width\", width) end\n      if height then frame:set_attribute(\"svg:height\", height) end\n      log:debug(\"image dimensions\", width, height)\n    end\n  end\n  return dom\nend\n"
  },
  {
    "path": "domfilters/make4ht-odtpartable.lua",
    "content": "-- find all tables inside paragraphs, replace the found paragraphs with the child table\nreturn function(dom)\n  for _,table in ipairs(dom:query_selector(\"text|p table|table\")) do\n    -- replace the paragraph by its child element\n    local parent = table:get_parent() \n    parent:replace_node(table)\n  end\n  return dom\nend\n"
  },
  {
    "path": "domfilters/make4ht-odtsvg.lua",
    "content": "-- we need to set dimensions for SVG images produced by \\Picture commands\nlocal log = logging.new \"odtsvg\"\nlocal function get_svg_dimensions(filename) \n  local width, height\n  log:debug(\"file exists\", filename, mkutils.file_exists(filename))\n  if mkutils.file_exists(filename) then \n    for line in io.lines(filename) do\n      width = line:match(\"width%s*=%s*[\\\"'](.-)[\\\"']\") or width\n      height = line:match(\"height%s*=%s*[\\\"'](.-)[\\\"']\") or height\n      -- stop parsing once we get both width and height\n      if width and height then break end\n    end\n  end\n  return width, height\nend\n\n\n-- process \nreturn function(dom)\n  for _, pic in ipairs(dom:query_selector(\"draw|image\")) do\n    local imagename = pic:get_attribute(\"xlink:href\")\n    -- update SVG images dimensions\n    log:debug(\"image\", imagename)\n    local parent = pic:get_parent()\n    local width =  parent:get_attribute(\"svg:width\")\n    local height = parent:get_attribute(\"svg:height\")\n    -- if width == \"0.0pt\" then width = nil end\n    -- if height == \"0.0pt\" then height = nil end\n    if not width or not height then\n      if imagename:match(\"svg$\") then\n        width, height = get_svg_dimensions(imagename) --  or width, height\n      elseif imagename:match(\"png$\") or imagename:match(\"jpe?g$\") then\n      end\n    end\n    log:debug(\"dimensions\", width, height)\n    parent:set_attribute(\"svg:width\", width)\n    parent:set_attribute(\"svg:height\", height)\n    -- if \n  end\n  return dom\nend\n\n"
  },
  {
    "path": "domfilters/make4ht-sectionid.lua",
    "content": "local mkutils   = require \"mkutils\"\nlocal log = logging.new(\"tocid\")\n-- Unicode data distributed with ConTeXt\n-- defines \"characters\" table\nif not mkutils.isModuleAvailable(\"make4ht-char-def\") then\n  log:warning(\"char-def module not found\")\n  log:warning(\"cannot fix section id's\")\n  return function(dom) return dom end\nend\n\nlocal chardata = require \"make4ht-char-def\"\n\n\n\nlocal toc = nil\n\nlocal function is_letter(info)\n  -- test if character is letter\n  local category = info.category or \"\"\n  return category:match(\"^l\") \nend\n\nlocal function is_space(info)\n  local category = info.category or \"\"\n  return category == \"zs\"\nend\n\n\nlocal function is_number(char)\n  return char >= 48 and char <= 57\nend\n\nlocal uchar = utf8.char\nlocal function normalize_letter(char, result)\n  local info = chardata[char] or {}\n  -- first get lower case of the letter\n  local lowercase = info.lccode or char\n  -- remove accents. the base letter is in the shcode field\n  local lowerinfo = chardata[lowercase] or {}\n  -- when no shcode, use the current lowercase char\n  local shcode = lowerinfo.shcode or lowercase\n  -- shcode can be table if it contains multiple characters\n  -- normaliz it to a table, so we can add all letters to \n  -- the resulting string\n  if type(shcode) ~= \"table\" then shcode = {shcode} end\n  for _, x in ipairs(shcode) do\n    result[#result+1] = uchar(x)\n  end\nend\n\nlocal escape_name = function(name)\n  local result = {}\n  -- remove LaTeX commands\n  name = name:gsub(\"\\\\[%a]+\", \"\")\n  name = name:gsub(\"^%s+\", \"\"):gsub(\"%s+$\", \"\")\n  for _,char in utf8.codes(name) do\n    local info = chardata[char] or {}\n    if is_space(info) then\n      result[#result+1] = \" \"\n    elseif is_letter(info) then\n      normalize_letter(char, result)\n    elseif is_number(char) then\n      result[#result+1] = uchar(char)\n    end\n  end\n  --- convert table with normalized characters to string\n  local name = table.concat(result)\n  -- remove spaces\n  name = name:gsub(\"%s+\", \"-\")\n  name = name:gsub(\"^%-\", \"\")\n  -- ids cannot start with number in HTML 4, so we will add x\n  name = name:gsub(\"^(%d)\", \"x%1\")\n  return name\nend\n\nlocal function parse_toc_line(line)\n  -- the section ids and titles are saved in the following format:\n  -- \\csname a:TocLink\\endcsname{1}{x1-20001}{QQ2-1-2}{Nazdar světe}\n  -- ............................... id ................. title ...\n  local id, name = line:match(\"a:TocLink.-{.-}{(.-)}{.-}(%b{})\")\n  if id then\n    return id, escape_name(name)\n  end\nend\n\nlocal used = {}\n\nlocal function parse_toc(filename)\n  local toc = {}\n  if not mkutils.file_exists(filename) then return nil, \"Cannot open TOC file \"  .. filename end\n  for line in io.lines(filename) do\n    local id, name = parse_toc_line(line)\n    -- if section name doesn't contain any text, it would lead to id which contains only number\n    -- this is invalid in HTML\n    if name == \"\" then name = \"_\" end\n    local orig_name = name\n    -- not all lines in the .4tc file contains TOC entries\n    if id then\n      -- test if the same name was used already. user should be notified\n      if used[name] then\n        -- update \n        name = name .. used[name]\n        log:debug(\"Duplicate id found: \".. orig_name .. \". New id: \" .. name)\n      end\n      used[orig_name] = (used[orig_name] or 0) + 1\n      toc[id] = name\n    end\n  end\n  return toc\nend\n\n-- we don't want to change the original id, as there may be links to it from the outside\n-- so we will set it to the parent element (which should be h[1-6])\nlocal function set_id(el, id)\n  local section = el:get_parent()\n  local section_id = section:get_attribute(\"id\")\n  if section_id and section_id~=id then -- if it already has id, we don't override it, but create dummy child instead\n    local new = section:create_element(\"span\", {id=id})\n    section:add_child_node(new,1)\n  else\n    section:set_attribute(\"id\", id)\n  end\n\nend\n\n    \n-- we want to remove <a id=\"xxx\"> elements from some elements, most notably <figure>\nlocal elements_to_remove = {\n  figure = true,\n  figcaption\n}\n\nlocal function remove_a(el, parent, id)\n  parent:set_attribute(\"id\", id)\n  el:remove_node()\nend\n\nreturn  function(dom, par)\n    local msg\n    toc, msg = toc or parse_toc(mkutils.file_in_builddir(par.input .. \".4tc\", par))\n    msg = msg or \"Cannot load TOC\"\n    -- don't do anyting if toc cannot be found\n    if not toc then \n      log:warning(msg) \n      return dom\n    end\n    -- if user selects the \"notoc\" option on the command line, we \n    -- will not update href links\n    local notoc = false\n    if par[\"tex4ht_sty_par\"]:match(\"notoc\") then notoc = true end\n    -- the HTML file can already contain ID that we want to assign\n    -- we will not set duplicate id from TOC in that case\n    local toc_ids = {}\n    for _, el in ipairs(dom:query_selector(\"[id]\")) do\n      local id = el:get_attribute(\"id\")\n      toc_ids[id] = true\n    end\n    -- process all elements with id atribute or <a href>\n    for _, el in ipairs(dom:query_selector \"[id],a[href]\") do\n      local id, href = el:get_attribute(\"id\"), el:get_attribute(\"href\") \n      if id then\n        local name = toc[id]\n        local parent = el:get_parent()\n        -- remove unnecessary <a> elements if the parent doesn't have id yet\n        if elements_to_remove[parent:get_element_name()] \n          and not parent:get_attribute(\"id\") \n          and el:get_element_name() == \"a\"\n        then\n          remove_a(el, parent, id)\n          set_id(el, name)\n        -- replace id with new section id\n        elseif name and not toc_ids[name] then\n          set_id(el, name)\n        else\n          if name then\n            log:debug(\"Document already contains id: \" .. name)\n          end\n        end\n      end\n      if href and notoc == false then\n        -- replace links to sections with new id\n        local base, anchor = href:match(\"^(.*)%#(.+)\")\n        local name = toc[anchor]\n        if name then\n          el:set_attribute(\"href\", base .. \"#\" .. name)\n        end\n      end\n    end\n    return dom\n  end\n\n\n"
  },
  {
    "path": "domfilters/make4ht-t4htlinks.lua",
    "content": "-- This filter is used by the ODT output format to fix links\nreturn  function(dom)\n  for _, link in ipairs(dom:query_selector(\"t4htlink\")) do\n    local name = link:get_attribute(\"name\")\n    local href = link:get_attribute(\"href\")\n    local children = link:get_children()\n    -- print(\"link\", name, href, #link._children, link:get_text())\n    -- add a link if it contains any subnodes and has href attribute\n    if #children > 0 and href then\n      link._name = \"text:a\"\n      href = href:gsub(\"^.+4oo%#\", \"#\")\n      link._attr = {[\"xlink:type\"]=\"simple\", [\"xlink:href\"]=href}\n      -- if the link is named, add a bookmark\n      if name then\n        local bookmark = link:create_element(\"text:bookmark\", {[\"text:name\"] = name})\n        link:add_child_node(bookmark)\n      end\n      -- add bookmark if element has name \n    elseif name then\n      link._name = \"text:bookmark\"\n      link._attr = {[\"text:name\"] = name}\n    else\n      -- just remove the link in other cases\n      link:remove_node()\n    end\n  end\n  return dom\nend\n"
  },
  {
    "path": "domfilters/make4ht-tablecaption.lua",
    "content": "local function get_parent_table(caption)\n  -- recursively find the parent table of a caption element, as it can be inside <tr> and <td>\n  local parent = caption:get_parent()\n  if parent and parent:get_element_name() == \"table\" then\n    return parent\n  elseif parent then\n    return get_parent_table(parent)\n  else\n    return nil\n  end\nend\n\nreturn function(dom)\n  -- the caption element must be a first element in table, it cannot be contained inside tr\n  for _, caption in ipairs(dom:query_selector(\"table caption\")) do\n    local table = get_parent_table(caption)\n    if table then\n      -- insert caption as the first child of table\n      table:add_child_node(caption:copy_node(),1)\n      -- remove the original caption\n      caption:remove_node()\n    end\n  end\n  return dom\nend\n"
  },
  {
    "path": "domfilters/make4ht-tablerows.lua",
    "content": "local log = logging.new (\"tablerows\")\nreturn function(dom)\n  local has_child_elements = function(child)\n    -- detect if the element contains child elements\n    local child_elements = 0\n    local children = child:get_children()\n    local last_child_pos\n    for pos, el in ipairs(children) do\n      last_child_pos = pos\n      local step = el:is_element() and 1 or 0\n      -- log:info(\"element name\", el._name)\n      child_elements = child_elements + step\n    end\n    -- longtable has <td><p></p></td> inside empty rows, we regard them as empty\n    if child_elements == 1 and children[last_child_pos]:get_element_name() == \"p\" and child:get_text():gsub(\"%s\", \"\") == \"\" then\n      child_elements = 0\n    end\n    return child_elements > 0\n  end\n  local is_empty_row = function(row)\n    local not_empty = false\n    local element_count = 0\n    -- ignore hline rows\n    local row_class = row:get_attribute(\"class\") \n    if row_class == \"hline\" or row_class == \"cline\" then return false end\n    -- detect if the row contain only one empty child\n    for _,child in ipairs(row:get_children() or {}) do\n      if child:is_element() then \n        element_count = element_count + 1\n        -- empty rows contain only one element, it is not empty otherwise\n        if element_count > 1 or has_child_elements(child) then return false end\n\n        -- detect if it contains only whitespace\n        not_empty = child:get_text():gsub(\"%s\",\"\") ~= \"\" or not_empty\n      end\n    end\n    -- print(\"element count\", element_count, not_empty)\n    return element_count == 1 and not_empty == false\n  end\n  local is_not_styled = function(row, css)\n    -- get the id attribute and escape it, so it can be used in regexp\n    local id = row:get_attribute(\"id\")\n    if not id then return true end -- no styling without id\n    local search_term = \"%#\" .. id:gsub(\"%-\", \"%%-\")\n    -- if the CSS file contains the row id (<td> elements can also have id\n    -- that matches this pattern, so we should keep the row if we match them too)\n    return not css:match(search_term)\n  end\n  local hline_hr = function(row)\n    -- remove <hr> elements from \"hline\" rows\n    for _, hr in ipairs(row:query_selector(\".hline hr\")) do\n      hr:remove_node()\n    end\n  end\n  local longtable_last_row = function(tbl)\n    -- longtable contains last row of empty cells\n    local rows= tbl:query_selector(\"tr\")\n    local last_row = rows[#rows]\n    if not last_row or last_row:get_attribute(\"class\") == \"hline\" then return end\n    for _, cell in ipairs(last_row:query_selector(\"td\")) do\n      -- loop over cells in the last row a and detect that they are empty. break processing if they are not.\n      if has_child_elements(cell) or not cell:get_text():match(\"^%s*$\") then\n        return \n      end\n    end\n    last_row:remove_node()\n  end\n  local load_css_files = function()\n    -- the empty rows can be styled using CSS, for example configuration for \n    -- Booktabs does that. We shouldn't remove such rows.\n    local cssfiles = {}\n    for  _, link in ipairs(dom:query_selector(\"head link\")) do\n      local src = link:get_attribute(\"href\")\n      if src then\n        local f = io.open(src, \"r\")\n        if f then\n          local contents = f:read(\"*all\")\n          f:close()\n          table.insert(cssfiles, contents)\n        end\n      end\n    end\n    return table.concat(cssfiles, \"\\n\")\n  end\n  local css = load_css_files()\n  for _, tbl in ipairs(dom:query_selector(\"table\")) do\n    -- find the empty rows\n    local rows = tbl:query_selector(\"tr\")\n    for count, row in ipairs(rows) do\n      if is_empty_row(row) and is_not_styled(row, css) then row:remove_node() end\n      hline_hr(row)\n    end\n    if tbl:get_attribute(\"class\") and tbl:get_attribute(\"class\"):match(\"longtable\") then\n      longtable_last_row(tbl)\n    end\n  end\n  return dom\nend\n\n"
  },
  {
    "path": "extensions/make4ht-ext-common_domfilters.lua",
    "content": "local M = {}\n\n\n-- this variable will hold the output format name\nlocal current_format \n\nlocal filter = require \"make4ht-domfilter\"\n-- local process = filter {\"fixinlines\", \"idcolons\", \"joincharacters\" }\n\n-- filters support only html formats\nfunction M.test(format)\n  current_format = format\n  -- if format == \"odt\" then return false end\n  return true\nend\n\nfunction M.modify_build(make)\n  -- number of filters that should be moved to the beginning\n  local count = 0\n  if current_format == \"odt\" then\n    -- some formats doesn't make sense in the ODT format\n    local process = filter ({\"joincharacters\", \"mathmlfixes\"}, \"commondomfilters\")\n    local charclasses = {mn = true, [\"text:span\"] = true, mi=true}\n    make:match(\"4oo$\", process, {charclasses= charclasses})\n    -- match math documents\n    make:match(\"4om$\", process, {charclasses= charclasses})\n    count = 2\n  else\n    local process = filter({\"fixinlines\", \"idcolons\", \"joincharacters\", \"tablecaption\", \"mathmlfixes\", \"tablerows\",\"booktabs\", \"sectionid\", \"itemparagraphs\"}, \"commondomfilters\")\n    make:match(\"html?$\", process)\n    count = 1\n  end\n  return make\nend\n\nreturn M\n"
  },
  {
    "path": "extensions/make4ht-ext-common_filters.lua",
    "content": "local M = {}\n\n\nlocal filter = require \"make4ht-filter\"\nlocal process = filter({\"cleanspan-nat\", \"fixligatures\", \"hruletohr\", \"entities\", \"fix-links\"}, \"commonfilters\")\n\n-- filters support only html formats\nfunction M.test(format)\n  if format == \"odt\" then return false end\n  return true\nend\n\nfunction M.modify_build(make)\n  make:match(\"html?$\", process)\n  local matches = make.matches\n  -- the filters should be first match to be executed, especially if tidy\n  -- should be executed as well\n  if #matches > 1 then\n    local last = matches[#matches]\n    table.insert(matches, 1, last)\n    matches[#matches] = nil\n  end\n  return make\nend\n\nreturn M\n"
  },
  {
    "path": "extensions/make4ht-ext-copy_images.lua",
    "content": "local M = {}\nlocal mkutils = require \"mkutils\"\nlocal domfilter = require \"make4ht-domfilter\"\n\nlocal copied_images = {}\n\nlocal function image_copy(path, parameters, img_dir)\n  if mkutils.is_url(path) then return nil, \"External image\" end\n  -- get image basename\n  local basename = path:match(\"([^/]+)$\")\n  -- if outdir is empty, keep it empty, otherwise add / separator\n  local outdir = parameters.outdir == \"\" and \"\" or parameters.outdir .. \"/\"\n  if img_dir ~= \"\" then \n    outdir = outdir .. img_dir .. \"/\"\n  end\n  -- handle trailing //\n  outdir = outdir:gsub(\"%/+\",\"/\")\n  local output_file = outdir .. basename\n  if outdir == \"\" then\n    mkutils.cp(path, output_file)\n  else\n    mkutils.copy(path, output_file)\n  end\nend\n\n-- filters support only html formats\nfunction M.test(format)\n  current_format = format\n  if format == \"odt\" then return false end\n  return true\nend\n\nfunction M.modify_build(make)\n  local ext_settings = get_filter_settings \"copy_images\" or {}\n  local img_dir = ext_settings.img_dir or \"\"\n  local img_extensions = ext_settings.extensions or {\"jpg\", \"png\", \"jpeg\", \"svg\"}\n  local process = domfilter({\n    function(dom, par)\n      for _, img in ipairs(dom:query_selector(\"img\")) do\n        local src = img:get_attribute(\"src\")\n        if src and not mkutils.is_url(src) then\n          -- remove path specification\n          src = src:match(\"([^/]+)$\")\n          if img_dir ~= \"\" then\n            src = img_dir .. \"/\" ..  src\n            src = src:gsub(\"%/+\", \"/\")\n          end\n          img:set_attribute(\"src\", src)\n        end\n      end\n      return dom\n    end\n  }, \"copy_images\")\n\n  -- add matcher for all image extensions\n  for _, ext in ipairs(img_extensions) do\n    make:match(ext .. \"$\", function(path, parameters)\n      image_copy(path, parameters, img_dir)\n      -- prevent further processing of the image\n      return false\n    end)\n  end\n\n  make:match(\"html$\", process, {img_dir = img_dir})\n  return make\nend\n\nreturn M\n"
  },
  {
    "path": "extensions/make4ht-ext-detect_engine.lua",
    "content": "-- support magic comments used by TeXShop and TeXWorks to detect used engine and format\n--\nlocal M = {}\nlocal log = logging.new(\"detect engine\")\nlocal htlatex = require \"make4ht-htlatex\"\n\n-- we must change build sequence when Plain TeX is requested\nlocal change_table = {\n  tex = {\n    htlatex = \"etex\",\n    command = htlatex.httex\n  }, \n  pdftex = {\n    htlatex = \"etex\",\n    command = htlatex.httex\n  },\n  etex = {\n    htlatex = \"etex\",\n    command = htlatex.httex\n  },\n  luatex = {\n    htlatex = \"dviluatex\",\n    command = htlatex.httex\n  },\n  xetex = {\n    htlatex = \"xetex -no-pdf\",\n    command = htlatex.httex\n  },\n  xelatex = {\n    htlatex = \"xelatex -no-pdf\",\n  },\n  lualatex = {\n    htlatex = \"dvilualatex\",\n  },\n  pdflatex = {\n    htlatex = \"latex\"\n  },\n  harflatex = {\n    htlatex = \"lualatex-dev --output-format=dvi\"\n  },\n  harftex= {\n    htlatex = \"harftex --output-format=dvi\",\n    command = htlatex.httex\n  }\n}\n\nlocal function find_magic_program(filename)\n  -- find the magic line containing program name\n  local get_comment = function(line)\n    return line:match(\"%s*%%%s*(.+)\")\n  end\n  local empty_line = function(line) return line:match(\"^%s*$\") end\n  for line in io.lines(filename) do\n    local comment = get_comment(line)\n    -- read line after line from the file, break the processing after first non comment or non empty line\n    if not comment and not empty_line(line) then return nil, \"Cannot find program name\" end\n    comment = comment or \"\" -- comment is nil for empty lines\n    local program = comment:match(\"!%s*[Tt][Ee][Xx].-program%s*=%s*([^%s]+)\")\n    if program then return program:lower() end\n  end\nend\n\n-- update htlatex entries with detected program\nlocal function update_build_sequence(program, build_seq)\n  -- handle Plain TeX\n  local replaces = change_table[program] or {}\n  local is_xetex = program:match(\"xe\") -- we must handle xetex in tex4ht\n  for pos, entry in ipairs(build_seq) do\n    if entry.name == \"htlatex\" then\n      -- handle httex\n      entry.command = replaces.command or entry.command\n      local params = entry.params or {}\n      params.htlatex = replaces.htlatex or params.htlatex\n      entry.params = params\n    elseif is_xetex and entry.name == \"tex4ht\" then\n      -- tex4ht must process .xdv file if the TeX file was compiled by XeTeX\n      entry.params.tex4ht_par = entry.params.tex4ht_par .. \" -.xdv\"\n    end\n  end\nend\n\n\nfunction M.modify_build(make)\n  -- find magic comments in the TeX file\n  local build_seq = make.build_seq\n  local tex_file = make.params.tex_file\n  local program, msg = find_magic_program(tex_file)\n  if program then\n    log:info(\"Found program name\", program)\n    update_build_sequence(program, build_seq)\n  else\n    log:warning(\"Cannot find magic line with the program name\")\n  end\n  return make\nend\n\nreturn M\n"
  },
  {
    "path": "extensions/make4ht-ext-dvisvgm_hashes.lua",
    "content": "local dvireader = require \"make4ht-dvireader\"\nlocal mkutils = require \"mkutils\"\nlocal filter = require \"make4ht-filter\"\nlocal log = logging.new \"dvisvgm_hashes\"\n\n\nlocal dvisvgm_par = {}\n\nlocal M = {}\n-- mapping between tex4ht image names and hashed image names\nlocal output_map = {}\nlocal dvisvgm_options = \"-n --exact --embed-bitmaps -c ${scale},${scale}\"\nlocal parallel_size = 64\nlocal make_command = \"make -j ${process_count} -f ${make_file}\"\nlocal test_make_command = \"make -v\"\n-- local parallel_size = 3\n\nlocal function make_hashed_name(base, hash)\n  return base .. \"-\" ..hash..\".svg\"\nend\n\n-- detect the number of available processors\nlocal cpu_cnt = 3  -- set a reasonable default for non-Linux systems\n\nif os.name == 'linux' then\n  cpu_cnt = 0\n  local cpuinfo=assert(io.open('/proc/cpuinfo', 'r'))\n  for line in cpuinfo:lines() do\n    if line:match('^processor') then\n      cpu_cnt = cpu_cnt + 1\n    end\n  end\n  -- set default number of threds if no CPU core have been found\n  if cpu_cnt == 0 then cpu_cnt = 1 end\n  cpuinfo:close()\nelseif os.name == 'cygwin' or os.type == 'windows' then\n  -- windows has NUMBER_OF_PROCESSORS environmental value\n  local nop = os.getenv('NUMBER_OF_PROCESSORS')\n  if tonumber(nop) then\n    cpu_cnt = nop\n  end\nend\n\n\n\n-- process output of dvisvgm and find output page numbers and corresponding files\nlocal function get_generated_pages(output, pages)\n  local pages = pages or {}\n  local pos = 1\n  local pos, finish, page = string.find(output, \"processing page (%d+)\", pos)\n  while(pos) do\n    pos, finish, file = string.find(output, \"output written to ([^\\n^\\r]+)\", finish)\n    pages[tonumber(page)] = file\n    if not finish then break end\n    pos, finish, page = string.find(output, \"processing page (%d+)\", finish)\n  end\n  return pages\nend\n\nlocal function make_ranges(pages)\n  local newpages = {}\n  local start, stop\n  for i=1,#pages do\n    local current = pages[i]\n    local next_el = pages[i+1] or current + 100 -- just select a big number\n    local diff = next_el - current\n    if diff == 1 then\n      if not start then start = current end\n    else\n      local element\n      if start then\n        element = start .. \"-\" .. current\n      else\n        element = current\n      end\n      newpages[#newpages+1] = element\n      start = nil\n    end\n  end\n  return newpages\nend\n\nlocal function read_log(dvisvgmlog)\n  local f = io.open(dvisvgmlog, \"rb\")\n  if not f then return nil, \"Cannot read dvisvgm log\" end\n  local output = f:read(\"*all\")\n  f:close()\n  return output\nend\n\n-- test the existence of GNU Make, which can execute tasks in parallel\nlocal function test_make()\n  local make = io.popen(test_make_command, \"r\")\n  local content = make:read(\"*all\")\n  make:close()\n  -- io.popen always returns valid handle, so we can find that the command doesn't exists only by checking that the \n  -- content is empty\n  return content~=nil and content ~= \"\"\nend\n\nlocal function save_file(filename, text)\n  local f = io.open(filename, \"w\")\n  f:write(text) \n  f:close()\nend\n\n\nlocal function make_makefile_command(idvfile, page_sequences)\n  local logs = {}\n  local all = {} -- list of targets in the \"all:\" makefile target\n  local targets = {}\n  local basename = idvfile:gsub(\".idv$\", \"\")\n  local makefilename = basename .. \"-images\" .. \".mk\"\n  -- build make targets\n  for i, ranges in ipairs(page_sequences) do\n    local target = basename .. \"-\" .. i\n    local logfile = target .. \".dlog\"\n    logs[#logs + 1] = logfile\n    all[#all+1] = target\n    local chunk = target .. \":\\n\\tdvisvgm -v4 \" .. dvisvgm_options .. \" -p \" .. ranges  .. \" \" .. idvfile .. \" 2> \" .. logfile .. \"\\n\"\n    targets[#targets + 1] = chunk\n  end\n  -- construct makefile and save it\n  local makefile = \"all: \" .. table.concat(all, \" \") .. \"\\n\\n\" .. table.concat(targets, \"\\n\")\n  save_file(makefilename, makefile)\n  local command = make_command % {process_count = cpu_cnt, make_file = makefilename}\n  log:debug(\"Makefile command: \" .. command)\n  return command, logs\nend\n\nlocal function prepare_command(idvfile, pages)\n  local logs = {}\n  if #pages > parallel_size and test_make() then \n    local page_sequences = {}\n    for i=1, #pages, parallel_size do\n      local current_pages = {}\n      for x = i, i+parallel_size -1 do\n        current_pages[#current_pages + 1] = pages[x]\n      end\n      table.insert(page_sequences,table.concat(make_ranges(current_pages), \",\"))\n    end\n    return make_makefile_command(idvfile, page_sequences)\n  end\n  -- else\n    local pagesequence = table.concat(make_ranges(pages), \",\")\n    -- the stderr from dvisvgm must be redirected and postprocessed\n    local dvisvgmlog = idvfile:gsub(\"idv$\", \"dlog\")\n    -- local dvisvgm = io.popen(\"dvisvgm -v4 -n --exact -c 1.15,1.15 -p \" .. pagesequence .. \" \" .. idvfile, \"r\")\n    local command = \"dvisvgm -v4 \" .. dvisvgm_options .. \" -p \" .. pagesequence .. \" \" .. idvfile .. \" 2> \" .. dvisvgmlog\n    return command, {dvisvgmlog}\n  -- end\nend\n\nlocal function execute_dvisvgm(idvfile, pages)\n  if #pages < 1 then return nil, \"No pages to convert\" end\n  local command, logs = prepare_command(idvfile, pages)\n  log:info(command)\n  os.execute(command)\n  local generated_pages = {}\n  for _, dvisvgmlog in ipairs(logs) do\n    local output = read_log(dvisvgmlog)\n    generated_pages = get_generated_pages(output, generated_pages)\n  end\n  return generated_pages\nend\n\nlocal function get_dvi_pages(arg)\n  -- list of pages to convert in this run\n  local to_convert = {}\n  local idv_file = arg.input .. \".idv\"\n  -- set extension options\n  local extoptions = mkutils.get_filter_settings \"dvisvgm_hashes\" or {}\n  dvisvgm_options = arg.options or extoptions.options or dvisvgm_options\n  parallel_size = arg.parallel_size or extoptions.parallel_size or parallel_size\n  cpu_cnt = arg.cpu_cnt or extoptions.cpu_cnt or cpu_cnt\n  dvisvgm_par.scale = arg.scale or extoptions.scale or 1.4\n  dvisvgm_options = dvisvgm_options % dvisvgm_par\n  make_command   = arg.make_command or extoptions.make_command or make_command\n  test_make_command = arg.test_make_command or extoptions.test_make_command or test_make_command\n  local f = io.open(idv_file, \"rb\")\n  if not f then return nil, \"Cannot open idv file: \" .. idv_file end\n  local content = f:read(\"*all\")\n  f:close()\n  local dvi_pages = dvireader.get_pages(content)\n  -- we must find page numbers and output name sfor the generated images\n  local lg = mkutils.parse_lg(arg.input ..\".lg\", arg.builddir)\n  for _, name in ipairs(lg.images) do\n    local page = tonumber(name.page)\n    local hash = dvi_pages[page]\n    local tex4ht_name = name.output\n    local output_name = make_hashed_name(arg.input, hash)\n    output_map[tex4ht_name] = output_name\n    if not mkutils.file_exists(output_name) then\n      log:debug(\"output file: \".. output_name)\n      to_convert[#to_convert+1] = page\n    end\n  end\n  local generated_files, msg = execute_dvisvgm(idv_file, to_convert)\n  if not generated_files then\n    return nil, msg\n  end\n\n  -- rename the generated files to the hashed filenames\n  for page, file in pairs(generated_files) do\n    os.rename(file, make_hashed_name(arg.input, dvi_pages[page]))\n  end\n\nend\n\nfunction M.test(format)\n  -- ODT format doesn't support SVG\n  if format == \"odt\" then return false end\n  return true\nend\n\nfunction M.modify_build(make)\n  -- this must be used in the .mk4 file as\n  -- Make:dvisvgm_hashes {}\n  make:add(\"dvisvgm_hashes\", function(arg)\n    get_dvi_pages(arg)\n  end, \n  {\n  })\n\n  -- insert dvisvgm_hashes command at the end of the build sequence -- it needs to be called after t4ht\n  make:dvisvgm_hashes {}\n\n  -- replace original image names with hashed names\n  local executed = false\n  make:match(\".*\", function(arg)\n    if not executed then\n      executed = true\n      local lgfiles = make.lgfile.files\n      for i, filename in ipairs(lgfiles) do\n        local replace = output_map[filename]\n        if replace then\n          lgfiles[i] = replace\n        end\n      end\n      -- tex4ebook process also the images table, so we need to replace generated filenames here as well\n      local lgimages = make.lgfile.images\n      for _, image in ipairs(lgimages) do\n        local  replace = output_map[image.output]\n        if replace then\n          image.output = replace\n        end\n      end\n    end\n  end)\n\n  -- fix src attributes\n  local process = filter({\n    function(str, filename)\n      return str:gsub('src=[\"\\'](.-)([\"\\'])', function(filename, endquote)\n        local newname = output_map[filename] or filename\n        log:debug(\"newname\", newname)\n        return 'src=' .. endquote .. newname  .. endquote\n      end)\n    end\n  }, \"dvisvgmhashes\")\n\n  make:match(\"htm.?$\", process)\n\n  -- disable the image processing\n  for _,v in ipairs(make.build_seq) do\n    if v.name == \"t4ht\" then\n      local t4ht_par = v.params.t4ht_par or make.params.t4ht_par or \"\"\n      v.params.t4ht_par = t4ht_par .. \" -p\"\n    end\n  end\n  make:image(\".\", function() return \"\" end)\n  return make\nend\n\nreturn M\n"
  },
  {
    "path": "extensions/make4ht-ext-inlinecss.lua",
    "content": "local M = {}\n\nlocal filter = require \"make4ht-domfilter\"\n\n-- filters support only html formats\nfunction M.test(format)\n  if format:match(\"html\") then return true end\n  return false\nend\n\nfunction M.modify_build(make)\n  local process = filter({\"inlinecss\"}, \"inlinecss\")\n  make:match(\"html?$\", process)\n  return make\nend\nreturn M\n"
  },
  {
    "path": "extensions/make4ht-ext-join_colors.lua",
    "content": "local M = {}\n\nlocal filter = require \"make4ht-domfilter\"\n\n-- filters support only html formats\nfunction M.test(format)\n  if format == \"odt\" then return false end\n  return true\nend\n\nfunction M.modify_build(make)\n  local process = filter({\"joincolors\"}, \"joincolors\")\n  make:match(\"html?$\", process)\n  return make\nend\nreturn M\n"
  },
  {
    "path": "extensions/make4ht-ext-latexmk_build.lua",
    "content": "-- use Latexmk in first LaTeX call\n-- only in the first call, because we don't need to execute  biber, etc. in the subsequent\n-- LaTeX calls, these are only for resolving the cross-references\nlocal M = {}\nlocal htlatex_names = {\n  htlatex = true,\n  autohtlatex = true,\n}\nfunction M.modify_build(make)\n  local used = false\n  local first \n  local build_seq = make.build_seq\n  -- find first htlatex call in the build sequence\n  for pos,v in ipairs(build_seq) do\n    if htlatex_names[v.name]  and not first then\n      first = pos\n    end\n  end\n  -- we need to save contents of the .tmp file, to prevent extra executions from latexmk\n  -- tex4ht command overwrites content that was set by LaTeX with it's own stuff\n  local tmp_file \n  make:add(\"save_tmp\", function(par)\n    local f = io.open(mkutils.file_in_builddir(par.input .. \".tmp\", par), \"r\")\n    if f then\n      tmp_file = f:read(\"*all\")\n      f:close()\n    end\n    return 0\n  end)\n  make:add(\"load_tmp\", function(par)\n    if tmp_file then\n      local f = io.open(mkutils.file_in_builddir(par.input .. \".tmp\", par), \"w\")\n      if f then\n        f:write(tmp_file)\n      end\n    end\n    return 0\n  end)\n  -- if htlatex was found\n  if first then\n    -- handle tmp file\n    make:load_tmp {}\n    make:save_tmp {}\n    -- add dummy latexmk call to the build sequence\n    make:latexmk {}\n    -- replace name, command and type in the first htlatex\n    -- call with values from the dummy latexmk call\n    local replaced = build_seq[first]\n    local latexmk = build_seq[#build_seq]\n    replaced.name = latexmk.name\n    replaced.command = latexmk.command\n    replaced.type = latexmk.type\n    -- remove the dummy latexmk\n    table.remove(build_seq)\n  end\n  -- remove htlatex calls from the build sequence, they are unnecessary\n  local new_build_seq = {}\n  for pos, v in ipairs(build_seq) do\n    if v.name ~= \"htlatex\" and v.name ~= \"tex4ht\" then\n      table.insert(new_build_seq, v)\n    elseif v.name == \"tex4ht\" then\n      -- insert save_tmp before tex4ht\n      table.insert(new_build_seq, build_seq[#build_seq])\n      -- remove save_tmp from the end\n      table.remove(build_seq)\n      -- and now insert tex4ht\n      table.insert(new_build_seq, v)\n    end\n  end\n  make.build_seq = new_build_seq\n  return make\nend\nreturn M\n"
  },
  {
    "path": "extensions/make4ht-ext-mathjaxnode.lua",
    "content": "local M = {}\n\n\nlocal filter = require \"make4ht-filter\"\nfunction M.test(format)\n  if format == \"odt\" then return false end\n  return true\nend\n\nfunction M.prepare_parameters(params)\n  params.tex4ht_sty_par = params.tex4ht_sty_par  .. \",mathml\"\n  return params\n\nend\nfunction M.modify_build(make)\n  local mathjax = filter({ \"mathjaxnode\"}, \"mathjaxnode\")\n  -- this extension needs mathml enabled\n  make:match(\"html?$\",mathjax)\n  return make\nend\n\nreturn M\n"
  },
  {
    "path": "extensions/make4ht-ext-mjcli.lua",
    "content": "local M = {}\n\n\nlocal filter = require \"make4ht-filter\"\nfunction M.test(format)\n  -- this extension works only for formats based on HTML, as it produces\n  -- custom HTML tags that would be ilegal in XML \n  if not format:match(\"html5?$\") then return false end\n  return true\nend\n\n-- \nlocal detected_latex = false\nfunction M.prepare_parameters(params)\n  -- mjcli supports both MathML and LaTeX math input\n  -- LaTeX math is keep if user uses \"mathjax\" option for make4ht\n  -- \"mathjax\" option used in \\Preamble in the .cfg file doesn't work \n  if params.tex4ht_sty_par:match(\"mathjax\") then\n    detected_latex = true\n  else\n    params.tex4ht_sty_par = params.tex4ht_sty_par  .. \",mathml\"\n  end\n  return params\n\nend\nfunction M.modify_build(make)\n  local mathjax = filter({ \"mjcli\"}, \"mjcli\")\n  local params = {}\n  if detected_latex then\n    params.latex = true\n  end\n  make:match(\"html?$\",mathjax, params)\n  return make\nend\n\nreturn M\n"
  },
  {
    "path": "extensions/make4ht-ext-nodynamicodt.lua",
    "content": "local M = {}\n\n-- this extension covnerts links, tables of contents and other dynamic content in the ODT format to plain text\n\nlocal filter = require \"make4ht-domfilter\"\n\n-- this extension only works for the ODT format\nM.test = function(format)\n  return format==\"odt\"\nend\n\nlocal function nodynamiccontent(dom)\n  for _,link in ipairs(dom:query_selector(\"text|a\")) do\n    -- change links to spans\n    link._name = \"text:span\"\n    -- remove attributes\n    link._attr = {}\n\n  end\n  for _, bibliography in ipairs(dom:query_selector(\"text|bibliography\")) do\n    -- remove links from bibliography\n    -- use div instead of bibliography\n    bibliography._name = \"text:div\"\n    -- remove bibliography-source elements\n    for _, source in ipairs(bibliography:query_selector(\"text:bibliography-source\")) do\n      source:remove_node()\n    end\n    for _, index in ipairs(bibliography:query_selector(\"text|index-body\")) do\n      -- use div instead of bibliography-entry\n      index._name = \"text:div\"\n    end\n\n  end\n  for _, toc in ipairs(dom:query_selector(\"text|table-of-content\")) do\n    -- remove links from toc\n    -- use div instead of table-of-contents\n    toc._name = \"text:div\"\n    for _, entry in ipairs(toc:query_selector(\"text|index-body, text|index-title\")) do\n      -- use div instead of table-of-contents-entry\n      entry._name = \"text:div\"\n    end\n  end\n  return dom\nend\n\nM.modify_build = function(make)\n  local process = filter({nodynamiccontent}, \"nodynamiccontent\")\n  Make:match(\"4oo$\",process)\n  return make\nend\n\nreturn M\n"
  },
  {
    "path": "extensions/make4ht-ext-odttemplate.lua",
    "content": "local M = {}\n\nlocal filter = require \"make4ht-filter\"\n\n-- this extension only works for the ODT format\nM.test = function(format)\n  return format==\"odt\"\nend\n\nM.modify_build = function(make)\n  local process = filter({\"odttemplate\"}, \"odttemplate\")\n  make:match(\"4oy$\", process)\n  return make\nend\n\nreturn M\n"
  },
  {
    "path": "extensions/make4ht-ext-preprocess_input.lua",
    "content": "-- preprocess R literate sources or Markdown files to LaTeX\nlocal M = {}\nlocal log = logging.new \"preprocess_input\"\nlocal mkutils = require \"mkutils\"\n\nlocal commands = {\n  knitr = { command = 'Rscript -e \"library(knitr); knit(\\'${tex_file}\\', output=\\'${tmp_file}\\')\"'},\n  pandoc = { command = 'pandoc -f ${input_format} -s -o \\'${tmp_file}\\' -t latex \\'${tex_file}\\''},\n  render = { command = 'Rscript -e \"library(rmarkdown); render(\\'${tex_file}\\', output_file=\\'${tmp_file}\\',output_format = \\'latex_document\\')\"'}\n}\nlocal filetypes = {\n  rnw = {sequence = {\"knitr\"} },\n  rtex = {sequence = {\"knitr\"}},\n  rmd = {sequence = {\"render\"}},\n  rrst = {sequence = {\"knitr\", \"pandoc\"}, options = {input_format = \"rst\"}},\n  md = {sequence = {\"pandoc\"}, options = {input_format = \"markdown\"}},\n  rst = {sequence = {\"pandoc\"}, options = {input_format = \"rst\"}},\n}\n\nlocal function get_temp_name(arg,curr, length)\n  -- we don't want to use the temp dir, because graphics would be then generated outside of \n  -- the directory of the source document. so we will make\n  local tmp_name = os.tmpname()\n  if pos == sequence then\n    -- base tmp_name on the input name in the last step of sequence\n    -- so the generated images won't have random names\n    tmp_name = arg.input .. \"-preprocess_input\"\n  else\n    tmp_name = tmp_name:match(\"([^/\\\\]+)$\")\n  end\n  return tmp_name\nend\n\n\n\nlocal function execute_sequence(sequence, arg, make)\n  -- keep track of all generated tmp files\n  local temp_files = {}\n  -- the temporary file for the current compilation step \n  -- should become the tex_file for the next one. It doesn't\n  -- matter that it isn't TeX file in some cases\n  local previous_temp \n  for pos, cmd_name in ipairs(sequence) do\n    local tmp_name = get_temp_name(arg,pos, #sequence)\n    temp_files[#temp_files+1] = tmp_name\n    -- make the temp file name accessible to the executed commands\n    arg.tmp_file = tmp_name\n    -- the current temporary file should become tex_file in the next step\n    -- in the first execution of the compilation sequence we will use the \n    -- actual input file name\n    arg.tex_file = previous_temp or arg.tex_file\n    previous_temp = tmp_name\n    -- get the command to execute\n    local cmd  = commands[cmd_name]\n    -- fill the command template with make4ht arguments and execute\n    local command = cmd.command % arg\n    log:info(command)\n    mkutils.execute(command)\n  end\n  return temp_files\nend\n\nlocal function get_preprocessing_pipeline(input_file)\n    -- detect the file extension\n    local extension = input_file:match(\"%.(.-)$\")\n    if not extension then return nil, \"Cannot get extension: \" .. input_file end\n    -- the table with file actions is case insensitive\n    -- the extension is converted to lowercase in order\n    -- to support both .rnw and .Rnw\n    extension = string.lower(extension)\n    local matched = filetypes[extension]\n    if not matched then return nil, \"Unsupported extension: \" .. extension end\n    return matched\nend\n\n-- join the make4ht params and command options tables\nlocal function make_options(arg, command_options)\n  local options = {}\n  local command_options = command_options or {}\n  for k,v in pairs(arg) do options[k] = v end\n  for k,v in pairs(command_options) do options[k] = v end\n  return options\nend\n\nM.modify_build = function(make)\n\n  -- get access to the main arguments\n  local arg = make.params\n  -- get the execution sequence for the input format\n  local matched, msg  = get_preprocessing_pipeline(arg.tex_file)\n  if not matched then \n    log:error(\"preprocess_input error: \".. msg)\n    return\n  end\n  -- prepare options \n  local options = make_options(arg, matched.options)\n  -- run the execution sequence\n  local temp_files = execute_sequence(matched.sequence or {}, options, make)\n  -- the last temporary file contains the actual TeX file\n  local last_temp_file = temp_files[#temp_files]\n  -- remove the intermediate temp files\n  if #temp_files > 2 then\n    for i = 1, #temp_files - 1 do\n      log:debug(\"Removing temporary file\", temp_files[i])\n      os.remove(temp_files[i])\n    end\n  end\n  if last_temp_file then\n    -- update all commands in the .mk4 file with the temp file as tex_file\n    local update_params = function(cmd)\n      local params = cmd.params\n      params.tex_file = last_temp_file\n      params.is_tmp_file = true\n    end\n    for _, cmd in ipairs(make.build_seq) do\n      update_params(cmd)\n    end\n    -- also update the main params\n    update_params(make)\n  end\n  return make\nend\n\nreturn M\n"
  },
  {
    "path": "extensions/make4ht-ext-staticsite.lua",
    "content": "local M = {}\nlocal filter = require \"make4ht-filter\"\nlocal mkutils = require \"mkutils\"\nlocal log = logging.new \"staticsite\"\n\n-- get the published file name\nlocal function get_slug(settings)\n  local published_name = mkutils.remove_extension(settings.tex_file) .. \".published\"\n  local config = get_filter_settings \"staticsite\"\n  local file_pattern = config.file_pattern or \"%Y-%m-%d-${input}\"\n  local time = os.time()\n\n  -- we must save the published date, so the subsequent compilations at different days\n  -- use the same name\n  if mkutils.file_exists(published_name) then\n    local f = io.open(published_name, \"r\")\n    local readtime  = f:read(\"*line\")\n    time = tonumber(readtime)\n    log:info(\"Already pubslished\", os.date(\"%Y-%m-%d %H:%M\", time))\n    f:close()\n  else\n    -- escape \n    -- slug must contain the unescaped input name\n    local f = io.open(published_name, \"w\")\n    log:info(\"Publishing article\", os.date(\"%Y-%m-%d %H:%M\", time))\n    f:write(time)\n    f:close()\n  end\n  -- set the updated and publishing times\n  local updated\n  -- the updated time will be set only when it is more than one day from the published time\n  local newtime = os.time()\n  if (newtime - time) > (24 * 3600) then updated = newtime end\n  filter_settings \"staticsite\" {\n    header = {\n      time = time,\n      updated = updated\n    }\n  }\n\n  -- make the output file name in the format YYYY-MM-DD-old-filename.html\n  local slug = os.date(file_pattern,time) % settings\n  return slug\nend\n\n\n-- it is necessary to set correct -jobname in latex_par parameters field\n-- in order to the get correct HTML file name\nlocal function update_jobname(slug, latex_par)\n  local latex_par = latex_par or \"\"\n  if latex_par:match(\"%-jobname\") then\n    local firstchar=latex_par:match(\"%-jobname=.\")\n    local replace_pattern=\"%-jobname=[^%s]+\"\n    if firstchar == \"'\" or firstchar=='\"' then\n      replace_pattern = \"%-jobname=\".. firstchar ..\"[^%\"..firstchar..\"]+\"\n    end\n    \n    return latex_par:gsub(replace_pattern, \"-jobname=\".. slug)\n  else\n    return latex_par .. \"-jobname=\"..slug\n  end\nend\n\n-- execute the function passed as parameter only once, when the file matching\n-- starts\nlocal function insert_filter(make, pattern, fn)\n  local insert_executed = false\n  table.insert(make.matches, 1, {\n    pattern=pattern,\n    params = make.params or {},\n    command = function()\n      if not insert_executed  then\n        fn()\n      end\n      insert_executed = true\n    end\n  })\nend\n\nlocal function remove_maketitle(make)\n  -- use DOM filter to remove \\maketitle block\n  local domfilter = require \"make4ht-domfilter\"\n  local process = domfilter({\n    function(dom)\n      local maketitles = dom:query_selector(\".maketitle\")\n      for _, el in ipairs(maketitles) do\n        log:debug(\"removing maketitle\")\n        el:remove_node()\n      end\n      return dom\n    end\n  }, \"staticsite\")\n  make:match(\"html$\", process)\nend\n\n\nlocal function copy_files(filename, par)\n  local function prepare_path(dir, subdir)\n    local f = filename\n    if par.builddir then\n        f = f:gsub(\"^\" .. par.builddir .. \"/\", \"\")\n    end\n    local path = dir .. \"/\" .. subdir .. \"/\" .. f\n    return path:gsub(\"//\", \"/\")\n  end\n  -- get extension settings\n  local site_settings = get_filter_settings \"staticsite\"\n  local site_root = site_settings.site_root or par.outdir \n  if site_root == \"\" then site_root = \"./\" end\n  local map = site_settings.map or {}\n  -- default path without subdir, will be used if the file is not matched\n  -- by any pattern in the map\n  local path = prepare_path(site_root, \"\")\n  for pattern, destination in pairs(map) do\n    if filename:match(pattern) then\n      path = prepare_path(site_root, destination)\n      break\n    end\n  end\n  -- it is possible to use string extrapolation in path, for example for slug\n  mkutils.copy(filename, path % par)\nend\n\nfunction M.modify_build(make)\n  -- it is necessary to insert the filters for YAML header and file copying as last matches\n  -- we use an bogus match which will be executed only once as the very first one to insert\n  -- the filters\n  -- I should make filter from this\n  local process = filter({\n    \"staticsite\"\n  }, \"staticsite\")\n\n  -- detect if we should remove maketitle\n  local site_settings = get_filter_settings \"staticsite\"\n  -- \\maketitle is removed by default, set `remove_maketitle=false` setting to disable that\n  if site_settings.remove_maketitle ~= false then\n    remove_maketitle(make)\n  end\n\n  local settings = make.params\n  -- get the published file name\n  local slug = get_slug(settings)\n  for _, cmd in ipairs(make.build_seq) do\n    -- all commands must use the published file name\n    cmd.params.input = slug\n    cmd.params.latex_par = update_jobname(slug, cmd.params.latex_par)\n  end\n\n  local quotepattern = '(['..(\"%^$().[]*+-?\"):gsub(\"(.)\", \"%%%1\")..'])'\n  local mainfile = string.gsub(slug, quotepattern, \"%%%1\")\n\n  -- run the following code once in the first match on the first file\n  insert_filter(make, \".*\", function()\n    -- for _, match in ipairs(make.matches) do\n    --   match.params.outdir = outdir\n    --   print(match.pattern, match.params.outdir)\n    -- end\n    local params = make.params\n    params.slug = slug\n    make:match(\"html?$\", process, params)\n    make:match(\".*\", copy_files, params)\n  end)\n\n  return make\nend\n\nreturn M\n"
  },
  {
    "path": "extensions/make4ht-ext-tidy.lua",
    "content": "local M = {}\n\nlocal log = logging.new \"tidy\"\nfunction M.test(format)\n  if format == \"odt\" then return false end\n  return true\nend\n\nlocal empty_elements = {\n  area=true,\n  base=true,\n  br=true,\n  col=true,\n  embed=true,\n  hr=true,\n  img=true,\n  input=true,\n  keygen=true,\n  link=true,\n  meta=true,\n  param=true,\n  source=true,\n  track=true,\n  wbr=true,\n}\n\n-- LuaXML cannot read HTML with unclosed tags (like <meta name=\"hello\" content=\"world\">)\n-- Tidy removes end slashes in the HTML output, so\n-- this function will add them back\nlocal function close_tags(s)\n  return s:gsub(\"<(%w+)([^>]-)>\", function(tag, rest)\n    local endslash = \"\"\n    if empty_elements[tag] then endslash = \" /\" end\n    return string.format(\"<%s%s%s>\", tag, rest, endslash)\n  end)\nend\n    \n\n\nfunction M.modify_build(make)\n  make:match(\"html?$\", function(filename, par)\n    local settings = get_filter_settings \"tidy\" or {}\n    par.options = par.options or settings.options or \"-utf8 -w 512 -ashtml -q\"\n    local command = \"tidy ${options}  ${filename}\" % par\n    log:info(\"running tidy: \".. command)\n    -- os.execute(command)\n    local run, msg = io.popen(command, \"r\")\n    local result = run:read(\"*all\")\n    run:close()\n    if not result or  result == \"\" then\n      log:warning(\"Cannot execute Tidy command\")\n      return nil\n    end\n    result = close_tags(result)\n    local f = io.open(filename, \"w\")\n    f:write(result)\n    f:close()\n  end)\n  return make\nend\n\nreturn M\n"
  },
  {
    "path": "filters/make4ht-cleanspan-nat.lua",
    "content": "-- cleanspan function submitted by Nat Kuhn \n-- http://www.natkuhn.com/\n\nlocal function filter(s)\n    local pattern = \"(<span%s+([^>]+)>[^<]*)</span>(%s*)<span%s+%2>\"\n    repeat\n      s, n = s:gsub(pattern, \"%1%3\")\n    until n == 0\n    return s\nend\n\nreturn filter\n"
  },
  {
    "path": "filters/make4ht-cleanspan.lua",
    "content": "-- make4ht-cleanspan4ht.lua \n-- fixes spurious <span> elements in tex4ht output\n\n\nfunction filter(input)\n\tlocal parse_args = function(s)\n\t\tlocal at = {}\n\t\ts:gsub(\"(%w+)%s*=%s*\\\"([^\\\"]-)\\\"\", function(k,w)\n\t\t\tat[k]=w\n\t\tend)\n\t\treturn at\n\tend\n\t-- local pattern = \"(<?/?[%w]*>?)<span[%s]*class=\\\"([^\\\"]+)\\\"[%s]*>\"\n  local pattern = \"(<?/?[%w]*>?)([%s]*)<span[%s]*([^>]-)>\"\n\tlocal last_class = \"\"\n\tlocal depth = 0\n\treturn  input:gsub(pattern, function(tag,space, args)\n\t\tlocal attr = parse_args(args) or {}\n\t\tlocal class = attr[\"class\"] or \"\"\n\t\tif tag == \"</span>\" then\n\t\t\tif class == last_class and class~= \"\"  then \n\t\t\t\tlast_class = class\n\t\t\t\treturn space .. \"\"\n\t\t\tend\n\t\telseif tag == \"\" then\n\t\t\tclass=\"\"\n\t\tend\n\t\tlast_class = class\n\t\treturn tag ..space .. '<span '..args ..'>'\n\tend)\nend\n\nreturn filter\n"
  },
  {
    "path": "filters/make4ht-domfilter.lua",
    "content": "local filter_lib = require \"make4ht-filterlib\"\nlocal dom    = require \"luaxml-domobject\"\nlocal mkutils    = require \"mkutils\"\nlocal log = logging.new \"domfilter\"\n\nlocal function load_filter(filtername)\n\treturn require(\"domfilters.make4ht-\"..filtername)\nend\n\n-- get snippet of the position where XML parsing failed\nlocal function get_html_snippet(str, errmsg)\n  -- we can get position in bytes from message like this:   \n  -- /home/mint/texmf/scripts/lua/LuaXML/luaxml-mod-xml.lua:175: Unbalanced Tag (/p) [char=1112]\n  local position = tonumber(errmsg:match(\"char=(%d+)\") or \"\")\n  if not position then return \"Cannot find error position\" end\n  -- number of bytes around the error position that shoule be printed\n  local error_context = 100\n  local start = position > error_context and position - error_context or 0\n  local stop = (position + error_context) < str:len() and position + error_context or str:len()\n  return str:sub(start, stop)\nend\n\n-- save processed names, in order to block multiple executions of the filter\n-- sequence on a same file\nlocal processed = {}\n\nlocal function filter(filters, name)\n  -- because XML parsing to DOM is potentially expensive operation\n  -- this filter will use cache for it's sequence\n  -- all requests to the domfilter will add new filters to the\n  -- one sequence, which will be executed on one DOM object.\n  -- it is possible to request a different sequence using\n  -- unique name parameter\n  local name = name or \"domfilter\"\n  local settings = mkutils.get_filter_settings(name) or {}\n  local sequence = settings.sequence or {}\n  local local_sequence = filter_lib.load_filters(filters, load_filter)\n  for _, filter in ipairs(local_sequence) do\n    table.insert(sequence, filter)\n  end\n  settings.sequence = sequence\n  mkutils.filter_settings (name) (settings)\n\n\treturn function(filename, parameters)\n    -- load processed files for the current filter name\n    local processed_files = processed[name] or {}\n    -- don't process the file again\n    if processed_files[filename] then\n      return nil\n    end\n    local input = filter_lib.load_input_file(filename)\n    if not input  then return nil, \"Cannot load the input file\" end\n    -- in pure XML, we need to ignore void_elements provided by LuaXML, because these can exist only in HTML\n    local no_void_elements = {docbook = {}, jats = {}, odt = {}, tei = {} }\n    local void_elements = no_void_elements[parameters.output_format]\n    -- we need to use pcall, because XML error would break the whole build process\n    -- domobject will be error object if DOM parsing failed\n    local status, domobject = pcall(function()\n      return dom.parse(input, void_elements)\n    end)\n    if not status then\n      log:warning(\"XML DOM parsing of \" .. filename .. \" failed:\")\n      log:warning(domobject)\n      log:debug(\"Error context:\\n\" .. (get_html_snippet(input, domobject) or \"\"))\n      log:debug(\"Trying HTML DOM parsing\")\n      status, domobject = pcall(function()\n        return dom.html_parse(input)\n      end)\n      if not status then\n        log:warning(\"HTML DOM parsing failed as well\")\n        return nil, \"DOM parsing failed\"\n      else \n        log:warning(\"HTML DOM parsing OK, DOM filters will be executed\")\n      end\n    end\n\t\tfor _,f in pairs(sequence) do\n\t\t\tdomobject = f(domobject,parameters)\n\t\tend\n    local output = domobject:serialize()\n    if output then\n      filter_lib.save_input_file(filename, output)\n    else\n      log:warning(\"DOM filter failed on \".. filename)\n    end\n    -- mark the filename as processed\n    processed_files[filename] = true\n    processed[name] = processed_files\n\tend\nend\nreturn filter\n"
  },
  {
    "path": "filters/make4ht-entities-to-unicode.lua",
    "content": "-- convert Unicode characters encoded as XML entities back to Unicode\n\nlocal utfchar = unicode.utf8.char\n-- list of disabled characters\nlocal disabled = { [\"&\"] = \"&amp;\", [\"<\"] = \"&lt;\", [\">\"] = \"&gt;\"}\nreturn  function(content)\n   local content = content:gsub(\"%&%#x([A-Fa-f0-9]+);\", function(entity)\n    -- convert hexadecimal entity to Unicode\n    local char_number = tonumber(entity, 16)\n    -- fix for non-breaking spaces, LO cannot open file when they are present as Unicode\n    if char_number == 160 then return  \"&#xA0;\" end\n    local newchar =  utfchar(char_number)\n    -- we don't want to break XML validity with forbidden characters\n    return disabled[newchar] or newchar\n  end)\n  return content\nend\n"
  },
  {
    "path": "filters/make4ht-entities.lua",
    "content": "-- Fix bad entities\n-- Sometimes, tex4ht produce named xml entities, which are prohobited in epub\n-- &nbsp;, for example\nfunction filter(s)\n\tlocal replaces = {\n\tnbsp = \"#160\"\n\t}\n\treturn s:gsub(\"&(%w+);\",function(x) \n\t\tlocal m = replaces[x] or x\n\t\treturn \"&\"..m..\";\"\n\tend)\nend\n\nreturn filter\n"
  },
  {
    "path": "filters/make4ht-filter.lua",
    "content": "local filter_lib = require \"make4ht-filterlib\"\n\nlocal function load_filter(filtername)\n\treturn require(\"filters.make4ht-\"..filtername)\nend\n\nfunction filter(filters)\n  local sequence = filter_lib.load_filters(filters, load_filter)\n\treturn function(filename, parameters)\n\t\tif not filename then return false, \"filters: no filename\" end\n    local input = filter_lib.load_input_file(filename)\n    if not input  then return nil, \"Cannot load the input file\" end\n\t\tfor _,f in pairs(sequence) do\n\t\t\tinput = f(input,parameters)\n\t\tend\n    filter_lib.save_input_file(filename, input)\n\tend\nend\nreturn filter\n"
  },
  {
    "path": "filters/make4ht-fix-links.lua",
    "content": "-- replace colons in `id` or `href` attributes for local links with underscores\n--\n\nlocal function fix_href_colons(s)\n  return s:gsub('(href=\".-\")', function(a)\n    if a:match(\"[a-z]%://\") then return a end\n    return a:gsub(\":\",\"_\")\n  end)\nend\n\nlocal function fix_id_colons(s)\n  return s:gsub('(id=\".-\")', function(a)\n    return a:gsub(\":\", \"_\")\n  end)\nend\n\nreturn function(s)\n  return fix_id_colons(fix_href_colons(s))\nend\n"
  },
  {
    "path": "filters/make4ht-fixligatures.lua",
    "content": "-- fix ligatures\n-- replace ligatures produced by tex4ht with their components\n-- this prevents problems with some readers\nlocal gsub = unicode.utf8.gsub\nfunction filter(s)\n\tlocal replaces = {\n\t\t[\"ﬁ\"] = \"fi\",\n\t\t[\"ﬃ\"] = \"ffi\",\n\t\t[\"ﬂ\"] = \"fl\",\n\t\t[\"ﬄ\"] = \"ffl\",\n\t\t[\"ﬀ\"] = \"ff\"\n\t}\n\treturn gsub(s, \"([ﬁﬃﬂﬄﬀ])\",function (x) return replaces[x] or x end)\nend\n\nreturn filter\n"
  },
  {
    "path": "filters/make4ht-hruletohr.lua",
    "content": "-- hruletohr\n-- \\hrule primitive is impossible to redefine catching all possible arguments\n-- with tex4ht, it is converted as series of underscores \n-- it seems that these underscores are always part of previous paragraph\n-- this assumption may be wrong, needs more real world testing\n\nlocal hruletohr = function(s)\n\treturn s:gsub(\"___+(.-)</p>\",\"%1</p>\\n<hr class=\\\"hrule\\\" />\")\nend\n\nreturn hruletohr\n"
  },
  {
    "path": "filters/make4ht-mathjaxnode.lua",
    "content": "local mkutils = require \"mkutils\"\nlocal log = logging.new(\"mathjaxnode\")\n-- other possible value is page2svg\nlocal mathnodepath = \"mjpage\"\n-- options for MathJax command\nlocal options = \"--output CommonHTML\"\n-- math fonts position\n-- don't alter fonts if not set\nlocal fontdir = nil\n-- if we copy fonts \nlocal fontdest = nil\nlocal fontformat = \"woff\"\nlocal cssfilename =  \"mathjax-chtml.css\"\n\nlocal function compile(text)\n  local tmpfile = os.tmpname()\n  log:info(\"Compile using MathJax\")\n  local command =  mathnodepath .. \" \".. options .. \" > \" .. tmpfile\n  log:info(command)\n  local commandhandle = io.popen(command,\"w\") \n  commandhandle:write(text)\n  commandhandle:close()\n  log:info(\"Result written to: \".. tmpfile)\n  local f = io.open(tmpfile)\n  local content = f:read(\"*all\")\n  f:close()\n  os.remove(tmpfile)\n  return content\nend\n\n-- save the css code from the html page generated by MathJax\nlocal function extract_css(contents)\n  local css = \"\"\n  local filename = cssfilename\n  contents = contents:gsub('<style [^>]+>(.-)</style>', function(style)\n    -- replace only the style for mathjax\n    if style:match \"%.mjx%-math\" then\n      css = style\n      return '<link rel=\"stylesheet\" type=\"text/css\" href=\"'..filename ..'\" />'\n    end\n  end)\n  -- local x = assert(io.open(file, \"w\"))\n  -- x:write(contents)\n  -- x:close()\n  return filename, contents, css\nend\n\n-- Update the paths to fonts to use the local versions\nlocal function use_fonts(css)\n  local family_pattern = \"font%-family:%s*(.-);.-%/([^%/]+)%.\".. fontformat\n  local family_build = \"@font-face {font-family: %s; src: url('%s/%s.%s') format('%s')}\"\n  local fontdir = fontdir:gsub(\"/$\",\"\")\n  css = css:gsub(\"(@font%-face%s*{.-})\", function(face)\n    if not face:match(\"url%(\") then return face end\n    -- print(face)\n    local family, filename = face:match(family_pattern)\n    log:info(\"use font: \",family, filename)\n    local newfile = string.format(\"%s/%s.%s\", fontdir, filename, fontformat)\n    Make:add_file(newfile)\n    return family_build:format(family, fontdir, filename, fontformat, fontformat)\n    -- return face\n  end)\n  return css\nend\n\n\nlocal function save_css(filename, css)\n  local f = io.open(filename, \"w\")\n  f:write(css)\n  f:close()\nend\n\nreturn function(text, arguments)\n  -- if arguments.prg then mathnodepath = arguments.prg end\n  local extoptions = mkutils.get_filter_settings \"mathjaxnode\" or {}\n  local arguments = arguments or {}\n  mathnodepath = arguments.prg or extoptions.prg or  mathnodepath\n  options      = arguments.options or extoptions.options or options\n  fontdir      = arguments.fontdir or extoptions.fontdir or fontdir\n  -- the following ne is unused ATM\n  fontdest     = arguments.fontdest or extoptions.fontdest or fontdest\n  fontformat   = arguments.fontformat or extoptions.fontformat or fontformat\n  cssfilename  = arguments.cssfilename or extoptions.cssfilename or cssfilename\n  local newtext = compile(text)\n  local cssfile, newtext,  css = extract_css(newtext)\n  -- use local font files if fontdir is present\n  if fontdir then\n    css = use_fonts(css)\n  end\n  save_css(cssfile, css)\n  Make:add_file(cssfile)\n  -- print(css)\n  log:info(\"CSS file: \" .. cssfile)\n  return newtext\nend\n"
  },
  {
    "path": "filters/make4ht-mjcli.lua",
    "content": "local mkutils = require \"mkutils\"\nlocal log = logging.new(\"mjcli\")\n-- other possible value is page2svg\nlocal mathnodepath = \"mjcli\"\n-- options for MathJax command\nlocal options = \"\"\n-- math fonts position\n-- don't alter fonts if not set\nlocal fontdir = nil\n-- if we copy fonts \nlocal fontdest = nil\nlocal fontformat = \"woff\"\nlocal cssfilename =  \"mathjax-chtml.css\"\n\nlocal function compile(filename, options)\n  -- local tmpfile = os.tmpname()\n  log:info(\"Compile using MathJax\")\n  local command =  mathnodepath .. \" \".. options .. \" \" .. filename\n  log:info(command)\n  local commandhandle, msg = io.popen(command,\"r\") \n  if not commandhandle then return nil, msg end\n  local content = commandhandle:read(\"*all\")\n  commandhandle:close()\n  return content\nend\n\nlocal saved_styles = {}\nlocal used_styles = {}\n\nlocal function make_css(saved_styles)\n  -- this buffer contains lines of the new CSS file\n  local buffer = {}\n  -- process table with saved CSS rules and make CSS file again\n  for _, rule in ipairs(saved_styles) do\n    buffer[#buffer+1] = rule.selector .. \" {\"\n    -- save CSS properties\n    for _, line in ipairs(rule.content) do\n      buffer[#buffer+1] = line\n    end\n    buffer[#buffer+1] = \"}\"\n    buffer[#buffer+1] = \"\" -- add blank line\n  end\n  return table.concat(buffer, \"\\n\")\nend\n\n-- MathJax generated CSS contains reusable declarations but also\n-- declarations of fixes for elements in the current file\n-- the idea is to reuse the common declarations and to save each\n-- fix. \nlocal function parse_css(css, file_class)\n  local status = \"init\"\n  local current = {}\n  local current_selector\n  for line in css:gmatch(\"([^\\n]+)\") do\n    if status == \"init\" then\n      local selector, rest = line:match(\"%s*(.-)%s*{(.-)\")\n      if selector then \n        current_selector = selector\n        -- if the current selector contains class, we must prepend the current file class\n        -- as the joined CSS file could contain multiple rules with the same class otherwise\n        if current_selector:match(\"%.\") then current_selector = \".\" .. file_class .. \" \" .. current_selector end\n        status = \"record\"\n      end\n    elseif status == \"record\" then\n      -- find end of the CSS rule\n      if line:match(\"}%s*$\") then\n        status = \"init\"\n        if not used_styles[current_selector] then\n          table.insert(saved_styles, {selector = current_selector, content = current})\n        end\n        current = {}\n        used_styles[current_selector] = true\n      else\n        table.insert(current, line)\n      end\n    end\n\n  end\n  -- save combined CSS for all files\n  return make_css(saved_styles)\nend\n\nlocal function make_file_class(name)\n  -- clean the filename to make it safe as a class name\n  return name:gsub(\"[%s%p%s]\", \"_\")\nend\n\n-- set class attribute in the body element of the current file\n-- this is necessary for the updated CSS file\nlocal function set_body_class(content, file_class)\n  content = content:gsub(\"<body(.-)>\", function(body)\n    if body:match(\"class\") then\n      -- add new class if there already is one\n      body = body:gsub(\"(class.-[\\\"'])\", \"%1\" .. file_class .. \" \")\n    else\n      body = body .. ' class=\"' .. file_class .. '\"'\n    end\n    return \"<body\" ..body ..\">\"\n  end)\n  return content\nend\n\n\n-- save the css code from the html page generated by MathJax\nlocal function extract_css(contents, currentfilename)\n  local css\n  local filename = cssfilename\n  local file_class = make_file_class(currentfilename)\n  -- detect all <style> tags\n  contents = contents:gsub('<style [^>]+>(.-)</style>', function(style)\n    -- replace only the style for mathjax\n    if style:match \"mjx%-container\" then\n      css = parse_css(style, file_class)\n      return '<link rel=\"stylesheet\" type=\"text/css\" href=\"'..filename ..'\" />'\n    end\n  end)\n  contents = set_body_class(contents, file_class)\n  return filename, contents, css\nend\n\n-- Update the paths to fonts to use the local versions\nlocal function use_fonts(css)\n  local family_pattern = \"font%-family:%s*(.-);.-%/([^%/]+)%.\".. fontformat\n  local family_build = \"@font-face {font-family: %s; src: url('%s/%s.%s') format('%s')}\"\n  local fontdir = fontdir:gsub(\"/$\",\"\")\n  css = css:gsub(\"(@font%-face%s*{.-})\", function(face)\n    if not face:match(\"url%(\") then return face end\n    -- print(face)\n    local family, filename = face:match(family_pattern)\n    log:info(\"use font: \",family, filename)\n    local newfile = string.format(\"%s/%s.%s\", fontdir, filename, fontformat)\n    Make:add_file(newfile)\n    return family_build:format(family, fontdir, filename, fontformat, fontformat)\n    -- return face\n  end)\n  return css\nend\n\n\nlocal function save_css(filename, css)\n  local f = io.open(filename, \"w\")\n  f:write(css)\n  f:close()\nend\n\nreturn function(text, arguments)\n  -- if arguments.prg then mathnodepath = arguments.prg end\n  local extoptions = mkutils.get_filter_settings \"mjcli\" or {}\n  local arguments = arguments or {}\n  mathnodepath = arguments.prg or extoptions.prg or  mathnodepath\n  local options      = arguments.options or extoptions.options or options\n  fontdir      = arguments.fontdir or extoptions.fontdir or fontdir\n  -- the following ne is unused ATM\n  fontdest     = arguments.fontdest or extoptions.fontdest or fontdest\n  fontformat   = arguments.fontformat or extoptions.fontformat or fontformat\n  cssfilename  = arguments.cssfilename or extoptions.cssfilename or arguments.input .. \"-mathjax.css\"\n  local is_latex = arguments.latex or extoptions.latex or false\n  local filename = arguments.filename\n\n  -- modify options to use LaTeX syntax by MathJax\n  if is_latex then\n    options = options .. \" -l\"\n  end\n\n  -- compile current html file with mathjax\n  local newtext, msg = compile(filename, options)\n  if not newtext then \n    log:error(msg)\n    return text\n  end\n  -- save CSS to a standalone file\n  local cssfile, newtext,  css = extract_css(newtext, filename)\n  -- use local font files if fontdir is present\n  if fontdir then\n    css = use_fonts(css)\n  end\n  if css then\n    save_css(cssfile, css)\n    Make:add_file(cssfile)\n    -- print(css)\n    log:info(\"CSS file: \" .. cssfile)\n  end\n  return newtext\nend\n"
  },
  {
    "path": "filters/make4ht-odttemplate.lua",
    "content": "local mkutils = require \"mkutils\"\nlocal zip = require \"zip\"\nlocal domobject = require \"luaxml-domobject\"\n\n\nlocal function get_template_filename(settings)\n  -- either get the template odt filename from tex4ht.sty options (make4ht filename.tex \"odttemplate=test.odt\")\n  local tex4ht_settings = settings.tex4ht_sty_par\n  local templatefile = tex4ht_settings:match(\"odttemplate=([^%,]+)\")\n  if templatefile then return templatefile end\n  -- read the template odt filename from settings\n  local filtersettings = get_filter_settings \"odttemplate\"\n  return settings.template or filtersettings.template\nend\n\nlocal function join_styles(old, new)\n  local old_dom = domobject.parse(old)\n  local new_dom = domobject.parse(new)\n\n  local template_styles = {}\n  local template_obj  -- <office:styles> element, we will add new styles from the generated ODT here\n\n  -- detect style names in the template file and save them in a table for easy accesss\n  for _, style in ipairs(new_dom:query_selector(\"office|styles *\")) do\n    template_obj = template_obj or style:get_parent()\n    local name = style:get_attribute(\"style:name\") -- get the <office:styles> element\n    if name then\n      template_styles[name] = true\n    end\n  end\n\n  -- process the generated styles and add ones not used in the template\n  for _, style in ipairs(old_dom:query_selector(\"office|styles *\")) do\n    local name = style:get_attribute(\"style:name\")\n    if name and not template_styles[name] then\n      template_obj:add_child_node(style)\n    end\n  end\n\n  -- return template with additional styles from the generated file\n  return new_dom:serialize()\nend\n\nreturn function(content, settings)\n  -- use settings added from the Make:match, or default settings saved in Make object\n  local templatefile = get_template_filename(settings)\n  -- don't do anything if the template file doesn't exist\n  if not templatefile or not mkutils.file_exists(templatefile) then return content end\n  local odtfile = zip.open(templatefile)\n  if odtfile then\n    local stylesfile = odtfile:open(\"styles.xml\")\n    -- just break if the styles cannot be found\n    if not stylesfile then return content end\n    local styles = stylesfile:read(\"*all\")\n    local newstyle = join_styles(content, styles)\n    return newstyle\n  end\n  -- just return content in the case of problems\n  return content\nend\n"
  },
  {
    "path": "filters/make4ht-staticsite.lua",
    "content": "local domobj = require \"luaxml-domobject\"\nlocal log = logging.new(\"staticsite\")\n-- save the header settings in YAML format\nlocal function make_yaml(tbl, level)\n  local t = {}\n  local level = level or 0\n  local indent = string.rep(\"  \", level)\n  -- indentation for multilen strings\n  local str_indent = string.rep(\"  \", level + 1)\n  local sorted = {}\n  for k, _ in pairs(tbl) do\n    sorted[#sorted+1] = k\n  end\n  table.sort(sorted)\n  for _,k in ipairs(sorted) do\n    local v = tbl[k]\n    if type(v)==\"string\" then\n      -- detect multiline strings\n      if v:match(\"\\n\") then\n        table.insert(t, string.format(indent .. \"%s: |\", k))\n        table.insert(t, str_indent .. (v:gsub(\"\\n\", \"\\n\".. str_indent)))\n      else\n        v = v:gsub(\"'\", \"''\")\n        table.insert(t, string.format(indent .. \"%s: '%s'\", k,v))\n      end\n    elseif type(v) == \"table\" then\n      table.insert(t,string.format(indent .. \"%s:\", k))\n      -- we need to differently process array and hash table\n      -- we don't support mixing types\n      if #v > 0 then\n        for x,y in ipairs(v) do\n          if type(y) == \"string\" then\n            -- each string can be printed on it's own line\n            table.insert(t, indent .. string.format(\"- '%s'\", y))\n          else\n            -- subtables need to be indented\n            -- table.insert(t, indent .. \"-\")\n            local subtable = make_yaml(y, level + 1)\n            -- we must insert dash at a correct place\n            local insert_dash = subtable:gsub(\"^(%s*)%s%s\", \"%1- \")\n            table.insert(t, insert_dash)\n          end\n        end\n      else\n        -- print indented table\n        table.insert(t, make_yaml(v,level + 1))\n      end\n    else\n      -- convert numbers and other values to string\n      table.insert(t, string.format(indent .. \"%s: %s\", k,tostring(v)))\n    end\n    \n  end\n  return table.concat(t,  \"\\n\")\nend\n\nlocal function update_properties(properties, dom)\n  -- enable properties update from the config or build file\n  local settings = get_filter_settings \"staticsite\" or {}\n  local header = settings.header or {}\n  -- set non-function properties first\n  for field, rule in pairs(header) do\n    if type(rule) ~=\"function\" then\n      properties[field] = rule\n    end\n  end\n  -- then execute functions. it ensures that all propeties set in header are available\n  for field, rule in pairs(header) do\n    -- it is possible to pass function as a rule, it will be executed with properties as a parameter\n    if type(rule) == \"function\" then\n      properties[field] = rule(properties, dom)\n    end\n  end\n  return properties\nend\n\nlocal function get_header(tbl)\n  local yaml = make_yaml(tbl)\n  return \"---\\n\".. yaml.. \"\\n---\\n\"\nend\n\nreturn function(s,par)\n  local dom = domobj.parse(s)\n  local properties = {}\n  local head = dom:query_selector(\"head\")[1]\n  properties.title = head:query_selector(\"title\")[1]:get_text()\n  local styles = {}\n  for _, link in ipairs(head:query_selector(\"link\")) do\n    local typ = link:get_attribute(\"type\")\n    if typ == \"text/css\" then \n      table.insert(styles, link:get_attribute(\"href\"))\n    end\n  end\n  properties.styles = styles\n  local metas = {}\n  for _, meta in ipairs(head:query_selector(\"meta\")) do\n    log:debug(\"parsed meta: \" .. meta:serialize())\n    table.insert(metas, {charset= meta:get_attribute(\"charset\"), content = meta:get_attribute(\"content\"), property = meta:get_attribute(\"property\"), name = meta:get_attribute(\"name\")})\n  end\n  properties.meta = metas\n  properties = update_properties(properties, dom)\n\n\n  local body = dom:query_selector(\"body\")[1]\n  log:debug(get_header(properties))\n  -- return s\n  return get_header(properties) .. body:serialize():gsub(\"<body.->\", \"\"):gsub(\"</body>\", \"\")\nend\n"
  },
  {
    "path": "filters/make4ht-svg-height.lua",
    "content": "\nlocal log = logging.new(\"svg-height\")\n-- Make:image(\"svg$\", \"dvisvgm -n -a -p ${page} -b preview -c 1.4,1.4 -s ${source} > ${output}\")\n\n\nlocal max = function(a,b)\n  return a > b and a or b\nend\n\nlocal function get_height(svg)\n  local height = svg:match(\"height='([0-9%.]+)pt'\")\n  return tonumber(height)\nend\n\nlocal function get_max_height(path,max_number)\n  local coordinates = {}\n  for number in path:gmatch(\"(%-?[0-9%.]+)\") do\n    table.insert(coordinates, tonumber(number))\n  end\n  for i = 2, #coordinates, 2 do\n    max_number = max(max_number, coordinates[i])\n  end\n  return max_number\nend\n\nlocal function update_height(svg, height)\n  return svg:gsub(\"height='.-pt'\", \"height='\"..height ..\"pt'\")\nend\n\n-- we need to fix the svg height\nreturn function(svg)\n  local max_height = 0\n  local height = get_height(svg)\n  for path in svg:gmatch(\"path d='([^']+)'\") do\n    -- find highest height in all paths in the svg file\n    max_height = get_max_height(path, max_height)\n  end\n  -- update the height only if the max_height is larger than height set in the SVG file\n  log:debug(\"max height and height\", max_height, height)\n  if max_height > height then\n    svg = update_height(svg, max_height)\n  end\n  return svg\nend\n\n"
  },
  {
    "path": "formats/make4ht-docbook.lua",
    "content": "local M = {}\nlocal mkutils = require \"mkutils\"\nlocal lfs     = require \"lfs\"\nlocal os      = require \"os\"\nlocal kpse    = require \"kpse\"\nlocal filter  = require \"make4ht-filter\"\nlocal domfilter  = require \"make4ht-domfilter\"\nlocal xtpipeslib = require \"make4ht-xtpipes\"\nlocal log = logging.new \"docbook\"\n\nfunction M.prepare_parameters(settings, extensions)\n  settings.tex4ht_sty_par = settings.tex4ht_sty_par ..\",docbook\"\n  settings = mkutils.extensions_prepare_parameters(extensions, settings)\n  return settings\nend\n\nlocal move_matches = xtpipeslib.move_matches\n\n-- call xtpipes from Lua\nlocal function call_xtpipes(make)\n  -- we must find root of the TeX distribution\n  local selfautoparent = xtpipeslib.get_selfautoparent()\n\n  if selfautoparent then\n    local matchfunction = xtpipeslib.get_xtpipes(selfautoparent)\n    make:match(\"xml$\", matchfunction)\n    move_matches(make)\n  else\n    log:warning \"Cannot locate xtpipes. Try to set TEXMFROOT variable to a root directory of your TeX distribution\"\n  end\nend\n\nfunction M.modify_build(make)\n  -- use xtpipes to fix some common docbook issues\n  call_xtpipes(make)\n  return make\nend\n\nreturn M\n"
  },
  {
    "path": "formats/make4ht-html5.lua",
    "content": "local M = {}\n\nlocal mkutils = require \"mkutils\"\n\nfunction M.prepare_extensions(extensions)\n  return mkutils.add_extensions(\"+common_domfilters\", extensions)\nend\n\nfunction M.prepare_parameters(parameters,extensions)\n  parameters.tex4ht_sty_par  = parameters.tex4ht_sty_par .. \",html5\"\n  parameters = mkutils.extensions_prepare_parameters(extensions,parameters)\n  return parameters\nend\n\n\nreturn M\n"
  },
  {
    "path": "formats/make4ht-jats.lua",
    "content": "local M = {}\nlocal xtpipeslib = require \"make4ht-xtpipes\"\nlocal domfilter = require \"make4ht-domfilter\"\n\n\n-- some elements need to be moved from the document flow to the document meta\nlocal article_meta \nlocal elements_to_move_to_meta = {}\nlocal function move_to_meta(el)\n  -- we don't move elements immediatelly, because it would prevent them from further \n  -- processing in the filter. so we save them in an array, and move them once \n  -- the full DOM was processed\n  table.insert(elements_to_move_to_meta, el)\nend\n\nlocal elements_to_move_to_title = {}\nlocal function move_to_title_group(el)\n  -- there can be only one title and subtitle\n  local name = el:get_element_name()\n  if not elements_to_move_to_title[name] then\n    elements_to_move_to_title[name] = el\n  end\nend\n\nlocal elements_to_move_to_contribs = {}\nlocal function move_to_contribs(el)\n  table.insert(elements_to_move_to_contribs, el)\nend\n\n\n\nlocal function process_moves()\n  if article_meta then\n    if elements_to_move_to_title[\"article-title\"] \n      and #article_meta:query_selector(\"title-group\") == 0 then -- don't move anything if user added title-group from a config file\n      local title_group = article_meta:create_element(\"title-group\")\n      for _, name in ipairs{ \"article-title\", \"subtitle\" } do\n        local v = elements_to_move_to_title[name] \n        if v then\n          title_group:add_child_node(v:copy_node())\n          v:remove_node()\n        end\n      end\n      article_meta:add_child_node(title_group, 1)\n    end\n    if #elements_to_move_to_contribs > 0 then\n      local contrib_group = article_meta:create_element(\"contrib-group\")\n      for _, el in ipairs(elements_to_move_to_contribs) do\n        contrib_group:add_child_node(el:copy_node())\n        el:remove_node()\n      end\n      article_meta:add_child_node(contrib_group)\n    end\n    for _, el in ipairs(elements_to_move_to_meta) do\n      -- move elemnt's copy, and remove the original\n      article_meta:add_child_node(el:copy_node())\n      el:remove_node()\n    end\n  end\nend\n\nlocal function has_no_text(el)\n  -- detect if element contains only whitespace\n  if el:get_text():match(\"^%s*$\") then\n    --- if it contains any elements, it has text\n    for _, child in ipairs(el:get_children()) do\n      if child:is_element() then return false end\n    end\n    return true\n  end\n  return false\nend\n\nlocal function is_xref_id(el)\n  return el:get_element_name() == \"xref\" and el:get_attribute(\"id\") and el:get_attribute(\"rid\") == nil and has_no_text(el)\nend\n-- set id to parent element for <xref> that contain only id\nlocal function xref_to_id(el)\n  local parent = el:get_parent()\n  -- set id only if it doesn't exist yet\n  if parent:get_attribute(\"id\") == nil then\n    print(parent:serialize())\n    parent:set_attribute(\"id\", el:get_attribute(\"id\"))\n    el:remove_node()\n  end\nend\n\nlocal function make_text(el)\n  local text = el:get_text():gsub(\"^%s*\", \"\"):gsub(\"%s*$\", \"\")\n  local text_el = el:create_text_node(text)\n  el._children = {text_el}\nend\n\nlocal function is_empty_par(el)\n  return el:get_element_name() == \"p\" and has_no_text(el)\nend\n\nlocal function handle_links(el, params)\n  -- we must distinguish between internal links in the document, and external links\n  -- to websites etc. these needs to be changed to the <ext-link> element.\n  local link = el:get_attribute(\"rid\") \n  if link then\n    -- try to remove \\jobname.xml from the beginning of the link\n    -- if the rest starts with #, then it is an internal link\n    local local_link = link:gsub(\"^\" .. params.input .. \".xml\", \"\")\n    if local_link:match(\"^%#\") then\n      -- the rid attribute should not start with #, it must be the exact ID used in the linked element\n      local_link = local_link:gsub(\"^%#\", \"\")\n      el:set_attribute(\"rid\", local_link)\n    else\n      -- change element to ext-link for extenal links\n      el._name = \"ext-link\"\n      el:set_attribute(\"rid\", nil)\n      el:set_attribute(\"xlink:href\", link)\n    end\n  end\nend\n\nlocal function handle_maketitle(el)\n  -- <maketitle> is special element produced by TeX4ht from LaTeX's \\maketitle\n  -- we need to pick interesting info from there, and move it to the header\n  local function is_empty(selector)\n    return #article_meta:query_selector(selector) == 0\n  end\n  -- move <aff> to <contrib>\n  local affiliations = {}\n  for _, aff in ipairs(el:query_selector(\"aff\")) do\n    local id = aff:get_attribute(\"id\") \n    if id then \n      for _,mark in ipairs(aff:query_selector(\"affmark\")) do mark:remove_node() end\n      affiliations[id] = aff:copy_node() \n    end\n  end\n  if is_empty(\"contrib\") then\n    for _, contrib in ipairs(el:query_selector(\"contrib\")) do\n      for _, affref in ipairs(contrib:query_selector(\"affref\")) do\n        local id = affref:get_attribute(\"rid\") or \"\"\n        -- we no longer need this node\n        affref:remove_node()\n        local linked_affiliation = affiliations[id]\n        if linked_affiliation then\n          contrib:add_child_node(linked_affiliation)\n        end\n      end\n      for _, string_name in ipairs(contrib:query_selector(\"string-name\")) do\n        make_text(string_name)\n      end\n      move_to_contribs(contrib:copy_node())\n      -- we need to remove it from here, even though we remove <maketitle> later\n      -- we got doubgle contributors without that\n      contrib:remove_node()\n    end\n  end\n  if is_empty(\"pub-date\") then\n    for _, date in ipairs(el:query_selector(\"date\")) do\n      date._name = \"pub-date\"\n      for _, s in ipairs(date:query_selector(\"string-date\")) do\n        make_text(s)\n      end\n      move_to_meta(date:copy_node())\n    end\n  end\n  el:remove_node()\nend\n\n\n\n\n\nfunction M.prepare_parameters(settings, extensions)\n  settings.tex4ht_sty_par = settings.tex4ht_sty_par ..\",jats\"\n  settings = mkutils.extensions_prepare_parameters(extensions, settings)\n  return settings\nend\n\nfunction M.prepare_extensions(extensions)\n  return extensions\nend\n\nfunction M.modify_build(make)\n  filter_settings(\"joincharacters\", {charclasses = {italic=true, bold=true}})\n\n  local process =  domfilter {\n    function(dom, params)\n      dom:traverse_elements(function(el)\n        -- some elements need special treatment\n        local el_name = el:get_element_name()\n        if is_xref_id(el) then\n          xref_to_id(el)\n        elseif el_name == \"article-meta\" then\n          -- save article-meta element for further processig\n          article_meta = el\n        elseif el_name == \"article-title\" then\n          move_to_title_group(el)\n        elseif el_name == \"subtitle\" then\n          move_to_title_group(el)\n        elseif el_name == \"abstract\" then\n          move_to_meta(el)\n        elseif el_name == \"string-name\" then\n          make_text(el)\n        elseif el_name == \"contrib\" then\n          move_to_contribs(el)\n        elseif is_empty_par(el) then\n          -- remove empty paragraphs\n          el:remove_node()\n        elseif el_name == \"xref\" then\n          handle_links(el, params)\n        elseif el_name == \"maketitle\" then\n          handle_maketitle(el)\n        elseif el_name == \"div\" and el:get_attribute(\"class\") == \"maketitle\" then\n          el:remove_node()\n        end\n\n      end)\n      -- move elements that are marked for move\n      process_moves()\n      return dom\n    end, \"joincharacters\",\"mathmlfixes\", \"tablerows\",\"booktabs\"\n  }\n  local charclasses = {[\"mml:mi\"] = true, [\"mml:mn\"] = true , italic = true, bold=true, roman = true, [\"mml:mtext\"] = true, mi=true, mn=true}\n  make:match(\"xml$\", process, {charclasses = charclasses})\n  return make\nend\n\nreturn M\n"
  },
  {
    "path": "formats/make4ht-odt.lua",
    "content": "local M = {}\nlocal mkutils = require \"mkutils\"\nlocal lfs     = require \"lfs\"\nlocal os      = require \"os\"\nlocal kpse    = require \"kpse\"\nlocal filter  = require \"make4ht-filter\"\nlocal domfilter  = require \"make4ht-domfilter\"\nlocal domobject  = require \"luaxml-domobject\"\nlocal xtpipeslib = require \"make4ht-xtpipes\"\nlocal log = logging.new \"odt\"\n\n\nfunction M.prepare_parameters(settings, extensions)\n  settings.tex4ht_sty_par = settings.tex4ht_sty_par ..\",ooffice\"\n  settings.tex4ht_par = settings.tex4ht_par .. \" ooffice/! -cmozhtf\"\n  -- settings.t4ht_par = settings.t4ht_par .. \" -cooxtpipes -coo \"\n  -- settings.t4ht_par = settings.t4ht_par .. \" -cooxtpipes \"\n  settings = mkutils.extensions_prepare_parameters(extensions, settings)\n  return settings\nend\n\n-- object for working with the ODT file\nlocal Odtfile = {}\nOdtfile.__index = Odtfile\n\nOdtfile.new = function(archivename)\n  local self = setmetatable({}, Odtfile)\n  -- create a temporary file\n  local tmpname = os.tmpname()\n  -- remove a temporary file, we are interested only in the unique file name\n  os.remove(tmpname)\n  -- get the unique dir name\n  tmpname = tmpname:match(\"([a-zA-Z0-9_%-%.]+)$\")\n  local status, msg = lfs.mkdir(tmpname)\n  if not status then return nil, msg end\n  -- make picture dir\n  lfs.mkdir(tmpname .. \"/Pictures\")\n  self.archivelocation = tmpname\n  self.name = archivename\n  return self\nend\n\nfunction Odtfile:copy(src, dest)\n  mkutils.cp(src, self.archivelocation .. \"/\" .. dest)\nend\n\nfunction Odtfile:move(src, dest)\n  mkutils.mv(src, self.archivelocation .. \"/\" .. dest)\nend\n\nfunction Odtfile:create_dir(dir)\n  local currentdir = lfs.currentdir()\n  lfs.chdir(self.archivelocation)\n  lfs.mkdir(dir)\n  lfs.chdir(currentdir)\nend\n\nfunction Odtfile:make_mimetype()\n  self.mimetypename = \"mimetype\"\n  local m, msg = io.open(self.mimetypename, \"w\")\n  if not m then\n    log:error(msg)\n    return nil, msg\n  end\n  m:write(\"application/vnd.oasis.opendocument.text\")\n  m:close()\nend\n\nfunction Odtfile:remove_mimetype()\n  os.remove(self.mimetypename)\nend\n\n\nfunction Odtfile:pack()\n  local currentdir = lfs.currentdir()\n  local zip_command = mkutils.find_zip()\n  lfs.chdir(self.archivelocation)\n  -- make temporary mime type file\n  self:make_mimetype()\n  mkutils.execute(zip_command .. ' -q0X \"' .. self.name .. '\" ' .. self.mimetypename)\n  -- remove it, so the next command doesn't overwrite it\n  self:remove_mimetype()\n  mkutils.execute(zip_command ..' -r \"' .. self.name .. '\" *')\n  lfs.chdir(currentdir)\n  mkutils.cp(self.archivelocation .. \"/\" .. self.name, mkutils.file_in_builddir(self.name, Make.params))\n  mkutils.delete_dir(self.archivelocation)\nend\n\n--- *************************\n--  *** fix picture sizes ***\n--  *************************\n--\nlocal function add_points(dimen)\n  if type(dimen) ~= \"string\" then return dimen end\n  -- convert SVG dimensions to points if only number is provided\n  if dimen:match(\"[0-9]$\") then return dimen .. \"pt\" end\n  return dimen\nend\n\nlocal function get_svg_dimensions(filename)\n  local width, height\n  if mkutils.file_exists(filename) then\n    for line in io.lines(filename) do\n      width = line:match(\"width%s*=%s*[\\\"'](.-)[\\\"']\") or width\n      height = line:match(\"height%s*=%s*[\\\"'](.-)[\\\"']\") or height\n      -- stop parsing once we get both width and height\n      if width and height then break end\n    end\n  end\n  width = add_points(width)\n  height = add_points(height)\n  return width, height\nend\n\nlocal function get_xbb_dimensions(filename)\n  local f = io.popen(\"ebb -x -O \" .. filename)\n  if f then\n    local content = f:read(\"*all\")\n    local width, height = content:match(\"%%BoundingBox: %d+ %d+ (%d+) (%d+)\")\n    return add_points(width), add_points(height)\n  end\n  return nil\nend\n--\nlocal function fix_picture_sizes(tmpdir)\n  local filename = tmpdir .. \"/content.xml\"\n  local f = io.open(filename, \"r\")\n  if not f then\n    log:warning(\"Cannot open \", filename, \"for picture size fixes\")\n    return nil\n  end\n  local content = f:read(\"*all\") or \"\"\n  f:close()\n  local status, dom= pcall(function()\n    return domobject.parse(content)\n  end)\n  if not status then\n    log:warning(\"Cannot parse DOM, the resulting ODT file will be most likely corrupted\")\n    return nil\n  end\n  for _, pic in ipairs(dom:query_selector(\"draw|image\")) do\n    local imagename = pic:get_attribute(\"xlink:href\")\n    -- update SVG images dimensions\n    log:debug(\"image\", imagename)\n    local parent = pic:get_parent()\n    local width =  parent:get_attribute(\"svg:width\")\n    local height = parent:get_attribute(\"svg:height\")\n    -- if width == \"0.0pt\" then width = nil end\n    -- if height == \"0.0pt\" then height = nil end\n    if not width or not height then\n      local imgfilename = tmpdir .. \"/\" .. imagename\n      if imagename:match(\"svg$\") then\n        width, height = get_svg_dimensions(imgfilename) --  or width, height\n      elseif imagename:match(\"png$\") or imagename:match(\"jpe?g$\") then\n        width, height = get_xbb_dimensions(imgfilename)\n      end\n    end\n    log:debug(\"new dimensions\", width, height)\n    parent:set_attribute(\"svg:width\", width)\n    parent:set_attribute(\"svg:height\", height)\n    -- if \n  end\n  -- save the modified DOM again\n  log:debug(\"Fixed picture sizes\")\n  local domcontent = dom:serialize()\n  local f, msg = io.open(filename, \"w\")\n  if not f then\n    log:error(msg)\n    return nil, msg\n  end\n  f:write(domcontent)\n  f:close()\nend\n\n-- fix font records in the lg file that don't correct Font_Size record\nlocal lg_fonts_processed=false\nlocal patched_lg_fonts = {}\nlocal function fix_lgfile_fonts(ignored_name, params)\n  -- this function is called from file match. we must use the name of the .lg file\n  local filename = mkutils.file_in_builddir(params.input .. \".lg\", params)\n  if not lg_fonts_processed then\n    local lines = {}\n    -- default font_size\n    local font_size = \"10\"\n    if mkutils.file_exists(filename) then\n      -- \n      for line in io.lines(filename) do\n        -- default font_size can be set in the .lg file\n        if line:match(\"Font_Size\") then\n          font_size = line:match(\"Font_Size:%s*(%d+)\")\n        elseif line:match(\"Font%(\") then\n          -- match Font record\n          local name, size, size2, size3 = line:match('Font%(\"([^\"]+)\",\"([%d]*)\",\"([%d]+)\",\"([%d]+)\"')\n          -- find if the first size is not set, and add the default font_size then\n          if size == \"\" then\n            line = string.format('Font(\"%s\",\"%s\",\"%s\",\"%s\")', name, font_size, size2, size3)\n            -- we must also save the font name and size for later post-processing, because \n            -- we will need to fix styles in content.xml too\n            patched_lg_fonts[name .. \"-\" .. font_size] = true\n          end\n        end\n        lines[#lines+1] = line\n      end\n      -- save changed lines to the lg file\n      local f = io.open(filename, \"w\")\n      for _,line in ipairs(lines) do\n        f:write(line .. \"\\n\")\n      end\n      f:close()\n    end\n    filter_settings \"odtfonts\" {patched_lg_fonts = patched_lg_fonts}\n  end\n  lg_fonts_processed=true\n  return true\nend\n\nlocal move_matches = xtpipeslib.move_matches\n\nlocal function insert_lgfile_fonts(make)\n  local params = make.params\n  local first_file = mkutils.file_in_builddir(params.input .. \".4oo\", params)\n  -- find the last file and escape it so it can be used \n  -- in filename match\n  make:match(first_file, fix_lgfile_fonts)\n  move_matches(make)\nend\n\n-- escape string to be used in the gsub search\nlocal function escape_file(filename)\n  local quotepattern = '(['..(\"%^$().[]*+-?\"):gsub(\"(.)\", \"%%%1\")..'])'\n  return filename:gsub(quotepattern, \"%%%1\")\nend\n\n\n-- call xtpipes from Lua\nlocal function call_xtpipes(make)\n  -- we must find root of the TeX distribution\n  local selfautoparent = xtpipeslib.get_selfautoparent()\n\n  if selfautoparent then\n    local matchfunction = xtpipeslib.get_xtpipes(selfautoparent)\n    make:match(\"4oo\", matchfunction)\n    make:match(\"4om\", matchfunction)\n    -- move last match to a first place\n    -- we need to move last two matches, for 4oo and 4om files\n    move_matches(make)\n    move_matches(make)\n    -- fix font records in the lg file\n    insert_lgfile_fonts(make)\n  else\n    log:warning \"Cannot locate xtpipes. Try to set TEXMFROOT variable to a root directory of your TeX distribution\"\n  end\nend\n\n-- sort output files according to their extensions\nlocal function prepare_output_files(lgfiles)\n  local groups = {}\n  for _, name in ipairs(lgfiles) do\n    local basename, extension = name:match(\"(.-)%.([^%.]+)$\")\n    local group = groups[extension] or {}\n    table.insert(group, basename)\n    groups[extension] = group\n    log:debug(\"prepare output file\", basename, extension)\n  end\n  return groups\nend\n\n-- execute function on all files in the group\n-- function fn takes current filename and table with various attributes\nlocal function exec_group(groups, name, fn)\n  for _, basename in ipairs(groups[name] or {}) do\n    fn{basename = basename, extension=name, filename = basename .. \".\" .. name}\n  end\nend\n\n-- remove <?xtpipes XML instructions, because they cause issues in some ODT processing\n-- applications\nlocal function remove_xtpipes(text)\n  -- remove <?x\n  return text:gsub(\"%<%?xtpipes.-%?%>\", \"\")\nend\n\nfunction M.modify_build(make)\n  local executed = false\n  -- execute xtpipes from the build file, instead of t4ht. this fixes issues with wrong paths\n  -- expanded in tex4ht.env in Miktex or Debian\n  call_xtpipes(make)\n  -- fix the image dimensions wrongly set by xtpipes\n  local domfilters = domfilter({\"t4htlinks\", \"odtpartable\"}, \"odtfilters\")\n  make:match(\"4oo$\", domfilters)\n  -- execute it before xtpipes, because we don't want xtpipes to mess with t4htlink elements\n  move_matches(make)\n  -- fixes for mathml\n  local mathmldomfilters = domfilter({\"joincharacters\",\"mathmlfixes\"}, \"mathmlfilters\")\n  make:match(\"4om$\", mathmldomfilters)\n  -- DOM filters that should be executed after xtpipes\n  local latedom = domfilter({\"odtfonts\"}, \"lateodtfilters\")\n  make:match(\"4oo$\", latedom)\n  -- convert XML entities for Unicode characters produced by Xtpipes to characters\n  local fixentities = filter {\"entities-to-unicode\", remove_xtpipes}\n  make:match(\"4oo\", fixentities)\n  make:match(\"4om\", fixentities)\n  -- we must handle outdir. make4ht copies the ODT file before it was packed, so\n  -- we will copy it again after packing later in this format file\n  local outdir = make.params[\"outdir\"]\n\n  -- build the ODT file. This match must be executed as a last one\n  -- this will be executed as a first match, just to find the last filename \n  -- in the lgfile\n  make:match(\".*\", function()\n    -- execute it only once\n    if not executed then\n      -- this is list of processed files\n      local lgfiles = make.lgfile.files\n      for k,v in ipairs(lgfiles) do\n        if v:match(\"odt$\") then table.remove(lgfiles, k) end\n      end\n      -- find the last file and escape it so it can be used \n      -- in filename match\n      local lastfile = escape_file(lgfiles[#lgfiles]) ..\"$\"\n      -- make match for the last file\n      -- odt packing will be done here\n      make:match(lastfile, function(filename, par)\n        local groups = prepare_output_files(make.lgfile.files)\n        -- we must remove any path from the basename\n        -- local basename = groups.odt[1]:match(\"([^/]+)$\")\n        local basename = make.params.input\n        local odtname = basename .. \".odt\"\n        local odt,msg = Odtfile.new(odtname)\n        if not odt then\n          log:error(\"Cannot create ODT file: \" .. msg)\n        end\n        -- helper function for simple file moving\n        local function move_file(group, dest)\n          exec_group(groups, group, function(par)\n            odt:move(\"${filename}\" % par, dest)\n          end)\n        end\n\n        -- the document text\n        exec_group(groups, \"4oo\", function(par)\n          odt:move(\"${filename}\" % par, \"content.xml\")\n          odt:create_dir(\"Pictures\")\n        end)\n\n        -- manifest\n        exec_group(groups, \"4of\", function(par)\n          odt:create_dir(\"META-INF\")\n          odt:move(\"${filename}\" % par, \"META-INF/manifest.xml\")\n        end)\n\n        -- math\n        exec_group(groups, \"4om\", function(par)\n          odt:create_dir(par.basename)\n          odt:move(\"${filename}\" % par, \"${basename}/content.xml\" % par)\n          -- copy the settings file to math subdir\n          local settings = groups[\"4os\"][1]\n          odt:copy(settings .. \".4os\", \"${basename}/settings.xml\" % par)\n        end)\n\n        -- these files are created only once, so it doesn't matter that they are\n        -- copied to one file\n        move_file(\"4os\", \"settings.xml\")\n        move_file(\"4ot\", \"meta.xml\")\n        move_file(\"4oy\", \"styles.xml\")\n\n        -- pictures\n        exec_group(groups, \"4og\", function(par)\n          -- add support for images in the TEXMF tree\n          if not mkutils.file_exists(par.basename) then\n            -- try to find the file in the directory with the original TeX file\n            local without_builddir = par.basename:gsub(\"^\" .. mkutils.escape_pattern(make.params.builddir or \"\") .. \"/\", \"\")\n            if mkutils.file_exists(without_builddir) then\n              par.basename = without_builddir\n            else\n              par.basename = kpse.find_file(par.basename, \"graphic/figure\") or kpse.find_file(without_builddir, \"graphic/figure\")\n              if not par.basename then return nil, \"Cannot find picture\" end\n            end\n          end\n          -- the Pictues dir is flat, without subdirs\n          odt:copy(\"${basename}\" % par, \"Pictures\")\n        end)\n\n        -- fix picture sizes in the content file\n        fix_picture_sizes(odt.archivelocation)\n\n        -- remove some spurious file\n        exec_group(groups, \"4od\", function(par)\n          os.remove(par.filename)\n        end)\n\n        odt:pack()\n        local build_filename = mkutils.file_in_builddir(odt.name, make.params)\n        if outdir and outdir ~= \"\" then\n          local outfilename = outdir .. \"/\" .. odt.name\n          log:info(\"Copying ODT file to the output dir: \" .. outfilename)\n          mkutils.copy(build_filename,outfilename)\n        elseif build_filename ~= odt.name then\n          mkutils.cp(build_filename, odt.name)\n        end\n      end)\n    end\n    executed = true\n  end)\n  return make\nend\nreturn M\n"
  },
  {
    "path": "formats/make4ht-tei.lua",
    "content": "local M = {}\nlocal xtpipeslib = require \"make4ht-xtpipes\"\n\nlocal domfilter = require \"make4ht-domfilter\"\n\nfunction M.prepare_parameters(settings, extensions)\n  settings.tex4ht_sty_par = settings.tex4ht_sty_par ..\",tei\"\n  settings = mkutils.extensions_prepare_parameters(extensions, settings)\n  return settings\nend\n\nfunction M.prepare_extensions(extensions)\n  return extensions\nend\n\nfunction M.modify_build(make)\n  local process = domfilter {\n    \"joincharacters\"\n  }\n\n  -- we use <hi> elements for characters styled using HTF fonts in TEI\n  -- use the `joincharacters` DOM filter to join them\n  filter_settings \"joincharacters\" {\n    charclasses = { hi=true, mn = true}\n  }\n\n  make:match(\"xml$\", process)\n  return make\nend\n\nreturn M\n"
  },
  {
    "path": "formats/make4ht-xhtml.lua",
    "content": "local M = {}\n\nlocal mkutils = require \"mkutils\"\n\nfunction M.prepare_extensions(extensions)\n  return mkutils.add_extensions(\"+common_domfilters\", extensions)\nend\n\nfunction M.prepare_parameters(parameters,extensions)\n  parameters = mkutils.extensions_prepare_parameters(extensions,parameters)\n  return parameters\nend\n\n\nreturn M\n"
  },
  {
    "path": "lapp-mk4.lua",
    "content": "-- lapp.lua\n-- Simple command-line parsing using human-readable specification\n-----------------------------\n--~ -- args.lua\n--~ local args = require ('lapp') [[\n--~ Testing parameter handling\n--~     -p               Plain flag (defaults to false)\n--~     -q,--quiet       Plain flag with GNU-style optional long name\n--~     -o  (string)     Required string option\n--~     -n  (number)     Required number option\n--~     -s (default 1.0) Option that takes a number, but will default\n--~     <start> (number) Required number argument\n--~     <input> (default stdin)  A parameter which is an input file\n--~     <output> (default stdout) One that is an output file\n--~ ]]\n--~ for k,v in pairs(args) do\n--~     print(k,v)\n--~ end\n-------------------------------\n--~ > args -pq -o help -n 2 2.3\n--~ input   file (781C1B78)\n--~ p       true\n--~ s       1\n--~ output  file (781C1B98)\n--~ quiet   true\n--~ start   2.3\n--~ o       help\n--~ n       2\n--------------------------------\n\nlapp = {}\n\nlocal append = table.insert\nlocal usage\nlocal open_files = {}\nlocal parms = {}\nlocal aliases = {}\nlocal parmlist = {}\n\nlocal filetypes = {\n    stdin = {io.stdin,'file-in'}, stdout = {io.stdout,'file-out'},\n    stderr = {io.stderr,'file-out'}\n}\n\nlocal function quit(msg,no_usage)\n    if msg then\n        io.stderr:write(msg..'\\n\\n')\n    end\n    if not no_usage then\n        io.stderr:write(usage)\n    end\n    os.exit(1);\nend\n\nlocal function help()\n  print(usage)\n  os.exit()\nend\n\nlocal function version()\n  return {version = true}\nend\n\nlocal function error(msg,no_usage)\n    quit(arg[0]:gsub('.+[\\\\/]','')..':'..msg,no_usage)\nend\n\nlocal function ltrim(line)\n    return line:gsub('^%s*','')\nend\n\nlocal function rtrim(line)\n    return line:gsub('%s*$','')\nend\n\nlocal function trim(s)\n    return ltrim(rtrim(s))\nend\n\nlocal function open (file,opt)\n    local val,err = io.open(file,opt)\n    if not val then error(err,true) end\n    append(open_files,val)\n    return val\nend\n\nlocal function xassert(condn,msg)\n    if not condn then\n        error(msg)\n    end\nend\n\nlocal function range_check(x,min,max,parm)\n    xassert(min <= x and max >= x,parm..' out of range')\nend\n\nlocal function xtonumber(s)\n    local val = tonumber(s)\n    if not val then error(\"unable to convert to number: \"..s) end\n    return val\nend\n\nlocal function is_filetype(type)\n    return type == 'file-in' or type == 'file-out'\nend\n\nlocal types = {}\n\nlocal function convert_parameter(ps,val)\n    if ps.converter then\n        val = ps.converter(val)\n    end\n    if ps.type == 'number' then\n        val = xtonumber(val)\n    elseif is_filetype(ps.type) then\n        val = open(val,(ps.type == 'file-in' and 'r') or 'w' )\n    elseif ps.type == 'boolean' then\n        val = true\n    end\n    if ps.constraint then\n        ps.constraint(val)\n    end\n    return val\nend\n\nfunction lapp.add_type (name,converter,constraint)\n    types[name] = {converter=converter,constraint=constraint}\nend\n\nlocal function force_short(short)\n    xassert(#short==1,short..\": short parameters should be one character\")\nend\n\nfunction process_options_string(str)\n    local res = {}\n    local varargs\n\n    local function check_varargs(s)\n        local res,cnt = s:gsub('%.%.%.$','')\n        varargs = cnt > 0\n        return res\n    end\n\n    local function set_result(ps,parm,val)\n        if not ps.varargs then\n            res[parm] = val\n        else\n            if not res[parm] then\n                res[parm] = { val }\n            else\n                append(res[parm],val)\n            end\n        end\n    end\n\n    usage = str\n\n    for line in str:gmatch('([^\\n]*)\\n') do\n        local optspec,optparm,i1,i2,defval,vtype,constraint\n        line = ltrim(line)\n        -- flags: either -<short> or -<short>,<long>\n        i1,i2,optspec = line:find('^%-(%S+)')\n        if i1 then\n            optspec = check_varargs(optspec)\n            local short,long = optspec:match('([^,]+),(.+)')\n            if short then\n                optparm = long:sub(3)\n                aliases[short] = optparm\n                force_short(short)\n            else\n                optparm = optspec\n                force_short(optparm)\n            end\n        else -- is it <parameter_name>?\n            i1,i2,optparm = line:find('(%b<>)')\n            if i1 then\n                -- so <input file...> becomes input_file ...\n                optparm = check_varargs(optparm:sub(2,-2)):gsub('%A','_')\n                append(parmlist,optparm)\n            end\n        end\n        if i1 then -- this is not a pure doc line\n            local last_i2 = i2\n            local sval\n            line = ltrim(line:sub(i2+1))\n            -- do we have (default <val>) or (<type>)?\n            i1,i2,typespec = line:find('^%s*(%b())')\n            if i1 then\n                typespec = trim(typespec:sub(2,-2)) -- trim the parens and any space\n                sval = typespec:match('default%s+(.+)')\n                if sval then\n                    local val = tonumber(sval)\n                    if val then -- we have a number!\n                        defval = val\n                        vtype = 'number'\n                    elseif filetypes[sval] then\n                        local ft = filetypes[sval]\n                        defval = ft[1]\n                        vtype = ft[2]\n                    else\n                        defval = sval\n                        vtype = 'string'\n                    end\n                else\n                    local min,max = typespec:match '([^%.]+)%.%.(.+)'\n                    if min then -- it's (min..max)\n                        vtype = 'number'\n                        min = xtonumber(min)\n                        max = xtonumber(max)\n                        constraint = function(x)\n                            range_check(x,min,max,optparm)\n                        end\n                    else -- () just contains type of required parameter\n                        vtype = typespec\n                    end\n                end\n            else -- must be a plain flag, no extra parameter required\n                defval = false\n                vtype = 'boolean'\n            end\n            local ps = {\n                type = vtype,\n                defval = defval,\n                required = defval == nil,\n                comment = line:sub((i2 or last_i2)+1) or optparm,\n                constraint = constraint,\n                varargs = varargs\n            }\n            if types[vtype] then\n                local converter = types[vtype].converter\n                if type(converter) == 'string' then\n                    ps.type = converter\n                else\n                    ps.converter = converter\n                end\n                ps.constraint = types[vtype].constraint\n            end\n            parms[optparm] = ps\n        end\n    end\n    -- cool, we have our parms, let's parse the command line args\n    local iparm = 1\n    local iextra = 1\n    local i = 1\n    local parm,ps,val\n    while i <= #arg do\n        -- look for a flag, -<short flags> or --<long flag>\n        local i1,i2,dash,parmstr = arg[i]:find('^(%-+)(%a.*)')\n        if i1 then -- we have a flag\n            if #dash == 2 then -- long option\n                parm = parmstr\n            else -- short option\n                if #parmstr == 1 then\n                    parm = parmstr\n                else -- multiple flags after a '-',?\n                    parm = parmstr:sub(1,1)\n                    if parmstr:find('^%a%d+') then\n                        -- a short option followed by a digit? (exception for AW ;))\n                        -- push ahead into the arg array\n                        table.insert(arg,i+1,parmstr:sub(2))\n                    else\n                        -- push multiple flags into the arg array!\n                        for k = 2,#parmstr do\n                            table.insert(arg,i+k-1,'-'..parmstr:sub(k,k))\n                        end\n                    end\n                end\n            end\n            if parm == 'h' or parm == 'help' then\n                help()\n            end\n            if parm == \"v\" or parm == \"version\" then\n              return version()\n            end\n            if aliases[parm] then parm = aliases[parm] end\n            ps = parms[parm]\n            if not ps then error(\"unrecognized parameter: \"..parm) end\n            if ps.type ~= 'boolean' then -- we need a value! This should follow\n                val = arg[i+1]\n                i = i + 1\n                xassert(val,parm..\" was expecting a value\")\n            end\n        else -- a parameter\n            parm = parmlist[iparm]\n            if not parm then\n               -- extra unnamed parameters are indexed starting at 1\n               parm = iextra\n               iextra = iextra + 1\n               ps = { type = 'string' }\n            else\n                ps = parms[parm]\n            end\n            if not ps.varargs then\n                iparm = iparm + 1\n            end\n            val = arg[i]\n        end\n        ps.used = true\n        val = convert_parameter(ps,val)\n        set_result(ps,parm,val)\n        if is_filetype(ps.type) then\n            set_result(ps,parm..'_name',arg[i])\n        end\n        if lapp.callback then\n            lapp.callback(parm,arg[i],res)\n        end\n        i = i + 1\n    end\n    -- check unused parms, set defaults and check if any required parameters were missed\n    for parm,ps in pairs(parms) do\n        if not ps.used then\n            if ps.required then error(\"missing required parameter: \"..parm) end\n            set_result(ps,parm,ps.defval)\n        end\n    end\n    return res\nend\n\nsetmetatable(lapp, {\n    __call = function(tbl,str) return process_options_string(str) end,\n    __index = {\n        open = open,\n        quit = quit,\n        error = error,\n        assert = xassert,\n    }\n})\n\nreturn lapp\n"
  },
  {
    "path": "make4ht",
    "content": "#!/usr/bin/env texlua\n-- Package make4ht. Author Michal Hoftich <michal.h21@gmail.com>\n-- This package is subject of LPPL license, version 1.3 \nkpse.set_program_name(\"luatex\")\n\n-- logging should be globally available\nlogging = require \"make4ht-logging\"\nif os.type == \"windows\" then logging.use_colors = false end\nlocal log = logging.new(\"make4ht\")\nlocal make4ht = require(\"make4ht-lib\")\nlocal lapp    = require(\"lapp-mk4\")\nlocal mkutils = require(\"mkutils\")\nlocal mkparams = require(\"mkparams\")\nlocal mk_config = require(\"make4ht-config\")\n-- args string is here just as sample, we dont pass it it to \n-- mkparams.get_args() so default args string is used\nlocal args    =  [[\nmake4ht - build system for TeX4ht\nUsage:\nmake4ht [options] filename [\"tex4ht.sty op.\" \"tex4ht op.\" \"t4ht op\" \"latex op\"]\n-c,--config (default xhtml) Custom config file\n-d,--output-dir (default nil)  Output directory\n-l,--lua  Use lualatex for document compilation\n-s,--shell-escape Enables running external programs from LaTeX\n-u,--utf8  For output documents in utf8 encoding\n-x,--xetex Use xelatex for document compilation\n<filename> (string) Input file name\n]]\n\n-- set version number. the template should be replaced by the\n-- actual version number by the build script\nlocal version = \"{{version}}\"\nmkparams.version_number = version\n\nlocal args = mkparams.get_args()\n\nlocal parameters = mkparams.process_args(args) \n\nlog:status(\"Conversion started\")\nlog:status(\"Input file: \" .. parameters.tex_file)\n\n\nif parameters.builddir and parameters.builddir ~= \"\" then\n  mkutils.make_path(parameters.builddir)\nend\n\nlocal mode = parameters.mode\nlocal build_file = parameters.build_file \n\n-- handle output formats\nlocal allowed_output_formats = {xhtml = true, html5=true, odt = true, docbook=true, tei=true, jats=true}\n-- formatter is Lua library which must provide at least prepare_parameters\n-- and process_build_sequence functions\nlocal formatter\nlocal output_format = parameters.output_format\nif allowed_output_formats[ output_format ] then\n  formatter = mkutils.load_output_format(output_format)\nelse\n  -- load html5 as default output format\n  if output_format then \n    log:warning(\"Cannot load output format: \".. output_format)\n  end\n  formatter = mkutils.load_output_format(\"html5\")\nend\n-- find make4ht configuration file\nlocal configname = \"make4ht\"\nlocal conffile = mk_config.find_config(configname) or mk_config.find_xdg_config(configname)\nif conffile then\n  log:info(\"Using configuration file: \" .. conffile)\n  mkutils.load_config(parameters, conffile)\nend\nlocal extensions = formatter.prepare_extensions(parameters.extensions)\nextensions = mkutils.load_extensions(extensions, output_format)\n\n\n\n-- run extensions with prepare_parameters function\nparameters = formatter.prepare_parameters(parameters,extensions)\nlocal make = mkutils.load_config(parameters, build_file)[\"Make\"]\nmake.params = parameters\nif make:length() < 1 then\n\tif mode == \"draft\" then\n\t\tmake:htlatex()\n  elseif mode == \"clean\" then\n    make:clean()\n    make.no_dvi_process = true\n\telse\n    -- automatically detect and execute number of necessary compilations by default\n    make:autohtlatex()\n\tend\nend\n\n\nif not args[\"no-tex4ht\"] and not make.no_dvi_process then\n  make:tex4ht()\nend\n\nlocal ext = args.xetex and \"xdv\" or \"dvi\"\nif #make.image_patterns > 0 then\n  make.params.t4ht_par = make.params.t4ht_par .. \" -p\"\nend\n\nif not make.no_dvi_process then\n  make:t4ht {ext = ext}\nend\n\n-- run extensions which modify the build sequence\nif #extensions > 0 then\n  make = mkutils.extensions_modify_build(extensions, make)\nend\n\n-- allow output formats to modify the build process at the end\nmake = formatter.modify_build(make) or make\n\nmake:match(\"tmp$\", function(filename,params) \n  -- remove the temporary tex file created when the input comes from the standard input\n  if params.is_tmp_file then\n    log:info(\"removing temp file\", params.tex_file)\n    os.remove(params.tex_file)\n  end\n  -- prevent copying of the temporary file to the outdir\n  return false,\"tmp file\" end\n)\n\nmake:match(\".*\",function(filename,par)\n\tlocal outdir =  '' --par[\"outdir\"] and par[\"outdir\"] ..\"/\" or ''\n\tif par['outdir'] ~= \"\" then \n    outdir = par['outdir'] .. '/' \n  else\n    -- don't run unnecessary copy without output dir\n    log:info(\"No output directory\")\n    return true\n  end\n  log:info(\"outdir: \"..outdir)\n  local outfilename = filename:gsub(\"^\" .. mkutils.escape_pattern(par.builddir or \"\"), \"\")\n  outfilename = outdir .. outfilename\n  mkutils.copy(filename,outfilename)\n\treturn true\nend)\n\nmake:run()\n\nlog:status(\"Conversion finished\")\nlogging.exit_status()\n\n"
  },
  {
    "path": "make4ht-aeneas-config.lua",
    "content": "local M = {}\n\nlocal mkutils = require \"mkutils\"\n\nlocal task_template = [[\n<task>\n    <task_language>${lang}</task_language>\n    <task_description>${file_desc}</task_description>\n    <task_custom_id>${file_id}</task_custom_id>\n    <is_text_file>${prefix}${html_file}</is_text_file>\n    <is_text_type>${text_type}</is_text_type>\n    <is_audio_file>${prefix}${audio_file}</is_audio_file>\n    <is_text_unparsed_id_sort>${id_sort}</is_text_unparsed_id_sort>\n    <is_text_unparsed_id_regex>${id_regex}</is_text_unparsed_id_regex>\n    <os_task_file_name>${sub_file}</os_task_file_name>\n    <os_task_file_format>${sub_format}</os_task_file_format>\n    <os_task_file_smil_page_ref>${html_file}</os_task_file_smil_page_ref>\n    <os_task_file_smil_audio_ref>${audio_file}</os_task_file_smil_audio_ref>\n</task>\n]]\n\n-- get html files\nlocal function get_html_files(config)\n  local config = config or {}\n  local files = {}\n  local filematch = config.file_match or  \"html$\"\n  -- this is a trick to get list of files from the LG file\n  for _, file in ipairs(Make.lgfile.files) do\n    if file:match(filematch) then table.insert(files, file) end\n  end\n  return files\nend\n\n-- prepare filename for the audio\nlocal function get_audio_file(filename, config)\n  local extension = config.audio_extension or \"mp3\"\n  local base = mkutils.remove_extension(filename)\n  return base .. \".\" .. extension\nend\n\nlocal function get_sub_file(filename, config)\n  local extension = config.sub_format or \"smil\"\n  local base = mkutils.remove_extension(filename)\n  return base .. \".\" .. extension\nend\n\n\n-- create task record for each HTML file\nlocal function prepare_tasks(files, configuration)\n  local tasks = {}\n  --  the map can contain info for particular files, otherwise we will interfere default values\n  local map = configuration.map or {}\n  -- task_template should be configurable\n  local task_template = configuration.task_template or task_template\n  for i, filename in ipairs(files) do\n    local filemap = map[filename] \n    if filemap ~= false then\n      filemap = filemap or {}\n      local taskconfig = configuration\n      taskconfig.html_file = filename\n      taskconfig.prefix = filemap.prefix or configuration.prefix\n      taskconfig.file_desc = filemap.description or configuration.description .. \" \" .. i\n      taskconfig.file_id = filemap.id or filename:gsub(\"[%/%.]\", \"_\")\n      taskconfig.text_type = filemap.text_type or configuration.text_type\n      taskconfig.audio_file = filemap.audio_file or get_audio_file(filename, configuration)\n      taskconfig.sub_file = filemap.sub_file or get_sub_file(filename, configuration)\n      taskconfig.id_sort= filemap.id_sort  or configuration.id_sort\n      taskconfig.id_prefix = filemap.id_regex or configuration.id_regex\n      taskconfig.sub_format = filemap.sub_format or configuration.sub_format\n      tasks[#tasks+1] = task_template % taskconfig\n      Make:add_file(taskconfig.audio_file)\n      Make:add_file(taskconfig.sub_file)\n    end\n  end\n  return tasks --table.concat(tasks, \"\\n\")\nend\n-- from https://www.readbeyond.it/aeneas/docs/clitutorial.html#xml-config-file-config-xml\nlocal config_template = [[\n<job>\n    <job_language>${lang}</job_language>\n    <job_description>${description}</job_description>\n    <tasks>\n    ${tasks}\n    </tasks>\n    <os_job_file_name>output_example4</os_job_file_name>\n    <os_job_file_container>zip</os_job_file_container>\n    <os_job_file_hierarchy_type>flat</os_job_file_hierarchy_type>\n    <os_job_file_hierarchy_prefix>${prefix}</os_job_file_hierarchy_prefix>\n</job>\n]]\n\n-- check if the config file exists\nlocal function is_config(filename)\n  return mkutils.file_exists(filename)\nend\n\n-- prepare Aeneas configuration\nlocal function prepare_configuration(parameters)\n  local config = parameters or {}\n  config.lang = parameters.lang \n  config.tasks = table.concat(prepare_tasks(parameters.files, config), \"\\n\")\n  return config\nend\n\n-- write Aeneeas configuration file in the XML format\nlocal function write_config(filename, configuration)\n  local cfg = config_template % configuration\n  print(cfg)\n  local f = io.open(filename, \"w\")\n  f:write(cfg)\n  f:close()\nend\n\n\nlocal function make_default_options(options)\n  local configuration = {}\n  local par = get_filter_settings \"aeneas-config\"\n  configuration.lang = options.lang or par.lang or \"en\"\n  configuration.description = options.description or par.description or \"Aeneas job\"\n  configuration.map = options.map or par.map or {}\n  configuration.text_type = options.text_type or par.text_type or \"unparsed\"\n  configuration.id_sort = options.id_sort or par.id_sort or \"numeric\"\n  configuration.id_regex = options.id_regex or par.id_regex or par.id_prefix .. \"[0-9]+\"\n  configuration.sub_format = options.sub_format or par.sub_format or \"smil\"\n  configuration.prefix = options.prefix or par.prefix or \"./\"\n  configuration.config_name = options.config_name or par.config_name or \"config.xml\"\n  configuration.keep_config = options.keep_config or par.keep_config\n  return configuration\nend\n\n\nlocal function configure_job(options)\n  local configuration = make_default_options(options)\n  local config_name = configuration.config_name\n  -- prepare the configuration in every case\n  configuration.files = get_html_files()\n  local configuration = prepare_configuration(configuration)\n  -- write the configuration only if the config file doesn't exist\n  -- and keep_config option is set to true\n  if is_config(config_name) and configuration.keep_config==true then\n  else\n    write_config(config_name, configuration)\n  end\nend\n\nlocal function execute_job(options)\n  local par = get_filter_settings \"aeneas-config\"\n  local configuration = make_default_options(options)\n  configuration.files = get_html_files()\n  -- we need to configure prepare_tasks to return calls to aeneas task convertor\n  configuration.python = options.python or par.python or \"python3\"\n  configuration.module = options.module or par.module or \"aeneas.tools.execute_task\"\n  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}\"'\n  local tasks = prepare_tasks(configuration.files, configuration)\n  -- execute the tasks\n  for _, v in ipairs(tasks) do\n    print(\"task\", v)\n    local proc = io.popen(v, \"r\")\n    local result = proc:read(\"*all\")\n    proc:close()\n    print(result)\n  end\nend\n\n-- the aeneas configuration must be executed at last processed file, after all filters\n-- have been executed\nlocal function get_last_lg_file()\n  local t = Make.lgfile.files\n  for i = #t, 1, -1 do\n    --  find last html file or the tmp file\n    local x = t[i]\n    if x:match \"html$\" or x:match \"tmp$\" then \n      return x \n    end\n  end\n  return t[#t]\nend\n\n-- write Aeneas job configuration file\n-- it doesn't execute Aeneas\nfunction M.write_job(par)\n  -- configuration table for Aeneas job\n  Make:match(\"tmp$\", function()\n    configure_job(par)\n  end)\nend\n\n-- execute Aeneas directly\nfunction M.execute(par)\n  Make:match(\"tmp$\", function(current_name)\n    -- there may be html files after the .tmp file\n    -- the aeneas must be executed after the Aeneas filter inserts the id\n    -- attributes, so it is necessary to execute this code as very last one\n    local last = get_last_lg_file()\n    -- execute the job if there are no HTML files after the tmp file\n    if current_name == last then\n      execute_job(par)\n    end\n    Make:match(last, function()\n      execute_job(par)\n    end)\n  end)\nend\n\n-- only register the audio and smil files as processed files\nfunction M.process_files(par)\n  Make:match(\"tmp$\", function()\n    local configuration = make_default_options(par)\n    local files = get_html_files()\n    prepare_tasks(files, configuration)\n  end)\nend\n\n\nreturn M\n"
  },
  {
    "path": "make4ht-config.lua",
    "content": "local m = {}\n\nlocal mkutils = require \"mkutils\"\n\nlocal file_exists = mkutils.file_exists\n-- function file_exists(name)\n-- \tlocal f=io.open(name,\"r\")\n-- \tif f~=nil then io.close(f) return true else return false end\n-- end\n\n\nlocal make_name = function(name)\n  return table.concat(name, \"/\")\n  -- return name:gsub(\"//\",\"/\")\nend\n\n-- find the config file in XDG_CONFIG_HOME  or in the HOME directry\n-- the XDG tree is looked up first, the $HOME is used only when it cannot be\n-- find in the former\nlocal xdg_config  = function(filename, xdg_config_name)\n  local dotfilename = \".\" .. filename\n  local xdg_config_name = xdg_config_name or \"config.lua\"\n  local xdg = os.getenv(\"XDG_CONFIG_HOME\") or ((os.getenv(\"HOME\") or \"\") ..  \"/.config\")\n  local home = os.getenv(\"HOME\") or os.getenv(\"USERPROFILE\")\n  if xdg then\n    -- filename like ~/.config/make4ht/config.lua\n    local fn = make_name{ xdg ,filename , xdg_config_name }\n    if file_exists(fn) then\n      return fn\n    end\n  end\n  if home then\n    -- ~/.make4ht\n    local fn = make_name{ home, dotfilename }\n    if file_exists(fn) then\n      return fn\n    end\n  end\nend\n\nlocal find_config = function(filename)\n  local filename = \".\" .. filename\n\tlocal current =  lfs.currentdir()\n\tlocal path = {}\n\tcurrent:gsub(\"([^/]+)\", function(s) table.insert(path,s) end)\n\tlocal begin = os.type == \"windows\" and \"\" or \"/\"\n\tfor i = #path, 1, -1 do\n\t\tlocal fn =begin .. table.concat(path,\"/\") .. \"/\".. filename\n\t\t-- print(\"testing\",fn)\n\t\tif file_exists(fn) then return fn end\n\t\ttable.remove(path)\n\tend\n\treturn false\nend\n\nlocal function load_config(filename, default)\n  local default = default or {}\n  default.table = table\n  default.string = string\n  default.io = io\n  default.os = os\n  default.math = math\n  default.print = print\n  default.ipairs = ipairs\n  default.pairs = pairs\n  local f = io.open(filename, \"r\")\n  local contents = f:read(\"*all\")\n  f:close()\n  load(contents,\"sandbox config\",\"bt\", default)()\n  return default\nend\n\n--[[\nlocal function load_config(filename, default)\n\tlocal default = default or {}\n\tif ~file_exists(filename) then \n\t\treturn nil, \"Cannot load config file \"..filename \n\tend\n\tlocal section = \"default\"\n\tlocal file  = io.open(filename,  \"r\")\n\tif ~file then return nil, \"Error opening config file\"..filename end\n\tfor line in file:lines() do\n\t\tlocal ts = line:match(\"\")\n\tend\n\tfile:close()\nend\n--]]\n\nm.find_config     = find_config\nm.find_xdg_config = xdg_config\nm.load_config     = load_config\nreturn m\n"
  },
  {
    "path": "make4ht-doc.tex",
    "content": "% \\documentclass{ltxdoc}\n\\documentclass{article}\n\n\n\\usepackage[english]{babel}\n\\usepackage{hyperref}\n\\newcommand\\authormail[1]{\\footnote{\\textless\\url{#1}\\textgreater}}\n\\ifdefined\\HCode\n\\renewcommand\\authormail[1]{\\space\\textless\\Link[#1]{}{}#1\\EndLink\\textgreater}\n\\fi\n\n\\usepackage{fontspec}\n\\setmainfont{TeX Gyre Schola}\n% \\setmonofont[Scale=MatchLowercase]{Inconsolatazi4}\n\\IfFontExistsTF{Noto Sans Mono Regular}{%\n  \\setmonofont[Scale=MatchLowercase]{Noto Sans Mono Regular}\n}{\\setmonofont{NotoMono-Regular.ttf}}\n\\usepackage{upquote}\n\n\\usepackage{microtype}\n\\providecommand\\tightlist{\\relax}\n\n\\title{The \\texttt{make4ht} build system}\n\\author{Michal Hoftich\\authormail{michal.h21@gmail.com}}\n\\date{Version \\version\\\\\\gitdate}\n\\begin{document}\n\\maketitle\n\\tableofcontents\n\\input{readme}\n\n\\input{changelog}\n\\end{document}\n"
  },
  {
    "path": "make4ht-dvireader.lua",
    "content": "-- This is not actually full DVI reader. It just calculates hash for each page,\n-- so it can be detected if it changed between compilations and needs to be\n-- converted to image using Dvisvgm or Dvipng\n--\n-- information about DVI format is from here: https://web.archive.org/web/20070403030353/http://www.math.umd.edu/~asnowden/comp-cont/dvi.html\n--\nlocal M\n\n-- the file after post_post is filled with bytes 223\nlocal endfill = 223\n\n-- numbers of bytes for each data type in DVI file\nlocal int = 4\nlocal byte = 1\nlocal sixteen = 2\n\nlocal function read_char(str, pos)\n  if pos and pos > string.len(str) then return nil end\n  return string.sub(str, pos, pos + 1)\nend\n\nlocal function read_byte(str, pos)\n  return string.byte(read_char(str, pos))\nend\n\n-- DVI file format uses signed big endian integers. This code doesn't take into account \n-- the sign, so it will return incorrect result for negative numbers. It doesn't matter \n-- for the original purpose of this library, but it should be fixed for general use.\nlocal function read_integer(str, pos)\n  local first = read_byte(str, pos)\n  local num = first * (256 ^ 3)\n  num = read_byte(str, pos + 1) * (256 ^ 2) + num\n  num = read_byte(str, pos + 2) * 256  + num\n  num = read_byte(str, pos + 3) + num\n  return num\nend\n\nlocal function read_sixteen(str, pos)\n  local num = read_byte(str, pos) * 256 \n  num = read_byte(str, pos + 1) + num\n  return num\nend\n\n-- select reader function with number of bytes of an argument\nlocal readers = {\n  [byte] = read_byte,\n  [int] = read_integer,\n  [sixteen] = read_sixteen\n}\n\n\nlocal opcodes = {\n  post_post = {\n    opcode = 249, args = {\n      {name=\"q\", type = int}, -- postamble address\n      {name=\"i\", type = byte}\n    }\n  },\n  post = {\n    opcode = 248,\n    args = {\n      {name=\"p\", type = int}, -- address of the last page\n      {name=\"num\", type = int},\n      {name=\"den\", type = int},\n      {name=\"mag\", type = int},\n      {name=\"l\", type = int},\n      {name=\"u\", type = int},\n      {name=\"s\", type = sixteen},\n      {name=\"t\", type = sixteen},\n    }\n  },\n  bop = {\n    opcode = 139,\n    args = {\n      {name=\"c0\", type=int},\n      {name=\"c1\", type=int},\n      {name=\"c2\", type=int},\n      {name=\"c3\", type=int},\n      {name=\"c4\", type=int},\n      {name=\"c5\", type=int},\n      {name=\"c6\", type=int},\n      {name=\"c7\", type=int},\n      {name=\"c8\", type=int},\n      {name=\"c9\", type=int},\n      {name=\"p\", type=int}, -- previous page\n    }\n  }\n}\n\nlocal function read_arguments(str, pos, args)\n  local t = {}\n  for _, v in ipairs(args) do\n    local fn =  readers[v.type]\n    t[v.name] = fn(str, pos)\n    -- seek the position. v.type contains size of the current data type in bytes\n    pos = pos + v.type\n  end\n  return t\nend\n\nlocal function read_opcode(opcode, str, pos)\n  local format = opcodes[opcode]\n  if not format then return nil, \"Cannot find opcode format: \" .. opcode end\n  -- check that opcode byte in the current position is the same as required opcode\n  local op = read_byte(str, pos)\n  if op ~= format.opcode then return nil, \"Wrong opcode \" .. op .. \" at position \" .. pos end\n  return read_arguments(str, pos+1, format.args)\nend\n\n-- find the postamble address\nlocal function get_postamble_addr(dvicontent)\n  local pos = string.len(dvicontent)\n  local last = read_char(dvicontent, pos)\n  -- skip endfill bytes at the end of file\n  while string.byte(last) == endfill do\n    pos = pos - 1\n    last = read_char(dvicontent, pos)\n  end\n  -- first read post_post to get address of the postamble\n  local post_postamble, msg = read_opcode(\"post_post\", dvicontent, pos-5)\n  if not post_postamble then return nil, msg end\n  -- return the postamble address\n  return post_postamble.q + 1\n  -- return read_opcode(\"post\", dvicontent, post_postamble.q + 1)\n\nend\n\nlocal function read_page(str, start, stop)\n  local function get_end_of_page(str, pos)\n    if read_byte(str, pos) == 140 then -- end of page\n      return pos\n    end\n    return get_end_of_page(str, pos - 1)\n  end\n  -- we reached the end of file\n  if start == 2^32-1 then return nil end\n  local current_page = read_opcode(\"bop\", str,  start + 1)\n  if not current_page then return nil end\n  local endofpage = get_end_of_page(str, stop)\n  -- get the page contents, but skip all parameters, because they can change\n  -- (especially pointer to the previous page)\n  local page = str:sub(start + 46, endofpage) \n  local page_obj = {\n    number = current_page.c0, -- the page number\n    hash = md5.sumhexa(page) -- hash the page contents\n  }\n  return page_obj, current_page.p, start\nend\n\nlocal function get_pages(dvicontent)\n  local pages = {}\n  local postamble_pos = get_postamble_addr(dvicontent)\n  local postamble = read_opcode(\"post\", dvicontent, postamble_pos)\n  local next_page_pos = postamble.p \n  local page, previous_page = nil, postamble_pos\n  local page_sequence = {}\n  while next_page_pos do\n    page, next_page_pos, previous_page = read_page(dvicontent, next_page_pos, previous_page)\n    page_sequence[#page_sequence+1] = page\n  end\n\n  -- reorder pages\n  for _, v in ipairs(page_sequence) do\n    pages[v.number] = v.hash\n  end\n  return pages\n\nend\n\n-- if arg[1] then\n--   local f = io.open(arg[1], \"r\")\n--   local dvicontent = f:read(\"*all\")\n--   f:close()\n--   local pages = get_pages(dvicontent)\n--   for k,v in pairs(pages) do \n--     print(k,v)\n--   end\n-- end\n\nreturn {\n  get_pages = get_pages\n}\n"
  },
  {
    "path": "make4ht-errorlogparser.lua",
    "content": "local m = {}\n\nlocal function get_filename(chunk)\n  local filename = chunk:match(\"([^\\n^%(]+)\") \n  if not filename then \n    return false, \"No filename detected\"\n  end\n  local first = filename:match(\"^[%./\\\\]+\")\n  if first then return filename end\n  return false\nend\n\nlocal function get_chunks(text)\n  -- parse log for particular included files\n  local chunks = {}\n  -- each file is enclosed in matching () brackets\n  local newtext = text:gsub(\"(%b())\", function(a)\n    local chunk = string.sub(a,2,-2)\n    -- if no filename had been found in the chunk, it is probably not file chunk\n    -- so just return the original text\n    local filename = get_filename(chunk)\n    if not filename then return a end\n    local children, text = get_chunks(chunk)\n    table.insert(chunks, {filename = filename, text = text, children = children})\n    return \"\"\n  end)\n  return chunks, newtext\nend\n\n\nfunction print_chunks(chunks, level)\n  local level = level or 0\n  local indent = string.rep(\"  \", level)\n  for k,v in ipairs(chunks) do\n    print(indent .. (v.filename or \"?\"), string.len(v.text))\n    print_chunks(v.children, level + 1)\n  end\nend\n\nlocal function parse_default_error(lines, i)\n  local line = lines[i]\n  -- get the error message \"! msg text\"\n  local err = line:match(\"^!(.+)\")\n  -- the next line should contain line number where error happened\n  local next_line = lines[i+1] or \"\"\n  local msg = {}\n  -- get the line number and first line of the error context\n  local line_no, msg_start = next_line:match(\"^l%.(%d+)(.+)\") \n  line_no = line_no or false \n  msg_start = msg_start or \"\"\n  msg[#msg+1] = msg_start .. \" <-\"\n  -- try to find rest of the error context. \n  for x = i+2, i+5 do\n    local next_line = lines[x] or \"\"\n    -- break on blank lines\n    if next_line:match(\"^%s*$\") then break end\n    msg[#msg+1] = next_line:gsub(\"^%s*\", \"\"):gsub(\"%s$\", \"\")\n  end\n  return err, line_no, table.concat(msg, \" \")\nend\n\nlocal  function parse_linenumber_error(lines, i)\n  -- parse errors from log created with the -file-line-number option\n  local line = lines[i]\n  local filename, line_no, err = line:match(\"^([^%:]+)%:(%d+)%:%s*(.*)\")\n  local msg = {}\n  -- get error context\n  for x = i+1, i+2 do\n    local next_line = lines[x] or \"\"\n    -- break on blank lines\n    if next_line:match(\"^%s*$\") then break end\n    msg[#msg+1] = next_line:gsub(\"^%s*\", \"\"):gsub(\"%s$\", \"\")\n  end\n  -- insert mark to the error\n  if #msg > 1 then\n    table.insert(msg, 2, \"<-\")\n  end\n  return err, line_no, table.concat(msg, \" \")\nend\n\n--- get error messages, linenumbers and contexts from a log file chunk\n---@param text string chunk from the long file where we should find errors\n---@return table errors error messages\n---@return table error_lines error line number \n---@return table error_messages error line contents\nlocal function parse_errors(text)\n  local lines = {}\n  local errors = {}\n  local find_line_no = false\n  local error_lines = {}\n  local error_messages = {}\n  for line in text:gmatch(\"([^\\n]+)\") do\n    lines[#lines+1] = line\n  end\n  for i = 1, #lines do\n    local line = lines[i]\n    local err, line_no, msg\n    if line:match(\"^!(.+)\") then\n      err, line_no, msg = parse_default_error(lines, i)\n    elseif line:match(\"^[^%:]+%:%d+%:.+\") then\n      err, line_no, msg = parse_linenumber_error(lines, i)\n    end\n    if err then\n      errors[#errors+1] = err\n      error_lines[#errors] = line_no\n      error_messages[#errors] = msg\n    end\n  end\n  return errors, error_lines, error_messages\nend\n\n\nlocal function get_errors(chunks, errors)\n  local errors =  errors or {}\n  for _, v in ipairs(chunks) do\n    local current_errors, error_lines, error_contexts = parse_errors(v.text)\n    for i, err in ipairs(current_errors) do\n      table.insert(errors, {filename = v.filename, error = err, line = error_lines[i], context = error_contexts[i] })\n    end\n    errors = get_errors(v.children, errors)\n  end\n  return errors\nend\n\nfunction m.get_missing_4ht_files(log)\n  local used_files = {}\n  local used_4ht_files = {}\n  local missing_4ht_files = {}\n  local pkg_names = {sty=true, cls=true}\n  for filename, ext in log:gmatch(\"[^%s]-([^%/^%\\\\^%.%s]+)%.([%w][%w]+)\") do\n    -- break ak\n    if ext == \"aux\" then break end\n    if pkg_names[ext] then\n      used_files[filename .. \".\" .. ext] = true\n    elseif ext == \"4ht\" then\n      used_4ht_files[filename] = true\n    end\n  end\n  for filename, _ in pairs(used_files) do\n    if not used_4ht_files[mkutils.remove_extension(filename)] then\n      table.insert(missing_4ht_files, filename)\n    end\n  end\n  return missing_4ht_files\nend\n\n\nfunction m.parse(log)\n  local chunks, newtext = get_chunks(log)\n  -- save the unparsed text that contains system messages\n  table.insert(chunks, {text = newtext, children = {}})\n  -- print_chunks(chunks)\n  local errors = get_errors(chunks)\n  -- for _,v in ipairs(errors) do \n    -- print(\"error\", v.filename, v.line, v.error)\n  -- end\n  return errors, chunks\nend\n\n\nm.print_chunks = print_chunks\n\nreturn m\n"
  },
  {
    "path": "make4ht-filterlib.lua",
    "content": "local M = {}\n\n-- the filter module  must implement the load_filter function\nfunction M.load_filters(filters, load_filter)\n\tlocal sequence = {}\n\tif type(filters) == \"string\" then\n\t\ttable.insert(sequence,load_filter(filters))\n\telseif type(filters) == \"table\" then\n\t\tfor _,n in ipairs(filters) do\n\t\t\tif type(n) == \"string\" then\n\t\t\t\ttable.insert(sequence,load_filter(n))\n\t\t\telseif type(n) == \"function\" then\n\t\t\t\ttable.insert(sequence, n)\n\t\t\tend\n\t\tend\n\telseif type(filters) == \"function\" then\n\t\ttable.insert(sequence, filters)\n\telse\n\t\treturn false, \"Argument to filter must be either\\ntable with filter names, or single filter name\"\n\tend\n  return sequence\nend\n\nfunction M.load_input_file(filename)\n  if not filename then return false, \"filters: no filename\" end\n  local input = nil\n\n  if filename then\n    local file = io.open(filename,\"r\")\n    input = file:read(\"*all\")\n    file:close()\n  end\n  return input\nend\n\nfunction M.save_input_file(filename, input)\n  local file = io.open(filename,\"w\")\n  file:write(input)\n  file:close()\nend\n\nreturn M\n"
  },
  {
    "path": "make4ht-htlatex.lua",
    "content": "local log = logging.new \"htlatex\"\nlocal autolog = logging.new \"autohtlatex\"\n\nlocal error_logparser = require(\"make4ht-errorlogparser\")\n\nlocal Make = Make or {}\n-- this function reads the LaTeX log file and tries to detect fatal errors in the compilation\nlocal function testlogfile(par)\n  local logfile = mkutils.file_in_builddir(par.input .. \".log\", par)\n  local f = io.open(logfile,\"r\")\n  if not f then\n    log:warning(\"Make4ht: cannot open log file \"..logfile)\n    return 1\n  end\n  local content = f:read(\"*a\")\n  -- test only the end of the log file, no need to run search functions on everything\n  local text = content:sub(-1256)\n  f:close()\n  -- parse log file for all errors in non-interactive modes\n  if par.interaction~=\"errorstopmode\" then\n    -- the error log parsing can be slow, so detect errors first\n    -- detect both default error messages (! msg) and -file-line-number errors (filename:lineno:msg)\n    if content:match(\"\\n!\") or content:match(\"[^:]+%:%d+%:.+\")  then\n      local errors, chunks = error_logparser.parse(content)\n      if #errors > 0 then\n        log:error(\"Compilation errors in the htlatex run\")\n        log:error(\"Filename\", \"Line\", \"Message\")\n        for _, err in ipairs(errors) do\n          log:error(err.filename or \"?\", err.line or \"?\", err.error)\n          log:status(err.context)\n        end\n      end\n    end\n  end\n  -- info about packages with no corresponding .4ht files\n  local missing_4ht = error_logparser.get_missing_4ht_files(content)\n  for _, filename in ipairs(missing_4ht) do log:info(\"Unsupported file: \" .. filename) end\n  -- test for fatal errors\n  if text:match(\"No pages of output\") or text:match(\"TeX capacity exceeded, sorry\") or text:match(\"That makes 100 errors\") or text:match(\"Emergency stop\") then return 1 end\n  return 0\nend\n\n\n-- Make this function available in the build files\nMake.testlogfile = testlogfile\n--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}\")\n\n-- template for calling LaTeX with tex4ht loaded\nMake.latex_command = \"${htlatex} --interaction=${interaction} ${build_dir_arg} ${latex_par} '\\\\makeatletter\"..\n\"\\\\def\\\\HCode{\\\\futurelet\\\\HCode\\\\HChar}\\\\def\\\\HChar{\\\\ifx\\\"\\\\HCode\"..\n\"\\\\def\\\\HCode\\\"##1\\\"{\\\\Link##1}\\\\expandafter\\\\HCode\\\\else\"..\n\"\\\\expandafter\\\\Link\\\\fi}\\\\def\\\\Link#1.a.b.c.{\"..\n\"\\\\let\\\\HCode\\\\documentstyle\\\\def\\\\documentstyle{\\\\let\\\\documentstyle\"..\n\"\\\\HCode\\\\expandafter\\\\def\\\\csname tex4ht\\\\endcsname{#1,html}\\\\def\"..\n\"\\\\HCode####1{\\\\documentstyle[tex4ht,}\\\\@ifnextchar[{\\\\HCode}{\"..\n\"\\\\documentstyle[tex4ht]}}\\\\RequirePackage[#1,html]{tex4ht}${packages}}\\\\makeatother\\\\HCode ${tex4ht_sty_par}.a.b.c.\"..\n\"\\\\input \\\"\\\\detokenize{${tex_file}}\\\"'\"\n\nMake.plain_command = '${htlatex} --interaction=${interaction} ${build_dir_arg} ${latex_par}' ..\n\"'\\\\def\\\\Link#1.a.b.c.{\\\\expandafter\\\\def\\\\csname tex4ht\\\\endcsname{\\\\expandafter\\\\def\\\\csname tex4ht\\\\endcsname{#1,html}\\\\input tex4ht.sty }}\" ..\n\"\\\\def\\\\HCode{\\\\futurelet\\\\HCode\\\\HChar}\\\\def\\\\HChar{\\\\ifx\\\"\\\\HCode\\\\def\\\\HCode\\\"##1\\\"{\\\\Link##1}\\\\expandafter\\\\HCode\\\\else\\\\expandafter\\\\Link\\\\fi}\" ..\n\"\\\\HCode ${tex4ht_sty_par}.a.b.c.\\\\input \\\"\\\\detokenize{${tex_file}}\\\"'\"\n\n\nlocal m = {}\n\nfunction m.htlatex(par, latex_command)\n  -- latex_command can be also plain_command for Plain TeX\n  local command = latex_command or Make.latex_command\n  local devnull = \" > /dev/null 2>&1\"\n  if os.type == \"windows\" then\n    command = command:gsub(\"'\",'')\n    devnull = \" > nul 2>&1\"\n  end\n  par.interaction = par.interaction or \"batchmode\"\n  if par.builddir~=\"\" then\n      par.build_dir_arg = \"--output-directory=${builddir}\" % par\n  else\n      par.build_dir_arg = \"\"\n  end\n  if par.interaction == \"batchmode\" then\n    command = command .. devnull\n  end\n  command = command % par\n  log:info(\"LaTeX call: \"..command)\n  os.execute(command)\n  return Make.testlogfile(par)\nend\n\nfunction m.httex(par)\n  local newpar = {}\n  for k,v in pairs(par) do newpar[k] = v end\n  -- change executable name from *latex to *tex\n  newpar.htlatex = newpar.htlatex:gsub(\"latex\", \"tex\")\n  -- plain tex command doesn't support etex extensions\n  -- which are necessary for TeX4ht. just quick hack to fix this\n  if newpar.htlatex == \"tex\" then newpar.htlatex = \"etex\" end\n  return m.htlatex(newpar, Make.plain_command)\nend\n\n\nlocal function get_checksum(main_file, extensions, par)\n  -- make checksum for temporary files \n  local checksum = \"\" \n  local extensions = extensions or {\"aux\", \"4tc\", \"xref\"}\n  for _, ext in ipairs(extensions) do\n    local filename = mkutils.file_in_builddir(main_file .. \".\" .. ext, par)\n    local f = io.open(filename, \"r\")\n    if f then\n      local content = f:read(\"*all\")\n      f:close()\n      -- make checksum of the file and previous checksum \n      -- this way, we will detect change in any file \n      checksum = md5.sumhexa(checksum .. content)\n    end\n  end\n  return checksum\nend\n\n-- this function runs htlatex multiple times until the checksum of temporary files doesn't change\nMake:add(\"autohtlatex\", function(par)\n  -- get checksum of temp files before compilation \n  local options = get_filter_settings \"autohtlatex\"\n  local extensions = par.auto_extensions or options.auto_extensions or {\"aux\", \"4tc\", \"xref\"}\n  local max_compilations  = par.max_compilations or options.max_compilations or  5\n  local checksum = get_checksum(par.input, extensions, par)\n  local status = m.htlatex(par)\n  -- stop processing on error \n  if status ~= 0 then\n    return status\n  end\n  -- get checksum after compilation \n  local newchecksum = get_checksum(par.input, extensions, par)\n  -- this is needed to prevent possible infinite loops \n  local compilation_count = 1\n  while checksum ~= newchecksum do\n    -- stop processing if we reach maximum number of compilations\n    if compilation_count > max_compilations then\n      autolog:info(\"Stopping after \" .. max_compilations .. \" compilations\")\n      return status\n    end\n    status = m.htlatex(par)\n    -- stop processing on error \n    if status ~= 0 then return status end\n    checksum = newchecksum\n    -- get checksum after compilation \n    newchecksum = get_checksum(par.input, extensions, par)\n    compilation_count = compilation_count + 1\n  end\n  return status\nend, {correct_exit= 0})\n\nreturn m\n"
  },
  {
    "path": "make4ht-indexing.lua",
    "content": "\nlocal M = {}\nlocal log = logging.new \"indexing\"\n\n-- Handle accented characters in files created with \\usepackage[utf]{inputenc}\n-- this code was originally part of https://github.com/michal-h21/iec2utf/\nlocal enc = {}\n\nlocal licrs = {}\nlocal codepoint2utf = unicode.utf8.char \nlocal used_encodings = {}\n\n-- load inputenc encoding file\nlocal function load_encfiles(f)\n\tlocal file= io.open(f,\"r\")\n\tlocal encodings = file:read(\"*all\")\n\tfile:close()\n\tfor codepoint, licr in encodings:gmatch('DeclareUnicodeCharacter(%b{})(%b{})') do\n\t\tlocal codepoint = codepoint2utf(tonumber(codepoint:sub(2,-2),16))\n\t\tlocal licr= licr:sub(2,-2):gsub('@tabacckludge','')\n\t\tlicrs[licr] = codepoint\n\tend\nend\n\nlocal function sanitize_licr(l)\n\treturn l:gsub(\" (.)\",function(s) if s:match(\"[%a]\") then return \" \"..s else return s end end):sub(2,-2)\nend\n\nlocal load_enc = function(enc)\n  -- use default encodings if used doesn't provide one\n  enc = enc or  {\"T1\",\"T2A\",\"T2B\",\"T2C\",\"T3\",\"T5\", \"LGR\"}\n\tfor _,e in pairs(enc) do\n\t\tlocal filename = e:lower() .. \"enc.dfu\"\n    -- don't process an enc file multiple times\n    if not used_encodings[filename] then\n      local dfufile = kpse.find_file(filename)\n      if dfufile then\n        load_encfiles(dfufile)\n      end\n    end\n    used_encodings[filename] = true\n\tend\nend\n\n\n\nlocal cache = {}\n\nlocal get_utf8 = function(input)\n\tlocal output = input:gsub('\\\\IeC[%s]*(%b{})',function(iec)\n    -- remove \\protect commands \n    local iec = iec:gsub(\"\\\\protect%s*\", \"\")\n\t\tlocal code = cache[iec] or licrs[sanitize_licr(iec)] or '\\\\IeC '..iec\n\t\t-- print(iec, code)\n\t\tcache[iec] = code\n\t\treturn code\n\tend)\n\treturn output\nend\n\n\n-- parse the idx file produced by tex4ht\n-- it replaces the document page numbers by index entry number\n-- each index entry can then link to place in the HTML file where the\n-- \\index command had been used\n\nlocal parse_idx = function(content)\n  -- index entry number\n  local current_entry = 0\n  -- map between index entry number and corresponding HTML file and destination\n  local map = {}\n  local buffer = {}\n\n  for line in content:gmatch(\"([^\\n]+)\") do\n    if line:match(\"^\\\\beforeentry\") then\n      -- increment index entry number\n      current_entry = current_entry + 1\n      local file, dest, locator = line:match(\"\\\\beforeentry%s*{(.-)}{(.-)}{(.-)}\")\n      -- if the third argument to \\beforeentry is not empty, \n      -- use it as a index entry locator instead of the index counter\n      if locator and locator == \"\" then locator = nil end\n      map[current_entry] = {file = file, dest = dest, locator = locator}\n    elseif line:match(\"^\\\\indexentry\") then\n      -- replace the page number with the current\n      -- index entry number\n      local result = line:gsub(\"%b{}$\", \"{\"..current_entry ..\"}\")\n      buffer[#buffer+1] = get_utf8(result)\n    else\n      buffer[#buffer+1] = line\n    end\n  end\n  -- return table with page to dest map and updated idx file\n  return {map = map, idx = table.concat(buffer, \"\\n\")}\nend\n\n\nlocal previous\n-- replace numbers in .ind file with links back to text\nlocal function replace_index_pages(rest, entries)\n  -- keep track of the previous page number\n  local count = 0\n  local delete_coma = false\n  return rest:gsub(\"(%s*%-*%s*)(,?%s*)(%{?)(%[?)(%d+)(%]?)(%}?)\", function(dash, coma, lbrace, lbracket, page, rbracket, rbrace)\n    if lbracket == \"[\" and rbracket == \"]\" then\n      -- don't process numbers in brackets, they are not page numbers\n      return nil\n    end\n    local entry = entries[tonumber(page)]\n    count = count + 1\n    if entry then\n      page = entry.locator or page\n      if delete_coma then\n        -- if the coma was marked for deletion, remove it. this may happen after line breaks in the index\n        coma = \"\"\n      end\n      -- if the page number is the same as the previous one, don't create a link\n      -- this can happen when we use section numbers as locators. for example, \n      -- we could get 1.1 -- 1.1, 1.1, so we want to keep only the first one\n      if page == previous then\n        previous = page\n        -- if the first page number on a line is the same as the previous one, we need to delete the coma,\n        -- otherwise the coma will be left in the output\n        if count == 1 then\n          delete_coma = true\n        end\n        return \"\"\n      else\n        previous = page\n        -- don't forget to reset the delete_coma flag after page change\n        delete_coma = false\n        -- construct link to the index entry\n        return dash .. coma.. lbrace ..  \"\\\\Link[\" .. entry.file ..\"]{\".. entry.dest ..\"}{}\" ..  page ..\"\\\\EndLink{}\" .. rbrace\n      end\n    else\n      return dash .. coma .. lbrace .. lbracket .. page .. rbracket .. rbrace\n    end\n end)\nend\n\nlocal function fix_subitems(start, rest)\n  -- in xindex, subentries start with a comma, so if the subentry itself is number, it would be mistaken for the page number\n  -- the start should contain just \\subitem -\\\n  if start:match(\"%s*\\\\subitem %-\\\\$\") then\n    -- the keyword in this case is the first item in the rest\n    local keyword, newrest = rest:match(\"(,?[^,]+,)(.+)\")\n    if keyword and newrest then\n      -- join the extracted keyword with the start, newrest should contain only actual page numbers\n      return start .. keyword, newrest\n    end\n  end\n  return start, rest\nend\n\n-- replace page numbers in the ind file with hyperlinks\nlocal fix_idx_pages = function(content, idxobj)\n  local buffer = {}\n  local entries = idxobj.map\n  for  line in content:gmatch(\"([^\\n]+)\")  do\n    local line, count = line:gsub(\"(%s*\\\\%a+[^%[^,]+)(.+)$\", function(start,rest)\n      -- reset the previous page number\n      previous = nil\n      start, rest = fix_subitems(start, rest)\n      -- there is a problem when index term itself contains numbers, like Bible verses (1:2),\n      -- because they will be detected as page numbers too. I cannot find a good solution \n      -- that wouldn't break something else.\n      -- There can be also commands with numbers in braces. These numbers in braces will be ignored, \n      -- as they may be not page numbers\n      return start .. replace_index_pages(rest, entries)    end)\n    -- longer index entries may be broken over several lines, in that case, we need to process only numbers\n    if count == 0 then\n      line = line:gsub(\"(%s*%d+.+)\", function(rest)\n        return replace_index_pages(rest, entries)\n      end)\n    end\n    buffer[#buffer+1] = line\n  end\n  return table.concat(buffer, \"\\n\")\nend\n\n-- prepare the .idx file produced by tex4ht\n-- for use with Xindy or Makeindex\nlocal prepare_idx = function(filename)\n  local f = io.open(filename, \"r\")\n  if not f then return nil, \"Cannot open file :\".. tostring(filename) end\n  local content = f:read(\"*all\")\n  local idx = parse_idx(content)\n  local idxname = os.tmpname()\n  local f = io.open(idxname, \"w\")\n  f:write(idx.idx)\n  f:close()\n  -- return the object with mapping between dummy page numbers \n  -- and link destinations in the files, and the temporary .idx file\n  -- these can be used for the processing with the index processor\n  return idx, idxname\nend\n\n-- add links to a index file\nlocal process_index = function(indname, idx)\n  local f = io.open(indname,  \"r\")\n  if not f then return  nil, \"Cannot open .ind file: \" .. tostring(indname) end\n  local content = f:read(\"*all\")\n  f:close()\n\n  local newcontent = fix_idx_pages(content, idx)\n  local f = io.open(indname,\"w\")\n  f:write(newcontent)\n  f:close()\n  return true\nend\n\nlocal get_idxname = function(par)\n  return par.idxfile or par.input .. \".idx\"\nend\n\nlocal prepare_tmp_idx = function(par)\n  par.idxfile = mkutils.file_in_builddir(get_idxname(par), par)\n  if not par.idxfile or not mkutils.file_exists(par.idxfile) then return nil, \"Cannot load idx file \" .. (par.idxfile or \"''\") end\n  -- construct the .ind name, based on the .idx name\n  par.indfile = par.indfile or par.idxfile:gsub(\"idx$\", \"ind\")\n  load_enc()\n  -- save hyperlinks and clean the .idx file\n  local idxdata, newidxfile = prepare_idx(par.idxfile)\n  if not idxdata then\n    -- if the prepare_idx function returns nil, the second reuturned value contains error msg\n    return nil, newidxfile\n  end\n  return  newidxfile, idxdata\nend\n\n\nlocal splitindex = function(par)\n  local files = {}\n  local idxfiles = {}\n  local buffer \n  local idxfile = get_idxname(par)\n  if not idxfile or not mkutils.file_exists(idxfile) then return nil, \"Cannot load idx file \" .. (idxfile or \"''\") end\n  for line in io.lines(idxfile) do\n    local file = line:match(\"indexentry%[(.-)%]\")\n    if file then\n      -- generate idx name for the current output file\n      file =  par.input .. \"-\" ..file .. \".idx\"\n      local current = files[file] or {}\n      -- remove file name from the index entry\n      local indexentry = line:gsub(\"indexentry%[.-%]\", \"indexentry\")\n      -- save the index entry and preseding line to the current buffer\n      table.insert(current, buffer)\n      table.insert(current, indexentry)\n      files[file] = current\n    end\n    -- \n    buffer = line\n  end\n  -- save idx files\n  for filename, contents in pairs(files) do\n    log:info(\"Saving split index file: \" .. filename)\n    idxfiles[#idxfiles+1] = filename\n    local f = io.open(filename, \"w\")\n    f:write(table.concat(contents, \"\\n\"))\n    f:close()\n  end\n  return idxfiles\nend\n\nlocal function run_indexing_command (command, par)\n  -- detect command name from the command. It will be the first word\n  local cmd_name = command:match(\"^[%a]+\") or \"indexing\"\n  local xindylog  = logging.new(cmd_name)\n  -- support split index\n  local subindexes = splitindex(par) or {}\n  if #subindexes > 0 then\n    -- call the command again on all files produced by splitindex\n    for _, subindex in ipairs(subindexes) do\n      -- make copy of the parameters\n      local t = {}\n      for k,v in pairs(par) do t[k] = v end\n      t.idxfile = subindex\n      run_indexing_command(command, t)\n    end\n    return nil\n  end\n  local newidxfile, idxdata = prepare_tmp_idx(par)\n  if not newidxfile then\n    -- the idxdata will contain error message in the case of error\n    xindylog:warning(idxdata)\n    return false\n  end\n  par.newidxfile = newidxfile\n  xindylog:debug(\"Prepared temporary idx file: \", newidxfile)\n  -- prepare modules\n  local xindy_call = command % par\n  xindylog:info(xindy_call)\n  local status = mkutils.execute(xindy_call)\n  -- insert correct links to the index\n  local status, msg = process_index(par.indfile, idxdata)\n  if not status then xindylog:warning(msg) end\n  -- remove the temporary idx file\n  os.remove(newidxfile)\n  -- null the indfile, it is necessary in order to support\n  -- multiple indices\n  par.indfile = nil\nend\n\n\nM.get_utf8 = get_utf8\nM.load_enc = load_enc\nM.parse_idx = parse_idx\nM.fix_idx_pages = fix_idx_pages\nM.prepare_idx = prepare_idx\nM.process_index = process_index\nM.prepare_tmp_idx = prepare_tmp_idx\nM.run_indexing_command = run_indexing_command\nreturn M\n"
  },
  {
    "path": "make4ht-lib.lua",
    "content": "-- Simple make system for tex4ht\n--kpse.set_program_name(\"luatex\")\n-- module(...,package.seeall)\nlocal m = {}\nlocal log = logging.new \"make4ht-lib\"\n\nMake = {}\n--Make.params = {}\nMake.build_seq = {}\n-- Patterns for matching output filenames\nMake.matches = {}\nMake.image_patterns = {}\nMake.run_count = {}\n\nMake.add = function(self,name,fn,par,rep)\n\tlocal par = par or {}\n\tself.params = self.params or {}\n\tMake[name] = function(self,p,typ)\n\t\tlocal params = {}\n\t\tfor k,v in pairs(self.params) do params[k] = v end\n\t\tfor k,v in pairs(par) do params[k] = v; log:info(\"setting param \"..k) end\n\t\tlocal typ = typ or \"make\"\n\t\tlocal p = p or {}\n\t\tlocal fn = fn\n\t\tfor k,v in pairs(p) do\n\t\t\tparams[k]=v\n\t\t\tlog:info(\"Adding: \",k,v)\n\t\tend\n\t\t-- print( fn % params)\n\t\tlocal command = {\n\t\t\tname=name,\n\t\t\ttype=typ,\n\t\t\tcommand = fn,\n\t\t\tparams = params,\n\t\t\trepetition = rep\n\t\t}\n\t\ttable.insert(self.build_seq,command)\n\tend\nend\n\nMake.length = function(self)\n\treturn #self.build_seq\nend\n\nMake.match = function(self, pattern, command, params) \n\tlocal params = params or {}\n\ttable.insert(self.matches,{pattern = pattern, command = command, params = params})\nend\n\nMake.run_command = function(self,filename,s)\n\tlocal command = s.command\n\tlocal params  = s.params\n\tparams[\"filename\"] = filename\n\tlog:info(\"parse_lg process file: \"..filename)\n\t--for k,v in pairs(params) do print(k..\": \"..v) end\n\tif type(command) == \"function\" then\n\t\treturn command(filename,params)\n\telseif type(command) == \"string\" then\n\t\tlocal run = command % params\n\t\tlog:info(\"Execute: \" .. run)\n    return mkutils.execute(run)\n\tend\n\treturn false, \"parse_lg: Command is not string or function\"\nend\n\nMake.image = function(self, pattern, command, params)\n\tlocal tab = {\n\t\tpattern = pattern,\n\t\tcommand = command,\n\t\tparams  = params\n\t}\n\ttable.insert(self.image_patterns, tab)\nend\n\nMake.image_convert =  function(self, images)\n\tlocal image_patterns = self.image_patterns or {}\n\tfor i, r in pairs(image_patterns) do\n\t\tlocal p = self.params or {}\n\t\tlocal v = r.params or {}\n\t\tfor k,v in pairs(v) do\n\t\t\tp[k]= v\n\t\tend\n\t\timage_patterns[i].params = p\n\tend\n\tfor _,i in ipairs(images) do\n\t\tlocal output = i.output\n\t\tfor _, x in ipairs(image_patterns) do\n\t\t\tlocal pattern = x.pattern\n\t\t\tif output:match(pattern) then\n\t\t\t\tlocal command = x.command\n\t\t\t\tlocal p = x.params or {}\n\t\t\t\tp.output = output\n\t\t\t\tp.page= i.page\n\t\t\t\tp.source = i.source\n\t\t\t\tif type(command) == \"function\" then\n\t\t\t\t\tcommand(p)\n\t\t\t\telseif type(command) == \"string\" then\n\t\t\t\t\tlocal c = command % p\n\t\t\t\t\tlog:info(\"Make4ht convert: \"..c)\n\t\t\t\t\tmkutils.execute(c)\n\t\t\t\tend\n\t\t\t\tbreak\n\t\t\tend\n\t\tend\n\tend\nend\n\nMake.file_matches = function(self, files)\n\tlocal statuses = {}\n\t-- First make params for all matchers\n\tfor k,v in ipairs(self.matches) do\n\t\tlocal v = self.matches[k].params or {}\n\t\tlocal p = self.params or {}\n\t\tfor i,j in pairs(p) do\n\t\t\tv[i] = j\n\t\tend\n\t\tself.matches[k].params = v\n\tend\n\t-- Loop over files, run command on matched\n\tfor _, file in ipairs(files)do\n\t\tstatuses[file] = {}\n\t\tfor _, s in ipairs(self.matches) do\n\t\t\tlocal pattern= s.pattern\n\t\t\tif file:match(pattern) then \n\t\t\t\tlocal status, msg = self:run_command(file,s)\n\t\t\t\tmsg = msg or \"No message given\"\n\t\t\t\ttable.insert(statuses[file],status)\n\t\t\t\tif status == false then\n\t\t\t\t\tlog:info(msg)\n\t\t\t\t\tbreak\n\t\t\t\tend\n\t\t\tend\n\t\tend\n\tend\n\treturn statuses\nend\n\n-- add files from the mk4 file\n-- we must add them to the table generated from the lg file, so they can be processed later\n--\nMake.add_file = function(self, filename)\n  -- self.lgfile should be present, as it is created once the lg_file was parsed for the first time\n  local lg = self.lgfile or {}\n  local files = lg.files or {}\n  -- run filters on the file\n  local filtertable = {filename}\n  -- should we care about return status?\n  self:file_matches(filtertable)\n  -- break if the file is present already \n  -- start at the end, it it was added by a build file, the file will be likely at the end\n  for i = #files,1,-1  do \n    if files[i] == filename then return false, \"File was already added\" end\n  end\n  -- save the added file to the lg_file\n  table.insert(lg.files, filename)\n  self.lg = lg\nend\n\nMake.run = function(self) \n\tlocal return_codes = {}\n  local params = self.params or {}\n\tfor _,v in ipairs(self.build_seq) do\n\t\t--print(\"sekvence: \"..v.name)\n\t\tfor p,n in pairs(v.params) do params[p] = n end\n\t\t--for c,_ in pairs(params) do print(\"build param: \"..c) end\n\t\tif type(v.command)==\"function\" then \n\t\t\ttable.insert(return_codes,{name=v.name,status = v.command(params)})\n\t\telseif type(v.command) ==\"string\" then\n\t\t\tlocal command = v.command % params\n\t\t\t-- Some commands should be executed only limited times, typicaly once\n\t\t\t-- tex4ht or t4ht for example\n\t\t\tlocal run_count = self.run_count[v.command] or 0\n\t\t\trun_count = run_count + 1\n\t\t\tself.run_count[v.command] = run_count\n\t\t\tlocal repetition = v.repetition\n\t\t\tif repetition and run_count > repetition then \n\t\t\t\tlog:warning (command ..\" can be executed only \"..repetition ..\"x\")\n\t\t\telse\n\t\t\t  log:info(\"executing: \" .. command)\n        local status = mkutils.execute(command)\n\t\t\t  table.insert(return_codes,{name=v.name,status=status})\n\t\t\tend\n\t\telse\n\t\t\tlog:warning(\"Unknown command type, must be string or function - \" ..v.name..\": \"..type(v.command))\n\t\tend\n\t\tlocal correct_exit = params.correct_exit or nil\n\t\tif correct_exit then\n\t\t\tlocal last_return = return_codes[#return_codes] or {}\n\t\t\tlocal current_status = last_return.status or 0\n\t\t\tif current_status ~= correct_exit then\n\t\t\t\tlocal last_name = last_return.name or \"unknown\"\n\t\t\t\tlog:fatal(\"Fatal error. Command \"..last_name ..\" returned exit code \"..current_status)\n\t\t\t\tos.exit(1)\n\t\t\tend\n\t\tend\n\tend\n  local lgfile = params.input and params.input .. \".lg\" or nil\n  if params.builddir~=\"\" then \n    lgfile = params.builddir .. \"/\" .. lgfile\n  end\n\tif lgfile then\n   \tself.lgfile = self.lgfile or mkutils.parse_lg(lgfile, params.builddir)\n    local lg = self.lgfile\n\t\t-- First convert images from lg files\n\t\tself:image_convert(lg[\"images\"])\n\t\t-- Then run file matchers on lg files and converted images\n\t\tlocal files = lg[\"files\"]\n\t\tfor _,v in ipairs(lg[\"images\"]) do \n\t\t\tlocal v = v.output\n\t\t\t-- print(v)\n\t\t\ttable.insert(files,v) \n\t\tend\n\t\tself:file_matches(files)\n\telse\n\t\tlog:warning(\"No lg file. tex4ht run failed?\")\n\tend\n\treturn return_codes\nend\n\nm.Make = Make\n\nreturn m\n\n--[[Make:add(\"hello\", \"hello ${world}\", {world = \"world\"})\nMake:add(\"ajaj\", \"ajaj\")\nMake:hello()\nMake:hello{world=\"světe\"}\nMake:hello()\nMake:run()\n--]]\n"
  },
  {
    "path": "make4ht-logging.lua",
    "content": "-- logging system for make4ht\n-- inspired by https://github.com/rxi/log.lua\nlocal logging = {}\n\nlocal levels = {}\n-- level of bugs that should be shown\n-- enable querying of current log level\nlogging.show_level = 1\nlocal max_width = 0\nlocal max_status = 0\n\nlogging.use_colors = true\n\nlogging.modes = {\n  {name = \"debug\", color = 34},\n  {name = \"info\", color = 32},\n  {name = \"status\", color = 37},\n  {name = \"warning\", color = 33}, \n  {name = \"error\", color = 31, status = 1},\n  {name = \"fatal\", color = 35, status = 2}\n}\n\n-- prepare table with mapping between mode names and corresponding levels\n\nfunction logging.prepare_levels(modes)\n  local modes = modes or logging.modes\n  logging.modes = modes\n  for level, mode in ipairs(modes) do\n    levels[mode.name] = level\n    mode.level = level\n    max_width = math.max(string.len(mode.name), max_width)\n  end\nend\n\n-- the logging level is set once\nfunction logging.set_level(name)\n  local level = levels[name] or 1\n  logging.show_level = level\nend\n\nfunction logging.print_msg(header, message, color)\n  local color = color or 0\n  -- use format for collors depending on the use_colors option\n  local header = \"[\" .. header .. \"]\"\n  local color_format =  logging.use_colors and string.format(\"\\27[%im%%s\\27[0m%%s\", color) or \"%s%s\"\n  -- the padding is maximal mode name width + brackets + space\n  local padded_header = string.format(\"%-\".. max_width + 3 .. \"s\", header)\n  print(string.format(color_format, padded_header, message))\nend\n\n-- \nfunction logging.new(module)\n  local obj = {\n    module = module,\n    output = function(self, output)\n      -- used for printing of output of commands\n      if logging.show_level <= (levels[\"debug\"] or 1) then\n        print(output)\n      end\n    end\n  }\n  obj.__index = obj\n  -- make a function for each mode\n  for _, mode in ipairs(logging.modes) do\n    local name = mode.name\n    local color = mode.color\n    local status = mode.status or 0\n    obj[name] = function(self, ...)\n      -- set make4ht exit status\n      max_status = math.max(status, max_status)\n      -- max width is saved in logging.prepare_levels\n      if mode.level >= logging.show_level then\n        -- support variable number of parameters\n        local table_with_holes = table.pack(...) \n        local table_without_holes = {}\n        -- trick used to support the nil values in the varargs\n        -- https://stackoverflow.com/a/7186820/2467963\n        for i= 1, table_with_holes.n do\n          table.insert(table_without_holes, tostring(table_with_holes[i]) or \"\")\n        end\n        local msg = table.concat(table_without_holes, \"\\t\")\n        logging.print_msg(string.upper(name),  string.format(\"%s: %s\", self.module, msg), color)\n      end\n    end\n  end\n  return setmetatable({}, obj)\n\nend\n\n-- exit make4ht with maximal error status\nfunction logging.exit_status()\n  os.exit(max_status)\nend\n\n\n-- prepare default levels\nlogging.prepare_levels()\n\n-- for _, mode in ipairs(logging.modes) do\n--   logging.print_msg(mode.name,\"xxxx\",  mode.color)\n-- end\n\n-- local cls = logging.new(\"sample\")\n-- cls:warning(\"hello\")\n-- cls:error(\"world\")\n-- cls:info(\"set new level\")\n-- logging.set_level(\"error\")\n-- cls:info(\"level set\")\n-- cls:error(\"just print the error\")\n--\n\n\nreturn logging\n  \n\n\n"
  },
  {
    "path": "make4ht-xtpipes.lua",
    "content": "local M = {}\n\nlocal mkutils = require \"mkutils\"\n\nlocal log = logging.new \"xtpipes\"\n-- find if tex4ht.jar exists in a path\nlocal function find_tex4ht_jar(path)\n  local jar_file = path .. \"/tex4ht/bin/tex4ht.jar\"\n  return mkutils.file_exists(jar_file)\nend\n\n-- return value of TEXMFROOT variable if it exists and if tex4ht.jar can be located inside\nlocal function get_texmfroot()\n  -- user can set TEXMFROOT environmental variable as the last resort\n  local root_directories = {kpse.var_value(\"TEXMFROOT\"), kpse.var_value(\"TEXMFDIST\"), os.getenv(\"TEXMFROOT\")}\n  for _, root in ipairs(root_directories) do\n    if root then\n      if find_tex4ht_jar(root) then return root end\n      -- TeX live locates files in texmf-dist subdirectory, but Miktex doesn't\n      local path = root .. \"/texmf-dist\"\n      if find_tex4ht_jar(path) then return path end\n    end\n  end\nend\n\n-- Miktex doesn't seem to set TeX variables such as TEXMFROOT\n-- we will try to find the TeX root using trick with locating package in TeX root\n-- there is a danger that this file is located in TEXMFHOME, the location will fail then\nlocal function find_texmfroot()\n  local tex4ht_path = kpse.find_file(\"tex4ht.sty\")\n  if tex4ht_path then\n    local path = tex4ht_path:gsub(\"/tex/generic/tex4ht/tex4ht.sty$\",\"\")\n    if find_tex4ht_jar(path) then return path end\n  end\n  return nil\nend\n\nfunction M.get_selfautoparent()\n  return get_texmfroot() or find_texmfroot()\nend\n\nlocal function replace_lg_file()\n  -- xtpipes expects the lg file to be placed in the current working dir, but with the --build option,\n  -- it is saved in the build dir. So we need to copy that file to the place where it is expected.\n  local params = Make.params\n  local basename = params.input\n  local lg_name = basename .. \".lg\"\n  local lg_in_builddir = mkutils.file_in_builddir(lg_name,params)\n  if lg_name ~= lg_in_builddir and mkutils.file_exists(lg_in_builddir) then\n    log:info(\"Creating temporary lg_file\", lg_name)\n    mkutils.cp(lg_in_builddir, lg_name)\n    return true, lg_name\n  end\n  -- don't copy the Lg file if --build_fir option isn't used\n  return false, lg_name\nend\n\nfunction M.get_xtpipes(selfautoparent)\n  -- make pattern using TeX distro path\n  local pattern = string.format('java -classpath \"%s/tex4ht/bin/tex4ht.jar\" xtpipes -i \"%s/tex4ht/xtpipes/\" -o \"${outputfile}\" \"${filename}\"', selfautoparent, selfautoparent)\n  -- call xtpipes on a temporary file\n  local matchfunction =  function(filename)\n    -- move the matched file to a temporary file, xtpipes will write it back to the original file\n    local basename = mkutils.remove_extension(filename)\n    local tmpfile = basename ..\".tmp\"\n    local remove, lg_filename = replace_lg_file()\n    mkutils.mv(filename, tmpfile)\n    local command = pattern % {filename = tmpfile, outputfile = filename}\n    log:info(\"execute: \" ..command)\n    local status, output = mkutils.execute(command)\n    -- remove temporary lg file if it was created\n    if remove then os.remove(lg_filename) end\n    if status > 0 then\n      -- if xtpipes failed to process the file, it may mean that it was bad-formed xml\n      -- we can try to make it well-formed using Tidy\n      local tidy_command = 'tidy -utf8 -xml -asxml -q -o \"${filename}\" \"${tmpfile}\"' % {tmpfile = tmpfile, filename = filename}\n      log:warning(\"Xtpipes failed\")\n      -- show_level 1 is debug mode, which prints command output as well\n      -- we need this condition to prevent multiple instances of the output\n      if logging.show_level > 1 then\n        print(output)\n      end\n      log:warning(\"Trying HTML tidy\")\n      log:debug(tidy_command)\n      local status, output = os.execute(tidy_command)\n      if status > 0 then\n        -- if tidy failed as well, just use the original file\n        -- it will probably produce corrupted ODT file though\n        log:warning(\"Tidy failed as well\")\n        if logging.show_level > 1 then\n          print(output)\n        end\n        mkutils.mv(tmpfile, filename)\n      end\n    end\n  end\n  return matchfunction\nend\n\n-- This function moves the last added file matching function to the first place\n-- in the execution order. This ensures that filters are executed in the\n-- correct order.\nfunction M.move_matches(make)\n  local matches = make.matches\n  local last = matches[#matches]\n  table.insert(matches, 1, last)\n  matches[#matches] = nil\nend\n\nM.get_texmfroot = get_texmfroot\nM.find_texmfroot = find_texmfroot\nM.find_tex4ht_jar = find_tex4ht_jar\nreturn M\n"
  },
  {
    "path": "mkparams.lua",
    "content": "local lapp = require \"lapp-mk4\"\nlocal mkutils = require \"mkutils\"\nlocal m = {} -- use ugly module system for new lua versions support\nlocal log = logging.new \"mkparams\"\n\n-- these two variables will be used in the version number\n-- progname will be set in get_args\nm.progname = \"make4ht\"\n-- set the version number before call to process_args()\nm.version_number = \"v0.1\"\n\nm.optiontext =  [[\n${progname} - build system for TeX4ht\nUsage:\n${progname} [options] filename [\"tex4ht.sty op.\"] [\"tex4ht op.\"] [\"t4ht op\"] [\"latex op\"]\n\nAvailable options:\n  -a,--loglevel (default status) Set log level.\n                possible values: debug, info, status, warning, error, fatal\n  -b,--backend (default tex4ht) Backend used for xml generation. \n                possible values: tex4ht or lua4ht\n  -c,--config (default xhtml) Custom config file\n  -d,--output-dir (default nil)  Output directory\n  -B,--build-dir (default nil)  Build directory\n  -e,--build-file (default nil)  If build file is different than `filename`.mk4\n  -f,--format  (default html5)  Output file format\n  -h,--help  Display this message\n  -j,--jobname (default nil)  Set the jobname\n  -l,--lua  Use lualatex for document compilation\n  -m,--mode (default default) Switch which can be used in the makefile \n  -n,--no-tex4ht Disable dvi file processing with the tex4ht command\n  -s,--shell-escape Enables running external programs from LaTeX\n  -u,--utf8  [obsolete] The document is generated in UTF8 encoding by default\n  -v,--version  Display version number\n  -x,--xetex Use xelatex for document compilation\n]]\n\n-- test if the current command line argument should be passed to tex4ht, t4ht or latex\nlocal function is_escapedargument(arg)\n  -- we need to ignore make4ht options which can be used without filename, ie --version and --help\n  local ignored_options = {[\"-h\"]=true, [\"--help\"]=true, [\"-v\"] = true, [\"--version\"]=true}\n  if ignored_options[arg] then return false end\n  -- in other cases, match if the argument starts with \"-\" character\n  return arg:match(\"^%-.+\")\nend\nlocal function get_args(parameters, optiontext)\n\tlocal parameters = parameters or {}\n\tparameters.progname = parameters.progname or \"make4ht\"\n  parameters.issue_tracker = parameters.issue_tracker or \"https://github.com/michal-h21/make4ht/issues\"\n\tparameters.postparams = parameters.postparams or \"\"\n\tlocal optiontext = optiontext or m.optiontext\n\tparameters.postfile = parameters.postfile or \"\"\n\toptiontext = optiontext .. parameters.postparams ..[[  <filename> (string) Input file name\n \nPositional optional arguments:\n  [\"tex4ht.sty op.\"]  Additional parameters for tex4ht.sty\n  [\"tex4ht op.\"]      Options for tex4ht command\n  [\"t4ht op\"]         Options for t4ht command\n  [\"latex op\"]        Additional options for LaTeX\n\nDocumentation:                  https://tug.org/applications/tex4ht/mn.html\nIssue tracker for tex4ht bugs:  https://puszcza.gnu.org.ua/bugs/?group=tex4ht\nIssue tracker for ${progname} bugs: ${issue_tracker}\n  ]] .. parameters.postfile \n  -- we can pass arguments for tex4ht and t4ht after filename, but it will confuse lapp, thinking that these \n  -- options are for make4ht. this may result in execution error or wrong option parsing\n  -- as fix, add a space before options at the end (we need to stop to add spaces as soon as we find\n  -- nonempty string which doesn't start with - it will be filename or tex4ht.sty options\n  if #arg > 1 then -- do this only if more than one argument is used\n    for i=#arg,1,-1 do\n      local current = arg[i]\n      if is_escapedargument(arg[i]) then\n        arg[i] = \" \".. arg[i]\n      -- empty parameter\n      elseif current == \"\" then\n      else\n        break\n      end\n    end\n  end\n\t--print(\"--------------\\n\" .. optiontext ..\"--------------\\n\")\n\treturn lapp(optiontext % parameters)\nend\n\n--- get outptut file format and list of extensions from --format option string\nlocal function get_format_extensions(format_string)\n  local format, rest = format_string:match(\"^([a-zA-Z0-9]+)(.*)\")\n  local extensions = {}\n  -- it is possible to pass only the extensions\n  rest = rest or format_string\n  rest:gsub(\"([%+%-])([^%+^%-]+)\",function(typ, name)\n    table.insert(extensions, {type = typ, name = name})\n  end)\n  return format, extensions\nend\n\n\n-- try to make safe filename \nlocal function escape_filename(input)\n  -- quoting don't work on Windows, so we will just\n  if os.type == \"windows\" then\n    return '\"' .. input .. '\"'\n  else\n    -- single quotes are safe in Unix\n    return \"'\" .. input .. \"'\"\n  end\nend\n\n-- detect if user specified -jobname in arguments to the TeX engine\n-- or used the --jobname option for make4ht\nlocal function handle_jobname(input, args)\n  -- parameters to the TeX engine\n  local latex_params = {}\n  local latex_cli_params = args[4] or \"\"\n  -- use the jobname as input name if it is specified\n  local jobname = args.jobname ~=\"nil\" and args.jobname or nil\n  if jobname or not latex_cli_params:match(\"%-jobname\") then\n    -- prefer jobname over input\n    input = jobname or input\n    -- we must strip out directories from jobname when full path to document is given\n    input = input:match(\"([^%/^%\\\\]+)$\")\n    -- input also cannot contain spaces, replace them with underscores\n    input = input:gsub(\"%s\", \"_\")\n    table.insert(latex_params,\"-jobname=\".. escape_filename(input))\n  else\n    -- when user specifies -jobname, we must change name of the input file,\n    -- in order to be able to process correct dvi file with tex4ht and t4ht\n    local newinput\n    -- first contains quotation character or first character of the name\n    local first, rest = latex_cli_params:match(\"%-jobname%s*=?%s*(.)(.*)\")\n    if first=='\"' then\n      newinput=rest:match('([^\"]+)')\n    elseif first==\"'\" then\n      newinput=rest:match(\"([^']+)\")\n    elseif type(first)== \"string\" then\n      -- if the jobname is unquoted, it cannot contain space\n      -- join the first character and rest\n      rest = first.. rest\n      newinput = rest:match(\"([^ ]+)\")\n    end\n    if newinput then\n      input = newinput\n    end\n  end\n  -- \n\ttable.insert(latex_params, latex_cli_params)\n  return latex_params, input\nend\n\nlocal function tex_file_not_exits(tex_file)\n  -- try to find the input file, return false if we cannot find it\n  return not (kpse.find_file(tex_file, \"tex\") or kpse.find_file(tex_file .. \".tex\", \"tex\"))\nend\n\n-- use standard input instead of file if the filename is just `-`\n-- return the filename and status if it is a tmp name\nlocal function handle_input_file(filename)\n  -- return the original file name if it isn't just dash\n  if filename ~= \"-\" then return filename, false end\n  -- generate the temporary name. the added extension is important\n  local tmp_name = os.tmpname()\n  local contents = io.read(\"*all\")\n  local f = io.open(tmp_name, \"w\")\n  f:write(contents)\n  f:close()\n  return tmp_name, true\nend\n\nlocal function process_args(args)\n\tlocal function get_inserter(args,tb)\n\t\treturn function(key, value)\n\t\t\t--local v = args[key] and value or \"\"\n\t\t\tlocal v = \"\"\n\t\t\tif args[key] then v = value end\n\t\t\ttable.insert(tb,v)\n\t\tend\n\tend\n\n  -- set error log level\n  logging.set_level(args.loglevel)\n  -- the default LaTeX --interaction parameter\n  local interaction = \"batchmode\"\n  if args.loglevel == \"debug\" then\n    interaction = \"errorstopmode\"\n  end\n\n  if args.version ==true then\n    print(string.format(\"%s version %s\", m.progname, m.version_number))\n    os.exit()\n  end\n\n\tlocal outdir = \"\"\n\tlocal packages = \"\"\n\n\tif  args[\"output-dir\"] ~= \"nil\" then\n\t\toutdir =  args[\"output-dir\"]  or \"\"\n\t\toutdir = outdir:gsub('\\\\','/')\n\t\toutdir = outdir:gsub('/$','')\n\tend\n\n\tlocal builddir = \"\"\n\n\tif  args[\"build-dir\"] ~= \"nil\" then\n\t\tbuilddir =  args[\"build-dir\"]  or \"\"\n\t\tbuilddir = builddir:gsub('\\\\','/')\n\t\tbuilddir = builddir:gsub('/$','')\n\tend\n\n  -- make4ht now requires UTF-8 output, because of DOM filters\n  -- numeric entites are expanded to Unicode characters. These\n  -- characters would be displayed incorrectly in 8 bit encodings.\n\n  args.utf8 = true\n\n\tif args.backend == \"lua4ht\" then\n\t\targs.lua = true\n\t\targs.xetex = nil\n\t\targs.utf8 = true\n\t\targs[\"no-tex4ht\"] = true\n\t\tpackages = packages ..\"\\\\RequirePackage{lua4ht}\"\n\tend\n\n\n\tlocal compiler = args.lua and \"dvilualatex\" or args.xetex and \"xelatex --no-pdf\" or \"latex\"\n  local tex_file, is_tmp_file = handle_input_file(args.filename)\n  -- test if the file exists\n  if not is_tmp_file and tex_file_not_exits(tex_file) then\n    log:warning(\"Cannot find input file: \" .. tex_file)\n  end\n\tlocal input = mkutils.remove_extension(tex_file)\n  -- the output file name can be influneced using -jobname parameter passed to the TeX engine\n\tlocal latex_params, input = handle_jobname(input, args) \n\tlocal insert_latex = get_inserter(args,latex_params)\n\tinsert_latex(\"shell-escape\",\"-shell-escape\")\n\t--table.insert(latex_params,args[\"shell-escape\"] and \"-shell-escape\")\n\n\n\tlocal t4sty = args[1] or \"\"\n\t-- test if first option is custom config file\n\tlocal cfg_tmp = t4sty:match(\"([^,^ ]+)\")\n\tif cfg_tmp and cfg_tmp ~= args.config then\n\t\tlocal fn = cfg_tmp..\".cfg\"\n\t\tlocal f = io.open(fn,\"r\")\n\t\tif f then \n\t\t\targs.config = cfg_tmp \n\t\t\tf:close()\n\t\tend\n\tend\n\t--[[if args[1] and args[1] ~= \"\" then \n\tt4sty = args[1] \n\telse\n\t--]]\n\t-- Different behaviour from htlatex\n\tlocal utf = args.utf8 and \",charset=utf-8\" or \"\"\n\tt4sty = args.config .. \",\" .. t4sty .. utf\n\t--end\n\n\tlocal tex4ht = \"\"\n  local dvi= args.xetex and \"xdv\" or \"dvi\"\n\tif args[2] and args[2] ~=\"\" then\n\t\ttex4ht = args[2]\n\telse\n\t\ttex4ht = args.utf8 and \" -cmozhtf -utf8\" or \"\"\n\tend\n  -- set the correct extension for tex4ht if xetex is used\n  if args.xetex then tex4ht = tex4ht .. \" -.xdv\" end\n\n\tlocal t4ht = args[3] or \"\"\n\n\tlocal mode = args.mode or \"default\"\n\n\tlocal build_file = input.. \".mk4\"\n\n\tif args[\"build-file\"] and args[\"build-file\"] ~= \"nil\" then\n\t\tbuild_file = args[\"build-file\"]\n\tend\n\n  local outformat, extensions\n  if args[\"format\"] and arg[\"format\"] ~= \"nil\" then\n    outformat, extensions = get_format_extensions(args[\"format\"])\n  end\n\n\tlocal parameters = {\n\t\thtlatex = compiler\n\t\t,input=input\n        ,tex_file=tex_file\n\t\t,packages=packages\n\t\t,latex_par=table.concat(latex_params,\" \")\n\t\t--,config=ebookutils.remove_extension(args.config)\n\t\t,tex4ht_sty_par=t4sty\n\t\t,tex4ht_par=tex4ht\n\t\t,t4ht_par=t4ht\n\t\t,mode = mode\n    ,dvi = dvi\n    ,build_file = build_file\n    ,output_format = outformat\n    ,extensions = extensions\n    ,is_tmp_file = is_tmp_file\n    ,interaction = interaction\n\t\t--,t4ht_dir_format=t4ht_dir_format\n\t}\n\tif outdir then parameters.outdir = outdir end\n\tif builddir then parameters.builddir = builddir end\n\tlog:info(\"Output dir: \"..outdir)\n\tlog:info(\"Compiler: \"..compiler)\n\tlog:info(\"Latex options: \".. table.concat(latex_params,\" \"))\n\tlog:info(\"tex4ht.sty: \"..t4sty)\n\tlog:info(\"tex4ht: \"..tex4ht)\n\tlog:info(\"build_file: \".. build_file)\n  if outformat~=\"nil\" then\n    log:info(\"Output format: \".. outformat) \n    for _, ex in ipairs(extensions) do\n      log:info(\"Extension: \".. ex.type .. ex.name)\n    end\n  end\n\treturn parameters\nend\nm.get_args = get_args\nm.get_format_extensions = get_format_extensions\nm.process_args = process_args\nreturn m\n"
  },
  {
    "path": "mkutils.lua",
    "content": "module(...,package.seeall)\n\nlocal log = logging.new(\"mkutils\")\n\nlocal make4ht = require(\"make4ht-lib\")\nlocal mkparams = require(\"mkparams\")\nlocal indexing = require(\"make4ht-indexing\")\n--template engine\nfunction interp(s, tab)\n  local tab = tab or {}\n  return (s:gsub('($%b{})', function(w) return tab[w:sub(3, -2)] or w end))\nend\n--print( interp(\"${name} is ${value}\", {name = \"foo\", value = \"bar\"}) )\n\nfunction addProperty(s,prop)\n  if prop ~=nil then\n    return s ..\" \"..prop\n  else\n    return s\n  end\nend\ngetmetatable(\"\").__mod = interp\ngetmetatable(\"\").__add = addProperty \n\n--print( \"${name} is ${value}\" % {name = \"foo\", value = \"bar\"} )\n-- Outputs \"foo is bar\"\n\nfunction is_url(path)\n  return path:match(\"^%a+://\")\nend\n\n\n-- merge two tables recursively\nfunction merge(t1, t2)\n  for k, v in pairs(t2) do\n    if (type(v) == \"table\") and (type(t1[k] or false) == \"table\") then\n      merge(t1[k], t2[k])\n    else\n      t1[k] = v\n    end\n  end\n  return t1\nend\n\nfunction string:split(sep)\n  local sep, fields = sep or \":\", {}\n  local pattern = string.format(\"([^%s]+)\", sep)\n  self:gsub(pattern, function(c) fields[#fields+1] = c end)\n  return fields\nend\n\nfunction remove_extension(path)\n  local found, len, remainder = string.find(path, \"^(.*)%.[^%.]*$\")\n  if found then\n    return remainder\n  else\n    return path\n  end\nend\n\n-- \n-- check if file exists\nfunction file_exists(file)\n  local f = io.open(file, \"rb\")\n  if f then f:close() end\n  return f ~= nil\nend\n\n-- check if Lua module exists\n-- source: https://stackoverflow.com/a/15434737/2467963\nfunction isModuleAvailable(name)\n  if package.loaded[name] then\n    return true\n  else\n    for _, searcher in ipairs(package.searchers or package.loaders) do\n      local loader = searcher(name)\n      if type(loader) == 'function' then\n        package.preload[name] = loader\n        return true\n      end\n    end\n    return false\n  end\nend\n\n\n-- searching for converted images\nfunction parse_lg(filename, builddir)\n  log:info(\"Parse LG\")\n  local dir = builddir~=\"\" and builddir .. \"/\" or \"\"\n  local outputimages,outputfiles,status={},{},nil\n  local fonts, used_fonts = {},{}\n  if not file_exists(filename) then\n    log:warning(\"Cannot read log file: \"..filename)\n  else\n    local usedfiles={}\n    for line in io.lines(filename) do\n      --- needs --- pokus.idv[1] ==> pokus0x.png --- \n      -- line:gsub(\"needs --- (.+?)[([0-9]+) ==> ([%a%d%p%.%-%_]*)\",function(name,page,k) table.insert(outputimages,k)end)\n      line:gsub(\"needs %-%-%- (.+)%[([0-9]+)%] ==> (.*) %-%-%-\",\n      function(file,page,output) \n        local rec = {\n          source=file,\n          page=page,\n          output=dir..output\n        }\n        table.insert(outputimages,rec)\n      end\n      )\n      line:gsub(\"File: (.*)\",  function(x)\n        local k = dir .. x\n        if not file_exists(k) then\n          k = x\n        end\n        if not usedfiles[k] then\n          table.insert(outputfiles,k)\n          usedfiles[k] = true\n        end\n      end)\n      line:gsub(\"htfcss: ([^%s]+)(.*)\",function(k,r)\n        local fields = {}\n        r:gsub(\"[%s]*([^%:]+):[%s]*([^;]+);\",function(c,v)\n          fields[c] = v\n        end)\n        fonts[k] = fields\n      end)\n\n      line:gsub('Font(\"([^\"]+)\",\"([%d]+)\",\"([%d]+)\",\"([%d]+)\"',function(n,s1,s2,s3)\n        table.insert(used_fonts,{n,s1,s2,s3})\n      end)\n\n    end\n    status=true\n  end\n  return {files = outputfiles, images = outputimages},status\nend\n\n\n-- \nlocal cp_func = os.type == \"unix\" and \"cp\" or \"copy\"\n-- maybe it would be better to actually move the files\n-- in reality it isn't.\n-- local cp_func = os.type == \"unix\" and \"mv\" or \"move\"\nfunction cp(src,dest)\n  if is_url(src) then\n    log.info(src .. \" is a URL, will leave as is\")\n    return\n  end\n  if not file_exists(src) then\n    -- try to find file using kpse library if it cannot be found\n    src = kpse.find_file(src) or src\n  end\n  local command = string.format('%s \"%s\" \"%s\"', cp_func, src, dest)\n  if cp_func == \"copy\" then command = command:gsub(\"/\",'\\\\') end\n  log:info(\"Copy: \"..command)\n  if not file_exists(src) then\n    log:error(\"File \" .. src .. \" doesn't exist\")\n  end\n  os.execute(command)\nend\n\nfunction mv(src, dest)\n  local mv_func = os.type == \"unix\" and \"mv \" or \"move \"\n  local command = string.format('%s \"%s\" \"%s\"', mv_func, src, dest)\n  -- fix windows paths\n  if mv_func == \"move\" then command = command:gsub(\"/\",'\\\\') end\n  log:info(\"Move: \".. command)\n  os.execute(command)\nend\n\nfunction delete_dir(path)\n  local cmd = os.type == \"unix\" and \"rm -rd \" or \"rd /s/q \"\n  os.execute(cmd .. path)\nend\n\nlocal used_dir = {}\n\nfunction prepare_path(path)\n  --local dirs = path:split(\"/\")\n  local dirs = {}\n  if path:match(\"^/\") then dirs = {\"\"}\n  elseif path:match(\"^~\") then\n    local home = os.getenv \"HOME\"\n    dirs = home:split \"/\"\n    path = path:gsub(\"^~/\",\"\")\n    table.insert(dirs,1,\"\")\n  end\n  if path:match(\"/$\")then path = path .. \" \" end\n  for _,d in pairs(path:split \"/\") do\n    table.insert(dirs,d)\n  end\n  table.remove(dirs,#dirs)\n  return dirs,table.concat(dirs,\"/\")\nend\n\n-- Find which part of path already exists\n-- and which directories have to be created\nfunction find_directories(dirs, pos)\n  local pos = pos or #dirs\n  -- we tried whole path and no dir exist\n  if pos < 1 then return dirs end\n  local path = \"\"\n  -- in the case of unix absolute path, empty string is inserted in dirs\n  if pos == 1 and dirs[pos] == \"\" then\n    path = \"/\"\n  else\n    path = table.concat(dirs,\"/\", 1,pos) .. \"/\"\n  end\n  if not lfs.chdir(path)  then -- recursion until we succesfully changed dir\n    -- or there are no elements in the dir table\n    return find_directories(dirs,pos - 1)\n  elseif pos ~= #dirs then -- if we succesfully changed dir\n    -- and we have dirs to create\n    local p = {}\n    for i = pos+1, #dirs do\n      table.insert(p, dirs[i])\n    end\n    return p\n  else  -- whole path exists\n    return {}\n  end\nend\n\nfunction mkdirectories(dirs)\n  if type(dirs) ~=\"table\" then\n    return false, \"mkdirectories: dirs is not table\"\n  end\n  local path = \"\"\n  for _,d in ipairs(dirs) do\n    path = path .. d .. \"/\"\n    local stat,msg = lfs.mkdir(path)\n    if not stat then return false, \"makedirectories error: \"..msg end\n  end\n  return true\nend\n\nfunction make_path(path)\n  -- we must create the build dir if it doesn't exist\n  local cwd = lfs.currentdir()\n  -- add dummy /foo dir. it won't be created, but without that, the top-level dir wouldn't be created\n  local parts = mkutils.prepare_path(path .. \"/foo\")\n  local to_create = mkutils.find_directories(parts)\n  mkutils.mkdirectories(to_create)\n  -- change back to the original dir\n  lfs.chdir(cwd)\nend\n\nfunction file_in_builddir(filename, par)\n  if par.builddir and par.builddir ~= \"\" then\n    local newname = par.builddir .. \"/\" .. filename\n    return newname\n  end\n  return filename\nend\n\nfunction copy_filter(src,dest, filter)\n  local src_f=io.open(src,\"rb\")\n  local dst_f=io.open(dest,\"w\")\n  local contents = src_f:read(\"*all\")\n  local filter = filter or function(s) return s end\n  src_f:close()\n  dst_f:write(filter(contents))\n  dst_f:close()\nend\n\n\n\nfunction copy(filename,outfilename)\n  local currdir = lfs.currentdir()\n  if filename == outfilename then return true end\n  local parts, path = prepare_path(outfilename)\n  if not used_dir[path] then \n    local to_create, msg = find_directories(parts)\n    if not to_create then\n      log:warning(msg)\n      return false\n    end\n    used_dir[path] = true\n    local stat, msg = mkdirectories(to_create)\n    if not stat then log:warning(msg) end\n  end\n  lfs.chdir(currdir)\n  cp(filename, path)\n  return true\nend\n\nfunction execute(command)\n  local f = io.popen(command, \"r\")\n  local output = f:read(\"*all\")\n  -- rc will contain return codes of the executed command\n  local rc =  {f:close()}\n  -- the status code is on the third position \n  -- https://stackoverflow.com/a/14031974/2467963\n  local status = rc[3]\n  -- print the command line output only when requested through\n  -- log  level\n  log:output(output)\n  return status, output\nend\n\n-- find the zip command\nfunction find_zip()\n  if io.popen(\"zip -v\",\"r\"):close() then\n    return \"zip\"\n  elseif io.popen(\"miktex-zip -v\",\"r\"):close() then\n    return \"miktex-zip\"\n  end\n  -- we cannot find the zip command\n  return \"zip\"\nend\n\n-- Config loading\nlocal function run(untrusted_code, env)\n  if untrusted_code:byte(1) == 27 then return nil, \"binary bytecode prohibited\" end\n  local untrusted_function = nil\n  untrusted_function, message = load(untrusted_code, nil, \"t\",env)\n  if not untrusted_function then return nil, message end\n  if not setfenv then setfenv = function(a,b) return true end end\n  setfenv(untrusted_function, env)\n  return pcall(untrusted_function)\nend\n\nfunction escape_pattern(str)\n  -- escape all magic characters in the string, so it can be used as a literal pattern\n  return (str:gsub(\"([%(%)%.%%%+%-%*%?%[%]%^%$])\", \"%%%1\"))\nend\n\nlocal main_settings = {}\nmain_settings.fonts = {}\n-- use global environment in the build file\n-- it used to be sandboxed, but it proved not to be useful at all\nlocal env = _G ---{}\n\n-- explicitly enale some functions and modules in the sandbox\n-- Function declarations:\nenv.pairs  = pairs\nenv.ipairs = ipairs\nenv.print  = print\nenv.split  = split\nenv.string = string\nenv.table  = table\nenv.copy   = copy\nenv.tonumber = tonumber\nenv.tostring = tostring\nenv.mkdirectories = mkdirectories\nenv.require = require\nenv.texio  = texio\nenv.type   = type\nenv.lfs    = lfs\nenv.os     = os\nenv.io     = io\nenv.math   = math\nenv.unicode = unicode\nenv.logging = logging\n\n\n-- it is necessary to use the settings table\n-- set in the Make environment by mkutils\nfunction env.set_settings(par)\n  local settings = env.settings\n  for k,v in pairs(par) do\n    settings[k] = v\n  end\nend\n\n-- Add a value to the current settings\nfunction env.settings_add(par)\n  local settings = env.settings\n  for k,v in pairs(par) do\n    local oldval = settings[k] or \"\"\n    settings[k] = oldval .. v\n  end\nend\n\nfunction env.get_filter_settings(name)\n  local settings = env.settings\n  -- local settings = self.params\n  local filters = settings.filter or {}\n  local filter_options = filters[name] or {}\n  return filter_options\nend\n\nfunction env.filter_settings(name)\n  -- local settings = Make.params\n  local settings = env.settings\n  local filters = settings.filter or {}\n  local filter_options = filters[name] or {}\n  return function(par)\n    filters[name] = merge(filter_options, par)\n    settings.filter = filters\n  end\nend\nenv.Font   = function(s)\n  local font_name = s[\"name\"]\n  if not font_name then return nil, \"Cannot find font name\" end\n  env.settings.fonts[font_name] = s\nend\n\nenv.Make   = make4ht.Make\nenv.Make.params = env.settings\nenv.Make:add(\"test\",\"test the variables:  ${tex4ht_sty_par} ${htlatex} ${input} ${config}\")\n\nlocal htlatex = require \"make4ht-htlatex\"\nenv.Make:add(\"htlatex\", htlatex.htlatex\n,{correct_exit=0})\nenv.Make:add(\"httex\", htlatex.httex, {\n  htlatex = \"etex\",\n  correct_exit=0\n})\n\nenv.Make:add(\"latexmk\", function(par)\n  local settings = get_filter_settings \"htlatex\" or {}\n  par.interaction = par.interaction or settings.interaction or \"batchmode\"\n  local command = Make.latex_command \n  -- add \" %O \" after the engine name. it should be filled by latexmk\n  command = command:gsub(\"%s\", \" %%O \", 1)\n  par.expanded = command % par\n  -- quotes in latex_command must be escaped, they cause Latexmk error\n  par.expanded = par.expanded:gsub('\"', '\\\\\"')\n  local newcommand = 'latexmk  -pdf- -ps- -auxdir=${builddir} -outdir=${builddir} -latex=\"${expanded}\" -dvi -jobname=${input} ${tex_file}' % par\n  log:info(\"LaTeX call: \" .. newcommand)\n  os.execute(newcommand)\n  return Make.testlogfile(par)\nend, {correct_exit= 0})\n\n\n\n-- env.Make:add(\"tex4ht\",\"tex4ht ${tex4ht_par} \\\"${input}.${dvi}\\\"\", nil, 1)\nenv.Make:add(\"tex4ht\",function(par)\n  -- detect if svg output is used\n  -- if yes, we need to pass the -g.svg option to tex4ht command\n  -- to support svg images for character pictures\n  local logfile = mkutils.file_in_builddir(par.input .. \".log\", par)\n  if file_exists(logfile) then\n    for line in io.lines(logfile) do\n      local options = line:match(\"TeX4ht package options:(.+)\")\n      if options then\n        log:info(options)\n        if options:match(\"svg\") then\n          par.tex4ht_par = (par.tex4ht_par or \"\") .. \" -g.svg\"\n        end\n        break\n      end\n    end\n  end\n  local cwd = lfs.currentdir()\n  if par.builddir~=\"\" then\n      lfs.chdir(par.builddir)\n  end\n  local command = \"tex4ht ${tex4ht_par} \\\"${input}.${dvi}\\\"\" % par\n  log:info(\"executing: \" .. command)\n  local status, output = execute(command)\n  lfs.chdir(cwd)\n  return status, output\nend\n, nil, 1)\nenv.Make:add(\"t4ht\", function(par)\n    par.ext = \"dvi\"\n    local cwd = lfs.currentdir()\n    if par.builddir ~= \"\" then\n        lfs.chdir(par.builddir)\n    end\n    local command = \"t4ht ${t4ht_par} \\\"${input}.${ext}\\\"\" % par\n    log:info(\"executing: \" .. command)\n    execute(command)\n    lfs.chdir(cwd)\nend\n)\n\nenv.Make:add(\"clean\", function(par)\n  -- remove all functions that process produced files\n  -- we will provide only one function, that remove all of them\n  Make.matches = {}\n  local main_name = mkutils.file_in_builddir( par.input, par)\n  local remove_file = function(filename)\n    if file_exists(filename) then\n      log:info(\"removing file: \" .. filename)\n      os.remove(filename)\n    end\n  end\n  -- try to find if the last converted file was in the ODT format\n  local lg_name =  main_name .. \".lg\"\n  local lg_file = parse_lg(lg_name, par.builddir)\n  local is_odt = false\n  if lg_file and lg_file.files then\n    for _, x in ipairs(lg_file.files) do\n      is_odt = x:match(\"odt$\") or is_odt\n    end\n  end\n  if is_odt then\n    Make:match(\"4om$\",function(filename)\n      -- math temporary file\n      local to_remove = filename:gsub(\"4om$\", \"tmp\")\n      remove_file(to_remove)\n      return false\n    end)\n    Make:match(\"4og$\", remove_file)\n  end\n  Make:match(\"tmp$\", function()\n    -- remove temporary and auxilary files\n    for _,ext in ipairs {\"aux\", \"xref\", \"tmp\", \"4tc\", \"4ct\", \"idv\", \"lg\",\"dvi\", \"log\", \"ncx\", \"idx\", \"ind\"} do\n      remove_file(main_name .. \".\" .. ext)\n    end\n  end)\n  Make:match(\".*\", function(filename, par)\n    -- remove only files that start with the input file basename\n    -- this should prevent removing of images. this also means that\n    -- images shouldn't be names as <filename>-hello.png for example\n    if filename:find(main_name, 1,true) then\n      -- log:info(\"Matched file\", filename)\n      remove_file(filename)\n    end\n  end)\n\nend)\n\n-- enable extension in the config file\n-- the following two functions must be here and not in make4ht-lib.lua\n-- because of the access to env.settings\nenv.Make.enable_extension = function(self,name)\n  table.insert(env.settings.extensions, {type=\"+\", name=name})\nend\n\n-- disable extension in the config file\nenv.Make.disable_extension = function(self,name)\n  table.insert(env.settings.extensions, {type=\"-\", name=name})\nend\n\nfunction load_config(settings, config_name)\n  local settings = settings or main_settings\n  -- the extensions requested from the command line should take precedence over\n  -- extensions enabled in the config file\n  local saved_extensions = settings.extensions\n  settings.extensions = {}\n  env.settings = settings\n  env.mode = settings.mode\n  if config_name and not file_exists(config_name) then\n    config_name = kpse.find_file(config_name, 'texmfscripts') or config_name\n  end\n  local f = io.open(config_name,\"r\")\n  if not f then \n    log:info(\"Cannot open config file\", config_name)\n    return  env\n  end\n  log:info(\"Using build file\", config_name)\n  local code = f:read(\"*all\")\n  local fn, msg = run(code,env)\n  if not fn then log:warning(msg) end\n  assert(fn)\n  -- reload extensions from command line arguments for the \"format\" parameter\n  for _,v in ipairs(saved_extensions) do\n    table.insert(settings.extensions, v)\n  end\n  return env\nend\n\nenv.Make:add(\"xindy\", function(par)\n  local xindylog = logging.new \"xindy\"\n  local settings = get_filter_settings \"xindy\" or {}\n  par.encoding  = settings.encoding or  par.encoding or \"utf8\"\n  par.language = settings.language or par.language or \"english\"\n  local modules = settings.modules or par.modules or {}\n  local t = {}\n  for k,v in ipairs(modules) do\n    xindylog:debug(\"Loading module: \" ..v)\n    t[#t+1] = \"-M \".. v\n  end\n  par.moduleopt = table.concat(t, \" \")\n  return  indexing.run_indexing_command(\"texindy -L ${language} -C ${encoding} ${moduleopt} -o ${indfile} ${newidxfile}\", par)\nend, {})\n\nenv.Make:add(\"makeindex\", function(par)\n  local makeindxcall = \"makeindex ${options} -t ${ilgfile} -o ${indfile} ${newidxfile}\"\n  local settings = get_filter_settings \"makeindex\" or {}\n  par.options = settings.options or par.options  or \"\"\n  par.ilgfile = par.input .. \".ilg\" \n  local status = indexing.run_indexing_command(makeindxcall, par)\n  return status\nend, {})\n\nenv.Make:add(\"xindex\", function(par)\n  local xindex_call = \"xindex -l ${language} ${options} -o ${indfile} ${newidxfile}\"\n  local settings = get_filter_settings \"xindex\" or {}\n  par.options = settings.options or par.options  or \"\"\n  par.language = settings.language or par.language or \"en\"\n  local status = indexing.run_indexing_command(xindex_call, par)\n  return status\nend, {})\n\n\n\nlocal function find_lua_file(name)\n  local extension_path = name:gsub(\"%.\", \"/\") .. \".lua\"\n  return kpse.find_file(extension_path, \"lua\")\nend\n\n-- for the BibLaTeX support\nenv.Make:add(\"biber\", \"biber ${input}\")\nenv.Make:add(\"bibtex\", \"bibtex ${input}\")\nenv.Make:add(\"pythontex\", \"pythontex ${input}\")\n\n--- load the output format plugins\nfunction load_output_format(format_name)\n  local format_library =  \"make4ht.formats.make4ht-\"..format_name\n  local is_format_file = find_lua_file(format_library)\n  if is_format_file then \n    local format = assert(require(format_library))\n    if format then\n      format.prepare_extensions = format.prepare_extensions or function(extensions) return extensions end\n      format.modify_build = format.modify_build or function(make) return make end\n    end\n    return format\n  end\nend\n\n--- Execute the prepare_parameters function in list of extensions\nfunction extensions_prepare_parameters(extensions, parameters)\n  for _, ext in ipairs(extensions) do\n    -- execute the extension only if it contains prepare_parameters function\n    local fn = ext.prepare_parameters\n    if fn then\n      parameters = fn(parameters)\n    end\n  end\n  return parameters\nend\n\n--- Modify the build sequence using extensions\n-- @param extensions list of extensions \n-- @make  Make object\nfunction extensions_modify_build(extensions, make)\n  for _, ext in ipairs(extensions) do\n    local fn = ext.modify_build\n    if fn then\n      make = fn(make)\n    end\n  end\n  return make\nend\n\n\n--- load one extension\n-- @param name  extension name\n-- @param format current output format\nfunction load_extension(name,format)\n  -- first test if the extension exists\n  local extension_library = \"make4ht.extensions.make4ht-ext-\" .. name\n  local is_extension_file = find_lua_file(extension_library)\n  -- don't try to load the extension if it doesn't exist\n  if not is_extension_file then return nil, \"cannot fint extension \" .. name  end\n  local extension = nil\n  local local_extension_path = package.searchpath(extension_library, package.path)\n  if local_extension_path then\n      extension = dofile(local_extension_path)\n  else\n      extension = require(\"make4ht.extensions.make4ht-ext-\".. name)\n  end\n  -- extensions can test if the current output format is supported\n  local test = extension.test\n  if test then\n    if test(format) then \n      return extension\n    end\n    -- if the test fail return nil\n    return nil, \"extension \" .. name .. \" is not supported in the \" .. format .. \" format\"\n  end\n  -- if the extension doesn't provide the test function, we will assume that\n  -- it supports every output format\n  return extension\nend\n\n--- load extensions\n-- @param extensions table created by mkparams.get_format_extensions function\n-- @param format  output type format. extensions may support only certain file\n-- formats\nfunction load_extensions(extensions, format)\n  local module_names = {}\n  local extension_table = {}\n  local extension_sequence = {}\n  -- process the extension table. it contains type field, which can enable or\n  -- diable the extension\n  for _, v in ipairs(extensions) do\n    local enable = v.type == \"+\" and true or nil\n    -- load extenisons in a correct order\n    -- don't load extensions multiple times\n    if enable and not module_names[v.name] then\n      table.insert(extension_sequence, v.name)\n    end\n    -- the last extension request can disable it\n    module_names[v.name] = enable\n  end\n  for _, name in ipairs(extension_sequence) do\n    -- the extension can be inserted into the extension_sequence, but disabled\n    -- later.\n    if module_names[name] == true then\n      local extension, msg= load_extension(name,format)\n      if extension then\n        log:info(\"Load extension\", name)\n        table.insert(extension_table, extension)\n      else\n        log:warning(\"Cannot load extension: \".. name)\n        log:warning(msg)\n      end\n    end\n  end\n  return extension_table\nend\n\n--- add new extensions to a list of loaded extensions\n-- @param added  string with extensions to be added in the form +ext1+ext2\nfunction add_extensions(added, extensions)\n  local _, newextensions = mkparams.get_format_extensions(\"dummyfmt\" .. added)\n  -- insert new extension at the beginning, in order to support disabling using\n  -- the -f option\n  for _, x in ipairs(extensions or {}) do table.insert(newextensions, x) end\n  return newextensions\nend\n\n-- I don't know if this is clean, but settings functions won't be available\n-- for filters and extensions otherwise\nfor k,v in pairs(env) do _G[k] = v end\n"
  },
  {
    "path": "test/dom-test.lua",
    "content": "require \"busted.runner\" ()\nkpse.set_program_name \"luatex\"\n\nlocal dom = require \"luaxml-domobject\"\n\ndescribe(\"Basic DOM functions\", function() \n  local document = [[\n  <html>\n  <head><title>pokus</title></head>\n  <body>\n  <h1>pokus</h1>\n  <p>nazdar</p>\n  </body>\n  </html>\n  ]]\n\n  local obj = dom.parse(document)\n  it(\"It should parse XML\", function()\n    assert.truthy(type(obj), \"table\")\n    assert.truthy(obj:root_node())\n  end)\n\n  it(\"Path retrieving should work\", function()\n    local path = obj:get_path(\"html body\")\n    assert.truthy(path)\n    assert.truthy(#path == 1)\n    assert.truthy(path[1]:is_element())\n    assert.truthy(#path[1]:get_children() == 5)\n  end)\n \n  describe(\"Basic DOM traversing should work\", function()\n    local matched = false\n    local count = 0\n    obj:traverse_elements(function(el)\n      count = count + 1\n      if obj:get_element_name(el) == \"p\" then\n        matched = true\n        it(\"Element matching should work\", function()\n          assert.truthy(el:root_node():get_node_type() == \"ROOT\")\n          assert.truthy(el:is_element())\n          assert.truthy(el:get_element_name()== \"p\")\n        end)\n        it(\"Node serializing should work\", function()\n          local p_serialize = el:serialize()\n          assert.truthy(p_serialize == \"<p>nazdar</p>\")\n        end)\n        el:remove_node(el)\n      end\n    end)\n    it(\"Traverse should find 7 elements and match one <p>\", function()\n      assert.truthy(matched)\n      assert.truthy(count == 7)\n    end)\n  end)\n\n  describe(\"Modified DOM object serializing\", function()\n    local serialized = obj:serialize()\n    assert.truthy(serialized)\n    assert.truthy(type(serialized) == \"string\")\n    assert.truthy(serialized:match(\"<html>\"))\n    assert.truthy(serialized:match(\"<p>\")== nil)\n  end)\n\n  -- css selector handling was moved to another module\n  -- describe(\"CSS selector handling\", function()\n  --   local selector = \"div#pokus span.ahoj, p, div.ahoj:first-child\"\n  --   local objects = obj:prepare_selector(selector)\n  --   assert.truthy(#objects == 3)\n  --   assert.truthy(obj:calculate_specificity(objects[1]) == 112)\n  --   local document = [[\n  --   <html>\n  --   <body>\n  --   <div class=\"ahoj\" id=\"pokus\">\n  --   <span>first child</span>\n  --   <span class=\"ahoj\">Pokus</span>\n  --   <p>Uff</p>\n  --   <b>Something different</b>\n  --   </div>\n  --   </body>\n  --   </html>\n  --   ]]\n  --   local newobj = dom.parse(document)\n  --   local matchedlist = newobj:get_selector_path(objects)\n  --   assert.truthy(#matchedlist == 3)\n  --   -- assert.truthy(#obj:prepare_selector(selector)==2)\n  -- end)\n\n\nend)\n"
  },
  {
    "path": "test/test-mkparams.lua",
    "content": "require \"busted.runner\" ()\nkpse.set_program_name \"luatex\"\nlocal mkparams = require \"mkparams\"\n\ndescribe(\"Test output format and extensions\", function()\n  it(\"Should parse the output formats\", function()\n    local format, extensions = mkparams.get_format_extensions(\"html5+latexmk+sample-disabled\")\n    assert.are.equal(format, \"html5\")\n    assert.are.equal(type(extensions), \"table\")\n    assert.are.equal(#extensions, 3)\n    assert.are.equal(extensions[2].name, \"sample\")\n    assert.are.equal(extensions[3].type, \"-\")\n  end)\nend)\n"
  },
  {
    "path": "tools/make_chardata.lua",
    "content": "kpse.set_program_name \"luatex\"\n-- create Lua module from UnicodeData\n-- we need mapping to lower case letters and decomposed base letters for accented characters\nlocal unicode_data = kpse.find_file(\"UnicodeData.txt\")\nlocal chardata = {}\nfor line in io.lines(unicode_data) do\n  local record = line:explode(\";\")\n  local char = tonumber(record[1], 16)\n  local category  = string.lower(record[3])\n  if category:match(\"^l\") or category == \"zs\" then\n    -- the decomposed field contains charcode for the base letter and accent\n    -- we care only about the base letter\n    local decomposed = record[6]:match(\"([%x]+)\")\n    decomposed = decomposed and tonumber(decomposed, 16)\n    -- the lowercase letter is the last field\n    local lower = record[#record - 1]\n    lower = lower and tonumber(lower, 16) or nil\n    chardata[#chardata+1] = {\n      char   = char,\n      shcode = decomposed,\n      lccode = lower,\n      category = category\n    }\n  end\nend\n\nprint \"return {\"\nlocal function add(fields, caption, value)\n  if value then\n    fields[#fields+1] = string.format(\"%s=%s\", caption, value)\n  end\nend\n\nfor _, data in ipairs(chardata) do\n  local fields = {}\n  -- we need to add qotes to force string\n  add(fields, \"category\", string.format('\"%s\"', data.category))\n  add(fields, \"lccode\", data.lccode)\n  add(fields, \"shcode\", data.shcode)\n  print(string.format(\"[%s] = {%s},\", data.char, table.concat(fields, \", \")))\nend\n\nprint \"}\"\n"
  },
  {
    "path": "tools/make_mathmlchardata.lua",
    "content": "-- This file generates Lua table with mapping of Unicode charcodes for different math font styles (bold, italic, bold-italic, etc.)\n-- The new version of MathML requires to use different charcodes for different font styles, \n-- so we need to replace characters in the MathML output depending on the value of the mathvariant attribute.\n\nkpse.set_program_name \"luatex\"\nlocal unicode = kpse.find_file(\"UnicodeData.txt\")\n\nlocal function get_chartype(chartype)\n  -- remove the extra information from the chartype and convert it to the format used in the mathvariant attribute\n  return chartype:gsub(\"MATHEMATICAL \", \"\")\n    :gsub(\"SYMBOL$\", \"\")\n    :gsub(\"%a+%s*$\", \"\")\n    :gsub(\"SMALL \", \"\")\n    :gsub(\"CAPITAL \", \"\")\n    :gsub(\"%s+$\", \"\")\n    :gsub(\"%s+\", \"-\")\n    :lower()\nend\n\n\nlocal function parse_unicode(unicode)\n  local unicode_data = {}\n  for line in io.lines(unicode) do\n    -- parse the UnicodeData.txt file to get the base code for the mathematical symbols\n    local code, chartype, basecode = line:match(\"^(%x+);([^;]+);[^;]+;[^;]+;[^;]+;([^;]+);\")\n    -- we are interested only in the mathematical symbols\n    if code and chartype:match(\"^MATHEMATICAL\") then\n      -- the basecode contains extra <font> tag, we need to remove it and convert the hexadecimal number to decimal\n      local base = tonumber(basecode:match(\"(%x+)$\"), 16)\n      -- remove the extra information from the chartype\n      chartype = get_chartype(chartype)\n      local char = tonumber(code, 16)\n      if base and char then\n        -- we need to store corresponding base code for each symbol in the current font style\n        local area = unicode_data[base] or {}\n        area[chartype] = char\n        unicode_data[base] = area\n        -- print(\"unicode\", char, chartype, base)\n      end\n    end\n  end\n  return unicode_data\nend\n\nlocal unicode_data = parse_unicode(unicode)\n\nprint \"-- This file is autogenerated from tools/make_mathmlchardata.lua\"\nprint \"return {\"\n\nlocal to_sort = {}\nfor base, data in pairs(unicode_data) do\n  local fields = {}\n  for chartype, char in pairs(data) do\n    fields[#fields+1] = string.format(\"['%s']=%s\", chartype, char)\n  end\n  to_sort[#to_sort+1] = string.format(\"[%05i] = {%s},\", base, table.concat(fields, \", \"))\nend\n\n-- sort characters\ntable.sort(to_sort)\nfor _, line in ipairs(to_sort) do print(line) end\n\nprint \"}\"\n"
  }
]