DEV Community

canonical
canonical

Posted on

How to Implement a Visual Word Template Similar to poi-tl with 800 Lines of Code

poi-tl is a Word template engine based on the Apache POI project. Compared to manually programming POI objects to construct Word documents, poi-tl can use ordinary Word files as base templates and replace custom tags within them to generate output files, thus achieving a certain degree of visual design. For example, tags are marked in the template using the {{xxx}} form.

poi-tl-example

Then, during execution, by passing in some control rules and data objects, the output file can be obtained:

LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();

Configure config = Configure.builder()
        .bind("goods", policy).bind("labors", policy).build(); 

XWPFTemplate template = XWPFTemplate.compile(resource, config).render(
  new HashMap<String, Object>() {{
      put("goods", goods);
      put("labors", labors);
    }}
);
Enter fullscreen mode Exit fullscreen mode

Generation result:
poi-tl-example-result

According to poi-tl's documentation: The template is a Docx format Word document. You can use Microsoft Office, WPS Office, Pages, or any other software you like to create the template, or you can use Apache POI code to generate the template. All tags start with {{ and end with }}. Tags can appear anywhere, including headers, footers, inside tables, text boxes, etc. poi-tl templates follow a "What You See Is What You Get" (WYSIWYG) design; the styles of the template and tags are fully preserved.

poi-tl is very powerful; it has built-in tags for conditionals, loops, images, looping table rows, looping table columns, and more. When the built-in tags are not fully applicable, the generation process can be customized through a plugin mechanism.

The implementation principle of poi-tl is roughly to first parse the Word template file into POI model objects, then identify the tag markers and convert them into a custom MetaTemplate structure for execution. Both the internal implementation of the template engine and the implementation of extension plugins require a considerable understanding of the POI object model.

The implementation method of the poi-tl engine can be said to be a relatively traditional object-oriented programming approach. Its implementation code is actually quite complex and not very intuitive or easy to understand. In today's world where descriptive programming is prevalent, is there a simpler and more direct way to convert Word files into WYSIWYG generation templates?

In the Nop platform, guided by the theory of reversible computation, we have also implemented a Word template engine. Its core code is only eight or nine hundred lines, but it provides extensibility and visual design capabilities that surpass poi-tl. Below, I will specifically introduce the key points of this technical solution.

Office Open XML (OOXML) is a DSL

Starting with the 2007 version, Office software began using the OpenXML standard file format to store Office documents. Opening a docx file as a zip file, we can see that it is a file package composed of a bunch of XML files. The main document content is in the document.xml file, and its content is roughly similar to:

<w:p w14:paraId="60F48F74" w14:textId="77777777" w:rsidR="00A81D38" w:rsidRPr="004C00AA" w:rsidRDefault="00A81D38" w:rsidP="00A81D38">
  <w:pPr>
    <w:pStyle w:val="a7"/>
    <w:rPr>
      <w:rFonts w:ascii="Hei" w:eastAsia="Hei"/>
    </w:rPr>
  </w:pPr>
  <w:r w:rsidRPr="004C00AA">
    <w:rPr>
      <w:rFonts w:ascii="Hei" w:eastAsia="Hei" w:hint="eastAsia"/>
      <w:lang w:val="zh-CN"/>
    </w:rPr>
    <w:t>Payment Notice</w:t>
  </w:r>
</w:p>
Enter fullscreen mode Exit fullscreen mode

Looking directly at document.xml, we find that it is actually simpler and more intuitive than the POI model objects. Even without referring to any documentation, based on the nested tag structure of XML and semantic parameter names, we can make educated guesses about the purpose of specific tags. Moreover, with the visual design capabilities of Office, we can make an adjustment in the office software and then compare the changes in the XML file before and after the adjustment to intuitively see the effect of specific attributes.

The Nop platform provides a file monitoring tool. When a docx file is modified, it automatically decompresses it to a specified directory and formats the document.xml file. It can also monitor the directory in reverse; when the document.xml file is modified, it automatically packages and generates the docx file. This allows debugging docx file attributes just like debugging HTML.

OpenXML can be seen as a Domain Specific Language (DSL) for describing office documents. The XML tag structure actually expresses its internal Abstract Syntax Tree (AST). Based on this understanding, we can find that to implement the templatization of Word files, it can be done directly at the text structure or XML structure level, similar to how JSP implements HTML templatization, without the need to first parse the XML into strongly typed POI objects and then weaken the object structure to convert it into a template structure.

An essential issue here is that objects at different levels have different types. For example, the object types for Paragraph and Table nodes are different; their attribute counts and types are different, and their operation methods and traversal methods are also different. However, for template generation, they are all tags + attributes + child nodes, completely consistent at the structural level. There is no need to distinguish objects at the template level!

According to reversible computation theory, beneath the diverse world of objects lies a thick structural layer, just as behind various architectural entities there exist unified civil engineering principles and tools. The Nop platform is an open-source implementation of reversible computation theory. Its overall technical strategy is formulated around the Tree structure. It views all AST trees as Tree structures and, conversely, all Tree structures as AST trees. Through the XLang programming language, it provides unified and general technical solutions for the Generation, Transformation, Validation, and Analysis of Trees. Because OpenXML is an XML-format DSL, the templatization of docx files can be directly implemented using the XPL template language in the Nop platform without any additional development! For example:

<c:for var="order" items="${entity.orders}">
  <w:tr w:rsidR="00AC64A8" w14:paraId="377D9E14" w14:textId="77777777" w:rsidTr="00F65D42">
    <w:tc>
      <w:p w14:paraId="4CB6B484" w14:textId="1AADBFB6" w:rsidR="00AC64A8" w:rsidRDefault="00871FB8" w:rsidP="00F65D42">
        <w:r w:rsidR="006F674D">
          <w:rPr>
            <w:rFonts w:ascii="Hei" w:eastAsia="Hei" w:hAnsi="Hei" w:cs="Hei"/>
          </w:rPr>
          <w:t>${order.saleDate}</w:t>
        </w:r>
      </w:p>
    </w:tc>
    ...
  </w:tr>
</c:for>
Enter fullscreen mode Exit fullscreen mode

Directly inserting the <c:for> tag enables looping of table rows, and dynamic text output is achieved through EL expressions like ${order.saleDate}.

For more commonly used functions, we can abstract them into custom tag libraries, making them usable anywhere. The difficulty of customization decreases by an order of magnitude compared to POI programming.

<c:lib from="/nop/ooxml/xlib/docx-gen.xlib" />
<!-- Output Image -->
<docx-gen:Drawing resource="${xxx}" name="yy" width="100" height="200" />
Enter fullscreen mode Exit fullscreen mode

Of course, the above approach is not uncommon; in fact, many people use the FreeMarker template language to generate docx files. The disadvantage of using FreeMarker is that it requires manually modifying and maintaining the template file. It cannot use Office software as a visual designer like poi-tl, allowing custom adjustments to the template file at any time through WYSIWYG editing tools. The Nop platform proposes a clever method that can achieve visual design functions similar to or even surpassing poi-tl. More importantly, based on the theoretical analysis of reversible computation, this method is actually universal and can be extended to various visual design tools based on other XML file formats!

Nop Word Template

The cleverness of the Nop Word Template lies in its utilization of a visual element within Word software that supports custom extended information: hyperlinks. The specific approach is as follows:

1. Add Hyperlinks to Text in the Template That Needs to Be Replaced

link-expr

The link text can be sample content, such as "express delivery," and the link address format is expr:EL expression, used to express how to obtain data, for example, expr: order.delivery.

The hyperlink element can retain all style settings, and its insertion position is accurate in the document structure (inserting fields sometimes places them outside paragraphs). At the same time, compared to poi-tl inserting text content directly into the document, using hyperlinks can better maintain the original display structure. Especially when the expression content is long, sample text can be used instead, avoiding situations where excessive text causes table deformation or line breaks. Expressing expressions through hyperlinks avoids occupying display space; when the mouse hovers over the hyperlink, the relevant content is automatically displayed.

If the display space on the interface is sufficient, the link text can also be used as the expression. In this case, the link address format is expr: or xpl:, meaning that when there is no expression content in the link address, the link text is used as the expression. For example, ${entity.consignee} in the figure.

expr: Indicates inserting an EL expression. The built-in expression syntax is similar to JavaScript.

xpl: Indicates inserting an XPL template language fragment, which supports embedded expression output like a${b}c and also supports more complex tag structures.

2. Hyperlinks Can Represent Inserting Complete Code Blocks

xpl-tag

Through xpl: hyperlinks, complete XPL tags can be inserted. In the specific tag implementation, any code block can be output. For example:

<package-diagrams outputMode="xml">
   <source>
       ....
       <w:drawing>
         ...
       </w:drawing>
   </source>
</package-diagrams>
Enter fullscreen mode Exit fullscreen mode

3. Represent Nested Block Structures by Inserting Paired Hyperlinks

link-xpl

Paired hyperlinks xpl:<c:for var="order" items="${entity.orders}>" and xpl:</c:for> can be inserted to indicate that the content between them needs to be wrapped in a <c:for> loop tag.

Compared to poi-tl, this approach is more flexible, allows introducing custom tags, has strict variable scope definitions, and does not require introducing various special convention syntaxes.

4. Add Hyperlinks to Images That Need to Be Replaced

link-image

expr can specify the image resource object (the return result of the expression only needs to be the IResource interface). Compared to poi-tl's image embedding method, this method allows visual adjustment of image size and display method.

5. Directly Embed EL Expressions

In Word text, EL expressions like ${expr} can be directly inserted. Sometimes, due to font reasons, an expression might be split into multiple <w:t> tags, causing the EL expression not to be parsed correctly.
In this case, you can select the expression text and add a hyperlink, setting the link content to xpl:. Also, note that characters like ${ must be English characters, not mistakenly used as Chinese characters.
If you indeed need to output the ${ characters in the final result, you can use the escape method: ${'$'}{'.

6. Introduce Initialization Code Through XplGenConfig Configuration

poi-tl is a so-called "logic-less" template engine, with no complex control structures or variable assignments, only tags. This approach reduces the complexity of the template engine implementation but also means that data preparation work needs to be implemented in Java code, and the built-in rendering strategies of the template are relatively rigid. Slight deviations from the default design scenario may require separate programming implementation in Java.

If we want to implement a Word template management platform, the template itself must have a certain degree of logical independence. Much initialization and data preparation work should be completed inside the template rather than relying on external code to prepare context data variables.

xpl-config

An XplGenConfig configuration table can be inserted at the end of the template, supporting the following configuration items:

  • dump: When the template is compiled, it is converted into XPL template language code. This switch controls whether to print the converted result for easy debugging.
  • dumpFile: When dump=true, controls which file the converted code should be output to. This process formats the XML (the XML in docx files has no indentation by default, making it inconvenient to view).
  • importLibs: Introduce custom tag libraries. By default, /nop/ooxml/xlib/docx-gen.xlib is introduced.
  • beforeGen: Initialization code executed before template generation.
  • afterGen: Code executed after template generation.

The Word template is converted into the XPL template language and then compiled and output as an XPL template. The converted code can be viewed through dumpFile, and its structure is roughly similar to:

<c:unit>
  <c:import from="/nop/test/orm-docx.xlib"/>
  <c:import from="/nop/ooxml/xlib/docx-gen.xlib"/>
  <c:out escape="none"><?xml version="1.0" encoding="UTF-8"?>
</c:out>
  <c:unit xpl:outputMode="none">
    <c:script>logInfo("test")</c:script>
  </c:unit>
  <w:document>
      ...
  </w:document>
</c:unit>
Enter fullscreen mode Exit fullscreen mode

When the XPL template compilation encounters an error, an exception is thrown, containing error information and accurate line numbers. The line number corresponds to the position in the dumpFile, for example:

io.nop.api.core.exceptions.NopEvalException:
NopEvalException[seq=1,errorCode=nop.err.commons.text.scan-unexpected-char,
params={pos=19, reader=${model.displayNam[e], expected=}, eof=true},
desc=The next character read is not the expected character [}]]
@_loc=[68:35:0:0]file:/C:/can/entropy-cloud/nop-ooxml/nop-ooxml-docx/dump-tpl.doc.xml
  @@c:unit/w:document/w:body/w:p[2]/w:r/w:t@@[68:12:0:0]file:/C:/can/entropy-cloud/nop-ooxml/nop-ooxml-docx/dump-tpl.doc.xml
Enter fullscreen mode Exit fullscreen mode

The above error information indicates a syntax error at column 35 of line 68 in dump-tpl.doc.xml. It also shows the internal stack trace of the XLang language, not the Java function stack trace. The actual corresponding code content is:

  <w:t>${model.displayName</w:t>
Enter fullscreen mode Exit fullscreen mode

Specific template examples and output results:

payment.docx

result-payment.docx

7. Automatic Line Break Display

If the text contains carriage returns and you want them to also cause line breaks when output to Word, you can use the <docx-gen:r-br> tag.

Tags prefixed with docx-gen:r- generate <w:r> text segments. Within the tag, the rPr child node can read the styles configured in Word.

word-br

Universal Visual Template Solution

The Word template solution introduced in the previous section is essentially a universal design solution, and its scope of application is not limited to Word templates.

Thinking back carefully, the only requirement for the underlying visual designer in the entire solution is that it allows associating specified content with a visual design element (such as a hyperlink), and this element allows attaching some custom metadata (for example, saving expression code through the hyperlink's URL). If the product of the designer is itself a structured DSL, then using these markers and associated metadata, it can easily be converted into a generation template.

Actually, we don't necessarily need to choose hyperlinks to maintain template design data. In Office 2003, it supported introducing custom tags directly into Word ML through an XSD (XML Schema Definition) definition file. Custom XML tags have the following presentation form:

word-custom-tags

If using this custom XML tag mechanism, not even development is needed; document.xml can be directly compiled as XPL template language. In Office 2003, using custom XML tags had an additional benefit: Office would generate corresponding input interfaces for tag parameters based on the structure definition in the XSD, providing basic format validation.

After Office 2007, due to patent disputes between Microsoft and third-party companies, Microsoft removed the custom XML tag functionality from Office.

To support this designer association, the XPL template language internally provides a series of simplified mechanisms, such as the x:decorator mechanism.

<Button>
  <x:decorator>
     <MyContainer1 xpl:skipIf="xxx" />
     <MyContainer2 xpl:if="yyy" />
  </x:decorator>
</Button>
Enter fullscreen mode Exit fullscreen mode

Equivalent to:

<MyContainer1 xpl:skipIf="xxx">
   <MyContainer2 xpl:if="yyy">
      <Button />
   </MyContainer>
</MyContainer1>
Enter fullscreen mode Exit fullscreen mode

x:decorator is similar to Monad in functional languages, converting nested node structures into linear structures. xpl:skipIf means that if the condition is true, skip this layer node and directly render the body. xpl:if means that if the condition is false, skip this node and all child nodes.

If a visual designer has basic design capabilities, then it must have some optional visual design elements, and some attributes on these elements are also useless but harmless. As the saying goes, there is a use called "useless use." Some optional functions support a gray design space, allowing unexpected evolution to occur within it.

If a visual designer adopts an open design rather than a closed design, it should allow introducing external schema constraints for extensible data and automatically generate visual editing interfaces based on the schema!

Currently, many low-code platforms claim to be able to integrate external components. By adding some additional descriptive information, external components can be introduced into the component panel and used directly by dragging and dropping. If we take a broader perspective and examine it from a more macroscopic angle, the component tree is merely a local abstract syntax tree, and introducing custom components merely means introducing custom schema constraints for a certain part of the abstract syntax tree. We can view the entire software system (not just the draggable canvas) as a huge abstract syntax tree. External schema constraints can be introduced at various levels of the entire syntax tree to implement customization of the entire software system.

Summary

Can we effectively operate these structures and achieve their templatization without much structural knowledge? The answer from reversible computation theory is yes.

Regarding reversible computation theory and specific technical implementation, you can refer to my previous articles.

The low-code platform NopPlatform, designed based on reversible computation theory, is open source:

Top comments (0)