Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 2e7b0dacf0
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 352 lines (302 sloc) 12.024 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351
"""
Hypertext: "It's Snakes all the Way Down"
=========================================

Who needs ``haml`` or templating languages? Hypertext is an in-language DSL for producing
perfectly valid (and safe) XHTML directly from Python code! It's a bit magical, that's true,
but it's thread-safe and it makes rendering HTML pages almost as compact as ``haml``.

Creating HTML elements is easy and straightforward ::

>>> import hypertext as H
>>> x = H.h1("hello", class_ = "highlight")
>>> x
<Element h1(class='highlight'), 1 subelements>

Invoking ``str`` (or via ``print``) on an HTML element renders it textually ::

>>> print x
<h1 class="highlight">hello</h1>

Alternatively, if you're not afraid of polluting your namespace ::

>>> from hypertext import *
>>> print h1("hello", class_ = "highlight")
<h1 class="highlight">hello</h1>

And there's a shortcut for specifying the class (a la ``haml``) ::

>>> print h1.highlight("hello")
<h1 class="highlight">hello</h1>

Which can be combined for specifying multiple classes... ::

>>> print h1.highlight.important("hello")
<h1 class="highlight important">hello</h1>

Element can be nested ::
>>> print div.content(h1.highlight("hello"))
<div class="content"><h1 class="highlight">hello</h1></div>

But also using ``with`` statement

>>> with div.content as root: # doctest:+ELLIPSIS
... h1.highlight("hello")
...
,,,
>>> print root
<div class="content"><h1 class="highlight">hello</h1></div>

.. note::
Don't mind the ``,,,``, it's there to make ``doctest`` happy

The attributes and text of elements can be specified after their construction ::

>>> with div.content as root:
... with h1:
... ATTR(class_ = "highlight")
... TEXT("hello")
...
>>> print root
<div class="content"><h1 class="highlight">hello</h1></div>

Text is HTML-escaped by default, but you can override it (if you trust its source) ::

>>> with div.content as root:
... dangerous = "<script>alert('oh no!');</script>"
... TEXT("hello %s" % (dangerous,))
... UNESCAPED("hello %s" % (dangerous,))
...
>>> print root
<div class="content">
hello &lt;script&gt;alert(&apos;oh no!&apos;);&lt;/script&gt;
hello <script>alert('oh no!');</script>
</div>

Putting it all together::

>>> with html as doc: # doctest:+ELLIPSIS
... with head:
... meta(http_equiv="Content-Type", content="text/html; charset=utf-8")
... title("welcome to my page")
... with body:
... ATTR(id="body")
... with div.content(id = "floop", data_role = "page").pretty:
... TEXT("Hello, my <name> is")
... strong("Bob")
... TEXT("Also known as", em("Robert"))
... UNESCAPED("and I <b>like</b>")
... with ul:
... for item in ["cats", "rats", "hats"]:
... li(item)
...
,,,
>>> print doc
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
<title>welcome to my page</title>
</head>
<body id="body">
<div data-role="page" class="content pretty" id="floop">
Hello, my &lt;name&gt; is
<strong>Bob</strong>
Also known as
<em>Robert</em>
and I <b>like</b>
<ul>
<li>cats</li>
<li>rats</li>
<li>hats</li>
</ul>
</div>
</body>
</html>

The use of ``with`` and the fact you're dealing with pure Python code makes things easy -
there's no need for templating languages, passing parameters back and forth, extending base
templates or including files. It's snakes all the way down!
"""
import threading


__all__ = ["Element", "TEXT", "UNESCAPED", "ATTR", "EMBED", "THIS", "PARENT"]

class Unescaped(str):
    __slots__ = ()
    def __repr__(self):
        return "Unescaped(%s)" % (str.__repr__(self))

_MAPPING = {"&" : "&amp;", "'" : "&apos;", '"' : "&quot;", "<" : "&lt;", ">" : "&gt;"}

def xml_escape(text):
    if isinstance(text, Unescaped):
        return str(text)
    else:
        return "".join(_MAPPING.get(ch, ch) for ch in str(text))

#===================================================================================================
# <magic>
#===================================================================================================
_per_thread = threading.local()

class Element(object):
    class __metaclass__(type):
        __slots__ = ()
        def __getattr__(cls, name):
            return cls(class_ = name)
        def __enter__(cls):
            return cls().__enter__()
        def __exit__(cls, t, v, tb):
            _per_thread._stack[-1].__exit__(t, v, tb)

    __slots__ = ["_attrs", "_elems"]
    TAG = None
    INLINE = False
    DOCTYPE = None
    DEFAULT_ATTRS = {}
    
    def __init__(self, *elems, **attrs):
        self._attrs = self.DEFAULT_ATTRS.copy()
        self._elems = []
        self._parent = None
        if getattr(_per_thread, "_stack", None):
            _per_thread._stack[-1]._elems.append(self)
            self._parent = _per_thread._stack[-1]
        else:
            self._parent = None
        self(*elems, **attrs)
    
    def __repr__(self):
        return "<Element %s(%s), %s subelements>" % (self.TAG if self.TAG else self.__class__.__name__.lower(),
            ", ".join("%s=%r" % (k, v) for k, v in self._attrs.items()), len(self._elems))
    def __str__(self):
        return render(self)
    
    def __enter__(self):
        if not hasattr(_per_thread, "_stack"):
            _per_thread._stack = []
        _per_thread._stack.append(self)
        return self
    def __exit__(self, t, v, tb):
        _per_thread._stack.pop(-1)
    
    def __getitem__(self, name):
        return self._attrs[name]
    def __delitem__(self, name):
        del self._attrs[name]
    def __setitem__(self, name, value):
        self._attrs[name] = value
    
    def __call__(self, *elems, **attrs):
        for elem in elems:
            if isinstance(elem, Element):
                if elem._parent is not None:
                    elem._parent._elems.remove(elem)
                elem._parent = self
            self._elems.append(elem)
        for k, v in attrs.items():
            if k.endswith("_"):
                k = k[:-1]
            self._attrs[k.replace("_", "-")] = v
        return self
    
    def __getattr__(self, name):
        if "class" in self._attrs:
            self._attrs["class"] += " " + name
        else:
            self._attrs["class"] = name
        return self

def THIS():
    """Return the current HTML element on the stack; raises ``IndexError`` in case the stack
is empty"""
    return _per_thread._stack[-1]
def PARENT(count = 1):
    """Returns the ``count``-parent of the current HTML element on the stack; raises ``IndexError``
if there's no ``count`` parent. ``count`` defaults to 1, which means the immedaite parent"""
    return _per_thread._stack[-1 - count]
def TEXT(*texts):
    """Appends the given texts (as well as HTML elements) to the current element"""
    THIS()(*texts)
def UNESCAPED(*texts):
    """Appends the given texts unescaped to the current element. Note: security risk, use
with caution"""
    THIS()(*(Unescaped(text) for text in texts))
def EMBED(element):
    """Embeds (appends) the given HTML element into the current element (transfers ownership)"""
    THIS()(element)
def ATTR(**kwargs):
    """Sets the given keyword-arguments as attributes of the current HTML element"""
    THIS()(**kwargs)
#===================================================================================================
# </magic>
#===================================================================================================

def _render(elem, level, dont_indent = False):
    indent = " " * level
    if not isinstance(elem, Element):
        result = xml_escape(elem)
    else:
        tag = elem.TAG if elem.TAG else elem.__class__.__name__.lower().rstrip("_")
        attrs = " ".join('%s="%s"' % (k, xml_escape(str(v))) for k, v in elem._attrs.items())
        if attrs:
            attrs = " " + attrs
        if elem._elems:
            elements = "\n".join(_render(e, level + 1) for e in elem._elems)
            if elem.INLINE or not "\n" in elements:
                elements = _render(elem._elems[0], level + 1, True)
                result = "<%s%s>%s</%s>" % (tag, attrs, elements, tag)
            else:
                result = "<%s%s>\n%s\n%s</%s>" % (tag, attrs, elements, indent, tag)
        else:
            result = "<%s%s/>" % (tag, attrs)
    if dont_indent:
        return result
    else:
        return indent + result

def render(root):
    """renders the given HTML element and prepends ``DOCTYPE`` if one exists"""
    raw = _render(root, 0)
    if root.DOCTYPE:
        raw = root.DOCTYPE + "\n" + raw
    return raw

#===================================================================================================
# HEAD elements
#===================================================================================================
class html(Element):
    DEFAULT_ATTRS = {"xmlns" : "http://www.w3.org/1999/xhtml"}
    DOCTYPE = ('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" '
        '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">')

class head(Element): pass
class link(Element): pass
class meta(Element): pass
class title(Element): INLINE = True
class script(Element): DEFAULT_ATTRS = {"type" : "text/javascript"}
class style(Element): DEFAULT_ATTRS = {"type" : "text/css"}

#===================================================================================================
# BODY elements
#===================================================================================================
class body(Element): pass
class p(Element): pass
class div(Element): pass
class blockquote(Element): pass
class dl(Element): pass
class dt(Element): pass
class dd(Element): pass
class li(Element): pass
class ul(Element): pass
class ol(Element): pass

class form(Element): pass
class input(Element): pass
class button(Element): pass
class select(Element): pass
class label(Element): pass
class optgroup(Element): pass
class option(Element): pass
class textarea(Element): pass
class legend(Element): pass

class table(Element): pass
class tr(Element): pass
class th(Element): pass
class td(Element): pass
class colgroup(Element): pass
class thead(Element): pass
class tbody(Element): pass
class tfoot(Element): pass

class frame(Element): pass
class iframe(Element): pass
class noframe(Element): pass
class frameset(Element): pass

class pre(Element): INLINE = True
class code(Element): INLINE = True
class span(Element): INLINE = True
class a(Element): INLINE = True
class br(Element): INLINE = True
class hr(Element): INLINE = True
class em(Element): INLINE = True
class strong(Element): INLINE = True
class cite(Element): INLINE = True
class h1(Element): INLINE = True
class h2(Element): INLINE = True
class h3(Element): INLINE = True
class h4(Element): INLINE = True
class h5(Element): INLINE = True
class h6(Element): INLINE = True
class i(Element): INLINE = True
class b(Element): INLINE = True
class u(Element): INLINE = True
class sub(Element): INLINE = True
class sup(Element): INLINE = True
class big(Element): INLINE = True
class small(Element): INLINE = True
class img(Element): INLINE = True

__all__.extend(cls.__name__ for cls in Element.__subclasses__())

if __name__ == "__main__":
    import doctest
    # nice trick from http://stackoverflow.com/a/9400829/434796
    doctest.ELLIPSIS_MARKER = ",,,"
    doctest.testmod()

Something went wrong with that request. Please try again.