Truncate HTML to a length, keeping tags balanced¶
Cutting an HTML string at a character count with a slice breaks it: you can land in the middle of a tag or leave an element open. Truncating the tree instead keeps the markup well-formed. Parse the HTML, walk it counting visible text, and drop everything past the budget; because you edit nodes rather than a string, every tag you keep stays balanced.
The walk trims the text node that crosses the limit and removes whole subtrees after it with
decompose():
import turbohtml
from turbohtml import Element, Text
def truncate(html, limit):
body = turbohtml.parse(html).find("body")
_trim(body, limit)
return "".join(child.serialize() for child in body)
def _trim(node, budget):
for child in list(node):
if budget <= 0:
child.decompose()
elif isinstance(child, Text):
if len(child.data) > budget:
child.data = child.data[:budget].rstrip() + "…"
budget = 0
else:
budget -= len(child.data)
elif isinstance(child, Element):
budget = _trim(child, budget)
return budget
print(truncate("<div><p>Hello world, this is long.</p><p>Second para.</p></div>", 11))
<div><p>Hello world…</p></div>
The count is over visible text, so tags do not eat the budget, and the second paragraph – reached after the budget ran out – drops whole rather than emptied. An element trimmed mid-text keeps its wrapper:
print(truncate("<article><h2>Title</h2><p>Body text that is fairly long here.</p></article>", 9))
<article><h2>Title</h2><p>Body…</p></article>
This is the Django Truncator.chars and TruncateHTMLParser behavior without the parser subclass: assign a
Text.data to rewrite a run of text, and call decompose() to remove a
node and its subtree. To keep a whole region rather than a length, Trim a document to a selector trims to a selector instead.