Simple Content Editing In Python

The counterpart of the template is the rendering logic written in python. TDI expects a so-called “model” object here, which provides callback methods for the modifiable nodes.

from tdi import html
template = html.from_string("""
<html>
<body>
    <h1 tdi="doctitle">doc title goes here</h1>
    <p tdi="intro">Intro goes here.</p>
</body>
</html>
""")

class Model(object):
    def render_doctitle(self, node):
        node.content = u"Editing Content & Attributes"

    def render_intro(self, node):
        node.content = u"Modifying content and markup attributes is easy."
        node['class'] = u"edit-intro"

model = Model()
template.render(model)

The model object can be anything you want, it is just expected to provide certain interfaces, if you want to modify nodes. It is not an error (by default) if a render_name method is missing. That way you can build up your logic step by step (or leave out methods intentionally).

Being so independent from the template source itself has interesting consequences:

  • You can apply different logic on the same template if you want to.
  • More importantly, you can apply the same model class to different templates.

Both of these items provide great flexibility and will influence the way how you reuse code – both on template and logic side.

Before we go on, here’s the output of the example above:

<html>
<body>
    <h1>Editing Content &amp; Attributes</h1>
    <p class="edit-intro">Modifying content and markup attributes is easy.</p>
</body>
</html>

The content of an HTML element (or as presented in python: of a node object) is set by assigning it to the content attribute of the node. The content is defined as everything between the start tag and the end tag of the node’s element. It’s the same whether the content was simple text before or nested elements. It’s all wiped and replaced by the text you assign to it.

HTML attributes are accessed through the subscription operator, i.e. using square brackets. The render_intro method sets the class attribute that way.

Note the following mechanisms:

TDI escapes input automatically

It knows how to escape content properly for HTML and applies appropriate escaping by default. That way you are on the safe side except you explicitely instruct TDI otherwise (sometimes it’s inevitable to insert literal HTML code, for example). How to bypass the escape mechanism is described in the section further down below.

TDI is unicode aware [1]

That means if you pass unicode to content or attributes it will be encoded however the template is encoded (or rather what TDI knows about it). In the example above the template’s character encoding is not indicated anywhere, so TDI falls back to US-ASCII as the least common denominator.

It is also possible to pass byte strings to TDI (although not recommended). Those byte strings are still escaped [2] but not transcoded otherwise. Whether they do or do not fit into the template encoding is not checked and your responsibility only. You can learn what TDI knows about the template encoding from the template’s encoding attribute. Read the character encoding section for details.

Removing Content

In a sense shaping a template with TDI often works like etching. Often you have certain nodes available, pick one to render and throw away all the others.

Technically most of the time the content isn’t actually removed, but merely prevented from being written to the output stream. The difference is mostly not important, it’s just that preventing output is a lot faster than actually removing content (“removing” nodes for example is just a bit flip).

There are three to four ways to remove content:

  1. Complete subtrees with → node.remove()
  2. Attributes with the del statement
  3. A node’s markup by assigning to the node’s → hiddenelement attribute. The initial value of this attribute reflects the - / + node flags.
  4. In case it’s not obvious, you can empty a node by assigning an empty string to → node.content
from tdi import html
template = html.from_string("""
<html>
<body>
    <h1 tdi="doctitle">doc title goes here</h1>
    <ul class="menu">
        <li><a href="menu1" tdi="menu1">some menu item</a></li>
        <li><a href="menu2" tdi="menu2">Editing Content &amp; Attributes</a></li>
        <li><a href="menu3" tdi="menu3">Other menu item</a></li>
    </ul>
    <p tdi="intro" class="edit-intro">Intro goes here.</p>
    <div class="list" tdi="list">
        ...
    </div>
</body>
</html>
""")

class Model(object):
    def __init__(self, possibilities, page):
        self._possibilities = possibilities
        self._page = page

    def render_doctitle(self, node):
        node.content = u"Editing Content & Attributes"

    def render_menu1(self, node):
        if self._page == 1:
            node.hiddenelement = True

    def render_menu2(self, node):
        if self._page == 2:
            node.hiddenelement = True

    def render_menu3(self, node):
        if self._page == 3:
            node.hiddenelement = True

    def render_intro(self, node):
        if not self._possibilities:
            del node['class']
            node.content = u"There are no possibilities listed right now."
        else:
            node.content = u"Modifying content and markup attributes is easy."

    def render_list(self, node):
        if not self._possibilities:
            node.remove()
            return
        # fill in possibilities here...

model = Model(possibilities=(), page=2)
template.render(model)

The example shows the possibilities (1), (2) and (3):

  • the render_menu<number> methods hide the link in case the particular menu item is active. (Granted, the code is crappy, but we’re going without loops in this chapter).
  • render_intro removes an attribute
  • render_list (conditionally) removes a complete node
<html>
<body>
    <h1>Editing Content &amp; Attributes</h1>
    <ul class="menu">
        <li><a href="menu1">some menu item</a></li>
        <li>Editing Content &amp; Attributes</li>
        <li><a href="menu3">Other menu item</a></li>
    </ul>
    <p>There are no possibilities listed right now.</p>
    
</body>
</html>

Bypassing Automatic Escaping - Dealing With Raw Content

Warning

Be careful. Only bypass automatic escaping with content you trust.

There are certain use cases for pasting ready-to-use HTML into the output. Examples are (elsewhere) generated code, banner code, inline scripts or styles etc. You need to circumvent TDI‘s automatic escaping mechanism in order to insert content literally.

from tdi import html
template = html.from_string("""
<div tdi="banner1"></div>
<div tdi="banner2"></div>
""")

class Model(object):
    def render_banner1(self, node):
        node.content = '<p>Banner!</p>'

    def render_banner2(self, node):
        node.raw.content = '<p>Banner!</p>'

template.render(Model())

The render_banner2 method passes the content in raw form to the output. The raw attribute is a small proxy object to the real node object and behaves as such (in a limited way), but assignments are just passed through. So you can assign content or attributes in raw form. That means:

  • If it’s already encoded, it should be encoded properly (according to the template). If you want to let TDI take care of it, pass unicode.
  • (raw attribute assignments need to include the attribute quotes)
  • You should make sure that the content doesn’t open the output for XSS attacks
  • Avoid raw content assignment whenever you can

render_banner1 assigns the same content to the regular node, and it’s properly escaped (just to show the difference):

<div>&lt;p&gt;Banner!&lt;/p&gt;</div>
<div><p>Banner!</p></div>

[1]If you don’t know about unicode, start here.
[2]Escaping byte strings assumes they’re ASCII compatible. For example, escaping UTF-16 encoded stuff that way will produce strange results. If you don’t know what UTF-16 is, don’t bother. Look up UTF-8 instead ;-).