├── examples ├── 0.jpg ├── 1.jpg ├── test.docx ├── order_tpl.docx ├── README.md ├── test.py └── order.py ├── setup.cfg ├── README_en.md ├── setup.py ├── pydocxtpl ├── __init__.py ├── table.py ├── writer.py ├── utils.py ├── ext.py ├── node.py └── text.py ├── LICENSE └── README.md /examples/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyu836/pydocxtpl/HEAD/examples/0.jpg -------------------------------------------------------------------------------- /examples/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyu836/pydocxtpl/HEAD/examples/1.jpg -------------------------------------------------------------------------------- /examples/test.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyu836/pydocxtpl/HEAD/examples/test.docx -------------------------------------------------------------------------------- /examples/order_tpl.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhangyu836/pydocxtpl/HEAD/examples/order_tpl.docx -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | 2 | # 3 | order_tpl.docx is adapted from docxtpl's [order_tpl.docx](https://github.com/elapouya/python-docx-template/blob/master/tests/templates/order_tpl.docx) . 4 | order.py is adapted from docxtpl's [order.py](https://github.com/elapouya/python-docx-template/blob/master/tests/order.py) . 5 | 6 | order_tpl.docx 改自 docxtpl 的 [order_tpl.docx](https://github.com/elapouya/python-docx-template/blob/master/tests/templates/order_tpl.docx) 。 7 | order.py 改自 docxtpl 的 [order.py](https://github.com/elapouya/python-docx-template/blob/master/tests/order.py) 。 -------------------------------------------------------------------------------- /examples/test.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | def pth(fname): 5 | pth = os.path.dirname(__file__) 6 | return os.path.join(pth, fname) 7 | 8 | from pydocxtpl import DocxWriter 9 | 10 | person_info = {'address': u'福建行中书省福宁州傲龙山庄', 'name': u'龙傲天', 'pic': pth('1.jpg')} 11 | person_info2 = {'address': u'Somewhere over the rainbow', 'name': u'Hello Wizard', 'pic': pth('0.jpg')} 12 | persons = [person_info, person_info2]# 13 | payload = {'persons': persons, 'test': 'A Big Company', 'logo': pth('1.jpg') } 14 | 15 | writer = DocxWriter(pth('test.docx')) 16 | writer.render(payload) 17 | writer.save(pth('test_result.docx')) -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | # This includes the license file(s) in the wheel. 3 | # https://wheel.readthedocs.io/en/stable/user_guide.html#including-license-files-in-the-generated-wheel-file 4 | license_files = LICENSE 5 | desciption-file = README_en.md 6 | 7 | [bdist_wheel] 8 | # This flag says to generate wheels that support both Python 2 and Python 9 | # 3. If your code will not run unchanged on both Python 2 and 3, you will 10 | # need to generate separate wheels for each Python version that you 11 | # support. Removing this line (or setting universal to 0) will prevent 12 | # bdist_wheel from trying to make a universal wheel. For more see: 13 | # https://packaging.python.org/guides/distributing-packages-using-setuptools/#wheels 14 | universal=1 -------------------------------------------------------------------------------- /examples/order.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Adapted from docxtpl's order.py 4 | ''' 5 | import os 6 | def pth(fname): 7 | pth = os.path.dirname(__file__) 8 | return os.path.join(pth, fname) 9 | 10 | from pydocxtpl import DocxWriter 11 | 12 | tpl = DocxWriter(pth('order_tpl.docx')) 13 | 14 | context = { 15 | 'customer_name': 'Eric', 16 | 'items': [ 17 | {'desc': 'Python interpreters', 'qty': 2, 'price': 'FREE'}, 18 | {'desc': 'Django projects', 'qty': 5403, 'price': 'FREE'}, 19 | {'desc': 'Guido', 'qty': 1, 'price': '100,000,000.00'}, 20 | ], 21 | 'in_europe': True, 22 | 'is_paid': False, 23 | 'company_name': 'The World Wide company', 24 | 'total_price': '100,000,000.00', 25 | } 26 | 27 | tpl.render(context) 28 | tpl.save(pth('order_result.docx')) 29 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | 2 | # pydocxtpl 3 | A docx templater. 4 | 5 | ## How to install 6 | 7 | ```shell 8 | pip install pydocxtpl 9 | ``` 10 | 11 | ## How to use 12 | 13 | To use pydocxtpl, you need to be familiar with the [syntax of jinja2 template](https://jinja.palletsprojects.com/). 14 | 15 | * code sample 16 | ```python 17 | from pydocxtpl import DocxWriter 18 | 19 | person_info = {'address': u'', 'name': u'', 'pic': '1.jpg'} 20 | person_info2 = {'address': u'Somewhere over the rainbow', 'name': u'Hello Wizard', 'pic': '0.jpg'} 21 | persons = [person_info, person_info2] 22 | payload = {'persons': persons} 23 | 24 | writer = DocxWriter('test.docx') 25 | writer.render(payload) 26 | writer.save('test_result.docx') 27 | ``` 28 | 29 | See [examples](https://github.com/zhangyu836/python-docx-templater/tree/main/examples). 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from io import open 3 | from setuptools import setup 4 | 5 | CUR_DIR = os.path.abspath(os.path.dirname(__file__)) 6 | README = os.path.join(CUR_DIR, "README_en.md") 7 | with open(README, 'r', encoding='utf-8') as fd: 8 | long_description = fd.read() 9 | 10 | setup( 11 | name = 'pydocxtpl', 12 | version = "0.2.1", 13 | author = 'Zhang Yu', 14 | author_email = 'zhangyu836@gmail.com', 15 | url = 'https://github.com/zhangyu836/python-docx-templater', 16 | packages = ['pydocxtpl'], 17 | install_requires = ['python-docx >= 0.8.10', 'jinja2', 'six'], 18 | description = ( 'A python module to generate docx files from a docx template' ), 19 | long_description = long_description, 20 | long_description_content_type = "text/markdown", 21 | platforms = ["Any platform "], 22 | license = 'MIT', 23 | keywords = ['Word', 'docx', 'template'] 24 | ) -------------------------------------------------------------------------------- /pydocxtpl/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from docx.oxml import CT_Body, CT_Tbl, CT_Row, CT_Tc, CT_R 4 | from docx.oxml import register_element_cls 5 | 6 | from .node import BodyX, node_clses 7 | from .text import ParagraghX, RunX, HyperlinkX 8 | from .text import CT_Para, CT_Run, CT_Hyperlink, CT_Drawing 9 | from .table import TableX, RowX, CellX 10 | 11 | register_element_cls('w:p', CT_Para) 12 | register_element_cls('w:hyperlink', CT_Hyperlink) 13 | register_element_cls('w:r', CT_Run) 14 | register_element_cls('w:drawing', CT_Drawing) 15 | 16 | def register_node_cls(xml_cls, cls): 17 | node_clses[xml_cls] = cls 18 | 19 | register_node_cls(CT_Body, BodyX) 20 | register_node_cls(CT_Para, ParagraghX) 21 | register_node_cls(CT_Run, RunX) 22 | register_node_cls(CT_Tbl, TableX) 23 | register_node_cls(CT_Row, RowX) 24 | register_node_cls(CT_Tc, CellX) 25 | register_node_cls(CT_Hyperlink, HyperlinkX) 26 | 27 | from .writer import DocxWriter 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Zhang Yu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pydocxtpl/table.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from docx.table import Table, _Row, _Cell 4 | from .node import RvNode 5 | 6 | class TableX(Table, RvNode): 7 | ext_tag = 'table' 8 | 9 | def __init__(self, element, parent): 10 | Table.__init__(self, element, parent) 11 | RvNode.__init__(self) 12 | self.unpack_table() 13 | 14 | def unpack_table(self): 15 | self.unpack_and_clear() 16 | 17 | class RowX(_Row, RvNode): 18 | ext_tag = 'row' 19 | 20 | def __init__(self, element, parent): 21 | _Row.__init__(self, element, parent) 22 | RvNode.__init__(self) 23 | self.unpack_row() 24 | 25 | def unpack_row(self): 26 | self.unpack_and_clear() 27 | 28 | def child_reenter(self): 29 | self.exit() 30 | self.enter() 31 | 32 | class CellX(_Cell, RvNode): 33 | ext_tag = 'cell' 34 | 35 | def __init__(self, element, parent): 36 | _Cell.__init__(self, element, parent) 37 | RvNode.__init__(self) 38 | self.unpack_cell() 39 | 40 | def unpack_cell(self): 41 | self.unpack_and_clear() 42 | 43 | class Range(object): 44 | pass 45 | 46 | -------------------------------------------------------------------------------- /pydocxtpl/writer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import print_function 4 | from docx import Document 5 | from docx.opc.constants import RELATIONSHIP_TYPE as RT 6 | from .node import Root 7 | from .ext import Env, NodeExtension, SegmentExtension, PicExtension 8 | 9 | class DocxWriter(object): 10 | 11 | reltypes = [RT.HEADER, RT.FOOTER] 12 | 13 | def __init__(self, fname, debug=False): 14 | self.debug = debug 15 | self.load(fname) 16 | 17 | def load(self, fname): 18 | self.prepare_env() 19 | self.tpls = [] 20 | self.roots = [] 21 | self.document = Document(fname) 22 | for rel in self.document.part.rels.values(): 23 | if rel.reltype in self.reltypes: 24 | root = Root(rel._target) 25 | if root.unpacked: 26 | self.roots.append(root) 27 | root = Root(self.document) 28 | if root.unpacked: 29 | self.roots.append(root) 30 | for root in self.roots: 31 | tpl_source = root.to_tag() 32 | if self.debug: 33 | root.tag_tree() 34 | print(tpl_source) 35 | jinja_tpl = self.jinja_env.from_string(tpl_source) 36 | self.tpls.append((root, jinja_tpl, tpl_source)) 37 | 38 | def prepare_env(self): 39 | self.jinja_env = Env(extensions=[NodeExtension, SegmentExtension, PicExtension]) 40 | 41 | def render(self, payload): 42 | for root,jinja_tpl,tpl_source in self.tpls: 43 | self.jinja_env.root = root 44 | rv = jinja_tpl.render(payload) 45 | 46 | def save(self, fname): 47 | self.document.save(fname) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # pydocxtpl 3 | 使用 docx 文件作为模板来生成 docx 文件。 4 | 5 | ## 安装 6 | 7 | ```shell 8 | pip install pydocxtpl 9 | ``` 10 | 11 | ## 使用 12 | 13 | 要使用 pydocxtpl,需要了解 [jinja2 模板的语法](http://docs.jinkan.org/docs/jinja2/templates.html) 。 14 | 如果用过 [docxtpl](https://github.com/elapouya/python-docx-template) ,应该就会用 pydocxtpl。 15 | 16 | * 示例代码 17 | ```python 18 | from pydocxtpl import DocxWriter 19 | 20 | person_info = {'address': u'福建行中书省福宁州傲龙山庄', 'name': u'龙傲天', 'pic': '1.jpg'} 21 | person_info2 = {'address': u'Somewhere over the rainbow', 'name': u'Hello Wizard', 'pic': '0.jpg'} 22 | persons = [person_info, person_info2] 23 | payload = {'persons': persons} 24 | 25 | writer = DocxWriter('test.docx') 26 | writer.render(payload) 27 | writer.save('test_result.docx') 28 | ``` 29 | 30 | 31 | ## 实现方法 32 | 33 | pydocxtpl 也是基于 python-docx 和 jinja2。 34 | 不过实现方法和 [docxtpl](https://github.com/elapouya/python-docx-template) 不太一样。 35 | pydocxtpl 会根据 docx 文件的 xml 树生成一棵包含 jinja2 tag 的树。 36 | pydocxtpl 会合并叶节点的 tag 来作为 jinja2 模板,使用 xml 树作为要生成的文档的模板。 37 | 渲染模板时,相当于通过 jinja2 选择所需的叶节点,叶节点及其枝干会使用相应的 xml 树节点生成所需的 xml 树。 38 | 这种方法应该也适用于其他文档。 39 | 40 | * 合并叶节点 tag 得到的 jinja2 模板。 41 | 42 | ```jinja2 43 | {%para '0,1,0' %} 44 | {%default '0,1,1,0' %} 45 | {%for person in persons%}{%seg '0,1,1,1,0'%}{%endseg%} 46 | {%run '0,1,1,2' %} 47 | {%run '0,1,1,3' %} 48 | {%seg '0,1,1,4,0'%}{{person.name}}{%endseg%} 49 | {%run '0,1,1,5' %} 50 | {%seg '0,1,1,6,0,0'%}{{ person.name}}{%endseg%} 51 | {%run '0,1,1,6,1' %} 52 | {%run '0,1,1,7' %} 53 | {%run '0,1,1,8' %} 54 | {%default '0,1,2,0' %} 55 | {%run '0,1,2,1' %} 56 | {%run '0,1,2,2' %} 57 | {%seg '0,1,2,3,0'%}{%endseg%} 58 | {%seg '0,1,2,3,1'%}{{person.address}}{%endseg%} 59 | {%run '0,1,2,4' %} 60 | {%default '0,1,3,0' %} 61 | {%run '0,1,3,1' %} 62 | {%seg '0,1,3,2,0'%}{{person.name}}{%endseg%} 63 | {%pic person.pic%}{%seg '0,1,3,3,0'%}{%endseg%} 64 | {% endfor%}{%seg '0,1,3,4,0'%}{%endseg%} 65 | {%para '0,1,4' %} 66 | {%default '0,1,5,0' %} 67 | {%default '0,1,5,1' %} 68 | {%default '0,1,5,2,0' %} 69 | {%default '0,1,5,2,1,0' %} 70 | {%default '0,1,5,2,1,1,0' %} 71 | {%for person in persons%}{%seg '0,1,5,2,1,1,1,0'%}{%endseg%} 72 | {%seg '0,1,5,2,1,1,2,0'%}{{person.name}}{%endseg%} 73 | {%default '0,1,5,2,2,0' %} 74 | {%default '0,1,5,2,2,1,0' %} 75 | {%seg '0,1,5,2,2,1,1,0'%}{{person.address}}{%endseg%} 76 | {%default '0,1,5,2,3,0' %} 77 | {%default '0,1,5,2,3,1,0' %} 78 | {%run '0,1,5,2,3,1,1' %} 79 | {% pic person.pic%}{%seg '0,1,5,2,3,1,2,0'%}{%endseg%} 80 | {%endfor%}{%seg '0,1,5,2,3,1,3,0'%}{%endseg%} 81 | {%para '0,1,6' %} 82 | {%default '0,1,7' %} 83 | {%headtail '0,2' %} 84 | ``` -------------------------------------------------------------------------------- /pydocxtpl/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import re 3 | 4 | BLOCK_START_STRING = '{%' 5 | BLOCK_END_STRING = '%}' 6 | VARIABLE_START_STRING = '{{' 7 | VARIABLE_END_STRING = '}}' 8 | TAGTEST = '%s.+%s|%s.+%s' % (BLOCK_START_STRING, BLOCK_END_STRING, VARIABLE_START_STRING, VARIABLE_END_STRING) 9 | XVTEST = '^ *%s *xv.+%s *$' % (BLOCK_START_STRING, BLOCK_END_STRING) 10 | BLOCKTEST = '%s.+%s' % (BLOCK_START_STRING, BLOCK_END_STRING) 11 | BLOCKMIX = '({%(?:(?!%}).)+?)(___\d*___)(.+?%})'# % (BLOCK_START_STRING, BLOCK_END_STRING) 12 | CONTROLSPLIT = '((?:%s.+?%s)+)' % (BLOCK_START_STRING, BLOCK_END_STRING) 13 | CONTROLSPLIT2 = '((?:%s(?:(?!yn ).)+?%s)+)' % (BLOCK_START_STRING, BLOCK_END_STRING) 14 | VARSPLIT = '((?:%s.+?%s)+)' % (VARIABLE_START_STRING, VARIABLE_END_STRING) 15 | 16 | FIXTEST = '({(?:___\d+___)?(?:{|%).+?(?:}|%)(?:___\d+___)?})' 17 | RUNSPLIT = '(___\d+___)' 18 | RUNSPLIT2 = '___(\d+)___' 19 | 20 | def tag_test(txt): 21 | p = re.compile(TAGTEST) 22 | rv = p.findall(txt) 23 | return bool(rv) 24 | 25 | def block_tag_test(txt): 26 | p = re.compile(BLOCKTEST) 27 | rv = p.findall(txt) 28 | return bool(rv) 29 | 30 | def control_split(txt): 31 | split_pattern = re.compile(CONTROLSPLIT) 32 | parts = split_pattern.split(txt) 33 | return parts 34 | 35 | def var_split(txt): 36 | split_pattern = re.compile(VARSPLIT) 37 | parts = split_pattern.split(txt) 38 | return parts 39 | 40 | #need a better way to handle this 41 | def fix_test(txt): 42 | split_pattern = re.compile(FIXTEST) 43 | parts = split_pattern.split(txt) 44 | for i, part in enumerate(parts): 45 | if i % 2 == 1: 46 | pattern = re.compile(RUNSPLIT) 47 | rv = pattern.findall(part) 48 | if rv : 49 | return True 50 | 51 | def tag_fix(txt): 52 | split_pattern = re.compile(FIXTEST) 53 | parts = split_pattern.split(txt) 54 | p = '' 55 | for i, part in enumerate(parts): 56 | if i % 2 == 1: 57 | p += fix_step2(part) 58 | else: 59 | p += part 60 | split_pattern2 = re.compile(RUNSPLIT2) 61 | parts = split_pattern2.split(p) 62 | d = {} 63 | for i in range(1, len(parts), 2): 64 | d[int(parts[i])] = parts[i + 1] 65 | return d 66 | 67 | def fix_step2(txt): 68 | split_pattern = re.compile(RUNSPLIT) 69 | parts = split_pattern.split(txt) 70 | p0 = '' 71 | p1 = '' 72 | for index,part in enumerate(parts): 73 | if index % 2 == 0: 74 | p0 += part 75 | else: 76 | p1 += part 77 | return p0+p1 78 | 79 | class TreeProperty(object): 80 | 81 | def __init__(self, name): 82 | self.name = name 83 | self._name = '_' + name 84 | 85 | def __set__(self, instance, value): 86 | instance.__dict__[self._name] = value 87 | 88 | def __get__(self, instance, cls): 89 | if not hasattr(instance, self._name): 90 | instance.__dict__[self._name] = getattr(instance._parent, self.name) 91 | return instance.__dict__[self._name] 92 | 93 | from types import MethodType 94 | def bind(method, instance): 95 | if isinstance(method, MethodType): 96 | instance.__dict__[method.__name__] = MethodType(method.im_func, instance) 97 | else: 98 | instance.__dict__[method.__name__] = MethodType(method, instance) -------------------------------------------------------------------------------- /pydocxtpl/ext.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import print_function 4 | import sys 5 | from six import text_type 6 | from jinja2 import Environment, nodes 7 | from jinja2.ext import Extension 8 | from jinja2.exceptions import TemplateSyntaxError 9 | 10 | from .text import RunX, Segment 11 | 12 | class Env(Environment): 13 | 14 | def handle_exception(self, *args, **kwargs): 15 | exc_type, exc_value, tb = sys.exc_info() 16 | red_fmt = '\033[31m%s\033[0m' 17 | blue_fmt = '\033[34m%s\033[0m' 18 | error_type = red_fmt % ('error type: %s' % exc_type) 19 | error_message = red_fmt % ('error message: %s' % exc_value) 20 | print(error_type) 21 | print(error_message) 22 | if exc_type is TemplateSyntaxError: 23 | lineno = exc_value.lineno 24 | source = kwargs['source'] 25 | src_lines = source.splitlines() 26 | for i, line in enumerate(src_lines): 27 | if i + 1 == lineno: 28 | line_str = red_fmt % ('line %d : %s' % (i + 1, line)) 29 | elif i + 1 in [lineno - 1, lineno + 1]: 30 | line_str = blue_fmt % ('line %d : %s' % (i + 1, line)) 31 | else: 32 | line_str = 'line %d : %s' % (i + 1, line) 33 | print(line_str) 34 | Environment.handle_exception(self, *args, **kwargs) 35 | 36 | class NodeExtension(Extension): 37 | tags = set(['node', 'root', 'body', 'para', 'run', 'hyperlink', 38 | 'table', 'row', 'cell', 'default', 'headtail']) 39 | 40 | def __init__(self, environment): 41 | super(self.__class__, self).__init__(environment) 42 | environment.extend(root = None) 43 | 44 | def parse(self, parser): 45 | lineno = next(parser.stream).lineno 46 | args = [parser.parse_expression()] 47 | body = [] 48 | return nodes.CallBlock(self.call_method('_node', args), 49 | [], [], body).set_lineno(lineno) 50 | 51 | def _node(self, key, caller): 52 | node = self.environment.root.get_node(key) 53 | return key 54 | 55 | class SegmentExtension(Extension): 56 | tags = set(['seg']) 57 | 58 | def parse(self, parser): 59 | lineno = next(parser.stream).lineno 60 | args = [parser.parse_expression()] 61 | body = parser.parse_statements(['name:endseg'], drop_needle=True) 62 | return nodes.CallBlock(self.call_method('_seg', args), 63 | [], [], body).set_lineno(lineno) 64 | 65 | def _seg(self, key, caller): 66 | node = self.environment.root.get_node(key) 67 | rv = caller() 68 | rv = node.process_rv(rv) 69 | return rv 70 | 71 | class PicExtension(Extension): 72 | tags = set(['pic', 'img']) 73 | 74 | def __init__(self, environment): 75 | super(self.__class__, self).__init__(environment) 76 | environment.extend(root = None) 77 | 78 | def parse(self, parser): 79 | lineno = next(parser.stream).lineno 80 | args = [parser.parse_expression()] 81 | body = [] 82 | return nodes.CallBlock(self.call_method('_node', args), 83 | [], [], body).set_lineno(lineno) 84 | 85 | def _node(self, fname, caller): 86 | node = self.environment.root.current_node 87 | fname = text_type(fname) 88 | if isinstance(node, RunX): 89 | node._parent.replace_pic(fname) 90 | elif isinstance(node, Segment): 91 | node._parent._parent.replace_pic(fname) 92 | return fname -------------------------------------------------------------------------------- /pydocxtpl/node.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import print_function 4 | from six import text_type 5 | from unicodedata import normalize 6 | from copy import deepcopy 7 | from docx.document import _Body 8 | from .utils import TreeProperty 9 | 10 | node_clses = {} 11 | class Node(object): 12 | node_map = TreeProperty('node_map') 13 | ext_tag = 'node' 14 | 15 | def __init__(self): 16 | self._children = [] 17 | self.unpacked = False 18 | self.no = 0 19 | 20 | @property 21 | def depth(self): 22 | if not hasattr(self, '_depth'): 23 | if not hasattr(self, '_parent') or self._parent is self: 24 | self._depth = 0 25 | else: 26 | self._depth = self._parent.depth + 1 27 | return self._depth 28 | 29 | @property 30 | def node_key(self): 31 | return '%s,%s' % (self._parent.node_key, self.no) 32 | 33 | @property 34 | def node_tag(self): 35 | return "{%%%s '%s' %%}" % (self.ext_tag, self.node_key) 36 | 37 | @property 38 | def print_tag(self): 39 | return self.node_tag 40 | 41 | def children_to_tag(self): 42 | x = [] 43 | for child in self._children: 44 | x.append(child.to_tag()) 45 | return '\n'.join(x) 46 | 47 | def to_tag(self): 48 | self.node_map[self.node_key] = self 49 | if self._children: 50 | return self.children_to_tag() 51 | return self.node_tag 52 | 53 | def tag_tree(self): 54 | print_tag = normalize("NFKD", text_type(self.print_tag)) 55 | print('\t' * self.depth, print_tag) 56 | for child in self._children: 57 | child.tag_tree() 58 | 59 | def get_node_cls(self, sub_element): 60 | return node_clses.get(type(sub_element), Default) 61 | 62 | def unpack_element(self): 63 | for sub_element in self._element: 64 | node_cls = self.get_node_cls(sub_element) 65 | child = node_cls(sub_element, self) 66 | self.add_child(child) 67 | if not self.unpacked and child.unpacked: 68 | self.unpacked = True 69 | 70 | def clear_element(self): 71 | if not self.unpacked: 72 | del self._children[:] 73 | return 74 | elms = self._element[:] 75 | for elm in elms: 76 | self._element.remove(elm) 77 | 78 | def unpack_and_clear(self): 79 | self.unpack_element() 80 | self.clear_element() 81 | 82 | def copy_element(self): 83 | return deepcopy(self._element) 84 | 85 | def add_child(self, child): 86 | child.no = len(self._children) 87 | #child._parent = self 88 | self._children.append(child) 89 | 90 | def enter(self): 91 | pass 92 | 93 | def reenter(self): 94 | self.enter() 95 | 96 | def child_reenter(self): 97 | pass 98 | 99 | def exit(self): 100 | pass 101 | 102 | def process_child_rv(self, rv): 103 | pass 104 | 105 | def __str__(self): 106 | return self.__class__.__name__ +' , ' + self.node_tag 107 | 108 | class HtNode(Node): 109 | ext_tag = 'headtail' 110 | 111 | def __init__(self, parent): 112 | self._parent = parent 113 | Node.__init__(self) 114 | 115 | def to_tag(self): 116 | if self.no == 0: 117 | return '' 118 | else: 119 | return Node.to_tag(self) 120 | 121 | def enter(self): 122 | if self.no != 0: 123 | self._parent.exit() 124 | 125 | def exit(self): 126 | if self.no == 0: 127 | self._parent.enter() 128 | 129 | 130 | class RvNode(Node): 131 | ext_tag = 'rv' 132 | 133 | def enter(self): 134 | self.rv = self.copy_element() 135 | 136 | def exit(self): 137 | self._parent.process_child_rv(self.rv) 138 | 139 | def process_child_rv(self, rv): 140 | self.rv.append(rv) 141 | 142 | class Root(RvNode): 143 | ext_tag = 'root' 144 | 145 | def __init__(self, root): 146 | self._root = root 147 | self._element = root._element 148 | self._parent = self 149 | self.part = root.part 150 | self.node_map = {} 151 | Node.__init__(self) 152 | self.head_node = head_node = HtNode(self) 153 | tail_node = HtNode(self) 154 | self.add_child(head_node) 155 | self.unpack_element() 156 | self.add_child(tail_node) 157 | self.current_node = head_node 158 | self.current_key = '' 159 | 160 | @property 161 | def node_key(self): 162 | return '0' 163 | 164 | def enter(self): 165 | self.clear_element() 166 | self.rv = self._element 167 | 168 | def exit(self): 169 | self.current_node = self.head_node 170 | self.current_key = '' 171 | 172 | def find_lca(self, pre, next): 173 | # find lowest common ancestor 174 | next_branch = [] 175 | if pre.depth > next.depth: 176 | for i in range(next.depth, pre.depth): 177 | pre.exit() 178 | # print(pre, 'pre up', pre._parent) 179 | pre = pre._parent 180 | 181 | elif pre.depth < next.depth: 182 | for i in range(pre.depth, next.depth): 183 | next_branch.insert(0, next) 184 | # print(next, 'next up', next._parent) 185 | next = next._parent 186 | if pre is next: 187 | pass 188 | else: 189 | pre_parent = pre._parent 190 | next_parent = next._parent 191 | while pre_parent != next_parent: 192 | # print(pre, next, 'up together') 193 | pre.exit() 194 | pre = pre_parent 195 | pre_parent = pre._parent 196 | next_branch.insert(0, next) 197 | next = next_parent 198 | next_parent = next._parent 199 | pre.exit() 200 | if pre_parent._children.index(pre) > pre_parent._children.index(next): 201 | pre_parent.child_reenter() 202 | next.reenter() 203 | else: 204 | next.enter() 205 | 206 | for next in next_branch: 207 | next.enter() 208 | 209 | def get_node(self, key): 210 | #if self.current_key == key: 211 | # return self.current_node 212 | self.last_node = self.current_node 213 | self.last_key = self.current_key 214 | self.current_node = self.node_map.get(key) 215 | self.current_key = key 216 | self.find_lca(self.last_node, self.current_node) 217 | return self.current_node 218 | 219 | 220 | class BodyX(_Body, RvNode): 221 | ext_tag= 'body' 222 | 223 | def __init__(self, element, parent): 224 | _Body.__init__(self, element, parent) 225 | RvNode.__init__(self) 226 | self.unpack_and_clear() 227 | 228 | 229 | class Default(RvNode): 230 | ext_tag = 'default' 231 | 232 | def __init__(self, element, parent): 233 | RvNode.__init__(self) 234 | self._element = element 235 | self._parent = parent -------------------------------------------------------------------------------- /pydocxtpl/text.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import print_function 4 | from docx.oxml import CT_P, CT_R 5 | from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne, ZeroOrMore, OneAndOnlyOne 6 | 7 | class CT_Drawing(BaseOxmlElement): 8 | inline = OneAndOnlyOne('wp:inline') 9 | 10 | class CT_Run(CT_R): 11 | drawing = ZeroOrOne('w:drawing') 12 | 13 | class CT_Hyperlink(BaseOxmlElement): 14 | r = ZeroOrMore('w:r') 15 | 16 | class CT_Para(CT_P): 17 | hyperlink = ZeroOrMore('w:hyperlink') 18 | 19 | from docx.text.paragraph import Paragraph 20 | from docx.text.run import Run 21 | from docx.shared import Parented 22 | 23 | from .node import RvNode 24 | from .utils import control_split, var_split, tag_test, fix_test, tag_fix 25 | 26 | class ParagraghX(Paragraph, RvNode): 27 | ext_tag = 'para' 28 | 29 | def __init__(self, element, parent): 30 | Paragraph.__init__(self, element, parent) 31 | RvNode.__init__(self) 32 | self.unpack_p() 33 | self.current_drawing = None 34 | 35 | def unpack_p(self): 36 | if not tag_test(self.text + self.hyperlink_text): 37 | return 38 | self.fix_and_unpack() 39 | 40 | def fix_and_unpack(self): 41 | text_4_fix = self.text_4_fix() 42 | if fix_test(text_4_fix): 43 | fixed = tag_fix(text_4_fix) 44 | #print(text_4_fix) 45 | #print(fixed) 46 | for i, sub_element in enumerate(self._element): 47 | if i in fixed: 48 | text = fixed[i] 49 | if text == '': 50 | if sub_element.text != '': 51 | continue 52 | #else:#drawing 53 | else: 54 | sub_element.text = text 55 | node_cls = self.get_node_cls(sub_element) 56 | child = node_cls(sub_element, self) 57 | self.add_child(child) 58 | self.unpacked = True 59 | self.clear_element() 60 | else: 61 | RvNode.unpack_and_clear(self) 62 | 63 | 64 | def register_drawing(self, drawing): 65 | self.current_drawing = drawing 66 | 67 | def replace_pic(self, fname): 68 | if self.current_drawing is None: 69 | return 70 | try: 71 | rId, image = self.part.get_or_add_image(fname) 72 | except: 73 | return 74 | inline = self.current_drawing.inline 75 | inline.docPr.id = self.part.next_id 76 | inline.docPr.name = fname 77 | inline.docPr.descr = fname 78 | pic = inline.graphic.graphicData.pic 79 | pic.nvPicPr.cNvPr.id = 0#pic_id 80 | pic.nvPicPr.cNvPr.name = fname 81 | pic.blipFill.blip.embed = rId 82 | 83 | @property 84 | def print_tag(self): 85 | return self.node_tag + ' ' + self.text 86 | 87 | @property 88 | def hyperlinks(self): 89 | return [Hyperlink(hyperlink, self) for hyperlink in self._p.hyperlink_lst] 90 | 91 | @property 92 | def hyperlink_text(self): 93 | text = '' 94 | for hyperlink in self.hyperlinks: 95 | text += hyperlink.text 96 | return text 97 | 98 | def text_4_fix(self): 99 | text = '' 100 | tmpl = '___%d___' 101 | for i,elm in enumerate(self._element): 102 | if isinstance(elm, CT_R): 103 | text += (tmpl % i ) + elm.text 104 | return text 105 | 106 | 107 | class RunX(Run, RvNode): 108 | ext_tag = 'run' 109 | 110 | def __init__(self, element, parent): 111 | Run.__init__(self, element, parent) 112 | RvNode.__init__(self) 113 | self.unpack_r() 114 | 115 | def unpack_r(self): 116 | if not tag_test(self.text): 117 | return 118 | self.unpacked = True 119 | parts = control_split(self.text) 120 | for index,part in enumerate(parts): 121 | if part == '': 122 | continue 123 | if index % 2 == 0: 124 | sub_parts = var_split(part) 125 | for sub_index, sub_part in enumerate(sub_parts): 126 | if sub_part == '': 127 | continue 128 | if sub_index % 2 == 0: 129 | child = TextSegment(sub_part, self) 130 | else: 131 | child = VarSegment(sub_part, self) 132 | self.add_child(child) 133 | else: 134 | child = ControlSegment(part, self) 135 | self.add_child(child) 136 | 137 | def enter(self): 138 | self.rv = self.copy_element() 139 | if self.rv.drawing is not None: 140 | self._parent.register_drawing(self.rv.drawing) 141 | self.children_rvs = '' 142 | 143 | def process_child_rv(self, rv): 144 | self.children_rvs += rv 145 | 146 | def exit(self): 147 | if self._children: 148 | self.rv.text = self.children_rvs 149 | self._parent.process_child_rv(self.rv) 150 | 151 | @property 152 | def print_tag(self): 153 | return self.node_tag + ' ' + self.text 154 | 155 | class Segment(RvNode): 156 | 157 | def __init__(self, text, parent): 158 | RvNode.__init__(self) 159 | self.text = text 160 | self._parent = parent 161 | 162 | def process_rv(self, rv): 163 | self.rv = rv 164 | return rv 165 | 166 | def enter(self): 167 | self.rv = '' 168 | 169 | class TextSegment(Segment): 170 | 171 | @property 172 | def node_tag(self): 173 | tmpl = "{%%seg '%s'%%}{%%endseg%%}" 174 | return tmpl % self.node_key 175 | 176 | def process_rv(self, rv): 177 | self.rv = self.text 178 | return self.text 179 | 180 | class VarSegment(Segment): 181 | 182 | @property 183 | def node_tag(self): 184 | tmpl = "{%%seg '%s'%%}%s{%%endseg%%}" 185 | return tmpl % (self.node_key, self.text) 186 | 187 | class ControlSegment(Segment): 188 | 189 | @property 190 | def node_tag(self): 191 | tmpl = "%s{%%seg '%s'%%}{%%endseg%%}" 192 | return tmpl % (self.text, self.node_key) 193 | 194 | 195 | class Hyperlink(Parented): 196 | 197 | def __init__(self, hyperlink, parent): 198 | super(Hyperlink, self).__init__(parent) 199 | self._hyperlink = self._element = hyperlink 200 | 201 | def add_run(self, text=None, style=None): 202 | r = self._hyperlink.add_r() 203 | run = Run(r, self) 204 | if text: 205 | run.text = text 206 | if style: 207 | run.style = style 208 | return run 209 | 210 | def clear(self): 211 | self._hyperlink.clear_content() 212 | return self 213 | 214 | @property 215 | def runs(self): 216 | return [Run(r, self) for r in self._hyperlink.r_lst] 217 | 218 | @property 219 | def text(self): 220 | text = '' 221 | for run in self.runs: 222 | text += run.text 223 | return text 224 | 225 | @text.setter 226 | def text(self, text): 227 | self.clear() 228 | self.add_run(text) 229 | 230 | from .utils import bind 231 | class HyperlinkX(Hyperlink, RvNode): 232 | ext_tag = 'hyperlink' 233 | 234 | def __init__(self, hyperlink, parent): 235 | Hyperlink.__init__(self, hyperlink, parent) 236 | RvNode.__init__(self) 237 | bind(ParagraghX.fix_and_unpack, self) 238 | bind(ParagraghX.text_4_fix, self) 239 | bind(ParagraghX.register_drawing, self) 240 | bind(ParagraghX.replace_pic, self) 241 | self.unpack_hyperlink() 242 | self.current_drawing = None 243 | 244 | def unpack_hyperlink(self): 245 | if not tag_test(self.text): 246 | return 247 | self.fix_and_unpack() 248 | 249 | @property 250 | def print_tag(self): 251 | return self.node_tag + ' ' + self.text 252 | --------------------------------------------------------------------------------