Compare commits
No commits in common. "master" and "revert-307-develop" have entirely different histories.
master
...
revert-307
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,23 +0,0 @@
|
||||
---
|
||||
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
20
.github/ISSUE_TEMPLATE/help-request.md
vendored
@ -1,20 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@ -1,20 +0,0 @@
|
||||
---
|
||||
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.
|
||||
4
.github/workflows/codestyle.yml
vendored
4
.github/workflows/codestyle.yml
vendored
@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
|
||||
python-version: [2.7, 3.5, 3.6, 3.7, 3.8]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@ -22,4 +22,4 @@ jobs:
|
||||
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
|
||||
flake8 . --count --max-line-length=127 --show-source --statistics
|
||||
@ -1,35 +0,0 @@
|
||||
# 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
|
||||
123
CHANGES.rst
123
CHANGES.rst
@ -1,117 +1,9 @@
|
||||
0.20.2 *(Unreleased)*
|
||||
-------------------
|
||||
- Move docxcompose to optional dependency (Thanks to Waket Zheng)
|
||||
|
||||
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)
|
||||
0.10.1 (2020-08-23)
|
||||
-------------------
|
||||
- Remove extension testing (#297)
|
||||
|
||||
0.10.0 (2020-05-25)
|
||||
-------------------
|
||||
- Fix spaces missing in some cases (#116, #227)
|
||||
|
||||
0.9.2 (2020-04-26)
|
||||
@ -119,9 +11,16 @@
|
||||
- Fix #271
|
||||
- Code styling
|
||||
|
||||
0.9.0 (2020-04-15)
|
||||
-------------------
|
||||
- New syntax : {%- and -%} to merge lines/paragraphs
|
||||
|
||||
0.8.1 (2020-04-14)
|
||||
-------------------
|
||||
- fix #266
|
||||
|
||||
0.8.0 (2020-04-10)
|
||||
-------------------
|
||||
- docxtpl is now able to use latest python-docx (0.8.10). Thanks to Dutchy-@github.
|
||||
|
||||
0.7.0 (2020-04-09)
|
||||
|
||||
11
Pipfile
11
Pipfile
@ -1,13 +1,12 @@
|
||||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
url = "https://pypi.python.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[dev-packages]
|
||||
"e1839a8" = {path = ".", editable = true}
|
||||
|
||||
[packages]
|
||||
|
||||
[dev-packages]
|
||||
docxtpl = {editable = true, path = "."}
|
||||
flake8 = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3"
|
||||
python_version = "3.6"
|
||||
|
||||
452
Pipfile.lock
generated
452
Pipfile.lock
generated
@ -1,419 +1,115 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "0368a5e08ceb2b4910a110742515b5ff1d04a3a3af2b91b49d922ef9aaab6915"
|
||||
"sha256": "a1c6cad5b879bb41a157c05cab3c9186e01c852c4ecf996f9e92a5541d905ca2"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3"
|
||||
"python_version": "3.6"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"url": "https://pypi.python.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": {
|
||||
"e1839a8": {
|
||||
"editable": true,
|
||||
"path": "."
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343",
|
||||
"sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.2.0"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d",
|
||||
"sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"
|
||||
"sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250",
|
||||
"sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.1.6"
|
||||
"version": "==2.11.1"
|
||||
},
|
||||
"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"
|
||||
"sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd",
|
||||
"sha256:0701f7965903a1c3f6f09328c1278ac0eee8f56f244e66af79cb224b7ef3801c",
|
||||
"sha256:1f2c4ec372bf1c4a2c7e4bb20845e8bcf8050365189d86806bad1e3ae473d081",
|
||||
"sha256:4235bc124fdcf611d02047d7034164897ade13046bda967768836629bc62784f",
|
||||
"sha256:5828c7f3e615f3975d48f40d4fe66e8a7b25f16b5e5705ffe1d22e43fb1f6261",
|
||||
"sha256:585c0869f75577ac7a8ff38d08f7aac9033da2c41c11352ebf86a04652758b7a",
|
||||
"sha256:5d467ce9c5d35b3bcc7172c06320dddb275fea6ac2037f72f0a4d7472035cea9",
|
||||
"sha256:63dbc21efd7e822c11d5ddbedbbb08cd11a41e0032e382a0fd59b0b08e405a3a",
|
||||
"sha256:7bc1b221e7867f2e7ff1933165c0cec7153dce93d0cdba6554b42a8beb687bdb",
|
||||
"sha256:8620ce80f50d023d414183bf90cc2576c2837b88e00bea3f33ad2630133bbb60",
|
||||
"sha256:8a0ebda56ebca1a83eb2d1ac266649b80af8dd4b4a3502b2c1e09ac2f88fe128",
|
||||
"sha256:90ed0e36455a81b25b7034038e40880189169c308a3df360861ad74da7b68c1a",
|
||||
"sha256:95e67224815ef86924fbc2b71a9dbd1f7262384bca4bc4793645794ac4200717",
|
||||
"sha256:afdb34b715daf814d1abea0317b6d672476b498472f1e5aacbadc34ebbc26e89",
|
||||
"sha256:b4b2c63cc7963aedd08a5f5a454c9f67251b1ac9e22fd9d72836206c42dc2a72",
|
||||
"sha256:d068f55bda3c2c3fcaec24bd083d9e2eede32c583faf084d6e4b9daaea77dde8",
|
||||
"sha256:d5b3c4b7edd2e770375a01139be11307f04341ec709cf724e0f26ebb1eef12c3",
|
||||
"sha256:deadf4df349d1dcd7b2853a2c8796593cc346600726eff680ed8ed11812382a7",
|
||||
"sha256:df533af6f88080419c5a604d0d63b2c33b1c0c4409aba7d0cb6de305147ea8c8",
|
||||
"sha256:e4aa948eb15018a657702fee0b9db47e908491c64d36b4a90f59a64741516e77",
|
||||
"sha256:e5d842c73e4ef6ed8c1bd77806bf84a7cb535f9c0cf9b2c74d02ebda310070e1",
|
||||
"sha256:ebec08091a22c2be870890913bdadd86fcd8e9f0f22bcb398abd3af914690c15",
|
||||
"sha256:edc15fcfd77395e24543be48871c251f38132bb834d9fdfdad756adb6ea37679",
|
||||
"sha256:f2b74784ed7e0bc2d02bd53e48ad6ba523c9b36c194260b7a5045071abbb1012",
|
||||
"sha256:fa071559f14bd1e92077b1b5f6c22cf09756c6de7139370249eb372854ce51e6",
|
||||
"sha256:fd52e796fee7171c4361d441796b64df1acfceb51f29e545e812f16d023c4bbc",
|
||||
"sha256:fe976a0f1ef09b3638778024ab9fb8cde3118f203364212c198f71341c0715ca"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.4.0"
|
||||
"version": "==4.5.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"
|
||||
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
|
||||
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
||||
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
||||
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
||||
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
|
||||
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
||||
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
||||
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
||||
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
||||
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
||||
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
||||
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
|
||||
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
||||
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
|
||||
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
||||
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
||||
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
||||
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
|
||||
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
|
||||
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
|
||||
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
|
||||
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
|
||||
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
|
||||
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
|
||||
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
|
||||
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
|
||||
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
|
||||
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
||||
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
||||
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
||||
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
|
||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
|
||||
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
|
||||
],
|
||||
"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"
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"python-docx": {
|
||||
"hashes": [
|
||||
"sha256:08c20d6058916fb19853fcf080f7f42b6270d89eac9fa5f8c15f691c0017fabe",
|
||||
"sha256:0cf1f22e95b9002addca7948e16f2cd7acdfd498047f1941ca5d293db7762efd"
|
||||
"sha256:bc76ecac6b2d00ce6442a69d03a6f35c71cd72293cd8405a7472dfe317920024"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.1.2"
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:2e308396e1d83de287ada2c2fd6e64286008fe6aca5008e0b6a8cb0e2c86eedd",
|
||||
"sha256:ea0e7655c05b74819f82e76e11a85b31779fee7c4969e82f72bab0664e8317e4"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==80.1.0"
|
||||
"version": "==0.8.10"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274",
|
||||
"sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
],
|
||||
"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"
|
||||
"version": "==1.14.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,6 +21,10 @@ 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.
|
||||
|
||||
Share
|
||||
-----
|
||||
|
||||
If you like this project, please rate and share it here : http://rate.re/github/elapouya/python-docx-template
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
@ -30,9 +34,8 @@ 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 :
|
||||
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
|
||||
|
||||
|
||||
56
docs/conf.py
56
docs/conf.py
@ -26,33 +26,33 @@
|
||||
# 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'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = "index"
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = "python-docx-template"
|
||||
copyright = "2015, Eric Lapouyade"
|
||||
project = u'python-docx-template'
|
||||
copyright = u'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.20"
|
||||
version = '0.9'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = "0.20.x"
|
||||
release = '0.9.x'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@ -66,7 +66,7 @@ release = "0.20.x"
|
||||
|
||||
# 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.
|
||||
@ -84,7 +84,7 @@ exclude_patterns = ["_build"]
|
||||
# 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 = []
|
||||
@ -97,7 +97,7 @@ pygments_style = "sphinx"
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = "sphinx_book_theme"
|
||||
html_theme = 'default'
|
||||
|
||||
# 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
|
||||
@ -126,7 +126,7 @@ html_theme = "sphinx_book_theme"
|
||||
# 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
|
||||
@ -175,7 +175,7 @@ html_static_path = ["_static"]
|
||||
# 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 ---------------------------------------------
|
||||
@ -193,13 +193,8 @@ latex_elements = {
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(
|
||||
"index",
|
||||
"python-docx-template.tex",
|
||||
"python-docx-template Documentation",
|
||||
"Eric Lapouyade",
|
||||
"manual",
|
||||
),
|
||||
('index', 'python-docx-template.tex', u'python-docx-template Documentation',
|
||||
u'Eric Lapouyade', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@ -228,13 +223,8 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(
|
||||
"index",
|
||||
"python-docx-template",
|
||||
"python-docx-template Documentation",
|
||||
["Eric Lapouyade"],
|
||||
1,
|
||||
)
|
||||
('index', 'python-docx-template', u'python-docx-template Documentation',
|
||||
[u'Eric Lapouyade'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
@ -247,15 +237,9 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(
|
||||
"index",
|
||||
"python-docx-template",
|
||||
"python-docx-template Documentation",
|
||||
"Eric Lapouyade",
|
||||
"python-docx-template",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
),
|
||||
('index', 'python-docx-template', u'python-docx-template Documentation',
|
||||
u'Eric Lapouyade', 'python-docx-template', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
|
||||
289
docs/index.rst
289
docs/index.rst
@ -8,14 +8,10 @@ Welcome to python-docx-template's documentation!
|
||||
|
||||
.. rubric:: Quickstart
|
||||
|
||||
To install using pip::
|
||||
To install::
|
||||
|
||||
pip install docxtpl
|
||||
|
||||
or using conda::
|
||||
|
||||
conda install docxtpl --channel conda-forge
|
||||
|
||||
Usage::
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
@ -42,6 +38,8 @@ 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
|
||||
------------------
|
||||
|
||||
@ -62,102 +60,51 @@ 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).
|
||||
|
||||
**Important:**
|
||||
|
||||
Always put space after a jinja2 starting var/tag delimiter and a space before the ending one :
|
||||
|
||||
Avoid::
|
||||
|
||||
{{myvariable}}
|
||||
{%if something%}
|
||||
|
||||
Use instead::
|
||||
|
||||
{{ myvariable }}
|
||||
{% if something %}
|
||||
|
||||
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 (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 %}
|
||||
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 tell python-docx-template to **remove** the paragraph, table row, table column or run where the begin and ending tags are located and only takes care about what is in between.
|
||||
|
||||
**IMPORTANT** : Do not use ``{%p``, ``{%tr``, ``{%tc`` or ``{%r`` twice in the same
|
||||
paragraph, row, column or run. Example :
|
||||
|
||||
Do not use this::
|
||||
Do not use this ::
|
||||
|
||||
{%p if display_paragraph %}Here is my paragraph {%p endif %}
|
||||
|
||||
But use this instead in your docx template::
|
||||
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
|
||||
.................
|
||||
|
||||
As part of jinja2, one can used double braces::
|
||||
|
||||
{{ <var> }}
|
||||
|
||||
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 opening braces.
|
||||
|
||||
**VERY IMPORTANT :** Variables must not contains characters like ``<``, ``>`` and ``&`` unless using Escaping_
|
||||
|
||||
**IMPORTANT :** Always put space after a starting var delimiter and a space before the ending one :
|
||||
|
||||
Avoid::
|
||||
|
||||
{{myvariable}}
|
||||
{{rmyrichtext}}
|
||||
|
||||
Use instead::
|
||||
|
||||
{{ 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.
|
||||
This syntax is possible because MS Word considers each line as a new paragraph and
|
||||
``{%p`` tags are not in the same paragraph in the second case.
|
||||
|
||||
Split and merge text
|
||||
....................
|
||||
@ -183,35 +130,31 @@ One can use *ENTER* or *SHIFT+ENTER* to split a text like below, then use ``{%-`
|
||||
|
||||
**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::
|
||||
Display variables
|
||||
.................
|
||||
|
||||
{_%, %_}, {_{ or }_}
|
||||
As part of jinja2, one can used double braces::
|
||||
|
||||
Tables
|
||||
......
|
||||
{{ <var> }}
|
||||
|
||||
Spanning
|
||||
~~~~~~~~
|
||||
But if ``<var>`` is a RichText_ object, you must specify that you are changing the actual 'run'::
|
||||
|
||||
You can span table cells horizontally in two ways, by using ``colspan`` tag (see tests/dynamic_table.py)::
|
||||
{{r <var> }}
|
||||
|
||||
{% colspan <var> %}
|
||||
Note the ``r`` right after the openning braces.
|
||||
|
||||
`<var>` must contain an integer for the number of columns to span. See tests/test_files/dynamic_table.py for an example.
|
||||
**IMPORTANT** : Do not use the ``r`` variable in your template because ``{{r}}`` could be interpreted as a ``{{r``
|
||||
without variable specified. Nevertheless, you can use a bigger variable name starting
|
||||
with 'r'. For example ``{{render_color}}`` will be interpreted as ``{{ render_color }}`` not as ``{{r ender_color}}``.
|
||||
|
||||
You can also span horizontally within a for loop (see tests/horizontal_merge.py)::
|
||||
**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.
|
||||
|
||||
{% hm %}
|
||||
|
||||
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::
|
||||
|
||||
@ -219,6 +162,24 @@ There is a special case when you want to change the background color of a table
|
||||
|
||||
`<var>` must contain the color's hexadecimal code *without* the hash sign
|
||||
|
||||
Column spanning
|
||||
...............
|
||||
|
||||
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::
|
||||
|
||||
{% 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
|
||||
........
|
||||
|
||||
In order to display ``{%``, ``%}``, ``{{`` or ``}}``, one can use::
|
||||
|
||||
{_%, %_}, {_{ or }_}
|
||||
|
||||
|
||||
.. _RichText:
|
||||
|
||||
RichText
|
||||
@ -227,31 +188,14 @@ 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, font and so on, but the best way is to use Microsoft Word to define your own *character* style
|
||||
You can change color, bold, italic, size 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()``
|
||||
|
||||
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
|
||||
+++++++++++++++++++++++
|
||||
|
||||
@ -263,61 +207,28 @@ You can add an hyperlink to a text by using a Richtext with this syntax::
|
||||
|
||||
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::
|
||||
|
||||
myimage = InlineImage(tpl, image_descriptor='test_files/python_logo.png', width=Mm(20), height=Mm(10))
|
||||
myimage = InlineImage(tpl,'test_files/python_logo.png',width=Mm(20))
|
||||
|
||||
You just have to specify the template object, the image file path and optionally width and/or height.
|
||||
You just have to specify the template object, the image file path and optionnally 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
|
||||
-------------
|
||||
|
||||
> Need to install with the subdoc extra: `pip install "docxtpl[subdoc]"`
|
||||
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`.
|
||||
|
||||
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`.
|
||||
Escaping, newline, new paragraph, Listing
|
||||
-----------------------------------------
|
||||
|
||||
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,
|
||||
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 4 ways :
|
||||
|
||||
* ``context = { 'var':R('my text') }`` and ``{{r <var> }}`` in the template (note the ``r``),
|
||||
@ -325,9 +236,12 @@ especially ``<``, ``>`` and ``&``. In order to use them, you must escape them. T
|
||||
* ``context = { 'var':escape('my text')}`` and ``{{ <var> }}`` in the template.
|
||||
* enable autoescaping when calling render method: ``tpl.render(context, autoescape=True)`` (default is autoescape=False)
|
||||
|
||||
The ``RichText()`` or ``R()`` offers newline, new paragraph, and page break features : just use ``\n``, ``\a``, or ``\f`` in the
|
||||
text, they will be converted accordingly.
|
||||
|
||||
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``, ``\a``, and ``\f``
|
||||
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::
|
||||
@ -335,7 +249,6 @@ 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
|
||||
@ -381,10 +294,10 @@ It works like medias replacement, except it is for embedded objects like embedde
|
||||
|
||||
Syntax to replace embedded_dummy.docx::
|
||||
|
||||
tpl.replace_embedded('embedded_dummy.docx','embedded_docx_i_want.docx')
|
||||
tpl.replace_embedded('embdded_dummy.docx','embdded_docx_i_want.docx')
|
||||
|
||||
|
||||
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
|
||||
WARNING : unlike replace_pic() method, embdded_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.
|
||||
|
||||
@ -401,26 +314,6 @@ The zipname starts with "word/embeddings/". Note that the file to be replaced is
|
||||
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
|
||||
@ -428,7 +321,7 @@ 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.
|
||||
beginning with a jinja2 tag provinding spaces : They will be ignored.
|
||||
To solve these problem, the solution is to use Richtext::
|
||||
|
||||
tpl.render({
|
||||
@ -441,11 +334,25 @@ And in your template, use the {{r notation::
|
||||
{{r test_space_r}} Spaces will be preserved
|
||||
{{r test_tabs_r}} Tabs will be displayed
|
||||
|
||||
Tables
|
||||
------
|
||||
|
||||
You can span table cells horizontally in two ways, by using ``colspan`` tag (see tests/dynamic_table.py)::
|
||||
|
||||
{% colspan <number of column to span> %}
|
||||
|
||||
or within a for loop (see tests/horizontal_merge.py)::
|
||||
|
||||
{% hm %}
|
||||
|
||||
You can also merge cells vertically within a for loop (see tests/vertical_merge.py)::
|
||||
|
||||
{% vm %}
|
||||
|
||||
Jinja custom filters
|
||||
--------------------
|
||||
|
||||
``render()`` accepts ``jinja_env`` optional argument : you may pass a jinja environment object.
|
||||
``render()`` accepts ``jinja_env`` optionnal 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
|
||||
@ -465,30 +372,6 @@ 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
|
||||
--------
|
||||
|
||||
|
||||
@ -1,18 +1,805 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
import functools
|
||||
import io
|
||||
|
||||
__version__ = "0.20.1"
|
||||
__version__ = '0.10.2'
|
||||
|
||||
# flake8: noqa
|
||||
from .inline_image import InlineImage
|
||||
from .listing import Listing
|
||||
from .richtext import RichText, R, RichTextParagraph, RP
|
||||
from .template import DocxTemplate
|
||||
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 .subdoc import Subdoc
|
||||
from html import escape
|
||||
except ImportError:
|
||||
pass
|
||||
# cgi.escape is deprecated in python 3.7
|
||||
from cgi import escape
|
||||
import re
|
||||
import six
|
||||
import binascii
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
NEWLINE_XML = '</w:t><w:br/><w:t xml:space="preserve">'
|
||||
NEWPARAGRAPH_XML = '</w:t></w:r></w:p><w:p><w:r><w:t xml:space="preserve">'
|
||||
TAB_XML = '</w:t></w:r><w:r><w:tab/></w:r><w:r><w:t xml:space="preserve">'
|
||||
PAGE_BREAK = '</w:t><w:br w:type="page"/><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.zipname_to_replace = {}
|
||||
self.pic_to_replace = {}
|
||||
self.pic_map = {}
|
||||
|
||||
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):
|
||||
""" Make a lots of cleanning 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 %} )
|
||||
src_xml = re.sub(r'(?<={)(<[^>]*>)+(?=[\{%])|(?<=[%\}])(<[^>]*>)+(?=\})', '',
|
||||
src_xml, flags=re.DOTALL)
|
||||
|
||||
# replace {{<some tags>jinja2 stuff<some other tags>}} by {{jinja2 stuff}}
|
||||
# same thing with {% ... %}
|
||||
# "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 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)
|
||||
|
||||
# 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(u'“', u'"')
|
||||
.replace(u'”', u'"')
|
||||
.replace(u"‘", u"'")
|
||||
.replace(u"’", u"'"))
|
||||
src_xml = re.sub(r'(?<=\{[\{%])(.*?)(?=[\}%]})', clean_tags, src_xml)
|
||||
|
||||
return src_xml
|
||||
|
||||
def render_xml(self, src_xml, context, jinja_env=None):
|
||||
src_xml = src_xml.replace(r'<w:p>', '\n<w:p>')
|
||||
try:
|
||||
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)])
|
||||
raise exc
|
||||
dst_xml = dst_xml.replace('\n<w:p>', '<w:p>')
|
||||
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) and (val.target_part.blob):
|
||||
yield relKey, self.xml_to_string(parse_xml(val.target_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, 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):
|
||||
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, jinja_env=None, autoescape=False):
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
# 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 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') 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 new_subdoc(self, docpath=None):
|
||||
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.pic_to_replace[embedded_file] = dst_file.read()
|
||||
else:
|
||||
with open(dst_file, 'rb') as fh:
|
||||
self.pic_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 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.pic_to_replace:
|
||||
self.build_pic_map()
|
||||
|
||||
# Do the actual replacement
|
||||
for embedded_file, stream in six.iteritems(self.pic_to_replace):
|
||||
if embedded_file not in self.pic_map:
|
||||
raise ValueError('Picture "%s" not found in the docx template'
|
||||
% embedded_file)
|
||||
self.pic_map[embedded_file][1]._blob = stream
|
||||
|
||||
def build_pic_map(self):
|
||||
"""Searches in docx template all the xml pictures tag and store them
|
||||
in pic_map dict"""
|
||||
if self.pic_to_replace:
|
||||
# Main document
|
||||
part = self.docx.part
|
||||
self.pic_map.update(self._img_filename_to_part(part))
|
||||
|
||||
# Header/Footer
|
||||
for relid, rel in six.iteritems(self.docx.part.rels):
|
||||
if rel.reltype in (REL_TYPE.HEADER, REL_TYPE.FOOTER):
|
||||
self.pic_map.update(self._img_filename_to_part(rel.target_part))
|
||||
|
||||
def get_pic_map(self):
|
||||
return self.pic_map
|
||||
|
||||
def _img_filename_to_part(self, doc_part):
|
||||
|
||||
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
|
||||
|
||||
# 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)
|
||||
# FIXME: figure out what exceptions are thrown here and catch more specific exceptions
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return part_map
|
||||
|
||||
def build_url_id(self, url):
|
||||
return self.docx._part.relate_to(url, REL_TYPE.HYPERLINK,
|
||||
is_external=True)
|
||||
|
||||
def save(self, filename, *args, **kwargs):
|
||||
self.pre_processing()
|
||||
self.docx.save(filename, *args, **kwargs)
|
||||
self.post_processing(filename)
|
||||
|
||||
def get_undeclared_template_variables(self, jinja_env=None):
|
||||
xml = self.get_xml()
|
||||
xml = self.patch_xml(xml)
|
||||
for uri in [self.HEADER_URI, self.FOOTER_URI]:
|
||||
for relKey, _xml in self.get_headers_footers_xml(uri):
|
||||
xml += self.patch_xml(_xml)
|
||||
if jinja_env:
|
||||
env = jinja_env
|
||||
else:
|
||||
env = Environment()
|
||||
parse_content = env.parse(xml)
|
||||
return meta.find_undeclared_variables(parse_content)
|
||||
|
||||
undeclared_template_variables = property(get_undeclared_template_variables)
|
||||
|
||||
|
||||
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)
|
||||
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()
|
||||
|
||||
|
||||
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):
|
||||
|
||||
# If a RichText is added
|
||||
if isinstance(text, RichText):
|
||||
self.xml += text.xml
|
||||
return
|
||||
|
||||
# If not a string : cast to string (ex: int, dict etc...)
|
||||
if not isinstance(text, (six.text_type, six.binary_type)):
|
||||
text = six.text_type(text)
|
||||
if not isinstance(text, six.text_type):
|
||||
text = text.decode('utf-8', errors='ignore')
|
||||
text = (escape(text)
|
||||
.replace('\n', NEWLINE_XML)
|
||||
.replace('\a', NEWPARAGRAPH_XML)
|
||||
.replace('\t', TAB_XML)
|
||||
.replace('\f', PAGE_BREAK))
|
||||
|
||||
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 subscript:
|
||||
prop += u'<w:vertAlign w:val="subscript"/>'
|
||||
if superscript:
|
||||
prop += u'<w:vertAlign w:val="superscript"/>'
|
||||
if bold:
|
||||
prop += u'<w:b/>'
|
||||
if italic:
|
||||
prop += u'<w:i/>'
|
||||
if underline:
|
||||
if underline not in ['single', 'double', 'thick', 'dotted', 'dash', 'dotDash', 'dotDotDash', 'wave']:
|
||||
underline = 'single'
|
||||
prop += u'<w:u w:val="%s"/>' % underline
|
||||
if strike:
|
||||
prop += u'<w:strike/>'
|
||||
if font:
|
||||
prop += (u'<w:rFonts w:ascii="{font}" w:hAnsi="{font}" w:cs="{font}"/>'
|
||||
.format(font=font))
|
||||
|
||||
xml = u'<w:r>'
|
||||
if prop:
|
||||
xml += u'<w:rPr>%s</w:rPr>' % prop
|
||||
xml += u'<w:t xml:space="preserve">%s</w:t></w:r>' % text
|
||||
if url_id:
|
||||
xml = (u'<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
|
||||
|
||||
|
||||
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):
|
||||
# If not a string : cast to string (ex: int, dict etc...)
|
||||
if not isinstance(text, (six.text_type, six.binary_type)):
|
||||
text = six.text_type(text)
|
||||
self.xml = (escape(text)
|
||||
.replace('\n', NEWLINE_XML)
|
||||
.replace('\a', NEWPARAGRAPH_XML)
|
||||
.replace('\t', TAB_XML)
|
||||
.replace('\f', PAGE_BREAK))
|
||||
|
||||
def __unicode__(self):
|
||||
return self.xml
|
||||
|
||||
def __str__(self):
|
||||
return self.xml
|
||||
|
||||
def __html__(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()
|
||||
|
||||
def __html__(self):
|
||||
return self._insert_image()
|
||||
|
||||
@ -1,175 +0,0 @@
|
||||
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()
|
||||
@ -1,78 +0,0 @@
|
||||
# -*- 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()
|
||||
@ -1,35 +0,0 @@
|
||||
# -*- 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
|
||||
@ -1,180 +0,0 @@
|
||||
# -*- 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
|
||||
@ -1,103 +0,0 @@
|
||||
# -*- 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()
|
||||
@ -1,926 +0,0 @@
|
||||
# -*- 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
1032
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,70 +0,0 @@
|
||||
[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"
|
||||
@ -1,5 +1,4 @@
|
||||
six
|
||||
python-docx
|
||||
docxcompose
|
||||
jinja2
|
||||
lxml
|
||||
sphinx-book-theme
|
||||
|
||||
69
setup.py
69
setup.py
@ -1,9 +1,8 @@
|
||||
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
|
||||
|
||||
@ -11,13 +10,13 @@ from setuptools import setup
|
||||
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
|
||||
|
||||
@ -28,15 +27,13 @@ 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")
|
||||
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
|
||||
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:
|
||||
@ -46,32 +43,28 @@ def get_version(pkg):
|
||||
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,
|
||||
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,
|
||||
)
|
||||
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)
|
||||
|
||||
@ -1,42 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate, RichText
|
||||
|
||||
tpl = DocxTemplate("templates/cellbg_tpl.docx")
|
||||
tpl = DocxTemplate('templates/cellbg_tpl.docx')
|
||||
|
||||
context = {
|
||||
"alerts": [
|
||||
'alerts': [
|
||||
{
|
||||
"date": "2015-03-10",
|
||||
"desc": RichText("Very critical alert", color="FF0000", bold=True),
|
||||
"type": "CRITICAL",
|
||||
"bg": "FF0000",
|
||||
'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-11',
|
||||
'desc': RichText('Just a warning'),
|
||||
'type': 'WARNING',
|
||||
'bg': 'FFDD00',
|
||||
},
|
||||
{
|
||||
"date": "2015-03-12",
|
||||
"desc": RichText("Information"),
|
||||
"type": "INFO",
|
||||
"bg": "8888FF",
|
||||
'date': '2015-03-12',
|
||||
'desc': RichText('Information'),
|
||||
'type': 'INFO',
|
||||
'bg': '8888FF',
|
||||
},
|
||||
{
|
||||
"date": "2015-03-13",
|
||||
"desc": RichText("Debug trace"),
|
||||
"type": "DEBUG",
|
||||
"bg": "FF00FF",
|
||||
'date': '2015-03-13',
|
||||
'desc': RichText('Debug trace'),
|
||||
'type': 'DEBUG',
|
||||
'bg': 'FF00FF',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/cellbg.docx")
|
||||
tpl.save('output/cellbg.docx')
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/comments_tpl.docx")
|
||||
|
||||
tpl.render({})
|
||||
tpl.save("output/comments.docx")
|
||||
@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: sandeeprah, Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
import jinja2
|
||||
@ -14,7 +14,7 @@ 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_value = value + ' ' + my_string_arg
|
||||
return return_value
|
||||
|
||||
|
||||
@ -24,12 +24,12 @@ def my_filterB(value, my_float_arg):
|
||||
|
||||
|
||||
# Then, declare them to jinja like this :
|
||||
jinja_env.filters["my_filterA"] = my_filterA
|
||||
jinja_env.filters["my_filterB"] = my_filterB
|
||||
jinja_env.filters['my_filterA'] = my_filterA
|
||||
jinja_env.filters['my_filterB'] = my_filterB
|
||||
|
||||
|
||||
context = {"base_value_string": " Hello", "base_value_float": 1.5}
|
||||
context = {'base_value_string': ' Hello', 'base_value_float': 1.5}
|
||||
|
||||
tpl = DocxTemplate("templates/custom_jinja_filters_tpl.docx")
|
||||
tpl = DocxTemplate('templates/custom_jinja_filters_tpl.docx')
|
||||
tpl.render(context, jinja_env)
|
||||
tpl.save("output/custom_jinja_filters.docx")
|
||||
tpl.save('output/custom_jinja_filters.docx')
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
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("templates/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("output/dynamic_table.docx")
|
||||
tpl.save('output/dynamic_table.docx')
|
||||
|
||||
@ -1,45 +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("templates/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("output/embedded_embedded_docx.docx")
|
||||
embedded_docx_tpl.save('output/embedded_embedded_docx.docx')
|
||||
|
||||
|
||||
# rendering the main document :
|
||||
tpl = DocxTemplate("templates/embedded_main_tpl.docx")
|
||||
# rendring the main document :
|
||||
tpl = DocxTemplate('templates/embedded_main_tpl.docx')
|
||||
|
||||
context = {
|
||||
"name": "John Doe",
|
||||
'name': 'John Doe',
|
||||
}
|
||||
|
||||
tpl.replace_embedded(
|
||||
"templates/embedded_dummy.docx", "templates/embedded_static_docx.docx"
|
||||
'templates/embedded_dummy.docx', 'templates/embedded_static_docx.docx'
|
||||
)
|
||||
tpl.replace_embedded(
|
||||
"templates/embedded_dummy2.docx", "output/embedded_embedded_docx.docx"
|
||||
'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"
|
||||
'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",
|
||||
'word/embeddings/Pr_sentation_Microsoft_Office_PowerPoint4.pptx',
|
||||
'templates/real_PowerPoint.pptx',
|
||||
)
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/embedded.docx")
|
||||
tpl.save('output/embedded.docx')
|
||||
|
||||
@ -1,54 +1,20 @@
|
||||
from docxtpl import DocxTemplate, R, Listing
|
||||
|
||||
tpl = DocxTemplate("templates/escape_tpl.docx")
|
||||
tpl = DocxTemplate('templates/escape_tpl.docx')
|
||||
|
||||
context = {
|
||||
"myvar": R(
|
||||
'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",
|
||||
'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 : <>& ..."),
|
||||
"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>"
|
||||
'mylisting': Listing(
|
||||
'the listing\nwith\nsome\nlines\nand special chars : <>&\f ... and a page break'
|
||||
),
|
||||
'page_break': R('\f'),
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/escape.docx")
|
||||
tpl.save('output/escape.docx')
|
||||
|
||||
@ -5,23 +5,25 @@
|
||||
import os
|
||||
from unicodedata import name
|
||||
|
||||
from six import iteritems, text_type
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
|
||||
XML_RESERVED = """<"&'>"""
|
||||
|
||||
tpl = DocxTemplate("templates/escape_tpl_auto.docx")
|
||||
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(),
|
||||
'nested_dict': {name(text_type(c)): c for c in XML_RESERVED},
|
||||
'autoescape': 'Escaped "str & ing"!',
|
||||
'autoescape_unicode': u'This is an escaped <unicode> example \u4f60 & \u6211',
|
||||
'iteritems': iteritems,
|
||||
}
|
||||
|
||||
tpl.render(context, autoescape=True)
|
||||
|
||||
OUTPUT = "output"
|
||||
OUTPUT = 'output'
|
||||
if not os.path.exists(OUTPUT):
|
||||
os.makedirs(OUTPUT)
|
||||
tpl.save(OUTPUT + "/escape_auto.docx")
|
||||
tpl.save(OUTPUT + '/escape_auto.docx')
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
# -*- 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)
|
||||
@ -1,180 +0,0 @@
|
||||
#!/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,25 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/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"
|
||||
'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("output/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("templates/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("output/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 = "output/header_footer_image.docx"
|
||||
DEST_FILE = 'output/header_footer_image.docx'
|
||||
|
||||
tpl = DocxTemplate("templates/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("templates/dummy_pic_for_header.png", "templates/python.png")
|
||||
tpl.replace_media('templates/dummy_pic_for_header.png', 'templates/python.png')
|
||||
tpl.render(context)
|
||||
tpl.save(DEST_FILE)
|
||||
|
||||
@ -1,29 +1,29 @@
|
||||
# -*- 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"
|
||||
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")
|
||||
tpl = DocxTemplate('templates/header_footer_image_tpl.docx')
|
||||
|
||||
context = {
|
||||
"mycompany": "The World Wide company",
|
||||
'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())
|
||||
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")
|
||||
tpl = DocxTemplate('templates/header_footer_image_tpl.docx')
|
||||
dummy_pic.seek(0)
|
||||
new_image.seek(0)
|
||||
tpl.replace_media(dummy_pic, new_image)
|
||||
@ -32,5 +32,5 @@ tpl.render(context)
|
||||
file_obj = io.BytesIO()
|
||||
tpl.save(file_obj)
|
||||
file_obj.seek(0)
|
||||
with open(DEST_FILE2, "wb") as f:
|
||||
with open(DEST_FILE2, 'wb') as f:
|
||||
f.write(file_obj.read())
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
# -*- 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,28 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2016-07-19
|
||||
|
||||
@author: AhnSeongHyun
|
||||
|
||||
Edited : 2016-07-19 by Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/header_footer_tpl_utf8.docx")
|
||||
tpl = DocxTemplate('templates/header_footer_tpl_utf8.docx')
|
||||
|
||||
sd = tpl.new_subdoc()
|
||||
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"
|
||||
u'This is a sub-document to check it does not break header and footer with utf-8 characters inside the template .docx'
|
||||
)
|
||||
|
||||
context = {
|
||||
"title": "헤더와 푸터",
|
||||
"company_name": "세계적 회사",
|
||||
"date": "2016-03-17",
|
||||
"mysubdoc": sd,
|
||||
'title': u'헤더와 푸터',
|
||||
'company_name': u'세계적 회사',
|
||||
'date': u'2016-03-17',
|
||||
'mysubdoc': sd,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/header_footer_utf8.docx")
|
||||
tpl.save('output/header_footer_utf8.docx')
|
||||
|
||||
@ -2,6 +2,6 @@
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/horizontal_merge_tpl.docx")
|
||||
tpl = DocxTemplate('templates/horizontal_merge_tpl.docx')
|
||||
tpl.render({})
|
||||
tpl.save("output/horizontal_merge.docx")
|
||||
tpl.save('output/horizontal_merge.docx')
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2017-01-14
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate, InlineImage
|
||||
|
||||
@ -11,41 +11,37 @@ from docxtpl import DocxTemplate, InlineImage
|
||||
from docx.shared import Mm
|
||||
import jinja2
|
||||
|
||||
tpl = DocxTemplate("templates/inline_image_tpl.docx")
|
||||
tpl = DocxTemplate('templates/inline_image_tpl.docx')
|
||||
|
||||
context = {
|
||||
"myimage": InlineImage(tpl, "templates/python_logo.png", width=Mm(20)),
|
||||
"myimageratio": InlineImage(
|
||||
tpl, "templates/python_jpeg.jpg", width=Mm(30), height=Mm(60)
|
||||
'myimage': InlineImage(tpl, 'templates/python_logo.png', width=Mm(20)),
|
||||
'myimageratio': InlineImage(
|
||||
tpl, 'templates/python_jpeg.jpg', width=Mm(30), height=Mm(60)
|
||||
),
|
||||
"frameworks": [
|
||||
'frameworks': [
|
||||
{
|
||||
"image": InlineImage(tpl, "templates/django.png", height=Mm(10)),
|
||||
"desc": "The web framework for perfectionists with deadlines",
|
||||
'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/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/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/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.",
|
||||
'image': InlineImage(tpl, 'templates/tornado.png', height=Mm(10)),
|
||||
'desc': 'Tornado is a Python web framework and asynchronous networking library.',
|
||||
},
|
||||
],
|
||||
}
|
||||
# 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")
|
||||
tpl.save('output/inline_image.docx')
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/less_cells_after_loop_tpl.docx")
|
||||
tpl = DocxTemplate('templates/less_cells_after_loop_tpl.docx')
|
||||
tpl.render({})
|
||||
tpl.save("output/less_cells_after_loop.docx")
|
||||
tpl.save('output/less_cells_after_loop.docx')
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
# -*- 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")
|
||||
@ -1,17 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/merge_paragraph_tpl.docx")
|
||||
tpl = DocxTemplate('templates/merge_paragraph_tpl.docx')
|
||||
|
||||
context = {
|
||||
"living_in_town": True,
|
||||
'living_in_town': True,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/merge_paragraph.docx")
|
||||
tpl.save('output/merge_paragraph.docx')
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
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)
|
||||
@ -1,40 +0,0 @@
|
||||
# -*- 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,45 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2016-03-26
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/nested_for_tpl.docx")
|
||||
tpl = DocxTemplate('templates/nested_for_tpl.docx')
|
||||
|
||||
context = {
|
||||
"dishes": [
|
||||
{"name": "Pizza", "ingredients": ["bread", "tomato", "ham", "cheese"]},
|
||||
'dishes': [
|
||||
{'name': 'Pizza', 'ingredients': ['bread', 'tomato', 'ham', 'cheese']},
|
||||
{
|
||||
"name": "Hamburger",
|
||||
"ingredients": ["bread", "chopped steak", "cheese", "sauce"],
|
||||
'name': 'Hamburger',
|
||||
'ingredients': ['bread', 'chopped steak', 'cheese', 'sauce'],
|
||||
},
|
||||
{
|
||||
"name": "Apple pie",
|
||||
"ingredients": ["flour", "apples", "suggar", "quince jelly"],
|
||||
'name': 'Apple pie',
|
||||
'ingredients': ['flour', 'apples', 'suggar', 'quince jelly'],
|
||||
},
|
||||
],
|
||||
"authors": [
|
||||
'authors': [
|
||||
{
|
||||
"name": "Saint-Exupery",
|
||||
"books": [
|
||||
{"title": "Le petit prince"},
|
||||
{"title": "L'aviateur"},
|
||||
{"title": "Vol de nuit"},
|
||||
'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"},
|
||||
'name': 'Barjavel',
|
||||
'books': [
|
||||
{'title': 'Ravage'},
|
||||
{'title': "La nuit des temps"},
|
||||
{'title': 'Le grand secret'},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/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("templates/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"},
|
||||
'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",
|
||||
'in_europe': True,
|
||||
'is_paid': False,
|
||||
'company_name': 'The World Wide company',
|
||||
'total_price': '100,000,000.00',
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/order.docx")
|
||||
tpl.save('output/order.docx')
|
||||
|
||||
@ -3,12 +3,12 @@ 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")
|
||||
tpl = DocxTemplate('templates/preserve_spaces_tpl.docx')
|
||||
|
||||
tags = ["tag_1", "tag_2"]
|
||||
replacement = ["looking", "too"]
|
||||
tags = ['tag_1', 'tag_2']
|
||||
replacement = ['looking', 'too']
|
||||
|
||||
context = dict(zip(tags, replacement))
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/preserve_spaces.docx")
|
||||
tpl.save('output/preserve_spaces.docx')
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2017-09-03
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
DEST_FILE = "output/replace_picture.docx"
|
||||
DEST_FILE = 'output/replace_picture.docx'
|
||||
|
||||
tpl = DocxTemplate("templates/replace_picture_tpl.docx")
|
||||
tpl = DocxTemplate('templates/replace_picture_tpl.docx')
|
||||
|
||||
context = {}
|
||||
|
||||
tpl.replace_pic("python_logo.png", "templates/python.png")
|
||||
tpl.replace_pic('python_logo.png', 'templates/python.png')
|
||||
tpl.render(context)
|
||||
tpl.save(DEST_FILE)
|
||||
|
||||
@ -1,64 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-26
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate, RichText
|
||||
|
||||
tpl = DocxTemplate("templates/richtext_tpl.docx")
|
||||
tpl = DocxTemplate('templates/richtext_tpl.docx')
|
||||
|
||||
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")
|
||||
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('\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('\n\n<cool>')
|
||||
|
||||
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)
|
||||
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 = RichText('an example of ')
|
||||
rt_embedded.add(rt)
|
||||
|
||||
context = {
|
||||
"example": rt_embedded,
|
||||
'example': rt_embedded,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/richtext.docx")
|
||||
tpl.save('output/richtext.docx')
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-26
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate, RichText
|
||||
|
||||
tpl = DocxTemplate("templates/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("output/richtext_and_if.docx")
|
||||
tpl.save('output/richtext_and_if.docx')
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
# -*- 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")
|
||||
@ -1,63 +0,0 @@
|
||||
"""
|
||||
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,17 +1,18 @@
|
||||
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")
|
||||
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:
|
||||
print("%s ..." % test)
|
||||
subprocess.call(["python", "./%s" % test])
|
||||
six.print_('%s ...' % test)
|
||||
subprocess.call(['python', './%s' % test])
|
||||
|
||||
print("Done.")
|
||||
six.print_('Done.')
|
||||
|
||||
@ -1,36 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2015-03-12
|
||||
|
||||
@author: Eric Lapouyade
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
from docx.shared import Inches
|
||||
|
||||
tpl = DocxTemplate("templates/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("templates/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])
|
||||
@ -38,8 +38,8 @@ for item in recordset:
|
||||
row_cells[2].text = item[2]
|
||||
|
||||
context = {
|
||||
"mysubdoc": sd,
|
||||
'mysubdoc': sd,
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/subdoc.docx")
|
||||
tpl.save('output/subdoc.docx')
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
from docxtpl import DocxTemplate
|
||||
from jinja2.exceptions import TemplateError
|
||||
import six
|
||||
|
||||
print("=" * 80)
|
||||
print("Generating template error for testing (so it is safe to ignore) :")
|
||||
print("." * 80)
|
||||
six.print_('=' * 80)
|
||||
six.print_("Generating template error for testing (so it is safe to ignore) :")
|
||||
six.print_('.' * 80)
|
||||
try:
|
||||
tpl = DocxTemplate("templates/template_error_tpl.docx")
|
||||
tpl.render({"test_variable": "test variable value"})
|
||||
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:")
|
||||
six.print_(six.text_type(the_error))
|
||||
if hasattr(the_error, 'docx_context'):
|
||||
six.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)
|
||||
six.print_(line)
|
||||
tpl.save('output/template_error.docx')
|
||||
six.print_('.' * 80)
|
||||
six.print_(" End of TemplateError Test ")
|
||||
six.print_('=' * 80)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,8 +0,0 @@
|
||||
{"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}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,23 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
'''
|
||||
Created : 2017-10-15
|
||||
|
||||
@author: Arthaslixin
|
||||
"""
|
||||
'''
|
||||
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/vertical_merge_tpl.docx")
|
||||
tpl = DocxTemplate('templates/vertical_merge_tpl.docx')
|
||||
|
||||
context = {
|
||||
"items": [
|
||||
{"desc": "Python interpreters", "qty": 2, "price": "FREE"},
|
||||
{"desc": "Django projects", "qty": 5403, "price": "FREE"},
|
||||
{"desc": "Guido", "qty": 1, "price": "100,000,000.00"},
|
||||
'items': [
|
||||
{'desc': 'Python interpreters', 'qty': 2, 'price': 'FREE'},
|
||||
{'desc': 'Django projects', 'qty': 5403, 'price': 'FREE'},
|
||||
{'desc': 'Guido', 'qty': 1, 'price': '100,000,000.00'},
|
||||
],
|
||||
"total_price": "100,000,000.00",
|
||||
"category": "Book",
|
||||
'total_price': '100,000,000.00',
|
||||
'category': 'Book',
|
||||
}
|
||||
|
||||
tpl.render(context)
|
||||
tpl.save("output/vertical_merge.docx")
|
||||
tpl.save('output/vertical_merge.docx')
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from docxtpl import DocxTemplate
|
||||
|
||||
tpl = DocxTemplate("templates/vertical_merge_nested_tpl.docx")
|
||||
tpl = DocxTemplate('templates/vertical_merge_nested_tpl.docx')
|
||||
tpl.render({})
|
||||
tpl.save("output/vertical_merge_nested.docx")
|
||||
tpl.save('output/vertical_merge_nested.docx')
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
from docxtpl import DocxTemplate, RichText
|
||||
|
||||
tpl = DocxTemplate("templates/word2016_tpl.docx")
|
||||
tpl = DocxTemplate('templates/word2016_tpl.docx')
|
||||
tpl.render(
|
||||
{
|
||||
"test_space": " ",
|
||||
"test_tabs": 5 * "\t",
|
||||
"test_space_r": RichText(" "),
|
||||
"test_tabs_r": RichText(5 * "\t"),
|
||||
'test_space': ' ',
|
||||
'test_tabs': 5 * '\t',
|
||||
'test_space_r': RichText(' '),
|
||||
'test_tabs_r': RichText(5 * '\t'),
|
||||
}
|
||||
)
|
||||
tpl.save("output/word2016.docx")
|
||||
tpl.save('output/word2016.docx')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user