Skip to content

HTML-style formatting #582

Open
Open

Description

@santerioksanen

Is your feature request related to a problem? Please describe.

First of all thank you for an excellent library!
In my use-case I need per-paragraph formatting, which is achievable using RichText-objects. These however require to clear the existing formatting, and are quite cumbersome to use. In our use-case we would also like to use the same context to render into HTML and Word-documents.

Describe the solution you'd like

I would like for a context-vairable: 'I am bolded text' to be formatted properly.
I have very limited understanding in Word styling, but according to my limited understanding, this would not be achievable by modifying the context-variables due to requirement to close the existing formatting.

In this case, the approach would be to:

  • First render the template with context, as is done now:
  • After this, add a step to modify the Word-styling

For modifying the Word styling:
Whenever encountering <b>, <i> or <u> (maybe even more styling tags?):

  • Read the styles from current content tag
  • Close the current content tag
  • Open a new content tag, and attach these styles + the bold/italic styles to the content tag
  • Write the content
  • Close the content tag, and open a new with styling from the innermost tag

Due to nesting possibilities, the process would need to be recursive.

Describe alternatives you've considered

Instead of HTML-tags, custom tags may also be used, f.ex: ([b], [i], [u])?

Would this be a good fit for the library?

Activity

  1. PHvL commented on Sep 5, 2025

    @PHvL

    Since we prepare the context for the template as pydantic schema anyway, we've added a BeforeValidator doing the conversion of HTML tags.

    from typing import Annotated
    from html.parser import HTMLParser
    import html.entities
    import re
    
    from docxtpl import RichText
    from pydantic import BaseModel, ConfigDict, BeforeValidator
    
    
    TEXT_PROPS_BOOL = {
    	'sub': 'subscript',
    	'sup': 'superscript',
    	'i': 'italic',
    	'b': 'bold',
    }
    
    
    class HTML_RICHTEXT(HTMLParser):
    	def __init__(self):
    		super().__init__()
    		self.reset()
    		self.fed = RichText('')
    		self.text_prop = {}
    
    	def handle_data(self, d):
    		self.fed.add(d, **self.text_prop)
    
    	def handle_starttag(self, tag, attrs):
    		if tag in TEXT_PROPS_BOOL:
    			self.text_prop[TEXT_PROPS_BOOL[tag]] = True
    
    	def handle_startendtag(self, tag, attrs):
    		if tag == 'br':
    			self.fed.add('\n')
    
    	def handle_endtag(self, tag):
    		if tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p']:
    			self.fed.add('\n')
    		elif tag in TEXT_PROPS_BOOL:
    			if TEXT_PROPS_BOOL[tag] in self.text_prop:
    				del self.text_prop[TEXT_PROPS_BOOL[tag]]
    
    	def handle_charref(self, name):
    		self.fed.add(chr(int(name[1:])))
    
    	def handle_entityref(self, name):
    		self.fed.add(chr(html.entities.name2codepoint[name]))
    
    	def get_data(self):
    		return self.fed
    
    
    def str_to_richtext(html: str|RichText) -> RichText:
    	if isinstance(html, RichText):
    		return html
    	if '<' in html:
    		s = HTML_RICHTEXT()
    		html = re.sub(r'[\r\n]+', '\n', html)
    		html = re.sub(r'\s+', ' ', html)
    		s.feed(html)
    		return s.get_data()
    	else:
    		return RichText(html)
    
    
    class Letter(BaseModel):
    	model_config = ConfigDict(arbitrary_types_allowed=True)
    	body: Annotated[RichText, BeforeValidator(str_to_richtext)]
    
    context = Letter(body='I am <b>bolded</b> text.').model_dump()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions