docxtpl-lint 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. docxtpl_lint-1.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +31 -0
  2. docxtpl_lint-1.1.0/.github/ISSUE_TEMPLATE/config.yml +1 -0
  3. docxtpl_lint-1.1.0/.github/ISSUE_TEMPLATE/rule_request.yml +20 -0
  4. docxtpl_lint-1.1.0/.github/pull_request_template.md +3 -0
  5. docxtpl_lint-1.1.0/.github/workflows/ci.yml +84 -0
  6. docxtpl_lint-1.1.0/.github/workflows/pages.yml +39 -0
  7. docxtpl_lint-1.1.0/.github/workflows/publish-pypi.yml +45 -0
  8. docxtpl_lint-1.1.0/.gitignore +9 -0
  9. docxtpl_lint-1.1.0/.pre-commit-hooks.yaml +7 -0
  10. docxtpl_lint-1.1.0/CHANGELOG.md +100 -0
  11. docxtpl_lint-1.1.0/CONTRIBUTING.md +75 -0
  12. docxtpl_lint-1.1.0/LICENSE +21 -0
  13. docxtpl_lint-1.1.0/PKG-INFO +257 -0
  14. docxtpl_lint-1.1.0/PLAN.md +46 -0
  15. docxtpl_lint-1.1.0/README.md +228 -0
  16. docxtpl_lint-1.1.0/README.zh.md +219 -0
  17. docxtpl_lint-1.1.0/action.yml +46 -0
  18. docxtpl_lint-1.1.0/benchmark/adversarial_adjudicated.json +114 -0
  19. docxtpl_lint-1.1.0/benchmark/expected.json +18 -0
  20. docxtpl_lint-1.1.0/docs/PLAN-v0.2.md +34 -0
  21. docxtpl_lint-1.1.0/docs/PLAN-v1.0.md +29 -0
  22. docxtpl_lint-1.1.0/docs/PLAN-v1.1.md +37 -0
  23. docxtpl_lint-1.1.0/docs/decisions.md +107 -0
  24. docxtpl_lint-1.1.0/docs/deepseek-review.md +954 -0
  25. docxtpl_lint-1.1.0/docs/demo-zh.svg +271 -0
  26. docxtpl_lint-1.1.0/docs/demo.svg +279 -0
  27. docxtpl_lint-1.1.0/docs/eval/adversarial_cases.json +88 -0
  28. docxtpl_lint-1.1.0/docs/eval/adversarial_results_v1.0.0.json +995 -0
  29. docxtpl_lint-1.1.0/docs/eval/deepseek-eval.md +59 -0
  30. docxtpl_lint-1.1.0/docs/eval/self-eval-transcript-v1.0.0.txt +387 -0
  31. docxtpl_lint-1.1.0/docs/eval/self-eval-transcript-v1.1.0.txt +135 -0
  32. docxtpl_lint-1.1.0/docs/eval/self-eval.md +69 -0
  33. docxtpl_lint-1.1.0/docs/playground.png +0 -0
  34. docxtpl_lint-1.1.0/docs/rules.md +471 -0
  35. docxtpl_lint-1.1.0/examples/clean.data.json +12 -0
  36. docxtpl_lint-1.1.0/examples/clean.docx +0 -0
  37. docxtpl_lint-1.1.0/examples/issue378_unclosed.docx +0 -0
  38. docxtpl_lint-1.1.0/examples/issue603_table_loop.docx +0 -0
  39. docxtpl_lint-1.1.0/examples/issue644_percent.docx +0 -0
  40. docxtpl_lint-1.1.0/pyproject.toml +48 -0
  41. docxtpl_lint-1.1.0/scripts/adversarial.py +114 -0
  42. docxtpl_lint-1.1.0/scripts/benchmark.py +112 -0
  43. docxtpl_lint-1.1.0/scripts/build_site.sh +24 -0
  44. docxtpl_lint-1.1.0/scripts/make_demo.py +42 -0
  45. docxtpl_lint-1.1.0/scripts/pyodide_smoke.mjs +57 -0
  46. docxtpl_lint-1.1.0/scripts/realworld.py +105 -0
  47. docxtpl_lint-1.1.0/site/index.html +115 -0
  48. docxtpl_lint-1.1.0/src/docxtpl_lint/__init__.py +6 -0
  49. docxtpl_lint-1.1.0/src/docxtpl_lint/__main__.py +3 -0
  50. docxtpl_lint-1.1.0/src/docxtpl_lint/cli.py +411 -0
  51. docxtpl_lint-1.1.0/src/docxtpl_lint/config.py +77 -0
  52. docxtpl_lint-1.1.0/src/docxtpl_lint/contract.py +298 -0
  53. docxtpl_lint-1.1.0/src/docxtpl_lint/core.py +866 -0
  54. docxtpl_lint-1.1.0/src/docxtpl_lint/fixes.py +187 -0
  55. docxtpl_lint-1.1.0/src/docxtpl_lint/messages.py +203 -0
  56. docxtpl_lint-1.1.0/src/docxtpl_lint/reader.py +123 -0
  57. docxtpl_lint-1.1.0/src/docxtpl_lint/verify.py +70 -0
  58. docxtpl_lint-1.1.0/tests/conftest.py +12 -0
  59. docxtpl_lint-1.1.0/tests/make_fixtures.py +104 -0
  60. docxtpl_lint-1.1.0/tests/rule_cases.py +184 -0
  61. docxtpl_lint-1.1.0/tests/test_docxtpl_behaviour.py +35 -0
  62. docxtpl_lint-1.1.0/tests/test_reader.py +122 -0
  63. docxtpl_lint-1.1.0/tests/test_rules.py +285 -0
  64. docxtpl_lint-1.1.0/tests/test_v02.py +128 -0
  65. docxtpl_lint-1.1.0/tests/test_v1.py +230 -0
  66. docxtpl_lint-1.1.0/tests/test_v11.py +79 -0
@@ -0,0 +1,31 @@
1
+ name: Bug report / false positive
2
+ description: docxtpl-lint reported something wrong, missed a problem, or crashed
3
+ labels: [bug]
4
+ body:
5
+ - type: dropdown
6
+ id: kind
7
+ attributes:
8
+ label: Kind
9
+ options: [False positive, Missed problem, Crash, Other]
10
+ validations: { required: true }
11
+ - type: textarea
12
+ id: output
13
+ attributes:
14
+ label: docxtpl-lint output
15
+ description: Paste the command and its output (`--no-color`).
16
+ render: text
17
+ validations: { required: true }
18
+ - type: textarea
19
+ id: docxtpl
20
+ attributes:
21
+ label: What docxtpl does with this template
22
+ description: Does `DocxTemplate(...).render(context)` raise, or is the output wrong? Paste the error if any.
23
+ - type: input
24
+ id: versions
25
+ attributes:
26
+ label: Versions
27
+ placeholder: docxtpl-lint 0.1.0, docxtpl 0.20.2, Python 3.12
28
+ validations: { required: true }
29
+ - type: markdown
30
+ attributes:
31
+ value: Please attach a **minimal .docx** that reproduces the problem (remove confidential content). Without one we usually cannot confirm the behaviour.
@@ -0,0 +1 @@
1
+ blank_issues_enabled: true
@@ -0,0 +1,20 @@
1
+ name: Rule request
2
+ description: Propose a new deterministic check
3
+ labels: [rule-request]
4
+ body:
5
+ - type: textarea
6
+ id: problem
7
+ attributes:
8
+ label: Template mistake
9
+ description: What do people write in the .docx, and what goes wrong?
10
+ validations: { required: true }
11
+ - type: textarea
12
+ id: behaviour
13
+ attributes:
14
+ label: What docxtpl does
15
+ description: Error message, or how the rendered output is wrong. Link upstream issues if any.
16
+ validations: { required: true }
17
+ - type: textarea
18
+ id: detect
19
+ attributes:
20
+ label: How it could be detected without false positives
@@ -0,0 +1,3 @@
1
+ - [ ] `pytest` and `ruff check . && ruff format --check .` pass
2
+ - [ ] New/changed docxtpl rule has a case in `tests/rule_cases.py` (rendered with real docxtpl)
3
+ - [ ] `CHANGELOG.md` updated (Unreleased)
@@ -0,0 +1,84 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ pull_request:
5
+ schedule:
6
+ - cron: "0 3 * * 1" # weekly: catch docxtpl upstream changes
7
+ jobs:
8
+ test:
9
+ runs-on: ${{ matrix.os }}
10
+ strategy:
11
+ fail-fast: false
12
+ matrix:
13
+ os: [ubuntu-latest]
14
+ python: ["3.10", "3.11", "3.12", "3.13"]
15
+ include:
16
+ - os: windows-latest
17
+ python: "3.12"
18
+ steps:
19
+ - uses: actions/checkout@v7
20
+ - uses: actions/setup-python@v7
21
+ with:
22
+ python-version: ${{ matrix.python }}
23
+ - run: pip install -e ".[dev]"
24
+ - run: ruff check . && ruff format --check .
25
+ - run: pytest -q
26
+ docxtpl-upstream:
27
+ # Ground truth against the newest docxtpl release and upstream master.
28
+ runs-on: ubuntu-latest
29
+ continue-on-error: true
30
+ strategy:
31
+ matrix:
32
+ docxtpl: ["docxtpl", "git+https://github.com/elapouya/python-docx-template.git"]
33
+ steps:
34
+ - uses: actions/checkout@v7
35
+ - uses: actions/setup-python@v7
36
+ with:
37
+ python-version: "3.12"
38
+ - run: pip install -e ".[dev]" && pip install --upgrade "${{ matrix.docxtpl }}"
39
+ - run: pytest -q tests/test_docxtpl_behaviour.py
40
+ benchmark:
41
+ # 0 false positives on python-docx-template's own test templates (benchmark/expected.json).
42
+ runs-on: ubuntu-latest
43
+ steps:
44
+ - uses: actions/checkout@v7
45
+ - uses: actions/setup-python@v7
46
+ with:
47
+ python-version: "3.12"
48
+ - run: pip install -e .[render]
49
+ - run: git clone --depth 1 https://github.com/elapouya/python-docx-template /tmp/docxtpl
50
+ - run: python scripts/benchmark.py /tmp/docxtpl/tests/templates --json benchmark-result.json
51
+ - run: cat benchmark-result.json >> "$GITHUB_STEP_SUMMARY"
52
+ if: always()
53
+ playground:
54
+ # The wheel must keep working inside Pyodide (browser playground).
55
+ runs-on: ubuntu-latest
56
+ steps:
57
+ - uses: actions/checkout@v7
58
+ - uses: actions/setup-python@v7
59
+ with:
60
+ python-version: "3.12"
61
+ - uses: actions/setup-node@v7
62
+ with:
63
+ node-version: "24"
64
+ - run: pip install build && python -m build --wheel && pip install dist/*.whl && bash scripts/build_site.sh
65
+ - run: npm install --no-save pyodide@314.0.7 && node scripts/pyodide_smoke.mjs _site
66
+ action:
67
+ # Dogfood the composite action on the bundled examples (path with a space included).
68
+ runs-on: ubuntu-latest
69
+ steps:
70
+ - uses: actions/checkout@v7
71
+ - run: mkdir -p "tmp dir" && cp examples/clean.docx "tmp dir/clean copy.docx"
72
+ - uses: ./
73
+ with:
74
+ files: |
75
+ examples/clean.docx
76
+ tmp dir/*.docx
77
+ build:
78
+ runs-on: ubuntu-latest
79
+ steps:
80
+ - uses: actions/checkout@v7
81
+ - uses: actions/setup-python@v7
82
+ with:
83
+ python-version: "3.12"
84
+ - run: pip install build twine && python -m build && twine check dist/*
@@ -0,0 +1,39 @@
1
+ name: Playground (GitHub Pages)
2
+ # Deploys site/ + the current wheel to GitHub Pages.
3
+ # Needs: Settings → Pages → Source: "GitHub Actions". Runs on pushes to main
4
+ # that touch the package or the site, or by hand. (A `release` trigger runs on the
5
+ # tag ref, which the github-pages environment rejects by default.)
6
+ on:
7
+ push:
8
+ branches: [main]
9
+ paths: ["src/**", "site/**", "scripts/build_site.sh", "examples/**", "pyproject.toml"]
10
+ workflow_dispatch:
11
+ permissions:
12
+ contents: read
13
+ pages: write
14
+ id-token: write
15
+ concurrency:
16
+ group: pages
17
+ cancel-in-progress: true
18
+ jobs:
19
+ build:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v7
23
+ - uses: actions/setup-python@v7
24
+ with:
25
+ python-version: "3.12"
26
+ - run: pip install build && python -m build --wheel && pip install dist/*.whl && bash scripts/build_site.sh
27
+ - uses: actions/configure-pages@v6
28
+ - uses: actions/upload-pages-artifact@v5
29
+ with:
30
+ path: _site
31
+ deploy:
32
+ needs: build
33
+ runs-on: ubuntu-latest
34
+ environment:
35
+ name: github-pages
36
+ url: ${{ steps.deployment.outputs.page_url }}
37
+ steps:
38
+ - id: deployment
39
+ uses: actions/deploy-pages@v5
@@ -0,0 +1,45 @@
1
+ name: Publish to PyPI
2
+ # PyPI Trusted Publishing (OIDC, no token stored in GitHub).
3
+ # Prepared but deliberately manual: it only runs when started from the Actions
4
+ # tab ("Run workflow") with the tag to publish. One-time setup on pypi.org:
5
+ # Publishing → Add a pending publisher → owner yq6666-66, repo docxtpl-lint,
6
+ # workflow publish-pypi.yml, environment pypi.
7
+ on:
8
+ workflow_dispatch:
9
+ inputs:
10
+ tag:
11
+ description: Tag to publish, e.g. v0.2.0
12
+ required: true
13
+ jobs:
14
+ build:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v7
18
+ with:
19
+ ref: ${{ inputs.tag }}
20
+ - uses: actions/setup-python@v7
21
+ with:
22
+ python-version: "3.12"
23
+ - run: pip install build twine && python -m build && twine check dist/*
24
+ - name: Version must match tag
25
+ run: |
26
+ v=$(python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
27
+ test "v$v" = "${{ inputs.tag }}"
28
+ - uses: actions/upload-artifact@v7
29
+ with:
30
+ name: dist
31
+ path: dist/
32
+ publish:
33
+ needs: build
34
+ runs-on: ubuntu-latest
35
+ environment:
36
+ name: pypi
37
+ url: https://pypi.org/project/docxtpl-lint/
38
+ permissions:
39
+ id-token: write
40
+ steps:
41
+ - uses: actions/download-artifact@v8
42
+ with:
43
+ name: dist
44
+ path: dist/
45
+ - uses: pypa/gh-action-pypi-publish@v1.14.2
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ _site/
8
+ node_modules/
9
+ package*.json
@@ -0,0 +1,7 @@
1
+ - id: docxtpl-lint
2
+ name: docxtpl-lint
3
+ description: Lint docxtpl / Jinja2 Word templates
4
+ entry: docxtpl-lint
5
+ language: python
6
+ files: \.docx$
7
+ exclude: (^|/)~\$
@@ -0,0 +1,100 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [1.1.0] - 2026-10-16
10
+
11
+ Found by an adversarial run (86 cases designed by DeepSeek, disputes settled by rendering with docxtpl) and a
12
+ self-evaluation from a fresh install (docs/eval/).
13
+
14
+ ### Added
15
+ - DTL210: `{%p %}` block opened and closed in the same paragraph (docxtpl raises).
16
+ - `--explain RULE`.
17
+ - `fix` suggests the missing `}` of a `{{ … }` tag.
18
+ - `scripts/adversarial.py` + `benchmark/adversarial_adjudicated.json`: the 86 cases run in the test-suite.
19
+ - `scripts/realworld.py`: invoice / Chinese contract / broken / 3000-paragraph templates used by the self-evaluation.
20
+
21
+ ### Fixed
22
+ - `}}` or `%}` inside a string literal no longer ends the tag (false DTL103).
23
+ - A tag missing its closing brace no longer swallows the following paragraphs (it was only a DTL002 warning and
24
+ hid later errors); it is reported as DTL001 and scanning continues at the next tag.
25
+ - Unclosed `{% raw %}` is reported (DTL101).
26
+ - DTL209: comments are not counted as lost text; text in other cells of a `{%tr %}` row and in other paragraphs of a
27
+ `{%tc %}` cell is now checked; no DTL209 next to DTL201/202/203 or a same-paragraph `{%p %}` pair.
28
+ - `--data` / `--schema` JSON with a UTF-8 BOM (Windows Notepad) is accepted.
29
+ - `infer` / `verify` / `diff` / `fix` exit with code 2 and a one-line message on unreadable input instead of a traceback.
30
+ - Playground: the 1.0.0 page failed to load (escaped newlines in the embedded Python); the Pyodide smoke test now runs
31
+ the page's own code. A hint appears if loading takes over 30 s.
32
+ - `fix` diff headers: no `a//tmp/...` double slash.
33
+
34
+ ### Changed
35
+ - `infer` writes `x-docxtpl-templates` only when several templates are merged.
36
+ - Clearer `--data` help text.
37
+
38
+ ## [1.0.0] - 2026-10-16
39
+
40
+ ### Added
41
+ - `docxtpl-lint infer`: data contract of one or more templates as JSON Schema (2020-12) plus sample data; lists from
42
+ `for` loops, numbers from `round`/`int`/`float`/`abs`, arithmetic and numeric comparisons, objects from `.items()`,
43
+ optional fields from `is defined`/`default`; merged schemas list the templates using each field
44
+ (`x-docxtpl-templates`).
45
+ - `docxtpl-lint verify` (optional extra `[render]`): render with the real docxtpl and sentinel data; DTL401 = rendered
46
+ value silently missing from the output, DTL402 = render fails.
47
+ - `docxtpl-lint diff old.docx new.docx [--fail-on-breaking]`: data contract changes between template versions,
48
+ with rename hints.
49
+ - `--schema schema.json`: DTL304 (variable not declared), DTL305 (loop over a non-iterable type).
50
+ - `docxtpl-lint fix`: fix suggestions as unified diffs of the paragraph text for DTL202, DTL203, DTL204, DTL102 (stray tag
51
+ or misspelt opening tag), DTL103 (unknown tag) and DTL301 (with `--data`); conservative matching (≤ 2 edits, no ties).
52
+ - DTL209: text next to a `{%p %}`/`{%tr %}`/`{%tc %}` tag is silently dropped by docxtpl (found with `verify`).
53
+ - Playground: fix suggestions, inferred JSON Schema and sample data.
54
+ - Benchmark runs `verify` on docxtpl's 40 templates (37 render with inferred data, 0 false DTL401).
55
+
56
+ ### Changed
57
+ - Pages deploy runs on pushes to `main` (the `release` trigger ran on the tag ref, which the `github-pages` environment
58
+ rejects).
59
+ - Development status: Production/Stable.
60
+
61
+ ## [0.2.0] - 2026-10-16
62
+
63
+ ### Added
64
+ - Browser playground (Pyodide, runs fully client-side), deployed to GitHub Pages by `pages.yml`.
65
+ - `[tool.docxtpl-lint]` configuration in `pyproject.toml` (`--config`, `--no-config`); unknown keys are errors.
66
+ - `--context N`: show neighbouring paragraphs next to each issue (default 1 on a terminal, 0 otherwise).
67
+ - DTL208: `cellbg`/`colspan`/`vm`/`hm` tag outside a table cell.
68
+ - `docs/rules.md` with one anchor per rule; `help_url` in JSON output and `helpUri` in SARIF.
69
+ - Benchmark against docxtpl's own 40 test templates (`scripts/benchmark.py`, CI job); stale allow-list entries fail it.
70
+ - Manual-only PyPI Trusted Publishing workflow (`publish-pypi.yml`).
71
+
72
+ ### Fixed
73
+ - False positives on docxtpl cell tags (`{% cellbg %}`, `{% colspan %}`, `{% vm %}`, `{% hm %}`) and on Word smart quotes inside tags: 7 → 0 on the benchmark.
74
+
75
+ ### Changed
76
+ - GitHub Actions updated to Node 24 versions (`checkout@v7`, `setup-python@v7`, ...); the composite action uses `setup-python@v7`.
77
+ - New dependency on Python < 3.11: `tomli`.
78
+
79
+ ## [0.1.0] - 2026-10-16
80
+
81
+ ### Added
82
+ - First release.
83
+ - Locations down to the table cell (`T1 R2 C2 > T2 R2 C1`, nested tables) for body, headers, footers and footnotes.
84
+ - Tag and block rules: DTL001 (tag never closed), DTL002 (tag spans paragraphs, warning), DTL101 (unclosed block),
85
+ DTL102 (stray end/else), DTL103 (other Jinja2 syntax errors, using docxtpl's default environment).
86
+ - docxtpl tag rules: DTL201 (`%`/`}` in prefixed tags, #644), DTL202 (space before prefix), DTL203 (tr/tc outside
87
+ a table), DTL204 (prefix mismatch, #603), DTL205 (tr block spans tables), DTL206 (tr open/close in the same row),
88
+ DTL207 (tc block spans rows).
89
+ - Data rules with `--data` (repeatable): DTL301 (missing path, nested and loop-aware, respects `is defined` /
90
+ `| default`), DTL302 (unused key, with `--report-unused-data`), DTL303 (loop over a non-iterable).
91
+ - `--format text|json|github|sarif`, `--lang en|zh`, `--select`, `--ignore`, `--warnings-as-errors`, `--list-vars`,
92
+ `--list-rules`, `--allow-cross-paragraph`; exit codes 0/1/2.
93
+ - File-level suppression comment `{# docxtpl-lint: disable=… #}`.
94
+ - GitHub Action, pre-commit hook, and test cases checked against real docxtpl 0.20.2 rendering.
95
+
96
+ [Unreleased]: https://github.com/yq6666-66/docxtpl-lint/compare/v1.1.0...HEAD
97
+ [1.1.0]: https://github.com/yq6666-66/docxtpl-lint/compare/v1.0.0...v1.1.0
98
+ [1.0.0]: https://github.com/yq6666-66/docxtpl-lint/compare/v0.2.0...v1.0.0
99
+ [0.2.0]: https://github.com/yq6666-66/docxtpl-lint/compare/v0.1.0...v0.2.0
100
+ [0.1.0]: https://github.com/yq6666-66/docxtpl-lint/releases/tag/v0.1.0
@@ -0,0 +1,75 @@
1
+ # Contributing to docxtpl-lint
2
+
3
+ Thanks for your interest in improving docxtpl-lint. This document covers the
4
+ practical steps for setting up, testing, and submitting changes.
5
+
6
+ ## Development setup
7
+
8
+ ```bash
9
+ python -m venv .venv
10
+ . .venv/bin/activate # Windows: .venv\Scripts\activate
11
+ pip install -e ".[dev]"
12
+ ```
13
+
14
+ ## Checks before submitting
15
+
16
+ Run the test suite:
17
+
18
+ ```bash
19
+ pytest
20
+ ```
21
+
22
+ Run linting and formatting checks:
23
+
24
+ ```bash
25
+ ruff check . && ruff format --check .
26
+ ```
27
+
28
+ Both must pass. Please keep changes focused; unrelated refactors make review
29
+ harder.
30
+
31
+ ## Project layout
32
+
33
+ - `src/docxtpl_lint/messages.py` — rule definitions: `id`, `severity`,
34
+ `summary`, and the `en`/`zh` messages.
35
+ - `src/docxtpl_lint/core.py` — rule implementations.
36
+
37
+ ## Adding a rule
38
+
39
+ Rules must be deterministic. Prefer missing a report over emitting a false
40
+ positive. Automatic fixes are out of scope — do not add them.
41
+
42
+ When adding a docxtpl-related rule:
43
+
44
+ 1. Define the rule in `src/docxtpl_lint/messages.py` and implement it in
45
+ `src/docxtpl_lint/core.py`.
46
+ 2. Add a **bad** case to `tests/rule_cases.py`, and a **good** case whenever
47
+ one is meaningful.
48
+ 3. `tests/test_docxtpl_behaviour.py` renders the cases with the real `docxtpl`
49
+ package to prove the problem actually exists (either it `raises`, or it
50
+ fails `silent`). Make sure your case is covered there.
51
+
52
+ ## Fixtures and demo assets
53
+
54
+ - Example `.docx` files are generated by `tests/make_fixtures.py`.
55
+ - README screenshots are generated by `scripts/make_demo.py` (requires
56
+ `rich`).
57
+
58
+ Regenerate these when your change affects them, and commit the results.
59
+
60
+ ## Reporting bugs
61
+
62
+ Open an issue with a minimal `.docx` that reproduces the problem and the
63
+ `docxtpl` version you are using. Without a reproduction we usually cannot
64
+ confirm the behaviour.
65
+
66
+ ## Commits and changelog
67
+
68
+ - Write commit messages in the imperative mood (e.g. "Add rule for ...").
69
+ - Add an entry to the `Unreleased` section of `CHANGELOG.md`.
70
+
71
+ ## Pull requests
72
+
73
+ Keep each PR scoped to one change. Describe what the rule or fix does, and
74
+ reference the issue it addresses. Ensure `pytest` and `ruff` pass locally
75
+ before requesting review.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 docxtpl-lint contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.