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 & 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:
- Complete subtrees with
→ node.remove()
- Attributes with the
del
statement - A node’s markup by assigning to the node’s
→ hiddenelement
attribute. The initial value of this attribute reflects the-
/+
node flags. - 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 & 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 attributerender_list
(conditionally) removes a complete node
<html>
<body>
<h1>Editing Content & Attributes</h1>
<ul class="menu">
<li><a href="menu1">some menu item</a></li>
<li>Editing Content & 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><p>Banner!</p></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 ;-). |