diff --git a/demo/doc-requirements.txt b/demo/doc-requirements.txt deleted file mode 100644 index df198c4..0000000 --- a/demo/doc-requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -sphinx==5.1.1 -sphinxcontrib-plantuml==0.24 -sphinx-needs==1.0.2 diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d4bb2cb..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# 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 = . -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) diff --git a/docs/conf.py b/docs/conf.py index cfd2f6a..fc5c881 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,14 +20,14 @@ extensions = [ 'sphinx_simplepdf', 'sphinxcontrib.plantuml', - 'sphinxcontrib.needs', + 'sphinx_needs', 'sphinx_copybutton', ] version = "1.6.0" templates_path = ['_templates'] -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'if_pdf_include.rst'] plantuml_output_format = "svg_img" local_plantuml_path = os.path.join(os.path.dirname(__file__), "utils", "plantuml.jar") diff --git a/docs/directives.rst b/docs/directives.rst index 99a5d95..fc84ac5 100644 --- a/docs/directives.rst +++ b/docs/directives.rst @@ -68,7 +68,7 @@ if-include include nested in a if-builder statement. You can list multiple files and use different builders. .. code-block:: rst - + .. if-builder:: simplepdf .. include:: ./path/to/my/file.xy @@ -79,9 +79,9 @@ include nested in a if-builder statement. You can list multiple files and use di is the same as .. code-block:: rst - - .. if-include:: simplepdf - + + .. if-include:: simplepdf + ./path/to/my/file.xy ./path/to/my/other/file.xy @@ -94,9 +94,9 @@ is the same as The following chapter should only be visible in the PDF version of this documentation -.. if-include:: simplepdf +.. if-include:: simplepdf - ./if_pdf_include.rst + .. include:: if_pdf_include.rst .. _pdf-include: diff --git a/docs/doc-requirements.txt b/docs/doc-requirements.txt deleted file mode 100644 index 067b274..0000000 --- a/docs/doc-requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -sphinx==5.1.1 -sphinxcontrib-plantuml==0.24 -sphinxcontrib-needs==0.7.8 -sphinx-copybutton \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index a1ce0b6..989133b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -99,6 +99,6 @@ Showcase directives css tech_details - examples/index + examples/sphinx_needs changelog - license \ No newline at end of file + license diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 954237b..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..860fd5f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,195 @@ +[build-system] +requires = ["flit_core >=3.4,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "sphinx-simplepdf" +version = "1.6.0" +description = "An easy to use PDF Builder for Sphinx with a modern PDF-Theme." +readme = "README.rst" +license = { text = "MIT" } +requires-python = ">=3.10" +authors = [{ name = "team useblocks", email = "info@useblocks.com" }] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Documentation", + "Topic :: Utilities", + "Framework :: Sphinx :: Extension", +] +dependencies = [ + "sphinx>=7.0", + "weasyprint>=66.0", + "libsass", + "beautifulsoup4", + "packaging>=20.0", # Modern replacement for pkg_resources.parse_version + "fonttools>=4.0.0", + "pillow>=9.1.0", + "pydyf>=0.10.0", + "cffi>=0.6", +] + +[project.entry-points."sphinx.html_themes"] +simplepdf_theme = "sphinx_simplepdf.themes.simplepdf_theme" + +[project.entry-points."sphinx.builders"] +simplepdf = "sphinx_simplepdf.builders.simplepdf" + + +[project.optional-dependencies] +dev = [ + "pre-commit>=3.5", + "pytest>=7.0", + "pytest-xdist>=3.0", + "tox>=4.0", + "tox-uv>=1.0", + "ruff>=0.4.0", + "pdfminer.six>=20220319" +] +docs = [ + "sphinx>=7.0", + "sphinxcontrib-plantuml", + "sphinx-needs", + "sphinx-copybutton", +] + +demo = [ + "sphinx>=7.0", + "sphinxcontrib-plantuml", + "sphinx-needs", + "sphinx-copybutton", +] + + +[project.urls] +Homepage = "https://github.com/useblocks/sphinx-simplepdf" +Documentation = "https://sphinx-simplepdf.readthedocs.io/en/latest/" +Repository = "https://github.com/useblocks/sphinx-simplepdf" +"Bug Tracker" = "https://github.com/useblocks/sphinx-simplepdf/issues" + +[tool.ruff] +line-length = 120 +target-version = "py310" + +[tool.ruff.lint] +extend-select = [ + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "F", # pyflakes (includes unused imports) + "I", # isort + "ICN", # flake8-import-conventions + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + "RUF", # Ruff-specific rules + "SIM", # flake8-simplify + "UP", # pyupgrade +] +extend-ignore = ["B904", "ISC001", "ICN001", "N818", "RUF012"] + +[tool.ruff.lint.isort] +known-first-party = ["sphinx_simplepdf"] +force-sort-within-sections = true + +[tool.tox] +# To use tox, see https://tox.readthedocs.io +# $ pipx install tox +# it is suggested to use with https://github.com/tox-dev/tox-uv or https://github.com/tox-dev/tox-conda +# $ pipx inject tox tox-uv + +legacy_tox_ini = """ +[tox] +env_list = + py{311,312,313}-sphinx{74,81,82} + docs + lint +requires = + tox>=4 + tox-uv>=1.0 + +[testenv] +deps = + sphinx>=7.0 + pytest + pdfminer.six>=20220319 + sphinx-simplepdf @ . +commands = + pytest {posargs:tests/} + +[testenv:py{310,311,312,313}-sphinx74] +deps = + sphinx>=7.4,<7.5 + pytest + pdfminer.six>=20220319 + sphinx-simplepdf @ . +commands = + pytest {posargs:tests/} + +[testenv:py{311,312,313}-sphinx81] +deps = + sphinx>=8.1,<8.2 + pytest + pdfminer.six>=20220319 + sphinx-simplepdf @ . +commands = + pytest {posargs:tests/} + +[testenv:py{311,312,313}-sphinx82] +deps = + sphinx>=8.2,<8.3 + pytest + pdfminer.six>=20220319 + sphinx-simplepdf @ . +commands = + pytest {posargs:tests/} + +[testenv:lint] +description = Run code quality checks +allowlist_externals = + ruff +deps = + ruff>=0.4.0 +commands = + ruff check sphinx_simplepdf/ + ruff format --check sphinx_simplepdf/ + +[testenv:docs] +description = Build documentation with Sphinx +basepython = python3.12 +deps = + sphinx>=7.0 +extras = docs +commands = + sphinx-build -a -E -j auto -b html docs/ docs/_build + +[testenv:demo] +description = Build demo with Sphinx +basepython = python3.12 +deps = + sphinx>=7.0,<8.0 +extras = demo +commands = + sphinx-build -a -E -j auto -b simplepdf demo/ demo/_build + +""" + + + +# [tool.setuptools.packages.find] +# where = ["sphinx_simplepdf/themes/simplepdf_theme"] + +# [tool.setuptools.package-data] +# sphinx_simplepdf=[ +# 'theme.conf', +# '*.html', +# 'static/styles/*.css', +# 'static/js/*.js', +# 'static/fonts/*.*' +# ] diff --git a/sphinx_simplepdf/builders/debug.py b/sphinx_simplepdf/builders/debug.py index f505082..a27ac40 100644 --- a/sphinx_simplepdf/builders/debug.py +++ b/sphinx_simplepdf/builders/debug.py @@ -1,7 +1,11 @@ import sys import pkgutil -import pkg_resources import platform +try: + from importlib.metadata import version +except ImportError: + from importlib_metadata import version + class DebugPython: @@ -17,11 +21,11 @@ def get_packages(self): final = {} for name in names: try: - version = pkg_resources.get_distribution(name).version + __version__ = version(name) except (Exception): final[name] = 'unknown' else: - final[name] = version + final[name] = __version__ return final diff --git a/sphinx_simplepdf/builders/simplepdf.py b/sphinx_simplepdf/builders/simplepdf.py index 25b6518..58fc39f 100644 --- a/sphinx_simplepdf/builders/simplepdf.py +++ b/sphinx_simplepdf/builders/simplepdf.py @@ -181,9 +181,9 @@ def finish(self) -> None: success = True break except subprocess.TimeoutExpired: - logger.warning(f"TimeoutExpired in weasyprint, retrying") + logger.info(f"TimeoutExpired in weasyprint, retrying") except subprocess.CalledProcessError as e: - logger.warning( + logger.info( f"CalledProcessError in weasyprint, retrying\n{str(e)}" ) finally: @@ -218,7 +218,7 @@ def finish(self) -> None: """ def _toctree_fix(self, html): - print("checking for potential toctree page numbering errors") + logger.info("checking for potential toctree page numbering errors") soup = BeautifulSoup(html, "html.parser") sidebar = soup.find("div", class_="sphinxsidebarwrapper") @@ -251,8 +251,7 @@ def _toctree_fix(self, html): references = {key: value for key, value in counts.items()} if references: - - print(f"found duplicate chapters:\n{references}") + logger.info(f"found duplicate chapters:\n{references}") for text in references.keys(): @@ -341,22 +340,34 @@ def _toctree_fix(self, html): continue for heading_tag in ["h1", "h2"]: - headings = soup.find_all(heading_tag, class_="") + headings = soup.find_all(heading_tag) for number, heading in enumerate(headings): - class_attr = heading.attrs["class"] if heading.has_attr("class") else [] - logger.debug(f"found heading {heading}") + class_attr = heading.get('class', []) if 0 == number: - class_attr.append("first") - if 0 == number % 2: - class_attr.append("even") - else: - class_attr.append("odd") - if len(headings) - 1 == number: - class_attr.append("last") - - heading.attrs["class"] = class_attr + class_attr +=["first"] + class_attr += [f"heading-{number}"] + heading['class'] = class_attr + + parent = heading.find_parent("section") + # is the parent a section + if parent and parent.name == 'section': + prev_span = parent.find_previous_sibling('span', id=True) + # check if previous sibling is span with an id + if prev_span: + # add classes for css handling + prev_span['class'] = prev_span.get('class', []) + ['anchor-before-heading'] + if 0 == number: + prev_span['class'] = prev_span.get('class', []) + ["first"] + if 0 == number % 2: + prev_span['class'] = prev_span.get('class', []) + ["even"] + else: + prev_span['class'] = prev_span.get('class', []) + ["odd"] + if len(headings) - 1 == number: + prev_span['class'] = prev_span.get('class', []) + ["last"] + logger.debug("DEBUG HTML START") logger.debug(soup.prettify(formatter="html")) + logger.debug("DEBUG HTML END") return str(soup) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..3f5cbb5 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test package for sphinx-simplepdf.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..174ed82 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,309 @@ +"""Central pytest configuration and fixtures for sphinx-simplepdf tests.""" +from pathlib import Path +from typing import Any +import shutil +import io +import re + +import pytest +from sphinx.application import Sphinx +from sphinx.testing.util import SphinxTestApp +from sphinx.util import logging as sphinx_logging +import logging + + +pytest_plugins = ("sphinx.testing.fixtures",) + +def copy_srcdir_to_tmpdir(srcdir: Path, tmp: Path) -> Path: + """ + Copy Source Directory to Temporary Directory. + + This function copies the contents of a source directory to a temporary + directory. It generates a random subdirectory within the temporary directory + to avoid conflicts and enable parallel processes to run without conflicts. + + :param srcdir: Path to the source directory. + :param tmp: Path to the temporary directory. + + :return: Path to the newly created directory in the temporary directory. + """ + srcdir = Path(__file__).parent.absolute() / srcdir + tmproot = tmp / Path(srcdir).name + shutil.copytree(srcdir, tmproot) + return tmproot + + +@pytest.fixture(scope="session") +def rootdir(): + """Root directory for test documentation projects.""" + return Path(__file__).parent / "doc_test" + + +@pytest.fixture +def content(request): + """ + Provide test document content dynamically. + + Usage in test: + @pytest.mark.parametrize('content', [ + {'index.rst': '.. title::\n\n Test\n'} + ], indirect=True) + def test_example(app): + ... + """ + return request.param if hasattr(request, "param") else {} + + +class SphinxBuild: + """Helper class to build Sphinx documentation and access results.""" + + def __init__(self, app: Sphinx, src: Path, status, warning): + self.app = app + self.src = src + self.outdir = None + self.warnings = [] + self.errors = [] + self.status_stream = status + self.warning_stream = warning + self.debug_output = [] + + def build(self, force_all: bool = True, raise_on_warning: bool = False, debug: bool = False): + """ + Build the documentation. + + Args: + force_all: Rebuild all files + raise_on_warning: Raise exception on warnings + """ + + if debug: + self.app.verbosity = 2 + + debug_buffer = io.StringIO() if debug else None + + # Eigenen Handler OHNE Sphinx-Filter für Debug + debug_handler = logging.StreamHandler(debug_buffer) + debug_handler.setLevel(logging.DEBUG) + debug_handler.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) + + # An Extension-Logger hängen (umgeht Sphinx-Filter!) + ext_logger = sphinx_logging.getLogger("sphinx_simplepdf") + ext_logger.logger.addHandler(debug_handler) + ext_logger.logger.setLevel(logging.DEBUG) + ext_logger.logger.propagate = False # Verhindere Doppel-Logging + + self.app.build(force_all=force_all) + self.outdir = Path(self.app.builder.outdir) + + self.status_output = self.status_stream.getvalue() + self.warnings = self.warning_stream.getvalue().splitlines() + + if debug: + self.debug_output = debug_buffer.getvalue().splitlines() + + if raise_on_warning and self.warnings: + raise AssertionError( + f"Build produced warnings:\n" + "\n".join(self.warnings) + ) + + return self + + def html_content(self, docname: str = "index") -> str: + """ + Get generated HTML content (before SimplePDF processing). + + Args: + docname: Document name without extension + + Returns: + HTML content as string + """ + html_file = self.outdir / f"{docname}.html" + if not html_file.exists(): + raise FileNotFoundError(f"HTML file not found: {html_file}") + return html_file.read_text(encoding="utf-8") + + def processed_html(self) -> str: + """ + Get processed HTML from SimplePDF builder (from debug output). + + The SimplePDF builder logs the processed HTML before PDF generation. + This extracts it from the warning stream. + + Returns: + Processed HTML content as string + """ + # Look for debug HTML output in warnings + # (SimplePDF should log processed HTML with specific marker) + + def strip_log_prefix(text): + """Entferne Log-Level-Prefixe wie 'DEBUG: ', 'INFO: ' etc.""" + return re.sub(r'^(DEBUG|INFO|WARNING|ERROR|CRITICAL):\s*', '', text, flags=re.MULTILINE) + + # print(f"-- debug output--\n{self.debug_output}\n-----") + for i, line in enumerate(self.debug_output): + if "DEBUG HTML START" in line: + # Find end marker + html_lines = [] + for next_line in self.debug_output[i+1:]: + if "DEBUG HTML END" in next_line: + break + html_lines.append(strip_log_prefix(next_line)) + return "\n".join(html_lines) + + raise ValueError("No processed HTML found in debug output") + + def pdf_path(self, basename: str = None) -> Path: + """ + Get path to generated PDF file. + + Args: + basename: PDF filename without extension (default: project name) + + Returns: + Path to PDF file + """ + if basename is None: + basename = self.app.config.project + + pdf_file = self.outdir / f"{basename}.pdf" + if not pdf_file.exists(): + raise FileNotFoundError(f"PDF file not found: {pdf_file}") + return pdf_file + + def pdf_exists(self, basename: str = None) -> bool: + """Check if PDF was generated.""" + try: + self.pdf_path(basename) + return True + except FileNotFoundError: + return False + + def has_warnings(self, pattern: str = None) -> bool: + """ + Check if build produced warnings. + + Args: + pattern: Optional regex pattern to match specific warnings + + Returns: + True if warnings exist (and match pattern if provided) + """ + if pattern is None: + return len(self.warnings) > 0 + + import re + regex = re.compile(pattern) + return any(regex.search(w) for w in self.warnings) + + def get_warnings_matching(self, pattern: str) -> list[str]: + """ + Get all warnings matching a pattern. + + Args: + pattern: Regex pattern to match + + Returns: + List of matching warning messages + """ + import re + regex = re.compile(pattern) + return [w for w in self.warnings if regex.search(w)] + + +@pytest.fixture +def sphinx_build(make_app, tmp_path): + """ + Main fixture to build Sphinx projects with SimplePDF. + + Usage: + def test_example(sphinx_build): + app = sphinx_build( + buildername='simplepdf', + srcdir='basic_doc', + confoverrides={'project': 'Test'} + ) + result = app.build() + assert result.pdf_exists() + """ + def _make_build( + buildername: str = "simplepdf", + srcdir: str = None, + confoverrides: dict[str, Any] = None, + **kwargs + ) -> SphinxBuild: + """ + Create and return SphinxBuild instance. + + Args: + buildername: Sphinx builder name + srcdir: Source directory name (relative to doc_test/) + confoverrides: Dictionary of conf.py overrides + **kwargs: Additional arguments for make_app + + Returns: + SphinxBuild instance + """ + if srcdir is None: + raise ValueError("srcdir is required") + + # Resolve source directory using pathlib + test_root = Path(__file__).parent / "doc_test" + src_path = test_root / srcdir + src_dir = copy_srcdir_to_tmpdir(f"doc_test/{srcdir}", tmp_path) + + if not src_path.exists(): + raise ValueError(f"Test document directory not found: {src_path}") + + status = io.StringIO() + warning = io.StringIO() + + # Create app with Path object (Sphinx 8.2 compatible) + app = make_app( + buildername=buildername, + # srcdir=src_path, + srcdir=src_dir, + status=status, + warning=warning, + confoverrides=confoverrides or {}, + freshenv = True, + **kwargs + ) + return SphinxBuild(app, src_path, status, warning) + + return _make_build + + +@pytest.fixture +def sphinx_build_factory(tmp_path, rootdir): + """ + Factory fixture to create multiple test builds. + + More flexible than sphinx_build for tests that need multiple builds. + """ + def _factory( + srcdir: str, + buildername: str = "simplepdf", + **kwargs + ): + src = rootdir / srcdir + app = SphinxTestApp( + buildername=buildername, + srcdir=src, + freshenv=True, + **kwargs + ) + return SphinxBuild(app, src) + + return _factory + + +@pytest.fixture +def minimal_conf(): + """Minimal conf.py configuration for SimplePDF.""" + return { + "extensions": ["sphinx_simplepdf"], + "simplepdf_theme": "simplepdf_theme", + "master_doc": "index", + "exclude_patterns": ["_build"], + } diff --git a/tests/doc_test/basic_doc/conf.py b/tests/doc_test/basic_doc/conf.py new file mode 100644 index 0000000..ebe3e0a --- /dev/null +++ b/tests/doc_test/basic_doc/conf.py @@ -0,0 +1,8 @@ +"""Minimal test configuration.""" +project = "BasicTest" +extensions = ["sphinx_simplepdf"] +master_doc = "index" +exclude_patterns = ["_build"] + +# SimplePDF settings +simplepdf_theme = "simplepdf_theme" diff --git a/tests/doc_test/basic_doc/content.rst b/tests/doc_test/basic_doc/content.rst new file mode 100644 index 0000000..9ef8114 --- /dev/null +++ b/tests/doc_test/basic_doc/content.rst @@ -0,0 +1,17 @@ +Additional Content +================== + +This is additional content for testing. + +Lists +----- + +* Item 1 +* Item 2 +* Item 3 + +Numbered list: + +1. First +2. Second +3. Third diff --git a/tests/doc_test/basic_doc/index.rst b/tests/doc_test/basic_doc/index.rst new file mode 100644 index 0000000..7d3c3f9 --- /dev/null +++ b/tests/doc_test/basic_doc/index.rst @@ -0,0 +1,22 @@ +Basic Test Document +=================== + +This is a minimal test document for SimplePDF. + +Section 1 +--------- + +Some content in section 1. + +Section 2 +--------- + +Some content in section 2. + +Code Example +------------ + +.. code-block:: python + + def hello_world(): + print("Hello, World!") diff --git a/tests/doc_test/with_images/_static/.gitkeep b/tests/doc_test/with_images/_static/.gitkeep new file mode 100644 index 0000000..5f9ec71 --- /dev/null +++ b/tests/doc_test/with_images/_static/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder for static files directory +# Add test_image.png here for actual tests diff --git a/tests/doc_test/with_images/conf.py b/tests/doc_test/with_images/conf.py new file mode 100644 index 0000000..81587dc --- /dev/null +++ b/tests/doc_test/with_images/conf.py @@ -0,0 +1,7 @@ +"""Configuration with images.""" +project = "ImagesTest" +extensions = ["sphinx_simplepdf"] +master_doc = "index" +exclude_patterns = ["_build"] + +simplepdf_theme = "simplepdf_theme" diff --git a/tests/doc_test/with_images/index.rst b/tests/doc_test/with_images/index.rst new file mode 100644 index 0000000..25403ff --- /dev/null +++ b/tests/doc_test/with_images/index.rst @@ -0,0 +1,18 @@ +Document with Images +==================== + +This document contains images for testing. + +Test Image +---------- + +.. image:: _static/test_image.png + :alt: Test image + :width: 200px + +The image above should be included in the PDF. + +Another Section +--------------- + +More content after the image. diff --git a/tests/doc_test/with_issues/broken_anchors.rst b/tests/doc_test/with_issues/broken_anchors.rst new file mode 100644 index 0000000..ee37d8c --- /dev/null +++ b/tests/doc_test/with_issues/broken_anchors.rst @@ -0,0 +1,14 @@ +Broken Anchors +============== + +This page contains broken internal references. + +Section with Broken Links +-------------------------- + +This is a reference to a `non-existent section <#does-not-exist>`_. + +Another broken reference: see :ref:`missing-label`. + +.. note:: + This note contains a broken link to :doc:`missing_document`. diff --git a/tests/doc_test/with_issues/conf.py b/tests/doc_test/with_issues/conf.py new file mode 100644 index 0000000..502eef7 --- /dev/null +++ b/tests/doc_test/with_issues/conf.py @@ -0,0 +1,7 @@ +"""Configuration for testing error/warning handling.""" +project = "IssuesTest" +extensions = ["sphinx_simplepdf"] +master_doc = "index" +exclude_patterns = ["_build"] + +simplepdf_theme = "simplepdf_theme" diff --git a/tests/doc_test/with_issues/index.rst b/tests/doc_test/with_issues/index.rst new file mode 100644 index 0000000..cf04aea --- /dev/null +++ b/tests/doc_test/with_issues/index.rst @@ -0,0 +1,14 @@ +Document with Issues +==================== + +This document contains intentional issues for testing warning handling. + +.. toctree:: + :maxdepth: 2 + + broken_anchors + +Main Content +------------ + +This document has broken references for testing. diff --git a/tests/doc_test/with_toc/_static/custom.css b/tests/doc_test/with_toc/_static/custom.css new file mode 100644 index 0000000..867208c --- /dev/null +++ b/tests/doc_test/with_toc/_static/custom.css @@ -0,0 +1,20 @@ +div.body h1{ + display: none; +} + +h2 { + page-break-before: avoid !important; + break-before:avoid !important; +} + +span.anchor-before-heading { + display: block; + page-break-before: always; + height: 0; + margin: 0; + padding: 0; +} + +span.anchor-before-heading.first { + page-break-before: avoid !important; +} diff --git a/tests/doc_test/with_toc/chapter1.rst b/tests/doc_test/with_toc/chapter1.rst new file mode 100644 index 0000000..7c08e48 --- /dev/null +++ b/tests/doc_test/with_toc/chapter1.rst @@ -0,0 +1,17 @@ +Chapter 1: Getting Started +========================== + +This is the first chapter of the test document. + +Section 1.1 +----------- + +Content for section 1.1. + +Section 1.2 +----------- + +Content for section 1.2. + +.. note:: + This is a note in chapter 1. diff --git a/tests/doc_test/with_toc/chapter2.rst b/tests/doc_test/with_toc/chapter2.rst new file mode 100644 index 0000000..0d6d339 --- /dev/null +++ b/tests/doc_test/with_toc/chapter2.rst @@ -0,0 +1,17 @@ +Chapter 2: Advanced Topics +========================== + +This is the second chapter. + +Section 2.1 +----------- + +Advanced content here. + +Section 2.2 +----------- + +More advanced topics. + +.. warning:: + This is a warning in chapter 2. diff --git a/tests/doc_test/with_toc/conf.py b/tests/doc_test/with_toc/conf.py new file mode 100644 index 0000000..92a8a38 --- /dev/null +++ b/tests/doc_test/with_toc/conf.py @@ -0,0 +1,10 @@ +"""Configuration with table of contents.""" +project = "TOCTest" +extensions = ["sphinx_simplepdf"] +master_doc = "index" +exclude_patterns = ["_build"] + +simplepdf_theme = "simplepdf_theme" +simplepdf_toc = True +html_css_files = ["custom.css" ] +html_static_path = ['_static'] diff --git a/tests/doc_test/with_toc/index.rst b/tests/doc_test/with_toc/index.rst new file mode 100644 index 0000000..0e3d3cf --- /dev/null +++ b/tests/doc_test/with_toc/index.rst @@ -0,0 +1,19 @@ +Document with TOC +================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + chapter1 + chapter2 + +Main Content +------------ + +This document has a table of contents that links to multiple chapters. + +Introduction +~~~~~~~~~~~~ + +This is the introduction section. diff --git a/tests/test_basic_build.py b/tests/test_basic_build.py new file mode 100644 index 0000000..ceebf49 --- /dev/null +++ b/tests/test_basic_build.py @@ -0,0 +1,58 @@ +"""Basic build tests for SimplePDF.""" +import pytest +from .utils import build_and_capture_stdout + + +def test_basic_build_succeeds(sphinx_build, capsys): + """Test that a basic document builds successfully.""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="basic_doc") + + assert result.pdf_exists() + # assert not result.has_warnings() + assert not result.has_warnings("ERROR:") + + +def test_html_generation(sphinx_build, capsys): + """Test that HTML is generated before PDF conversion.""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="basic_doc") + + html = result.html_content("index") + assert "" in html + + +def test_build_with_custom_project_name(sphinx_build, capsys): + """Test that custom project name is used for PDF filename.""" + project_name = "MyCustomProject" + result = build_and_capture_stdout(sphinx_build, capsys, + srcdir="basic_doc", + confoverrides={"project": project_name} + ) + + assert result.pdf_exists(project_name) + assert not result.has_warnings("ERROR:") + + + +def test_rebuild_does_not_fail(sphinx_build): + """Test that rebuilding does not cause errors.""" + builder = sphinx_build(srcdir="basic_doc") + + # First build + result1 = builder.build() + assert result1.pdf_exists() + + # Second build + result2 = builder.build() + assert result2.pdf_exists() + + +@pytest.mark.parametrize("srcdir", [ + "basic_doc", + "with_images", + "with_toc", +]) +def test_various_document_types(sphinx_build, capsys, srcdir): + """Test that various document types build successfully.""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir=srcdir) + assert result.pdf_exists() diff --git a/tests/test_html_processing.py b/tests/test_html_processing.py new file mode 100644 index 0000000..02b05f0 --- /dev/null +++ b/tests/test_html_processing.py @@ -0,0 +1,55 @@ +"""Tests for HTML processing in SimplePDF builder.""" +import pytest +import re +from .utils import build_and_capture_stdout, prettify_html + + +def test_html_is_processed(sphinx_build, capsys): + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="basic_doc", build_kwargs={"debug":True}) + + # Original HTML should exist + original_html = result.html_content("index") + assert original_html + + # Processed HTML should be in debug output + # (requires SimplePDF to log processed HTML) + try: + processed_html = result.processed_html() + pretty_original = prettify_html(original_html) + assert processed_html + # no toctree, no fix! + assert processed_html == pretty_original + except ValueError: + pytest.skip("SimplePDF debug output not available") + + +def test_anchors_are_preserved(sphinx_build): + """Test that anchors/IDs are preserved in HTML processing.""" + result = sphinx_build(srcdir="with_toc").build() + + html = result.html_content("index") + + # Check for section anchors + assert 'id="' in html or 'href="#' in html + + +def test_image_paths_are_resolved(sphinx_build): + """Test that image paths are correctly resolved.""" + result = sphinx_build(srcdir="with_images").build() + + html = result.html_content("index") + + # Images should be referenced + assert " 0 + + +def test_pdf_contains_content(sphinx_build, capsys): + """Test that PDF contains actual content (not empty).""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="basic_doc") + + pdf_path = result.pdf_path() + # PDF should be reasonably sized (> 1KB) + assert pdf_path.stat().st_size > 1024 + + +def test_pdf_with_images(sphinx_build, capsys): + """Test that PDF with images is generated.""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="with_images") + + # Should build without errors + assert result.pdf_exists() + # PDF with images should be larger + pdf_path = result.pdf_path() + assert pdf_path.stat().st_size > 5000 + + +def test_pdf_with_toc(sphinx_build, capsys): + """Test that PDF with table of contents is generated.""" + # result = sphinx_build(srcdir="with_toc").build() + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="with_toc", build_kwargs={"debug": True}) + + assert result.pdf_exists() + pdf_path = result.pdf_path() + + # Check for specific WeasyPrint anchor warnings + anchor_warnings = result.get_warnings_matching(r"(anchor|link|reference)") + assert len(anchor_warnings) == 0 + + assert 5 == page_count(pdf_path) + + text = extract_pdf_text(pdf_path) + + assert """ +Table of Contents + +Contents: + +Chapter 1: Getting Started + +• Section 1.1 +• Section 1.2 + +Chapter 2: Advanced Topics + +• Section 2.1 +• Section 2.2 + +3 + +3 + +3 + +4 + +4 + +4 +""" in text + + +@pytest.mark.parametrize("page_format", ["A4", "Letter", "A5"]) +def test_pdf_different_page_formats(sphinx_build, capsys, page_format): + """Test PDF generation with different page formats.""" + result = build_and_capture_stdout(sphinx_build, capsys, + srcdir="basic_doc", + confoverrides={"simplepdf_page_size": page_format} + ) + + assert result.pdf_exists() diff --git a/tests/test_theme_features.py b/tests/test_theme_features.py new file mode 100644 index 0000000..e117151 --- /dev/null +++ b/tests/test_theme_features.py @@ -0,0 +1,44 @@ +"""Tests for SimplePDF theme features.""" +import pytest + + +def test_default_theme_applied(sphinx_build): + """Test that default SimplePDF theme is applied.""" + result = sphinx_build(srcdir="basic_doc").build() + + assert result.pdf_exists() + + +def test_custom_theme_settings(sphinx_build): + """Test that custom theme settings are applied.""" + result = sphinx_build( + srcdir="basic_doc", + confoverrides={ + "simplepdf_theme_options": { + "primary_color": "#FF0000", + } + } + ).build() + + assert result.pdf_exists() + + +def test_toc_generation(sphinx_build): + """Test that table of contents is generated.""" + result = sphinx_build( + srcdir="with_toc", + confoverrides={"simplepdf_toc": True} + ).build() + + assert result.pdf_exists() + + +@pytest.mark.parametrize("font_size", ["10pt", "12pt", "14pt"]) +def test_different_font_sizes(sphinx_build, font_size): + """Test PDF generation with different font sizes.""" + result = sphinx_build( + srcdir="basic_doc", + confoverrides={"simplepdf_font_size": font_size} + ).build() + + assert result.pdf_exists() diff --git a/tests/test_warnings.py b/tests/test_warnings.py new file mode 100644 index 0000000..c90333e --- /dev/null +++ b/tests/test_warnings.py @@ -0,0 +1,62 @@ +"""Tests for warnings and error handling.""" +import pytest +import re + +from .utils import build_and_capture_stdout + + +def test_broken_anchors_warning(sphinx_build, capsys): + """Test that broken anchors produce warnings.""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="with_issues") + + # Should have warnings about broken links/anchors + assert result.has_warnings() + + # Check for specific WeasyPrint anchor warnings + anchor_warnings = result.get_warnings_matching(r"(anchor|link|reference)") + assert len(anchor_warnings) > 0 + + +def test_missing_image_warning(sphinx_build, tmp_path): + """Test that missing images produce warnings.""" + # This would require a test doc with broken image reference + pytest.skip("Requires test doc with broken image") + + +def test_build_warnings_are_captured(sphinx_build, capsys): + """Test that all build warnings are captured.""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="with_issues") + + # Warnings should be accessible + assert isinstance(result.warnings, list) + + +def test_weasyprint_warnings_logged(sphinx_build, capsys): + """Test that WeasyPrint warnings are logged.""" + result = build_and_capture_stdout(sphinx_build, capsys, srcdir="with_issues") + + # Look for WeasyPrint-specific warnings + weasy_warnings = result.get_warnings_matching(r"(weasyprint|WeasyPrint)") + # May or may not have WeasyPrint warnings depending on document + assert isinstance(weasy_warnings, list) + + +@pytest.mark.parametrize("strict_mode", [True, False]) +def test_strict_mode_handling(sphinx_build, capsys, strict_mode): + """Test behavior with strict mode enabled/disabled.""" + confoverrides = {"nitpicky": strict_mode} + + if strict_mode: + # In strict mode, warnings may cause build to fail + with pytest.raises((AssertionError, Exception)): + sphinx_build( + srcdir="with_issues", + confoverrides=confoverrides + ).build(raise_on_warning=True) + else: + # Without strict mode, warnings are logged but build succeeds + result = build_and_capture_stdout(sphinx_build, capsys, + srcdir="with_issues", + confoverrides=confoverrides + ) + assert result.pdf_exists() diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..aa6991a --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,27 @@ +from pdfminer.high_level import extract_text, extract_pages +from bs4 import BeautifulSoup + + +def extract_pdf_text(pdf_path): + """Extrahiere gesamten Text aus PDF.""" + return extract_text(str(pdf_path)) + +def page_count(pdf_path): + """get page count of pdf""" + page_count = 0 + for page_layout in extract_pages(pdf_path): + page_count += 1 + return page_count + +def prettify_html(html): + soup = BeautifulSoup(html, "html.parser") + return str(soup.prettify(formatter="html")) + + +def build_and_capture_stdout(sphinx_build, capsys, srcdir, build_kwargs=None, **sphinx_kwargs): + """Baue das PDF und liefere das captured stdout zurück.""" + build_kwargs = build_kwargs or {} + result = sphinx_build(srcdir=srcdir, **sphinx_kwargs).build(**build_kwargs) + captured = capsys.readouterr() + result.warnings += captured.out.splitlines() + return result