Compare commits
349 Commits
revert-91-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f96b0b6413 | ||
|
|
560b4b3311 | ||
|
|
e77cbf8c7b | ||
|
|
7a6ddbcc54 | ||
|
|
d9bb19cdd4 | ||
|
|
416729dddc | ||
|
|
cbbeae57bd | ||
|
|
606d189787 | ||
|
|
399761f9c9 | ||
|
|
4477729a57 | ||
|
|
b391b213eb | ||
|
|
e766040c5b | ||
|
|
fb0e3583da | ||
|
|
40861400b3 | ||
|
|
9392b06da4 | ||
|
|
a48a6a96b2 | ||
|
|
685425095b | ||
|
|
08a5d748f6 | ||
|
|
ebc770a291 | ||
|
|
97df786298 | ||
|
|
994da90f66 | ||
|
|
aa7b7a77e3 | ||
|
|
b97fa32f10 | ||
|
|
def1205179 | ||
|
|
eed645c0c5 | ||
|
|
60caff83fa | ||
|
|
65e00b189e | ||
|
|
0f42e5a4c8 | ||
|
|
bc92389ee1 | ||
|
|
0ef74a740e | ||
|
|
0d616add84 | ||
|
|
9124386b4c | ||
|
|
1cca257016 | ||
|
|
a449f01f36 | ||
|
|
2812487300 | ||
|
|
2886a851e0 | ||
|
|
0061b556ec | ||
|
|
0607e7175d | ||
|
|
d99607003c | ||
|
|
b9be3a5b50 | ||
|
|
83ecb60bdd | ||
|
|
a10c3c16e4 | ||
|
|
d472308ae5 | ||
|
|
ce25ec8228 | ||
|
|
ac38610947 | ||
|
|
a50b52b317 | ||
|
|
f3ba468927 | ||
|
|
b7872d7723 | ||
|
|
0642badebc | ||
|
|
664bb845fa | ||
|
|
e0d5eb0e0a | ||
|
|
d322120d2a | ||
|
|
58de890f92 | ||
|
|
bee0706645 | ||
|
|
24de22bfc6 | ||
|
|
d43c6debbf | ||
|
|
d539119fed | ||
|
|
d042d4592c | ||
|
|
af2e09209b | ||
|
|
d9801fd6e4 | ||
|
|
6e51079b2a | ||
|
|
9af9d9e69b | ||
|
|
708581fa6e | ||
|
|
e3b40ec959 | ||
|
|
724623671c | ||
|
|
ef09ce6277 | ||
|
|
ce23f81cbe | ||
|
|
5d9bba118a | ||
|
|
006d5bc506 | ||
|
|
4047845c3a | ||
|
|
1a4a5f5723 | ||
|
|
650b28d852 | ||
|
|
890549d705 | ||
|
|
52d9b28a31 | ||
|
|
4857a6ed72 | ||
|
|
bd4b2be81d | ||
|
|
ea987f9e38 | ||
|
|
a9122c34d4 | ||
|
|
b75867397a | ||
|
|
badce481c6 | ||
|
|
85a7b92bf3 | ||
|
|
0660a0f37e | ||
|
|
2fd2140caf | ||
|
|
d4415c34a4 | ||
|
|
a712baff03 | ||
|
|
7816ea6270 | ||
|
|
5f21feb72d | ||
|
|
7290528b3d | ||
|
|
a0f6589d34 | ||
|
|
0ab1144a8d | ||
|
|
6da5022b41 | ||
|
|
bda05f2101 | ||
|
|
25a6e42850 | ||
|
|
624bc15a10 | ||
|
|
43ceb38465 | ||
|
|
2681c4801d | ||
|
|
c1f994e632 | ||
|
|
9b4e623c3c | ||
|
|
6bfd2eb85a | ||
|
|
d283f3f88a | ||
|
|
b4e533ae66 | ||
|
|
4e073ea451 | ||
|
|
8a3c2051ad | ||
|
|
9a7ac8f4e9 | ||
|
|
00b9a97222 | ||
|
|
605844aa17 | ||
|
|
b95a23395f | ||
|
|
23038b98de | ||
|
|
19733a6b5b | ||
|
|
d2037d2a54 | ||
|
|
e574b6c650 | ||
|
|
589262664f | ||
|
|
d22aeb41db | ||
|
|
4a9acdcf79 | ||
|
|
234ec65e9d | ||
|
|
e3e1ab58d1 | ||
|
|
727d07fa3e | ||
|
|
0238472033 | ||
|
|
5e8aeae272 | ||
|
|
5dfe448a9a | ||
|
|
541d5ce7cb | ||
|
|
b0a5de05aa | ||
|
|
d900f9296a | ||
|
|
1f3ef9b236 | ||
|
|
e4737ddfbb | ||
|
|
7b2d7e1946 | ||
|
|
2dee2ddd1c | ||
|
|
5f3fe03108 | ||
|
|
b6f9c992f5 | ||
|
|
a78462ee0f | ||
|
|
8004fcf6bc | ||
|
|
a45cc806d8 | ||
|
|
21a0a46a43 | ||
|
|
9708802400 | ||
|
|
61c8924663 | ||
|
|
b7d782ab20 | ||
|
|
6722d63537 | ||
|
|
bf2240f4b2 | ||
|
|
ab61d39847 | ||
|
|
f780714a52 | ||
|
|
30712bbc67 | ||
|
|
0a1cb24b12 | ||
|
|
7a65b2a310 | ||
|
|
a435b1c46a | ||
|
|
ef622f28e5 | ||
|
|
44fb098904 | ||
|
|
ba9a4629e9 | ||
|
|
60151d6677 | ||
|
|
af2b027c9f | ||
|
|
f4042d9d53 | ||
|
|
537dc4485a | ||
|
|
7ac23c81d0 | ||
|
|
b8c6cb67f5 | ||
|
|
63cd4c31f7 | ||
|
|
883e9f7e0b | ||
|
|
2c578949b9 | ||
|
|
d90087d6f7 | ||
|
|
6b41cc2579 | ||
|
|
8d512d2edd | ||
|
|
2b2628c6df | ||
|
|
04d1bfc5fe | ||
|
|
30578cf88c | ||
|
|
6496abf307 | ||
|
|
bc6d88251a | ||
|
|
3d95dd88f7 | ||
|
|
0c5ebcbc54 | ||
|
|
79994ed2c8 | ||
|
|
53d582c407 | ||
|
|
4af6f2282f | ||
|
|
344a7d1676 | ||
|
|
44a0819625 | ||
|
|
b616daf50a | ||
|
|
7282856484 | ||
|
|
e67b27f06e | ||
|
|
f9b95afe7a | ||
|
|
9946c74fdc | ||
|
|
528c3b1ca5 | ||
|
|
3413052ef2 | ||
|
|
c1e7a1cf6f | ||
|
|
ff2fa37b2b | ||
|
|
cf8e493e93 | ||
|
|
5faef9f429 | ||
|
|
b9fa41b714 | ||
|
|
72b6754a18 | ||
|
|
e895607d26 | ||
|
|
53ed9bba32 | ||
|
|
8428b72dec | ||
|
|
b1453e6eba | ||
|
|
fe4e76459f | ||
|
|
92613a9435 | ||
|
|
3b4de31734 | ||
|
|
4840f9e709 | ||
|
|
6c21fab272 | ||
|
|
4d81ef8eaa | ||
|
|
74d46579be | ||
|
|
1b9ec6f253 | ||
|
|
7363428782 | ||
|
|
53bf8175d9 | ||
|
|
8bf2fa511d | ||
|
|
5ee1c03410 | ||
|
|
01fd27df11 | ||
|
|
581717e5ee | ||
|
|
aba8c6bb7b | ||
|
|
a9ebb9d78d | ||
|
|
9175f0cbd6 | ||
|
|
45a46eb924 | ||
|
|
ccdf20a121 | ||
|
|
97be69b950 | ||
|
|
ccac13390a | ||
|
|
2b75638aa8 | ||
|
|
cba0521d55 | ||
|
|
e8bc36c57e | ||
|
|
888dd085f6 | ||
|
|
dc571cfaf7 | ||
|
|
227abbbb54 | ||
|
|
444f862efa | ||
|
|
17a2c6e1fe | ||
|
|
4efd49c8f2 | ||
|
|
114e5149bc | ||
|
|
b65d04750e | ||
|
|
72a9be558a | ||
|
|
dd588034e9 | ||
|
|
a7c5531f20 | ||
|
|
4fe1cc507d | ||
|
|
9a4dd4e28c | ||
|
|
74fbb615bb | ||
|
|
e3bf22376a | ||
|
|
1f80377013 | ||
|
|
1bba2f92b7 | ||
|
|
b01f033d14 | ||
|
|
b3094f660a | ||
|
|
671c7c78a7 | ||
|
|
68fb31ec22 | ||
|
|
28ed545e85 | ||
|
|
333a046180 | ||
|
|
ab7adb5d21 | ||
|
|
9f1cb53205 | ||
|
|
fbb930d3ea | ||
|
|
d9b5a4d2ab | ||
|
|
d3ddb59d1c | ||
|
|
a021c517f5 | ||
|
|
b46f4f8ff5 | ||
|
|
53ad9eedc4 | ||
|
|
c86b1ffeb5 | ||
|
|
5c89f43eb8 | ||
|
|
e956892d61 | ||
|
|
20819a7368 | ||
|
|
cd77712b5e | ||
|
|
8472d4ed55 | ||
|
|
a69aa8777f | ||
|
|
7cb606e2da | ||
|
|
3291da7ff3 | ||
|
|
527de6190f | ||
|
|
0bbe5a80b9 | ||
|
|
53be7aa768 | ||
|
|
355432ccb0 | ||
|
|
12e709f80b | ||
|
|
651b59fc21 | ||
|
|
01825a1fa2 | ||
|
|
2fad92046e | ||
|
|
e6b7b58c92 | ||
|
|
5f6a5c4d6c | ||
|
|
68d2fd6e84 | ||
|
|
bafdf1a310 | ||
|
|
758daeeaab | ||
|
|
aa088a274b | ||
|
|
8ffc7b80e8 | ||
|
|
195d6a7090 | ||
|
|
126b2a6b37 | ||
|
|
a81c7cf31a | ||
|
|
b48b6b1b82 | ||
|
|
484c2c32bc | ||
|
|
32e39b7b67 | ||
|
|
25825a79f1 | ||
|
|
cd89aa33aa | ||
|
|
0382ece29d | ||
|
|
77043c1a66 | ||
|
|
c15ece28c1 | ||
|
|
e5c4c14966 | ||
|
|
20e30c541c | ||
|
|
b0560c87b9 | ||
|
|
9aa6019d25 | ||
|
|
8b351236f7 | ||
|
|
d73a47fc6b | ||
|
|
01f4d16767 | ||
|
|
fe7362f4d4 | ||
|
|
be2e9942bc | ||
|
|
4564fed0b3 | ||
|
|
d5335cb36f | ||
|
|
5ac9c5f2c6 | ||
|
|
2420a560e3 | ||
|
|
bc9820e7cf | ||
|
|
69f8320fd4 | ||
|
|
8dc7cc7de3 | ||
|
|
7e72603441 | ||
|
|
40bf39cde7 | ||
|
|
5ee43cf3e1 | ||
|
|
c04483e4d5 | ||
|
|
344fe8215d | ||
|
|
0c2b27584d | ||
|
|
4ea5d5fdaa | ||
|
|
b987864c3f | ||
|
|
a3b3fdfa18 | ||
|
|
fe9a34ebbd | ||
|
|
01b2f2e397 | ||
|
|
b84b52e97e | ||
|
|
0c02520b6d | ||
|
|
98ad7eebf7 | ||
|
|
4b0b1da2eb | ||
|
|
d6bfc8be79 | ||
|
|
f807749be5 | ||
|
|
aa903ac99f | ||
|
|
ba1d4458ed | ||
|
|
5ae0dbbf29 | ||
|
|
7f994dc107 | ||
|
|
cb6971009f | ||
|
|
0a3627d23a | ||
|
|
23fbe8c048 | ||
|
|
c9afd31320 | ||
|
|
3b221fb0c6 | ||
|
|
5456b1cfad | ||
|
|
76345f454f | ||
|
|
871dd85d49 | ||
|
|
95ffebb3fe | ||
|
|
86350034bb | ||
|
|
8453ad58de | ||
|
|
197807d272 | ||
|
|
a3c15b226e | ||
|
|
7437268425 | ||
|
|
f2e76d0e61 | ||
|
|
aa9889fc26 | ||
|
|
ee9cfd1d3e | ||
|
|
fc9eb4733f | ||
|
|
03703cb84a | ||
|
|
f6bbbcf52a | ||
|
|
57fbf33ff5 | ||
|
|
3d96e6e5f1 | ||
|
|
41671a0363 | ||
|
|
e35f8858f9 | ||
|
|
10dba16ce9 | ||
|
|
52df98c914 | ||
|
|
0903bf42fc | ||
|
|
992b52f223 | ||
|
|
a7348b2dfb | ||
|
|
2a7630a80b | ||
|
|
12f759cb8e | ||
|
|
399d8d995c | ||
|
|
a322097223 | ||
|
|
b94fad8232 |
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report to help us fix the code
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Describe the bug
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
## To Reproduce
|
||||
Please, provide a fully standalone runnable test case : minimal python code + docx template + other files if needed (images etc...)
|
||||
|
||||
## Expected behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
## Screenshots
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
## Additional context
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/help-request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Help request
|
||||
about: Ask help to the community
|
||||
title: ''
|
||||
labels: help wanted
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Describe your problem
|
||||
A clear and concise description of what you want to do
|
||||
|
||||
## More details about your problem
|
||||
Steps to reproduce the behavior, expected behavior etc...
|
||||
|
||||
## Provide a test case
|
||||
If applicable, provide minimal python code + docx template + other files (images etc...) to reproduce the behavior
|
||||
|
||||
## Screenshots
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
20
.github/ISSUE_TEMPLATE/request-for-enhancement.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Request for enhancement
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: Request for enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Is your feature request related to a problem? Please describe.
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
## Describe the solution you'd like
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
## Describe alternatives you've considered
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
## Additional context
|
||||
Add any other context or screenshots about the feature request here.
|
||||
25
.github/workflows/codestyle.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
# As per the example on https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions
|
||||
name: Python codestyling
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
pip install flake8
|
||||
# stop the build if there are code styling problems. The GitHub editor is 127 chars wide.
|
||||
flake8 . --count --max-line-length=127 --show-source --statistics
|
||||
1
.gitignore
vendored
@ -21,6 +21,7 @@ var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
tests/output/*
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
|
||||
35
.readthedocs.yaml
Normal file
@ -0,0 +1,35 @@
|
||||
# Read the Docs configuration file for Sphinx projects
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the OS, Python version and other tools you might need
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.12"
|
||||
# You can also specify other tool versions:
|
||||
# nodejs: "20"
|
||||
# rust: "1.70"
|
||||
# golang: "1.20"
|
||||
|
||||
# Build documentation in the "docs/" directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
|
||||
# builder: "dirhtml"
|
||||
# Fail on all warnings to avoid broken references
|
||||
# fail_on_warning: true
|
||||
|
||||
# Optionally build your docs in additional formats such as PDF and ePub
|
||||
# formats:
|
||||
# - pdf
|
||||
# - epub
|
||||
|
||||
# Optional but recommended, declare the Python requirements required
|
||||
# to build your documentation
|
||||
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
||||
python:
|
||||
install:
|
||||
- requirements: requirements.txt
|
||||
255
CHANGES.rst
@ -1,118 +1,225 @@
|
||||
0.4.1 (2017-09-10)
|
||||
------------------
|
||||
- Improve image attachment for InlineImage ojects
|
||||
0.20.2 *(Unreleased)*
|
||||
-------------------
|
||||
- Move docxcompose to optional dependency (Thanks to Waket Zheng)
|
||||
|
||||
0.4.0 (2017-09-09)
|
||||
------------------
|
||||
0.20.1 (2025-07-15)
|
||||
-------------------
|
||||
- Fix and improve get_undeclared_template_variables() method (Thanks to Pablo Esteban)
|
||||
|
||||
0.20.0 (2024-12-29)
|
||||
-------------------
|
||||
- Add RichTextParagraph (Thanks to ST-Imrie)
|
||||
- Add RTL support for bold/italic (Thanks to bm-rana)
|
||||
- Update documentation
|
||||
|
||||
0.19.1 (2024-12-29)
|
||||
-------------------
|
||||
- PR #575 : fix unicode in footnotes (Thanks to Jonathan Pyle)
|
||||
|
||||
0.19.0 (2024-11-12)
|
||||
-------------------
|
||||
- Support rendering variables in footnotes (Thanks to Bart Broere)
|
||||
|
||||
0.18.0 (2024-07-21)
|
||||
-------------------
|
||||
- IMPORTANT : Remove Python 2.x support
|
||||
- Add hyperlink option in InlineImage (Thanks to Jean Marcos da Rosa)
|
||||
- Update index.rst (Thanks to jkpet)
|
||||
- Add poetry env
|
||||
- Black all files
|
||||
|
||||
0.17.0 (2024-05-01)
|
||||
-------------------
|
||||
- Add support to python-docx 1.1.1
|
||||
|
||||
0.16.8 (2024-02-23)
|
||||
-------------------
|
||||
- PR #527 : upgrade Jinja2 in Pipfile.lock
|
||||
|
||||
0.16.7 (2023-05-08)
|
||||
-------------------
|
||||
- PR #493 - thanks to AdrianVorobel
|
||||
|
||||
0.16.6 (2023-03-12)
|
||||
-------------------
|
||||
- PR #482 - thanks to dreizehnutters
|
||||
|
||||
0.16.5 (2023-01-07)
|
||||
-------------------
|
||||
- PR #467 - thanks to Slarag
|
||||
- fix #465
|
||||
- fix #464
|
||||
|
||||
0.16.4 (2022-08-04)
|
||||
-------------------
|
||||
- Regional fonts for RichText
|
||||
- Reorganize documentation
|
||||
|
||||
0.16.3 (2022-07-14)
|
||||
-------------------
|
||||
- fix #448
|
||||
|
||||
0.16.2 (2022-07-14)
|
||||
-------------------
|
||||
- fix #444
|
||||
- fix #443
|
||||
|
||||
0.16.1 (2022-06-12)
|
||||
-------------------
|
||||
- PR #442
|
||||
|
||||
0.16.0 (2022-04-16)
|
||||
-------------------
|
||||
- add jinja2 comment support - Thanks to staffanm
|
||||
|
||||
0.15.2 (2022-01-12)
|
||||
-------------------
|
||||
- fix #408
|
||||
- Multi-rendering with same DocxTemplate object is now possible
|
||||
see tests/multi_rendering.py
|
||||
- fix #392
|
||||
- fix #398
|
||||
|
||||
0.14.1 (2021-10-01)
|
||||
-------------------
|
||||
- One can now use python -m docxtpl on command line
|
||||
to generate a docx from a template and a json file as a context
|
||||
Thanks to Lcrs123@github
|
||||
|
||||
0.12.0 (2021-08-15)
|
||||
-------------------
|
||||
- Code has be split into many files for better readability
|
||||
- Use docxcomposer to attach parts when a docx file is given to create a subdoc
|
||||
Images, styles etc... must now be taken in account in subdocs
|
||||
- Some internal XML IDs are now renumbered to avoid collision, thus images are not randomly disapearing anymore.
|
||||
- fix #372
|
||||
- fix #374
|
||||
- fix #375
|
||||
- fix #369
|
||||
- fix #368
|
||||
- fix #347
|
||||
- fix #181
|
||||
- fix #61
|
||||
|
||||
0.11.5 (2021-05-09)
|
||||
-------------------
|
||||
- PR #351
|
||||
- It is now possible to put InlineImage in header/footer
|
||||
- fix #323
|
||||
- fix #320
|
||||
- \\n, \\a, \\t and \\f are now accepted in simple context string. Thanks to chabErch@github
|
||||
|
||||
0.10.5 (2020-10-15)
|
||||
-------------------
|
||||
- Remove extension testing (#297)
|
||||
- Fix spaces missing in some cases (#116, #227)
|
||||
|
||||
0.9.2 (2020-04-26)
|
||||
-------------------
|
||||
- Fix #271
|
||||
- Code styling
|
||||
|
||||
0.8.1 (2020-04-14)
|
||||
-------------------
|
||||
- fix #266
|
||||
- docxtpl is now able to use latest python-docx (0.8.10). Thanks to Dutchy-@github.
|
||||
|
||||
0.7.0 (2020-04-09)
|
||||
-------------------
|
||||
- Add replace_zipname() method to replace Excel and PowerPoint embedded files
|
||||
|
||||
0.6.4 (2020-04-06)
|
||||
-------------------
|
||||
- Add the possibility to add RichText to a Richtext
|
||||
- Prevent lxml from attempting to parse None
|
||||
- PR #207 and #209
|
||||
- Handle spaces correctly when run are split by Jinja code (#205)
|
||||
- PR #203
|
||||
- DocxTemplate now accepts file-like objects (Thanks to edufresne)
|
||||
|
||||
0.5.20 (2019-05-23)
|
||||
-------------------
|
||||
- Fix #199
|
||||
- Add support for file-like objects for replace_media (#197)
|
||||
- Fix #176
|
||||
- Delegated autoescaping to Jinja2 Environment (#175)
|
||||
- Force to use python-docx 0.8.7 (#170)
|
||||
- Add getting undeclared variables in the template (#171)
|
||||
- Added `PAGE_BREAK` feature (#168)
|
||||
- Fixed issue #159: autoescaped values for both str and unicode.
|
||||
- Fix tables with gridSpan that have less cells after the tc forloop (#164)
|
||||
- Smart double quotes in jinja tags are now converted into simple double quotes
|
||||
- Smart quotes in jinja tags are now converted into simple quotes
|
||||
- Add custom jinja filter example in tests/
|
||||
- Reformat the code to be a little more PEP8 compliant
|
||||
- Add {% hm %} tag for table columns horizontal merging (Thanks to nickgashkov)
|
||||
- Split tests/tests_files dir into templates and output dirs
|
||||
- autoescape support for python 2.7
|
||||
- fix issue #154
|
||||
- Render can now autoescape context dict
|
||||
- Fix invalid xml parse because using {% vm %}
|
||||
- Cast to string non-string value given to RichText or Listing objects
|
||||
- Import html.escape instead of cgi.escape (deprecated)
|
||||
- Declare package as python2 and python3 compatible for wheel distrib
|
||||
- Add sub/superscript in RichText
|
||||
- Fix table vertical merge
|
||||
- An hyperlink can now be used in RichText
|
||||
|
||||
0.4.13 (2018-06-21)
|
||||
-------------------
|
||||
- Subdocument can now be based on an existing docx
|
||||
- Add font option in RichText
|
||||
- Better tabs and spaces management for MS Word 2016
|
||||
- Wheel distribution
|
||||
- Manage autoscaping on InlineImage, Richtext and Subdoc
|
||||
- Purge MANIFEST.in file
|
||||
- Accept variables starting with 'r' in {{}} when no space after {{
|
||||
- Remove debug traces
|
||||
- Add {% vm %} to merge cell vertically within a loop (Thanks to Arthaslixin)
|
||||
- use six.iteritems() instead of iteritems for python 3 compatibility
|
||||
- Fixed Bug #95 on replace_pic() method
|
||||
- Add replace_pic() method to replace pictures from its filename (Thanks to Riccardo Gusmeroli)
|
||||
- Improve image attachment for InlineImage ojects
|
||||
- Add replace_media() method (useful for header/footer images)
|
||||
- Add replace_embedded() method (useful for embedding docx)
|
||||
|
||||
0.3.9 (2017-06-27)
|
||||
------------------
|
||||
- Fix exception in fix_table()
|
||||
|
||||
0.3.8 (2017-06-20)
|
||||
------------------
|
||||
- Fix bug when using more than one {{r }} or {%r %} in the same run
|
||||
|
||||
0.3.7 (2017-06-13)
|
||||
------------------
|
||||
- Fix git tag v0.3.6 was in fact for 0.3.5 package version
|
||||
so create a tag 0.3.7 for 0.3.7 package version
|
||||
|
||||
0.3.6 (2017-06-10)
|
||||
------------------
|
||||
- Better head/footer jinja2 handling (Thanks to hugokernel)
|
||||
|
||||
0.3.5 (2017-02-20)
|
||||
------------------
|
||||
- Fix bug where one is using '%' (modulo operator) inside a tag
|
||||
|
||||
0.3.4 (2017-02-14)
|
||||
------------------
|
||||
- Add Listing class to manage \n and \a (new paragraph) and escape text AND keep current styling
|
||||
|
||||
0.3.3 (2017-02-07)
|
||||
------------------
|
||||
- Add {%tc } tags for dynamic table columns (Thanks to majkls23)
|
||||
|
||||
0.3.2 (2017-01-16)
|
||||
------------------
|
||||
- Remove version limitation over sphinx package in setup.py
|
||||
|
||||
0.3.1 (2017-01-16)
|
||||
------------------
|
||||
- Add PNG & JPEG in tests/test_files/
|
||||
|
||||
0.3.0 (2017-01-15)
|
||||
------------------
|
||||
- You can now add images directly without using subdoc, it is much more faster.
|
||||
|
||||
0.2.5 (2017-01-14)
|
||||
------------------
|
||||
- Add dynamic colspan tag for tables
|
||||
|
||||
0.2.4 (2016-11-30)
|
||||
------------------
|
||||
- Fix /n in RichText class
|
||||
|
||||
0.2.3 (2016-08-09)
|
||||
------------------
|
||||
- Add Python 3 support for footer and header
|
||||
|
||||
0.2.2 (2016-06-11)
|
||||
------------------
|
||||
- Fix bug when using utf-8 chracters inside footer or header in .docx template
|
||||
It now detects header/footer encoding automatically
|
||||
|
||||
0.2.1 (2016-06-11)
|
||||
------------------
|
||||
- Fix bug where using subdocs is corrupting header and footer in generated docx
|
||||
Thanks to Denny Weinberg for his help.
|
||||
|
||||
0.2.0 (2016-03-17)
|
||||
------------------
|
||||
- Add Header and Footer support (Thanks to Denny Weinberg)
|
||||
|
||||
0.1.11 (2016-03-1)
|
||||
------------------
|
||||
- '>' and '<' can now be used inside jinja tags
|
||||
|
||||
0.1.10 (2016-02-11)
|
||||
-------------------
|
||||
- render() accepts optionnal jinja_env argument :
|
||||
useful to set custom filters and other things
|
||||
|
||||
0.1.9 (2016-01-18)
|
||||
------------------
|
||||
- better subdoc management : accept tables
|
||||
|
||||
0.1.8 (2015-11-05)
|
||||
------------------
|
||||
- better xml code cleaning around Jinja2 tags
|
||||
|
||||
0.1.7 (2015-09-09)
|
||||
------------------
|
||||
- python 3 support
|
||||
|
||||
0.1.6 (2015-05-11)
|
||||
------------------
|
||||
- remove debug code
|
||||
- add lxml dependency
|
||||
|
||||
0.1.5 (2015-05-11)
|
||||
------------------
|
||||
- fix template filter with quote
|
||||
|
||||
0.1.4 (2015-03-27)
|
||||
------------------
|
||||
- add RichText support
|
||||
|
||||
0.1.3 (2015-03-13)
|
||||
------------------
|
||||
- add subdoc support
|
||||
- add some exemples in tests/
|
||||
|
||||
0.1.2 (2015-03-12)
|
||||
------------------
|
||||
- First running version
|
||||
|
||||
@ -1,7 +1,2 @@
|
||||
graft docs
|
||||
prune docs/_build
|
||||
graft pyquery
|
||||
graft tests
|
||||
include *_fixt.py *.rst *.cfg *.ini
|
||||
global-exclude *.pyc
|
||||
global-exclude __pycache__
|
||||
13
Pipfile
Normal file
@ -0,0 +1,13 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
|
||||
[dev-packages]
|
||||
docxtpl = {editable = true, path = "."}
|
||||
flake8 = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3"
|
||||
419
Pipfile.lock
generated
Normal file
@ -0,0 +1,419 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "0368a5e08ceb2b4910a110742515b5ff1d04a3a3af2b91b49d922ef9aaab6915"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {},
|
||||
"develop": {
|
||||
"babel": {
|
||||
"hashes": [
|
||||
"sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d",
|
||||
"sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.17.0"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f",
|
||||
"sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd",
|
||||
"sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea",
|
||||
"sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981",
|
||||
"sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b",
|
||||
"sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7",
|
||||
"sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8",
|
||||
"sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175",
|
||||
"sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d",
|
||||
"sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392",
|
||||
"sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad",
|
||||
"sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f",
|
||||
"sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f",
|
||||
"sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b",
|
||||
"sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875",
|
||||
"sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3",
|
||||
"sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800",
|
||||
"sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65",
|
||||
"sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2",
|
||||
"sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812",
|
||||
"sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50",
|
||||
"sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==24.10.0"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2",
|
||||
"sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==8.1.8"
|
||||
},
|
||||
"docxcompose": {
|
||||
"hashes": [
|
||||
"sha256:bcf2799a0b63c29eb77a3d799a2f28443ae0f69f8691ff3d753f706be515c3e9"
|
||||
],
|
||||
"version": "==1.4.0"
|
||||
},
|
||||
"docxtpl": {
|
||||
"editable": true,
|
||||
"path": "."
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343",
|
||||
"sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.2.0"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d",
|
||||
"sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.1.6"
|
||||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
"sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5",
|
||||
"sha256:073eb6dcdf1f587d9b88c8c93528b57eccda40209cf9be549d469b942b41d70b",
|
||||
"sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49",
|
||||
"sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c",
|
||||
"sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b",
|
||||
"sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba",
|
||||
"sha256:0e108352e203c7afd0eb91d782582f00a0b16a948d204d4dec8565024fafeea5",
|
||||
"sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7",
|
||||
"sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422",
|
||||
"sha256:1320091caa89805df7dcb9e908add28166113dcd062590668514dbd510798c88",
|
||||
"sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8",
|
||||
"sha256:14479c2ad1cb08b62bb941ba8e0e05938524ee3c3114644df905d2331c76cd57",
|
||||
"sha256:151d6c40bc9db11e960619d2bf2ec5829f0aaffb10b41dcf6ad2ce0f3c0b2325",
|
||||
"sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a",
|
||||
"sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982",
|
||||
"sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8",
|
||||
"sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55",
|
||||
"sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2",
|
||||
"sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df",
|
||||
"sha256:226046e386556a45ebc787871d6d2467b32c37ce76c2680f5c608e25823ffc84",
|
||||
"sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551",
|
||||
"sha256:24f6df5f24fc3385f622c0c9d63fe34604893bc1a5bdbb2dbf5870f85f9a404a",
|
||||
"sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740",
|
||||
"sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e",
|
||||
"sha256:2b31a3a77501d86d8ade128abb01082724c0dfd9524f542f2f07d693c9f1175f",
|
||||
"sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60",
|
||||
"sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e",
|
||||
"sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6",
|
||||
"sha256:32697d2ea994e0db19c1df9e40275ffe84973e4232b5c274f47e7c1ec9763cdd",
|
||||
"sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd",
|
||||
"sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609",
|
||||
"sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20",
|
||||
"sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6",
|
||||
"sha256:4025bf2884ac4370a3243c5aa8d66d3cb9e15d3ddd0af2d796eccc5f0244390e",
|
||||
"sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61",
|
||||
"sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4",
|
||||
"sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776",
|
||||
"sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779",
|
||||
"sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6",
|
||||
"sha256:48b4afaf38bf79109bb060d9016fad014a9a48fb244e11b94f74ae366a64d252",
|
||||
"sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c",
|
||||
"sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92",
|
||||
"sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5",
|
||||
"sha256:4cd915c0fb1bed47b5e6d6edd424ac25856252f09120e3e8ba5154b6b921860e",
|
||||
"sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f",
|
||||
"sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54",
|
||||
"sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877",
|
||||
"sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e",
|
||||
"sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37",
|
||||
"sha256:53d9469ab5460402c19553b56c3648746774ecd0681b1b27ea74d5d8a3ef5590",
|
||||
"sha256:56dbdbab0551532bb26c19c914848d7251d73edb507c3079d6805fa8bba5b706",
|
||||
"sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142",
|
||||
"sha256:5cca36a194a4eb4e2ed6be36923d3cffd03dcdf477515dea687185506583d4c9",
|
||||
"sha256:5f11a1526ebd0dee85e7b1e39e39a0cc0d9d03fb527f56d8457f6df48a10dc0c",
|
||||
"sha256:61c7bbf432f09ee44b1ccaa24896d21075e533cd01477966a5ff5a71d88b2f56",
|
||||
"sha256:639978bccb04c42677db43c79bdaa23785dc7f9b83bfd87570da8207872f1ce5",
|
||||
"sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987",
|
||||
"sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729",
|
||||
"sha256:67ed8a40665b84d161bae3181aa2763beea3747f748bca5874b4af4d75998f87",
|
||||
"sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7",
|
||||
"sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7",
|
||||
"sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf",
|
||||
"sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28",
|
||||
"sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056",
|
||||
"sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7",
|
||||
"sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e",
|
||||
"sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0",
|
||||
"sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872",
|
||||
"sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079",
|
||||
"sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4",
|
||||
"sha256:7be701c24e7f843e6788353c055d806e8bd8466b52907bafe5d13ec6a6dbaecd",
|
||||
"sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9",
|
||||
"sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121",
|
||||
"sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7",
|
||||
"sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b",
|
||||
"sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d",
|
||||
"sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76",
|
||||
"sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530",
|
||||
"sha256:9776af1aad5a4b4a1317242ee2bea51da54b2a7b7b48674be736d463c999f37d",
|
||||
"sha256:97dac543661e84a284502e0cf8a67b5c711b0ad5fb661d1bd505c02f8cf716d7",
|
||||
"sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9",
|
||||
"sha256:9b4a3bd174cc9cdaa1afbc4620c049038b441d6ba07629d89a83b408e54c35cd",
|
||||
"sha256:9c886b481aefdf818ad44846145f6eaf373a20d200b5ce1a5c8e1bc2d8745410",
|
||||
"sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40",
|
||||
"sha256:a11a96c3b3f7551c8a8109aa65e8594e551d5a84c76bf950da33d0fb6dfafab7",
|
||||
"sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b",
|
||||
"sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5",
|
||||
"sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5",
|
||||
"sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1",
|
||||
"sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997",
|
||||
"sha256:ab339536aa798b1e17750733663d272038bf28069761d5be57cb4a9b0137b4f8",
|
||||
"sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc",
|
||||
"sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563",
|
||||
"sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c",
|
||||
"sha256:b0989737a3ba6cf2a16efb857fb0dfa20bc5c542737fddb6d893fde48be45433",
|
||||
"sha256:b108134b9667bcd71236c5a02aad5ddd073e372fb5d48ea74853e009fe38acb6",
|
||||
"sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4",
|
||||
"sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4",
|
||||
"sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f",
|
||||
"sha256:b7c86884ad23d61b025989d99bfdd92a7351de956e01c61307cb87035960bcb1",
|
||||
"sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa",
|
||||
"sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f",
|
||||
"sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e",
|
||||
"sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063",
|
||||
"sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4",
|
||||
"sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5",
|
||||
"sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571",
|
||||
"sha256:c70e93fba207106cb16bf852e421c37bbded92acd5964390aad07cb50d60f5cf",
|
||||
"sha256:ca755eebf0d9e62d6cb013f1261e510317a41bf4650f22963474a663fdfe02aa",
|
||||
"sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d",
|
||||
"sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de",
|
||||
"sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd",
|
||||
"sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86",
|
||||
"sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82",
|
||||
"sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f",
|
||||
"sha256:dc0af80267edc68adf85f2a5d9be1cdf062f973db6790c1d065e45025fa26140",
|
||||
"sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250",
|
||||
"sha256:de6f6bb8a7840c7bf216fb83eec4e2f79f7325eca8858167b68708b929ab2172",
|
||||
"sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba",
|
||||
"sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751",
|
||||
"sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff",
|
||||
"sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c",
|
||||
"sha256:eaf24066ad0b30917186420d51e2e3edf4b0e2ea68d8cd885b14dc8afdcf6556",
|
||||
"sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44",
|
||||
"sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8",
|
||||
"sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7",
|
||||
"sha256:fa0e294046de09acd6146be0ed6727d1f42ded4ce3ea1e9a19c11b6774eea27c",
|
||||
"sha256:fb54f7c6bafaa808f27166569b1511fc42701a7713858dddc08afdde9746849e",
|
||||
"sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.4.0"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4",
|
||||
"sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30",
|
||||
"sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0",
|
||||
"sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9",
|
||||
"sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396",
|
||||
"sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13",
|
||||
"sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028",
|
||||
"sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca",
|
||||
"sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557",
|
||||
"sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832",
|
||||
"sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0",
|
||||
"sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b",
|
||||
"sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579",
|
||||
"sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a",
|
||||
"sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c",
|
||||
"sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff",
|
||||
"sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c",
|
||||
"sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22",
|
||||
"sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094",
|
||||
"sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb",
|
||||
"sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e",
|
||||
"sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5",
|
||||
"sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a",
|
||||
"sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d",
|
||||
"sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a",
|
||||
"sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b",
|
||||
"sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8",
|
||||
"sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225",
|
||||
"sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c",
|
||||
"sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144",
|
||||
"sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f",
|
||||
"sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87",
|
||||
"sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d",
|
||||
"sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93",
|
||||
"sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf",
|
||||
"sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158",
|
||||
"sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84",
|
||||
"sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb",
|
||||
"sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48",
|
||||
"sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171",
|
||||
"sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c",
|
||||
"sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6",
|
||||
"sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd",
|
||||
"sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d",
|
||||
"sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1",
|
||||
"sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d",
|
||||
"sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca",
|
||||
"sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a",
|
||||
"sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29",
|
||||
"sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe",
|
||||
"sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798",
|
||||
"sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c",
|
||||
"sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8",
|
||||
"sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f",
|
||||
"sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f",
|
||||
"sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a",
|
||||
"sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178",
|
||||
"sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0",
|
||||
"sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79",
|
||||
"sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430",
|
||||
"sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==3.0.2"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
|
||||
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
"sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505",
|
||||
"sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484",
|
||||
"sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==25.0"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
"sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
|
||||
"sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.12.1"
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94",
|
||||
"sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==4.3.7"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9",
|
||||
"sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==2.13.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a",
|
||||
"sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==3.3.2"
|
||||
},
|
||||
"python-docx": {
|
||||
"hashes": [
|
||||
"sha256:08c20d6058916fb19853fcf080f7f42b6270d89eac9fa5f8c15f691c0017fabe",
|
||||
"sha256:0cf1f22e95b9002addca7948e16f2cd7acdfd498047f1941ca5d293db7762efd"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.1.2"
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:2e308396e1d83de287ada2c2fd6e64286008fe6aca5008e0b6a8cb0e2c86eedd",
|
||||
"sha256:ea0e7655c05b74819f82e76e11a85b31779fee7c4969e82f72bab0664e8317e4"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==80.1.0"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274",
|
||||
"sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.17.0"
|
||||
},
|
||||
"tomli": {
|
||||
"hashes": [
|
||||
"sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6",
|
||||
"sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd",
|
||||
"sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c",
|
||||
"sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b",
|
||||
"sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8",
|
||||
"sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6",
|
||||
"sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77",
|
||||
"sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff",
|
||||
"sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea",
|
||||
"sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192",
|
||||
"sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249",
|
||||
"sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee",
|
||||
"sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4",
|
||||
"sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98",
|
||||
"sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8",
|
||||
"sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4",
|
||||
"sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281",
|
||||
"sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744",
|
||||
"sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69",
|
||||
"sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13",
|
||||
"sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140",
|
||||
"sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e",
|
||||
"sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e",
|
||||
"sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc",
|
||||
"sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff",
|
||||
"sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec",
|
||||
"sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2",
|
||||
"sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222",
|
||||
"sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106",
|
||||
"sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272",
|
||||
"sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a",
|
||||
"sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"version": "==2.2.1"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c",
|
||||
"sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"
|
||||
],
|
||||
"markers": "python_version < '3.11'",
|
||||
"version": "==4.13.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
18
README.rst
@ -14,19 +14,25 @@ This package uses 2 major packages :
|
||||
|
||||
python-docx-template has been created because python-docx is powerful for creating documents but not for modifying them.
|
||||
|
||||
The idea is to begin to create an exemple of the document you want to generate with microsoft word, it can be as complex as you want :
|
||||
The idea is to begin to create an example of the document you want to generate with microsoft word, it can be as complex as you want :
|
||||
pictures, index tables, footer, header, variables, anything you can do with word.
|
||||
Then, as you are still editing the document with microsoft word, you insert jinja2-like tags directly in the document.
|
||||
You save the document as a .docx file (xml format) : it will be your .docx template file.
|
||||
|
||||
Now you can use python-docx-template to generate as many word documents you want from this .docx template and context variables you will associate.
|
||||
|
||||
Share
|
||||
-----
|
||||
|
||||
If you like this project, please rate and share it here : http://rate.re/github/elapouya/python-docx-template
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
Please, `read the doc <http://docxtpl.readthedocs.org>`_
|
||||
Please, `read the doc <http://docxtpl.readthedocs.org>`_
|
||||
|
||||
Other projects
|
||||
--------------
|
||||
|
||||
If you like python-docx-template, please have a look at some of my other projects :
|
||||
|
||||
- `django-listing <https://github.com/elapouya/django-listing>`_ : A listing/table library on steroid for Djano
|
||||
- `python-textops3 <https://github.com/elapouya/python-textops3>`_ : Chainable text operations
|
||||
- `django-robohash-svg <https://github.com/elapouya/django-robohash-svg>`_ : Create svg robots avatars
|
||||
|
||||
|
||||
157
docs/conf.py
@ -12,215 +12,215 @@
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
"sphinx.ext.autodoc",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = u'python-docx-template'
|
||||
copyright = u'2015, Eric Lapouyade'
|
||||
project = "python-docx-template"
|
||||
copyright = "2015, Eric Lapouyade"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.1'
|
||||
version = "0.20"
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.1.5'
|
||||
release = "0.20.x"
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
# language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['_build']
|
||||
exclude_patterns = ["_build"]
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
# modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
#keep_warnings = False
|
||||
# keep_warnings = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'default'
|
||||
html_theme = "sphinx_book_theme"
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
# html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
html_static_path = ["_static"]
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#html_extra_path = []
|
||||
# html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
# html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
# html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'python-docx-templatedoc'
|
||||
htmlhelp_basename = "python-docx-templatedoc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
# 'papersize': 'letterpaper',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
# 'pointsize': '10pt',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
# 'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'python-docx-template.tex', u'python-docx-template Documentation',
|
||||
u'Eric Lapouyade', 'manual'),
|
||||
(
|
||||
"index",
|
||||
"python-docx-template.tex",
|
||||
"python-docx-template Documentation",
|
||||
"Eric Lapouyade",
|
||||
"manual",
|
||||
),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
# latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
@ -228,12 +228,17 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'python-docx-template', u'python-docx-template Documentation',
|
||||
[u'Eric Lapouyade'], 1)
|
||||
(
|
||||
"index",
|
||||
"python-docx-template",
|
||||
"python-docx-template Documentation",
|
||||
["Eric Lapouyade"],
|
||||
1,
|
||||
)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
# man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
@ -242,19 +247,25 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'python-docx-template', u'python-docx-template Documentation',
|
||||
u'Eric Lapouyade', 'python-docx-template', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(
|
||||
"index",
|
||||
"python-docx-template",
|
||||
"python-docx-template Documentation",
|
||||
"Eric Lapouyade",
|
||||
"python-docx-template",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
# texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
# texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
# texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#texinfo_no_detailmenu = False
|
||||
# texinfo_no_detailmenu = False
|
||||
|
||||
391
docs/index.rst
@ -8,10 +8,14 @@ Welcome to python-docx-template's documentation!
|
||||
|
||||
.. rubric:: Quickstart
|
||||
|
||||
To install::
|
||||
To install using pip::
|
||||
|
||||
pip install docxtpl
|
||||
|
||||
or using conda::
|
||||
|
||||
conda install docxtpl --channel conda-forge
|
||||
|
||||
Usage::
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
@ -38,8 +42,6 @@ You save the document as a .docx file (xml format) : it will be your .docx templ
|
||||
|
||||
Now you can use python-docx-template to generate as many word documents you want from this .docx template and context variables you will associate.
|
||||
|
||||
Note : python-docx-template as been tested with MS Word 97, it may not work with other version.
|
||||
|
||||
Jinja2-like syntax
|
||||
------------------
|
||||
|
||||
@ -49,10 +51,16 @@ Nevertheless there are some restrictions and extensions to make it work inside a
|
||||
Restrictions
|
||||
++++++++++++
|
||||
|
||||
The usual jinja2 tags, are only to be used inside a same run of a same paragraph, it can not be used across several paragraphs, table rows, runs.
|
||||
The usual jinja2 tags, are only to be used inside the same run of a same paragraph, it can not be used across several paragraphs, table rows, runs.
|
||||
If you want to manage paragraphs, table rows and a whole run with its style, you must use special tag syntax as explained in next chapter.
|
||||
|
||||
Note : a 'run' for microsoft word is a sequence of characters with the same style. For example, if you create a paragraph with all characters the same style :
|
||||
word will create internally only one 'run' in the paragraph. Now, if you put in bold a text in the middle of this paragraph, word will transform the previous 'run' into 3 'runs' (normal - bold - normal).
|
||||
**Note:**
|
||||
|
||||
a 'run' for Microsoft Word is a sequence of characters with the same style.
|
||||
For example, if you create a paragraph with all characters of the same style,
|
||||
MS Word will create internally only one 'run' in the paragraph. Now,
|
||||
if you put in bold a text in the middle of this paragraph,
|
||||
word will transform the previous 'run' into 3 different 'runs' (normal - bold - normal).
|
||||
|
||||
Extensions
|
||||
++++++++++
|
||||
@ -60,15 +68,56 @@ Extensions
|
||||
Tags
|
||||
....
|
||||
|
||||
In order to manage paragraphs, table rows, table columns, runs, special syntax has to be used ::
|
||||
In order to manage paragraphs, table rows, table columns, runs, special syntax has to be used::
|
||||
|
||||
{%p jinja2_tag %} for paragraphs
|
||||
{%tr jinja2_tag %} for table rows
|
||||
{%tc jinja2_tag %} for table columns
|
||||
{%r jinja2_tag %} for runs
|
||||
|
||||
By using these tags, python-docx-template will take care to put the real jinja2 tags at the right place into the document's xml source code.
|
||||
In addition, these tags also tells python-docx-template to remove the paragraph, table row, table column or run where are located the begin and ending tags and only takes care about what is in between.
|
||||
By using these tags, python-docx-template will take care to put the real jinja2 tags (without the `p`, `tr`, `tc` or `r`) at the right place into the document's xml source code.
|
||||
In addition, these tags also tell python-docx-template to **remove** the paragraph, table row, table column or run where the tags are located.
|
||||
|
||||
For example, if you have this kind of template::
|
||||
|
||||
{%p if display_paragraph %}
|
||||
One or many paragraphs
|
||||
{%p endif %}
|
||||
|
||||
The first and last paragraphs (those containing ``{%p ... %}`` tags) will never appear in generated docx, regardless of the ``display_paragraph`` value.
|
||||
|
||||
Here only::
|
||||
|
||||
One or many paragraphs
|
||||
|
||||
will appear in generated docx if ``display_paragraph`` is True, otherwise, no paragraph at all are displayed.
|
||||
|
||||
**IMPORTANT :** Always put space after a starting tag delimiter and a space before the ending one :
|
||||
|
||||
Avoid::
|
||||
|
||||
{%if something%}
|
||||
{%pif display_paragraph%}
|
||||
|
||||
Use instead::
|
||||
|
||||
{% if something %}
|
||||
{%p if display_paragraph %}
|
||||
|
||||
**IMPORTANT** : Do not use ``{%p``, ``{%tr``, ``{%tc`` or ``{%r`` twice in the same
|
||||
paragraph, row, column or run. Example :
|
||||
|
||||
Do not use this::
|
||||
|
||||
{%p if display_paragraph %}Here is my paragraph {%p endif %}
|
||||
|
||||
But use this instead in your docx template::
|
||||
|
||||
{%p if display_paragraph %}
|
||||
Here is my paragraph
|
||||
{%p endif %}
|
||||
|
||||
This syntax is possible because MS Word considers each line as a new paragraph (if you do not use SHIFT-RETURN).
|
||||
|
||||
Display variables
|
||||
.................
|
||||
@ -77,37 +126,100 @@ As part of jinja2, one can used double braces::
|
||||
|
||||
{{ <var> }}
|
||||
|
||||
But if ``<var>`` is an RichText object, you must specify that you are changing the actual 'run' ::
|
||||
if ``<var>`` is a string, ``\n``, ``\a``, ``\t`` and ``\f`` will be translated respectively into newlines, new paragraphs, tabs and page breaks
|
||||
|
||||
But if ``<var>`` is a RichText_ object, you must specify that you are changing the actual 'run'::
|
||||
|
||||
{{r <var> }}
|
||||
|
||||
Note the ``r`` right after the openning braces
|
||||
Note the ``r`` right after the opening braces.
|
||||
|
||||
Cell color
|
||||
..........
|
||||
**VERY IMPORTANT :** Variables must not contains characters like ``<``, ``>`` and ``&`` unless using Escaping_
|
||||
|
||||
There is a special case when you want to change the background color of a table cell, you must put the following tag at the very beginning of the cell ::
|
||||
**IMPORTANT :** Always put space after a starting var delimiter and a space before the ending one :
|
||||
|
||||
{% cellbg <var> %}
|
||||
Avoid::
|
||||
|
||||
`<var>` must contain the color's hexadecimal code *without* the hash sign
|
||||
{{myvariable}}
|
||||
{{rmyrichtext}}
|
||||
|
||||
Column spanning
|
||||
...............
|
||||
Use instead::
|
||||
|
||||
If you want to dynamically span a table cell over many column (this is useful when you have a table with a dynamic column count),
|
||||
you must put the following tag at the very beginning of the cell to span ::
|
||||
{{ myvariable }}
|
||||
{{r myrichtext }}
|
||||
|
||||
Comments
|
||||
........
|
||||
|
||||
You can add jinja-like comments in your template::
|
||||
|
||||
{#p this is a comment as a paragraph #}
|
||||
{#tr this is a comment as a table row #}
|
||||
{#tc this is a comment as a table cell #}
|
||||
|
||||
See tests/templates/comments_tpl.docx for an example.
|
||||
|
||||
Split and merge text
|
||||
....................
|
||||
|
||||
* You can merge a jinja2 tag with previous line by using ``{%-``
|
||||
* You can merge a jinja2 tag with next line by using ``-%}``
|
||||
|
||||
A text containing Jinja2 tags may be unreadable if too long::
|
||||
|
||||
My house is located {% if living_in_town %} in urban area {% else %} in countryside {% endif %} and I love it.
|
||||
|
||||
One can use *ENTER* or *SHIFT+ENTER* to split a text like below, then use ``{%-`` and ``-%}`` to tell docxtpl to merge the whole thing::
|
||||
|
||||
My house is located
|
||||
{%- if living_in_town -%}
|
||||
in urban area
|
||||
{%- else -%}
|
||||
in countryside
|
||||
{%- endif -%}
|
||||
and I love it.
|
||||
|
||||
**IMPORTANT :** Use an unbreakable space (*CTRL+SHIFT+SPACE*) when a space is wanted at line beginning or ending.
|
||||
|
||||
**IMPORTANT 2 :** ``{%- xxx -%}`` tags must be alone in a line : do not add some text before or after on the same line.
|
||||
|
||||
Escaping delimiters
|
||||
...................
|
||||
|
||||
In order to display ``{%``, ``%}``, ``{{`` or ``}}``, one can use::
|
||||
|
||||
{_%, %_}, {_{ or }_}
|
||||
|
||||
Tables
|
||||
......
|
||||
|
||||
Spanning
|
||||
~~~~~~~~
|
||||
|
||||
You can span table cells horizontally in two ways, by using ``colspan`` tag (see tests/dynamic_table.py)::
|
||||
|
||||
{% colspan <var> %}
|
||||
|
||||
`<var>` must contain an integer for the number of columns to span. See tests/test_files/dynamic_table.py for an example.
|
||||
|
||||
Escaping
|
||||
........
|
||||
You can also span horizontally within a for loop (see tests/horizontal_merge.py)::
|
||||
|
||||
In order to display ``{%``, ``%}``, ``{{`` or ``}}``, one can use ::
|
||||
{% hm %}
|
||||
|
||||
{_%, %_}, {_{ or }_}
|
||||
You can also merge cells vertically within a for loop (see tests/vertical_merge.py)::
|
||||
|
||||
{% vm %}
|
||||
|
||||
Cell color
|
||||
~~~~~~~~~~
|
||||
|
||||
There is a special case when you want to change the background color of a table cell, you must put the following tag at the very beginning of the cell::
|
||||
|
||||
{% cellbg <var> %}
|
||||
|
||||
`<var>` must contain the color's hexadecimal code *without* the hash sign
|
||||
|
||||
.. _RichText:
|
||||
|
||||
RichText
|
||||
--------
|
||||
@ -115,61 +227,139 @@ RichText
|
||||
When you use ``{{ <var> }}`` tag in your template, it will be replaced by the string contained within `var` variable.
|
||||
BUT it will keep the current style.
|
||||
If you want to add dynamically changeable style, you have to use both : the ``{{r <var> }}`` tag AND a ``RichText`` object within `var` variable.
|
||||
You can change color, bold, italic, size and so on, but the best way is to use Microsoft Word to define your own *caracter* style
|
||||
You can change color, bold, italic, size, font and so on, but the best way is to use Microsoft Word to define your own *character* style
|
||||
( Home tab -> modify style -> manage style button -> New style, select ‘Character style’ in the form ), see example in `tests/richtext.py`
|
||||
Instead of using ``RichText()``, one can use its shortcut : ``R()``
|
||||
*Important* : When you use ``{{r }}`` it removes the current character styling from your docx template, this means that if
|
||||
|
||||
The ``RichText()`` or ``R()`` offers newline, new paragraph, and page break features : just use ``\n``, ``\a``, ``\t`` or ``\f`` in the
|
||||
text, they will be converted accordingly.
|
||||
|
||||
There is a specific case for font: if your font is not displayed correctly, it may be because it is defined
|
||||
only for a region. To know your region, it requires a little work by analyzing the document.xml inside the docx template (this is a zip file).
|
||||
To specify a region, you have to prefix your font name this that region and a column::
|
||||
|
||||
ch = RichText('测试TEST', font='eastAsia:微软雅黑')
|
||||
|
||||
**Important** : When you use ``{{r }}`` it removes the current character styling from your docx template, this means that if
|
||||
you do not specify a style in ``RichText()``, the style will go back to a microsoft word default style.
|
||||
This will affect only character styles, not the paragraph styles (MSWord manages this 2 kind of styles).
|
||||
|
||||
**IMPORTANT** : Do not use 2 times ``{{r`` in the same run. Use RichText.add()
|
||||
method to concatenate several strings and styles at python side and only one
|
||||
``{{r`` at template side.
|
||||
|
||||
**Important** : ``RichText`` objects are rendered into xml *before* any filter is applied
|
||||
thus ``RichText`` are not compatible with Jinja2 filters. You cannot write in your template something like ``{{r <var>|lower }}``.
|
||||
Only solution is instead to do any filtering into your python code when creating the ``RichText`` object.
|
||||
|
||||
Hyperlink with RichText
|
||||
+++++++++++++++++++++++
|
||||
|
||||
You can add an hyperlink to a text by using a Richtext with this syntax::
|
||||
|
||||
tpl=DocxTemplate('your_template.docx')
|
||||
rt = RichText('You can add an hyperlink, here to ')
|
||||
rt.add('google',url_id=tpl.build_url_id('http://google.com'))
|
||||
|
||||
Put ``rt`` in your context, then use ``{{r rt}}`` in your template
|
||||
|
||||
RichTextParagraph
|
||||
-----------------
|
||||
|
||||
If you want to change paragraph properties, you can use ``RichTextParagraph()`` or ``RP()`` object.
|
||||
It must be added to the template by using ``{{p <var> }}``.
|
||||
Have a look to the example here ``tests/richtextparagraph.py``.
|
||||
|
||||
|
||||
Inline image
|
||||
------------
|
||||
|
||||
You can dynamically add one or many images into your document (tested with JPEG and PNG files).
|
||||
just add ``{{ <var> }}`` tag in your template where ``<var>`` is an instance of doxtpl.InlineImage ::
|
||||
just add ``{{ <var> }}`` tag in your template where ``<var>`` is an instance of doxtpl.InlineImage::
|
||||
|
||||
myimage = InlineImage(tpl,'test_files/python_logo.png',width=Mm(20))
|
||||
myimage = InlineImage(tpl, image_descriptor='test_files/python_logo.png', width=Mm(20), height=Mm(10))
|
||||
|
||||
You just have to specify the template object, the image file path and optionnally width and/or height.
|
||||
You just have to specify the template object, the image file path and optionally width and/or height.
|
||||
For height and width you have to use millimeters (Mm), inches (Inches) or points(Pt) class.
|
||||
Please see tests/inline_image.py for an example.
|
||||
|
||||
Sub-documents
|
||||
-------------
|
||||
|
||||
A template variable can contain a complex and built from scratch with python-docx word document.
|
||||
To do so, get first a sub-document object from template object and use it as a python-docx document object, see example in `tests/subdoc.py`.
|
||||
> Need to install with the subdoc extra: `pip install "docxtpl[subdoc]"`
|
||||
|
||||
Escaping, newline, new paragraph, Listing
|
||||
-----------------------------------------
|
||||
A template variable can contain a complex subdoc object and be built from scratch using python-docx document methods.
|
||||
To do so, first, get the sub-document object from your template object, then use it by treating it as a python-docx document object.
|
||||
See example in `tests/subdoc.py`.
|
||||
|
||||
When you use a ``{{ <var> }}``, you are modifying an **XML** word document, this means you cannot use all chars,
|
||||
especially ``<``, ``>`` and ``&``. In order to use them, you must escape them. There are 3 ways :
|
||||
Since docxtpl V0.12.0, it is now possible to merge an existing .docx as a subdoc, just specify its path when
|
||||
calling method `new_subdoc()` ::
|
||||
|
||||
tpl = DocxTemplate('templates/merge_docx_master_tpl.docx')
|
||||
sd = tpl.new_subdoc('templates/merge_docx_subdoc.docx')
|
||||
|
||||
context = {
|
||||
'mysubdoc': sd,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save('output/merge_docx.docx')
|
||||
|
||||
In the above example, the content of 'templates/merge_docx_subdoc.docx' will be inserted into the parent document in place of the declared
|
||||
variable `{{p mysubdoc }}`.
|
||||
|
||||
See `tests/merge_docx.py` for full code.
|
||||
|
||||
.. _Escaping:
|
||||
|
||||
Escaping
|
||||
--------
|
||||
|
||||
By default, no escaping is done : read carefully this chapter if you want to avoid crashes during docx generation.
|
||||
|
||||
When you use a ``{{ <var> }}``, under the hood, you are modifying an **XML** word document, this means you cannot use all chars,
|
||||
especially ``<``, ``>`` and ``&``. In order to use them, you must escape them. There are 4 ways :
|
||||
|
||||
* ``context = { 'var':R('my text') }`` and ``{{r <var> }}`` in the template (note the ``r``),
|
||||
* ``context = { 'var':'my text'}`` and ``{{ <var>|e }}`` in your word template
|
||||
* ``context = { 'var':escape('my text')}`` and ``{{ <var> }}`` in the template.
|
||||
|
||||
The ``RichText()`` or ``R()`` offers newline and new paragraph feature : just use ``\n`` or ``\a`` in the
|
||||
text, they will be converted accordingly.
|
||||
* enable autoescaping when calling render method: ``tpl.render(context, autoescape=True)`` (default is autoescape=False)
|
||||
|
||||
See tests/escape.py example for more informations.
|
||||
|
||||
Another solution, if you want to include a listing into your document, that is to escape the text and manage \n and \a,
|
||||
Another solution, if you want to include a listing into your document, that is to escape the text and manage ``\n``, ``\a``, and ``\f``
|
||||
you can use the ``Listing`` class :
|
||||
|
||||
in your python code ::
|
||||
in your python code::
|
||||
|
||||
context = { 'mylisting':Listing('the listing\nwith\nsome\nlines \a and some paragraph \a and special chars : <>&') }
|
||||
|
||||
in your docx template just use ``{{ mylisting }}``
|
||||
|
||||
With ``Listing()``, you will keep the current character styling (except after a ``\a`` as you start a new paragraph).
|
||||
|
||||
Replace docx pictures
|
||||
---------------------
|
||||
|
||||
It is not possible to dynamically add images in header/footer, but you can change them.
|
||||
The idea is to put a dummy picture in your template, render the template as usual, then replace the dummy picture with another one.
|
||||
You can do that for all medias at the same time.
|
||||
Note: the aspect ratio will be the same as the replaced image
|
||||
Note2 : Specify the filename that has been used to insert the image in the docx template (only its basename, not the full path)
|
||||
|
||||
Syntax to replace dummy_header_pic.jpg::
|
||||
|
||||
tpl.replace_pic('dummy_header_pic.jpg','header_pic_i_want.jpg')
|
||||
|
||||
|
||||
The replacement occurs in headers, footers and the whole document's body.
|
||||
|
||||
|
||||
Replace docx medias
|
||||
-------------------
|
||||
|
||||
It is not possible to dynamically add images in header/footer, but you can change them.
|
||||
The idea is to put a dummy picture in your template, render the template as usual, then replace the dummy picture with another one.
|
||||
It is not possible to dynamically add other medias than images in header/footer, but you can change them.
|
||||
The idea is to put a dummy media in your template, render the template as usual, then replace the dummy media with another one.
|
||||
You can do that for all medias at the same time.
|
||||
Note: for images, the aspect ratio will be the same as the replaced image
|
||||
Note2 : it is important to have the source media files as they are required to calculate their CRC to find them in the docx.
|
||||
@ -180,8 +370,9 @@ Syntax to replace dummy_header_pic.jpg::
|
||||
tpl.replace_media('dummy_header_pic.jpg','header_pic_i_want.jpg')
|
||||
|
||||
|
||||
dummy_header_pic.jpg must exist in the template directory when rendering and saving the generated docx. It must be the same
|
||||
WARNING : unlike replace_pic() method, dummy_header_pic.jpg MUST exist in the template directory when rendering and saving the generated docx. It must be the same
|
||||
file as the one inserted manually in the docx template.
|
||||
The replacement occurs in headers, footers and the whole document's body.
|
||||
|
||||
Replace embedded objects
|
||||
------------------------
|
||||
@ -190,32 +381,134 @@ It works like medias replacement, except it is for embedded objects like embedde
|
||||
|
||||
Syntax to replace embedded_dummy.docx::
|
||||
|
||||
tpl.replace_embedded('embdded_dummy.docx','embdded_docx_i_want.docx')
|
||||
tpl.replace_embedded('embedded_dummy.docx','embedded_docx_i_want.docx')
|
||||
|
||||
|
||||
embdded_dummy.docx must exist in the template directory when rendering and saving the generated docx. It must be the same
|
||||
WARNING : unlike replace_pic() method, embedded_dummy.docx MUST exist in the template directory when rendering and saving the generated docx. It must be the same
|
||||
file as the one inserted manually in the docx template.
|
||||
The replacement occurs in headers, footers and the whole document's body.
|
||||
|
||||
Note that `replace_embedded()` may not work on other documents than embedded docx.
|
||||
Instead, you should use zipname replacement::
|
||||
|
||||
tpl.replace_zipname(
|
||||
'word/embeddings/Feuille_Microsoft_Office_Excel1.xlsx',
|
||||
'my_excel_file.xlsx')
|
||||
|
||||
The zipname is the one you can find when you open docx with WinZip, 7zip (Windows) or unzip -l (Linux).
|
||||
The zipname starts with "word/embeddings/". Note that the file to be replaced is renamed by MSWord, so you have to guess a little bit...
|
||||
|
||||
This works for embedded MSWord file like Excel or PowerPoint file, but won't work for others like PDF, Python or even Text files :
|
||||
For these ones, MSWord generate an oleObjectNNN.bin file which is no use to be replaced as it is encoded.
|
||||
|
||||
Get Defined Variables
|
||||
---------------------
|
||||
|
||||
In order to get the missing variables after rendering use ::
|
||||
|
||||
tpl=DocxTemplate('your_template.docx')
|
||||
tpl.render(context_dict)
|
||||
set_of_variables = tpl.get_undeclared_template_variables(context=context_dict)
|
||||
|
||||
**IMPORTANT** : If `context` is not passed, you will get a set with all keys you need, e.g. to be prompted to a user or written in a file for manual processing.
|
||||
|
||||
Multiple rendering
|
||||
------------------
|
||||
|
||||
Since v0.15.0, it is possible to create ``DocxTemplate`` object once and call
|
||||
``render(context)`` several times. Note that if you want to use replacement
|
||||
methods like ``replace_media()``, ``replace_embedded()`` and/or ``replace_zipname()``
|
||||
during multiple rendering, you will have to call ``reset_replacements()``
|
||||
at rendering loop start.
|
||||
|
||||
|
||||
|
||||
Microsoft Word 2016 special cases
|
||||
---------------------------------
|
||||
|
||||
MS Word 2016 will ignore ``\t`` tabulations. This is special to that version.
|
||||
Libreoffice or Wordpad do not have this problem. The same thing occurs for line
|
||||
beginning with a jinja2 tag providing spaces : They will be ignored.
|
||||
To solve these problem, the solution is to use Richtext::
|
||||
|
||||
tpl.render({
|
||||
'test_space_r' : RichText(' '),
|
||||
'test_tabs_r': RichText(5*'\t'),
|
||||
})
|
||||
|
||||
And in your template, use the {{r notation::
|
||||
|
||||
{{r test_space_r}} Spaces will be preserved
|
||||
{{r test_tabs_r}} Tabs will be displayed
|
||||
|
||||
|
||||
Jinja custom filters
|
||||
--------------------
|
||||
|
||||
``render()`` accepts ``jinja_env`` optionnal argument : you may pass a jinja environment object.
|
||||
``render()`` accepts ``jinja_env`` optional argument : you may pass a jinja environment object.
|
||||
By this way you will be able to add some custom jinja filters::
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
import jinja2
|
||||
|
||||
def multiply_by(value, by):
|
||||
return value * by
|
||||
|
||||
doc = DocxTemplate("my_word_template.docx")
|
||||
context = { 'company_name' : "World company" }
|
||||
context = { 'price_dollars' : 5.00 }
|
||||
jinja_env = jinja2.Environment()
|
||||
jinja_env.filters['myfilter'] = myfilterfunc
|
||||
jinja_env.filters['multiply_by'] = multiply_by
|
||||
doc.render(context,jinja_env)
|
||||
doc.save("generated_doc.docx")
|
||||
|
||||
Then in your template, you will be able to use::
|
||||
|
||||
Euros price : {{ price_dollars|multiply_by(0.88) }}
|
||||
|
||||
|
||||
Command-line execution
|
||||
----------------------
|
||||
|
||||
One can use `docxtpl` module directly on command line to generate a docx from a template and a json file as a context::
|
||||
|
||||
usage: python -m docxtpl [-h] [-o] [-q] template_path json_path output_filename
|
||||
|
||||
Make docx file from existing template docx and json data.
|
||||
|
||||
positional arguments:
|
||||
template_path The path to the template docx file.
|
||||
json_path The path to the json file with the data.
|
||||
output_filename The filename to save the generated docx.
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-o, --overwrite If output file already exists, overwrites without asking
|
||||
for confirmation
|
||||
-q, --quiet Do not display unnecessary messages
|
||||
|
||||
|
||||
See tests/module_execute.py for an example.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
The best way to see how it works is to read examples, they are located in `tests/` directory. Templates and generated .docx files are in `tests/test_files/`.
|
||||
The best way to see how it works is to read examples, they are located in `tests/` directory.
|
||||
Docx test templates are in `tests/templates/`. To generate final docx files::
|
||||
|
||||
cd tests/
|
||||
python runtests.py
|
||||
|
||||
Generated files are located in `tests/output` directory.
|
||||
|
||||
If you are not sure about your python environment, python-docx-template provides Pipfiles
|
||||
for that::
|
||||
|
||||
pip install pipenv (if not already done)
|
||||
cd python-docx-template (where Pipfiles are)
|
||||
pipenv install --python 3.6 -d
|
||||
pipenv shell
|
||||
cd tests/
|
||||
python runtests.py
|
||||
|
||||
Share
|
||||
-----
|
||||
|
||||
@ -1,458 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
"""
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
'''
|
||||
|
||||
__version__ = '0.4.1'
|
||||
|
||||
from lxml import etree
|
||||
from docx import Document
|
||||
from docx.opc.oxml import serialize_part_xml, parse_xml
|
||||
import docx.oxml.ns
|
||||
from docx.opc.constants import RELATIONSHIP_TYPE as REL_TYPE
|
||||
from jinja2 import Template
|
||||
from cgi import escape
|
||||
import re
|
||||
import six
|
||||
import binascii
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
NEWLINE = '</w:t><w:br/><w:t xml:space="preserve">'
|
||||
NEWPARAGRAPH = '</w:t></w:r></w:p><w:p><w:r><w:t xml:space="preserve">'
|
||||
|
||||
class DocxTemplate(object):
|
||||
""" Class for managing docx files as they were jinja2 templates """
|
||||
|
||||
HEADER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
|
||||
FOOTER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
|
||||
|
||||
def __init__(self, docx):
|
||||
self.docx = Document(docx)
|
||||
self.crc_to_new_media = {}
|
||||
self.crc_to_new_embedded = {}
|
||||
self.media_to_replace = {}
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.docx, name)
|
||||
|
||||
def xml_to_string(self, xml, encoding='unicode'):
|
||||
# Be careful : pretty_print MUST be set to False, otherwise patch_xml() won't work properly
|
||||
return etree.tostring(xml, encoding='unicode', pretty_print=False)
|
||||
|
||||
def get_docx(self):
|
||||
return self.docx
|
||||
|
||||
def get_xml(self):
|
||||
return self.xml_to_string(self.docx._element.body)
|
||||
|
||||
def write_xml(self,filename):
|
||||
with open(filename,'w') as fh:
|
||||
fh.write(self.get_xml())
|
||||
|
||||
def patch_xml(self,src_xml):
|
||||
# strip all xml tags inside {% %} and {{ }} that MS word can insert into xml source
|
||||
src_xml = re.sub(r'(?<={)(<[^>]*>)+(?=[\{%])|(?<=[%\}])(<[^>]*>)+(?=\})','',src_xml,flags=re.DOTALL)
|
||||
def striptags(m):
|
||||
return re.sub('</w:t>.*?(<w:t>|<w:t [^>]*>)','',m.group(0),flags=re.DOTALL)
|
||||
src_xml = re.sub(r'{%(?:(?!%}).)*|{{(?:(?!}}).)*',striptags,src_xml,flags=re.DOTALL)
|
||||
|
||||
# manage table cell colspan
|
||||
def colspan(m):
|
||||
cell_xml = m.group(1) + m.group(3)
|
||||
cell_xml = re.sub(r'<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>','',cell_xml,flags=re.DOTALL)
|
||||
cell_xml = re.sub(r'<w:gridSpan[^/]*/>','', cell_xml, count=1)
|
||||
return re.sub(r'(<w:tcPr[^>]*>)',r'\1<w:gridSpan w:val="{{%s}}"/>' % m.group(2), cell_xml)
|
||||
src_xml = re.sub(r'(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*colspan\s+([^%]*)\s*%}(.*?</w:tc>)',colspan,src_xml,flags=re.DOTALL)
|
||||
|
||||
# manage table cell background color
|
||||
def cellbg(m):
|
||||
cell_xml = m.group(1) + m.group(3)
|
||||
cell_xml = re.sub(r'<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>','',cell_xml,flags=re.DOTALL)
|
||||
cell_xml = re.sub(r'<w:shd[^/]*/>','', cell_xml, count=1)
|
||||
return re.sub(r'(<w:tcPr[^>]*>)',r'\1<w:shd w:val="clear" w:color="auto" w:fill="{{%s}}"/>' % m.group(2), cell_xml)
|
||||
src_xml = re.sub(r'(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?</w:tc>)',cellbg,src_xml,flags=re.DOTALL)
|
||||
|
||||
# avoid {{r and {%r tags to strip MS xml tags too far
|
||||
src_xml = re.sub(r'({{r.*?}}|{%r.*?%})',r'</w:t></w:r><w:r><w:t>\1</w:t></w:r><w:r><w:t>',src_xml,flags=re.DOTALL)
|
||||
|
||||
for y in ['tr', 'tc', 'p', 'r']:
|
||||
# replace into xml code the row/paragraph/run containing {%y xxx %} or {{y xxx}} template tag
|
||||
# by {% xxx %} or {{ xx }} without any surronding <w:y> tags :
|
||||
# This is mandatory to have jinja2 generating correct xml code
|
||||
pat = r'<w:%(y)s[ >](?:(?!<w:%(y)s[ >]).)*({%%|{{)%(y)s ([^}%%]*(?:%%}|}})).*?</w:%(y)s>' % {'y':y}
|
||||
src_xml = re.sub(pat, r'\1 \2',src_xml,flags=re.DOTALL)
|
||||
|
||||
def clean_tags(m):
|
||||
return m.group(0).replace(r"‘","'").replace('<','<').replace('>','>')
|
||||
src_xml = re.sub(r'(?<=\{[\{%])(.*?)(?=[\}%]})',clean_tags,src_xml)
|
||||
|
||||
return src_xml
|
||||
|
||||
def render_xml(self,src_xml,context,jinja_env=None):
|
||||
if jinja_env:
|
||||
template = jinja_env.from_string(src_xml)
|
||||
else:
|
||||
template = Template(src_xml)
|
||||
dst_xml = template.render(context)
|
||||
dst_xml = dst_xml.replace('{_{','{{').replace('}_}','}}').replace('{_%','{%').replace('%_}','%}')
|
||||
return dst_xml
|
||||
|
||||
def build_xml(self,context,jinja_env=None):
|
||||
xml = self.get_xml()
|
||||
xml = self.patch_xml(xml)
|
||||
xml = self.render_xml(xml, context, jinja_env)
|
||||
return xml
|
||||
|
||||
def map_tree(self, tree):
|
||||
root = self.docx._element
|
||||
body = root.body
|
||||
root.replace(body, tree)
|
||||
|
||||
def get_headers_footers_xml(self, uri):
|
||||
for relKey, val in self.docx._part._rels.items():
|
||||
if val.reltype == uri:
|
||||
yield relKey, self.xml_to_string(parse_xml(val._target._blob))
|
||||
|
||||
def get_headers_footers_encoding(self,xml):
|
||||
m = re.match(r'<\?xml[^\?]+\bencoding="([^"]+)"',xml,re.I)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return 'utf-8'
|
||||
|
||||
def build_headers_footers_xml(self,context, uri,jinja_env=None):
|
||||
for relKey, xml in self.get_headers_footers_xml(uri):
|
||||
encoding = self.get_headers_footers_encoding(xml)
|
||||
xml = self.patch_xml(xml)
|
||||
xml = self.render_xml(xml, context, jinja_env)
|
||||
yield relKey, xml.encode(encoding)
|
||||
|
||||
def map_headers_footers_xml(self, relKey, xml):
|
||||
self.docx._part._rels[relKey]._target._blob = xml
|
||||
|
||||
def render(self,context,jinja_env=None):
|
||||
# Body
|
||||
xml_src = self.build_xml(context,jinja_env)
|
||||
|
||||
# fix tables if needed
|
||||
tree = self.fix_tables(xml_src)
|
||||
|
||||
self.map_tree(tree)
|
||||
|
||||
# Headers
|
||||
for relKey, xml in self.build_headers_footers_xml(context, self.HEADER_URI, jinja_env):
|
||||
self.map_headers_footers_xml(relKey, xml)
|
||||
|
||||
# Footers
|
||||
for relKey, xml in self.build_headers_footers_xml(context, self.FOOTER_URI, jinja_env):
|
||||
self.map_headers_footers_xml(relKey, xml)
|
||||
|
||||
# using of TC tag in for cycle can cause that count of columns does not correspond to
|
||||
# real count of columns in row. This function is able to fix it.
|
||||
def fix_tables(self, xml):
|
||||
tree = etree.fromstring(xml)
|
||||
# get namespace
|
||||
ns = '{' + tree.nsmap['w'] + '}'
|
||||
# walk trough xml and find table
|
||||
for t in tree.iter(ns+'tbl'):
|
||||
tblGrid = t.find(ns+'tblGrid')
|
||||
columns = tblGrid.findall(ns+'gridCol')
|
||||
to_add = 0
|
||||
# walk trough all rows and try to find if there is higher cell count
|
||||
for r in t.iter(ns+'tr'):
|
||||
cells = r.findall(ns+'tc')
|
||||
if (len(columns) + to_add) < len(cells):
|
||||
to_add = len(cells) - len(columns)
|
||||
# is neccessary to add columns?
|
||||
if to_add > 0:
|
||||
# at first, calculate width of table according to columns
|
||||
# (we want to preserve it)
|
||||
width = 0.0
|
||||
new_average = None
|
||||
for c in columns:
|
||||
if not c.get(ns+'w') == None:
|
||||
width += float(c.get(ns+'w'))
|
||||
# try to keep proportion of table
|
||||
if width > 0:
|
||||
old_average = width / len(columns)
|
||||
new_average = width / (len(columns) + to_add)
|
||||
# scale the old columns
|
||||
for c in columns:
|
||||
c.set(ns+'w', str(int(float(c.get(ns+'w')) * new_average/old_average)))
|
||||
# add new columns
|
||||
for i in range(to_add):
|
||||
etree.SubElement(tblGrid, ns+'gridCol', {ns+'w': str(int(new_average))})
|
||||
return tree
|
||||
|
||||
def new_subdoc(self):
|
||||
return Subdoc(self)
|
||||
|
||||
@staticmethod
|
||||
def get_file_crc(filename):
|
||||
with open(filename, 'rb') as fh:
|
||||
buf = fh.read()
|
||||
crc = (binascii.crc32(buf) & 0xFFFFFFFF)
|
||||
return crc
|
||||
|
||||
def replace_media(self,src_file,dst_file):
|
||||
"""Replace one media by another one into a docx
|
||||
|
||||
This has been done mainly because it is not possible to add images in docx header/footer.
|
||||
With this function, put a dummy picture in your header/footer, then specify it with its replacement in this function
|
||||
|
||||
Syntax: tpl.replace_media('dummy_media_to_replace.png','media_to_paste.jpg')
|
||||
|
||||
Note: for images, the aspect ratio will be the same as the replaced image
|
||||
Note2 : it is important to have the source media file as it is required to calculate its CRC to find them in the docx
|
||||
"""
|
||||
with open(dst_file, 'rb') as fh:
|
||||
crc = self.get_file_crc(src_file)
|
||||
self.crc_to_new_media[crc] = fh.read()
|
||||
|
||||
def replace_pic(self,embedded_file,dst_file):
|
||||
"""Replace embedded picture with original-name given by embedded_file.
|
||||
The new picture is given by dst_file.
|
||||
|
||||
Notes:
|
||||
1) embedded_file and dst_file must have the same extension/format
|
||||
2) the aspect ratio will be the same as the replaced image
|
||||
3) There is no need to keep the original file name (compare
|
||||
function replace_embedded).
|
||||
|
||||
Oct 2017 - Riccardo Gusmeroli - riccardo.gusmeroli@polimi.it
|
||||
"""
|
||||
|
||||
emp_path,emb_ext=os.path.splitext(embedded_file)
|
||||
dst_path,dst_ext=os.path.splitext(dst_file)
|
||||
|
||||
if emb_ext!=dst_ext:
|
||||
raise ValueError('replace_pic: extensions must match')
|
||||
|
||||
with open(dst_file, 'rb') as fh:
|
||||
self.media_to_replace[embedded_file]=fh.read()
|
||||
|
||||
def replace_embedded(self,src_file,dst_file):
|
||||
"""Replace one embdded object by another one into a docx
|
||||
|
||||
This has been done mainly because it is not possible to add images in docx header/footer.
|
||||
With this function, put a dummy picture in your header/footer, then specify it with its replacement in this function
|
||||
|
||||
Syntax: tpl.replace_embedded('dummy_doc.docx','doc_to_paste.docx')
|
||||
|
||||
Note2 : it is important to have the source file as it is required to calculate its CRC to find them in the docx
|
||||
"""
|
||||
with open(dst_file, 'rb') as fh:
|
||||
crc = self.get_file_crc(src_file)
|
||||
self.crc_to_new_embedded[crc] = fh.read()
|
||||
|
||||
def post_processing(self,docx_filename):
|
||||
if self.crc_to_new_media or self.crc_to_new_embedded:
|
||||
backup_filename = '%s_docxtpl_before_replace_medias' % docx_filename
|
||||
os.rename(docx_filename,backup_filename)
|
||||
|
||||
with zipfile.ZipFile(backup_filename) as zin:
|
||||
with zipfile.ZipFile(docx_filename, 'w') as zout:
|
||||
for item in zin.infolist():
|
||||
buf = zin.read(item.filename)
|
||||
if item.filename.startswith('word/media/') and item.CRC in self.crc_to_new_media:
|
||||
zout.writestr(item, self.crc_to_new_media[item.CRC])
|
||||
elif item.filename.startswith('word/embeddings/') and item.CRC in self.crc_to_new_embedded:
|
||||
zout.writestr(item, self.crc_to_new_embedded[item.CRC])
|
||||
else:
|
||||
zout.writestr(item, buf)
|
||||
|
||||
os.remove(backup_filename)
|
||||
|
||||
def pre_processing(self):
|
||||
|
||||
if self.media_to_replace:
|
||||
|
||||
pic_map={}
|
||||
|
||||
# Main document
|
||||
part=self.docx.part
|
||||
pic_map.update(self._img_filename_to_part(part))
|
||||
|
||||
# Header/Footer
|
||||
for relid, rel in self.docx.part.rels.iteritems():
|
||||
if rel.reltype in (REL_TYPE.HEADER,REL_TYPE.FOOTER):
|
||||
pic_map.update(self._img_filename_to_part(rel.target_part))
|
||||
|
||||
# Do the actual replacement
|
||||
for embedded_file,stream in self.media_to_replace.iteritems():
|
||||
pic_map[embedded_file][1]._blob=stream
|
||||
|
||||
def _img_filename_to_part(self,doc_part):
|
||||
|
||||
et=etree.fromstring(doc_part.blob)
|
||||
|
||||
part_map={}
|
||||
|
||||
vinl=et.xpath('//w:p/w:r/w:drawing/wp:inline',namespaces=docx.oxml.ns.nsmap)
|
||||
for inl in vinl:
|
||||
rel=None
|
||||
# Either IMAGE, CHART, SMART_ART, ...
|
||||
try:
|
||||
gd=inl.xpath('a:graphic/a:graphicData',namespaces=docx.oxml.ns.nsmap)[0]
|
||||
if gd.attrib['uri']==docx.oxml.ns.nsmap['pic']:
|
||||
# Either PICTURE or LINKED_PICTURE image
|
||||
blip=gd.xpath('pic:pic/pic:blipFill/a:blip',namespaces=docx.oxml.ns.nsmap)[0]
|
||||
dest=blip.xpath('@r:embed',namespaces=docx.oxml.ns.nsmap)
|
||||
if len(dest)>0:
|
||||
rel=dest[0]
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
#title=inl.xpath('wp:docPr/@title',namespaces=docx.oxml.ns.nsmap)[0]
|
||||
name=gd.xpath('pic:pic/pic:nvPicPr/pic:cNvPr/@name',namespaces=docx.oxml.ns.nsmap)[0]
|
||||
|
||||
part_map[name]=(doc_part.rels[rel].target_ref,doc_part.rels[rel].target_part)
|
||||
|
||||
except:
|
||||
continue
|
||||
|
||||
return part_map
|
||||
|
||||
def save(self,filename,*args,**kwargs):
|
||||
self.pre_processing()
|
||||
self.docx.save(filename,*args,**kwargs)
|
||||
self.post_processing(filename)
|
||||
|
||||
|
||||
class Subdoc(object):
|
||||
""" Class for subdocument to insert into master document """
|
||||
def __init__(self, tpl):
|
||||
self.tpl = tpl
|
||||
self.docx = tpl.get_docx()
|
||||
self.subdocx = Document()
|
||||
self.subdocx._part = self.docx._part
|
||||
|
||||
def __getattr__(self, name) :
|
||||
return getattr(self.subdocx, name)
|
||||
|
||||
def _get_xml(self):
|
||||
if self.subdocx._element.body.sectPr is not None:
|
||||
self.subdocx._element.body.remove(self.subdocx._element.body.sectPr)
|
||||
xml = re.sub(r'</?w:body[^>]*>','',etree.tostring(self.subdocx._element.body, encoding='unicode', pretty_print=False))
|
||||
return xml
|
||||
|
||||
def __unicode__(self):
|
||||
return self._get_xml()
|
||||
|
||||
def __str__(self):
|
||||
return self._get_xml()
|
||||
|
||||
|
||||
class RichText(object):
|
||||
""" class to generate Rich Text when using templates variables
|
||||
|
||||
This is much faster than using Subdoc class, but this only for texts INSIDE an existing paragraph.
|
||||
"""
|
||||
def __init__(self, text=None, **text_prop):
|
||||
self.xml = ''
|
||||
if text:
|
||||
self.add(text, **text_prop)
|
||||
|
||||
def add(self, text, style=None,
|
||||
color=None,
|
||||
highlight=None,
|
||||
size=None,
|
||||
bold=False,
|
||||
italic=False,
|
||||
underline=False,
|
||||
strike=False):
|
||||
|
||||
|
||||
if not isinstance(text, six.text_type):
|
||||
text = text.decode('utf-8',errors='ignore')
|
||||
text = escape(text).replace('\n',NEWLINE).replace('\a',NEWPARAGRAPH)
|
||||
|
||||
prop = u''
|
||||
|
||||
if style:
|
||||
prop += u'<w:rStyle w:val="%s"/>' % style
|
||||
if color:
|
||||
if color[0] == '#':
|
||||
color = color[1:]
|
||||
prop += u'<w:color w:val="%s"/>' % color
|
||||
if highlight:
|
||||
if highlight[0] == '#':
|
||||
highlight = highlight[1:]
|
||||
prop += u'<w:highlight w:val="%s"/>' % highlight
|
||||
if size:
|
||||
prop += u'<w:sz w:val="%s"/>' % size
|
||||
prop += u'<w:szCs w:val="%s"/>' % size
|
||||
if bold:
|
||||
prop += u'<w:b/>'
|
||||
if italic:
|
||||
prop += u'<w:i/>'
|
||||
if underline:
|
||||
if underline not in ['single','double']:
|
||||
underline = 'single'
|
||||
prop += u'<w:u w:val="%s"/>' % underline
|
||||
if strike:
|
||||
prop += u'<w:strike/>'
|
||||
|
||||
self.xml += u'<w:r>'
|
||||
if prop:
|
||||
self.xml += u'<w:rPr>%s</w:rPr>' % prop
|
||||
self.xml += u'<w:t xml:space="preserve">%s</w:t></w:r>' % text
|
||||
|
||||
def __unicode__(self):
|
||||
return self.xml
|
||||
|
||||
def __str__(self):
|
||||
return self.xml
|
||||
|
||||
R = RichText
|
||||
|
||||
class Listing(object):
|
||||
r"""class to manage \n and \a without to use RichText, by this way you keep the current template styling
|
||||
|
||||
use {{ mylisting }} in your template and context={ mylisting:Listing(the_listing_with_newlines) }
|
||||
"""
|
||||
def __init__(self, text):
|
||||
self.xml = escape(text).replace('\n',NEWLINE).replace('\a',NEWPARAGRAPH)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.xml
|
||||
|
||||
def __str__(self):
|
||||
return self.xml
|
||||
|
||||
|
||||
class InlineImage(object):
|
||||
"""Class to generate an inline image
|
||||
|
||||
This is much faster than using Subdoc class.
|
||||
"""
|
||||
tpl = None
|
||||
image_descriptor = None
|
||||
width = None
|
||||
height = None
|
||||
|
||||
def __init__(self, tpl, image_descriptor, width=None, height=None):
|
||||
self.tpl, self.image_descriptor = tpl, image_descriptor
|
||||
self.width, self.height = width, height
|
||||
|
||||
def _insert_image(self):
|
||||
pic = self.tpl.docx._part.new_pic_inline(
|
||||
self.image_descriptor,
|
||||
self.width,
|
||||
self.height
|
||||
).xml
|
||||
return '</w:t></w:r><w:r><w:drawing>%s</w:drawing></w:r><w:r>' \
|
||||
'<w:t xml:space="preserve">' % pic
|
||||
|
||||
def __unicode__(self):
|
||||
return self._insert_image()
|
||||
|
||||
def __str__(self):
|
||||
return self._insert_image()
|
||||
|
||||
|
||||
|
||||
"""
|
||||
|
||||
__version__ = "0.20.1"
|
||||
|
||||
# flake8: noqa
|
||||
from .inline_image import InlineImage
|
||||
from .listing import Listing
|
||||
from .richtext import RichText, R, RichTextParagraph, RP
|
||||
from .template import DocxTemplate
|
||||
try:
|
||||
from .subdoc import Subdoc
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
175
docxtpl/__main__.py
Normal file
@ -0,0 +1,175 @@
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
|
||||
from .template import DocxTemplate, TemplateError
|
||||
|
||||
TEMPLATE_ARG = "template_path"
|
||||
JSON_ARG = "json_path"
|
||||
OUTPUT_ARG = "output_filename"
|
||||
OVERWRITE_ARG = "overwrite"
|
||||
QUIET_ARG = "quiet"
|
||||
|
||||
|
||||
def make_arg_parser():
|
||||
parser = argparse.ArgumentParser(
|
||||
usage="python -m docxtpl [-h] [-o] [-q] {} {} {}".format(
|
||||
TEMPLATE_ARG, JSON_ARG, OUTPUT_ARG
|
||||
),
|
||||
description="Make docx file from existing template docx and json data.",
|
||||
)
|
||||
parser.add_argument(
|
||||
TEMPLATE_ARG, type=str, help="The path to the template docx file."
|
||||
)
|
||||
parser.add_argument(
|
||||
JSON_ARG, type=str, help="The path to the json file with the data."
|
||||
)
|
||||
parser.add_argument(
|
||||
OUTPUT_ARG, type=str, help="The filename to save the generated docx."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-" + OVERWRITE_ARG[0],
|
||||
"--" + OVERWRITE_ARG,
|
||||
action="store_true",
|
||||
help="If output file already exists, overwrites without asking for confirmation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-" + QUIET_ARG[0],
|
||||
"--" + QUIET_ARG,
|
||||
action="store_true",
|
||||
help="Do not display unnecessary messages",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
def get_args(parser):
|
||||
try:
|
||||
parsed_args = vars(parser.parse_args())
|
||||
return parsed_args
|
||||
# Argument errors raise a SystemExit with code 2. Normal usage of the
|
||||
# --help or -h flag raises a SystemExit with code 0.
|
||||
except SystemExit as e:
|
||||
if e.code == 0:
|
||||
raise SystemExit
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Correct usage is:\n{parser.usage}".format(parser=parser)
|
||||
)
|
||||
|
||||
|
||||
def is_argument_valid(arg_name, arg_value, overwrite):
|
||||
# Basic checks for the arguments
|
||||
if arg_name == TEMPLATE_ARG:
|
||||
return os.path.isfile(arg_value) and arg_value.endswith(".docx")
|
||||
elif arg_name == JSON_ARG:
|
||||
return os.path.isfile(arg_value) and arg_value.endswith(".json")
|
||||
elif arg_name == OUTPUT_ARG:
|
||||
return arg_value.endswith(".docx") and check_exists_ask_overwrite(
|
||||
arg_value, overwrite
|
||||
)
|
||||
elif arg_name in [OVERWRITE_ARG, QUIET_ARG]:
|
||||
return arg_value in [True, False]
|
||||
|
||||
|
||||
def check_exists_ask_overwrite(arg_value, overwrite):
|
||||
# If output file does not exist or command was run with overwrite option,
|
||||
# returns True, else asks for overwrite confirmation. If overwrite is
|
||||
# confirmed returns True, else raises OSError.
|
||||
if os.path.exists(arg_value) and not overwrite:
|
||||
try:
|
||||
msg = (
|
||||
"File %s already exists, would you like to overwrite the existing file? "
|
||||
"(y/n)" % arg_value
|
||||
)
|
||||
if input(msg).lower() == "y":
|
||||
return True
|
||||
else:
|
||||
raise OSError
|
||||
except OSError:
|
||||
raise RuntimeError(
|
||||
"File %s already exists, please choose a different name." % arg_value
|
||||
)
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def validate_all_args(parsed_args):
|
||||
overwrite = parsed_args[OVERWRITE_ARG]
|
||||
# Raises AssertionError if any of the arguments is not validated
|
||||
try:
|
||||
for arg_name, arg_value in parsed_args.items():
|
||||
if not is_argument_valid(arg_name, arg_value, overwrite):
|
||||
raise AssertionError
|
||||
except AssertionError:
|
||||
raise RuntimeError(
|
||||
'The specified {arg_name} "{arg_value}" is not valid.'.format(
|
||||
arg_name=arg_name, arg_value=arg_value
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_json_data(json_path):
|
||||
with open(json_path) as file:
|
||||
try:
|
||||
json_data = json.load(file)
|
||||
return json_data
|
||||
except json.JSONDecodeError as e:
|
||||
print(
|
||||
"There was an error on line {e.lineno}, column {e.colno} while trying "
|
||||
"to parse file {json_path}".format(e=e, json_path=json_path)
|
||||
)
|
||||
raise RuntimeError("Failed to get json data.")
|
||||
|
||||
|
||||
def make_docxtemplate(template_path):
|
||||
try:
|
||||
return DocxTemplate(template_path)
|
||||
except TemplateError:
|
||||
raise RuntimeError("Could not create docx template.")
|
||||
|
||||
|
||||
def render_docx(doc, json_data):
|
||||
try:
|
||||
doc.render(json_data)
|
||||
return doc
|
||||
except TemplateError:
|
||||
raise RuntimeError("An error ocurred while trying to render the docx")
|
||||
|
||||
|
||||
def save_file(doc, parsed_args):
|
||||
try:
|
||||
output_path = parsed_args[OUTPUT_ARG]
|
||||
doc.save(output_path)
|
||||
if not parsed_args[QUIET_ARG]:
|
||||
print(
|
||||
"Document successfully generated and saved at {output_path}".format(
|
||||
output_path=output_path
|
||||
)
|
||||
)
|
||||
except OSError as e:
|
||||
print("{e.strerror}. Could not save file {e.filename}.".format(e=e))
|
||||
raise RuntimeError("Failed to save file.")
|
||||
|
||||
|
||||
def main():
|
||||
parser = make_arg_parser()
|
||||
# Everything is in a try-except block that catches a RuntimeError that is
|
||||
# raised if any of the individual functions called cause an error
|
||||
# themselves, terminating the main function.
|
||||
parsed_args = get_args(parser)
|
||||
try:
|
||||
validate_all_args(parsed_args)
|
||||
json_data = get_json_data(os.path.abspath(parsed_args[JSON_ARG]))
|
||||
doc = make_docxtemplate(os.path.abspath(parsed_args[TEMPLATE_ARG]))
|
||||
doc = render_docx(doc, json_data)
|
||||
save_file(doc, parsed_args)
|
||||
except RuntimeError as e:
|
||||
print("Error: " + e.__str__())
|
||||
return
|
||||
finally:
|
||||
if not parsed_args[QUIET_ARG]:
|
||||
print("Exiting program!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
78
docxtpl/inline_image.py
Normal file
@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2021-07-30
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
from docx.oxml import OxmlElement, parse_xml
|
||||
from docx.oxml.ns import qn
|
||||
|
||||
|
||||
class InlineImage(object):
|
||||
"""Class to generate an inline image
|
||||
|
||||
This is much faster than using Subdoc class.
|
||||
"""
|
||||
|
||||
tpl = None
|
||||
image_descriptor = None
|
||||
width = None
|
||||
height = None
|
||||
anchor = None
|
||||
|
||||
def __init__(self, tpl, image_descriptor, width=None, height=None, anchor=None):
|
||||
self.tpl, self.image_descriptor = tpl, image_descriptor
|
||||
self.width, self.height = width, height
|
||||
self.anchor = anchor
|
||||
|
||||
def _add_hyperlink(self, run, url, part):
|
||||
# Create a relationship for the hyperlink
|
||||
r_id = part.relate_to(
|
||||
url,
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
||||
is_external=True,
|
||||
)
|
||||
|
||||
# Find the <wp:docPr> and <pic:cNvPr> element
|
||||
docPr = run.xpath(".//wp:docPr")[0]
|
||||
cNvPr = run.xpath(".//pic:cNvPr")[0]
|
||||
|
||||
# Create the <a:hlinkClick> element
|
||||
hlinkClick1 = OxmlElement("a:hlinkClick")
|
||||
hlinkClick1.set(qn("r:id"), r_id)
|
||||
hlinkClick2 = OxmlElement("a:hlinkClick")
|
||||
hlinkClick2.set(qn("r:id"), r_id)
|
||||
|
||||
# Insert the <a:hlinkClick> element right after the <wp:docPr> element
|
||||
docPr.append(hlinkClick1)
|
||||
cNvPr.append(hlinkClick2)
|
||||
|
||||
return run
|
||||
|
||||
def _insert_image(self):
|
||||
pic = self.tpl.current_rendering_part.new_pic_inline(
|
||||
self.image_descriptor,
|
||||
self.width,
|
||||
self.height,
|
||||
).xml
|
||||
if self.anchor:
|
||||
run = parse_xml(pic)
|
||||
if run.xpath(".//a:blip"):
|
||||
hyperlink = self._add_hyperlink(
|
||||
run, self.anchor, self.tpl.current_rendering_part
|
||||
)
|
||||
pic = hyperlink.xml
|
||||
|
||||
return (
|
||||
"</w:t></w:r><w:r><w:drawing>%s</w:drawing></w:r><w:r>"
|
||||
'<w:t xml:space="preserve">' % pic
|
||||
)
|
||||
|
||||
def __unicode__(self):
|
||||
return self._insert_image()
|
||||
|
||||
def __str__(self):
|
||||
return self._insert_image()
|
||||
|
||||
def __html__(self):
|
||||
return self._insert_image()
|
||||
35
docxtpl/listing.py
Normal file
@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2021-07-30
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
try:
|
||||
from html import escape
|
||||
except ImportError:
|
||||
# cgi.escape is deprecated in python 3.7
|
||||
from cgi import escape
|
||||
|
||||
|
||||
class Listing(object):
|
||||
r"""class to manage \n and \a without to use RichText,
|
||||
by this way you keep the current template styling
|
||||
|
||||
use {{ mylisting }} in your template and
|
||||
context={ mylisting:Listing(the_listing_with_newlines) }
|
||||
"""
|
||||
|
||||
def __init__(self, text):
|
||||
# If not a string : cast to string (ex: int, dict etc...)
|
||||
if not isinstance(text, (str, bytes)):
|
||||
text = str(text)
|
||||
self.xml = escape(text)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.xml
|
||||
|
||||
def __str__(self):
|
||||
return self.xml
|
||||
|
||||
def __html__(self):
|
||||
return self.xml
|
||||
180
docxtpl/richtext.py
Normal file
@ -0,0 +1,180 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2021-07-30
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
try:
|
||||
from html import escape
|
||||
except ImportError:
|
||||
# cgi.escape is deprecated in python 3.7
|
||||
from cgi import escape
|
||||
|
||||
|
||||
class RichText(object):
|
||||
"""class to generate Rich Text when using templates variables
|
||||
|
||||
This is much faster than using Subdoc class,
|
||||
but this only for texts INSIDE an existing paragraph.
|
||||
"""
|
||||
|
||||
def __init__(self, text=None, **text_prop):
|
||||
self.xml = ""
|
||||
if text:
|
||||
self.add(text, **text_prop)
|
||||
|
||||
def add(
|
||||
self,
|
||||
text,
|
||||
style=None,
|
||||
color=None,
|
||||
highlight=None,
|
||||
size=None,
|
||||
subscript=None,
|
||||
superscript=None,
|
||||
bold=False,
|
||||
italic=False,
|
||||
underline=False,
|
||||
strike=False,
|
||||
font=None,
|
||||
url_id=None,
|
||||
rtl=False,
|
||||
lang=None,
|
||||
):
|
||||
|
||||
# If a RichText is added
|
||||
if isinstance(text, RichText):
|
||||
self.xml += text.xml
|
||||
return
|
||||
|
||||
# # If nothing to add : just return
|
||||
# if text is None or text == "":
|
||||
# return
|
||||
|
||||
# If not a string : cast to string (ex: int, dict etc...)
|
||||
if not isinstance(text, (str, bytes)):
|
||||
text = str(text)
|
||||
if not isinstance(text, str):
|
||||
text = text.decode("utf-8", errors="ignore")
|
||||
text = escape(text)
|
||||
|
||||
prop = ""
|
||||
|
||||
if style:
|
||||
prop += '<w:rStyle w:val="%s"/>' % style
|
||||
if color:
|
||||
if color[0] == "#":
|
||||
color = color[1:]
|
||||
prop += '<w:color w:val="%s"/>' % color
|
||||
if highlight:
|
||||
if highlight[0] == "#":
|
||||
highlight = highlight[1:]
|
||||
prop += '<w:shd w:fill="%s"/>' % highlight
|
||||
if size:
|
||||
prop += '<w:sz w:val="%s"/>' % size
|
||||
prop += '<w:szCs w:val="%s"/>' % size
|
||||
if subscript:
|
||||
prop += '<w:vertAlign w:val="subscript"/>'
|
||||
if superscript:
|
||||
prop += '<w:vertAlign w:val="superscript"/>'
|
||||
if bold:
|
||||
prop += "<w:b/>"
|
||||
if rtl:
|
||||
prop += "<w:bCs/>"
|
||||
if italic:
|
||||
prop += "<w:i/>"
|
||||
if rtl:
|
||||
prop += "<w:iCs/>"
|
||||
if underline:
|
||||
if underline not in [
|
||||
"single",
|
||||
"double",
|
||||
"thick",
|
||||
"dotted",
|
||||
"dash",
|
||||
"dotDash",
|
||||
"dotDotDash",
|
||||
"wave",
|
||||
]:
|
||||
underline = "single"
|
||||
prop += '<w:u w:val="%s"/>' % underline
|
||||
if strike:
|
||||
prop += "<w:strike/>"
|
||||
if font:
|
||||
regional_font = ""
|
||||
if ":" in font:
|
||||
region, font = font.split(":", 1)
|
||||
regional_font = ' w:{region}="{font}"'.format(font=font, region=region)
|
||||
prop += '<w:rFonts w:ascii="{font}" w:hAnsi="{font}" w:cs="{font}"{regional_font}/>'.format(
|
||||
font=font, regional_font=regional_font
|
||||
)
|
||||
if rtl:
|
||||
prop += '<w:rtl w:val="true"/>'
|
||||
if lang:
|
||||
prop += '<w:lang w:val="%s"/>' % lang
|
||||
xml = "<w:r>"
|
||||
if prop:
|
||||
xml += "<w:rPr>%s</w:rPr>" % prop
|
||||
xml += '<w:t xml:space="preserve">%s</w:t></w:r>' % text
|
||||
if url_id:
|
||||
xml = '<w:hyperlink r:id="%s" w:tgtFrame="_blank">%s</w:hyperlink>' % (
|
||||
url_id,
|
||||
xml,
|
||||
)
|
||||
self.xml += xml
|
||||
|
||||
def __unicode__(self):
|
||||
return self.xml
|
||||
|
||||
def __str__(self):
|
||||
return self.xml
|
||||
|
||||
def __html__(self):
|
||||
return self.xml
|
||||
|
||||
|
||||
class RichTextParagraph(object):
|
||||
"""class to generate Rich Text Paragraphs when using templates variables
|
||||
|
||||
This is much faster than using Subdoc class,
|
||||
but this only for texts OUTSIDE an existing paragraph.
|
||||
"""
|
||||
|
||||
def __init__(self, text=None, **text_prop):
|
||||
self.xml = ""
|
||||
if text:
|
||||
self.add(text, **text_prop)
|
||||
|
||||
def add(
|
||||
self,
|
||||
text,
|
||||
parastyle=None,
|
||||
):
|
||||
|
||||
# If a RichText is added
|
||||
if not isinstance(text, RichText):
|
||||
text = RichText(text)
|
||||
|
||||
prop = ""
|
||||
if parastyle:
|
||||
prop += '<w:pStyle w:val="%s"/>' % parastyle
|
||||
|
||||
xml = "<w:p>"
|
||||
if prop:
|
||||
xml += "<w:pPr>%s</w:pPr>" % prop
|
||||
xml += text.xml
|
||||
xml += "</w:p>"
|
||||
self.xml += xml
|
||||
|
||||
def __unicode__(self):
|
||||
return self.xml
|
||||
|
||||
def __str__(self):
|
||||
return self.xml
|
||||
|
||||
def __html__(self):
|
||||
return self.xml
|
||||
|
||||
|
||||
R = RichText
|
||||
RP = RichTextParagraph
|
||||
103
docxtpl/subdoc.py
Normal file
@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2021-07-30
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
|
||||
from docx import Document
|
||||
from docx.oxml import CT_SectPr
|
||||
from docx.opc.constants import RELATIONSHIP_TYPE as RT
|
||||
from docxcompose.properties import CustomProperties
|
||||
from docxcompose.utils import xpath
|
||||
from docxcompose.composer import Composer
|
||||
from docxcompose.utils import NS
|
||||
from lxml import etree
|
||||
import re
|
||||
|
||||
|
||||
class SubdocComposer(Composer):
|
||||
def attach_parts(self, doc, remove_property_fields=True):
|
||||
"""Attach docx parts instead of appending the whole document
|
||||
thus subdoc insertion can be delegated to jinja2"""
|
||||
self.reset_reference_mapping()
|
||||
|
||||
# Remove custom property fields but keep the values
|
||||
if remove_property_fields:
|
||||
cprops = CustomProperties(doc)
|
||||
for name in cprops.keys():
|
||||
cprops.dissolve_fields(name)
|
||||
|
||||
self._create_style_id_mapping(doc)
|
||||
|
||||
for element in doc.element.body:
|
||||
if isinstance(element, CT_SectPr):
|
||||
continue
|
||||
self.add_referenced_parts(doc.part, self.doc.part, element)
|
||||
self.add_styles(doc, element)
|
||||
self.add_numberings(doc, element)
|
||||
self.restart_first_numbering(doc, element)
|
||||
self.add_images(doc, element)
|
||||
self.add_diagrams(doc, element)
|
||||
self.add_shapes(doc, element)
|
||||
self.add_footnotes(doc, element)
|
||||
self.remove_header_and_footer_references(doc, element)
|
||||
|
||||
self.add_styles_from_other_parts(doc)
|
||||
self.renumber_bookmarks()
|
||||
self.renumber_docpr_ids()
|
||||
self.renumber_nvpicpr_ids()
|
||||
self.fix_section_types(doc)
|
||||
|
||||
def add_diagrams(self, doc, element):
|
||||
# While waiting docxcompose 1.3.3
|
||||
dgm_rels = xpath(element, ".//dgm:relIds[@r:dm]")
|
||||
for dgm_rel in dgm_rels:
|
||||
for item, rt_type in (
|
||||
("dm", RT.DIAGRAM_DATA),
|
||||
("lo", RT.DIAGRAM_LAYOUT),
|
||||
("qs", RT.DIAGRAM_QUICK_STYLE),
|
||||
("cs", RT.DIAGRAM_COLORS),
|
||||
):
|
||||
dm_rid = dgm_rel.get("{%s}%s" % (NS["r"], item))
|
||||
dm_part = doc.part.rels[dm_rid].target_part
|
||||
new_rid = self.doc.part.relate_to(dm_part, rt_type)
|
||||
dgm_rel.set("{%s}%s" % (NS["r"], item), new_rid)
|
||||
|
||||
|
||||
class Subdoc(object):
|
||||
"""Class for subdocument to insert into master document"""
|
||||
|
||||
def __init__(self, tpl, docpath=None):
|
||||
self.tpl = tpl
|
||||
self.docx = tpl.get_docx()
|
||||
self.subdocx = Document(docpath)
|
||||
if docpath:
|
||||
compose = SubdocComposer(self.docx)
|
||||
compose.attach_parts(self.subdocx)
|
||||
else:
|
||||
self.subdocx._part = self.docx._part
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.subdocx, name)
|
||||
|
||||
def _get_xml(self):
|
||||
if self.subdocx.element.body.sectPr is not None:
|
||||
self.subdocx.element.body.remove(self.subdocx.element.body.sectPr)
|
||||
xml = re.sub(
|
||||
r"</?w:body[^>]*>",
|
||||
"",
|
||||
etree.tostring(
|
||||
self.subdocx.element.body, encoding="unicode", pretty_print=False
|
||||
),
|
||||
)
|
||||
return xml
|
||||
|
||||
def __unicode__(self):
|
||||
return self._get_xml()
|
||||
|
||||
def __str__(self):
|
||||
return self._get_xml()
|
||||
|
||||
def __html__(self):
|
||||
return self._get_xml()
|
||||
926
docxtpl/template.py
Normal file
@ -0,0 +1,926 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
|
||||
from os import PathLike
|
||||
from typing import TYPE_CHECKING, Any, Optional, IO, Union, Dict, Set
|
||||
import functools
|
||||
import io
|
||||
from lxml import etree
|
||||
from docx import Document
|
||||
from docx.opc.oxml import parse_xml
|
||||
from docx.opc.part import XmlPart
|
||||
import docx.oxml.ns
|
||||
from docx.opc.constants import RELATIONSHIP_TYPE as REL_TYPE
|
||||
from jinja2 import Environment, Template, meta
|
||||
from jinja2.exceptions import TemplateError
|
||||
|
||||
try:
|
||||
from html import escape # noqa: F401
|
||||
except ImportError:
|
||||
# cgi.escape is deprecated in python 3.7
|
||||
from cgi import escape # noqa: F401
|
||||
import re
|
||||
import binascii
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .subdoc import Subdoc
|
||||
|
||||
|
||||
class DocxTemplate(object):
|
||||
"""Class for managing docx files as they were jinja2 templates"""
|
||||
|
||||
HEADER_URI = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
|
||||
)
|
||||
FOOTER_URI = (
|
||||
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
|
||||
)
|
||||
|
||||
def __init__(self, template_file: Union[IO[bytes], str, PathLike]) -> None:
|
||||
self.template_file = template_file
|
||||
self.reset_replacements()
|
||||
self.docx = None
|
||||
self.is_rendered = False
|
||||
self.is_saved = False
|
||||
self.allow_missing_pics = False
|
||||
|
||||
def init_docx(self, reload: bool = True):
|
||||
if not self.docx or (self.is_rendered and reload):
|
||||
self.docx = Document(self.template_file)
|
||||
self.is_rendered = False
|
||||
|
||||
def render_init(self):
|
||||
self.init_docx()
|
||||
self.pic_map = {}
|
||||
self.current_rendering_part = None
|
||||
self.docx_ids_index = 1000
|
||||
self.is_saved = False
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.docx, name)
|
||||
|
||||
def xml_to_string(self, xml, encoding="unicode"):
|
||||
# Be careful : pretty_print MUST be set to False, otherwise patch_xml()
|
||||
# won't work properly
|
||||
return etree.tostring(xml, encoding="unicode", pretty_print=False)
|
||||
|
||||
def get_docx(self):
|
||||
self.init_docx()
|
||||
return self.docx
|
||||
|
||||
def get_xml(self):
|
||||
return self.xml_to_string(self.docx._element.body)
|
||||
|
||||
def write_xml(self, filename):
|
||||
with open(filename, "w") as fh:
|
||||
fh.write(self.get_xml())
|
||||
|
||||
def patch_xml(self, src_xml):
|
||||
"""Make a lots of cleaning to have a raw xml understandable by jinja2 :
|
||||
strip all unnecessary xml tags, manage table cell background color and colspan,
|
||||
unescape html entities, etc..."""
|
||||
|
||||
# replace {<something>{ by {{ ( works with {{ }} {% and %} {# and #})
|
||||
src_xml = re.sub(
|
||||
r"(?<={)(<[^>]*>)+(?=[\{%\#])|(?<=[%\}\#])(<[^>]*>)+(?=\})",
|
||||
"",
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# replace {{<some tags>jinja2 stuff<some other tags>}} by {{jinja2 stuff}}
|
||||
# same thing with {% ... %} and {# #}
|
||||
# "jinja2 stuff" could a variable, a 'if' etc... anything jinja2 will understand
|
||||
def striptags(m):
|
||||
return re.sub(
|
||||
"</w:t>.*?(<w:t>|<w:t [^>]*>)", "", m.group(0), flags=re.DOTALL
|
||||
)
|
||||
|
||||
src_xml = re.sub(
|
||||
r"{%(?:(?!%}).)*|{#(?:(?!#}).)*|{{(?:(?!}}).)*",
|
||||
striptags,
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# manage table cell colspan
|
||||
def colspan(m):
|
||||
cell_xml = m.group(1) + m.group(3)
|
||||
cell_xml = re.sub(
|
||||
r"<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>",
|
||||
"",
|
||||
cell_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
cell_xml = re.sub(r"<w:gridSpan[^/]*/>", "", cell_xml, count=1)
|
||||
return re.sub(
|
||||
r"(<w:tcPr[^>]*>)",
|
||||
r'\1<w:gridSpan w:val="{{%s}}"/>' % m.group(2),
|
||||
cell_xml,
|
||||
)
|
||||
|
||||
src_xml = re.sub(
|
||||
r"(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*colspan\s+([^%]*)\s*%}(.*?</w:tc>)",
|
||||
colspan,
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# manage table cell background color
|
||||
def cellbg(m):
|
||||
cell_xml = m.group(1) + m.group(3)
|
||||
cell_xml = re.sub(
|
||||
r"<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>",
|
||||
"",
|
||||
cell_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
cell_xml = re.sub(r"<w:shd[^/]*/>", "", cell_xml, count=1)
|
||||
return re.sub(
|
||||
r"(<w:tcPr[^>]*>)",
|
||||
r'\1<w:shd w:val="clear" w:color="auto" w:fill="{{%s}}"/>' % m.group(2),
|
||||
cell_xml,
|
||||
)
|
||||
|
||||
src_xml = re.sub(
|
||||
r"(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?</w:tc>)",
|
||||
cellbg,
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# ensure space preservation
|
||||
src_xml = re.sub(
|
||||
r"<w:t>((?:(?!<w:t>).)*)({{.*?}}|{%.*?%})",
|
||||
r'<w:t xml:space="preserve">\1\2',
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
src_xml = re.sub(
|
||||
r"({{r\s.*?}}|{%r\s.*?%})",
|
||||
r'</w:t></w:r><w:r><w:t xml:space="preserve">\1</w:t></w:r><w:r><w:t xml:space="preserve">',
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# {%- will merge with previous paragraph text
|
||||
src_xml = re.sub(r"</w:t>(?:(?!</w:t>).)*?{%-", "{%", src_xml, flags=re.DOTALL)
|
||||
# -%} will merge with next paragraph text
|
||||
src_xml = re.sub(
|
||||
r"-%}(?:(?!<w:t[ >]|{%|{{).)*?<w:t[^>]*?>", "%}", src_xml, flags=re.DOTALL
|
||||
)
|
||||
|
||||
for y in ["tr", "tc", "p", "r"]:
|
||||
# replace into xml code the row/paragraph/run containing
|
||||
# {%y xxx %} or {{y xxx}} template tag
|
||||
# by {% xxx %} or {{ xx }} without any surrounding <w:y> tags :
|
||||
# This is mandatory to have jinja2 generating correct xml code
|
||||
pat = (
|
||||
r"<w:%(y)s[ >](?:(?!<w:%(y)s[ >]).)*({%%|{{)%(y)s ([^}%%]*(?:%%}|}})).*?</w:%(y)s>"
|
||||
% {"y": y}
|
||||
)
|
||||
src_xml = re.sub(pat, r"\1 \2", src_xml, flags=re.DOTALL)
|
||||
|
||||
for y in ["tr", "tc", "p"]:
|
||||
# same thing, but for {#y xxx #} (but not where y == 'r', since that
|
||||
# makes less sense to use comments in that context
|
||||
pat = (
|
||||
r"<w:%(y)s[ >](?:(?!<w:%(y)s[ >]).)*({#)%(y)s ([^}#]*(?:#})).*?</w:%(y)s>"
|
||||
% {"y": y}
|
||||
)
|
||||
src_xml = re.sub(pat, r"\1 \2", src_xml, flags=re.DOTALL)
|
||||
|
||||
# add vMerge
|
||||
# use {% vm %} to make this table cell and its copies
|
||||
# be vertically merged within a {% for %}
|
||||
def v_merge_tc(m):
|
||||
def v_merge(m1):
|
||||
return (
|
||||
'<w:vMerge w:val="{% if loop.first %}restart{% else %}continue{% endif %}"/>'
|
||||
+ m1.group(1) # Everything between ``</w:tcPr>`` and ``<w:t>``.
|
||||
+ "{% if loop.first %}"
|
||||
+ m1.group(2) # Everything before ``{% vm %}``.
|
||||
+ m1.group(3) # Everything after ``{% vm %}``.
|
||||
+ "{% endif %}"
|
||||
+ m1.group(4) # ``</w:t>``.
|
||||
)
|
||||
|
||||
return re.sub(
|
||||
r"(</w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*vm\s*%})(.*?)(</w:t>)",
|
||||
v_merge,
|
||||
m.group(),
|
||||
# Everything between ``</w:tc>`` and ``</w:tc>`` with ``{% vm %}`` inside.
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
src_xml = re.sub(
|
||||
r"<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*vm\s*%}.*?</w:tc[ >]",
|
||||
v_merge_tc,
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# Use ``{% hm %}`` to make table cell become horizontally merged within
|
||||
# a ``{% for %}``.
|
||||
def h_merge_tc(m):
|
||||
xml_to_patch = (
|
||||
m.group()
|
||||
) # Everything between ``</w:tc>`` and ``</w:tc>`` with ``{% hm %}`` inside.
|
||||
|
||||
def with_gridspan(m1):
|
||||
return (
|
||||
m1.group(1) # ``w:gridSpan w:val="``.
|
||||
+ "{{ "
|
||||
+ m1.group(2)
|
||||
+ " * loop.length }}" # Content of ``w:val``, multiplied by loop length.
|
||||
+ m1.group(3) # Closing quotation mark.
|
||||
)
|
||||
|
||||
def without_gridspan(m2):
|
||||
return (
|
||||
'<w:gridSpan w:val="{{ loop.length }}"/>'
|
||||
+ m2.group(1) # Everything between ``</w:tcPr>`` and ``<w:t>``.
|
||||
+ m2.group(2) # Everything before ``{% hm %}``.
|
||||
+ m2.group(3) # Everything after ``{% hm %}``.
|
||||
+ m2.group(4) # ``</w:t>``.
|
||||
)
|
||||
|
||||
if re.search(r"w:gridSpan", xml_to_patch):
|
||||
# Simple case, there's already ``gridSpan``, multiply its value.
|
||||
|
||||
xml = re.sub(
|
||||
r'(w:gridSpan w:val=")(\d+)(")',
|
||||
with_gridspan,
|
||||
xml_to_patch,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
xml = re.sub(
|
||||
r"{%\s*hm\s*%}",
|
||||
"",
|
||||
xml, # Patched xml.
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
else:
|
||||
# There're no ``gridSpan``, add one.
|
||||
xml = re.sub(
|
||||
r"(</w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*hm\s*%})(.*?)(</w:t>)",
|
||||
without_gridspan,
|
||||
xml_to_patch,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
# Discard every other cell generated in loop.
|
||||
return "{% if loop.first %}" + xml + "{% endif %}"
|
||||
|
||||
src_xml = re.sub(
|
||||
r"<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*hm\s*%}.*?</w:tc[ >]",
|
||||
h_merge_tc,
|
||||
src_xml,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
def clean_tags(m):
|
||||
return (
|
||||
m.group(0)
|
||||
.replace(r"‘", "'")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("“", '"')
|
||||
.replace("”", '"')
|
||||
.replace("‘", "'")
|
||||
.replace("’", "'")
|
||||
)
|
||||
|
||||
src_xml = re.sub(r"(?<=\{[\{%])(.*?)(?=[\}%]})", clean_tags, src_xml)
|
||||
|
||||
return src_xml
|
||||
|
||||
def render_xml_part(self, src_xml, part, context, jinja_env=None):
|
||||
src_xml = re.sub(r"<w:p([ >])", r"\n<w:p\1", src_xml)
|
||||
try:
|
||||
self.current_rendering_part = part
|
||||
if jinja_env:
|
||||
template = jinja_env.from_string(src_xml)
|
||||
else:
|
||||
template = Template(src_xml)
|
||||
dst_xml = template.render(context)
|
||||
except TemplateError as exc:
|
||||
if hasattr(exc, "lineno") and exc.lineno is not None:
|
||||
line_number = max(exc.lineno - 4, 0)
|
||||
exc.docx_context = map(
|
||||
lambda x: re.sub(r"<[^>]+>", "", x),
|
||||
src_xml.splitlines()[line_number: (line_number + 7)], # fmt: skip
|
||||
)
|
||||
|
||||
raise exc
|
||||
dst_xml = re.sub(r"\n<w:p([ >])", r"<w:p\1", dst_xml)
|
||||
dst_xml = (
|
||||
dst_xml.replace("{_{", "{{")
|
||||
.replace("}_}", "}}")
|
||||
.replace("{_%", "{%")
|
||||
.replace("%_}", "%}")
|
||||
)
|
||||
dst_xml = self.resolve_listing(dst_xml)
|
||||
return dst_xml
|
||||
|
||||
def render_properties(
|
||||
self, context: Dict[str, Any], jinja_env: Optional[Environment] = None
|
||||
) -> None:
|
||||
# List of string attributes of docx.opc.coreprops.CoreProperties which are strings.
|
||||
# It seems that some attributes cannot be written as strings. Those are commented out.
|
||||
properties = [
|
||||
"author",
|
||||
# 'category',
|
||||
"comments",
|
||||
# 'content_status',
|
||||
"identifier",
|
||||
# 'keywords',
|
||||
"language",
|
||||
# 'last_modified_by',
|
||||
"subject",
|
||||
"title",
|
||||
# 'version',
|
||||
]
|
||||
if jinja_env is None:
|
||||
jinja_env = Environment()
|
||||
|
||||
for prop in properties:
|
||||
initial = getattr(self.docx.core_properties, prop)
|
||||
template = jinja_env.from_string(initial)
|
||||
rendered = template.render(context)
|
||||
setattr(self.docx.core_properties, prop, rendered)
|
||||
|
||||
def render_footnotes(
|
||||
self, context: Dict[str, Any], jinja_env: Optional[Environment] = None
|
||||
) -> None:
|
||||
if jinja_env is None:
|
||||
jinja_env = Environment()
|
||||
|
||||
for section in self.docx.sections:
|
||||
for part in section.part.package.parts:
|
||||
if part.content_type == (
|
||||
"application/vnd.openxmlformats-officedocument"
|
||||
".wordprocessingml.footnotes+xml"
|
||||
):
|
||||
xml = self.patch_xml(
|
||||
part.blob.decode("utf-8")
|
||||
if isinstance(part.blob, bytes)
|
||||
else part.blob
|
||||
)
|
||||
xml = self.render_xml_part(xml, part, context, jinja_env)
|
||||
part._blob = xml.encode("utf-8")
|
||||
|
||||
def resolve_listing(self, xml):
|
||||
|
||||
def resolve_text(run_properties, paragraph_properties, m):
|
||||
xml = m.group(0).replace(
|
||||
"\t",
|
||||
"</w:t></w:r>"
|
||||
"<w:r>%s<w:tab/></w:r>"
|
||||
'<w:r>%s<w:t xml:space="preserve">' % (run_properties, run_properties),
|
||||
)
|
||||
xml = xml.replace(
|
||||
"\a",
|
||||
"</w:t></w:r></w:p>"
|
||||
'<w:p>%s<w:r>%s<w:t xml:space="preserve">'
|
||||
% (paragraph_properties, run_properties),
|
||||
)
|
||||
xml = xml.replace("\n", '</w:t><w:br/><w:t xml:space="preserve">')
|
||||
xml = xml.replace(
|
||||
"\f",
|
||||
"</w:t></w:r></w:p>"
|
||||
'<w:p><w:r><w:br w:type="page"/></w:r></w:p>'
|
||||
'<w:p>%s<w:r>%s<w:t xml:space="preserve">'
|
||||
% (paragraph_properties, run_properties),
|
||||
)
|
||||
return xml
|
||||
|
||||
def resolve_run(paragraph_properties, m):
|
||||
run_properties = re.search(r"<w:rPr>.*?</w:rPr>", m.group(0))
|
||||
run_properties = run_properties.group(0) if run_properties else ""
|
||||
return re.sub(
|
||||
r"<w:t(?: [^>]*)?>.*?</w:t>",
|
||||
lambda x: resolve_text(run_properties, paragraph_properties, x),
|
||||
m.group(0),
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
def resolve_paragraph(m):
|
||||
paragraph_properties = re.search(r"<w:pPr>.*?</w:pPr>", m.group(0))
|
||||
paragraph_properties = (
|
||||
paragraph_properties.group(0) if paragraph_properties else ""
|
||||
)
|
||||
return re.sub(
|
||||
r"<w:r(?: [^>]*)?>.*?</w:r>",
|
||||
lambda x: resolve_run(paragraph_properties, x),
|
||||
m.group(0),
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
xml = re.sub(
|
||||
r"<w:p(?: [^>]*)?>.*?</w:p>", resolve_paragraph, xml, flags=re.DOTALL
|
||||
)
|
||||
|
||||
return xml
|
||||
|
||||
def build_xml(self, context, jinja_env=None):
|
||||
xml = self.get_xml()
|
||||
xml = self.patch_xml(xml)
|
||||
xml = self.render_xml_part(xml, self.docx._part, context, jinja_env)
|
||||
return xml
|
||||
|
||||
def map_tree(self, tree):
|
||||
root = self.docx._element
|
||||
body = root.body
|
||||
root.replace(body, tree)
|
||||
|
||||
def get_headers_footers(self, uri):
|
||||
for relKey, val in self.docx._part.rels.items():
|
||||
if (val.reltype == uri) and (val.target_part.blob):
|
||||
yield relKey, val.target_part
|
||||
|
||||
def get_part_xml(self, part):
|
||||
return self.xml_to_string(parse_xml(part.blob))
|
||||
|
||||
def get_headers_footers_encoding(self, xml):
|
||||
m = re.match(r'<\?xml[^\?]+\bencoding="([^"]+)"', xml, re.I)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return "utf-8"
|
||||
|
||||
def build_headers_footers_xml(self, context, uri, jinja_env=None):
|
||||
for relKey, part in self.get_headers_footers(uri):
|
||||
xml = self.get_part_xml(part)
|
||||
encoding = self.get_headers_footers_encoding(xml)
|
||||
xml = self.patch_xml(xml)
|
||||
xml = self.render_xml_part(xml, part, context, jinja_env)
|
||||
yield relKey, xml.encode(encoding)
|
||||
|
||||
def map_headers_footers_xml(self, relKey, xml):
|
||||
part = self.docx._part.rels[relKey].target_part
|
||||
new_part = XmlPart.load(part.partname, part.content_type, xml, part.package)
|
||||
for rId, rel in part.rels.items():
|
||||
new_part.load_rel(rel.reltype, rel._target, rel.rId, rel.is_external)
|
||||
self.docx._part.rels[relKey]._target = new_part
|
||||
|
||||
def render(
|
||||
self,
|
||||
context: Dict[str, Any],
|
||||
jinja_env: Optional[Environment] = None,
|
||||
autoescape: bool = False,
|
||||
) -> None:
|
||||
# init template working attributes
|
||||
self.render_init()
|
||||
|
||||
if autoescape:
|
||||
if not jinja_env:
|
||||
jinja_env = Environment(autoescape=autoescape)
|
||||
else:
|
||||
jinja_env.autoescape = autoescape
|
||||
|
||||
# Body
|
||||
xml_src = self.build_xml(context, jinja_env)
|
||||
|
||||
# fix tables if needed
|
||||
tree = self.fix_tables(xml_src)
|
||||
|
||||
# fix docPr ID's
|
||||
self.fix_docpr_ids(tree)
|
||||
|
||||
# Replace body xml tree
|
||||
self.map_tree(tree)
|
||||
|
||||
# Headers
|
||||
headers = self.build_headers_footers_xml(context, self.HEADER_URI, jinja_env)
|
||||
for relKey, xml in headers:
|
||||
self.map_headers_footers_xml(relKey, xml)
|
||||
|
||||
# Footers
|
||||
footers = self.build_headers_footers_xml(context, self.FOOTER_URI, jinja_env)
|
||||
for relKey, xml in footers:
|
||||
self.map_headers_footers_xml(relKey, xml)
|
||||
|
||||
self.render_properties(context, jinja_env)
|
||||
|
||||
self.render_footnotes(context, jinja_env)
|
||||
|
||||
# set rendered flag
|
||||
self.is_rendered = True
|
||||
|
||||
# using of TC tag in for cycle can cause that count of columns does not
|
||||
# correspond to real count of columns in row. This function is able to fix it.
|
||||
def fix_tables(self, xml):
|
||||
parser = etree.XMLParser(recover=True)
|
||||
tree = etree.fromstring(xml, parser=parser)
|
||||
# get namespace
|
||||
ns = "{" + tree.nsmap["w"] + "}"
|
||||
# walk trough xml and find table
|
||||
for t in tree.iter(ns + "tbl"):
|
||||
tblGrid = t.find(ns + "tblGrid")
|
||||
columns = tblGrid.findall(ns + "gridCol")
|
||||
to_add = 0
|
||||
# walk trough all rows and try to find if there is higher cell count
|
||||
for r in t.iter(ns + "tr"):
|
||||
cells = r.findall(ns + "tc")
|
||||
if (len(columns) + to_add) < len(cells):
|
||||
to_add = len(cells) - len(columns)
|
||||
# is necessary to add columns?
|
||||
if to_add > 0:
|
||||
# at first, calculate width of table according to columns
|
||||
# (we want to preserve it)
|
||||
width = 0.0
|
||||
new_average = None
|
||||
for c in columns:
|
||||
if not c.get(ns + "w") is None:
|
||||
width += float(c.get(ns + "w"))
|
||||
# try to keep proportion of table
|
||||
if width > 0:
|
||||
old_average = width / len(columns)
|
||||
new_average = width / (len(columns) + to_add)
|
||||
# scale the old columns
|
||||
for c in columns:
|
||||
c.set(
|
||||
ns + "w",
|
||||
str(
|
||||
int(float(c.get(ns + "w")) * new_average / old_average)
|
||||
),
|
||||
)
|
||||
# add new columns
|
||||
for i in range(to_add):
|
||||
etree.SubElement(
|
||||
tblGrid, ns + "gridCol", {ns + "w": str(int(new_average))}
|
||||
)
|
||||
|
||||
# Refetch columns after columns addition.
|
||||
columns = tblGrid.findall(ns + "gridCol")
|
||||
columns_len = len(columns)
|
||||
|
||||
cells_len_max = 0
|
||||
|
||||
def get_cell_len(total, cell):
|
||||
tc_pr = cell.find(ns + "tcPr")
|
||||
grid_span = None if tc_pr is None else tc_pr.find(ns + "gridSpan")
|
||||
|
||||
if grid_span is not None:
|
||||
return total + int(grid_span.get(ns + "val"))
|
||||
|
||||
return total + 1
|
||||
|
||||
# Calculate max of table cells to compare with `gridCol`.
|
||||
for r in t.iter(ns + "tr"):
|
||||
cells = r.findall(ns + "tc")
|
||||
cells_len = functools.reduce(get_cell_len, cells, 0)
|
||||
cells_len_max = max(cells_len_max, cells_len)
|
||||
|
||||
to_remove = columns_len - cells_len_max
|
||||
|
||||
# If after the loop, there're less columns, than
|
||||
# originally was, remove extra `gridCol` declarations.
|
||||
if to_remove > 0:
|
||||
# Have to keep track of the removed width to scale the
|
||||
# table back to its original width.
|
||||
removed_width = 0.0
|
||||
|
||||
for c in columns[-to_remove:]:
|
||||
removed_width += float(c.get(ns + "w"))
|
||||
|
||||
tblGrid.remove(c)
|
||||
|
||||
columns_left = tblGrid.findall(ns + "gridCol")
|
||||
|
||||
# Distribute `removed_width` across all columns that has
|
||||
# left after extras removal.
|
||||
extra_space = 0
|
||||
if len(columns_left) > 0:
|
||||
extra_space = removed_width / len(columns_left)
|
||||
extra_space = int(extra_space)
|
||||
|
||||
for c in columns_left:
|
||||
c.set(ns + "w", str(int(float(c.get(ns + "w")) + extra_space)))
|
||||
|
||||
return tree
|
||||
|
||||
def fix_docpr_ids(self, tree):
|
||||
# some Ids may have some collisions : so renumbering all of them :
|
||||
for elt in tree.xpath("//wp:docPr", namespaces=docx.oxml.ns.nsmap):
|
||||
self.docx_ids_index += 1
|
||||
elt.attrib["id"] = str(self.docx_ids_index)
|
||||
|
||||
def new_subdoc(self, docpath=None) -> Subdoc:
|
||||
from .subdoc import Subdoc
|
||||
|
||||
self.init_docx()
|
||||
return Subdoc(self, docpath)
|
||||
|
||||
@staticmethod
|
||||
def get_file_crc(file_obj):
|
||||
if hasattr(file_obj, "read"):
|
||||
buf = file_obj.read()
|
||||
else:
|
||||
with open(file_obj, "rb") as fh:
|
||||
buf = fh.read()
|
||||
|
||||
crc = binascii.crc32(buf) & 0xFFFFFFFF
|
||||
return crc
|
||||
|
||||
def replace_media(self, src_file, dst_file):
|
||||
"""Replace one media by another one into a docx
|
||||
|
||||
This has been done mainly because it is not possible to add images in
|
||||
docx header/footer.
|
||||
With this function, put a dummy picture in your header/footer,
|
||||
then specify it with its replacement in this function using the file path
|
||||
or file-like objects.
|
||||
|
||||
Syntax: tpl.replace_media('dummy_media_to_replace.png','media_to_paste.jpg')
|
||||
-- or --
|
||||
tpl.replace_media(io.BytesIO(image_stream), io.BytesIO(new_image_stream))
|
||||
|
||||
Note: for images, the aspect ratio will be the same as the replaced image
|
||||
|
||||
Note2: it is important to have the source media file as it is required
|
||||
to calculate its CRC to find them in the docx
|
||||
"""
|
||||
|
||||
crc = self.get_file_crc(src_file)
|
||||
if hasattr(dst_file, "read"):
|
||||
self.crc_to_new_media[crc] = dst_file.read()
|
||||
else:
|
||||
with open(dst_file, "rb") as fh:
|
||||
self.crc_to_new_media[crc] = fh.read()
|
||||
|
||||
def replace_pic(self, embedded_file, dst_file):
|
||||
"""Replace embedded picture with original-name given by embedded_file.
|
||||
(give only the file basename, not the full path)
|
||||
The new picture is given by dst_file (either a filename or a file-like
|
||||
object)
|
||||
|
||||
Notes:
|
||||
1) embedded_file and dst_file must have the same extension/format
|
||||
in case dst_file is a file-like object, no check is done on
|
||||
format compatibility
|
||||
2) the aspect ratio will be the same as the replaced image
|
||||
3) There is no need to keep the original file (this is not the case
|
||||
for replace_embedded and replace_media)
|
||||
"""
|
||||
|
||||
if hasattr(dst_file, "read"):
|
||||
# NOTE: file extension not checked
|
||||
self.pics_to_replace[embedded_file] = dst_file.read()
|
||||
else:
|
||||
with open(dst_file, "rb") as fh:
|
||||
self.pics_to_replace[embedded_file] = fh.read()
|
||||
|
||||
def replace_embedded(self, src_file, dst_file):
|
||||
"""Replace one embedded object by another one into a docx
|
||||
|
||||
This has been done mainly because it is not possible to add images
|
||||
in docx header/footer.
|
||||
With this function, put a dummy picture in your header/footer,
|
||||
then specify it with its replacement in this function
|
||||
|
||||
Syntax: tpl.replace_embedded('dummy_doc.docx','doc_to_paste.docx')
|
||||
|
||||
Note2 : it is important to have the source file as it is required to
|
||||
calculate its CRC to find them in the docx
|
||||
"""
|
||||
with open(dst_file, "rb") as fh:
|
||||
crc = self.get_file_crc(src_file)
|
||||
self.crc_to_new_embedded[crc] = fh.read()
|
||||
|
||||
def replace_zipname(self, zipname, dst_file):
|
||||
"""Replace one file in the docx file
|
||||
|
||||
First note that a MSWord .docx file is in fact a zip file.
|
||||
|
||||
This method can be used to replace document embedded in the docx template.
|
||||
|
||||
Some embedded document may have been modified by MSWord while saving
|
||||
the template : thus replace_embedded() cannot be used as CRC is not the
|
||||
same as the original file.
|
||||
|
||||
This method works for embedded MSWord file like Excel or PowerPoint file,
|
||||
but won't work for others like PDF, Python or even Text files :
|
||||
For these ones, MSWord generate an oleObjectNNN.bin file which is no
|
||||
use to be replaced as it is encoded.
|
||||
|
||||
Syntax:
|
||||
|
||||
tpl.replace_zipname(
|
||||
'word/embeddings/Feuille_Microsoft_Office_Excel1.xlsx',
|
||||
'my_excel_file.xlsx')
|
||||
|
||||
The zipname is the one you can find when you open docx with WinZip,
|
||||
7zip (Windows) or unzip -l (Linux). The zipname starts with
|
||||
"word/embeddings/". Note that the file is renamed by MSWord,
|
||||
so you have to guess a little bit...
|
||||
"""
|
||||
with open(dst_file, "rb") as fh:
|
||||
self.zipname_to_replace[zipname] = fh.read()
|
||||
|
||||
def reset_replacements(self):
|
||||
"""Reset replacement dictionaries
|
||||
|
||||
This will reset data for image/embedded/zipname replacement
|
||||
|
||||
This is useful when calling several times render() with different
|
||||
image/embedded/zipname replacements without re-instantiating
|
||||
DocxTemplate object.
|
||||
In this case, the right sequence for each rendering will be :
|
||||
- reset_replacements(...)
|
||||
- replace_zipname(...), replace_media(...) and/or replace_embedded(...),
|
||||
- render(...)
|
||||
|
||||
If you instantiate DocxTemplate object before each render(),
|
||||
this method is useless.
|
||||
"""
|
||||
self.crc_to_new_media = {}
|
||||
self.crc_to_new_embedded = {}
|
||||
self.zipname_to_replace = {}
|
||||
self.pics_to_replace = {}
|
||||
|
||||
def post_processing(self, docx_file):
|
||||
if self.crc_to_new_media or self.crc_to_new_embedded or self.zipname_to_replace:
|
||||
|
||||
if hasattr(docx_file, "read"):
|
||||
tmp_file = io.BytesIO()
|
||||
DocxTemplate(docx_file).save(tmp_file)
|
||||
tmp_file.seek(0)
|
||||
docx_file.seek(0)
|
||||
docx_file.truncate()
|
||||
docx_file.seek(0)
|
||||
|
||||
else:
|
||||
tmp_file = "%s_docxtpl_before_replace_medias" % docx_file
|
||||
os.rename(docx_file, tmp_file)
|
||||
|
||||
with zipfile.ZipFile(tmp_file) as zin:
|
||||
with zipfile.ZipFile(docx_file, "w") as zout:
|
||||
for item in zin.infolist():
|
||||
buf = zin.read(item.filename)
|
||||
if item.filename in self.zipname_to_replace:
|
||||
zout.writestr(item, self.zipname_to_replace[item.filename])
|
||||
elif (
|
||||
item.filename.startswith("word/media/")
|
||||
and item.CRC in self.crc_to_new_media
|
||||
):
|
||||
zout.writestr(item, self.crc_to_new_media[item.CRC])
|
||||
elif (
|
||||
item.filename.startswith("word/embeddings/")
|
||||
and item.CRC in self.crc_to_new_embedded
|
||||
):
|
||||
zout.writestr(item, self.crc_to_new_embedded[item.CRC])
|
||||
else:
|
||||
zout.writestr(item, buf)
|
||||
|
||||
if not hasattr(tmp_file, "read"):
|
||||
os.remove(tmp_file)
|
||||
if hasattr(docx_file, "read"):
|
||||
docx_file.seek(0)
|
||||
|
||||
def pre_processing(self):
|
||||
|
||||
if self.pics_to_replace:
|
||||
self._replace_pics()
|
||||
|
||||
def _replace_pics(self):
|
||||
"""Replaces pictures xml tags in the docx template with pictures provided by the user"""
|
||||
|
||||
replaced_pics = {key: False for key in self.pics_to_replace}
|
||||
|
||||
# Main document
|
||||
part = self.docx.part
|
||||
self._replace_docx_part_pics(part, replaced_pics)
|
||||
|
||||
# Header/Footer
|
||||
for relid, rel in part.rels.items():
|
||||
if rel.reltype in (REL_TYPE.HEADER, REL_TYPE.FOOTER):
|
||||
self._replace_docx_part_pics(rel.target_part, replaced_pics)
|
||||
|
||||
if not self.allow_missing_pics:
|
||||
# make sure all template images defined by user were replaced
|
||||
for img_id, replaced in replaced_pics.items():
|
||||
if not replaced:
|
||||
raise ValueError(
|
||||
"Picture %s not found in the docx template" % img_id
|
||||
)
|
||||
|
||||
def get_pic_map(self):
|
||||
return self.pic_map
|
||||
|
||||
def _replace_docx_part_pics(self, doc_part, replaced_pics):
|
||||
|
||||
et = etree.fromstring(doc_part.blob)
|
||||
|
||||
part_map = {}
|
||||
|
||||
gds = et.xpath("//a:graphic/a:graphicData", namespaces=docx.oxml.ns.nsmap)
|
||||
for gd in gds:
|
||||
rel = None
|
||||
# Either IMAGE, CHART, SMART_ART, ...
|
||||
try:
|
||||
if gd.attrib["uri"] == docx.oxml.ns.nsmap["pic"]:
|
||||
# Either PICTURE or LINKED_PICTURE image
|
||||
blip = gd.xpath(
|
||||
"pic:pic/pic:blipFill/a:blip", namespaces=docx.oxml.ns.nsmap
|
||||
)[0]
|
||||
dest = blip.xpath("@r:embed", namespaces=docx.oxml.ns.nsmap)
|
||||
if len(dest) > 0:
|
||||
rel = dest[0]
|
||||
else:
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
non_visual_properties = "pic:pic/pic:nvPicPr/pic:cNvPr/"
|
||||
filename = gd.xpath(
|
||||
"%s@name" % non_visual_properties, namespaces=docx.oxml.ns.nsmap
|
||||
)[0]
|
||||
titles = gd.xpath(
|
||||
"%s@title" % non_visual_properties, namespaces=docx.oxml.ns.nsmap
|
||||
)
|
||||
if titles:
|
||||
title = titles[0]
|
||||
else:
|
||||
title = ""
|
||||
descriptions = gd.xpath(
|
||||
"%s@descr" % non_visual_properties, namespaces=docx.oxml.ns.nsmap
|
||||
)
|
||||
if descriptions:
|
||||
description = descriptions[0]
|
||||
else:
|
||||
description = ""
|
||||
|
||||
part_map[filename] = (
|
||||
doc_part.rels[rel].target_ref,
|
||||
doc_part.rels[rel].target_part,
|
||||
)
|
||||
|
||||
# replace data
|
||||
for img_id, img_data in self.pics_to_replace.items():
|
||||
if img_id == filename or img_id == title or img_id == description:
|
||||
part_map[filename][1]._blob = img_data
|
||||
replaced_pics[img_id] = True
|
||||
break
|
||||
|
||||
# FIXME: figure out what exceptions are thrown here
|
||||
# and catch more specific exceptions
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
self.pic_map.update(part_map)
|
||||
|
||||
def build_url_id(self, url):
|
||||
self.init_docx()
|
||||
return self.docx._part.relate_to(url, REL_TYPE.HYPERLINK, is_external=True)
|
||||
|
||||
def save(self, filename: Union[IO[bytes], str, PathLike], *args, **kwargs) -> None:
|
||||
# case where save() is called without doing rendering
|
||||
# ( user wants only to replace image/embedded/zipname )
|
||||
if not self.is_saved and not self.is_rendered:
|
||||
self.docx = Document(self.template_file)
|
||||
self.pre_processing()
|
||||
self.docx.save(filename, *args, **kwargs)
|
||||
self.post_processing(filename)
|
||||
self.is_saved = True
|
||||
|
||||
def get_undeclared_template_variables(
|
||||
self,
|
||||
jinja_env: Optional[Environment] = None,
|
||||
context: Optional[Dict[str, Any]] = None,
|
||||
) -> Set[str]:
|
||||
# Create a temporary document to analyze the template without affecting the current state
|
||||
temp_doc = Document(self.template_file)
|
||||
|
||||
# Get XML from the temporary document
|
||||
xml = self.xml_to_string(temp_doc._element.body)
|
||||
xml = self.patch_xml(xml)
|
||||
|
||||
# Add headers and footers
|
||||
for uri in [self.HEADER_URI, self.FOOTER_URI]:
|
||||
for relKey, val in temp_doc._part.rels.items():
|
||||
if (val.reltype == uri) and (val.target_part.blob):
|
||||
_xml = self.xml_to_string(parse_xml(val.target_part.blob))
|
||||
xml += self.patch_xml(_xml)
|
||||
|
||||
if jinja_env:
|
||||
env = jinja_env
|
||||
else:
|
||||
env = Environment()
|
||||
|
||||
parse_content = env.parse(xml)
|
||||
all_variables = meta.find_undeclared_variables(parse_content)
|
||||
|
||||
# If context is provided, return only variables that are not in the context
|
||||
if context is not None:
|
||||
provided_variables = set(context.keys())
|
||||
return all_variables - provided_variables
|
||||
|
||||
# If no context provided, return all variables (original behavior)
|
||||
return all_variables
|
||||
1032
poetry.lock
generated
Normal file
70
pyproject.toml
Normal file
@ -0,0 +1,70 @@
|
||||
[project]
|
||||
name = "docxtpl"
|
||||
dynamic = ["version"]
|
||||
description = "Python docx template engine"
|
||||
authors = [{name="Eric Lapouyade", email="elapouya@proton.me"}]
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
license = {text="LGPL-2.1-only"}
|
||||
classifiers=[
|
||||
"Intended Audience :: Developers",
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
]
|
||||
keywords = ["jinja2"]
|
||||
dependencies = [
|
||||
"python-docx",
|
||||
"jinja2",
|
||||
"lxml",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
subdoc = ["docxcompose"]
|
||||
docs = ["Sphinx", "sphinxcontrib-napoleon"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"mypy >=1.18.2; python_version >= '3.9'",
|
||||
"lxml-stubs >=0.5.1; python_version >= '3.9'",
|
||||
"flake8 >=7.3.0; python_version >= '3.9'"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/elapouya/python-docx-template"
|
||||
repository = "https://github.com/elapouya/python-docx-template.git"
|
||||
document = "https://docxtpl.readthedocs.org"
|
||||
|
||||
[tool.poetry]
|
||||
version = "0.0.0"
|
||||
|
||||
[tool.poetry.requires-plugins]
|
||||
poetry-dynamic-versioning = { version = ">=1.0.0,<2.0.0", extras = ["plugin"] }
|
||||
|
||||
[tool.poetry-dynamic-versioning]
|
||||
enable = true
|
||||
|
||||
[tool.poetry-dynamic-versioning.from-file]
|
||||
source = "docxtpl/__init__.py"
|
||||
pattern = '__version__ = "(.+)"'
|
||||
|
||||
[tool.mypy]
|
||||
pretty = true
|
||||
python_version = "3.9"
|
||||
check_untyped_defs = true
|
||||
warn_unused_ignores = true
|
||||
exclude = ["docs", "build", "setup.py"]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["docxcompose.*"]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core", "poetry-dynamic-versioning >=1.0.0,<2.0.0"]
|
||||
build-backend = "poetry_dynamic_versioning.backend"
|
||||
5
requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
python-docx
|
||||
docxcompose
|
||||
jinja2
|
||||
lxml
|
||||
sphinx-book-theme
|
||||
79
setup.py
@ -1,17 +1,23 @@
|
||||
from setuptools import setup
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
# To register onto Pypi :
|
||||
# python setup.py sdist bdist_wheel upload
|
||||
|
||||
|
||||
def read(*names):
|
||||
values = dict()
|
||||
for name in names:
|
||||
filename = name + '.rst'
|
||||
filename = name + ".rst"
|
||||
if os.path.isfile(filename):
|
||||
fd = open(filename)
|
||||
value = fd.read()
|
||||
fd.close()
|
||||
else:
|
||||
value = ''
|
||||
value = ""
|
||||
values[name] = value
|
||||
return values
|
||||
|
||||
@ -22,35 +28,50 @@ long_description = """
|
||||
News
|
||||
====
|
||||
%(CHANGES)s
|
||||
""" % read('README', 'CHANGES')
|
||||
""" % read(
|
||||
"README", "CHANGES"
|
||||
)
|
||||
|
||||
|
||||
def get_version(pkg):
|
||||
path = os.path.join(os.path.dirname(__file__),pkg,'__init__.py')
|
||||
with open(path) as fh:
|
||||
m = re.search(r'^__version__\s*=\s*[\'"]([^\'"]+)[\'"]',fh.read(),re.M)
|
||||
path = os.path.join(os.path.dirname(__file__), pkg, "__init__.py")
|
||||
if sys.version_info >= (3, 0):
|
||||
fh = open(path, encoding="utf-8") # required to read utf-8 file on windows
|
||||
else:
|
||||
fh = open(path) # encoding parameter does not exist in python 2
|
||||
with fh:
|
||||
m = re.search(r'^__version__\s*=\s*[\'"]([^\'"]+)[\'"]', fh.read(), re.M)
|
||||
if m:
|
||||
return m.group(1)
|
||||
raise RuntimeError("Unable to find __version__ string in %s." % path)
|
||||
|
||||
setup(name='docxtpl',
|
||||
version=get_version('docxtpl'),
|
||||
description='Python docx template engine',
|
||||
long_description=long_description,
|
||||
classifiers=[
|
||||
"Intended Audience :: Developers",
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python :: 2",
|
||||
"Programming Language :: Python :: 2.7",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.4",
|
||||
],
|
||||
keywords='jinja2',
|
||||
url='https://github.com/elapouya/python-docx-template',
|
||||
author='Eric Lapouyade',
|
||||
author_email='elapouya@gmail.com',
|
||||
license='LGPL 2.1',
|
||||
packages=['docxtpl'],
|
||||
install_requires=['six', 'python-docx', 'jinja2', 'lxml'],
|
||||
extras_require={'docs': ['Sphinx', 'sphinxcontrib-napoleon']},
|
||||
eager_resources=['docs'],
|
||||
zip_safe=False)
|
||||
|
||||
setup(
|
||||
name="docxtpl",
|
||||
version=get_version("docxtpl"),
|
||||
description="Python docx template engine",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/x-rst",
|
||||
classifiers=[
|
||||
"Intended Audience :: Developers",
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
],
|
||||
keywords="jinja2",
|
||||
url="https://github.com/elapouya/python-docx-template",
|
||||
author="Eric Lapouyade",
|
||||
license="LGPL-2.1-only",
|
||||
license_files=[],
|
||||
packages=["docxtpl"],
|
||||
install_requires=["python-docx>=1.1.1", "jinja2", "lxml"],
|
||||
extras_require={"docs": ["Sphinx", "sphinxcontrib-napoleon"], "subdoc": ["docxcompose"]},
|
||||
eager_resources=["docs"],
|
||||
zip_safe=False,
|
||||
)
|
||||
|
||||
@ -1,22 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
"""
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
'''
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate, RichText
|
||||
|
||||
tpl=DocxTemplate('test_files/cellbg_tpl.docx')
|
||||
tpl = DocxTemplate("templates/cellbg_tpl.docx")
|
||||
|
||||
context = {
|
||||
'alerts' : [
|
||||
{'date' : '2015-03-10', 'desc' : RichText('Very critical alert',color='FF0000', bold=True), 'type' : 'CRITICAL', 'bg': 'FF0000' },
|
||||
{'date' : '2015-03-11', 'desc' : RichText('Just a warning'), 'type' : 'WARNING', 'bg': 'FFDD00' },
|
||||
{'date' : '2015-03-12', 'desc' : RichText('Information'), 'type' : 'INFO', 'bg': '8888FF' },
|
||||
{'date' : '2015-03-13', 'desc' : RichText('Debug trace'), 'type' : 'DEBUG', 'bg': 'FF00FF' },
|
||||
"alerts": [
|
||||
{
|
||||
"date": "2015-03-10",
|
||||
"desc": RichText("Very critical alert", color="FF0000", bold=True),
|
||||
"type": "CRITICAL",
|
||||
"bg": "FF0000",
|
||||
},
|
||||
{
|
||||
"date": "2015-03-11",
|
||||
"desc": RichText("Just a warning"),
|
||||
"type": "WARNING",
|
||||
"bg": "FFDD00",
|
||||
},
|
||||
{
|
||||
"date": "2015-03-12",
|
||||
"desc": RichText("Information"),
|
||||
"type": "INFO",
|
||||
"bg": "8888FF",
|
||||
},
|
||||
{
|
||||
"date": "2015-03-13",
|
||||
"desc": RichText("Debug trace"),
|
||||
"type": "DEBUG",
|
||||
"bg": "FF00FF",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save('test_files/cellbg.docx')
|
||||
tpl.save("output/cellbg.docx")
|
||||
|
||||
6
tests/comments.py
Normal file
@ -0,0 +1,6 @@
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/comments_tpl.docx")
|
||||
|
||||
tpl.render({})
|
||||
tpl.save("output/comments.docx")
|
||||
35
tests/custom_jinja_filters.py
Normal file
@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: sandeeprah, Eric Lapouyade
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
import jinja2
|
||||
|
||||
jinja_env = jinja2.Environment()
|
||||
|
||||
|
||||
# to create new filters, first create functions that accept the value to filter
|
||||
# as first argument, and filter parameters as next arguments
|
||||
def my_filterA(value, my_string_arg):
|
||||
return_value = value + " " + my_string_arg
|
||||
return return_value
|
||||
|
||||
|
||||
def my_filterB(value, my_float_arg):
|
||||
return_value = value + my_float_arg
|
||||
return return_value
|
||||
|
||||
|
||||
# Then, declare them to jinja like this :
|
||||
jinja_env.filters["my_filterA"] = my_filterA
|
||||
jinja_env.filters["my_filterB"] = my_filterB
|
||||
|
||||
|
||||
context = {"base_value_string": " Hello", "base_value_float": 1.5}
|
||||
|
||||
tpl = DocxTemplate("templates/custom_jinja_filters_tpl.docx")
|
||||
tpl.render(context, jinja_env)
|
||||
tpl.save("output/custom_jinja_filters.docx")
|
||||
10
tests/doc_properties.py
Normal file
@ -0,0 +1,10 @@
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
doctemplate = r"templates/doc_properties_tpl.docx"
|
||||
|
||||
tpl = DocxTemplate(doctemplate)
|
||||
|
||||
context = {"test": "HelloWorld"}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/doc_properties.docx")
|
||||
@ -1,15 +1,15 @@
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl=DocxTemplate('test_files/dynamic_table_tpl.docx')
|
||||
tpl = DocxTemplate("templates/dynamic_table_tpl.docx")
|
||||
|
||||
context = {
|
||||
'col_labels' : ['fruit', 'vegetable', 'stone', 'thing'],
|
||||
'tbl_contents': [
|
||||
{'label': 'yellow', 'cols': ['banana', 'capsicum', 'pyrite', 'taxi']},
|
||||
{'label': 'red', 'cols': ['apple', 'tomato', 'cinnabar', 'doubledecker']},
|
||||
{'label': 'green', 'cols': ['guava', 'cucumber', 'aventurine', 'card']},
|
||||
]
|
||||
"col_labels": ["fruit", "vegetable", "stone", "thing"],
|
||||
"tbl_contents": [
|
||||
{"label": "yellow", "cols": ["banana", "capsicum", "pyrite", "taxi"]},
|
||||
{"label": "red", "cols": ["apple", "tomato", "cinnabar", "doubledecker"]},
|
||||
{"label": "green", "cols": ["guava", "cucumber", "aventurine", "card"]},
|
||||
],
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save('test_files/dynamic_table.docx')
|
||||
tpl.save("output/dynamic_table.docx")
|
||||
|
||||
@ -1,29 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
"""
|
||||
Created : 2017-09-09
|
||||
|
||||
@author: Eric Lapouyade
|
||||
'''
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
# rendering the "dynamic embedded docx":
|
||||
embedded_docx_tpl=DocxTemplate('test_files/embedded_embedded_docx_tpl.docx')
|
||||
embedded_docx_tpl = DocxTemplate("templates/embedded_embedded_docx_tpl.docx")
|
||||
context = {
|
||||
'name' : 'John Doe',
|
||||
"name": "John Doe",
|
||||
}
|
||||
embedded_docx_tpl.render(context)
|
||||
embedded_docx_tpl.save('test_files/embedded_embedded_docx.docx')
|
||||
embedded_docx_tpl.save("output/embedded_embedded_docx.docx")
|
||||
|
||||
|
||||
# rendring the main document :
|
||||
tpl=DocxTemplate('test_files/embedded_main_tpl.docx')
|
||||
# rendering the main document :
|
||||
tpl = DocxTemplate("templates/embedded_main_tpl.docx")
|
||||
|
||||
context = {
|
||||
'name' : 'John Doe',
|
||||
"name": "John Doe",
|
||||
}
|
||||
|
||||
tpl.replace_embedded('test_files/embedded_dummy.docx','test_files/embedded_static_docx.docx')
|
||||
tpl.replace_embedded('test_files/embedded_dummy2.docx','test_files/embedded_embedded_docx.docx')
|
||||
tpl.replace_embedded(
|
||||
"templates/embedded_dummy.docx", "templates/embedded_static_docx.docx"
|
||||
)
|
||||
tpl.replace_embedded(
|
||||
"templates/embedded_dummy2.docx", "output/embedded_embedded_docx.docx"
|
||||
)
|
||||
|
||||
# The zipname is the one you can find when you open docx with WinZip, 7zip (Windows)
|
||||
# or unzip -l (Linux). The zipname starts with "word/embeddings/".
|
||||
# Note that the file is renamed by MSWord, so you have to guess a little bit...
|
||||
tpl.replace_zipname(
|
||||
"word/embeddings/Feuille_Microsoft_Office_Excel3.xlsx", "templates/real_Excel.xlsx"
|
||||
)
|
||||
tpl.replace_zipname(
|
||||
"word/embeddings/Pr_sentation_Microsoft_Office_PowerPoint4.pptx",
|
||||
"templates/real_PowerPoint.pptx",
|
||||
)
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save('test_files/embedded.docx')
|
||||
tpl.save("output/embedded.docx")
|
||||
|
||||
@ -1,12 +1,54 @@
|
||||
from docxtpl import *
|
||||
from docxtpl import DocxTemplate, R, Listing
|
||||
|
||||
tpl = DocxTemplate("test_files/escape_tpl.docx")
|
||||
tpl = DocxTemplate("templates/escape_tpl.docx")
|
||||
|
||||
context = {'myvar': R('"less than" must be escaped : <, this can be done with RichText() or R()'),
|
||||
'myescvar':'It can be escaped with a "|e" jinja filter in the template too : < ',
|
||||
'nlnp' : R('Here is a multiple\nlines\nstring\aand some\aother\aparagraphs\aNOTE: the current character styling is removed'),
|
||||
'mylisting': Listing('the listing\nwith\nsome\nlines\nand special chars : <>&'),
|
||||
}
|
||||
context = {
|
||||
"myvar": R(
|
||||
'"less than" must be escaped : <, this can be done with RichText() or R()'
|
||||
),
|
||||
"myescvar": 'It can be escaped with a "|e" jinja filter in the template too : < ',
|
||||
"nlnp": R(
|
||||
"Here is a multiple\nlines\nstring\aand some\aother\aparagraphs",
|
||||
color="#ff00ff",
|
||||
),
|
||||
"mylisting": Listing("the listing\nwith\nsome\nlines\nand special chars : <>& ..."),
|
||||
"page_break": R("\f"),
|
||||
"new_listing": """
|
||||
This is a new listing
|
||||
Now, does not require Listing() Object
|
||||
Here is a \t tab\a
|
||||
Here is a new paragraph\a
|
||||
Here is a page break : \f
|
||||
That's it
|
||||
""",
|
||||
"some_html": (
|
||||
"HTTP/1.1 200 OK\n"
|
||||
"Server: Apache-Coyote/1.1\n"
|
||||
"Cache-Control: no-store\n"
|
||||
"Expires: Thu, 01 Jan 1970 00:00:00 GMT\n"
|
||||
"Pragma: no-cache\n"
|
||||
"Content-Type: text/html;charset=UTF-8\n"
|
||||
"Content-Language: zh-CN\n"
|
||||
"Date: Thu, 22 Oct 2020 10:59:40 GMT\n"
|
||||
"Content-Length: 9866\n"
|
||||
"\n"
|
||||
"<html>\n"
|
||||
"<head>\n"
|
||||
" <title>Struts Problem Report</title>\n"
|
||||
" <style>\n"
|
||||
" \tpre {\n"
|
||||
"\t \tmargin: 0;\n"
|
||||
"\t padding: 0;\n"
|
||||
"\t } "
|
||||
"\n"
|
||||
" </style>\n"
|
||||
"</head>\n"
|
||||
"<body>\n"
|
||||
"...\n"
|
||||
"</body>\n"
|
||||
"</html>"
|
||||
),
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("test_files/escape.docx")
|
||||
tpl.save("output/escape.docx")
|
||||
|
||||
27
tests/escape_auto.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""
|
||||
@author: Max Podolskii
|
||||
"""
|
||||
|
||||
import os
|
||||
from unicodedata import name
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
|
||||
XML_RESERVED = """<"&'>"""
|
||||
|
||||
tpl = DocxTemplate("templates/escape_tpl_auto.docx")
|
||||
|
||||
context = {
|
||||
"nested_dict": {name(str(c)): c for c in XML_RESERVED},
|
||||
"autoescape": 'Escaped "str & ing"!',
|
||||
"autoescape_unicode": "This is an escaped <unicode> example \u4f60 & \u6211",
|
||||
"iteritems": lambda x: x.items(),
|
||||
}
|
||||
|
||||
tpl.render(context, autoescape=True)
|
||||
|
||||
OUTPUT = "output"
|
||||
if not os.path.exists(OUTPUT):
|
||||
os.makedirs(OUTPUT)
|
||||
tpl.save(OUTPUT + "/escape_auto.docx")
|
||||
17
tests/footnotes.py
Normal file
@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2024-09-23
|
||||
|
||||
@author: Bart Broere
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
DEST_FILE = "output/footnotes.docx"
|
||||
|
||||
tpl = DocxTemplate("templates/footnotes_tpl.docx")
|
||||
|
||||
context = {"a_jinja_variable": "A Jinja variable!"}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save(DEST_FILE)
|
||||
180
tests/get_undeclared_variables.py
Normal file
@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Test for get_undeclared_template_variables method
|
||||
|
||||
This test demonstrates the correct behavior of get_undeclared_template_variables:
|
||||
1. Before rendering - finds all template variables
|
||||
2. After rendering with incomplete context - finds missing variables
|
||||
3. After rendering with complete context - returns empty set
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
|
||||
def test_before_render():
|
||||
"""Test that get_undeclared_template_variables finds all variables before rendering"""
|
||||
print("=== Test 1: Before render ===")
|
||||
tpl = DocxTemplate("templates/get_undeclared_variables.docx")
|
||||
undeclared = tpl.get_undeclared_template_variables()
|
||||
print(f"Variables found: {undeclared}")
|
||||
|
||||
# Should find all variables
|
||||
expected_vars = {
|
||||
"name",
|
||||
"age",
|
||||
"email",
|
||||
"is_student",
|
||||
"has_degree",
|
||||
"degree_field",
|
||||
"skills",
|
||||
"projects",
|
||||
"company_name",
|
||||
"page_number",
|
||||
"generation_date",
|
||||
"author",
|
||||
}
|
||||
|
||||
if undeclared == expected_vars:
|
||||
print("PASS: Found all expected variables before render")
|
||||
else:
|
||||
print(f"FAIL: Expected {expected_vars}, got {undeclared}")
|
||||
|
||||
return undeclared == expected_vars
|
||||
|
||||
|
||||
def test_after_incomplete_render():
|
||||
"""Test that get_undeclared_template_variables finds missing variables after incomplete render"""
|
||||
print("\n=== Test 2: After incomplete render ===")
|
||||
tpl = DocxTemplate("templates/get_undeclared_variables.docx")
|
||||
|
||||
# Provide only some variables (missing several)
|
||||
context = {
|
||||
"name": "John Doe",
|
||||
"age": 25,
|
||||
"email": "john@example.com",
|
||||
"is_student": True,
|
||||
"skills": ["Python", "Django"],
|
||||
"company_name": "Test Corp",
|
||||
"author": "Test Author",
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
undeclared = tpl.get_undeclared_template_variables(context=context)
|
||||
print(f"Missing variables: {undeclared}")
|
||||
|
||||
# Should find missing variables
|
||||
expected_missing = {
|
||||
"has_degree",
|
||||
"degree_field",
|
||||
"projects",
|
||||
"page_number",
|
||||
"generation_date",
|
||||
}
|
||||
|
||||
if undeclared == expected_missing:
|
||||
print("PASS: Found missing variables after incomplete render")
|
||||
else:
|
||||
print(f"FAIL: Expected missing {expected_missing}, got {undeclared}")
|
||||
|
||||
return undeclared == expected_missing
|
||||
|
||||
|
||||
def test_after_complete_render():
|
||||
"""Test that get_undeclared_template_variables returns empty set after complete render"""
|
||||
print("\n=== Test 3: After complete render ===")
|
||||
tpl = DocxTemplate("templates/get_undeclared_variables.docx")
|
||||
|
||||
# Provide all variables
|
||||
context = {
|
||||
"name": "John Doe",
|
||||
"age": 25,
|
||||
"email": "john@example.com",
|
||||
"is_student": True,
|
||||
"has_degree": True,
|
||||
"degree_field": "Computer Science",
|
||||
"skills": ["Python", "Django", "JavaScript"],
|
||||
"projects": [
|
||||
{"name": "Project A", "year": 2023, "description": "A great project"},
|
||||
{"name": "Project B", "year": 2024, "description": "Another great project"},
|
||||
],
|
||||
"company_name": "Test Corp",
|
||||
"page_number": 1,
|
||||
"generation_date": "2024-01-15",
|
||||
"author": "Test Author",
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
undeclared = tpl.get_undeclared_template_variables(context=context)
|
||||
print(f"Undeclared variables: {undeclared}")
|
||||
|
||||
# Should return empty set
|
||||
if undeclared == set():
|
||||
print("PASS: No undeclared variables after complete render")
|
||||
else:
|
||||
print(f"FAIL: Expected empty set, got {undeclared}")
|
||||
|
||||
return undeclared == set()
|
||||
|
||||
|
||||
def test_with_custom_jinja_env():
|
||||
"""Test that get_undeclared_template_variables works with custom Jinja environment"""
|
||||
print("\n=== Test 4: With custom Jinja environment ===")
|
||||
from jinja2 import Environment
|
||||
|
||||
tpl = DocxTemplate("templates/get_undeclared_variables.docx")
|
||||
custom_env = Environment()
|
||||
|
||||
undeclared = tpl.get_undeclared_template_variables(jinja_env=custom_env)
|
||||
print(f"Variables found with custom env: {undeclared}")
|
||||
|
||||
# Should find all variables
|
||||
expected_vars = {
|
||||
"name",
|
||||
"age",
|
||||
"email",
|
||||
"is_student",
|
||||
"has_degree",
|
||||
"degree_field",
|
||||
"skills",
|
||||
"projects",
|
||||
"company_name",
|
||||
"page_number",
|
||||
"generation_date",
|
||||
"author",
|
||||
}
|
||||
|
||||
if undeclared == expected_vars:
|
||||
print("PASS: Custom Jinja environment works correctly")
|
||||
else:
|
||||
print(f"FAIL: Expected {expected_vars}, got {undeclared}")
|
||||
|
||||
return undeclared == expected_vars
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Testing get_undeclared_template_variables method...")
|
||||
print("=" * 50)
|
||||
|
||||
# Run all tests
|
||||
test1_passed = test_before_render()
|
||||
test2_passed = test_after_incomplete_render()
|
||||
test3_passed = test_after_complete_render()
|
||||
test4_passed = test_with_custom_jinja_env()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("SUMMARY:")
|
||||
print(f"Test 1 (Before render): {'PASS' if test1_passed else 'FAIL'}")
|
||||
print(f"Test 2 (After incomplete render): {'PASS' if test2_passed else 'FAIL'}")
|
||||
print(f"Test 3 (After complete render): {'PASS' if test3_passed else 'FAIL'}")
|
||||
print(f"Test 4 (Custom Jinja env): {'PASS' if test4_passed else 'FAIL'}")
|
||||
|
||||
all_passed = test1_passed and test2_passed and test3_passed and test4_passed
|
||||
|
||||
if all_passed:
|
||||
print("ALL TESTS PASSED!")
|
||||
else:
|
||||
print("SOME TESTS FAILED!")
|
||||
|
||||
print("\nNote: This test demonstrates that get_undeclared_template_variables")
|
||||
print("now correctly analyzes the original template, not the rendered document.")
|
||||
@ -1,23 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
"""
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
'''
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl=DocxTemplate('test_files/header_footer_tpl.docx')
|
||||
tpl = DocxTemplate("templates/header_footer_tpl.docx")
|
||||
|
||||
sd = tpl.new_subdoc()
|
||||
p = sd.add_paragraph('This is a sub-document to check it does not break header and footer')
|
||||
p = sd.add_paragraph(
|
||||
"This is a sub-document to check it does not break header and footer"
|
||||
)
|
||||
|
||||
context = {
|
||||
'title' : 'Header and footer test',
|
||||
'company_name' : 'The World Wide company',
|
||||
'date' : '2016-03-17',
|
||||
'mysubdoc' : sd,
|
||||
"title": "Header and footer test",
|
||||
"company_name": "The World Wide company",
|
||||
"date": "2016-03-17",
|
||||
"mysubdoc": sd,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save('test_files/header_footer.docx')
|
||||
tpl.save("output/header_footer.docx")
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
"""
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
'''
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl=DocxTemplate('test_files/header_footer_entities_tpl.docx')
|
||||
tpl = DocxTemplate("templates/header_footer_entities_tpl.docx")
|
||||
|
||||
context = {
|
||||
'title' : 'Header and footer test',
|
||||
"title": "Header and footer test",
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save('test_files/header_footer_entities.docx')
|
||||
tpl.save("output/header_footer_entities.docx")
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
"""
|
||||
Created : 2017-09-03
|
||||
|
||||
@author: Eric Lapouyade
|
||||
'''
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
DEST_FILE = 'test_files/header_footer_image.docx'
|
||||
DEST_FILE = "output/header_footer_image.docx"
|
||||
|
||||
tpl=DocxTemplate('test_files/header_footer_image_tpl.docx')
|
||||
tpl = DocxTemplate("templates/header_footer_image_tpl.docx")
|
||||
|
||||
context = {
|
||||
'mycompany' : 'The World Wide company',
|
||||
"mycompany": "The World Wide company",
|
||||
}
|
||||
tpl.replace_media('test_files/dummy_pic_for_header.png','test_files/python.png')
|
||||
tpl.replace_media("templates/dummy_pic_for_header.png", "templates/python.png")
|
||||
tpl.render(context)
|
||||
tpl.save(DEST_FILE)
|
||||
tpl.save(DEST_FILE)
|
||||
|
||||
36
tests/header_footer_image_file_obj.py
Normal file
@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2019-05-22
|
||||
|
||||
@author: Eric Dufresne
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
import io
|
||||
|
||||
DEST_FILE = "output/header_footer_image_file_obj.docx"
|
||||
DEST_FILE2 = "output/header_footer_image_file_obj2.docx"
|
||||
|
||||
tpl = DocxTemplate("templates/header_footer_image_tpl.docx")
|
||||
|
||||
context = {
|
||||
"mycompany": "The World Wide company",
|
||||
}
|
||||
|
||||
dummy_pic = io.BytesIO(open("templates/dummy_pic_for_header.png", "rb").read())
|
||||
new_image = io.BytesIO(open("templates/python.png", "rb").read())
|
||||
tpl.replace_media(dummy_pic, new_image)
|
||||
tpl.render(context)
|
||||
tpl.save(DEST_FILE)
|
||||
|
||||
tpl = DocxTemplate("templates/header_footer_image_tpl.docx")
|
||||
dummy_pic.seek(0)
|
||||
new_image.seek(0)
|
||||
tpl.replace_media(dummy_pic, new_image)
|
||||
tpl.render(context)
|
||||
|
||||
file_obj = io.BytesIO()
|
||||
tpl.save(file_obj)
|
||||
file_obj.seek(0)
|
||||
with open(DEST_FILE2, "wb") as f:
|
||||
f.write(file_obj.read())
|
||||
24
tests/header_footer_inline_image.py
Normal file
@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2021-04-06
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate, InlineImage
|
||||
|
||||
# for height and width you have to use millimeters (Mm), inches or points(Pt) class :
|
||||
from docx.shared import Mm
|
||||
|
||||
tpl = DocxTemplate("templates/header_footer_inline_image_tpl.docx")
|
||||
|
||||
context = {
|
||||
"inline_image": InlineImage(tpl, "templates/django.png", height=Mm(10)),
|
||||
"images": [
|
||||
InlineImage(tpl, "templates/python.png", height=Mm(10)),
|
||||
InlineImage(tpl, "templates/python.png", height=Mm(10)),
|
||||
InlineImage(tpl, "templates/python.png", height=Mm(10)),
|
||||
],
|
||||
}
|
||||
tpl.render(context)
|
||||
tpl.save("output/header_footer_inline_image.docx")
|
||||
@ -1,25 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
"""
|
||||
Created : 2016-07-19
|
||||
|
||||
@author: AhnSeongHyun
|
||||
|
||||
Edited : 2016-07-19 by Eric Lapouyade
|
||||
'''
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl=DocxTemplate('test_files/header_footer_tpl_utf8.docx')
|
||||
tpl = DocxTemplate("templates/header_footer_tpl_utf8.docx")
|
||||
|
||||
sd = tpl.new_subdoc()
|
||||
p = sd.add_paragraph(u'This is a sub-document to check it does not break header and footer with utf-8 characters inside the template .docx')
|
||||
p = sd.add_paragraph(
|
||||
"This is a sub-document to check it does not break header and footer with utf-8 "
|
||||
"characters inside the template .docx"
|
||||
)
|
||||
|
||||
context = {
|
||||
'title' : u'헤더와 푸터',
|
||||
'company_name' : u'세계적 회사',
|
||||
'date' : u'2016-03-17',
|
||||
'mysubdoc' : sd,
|
||||
"title": "헤더와 푸터",
|
||||
"company_name": "세계적 회사",
|
||||
"date": "2016-03-17",
|
||||
"mysubdoc": sd,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save('test_files/header_footer_utf8.docx')
|
||||
tpl.save("output/header_footer_utf8.docx")
|
||||
|
||||
7
tests/horizontal_merge.py
Normal file
@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/horizontal_merge_tpl.docx")
|
||||
tpl.render({})
|
||||
tpl.save("output/horizontal_merge.docx")
|
||||
@ -1,36 +1,51 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
"""
|
||||
Created : 2017-01-14
|
||||
|
||||
@author: Eric Lapouyade
|
||||
'''
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate, InlineImage
|
||||
# for height and width you have to use millimeters (Mm), inches or points(Pt) class :
|
||||
from docx.shared import Mm, Inches, Pt
|
||||
|
||||
tpl=DocxTemplate('test_files/inline_image_tpl.docx')
|
||||
# for height and width you have to use millimeters (Mm), inches or points(Pt) class :
|
||||
from docx.shared import Mm
|
||||
import jinja2
|
||||
|
||||
tpl = DocxTemplate("templates/inline_image_tpl.docx")
|
||||
|
||||
context = {
|
||||
'myimage' : InlineImage(tpl,'test_files/python_logo.png',width=Mm(20)),
|
||||
'myimageratio': InlineImage(tpl, 'test_files/python_jpeg.jpg', width=Mm(30), height=Mm(60)),
|
||||
|
||||
'frameworks' : [{'image' : InlineImage(tpl,'test_files/django.png',height=Mm(10)),
|
||||
'desc' : 'The web framework for perfectionists with deadlines'},
|
||||
|
||||
{'image' : InlineImage(tpl,'test_files/zope.png',height=Mm(10)),
|
||||
'desc' : 'Zope is a leading Open Source Application Server and Content Management Framework'},
|
||||
|
||||
{'image': InlineImage(tpl, 'test_files/pyramid.png', height=Mm(10)),
|
||||
'desc': 'Pyramid is a lightweight Python web framework aimed at taking small web apps into big web apps.'},
|
||||
|
||||
{'image' : InlineImage(tpl,'test_files/bottle.png',height=Mm(10)),
|
||||
'desc' : 'Bottle is a fast, simple and lightweight WSGI micro web-framework for Python'},
|
||||
|
||||
{'image': InlineImage(tpl, 'test_files/tornado.png', height=Mm(10)),
|
||||
'desc': 'Tornado is a Python web framework and asynchronous networking library.'},
|
||||
]
|
||||
"myimage": InlineImage(tpl, "templates/python_logo.png", width=Mm(20)),
|
||||
"myimageratio": InlineImage(
|
||||
tpl, "templates/python_jpeg.jpg", width=Mm(30), height=Mm(60)
|
||||
),
|
||||
"frameworks": [
|
||||
{
|
||||
"image": InlineImage(tpl, "templates/django.png", height=Mm(10)),
|
||||
"desc": "The web framework for perfectionists with deadlines",
|
||||
},
|
||||
{
|
||||
"image": InlineImage(tpl, "templates/zope.png", height=Mm(10)),
|
||||
"desc": "Zope is a leading Open Source Application Server "
|
||||
"and Content Management Framework",
|
||||
},
|
||||
{
|
||||
"image": InlineImage(tpl, "templates/pyramid.png", height=Mm(10)),
|
||||
"desc": "Pyramid is a lightweight Python web framework aimed at taking "
|
||||
"small web apps into big web apps.",
|
||||
},
|
||||
{
|
||||
"image": InlineImage(tpl, "templates/bottle.png", height=Mm(10)),
|
||||
"desc": "Bottle is a fast, simple and lightweight WSGI micro web-framework "
|
||||
"for Python",
|
||||
},
|
||||
{
|
||||
"image": InlineImage(tpl, "templates/tornado.png", height=Mm(10)),
|
||||
"desc": "Tornado is a Python web framework and asynchronous networking "
|
||||
"library.",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save('test_files/inline_image.docx')
|
||||
# testing that it works also when autoescape has been forced to True
|
||||
jinja_env = jinja2.Environment(autoescape=True)
|
||||
tpl.render(context, jinja_env)
|
||||
tpl.save("output/inline_image.docx")
|
||||
|
||||
5
tests/less_cells_after_loop.py
Normal file
@ -0,0 +1,5 @@
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/less_cells_after_loop_tpl.docx")
|
||||
tpl.render({})
|
||||
tpl.save("output/less_cells_after_loop.docx")
|
||||
19
tests/merge_docx.py
Normal file
@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2021-07-30
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
|
||||
tpl = DocxTemplate("templates/merge_docx_master_tpl.docx")
|
||||
sd = tpl.new_subdoc("templates/merge_docx_subdoc.docx")
|
||||
|
||||
context = {
|
||||
"mysubdoc": sd,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/merge_docx.docx")
|
||||
17
tests/merge_paragraph.py
Normal file
@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/merge_paragraph_tpl.docx")
|
||||
|
||||
context = {
|
||||
"living_in_town": True,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/merge_paragraph.docx")
|
||||
25
tests/module_execute.py
Normal file
@ -0,0 +1,25 @@
|
||||
import os
|
||||
|
||||
TEMPLATE_PATH = "templates/module_execute_tpl.docx"
|
||||
JSON_PATH = "templates/module_execute.json"
|
||||
OUTPUT_FILENAME = "output/module_execute.docx"
|
||||
OVERWRITE = "-o"
|
||||
QUIET = "-q"
|
||||
|
||||
|
||||
if os.path.exists(OUTPUT_FILENAME):
|
||||
os.unlink(OUTPUT_FILENAME)
|
||||
|
||||
os.chdir(os.path.dirname(__file__))
|
||||
cmd = "python -m docxtpl %s %s %s %s %s" % (
|
||||
TEMPLATE_PATH,
|
||||
JSON_PATH,
|
||||
OUTPUT_FILENAME,
|
||||
OVERWRITE,
|
||||
QUIET,
|
||||
)
|
||||
print('Executing "%s" ...' % cmd)
|
||||
os.system(cmd)
|
||||
|
||||
if os.path.exists(OUTPUT_FILENAME):
|
||||
print(" --> File %s has been generated." % OUTPUT_FILENAME)
|
||||
40
tests/multi_rendering.py
Normal file
@ -0,0 +1,40 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2021-12-20
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/multi_rendering_tpl.docx")
|
||||
|
||||
documents_data = [
|
||||
{
|
||||
"dest_file": "multi_render1.docx",
|
||||
"context": {
|
||||
"title": "Title ONE",
|
||||
"body": "This is the body for first document",
|
||||
},
|
||||
},
|
||||
{
|
||||
"dest_file": "multi_render2.docx",
|
||||
"context": {
|
||||
"title": "Title TWO",
|
||||
"body": "This is the body for second document",
|
||||
},
|
||||
},
|
||||
{
|
||||
"dest_file": "multi_render3.docx",
|
||||
"context": {
|
||||
"title": "Title THREE",
|
||||
"body": "This is the body for third document",
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
for document_data in documents_data:
|
||||
dest_file = document_data["dest_file"]
|
||||
context = document_data["context"]
|
||||
tpl.render(context)
|
||||
tpl.save("output/%s" % dest_file)
|
||||
@ -1,33 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
"""
|
||||
Created : 2016-03-26
|
||||
|
||||
@author: Eric Lapouyade
|
||||
'''
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl=DocxTemplate('test_files/nested_for_tpl.docx')
|
||||
tpl = DocxTemplate("templates/nested_for_tpl.docx")
|
||||
|
||||
context = {
|
||||
'dishes' : [
|
||||
{'name' : 'Pizza', 'ingredients' : ['bread','tomato', 'ham', 'cheese']},
|
||||
{'name' : 'Hamburger', 'ingredients' : ['bread','chopped steak', 'cheese', 'sauce']},
|
||||
{'name' : 'Apple pie', 'ingredients' : ['flour','apples', 'suggar', 'quince jelly']},
|
||||
"dishes": [
|
||||
{"name": "Pizza", "ingredients": ["bread", "tomato", "ham", "cheese"]},
|
||||
{
|
||||
"name": "Hamburger",
|
||||
"ingredients": ["bread", "chopped steak", "cheese", "sauce"],
|
||||
},
|
||||
{
|
||||
"name": "Apple pie",
|
||||
"ingredients": ["flour", "apples", "suggar", "quince jelly"],
|
||||
},
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Saint-Exupery",
|
||||
"books": [
|
||||
{"title": "Le petit prince"},
|
||||
{"title": "L'aviateur"},
|
||||
{"title": "Vol de nuit"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Barjavel",
|
||||
"books": [
|
||||
{"title": "Ravage"},
|
||||
{"title": "La nuit des temps"},
|
||||
{"title": "Le grand secret"},
|
||||
],
|
||||
},
|
||||
],
|
||||
'authors' : [
|
||||
{'name' : 'Saint-Exupery', 'books' : [
|
||||
{'title' : 'Le petit prince'},
|
||||
{'title' : "L'aviateur"},
|
||||
{'title' : 'Vol de nuit'},
|
||||
]},
|
||||
{'name' : 'Barjavel', 'books' : [
|
||||
{'title' : 'Ravage'},
|
||||
{'title' : "La nuit des temps"},
|
||||
{'title' : 'Le grand secret'},
|
||||
]},
|
||||
]
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save('test_files/nested_for.docx')
|
||||
tpl.save("output/nested_for.docx")
|
||||
|
||||
@ -1,26 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
"""
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
'''
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl=DocxTemplate('test_files/order_tpl.docx')
|
||||
tpl = DocxTemplate("templates/order_tpl.docx")
|
||||
|
||||
context = {
|
||||
'customer_name' : 'Eric',
|
||||
'items' : [
|
||||
{'desc' : 'Python interpreters', 'qty' : 2, 'price' : 'FREE' },
|
||||
{'desc' : 'Django projects', 'qty' : 5403, 'price' : 'FREE' },
|
||||
{'desc' : 'Guido', 'qty' : 1, 'price' : '100,000,000.00' },
|
||||
],
|
||||
'in_europe' : True,
|
||||
'is_paid': False,
|
||||
'company_name' : 'The World Wide company',
|
||||
'total_price' : '100,000,000.00'
|
||||
context = {
|
||||
"customer_name": "Eric",
|
||||
"items": [
|
||||
{"desc": "Python interpreters", "qty": 2, "price": "FREE"},
|
||||
{"desc": "Django projects", "qty": 5403, "price": "FREE"},
|
||||
{"desc": "Guido", "qty": 1, "price": "100,000,000.00"},
|
||||
],
|
||||
"in_europe": True,
|
||||
"is_paid": False,
|
||||
"company_name": "The World Wide company",
|
||||
"total_price": "100,000,000.00",
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save('test_files/order.docx')
|
||||
tpl.save("output/order.docx")
|
||||
|
||||
14
tests/preserve_spaces.py
Normal file
@ -0,0 +1,14 @@
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
# With old docxtpl version, "... for spicy ..." was replaced by "... forspicy..."
|
||||
# This test is for checking that is some cases the spaces are not lost anymore
|
||||
|
||||
tpl = DocxTemplate("templates/preserve_spaces_tpl.docx")
|
||||
|
||||
tags = ["tag_1", "tag_2"]
|
||||
replacement = ["looking", "too"]
|
||||
|
||||
context = dict(zip(tags, replacement))
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/preserve_spaces.docx")
|
||||
18
tests/replace_picture.py
Normal file
@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2017-09-03
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
DEST_FILE = "output/replace_picture.docx"
|
||||
|
||||
tpl = DocxTemplate("templates/replace_picture_tpl.docx")
|
||||
|
||||
context = {}
|
||||
|
||||
tpl.replace_pic("python_logo.png", "templates/python.png")
|
||||
tpl.render(context)
|
||||
tpl.save(DEST_FILE)
|
||||
@ -1,36 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
"""
|
||||
Created : 2015-03-26
|
||||
|
||||
@author: Eric Lapouyade
|
||||
'''
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate, RichText
|
||||
|
||||
tpl=DocxTemplate('test_files/richtext_tpl.docx')
|
||||
tpl = DocxTemplate("templates/richtext_tpl.docx")
|
||||
|
||||
rt = RichText('an exemple of ')
|
||||
rt.add('a rich text', style='myrichtextstyle')
|
||||
rt.add(' with ')
|
||||
rt.add('some italic', italic=True)
|
||||
rt.add(' and ')
|
||||
rt.add('some violet', color='#ff00ff')
|
||||
rt.add(' and ')
|
||||
rt.add('some striked', strike=True)
|
||||
rt.add(' and ')
|
||||
rt.add('some small', size=14)
|
||||
rt.add(' or ')
|
||||
rt.add('big', size=60)
|
||||
rt.add(' text.')
|
||||
rt.add(' Et voilà ! ')
|
||||
rt.add('\n1st line')
|
||||
rt.add('\n2nd line')
|
||||
rt.add('\n3rd line')
|
||||
rt.add('\n\n<cool>')
|
||||
rt = RichText()
|
||||
rt.add("a rich text", style="myrichtextstyle")
|
||||
rt.add(" with ")
|
||||
rt.add("some italic", italic=True)
|
||||
rt.add(" and ")
|
||||
rt.add("some violet", color="#ff00ff")
|
||||
rt.add(" and ")
|
||||
rt.add("some striked", strike=True)
|
||||
rt.add(" and ")
|
||||
rt.add("some Highlighted", highlight="#ffff00")
|
||||
rt.add(" and ")
|
||||
rt.add("some small", size=14)
|
||||
rt.add(" or ")
|
||||
rt.add("big", size=60)
|
||||
rt.add(" text.")
|
||||
rt.add("\nYou can add an hyperlink, here to ")
|
||||
rt.add("google", url_id=tpl.build_url_id("http://google.com"))
|
||||
rt.add("\nEt voilà ! ")
|
||||
rt.add("\n1st line")
|
||||
rt.add("\n2nd line")
|
||||
rt.add("\n3rd line")
|
||||
rt.add("\aA new paragraph : <cool>\a")
|
||||
rt.add("--- A page break here (see next page) ---\f")
|
||||
|
||||
for ul in [
|
||||
"single",
|
||||
"double",
|
||||
"thick",
|
||||
"dotted",
|
||||
"dash",
|
||||
"dotDash",
|
||||
"dotDotDash",
|
||||
"wave",
|
||||
]:
|
||||
rt.add("\nUnderline : " + ul + " \n", underline=ul)
|
||||
rt.add("\nFonts :\n", underline=True)
|
||||
rt.add("Arial\n", font="Arial")
|
||||
rt.add("Courier New\n", font="Courier New")
|
||||
rt.add("Times New Roman\n", font="Times New Roman")
|
||||
rt.add("\n\nHere some")
|
||||
rt.add("superscript", superscript=True)
|
||||
rt.add(" and some")
|
||||
rt.add("subscript", subscript=True)
|
||||
|
||||
rt_embedded = RichText("an example of ")
|
||||
rt_embedded.add(rt)
|
||||
|
||||
context = {
|
||||
'example' : rt,
|
||||
"example": rt_embedded,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save('test_files/richtext.docx')
|
||||
tpl.save("output/richtext.docx")
|
||||
|
||||
@ -1,18 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
"""
|
||||
Created : 2015-03-26
|
||||
|
||||
@author: Eric Lapouyade
|
||||
'''
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate, RichText
|
||||
|
||||
tpl=DocxTemplate('test_files/richtext_and_if_tpl.docx')
|
||||
tpl = DocxTemplate("templates/richtext_and_if_tpl.docx")
|
||||
|
||||
|
||||
context = {
|
||||
'foobar': RichText('Foobar!', color='ff0000')
|
||||
}
|
||||
context = {"foobar": RichText("Foobar!", color="ff0000")}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save('test_files/richtext_and_if.docx')
|
||||
tpl.save("output/richtext_and_if.docx")
|
||||
|
||||
21
tests/richtext_eastAsia.py
Normal file
@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created : 2022-08-03
|
||||
@author: Dongfang Song
|
||||
"""
|
||||
|
||||
|
||||
from docxtpl import DocxTemplate, RichText
|
||||
|
||||
tpl = DocxTemplate("templates/richtext_eastAsia_tpl.docx")
|
||||
rt = RichText("测试TEST", font="eastAsia:Microsoft YaHei")
|
||||
ch = RichText("测试TEST", font="eastAsia:微软雅黑")
|
||||
sun = RichText("测试TEST", font="eastAsia:SimSun")
|
||||
context = {
|
||||
"example": rt,
|
||||
"Chinese": ch,
|
||||
"simsun": sun,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/richtext_eastAsia.docx")
|
||||
63
tests/richtextparagraph.py
Normal file
@ -0,0 +1,63 @@
|
||||
"""
|
||||
Created : 2025-02-28
|
||||
|
||||
@author: Hannah Imrie
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate, RichText, RichTextParagraph
|
||||
|
||||
tpl = DocxTemplate("templates/richtext_paragraph_tpl.docx")
|
||||
|
||||
rtp = RichTextParagraph()
|
||||
rt = RichText()
|
||||
|
||||
rtp.add(
|
||||
"The rich text paragraph function allows paragraph styles to be added to text",
|
||||
parastyle="myrichparastyle",
|
||||
)
|
||||
rtp.add("Any built in paragraph style can be used", parastyle="IntenseQuote")
|
||||
rtp.add(
|
||||
"or you can add your own, unlocking all style options", parastyle="createdStyle"
|
||||
)
|
||||
rtp.add(
|
||||
"To use, just create a style in your template word doc with the formatting you want "
|
||||
"and call it in the code.",
|
||||
parastyle="normal",
|
||||
)
|
||||
|
||||
rtp.add("This allows for the use of")
|
||||
rtp.add("custom bullet\apoints", parastyle="SquareBullet")
|
||||
rtp.add("Numbered Bullet Points", parastyle="BasicNumbered")
|
||||
rtp.add("and Alpha Bullet Points.", parastyle="alphaBracketNumbering")
|
||||
rtp.add("You can", parastyle="normal")
|
||||
rtp.add("set the", parastyle="centerAlign")
|
||||
rtp.add("text alignment", parastyle="rightAlign")
|
||||
rtp.add(
|
||||
"as well as the spacing between lines of text. Like this for example, "
|
||||
"this text has very tight spacing between the lines.\aIt also has no space between "
|
||||
"paragraphs of the same style.",
|
||||
parastyle="TightLineSpacing",
|
||||
)
|
||||
rtp.add(
|
||||
"Unlike this one, which has extra large spacing between lines for when you want to "
|
||||
"space things out a bit or just write a little less.",
|
||||
parastyle="WideLineSpacing",
|
||||
)
|
||||
rtp.add(
|
||||
"You can also set the background colour of a line.", parastyle="LineShadingGreen"
|
||||
)
|
||||
|
||||
rt.add("This works with ")
|
||||
rt.add("Rich ", bold=True)
|
||||
rt.add("Text ", italic=True)
|
||||
rt.add("Strings", underline="single")
|
||||
rt.add(" too.")
|
||||
|
||||
rtp.add(rt, parastyle="SquareBullet")
|
||||
|
||||
context = {
|
||||
"example": rtp,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/richtext_paragraph.docx")
|
||||
@ -1,13 +1,17 @@
|
||||
import subprocess
|
||||
import glob
|
||||
import six
|
||||
import os
|
||||
|
||||
tests = sorted(glob.glob('[A-Za-z]*.py'))
|
||||
excludes = ['runtests.py']
|
||||
tests = sorted(glob.glob("[A-Za-z]*.py"))
|
||||
excludes = ["runtests.py"]
|
||||
|
||||
output_dir = os.path.join(os.path.dirname(__file__), "output")
|
||||
if not os.path.exists(output_dir):
|
||||
os.mkdir(output_dir)
|
||||
|
||||
for test in tests:
|
||||
if test not in excludes:
|
||||
six.print_('%s ...' % test)
|
||||
subprocess.call(['python','./%s' % test])
|
||||
print("%s ..." % test)
|
||||
subprocess.call(["python", "./%s" % test])
|
||||
|
||||
six.print_('Done.')
|
||||
print("Done.")
|
||||
|
||||
@ -1,38 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
"""
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
'''
|
||||
"""
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
from docx.shared import Inches
|
||||
|
||||
tpl=DocxTemplate('test_files/subdoc_tpl.docx')
|
||||
tpl = DocxTemplate("templates/subdoc_tpl.docx")
|
||||
|
||||
sd = tpl.new_subdoc()
|
||||
p = sd.add_paragraph('This is a sub-document inserted into a bigger one')
|
||||
p = sd.add_paragraph('It has been ')
|
||||
p.add_run('dynamically').style = 'dynamic'
|
||||
p.add_run(' generated with python by using ')
|
||||
p.add_run('python-docx').italic = True
|
||||
p.add_run(' library')
|
||||
p = sd.add_paragraph("This is a sub-document inserted into a bigger one")
|
||||
p = sd.add_paragraph("It has been ")
|
||||
p.add_run("dynamically").style = "dynamic"
|
||||
p.add_run(" generated with python by using ")
|
||||
p.add_run("python-docx").italic = True
|
||||
p.add_run(" library")
|
||||
|
||||
sd.add_heading('Heading, level 1', level=1)
|
||||
sd.add_paragraph('This is an Intense quote', style='IntenseQuote')
|
||||
sd.add_heading("Heading, level 1", level=1)
|
||||
sd.add_paragraph("This is an Intense quote", style="IntenseQuote")
|
||||
|
||||
sd.add_paragraph('A picture :')
|
||||
sd.add_picture('test_files/python_logo.png', width=Inches(1.25))
|
||||
sd.add_paragraph("A picture :")
|
||||
sd.add_picture("templates/python_logo.png", width=Inches(1.25))
|
||||
|
||||
sd.add_paragraph('A Table :')
|
||||
sd.add_paragraph("A Table :")
|
||||
table = sd.add_table(rows=1, cols=3)
|
||||
hdr_cells = table.rows[0].cells
|
||||
hdr_cells[0].text = 'Qty'
|
||||
hdr_cells[1].text = 'Id'
|
||||
hdr_cells[2].text = 'Desc'
|
||||
recordset=( (1,101,'Spam'),
|
||||
(2,42,'Eggs'),
|
||||
(3,631,'Spam,spam, eggs, and ham') )
|
||||
hdr_cells[0].text = "Qty"
|
||||
hdr_cells[1].text = "Id"
|
||||
hdr_cells[2].text = "Desc"
|
||||
recordset = ((1, 101, "Spam"), (2, 42, "Eggs"), (3, 631, "Spam,spam, eggs, and ham"))
|
||||
for item in recordset:
|
||||
row_cells = table.add_row().cells
|
||||
row_cells[0].text = str(item[0])
|
||||
@ -40,8 +38,8 @@ for item in recordset:
|
||||
row_cells[2].text = item[2]
|
||||
|
||||
context = {
|
||||
'mysubdoc' : sd,
|
||||
"mysubdoc": sd,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save('test_files/subdoc.docx')
|
||||
tpl.save("output/subdoc.docx")
|
||||
|
||||
19
tests/template_error.py
Normal file
@ -0,0 +1,19 @@
|
||||
from docxtpl import DocxTemplate
|
||||
from jinja2.exceptions import TemplateError
|
||||
|
||||
print("=" * 80)
|
||||
print("Generating template error for testing (so it is safe to ignore) :")
|
||||
print("." * 80)
|
||||
try:
|
||||
tpl = DocxTemplate("templates/template_error_tpl.docx")
|
||||
tpl.render({"test_variable": "test variable value"})
|
||||
except TemplateError as the_error:
|
||||
print(str(the_error))
|
||||
if hasattr(the_error, "docx_context"):
|
||||
print("Context:")
|
||||
for line in the_error.docx_context:
|
||||
print(line)
|
||||
tpl.save("output/template_error.docx")
|
||||
print("." * 80)
|
||||
print(" End of TemplateError Test ")
|
||||
print("=" * 80)
|
||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
BIN
tests/templates/comments_tpl.docx
Normal file
BIN
tests/templates/custom_jinja_filters_tpl.docx
Normal file
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
BIN
tests/templates/doc_properties_tpl.docx
Normal file
BIN
tests/templates/dummy_Excel.xlsx
Normal file
BIN
tests/templates/dummy_PowerPoint.pptx
Normal file
BIN
tests/templates/dummy_pic_for_header.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
tests/templates/embedded_dummy.docx
Normal file
BIN
tests/templates/embedded_dummy2.docx
Normal file
BIN
tests/templates/embedded_embedded_docx_tpl.docx
Normal file
BIN
tests/templates/embedded_main_tpl.docx
Normal file
BIN
tests/templates/embedded_static_docx.docx
Normal file
BIN
tests/templates/escape_tpl.docx
Normal file
BIN
tests/templates/escape_tpl_auto.docx
Normal file
BIN
tests/templates/footnotes_tpl.docx
Normal file
BIN
tests/templates/get_undeclared_variables.docx
Normal file
BIN
tests/templates/header_footer_image_tpl.docx
Normal file
BIN
tests/templates/header_footer_inline_image_tpl.docx
Normal file
BIN
tests/templates/horizontal_merge_tpl.docx
Normal file
BIN
tests/templates/less_cells_after_loop_tpl.docx
Normal file
BIN
tests/templates/merge_docx_master_tpl.docx
Normal file
BIN
tests/templates/merge_paragraph_tpl.docx
Normal file
8
tests/templates/module_execute.json
Normal file
@ -0,0 +1,8 @@
|
||||
{"json_dict_var" : {"json_dict_var":"successfully inserted"},
|
||||
"json_array_var": ["json","array","var","successfully", "inserted"],
|
||||
"json_string_var":"json_string_var successfully inserted",
|
||||
"json_int_var":123,
|
||||
"json_float_var":1.234,
|
||||
"json_true_var":true,
|
||||
"json_false_var":false,
|
||||
"json_none_var":null}
|
||||
BIN
tests/templates/module_execute_tpl.docx
Normal file
BIN
tests/templates/multi_rendering_tpl.docx
Normal file
BIN
tests/templates/preserve_spaces_tpl.docx
Normal file
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
BIN
tests/templates/python.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 176 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |