Development

How to set up the project, run the checks, and find your way around the code.

Setup

You'll need uv and pandoc.

git clone https://github.com/bradmontgomery/word2html.git
cd word2html
uv sync

Commands

CommandWhat it does
uv run pytestRun the tests
uv run pytest --covRun the tests, and require at least 90% coverage
uv run ruff checkLint
uv run ruff formatFormat the code
uv run mypyCheck types in strict mode
uv buildBuild the wheel and source distribution
uv run word2html file.docxRun your working copy

How a conversion works

  1. Preflight (docx.py). Validates the file and its archive, parses every XML part as a stream with limits, and looks for hidden text and images that'll be lost. It also notes whether the document has any media.
  2. Convert (pandoc.py). Finds pandoc, checks its version, and runs it with memory and time limits. --embed-resources is only used if there's media, because it needs about six times the memory.
  3. Scan (html_scan.py). Parses the final HTML to find unsafe links and active content.
  4. Decide (main.py). Refuses active content always, and refuses unsafe links and hidden text with --strict.
  5. Write (main.py). Atomically replaces the output file.

Project layout

PathContents
word2html/main.pyconvert_to_html(), atomic writes, and the command line
word2html/docx.pyPreflight validation and hidden-text detection
word2html/pandoc.pyFinding and running pandoc, and the minimum version
word2html/html_scan.pyUnsafe link and active content scanning
word2html/common.pyLimits, Finding, and terminal output sanitizing
word2html/errors.pyExceptions
tests/The test suite
docs/This documentation, as static HTML
.github/The CI workflow and Dependabot configuration

Tests

Tests use real pandoc wherever behaviour depends on it, and the suite fails rather than skips if pandoc is missing. A small stub stands in for pandoc only to simulate states that are hard to produce for real, such as a hanging or too-old pandoc.

Test documents are built in code by helpers in tests/conftest.py, so no binary files are committed:

def test_hidden_text_warns(make_docx):
    path = make_docx(body=para(run("secret", "<w:vanish/>")))
    result = convert_to_html(path)
    assert result.warnings[0].kind is FindingKind.HIDDEN_TEXT
FileCovers
test_convert.pyNormal conversions, and what's kept or dropped
test_paths.pyUnusual filenames, including the wrong-file regressions
test_links.pyUnsafe links and --strict
test_active_content.pyActive content and allowed media
test_hidden_text.pyHidden text detection
test_malformed.pyCorrupt and hostile documents
test_limits.pyResource limits
test_output.pyAtomic writes, symlinks, and permissions
test_cli.pyExit codes, options, and pandoc failures
test_docs.pyThat these docs have no broken links and still match the code

When you add a guard, make sure a test fails without it: temporarily disable the guard, and confirm the suite notices.

Continuous integration

GitHub Actions runs on every pull request and push to main:

JobWhat it checks
lintThe lockfile, ruff, formatting, and mypy
testThe tests with coverage, on Linux and macOS, with Python 3.12 to 3.15 and pandoc 3.13
pandoc-minimumThe tests on the oldest supported pandoc
packageInstalls the built wheel in a clean environment and converts a document

Actions are pinned to full commit hashes, and Dependabot proposes weekly updates for actions and Python packages. The pandoc versions in ci.yml are updated by hand.

Changing the minimum pandoc version

Update these together. test_docs.py catches a mismatch in the code and these docs:

Working on these docs

The site is plain HTML with one stylesheet and no build step. To preview it:

python3 -m http.server --directory docs 8000

Then open http://localhost:8000. When you add a page, add it to the sidebar on every page. Keep links relative, because the site is served from /word2html/.

Publishing with GitHub Pages

  1. Merge the docs to main.
  2. In the repository, open Settings › Pages.
  3. Under Build and deployment, set Source to Deploy from a branch.
  4. Choose the main branch and the /docs folder, then save.

The site is published to bradmontgomery.github.io/word2html. The empty docs/.nojekyll file tells GitHub to serve the files as they are.

Releasing

  1. Update version in pyproject.toml, then run uv lock.
  2. Update the version in the docs. test_docs.py checks this.
  3. Merge to main, after CI passes.
  4. Run uv build, and then uv publish.