Compare commits

...

349 Commits

Author SHA1 Message Date
Eric Lapouyade
f96b0b6413
Merge pull request #617 from waketzheng/project-section
feat: use project section
2025-09-24 13:48:54 +02:00
Waket Zheng
560b4b3311 Use poetry-dynamic-versioning instead of pdm 2025-09-24 12:25:32 +08:00
Waket Zheng
e77cbf8c7b Fix pip install error with --editable 2025-09-23 18:58:34 +08:00
Waket Zheng
7a6ddbcc54 Move docxcompose to optional dependency 2025-09-23 18:25:51 +08:00
Eric Lapouyade
d9bb19cdd4 Update setup.py 2025-07-15 17:15:05 +02:00
Eric Lapouyade
416729dddc Run black on template.py 2025-07-15 15:46:03 +02:00
Eric Lapouyade
cbbeae57bd Fix and improve get_undeclared_template_variables() method 2025-07-15 15:39:11 +02:00
Pablo Esteban
606d189787 - The get_undeclared_template_variables method now analyzes the original template, regardless of whether it has been rendered.
- Added optional context parameter to return only variables not present in the provided context.
- Added test tests/get_undeclared_variables.py:
    - Verifies behavior before rendering (all variables)
    - Verifies after rendering with incomplete context (only missing variables)
    - Verifies after rendering with complete context (empty set)
    - Verifies compatibility with custom Jinja2 environment
- All tests use asserts and are ready for CI integration.

Closes #585
2025-07-11 14:32:20 +02:00
Eric Lapouyade
399761f9c9 Update sphynx conf.py 2025-05-06 16:22:18 +02:00
Eric Lapouyade
4477729a57 move requirements.txt 2025-05-06 16:21:27 +02:00
Eric Lapouyade
b391b213eb update sphinx theme 2025-05-06 16:17:56 +02:00
Eric Lapouyade
e766040c5b Add .readthedocs.yaml 2025-05-06 16:04:07 +02:00
Eric Lapouyade
fb0e3583da v0.20.0 2025-05-06 15:51:54 +02:00
Eric Lapouyade
40861400b3 black 2025-05-02 16:42:32 +02:00
Eric Lapouyade
9392b06da4 add RichTextParagraph class 2025-05-02 16:29:22 +02:00
Eric Lapouyade
a48a6a96b2 Merge branch 'refs/heads/ST-Imrie-master' 2025-05-02 16:08:08 +02:00
Eric Lapouyade
685425095b
Merge pull request #592 from bm-rana/bold-rtl-fix
Add RTL support for bold/italic text
2025-05-02 15:58:19 +02:00
Eric Lapouyade
08a5d748f6
Merge pull request #593 from aperechnev/master
Possibility to skip missing pictures
2025-05-02 15:56:50 +02:00
ST-Imrie
ebc770a291 Updated RichText Jinja tags from rr/rp to r/q and updated test template 2025-05-01 11:39:48 +01:00
Alexander Pérechnev
97df786298 Added possibility to skip missing pictures instead of raising ValueError exception. 2025-04-04 17:38:56 +03:00
rana saab
994da90f66 fixed rtl italic formatting 2025-04-03 13:37:17 +03:00
ST-Imrie
aa7b7a77e3 Rich Text Paragraph Formatting Update 2025-02-28 17:53:26 +00:00
Eric Lapouyade
b97fa32f10 v0.19.1 2024-12-29 18:53:25 +01:00
Eric Lapouyade
def1205179
Merge pull request #575 from jhpyle/patch-1
encode XML as bytes in render_footnotes()
2024-12-29 18:43:33 +01:00
Jonathan Pyle
eed645c0c5
encode XML as bytes in render_footnotes()
`part._blob` should have the class `bytes`.
2024-11-29 15:23:46 -05:00
Eric Lapouyade
60caff83fa Flake8 : whitespace fix 2024-11-12 14:37:12 +01:00
Eric Lapouyade
65e00b189e Flake8 : whitespace fix 2024-11-12 14:31:55 +01:00
Eric Lapouyade
0f42e5a4c8 v0.19.0 2024-11-12 14:27:06 +01:00
Eric Lapouyade
bc92389ee1 Merge branch 'bartbroere-patch-1' 2024-11-12 14:08:49 +01:00
Bart Broere
0ef74a740e
Change to isinstance for type checking 2024-10-24 07:28:49 +02:00
Bart Broere
0d616add84
Apply suggestion since part.blob changes type in the loop
Co-authored-by: Chatnoir Miki <cmiki@amono.me>
2024-10-24 07:27:59 +02:00
Bart Broere
9124386b4c Fix flake8 2024-09-23 06:25:22 +00:00
Bart Broere
1cca257016 Add a test and use existing XML patching method 2024-09-23 06:24:41 +00:00
Bart Broere
a449f01f36 Fix flake8 2024-09-18 07:35:12 +00:00
Bart Broere
2812487300
Simplify the code 2024-09-17 21:19:19 +02:00
Bart Broere
2886a851e0
Change the XML library to lxml to avoid namespace renaming and other XML problems 2024-09-17 21:07:21 +02:00
Bart Broere
0061b556ec
[WIP] Support rendering variables in footnotes 2024-09-17 15:09:29 +02:00
Eric Lapouyade
0607e7175d
Merge pull request #556 from a-detiste/master 2024-08-01 01:55:58 +02:00
Alexandre Detiste
d99607003c remove leftover "six" reference 2024-07-30 23:12:40 +02:00
Eric Lapouyade
b9be3a5b50 Update CHANGES.rst 2024-07-21 16:48:36 +02:00
Eric Lapouyade
83ecb60bdd Merge remote-tracking branch 'origin/master' 2024-07-21 16:43:01 +02:00
Eric Lapouyade
a10c3c16e4 Remove python 2.x support 2024-07-21 16:42:41 +02:00
Eric Lapouyade
d472308ae5
Merge pull request #555 from jkpet/patch-1
Update index.rst
2024-07-21 16:22:06 +02:00
Eric Lapouyade
ce25ec8228
Merge pull request #554 from elapouya/dependabot/pip/setuptools-70.0.0
Bump setuptools from 69.5.1 to 70.0.0
2024-07-21 16:20:56 +02:00
Eric Lapouyade
ac38610947 Code styling 2024-07-21 16:17:47 +02:00
Eric Lapouyade
a50b52b317 PR #552 + black all files 2024-07-21 16:10:44 +02:00
Eric Lapouyade
f3ba468927
Merge pull request #552 from jeanmarcosdarosa/jeanmarcosdarosa-patch-hyperlink
Added hyperlink option in InlineImage
2024-07-21 16:01:35 +02:00
Eric Lapouyade
b7872d7723 Add poetry env 2024-07-21 15:37:37 +02:00
jkpet
0642badebc
Update index.rst
Updating 'sub-document' documentation to make explicit what was already implicit.
2024-07-19 17:03:07 +10:00
dependabot[bot]
664bb845fa
Bump setuptools from 69.5.1 to 70.0.0
Bumps [setuptools](https://github.com/pypa/setuptools) from 69.5.1 to 70.0.0.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v69.5.1...v70.0.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-15 19:38:23 +00:00
Jean Marcos da Rosa
e0d5eb0e0a
Added hyperlink option in InlineImage 2024-07-09 10:33:26 -03:00
Eric Lapouyade
d322120d2a Fix CHANGES.rst 2024-05-01 16:56:11 +02:00
Eric Lapouyade
58de890f92 v0.17.0 -> requires python-docx 1.1.1+ 2024-05-01 16:50:16 +02:00
Eric Lapouyade
bee0706645
Merge pull request #527 from dralexxx/update_jinja2_version
regen pipfile.lock with newer jinja2 dependency version
2024-02-12 09:31:23 +01:00
dralex
24de22bfc6 regen pipfile.lock with newer jinja2 dependency version 2024-02-10 21:17:49 +01:00
Eric Lapouyade
d43c6debbf Changed README.rst 2023-05-08 11:44:44 +02:00
Eric Lapouyade
d539119fed PR #493 2023-05-08 11:15:41 +02:00
Eric Lapouyade
d042d4592c Merge branch 'quintagroup-fix/resolve_listing_function' 2023-05-08 11:12:28 +02:00
Adrian Vorobel
af2e09209b Fix: add whitespace to regexp, as they capture wrong xml tags (w:tc, w:tcBorders, etc.) 2023-05-05 12:44:04 +03:00
elapouya
d9801fd6e4 PR #482 2023-03-12 11:04:03 +01:00
Eric Lapouyade
6e51079b2a
Merge pull request #482 from dreizehnutters/master
fixed element properties for highlighted text
2023-03-12 10:41:57 +01:00
geb
9af9d9e69b
fixed element properties for highlighted text 2023-03-01 09:07:18 +01:00
elapouya
708581fa6e remove unused code in doc_properties.py 2023-01-07 14:09:28 +01:00
elapouya
e3b40ec959 Remove python 3.6 github style checking 2023-01-07 14:05:32 +01:00
elapouya
724623671c Merge branch 'Slarag-patch-doc-properties' 2023-01-07 14:02:44 +01:00
elapouya
ef09ce6277 0.16.5 2023-01-07 14:02:11 +01:00
Slarag
ce23f81cbe Fix #464 2022-11-18 14:18:32 +01:00
Eric Lapouyade
5d9bba118a
Merge pull request #455 from giancab25/documentation-edits-for-subdoc
Improved the readability of the Sub-documents section of documentation.
2022-09-11 11:19:24 +02:00
giancab25
006d5bc506 Improved the readability of the Sub-documents section of the documentation 2022-08-24 20:28:32 -04:00
Eric Lapouyade
4047845c3a Reorganize documentation 2022-08-05 11:10:11 +02:00
Eric Lapouyade
1a4a5f5723 Reorganize documentation 2022-08-05 11:04:07 +02:00
Eric Lapouyade
650b28d852 flake8 issue 2022-08-04 10:31:45 +02:00
Eric Lapouyade
890549d705 Merge remote-tracking branch 'origin/master' 2022-08-04 10:28:03 +02:00
Eric Lapouyade
52d9b28a31 Merge branch 'sailingnn-master' 2022-08-04 10:27:29 +02:00
Eric Lapouyade
4857a6ed72 Modify PR to be more generic 2022-08-04 10:27:02 +02:00
Eric Lapouyade
bd4b2be81d
Merge pull request #449 from krysros/patch-1
Typo in comment
2022-08-04 10:18:25 +02:00
Eric Lapouyade
ea987f9e38 Merge branch 'master' of https://github.com/sailingnn/python-docx-template into sailingnn-master 2022-08-04 09:46:44 +02:00
sailingnn
a9122c34d4
Update richtext_eastAsia.py 2022-08-04 09:05:06 +08:00
sailingnn
b75867397a
add eastAsia template
adding eastAsia template for test
2022-08-04 09:01:33 +08:00
sailingnn
badce481c6
add eastAsia test
add eastAsia test
2022-08-04 08:58:59 +08:00
Eric Lapouyade
85a7b92bf3
Merge pull request #451 from timgates42/bugfix_typos
docs: Fix a few typos
2022-07-25 12:10:21 +02:00
sailingnn
0660a0f37e
Update richtext.py
add support for eastAsia fonts in RichText
2022-07-21 09:01:30 +08:00
Tim Gates
2fd2140caf
docs: Fix a few typos
There are small typos in:
- docs/index.rst
- docxtpl/__main__.py
- docxtpl/template.py
- tests/embedded.py

Fixes:
- Should read `surrounding` rather than `surronding`.
- Should read `rendering` rather than `rendring`.
- Should read `providing` rather than `provinding`.
- Should read `optionally` rather than `optionnally`.
- Should read `optional` rather than `optionnal`.
- Should read `necessary` rather than `neccessary`.
- Should read `existing` rather than `exsting`.
- Should read `embedded` rather than `embdded`.
- Should read `dictionaries` rather than `dictionnaries`.
- Should read `cleaning` rather than `cleanning`.
- Should read `catches` rather than `cacthes`.

Signed-off-by: Tim Gates <tim.gates@iress.com>
2022-07-21 08:16:56 +10:00
Krystian Rosiński
d4415c34a4
Typo in comment 2022-07-17 13:54:19 +02:00
Eric Lapouyade
a712baff03 fix #448 2022-07-14 18:16:20 +02:00
Eric Lapouyade
7816ea6270 update changelog 2022-07-14 15:25:24 +02:00
Eric Lapouyade
5f21feb72d fix #444 2022-07-14 15:06:21 +02:00
elapouya
7290528b3d flake8 fix 2022-06-12 16:28:02 +02:00
elapouya
a0f6589d34 PR #442 2022-06-12 16:17:27 +02:00
elapouya
0ab1144a8d Merge branch 'karolzlot-type_hints1' 2022-06-12 16:15:46 +02:00
elapouya
6da5022b41 Merge branch 'type_hints1' of https://github.com/karolzlot/python-docx-template into karolzlot-type_hints1
PR #442
2022-06-12 16:02:44 +02:00
Karol Zlot
bda05f2101 return None 2022-06-12 10:44:24 +02:00
Karol Zlot
25a6e42850 Reverse name changes 2022-06-12 10:43:03 +02:00
elapouya
624bc15a10 fix #443 2022-06-12 10:30:44 +02:00
Karol Zlot
43ceb38465 Add type hints to get_undeclared_template_variables 2022-06-06 23:49:48 +02:00
Karol Zlot
2681c4801d Add first type hints 2022-06-06 23:44:07 +02:00
elapouya
c1f994e632 Changed escape.py test, see #346 2022-05-21 15:06:24 +02:00
Eric Lapouyade
9b4e623c3c Add documentation for jinja-like comments in templates 2022-04-16 12:18:07 +02:00
Eric Lapouyade
6bfd2eb85a PR #427 2022-04-16 11:02:40 +02:00
Eric Lapouyade
d283f3f88a PR #427 2022-04-16 10:58:42 +02:00
Eric Lapouyade
b4e533ae66 PR #427 2022-04-16 10:55:42 +02:00
Staffan Malmgren
4e073ea451 Added support for jinja comments for paragraphs and table rows+cells 2022-03-28 23:43:34 +02:00
Eric Lapouyade
8a3c2051ad fix codestyle.yml 2022-03-12 18:43:11 +01:00
Eric Lapouyade
9a7ac8f4e9 fix codestyle.yml 2022-03-12 18:40:52 +01:00
Eric Lapouyade
00b9a97222 fix codestyle.yml 2022-03-12 18:39:59 +01:00
Eric Lapouyade
605844aa17 fix codestyle.ymle 2022-03-12 18:38:25 +01:00
Eric Lapouyade
b95a23395f fix #422 2022-03-12 18:31:03 +01:00
Eric Lapouyade
23038b98de fix #408 2022-01-12 15:25:07 +01:00
Eric Lapouyade
19733a6b5b
Merge pull request #402 from 42sol-eu/master
Add documentation for get_undeclared_template_variables() method
2021-12-30 12:07:43 +01:00
Andreas **Felix** Häberle
d2037d2a54
added interface function to check variables in document 2021-12-22 18:49:35 +01:00
Eric Lapouyade
e574b6c650 remove f-string for python < 3.6 compability 2021-12-20 16:31:29 +01:00
Eric Lapouyade
589262664f fix #392 2021-12-20 12:07:12 +01:00
Eric Lapouyade
d22aeb41db Fix #398 2021-12-19 12:18:57 +01:00
Eric Lapouyade
4a9acdcf79
Merge pull request #386 from Lcrs123/make_module_executable
Improvements for template used in test module_execute.py
2021-10-06 15:10:06 +02:00
Lucas
234ec65e9d improvements for test template 2021-10-06 09:13:39 -03:00
Eric Lapouyade
e3e1ab58d1 Fix typo 2021-10-01 16:09:11 +02:00
Eric Lapouyade
727d07fa3e Changed code to be python < 3.6 compatible 2021-10-01 12:34:32 +02:00
Eric Lapouyade
0238472033 Changed code to be python < 3.6 compatible 2021-10-01 12:29:56 +02:00
Eric Lapouyade
5e8aeae272 Changed code to be python < 3.6 compatible 2021-10-01 12:25:45 +02:00
Eric Lapouyade
5dfe448a9a Changed code to be python < 3.6 compatible 2021-10-01 12:20:46 +02:00
Eric Lapouyade
541d5ce7cb Changed code to be python < 3.6 compatible 2021-10-01 12:11:43 +02:00
Eric Lapouyade
b0a5de05aa Codestyling 2021-10-01 08:31:19 +02:00
Eric Lapouyade
d900f9296a Merge branch 'Lcrs123-make_module_executable' 2021-09-30 17:08:23 +02:00
Eric Lapouyade
1f3ef9b236 Add quiet option, update doc, modify test 2021-09-30 17:08:05 +02:00
Lucas
e4737ddfbb Added test for main function, template and json for running test 2021-09-30 09:41:58 -03:00
Lucas
7b2d7e1946 Added argument for overwrite without confirmation and constants for easier referencing of arguments 2021-09-30 09:39:40 -03:00
Lucas
2dee2ddd1c Small change in error message for main() 2021-09-29 13:04:47 -03:00
Lucas
5f3fe03108 Small change in error catching in validate function 2021-09-29 11:04:42 -03:00
Lucas
b6f9c992f5 Added function for rendering docx instead of directly calling method on DocxTemplate instance. Added exception chaining to a few functions there were missing it. 2021-09-29 10:35:26 -03:00
Lucas
a78462ee0f Reformat, added comments and small fixes in error catching 2021-09-29 09:46:16 -03:00
Lucas
8004fcf6bc Added basic functionality to make the module executable 2021-09-28 15:25:09 -03:00
Lucas
a45cc806d8 Added basic functionality to make the module executable 2021-09-28 15:12:29 -03:00
Eric Lapouyade
21a0a46a43
Merge pull request #376 from DeanWunder/patch-1
Update index.rst, fixed a misspell.
2021-08-16 01:20:41 +02:00
DeanWunder
9708802400
Update index.rst, fixed a misspell. 2021-08-16 08:39:08 +10:00
Eric Lapouyade
61c8924663 V0.12.0 2021-08-15 15:50:45 +02:00
Eric Lapouyade
b7d782ab20 Should solve #372 2021-08-15 02:58:50 +02:00
Eric Lapouyade
6722d63537 code styling 2021-07-31 14:16:23 +02:00
Eric Lapouyade
bf2240f4b2 code styling 2021-07-31 14:12:22 +02:00
Eric Lapouyade
ab61d39847 code styling 2021-07-31 13:53:44 +02:00
Eric Lapouyade
f780714a52 Added requirements.txt 2021-07-31 13:44:28 +02:00
Eric Lapouyade
30712bbc67 Working on better subdocs 2021-07-31 12:49:47 +02:00
Eric Lapouyade
0a1cb24b12 Update issue templates 2021-07-16 11:18:49 +02:00
Eric Lapouyade
7a65b2a310 Update issue templates 2021-07-16 11:07:42 +02:00
Eric Lapouyade
a435b1c46a Update issue templates 2021-07-16 11:05:16 +02:00
Eric Lapouyade
ef622f28e5 Update issue templates 2021-07-16 11:02:03 +02:00
Eric Lapouyade
44fb098904 Update issue templates 2021-07-16 11:00:01 +02:00
Eric Lapouyade
ba9a4629e9 PR #351 2021-05-09 12:36:28 +02:00
Eric Lapouyade
60151d6677 Merge branch 'alanoe-ae/support_img_title_and_desc_in_replace_pic' 2021-05-09 12:32:12 +02:00
Eric Lapouyade
af2b027c9f Merge branch 'ae/support_img_title_and_desc_in_replace_pic' of https://github.com/alanoe/python-docx-template into alanoe-ae/support_img_title_and_desc_in_replace_pic 2021-05-09 10:26:08 +02:00
Eric Lapouyade
f4042d9d53
Merge pull request #354 from elapouya/revert-350-master
Revert "fix issue #345"
2021-05-09 10:02:05 +02:00
Eric Lapouyade
537dc4485a
Revert "fix issue #345" 2021-05-09 10:00:26 +02:00
Eric Lapouyade
7ac23c81d0
Merge pull request #353 from XtremeGood/master
Added attributes in the documentation for InlineImage
2021-05-09 09:56:57 +02:00
Eric Lapouyade
b8c6cb67f5
Merge pull request #350 from wafi543/master
fix issue #345
2021-05-09 09:49:57 +02:00
Alan Evangelista
63cd4c31f7 Also look for title and description when replacing pictures
Before, the only searched picture tag was 'name', which corresponds to the file
name of the image added to the docx. Also searching in title and description tags
makes creating docx templates to be rendered by this more flexible and easier
in document editors which do not support viewing the image's original filename,
such as Google Docs.

Also, picture replacement was comprised of picture map creation and picture
replacing. Both processes were merged in order to implement the above change
more easily.
2021-05-05 18:21:42 -03:00
Vishesh Mangla
883e9f7e0b
Added attribute names for InlineImage 2021-05-03 22:58:56 +05:30
Vishesh Mangla
2c578949b9
Update index.rst 2021-05-03 22:57:28 +05:30
Alan Evangelista
d90087d6f7 Rename pic_to_replace to pics_to_replace
Latter name is more intuitive because this dictionary may hold the metadata
of several pictures.
2021-04-20 11:59:49 -03:00
wafi543
6b41cc2579 continued fix #345 2021-04-19 14:17:38 +03:00
wafi543
8d512d2edd Flake8 Rules 2021-04-19 12:49:04 +03:00
wafi543
2b2628c6df fix #345 2021-04-19 12:35:38 +03:00
Eric Lapouyade
04d1bfc5fe Merge branch 'idrisnacer-master' 2021-04-06 09:47:20 +02:00
Eric Lapouyade
30578cf88c InlineImage now possible in header/footer 2021-04-06 09:46:57 +02:00
idris nacer
6496abf307 invoke new_pic_inline from current_rendering_part
invoke new_pic_inline from current_rendering_part (header, document body or footer), so the image relationship gets created correctly

Remove white spaces
2021-03-29 11:08:03 +01:00
Eric Lapouyade
bc6d88251a Update package informations 2021-01-09 13:56:38 +01:00
Eric Lapouyade
3d95dd88f7 flake8 code styling fix 2020-11-09 09:30:21 +01:00
Eric Lapouyade
0c5ebcbc54 flake8 code styling fix 2020-11-09 09:22:33 +01:00
Eric Lapouyade
79994ed2c8 fix #323 2020-11-09 09:12:16 +01:00
Eric Lapouyade
53d582c407 fix #320 2020-10-27 10:07:42 +01:00
Eric Lapouyade
4af6f2282f flake8 fixes 2020-10-19 14:16:07 +02:00
Eric Lapouyade
344a7d1676 flake8 fixes 2020-10-19 14:10:24 +02:00
Eric Lapouyade
44a0819625 \n, \a, \t and \f are now accepted in simple context string 2020-10-19 13:59:36 +02:00
Eric Lapouyade
b616daf50a
Merge pull request #314 from jfcorbett/docs/add-conda-install-instructions
Add conda install instructions
2020-10-15 15:11:28 +02:00
Eric Lapouyade
7282856484 revert 0.10.4 2020-10-15 15:07:29 +02:00
JEACO
e67b27f06e Add conda install instructions 2020-10-15 13:33:18 +02:00
Eric Lapouyade
f9b95afe7a Fix changelog 2020-10-15 09:02:55 +02:00
Eric Lapouyade
9946c74fdc PR #307 + #312 2020-10-15 08:57:03 +02:00
Eric Lapouyade
528c3b1ca5 Revert escape.py test files 2020-10-14 17:09:39 +02:00
Eric Lapouyade
3413052ef2 Merge remote-tracking branch 'origin/master' 2020-10-14 16:58:33 +02:00
Eric Lapouyade
c1e7a1cf6f
Merge pull request #311 from elapouya/revert-307-develop
Revert "Add line break resolving with styles preserving"
2020-10-14 16:58:10 +02:00
Eric Lapouyade
ff2fa37b2b
Revert "Add line break resolving with styles preserving" 2020-10-14 16:54:15 +02:00
Eric Lapouyade
cf8e493e93 Update escape.py to test new feature 2020-10-14 16:52:58 +02:00
Eric Lapouyade
5faef9f429 Merge branch 'chabErch-develop' 2020-10-14 15:08:06 +02:00
ChabErch
b9fa41b714 Compatible fix 2020-10-11 21:22:22 +03:00
ChabErch
72b6754a18 Add line break resolving with styles preserving 2020-10-04 23:09:41 +03:00
Eric Lapouyade
e895607d26
Merge pull request #304 from timgates42/bugfix_typo_embedded
docs: Fix simple typo, embdded -> embedded
2020-09-14 19:12:20 +02:00
Tim Gates
53ed9bba32
docs: Fix simple typo, embdded -> embedded
There is a small typo in docs/index.rst, docxtpl/__init__.py.

Should read `embedded` rather than `embdded`.
2020-09-14 06:28:17 +10:00
Eric Lapouyade
8428b72dec
Merge pull request #299 from oraix/fix_underline
Support more types of underline
2020-08-30 16:18:46 +02:00
UncleOraix
b1453e6eba Support more types of underline 2020-08-28 23:51:42 +08:00
Eric Lapouyade
fe4e76459f Remove extension testing in replace_pic() 2020-08-23 18:37:51 +02:00
Eric Lapouyade
92613a9435 Merge remote-tracking branch 'origin/master' 2020-08-23 18:22:49 +02:00
Eric Lapouyade
3b4de31734 Remove extension testing in replace_pic() 2020-08-23 18:22:25 +02:00
Eric Lapouyade
4840f9e709
Merge pull request #294 from dusty-phillips/patch-1
Fix spelling mistake
2020-08-16 00:51:10 +02:00
Dusty Phillips
6c21fab272
Fix spelling mistake 2020-08-15 16:53:13 -03:00
elapouya
4d81ef8eaa Update doc 2020-07-31 18:43:14 +02:00
Eric Lapouyade
74d46579be Fix codestyling (lint with flake8) 2020-05-24 12:07:24 +02:00
Eric Lapouyade
1b9ec6f253 Merge remote-tracking branch 'origin/master' 2020-05-24 11:55:53 +02:00
Eric Lapouyade
7363428782 v0.10.0 2020-05-24 11:54:49 +02:00
Eric Lapouyade
53bf8175d9
Merge pull request #276 from craigmbooth/patch-1
Fix "ingore" typo in index.rst
2020-05-19 07:28:30 +02:00
Craig Booth
8bf2fa511d
Fix typo in index.rst 2020-05-18 20:25:58 -05:00
Eric Lapouyade
5ee1c03410 fix get_undeclared_template_variables() to include header/footer 2020-05-11 17:55:53 +02:00
Eric Lapouyade
01fd27df11 Update doc 2020-04-29 14:41:27 +02:00
Eric Lapouyade
581717e5ee Fix #271 2020-04-26 10:50:40 +02:00
Eric Lapouyade
aba8c6bb7b Fix #271 2020-04-24 16:15:32 +02:00
Eric Lapouyade
a9ebb9d78d Fix codestyling 2020-04-17 10:47:48 +02:00
Edwin Smulders
9175f0cbd6 Fix codestyling according to flake8 rules 2020-04-16 23:06:57 +02:00
Edwin Smulders
45a46eb924 Add a Github action for codestyling 2020-04-16 14:53:13 +02:00
Eric Lapouyade
ccdf20a121 v0.9.0 2020-04-15 15:11:47 +02:00
Eric Lapouyade
97be69b950 {%- xxx -%} merging feature 2020-04-14 22:51:36 +02:00
Eric Lapouyade
ccac13390a fix #266 2020-04-14 10:35:58 +02:00
Eric Lapouyade
2b75638aa8 v0.8.0 2020-04-10 11:29:23 +02:00
Eric Lapouyade
cba0521d55 Merge branch 'Dutchy--master' 2020-04-10 11:21:33 +02:00
Edwin Smulders
e8bc36c57e Make headers and footers work with both python-docx 0.8.7 and 0.8.10 - attempt 2 2020-04-09 17:41:25 +02:00
Eric Lapouyade
888dd085f6 v0.7.0 2020-04-09 16:01:45 +02:00
Eric Lapouyade
dc571cfaf7
Merge pull request #263 from Dutchy-/master
Make headers and footers work with both python-docx 0.8.7 and 0.8.10
2020-04-09 14:29:18 +02:00
Eric Lapouyade
227abbbb54
Merge pull request #262 from komawar/minor-ref-fix
Reference RichText section
2020-04-09 14:29:02 +02:00
Eric Lapouyade
444f862efa Add replace_zipname() 2020-04-09 14:26:22 +02:00
Edwin Smulders
17a2c6e1fe Make headers and footers work with both python-docx 0.8.7 and 0.8.10 2020-04-08 16:50:06 +02:00
Nikhil Komawar
4efd49c8f2 Reference RichText section
This will enabling RichText section in the documentation as it appears
after the concerened section. Easier to navigate and avoid confusion.
2020-04-08 12:44:47 +05:30
Eric Lapouyade
114e5149bc
Merge pull request #260 from komawar/fix-documentation-grammar
Fix grammar in index.rst
2020-04-06 16:14:01 +02:00
Nikhil Komawar
b65d04750e Fix grammar in index.rst
This commit fixes grammar errors in index.rst related to the issue
opened in issue #259
2020-04-06 11:34:54 +05:30
Eric Lapouyade
72a9be558a v0.6.4 2020-04-06 07:06:41 +02:00
Eric Lapouyade
dd588034e9
Merge pull request #258 from awesomo4000/fix-lxml-parse
Prevent lxml from attempting to parse None
2020-04-06 06:59:31 +02:00
Aaron Rhodes
a7c5531f20 Prevent lxml from attempting to parse None 2020-03-30 17:50:07 -04:00
Eric Lapouyade
4fe1cc507d
Merge pull request #236 from mneitsabes/master
Add the ability to add a RichText object to another RichText object
2019-11-09 20:41:21 +01:00
Sébastien MAUROY
9a4dd4e28c Adds the ability to add a RichText object to another RichText object 2019-11-09 12:14:24 +01:00
Eric Lapouyade
74fbb615bb PR #207 and #209 2019-06-20 16:25:03 +02:00
Eric Lapouyade
e3bf22376a
Merge pull request #207 from edufresne/master
Truncate zip file before re-writing it for  replace_media with file-like objects
2019-06-17 16:26:10 +02:00
Eric Lapouyade
1f80377013
Merge pull request #209 from vesatoivonen/issue-208
Allow jinja environment to be passed to undeclared_template_variables
2019-06-17 16:25:19 +02:00
Vesa Toivonen
1bba2f92b7 Keep undeclared_template_variables as a property 2019-06-17 11:07:14 +03:00
Vesa Toivonen
b01f033d14 Allow jinja environment to be passed to undeclared_template_variables 2019-06-12 10:54:57 +03:00
ericdufresne
b3094f660a Truncate zip file before re-writing it. This prevents a misplaced zip file table of contents which can cause Microsoft word to go into recovery mode for a newly generated document. 2019-06-11 10:03:53 -04:00
Eric Lapouyade
671c7c78a7 Handle spaces correctly when run are split by Jinja code (#205) 2019-06-09 12:38:22 +02:00
Eric Lapouyade
68fb31ec22
Merge pull request #205 from toloco/fix-spacing
Handle spaces correctly when run are split by Jinja code
2019-06-09 12:31:28 +02:00
Tolo Palmer
28ed545e85 Bump version 2019-06-07 11:16:38 +01:00
Tolo Palmer
333a046180 Handle spaces correctly when run are split by Jinja code 2019-06-07 11:04:34 +01:00
Eric Lapouyade
ab7adb5d21 v0.6.0 : DocxTemplate now accepts file-like objects 2019-06-05 09:44:35 +02:00
Eric Lapouyade
9f1cb53205
Merge pull request #203 from edufresne/master
Fix need to "Recover document" after using replace_media and saving to file-like object
2019-06-05 09:06:30 +02:00
ericdufresne
fbb930d3ea Provide better support for file-like objects with replace-media 2019-06-03 11:34:16 -04:00
Eric Lapouyade
d9b5a4d2ab v0.6.0 : DocxTemplate now accepts file-like objects 2019-06-03 16:31:45 +02:00
Eric Lapouyade
d3ddb59d1c
Merge pull request #201 from edufresne/master
#200 - fix issue with reading and writing to file-like object using replace_media
2019-06-03 16:02:26 +02:00
ericdufresne
a021c517f5 Issue- #200 2019-05-30 10:34:39 -04:00
Eric Lapouyade
b46f4f8ff5 fix #199 2019-05-23 15:58:51 +02:00
Eric Lapouyade
53ad9eedc4
Merge pull request #199 from jfcorbett/setup-encoding
Explicitly specify utf-8 encoding when opening file in setup.py
2019-05-23 15:00:30 +02:00
JEACO
c86b1ffeb5 Explicitly specify utf-8 encoding when opening file in setup.py 2019-05-23 13:53:31 +02:00
Eric Lapouyade
5c89f43eb8 Add support for file-like objects for replace_media (#197) 2019-05-23 08:37:34 +02:00
Eric Lapouyade
e956892d61
Merge pull request #198 from edufresne/master
Issue #197 - Add support for file-like objects for replace_media
2019-05-23 08:25:00 +02:00
ericdufresne
20819a7368 Fixed doc typo 2019-05-22 15:32:48 -04:00
ericdufresne
cd77712b5e Issue #197 - Add support for file-like objects for replace_media 2019-05-22 15:15:19 -04:00
Eric Lapouyade
8472d4ed55
Merge pull request #196 from mzetea/master
fixed division by zero situation
2019-05-22 17:05:24 +02:00
Mircea Zetea
a69aa8777f fixed division by zero situation 2019-05-22 17:28:13 +03:00
elapouya
7cb606e2da Fix #176 2019-05-20 09:57:03 +02:00
Eric Lapouyade
3291da7ff3
Merge pull request #177 from nickgashkov/fix/issue-176
Fix #176
2019-01-24 16:07:41 +01:00
Nick Gashkov
527de6190f Fix #176 2019-01-24 16:33:54 +03:00
Eric Lapouyade
0bbe5a80b9 Delegated autoescaping to Jinja2 Environment (#175) 2019-01-20 11:23:14 +01:00
Eric Lapouyade
53be7aa768
Merge pull request #175 from mx2048/master
Delegated autoescaping to Jinja2 Environment.
2019-01-20 10:55:37 +01:00
Max Podolskii
355432ccb0 Delegated autoescaping to Jinja2 Environment. 2019-01-17 14:29:26 -08:00
elapouya
12e709f80b Force to use python-docx 0.8.7 2019-01-11 15:13:11 +01:00
Eric Lapouyade
651b59fc21
Merge pull request #171 from anhphamduy/master
Getting undeclared variables in a template
2019-01-11 15:00:32 +01:00
Anh Pham
01825a1fa2 Add getting undeclared variables in the template 2019-01-11 15:56:03 +11:00
Eric Lapouyade
2fad92046e Added PAGE_BREAK feature (#168) 2019-01-02 11:41:41 +01:00
Eric Lapouyade
e6b7b58c92
Merge pull request #168 from mx2048/master
Added `PAGE_BREAK` feature
2019-01-02 10:35:57 +01:00
Max Podolskii
5f6a5c4d6c Added PAGE_BREAK feature: use '\f' in your text to get a manual page break rendered. 2018-12-27 22:21:46 -08:00
Eric Lapouyade
68d2fd6e84 Fixed issue #159: autoescaped values for both str and unicode. 2018-12-23 11:36:20 +01:00
Eric Lapouyade
bafdf1a310
Merge pull request #166 from mx2048/master
Fixed issue #159: autoescaped values for both str and unicode.
2018-12-23 11:23:33 +01:00
Max Podolskii
758daeeaab Fixed issue #159: autoescaped values for both str and unicode. 2018-12-19 23:13:06 -08:00
Eric Lapouyade
aa088a274b Fix tables with gridSpan that have less cells after the tc forloop (#164) 2018-12-18 08:22:36 +01:00
Eric Lapouyade
8ffc7b80e8
Merge pull request #164 from nickgashkov/fix/less-cells-after-loop-grid-span
Fix tables with gridSpan that have less cells after the tc forloop
2018-12-18 08:09:00 +01:00
Nick Gashkov
195d6a7090 Fix tables with gridSpan that still have less cells after the tc forloop 2018-12-17 15:27:14 +03:00
Eric Lapouyade
126b2a6b37
Merge pull request #163 from elapouya/revert-162-fix/less-cells-after-loop
Revert "Fix tables that have less cells after the tc forloop"
2018-12-17 09:01:00 +01:00
Eric Lapouyade
a81c7cf31a
Revert "Fix tables that have less cells after the tc forloop" 2018-12-17 08:57:41 +01:00
Eric Lapouyade
b48b6b1b82
Merge pull request #162 from nickgashkov/fix/less-cells-after-loop
Fix tables that have less cells after the tc forloop
2018-12-17 08:45:40 +01:00
Nick Gashkov
484c2c32bc Fix 'gridSpan' of tables that have less cells after the '{%tc for%}' 2018-12-13 19:30:54 +03:00
elapouya
32e39b7b67 Update doc 2018-12-05 09:28:44 +01:00
elapouya
25825a79f1 Update doc 2018-12-05 09:24:25 +01:00
elapouya
cd89aa33aa Update doc 2018-12-05 09:14:55 +01:00
elapouya
0382ece29d Update doc 2018-12-05 09:08:41 +01:00
elapouya
77043c1a66 Update doc 2018-12-05 09:07:33 +01:00
elapouya
c15ece28c1 Smart double quotes in jinja tags are now converted into simple double quotes 2018-11-21 09:41:44 +01:00
Eric Lapouyade
e5c4c14966 - Smart quotes in jinja tags are now converted into simple quotes
- Add custom jinja filter example in tests/
- Reformat the code to be a little more PEP8 compliant
2018-11-20 15:30:12 +01:00
Eric Lapouyade
20e30c541c v0.5.9 2018-11-18 17:56:14 +01:00
Eric Lapouyade
b0560c87b9
Merge pull request #158 from nickgashkov/feature/horizontal-merge
Implemented horizontal merge tag
2018-11-18 16:46:09 +01:00
Nick Gashkov
9aa6019d25 Add horizontal merge '{% hm %}' tag support 2018-11-16 16:44:12 +03:00
Eric Lapouyade
8b351236f7 fix #154 2018-11-08 12:20:09 +01:00
Eric Lapouyade
d73a47fc6b Merge remote-tracking branch 'origin/master' 2018-11-08 12:13:23 +01:00
Eric Lapouyade
01f4d16767
Merge pull request #155 from mx2048/master
Added compatibility with Python 2.7 as to the method `escape_values`.
2018-11-08 12:12:46 +01:00
Eric Lapouyade
fe7362f4d4 run tests 2018-11-08 12:11:59 +01:00
Eric Lapouyade
be2e9942bc run rests 2018-11-08 12:07:24 +01:00
Max P
4564fed0b3 Added compatibility with Python 2.7 as to the method escape_values. 2018-11-07 17:38:57 -08:00
Max P
d5335cb36f Removed import sys and corresponding code. 2018-11-07 17:18:06 -08:00
Max P
5ac9c5f2c6 Imported escape and unescape methods for Python 2.7 2018-11-07 17:14:39 -08:00
Max P
2420a560e3 Fixed SyntaxError connected with nonlocal Python3-specific keyword. 2018-11-07 17:09:04 -08:00
elapouya
bc9820e7cf Render can now autoescape context dict 2018-11-07 16:56:25 +01:00
Eric Lapouyade
69f8320fd4
Merge pull request #153 from mx2048/master
Auto escape strings
2018-11-07 16:31:17 +01:00
Max P
8dc7cc7de3 Added reminder for the test files. 2018-11-05 18:31:16 -08:00
Max P
7e72603441 Added test files for the auto escape. 2018-11-05 18:29:33 -08:00
Max P
40bf39cde7 Added method escape_values. Exclusively for Python3. 2018-11-05 18:17:43 -08:00
Max P
5ee43cf3e1 Imported unescape. 2018-11-05 17:36:05 -08:00
Eric Lapouyade
c04483e4d5 v0.5.6 - Fix #152 2018-10-18 10:07:28 +02:00
Eric Lapouyade
344fe8215d
Merge pull request #152 from devgt/master
Fix invalid xml parse because using {%vm%}
2018-10-16 15:54:51 +02:00
Vuong Quoc Binh
0c2b27584d Fix invalid xml 2018-10-16 15:13:25 +07:00
elapouya
4ea5d5fdaa Cast to string, import cgi -> html 2018-10-05 14:24:05 +02:00
elapouya
b987864c3f Declare package as python2 and python3 compatible 2018-09-19 09:47:00 +02:00
elapouya
a3b3fdfa18 Add sub/superscript in RichText 2018-09-19 09:42:48 +02:00
Eric Lapouyade
fe9a34ebbd
Merge pull request #145 from jsve/add-subscript-and-superscript-to-list-of-styling-props
added props for sub and super
2018-09-19 08:31:47 +02:00
Johan Svensson
01b2f2e397 added props for sub and super
tested on word for macOS, version 16.17 (180909). Documentation found here: http://www.datypic.com/sc/ooxml/e-w_vertAlign-1.html
2018-09-18 21:22:10 +02:00
Eric Lapouyade
b84b52e97e v0.5.2 2018-09-13 09:45:34 +02:00
Eric Lapouyade
0c02520b6d
Merge pull request #142 from nickgashkov/fix/vmerge-w-t-attributes
Fix vertical merge tag for table cells with XML attributes
2018-09-12 17:14:31 +02:00
Nick Gashkov
98ad7eebf7 Fix '{% vm %}' tag for table cells with XML attributes
Updated regex to match cell content inside '<w:t>' even if <w:t> has XML
attributes such as 'xml:space="preserve"'. Updated test to have this kind
of '<w:t>'.
2018-09-12 12:14:56 +03:00
Eric Lapouyade
4b0b1da2eb
Merge pull request #141 from nickgashkov/fix/six-print
Fix test incompatibility with Python 3 versions
2018-09-11 15:44:37 +02:00
Nick Gashkov
d6bfc8be79 Fix test incompatibility with Python 3 versions
Replaced 'print' instruction with call of a 'six' package's
implementation compatible with Python 2 as well as Python 3.
2018-09-11 16:08:19 +03:00
elapouya
f807749be5 Fix table vertical merge 2018-09-05 16:31:38 +02:00
elapouya
aa903ac99f Testing new cell vertical merge 2018-09-03 10:35:20 +02:00
Eric Lapouyade
ba1d4458ed
Merge pull request #140 from nickgashkov/fix/vmerge-nested
Fix broken tables caused by vertical merge inside complex nested tables
2018-09-03 10:20:14 +02:00
Nick Gashkov
5ae0dbbf29 Fix broken tables caused by vertical merge inside complex nested tables
Updated regex pattern to make '{% vm %}' replace a two-step process:
  - Search for <w:tc>...{% vm %}...</w:tc>;
  - Parse table cell and put jinja tags inside correct places.
2018-08-31 18:57:54 +03:00
elapouya
7f994dc107 Use 'six' in template_error.py test 2018-08-07 10:51:19 +02:00
Eric Lapouyade
cb6971009f
Merge pull request #135 from jhpyle/error-context
Added test script for TemplateError trapping
2018-08-07 10:19:57 +02:00
Jonathan Pyle
0a3627d23a Added test script for TemplateError trapping 2018-08-06 22:04:00 -04:00
elapouya
23fbe8c048 Add Hyperlink in RichText 2018-08-03 15:26:56 +02:00
Eric Lapouyade
c9afd31320
Merge pull request #134 from jhpyle/error-context
add docx file context to TemplateError exceptions
2018-08-03 11:26:21 +02:00
Jonathan Pyle
3b221fb0c6 add docx file context to TemplateError exceptions 2018-08-02 14:30:00 -04:00
Eric Lapouyade
5456b1cfad Subdocument can now be based on an existing docx 2018-06-21 15:10:11 +02:00
Eric Lapouyade
76345f454f
Merge pull request #124 from isaacsucn/master
Insert exist document as subdocument
2018-06-21 12:16:40 +02:00
isaacsucn
871dd85d49 Inset exist document as subdocument 2018-06-21 17:57:42 +08:00
Eric Lapouyade
95ffebb3fe Add font option in RichText 2018-06-07 16:42:12 +02:00
Eric Lapouyade
86350034bb
Merge pull request #123 from f1remind/master
Experimental font option for RichText objects
2018-06-07 15:53:59 +02:00
Jan-Niklas Nowak
8453ad58de Fixed Typo
Added comma after 'strike'
2018-06-06 15:26:16 +02:00
Jan-Niklas Nowak
197807d272 Experimental font option for RichText objects 2018-06-06 14:31:28 +02:00
elapouya
a3c15b226e Fix runtests.py 2018-05-16 14:03:37 +02:00
elapouya
7437268425 Better tabs and spaces management for MS Word 2016 2018-05-16 13:56:50 +02:00
Eric Lapouyade
f2e76d0e61 Add jinja2 tags test file 2018-05-15 18:21:20 +02:00
Eric Lapouyade
aa9889fc26 Manage autoscaping on InlineImage, Richtext and Subdoc 2018-05-08 14:20:43 +02:00
Eric Lapouyade
ee9cfd1d3e Purge MANIFEST.in file 2018-04-20 18:35:14 +02:00
Eric Lapouyade
fc9eb4733f Accept variables starting with 'r' in {{}} when no space after {{ 2018-02-03 10:58:33 +01:00
Eric Lapouyade
03703cb84a
Merge pull request #100 from anselal/patch-1
Fix minor typo
2017-12-20 15:28:36 +01:00
Anastasios Selalmazidis
f6bbbcf52a
Fix minor typo 2017-12-20 12:07:31 +02:00
elapouya
57fbf33ff5 Remove debug traces 2017-10-16 08:19:15 +02:00
Eric Lapouyade
3d96e6e5f1 Add {% vm %} to merge cell vertically within a loop 2017-10-15 16:29:41 +02:00
Eric Lapouyade
41671a0363 Merge pull request #93 from Arthaslixin/master
Add vertical cell merge support in a loop
2017-10-15 11:41:18 +02:00
Arthas
e35f8858f9 del .DS_Store 2017-10-15 11:01:59 +08:00
Arthas
10dba16ce9 recover order.py 2017-10-15 10:57:58 +08:00
Arthas
52df98c914 recover order.py&doc and create vertical_merge.py&doc 2017-10-15 10:52:56 +08:00
elapouya
0903bf42fc use six.iteritems() instead of iteritems for python 3 compatibility 2017-10-13 10:04:48 +02:00
elapouya
992b52f223 Fixed Bug #95 on replace_pic() method 2017-10-13 09:47:25 +02:00
Arthas
a7348b2dfb modify testfiles for virtical merge 2017-10-10 00:09:00 +08:00
Arthas
2a7630a80b add {% vm %} to make the table cell virtically merged within a {% for %} 2017-10-09 23:18:25 +08:00
Eric Lapouyade
12f759cb8e Merge pull request #92 from rgusmero/master
Added possibility to pass a file-like object to replace_pic
2017-10-06 08:15:50 +02:00
Riccardo Gusmeroli
399d8d995c Pass a file-like object to replace_pic
Added possibilitu to pass a file-like object to replace_pic
2017-10-06 08:05:02 +02:00
Riccardo Gusmeroli
a322097223 Merge pull request #1 from elapouya/master
Sync
2017-10-06 08:03:28 +02:00
Eric Lapouyade
b94fad8232 Add replace_pic() method to replace pictures from its filename (Thanks to Riccardo Gusmeroli) 2017-10-05 18:01:36 +02:00
127 changed files with 4797 additions and 875 deletions

23
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,23 @@
---
name: Bug report
about: Create a bug report to help us fix the code
title: ''
labels: bug
assignees: ''
---
## Describe the bug
A clear and concise description of what the bug is.
## To Reproduce
Please, provide a fully standalone runnable test case : minimal python code + docx template + other files if needed (images etc...)
## Expected behavior
A clear and concise description of what you expected to happen.
## Screenshots
If applicable, add screenshots to help explain your problem.
## Additional context
Add any other context about the problem here.

20
.github/ISSUE_TEMPLATE/help-request.md vendored Normal file
View File

@ -0,0 +1,20 @@
---
name: Help request
about: Ask help to the community
title: ''
labels: help wanted
assignees: ''
---
## Describe your problem
A clear and concise description of what you want to do
## More details about your problem
Steps to reproduce the behavior, expected behavior etc...
## Provide a test case
If applicable, provide minimal python code + docx template + other files (images etc...) to reproduce the behavior
## Screenshots
If applicable, add screenshots to help explain your problem.

View File

@ -0,0 +1,20 @@
---
name: Request for enhancement
about: Suggest an idea for this project
title: ''
labels: Request for enhancement
assignees: ''
---
## Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
## Describe the solution you'd like
A clear and concise description of what you want to happen.
## Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
## Additional context
Add any other context or screenshots about the feature request here.

25
.github/workflows/codestyle.yml vendored Normal file
View File

@ -0,0 +1,25 @@
# As per the example on https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions
name: Python codestyling
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Lint with flake8
run: |
pip install flake8
# stop the build if there are code styling problems. The GitHub editor is 127 chars wide.
flake8 . --count --max-line-length=127 --show-source --statistics

1
.gitignore vendored
View File

@ -21,6 +21,7 @@ var/
*.egg-info/
.installed.cfg
*.egg
tests/output/*
# PyInstaller
# Usually these files are written by a python script from a template

35
.readthedocs.yaml Normal file
View File

@ -0,0 +1,35 @@
# Read the Docs configuration file for Sphinx projects
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the OS, Python version and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.12"
# You can also specify other tool versions:
# nodejs: "20"
# rust: "1.70"
# golang: "1.20"
# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: docs/conf.py
# You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
# builder: "dirhtml"
# Fail on all warnings to avoid broken references
# fail_on_warning: true
# Optionally build your docs in additional formats such as PDF and ePub
# formats:
# - pdf
# - epub
# Optional but recommended, declare the Python requirements required
# to build your documentation
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
python:
install:
- requirements: requirements.txt

View File

@ -1,118 +1,225 @@
0.4.1 (2017-09-10)
------------------
- Improve image attachment for InlineImage ojects
0.20.2 *(Unreleased)*
-------------------
- Move docxcompose to optional dependency (Thanks to Waket Zheng)
0.4.0 (2017-09-09)
------------------
0.20.1 (2025-07-15)
-------------------
- Fix and improve get_undeclared_template_variables() method (Thanks to Pablo Esteban)
0.20.0 (2024-12-29)
-------------------
- Add RichTextParagraph (Thanks to ST-Imrie)
- Add RTL support for bold/italic (Thanks to bm-rana)
- Update documentation
0.19.1 (2024-12-29)
-------------------
- PR #575 : fix unicode in footnotes (Thanks to Jonathan Pyle)
0.19.0 (2024-11-12)
-------------------
- Support rendering variables in footnotes (Thanks to Bart Broere)
0.18.0 (2024-07-21)
-------------------
- IMPORTANT : Remove Python 2.x support
- Add hyperlink option in InlineImage (Thanks to Jean Marcos da Rosa)
- Update index.rst (Thanks to jkpet)
- Add poetry env
- Black all files
0.17.0 (2024-05-01)
-------------------
- Add support to python-docx 1.1.1
0.16.8 (2024-02-23)
-------------------
- PR #527 : upgrade Jinja2 in Pipfile.lock
0.16.7 (2023-05-08)
-------------------
- PR #493 - thanks to AdrianVorobel
0.16.6 (2023-03-12)
-------------------
- PR #482 - thanks to dreizehnutters
0.16.5 (2023-01-07)
-------------------
- PR #467 - thanks to Slarag
- fix #465
- fix #464
0.16.4 (2022-08-04)
-------------------
- Regional fonts for RichText
- Reorganize documentation
0.16.3 (2022-07-14)
-------------------
- fix #448
0.16.2 (2022-07-14)
-------------------
- fix #444
- fix #443
0.16.1 (2022-06-12)
-------------------
- PR #442
0.16.0 (2022-04-16)
-------------------
- add jinja2 comment support - Thanks to staffanm
0.15.2 (2022-01-12)
-------------------
- fix #408
- Multi-rendering with same DocxTemplate object is now possible
see tests/multi_rendering.py
- fix #392
- fix #398
0.14.1 (2021-10-01)
-------------------
- One can now use python -m docxtpl on command line
to generate a docx from a template and a json file as a context
Thanks to Lcrs123@github
0.12.0 (2021-08-15)
-------------------
- Code has be split into many files for better readability
- Use docxcomposer to attach parts when a docx file is given to create a subdoc
Images, styles etc... must now be taken in account in subdocs
- Some internal XML IDs are now renumbered to avoid collision, thus images are not randomly disapearing anymore.
- fix #372
- fix #374
- fix #375
- fix #369
- fix #368
- fix #347
- fix #181
- fix #61
0.11.5 (2021-05-09)
-------------------
- PR #351
- It is now possible to put InlineImage in header/footer
- fix #323
- fix #320
- \\n, \\a, \\t and \\f are now accepted in simple context string. Thanks to chabErch@github
0.10.5 (2020-10-15)
-------------------
- Remove extension testing (#297)
- Fix spaces missing in some cases (#116, #227)
0.9.2 (2020-04-26)
-------------------
- Fix #271
- Code styling
0.8.1 (2020-04-14)
-------------------
- fix #266
- docxtpl is now able to use latest python-docx (0.8.10). Thanks to Dutchy-@github.
0.7.0 (2020-04-09)
-------------------
- Add replace_zipname() method to replace Excel and PowerPoint embedded files
0.6.4 (2020-04-06)
-------------------
- Add the possibility to add RichText to a Richtext
- Prevent lxml from attempting to parse None
- PR #207 and #209
- Handle spaces correctly when run are split by Jinja code (#205)
- PR #203
- DocxTemplate now accepts file-like objects (Thanks to edufresne)
0.5.20 (2019-05-23)
-------------------
- Fix #199
- Add support for file-like objects for replace_media (#197)
- Fix #176
- Delegated autoescaping to Jinja2 Environment (#175)
- Force to use python-docx 0.8.7 (#170)
- Add getting undeclared variables in the template (#171)
- Added `PAGE_BREAK` feature (#168)
- Fixed issue #159: autoescaped values for both str and unicode.
- Fix tables with gridSpan that have less cells after the tc forloop (#164)
- Smart double quotes in jinja tags are now converted into simple double quotes
- Smart quotes in jinja tags are now converted into simple quotes
- Add custom jinja filter example in tests/
- Reformat the code to be a little more PEP8 compliant
- Add {% hm %} tag for table columns horizontal merging (Thanks to nickgashkov)
- Split tests/tests_files dir into templates and output dirs
- autoescape support for python 2.7
- fix issue #154
- Render can now autoescape context dict
- Fix invalid xml parse because using {% vm %}
- Cast to string non-string value given to RichText or Listing objects
- Import html.escape instead of cgi.escape (deprecated)
- Declare package as python2 and python3 compatible for wheel distrib
- Add sub/superscript in RichText
- Fix table vertical merge
- An hyperlink can now be used in RichText
0.4.13 (2018-06-21)
-------------------
- Subdocument can now be based on an existing docx
- Add font option in RichText
- Better tabs and spaces management for MS Word 2016
- Wheel distribution
- Manage autoscaping on InlineImage, Richtext and Subdoc
- Purge MANIFEST.in file
- Accept variables starting with 'r' in {{}} when no space after {{
- Remove debug traces
- Add {% vm %} to merge cell vertically within a loop (Thanks to Arthaslixin)
- use six.iteritems() instead of iteritems for python 3 compatibility
- Fixed Bug #95 on replace_pic() method
- Add replace_pic() method to replace pictures from its filename (Thanks to Riccardo Gusmeroli)
- Improve image attachment for InlineImage ojects
- Add replace_media() method (useful for header/footer images)
- Add replace_embedded() method (useful for embedding docx)
0.3.9 (2017-06-27)
------------------
- Fix exception in fix_table()
0.3.8 (2017-06-20)
------------------
- Fix bug when using more than one {{r }} or {%r %} in the same run
0.3.7 (2017-06-13)
------------------
- Fix git tag v0.3.6 was in fact for 0.3.5 package version
so create a tag 0.3.7 for 0.3.7 package version
0.3.6 (2017-06-10)
------------------
- Better head/footer jinja2 handling (Thanks to hugokernel)
0.3.5 (2017-02-20)
------------------
- Fix bug where one is using '%' (modulo operator) inside a tag
0.3.4 (2017-02-14)
------------------
- Add Listing class to manage \n and \a (new paragraph) and escape text AND keep current styling
0.3.3 (2017-02-07)
------------------
- Add {%tc } tags for dynamic table columns (Thanks to majkls23)
0.3.2 (2017-01-16)
------------------
- Remove version limitation over sphinx package in setup.py
0.3.1 (2017-01-16)
------------------
- Add PNG & JPEG in tests/test_files/
0.3.0 (2017-01-15)
------------------
- You can now add images directly without using subdoc, it is much more faster.
0.2.5 (2017-01-14)
------------------
- Add dynamic colspan tag for tables
0.2.4 (2016-11-30)
------------------
- Fix /n in RichText class
0.2.3 (2016-08-09)
------------------
- Add Python 3 support for footer and header
0.2.2 (2016-06-11)
------------------
- Fix bug when using utf-8 chracters inside footer or header in .docx template
It now detects header/footer encoding automatically
0.2.1 (2016-06-11)
------------------
- Fix bug where using subdocs is corrupting header and footer in generated docx
Thanks to Denny Weinberg for his help.
0.2.0 (2016-03-17)
------------------
- Add Header and Footer support (Thanks to Denny Weinberg)
0.1.11 (2016-03-1)
------------------
- '>' and '<' can now be used inside jinja tags
0.1.10 (2016-02-11)
-------------------
- render() accepts optionnal jinja_env argument :
useful to set custom filters and other things
0.1.9 (2016-01-18)
------------------
- better subdoc management : accept tables
0.1.8 (2015-11-05)
------------------
- better xml code cleaning around Jinja2 tags
0.1.7 (2015-09-09)
------------------
- python 3 support
0.1.6 (2015-05-11)
------------------
- remove debug code
- add lxml dependency
0.1.5 (2015-05-11)
------------------
- fix template filter with quote
0.1.4 (2015-03-27)
------------------
- add RichText support
0.1.3 (2015-03-13)
------------------
- add subdoc support
- add some exemples in tests/
0.1.2 (2015-03-12)
------------------
- First running version

View File

@ -1,7 +1,2 @@
graft docs
prune docs/_build
graft pyquery
graft tests
include *_fixt.py *.rst *.cfg *.ini
global-exclude *.pyc
global-exclude __pycache__

13
Pipfile Normal file
View File

@ -0,0 +1,13 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
[dev-packages]
docxtpl = {editable = true, path = "."}
flake8 = "*"
[requires]
python_version = "3"

419
Pipfile.lock generated Normal file
View File

@ -0,0 +1,419 @@
{
"_meta": {
"hash": {
"sha256": "0368a5e08ceb2b4910a110742515b5ff1d04a3a3af2b91b49d922ef9aaab6915"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {},
"develop": {
"babel": {
"hashes": [
"sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d",
"sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"
],
"markers": "python_version >= '3.8'",
"version": "==2.17.0"
},
"black": {
"hashes": [
"sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f",
"sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd",
"sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea",
"sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981",
"sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b",
"sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7",
"sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8",
"sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175",
"sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d",
"sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392",
"sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad",
"sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f",
"sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f",
"sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b",
"sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875",
"sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3",
"sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800",
"sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65",
"sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2",
"sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812",
"sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50",
"sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"
],
"markers": "python_version >= '3.9'",
"version": "==24.10.0"
},
"click": {
"hashes": [
"sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2",
"sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.8"
},
"docxcompose": {
"hashes": [
"sha256:bcf2799a0b63c29eb77a3d799a2f28443ae0f69f8691ff3d753f706be515c3e9"
],
"version": "==1.4.0"
},
"docxtpl": {
"editable": true,
"path": "."
},
"flake8": {
"hashes": [
"sha256:93b92ba5bdb60754a6da14fa3b93a9361fd00a59632ada61fd7b130436c40343",
"sha256:fa558ae3f6f7dbf2b4f22663e5343b6b6023620461f8d4ff2019ef4b5ee70426"
],
"index": "pypi",
"version": "==7.2.0"
},
"jinja2": {
"hashes": [
"sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d",
"sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"
],
"markers": "python_version >= '3.7'",
"version": "==3.1.6"
},
"lxml": {
"hashes": [
"sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5",
"sha256:073eb6dcdf1f587d9b88c8c93528b57eccda40209cf9be549d469b942b41d70b",
"sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49",
"sha256:0a01ce7d8479dce84fc03324e3b0c9c90b1ece9a9bb6a1b6c9025e7e4520e78c",
"sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b",
"sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba",
"sha256:0e108352e203c7afd0eb91d782582f00a0b16a948d204d4dec8565024fafeea5",
"sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7",
"sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422",
"sha256:1320091caa89805df7dcb9e908add28166113dcd062590668514dbd510798c88",
"sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8",
"sha256:14479c2ad1cb08b62bb941ba8e0e05938524ee3c3114644df905d2331c76cd57",
"sha256:151d6c40bc9db11e960619d2bf2ec5829f0aaffb10b41dcf6ad2ce0f3c0b2325",
"sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a",
"sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982",
"sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8",
"sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55",
"sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2",
"sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df",
"sha256:226046e386556a45ebc787871d6d2467b32c37ce76c2680f5c608e25823ffc84",
"sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551",
"sha256:24f6df5f24fc3385f622c0c9d63fe34604893bc1a5bdbb2dbf5870f85f9a404a",
"sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740",
"sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e",
"sha256:2b31a3a77501d86d8ade128abb01082724c0dfd9524f542f2f07d693c9f1175f",
"sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60",
"sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e",
"sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6",
"sha256:32697d2ea994e0db19c1df9e40275ffe84973e4232b5c274f47e7c1ec9763cdd",
"sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd",
"sha256:3b9c2754cef6963f3408ab381ea55f47dabc6f78f4b8ebb0f0b25cf1ac1f7609",
"sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20",
"sha256:3e6d5557989cdc3ebb5302bbdc42b439733a841891762ded9514e74f60319ad6",
"sha256:4025bf2884ac4370a3243c5aa8d66d3cb9e15d3ddd0af2d796eccc5f0244390e",
"sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61",
"sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4",
"sha256:43d549b876ce64aa18b2328faff70f5877f8c6dede415f80a2f799d31644d776",
"sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779",
"sha256:47fb24cc0f052f0576ea382872b3fc7e1f7e3028e53299ea751839418ade92a6",
"sha256:48b4afaf38bf79109bb060d9016fad014a9a48fb244e11b94f74ae366a64d252",
"sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c",
"sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92",
"sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5",
"sha256:4cd915c0fb1bed47b5e6d6edd424ac25856252f09120e3e8ba5154b6b921860e",
"sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f",
"sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54",
"sha256:50441c9de951a153c698b9b99992e806b71c1f36d14b154592580ff4a9d0d877",
"sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e",
"sha256:53370c26500d22b45182f98847243efb518d268374a9570409d2e2276232fd37",
"sha256:53d9469ab5460402c19553b56c3648746774ecd0681b1b27ea74d5d8a3ef5590",
"sha256:56dbdbab0551532bb26c19c914848d7251d73edb507c3079d6805fa8bba5b706",
"sha256:5a99d86351f9c15e4a901fc56404b485b1462039db59288b203f8c629260a142",
"sha256:5cca36a194a4eb4e2ed6be36923d3cffd03dcdf477515dea687185506583d4c9",
"sha256:5f11a1526ebd0dee85e7b1e39e39a0cc0d9d03fb527f56d8457f6df48a10dc0c",
"sha256:61c7bbf432f09ee44b1ccaa24896d21075e533cd01477966a5ff5a71d88b2f56",
"sha256:639978bccb04c42677db43c79bdaa23785dc7f9b83bfd87570da8207872f1ce5",
"sha256:63e7968ff83da2eb6fdda967483a7a023aa497d85ad8f05c3ad9b1f2e8c84987",
"sha256:664cdc733bc87449fe781dbb1f309090966c11cc0c0cd7b84af956a02a8a4729",
"sha256:67ed8a40665b84d161bae3181aa2763beea3747f748bca5874b4af4d75998f87",
"sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7",
"sha256:6854f8bd8a1536f8a1d9a3655e6354faa6406621cf857dc27b681b69860645c7",
"sha256:696ea9e87442467819ac22394ca36cb3d01848dad1be6fac3fb612d3bd5a12cf",
"sha256:6ef80aeac414f33c24b3815ecd560cee272786c3adfa5f31316d8b349bfade28",
"sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056",
"sha256:75133890e40d229d6c5837b0312abbe5bac1c342452cf0e12523477cd3aa21e7",
"sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e",
"sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0",
"sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872",
"sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079",
"sha256:7a62cc23d754bb449d63ff35334acc9f5c02e6dae830d78dab4dd12b78a524f4",
"sha256:7be701c24e7f843e6788353c055d806e8bd8466b52907bafe5d13ec6a6dbaecd",
"sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9",
"sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121",
"sha256:8f82125bc7203c5ae8633a7d5d20bcfdff0ba33e436e4ab0abc026a53a8960b7",
"sha256:91505d3ddebf268bb1588eb0f63821f738d20e1e7f05d3c647a5ca900288760b",
"sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d",
"sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76",
"sha256:9459e6892f59ecea2e2584ee1058f5d8f629446eab52ba2305ae13a32a059530",
"sha256:9776af1aad5a4b4a1317242ee2bea51da54b2a7b7b48674be736d463c999f37d",
"sha256:97dac543661e84a284502e0cf8a67b5c711b0ad5fb661d1bd505c02f8cf716d7",
"sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9",
"sha256:9b4a3bd174cc9cdaa1afbc4620c049038b441d6ba07629d89a83b408e54c35cd",
"sha256:9c886b481aefdf818ad44846145f6eaf373a20d200b5ce1a5c8e1bc2d8745410",
"sha256:9ceaf423b50ecfc23ca00b7f50b64baba85fb3fb91c53e2c9d00bc86150c7e40",
"sha256:a11a96c3b3f7551c8a8109aa65e8594e551d5a84c76bf950da33d0fb6dfafab7",
"sha256:a3bcdde35d82ff385f4ede021df801b5c4a5bcdfb61ea87caabcebfc4945dc1b",
"sha256:a7fb111eef4d05909b82152721a59c1b14d0f365e2be4c742a473c5d7372f4f5",
"sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5",
"sha256:a8c9b7f16b63e65bbba889acb436a1034a82d34fa09752d754f88d708eca80e1",
"sha256:a8ef956fce64c8551221f395ba21d0724fed6b9b6242ca4f2f7beb4ce2f41997",
"sha256:ab339536aa798b1e17750733663d272038bf28069761d5be57cb4a9b0137b4f8",
"sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc",
"sha256:aea7c06667b987787c7d1f5e1dfcd70419b711cdb47d6b4bb4ad4b76777a0563",
"sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c",
"sha256:b0989737a3ba6cf2a16efb857fb0dfa20bc5c542737fddb6d893fde48be45433",
"sha256:b108134b9667bcd71236c5a02aad5ddd073e372fb5d48ea74853e009fe38acb6",
"sha256:b12cb6527599808ada9eb2cd6e0e7d3d8f13fe7bbb01c6311255a15ded4c7ab4",
"sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4",
"sha256:b67319b4aef1a6c56576ff544b67a2a6fbd7eaee485b241cabf53115e8908b8f",
"sha256:b7c86884ad23d61b025989d99bfdd92a7351de956e01c61307cb87035960bcb1",
"sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa",
"sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f",
"sha256:bda3ea44c39eb74e2488297bb39d47186ed01342f0022c8ff407c250ac3f498e",
"sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063",
"sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4",
"sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5",
"sha256:c6364038c519dffdbe07e3cf42e6a7f8b90c275d4d1617a69bb59734c1a2d571",
"sha256:c70e93fba207106cb16bf852e421c37bbded92acd5964390aad07cb50d60f5cf",
"sha256:ca755eebf0d9e62d6cb013f1261e510317a41bf4650f22963474a663fdfe02aa",
"sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d",
"sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de",
"sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd",
"sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86",
"sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82",
"sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f",
"sha256:dc0af80267edc68adf85f2a5d9be1cdf062f973db6790c1d065e45025fa26140",
"sha256:de5b4e1088523e2b6f730d0509a9a813355b7f5659d70eb4f319c76beea2e250",
"sha256:de6f6bb8a7840c7bf216fb83eec4e2f79f7325eca8858167b68708b929ab2172",
"sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba",
"sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751",
"sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff",
"sha256:e7bc6df34d42322c5289e37e9971d6ed114e3776b45fa879f734bded9d1fea9c",
"sha256:eaf24066ad0b30917186420d51e2e3edf4b0e2ea68d8cd885b14dc8afdcf6556",
"sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44",
"sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8",
"sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7",
"sha256:fa0e294046de09acd6146be0ed6727d1f42ded4ce3ea1e9a19c11b6774eea27c",
"sha256:fb54f7c6bafaa808f27166569b1511fc42701a7713858dddc08afdde9746849e",
"sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539"
],
"markers": "python_version >= '3.6'",
"version": "==5.4.0"
},
"markupsafe": {
"hashes": [
"sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4",
"sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30",
"sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0",
"sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9",
"sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396",
"sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13",
"sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028",
"sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca",
"sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557",
"sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832",
"sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0",
"sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b",
"sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579",
"sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a",
"sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c",
"sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff",
"sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c",
"sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22",
"sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094",
"sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb",
"sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e",
"sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5",
"sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a",
"sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d",
"sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a",
"sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b",
"sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8",
"sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225",
"sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c",
"sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144",
"sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f",
"sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87",
"sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d",
"sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93",
"sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf",
"sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158",
"sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84",
"sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb",
"sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48",
"sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171",
"sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c",
"sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6",
"sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd",
"sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d",
"sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1",
"sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d",
"sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca",
"sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a",
"sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29",
"sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe",
"sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798",
"sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c",
"sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8",
"sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f",
"sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f",
"sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a",
"sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178",
"sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0",
"sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79",
"sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430",
"sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"
],
"markers": "python_version >= '3.9'",
"version": "==3.0.2"
},
"mccabe": {
"hashes": [
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
],
"markers": "python_version >= '3.6'",
"version": "==0.7.0"
},
"mypy-extensions": {
"hashes": [
"sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505",
"sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"
],
"markers": "python_version >= '3.8'",
"version": "==1.1.0"
},
"packaging": {
"hashes": [
"sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484",
"sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"
],
"markers": "python_version >= '3.8'",
"version": "==25.0"
},
"pathspec": {
"hashes": [
"sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08",
"sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"
],
"markers": "python_version >= '3.8'",
"version": "==0.12.1"
},
"platformdirs": {
"hashes": [
"sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94",
"sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"
],
"markers": "python_version >= '3.9'",
"version": "==4.3.7"
},
"pycodestyle": {
"hashes": [
"sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9",
"sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae"
],
"markers": "python_version >= '3.9'",
"version": "==2.13.0"
},
"pyflakes": {
"hashes": [
"sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a",
"sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b"
],
"markers": "python_version >= '3.9'",
"version": "==3.3.2"
},
"python-docx": {
"hashes": [
"sha256:08c20d6058916fb19853fcf080f7f42b6270d89eac9fa5f8c15f691c0017fabe",
"sha256:0cf1f22e95b9002addca7948e16f2cd7acdfd498047f1941ca5d293db7762efd"
],
"markers": "python_version >= '3.7'",
"version": "==1.1.2"
},
"setuptools": {
"hashes": [
"sha256:2e308396e1d83de287ada2c2fd6e64286008fe6aca5008e0b6a8cb0e2c86eedd",
"sha256:ea0e7655c05b74819f82e76e11a85b31779fee7c4969e82f72bab0664e8317e4"
],
"markers": "python_version >= '3.9'",
"version": "==80.1.0"
},
"six": {
"hashes": [
"sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274",
"sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.17.0"
},
"tomli": {
"hashes": [
"sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6",
"sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd",
"sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c",
"sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b",
"sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8",
"sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6",
"sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77",
"sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff",
"sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea",
"sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192",
"sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249",
"sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee",
"sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4",
"sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98",
"sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8",
"sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4",
"sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281",
"sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744",
"sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69",
"sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13",
"sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140",
"sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e",
"sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e",
"sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc",
"sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff",
"sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec",
"sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2",
"sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222",
"sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106",
"sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272",
"sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a",
"sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"
],
"markers": "python_version < '3.11'",
"version": "==2.2.1"
},
"typing-extensions": {
"hashes": [
"sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c",
"sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"
],
"markers": "python_version < '3.11'",
"version": "==4.13.2"
}
}
}

View File

@ -14,19 +14,25 @@ This package uses 2 major packages :
python-docx-template has been created because python-docx is powerful for creating documents but not for modifying them.
The idea is to begin to create an exemple of the document you want to generate with microsoft word, it can be as complex as you want :
The idea is to begin to create an example of the document you want to generate with microsoft word, it can be as complex as you want :
pictures, index tables, footer, header, variables, anything you can do with word.
Then, as you are still editing the document with microsoft word, you insert jinja2-like tags directly in the document.
You save the document as a .docx file (xml format) : it will be your .docx template file.
Now you can use python-docx-template to generate as many word documents you want from this .docx template and context variables you will associate.
Share
-----
If you like this project, please rate and share it here : http://rate.re/github/elapouya/python-docx-template
Documentation
-------------
Please, `read the doc <http://docxtpl.readthedocs.org>`_
Please, `read the doc <http://docxtpl.readthedocs.org>`_
Other projects
--------------
If you like python-docx-template, please have a look at some of my other projects :
- `django-listing <https://github.com/elapouya/django-listing>`_ : A listing/table library on steroid for Djano
- `python-textops3 <https://github.com/elapouya/python-textops3>`_ : Chainable text operations
- `django-robohash-svg <https://github.com/elapouya/django-robohash-svg>`_ : Create svg robots avatars

View File

@ -12,215 +12,215 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
"sphinx.ext.autodoc",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = '.rst'
source_suffix = ".rst"
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = u'python-docx-template'
copyright = u'2015, Eric Lapouyade'
project = "python-docx-template"
copyright = "2015, Eric Lapouyade"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.1'
version = "0.20"
# The full version, including alpha/beta/rc tags.
release = '0.1.5'
release = "0.20.x"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# keep_warnings = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
html_theme = "sphinx_book_theme"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'python-docx-templatedoc'
htmlhelp_basename = "python-docx-templatedoc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'python-docx-template.tex', u'python-docx-template Documentation',
u'Eric Lapouyade', 'manual'),
(
"index",
"python-docx-template.tex",
"python-docx-template Documentation",
"Eric Lapouyade",
"manual",
),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
@ -228,12 +228,17 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'python-docx-template', u'python-docx-template Documentation',
[u'Eric Lapouyade'], 1)
(
"index",
"python-docx-template",
"python-docx-template Documentation",
["Eric Lapouyade"],
1,
)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
@ -242,19 +247,25 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'python-docx-template', u'python-docx-template Documentation',
u'Eric Lapouyade', 'python-docx-template', 'One line description of project.',
'Miscellaneous'),
(
"index",
"python-docx-template",
"python-docx-template Documentation",
"Eric Lapouyade",
"python-docx-template",
"One line description of project.",
"Miscellaneous",
),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# texinfo_no_detailmenu = False

View File

@ -8,10 +8,14 @@ Welcome to python-docx-template's documentation!
.. rubric:: Quickstart
To install::
To install using pip::
pip install docxtpl
or using conda::
conda install docxtpl --channel conda-forge
Usage::
from docxtpl import DocxTemplate
@ -38,8 +42,6 @@ You save the document as a .docx file (xml format) : it will be your .docx templ
Now you can use python-docx-template to generate as many word documents you want from this .docx template and context variables you will associate.
Note : python-docx-template as been tested with MS Word 97, it may not work with other version.
Jinja2-like syntax
------------------
@ -49,10 +51,16 @@ Nevertheless there are some restrictions and extensions to make it work inside a
Restrictions
++++++++++++
The usual jinja2 tags, are only to be used inside a same run of a same paragraph, it can not be used across several paragraphs, table rows, runs.
The usual jinja2 tags, are only to be used inside the same run of a same paragraph, it can not be used across several paragraphs, table rows, runs.
If you want to manage paragraphs, table rows and a whole run with its style, you must use special tag syntax as explained in next chapter.
Note : a 'run' for microsoft word is a sequence of characters with the same style. For example, if you create a paragraph with all characters the same style :
word will create internally only one 'run' in the paragraph. Now, if you put in bold a text in the middle of this paragraph, word will transform the previous 'run' into 3 'runs' (normal - bold - normal).
**Note:**
a 'run' for Microsoft Word is a sequence of characters with the same style.
For example, if you create a paragraph with all characters of the same style,
MS Word will create internally only one 'run' in the paragraph. Now,
if you put in bold a text in the middle of this paragraph,
word will transform the previous 'run' into 3 different 'runs' (normal - bold - normal).
Extensions
++++++++++
@ -60,15 +68,56 @@ Extensions
Tags
....
In order to manage paragraphs, table rows, table columns, runs, special syntax has to be used ::
In order to manage paragraphs, table rows, table columns, runs, special syntax has to be used::
{%p jinja2_tag %} for paragraphs
{%tr jinja2_tag %} for table rows
{%tc jinja2_tag %} for table columns
{%r jinja2_tag %} for runs
By using these tags, python-docx-template will take care to put the real jinja2 tags at the right place into the document's xml source code.
In addition, these tags also tells python-docx-template to remove the paragraph, table row, table column or run where are located the begin and ending tags and only takes care about what is in between.
By using these tags, python-docx-template will take care to put the real jinja2 tags (without the `p`, `tr`, `tc` or `r`) at the right place into the document's xml source code.
In addition, these tags also tell python-docx-template to **remove** the paragraph, table row, table column or run where the tags are located.
For example, if you have this kind of template::
{%p if display_paragraph %}
One or many paragraphs
{%p endif %}
The first and last paragraphs (those containing ``{%p ... %}`` tags) will never appear in generated docx, regardless of the ``display_paragraph`` value.
Here only::
One or many paragraphs
will appear in generated docx if ``display_paragraph`` is True, otherwise, no paragraph at all are displayed.
**IMPORTANT :** Always put space after a starting tag delimiter and a space before the ending one :
Avoid::
{%if something%}
{%pif display_paragraph%}
Use instead::
{% if something %}
{%p if display_paragraph %}
**IMPORTANT** : Do not use ``{%p``, ``{%tr``, ``{%tc`` or ``{%r`` twice in the same
paragraph, row, column or run. Example :
Do not use this::
{%p if display_paragraph %}Here is my paragraph {%p endif %}
But use this instead in your docx template::
{%p if display_paragraph %}
Here is my paragraph
{%p endif %}
This syntax is possible because MS Word considers each line as a new paragraph (if you do not use SHIFT-RETURN).
Display variables
.................
@ -77,37 +126,100 @@ As part of jinja2, one can used double braces::
{{ <var> }}
But if ``<var>`` is an RichText object, you must specify that you are changing the actual 'run' ::
if ``<var>`` is a string, ``\n``, ``\a``, ``\t`` and ``\f`` will be translated respectively into newlines, new paragraphs, tabs and page breaks
But if ``<var>`` is a RichText_ object, you must specify that you are changing the actual 'run'::
{{r <var> }}
Note the ``r`` right after the openning braces
Note the ``r`` right after the opening braces.
Cell color
..........
**VERY IMPORTANT :** Variables must not contains characters like ``<``, ``>`` and ``&`` unless using Escaping_
There is a special case when you want to change the background color of a table cell, you must put the following tag at the very beginning of the cell ::
**IMPORTANT :** Always put space after a starting var delimiter and a space before the ending one :
{% cellbg <var> %}
Avoid::
`<var>` must contain the color's hexadecimal code *without* the hash sign
{{myvariable}}
{{rmyrichtext}}
Column spanning
...............
Use instead::
If you want to dynamically span a table cell over many column (this is useful when you have a table with a dynamic column count),
you must put the following tag at the very beginning of the cell to span ::
{{ myvariable }}
{{r myrichtext }}
Comments
........
You can add jinja-like comments in your template::
{#p this is a comment as a paragraph #}
{#tr this is a comment as a table row #}
{#tc this is a comment as a table cell #}
See tests/templates/comments_tpl.docx for an example.
Split and merge text
....................
* You can merge a jinja2 tag with previous line by using ``{%-``
* You can merge a jinja2 tag with next line by using ``-%}``
A text containing Jinja2 tags may be unreadable if too long::
My house is located {% if living_in_town %} in urban area {% else %} in countryside {% endif %} and I love it.
One can use *ENTER* or *SHIFT+ENTER* to split a text like below, then use ``{%-`` and ``-%}`` to tell docxtpl to merge the whole thing::
My house is located
{%- if living_in_town -%}
in urban area
{%- else -%}
in countryside
{%- endif -%}
and I love it.
**IMPORTANT :** Use an unbreakable space (*CTRL+SHIFT+SPACE*) when a space is wanted at line beginning or ending.
**IMPORTANT 2 :** ``{%- xxx -%}`` tags must be alone in a line : do not add some text before or after on the same line.
Escaping delimiters
...................
In order to display ``{%``, ``%}``, ``{{`` or ``}}``, one can use::
{_%, %_}, {_{ or }_}
Tables
......
Spanning
~~~~~~~~
You can span table cells horizontally in two ways, by using ``colspan`` tag (see tests/dynamic_table.py)::
{% colspan <var> %}
`<var>` must contain an integer for the number of columns to span. See tests/test_files/dynamic_table.py for an example.
Escaping
........
You can also span horizontally within a for loop (see tests/horizontal_merge.py)::
In order to display ``{%``, ``%}``, ``{{`` or ``}}``, one can use ::
{% hm %}
{_%, %_}, {_{ or }_}
You can also merge cells vertically within a for loop (see tests/vertical_merge.py)::
{% vm %}
Cell color
~~~~~~~~~~
There is a special case when you want to change the background color of a table cell, you must put the following tag at the very beginning of the cell::
{% cellbg <var> %}
`<var>` must contain the color's hexadecimal code *without* the hash sign
.. _RichText:
RichText
--------
@ -115,61 +227,139 @@ RichText
When you use ``{{ <var> }}`` tag in your template, it will be replaced by the string contained within `var` variable.
BUT it will keep the current style.
If you want to add dynamically changeable style, you have to use both : the ``{{r <var> }}`` tag AND a ``RichText`` object within `var` variable.
You can change color, bold, italic, size and so on, but the best way is to use Microsoft Word to define your own *caracter* style
You can change color, bold, italic, size, font and so on, but the best way is to use Microsoft Word to define your own *character* style
( Home tab -> modify style -> manage style button -> New style, select Character style in the form ), see example in `tests/richtext.py`
Instead of using ``RichText()``, one can use its shortcut : ``R()``
*Important* : When you use ``{{r }}`` it removes the current character styling from your docx template, this means that if
The ``RichText()`` or ``R()`` offers newline, new paragraph, and page break features : just use ``\n``, ``\a``, ``\t`` or ``\f`` in the
text, they will be converted accordingly.
There is a specific case for font: if your font is not displayed correctly, it may be because it is defined
only for a region. To know your region, it requires a little work by analyzing the document.xml inside the docx template (this is a zip file).
To specify a region, you have to prefix your font name this that region and a column::
ch = RichText('测试TEST', font='eastAsia:微软雅黑')
**Important** : When you use ``{{r }}`` it removes the current character styling from your docx template, this means that if
you do not specify a style in ``RichText()``, the style will go back to a microsoft word default style.
This will affect only character styles, not the paragraph styles (MSWord manages this 2 kind of styles).
**IMPORTANT** : Do not use 2 times ``{{r`` in the same run. Use RichText.add()
method to concatenate several strings and styles at python side and only one
``{{r`` at template side.
**Important** : ``RichText`` objects are rendered into xml *before* any filter is applied
thus ``RichText`` are not compatible with Jinja2 filters. You cannot write in your template something like ``{{r <var>|lower }}``.
Only solution is instead to do any filtering into your python code when creating the ``RichText`` object.
Hyperlink with RichText
+++++++++++++++++++++++
You can add an hyperlink to a text by using a Richtext with this syntax::
tpl=DocxTemplate('your_template.docx')
rt = RichText('You can add an hyperlink, here to ')
rt.add('google',url_id=tpl.build_url_id('http://google.com'))
Put ``rt`` in your context, then use ``{{r rt}}`` in your template
RichTextParagraph
-----------------
If you want to change paragraph properties, you can use ``RichTextParagraph()`` or ``RP()`` object.
It must be added to the template by using ``{{p <var> }}``.
Have a look to the example here ``tests/richtextparagraph.py``.
Inline image
------------
You can dynamically add one or many images into your document (tested with JPEG and PNG files).
just add ``{{ <var> }}`` tag in your template where ``<var>`` is an instance of doxtpl.InlineImage ::
just add ``{{ <var> }}`` tag in your template where ``<var>`` is an instance of doxtpl.InlineImage::
myimage = InlineImage(tpl,'test_files/python_logo.png',width=Mm(20))
myimage = InlineImage(tpl, image_descriptor='test_files/python_logo.png', width=Mm(20), height=Mm(10))
You just have to specify the template object, the image file path and optionnally width and/or height.
You just have to specify the template object, the image file path and optionally width and/or height.
For height and width you have to use millimeters (Mm), inches (Inches) or points(Pt) class.
Please see tests/inline_image.py for an example.
Sub-documents
-------------
A template variable can contain a complex and built from scratch with python-docx word document.
To do so, get first a sub-document object from template object and use it as a python-docx document object, see example in `tests/subdoc.py`.
> Need to install with the subdoc extra: `pip install "docxtpl[subdoc]"`
Escaping, newline, new paragraph, Listing
-----------------------------------------
A template variable can contain a complex subdoc object and be built from scratch using python-docx document methods.
To do so, first, get the sub-document object from your template object, then use it by treating it as a python-docx document object.
See example in `tests/subdoc.py`.
When you use a ``{{ <var> }}``, you are modifying an **XML** word document, this means you cannot use all chars,
especially ``<``, ``>`` and ``&``. In order to use them, you must escape them. There are 3 ways :
Since docxtpl V0.12.0, it is now possible to merge an existing .docx as a subdoc, just specify its path when
calling method `new_subdoc()` ::
tpl = DocxTemplate('templates/merge_docx_master_tpl.docx')
sd = tpl.new_subdoc('templates/merge_docx_subdoc.docx')
context = {
'mysubdoc': sd,
}
tpl.render(context)
tpl.save('output/merge_docx.docx')
In the above example, the content of 'templates/merge_docx_subdoc.docx' will be inserted into the parent document in place of the declared
variable `{{p mysubdoc }}`.
See `tests/merge_docx.py` for full code.
.. _Escaping:
Escaping
--------
By default, no escaping is done : read carefully this chapter if you want to avoid crashes during docx generation.
When you use a ``{{ <var> }}``, under the hood, you are modifying an **XML** word document, this means you cannot use all chars,
especially ``<``, ``>`` and ``&``. In order to use them, you must escape them. There are 4 ways :
* ``context = { 'var':R('my text') }`` and ``{{r <var> }}`` in the template (note the ``r``),
* ``context = { 'var':'my text'}`` and ``{{ <var>|e }}`` in your word template
* ``context = { 'var':escape('my text')}`` and ``{{ <var> }}`` in the template.
The ``RichText()`` or ``R()`` offers newline and new paragraph feature : just use ``\n`` or ``\a`` in the
text, they will be converted accordingly.
* enable autoescaping when calling render method: ``tpl.render(context, autoescape=True)`` (default is autoescape=False)
See tests/escape.py example for more informations.
Another solution, if you want to include a listing into your document, that is to escape the text and manage \n and \a,
Another solution, if you want to include a listing into your document, that is to escape the text and manage ``\n``, ``\a``, and ``\f``
you can use the ``Listing`` class :
in your python code ::
in your python code::
context = { 'mylisting':Listing('the listing\nwith\nsome\nlines \a and some paragraph \a and special chars : <>&') }
in your docx template just use ``{{ mylisting }}``
With ``Listing()``, you will keep the current character styling (except after a ``\a`` as you start a new paragraph).
Replace docx pictures
---------------------
It is not possible to dynamically add images in header/footer, but you can change them.
The idea is to put a dummy picture in your template, render the template as usual, then replace the dummy picture with another one.
You can do that for all medias at the same time.
Note: the aspect ratio will be the same as the replaced image
Note2 : Specify the filename that has been used to insert the image in the docx template (only its basename, not the full path)
Syntax to replace dummy_header_pic.jpg::
tpl.replace_pic('dummy_header_pic.jpg','header_pic_i_want.jpg')
The replacement occurs in headers, footers and the whole document's body.
Replace docx medias
-------------------
It is not possible to dynamically add images in header/footer, but you can change them.
The idea is to put a dummy picture in your template, render the template as usual, then replace the dummy picture with another one.
It is not possible to dynamically add other medias than images in header/footer, but you can change them.
The idea is to put a dummy media in your template, render the template as usual, then replace the dummy media with another one.
You can do that for all medias at the same time.
Note: for images, the aspect ratio will be the same as the replaced image
Note2 : it is important to have the source media files as they are required to calculate their CRC to find them in the docx.
@ -180,8 +370,9 @@ Syntax to replace dummy_header_pic.jpg::
tpl.replace_media('dummy_header_pic.jpg','header_pic_i_want.jpg')
dummy_header_pic.jpg must exist in the template directory when rendering and saving the generated docx. It must be the same
WARNING : unlike replace_pic() method, dummy_header_pic.jpg MUST exist in the template directory when rendering and saving the generated docx. It must be the same
file as the one inserted manually in the docx template.
The replacement occurs in headers, footers and the whole document's body.
Replace embedded objects
------------------------
@ -190,32 +381,134 @@ It works like medias replacement, except it is for embedded objects like embedde
Syntax to replace embedded_dummy.docx::
tpl.replace_embedded('embdded_dummy.docx','embdded_docx_i_want.docx')
tpl.replace_embedded('embedded_dummy.docx','embedded_docx_i_want.docx')
embdded_dummy.docx must exist in the template directory when rendering and saving the generated docx. It must be the same
WARNING : unlike replace_pic() method, embedded_dummy.docx MUST exist in the template directory when rendering and saving the generated docx. It must be the same
file as the one inserted manually in the docx template.
The replacement occurs in headers, footers and the whole document's body.
Note that `replace_embedded()` may not work on other documents than embedded docx.
Instead, you should use zipname replacement::
tpl.replace_zipname(
'word/embeddings/Feuille_Microsoft_Office_Excel1.xlsx',
'my_excel_file.xlsx')
The zipname is the one you can find when you open docx with WinZip, 7zip (Windows) or unzip -l (Linux).
The zipname starts with "word/embeddings/". Note that the file to be replaced is renamed by MSWord, so you have to guess a little bit...
This works for embedded MSWord file like Excel or PowerPoint file, but won't work for others like PDF, Python or even Text files :
For these ones, MSWord generate an oleObjectNNN.bin file which is no use to be replaced as it is encoded.
Get Defined Variables
---------------------
In order to get the missing variables after rendering use ::
tpl=DocxTemplate('your_template.docx')
tpl.render(context_dict)
set_of_variables = tpl.get_undeclared_template_variables(context=context_dict)
**IMPORTANT** : If `context` is not passed, you will get a set with all keys you need, e.g. to be prompted to a user or written in a file for manual processing.
Multiple rendering
------------------
Since v0.15.0, it is possible to create ``DocxTemplate`` object once and call
``render(context)`` several times. Note that if you want to use replacement
methods like ``replace_media()``, ``replace_embedded()`` and/or ``replace_zipname()``
during multiple rendering, you will have to call ``reset_replacements()``
at rendering loop start.
Microsoft Word 2016 special cases
---------------------------------
MS Word 2016 will ignore ``\t`` tabulations. This is special to that version.
Libreoffice or Wordpad do not have this problem. The same thing occurs for line
beginning with a jinja2 tag providing spaces : They will be ignored.
To solve these problem, the solution is to use Richtext::
tpl.render({
'test_space_r' : RichText(' '),
'test_tabs_r': RichText(5*'\t'),
})
And in your template, use the {{r notation::
{{r test_space_r}} Spaces will be preserved
{{r test_tabs_r}} Tabs will be displayed
Jinja custom filters
--------------------
``render()`` accepts ``jinja_env`` optionnal argument : you may pass a jinja environment object.
``render()`` accepts ``jinja_env`` optional argument : you may pass a jinja environment object.
By this way you will be able to add some custom jinja filters::
from docxtpl import DocxTemplate
import jinja2
def multiply_by(value, by):
return value * by
doc = DocxTemplate("my_word_template.docx")
context = { 'company_name' : "World company" }
context = { 'price_dollars' : 5.00 }
jinja_env = jinja2.Environment()
jinja_env.filters['myfilter'] = myfilterfunc
jinja_env.filters['multiply_by'] = multiply_by
doc.render(context,jinja_env)
doc.save("generated_doc.docx")
Then in your template, you will be able to use::
Euros price : {{ price_dollars|multiply_by(0.88) }}
Command-line execution
----------------------
One can use `docxtpl` module directly on command line to generate a docx from a template and a json file as a context::
usage: python -m docxtpl [-h] [-o] [-q] template_path json_path output_filename
Make docx file from existing template docx and json data.
positional arguments:
template_path The path to the template docx file.
json_path The path to the json file with the data.
output_filename The filename to save the generated docx.
optional arguments:
-h, --help show this help message and exit
-o, --overwrite If output file already exists, overwrites without asking
for confirmation
-q, --quiet Do not display unnecessary messages
See tests/module_execute.py for an example.
Examples
--------
The best way to see how it works is to read examples, they are located in `tests/` directory. Templates and generated .docx files are in `tests/test_files/`.
The best way to see how it works is to read examples, they are located in `tests/` directory.
Docx test templates are in `tests/templates/`. To generate final docx files::
cd tests/
python runtests.py
Generated files are located in `tests/output` directory.
If you are not sure about your python environment, python-docx-template provides Pipfiles
for that::
pip install pipenv (if not already done)
cd python-docx-template (where Pipfiles are)
pipenv install --python 3.6 -d
pipenv shell
cd tests/
python runtests.py
Share
-----

View File

@ -1,458 +1,18 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: Eric Lapouyade
'''
__version__ = '0.4.1'
from lxml import etree
from docx import Document
from docx.opc.oxml import serialize_part_xml, parse_xml
import docx.oxml.ns
from docx.opc.constants import RELATIONSHIP_TYPE as REL_TYPE
from jinja2 import Template
from cgi import escape
import re
import six
import binascii
import os
import zipfile
NEWLINE = '</w:t><w:br/><w:t xml:space="preserve">'
NEWPARAGRAPH = '</w:t></w:r></w:p><w:p><w:r><w:t xml:space="preserve">'
class DocxTemplate(object):
""" Class for managing docx files as they were jinja2 templates """
HEADER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
FOOTER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
def __init__(self, docx):
self.docx = Document(docx)
self.crc_to_new_media = {}
self.crc_to_new_embedded = {}
self.media_to_replace = {}
def __getattr__(self, name):
return getattr(self.docx, name)
def xml_to_string(self, xml, encoding='unicode'):
# Be careful : pretty_print MUST be set to False, otherwise patch_xml() won't work properly
return etree.tostring(xml, encoding='unicode', pretty_print=False)
def get_docx(self):
return self.docx
def get_xml(self):
return self.xml_to_string(self.docx._element.body)
def write_xml(self,filename):
with open(filename,'w') as fh:
fh.write(self.get_xml())
def patch_xml(self,src_xml):
# strip all xml tags inside {% %} and {{ }} that MS word can insert into xml source
src_xml = re.sub(r'(?<={)(<[^>]*>)+(?=[\{%])|(?<=[%\}])(<[^>]*>)+(?=\})','',src_xml,flags=re.DOTALL)
def striptags(m):
return re.sub('</w:t>.*?(<w:t>|<w:t [^>]*>)','',m.group(0),flags=re.DOTALL)
src_xml = re.sub(r'{%(?:(?!%}).)*|{{(?:(?!}}).)*',striptags,src_xml,flags=re.DOTALL)
# manage table cell colspan
def colspan(m):
cell_xml = m.group(1) + m.group(3)
cell_xml = re.sub(r'<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>','',cell_xml,flags=re.DOTALL)
cell_xml = re.sub(r'<w:gridSpan[^/]*/>','', cell_xml, count=1)
return re.sub(r'(<w:tcPr[^>]*>)',r'\1<w:gridSpan w:val="{{%s}}"/>' % m.group(2), cell_xml)
src_xml = re.sub(r'(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*colspan\s+([^%]*)\s*%}(.*?</w:tc>)',colspan,src_xml,flags=re.DOTALL)
# manage table cell background color
def cellbg(m):
cell_xml = m.group(1) + m.group(3)
cell_xml = re.sub(r'<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>','',cell_xml,flags=re.DOTALL)
cell_xml = re.sub(r'<w:shd[^/]*/>','', cell_xml, count=1)
return re.sub(r'(<w:tcPr[^>]*>)',r'\1<w:shd w:val="clear" w:color="auto" w:fill="{{%s}}"/>' % m.group(2), cell_xml)
src_xml = re.sub(r'(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?</w:tc>)',cellbg,src_xml,flags=re.DOTALL)
# avoid {{r and {%r tags to strip MS xml tags too far
src_xml = re.sub(r'({{r.*?}}|{%r.*?%})',r'</w:t></w:r><w:r><w:t>\1</w:t></w:r><w:r><w:t>',src_xml,flags=re.DOTALL)
for y in ['tr', 'tc', 'p', 'r']:
# replace into xml code the row/paragraph/run containing {%y xxx %} or {{y xxx}} template tag
# by {% xxx %} or {{ xx }} without any surronding <w:y> tags :
# This is mandatory to have jinja2 generating correct xml code
pat = r'<w:%(y)s[ >](?:(?!<w:%(y)s[ >]).)*({%%|{{)%(y)s ([^}%%]*(?:%%}|}})).*?</w:%(y)s>' % {'y':y}
src_xml = re.sub(pat, r'\1 \2',src_xml,flags=re.DOTALL)
def clean_tags(m):
return m.group(0).replace(r"&#8216;","'").replace('&lt;','<').replace('&gt;','>')
src_xml = re.sub(r'(?<=\{[\{%])(.*?)(?=[\}%]})',clean_tags,src_xml)
return src_xml
def render_xml(self,src_xml,context,jinja_env=None):
if jinja_env:
template = jinja_env.from_string(src_xml)
else:
template = Template(src_xml)
dst_xml = template.render(context)
dst_xml = dst_xml.replace('{_{','{{').replace('}_}','}}').replace('{_%','{%').replace('%_}','%}')
return dst_xml
def build_xml(self,context,jinja_env=None):
xml = self.get_xml()
xml = self.patch_xml(xml)
xml = self.render_xml(xml, context, jinja_env)
return xml
def map_tree(self, tree):
root = self.docx._element
body = root.body
root.replace(body, tree)
def get_headers_footers_xml(self, uri):
for relKey, val in self.docx._part._rels.items():
if val.reltype == uri:
yield relKey, self.xml_to_string(parse_xml(val._target._blob))
def get_headers_footers_encoding(self,xml):
m = re.match(r'<\?xml[^\?]+\bencoding="([^"]+)"',xml,re.I)
if m:
return m.group(1)
return 'utf-8'
def build_headers_footers_xml(self,context, uri,jinja_env=None):
for relKey, xml in self.get_headers_footers_xml(uri):
encoding = self.get_headers_footers_encoding(xml)
xml = self.patch_xml(xml)
xml = self.render_xml(xml, context, jinja_env)
yield relKey, xml.encode(encoding)
def map_headers_footers_xml(self, relKey, xml):
self.docx._part._rels[relKey]._target._blob = xml
def render(self,context,jinja_env=None):
# Body
xml_src = self.build_xml(context,jinja_env)
# fix tables if needed
tree = self.fix_tables(xml_src)
self.map_tree(tree)
# Headers
for relKey, xml in self.build_headers_footers_xml(context, self.HEADER_URI, jinja_env):
self.map_headers_footers_xml(relKey, xml)
# Footers
for relKey, xml in self.build_headers_footers_xml(context, self.FOOTER_URI, jinja_env):
self.map_headers_footers_xml(relKey, xml)
# using of TC tag in for cycle can cause that count of columns does not correspond to
# real count of columns in row. This function is able to fix it.
def fix_tables(self, xml):
tree = etree.fromstring(xml)
# get namespace
ns = '{' + tree.nsmap['w'] + '}'
# walk trough xml and find table
for t in tree.iter(ns+'tbl'):
tblGrid = t.find(ns+'tblGrid')
columns = tblGrid.findall(ns+'gridCol')
to_add = 0
# walk trough all rows and try to find if there is higher cell count
for r in t.iter(ns+'tr'):
cells = r.findall(ns+'tc')
if (len(columns) + to_add) < len(cells):
to_add = len(cells) - len(columns)
# is neccessary to add columns?
if to_add > 0:
# at first, calculate width of table according to columns
# (we want to preserve it)
width = 0.0
new_average = None
for c in columns:
if not c.get(ns+'w') == None:
width += float(c.get(ns+'w'))
# try to keep proportion of table
if width > 0:
old_average = width / len(columns)
new_average = width / (len(columns) + to_add)
# scale the old columns
for c in columns:
c.set(ns+'w', str(int(float(c.get(ns+'w')) * new_average/old_average)))
# add new columns
for i in range(to_add):
etree.SubElement(tblGrid, ns+'gridCol', {ns+'w': str(int(new_average))})
return tree
def new_subdoc(self):
return Subdoc(self)
@staticmethod
def get_file_crc(filename):
with open(filename, 'rb') as fh:
buf = fh.read()
crc = (binascii.crc32(buf) & 0xFFFFFFFF)
return crc
def replace_media(self,src_file,dst_file):
"""Replace one media by another one into a docx
This has been done mainly because it is not possible to add images in docx header/footer.
With this function, put a dummy picture in your header/footer, then specify it with its replacement in this function
Syntax: tpl.replace_media('dummy_media_to_replace.png','media_to_paste.jpg')
Note: for images, the aspect ratio will be the same as the replaced image
Note2 : it is important to have the source media file as it is required to calculate its CRC to find them in the docx
"""
with open(dst_file, 'rb') as fh:
crc = self.get_file_crc(src_file)
self.crc_to_new_media[crc] = fh.read()
def replace_pic(self,embedded_file,dst_file):
"""Replace embedded picture with original-name given by embedded_file.
The new picture is given by dst_file.
Notes:
1) embedded_file and dst_file must have the same extension/format
2) the aspect ratio will be the same as the replaced image
3) There is no need to keep the original file name (compare
function replace_embedded).
Oct 2017 - Riccardo Gusmeroli - riccardo.gusmeroli@polimi.it
"""
emp_path,emb_ext=os.path.splitext(embedded_file)
dst_path,dst_ext=os.path.splitext(dst_file)
if emb_ext!=dst_ext:
raise ValueError('replace_pic: extensions must match')
with open(dst_file, 'rb') as fh:
self.media_to_replace[embedded_file]=fh.read()
def replace_embedded(self,src_file,dst_file):
"""Replace one embdded object by another one into a docx
This has been done mainly because it is not possible to add images in docx header/footer.
With this function, put a dummy picture in your header/footer, then specify it with its replacement in this function
Syntax: tpl.replace_embedded('dummy_doc.docx','doc_to_paste.docx')
Note2 : it is important to have the source file as it is required to calculate its CRC to find them in the docx
"""
with open(dst_file, 'rb') as fh:
crc = self.get_file_crc(src_file)
self.crc_to_new_embedded[crc] = fh.read()
def post_processing(self,docx_filename):
if self.crc_to_new_media or self.crc_to_new_embedded:
backup_filename = '%s_docxtpl_before_replace_medias' % docx_filename
os.rename(docx_filename,backup_filename)
with zipfile.ZipFile(backup_filename) as zin:
with zipfile.ZipFile(docx_filename, 'w') as zout:
for item in zin.infolist():
buf = zin.read(item.filename)
if item.filename.startswith('word/media/') and item.CRC in self.crc_to_new_media:
zout.writestr(item, self.crc_to_new_media[item.CRC])
elif item.filename.startswith('word/embeddings/') and item.CRC in self.crc_to_new_embedded:
zout.writestr(item, self.crc_to_new_embedded[item.CRC])
else:
zout.writestr(item, buf)
os.remove(backup_filename)
def pre_processing(self):
if self.media_to_replace:
pic_map={}
# Main document
part=self.docx.part
pic_map.update(self._img_filename_to_part(part))
# Header/Footer
for relid, rel in self.docx.part.rels.iteritems():
if rel.reltype in (REL_TYPE.HEADER,REL_TYPE.FOOTER):
pic_map.update(self._img_filename_to_part(rel.target_part))
# Do the actual replacement
for embedded_file,stream in self.media_to_replace.iteritems():
pic_map[embedded_file][1]._blob=stream
def _img_filename_to_part(self,doc_part):
et=etree.fromstring(doc_part.blob)
part_map={}
vinl=et.xpath('//w:p/w:r/w:drawing/wp:inline',namespaces=docx.oxml.ns.nsmap)
for inl in vinl:
rel=None
# Either IMAGE, CHART, SMART_ART, ...
try:
gd=inl.xpath('a:graphic/a:graphicData',namespaces=docx.oxml.ns.nsmap)[0]
if gd.attrib['uri']==docx.oxml.ns.nsmap['pic']:
# Either PICTURE or LINKED_PICTURE image
blip=gd.xpath('pic:pic/pic:blipFill/a:blip',namespaces=docx.oxml.ns.nsmap)[0]
dest=blip.xpath('@r:embed',namespaces=docx.oxml.ns.nsmap)
if len(dest)>0:
rel=dest[0]
else:
continue
else:
continue
#title=inl.xpath('wp:docPr/@title',namespaces=docx.oxml.ns.nsmap)[0]
name=gd.xpath('pic:pic/pic:nvPicPr/pic:cNvPr/@name',namespaces=docx.oxml.ns.nsmap)[0]
part_map[name]=(doc_part.rels[rel].target_ref,doc_part.rels[rel].target_part)
except:
continue
return part_map
def save(self,filename,*args,**kwargs):
self.pre_processing()
self.docx.save(filename,*args,**kwargs)
self.post_processing(filename)
class Subdoc(object):
""" Class for subdocument to insert into master document """
def __init__(self, tpl):
self.tpl = tpl
self.docx = tpl.get_docx()
self.subdocx = Document()
self.subdocx._part = self.docx._part
def __getattr__(self, name) :
return getattr(self.subdocx, name)
def _get_xml(self):
if self.subdocx._element.body.sectPr is not None:
self.subdocx._element.body.remove(self.subdocx._element.body.sectPr)
xml = re.sub(r'</?w:body[^>]*>','',etree.tostring(self.subdocx._element.body, encoding='unicode', pretty_print=False))
return xml
def __unicode__(self):
return self._get_xml()
def __str__(self):
return self._get_xml()
class RichText(object):
""" class to generate Rich Text when using templates variables
This is much faster than using Subdoc class, but this only for texts INSIDE an existing paragraph.
"""
def __init__(self, text=None, **text_prop):
self.xml = ''
if text:
self.add(text, **text_prop)
def add(self, text, style=None,
color=None,
highlight=None,
size=None,
bold=False,
italic=False,
underline=False,
strike=False):
if not isinstance(text, six.text_type):
text = text.decode('utf-8',errors='ignore')
text = escape(text).replace('\n',NEWLINE).replace('\a',NEWPARAGRAPH)
prop = u''
if style:
prop += u'<w:rStyle w:val="%s"/>' % style
if color:
if color[0] == '#':
color = color[1:]
prop += u'<w:color w:val="%s"/>' % color
if highlight:
if highlight[0] == '#':
highlight = highlight[1:]
prop += u'<w:highlight w:val="%s"/>' % highlight
if size:
prop += u'<w:sz w:val="%s"/>' % size
prop += u'<w:szCs w:val="%s"/>' % size
if bold:
prop += u'<w:b/>'
if italic:
prop += u'<w:i/>'
if underline:
if underline not in ['single','double']:
underline = 'single'
prop += u'<w:u w:val="%s"/>' % underline
if strike:
prop += u'<w:strike/>'
self.xml += u'<w:r>'
if prop:
self.xml += u'<w:rPr>%s</w:rPr>' % prop
self.xml += u'<w:t xml:space="preserve">%s</w:t></w:r>' % text
def __unicode__(self):
return self.xml
def __str__(self):
return self.xml
R = RichText
class Listing(object):
r"""class to manage \n and \a without to use RichText, by this way you keep the current template styling
use {{ mylisting }} in your template and context={ mylisting:Listing(the_listing_with_newlines) }
"""
def __init__(self, text):
self.xml = escape(text).replace('\n',NEWLINE).replace('\a',NEWPARAGRAPH)
def __unicode__(self):
return self.xml
def __str__(self):
return self.xml
class InlineImage(object):
"""Class to generate an inline image
This is much faster than using Subdoc class.
"""
tpl = None
image_descriptor = None
width = None
height = None
def __init__(self, tpl, image_descriptor, width=None, height=None):
self.tpl, self.image_descriptor = tpl, image_descriptor
self.width, self.height = width, height
def _insert_image(self):
pic = self.tpl.docx._part.new_pic_inline(
self.image_descriptor,
self.width,
self.height
).xml
return '</w:t></w:r><w:r><w:drawing>%s</w:drawing></w:r><w:r>' \
'<w:t xml:space="preserve">' % pic
def __unicode__(self):
return self._insert_image()
def __str__(self):
return self._insert_image()
"""
__version__ = "0.20.1"
# flake8: noqa
from .inline_image import InlineImage
from .listing import Listing
from .richtext import RichText, R, RichTextParagraph, RP
from .template import DocxTemplate
try:
from .subdoc import Subdoc
except ImportError:
pass

175
docxtpl/__main__.py Normal file
View File

@ -0,0 +1,175 @@
import argparse
import json
import os
from .template import DocxTemplate, TemplateError
TEMPLATE_ARG = "template_path"
JSON_ARG = "json_path"
OUTPUT_ARG = "output_filename"
OVERWRITE_ARG = "overwrite"
QUIET_ARG = "quiet"
def make_arg_parser():
parser = argparse.ArgumentParser(
usage="python -m docxtpl [-h] [-o] [-q] {} {} {}".format(
TEMPLATE_ARG, JSON_ARG, OUTPUT_ARG
),
description="Make docx file from existing template docx and json data.",
)
parser.add_argument(
TEMPLATE_ARG, type=str, help="The path to the template docx file."
)
parser.add_argument(
JSON_ARG, type=str, help="The path to the json file with the data."
)
parser.add_argument(
OUTPUT_ARG, type=str, help="The filename to save the generated docx."
)
parser.add_argument(
"-" + OVERWRITE_ARG[0],
"--" + OVERWRITE_ARG,
action="store_true",
help="If output file already exists, overwrites without asking for confirmation",
)
parser.add_argument(
"-" + QUIET_ARG[0],
"--" + QUIET_ARG,
action="store_true",
help="Do not display unnecessary messages",
)
return parser
def get_args(parser):
try:
parsed_args = vars(parser.parse_args())
return parsed_args
# Argument errors raise a SystemExit with code 2. Normal usage of the
# --help or -h flag raises a SystemExit with code 0.
except SystemExit as e:
if e.code == 0:
raise SystemExit
else:
raise RuntimeError(
"Correct usage is:\n{parser.usage}".format(parser=parser)
)
def is_argument_valid(arg_name, arg_value, overwrite):
# Basic checks for the arguments
if arg_name == TEMPLATE_ARG:
return os.path.isfile(arg_value) and arg_value.endswith(".docx")
elif arg_name == JSON_ARG:
return os.path.isfile(arg_value) and arg_value.endswith(".json")
elif arg_name == OUTPUT_ARG:
return arg_value.endswith(".docx") and check_exists_ask_overwrite(
arg_value, overwrite
)
elif arg_name in [OVERWRITE_ARG, QUIET_ARG]:
return arg_value in [True, False]
def check_exists_ask_overwrite(arg_value, overwrite):
# If output file does not exist or command was run with overwrite option,
# returns True, else asks for overwrite confirmation. If overwrite is
# confirmed returns True, else raises OSError.
if os.path.exists(arg_value) and not overwrite:
try:
msg = (
"File %s already exists, would you like to overwrite the existing file? "
"(y/n)" % arg_value
)
if input(msg).lower() == "y":
return True
else:
raise OSError
except OSError:
raise RuntimeError(
"File %s already exists, please choose a different name." % arg_value
)
else:
return True
def validate_all_args(parsed_args):
overwrite = parsed_args[OVERWRITE_ARG]
# Raises AssertionError if any of the arguments is not validated
try:
for arg_name, arg_value in parsed_args.items():
if not is_argument_valid(arg_name, arg_value, overwrite):
raise AssertionError
except AssertionError:
raise RuntimeError(
'The specified {arg_name} "{arg_value}" is not valid.'.format(
arg_name=arg_name, arg_value=arg_value
)
)
def get_json_data(json_path):
with open(json_path) as file:
try:
json_data = json.load(file)
return json_data
except json.JSONDecodeError as e:
print(
"There was an error on line {e.lineno}, column {e.colno} while trying "
"to parse file {json_path}".format(e=e, json_path=json_path)
)
raise RuntimeError("Failed to get json data.")
def make_docxtemplate(template_path):
try:
return DocxTemplate(template_path)
except TemplateError:
raise RuntimeError("Could not create docx template.")
def render_docx(doc, json_data):
try:
doc.render(json_data)
return doc
except TemplateError:
raise RuntimeError("An error ocurred while trying to render the docx")
def save_file(doc, parsed_args):
try:
output_path = parsed_args[OUTPUT_ARG]
doc.save(output_path)
if not parsed_args[QUIET_ARG]:
print(
"Document successfully generated and saved at {output_path}".format(
output_path=output_path
)
)
except OSError as e:
print("{e.strerror}. Could not save file {e.filename}.".format(e=e))
raise RuntimeError("Failed to save file.")
def main():
parser = make_arg_parser()
# Everything is in a try-except block that catches a RuntimeError that is
# raised if any of the individual functions called cause an error
# themselves, terminating the main function.
parsed_args = get_args(parser)
try:
validate_all_args(parsed_args)
json_data = get_json_data(os.path.abspath(parsed_args[JSON_ARG]))
doc = make_docxtemplate(os.path.abspath(parsed_args[TEMPLATE_ARG]))
doc = render_docx(doc, json_data)
save_file(doc, parsed_args)
except RuntimeError as e:
print("Error: " + e.__str__())
return
finally:
if not parsed_args[QUIET_ARG]:
print("Exiting program!")
if __name__ == "__main__":
main()

78
docxtpl/inline_image.py Normal file
View File

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
"""
Created : 2021-07-30
@author: Eric Lapouyade
"""
from docx.oxml import OxmlElement, parse_xml
from docx.oxml.ns import qn
class InlineImage(object):
"""Class to generate an inline image
This is much faster than using Subdoc class.
"""
tpl = None
image_descriptor = None
width = None
height = None
anchor = None
def __init__(self, tpl, image_descriptor, width=None, height=None, anchor=None):
self.tpl, self.image_descriptor = tpl, image_descriptor
self.width, self.height = width, height
self.anchor = anchor
def _add_hyperlink(self, run, url, part):
# Create a relationship for the hyperlink
r_id = part.relate_to(
url,
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
is_external=True,
)
# Find the <wp:docPr> and <pic:cNvPr> element
docPr = run.xpath(".//wp:docPr")[0]
cNvPr = run.xpath(".//pic:cNvPr")[0]
# Create the <a:hlinkClick> element
hlinkClick1 = OxmlElement("a:hlinkClick")
hlinkClick1.set(qn("r:id"), r_id)
hlinkClick2 = OxmlElement("a:hlinkClick")
hlinkClick2.set(qn("r:id"), r_id)
# Insert the <a:hlinkClick> element right after the <wp:docPr> element
docPr.append(hlinkClick1)
cNvPr.append(hlinkClick2)
return run
def _insert_image(self):
pic = self.tpl.current_rendering_part.new_pic_inline(
self.image_descriptor,
self.width,
self.height,
).xml
if self.anchor:
run = parse_xml(pic)
if run.xpath(".//a:blip"):
hyperlink = self._add_hyperlink(
run, self.anchor, self.tpl.current_rendering_part
)
pic = hyperlink.xml
return (
"</w:t></w:r><w:r><w:drawing>%s</w:drawing></w:r><w:r>"
'<w:t xml:space="preserve">' % pic
)
def __unicode__(self):
return self._insert_image()
def __str__(self):
return self._insert_image()
def __html__(self):
return self._insert_image()

35
docxtpl/listing.py Normal file
View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
"""
Created : 2021-07-30
@author: Eric Lapouyade
"""
try:
from html import escape
except ImportError:
# cgi.escape is deprecated in python 3.7
from cgi import escape
class Listing(object):
r"""class to manage \n and \a without to use RichText,
by this way you keep the current template styling
use {{ mylisting }} in your template and
context={ mylisting:Listing(the_listing_with_newlines) }
"""
def __init__(self, text):
# If not a string : cast to string (ex: int, dict etc...)
if not isinstance(text, (str, bytes)):
text = str(text)
self.xml = escape(text)
def __unicode__(self):
return self.xml
def __str__(self):
return self.xml
def __html__(self):
return self.xml

180
docxtpl/richtext.py Normal file
View File

@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
"""
Created : 2021-07-30
@author: Eric Lapouyade
"""
try:
from html import escape
except ImportError:
# cgi.escape is deprecated in python 3.7
from cgi import escape
class RichText(object):
"""class to generate Rich Text when using templates variables
This is much faster than using Subdoc class,
but this only for texts INSIDE an existing paragraph.
"""
def __init__(self, text=None, **text_prop):
self.xml = ""
if text:
self.add(text, **text_prop)
def add(
self,
text,
style=None,
color=None,
highlight=None,
size=None,
subscript=None,
superscript=None,
bold=False,
italic=False,
underline=False,
strike=False,
font=None,
url_id=None,
rtl=False,
lang=None,
):
# If a RichText is added
if isinstance(text, RichText):
self.xml += text.xml
return
# # If nothing to add : just return
# if text is None or text == "":
# return
# If not a string : cast to string (ex: int, dict etc...)
if not isinstance(text, (str, bytes)):
text = str(text)
if not isinstance(text, str):
text = text.decode("utf-8", errors="ignore")
text = escape(text)
prop = ""
if style:
prop += '<w:rStyle w:val="%s"/>' % style
if color:
if color[0] == "#":
color = color[1:]
prop += '<w:color w:val="%s"/>' % color
if highlight:
if highlight[0] == "#":
highlight = highlight[1:]
prop += '<w:shd w:fill="%s"/>' % highlight
if size:
prop += '<w:sz w:val="%s"/>' % size
prop += '<w:szCs w:val="%s"/>' % size
if subscript:
prop += '<w:vertAlign w:val="subscript"/>'
if superscript:
prop += '<w:vertAlign w:val="superscript"/>'
if bold:
prop += "<w:b/>"
if rtl:
prop += "<w:bCs/>"
if italic:
prop += "<w:i/>"
if rtl:
prop += "<w:iCs/>"
if underline:
if underline not in [
"single",
"double",
"thick",
"dotted",
"dash",
"dotDash",
"dotDotDash",
"wave",
]:
underline = "single"
prop += '<w:u w:val="%s"/>' % underline
if strike:
prop += "<w:strike/>"
if font:
regional_font = ""
if ":" in font:
region, font = font.split(":", 1)
regional_font = ' w:{region}="{font}"'.format(font=font, region=region)
prop += '<w:rFonts w:ascii="{font}" w:hAnsi="{font}" w:cs="{font}"{regional_font}/>'.format(
font=font, regional_font=regional_font
)
if rtl:
prop += '<w:rtl w:val="true"/>'
if lang:
prop += '<w:lang w:val="%s"/>' % lang
xml = "<w:r>"
if prop:
xml += "<w:rPr>%s</w:rPr>" % prop
xml += '<w:t xml:space="preserve">%s</w:t></w:r>' % text
if url_id:
xml = '<w:hyperlink r:id="%s" w:tgtFrame="_blank">%s</w:hyperlink>' % (
url_id,
xml,
)
self.xml += xml
def __unicode__(self):
return self.xml
def __str__(self):
return self.xml
def __html__(self):
return self.xml
class RichTextParagraph(object):
"""class to generate Rich Text Paragraphs when using templates variables
This is much faster than using Subdoc class,
but this only for texts OUTSIDE an existing paragraph.
"""
def __init__(self, text=None, **text_prop):
self.xml = ""
if text:
self.add(text, **text_prop)
def add(
self,
text,
parastyle=None,
):
# If a RichText is added
if not isinstance(text, RichText):
text = RichText(text)
prop = ""
if parastyle:
prop += '<w:pStyle w:val="%s"/>' % parastyle
xml = "<w:p>"
if prop:
xml += "<w:pPr>%s</w:pPr>" % prop
xml += text.xml
xml += "</w:p>"
self.xml += xml
def __unicode__(self):
return self.xml
def __str__(self):
return self.xml
def __html__(self):
return self.xml
R = RichText
RP = RichTextParagraph

103
docxtpl/subdoc.py Normal file
View File

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
"""
Created : 2021-07-30
@author: Eric Lapouyade
"""
from docx import Document
from docx.oxml import CT_SectPr
from docx.opc.constants import RELATIONSHIP_TYPE as RT
from docxcompose.properties import CustomProperties
from docxcompose.utils import xpath
from docxcompose.composer import Composer
from docxcompose.utils import NS
from lxml import etree
import re
class SubdocComposer(Composer):
def attach_parts(self, doc, remove_property_fields=True):
"""Attach docx parts instead of appending the whole document
thus subdoc insertion can be delegated to jinja2"""
self.reset_reference_mapping()
# Remove custom property fields but keep the values
if remove_property_fields:
cprops = CustomProperties(doc)
for name in cprops.keys():
cprops.dissolve_fields(name)
self._create_style_id_mapping(doc)
for element in doc.element.body:
if isinstance(element, CT_SectPr):
continue
self.add_referenced_parts(doc.part, self.doc.part, element)
self.add_styles(doc, element)
self.add_numberings(doc, element)
self.restart_first_numbering(doc, element)
self.add_images(doc, element)
self.add_diagrams(doc, element)
self.add_shapes(doc, element)
self.add_footnotes(doc, element)
self.remove_header_and_footer_references(doc, element)
self.add_styles_from_other_parts(doc)
self.renumber_bookmarks()
self.renumber_docpr_ids()
self.renumber_nvpicpr_ids()
self.fix_section_types(doc)
def add_diagrams(self, doc, element):
# While waiting docxcompose 1.3.3
dgm_rels = xpath(element, ".//dgm:relIds[@r:dm]")
for dgm_rel in dgm_rels:
for item, rt_type in (
("dm", RT.DIAGRAM_DATA),
("lo", RT.DIAGRAM_LAYOUT),
("qs", RT.DIAGRAM_QUICK_STYLE),
("cs", RT.DIAGRAM_COLORS),
):
dm_rid = dgm_rel.get("{%s}%s" % (NS["r"], item))
dm_part = doc.part.rels[dm_rid].target_part
new_rid = self.doc.part.relate_to(dm_part, rt_type)
dgm_rel.set("{%s}%s" % (NS["r"], item), new_rid)
class Subdoc(object):
"""Class for subdocument to insert into master document"""
def __init__(self, tpl, docpath=None):
self.tpl = tpl
self.docx = tpl.get_docx()
self.subdocx = Document(docpath)
if docpath:
compose = SubdocComposer(self.docx)
compose.attach_parts(self.subdocx)
else:
self.subdocx._part = self.docx._part
def __getattr__(self, name):
return getattr(self.subdocx, name)
def _get_xml(self):
if self.subdocx.element.body.sectPr is not None:
self.subdocx.element.body.remove(self.subdocx.element.body.sectPr)
xml = re.sub(
r"</?w:body[^>]*>",
"",
etree.tostring(
self.subdocx.element.body, encoding="unicode", pretty_print=False
),
)
return xml
def __unicode__(self):
return self._get_xml()
def __str__(self):
return self._get_xml()
def __html__(self):
return self._get_xml()

926
docxtpl/template.py Normal file
View File

@ -0,0 +1,926 @@
# -*- coding: utf-8 -*-
"""
Created : 2015-03-12
@author: Eric Lapouyade
"""
from os import PathLike
from typing import TYPE_CHECKING, Any, Optional, IO, Union, Dict, Set
import functools
import io
from lxml import etree
from docx import Document
from docx.opc.oxml import parse_xml
from docx.opc.part import XmlPart
import docx.oxml.ns
from docx.opc.constants import RELATIONSHIP_TYPE as REL_TYPE
from jinja2 import Environment, Template, meta
from jinja2.exceptions import TemplateError
try:
from html import escape # noqa: F401
except ImportError:
# cgi.escape is deprecated in python 3.7
from cgi import escape # noqa: F401
import re
import binascii
import os
import zipfile
if TYPE_CHECKING:
from .subdoc import Subdoc
class DocxTemplate(object):
"""Class for managing docx files as they were jinja2 templates"""
HEADER_URI = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
)
FOOTER_URI = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
)
def __init__(self, template_file: Union[IO[bytes], str, PathLike]) -> None:
self.template_file = template_file
self.reset_replacements()
self.docx = None
self.is_rendered = False
self.is_saved = False
self.allow_missing_pics = False
def init_docx(self, reload: bool = True):
if not self.docx or (self.is_rendered and reload):
self.docx = Document(self.template_file)
self.is_rendered = False
def render_init(self):
self.init_docx()
self.pic_map = {}
self.current_rendering_part = None
self.docx_ids_index = 1000
self.is_saved = False
def __getattr__(self, name):
return getattr(self.docx, name)
def xml_to_string(self, xml, encoding="unicode"):
# Be careful : pretty_print MUST be set to False, otherwise patch_xml()
# won't work properly
return etree.tostring(xml, encoding="unicode", pretty_print=False)
def get_docx(self):
self.init_docx()
return self.docx
def get_xml(self):
return self.xml_to_string(self.docx._element.body)
def write_xml(self, filename):
with open(filename, "w") as fh:
fh.write(self.get_xml())
def patch_xml(self, src_xml):
"""Make a lots of cleaning to have a raw xml understandable by jinja2 :
strip all unnecessary xml tags, manage table cell background color and colspan,
unescape html entities, etc..."""
# replace {<something>{ by {{ ( works with {{ }} {% and %} {# and #})
src_xml = re.sub(
r"(?<={)(<[^>]*>)+(?=[\{%\#])|(?<=[%\}\#])(<[^>]*>)+(?=\})",
"",
src_xml,
flags=re.DOTALL,
)
# replace {{<some tags>jinja2 stuff<some other tags>}} by {{jinja2 stuff}}
# same thing with {% ... %} and {# #}
# "jinja2 stuff" could a variable, a 'if' etc... anything jinja2 will understand
def striptags(m):
return re.sub(
"</w:t>.*?(<w:t>|<w:t [^>]*>)", "", m.group(0), flags=re.DOTALL
)
src_xml = re.sub(
r"{%(?:(?!%}).)*|{#(?:(?!#}).)*|{{(?:(?!}}).)*",
striptags,
src_xml,
flags=re.DOTALL,
)
# manage table cell colspan
def colspan(m):
cell_xml = m.group(1) + m.group(3)
cell_xml = re.sub(
r"<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>",
"",
cell_xml,
flags=re.DOTALL,
)
cell_xml = re.sub(r"<w:gridSpan[^/]*/>", "", cell_xml, count=1)
return re.sub(
r"(<w:tcPr[^>]*>)",
r'\1<w:gridSpan w:val="{{%s}}"/>' % m.group(2),
cell_xml,
)
src_xml = re.sub(
r"(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*colspan\s+([^%]*)\s*%}(.*?</w:tc>)",
colspan,
src_xml,
flags=re.DOTALL,
)
# manage table cell background color
def cellbg(m):
cell_xml = m.group(1) + m.group(3)
cell_xml = re.sub(
r"<w:r[ >](?:(?!<w:r[ >]).)*<w:t></w:t>.*?</w:r>",
"",
cell_xml,
flags=re.DOTALL,
)
cell_xml = re.sub(r"<w:shd[^/]*/>", "", cell_xml, count=1)
return re.sub(
r"(<w:tcPr[^>]*>)",
r'\1<w:shd w:val="clear" w:color="auto" w:fill="{{%s}}"/>' % m.group(2),
cell_xml,
)
src_xml = re.sub(
r"(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?</w:tc>)",
cellbg,
src_xml,
flags=re.DOTALL,
)
# ensure space preservation
src_xml = re.sub(
r"<w:t>((?:(?!<w:t>).)*)({{.*?}}|{%.*?%})",
r'<w:t xml:space="preserve">\1\2',
src_xml,
flags=re.DOTALL,
)
src_xml = re.sub(
r"({{r\s.*?}}|{%r\s.*?%})",
r'</w:t></w:r><w:r><w:t xml:space="preserve">\1</w:t></w:r><w:r><w:t xml:space="preserve">',
src_xml,
flags=re.DOTALL,
)
# {%- will merge with previous paragraph text
src_xml = re.sub(r"</w:t>(?:(?!</w:t>).)*?{%-", "{%", src_xml, flags=re.DOTALL)
# -%} will merge with next paragraph text
src_xml = re.sub(
r"-%}(?:(?!<w:t[ >]|{%|{{).)*?<w:t[^>]*?>", "%}", src_xml, flags=re.DOTALL
)
for y in ["tr", "tc", "p", "r"]:
# replace into xml code the row/paragraph/run containing
# {%y xxx %} or {{y xxx}} template tag
# by {% xxx %} or {{ xx }} without any surrounding <w:y> tags :
# This is mandatory to have jinja2 generating correct xml code
pat = (
r"<w:%(y)s[ >](?:(?!<w:%(y)s[ >]).)*({%%|{{)%(y)s ([^}%%]*(?:%%}|}})).*?</w:%(y)s>"
% {"y": y}
)
src_xml = re.sub(pat, r"\1 \2", src_xml, flags=re.DOTALL)
for y in ["tr", "tc", "p"]:
# same thing, but for {#y xxx #} (but not where y == 'r', since that
# makes less sense to use comments in that context
pat = (
r"<w:%(y)s[ >](?:(?!<w:%(y)s[ >]).)*({#)%(y)s ([^}#]*(?:#})).*?</w:%(y)s>"
% {"y": y}
)
src_xml = re.sub(pat, r"\1 \2", src_xml, flags=re.DOTALL)
# add vMerge
# use {% vm %} to make this table cell and its copies
# be vertically merged within a {% for %}
def v_merge_tc(m):
def v_merge(m1):
return (
'<w:vMerge w:val="{% if loop.first %}restart{% else %}continue{% endif %}"/>'
+ m1.group(1) # Everything between ``</w:tcPr>`` and ``<w:t>``.
+ "{% if loop.first %}"
+ m1.group(2) # Everything before ``{% vm %}``.
+ m1.group(3) # Everything after ``{% vm %}``.
+ "{% endif %}"
+ m1.group(4) # ``</w:t>``.
)
return re.sub(
r"(</w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*vm\s*%})(.*?)(</w:t>)",
v_merge,
m.group(),
# Everything between ``</w:tc>`` and ``</w:tc>`` with ``{% vm %}`` inside.
flags=re.DOTALL,
)
src_xml = re.sub(
r"<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*vm\s*%}.*?</w:tc[ >]",
v_merge_tc,
src_xml,
flags=re.DOTALL,
)
# Use ``{% hm %}`` to make table cell become horizontally merged within
# a ``{% for %}``.
def h_merge_tc(m):
xml_to_patch = (
m.group()
) # Everything between ``</w:tc>`` and ``</w:tc>`` with ``{% hm %}`` inside.
def with_gridspan(m1):
return (
m1.group(1) # ``w:gridSpan w:val="``.
+ "{{ "
+ m1.group(2)
+ " * loop.length }}" # Content of ``w:val``, multiplied by loop length.
+ m1.group(3) # Closing quotation mark.
)
def without_gridspan(m2):
return (
'<w:gridSpan w:val="{{ loop.length }}"/>'
+ m2.group(1) # Everything between ``</w:tcPr>`` and ``<w:t>``.
+ m2.group(2) # Everything before ``{% hm %}``.
+ m2.group(3) # Everything after ``{% hm %}``.
+ m2.group(4) # ``</w:t>``.
)
if re.search(r"w:gridSpan", xml_to_patch):
# Simple case, there's already ``gridSpan``, multiply its value.
xml = re.sub(
r'(w:gridSpan w:val=")(\d+)(")',
with_gridspan,
xml_to_patch,
flags=re.DOTALL,
)
xml = re.sub(
r"{%\s*hm\s*%}",
"",
xml, # Patched xml.
flags=re.DOTALL,
)
else:
# There're no ``gridSpan``, add one.
xml = re.sub(
r"(</w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*hm\s*%})(.*?)(</w:t>)",
without_gridspan,
xml_to_patch,
flags=re.DOTALL,
)
# Discard every other cell generated in loop.
return "{% if loop.first %}" + xml + "{% endif %}"
src_xml = re.sub(
r"<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*hm\s*%}.*?</w:tc[ >]",
h_merge_tc,
src_xml,
flags=re.DOTALL,
)
def clean_tags(m):
return (
m.group(0)
.replace(r"&#8216;", "'")
.replace("&lt;", "<")
.replace("&gt;", ">")
.replace("", '"')
.replace("", '"')
.replace("", "'")
.replace("", "'")
)
src_xml = re.sub(r"(?<=\{[\{%])(.*?)(?=[\}%]})", clean_tags, src_xml)
return src_xml
def render_xml_part(self, src_xml, part, context, jinja_env=None):
src_xml = re.sub(r"<w:p([ >])", r"\n<w:p\1", src_xml)
try:
self.current_rendering_part = part
if jinja_env:
template = jinja_env.from_string(src_xml)
else:
template = Template(src_xml)
dst_xml = template.render(context)
except TemplateError as exc:
if hasattr(exc, "lineno") and exc.lineno is not None:
line_number = max(exc.lineno - 4, 0)
exc.docx_context = map(
lambda x: re.sub(r"<[^>]+>", "", x),
src_xml.splitlines()[line_number: (line_number + 7)], # fmt: skip
)
raise exc
dst_xml = re.sub(r"\n<w:p([ >])", r"<w:p\1", dst_xml)
dst_xml = (
dst_xml.replace("{_{", "{{")
.replace("}_}", "}}")
.replace("{_%", "{%")
.replace("%_}", "%}")
)
dst_xml = self.resolve_listing(dst_xml)
return dst_xml
def render_properties(
self, context: Dict[str, Any], jinja_env: Optional[Environment] = None
) -> None:
# List of string attributes of docx.opc.coreprops.CoreProperties which are strings.
# It seems that some attributes cannot be written as strings. Those are commented out.
properties = [
"author",
# 'category',
"comments",
# 'content_status',
"identifier",
# 'keywords',
"language",
# 'last_modified_by',
"subject",
"title",
# 'version',
]
if jinja_env is None:
jinja_env = Environment()
for prop in properties:
initial = getattr(self.docx.core_properties, prop)
template = jinja_env.from_string(initial)
rendered = template.render(context)
setattr(self.docx.core_properties, prop, rendered)
def render_footnotes(
self, context: Dict[str, Any], jinja_env: Optional[Environment] = None
) -> None:
if jinja_env is None:
jinja_env = Environment()
for section in self.docx.sections:
for part in section.part.package.parts:
if part.content_type == (
"application/vnd.openxmlformats-officedocument"
".wordprocessingml.footnotes+xml"
):
xml = self.patch_xml(
part.blob.decode("utf-8")
if isinstance(part.blob, bytes)
else part.blob
)
xml = self.render_xml_part(xml, part, context, jinja_env)
part._blob = xml.encode("utf-8")
def resolve_listing(self, xml):
def resolve_text(run_properties, paragraph_properties, m):
xml = m.group(0).replace(
"\t",
"</w:t></w:r>"
"<w:r>%s<w:tab/></w:r>"
'<w:r>%s<w:t xml:space="preserve">' % (run_properties, run_properties),
)
xml = xml.replace(
"\a",
"</w:t></w:r></w:p>"
'<w:p>%s<w:r>%s<w:t xml:space="preserve">'
% (paragraph_properties, run_properties),
)
xml = xml.replace("\n", '</w:t><w:br/><w:t xml:space="preserve">')
xml = xml.replace(
"\f",
"</w:t></w:r></w:p>"
'<w:p><w:r><w:br w:type="page"/></w:r></w:p>'
'<w:p>%s<w:r>%s<w:t xml:space="preserve">'
% (paragraph_properties, run_properties),
)
return xml
def resolve_run(paragraph_properties, m):
run_properties = re.search(r"<w:rPr>.*?</w:rPr>", m.group(0))
run_properties = run_properties.group(0) if run_properties else ""
return re.sub(
r"<w:t(?: [^>]*)?>.*?</w:t>",
lambda x: resolve_text(run_properties, paragraph_properties, x),
m.group(0),
flags=re.DOTALL,
)
def resolve_paragraph(m):
paragraph_properties = re.search(r"<w:pPr>.*?</w:pPr>", m.group(0))
paragraph_properties = (
paragraph_properties.group(0) if paragraph_properties else ""
)
return re.sub(
r"<w:r(?: [^>]*)?>.*?</w:r>",
lambda x: resolve_run(paragraph_properties, x),
m.group(0),
flags=re.DOTALL,
)
xml = re.sub(
r"<w:p(?: [^>]*)?>.*?</w:p>", resolve_paragraph, xml, flags=re.DOTALL
)
return xml
def build_xml(self, context, jinja_env=None):
xml = self.get_xml()
xml = self.patch_xml(xml)
xml = self.render_xml_part(xml, self.docx._part, context, jinja_env)
return xml
def map_tree(self, tree):
root = self.docx._element
body = root.body
root.replace(body, tree)
def get_headers_footers(self, uri):
for relKey, val in self.docx._part.rels.items():
if (val.reltype == uri) and (val.target_part.blob):
yield relKey, val.target_part
def get_part_xml(self, part):
return self.xml_to_string(parse_xml(part.blob))
def get_headers_footers_encoding(self, xml):
m = re.match(r'<\?xml[^\?]+\bencoding="([^"]+)"', xml, re.I)
if m:
return m.group(1)
return "utf-8"
def build_headers_footers_xml(self, context, uri, jinja_env=None):
for relKey, part in self.get_headers_footers(uri):
xml = self.get_part_xml(part)
encoding = self.get_headers_footers_encoding(xml)
xml = self.patch_xml(xml)
xml = self.render_xml_part(xml, part, context, jinja_env)
yield relKey, xml.encode(encoding)
def map_headers_footers_xml(self, relKey, xml):
part = self.docx._part.rels[relKey].target_part
new_part = XmlPart.load(part.partname, part.content_type, xml, part.package)
for rId, rel in part.rels.items():
new_part.load_rel(rel.reltype, rel._target, rel.rId, rel.is_external)
self.docx._part.rels[relKey]._target = new_part
def render(
self,
context: Dict[str, Any],
jinja_env: Optional[Environment] = None,
autoescape: bool = False,
) -> None:
# init template working attributes
self.render_init()
if autoescape:
if not jinja_env:
jinja_env = Environment(autoescape=autoescape)
else:
jinja_env.autoescape = autoescape
# Body
xml_src = self.build_xml(context, jinja_env)
# fix tables if needed
tree = self.fix_tables(xml_src)
# fix docPr ID's
self.fix_docpr_ids(tree)
# Replace body xml tree
self.map_tree(tree)
# Headers
headers = self.build_headers_footers_xml(context, self.HEADER_URI, jinja_env)
for relKey, xml in headers:
self.map_headers_footers_xml(relKey, xml)
# Footers
footers = self.build_headers_footers_xml(context, self.FOOTER_URI, jinja_env)
for relKey, xml in footers:
self.map_headers_footers_xml(relKey, xml)
self.render_properties(context, jinja_env)
self.render_footnotes(context, jinja_env)
# set rendered flag
self.is_rendered = True
# using of TC tag in for cycle can cause that count of columns does not
# correspond to real count of columns in row. This function is able to fix it.
def fix_tables(self, xml):
parser = etree.XMLParser(recover=True)
tree = etree.fromstring(xml, parser=parser)
# get namespace
ns = "{" + tree.nsmap["w"] + "}"
# walk trough xml and find table
for t in tree.iter(ns + "tbl"):
tblGrid = t.find(ns + "tblGrid")
columns = tblGrid.findall(ns + "gridCol")
to_add = 0
# walk trough all rows and try to find if there is higher cell count
for r in t.iter(ns + "tr"):
cells = r.findall(ns + "tc")
if (len(columns) + to_add) < len(cells):
to_add = len(cells) - len(columns)
# is necessary to add columns?
if to_add > 0:
# at first, calculate width of table according to columns
# (we want to preserve it)
width = 0.0
new_average = None
for c in columns:
if not c.get(ns + "w") is None:
width += float(c.get(ns + "w"))
# try to keep proportion of table
if width > 0:
old_average = width / len(columns)
new_average = width / (len(columns) + to_add)
# scale the old columns
for c in columns:
c.set(
ns + "w",
str(
int(float(c.get(ns + "w")) * new_average / old_average)
),
)
# add new columns
for i in range(to_add):
etree.SubElement(
tblGrid, ns + "gridCol", {ns + "w": str(int(new_average))}
)
# Refetch columns after columns addition.
columns = tblGrid.findall(ns + "gridCol")
columns_len = len(columns)
cells_len_max = 0
def get_cell_len(total, cell):
tc_pr = cell.find(ns + "tcPr")
grid_span = None if tc_pr is None else tc_pr.find(ns + "gridSpan")
if grid_span is not None:
return total + int(grid_span.get(ns + "val"))
return total + 1
# Calculate max of table cells to compare with `gridCol`.
for r in t.iter(ns + "tr"):
cells = r.findall(ns + "tc")
cells_len = functools.reduce(get_cell_len, cells, 0)
cells_len_max = max(cells_len_max, cells_len)
to_remove = columns_len - cells_len_max
# If after the loop, there're less columns, than
# originally was, remove extra `gridCol` declarations.
if to_remove > 0:
# Have to keep track of the removed width to scale the
# table back to its original width.
removed_width = 0.0
for c in columns[-to_remove:]:
removed_width += float(c.get(ns + "w"))
tblGrid.remove(c)
columns_left = tblGrid.findall(ns + "gridCol")
# Distribute `removed_width` across all columns that has
# left after extras removal.
extra_space = 0
if len(columns_left) > 0:
extra_space = removed_width / len(columns_left)
extra_space = int(extra_space)
for c in columns_left:
c.set(ns + "w", str(int(float(c.get(ns + "w")) + extra_space)))
return tree
def fix_docpr_ids(self, tree):
# some Ids may have some collisions : so renumbering all of them :
for elt in tree.xpath("//wp:docPr", namespaces=docx.oxml.ns.nsmap):
self.docx_ids_index += 1
elt.attrib["id"] = str(self.docx_ids_index)
def new_subdoc(self, docpath=None) -> Subdoc:
from .subdoc import Subdoc
self.init_docx()
return Subdoc(self, docpath)
@staticmethod
def get_file_crc(file_obj):
if hasattr(file_obj, "read"):
buf = file_obj.read()
else:
with open(file_obj, "rb") as fh:
buf = fh.read()
crc = binascii.crc32(buf) & 0xFFFFFFFF
return crc
def replace_media(self, src_file, dst_file):
"""Replace one media by another one into a docx
This has been done mainly because it is not possible to add images in
docx header/footer.
With this function, put a dummy picture in your header/footer,
then specify it with its replacement in this function using the file path
or file-like objects.
Syntax: tpl.replace_media('dummy_media_to_replace.png','media_to_paste.jpg')
-- or --
tpl.replace_media(io.BytesIO(image_stream), io.BytesIO(new_image_stream))
Note: for images, the aspect ratio will be the same as the replaced image
Note2: it is important to have the source media file as it is required
to calculate its CRC to find them in the docx
"""
crc = self.get_file_crc(src_file)
if hasattr(dst_file, "read"):
self.crc_to_new_media[crc] = dst_file.read()
else:
with open(dst_file, "rb") as fh:
self.crc_to_new_media[crc] = fh.read()
def replace_pic(self, embedded_file, dst_file):
"""Replace embedded picture with original-name given by embedded_file.
(give only the file basename, not the full path)
The new picture is given by dst_file (either a filename or a file-like
object)
Notes:
1) embedded_file and dst_file must have the same extension/format
in case dst_file is a file-like object, no check is done on
format compatibility
2) the aspect ratio will be the same as the replaced image
3) There is no need to keep the original file (this is not the case
for replace_embedded and replace_media)
"""
if hasattr(dst_file, "read"):
# NOTE: file extension not checked
self.pics_to_replace[embedded_file] = dst_file.read()
else:
with open(dst_file, "rb") as fh:
self.pics_to_replace[embedded_file] = fh.read()
def replace_embedded(self, src_file, dst_file):
"""Replace one embedded object by another one into a docx
This has been done mainly because it is not possible to add images
in docx header/footer.
With this function, put a dummy picture in your header/footer,
then specify it with its replacement in this function
Syntax: tpl.replace_embedded('dummy_doc.docx','doc_to_paste.docx')
Note2 : it is important to have the source file as it is required to
calculate its CRC to find them in the docx
"""
with open(dst_file, "rb") as fh:
crc = self.get_file_crc(src_file)
self.crc_to_new_embedded[crc] = fh.read()
def replace_zipname(self, zipname, dst_file):
"""Replace one file in the docx file
First note that a MSWord .docx file is in fact a zip file.
This method can be used to replace document embedded in the docx template.
Some embedded document may have been modified by MSWord while saving
the template : thus replace_embedded() cannot be used as CRC is not the
same as the original file.
This method works for embedded MSWord file like Excel or PowerPoint file,
but won't work for others like PDF, Python or even Text files :
For these ones, MSWord generate an oleObjectNNN.bin file which is no
use to be replaced as it is encoded.
Syntax:
tpl.replace_zipname(
'word/embeddings/Feuille_Microsoft_Office_Excel1.xlsx',
'my_excel_file.xlsx')
The zipname is the one you can find when you open docx with WinZip,
7zip (Windows) or unzip -l (Linux). The zipname starts with
"word/embeddings/". Note that the file is renamed by MSWord,
so you have to guess a little bit...
"""
with open(dst_file, "rb") as fh:
self.zipname_to_replace[zipname] = fh.read()
def reset_replacements(self):
"""Reset replacement dictionaries
This will reset data for image/embedded/zipname replacement
This is useful when calling several times render() with different
image/embedded/zipname replacements without re-instantiating
DocxTemplate object.
In this case, the right sequence for each rendering will be :
- reset_replacements(...)
- replace_zipname(...), replace_media(...) and/or replace_embedded(...),
- render(...)
If you instantiate DocxTemplate object before each render(),
this method is useless.
"""
self.crc_to_new_media = {}
self.crc_to_new_embedded = {}
self.zipname_to_replace = {}
self.pics_to_replace = {}
def post_processing(self, docx_file):
if self.crc_to_new_media or self.crc_to_new_embedded or self.zipname_to_replace:
if hasattr(docx_file, "read"):
tmp_file = io.BytesIO()
DocxTemplate(docx_file).save(tmp_file)
tmp_file.seek(0)
docx_file.seek(0)
docx_file.truncate()
docx_file.seek(0)
else:
tmp_file = "%s_docxtpl_before_replace_medias" % docx_file
os.rename(docx_file, tmp_file)
with zipfile.ZipFile(tmp_file) as zin:
with zipfile.ZipFile(docx_file, "w") as zout:
for item in zin.infolist():
buf = zin.read(item.filename)
if item.filename in self.zipname_to_replace:
zout.writestr(item, self.zipname_to_replace[item.filename])
elif (
item.filename.startswith("word/media/")
and item.CRC in self.crc_to_new_media
):
zout.writestr(item, self.crc_to_new_media[item.CRC])
elif (
item.filename.startswith("word/embeddings/")
and item.CRC in self.crc_to_new_embedded
):
zout.writestr(item, self.crc_to_new_embedded[item.CRC])
else:
zout.writestr(item, buf)
if not hasattr(tmp_file, "read"):
os.remove(tmp_file)
if hasattr(docx_file, "read"):
docx_file.seek(0)
def pre_processing(self):
if self.pics_to_replace:
self._replace_pics()
def _replace_pics(self):
"""Replaces pictures xml tags in the docx template with pictures provided by the user"""
replaced_pics = {key: False for key in self.pics_to_replace}
# Main document
part = self.docx.part
self._replace_docx_part_pics(part, replaced_pics)
# Header/Footer
for relid, rel in part.rels.items():
if rel.reltype in (REL_TYPE.HEADER, REL_TYPE.FOOTER):
self._replace_docx_part_pics(rel.target_part, replaced_pics)
if not self.allow_missing_pics:
# make sure all template images defined by user were replaced
for img_id, replaced in replaced_pics.items():
if not replaced:
raise ValueError(
"Picture %s not found in the docx template" % img_id
)
def get_pic_map(self):
return self.pic_map
def _replace_docx_part_pics(self, doc_part, replaced_pics):
et = etree.fromstring(doc_part.blob)
part_map = {}
gds = et.xpath("//a:graphic/a:graphicData", namespaces=docx.oxml.ns.nsmap)
for gd in gds:
rel = None
# Either IMAGE, CHART, SMART_ART, ...
try:
if gd.attrib["uri"] == docx.oxml.ns.nsmap["pic"]:
# Either PICTURE or LINKED_PICTURE image
blip = gd.xpath(
"pic:pic/pic:blipFill/a:blip", namespaces=docx.oxml.ns.nsmap
)[0]
dest = blip.xpath("@r:embed", namespaces=docx.oxml.ns.nsmap)
if len(dest) > 0:
rel = dest[0]
else:
continue
else:
continue
non_visual_properties = "pic:pic/pic:nvPicPr/pic:cNvPr/"
filename = gd.xpath(
"%s@name" % non_visual_properties, namespaces=docx.oxml.ns.nsmap
)[0]
titles = gd.xpath(
"%s@title" % non_visual_properties, namespaces=docx.oxml.ns.nsmap
)
if titles:
title = titles[0]
else:
title = ""
descriptions = gd.xpath(
"%s@descr" % non_visual_properties, namespaces=docx.oxml.ns.nsmap
)
if descriptions:
description = descriptions[0]
else:
description = ""
part_map[filename] = (
doc_part.rels[rel].target_ref,
doc_part.rels[rel].target_part,
)
# replace data
for img_id, img_data in self.pics_to_replace.items():
if img_id == filename or img_id == title or img_id == description:
part_map[filename][1]._blob = img_data
replaced_pics[img_id] = True
break
# FIXME: figure out what exceptions are thrown here
# and catch more specific exceptions
except Exception:
continue
self.pic_map.update(part_map)
def build_url_id(self, url):
self.init_docx()
return self.docx._part.relate_to(url, REL_TYPE.HYPERLINK, is_external=True)
def save(self, filename: Union[IO[bytes], str, PathLike], *args, **kwargs) -> None:
# case where save() is called without doing rendering
# ( user wants only to replace image/embedded/zipname )
if not self.is_saved and not self.is_rendered:
self.docx = Document(self.template_file)
self.pre_processing()
self.docx.save(filename, *args, **kwargs)
self.post_processing(filename)
self.is_saved = True
def get_undeclared_template_variables(
self,
jinja_env: Optional[Environment] = None,
context: Optional[Dict[str, Any]] = None,
) -> Set[str]:
# Create a temporary document to analyze the template without affecting the current state
temp_doc = Document(self.template_file)
# Get XML from the temporary document
xml = self.xml_to_string(temp_doc._element.body)
xml = self.patch_xml(xml)
# Add headers and footers
for uri in [self.HEADER_URI, self.FOOTER_URI]:
for relKey, val in temp_doc._part.rels.items():
if (val.reltype == uri) and (val.target_part.blob):
_xml = self.xml_to_string(parse_xml(val.target_part.blob))
xml += self.patch_xml(_xml)
if jinja_env:
env = jinja_env
else:
env = Environment()
parse_content = env.parse(xml)
all_variables = meta.find_undeclared_variables(parse_content)
# If context is provided, return only variables that are not in the context
if context is not None:
provided_variables = set(context.keys())
return all_variables - provided_variables
# If no context provided, return all variables (original behavior)
return all_variables

1032
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

70
pyproject.toml Normal file
View File

@ -0,0 +1,70 @@
[project]
name = "docxtpl"
dynamic = ["version"]
description = "Python docx template engine"
authors = [{name="Eric Lapouyade", email="elapouya@proton.me"}]
readme = "README.rst"
requires-python = ">=3.7"
license = {text="LGPL-2.1-only"}
classifiers=[
"Intended Audience :: Developers",
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
keywords = ["jinja2"]
dependencies = [
"python-docx",
"jinja2",
"lxml",
]
[project.optional-dependencies]
subdoc = ["docxcompose"]
docs = ["Sphinx", "sphinxcontrib-napoleon"]
[dependency-groups]
dev = [
"mypy >=1.18.2; python_version >= '3.9'",
"lxml-stubs >=0.5.1; python_version >= '3.9'",
"flake8 >=7.3.0; python_version >= '3.9'"
]
[project.urls]
homepage = "https://github.com/elapouya/python-docx-template"
repository = "https://github.com/elapouya/python-docx-template.git"
document = "https://docxtpl.readthedocs.org"
[tool.poetry]
version = "0.0.0"
[tool.poetry.requires-plugins]
poetry-dynamic-versioning = { version = ">=1.0.0,<2.0.0", extras = ["plugin"] }
[tool.poetry-dynamic-versioning]
enable = true
[tool.poetry-dynamic-versioning.from-file]
source = "docxtpl/__init__.py"
pattern = '__version__ = "(.+)"'
[tool.mypy]
pretty = true
python_version = "3.9"
check_untyped_defs = true
warn_unused_ignores = true
exclude = ["docs", "build", "setup.py"]
[[tool.mypy.overrides]]
module = ["docxcompose.*"]
ignore_missing_imports = true
[build-system]
requires = ["poetry-core", "poetry-dynamic-versioning >=1.0.0,<2.0.0"]
build-backend = "poetry_dynamic_versioning.backend"

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
python-docx
docxcompose
jinja2
lxml
sphinx-book-theme

View File

@ -1,17 +1,23 @@
from setuptools import setup
import os
import re
import sys
from setuptools import setup
# To register onto Pypi :
# python setup.py sdist bdist_wheel upload
def read(*names):
values = dict()
for name in names:
filename = name + '.rst'
filename = name + ".rst"
if os.path.isfile(filename):
fd = open(filename)
value = fd.read()
fd.close()
else:
value = ''
value = ""
values[name] = value
return values
@ -22,35 +28,50 @@ long_description = """
News
====
%(CHANGES)s
""" % read('README', 'CHANGES')
""" % read(
"README", "CHANGES"
)
def get_version(pkg):
path = os.path.join(os.path.dirname(__file__),pkg,'__init__.py')
with open(path) as fh:
m = re.search(r'^__version__\s*=\s*[\'"]([^\'"]+)[\'"]',fh.read(),re.M)
path = os.path.join(os.path.dirname(__file__), pkg, "__init__.py")
if sys.version_info >= (3, 0):
fh = open(path, encoding="utf-8") # required to read utf-8 file on windows
else:
fh = open(path) # encoding parameter does not exist in python 2
with fh:
m = re.search(r'^__version__\s*=\s*[\'"]([^\'"]+)[\'"]', fh.read(), re.M)
if m:
return m.group(1)
raise RuntimeError("Unable to find __version__ string in %s." % path)
setup(name='docxtpl',
version=get_version('docxtpl'),
description='Python docx template engine',
long_description=long_description,
classifiers=[
"Intended Audience :: Developers",
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
],
keywords='jinja2',
url='https://github.com/elapouya/python-docx-template',
author='Eric Lapouyade',
author_email='elapouya@gmail.com',
license='LGPL 2.1',
packages=['docxtpl'],
install_requires=['six', 'python-docx', 'jinja2', 'lxml'],
extras_require={'docs': ['Sphinx', 'sphinxcontrib-napoleon']},
eager_resources=['docs'],
zip_safe=False)
setup(
name="docxtpl",
version=get_version("docxtpl"),
description="Python docx template engine",
long_description=long_description,
long_description_content_type="text/x-rst",
classifiers=[
"Intended Audience :: Developers",
"Development Status :: 4 - Beta",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
],
keywords="jinja2",
url="https://github.com/elapouya/python-docx-template",
author="Eric Lapouyade",
license="LGPL-2.1-only",
license_files=[],
packages=["docxtpl"],
install_requires=["python-docx>=1.1.1", "jinja2", "lxml"],
extras_require={"docs": ["Sphinx", "sphinxcontrib-napoleon"], "subdoc": ["docxcompose"]},
eager_resources=["docs"],
zip_safe=False,
)

View File

@ -1,22 +1,42 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate, RichText
tpl=DocxTemplate('test_files/cellbg_tpl.docx')
tpl = DocxTemplate("templates/cellbg_tpl.docx")
context = {
'alerts' : [
{'date' : '2015-03-10', 'desc' : RichText('Very critical alert',color='FF0000', bold=True), 'type' : 'CRITICAL', 'bg': 'FF0000' },
{'date' : '2015-03-11', 'desc' : RichText('Just a warning'), 'type' : 'WARNING', 'bg': 'FFDD00' },
{'date' : '2015-03-12', 'desc' : RichText('Information'), 'type' : 'INFO', 'bg': '8888FF' },
{'date' : '2015-03-13', 'desc' : RichText('Debug trace'), 'type' : 'DEBUG', 'bg': 'FF00FF' },
"alerts": [
{
"date": "2015-03-10",
"desc": RichText("Very critical alert", color="FF0000", bold=True),
"type": "CRITICAL",
"bg": "FF0000",
},
{
"date": "2015-03-11",
"desc": RichText("Just a warning"),
"type": "WARNING",
"bg": "FFDD00",
},
{
"date": "2015-03-12",
"desc": RichText("Information"),
"type": "INFO",
"bg": "8888FF",
},
{
"date": "2015-03-13",
"desc": RichText("Debug trace"),
"type": "DEBUG",
"bg": "FF00FF",
},
],
}
tpl.render(context)
tpl.save('test_files/cellbg.docx')
tpl.save("output/cellbg.docx")

6
tests/comments.py Normal file
View File

@ -0,0 +1,6 @@
from docxtpl import DocxTemplate
tpl = DocxTemplate("templates/comments_tpl.docx")
tpl.render({})
tpl.save("output/comments.docx")

View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
"""
Created : 2015-03-12
@author: sandeeprah, Eric Lapouyade
"""
from docxtpl import DocxTemplate
import jinja2
jinja_env = jinja2.Environment()
# to create new filters, first create functions that accept the value to filter
# as first argument, and filter parameters as next arguments
def my_filterA(value, my_string_arg):
return_value = value + " " + my_string_arg
return return_value
def my_filterB(value, my_float_arg):
return_value = value + my_float_arg
return return_value
# Then, declare them to jinja like this :
jinja_env.filters["my_filterA"] = my_filterA
jinja_env.filters["my_filterB"] = my_filterB
context = {"base_value_string": " Hello", "base_value_float": 1.5}
tpl = DocxTemplate("templates/custom_jinja_filters_tpl.docx")
tpl.render(context, jinja_env)
tpl.save("output/custom_jinja_filters.docx")

10
tests/doc_properties.py Normal file
View File

@ -0,0 +1,10 @@
from docxtpl import DocxTemplate
doctemplate = r"templates/doc_properties_tpl.docx"
tpl = DocxTemplate(doctemplate)
context = {"test": "HelloWorld"}
tpl.render(context)
tpl.save("output/doc_properties.docx")

View File

@ -1,15 +1,15 @@
from docxtpl import DocxTemplate
tpl=DocxTemplate('test_files/dynamic_table_tpl.docx')
tpl = DocxTemplate("templates/dynamic_table_tpl.docx")
context = {
'col_labels' : ['fruit', 'vegetable', 'stone', 'thing'],
'tbl_contents': [
{'label': 'yellow', 'cols': ['banana', 'capsicum', 'pyrite', 'taxi']},
{'label': 'red', 'cols': ['apple', 'tomato', 'cinnabar', 'doubledecker']},
{'label': 'green', 'cols': ['guava', 'cucumber', 'aventurine', 'card']},
]
"col_labels": ["fruit", "vegetable", "stone", "thing"],
"tbl_contents": [
{"label": "yellow", "cols": ["banana", "capsicum", "pyrite", "taxi"]},
{"label": "red", "cols": ["apple", "tomato", "cinnabar", "doubledecker"]},
{"label": "green", "cols": ["guava", "cucumber", "aventurine", "card"]},
],
}
tpl.render(context)
tpl.save('test_files/dynamic_table.docx')
tpl.save("output/dynamic_table.docx")

View File

@ -1,29 +1,45 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2017-09-09
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
# rendering the "dynamic embedded docx":
embedded_docx_tpl=DocxTemplate('test_files/embedded_embedded_docx_tpl.docx')
embedded_docx_tpl = DocxTemplate("templates/embedded_embedded_docx_tpl.docx")
context = {
'name' : 'John Doe',
"name": "John Doe",
}
embedded_docx_tpl.render(context)
embedded_docx_tpl.save('test_files/embedded_embedded_docx.docx')
embedded_docx_tpl.save("output/embedded_embedded_docx.docx")
# rendring the main document :
tpl=DocxTemplate('test_files/embedded_main_tpl.docx')
# rendering the main document :
tpl = DocxTemplate("templates/embedded_main_tpl.docx")
context = {
'name' : 'John Doe',
"name": "John Doe",
}
tpl.replace_embedded('test_files/embedded_dummy.docx','test_files/embedded_static_docx.docx')
tpl.replace_embedded('test_files/embedded_dummy2.docx','test_files/embedded_embedded_docx.docx')
tpl.replace_embedded(
"templates/embedded_dummy.docx", "templates/embedded_static_docx.docx"
)
tpl.replace_embedded(
"templates/embedded_dummy2.docx", "output/embedded_embedded_docx.docx"
)
# The zipname is the one you can find when you open docx with WinZip, 7zip (Windows)
# or unzip -l (Linux). The zipname starts with "word/embeddings/".
# Note that the file is renamed by MSWord, so you have to guess a little bit...
tpl.replace_zipname(
"word/embeddings/Feuille_Microsoft_Office_Excel3.xlsx", "templates/real_Excel.xlsx"
)
tpl.replace_zipname(
"word/embeddings/Pr_sentation_Microsoft_Office_PowerPoint4.pptx",
"templates/real_PowerPoint.pptx",
)
tpl.render(context)
tpl.save('test_files/embedded.docx')
tpl.save("output/embedded.docx")

View File

@ -1,12 +1,54 @@
from docxtpl import *
from docxtpl import DocxTemplate, R, Listing
tpl = DocxTemplate("test_files/escape_tpl.docx")
tpl = DocxTemplate("templates/escape_tpl.docx")
context = {'myvar': R('"less than" must be escaped : <, this can be done with RichText() or R()'),
'myescvar':'It can be escaped with a "|e" jinja filter in the template too : < ',
'nlnp' : R('Here is a multiple\nlines\nstring\aand some\aother\aparagraphs\aNOTE: the current character styling is removed'),
'mylisting': Listing('the listing\nwith\nsome\nlines\nand special chars : <>&'),
}
context = {
"myvar": R(
'"less than" must be escaped : <, this can be done with RichText() or R()'
),
"myescvar": 'It can be escaped with a "|e" jinja filter in the template too : < ',
"nlnp": R(
"Here is a multiple\nlines\nstring\aand some\aother\aparagraphs",
color="#ff00ff",
),
"mylisting": Listing("the listing\nwith\nsome\nlines\nand special chars : <>& ..."),
"page_break": R("\f"),
"new_listing": """
This is a new listing
Now, does not require Listing() Object
Here is a \t tab\a
Here is a new paragraph\a
Here is a page break : \f
That's it
""",
"some_html": (
"HTTP/1.1 200 OK\n"
"Server: Apache-Coyote/1.1\n"
"Cache-Control: no-store\n"
"Expires: Thu, 01 Jan 1970 00:00:00 GMT\n"
"Pragma: no-cache\n"
"Content-Type: text/html;charset=UTF-8\n"
"Content-Language: zh-CN\n"
"Date: Thu, 22 Oct 2020 10:59:40 GMT\n"
"Content-Length: 9866\n"
"\n"
"<html>\n"
"<head>\n"
" <title>Struts Problem Report</title>\n"
" <style>\n"
" \tpre {\n"
"\t \tmargin: 0;\n"
"\t padding: 0;\n"
"\t } "
"\n"
" </style>\n"
"</head>\n"
"<body>\n"
"...\n"
"</body>\n"
"</html>"
),
}
tpl.render(context)
tpl.save("test_files/escape.docx")
tpl.save("output/escape.docx")

27
tests/escape_auto.py Normal file
View File

@ -0,0 +1,27 @@
"""
@author: Max Podolskii
"""
import os
from unicodedata import name
from docxtpl import DocxTemplate
XML_RESERVED = """<"&'>"""
tpl = DocxTemplate("templates/escape_tpl_auto.docx")
context = {
"nested_dict": {name(str(c)): c for c in XML_RESERVED},
"autoescape": 'Escaped "str & ing"!',
"autoescape_unicode": "This is an escaped <unicode> example \u4f60 & \u6211",
"iteritems": lambda x: x.items(),
}
tpl.render(context, autoescape=True)
OUTPUT = "output"
if not os.path.exists(OUTPUT):
os.makedirs(OUTPUT)
tpl.save(OUTPUT + "/escape_auto.docx")

17
tests/footnotes.py Normal file
View File

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
"""
Created : 2024-09-23
@author: Bart Broere
"""
from docxtpl import DocxTemplate
DEST_FILE = "output/footnotes.docx"
tpl = DocxTemplate("templates/footnotes_tpl.docx")
context = {"a_jinja_variable": "A Jinja variable!"}
tpl.render(context)
tpl.save(DEST_FILE)

View File

@ -0,0 +1,180 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Test for get_undeclared_template_variables method
This test demonstrates the correct behavior of get_undeclared_template_variables:
1. Before rendering - finds all template variables
2. After rendering with incomplete context - finds missing variables
3. After rendering with complete context - returns empty set
"""
from docxtpl import DocxTemplate
def test_before_render():
"""Test that get_undeclared_template_variables finds all variables before rendering"""
print("=== Test 1: Before render ===")
tpl = DocxTemplate("templates/get_undeclared_variables.docx")
undeclared = tpl.get_undeclared_template_variables()
print(f"Variables found: {undeclared}")
# Should find all variables
expected_vars = {
"name",
"age",
"email",
"is_student",
"has_degree",
"degree_field",
"skills",
"projects",
"company_name",
"page_number",
"generation_date",
"author",
}
if undeclared == expected_vars:
print("PASS: Found all expected variables before render")
else:
print(f"FAIL: Expected {expected_vars}, got {undeclared}")
return undeclared == expected_vars
def test_after_incomplete_render():
"""Test that get_undeclared_template_variables finds missing variables after incomplete render"""
print("\n=== Test 2: After incomplete render ===")
tpl = DocxTemplate("templates/get_undeclared_variables.docx")
# Provide only some variables (missing several)
context = {
"name": "John Doe",
"age": 25,
"email": "john@example.com",
"is_student": True,
"skills": ["Python", "Django"],
"company_name": "Test Corp",
"author": "Test Author",
}
tpl.render(context)
undeclared = tpl.get_undeclared_template_variables(context=context)
print(f"Missing variables: {undeclared}")
# Should find missing variables
expected_missing = {
"has_degree",
"degree_field",
"projects",
"page_number",
"generation_date",
}
if undeclared == expected_missing:
print("PASS: Found missing variables after incomplete render")
else:
print(f"FAIL: Expected missing {expected_missing}, got {undeclared}")
return undeclared == expected_missing
def test_after_complete_render():
"""Test that get_undeclared_template_variables returns empty set after complete render"""
print("\n=== Test 3: After complete render ===")
tpl = DocxTemplate("templates/get_undeclared_variables.docx")
# Provide all variables
context = {
"name": "John Doe",
"age": 25,
"email": "john@example.com",
"is_student": True,
"has_degree": True,
"degree_field": "Computer Science",
"skills": ["Python", "Django", "JavaScript"],
"projects": [
{"name": "Project A", "year": 2023, "description": "A great project"},
{"name": "Project B", "year": 2024, "description": "Another great project"},
],
"company_name": "Test Corp",
"page_number": 1,
"generation_date": "2024-01-15",
"author": "Test Author",
}
tpl.render(context)
undeclared = tpl.get_undeclared_template_variables(context=context)
print(f"Undeclared variables: {undeclared}")
# Should return empty set
if undeclared == set():
print("PASS: No undeclared variables after complete render")
else:
print(f"FAIL: Expected empty set, got {undeclared}")
return undeclared == set()
def test_with_custom_jinja_env():
"""Test that get_undeclared_template_variables works with custom Jinja environment"""
print("\n=== Test 4: With custom Jinja environment ===")
from jinja2 import Environment
tpl = DocxTemplate("templates/get_undeclared_variables.docx")
custom_env = Environment()
undeclared = tpl.get_undeclared_template_variables(jinja_env=custom_env)
print(f"Variables found with custom env: {undeclared}")
# Should find all variables
expected_vars = {
"name",
"age",
"email",
"is_student",
"has_degree",
"degree_field",
"skills",
"projects",
"company_name",
"page_number",
"generation_date",
"author",
}
if undeclared == expected_vars:
print("PASS: Custom Jinja environment works correctly")
else:
print(f"FAIL: Expected {expected_vars}, got {undeclared}")
return undeclared == expected_vars
if __name__ == "__main__":
print("Testing get_undeclared_template_variables method...")
print("=" * 50)
# Run all tests
test1_passed = test_before_render()
test2_passed = test_after_incomplete_render()
test3_passed = test_after_complete_render()
test4_passed = test_with_custom_jinja_env()
print("\n" + "=" * 50)
print("SUMMARY:")
print(f"Test 1 (Before render): {'PASS' if test1_passed else 'FAIL'}")
print(f"Test 2 (After incomplete render): {'PASS' if test2_passed else 'FAIL'}")
print(f"Test 3 (After complete render): {'PASS' if test3_passed else 'FAIL'}")
print(f"Test 4 (Custom Jinja env): {'PASS' if test4_passed else 'FAIL'}")
all_passed = test1_passed and test2_passed and test3_passed and test4_passed
if all_passed:
print("ALL TESTS PASSED!")
else:
print("SOME TESTS FAILED!")
print("\nNote: This test demonstrates that get_undeclared_template_variables")
print("now correctly analyzes the original template, not the rendered document.")

View File

@ -1,23 +1,25 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl=DocxTemplate('test_files/header_footer_tpl.docx')
tpl = DocxTemplate("templates/header_footer_tpl.docx")
sd = tpl.new_subdoc()
p = sd.add_paragraph('This is a sub-document to check it does not break header and footer')
p = sd.add_paragraph(
"This is a sub-document to check it does not break header and footer"
)
context = {
'title' : 'Header and footer test',
'company_name' : 'The World Wide company',
'date' : '2016-03-17',
'mysubdoc' : sd,
"title": "Header and footer test",
"company_name": "The World Wide company",
"date": "2016-03-17",
"mysubdoc": sd,
}
tpl.render(context)
tpl.save('test_files/header_footer.docx')
tpl.save("output/header_footer.docx")

View File

@ -1,17 +1,17 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl=DocxTemplate('test_files/header_footer_entities_tpl.docx')
tpl = DocxTemplate("templates/header_footer_entities_tpl.docx")
context = {
'title' : 'Header and footer test',
"title": "Header and footer test",
}
tpl.render(context)
tpl.save('test_files/header_footer_entities.docx')
tpl.save("output/header_footer_entities.docx")

View File

@ -1,19 +1,19 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2017-09-03
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
DEST_FILE = 'test_files/header_footer_image.docx'
DEST_FILE = "output/header_footer_image.docx"
tpl=DocxTemplate('test_files/header_footer_image_tpl.docx')
tpl = DocxTemplate("templates/header_footer_image_tpl.docx")
context = {
'mycompany' : 'The World Wide company',
"mycompany": "The World Wide company",
}
tpl.replace_media('test_files/dummy_pic_for_header.png','test_files/python.png')
tpl.replace_media("templates/dummy_pic_for_header.png", "templates/python.png")
tpl.render(context)
tpl.save(DEST_FILE)
tpl.save(DEST_FILE)

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
"""
Created : 2019-05-22
@author: Eric Dufresne
"""
from docxtpl import DocxTemplate
import io
DEST_FILE = "output/header_footer_image_file_obj.docx"
DEST_FILE2 = "output/header_footer_image_file_obj2.docx"
tpl = DocxTemplate("templates/header_footer_image_tpl.docx")
context = {
"mycompany": "The World Wide company",
}
dummy_pic = io.BytesIO(open("templates/dummy_pic_for_header.png", "rb").read())
new_image = io.BytesIO(open("templates/python.png", "rb").read())
tpl.replace_media(dummy_pic, new_image)
tpl.render(context)
tpl.save(DEST_FILE)
tpl = DocxTemplate("templates/header_footer_image_tpl.docx")
dummy_pic.seek(0)
new_image.seek(0)
tpl.replace_media(dummy_pic, new_image)
tpl.render(context)
file_obj = io.BytesIO()
tpl.save(file_obj)
file_obj.seek(0)
with open(DEST_FILE2, "wb") as f:
f.write(file_obj.read())

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
"""
Created : 2021-04-06
@author: Eric Lapouyade
"""
from docxtpl import DocxTemplate, InlineImage
# for height and width you have to use millimeters (Mm), inches or points(Pt) class :
from docx.shared import Mm
tpl = DocxTemplate("templates/header_footer_inline_image_tpl.docx")
context = {
"inline_image": InlineImage(tpl, "templates/django.png", height=Mm(10)),
"images": [
InlineImage(tpl, "templates/python.png", height=Mm(10)),
InlineImage(tpl, "templates/python.png", height=Mm(10)),
InlineImage(tpl, "templates/python.png", height=Mm(10)),
],
}
tpl.render(context)
tpl.save("output/header_footer_inline_image.docx")

View File

@ -1,25 +1,28 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2016-07-19
@author: AhnSeongHyun
Edited : 2016-07-19 by Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl=DocxTemplate('test_files/header_footer_tpl_utf8.docx')
tpl = DocxTemplate("templates/header_footer_tpl_utf8.docx")
sd = tpl.new_subdoc()
p = sd.add_paragraph(u'This is a sub-document to check it does not break header and footer with utf-8 characters inside the template .docx')
p = sd.add_paragraph(
"This is a sub-document to check it does not break header and footer with utf-8 "
"characters inside the template .docx"
)
context = {
'title' : u'헤더와 푸터',
'company_name' : u'세계적 회사',
'date' : u'2016-03-17',
'mysubdoc' : sd,
"title": "헤더와 푸터",
"company_name": "세계적 회사",
"date": "2016-03-17",
"mysubdoc": sd,
}
tpl.render(context)
tpl.save('test_files/header_footer_utf8.docx')
tpl.save("output/header_footer_utf8.docx")

View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
from docxtpl import DocxTemplate
tpl = DocxTemplate("templates/horizontal_merge_tpl.docx")
tpl.render({})
tpl.save("output/horizontal_merge.docx")

View File

@ -1,36 +1,51 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2017-01-14
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate, InlineImage
# for height and width you have to use millimeters (Mm), inches or points(Pt) class :
from docx.shared import Mm, Inches, Pt
tpl=DocxTemplate('test_files/inline_image_tpl.docx')
# for height and width you have to use millimeters (Mm), inches or points(Pt) class :
from docx.shared import Mm
import jinja2
tpl = DocxTemplate("templates/inline_image_tpl.docx")
context = {
'myimage' : InlineImage(tpl,'test_files/python_logo.png',width=Mm(20)),
'myimageratio': InlineImage(tpl, 'test_files/python_jpeg.jpg', width=Mm(30), height=Mm(60)),
'frameworks' : [{'image' : InlineImage(tpl,'test_files/django.png',height=Mm(10)),
'desc' : 'The web framework for perfectionists with deadlines'},
{'image' : InlineImage(tpl,'test_files/zope.png',height=Mm(10)),
'desc' : 'Zope is a leading Open Source Application Server and Content Management Framework'},
{'image': InlineImage(tpl, 'test_files/pyramid.png', height=Mm(10)),
'desc': 'Pyramid is a lightweight Python web framework aimed at taking small web apps into big web apps.'},
{'image' : InlineImage(tpl,'test_files/bottle.png',height=Mm(10)),
'desc' : 'Bottle is a fast, simple and lightweight WSGI micro web-framework for Python'},
{'image': InlineImage(tpl, 'test_files/tornado.png', height=Mm(10)),
'desc': 'Tornado is a Python web framework and asynchronous networking library.'},
]
"myimage": InlineImage(tpl, "templates/python_logo.png", width=Mm(20)),
"myimageratio": InlineImage(
tpl, "templates/python_jpeg.jpg", width=Mm(30), height=Mm(60)
),
"frameworks": [
{
"image": InlineImage(tpl, "templates/django.png", height=Mm(10)),
"desc": "The web framework for perfectionists with deadlines",
},
{
"image": InlineImage(tpl, "templates/zope.png", height=Mm(10)),
"desc": "Zope is a leading Open Source Application Server "
"and Content Management Framework",
},
{
"image": InlineImage(tpl, "templates/pyramid.png", height=Mm(10)),
"desc": "Pyramid is a lightweight Python web framework aimed at taking "
"small web apps into big web apps.",
},
{
"image": InlineImage(tpl, "templates/bottle.png", height=Mm(10)),
"desc": "Bottle is a fast, simple and lightweight WSGI micro web-framework "
"for Python",
},
{
"image": InlineImage(tpl, "templates/tornado.png", height=Mm(10)),
"desc": "Tornado is a Python web framework and asynchronous networking "
"library.",
},
],
}
tpl.render(context)
tpl.save('test_files/inline_image.docx')
# testing that it works also when autoescape has been forced to True
jinja_env = jinja2.Environment(autoescape=True)
tpl.render(context, jinja_env)
tpl.save("output/inline_image.docx")

View File

@ -0,0 +1,5 @@
from docxtpl import DocxTemplate
tpl = DocxTemplate("templates/less_cells_after_loop_tpl.docx")
tpl.render({})
tpl.save("output/less_cells_after_loop.docx")

19
tests/merge_docx.py Normal file
View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
"""
Created : 2021-07-30
@author: Eric Lapouyade
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate("templates/merge_docx_master_tpl.docx")
sd = tpl.new_subdoc("templates/merge_docx_subdoc.docx")
context = {
"mysubdoc": sd,
}
tpl.render(context)
tpl.save("output/merge_docx.docx")

17
tests/merge_paragraph.py Normal file
View File

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
"""
Created : 2015-03-12
@author: Eric Lapouyade
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate("templates/merge_paragraph_tpl.docx")
context = {
"living_in_town": True,
}
tpl.render(context)
tpl.save("output/merge_paragraph.docx")

25
tests/module_execute.py Normal file
View File

@ -0,0 +1,25 @@
import os
TEMPLATE_PATH = "templates/module_execute_tpl.docx"
JSON_PATH = "templates/module_execute.json"
OUTPUT_FILENAME = "output/module_execute.docx"
OVERWRITE = "-o"
QUIET = "-q"
if os.path.exists(OUTPUT_FILENAME):
os.unlink(OUTPUT_FILENAME)
os.chdir(os.path.dirname(__file__))
cmd = "python -m docxtpl %s %s %s %s %s" % (
TEMPLATE_PATH,
JSON_PATH,
OUTPUT_FILENAME,
OVERWRITE,
QUIET,
)
print('Executing "%s" ...' % cmd)
os.system(cmd)
if os.path.exists(OUTPUT_FILENAME):
print(" --> File %s has been generated." % OUTPUT_FILENAME)

40
tests/multi_rendering.py Normal file
View File

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
"""
Created : 2021-12-20
@author: Eric Lapouyade
"""
from docxtpl import DocxTemplate
tpl = DocxTemplate("templates/multi_rendering_tpl.docx")
documents_data = [
{
"dest_file": "multi_render1.docx",
"context": {
"title": "Title ONE",
"body": "This is the body for first document",
},
},
{
"dest_file": "multi_render2.docx",
"context": {
"title": "Title TWO",
"body": "This is the body for second document",
},
},
{
"dest_file": "multi_render3.docx",
"context": {
"title": "Title THREE",
"body": "This is the body for third document",
},
},
]
for document_data in documents_data:
dest_file = document_data["dest_file"]
context = document_data["context"]
tpl.render(context)
tpl.save("output/%s" % dest_file)

View File

@ -1,33 +1,45 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2016-03-26
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl=DocxTemplate('test_files/nested_for_tpl.docx')
tpl = DocxTemplate("templates/nested_for_tpl.docx")
context = {
'dishes' : [
{'name' : 'Pizza', 'ingredients' : ['bread','tomato', 'ham', 'cheese']},
{'name' : 'Hamburger', 'ingredients' : ['bread','chopped steak', 'cheese', 'sauce']},
{'name' : 'Apple pie', 'ingredients' : ['flour','apples', 'suggar', 'quince jelly']},
"dishes": [
{"name": "Pizza", "ingredients": ["bread", "tomato", "ham", "cheese"]},
{
"name": "Hamburger",
"ingredients": ["bread", "chopped steak", "cheese", "sauce"],
},
{
"name": "Apple pie",
"ingredients": ["flour", "apples", "suggar", "quince jelly"],
},
],
"authors": [
{
"name": "Saint-Exupery",
"books": [
{"title": "Le petit prince"},
{"title": "L'aviateur"},
{"title": "Vol de nuit"},
],
},
{
"name": "Barjavel",
"books": [
{"title": "Ravage"},
{"title": "La nuit des temps"},
{"title": "Le grand secret"},
],
},
],
'authors' : [
{'name' : 'Saint-Exupery', 'books' : [
{'title' : 'Le petit prince'},
{'title' : "L'aviateur"},
{'title' : 'Vol de nuit'},
]},
{'name' : 'Barjavel', 'books' : [
{'title' : 'Ravage'},
{'title' : "La nuit des temps"},
{'title' : 'Le grand secret'},
]},
]
}
tpl.render(context)
tpl.save('test_files/nested_for.docx')
tpl.save("output/nested_for.docx")

View File

@ -1,26 +1,26 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
tpl=DocxTemplate('test_files/order_tpl.docx')
tpl = DocxTemplate("templates/order_tpl.docx")
context = {
'customer_name' : 'Eric',
'items' : [
{'desc' : 'Python interpreters', 'qty' : 2, 'price' : 'FREE' },
{'desc' : 'Django projects', 'qty' : 5403, 'price' : 'FREE' },
{'desc' : 'Guido', 'qty' : 1, 'price' : '100,000,000.00' },
],
'in_europe' : True,
'is_paid': False,
'company_name' : 'The World Wide company',
'total_price' : '100,000,000.00'
context = {
"customer_name": "Eric",
"items": [
{"desc": "Python interpreters", "qty": 2, "price": "FREE"},
{"desc": "Django projects", "qty": 5403, "price": "FREE"},
{"desc": "Guido", "qty": 1, "price": "100,000,000.00"},
],
"in_europe": True,
"is_paid": False,
"company_name": "The World Wide company",
"total_price": "100,000,000.00",
}
tpl.render(context)
tpl.save('test_files/order.docx')
tpl.save("output/order.docx")

14
tests/preserve_spaces.py Normal file
View File

@ -0,0 +1,14 @@
from docxtpl import DocxTemplate
# With old docxtpl version, "... for spicy ..." was replaced by "... forspicy..."
# This test is for checking that is some cases the spaces are not lost anymore
tpl = DocxTemplate("templates/preserve_spaces_tpl.docx")
tags = ["tag_1", "tag_2"]
replacement = ["looking", "too"]
context = dict(zip(tags, replacement))
tpl.render(context)
tpl.save("output/preserve_spaces.docx")

18
tests/replace_picture.py Normal file
View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
"""
Created : 2017-09-03
@author: Eric Lapouyade
"""
from docxtpl import DocxTemplate
DEST_FILE = "output/replace_picture.docx"
tpl = DocxTemplate("templates/replace_picture_tpl.docx")
context = {}
tpl.replace_pic("python_logo.png", "templates/python.png")
tpl.render(context)
tpl.save(DEST_FILE)

View File

@ -1,36 +1,64 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-26
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate, RichText
tpl=DocxTemplate('test_files/richtext_tpl.docx')
tpl = DocxTemplate("templates/richtext_tpl.docx")
rt = RichText('an exemple of ')
rt.add('a rich text', style='myrichtextstyle')
rt.add(' with ')
rt.add('some italic', italic=True)
rt.add(' and ')
rt.add('some violet', color='#ff00ff')
rt.add(' and ')
rt.add('some striked', strike=True)
rt.add(' and ')
rt.add('some small', size=14)
rt.add(' or ')
rt.add('big', size=60)
rt.add(' text.')
rt.add(' Et voilà ! ')
rt.add('\n1st line')
rt.add('\n2nd line')
rt.add('\n3rd line')
rt.add('\n\n<cool>')
rt = RichText()
rt.add("a rich text", style="myrichtextstyle")
rt.add(" with ")
rt.add("some italic", italic=True)
rt.add(" and ")
rt.add("some violet", color="#ff00ff")
rt.add(" and ")
rt.add("some striked", strike=True)
rt.add(" and ")
rt.add("some Highlighted", highlight="#ffff00")
rt.add(" and ")
rt.add("some small", size=14)
rt.add(" or ")
rt.add("big", size=60)
rt.add(" text.")
rt.add("\nYou can add an hyperlink, here to ")
rt.add("google", url_id=tpl.build_url_id("http://google.com"))
rt.add("\nEt voilà ! ")
rt.add("\n1st line")
rt.add("\n2nd line")
rt.add("\n3rd line")
rt.add("\aA new paragraph : <cool>\a")
rt.add("--- A page break here (see next page) ---\f")
for ul in [
"single",
"double",
"thick",
"dotted",
"dash",
"dotDash",
"dotDotDash",
"wave",
]:
rt.add("\nUnderline : " + ul + " \n", underline=ul)
rt.add("\nFonts :\n", underline=True)
rt.add("Arial\n", font="Arial")
rt.add("Courier New\n", font="Courier New")
rt.add("Times New Roman\n", font="Times New Roman")
rt.add("\n\nHere some")
rt.add("superscript", superscript=True)
rt.add(" and some")
rt.add("subscript", subscript=True)
rt_embedded = RichText("an example of ")
rt_embedded.add(rt)
context = {
'example' : rt,
"example": rt_embedded,
}
tpl.render(context)
tpl.save('test_files/richtext.docx')
tpl.save("output/richtext.docx")

View File

@ -1,18 +1,16 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-26
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate, RichText
tpl=DocxTemplate('test_files/richtext_and_if_tpl.docx')
tpl = DocxTemplate("templates/richtext_and_if_tpl.docx")
context = {
'foobar': RichText('Foobar!', color='ff0000')
}
context = {"foobar": RichText("Foobar!", color="ff0000")}
tpl.render(context)
tpl.save('test_files/richtext_and_if.docx')
tpl.save("output/richtext_and_if.docx")

View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
"""
Created : 2022-08-03
@author: Dongfang Song
"""
from docxtpl import DocxTemplate, RichText
tpl = DocxTemplate("templates/richtext_eastAsia_tpl.docx")
rt = RichText("测试TEST", font="eastAsia:Microsoft YaHei")
ch = RichText("测试TEST", font="eastAsia:微软雅黑")
sun = RichText("测试TEST", font="eastAsia:SimSun")
context = {
"example": rt,
"Chinese": ch,
"simsun": sun,
}
tpl.render(context)
tpl.save("output/richtext_eastAsia.docx")

View File

@ -0,0 +1,63 @@
"""
Created : 2025-02-28
@author: Hannah Imrie
"""
from docxtpl import DocxTemplate, RichText, RichTextParagraph
tpl = DocxTemplate("templates/richtext_paragraph_tpl.docx")
rtp = RichTextParagraph()
rt = RichText()
rtp.add(
"The rich text paragraph function allows paragraph styles to be added to text",
parastyle="myrichparastyle",
)
rtp.add("Any built in paragraph style can be used", parastyle="IntenseQuote")
rtp.add(
"or you can add your own, unlocking all style options", parastyle="createdStyle"
)
rtp.add(
"To use, just create a style in your template word doc with the formatting you want "
"and call it in the code.",
parastyle="normal",
)
rtp.add("This allows for the use of")
rtp.add("custom bullet\apoints", parastyle="SquareBullet")
rtp.add("Numbered Bullet Points", parastyle="BasicNumbered")
rtp.add("and Alpha Bullet Points.", parastyle="alphaBracketNumbering")
rtp.add("You can", parastyle="normal")
rtp.add("set the", parastyle="centerAlign")
rtp.add("text alignment", parastyle="rightAlign")
rtp.add(
"as well as the spacing between lines of text. Like this for example, "
"this text has very tight spacing between the lines.\aIt also has no space between "
"paragraphs of the same style.",
parastyle="TightLineSpacing",
)
rtp.add(
"Unlike this one, which has extra large spacing between lines for when you want to "
"space things out a bit or just write a little less.",
parastyle="WideLineSpacing",
)
rtp.add(
"You can also set the background colour of a line.", parastyle="LineShadingGreen"
)
rt.add("This works with ")
rt.add("Rich ", bold=True)
rt.add("Text ", italic=True)
rt.add("Strings", underline="single")
rt.add(" too.")
rtp.add(rt, parastyle="SquareBullet")
context = {
"example": rtp,
}
tpl.render(context)
tpl.save("output/richtext_paragraph.docx")

View File

@ -1,13 +1,17 @@
import subprocess
import glob
import six
import os
tests = sorted(glob.glob('[A-Za-z]*.py'))
excludes = ['runtests.py']
tests = sorted(glob.glob("[A-Za-z]*.py"))
excludes = ["runtests.py"]
output_dir = os.path.join(os.path.dirname(__file__), "output")
if not os.path.exists(output_dir):
os.mkdir(output_dir)
for test in tests:
if test not in excludes:
six.print_('%s ...' % test)
subprocess.call(['python','./%s' % test])
print("%s ..." % test)
subprocess.call(["python", "./%s" % test])
six.print_('Done.')
print("Done.")

View File

@ -1,38 +1,36 @@
# -*- coding: utf-8 -*-
'''
"""
Created : 2015-03-12
@author: Eric Lapouyade
'''
"""
from docxtpl import DocxTemplate
from docx.shared import Inches
tpl=DocxTemplate('test_files/subdoc_tpl.docx')
tpl = DocxTemplate("templates/subdoc_tpl.docx")
sd = tpl.new_subdoc()
p = sd.add_paragraph('This is a sub-document inserted into a bigger one')
p = sd.add_paragraph('It has been ')
p.add_run('dynamically').style = 'dynamic'
p.add_run(' generated with python by using ')
p.add_run('python-docx').italic = True
p.add_run(' library')
p = sd.add_paragraph("This is a sub-document inserted into a bigger one")
p = sd.add_paragraph("It has been ")
p.add_run("dynamically").style = "dynamic"
p.add_run(" generated with python by using ")
p.add_run("python-docx").italic = True
p.add_run(" library")
sd.add_heading('Heading, level 1', level=1)
sd.add_paragraph('This is an Intense quote', style='IntenseQuote')
sd.add_heading("Heading, level 1", level=1)
sd.add_paragraph("This is an Intense quote", style="IntenseQuote")
sd.add_paragraph('A picture :')
sd.add_picture('test_files/python_logo.png', width=Inches(1.25))
sd.add_paragraph("A picture :")
sd.add_picture("templates/python_logo.png", width=Inches(1.25))
sd.add_paragraph('A Table :')
sd.add_paragraph("A Table :")
table = sd.add_table(rows=1, cols=3)
hdr_cells = table.rows[0].cells
hdr_cells[0].text = 'Qty'
hdr_cells[1].text = 'Id'
hdr_cells[2].text = 'Desc'
recordset=( (1,101,'Spam'),
(2,42,'Eggs'),
(3,631,'Spam,spam, eggs, and ham') )
hdr_cells[0].text = "Qty"
hdr_cells[1].text = "Id"
hdr_cells[2].text = "Desc"
recordset = ((1, 101, "Spam"), (2, 42, "Eggs"), (3, 631, "Spam,spam, eggs, and ham"))
for item in recordset:
row_cells = table.add_row().cells
row_cells[0].text = str(item[0])
@ -40,8 +38,8 @@ for item in recordset:
row_cells[2].text = item[2]
context = {
'mysubdoc' : sd,
"mysubdoc": sd,
}
tpl.render(context)
tpl.save('test_files/subdoc.docx')
tpl.save("output/subdoc.docx")

19
tests/template_error.py Normal file
View File

@ -0,0 +1,19 @@
from docxtpl import DocxTemplate
from jinja2.exceptions import TemplateError
print("=" * 80)
print("Generating template error for testing (so it is safe to ignore) :")
print("." * 80)
try:
tpl = DocxTemplate("templates/template_error_tpl.docx")
tpl.render({"test_variable": "test variable value"})
except TemplateError as the_error:
print(str(the_error))
if hasattr(the_error, "docx_context"):
print("Context:")
for line in the_error.docx_context:
print(line)
tpl.save("output/template_error.docx")
print("." * 80)
print(" End of TemplateError Test ")
print("=" * 80)

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,8 @@
{"json_dict_var" : {"json_dict_var":"successfully inserted"},
"json_array_var": ["json","array","var","successfully", "inserted"],
"json_string_var":"json_string_var successfully inserted",
"json_int_var":123,
"json_float_var":1.234,
"json_true_var":true,
"json_false_var":false,
"json_none_var":null}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
tests/templates/python.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 176 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More