From ff79cdbf7d4d95e6a84d6b002e28e6df86847954 Mon Sep 17 00:00:00 2001 From: Fredrik Tolf Date: Sun, 13 May 2012 03:51:22 +0200 Subject: [PATCH] Added a HTML-generation engine that might hopefully be useful. --- setup.py | 2 +- wrw/sp/__init__.py | 0 wrw/sp/cons.py | 61 ++++++++++++++++ wrw/sp/util.py | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++++ wrw/sp/xhtml.py | 53 ++++++++++++++ 5 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 wrw/sp/__init__.py create mode 100644 wrw/sp/cons.py create mode 100644 wrw/sp/util.py create mode 100644 wrw/sp/xhtml.py diff --git a/setup.py b/setup.py index a3f45d2..3775c63 100755 --- a/setup.py +++ b/setup.py @@ -7,6 +7,6 @@ setup(name = "wrw", description = "Simple WSGI request wrapper library", author = "Fredrik Tolf", author_email = "fredrik@dolda2000.com", - packages = ["wrw"], + packages = ["wrw", "wrw.sp"], package_data = {"wrw": ["makolib/*.mako"]}, license = "GPL-3") diff --git a/wrw/sp/__init__.py b/wrw/sp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wrw/sp/cons.py b/wrw/sp/cons.py new file mode 100644 index 0000000..5341b20 --- /dev/null +++ b/wrw/sp/cons.py @@ -0,0 +1,61 @@ +import xml.dom.minidom + +class node(object): + def __str__(self): + doc = xml.dom.minidom.Document() + return self.__todom__(doc).toxml() + +class text(node, unicode): + def __todom__(self, doc): + return doc.createTextNode(self) + +class element(node): + def __init__(self, ns, name, ctx): + self.ns = ns + self.name = unicode(name) + self.ctx = ctx + self.attrs = {} + self.children = [] + + def __call__(self, *children, **attrs): + for child in children: + self.children.append(self.ctx.nodefrom(child)) + for k, v in attrs.iteritems(): + self.attrs[unicode(k)] = unicode(v) + return self + + def __todom__(self, doc): + el = doc.createElementNS(self.ns, self.name) + for k, v in self.attrs.iteritems(): + el.setAttribute(k, v) + for child in self.children: + el.appendChild(child.__todom__(doc)) + return el + +class context(object): + def __init__(self): + self.nodeconv = {} + self.nodeconv[str] = lambda ob: text(ob, "utf-8") + self.nodeconv[unicode] = text + self.nodeconv[int] = text + self.nodeconv[long] = text + self.nodeconv[float] = text + + def nodefrom(self, ob): + if isinstance(ob, node): + return ob + if hasattr(ob, "__tonode__"): + return ob.__tonode__() + if type(ob) in self.nodeconv: + return self.nodeconv[type(ob)](ob) + raise Exception("No node conversion known for %s objects" % str(type(ob))) + +class constructor(object): + def __init__(self, ns, elcls = element, ctx=None): + self._ns = ns + self._elcls = elcls + if ctx is None: ctx = context() + self._ctx = ctx + + def __getattr__(self, name): + return self._elcls(self._ns, name, self._ctx) diff --git a/wrw/sp/util.py b/wrw/sp/util.py new file mode 100644 index 0000000..96e9880 --- /dev/null +++ b/wrw/sp/util.py @@ -0,0 +1,201 @@ +import cons + +def findnsnames(el): + names = {} + nid = [1] + def proc(el): + if isinstance(el, cons.element): + if el.ns not in names: + names[el.ns] = u"n" + unicode(nid[0]) + nid[:] = [nid[0] + 1] + for ch in el.children: + proc(ch) + proc(el) + if None in names: + names[None] = None + else: + names[el.ns] = None + return names + +class formatter(object): + def __init__(self, out, root, nsnames=None, charset="utf-8", doctype=None): + self.root = root + if nsnames is None: + nsnames = findnsnames(root) + self.nsnames = nsnames + self.out = out + self.charset = charset + self.doctype = doctype + + def write(self, text): + self.out.write(text.encode(self.charset)) + + def quotewrite(self, buf): + for ch in buf: + if ch == u'&': + self.write(u"&") + elif ch == u'<': + self.write(u"<") + elif ch == u'>': + self.write(u">") + else: + self.write(ch) + + def text(self, el): + self.quotewrite(el) + + def attrval(self, buf): + qc, qt = (u"'", u"'") if u'"' in buf else (u'"', u""") + self.write(qc) + for ch in buf: + if ch == u'&': + self.write(u"&") + elif ch == u'<': + self.write(u"<") + elif ch == u'>': + self.write(u">") + elif ch == qc: + self.write(qt) + else: + self.write(ch) + self.write(qc) + + def attr(self, k, v): + self.write(k) + self.write(u'=') + self.attrval(v) + + def shorttag(self, el, **extra): + self.write(u'<' + self.elname(el)) + for k, v in el.attrs.iteritems(): + self.write(u' ') + self.attr(k, v) + for k, v in extra.iteritems(): + self.write(u' ') + self.attr(k, v) + self.write(u" />") + + def elname(self, el): + ns = self.nsnames[el.ns] + if ns is None: + return el.name + else: + return ns + u':' + el.name + + def starttag(self, el, **extra): + self.write(u'<' + self.elname(el)) + for k, v in el.attrs.iteritems(): + self.write(u' ') + self.attr(k, v) + for k, v in extra.iteritems(): + self.write(u' ') + self.attr(k, v) + self.write(u'>') + + def endtag(self, el): + self.write(u'') + + def longtag(self, el): + self.starttag(el, **extra) + for ch in el.children: + self.node(ch) + self.endtag(el) + + def element(self, el, **extra): + if len(el.children) == 0: + self.shorttag(el, **extra) + else: + self.longtag(el, **extra) + + def node(self, el): + if isinstance(el, cons.element): + self.element(el) + elif isinstance(el, cons.text): + self.text(el) + else: + raise Exception("Unknown object in element tree: " + el) + + def start(self): + self.write(u'\n') + if self.doctype: + self.write(u'\n' % (self.root.name, + self.doctype[0], + self.doctype[1])) + extra = {} + for uri, nm in self.nsnames.iteritems(): + if uri is None: + continue + if nm is None: + extra[u"xmlns"] = uri + else: + extra[u"xmlns:" + nm] = uri + self.element(self.root, **extra) + + @classmethod + def output(cls, out, el, *args, **kw): + cls(out=out, root=el, *args, **kw).start() + + def update(self, **ch): + ret = type(self).__new__(type(self)) + ret.__dict__.update(self.__dict__) + ret.__dict__.update(ch) + return ret + +class iwriter(object): + def __init__(self, out): + self.out = out + self.atbol = True + self.col = 0 + + def write(self, buf): + for c in buf: + if c == '\n': + self.col = 0 + else: + self.col += 1 + self.out.write(c) + self.atbol = False + + def indent(self, indent): + if self.atbol: + return + if self.col != 0: + self.write('\n') + self.write(indent) + self.atbol = True + +class indenter(formatter): + def __init__(self, indent=u" ", *args, **kw): + super(indenter, self).__init__(*args, **kw) + self.out = iwriter(self.out) + self.indent = indent + self.curind = u"" + + def simple(self, el): + for ch in el.children: + if not isinstance(ch, cons.text): + return False + return True + + def longtag(self, el, **extra): + self.starttag(el, **extra) + sub = self + reind = False + if not self.simple(el): + sub = self.update(curind=self.curind + self.indent) + sub.out.indent(sub.curind) + reind = True + for ch in el.children: + sub.node(ch) + if reind: + self.out.indent(self.curind) + self.endtag(el) + + def element(self, el, **extra): + super(indenter, self).element(el, **extra) + if self.out.col > 80 and self.simple(el): + self.out.indent(self.curind) + + def start(self): + super(indenter, self).start() + self.write('\n') diff --git a/wrw/sp/xhtml.py b/wrw/sp/xhtml.py new file mode 100644 index 0000000..abf4b96 --- /dev/null +++ b/wrw/sp/xhtml.py @@ -0,0 +1,53 @@ +import xml.dom.minidom, StringIO +import cons as _cons +import util +dom = xml.dom.minidom.getDOMImplementation() + +ns = u"http://www.w3.org/1999/xhtml" +doctype = u"-//W3C//DTD XHTML 1.1//EN" +dtd = u"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" + +class htmlelement(_cons.element): + def __todoc__(self): + doc = dom.createDocument(None, None, None) + doc.appendChild(dom.createDocumentType("html", doctype, dtd)) + doc.appendChild(self.__todom__(doc)) + return doc + +def cons(ctx=None): + return _cons.constructor(ns, htmlelement, ctx) + +def head(title=None, css=None): + h = cons() + head = h.head + if title: + head(h.title(title)) + if isinstance(css, str) or isinstance(css, unicode): + head(h.link(rel="stylesheet", type="text/css", href=css)) + elif css: + for ss in css: + head(h.link(rel="stylesheet", type="text/css", href=ss)) + return head + +class htmlformatter(util.formatter): + allowshort = set([u"br", u"hr", u"img", u"input"]) + def element(self, el, **extra): + if el.name in self.allowshort: + super(htmlformatter, self).element(el, **extra) + else: + self.longtag(el, **extra) + +class htmlindenter(util.indenter, htmlformatter): + pass + +def forreq(req, tree): + # XXX: Use proper Content-Type for clients accepting it. + req.ohead["Content-Type"] = "text/html; charset=utf-8" + buf = StringIO.StringIO() + htmlindenter.output(buf, tree, doctype=(doctype, dtd), charset="utf-8") + return [buf.getvalue()] + +def xhtmlresp(callable): + def wrapper(req): + return forreq(req, callable(req)) + return wrapper -- 2.11.0