Compare commits

...

65 Commits
v1.1 ... main

Author SHA1 Message Date
Nicolas Kruse 3f9cd56221
Updated Version to 1.2.4 in pyproject.toml 2025-09-24 15:58:20 +02:00
Nicolas 13c2aaac8b ci script changed to absolut cache path 2025-09-10 00:08:55 +02:00
Nicolas 4c396cd5b3 section_class for add_markdown fixed 2025-09-10 00:08:55 +02:00
Nicolas Kruse c7123e23d9
Merge pull request #4 from Nonannet/dev
section_class value extended to format latex using "environment". Tes…
2025-09-09 23:10:58 +02:00
Nicolas df592aef6a ci script updated to run docs build only on push 2025-09-09 22:57:02 +02:00
Nicolas 65b9a300b9 section_class value extended to format latex using "environment". Test added and default updated with a "fineprint" class/environment 2025-09-09 22:15:35 +02:00
Nicolas 3098226c75 path in ci fixed 2025-08-01 14:59:28 +02:00
Nicolas cb563a2df5 Updated CI and Docs deployment. 2025-08-01 14:05:31 +02:00
Nicolas Kruse 1e7e7bfead New Python version added in CI and typing fixed in __init__.py 2025-08-01 12:23:54 +02:00
Nicolas a90c411ff1 Update deploy method and docs api path 2025-08-01 12:09:15 +02:00
Nicolas 726e6c7de0 py.typed file added indication packet uses type annotations 2025-07-06 17:16:45 +02:00
Nicolas 5f6147c2b9 Readme updated (- conda added for installing) 2025-06-25 08:40:56 +02:00
Nicolas Kruse 6120a38241 CI: added caching for latex and pip 2025-06-10 10:57:52 +02:00
Nicolas c72a1e70c4 CI Script: $ProgressPreference = 'SilentlyContinue' added 2025-06-09 17:01:14 +02:00
Nicolas 001e5b8a10 CI script: changed to preinstalled miktex package 2025-06-09 16:11:32 +02:00
Nicolas bd7d70a0b7 CI script: URL for miktexsetup changed 2025-06-09 16:06:37 +02:00
Nicolas cba2dd690f Merge branch 'main' of https://github.com/Nonannet/pyladoc 2025-06-09 16:02:33 +02:00
Nicolas a8ca6aec75 CI script updated to cache latex installation on windows 2025-06-09 16:02:30 +02:00
Nicolas Kruse 83cf954e95 Codestyle issues fixed 2025-06-06 19:31:36 +02:00
Nicolas 01147ef648 Doc generation updated, changed to pydata_sphinx_theme 2025-06-06 19:21:38 +02:00
Nicolas Kruse 3ee02b73b8
Updated version in pyproject.toml to 1.2.3 2025-06-02 19:56:21 +02:00
Nicolas 71f211022f auto column detection for minus characters fixed; test modified to catch it 2025-06-02 09:20:15 +02:00
Nicolas 1f9f2f93e7 Merge branch 'main' of https://github.com/Nonannet/pyladoc 2025-06-02 08:54:44 +02:00
Nicolas Kruse 10ace61abc
Added missing minus for valid number chars for collum auto format detection in latex.py 2025-06-01 22:41:02 +02:00
Nicolas dfa450f05c debug print line in inject_to_template removed 2025-05-31 11:48:22 +02:00
Nicolas 664a08b2dc arguments for inject_to_template in "to_pdf" function and code style issues fixed 2025-05-31 11:36:37 +02:00
Nicolas 5ac7659f01 inject_to_template function fixed 2025-05-31 11:32:05 +02:00
Nicolas ecc97bd9a6 coding style issues fixed 2025-05-30 17:08:38 +02:00
Nicolas a65bb1c8e5 Template injection function extended to multiple fields; tests added and adjusted 2025-05-30 12:51:34 +02:00
Nicolas a413d771b3 readme updated 2025-05-26 12:58:15 +02:00
Nicolas 727ab55b3d CI: test-dependencies lxml and requests added for "build-ubuntu-no-optional-dependencies" 2025-05-26 12:36:20 +02:00
Nicolas b20773364c path format in ci.yml fixed 2025-05-26 12:33:24 +02:00
Nicolas ad6fe7c6e7 Test for missing optional dependencies added 2025-05-26 12:31:35 +02:00
Nicolas 37ad1231c8 type: ignore[truthy-function] added in latex.py for checking optional dependency 2025-05-26 10:35:48 +02:00
Nicolas 250885d44f fixed missing optional dependency of pandas/Styler 2025-05-26 10:13:34 +02:00
Nicolas 76944d1829 docs added 2025-05-26 09:45:58 +02:00
Nicolas c4fdb5040a figure_scale argument to function .to_pdf(..) added 2025-05-23 13:52:59 +02:00
Nicolas Kruse b3c2f5e384 Tests update to write no files to tests/out it WRITE_RESULT_FILES = False 2025-05-21 16:50:23 +02:00
Nicolas Kruse dba8f5997e latex backend can be selected now 2025-05-21 16:46:40 +02:00
Nicolas Kruse c908602657 CI: more tests 2025-05-19 16:36:57 +02:00
Nicolas Kruse f8c59c3c4b CI: testing 2025-05-19 15:52:09 +02:00
Nicolas Kruse 2b5fde630b CI path experiment 2025-05-19 15:34:22 +02:00
Nicolas Kruse 1abc883ee6 CI: other path 2025-05-19 15:12:12 +02:00
Nicolas Kruse 53f7f3c828 path for MiKTeX changed 2025-05-19 15:00:06 +02:00
Nicolas Kruse aacb99a40f Windows CI: xetex with mpm added 2025-05-19 14:37:03 +02:00
Nicolas Kruse efe702a522 CI script updated 2025-05-19 14:18:48 +02:00
Nicolas f2588bb7d7 version number updated to v1.2.2 2025-05-19 12:20:30 +02:00
Nicolas 932ead8ef2 github action for publishing added 2025-05-19 12:13:56 +02:00
Nicolas 507d88bc38 code according to flake8 and mypy updated 2025-05-19 12:08:47 +02:00
Nicolas 2083a6ed41 example html results added 2025-05-19 12:08:47 +02:00
Nicolas Kruse c647bceafc min. python version updated in project file 2025-05-19 12:08:47 +02:00
Nicolas Kruse bdfaa08f37 tests updated 2025-05-19 12:08:47 +02:00
Nicolas Kruse c6979f3c6a renumbering of SVG-ids added to match HTML specification 2025-05-19 12:08:47 +02:00
Nicolas 39142a1e22 Readme updated (link for image, LaTeX setup) 2025-05-13 11:12:21 +02:00
Nicolas 2bb4e55dd0 CI config changed to only upload files from first job 2025-05-13 10:26:56 +02:00
Nicolas d9fbe49bcf typing stubs added for dev-dependencies 2025-05-13 10:13:27 +02:00
Nicolas bda9d046ee Github action for CI added 2025-05-13 10:03:09 +02:00
Nicolas 807cb96638 version number updated to v1.2 2025-05-10 13:43:23 +02:00
Nicolas 98d40a21f6 test/example outputs updated 2025-05-04 14:36:50 +02:00
Nicolas d2193c2526 tests updated for linked HTML references 2025-05-04 14:35:57 +02:00
Nicolas 0d5c5c727c referencing issue fixed 2025-05-04 14:35:25 +02:00
Nicolas a90d055d3e blank lines added/removed 2025-05-02 16:41:50 +02:00
Nicolas 59a53f1be2 Linking to fig, table and equation references added for HTML output 2025-05-02 16:40:41 +02:00
Nicolas dda979487f Image of example output added in the readme 2025-04-15 10:37:09 +02:00
Nicolas 413be9b625 Readme updated 2025-04-14 21:16:43 +02:00
39 changed files with 962 additions and 5362 deletions

View File

@ -12,9 +12,10 @@ exclude =
__pycache__, __pycache__,
build, build,
dist, dist,
.conda .conda,
.venv .venv,
venv venv,
docs/source/api
# Enable specific plugins or options # Enable specific plugins or options
# Example: Enabling flake8-docstrings # Example: Enabling flake8-docstrings

200
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,200 @@
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-ubuntu-no-optional-dependencies:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", 3.13]
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install from source and install pytest
run: |
python -m pip install --upgrade pip
python -m pip install -e .
python -m pip install pytest lxml requests
- name: Run tests with pytest (no matplotlib, no pandas)
run: |
pytest tests/test_rendering_markdown.py::test_markdown_styling
pytest tests/test_rendering_markdown.py::test_markdown_table
- name: Install matplotlib
run: |
python -m pip install matplotlib
- name: Run tests with pytest rendering equations (with matplotlib)
run: |
pytest tests/test_rendering_markdown.py::test_markdown_equations
build-ubuntu:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", 3.13]
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install LaTeX
run: sudo apt-get install -y texlive-latex-extra texlive-fonts-recommended lmodern texlive-xetex texlive-science
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e .[dev]
- name: Lint code with flake8
run: flake8
- name: Type checking with mypy
run: mypy
- name: Run tests with pytest
run: pytest
- name: Upload rendered files
uses: actions/upload-artifact@v4
if: strategy.job-index == 0
with:
name: rendering-results-ubuntu
path: tests/out/test_*_render*
build-windows:
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.10"]
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Cache MiKTeX Portable
uses: actions/cache@v4
id: miktex
with:
path: C:\tmp\cache
key: miktex-portable-${{ runner.os }}-24.1-x64
- if: ${{ steps.miktex.outputs.cache-hit != 'true' }}
name: Set up MiKTeX Portable
run: |
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest https://www.nonan.net/w/files/miktex-portable-Win-x64.zip -OutFile miktex-portable-Win-x64.zip
Expand-Archive miktex-portable-Win-x64.zip -DestinationPath C:\tmp\cache\
- name: Copy miktex directory
run: |
robocopy C:\tmp\cache\miktex-portable C:\tmp\test_miktex\miktex-portable /E /NFL /NDL
if ($LASTEXITCODE -eq 1) { exit 0 }
- name: Add miktex to PATH
run: |
echo "PATH=$PATH;C:\tmp\test_miktex\miktex-portable\texmfs\install\miktex\bin\x64;C:\Program Files\Git\usr\bin" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: test xelatex
run: xelatex --version
- name: Install Python dependencies
run: |
python -m pip install -e .[dev]
- name: Run tests with pytest
run: pytest
- name: Upload rendered files
uses: actions/upload-artifact@v4
if: strategy.job-index == 0
with:
name: rendering-results-windows
path: tests/out/test_*_render*.pdf
build-docs:
if: github.event_name == 'push'
needs: build-ubuntu
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download rendering outputs
uses: actions/download-artifact@v4
with:
name: rendering-results-ubuntu
path: docs/build/html/files/
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: "3.x"
- name: Install package and dependencies
run: pip install .[doc_build]
- name: Generate Class List
run: python ./docs/source/generate_class_list.py
- name: Build Docs
run: |
mkdir -p docs/source/media
cp media/* docs/source/media/
mkdir -p docs/source/tests
cp tests/test_rendering_example*.py docs/source/tests/
cp LICENSE docs/source/LICENSE.md
cd docs
sphinx-apidoc -o source/ ../src/ -M --no-toc
rm ./source/*.rst
make html
touch ./build/html/.nojekyll
mkdir -p ./build/html/_autogenerated
cp ./build/html/api/* ./build/html/_autogenerated/
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs/build/html
deploy:
if: github.event_name == 'push'
needs: build-docs
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

39
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Publish to PyPI
on:
push:
tags:
- "v*"
jobs:
publish:
name: Build and publish
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/${{ github.event.repository.name }}/
steps:
- uses: actions/checkout@v3
- name: Ensure this is main branch
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
run: echo "Proceeding with publish"
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install build tools
run: python -m pip install --upgrade build twine
- name: Build package
run: python -m build
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
run: python -m twine upload dist/*

4
.gitignore vendored
View File

@ -69,7 +69,8 @@ instance/
.scrapy .scrapy
# Sphinx documentation # Sphinx documentation
docs/_build/ docs/build/
docs/source/api/
# PyBuilder # PyBuilder
.pybuilder/ .pybuilder/
@ -132,3 +133,4 @@ pyModbusTCP_old/
test.py test.py
test_*.ipynb test_*.ipynb
settings.json settings.json
tests/out/test*

View File

@ -1,41 +1,47 @@
# Pyladoc # Pyladoc
## Description ## Description
Pyladoc is a python package for programmatically generating HTML and Pyladoc is a Python package for programmatically generating HTML and
PDF/LaTeX output. This package targets specifically applications where reports PDF/LaTeX output. This package specifically targets applications where reports
or results with Pandas-tables and Matplotlib-figures are generated programmatically or results with Pandas tables and Matplotlib figures are generated
to be displayed as website and as PDF document without any manual formatting to be displayed as a website and as a PDF document without involving any manual
steps. formatting steps.
This package focuses on the "Document in Code" approach for cases This package focuses on the "Document in Code" approach for cases
where a lot of calculations and data handling is done but not a lot of where a lot of calculations and data handling is done but not a lot of
document text needs to be displayed. document text needs to be displayed. The multiline string capability of Python
handles this very well. In comparison to "Code in Document" templates,
Python tools support this approach out of the box—similar to docstrings.
As backend for PDF generation LaTeX is used. There are excellent engines for LaTeX is used as the backend for PDF generation. There are excellent engines for
rendering HTML to PDF available, but even if there is no requirement for an rendering HTML to PDF, but even if there is no requirement for
accurate typesetting, placing programmatically content of variable accurate typesetting, placing programmatically generated content of variable
composition and element sizes on fixed size pages without manual intervention composition and element sizes on fixed-size pages without manual intervention
is a hard problem that LaTeX is very capable of. is a hard problem where LaTeX is superior.
## Example outputs ## Example outputs
The following documents are generated by [tests/test_rendering_example1_doc.py](tests/test_rendering_example1_doc.py):
- HTML: [test_html_render1.html](https://html-preview.github.io/?url=https://github.com/Nonannet/pyladoc/blob/main/tests/out/test_html_render1.html) [![example output](media/output_example.png)](https://nonannet.github.io/pyladoc/files/test_latex_render1.pdf)
- PDF: [test_latex_render1.pdf](https://raw.githubusercontent.com/Nonannet/pyladoc/refs/heads/main/tests/out/test_latex_render1.pdf)
- HTML: [test_html_render1.html](https://nonannet.github.io/pyladoc/files/test_html_render1.html)
- PDF: [test_latex_render1.pdf](https://nonannet.github.io/pyladoc/files/test_latex_render1.pdf) ([LaTeX](https://nonannet.github.io/pyladoc/files/test_html_render1.tex))
The documents are generated by the script [tests/test_rendering_example1_doc.py](tests/test_rendering_example1_doc.py).
### Supported primitives ### Supported primitives
- Text (can be Markdown or HTML formatted) - Text (can be Markdown or HTML formatted)
- Headings - Headings
- Tables (Pandas, Markdown or HTML) - Tables (Pandas, Markdown or HTML)
- Matplotlib figures - Matplotlib figures
- LaTeX equations (Block or inline) - LaTeX equations (block or inline)
- Named references for figures, tables and equation - Named references for figures, tables, and equations
### Key Features ### Key Features
- HTML and PDF/LaTeX rendering of the same document - HTML and PDF/LaTeX rendering of the same document
- Single file output including figures - Single file output including figures
- Figure and equation embedding in HTML by inline SVG, SVG in Base64 or PNG in Base64 - Figure and equation embedding in HTML by inline SVG, SVG in Base64, or PNG in Base64
- Figure embedding in LaTeX as PGF/TikZ - Figure embedding in LaTeX as PGF/TikZ
- Tested on Linux and Windows
### Usage Scenarios ### Usage Scenarios
- Web services - Web services
@ -48,8 +54,30 @@ It can be installed with pip:
pip install pyladoc pip install pyladoc
``` ```
As well as with conda:
```bash
conda install conda-forge::pyladoc
```
## Dependencies
Pyladoc depends on the markdown package.
Optional dependencies are:
- Matplotlib Python package for rendering LaTeX equations for HTML output
- LaTeX for exporting to PDF or exporting Matplotlib figures to LaTeX (PGF/TikZ rendering)
- Pandas and Jinja2 for rendering Pandas tables
- Matplotlib for rendering Matplotlib figures (obviously)
For the included template, the `miktex` LaTeX distribution works on Windows
and the following LaTeX setup works on Ubuntu (both tested in CI):
```bash
sudo apt-get update
sudo apt-get install -y texlive-latex-extra texlive-fonts-recommended lmodern texlive-xetex texlive-science
```
## Usage ## Usage
It is easy to use as the following example code shows: It is easy to use, as the following example code shows:
```python ```python
import pyladoc import pyladoc
@ -85,7 +113,7 @@ doc.to_pdf('test.pdf')
``` ```
## Contributing ## Contributing
Contributions are welcome, please open an issue or submit a pull request on GitHub. Contributions are welcome; please open an issue or submit a pull request on GitHub.
## Developer Guide ## Developer Guide
To get started with developing the `pyladoc` package, follow these steps. To get started with developing the `pyladoc` package, follow these steps.
@ -97,14 +125,14 @@ git clone https://github.com/Nonannet/pyladoc.git
cd pyladoc cd pyladoc
``` ```
It's recommended to setup an venv: It's recommended to set up a venv:
```bash ```bash
python -m venv venv python -m venv .venv
source venv/bin/activate # On Windows use `venv\Scripts\activate` source .venv/bin/activate # On Windows use `.venv\Scripts\activate`
``` ```
Install the package and dev-dependencies while keeping files in the Install the package and development dependencies while keeping files in the
current directory: current directory:
```bash ```bash

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

35
docs/make.bat Normal file
View File

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

33
docs/source/conf.py Normal file
View File

@ -0,0 +1,33 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
import os
import sys
sys.path.insert(0, os.path.abspath("../src/"))
project = 'pyladoc'
copyright = '2025, Nicolas Kruse'
author = 'Nicolas Kruse'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx_autodoc_typehints", "myst_parser"]
templates_path = ['_templates']
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
# html_theme = 'alabaster'
html_theme = 'pydata_sphinx_theme'
html_static_path = ['_static']
autodoc_inherit_docstrings = True
autoclass_content = 'both'

View File

@ -0,0 +1,78 @@
import importlib
import inspect
import fnmatch
from io import TextIOWrapper
import os
def write_classes(f: TextIOWrapper, patterns: list[str], module_name: str, title: str, description: str = '', exclude: list[str] = []) -> None:
"""Write the classes to the file."""
module = importlib.import_module(module_name)
classes = [
name for name, obj in inspect.getmembers(module, inspect.isclass)
if (obj.__module__ == module_name and
any(fnmatch.fnmatch(name, pat) for pat in patterns if pat not in exclude) and
obj.__doc__ and '(Automatic generated stub)' not in obj.__doc__)
]
if description:
f.write(f'{description}\n\n')
write_dochtree(f, title, classes)
for cls in classes:
with open(f'docs/source/api/{cls}.md', 'w') as f2:
f2.write(f'# {module_name}.{cls}\n')
f2.write('```{eval-rst}\n')
f2.write(f'.. autoclass:: {module_name}.{cls}\n')
f2.write(' :members:\n')
f2.write(' :undoc-members:\n')
f2.write(' :show-inheritance:\n')
f2.write(' :inherited-members:\n')
f2.write('```\n\n')
def write_functions(f: TextIOWrapper, patterns: list[str], module_name: str, title: str, description: str = '', exclude: list[str] = []) -> None:
"""Write the classes to the file."""
module = importlib.import_module(module_name)
functions = [
name for name, obj in inspect.getmembers(module, inspect.isfunction)
if (obj.__module__ == module_name and
any(fnmatch.fnmatch(name, pat) for pat in patterns if pat not in exclude))
]
if description:
f.write(f'{description}\n\n')
write_dochtree(f, title, functions)
for func in functions:
if not func.startswith('_'):
with open(f'docs/source/api/{func}.md', 'w') as f2:
f2.write(f'# {module_name}.{func}\n')
f2.write('```{eval-rst}\n')
f2.write(f'.. autofunction:: {module_name}.{func}\n')
f2.write('```\n\n')
def write_dochtree(f: TextIOWrapper, title: str, items: list[str]):
f.write('```{toctree}\n')
f.write(':maxdepth: 1\n')
f.write(f':caption: {title}:\n')
for text in items:
if not text.startswith('_'):
f.write(f"{text}\n")
f.write('```\n\n')
if __name__ == "__main__":
# Ensure the output directory exists
os.makedirs('docs/source/api', exist_ok=True)
with open('docs/source/api/index.md', 'w') as f:
f.write('# Classes and functions\n\n')
write_classes(f, ['DocumentWriter'], 'pyladoc', title='DocumentWriter Class')
write_functions(f, ['*'], 'pyladoc', title='Functions')
write_functions(f, ['*'], 'pyladoc.latex', title='Submodule latex')

9
docs/source/index.md Normal file
View File

@ -0,0 +1,9 @@
```{toctree}
:maxdepth: 1
:hidden:
api/index
repo
```
```{include} ../../README.md
```

3
docs/source/repo.md Normal file
View File

@ -0,0 +1,3 @@
# Code repository
Code repository is on GitHub: [github.com/Nonannet/pyladoc](https://github.com/Nonannet/pyladoc).

BIN
media/output_example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@ -1,15 +1,15 @@
[project] [project]
name = "pyladoc" name = "pyladoc"
version = "1.1.0" version = "1.2.4"
authors = [ authors = [
{ name="Nicolas Kruse", email="nicolas.kruse@nonan.net" }, { name="Nicolas Kruse", email="nicolas.kruse@nonan.net" },
] ]
description = "Package for generating HTML and PDF/latex from python code" description = "Package for generating HTML and PDF/latex from python code"
readme = "README.md" readme = "README.md"
requires-python = ">=3.8" requires-python = ">=3.10"
license = "MIT"
classifiers = [ classifiers = [
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
] ]
dependencies = [ dependencies = [
@ -19,16 +19,23 @@ dependencies = [
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"pytest", "flake8", "mypy", "pytest", "flake8", "mypy",
"lxml", "types-lxml", "lxml", "types-lxml", "types-Markdown", "pandas-stubs",
"requests", "requests",
"matplotlib>=3.1.1", "matplotlib>=3.1.1",
"pandas>=2.0.0", "Jinja2", "pandas>=2.0.0", "Jinja2",
] ]
doc_build = [
"sphinx",
"pydata_sphinx_theme",
"sphinx-autodoc-typehints",
"myst-parser"
]
[project.urls] [project.urls]
Homepage = "https://github.com/Nonannet/pyladoc" Homepage = "https://github.com/Nonannet/pyladoc"
Repository = "https://github.com/Nonannet/pyladoc" Repository = "https://github.com/Nonannet/pyladoc"
Issues = "https://github.com/Nonannet/pyladoc/issues" Issues = "https://github.com/Nonannet/pyladoc/issues"
documentation = "https://nonannet.github.io/pyladoc/"
[build-system] [build-system]
requires = ["setuptools>=61.0", "wheel"] requires = ["setuptools>=61.0", "wheel"]
@ -38,7 +45,7 @@ build-backend = "setuptools.build_meta"
where = ["src"] where = ["src"]
[tool.setuptools.package-data] [tool.setuptools.package-data]
pyladoc = ["templates/*"] pyladoc = ["templates/*", "py.typed"]
[tool.mypy] [tool.mypy]
files = ["src"] files = ["src"]

View File

@ -8,6 +8,7 @@ from . import latex
import pkgutil import pkgutil
from html.parser import HTMLParser from html.parser import HTMLParser
from io import StringIO from io import StringIO
from . import svg_tools
HTML_OUTPUT = 0 HTML_OUTPUT = 0
LATEX_OUTPUT = 1 LATEX_OUTPUT = 1
@ -32,6 +33,7 @@ else:
Table = DataFrame | Styler Table = DataFrame | Styler
except ImportError: except ImportError:
Table = DataFrame Table = DataFrame
Styler = None
try: try:
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
@ -58,17 +60,6 @@ def _markdown_to_html(text: str) -> str:
return html_text return html_text
def _clean_svg(svg_text: str) -> str:
# remove all tags not alllowd for inline svg from metadata:
svg_text = re.sub(r'<metadata>.*?</metadata>', '', svg_text, flags=re.DOTALL)
# remove illegal path-tags without d attribute:
return re.sub(r'<path(?![^>]*\sd=)\s.*?/>', '', svg_text, flags=re.DOTALL)
# def _get_templ_vars(template: str) -> list[str]:
# return re.findall("<!---START (.+?)--->.*?<!---END .+?--->", template, re.DOTALL)
def _drop_indent(text: str, amount: int) -> str: def _drop_indent(text: str, amount: int) -> str:
""" """
Drops a specific number of indentation spaces from a multiline text. Drops a specific number of indentation spaces from a multiline text.
@ -99,8 +90,9 @@ def _save_figure(fig: Figure, buff: io.BytesIO, figure_format: FFormat, font_fam
yield ax.xaxis.label yield ax.xaxis.label
yield ax.yaxis.label yield ax.yaxis.label
yield from ax.get_xticklabels() + ax.get_yticklabels() yield from ax.get_xticklabels() + ax.get_yticklabels()
legend: Mpl_Legend = ax.get_legend() legend = ax.get_legend()
if legend: if legend:
assert isinstance(legend, Mpl_Legend)
yield from legend.get_texts() yield from legend.get_texts()
# Store current figure settings # Store current figure settings
@ -142,6 +134,7 @@ def escape_html(text: str) -> str:
def figure_to_string(fig: Figure, def figure_to_string(fig: Figure,
unique_id: str,
figure_format: FFormat = 'svg', figure_format: FFormat = 'svg',
font_family: str | None = None, font_family: str | None = None,
scale: float = 1, scale: float = 1,
@ -175,7 +168,7 @@ def figure_to_string(fig: Figure,
elif figure_format == 'svg' and not base64: elif figure_format == 'svg' and not base64:
i = buff.read(2028).find(b'<svg') # skip xml and DOCTYPE header i = buff.read(2028).find(b'<svg') # skip xml and DOCTYPE header
buff.seek(max(i, 0)) buff.seek(max(i, 0))
return _clean_svg(buff.read().decode('utf-8')) return svg_tools.update_svg_ids(svg_tools.clean_svg(buff.read().decode('utf-8')), unique_id)
else: else:
image_mime = {"png": "image/png", "svg": "image/svg+xml"} image_mime = {"png": "image/png", "svg": "image/svg+xml"}
@ -218,7 +211,7 @@ def _fillin_reference_names(input_string: str, item_index: dict[str, int]) -> st
for start, end, ref in replacements: for start, end, ref in replacements:
assert ref in item_index, f"Reference {ref} does not exist in the document" assert ref in item_index, f"Reference {ref} does not exist in the document"
ret.append(input_string[current_pos:start - 1]) ret.append(input_string[current_pos:start - 1])
ret.append(str(item_index[ref])) ret.append(f'<a href="#pyld-ref-{latex.normalize_label_text(ref)}">{item_index[ref]}</a>')
current_pos = end current_pos = end
return ''.join(ret) + input_string[current_pos:] return ''.join(ret) + input_string[current_pos:]
@ -254,30 +247,40 @@ def _create_document_writer() -> 'DocumentWriter':
return new_dwr return new_dwr
def inject_to_template(content: str, template_path: str = '', internal_template: str = '') -> str: def inject_to_template(fields_dict: dict[str, str],
template_path: str = '',
internal_template: str = '',
template_string: str = '',
) -> str:
""" """
injects a content string into a template. The placeholder <!--CONTENT--> injects content fields into a template. The placeholder <!--CONTENT-->
will be replaced by the content. If the placeholder is prefixed with a will be replaced by the content. If the placeholder is prefixed with a
'%' comment character, this character will be replaced as well. '%' comment character, this character will be replaced as well.
Args: Args:
fields_dict: A dictionary with field names as keys and content as values
template_path: Path to a template file template_path: Path to a template file
internal_template: Path to a internal default template internal_template: Path to a internal default template
template_string: A template string to use directly
Returns: Returns:
Template with included content Template with included content
""" """
assert isinstance(fields_dict, dict), 'fields_dict must be a dictionary'
if template_path: if template_path:
with open(template_path, 'r') as f: with open(template_path, 'r') as f:
template = f.read() template = f.read()
elif internal_template: elif internal_template:
template = _get_pkgutil_string(internal_template) template = _get_pkgutil_string(internal_template)
elif template_string:
template = template_string
else: else:
raise Exception('No template provided') raise Exception('No template provided')
assert '<!--CONTENT-->' in template, 'No <!--CONTENT--> expression in template located' def replace_field(match: re.Match[str]) -> str:
prep_template = re.sub(r"\%?\s*<!--CONTENT-->", '<!--CONTENT-->', template) return fields_dict.get(match.group(1), match.group(0))
return prep_template.replace('<!--CONTENT-->', content)
return re.sub(r"(?:\%+\s*)?\<!--(.*?)-->", replace_field, template, 0, re.MULTILINE)
class DocumentWriter(): class DocumentWriter():
@ -298,13 +301,13 @@ class DocumentWriter():
self._item_index: dict[str, int] = {} self._item_index: dict[str, int] = {}
self._fig_scale: float = 1 self._fig_scale: float = 1
def _add_item(self, ref_id: str, ref_type: str, caption_prefix: str) -> str: def _add_item(self, ref_id: str, ref_type: str, caption_prefix: str) -> tuple[str, str]:
current_index = self._item_count.get(ref_type, 0) + 1 current_index = self._item_count.get(ref_type, 0) + 1
if not ref_id: if not ref_id:
ref_id = str(current_index) ref_id = f"auto{current_index}"
self._item_index[f"{ref_type}:{ref_id}"] = current_index self._item_index[f"{ref_type}:{ref_id}"] = current_index
self._item_count[ref_type] = current_index self._item_count[ref_type] = current_index
return caption_prefix.format(current_index) return caption_prefix.format(current_index), latex.normalize_label_text(f"{ref_type}:{ref_id}")
def _equation_embedding_reescaping(self, text: str) -> str: def _equation_embedding_reescaping(self, text: str) -> str:
""" """
@ -333,11 +336,10 @@ class DocumentWriter():
parts = latex_label.split(':') parts = latex_label.split(':')
ref_type = parts[0] ref_type = parts[0]
ref_id = parts[1] ref_id = parts[1]
caption = self._add_item(ref_id, ref_type, '({})') caption, reference = self._add_item(ref_id, ref_type, '({})')
return (f'\n<latex type="block" ref_type="{ref_type}"' return (f'<latex type="block" reference="{reference}" caption="{caption}">{content}</latex>')
f' ref_id="{ref_id}" caption="{caption}">{content}</latex>\n')
else: else:
return f'\n<latex type="block">{content}</latex>\n' return f'<latex type="block">{content}</latex>'
result = block_pattern.sub(block_repl, text) result = block_pattern.sub(block_repl, text)
@ -349,16 +351,16 @@ class DocumentWriter():
return inline_pattern.sub(inline_repl, result) return inline_pattern.sub(inline_repl, result)
def _get_equation_html(self, latex_equation: str, caption: str, block: bool = False) -> str: def _get_equation_html(self, latex_equation: str, caption: str, reference: str, block: bool = False) -> str:
fig = latex_to_figure(latex_equation) fig = latex_to_figure(latex_equation)
if block: if block:
ret = ('<div class="equation-container">' fig_str = figure_to_string(fig, reference, self._figure_format, base64=self._base64_svgs)
'<div class="equation">%s</div>' ret = ('<div class="equation-container" '
'<div class="equation-number">%s</div></div>') % ( f'id="pyld-ref-{reference}">'
figure_to_string(fig, self._figure_format, base64=self._base64_svgs), f'<div class="equation">{fig_str}</div>'
caption) f'<div class="equation-number">{caption}</div></div>')
else: else:
ret = '<span class="inline-equation">' + figure_to_string(fig, self._figure_format, base64=self._base64_svgs) + '</span>' ret = '<span class="inline-equation">' + figure_to_string(fig, reference, self._figure_format, base64=self._base64_svgs) + '</span>'
plt.close(fig) plt.close(fig)
return ret return ret
@ -372,34 +374,56 @@ class DocumentWriter():
self.modified_html = StringIO() self.modified_html = StringIO()
self.in_latex: bool = False self.in_latex: bool = False
self.eq_caption: str = '' self.eq_caption: str = ''
self.reference: str = ''
self.block: bool = False self.block: bool = False
self.p_tags: int = 0
self.dw = document_writer self.dw = document_writer
self.latex_count = 0
self.self_closing = False
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
if tag == 'hr': if tag == 'hr':
self.modified_html.write(f"<{tag}>") self.modified_html.write(f"<{tag}>")
self.self_closing = True
elif tag == 'latex': elif tag == 'latex':
self.in_latex = True self.in_latex = True
attr_dict = {k: v if v else '' for k, v in attrs} attr_dict = {k: v if v else '' for k, v in attrs}
self.eq_caption = attr_dict.get('caption', '') self.eq_caption = attr_dict.get('caption', '')
if 'reference' in attr_dict:
self.reference = attr_dict['reference']
else:
self.latex_count += 1
self.reference = f"auto_id_{self.latex_count}"
self.block = attr_dict.get('type') == 'block' self.block = attr_dict.get('type') == 'block'
elif not self.in_latex: elif not self.in_latex:
tag_text = self.get_starttag_text() tag_text = self.get_starttag_text()
if tag_text: if tag_text:
self.self_closing = tag_text.endswith('/>')
self.modified_html.write(tag_text) self.modified_html.write(tag_text)
if tag == 'p':
self.p_tags += 1
def handle_data(self, data: str) -> None: def handle_data(self, data: str) -> None:
if self.in_latex: if self.in_latex:
self.modified_html.write( eq_html = self.dw._get_equation_html(data, self.eq_caption, self.reference, self.block)
self.dw._get_equation_html(data, self.eq_caption, self.block)) if self.p_tags > 0 and self.block:
# If a block equation (with divs) is inside a p tag: close and reopen it
self.modified_html.write(f"</p>{eq_html}<p>")
else:
self.modified_html.write(eq_html)
else: else:
self.modified_html.write(data) self.modified_html.write(data)
def handle_endtag(self, tag: str) -> None: def handle_endtag(self, tag: str) -> None:
if tag == 'latex': if tag == 'latex':
self.in_latex = False self.in_latex = False
elif self.self_closing:
self.self_closing = False
else: else:
self.modified_html.write(f"</{tag}>") self.modified_html.write(f"</{tag}>")
if tag == 'p' and self.p_tags > 0:
self.p_tags -= 1
parser = HTMLPostProcessor(self) parser = HTMLPostProcessor(self)
parser.feed(html_code) parser.feed(html_code)
@ -431,18 +455,19 @@ class DocumentWriter():
""" """
def render_to_html() -> str: def render_to_html() -> str:
caption_prefix = self._add_item(ref_id, ref_type, prefix_pattern) caption_prefix, reference = self._add_item(ref_id, ref_type, prefix_pattern)
return '<div class="figure">%s%s</div>' % ( return '<div id="pyld-ref-%s" class="figure">%s%s</div>' % (
figure_to_string(fig, self._figure_format, base64=self._base64_svgs, scale=self._fig_scale), reference,
figure_to_string(fig, reference, self._figure_format, base64=self._base64_svgs, scale=self._fig_scale),
'<br>' + caption_prefix + escape_html(caption) if caption else '') '<br>' + caption_prefix + escape_html(caption) if caption else '')
def render_to_latex() -> str: def render_to_latex() -> str:
self._add_item(ref_id, ref_type, prefix_pattern) _, reference = self._add_item(ref_id, ref_type, prefix_pattern)
return '\\begin{figure}%s\n%s\n\\caption{%s}\n%s\\end{figure}' % ( return '\\begin{figure}%s\n%s\n\\caption{%s}\n%s\\end{figure}' % (
'\n\\centering' if centered else '', '\n\\centering' if centered else '',
figure_to_string(fig, 'pgf', self._font_family, scale=self._fig_scale), figure_to_string(fig, reference, 'pgf', self._font_family, scale=self._fig_scale),
latex.escape_text(caption), latex.escape_text(caption),
'\\label{%s}\n' % latex.normalize_label_text(ref_type + ':' + ref_id) if ref_id else '') '\\label{%s}\n' % latex.normalize_label_text(reference) if ref_id else '')
self._doc.append([render_to_html, render_to_latex]) self._doc.append([render_to_html, render_to_latex])
@ -462,27 +487,27 @@ class DocumentWriter():
centered: Whether to center the table in LaTeX output centered: Whether to center the table in LaTeX output
""" """
assert Table and isinstance(table, Table), 'Table has to be a pandas DataFrame oder DataFrame Styler' assert Table and isinstance(table, Table), 'Table has to be a pandas DataFrame oder DataFrame Styler'
styler = table if isinstance(table, Styler) else getattr(table, 'style', None) styler = table if Styler and isinstance(table, Styler) else getattr(table, 'style', None) # type: ignore[truthy-function]
assert isinstance(styler, Styler), 'Jinja2 package is required for rendering tables' assert Styler and isinstance(styler, Styler), 'Jinja2 package is required for rendering pandas tables' # type: ignore[truthy-function]
def render_to_html() -> str: def render_to_html() -> str:
caption_prefix = self._add_item(ref_id, ref_type, prefix_pattern) caption_prefix, reference = self._add_item(ref_id, ref_type, prefix_pattern)
html_string = styler.to_html(table_uuid=ref_id, caption=caption_prefix + escape_html(caption)) html_string = styler.to_html(table_uuid=ref_id, caption=caption_prefix + escape_html(caption))
return re.sub(r'<style.*?>.*?</style>', '', html_string, flags=re.DOTALL) return f'<div id="pyld-ref-{reference}">' + re.sub(r'<style.*?>.*?</style>', '', html_string, flags=re.DOTALL) + '</div>'
def render_to_latex() -> str: def render_to_latex() -> str:
self._add_item(ref_id, ref_type, prefix_pattern) _, reference = self._add_item(ref_id, ref_type, prefix_pattern)
ref_label = latex.normalize_label_text(ref_type + ':' + ref_id)
if self._table_renderer == 'pandas': if self._table_renderer == 'pandas':
return styler.to_latex( return styler.to_latex(
label=ref_label, label=reference,
hrules=True, hrules=True,
convert_css=True, convert_css=True,
siunitx=True, siunitx=True,
caption=latex.escape_text(caption), caption=latex.escape_text(caption),
position_float='centering' if centered else None) position_float='centering' if centered else None)
else: else:
return latex.render_pandas_styler_table(styler, caption, ref_label, centered) return latex.render_pandas_styler_table(styler, caption, reference, centered)
self._doc.append([render_to_html, render_to_latex]) self._doc.append([render_to_html, render_to_latex])
@ -581,12 +606,12 @@ class DocumentWriter():
""" """
def render_to_html() -> str: def render_to_html() -> str:
caption = self._add_item(ref_id, ref_type, '({})') caption, reference = self._add_item(ref_id, ref_type, '({})')
return self._get_equation_html(latex_equation, caption) return self._get_equation_html(latex_equation, caption, reference, block=True)
def render_to_latex() -> str: def render_to_latex() -> str:
self._add_item(ref_id, ref_type, '') _, reference = self._add_item(ref_id, ref_type, '')
return latex.get_equation_code(latex_equation, ref_type, ref_id) return latex.get_equation_code(latex_equation, reference, block=True)
self._doc.append([render_to_html, render_to_latex]) self._doc.append([render_to_html, render_to_latex])
@ -596,20 +621,19 @@ class DocumentWriter():
Args: Args:
text: The markdown text to add text: The markdown text to add
section_class: The class for the text section section_class: The HTML-class and LaTeX-environment name for the text section
""" """
norm_text = _normalize_text_indent(str(text)) norm_text = _normalize_text_indent(str(text))
def render_to_html() -> str: def render_to_html() -> str:
html = self._html_post_processing(_markdown_to_html(self._equation_embedding_reescaping(norm_text))) html = _markdown_to_html(self._equation_embedding_reescaping(norm_text))
if section_class: if section_class:
return '<div class="' + section_class + '">' + html + '</div>' return '<div class="' + section_class + '">' + html + '</div>'
else: else:
return html return html
def render_to_latex() -> str: def render_to_latex() -> str:
html = _markdown_to_html( html = render_to_html()
self._equation_embedding_reescaping(norm_text))
return latex.from_html(html) return latex.from_html(html)
self._doc.append([render_to_html, render_to_latex]) self._doc.append([render_to_html, render_to_latex])
@ -635,7 +659,7 @@ class DocumentWriter():
self._base64_svgs = base64_svgs self._base64_svgs = base64_svgs
self._fig_scale = figure_scale self._fig_scale = figure_scale
return _fillin_reference_names(self._render_doc(HTML_OUTPUT), self._item_index) return self._html_post_processing(_fillin_reference_names(self._render_doc(HTML_OUTPUT), self._item_index))
def to_latex(self, font_family: Literal[None, 'serif', 'sans-serif'] = None, def to_latex(self, font_family: Literal[None, 'serif', 'sans-serif'] = None,
table_renderer: TRenderer = 'simple', figure_scale: float = 1) -> str: table_renderer: TRenderer = 'simple', figure_scale: float = 1) -> str:
@ -660,27 +684,37 @@ class DocumentWriter():
def to_pdf(self, file_path: str, def to_pdf(self, file_path: str,
font_family: Literal[None, 'serif', 'sans-serif'] = None, font_family: Literal[None, 'serif', 'sans-serif'] = None,
table_renderer: TRenderer = 'simple', table_renderer: TRenderer = 'simple',
latex_template_path: str = '') -> bool: latex_template_path: str = '',
fields_dict: dict[str, str] = {},
figure_scale: float = 1,
engine: latex.LatexEngine = 'pdflatex') -> bool:
""" """
Export the document to a PDF file using LaTeX. Export the document to a PDF file using LaTeX.
Args: Args:
file_path: The path to save the PDF file to file_path: The path to save the PDF file to
font_family: Overwrites the front family for figures and the template font_family: Overwrites the front family for figures and the template
table_renderer: The renderer for tables (simple: renderer with column type
guessing for text and numbers; pandas: using the internal pandas LaTeX renderer)
latex_template_path: Path to a LaTeX template file. The latex_template_path: Path to a LaTeX template file. The
expression <!--CONTENT--> will be replaced by the generated content. expression <!--CONTENT--> will be replaced by the generated content.
If no path is provided a default template is used. If no path is provided a default template is used.
fields_dict: A dictionary with field names as keys and content as values
replacing the placeholders <!--KEY--> in the template.
figure_scale: Scaling factor for the figure size
engine: LaTeX engine (pdflatex, lualatex, xelatex or tectonic)
Returns: Returns:
True if the PDF file was successfully created True if the PDF file was successfully created
""" """
latex_code = inject_to_template(self.to_latex(font_family, table_renderer), content = self.to_latex(font_family, table_renderer, figure_scale)
latex_code = inject_to_template({'CONTENT': content} | fields_dict,
latex_template_path, latex_template_path,
'templates/default_template.tex') internal_template='templates/default_template.tex')
if font_family == 'sans-serif': if font_family == 'sans-serif':
latex_code = latex.inject_latex_command(latex_code, '\\renewcommand{\\familydefault}{\\sfdefault}') latex_code = latex.inject_latex_command(latex_code, '\\renewcommand{\\familydefault}{\\sfdefault}')
success, errors, warnings = latex.compile(latex_code, file_path) success, errors, warnings = latex.compile(latex_code, file_path, engine=engine)
if not success: if not success:
print('Errors:') print('Errors:')

View File

@ -1,6 +1,5 @@
from html.parser import HTMLParser from html.parser import HTMLParser
from typing import Generator, Any from typing import Generator, Any, Literal, get_args, TYPE_CHECKING
from pandas.io.formats.style import Styler
import re import re
import os import os
import shutil import shutil
@ -8,9 +7,15 @@ import subprocess
import tempfile import tempfile
from .latex_escaping import unicode_to_latex_dict, latex_escape_dict from .latex_escaping import unicode_to_latex_dict, latex_escape_dict
if TYPE_CHECKING:
from pandas.io.formats.style import Styler
else:
try:
from pandas.io.formats.style import Styler
except ImportError:
Styler = None
def basic_formatter(value: Any) -> str: LatexEngine = Literal['pdflatex', 'lualatex', 'xelatex', 'tectonic']
return escape_text(str(value))
def to_ascii(text: str) -> str: def to_ascii(text: str) -> str:
@ -82,19 +87,18 @@ def escape_text(text: str) -> str:
return ''.join(ret) return ''.join(ret)
def get_equation_code(equation: str, ref_id: str, ref_type: str, block: bool = False) -> str: def get_equation_code(equation: str, reference: str | None, block: bool = False) -> str:
""" """
Converts an equation string to LaTeX code. Converts an equation string to LaTeX code.
Args: Args:
equation: The LaTeX equation string. equation: The LaTeX equation string.
ref_id: The reference ID for the equation. reference: The reference type and ID for the equation separated by a ':'.
ref_type: The type of reference (e.g., 'eq', 'fig', etc.).
""" """
if block: if block:
if ref_id: if reference:
return '\\begin{equation}\\label{%s:%s}%s\\end{equation}' % ( return '\\begin{equation}\\label{%s}%s\\end{equation}' % (
normalize_label_text(ref_type), normalize_label_text(ref_id), equation) normalize_label_text(reference), equation)
else: else:
return '\\[%s\\]' % equation return '\\[%s\\]' % equation
else: else:
@ -114,13 +118,16 @@ def render_pandas_styler_table(df_style: Styler, caption: str = '', label: str =
Returns: Returns:
The LaTeX code. The LaTeX code.
""" """
assert Styler, 'Jinja2 package is required for rendering pandas tables' # type: ignore[truthy-function]
assert isinstance(df_style, Styler), 'df_style has to be of type Styler'
def iter_table(table: dict[str, Any]) -> Generator[str, None, None]: def iter_table(table: dict[str, Any]) -> Generator[str, None, None]:
yield '\\begin{table}\n' yield '\\begin{table}\n'
if centering: if centering:
yield '\\centering\n' yield '\\centering\n'
# Guess column type # Guess column type
numeric = re.compile(r'^[<>]?\s*(?:\d+,?)+(?:\.\d+)?(?:\s\D.*)?$') numeric = re.compile('^[<>\\-\u2212]?\\s*(?:\\d+,?)+(?:\\.\\d+)?(?:\\s\\D.*)?$')
formats = ['S' if all( formats = ['S' if all(
(numeric.match(line[ci]['display_value'].strip()) for line in table['body']) (numeric.match(line[ci]['display_value'].strip()) for line in table['body'])
) else 'l' for ci in range(len(table['body'][0])) if table['body'][0][ci]['is_visible']] ) else 'l' for ci in range(len(table['body'][0])) if table['body'][0][ci]['is_visible']]
@ -184,6 +191,7 @@ def from_html(html_code: str) -> str:
self.header_flag = False self.header_flag = False
self.attr_dict: dict[str, str] = {} self.attr_dict: dict[str, str] = {}
self.equation_flag = False self.equation_flag = False
self.class_name: str = ''
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
self.attr_dict = {k: v if v else '' for k, v in attrs} self.attr_dict = {k: v if v else '' for k, v in attrs}
@ -210,6 +218,10 @@ def from_html(html_code: str) -> str:
self.latex_code.append("\n\n\\noindent\\rule[0.5ex]{\\linewidth}{1pt}\n\n") self.latex_code.append("\n\n\\noindent\\rule[0.5ex]{\\linewidth}{1pt}\n\n")
elif tag == 'latex': elif tag == 'latex':
self.equation_flag = True self.equation_flag = True
elif tag == 'div':
self.class_name = self.attr_dict.get('class', '')
if self.class_name:
self.latex_code.append(f"\n\\begin{{{self.class_name}}}\n")
def handle_endtag(self, tag: str) -> None: def handle_endtag(self, tag: str) -> None:
if tag in html_to_latex: if tag in html_to_latex:
@ -235,13 +247,15 @@ def from_html(html_code: str) -> str:
self.latex_code.append("}") self.latex_code.append("}")
elif tag == 'latex': elif tag == 'latex':
self.equation_flag = False self.equation_flag = False
elif tag == 'div':
if self.class_name:
self.latex_code.append(f"\n\\end{{{self.class_name}}}\n")
def handle_data(self, data: str) -> None: def handle_data(self, data: str) -> None:
if self.equation_flag: if self.equation_flag:
block = self.attr_dict.get('type') == 'block' block = self.attr_dict.get('type') == 'block'
ref_id = self.attr_dict.get('ref_id', '') reference = self.attr_dict.get('reference')
ref_type = self.attr_dict.get('ref_type', 'eq') self.latex_code.append(get_equation_code(data, reference, block))
self.latex_code.append(get_equation_code(data, ref_id, ref_type, block))
elif data.strip(): elif data.strip():
self.latex_code.append(escape_text(data)) self.latex_code.append(escape_text(data))
@ -250,7 +264,7 @@ def from_html(html_code: str) -> str:
return ''.join(parser.latex_code) return ''.join(parser.latex_code)
def compile(latex_code: str, output_file: str = '', encoding: str = 'utf-8') -> tuple[bool, list[str], list[str]]: def compile(latex_code: str, output_file: str = '', encoding: str = 'utf-8', engine: LatexEngine = 'pdflatex') -> tuple[bool, list[str], list[str]]:
""" """
Compiles LaTeX code to a PDF file. Compiles LaTeX code to a PDF file.
@ -258,6 +272,7 @@ def compile(latex_code: str, output_file: str = '', encoding: str = 'utf-8') ->
latex_code: The LaTeX code to compile. latex_code: The LaTeX code to compile.
output_file: The output file path. output_file: The output file path.
encoding: The encoding of the LaTeX code. encoding: The encoding of the LaTeX code.
engine: LaTeX engine (pdflatex, lualatex, xelatex or tectonic)
Returns: Returns:
A tuple with three elements: A tuple with three elements:
@ -266,8 +281,13 @@ def compile(latex_code: str, output_file: str = '', encoding: str = 'utf-8') ->
- A list of warnings. - A list of warnings.
""" """
assert engine in get_args(LatexEngine), "engine must be pdflatex, lualatex, xelatex or tectonic"
with tempfile.TemporaryDirectory() as tmp_path: with tempfile.TemporaryDirectory() as tmp_path:
command = ['pdflatex', '-halt-on-error', '--output-directory', tmp_path] if engine == 'tectonic':
command = ['tectonic', '--outdir', tmp_path, '-']
else:
command = [engine, '--halt-on-error', '--output-directory', tmp_path]
errors: list[str] = [] errors: list[str] = []
warnings: list[str] = [] warnings: list[str] = []
@ -305,6 +325,17 @@ def compile(latex_code: str, output_file: str = '', encoding: str = 'utf-8') ->
def inject_latex_command(text: str, command: str) -> str: def inject_latex_command(text: str, command: str) -> str:
"""
Injects a provided LaTeX code under the last line
starting with \\usepackage.
Args:
text: input LaTeX code
command: code to inject
Returns:
LaTeX code with injected command
"""
lines = text.splitlines() lines = text.splitlines()
last_package_index = -1 last_package_index = -1

View File

@ -68,7 +68,8 @@ unicode_to_latex_dict = {
'£': r'{\pounds}', '£': r'{\pounds}',
'¥': r'{\yen}', '¥': r'{\yen}',
'\u00A0': r'~', # Non-breaking space '\u00A0': r'~', # Non-breaking space
'\u2007': ' ' # Figure space '\u2007': ' ', # Figure space
'\u2212': '-' # Unicode minus sign
} }
latex_escape_dict = { latex_escape_dict = {

0
src/pyladoc/py.typed Normal file
View File

38
src/pyladoc/svg_tools.py Normal file
View File

@ -0,0 +1,38 @@
import re
from re import Match
def update_svg_ids(input_svg: str, unique_id: str) -> str:
"""Add a unique ID part to all svg IDs and update references ti these IDs"""
id_mapping: dict[str, str] = {}
def update_ids(match: Match[str]) -> str:
old_id = match.group(1)
new_id = f"svg-{unique_id}-{old_id}"
id_mapping[old_id] = new_id
return f' id="{new_id}"'
def update_references(match: Match[str]) -> str:
old_ref = match.group(1)
new_ref = id_mapping.get(old_ref, old_ref)
if match.group(0).startswith('xlink:href'):
return f'xlink:href="#{new_ref}"'
else:
return f'url(#{new_ref})'
# Update IDs
svg_string = re.sub(r'\sid="(.*?)"', update_ids, input_svg)
# Update references to IDs
svg_string = re.sub(r'url\(#([^\)]+)\)', update_references, svg_string)
svg_string = re.sub(r'xlink:href="#([^\"]+)"', update_references, svg_string)
return svg_string
def clean_svg(svg_text: str) -> str:
# remove all tags not alllowd for inline svg from metadata:
svg_text = re.sub(r'<metadata>.*?</metadata>', '', svg_text, flags=re.DOTALL)
# remove illegal path-tags without d attribute:
return re.sub(r'<path(?![^>]*\sd=)\s.*?/>', '', svg_text, flags=re.DOTALL)

View File

@ -16,11 +16,17 @@
\usepackage{booktabs} % For professional-looking tables \usepackage{booktabs} % For professional-looking tables
\usepackage{pgf} % For using pgf grafics \usepackage{pgf} % For using pgf grafics
\usepackage{textcomp, gensymb} % provides \degree symbol \usepackage{textcomp, gensymb} % provides \degree symbol
\usepackage{xcolor} % For colored text
\sisetup{ \sisetup{
table-align-text-post = false table-align-text-post = false
} }
% Define fine print environment
\newenvironment{fineprint}
{\par\vspace{0.5\baselineskip}\noindent\footnotesize\color{gray}}
{\par\vspace{0.5\baselineskip}}
% Geometry Settings % Geometry Settings
\geometry{margin=1in} % 1-inch margins \geometry{margin=1in} % 1-inch margins

View File

@ -21,6 +21,12 @@
padding-bottom: 50px; padding-bottom: 50px;
} }
div.fineprint
{
font-size: smaller;
color: grey;
}
div h1 div h1
{ {
font-size: 32px; font-size: 32px;

0
tests/__init__.py Normal file
View File

View File

@ -2,10 +2,7 @@ from typing import Generator, Any
from lxml import etree from lxml import etree
from lxml.etree import _Element as EElement # type: ignore from lxml.etree import _Element as EElement # type: ignore
import requests import requests
import pyladoc
with open('src/pyladoc/templates/test_template.html', mode='rt', encoding='utf-8') as f:
html_test_template = f.read()
def add_line_numbers(multiline_string: str) -> str: def add_line_numbers(multiline_string: str) -> str:
@ -53,7 +50,7 @@ def validate_html(html_string: str, validate_online: bool = False, check_for: li
assert tag_type in tags, f"Tag {tag_type} not found in the html code" assert tag_type in tags, f"Tag {tag_type} not found in the html code"
if validate_online: if validate_online:
test_page = html_test_template.replace('<!--CONTENT-->', html_string) test_page = pyladoc.inject_to_template({'CONTENT': html_string}, internal_template='templates/test_template.html')
validation_result = validate_html_with_w3c(test_page) validation_result = validate_html_with_w3c(test_page)
assert 'messages' in validation_result, 'Validate request failed' assert 'messages' in validation_result, 'Validate request failed'
if validation_result['messages']: if validation_result['messages']:

3
tests/out/README.md Normal file
View File

@ -0,0 +1,3 @@
# Rendering Test Outputs
This is the target directory for the test renderings.

File diff suppressed because it is too large Load Diff

View File

@ -1,613 +0,0 @@
\section{Thermal Conductivity of Mixtures}
The determination of the thermal conductivity of gas mixtures is a central aspect of modeling
transport phenomena, particularly in high-temperature and high-pressure processes. Among the
most established approaches is the empirical equation introduced by Wassiljewa, which was
subsequently refined by Mason and Saxena to improve its applicability to multicomponent systems.
This model offers a reliable means of estimating the thermal conductivity of gas mixtures based
on the properties of the pure components and their molar interactions.
The thermal conductivity of a gas mixture, denoted by \(\lambda_{\text{mix}}\), can expressed as
shown in equation \ref{eq:lambda-mixture}.
\begin{equation}\label{eq:lambda-mixture}\lambda_{ ext{mix}} = \sum_{i=1}^{n} \frac{x_i \lambda_i}{\sum_{j=1}^{n} x_j \Phi_{ij}}\end{equation}
In this equation, \(x_i\) represents the molar fraction of component \(i\) within the mixture,
while \(\lambda_i\) denotes the thermal conductivity of the pure substance \(i\). The denominator
contains the interaction parameter \(\Phi_{ij}\), which describes the influence of component
\(j\) on the transport properties of component \(i\).
The interaction parameter \(\Phi_{ij}\) is given by the relation shown in equation \ref{eq:interaction-parameter}.
\begin{equation}\label{eq:interaction-parameter}\Phi_{ij} = \frac{1}{\sqrt{8}} \left(1 + \frac{M_i}{M_j} \right)^{-1/2} \left[ 1 + \left( \frac{\lambda_i}{\lambda_j} \right)^{1/2} \left( \frac{M_j}{M_i} \right)^{1/4} \right]^2\end{equation}
Here, \(M_i\) and \(M_j\) are the molar masses of the components \(i\) and \(j\), respectively.
Molar masses and thermal conductivity of the pure substances are listed in table \ref{table:gas-probs}.
The structure of this expression illustrates the nonlinear dependence of the interaction term on
both the molar mass ratio and the square root of the conductivity ratio of the involved species.
\begin{table}
\centering
\caption{Properties of some gases}
\label{table:gas-probs}
\begin{tabular}{lSS}
\toprule
\text{Gas} & \text{Molar mass in g/mol} & \text{Thermal conductivity in W/m/K} \\
\midrule
H2 & 2.016 & 0.1805 \\
O2 & 32.00 & 0.0263 \\
N2 & 28.02 & 0.0258 \\
CO2 & 44.01 & 0.0166 \\
CH4 & 16.04 & 0.0341 \\
Ar & 39.95 & 0.0177 \\
He & 4.0026 & 0.1513 \\
\bottomrule
\end{tabular}
\end{table}This formulation acknowledges that the transport properties of a gas mixture are not a simple
linear combination of the individual conductivities. Rather, they are governed by intermolecular
interactions, which affect the energy exchange and diffusion behavior of each component. These
interactions are particularly significant at elevated pressures or in cases where the gas components
exhibit widely differing molecular masses or transport properties.
The equation proposed by Wassiljewa and refined by Mason and Saxena assumes that binary interactions
dominate the behavior of the mixture, while higher-order (three-body or more) interactions are
neglected. It also presumes that the gases approximate ideal behavior, although in practical
applications, moderate deviations from ideality are tolerated without significant loss of accuracy.
In figure \ref{fig:mixture} the resulting thermal conductivity of an H2/CO2-mixture is shown.
\begin{figure}
\centering
\begingroup%
\makeatletter%
\begin{pgfpicture}%
\pgfpathrectangle{\pgfpointorigin}{\pgfqpoint{6.400000in}{4.800000in}}%
\pgfusepath{use as bounding box, clip}%
\begin{pgfscope}%
\pgfsetbuttcap%
\pgfsetmiterjoin%
\definecolor{currentfill}{rgb}{1.000000,1.000000,1.000000}%
\pgfsetfillcolor{currentfill}%
\pgfsetlinewidth{0.000000pt}%
\definecolor{currentstroke}{rgb}{1.000000,1.000000,1.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfpathmoveto{\pgfqpoint{0.000000in}{0.000000in}}%
\pgfpathlineto{\pgfqpoint{6.400000in}{0.000000in}}%
\pgfpathlineto{\pgfqpoint{6.400000in}{4.800000in}}%
\pgfpathlineto{\pgfqpoint{0.000000in}{4.800000in}}%
\pgfpathlineto{\pgfqpoint{0.000000in}{0.000000in}}%
\pgfpathclose%
\pgfusepath{fill}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetbuttcap%
\pgfsetmiterjoin%
\definecolor{currentfill}{rgb}{1.000000,1.000000,1.000000}%
\pgfsetfillcolor{currentfill}%
\pgfsetlinewidth{0.000000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetstrokeopacity{0.000000}%
\pgfsetdash{}{0pt}%
\pgfpathmoveto{\pgfqpoint{0.800000in}{0.528000in}}%
\pgfpathlineto{\pgfqpoint{5.760000in}{0.528000in}}%
\pgfpathlineto{\pgfqpoint{5.760000in}{4.224000in}}%
\pgfpathlineto{\pgfqpoint{0.800000in}{4.224000in}}%
\pgfpathlineto{\pgfqpoint{0.800000in}{0.528000in}}%
\pgfpathclose%
\pgfusepath{fill}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetbuttcap%
\pgfsetroundjoin%
\definecolor{currentfill}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetfillcolor{currentfill}%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfsys@defobject{currentmarker}{\pgfqpoint{0.000000in}{-0.048611in}}{\pgfqpoint{0.000000in}{0.000000in}}{%
\pgfpathmoveto{\pgfqpoint{0.000000in}{0.000000in}}%
\pgfpathlineto{\pgfqpoint{0.000000in}{-0.048611in}}%
\pgfusepath{stroke,fill}%
}%
\begin{pgfscope}%
\pgfsys@transformshift{1.025455in}{0.528000in}%
\pgfsys@useobject{currentmarker}{}%
\end{pgfscope}%
\end{pgfscope}%
\begin{pgfscope}%
\definecolor{textcolor}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{textcolor}%
\pgfsetfillcolor{textcolor}%
\pgftext[x=1.025455in,y=0.430778in,,top]{\color{textcolor}{\sffamily\fontsize{10.000000}{12.000000}\selectfont\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}\catcode`\%=\active\def%{\%}0}}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetbuttcap%
\pgfsetroundjoin%
\definecolor{currentfill}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetfillcolor{currentfill}%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfsys@defobject{currentmarker}{\pgfqpoint{0.000000in}{-0.048611in}}{\pgfqpoint{0.000000in}{0.000000in}}{%
\pgfpathmoveto{\pgfqpoint{0.000000in}{0.000000in}}%
\pgfpathlineto{\pgfqpoint{0.000000in}{-0.048611in}}%
\pgfusepath{stroke,fill}%
}%
\begin{pgfscope}%
\pgfsys@transformshift{1.927273in}{0.528000in}%
\pgfsys@useobject{currentmarker}{}%
\end{pgfscope}%
\end{pgfscope}%
\begin{pgfscope}%
\definecolor{textcolor}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{textcolor}%
\pgfsetfillcolor{textcolor}%
\pgftext[x=1.927273in,y=0.430778in,,top]{\color{textcolor}{\sffamily\fontsize{10.000000}{12.000000}\selectfont\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}\catcode`\%=\active\def%{\%}20}}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetbuttcap%
\pgfsetroundjoin%
\definecolor{currentfill}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetfillcolor{currentfill}%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfsys@defobject{currentmarker}{\pgfqpoint{0.000000in}{-0.048611in}}{\pgfqpoint{0.000000in}{0.000000in}}{%
\pgfpathmoveto{\pgfqpoint{0.000000in}{0.000000in}}%
\pgfpathlineto{\pgfqpoint{0.000000in}{-0.048611in}}%
\pgfusepath{stroke,fill}%
}%
\begin{pgfscope}%
\pgfsys@transformshift{2.829091in}{0.528000in}%
\pgfsys@useobject{currentmarker}{}%
\end{pgfscope}%
\end{pgfscope}%
\begin{pgfscope}%
\definecolor{textcolor}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{textcolor}%
\pgfsetfillcolor{textcolor}%
\pgftext[x=2.829091in,y=0.430778in,,top]{\color{textcolor}{\sffamily\fontsize{10.000000}{12.000000}\selectfont\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}\catcode`\%=\active\def%{\%}40}}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetbuttcap%
\pgfsetroundjoin%
\definecolor{currentfill}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetfillcolor{currentfill}%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfsys@defobject{currentmarker}{\pgfqpoint{0.000000in}{-0.048611in}}{\pgfqpoint{0.000000in}{0.000000in}}{%
\pgfpathmoveto{\pgfqpoint{0.000000in}{0.000000in}}%
\pgfpathlineto{\pgfqpoint{0.000000in}{-0.048611in}}%
\pgfusepath{stroke,fill}%
}%
\begin{pgfscope}%
\pgfsys@transformshift{3.730909in}{0.528000in}%
\pgfsys@useobject{currentmarker}{}%
\end{pgfscope}%
\end{pgfscope}%
\begin{pgfscope}%
\definecolor{textcolor}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{textcolor}%
\pgfsetfillcolor{textcolor}%
\pgftext[x=3.730909in,y=0.430778in,,top]{\color{textcolor}{\sffamily\fontsize{10.000000}{12.000000}\selectfont\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}\catcode`\%=\active\def%{\%}60}}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetbuttcap%
\pgfsetroundjoin%
\definecolor{currentfill}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetfillcolor{currentfill}%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfsys@defobject{currentmarker}{\pgfqpoint{0.000000in}{-0.048611in}}{\pgfqpoint{0.000000in}{0.000000in}}{%
\pgfpathmoveto{\pgfqpoint{0.000000in}{0.000000in}}%
\pgfpathlineto{\pgfqpoint{0.000000in}{-0.048611in}}%
\pgfusepath{stroke,fill}%
}%
\begin{pgfscope}%
\pgfsys@transformshift{4.632727in}{0.528000in}%
\pgfsys@useobject{currentmarker}{}%
\end{pgfscope}%
\end{pgfscope}%
\begin{pgfscope}%
\definecolor{textcolor}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{textcolor}%
\pgfsetfillcolor{textcolor}%
\pgftext[x=4.632727in,y=0.430778in,,top]{\color{textcolor}{\sffamily\fontsize{10.000000}{12.000000}\selectfont\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}\catcode`\%=\active\def%{\%}80}}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetbuttcap%
\pgfsetroundjoin%
\definecolor{currentfill}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetfillcolor{currentfill}%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfsys@defobject{currentmarker}{\pgfqpoint{0.000000in}{-0.048611in}}{\pgfqpoint{0.000000in}{0.000000in}}{%
\pgfpathmoveto{\pgfqpoint{0.000000in}{0.000000in}}%
\pgfpathlineto{\pgfqpoint{0.000000in}{-0.048611in}}%
\pgfusepath{stroke,fill}%
}%
\begin{pgfscope}%
\pgfsys@transformshift{5.534545in}{0.528000in}%
\pgfsys@useobject{currentmarker}{}%
\end{pgfscope}%
\end{pgfscope}%
\begin{pgfscope}%
\definecolor{textcolor}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{textcolor}%
\pgfsetfillcolor{textcolor}%
\pgftext[x=5.534545in,y=0.430778in,,top]{\color{textcolor}{\sffamily\fontsize{10.000000}{12.000000}\selectfont\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}\catcode`\%=\active\def%{\%}100}}%
\end{pgfscope}%
\begin{pgfscope}%
\definecolor{textcolor}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{textcolor}%
\pgfsetfillcolor{textcolor}%
\pgftext[x=3.280000in,y=0.240809in,,top]{\color{textcolor}{\sffamily\fontsize{10.000000}{12.000000}\selectfont\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}\catcode`\%=\active\def%{\%}H2 molar fraction / %}}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetbuttcap%
\pgfsetroundjoin%
\definecolor{currentfill}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetfillcolor{currentfill}%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfsys@defobject{currentmarker}{\pgfqpoint{-0.048611in}{0.000000in}}{\pgfqpoint{-0.000000in}{0.000000in}}{%
\pgfpathmoveto{\pgfqpoint{-0.000000in}{0.000000in}}%
\pgfpathlineto{\pgfqpoint{-0.048611in}{0.000000in}}%
\pgfusepath{stroke,fill}%
}%
\begin{pgfscope}%
\pgfsys@transformshift{0.800000in}{0.868203in}%
\pgfsys@useobject{currentmarker}{}%
\end{pgfscope}%
\end{pgfscope}%
\begin{pgfscope}%
\definecolor{textcolor}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{textcolor}%
\pgfsetfillcolor{textcolor}%
\pgftext[x=0.305168in, y=0.815441in, left, base]{\color{textcolor}{\sffamily\fontsize{10.000000}{12.000000}\selectfont\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}\catcode`\%=\active\def%{\%}0.025}}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetbuttcap%
\pgfsetroundjoin%
\definecolor{currentfill}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetfillcolor{currentfill}%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfsys@defobject{currentmarker}{\pgfqpoint{-0.048611in}{0.000000in}}{\pgfqpoint{-0.000000in}{0.000000in}}{%
\pgfpathmoveto{\pgfqpoint{-0.000000in}{0.000000in}}%
\pgfpathlineto{\pgfqpoint{-0.048611in}{0.000000in}}%
\pgfusepath{stroke,fill}%
}%
\begin{pgfscope}%
\pgfsys@transformshift{0.800000in}{1.380710in}%
\pgfsys@useobject{currentmarker}{}%
\end{pgfscope}%
\end{pgfscope}%
\begin{pgfscope}%
\definecolor{textcolor}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{textcolor}%
\pgfsetfillcolor{textcolor}%
\pgftext[x=0.305168in, y=1.327949in, left, base]{\color{textcolor}{\sffamily\fontsize{10.000000}{12.000000}\selectfont\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}\catcode`\%=\active\def%{\%}0.050}}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetbuttcap%
\pgfsetroundjoin%
\definecolor{currentfill}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetfillcolor{currentfill}%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfsys@defobject{currentmarker}{\pgfqpoint{-0.048611in}{0.000000in}}{\pgfqpoint{-0.000000in}{0.000000in}}{%
\pgfpathmoveto{\pgfqpoint{-0.000000in}{0.000000in}}%
\pgfpathlineto{\pgfqpoint{-0.048611in}{0.000000in}}%
\pgfusepath{stroke,fill}%
}%
\begin{pgfscope}%
\pgfsys@transformshift{0.800000in}{1.893218in}%
\pgfsys@useobject{currentmarker}{}%
\end{pgfscope}%
\end{pgfscope}%
\begin{pgfscope}%
\definecolor{textcolor}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{textcolor}%
\pgfsetfillcolor{textcolor}%
\pgftext[x=0.305168in, y=1.840456in, left, base]{\color{textcolor}{\sffamily\fontsize{10.000000}{12.000000}\selectfont\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}\catcode`\%=\active\def%{\%}0.075}}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetbuttcap%
\pgfsetroundjoin%
\definecolor{currentfill}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetfillcolor{currentfill}%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfsys@defobject{currentmarker}{\pgfqpoint{-0.048611in}{0.000000in}}{\pgfqpoint{-0.000000in}{0.000000in}}{%
\pgfpathmoveto{\pgfqpoint{-0.000000in}{0.000000in}}%
\pgfpathlineto{\pgfqpoint{-0.048611in}{0.000000in}}%
\pgfusepath{stroke,fill}%
}%
\begin{pgfscope}%
\pgfsys@transformshift{0.800000in}{2.405725in}%
\pgfsys@useobject{currentmarker}{}%
\end{pgfscope}%
\end{pgfscope}%
\begin{pgfscope}%
\definecolor{textcolor}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{textcolor}%
\pgfsetfillcolor{textcolor}%
\pgftext[x=0.305168in, y=2.352964in, left, base]{\color{textcolor}{\sffamily\fontsize{10.000000}{12.000000}\selectfont\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}\catcode`\%=\active\def%{\%}0.100}}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetbuttcap%
\pgfsetroundjoin%
\definecolor{currentfill}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetfillcolor{currentfill}%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfsys@defobject{currentmarker}{\pgfqpoint{-0.048611in}{0.000000in}}{\pgfqpoint{-0.000000in}{0.000000in}}{%
\pgfpathmoveto{\pgfqpoint{-0.000000in}{0.000000in}}%
\pgfpathlineto{\pgfqpoint{-0.048611in}{0.000000in}}%
\pgfusepath{stroke,fill}%
}%
\begin{pgfscope}%
\pgfsys@transformshift{0.800000in}{2.918233in}%
\pgfsys@useobject{currentmarker}{}%
\end{pgfscope}%
\end{pgfscope}%
\begin{pgfscope}%
\definecolor{textcolor}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{textcolor}%
\pgfsetfillcolor{textcolor}%
\pgftext[x=0.305168in, y=2.865472in, left, base]{\color{textcolor}{\sffamily\fontsize{10.000000}{12.000000}\selectfont\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}\catcode`\%=\active\def%{\%}0.125}}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetbuttcap%
\pgfsetroundjoin%
\definecolor{currentfill}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetfillcolor{currentfill}%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfsys@defobject{currentmarker}{\pgfqpoint{-0.048611in}{0.000000in}}{\pgfqpoint{-0.000000in}{0.000000in}}{%
\pgfpathmoveto{\pgfqpoint{-0.000000in}{0.000000in}}%
\pgfpathlineto{\pgfqpoint{-0.048611in}{0.000000in}}%
\pgfusepath{stroke,fill}%
}%
\begin{pgfscope}%
\pgfsys@transformshift{0.800000in}{3.430741in}%
\pgfsys@useobject{currentmarker}{}%
\end{pgfscope}%
\end{pgfscope}%
\begin{pgfscope}%
\definecolor{textcolor}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{textcolor}%
\pgfsetfillcolor{textcolor}%
\pgftext[x=0.305168in, y=3.377979in, left, base]{\color{textcolor}{\sffamily\fontsize{10.000000}{12.000000}\selectfont\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}\catcode`\%=\active\def%{\%}0.150}}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetbuttcap%
\pgfsetroundjoin%
\definecolor{currentfill}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetfillcolor{currentfill}%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfsys@defobject{currentmarker}{\pgfqpoint{-0.048611in}{0.000000in}}{\pgfqpoint{-0.000000in}{0.000000in}}{%
\pgfpathmoveto{\pgfqpoint{-0.000000in}{0.000000in}}%
\pgfpathlineto{\pgfqpoint{-0.048611in}{0.000000in}}%
\pgfusepath{stroke,fill}%
}%
\begin{pgfscope}%
\pgfsys@transformshift{0.800000in}{3.943248in}%
\pgfsys@useobject{currentmarker}{}%
\end{pgfscope}%
\end{pgfscope}%
\begin{pgfscope}%
\definecolor{textcolor}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{textcolor}%
\pgfsetfillcolor{textcolor}%
\pgftext[x=0.305168in, y=3.890487in, left, base]{\color{textcolor}{\sffamily\fontsize{10.000000}{12.000000}\selectfont\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}\catcode`\%=\active\def%{\%}0.175}}%
\end{pgfscope}%
\begin{pgfscope}%
\definecolor{textcolor}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{textcolor}%
\pgfsetfillcolor{textcolor}%
\pgftext[x=0.249612in,y=2.376000in,,bottom,rotate=90.000000]{\color{textcolor}{\sffamily\fontsize{10.000000}{12.000000}\selectfont\catcode`\^=\active\def^{\ifmmode\sp\else\^{}\fi}\catcode`\%=\active\def%{\%}Thermal Conductivity / (W/m·K)}}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfpathrectangle{\pgfqpoint{0.800000in}{0.528000in}}{\pgfqpoint{4.960000in}{3.696000in}}%
\pgfusepath{clip}%
\pgfsetrectcap%
\pgfsetroundjoin%
\pgfsetlinewidth{1.505625pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfpathmoveto{\pgfqpoint{1.025455in}{0.696000in}}%
\pgfpathlineto{\pgfqpoint{1.071001in}{0.697319in}}%
\pgfpathlineto{\pgfqpoint{1.116547in}{0.698664in}}%
\pgfpathlineto{\pgfqpoint{1.162094in}{0.700036in}}%
\pgfpathlineto{\pgfqpoint{1.207640in}{0.701436in}}%
\pgfpathlineto{\pgfqpoint{1.253186in}{0.702865in}}%
\pgfpathlineto{\pgfqpoint{1.298733in}{0.704324in}}%
\pgfpathlineto{\pgfqpoint{1.344279in}{0.705814in}}%
\pgfpathlineto{\pgfqpoint{1.389826in}{0.707335in}}%
\pgfpathlineto{\pgfqpoint{1.435372in}{0.708889in}}%
\pgfpathlineto{\pgfqpoint{1.480918in}{0.710476in}}%
\pgfpathlineto{\pgfqpoint{1.526465in}{0.712098in}}%
\pgfpathlineto{\pgfqpoint{1.572011in}{0.713757in}}%
\pgfpathlineto{\pgfqpoint{1.617557in}{0.715452in}}%
\pgfpathlineto{\pgfqpoint{1.663104in}{0.717186in}}%
\pgfpathlineto{\pgfqpoint{1.708650in}{0.718960in}}%
\pgfpathlineto{\pgfqpoint{1.754197in}{0.720775in}}%
\pgfpathlineto{\pgfqpoint{1.799743in}{0.722632in}}%
\pgfpathlineto{\pgfqpoint{1.845289in}{0.724534in}}%
\pgfpathlineto{\pgfqpoint{1.890836in}{0.726482in}}%
\pgfpathlineto{\pgfqpoint{1.936382in}{0.728476in}}%
\pgfpathlineto{\pgfqpoint{1.981928in}{0.730520in}}%
\pgfpathlineto{\pgfqpoint{2.027475in}{0.732615in}}%
\pgfpathlineto{\pgfqpoint{2.073021in}{0.734763in}}%
\pgfpathlineto{\pgfqpoint{2.118567in}{0.736966in}}%
\pgfpathlineto{\pgfqpoint{2.164114in}{0.739226in}}%
\pgfpathlineto{\pgfqpoint{2.209660in}{0.741545in}}%
\pgfpathlineto{\pgfqpoint{2.255207in}{0.743926in}}%
\pgfpathlineto{\pgfqpoint{2.300753in}{0.746371in}}%
\pgfpathlineto{\pgfqpoint{2.346299in}{0.748883in}}%
\pgfpathlineto{\pgfqpoint{2.391846in}{0.751464in}}%
\pgfpathlineto{\pgfqpoint{2.437392in}{0.754118in}}%
\pgfpathlineto{\pgfqpoint{2.482938in}{0.756847in}}%
\pgfpathlineto{\pgfqpoint{2.528485in}{0.759656in}}%
\pgfpathlineto{\pgfqpoint{2.574031in}{0.762546in}}%
\pgfpathlineto{\pgfqpoint{2.619578in}{0.765523in}}%
\pgfpathlineto{\pgfqpoint{2.665124in}{0.768589in}}%
\pgfpathlineto{\pgfqpoint{2.710670in}{0.771749in}}%
\pgfpathlineto{\pgfqpoint{2.756217in}{0.775008in}}%
\pgfpathlineto{\pgfqpoint{2.801763in}{0.778370in}}%
\pgfpathlineto{\pgfqpoint{2.847309in}{0.781840in}}%
\pgfpathlineto{\pgfqpoint{2.892856in}{0.785423in}}%
\pgfpathlineto{\pgfqpoint{2.938402in}{0.789124in}}%
\pgfpathlineto{\pgfqpoint{2.983949in}{0.792951in}}%
\pgfpathlineto{\pgfqpoint{3.029495in}{0.796909in}}%
\pgfpathlineto{\pgfqpoint{3.075041in}{0.801005in}}%
\pgfpathlineto{\pgfqpoint{3.120588in}{0.805247in}}%
\pgfpathlineto{\pgfqpoint{3.166134in}{0.809642in}}%
\pgfpathlineto{\pgfqpoint{3.211680in}{0.814198in}}%
\pgfpathlineto{\pgfqpoint{3.257227in}{0.818926in}}%
\pgfpathlineto{\pgfqpoint{3.302773in}{0.823834in}}%
\pgfpathlineto{\pgfqpoint{3.348320in}{0.828933in}}%
\pgfpathlineto{\pgfqpoint{3.393866in}{0.834235in}}%
\pgfpathlineto{\pgfqpoint{3.439412in}{0.839752in}}%
\pgfpathlineto{\pgfqpoint{3.484959in}{0.845496in}}%
\pgfpathlineto{\pgfqpoint{3.530505in}{0.851484in}}%
\pgfpathlineto{\pgfqpoint{3.576051in}{0.857729in}}%
\pgfpathlineto{\pgfqpoint{3.621598in}{0.864249in}}%
\pgfpathlineto{\pgfqpoint{3.667144in}{0.871063in}}%
\pgfpathlineto{\pgfqpoint{3.712691in}{0.878191in}}%
\pgfpathlineto{\pgfqpoint{3.758237in}{0.885655in}}%
\pgfpathlineto{\pgfqpoint{3.803783in}{0.893479in}}%
\pgfpathlineto{\pgfqpoint{3.849330in}{0.901690in}}%
\pgfpathlineto{\pgfqpoint{3.894876in}{0.910317in}}%
\pgfpathlineto{\pgfqpoint{3.940422in}{0.919392in}}%
\pgfpathlineto{\pgfqpoint{3.985969in}{0.928951in}}%
\pgfpathlineto{\pgfqpoint{4.031515in}{0.939034in}}%
\pgfpathlineto{\pgfqpoint{4.077062in}{0.949685in}}%
\pgfpathlineto{\pgfqpoint{4.122608in}{0.960953in}}%
\pgfpathlineto{\pgfqpoint{4.168154in}{0.972893in}}%
\pgfpathlineto{\pgfqpoint{4.213701in}{0.985566in}}%
\pgfpathlineto{\pgfqpoint{4.259247in}{0.999042in}}%
\pgfpathlineto{\pgfqpoint{4.304793in}{1.013399in}}%
\pgfpathlineto{\pgfqpoint{4.350340in}{1.028727in}}%
\pgfpathlineto{\pgfqpoint{4.395886in}{1.045125in}}%
\pgfpathlineto{\pgfqpoint{4.441433in}{1.062710in}}%
\pgfpathlineto{\pgfqpoint{4.486979in}{1.081614in}}%
\pgfpathlineto{\pgfqpoint{4.532525in}{1.101991in}}%
\pgfpathlineto{\pgfqpoint{4.578072in}{1.124018in}}%
\pgfpathlineto{\pgfqpoint{4.623618in}{1.147903in}}%
\pgfpathlineto{\pgfqpoint{4.669164in}{1.173889in}}%
\pgfpathlineto{\pgfqpoint{4.714711in}{1.202263in}}%
\pgfpathlineto{\pgfqpoint{4.760257in}{1.233369in}}%
\pgfpathlineto{\pgfqpoint{4.805803in}{1.267615in}}%
\pgfpathlineto{\pgfqpoint{4.851350in}{1.305499in}}%
\pgfpathlineto{\pgfqpoint{4.896896in}{1.347625in}}%
\pgfpathlineto{\pgfqpoint{4.942443in}{1.394741in}}%
\pgfpathlineto{\pgfqpoint{4.987989in}{1.447778in}}%
\pgfpathlineto{\pgfqpoint{5.033535in}{1.507912in}}%
\pgfpathlineto{\pgfqpoint{5.079082in}{1.576651in}}%
\pgfpathlineto{\pgfqpoint{5.124628in}{1.655955in}}%
\pgfpathlineto{\pgfqpoint{5.170174in}{1.748426in}}%
\pgfpathlineto{\pgfqpoint{5.215721in}{1.857582in}}%
\pgfpathlineto{\pgfqpoint{5.261267in}{1.988306in}}%
\pgfpathlineto{\pgfqpoint{5.306814in}{2.147564in}}%
\pgfpathlineto{\pgfqpoint{5.352360in}{2.345643in}}%
\pgfpathlineto{\pgfqpoint{5.397906in}{2.598377in}}%
\pgfpathlineto{\pgfqpoint{5.443453in}{2.931436in}}%
\pgfpathlineto{\pgfqpoint{5.488999in}{3.389276in}}%
\pgfpathlineto{\pgfqpoint{5.534545in}{4.056000in}}%
\pgfusepath{stroke}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetrectcap%
\pgfsetmiterjoin%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfpathmoveto{\pgfqpoint{0.800000in}{0.528000in}}%
\pgfpathlineto{\pgfqpoint{0.800000in}{4.224000in}}%
\pgfusepath{stroke}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetrectcap%
\pgfsetmiterjoin%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfpathmoveto{\pgfqpoint{5.760000in}{0.528000in}}%
\pgfpathlineto{\pgfqpoint{5.760000in}{4.224000in}}%
\pgfusepath{stroke}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetrectcap%
\pgfsetmiterjoin%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfpathmoveto{\pgfqpoint{0.800000in}{0.528000in}}%
\pgfpathlineto{\pgfqpoint{5.760000in}{0.528000in}}%
\pgfusepath{stroke}%
\end{pgfscope}%
\begin{pgfscope}%
\pgfsetrectcap%
\pgfsetmiterjoin%
\pgfsetlinewidth{0.803000pt}%
\definecolor{currentstroke}{rgb}{0.000000,0.000000,0.000000}%
\pgfsetstrokecolor{currentstroke}%
\pgfsetdash{}{0pt}%
\pgfpathmoveto{\pgfqpoint{0.800000in}{4.224000in}}%
\pgfpathlineto{\pgfqpoint{5.760000in}{4.224000in}}%
\pgfusepath{stroke}%
\end{pgfscope}%
\end{pgfpicture}%
\makeatother%
\endgroup%
\caption{Thermal Conductivity of H2/CO2 mixtures}
\label{fig:mixture}
\end{figure}In engineering practice, the accurate determination of \(\lambda_{\text{mix}}\) is essential
for the prediction of heat transfer in systems such as membrane modules, chemical reactors, and
combustion chambers. In the context of membrane-based gas separation, for instance, the thermal
conductivity of the gas mixture influences the local temperature distribution, which in turn affects
both the permeation behavior and the structural stability of the membrane.
It is important to note that the calculated mixture conductivity reflects only the gas phase
behavior. In porous systems such as carbon membranes, additional effects must be considered.
These include the solid-phase thermal conduction through the membrane matrix, radiative transport
in pore channels at high temperatures, and transport in the Knudsen regime for narrow pores.
To account for these complexities, models based on effective medium theory, such as those of
Maxwell-Eucken or Bruggeman, are frequently employed. These models combine the conductivities of
individual phases (gas and solid) with geometrical factors that reflect the morphology of the
porous structure.
\noindent\rule[0.5ex]{\linewidth}{1pt}
Expanded by more or less sensible AI jabbering; based on: \href{https://doi.org/10.14279/depositonce-7390}{doi:10.14279/depositonce-7390}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +0,0 @@
<h1>Special caracters</h1>
<p>Umlaute: ÖÄÜ öäü</p>
<p>Other: ß, €, @, $, %, ~, µ</p>
<p>Units: m³, cm²</p>
<p>Controll characters: <, >, ", ', &, |, /, \</p>

View File

@ -1,39 +0,0 @@
<h1>Source Equations</h1>
<ol>
<li>$4(3x + 2) - 5(x - 1) = 3x + 14$</li>
<li>$
rac{2y + 5}{4} +
rac{3y - 1}{2} = 5$</li>
<li>$
rac{5}{x + 2} +
rac{2}{x - 2} = 3$</li>
<li>$8(3b - 5) + 4(b + 2) = 60$</li>
<li>$2c^2 - 3c - 5 = 0$</li>
<li>$4(2d - 1) + 5(3d + 2) = 7d + 28$</li>
<li>$q^2 + 6q + 9 = 16$</li>
</ol>
<h1>Result Equations</h1>
<ol>
<li>$x =
rac{1}{4}$</li>
<li>$y =
rac{17}{8}$</li>
<li>$z =
rac{7}{3}$</li>
<li>$x = 1$ or $x = -6$</li>
<li>$a =
rac{1}{3}$ or $a = 2$</li>
<li>$x = -
rac{2}{3}$ or $x = 3$</li>
<li>$b =
rac{23}{7}$</li>
</ol>
<h1>Step by Step</h1>
<ol>
<li>Distribute: $12x + 8 - 5x + 5 = 3x + 14$</li>
<li>Combine like terms: $7x + 13 = 3x + 14$</li>
<li>Subtract $3x$: $4x + 13 = 14$</li>
<li>Subtract $13$: $4x = 1$</li>
<li>Divide by $4$: $x =
rac{1}{4}$</li>
</ol>

View File

@ -1,44 +0,0 @@
<p>Below is an in-depth explanation of the AArch64 (ARM64)
unconditional branch instruction—often simply called the
“B” instruction—and how its 26bit immediate field (imm26)
is laid out and later relocated during linking.</p>
<hr></hr>
<h2>Instruction Layout</h2>
<p>The unconditional branch in AArch64 is encoded in a 32bit
instruction. Its layout is as follows:</p>
<pre><code>Bits: 31 26 25 0
+-------------+------------------------------+
| Opcode | imm26 |
+-------------+------------------------------+
</code></pre>
<ul>
<li><strong>Opcode (bits 31:26):</strong></li>
<li>For a plain branch (<code>B</code>), the opcode is <code>000101</code>.</li>
<li>
<p>For a branch with link (<code>BL</code>), which saves the return
address (i.e., a call), the opcode is <code>100101</code>.
These 6 bits determine the instruction type.</p>
</li>
<li>
<p><strong>Immediate Field (imm26, bits 25:0):</strong></p>
</li>
<li>This 26bit field holds a signed immediate value.</li>
<li>
<p><strong>Offset Calculation:</strong> At runtime, the processor:</p>
<ol>
<li><strong>Shifts</strong> the 26bit immediate left by 2 bits.
(Because instructions are 4-byte aligned,
the two least-significant bits are always zero.)</li>
<li><strong>Sign-extends</strong> the resulting 28bit value to
the full register width (typically 64 bits).</li>
<li><strong>Adds</strong> this value to the program counter
(PC) to obtain the branch target.</li>
</ol>
</li>
<li>
<p><strong>Reach:</strong></p>
</li>
<li>With a 26bit signed field thats effectively 28 bits
after the shift, the branch can cover a range
of approximately ±128 MB from the current instruction.</li>
</ul>

View File

@ -1,77 +0,0 @@
<h2>Klemmen</h2>
<table>
<thead>
<tr>
<th style="text-align: right;">Anz.</th>
<th>Typ</th>
<th>Beschreibung</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;">12</td>
<td>BK9050</td>
<td>Buskoppler</td>
</tr>
<tr>
<td style="text-align: right;">2</td>
<td>KL1104</td>
<td>4 Digitaleingänge</td>
</tr>
<tr>
<td style="text-align: right;">2</td>
<td>KL2404</td>
<td>4 Digitalausgänge (0,5 A)</td>
</tr>
<tr>
<td style="text-align: right;">3</td>
<td>KL2424</td>
<td>4 Digitalausgänge (2 A)</td>
</tr>
<tr>
<td style="text-align: right;">2</td>
<td>KL4004</td>
<td>4 Analogausgänge</td>
</tr>
<tr>
<td style="text-align: right;">1</td>
<td>KL4002</td>
<td>2 Analogausgänge</td>
</tr>
<tr>
<td style="text-align: right;">22</td>
<td>KL9188</td>
<td>Potenzialverteilungsklemme</td>
</tr>
<tr>
<td style="text-align: right;">1</td>
<td>KL9100</td>
<td>Potenzialeinspeiseklemme</td>
</tr>
<tr>
<td style="text-align: right;">3</td>
<td>KL3054</td>
<td>4 Analogeingänge</td>
</tr>
<tr>
<td style="text-align: right;">5</td>
<td>KL3214</td>
<td>PT100 4 Temperatureingänge (3-Leiter)</td>
</tr>
<tr>
<td style="text-align: right;">3</td>
<td>KL3202</td>
<td>PT100 2 Temperatureingänge (3-Leiter)</td>
</tr>
<tr>
<td style="text-align: right;">1</td>
<td>KL2404</td>
<td>4 Digitalausgänge</td>
</tr>
<tr>
<td style="text-align: right;">2</td>
<td>KL9010</td>
<td>Endklemme</td>
</tr>
</tbody>
</table>

View File

@ -22,9 +22,7 @@ def test_latex_embedding2():
contains the interaction parameter <latex>\Phi_{ij}</latex>, which describes the influence of component contains the interaction parameter <latex>\Phi_{ij}</latex>, which describes the influence of component
<latex>j</latex> on the transport properties of component <latex>i</latex>. <latex>j</latex> on the transport properties of component <latex>i</latex>.
The interaction parameter <latex>\Phi_{ij}</latex> is given by the relation shown in @eq:ExampleFormula2. The interaction parameter <latex>\Phi_{ij}</latex> is given by the relation shown in @eq:ExampleFormula2.<latex type="block" reference="eq:ExampleFormula2" caption="(1)">\Phi_{ij} = \frac{1}{\sqrt{8}} \left(1 + \frac{M_i}{M_j} \right)^{-1/2} \left[ 1 + \left( \frac{\lambda_i}{\lambda_j} \right)^{1/2} \left( \frac{M_j}{M_i} \right)^{1/4} \right]^2</latex>""")
<latex type="block" ref_type="eq" ref_id="ExampleFormula2" caption="(1)">\Phi_{ij} = \frac{1}{\sqrt{8}} \left(1 + \frac{M_i}{M_j} \right)^{-1/2} \left[ 1 + \left( \frac{\lambda_i}{\lambda_j} \right)^{1/2} \left( \frac{M_j}{M_i} \right)^{1/4} \right]^2</latex>
""")
dummy = pyladoc.DocumentWriter() dummy = pyladoc.DocumentWriter()
result_string = dummy._equation_embedding_reescaping(test_input) result_string = dummy._equation_embedding_reescaping(test_input)
@ -44,9 +42,7 @@ def test_latex_embedding():
""") """)
expected_output = pyladoc._normalize_text_indent(r""" expected_output = pyladoc._normalize_text_indent(r"""
# Test # Test<latex type="block" reference="eq:ExampleFormula2" caption="(1)">\Phi_{ij} = \frac{1}{\sqrt{8}}</latex>This <latex>i</latex> is inline LaTeX.
<latex type="block" ref_type="eq" ref_id="ExampleFormula2" caption="(1)">\Phi_{ij} = \frac{1}{\sqrt{8}}</latex>
This <latex>i</latex> is inline LaTeX.
""") """)
dummy = pyladoc.DocumentWriter() dummy = pyladoc.DocumentWriter()

View File

@ -1,6 +1,7 @@
import pyladoc import pyladoc
import pandas as pd import pandas as pd
def test_readme_example(): def test_readme_example():
doc = pyladoc.DocumentWriter() doc = pyladoc.DocumentWriter()

View File

@ -1,10 +1,10 @@
import pyladoc import pyladoc
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pandas as pd import pandas as pd
import document_validation from . import document_validation
import numpy as np import numpy as np
VALIDATE_HTML_CODE_ONLINE = False VALIDATE_HTML_CODE_ONLINE = True
WRITE_RESULT_FILES = True WRITE_RESULT_FILES = True
@ -158,7 +158,7 @@ def test_html_render():
if WRITE_RESULT_FILES: if WRITE_RESULT_FILES:
with open('tests/out/test_html_render1.html', 'w', encoding='utf-8') as f: with open('tests/out/test_html_render1.html', 'w', encoding='utf-8') as f:
f.write(pyladoc.inject_to_template(html_code, internal_template='templates/test_template.html')) f.write(pyladoc.inject_to_template({'CONTENT': html_code}, internal_template='templates/test_template.html'))
def test_latex_render(): def test_latex_render():
@ -169,6 +169,8 @@ def test_latex_render():
f.write(doc.to_latex()) f.write(doc.to_latex())
assert doc.to_pdf('tests/out/test_latex_render1.pdf', font_family='serif') assert doc.to_pdf('tests/out/test_latex_render1.pdf', font_family='serif')
else:
assert doc.to_pdf('', font_family='serif') # Write only to temp folder
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,7 +1,7 @@
import pyladoc import pyladoc
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import pandas as pd import pandas as pd
import document_validation from . import document_validation
VALIDATE_HTML_CODE_ONLINE = False VALIDATE_HTML_CODE_ONLINE = False
WRITE_RESULT_FILES = True WRITE_RESULT_FILES = True
@ -71,16 +71,23 @@ def make_document():
mydataset = { mydataset = {
'Row1': ["Line1", "Line2", "Line3", "Line4", "Line5"], 'Row1': ["Line1", "Line2", "Line3", "Line4", "Line5"],
'Row2': [120, '95 km/h', 110, '105 km/h', 130], 'Row2': [120, '95 km/h', 110, '105 km/h', 130],
'Row3': ['12 g/km', '> 150 g/km', '110 g/km', '1140 g/km', '13.05 g/km'], 'Row3': ['12 g/km', '> 150 g/km', '-110 g/km', '1140 g/km', '\u221213.05 g/km'],
'Row4': ['5 stars', '4 stars', '5 stars', '4.5 stars', '5 stars'], 'Row4': ['5 stars', '4 stars', '5 stars', '4.5 stars', '5 stars'],
'Row5': [3.5, 7.8, 8.5, 6.9, 4.2], 'Row5': [3.5, 7.8, 8.5, 6.9, 4.2],
'Row6': ['1850 kg', '1500 kg', '1400 kg', '1600 kg', '1700 kg'], 'Row6': ['1850 kg', '150 kg', '140 kg', '1600 kg', '17.55 kg'],
'Row7': ['600 Nm', '250 Nm', '280 Nm', '320 Nm', '450 Nm'] 'Row7': ['600 Nm', '250 Nm', '280,8 Nm', '320 Nm', '450 Nm']
} }
df = pd.DataFrame(mydataset) df = pd.DataFrame(mydataset)
doc.add_table(df.style.hide(axis="index"), 'This is a example table', 'example1') doc.add_table(df.style.hide(axis="index"), 'This is a example table', 'example1')
doc.add_text("This is a fine print test text section. It uses smaller text and uses grey color. This is a fine print test"
"text section. It uses smaller text and uses grey color.", section_class='fineprint')
doc.add_text("Standard text section. This is normal text without any special formatting. It uses the default text size and color.")
doc.add_markdown("This is a **fine print** test text section. It uses **smaller text** and uses **grey** color.", section_class='fineprint')
return doc return doc
@ -92,15 +99,23 @@ def test_html_render():
if WRITE_RESULT_FILES: if WRITE_RESULT_FILES:
with open('tests/out/test_html_render2.html', 'w', encoding='utf-8') as f: with open('tests/out/test_html_render2.html', 'w', encoding='utf-8') as f:
f.write(pyladoc.inject_to_template(html_code, internal_template='templates/test_template.html')) f.write(pyladoc.inject_to_template({'CONTENT': html_code}, internal_template='templates/test_template.html'))
def test_latex_render(): def test_latex_render():
doc = make_document() doc = make_document()
# print(doc.to_latex()) latex_code = doc.to_latex()
assert r'\begin{tabular}{lSSSSSS}' in latex_code, "Table format not correct in LaTeX output"
if WRITE_RESULT_FILES:
with open('tests/out/test_html_render2.tex', 'w', encoding='utf-8') as f:
f.write(latex_code)
assert doc.to_pdf('tests/out/test_latex_render2.pdf', font_family='serif') assert doc.to_pdf('tests/out/test_latex_render2.pdf', font_family='serif')
else:
assert doc.to_pdf('', font_family='serif') # Write only to temp folder
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,5 +1,5 @@
import pyladoc import pyladoc
import document_validation from . import document_validation
VALIDATE_HTML_CODE_ONLINE = False VALIDATE_HTML_CODE_ONLINE = False
WRITE_RESULT_FILES = True WRITE_RESULT_FILES = True

151
tests/test_svg.py Normal file
View File

@ -0,0 +1,151 @@
import pyladoc
def test_update_svg_ids():
test_str = r"""
<g id="figure_1">
<g id="patch_1">
<path d="M 0 15.0336
L 24.570183 15.0336
L 24.570183 0
L 0 0
z
" style="fill: #ffffff"/>
</g>
<g id="axes_1">
<g id="text_1">
<!-- $\lambda_{\text{mix}}$ -->
<g transform="translate(3.042219 10.351343) scale(0.1 -0.1)">
<defs>
<path id="DejaVuSans-Oblique-3bb" d="M 2350 4316
" clip-path="url(#p8dcad2f367)" style="fill: none; stroke: #000000; stroke-width: 1.5; stroke-linecap: square"/>
<clipPath id="p8dcad2f367">
<rect x="57.6" y="41.472" width="357.12" height="266.112"/>
</clipPath>
</defs>
<path id="DejaVuSans-Oblique-78" d="M 3841 3500
L 2234 1784
</defs>
<use xlink:href="#DejaVuSans-Oblique-78" transform="translate(0 0.3125)"/>
<use xlink:href="#DejaVuSans-Oblique-69" transform="translate(59.179688 -16.09375) scale(0.7)"/>
</g>
</g>
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="24.570183pt" height="15.0336pt" viewBox="0 0 24.570183 15.0336" xmlns="http://www.w3.org/2000/svg" version="1.1">
<defs>
<style type="text/css">*{stroke-linejoin: round; stroke-linecap: butt}</style>
</defs>
<g id="figure_1">
<g id="patch_1">
<path d="M 0 15.0336
L 24.570183 15.0336
L 24.570183 0
L 0 0
z
" style="fill: #ffffff"/>
</g>
<g id="axes_1">
<g id="text_1">
<!-- $\lambda_{\text{mix}}$ -->
<g transform="translate(3.042219 10.351343) scale(0.1 -0.1)">
<defs>
<path id="DejaVuSans-Oblique-3bb" d="M 2350 4316
L 3125 0
L 2516 0
L 2038 2588
L 328 0
L -281 0
L 1903 3356
L 1794 3975
Q 1725 4369 1391 4369
L 1091 4369
L 1184 4863
L 1550 4856
Q 2253 4847 2350 4316
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-6d" d="M 3328 2828
Q 3544 3216 3844 3400
Q 4144 3584 4550 3584
Q 5097 3584 5394 3201
Q 5691 2819 5691 2113
L 5691 0
L 5113 0
L 5113 2094
Q 5113 2597 4934 2840
Q 4756 3084 4391 3084
Q 3944 3084 3684 2787
Q 3425 2491 3425 1978
L 3425 0
L 2847 0
L 2847 2094
Q 2847 2600 2669 2842
Q 2491 3084 2119 3084
Q 1678 3084 1418 2786
Q 1159 2488 1159 1978
L 1159 0
L 581 0
L 581 3500
L 1159 3500
L 1159 2956
Q 1356 3278 1631 3431
Q 1906 3584 2284 3584
Q 2666 3584 2933 3390
Q 3200 3197 3328 2828
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-69" d="M 603 3500
L 1178 3500
L 1178 0
L 603 0
L 603 3500
z
M 603 4863
L 1178 4863
L 1178 4134
L 603 4134
L 603 4863
z
" transform="scale(0.015625)"/>
<path id="DejaVuSans-78" d="M 3513 3500
L 2247 1797
L 3578 0
L 2900 0
L 1881 1375
L 863 0
L 184 0
L 1544 1831
L 300 3500
L 978 3500
L 1906 2253
L 2834 3500
L 3513 3500
z
" transform="scale(0.015625)"/>
</defs>
<use xlink:href="#DejaVuSans-Oblique-3bb" transform="translate(0 0.015625)"/>
<use xlink:href="#DejaVuSans-6d" transform="translate(59.179688 -16.390625) scale(0.7)"/>
<use xlink:href="#DejaVuSans-69" transform="translate(127.368164 -16.390625) scale(0.7)"/>
<use xlink:href="#DejaVuSans-78" transform="translate(146.816406 -16.390625) scale(0.7)"/>
</g>
</g>
</g>
</g>
</svg>
"""
unique_id = 'xx-rgerergre-yy-trhsrthrst--xx'
result = pyladoc.svg_tools.update_svg_ids(test_str, unique_id)
print(result)
assert result.replace(f"svg-{unique_id}-", '') == test_str

View File

@ -0,0 +1,86 @@
import pyladoc
def test_inject_to_template_html():
template = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><!--TITLE--></title>
</head>
<!-- some comment -->
<body>
<!--CONTENT-->
</body>
</html>
"""
content = "Hello, World!"
title = "Test Title"
result = pyladoc.inject_to_template({'CONTENT': content, 'TITLE': title}, template_string=template)
print(result)
assert "Hello, World!" in result
assert "<!-- some comment -->" in result # Keep unrelated HTML comments
assert "<title>Test Title</title>" in result
def test_inject_to_template_latex():
template = """
\\documentclass[a4paper,12pt]{article}
% Packages
\\usepackage[utf8]{inputenc}
\\usepackage[T1]{fontenc}
\\usepackage{lmodern} % Load Latin Modern font
\\usepackage{graphicx} % For including images
\\usepackage{amsmath} % For mathematical symbols
\\usepackage{amssymb} % For additional symbols
\\usepackage{hyperref} % For hyperlinks
\\usepackage{caption} % For customizing captions
\\usepackage{geometry} % To set margins
\\usepackage{natbib} % For citations
\\usepackage{float} % For fixing figure positions
\\usepackage{siunitx} % For scientific units
\\usepackage{booktabs} % For professional-looking tables
\\usepackage{pgf} % For using pgf grafics
\\usepackage{textcomp, gensymb} % provides \\degree symbol
\\sisetup{
table-align-text-post = false
}
% Geometry Settings
\\geometry{margin=1in} % 1-inch margins
% Title and Author Information
\\title{<!--PROJECT-->}
<!--AUTHOR-->
\\date{\\today}
\begin{document}
% Title Page
\\maketitle
% <!--CONTENT-->
\\end{document}
"""
content = "Hello, World!"
project_name = "Test Project"
author_name = "Otto"
result = pyladoc.inject_to_template(
{'CONTENT': content, 'PROJECT': project_name, 'AUTHOR': author_name},
template_string=template)
print(result)
assert "\nOtto\n" in result
assert "\\title{Test Project}\n" in result
assert "Hello, World!" in result