Mercurial > genshi > mirror
changeset 347:ffa7dea6e8fd experimental-inline
cspeedups branch: Merged [423:426/trunk].
author | cmlenz |
---|---|
date | Fri, 10 Nov 2006 17:38:50 +0000 |
parents | bb881d1b4b5c |
children | 47e6c0100514 |
files | ChangeLog UPGRADE.txt examples/basic/test.html genshi/builder.py genshi/core.py genshi/filters.py genshi/output.py genshi/template/core.py genshi/template/directives.py genshi/tests/builder.py genshi/tests/output.py |
diffstat | 11 files changed, 154 insertions(+), 129 deletions(-) [+] |
line wrap: on
line diff
--- a/ChangeLog +++ b/ChangeLog @@ -28,6 +28,11 @@ `genshi.template` package. * Results of expression evaluation are no longer implicitly called if they are callable. + * Instances of the `genshi.core.Attrs` class are now immutable (they are + subclasses of `tuple` instead of `list`). + * Preserve whitespace in HTML `<pre>` elements also when they contained any + child elements. + Version 0.3.4 http://svn.edgewall.org/repos/genshi/tags/0.3.4/
--- a/UPGRADE.txt +++ b/UPGRADE.txt @@ -14,6 +14,10 @@ called if they are callable. If you have been using that feature, you will need to add the parenthesis to actually call the function. +Instances of `genshi.core.Attrs` are now immutable. Filters +manipulating the attributes in a stream may need to be updated. See +the docstring of the `Attrs` for more information. + Upgrading from Markup ---------------------
--- a/examples/basic/test.html +++ b/examples/basic/test.html @@ -10,7 +10,6 @@ <body class="$bozz"> <ul py:attrs="{'id': 'second', 'class': None}" py:if="len(items) > 0"> <li py:for="item in items">Item $prefix${item.split()[-1]}</li> - XYZ ${hey} </ul> ${macro1()} ${macro1()} ${macro1()} ${macro2('john')}
--- a/genshi/builder.py +++ b/genshi/builder.py @@ -73,6 +73,16 @@ return Stream(self._generate()) +def _value_to_unicode(value): + if isinstance(value, unicode): + return value + return unicode(value) + +def _kwargs_to_attrs(kwargs): + return [(k.rstrip('_').replace('_', '-'), _value_to_unicode(v)) + for k, v in kwargs.items() if v is not None] + + class Element(Fragment): """Simple XML output generator based on the builder pattern. @@ -157,20 +167,10 @@ def __init__(self, tag_, **attrib): Fragment.__init__(self) self.tag = QName(tag_) - self.attrib = Attrs() - for attr, value in attrib.items(): - if value is not None: - if not isinstance(value, basestring): - value = unicode(value) - self.attrib.append((QName(attr.rstrip('_').replace('_', '-')), - value)) + self.attrib = Attrs(_kwargs_to_attrs(attrib)) def __call__(self, *args, **kwargs): - for attr, value in kwargs.items(): - if value is not None: - if not isinstance(value, basestring): - value = unicode(value) - self.attrib.set(attr.rstrip('_').replace('_', '-'), value) + self.attrib |= Attrs(_kwargs_to_attrs(kwargs)) Fragment.__call__(self, *args) return self
--- a/genshi/core.py +++ b/genshi/core.py @@ -213,11 +213,11 @@ yield event -class Attrs(list): - """Sequence type that stores the attributes of an element. +class Attrs(tuple): + """Immutable sequence type that stores the attributes of an element. - The order of the attributes is preserved, while accessing and manipulating - attributes by name is also supported. + Ordering of the attributes is preserved, while accessing by name is also + supported. >>> attrs = Attrs([('href', '#'), ('title', 'Foo')]) >>> attrs @@ -227,58 +227,53 @@ True >>> 'tabindex' in attrs False - >>> attrs.get(u'title') 'Foo' - >>> attrs.set(u'title', 'Bar') + + Instances may not be manipulated directly. Instead, the operators `|` and + `-` can be used to produce new instances that have specific attributes + added, replaced or removed. + + To remove an attribute, use the `-` operator. The right hand side can be + either a string or a set/sequence of strings, identifying the name(s) of + the attribute(s) to remove: + + >>> attrs - 'title' + Attrs([(QName(u'href'), '#')]) + >>> attrs - ('title', 'href') + Attrs() + + The original instance is not modified, but the operator can of course be + used with an assignment: + >>> attrs - Attrs([(QName(u'href'), '#'), (QName(u'title'), 'Bar')]) - >>> attrs.remove(u'title') + Attrs([(QName(u'href'), '#'), (QName(u'title'), 'Foo')]) + >>> attrs -= 'title' >>> attrs Attrs([(QName(u'href'), '#')]) - New attributes added using the `set()` method are appended to the end of - the list: - - >>> attrs.set(u'accesskey', 'k') - >>> attrs - Attrs([(QName(u'href'), '#'), (QName(u'accesskey'), 'k')]) - - An `Attrs` instance can also be initialized with keyword arguments. + To add a new attribute, use the `|` operator, where the right hand value + is a sequence of `(name, value)` tuples (which includes `Attrs` instances): - >>> attrs = Attrs(class_='bar', href='#', title='Foo') - >>> attrs.get('class') - 'bar' - >>> attrs.get('href') - '#' - >>> attrs.get('title') - 'Foo' + >>> attrs | [(u'title', 'Bar')] + Attrs([(QName(u'href'), '#'), (QName(u'title'), 'Bar')]) - Reserved words can be used by appending a trailing underscore to the name, - and any other underscore is replaced by a dash: + If the attributes already contain an attribute with a given name, the value + of that attribute is replaced: - >>> attrs = Attrs(class_='bar', accept_charset='utf-8') - >>> attrs.get('class') - 'bar' - >>> attrs.get('accept-charset') - 'utf-8' + >>> attrs | [(u'href', 'http://example.org/')] + Attrs([(QName(u'href'), 'http://example.org/')]) - Thus this shorthand can not be used if attribute names should contain - actual underscore characters. """ __slots__ = [] - def __init__(self, attrib=None, **kwargs): + def __new__(cls, items=()): """Create the `Attrs` instance. - If the `attrib` parameter is provided, it is expected to be a sequence + If the `items` parameter is provided, it is expected to be a sequence of `(name, value)` tuples. """ - if attrib is None: - attrib = [] - list.__init__(self, [(QName(name), value) for name, value in attrib]) - for name, value in kwargs.items(): - self.set(name.rstrip('_').replace('_', '-'), value) + return tuple.__new__(cls, [(QName(name), val) for name, val in items]) def __contains__(self, name): """Return whether the list includes an attribute with the specified @@ -288,10 +283,29 @@ if attr == name: return True + def __getslice__(self, i, j): + return Attrs(tuple.__getslice__(self, i, j)) + + def __or__(self, attrs): + """Return a new instance that contains the attributes in `attrs` in + addition to any already existing attributes. + """ + repl = dict([(an, av) for an, av in attrs if an in self]) + return Attrs([(sn, repl.get(sn, sv)) for sn, sv in self] + + [(an, av) for an, av in attrs if an not in self]) + def __repr__(self): if not self: return 'Attrs()' - return 'Attrs(%s)' % list.__repr__(self) + return 'Attrs([%s])' % ', '.join([repr(item) for item in self]) + + def __sub__(self, names): + """Return a new instance with all attributes with a name in `names` are + removed. + """ + if isinstance(names, basestring): + names = (names,) + return Attrs([(name, val) for name, val in self if name not in names]) def get(self, name, default=None): """Return the value of the attribute with the specified name, or the @@ -302,30 +316,6 @@ return value return default - def remove(self, name): - """Remove the attribute with the specified name. - - If no such attribute is found, this method does nothing. - """ - for idx, (attr, _) in enumerate(self): - if attr == name: - del self[idx] - break - - def set(self, name, value): - """Set the specified attribute to the given value. - - If an attribute with the specified name is already in the list, the - value of the existing entry is updated. Otherwise, a new attribute is - appended to the end of the list. - """ - for idx, (attr, _) in enumerate(self): - if attr == name: - self[idx] = (QName(attr), value) - break - else: - self.append((QName(name), value)) - def totuple(self): """Return the attributes as a markup event.
--- a/genshi/filters.py +++ b/genshi/filters.py @@ -73,23 +73,23 @@ for kind, data, pos in stream: if kind is START: - tag, attrib = data + tag, attrs = data tagname = tag.localname if tagname == 'form' and ( - self.name and attrib.get('name') == self.name or - self.id and attrib.get('id') == self.id or + self.name and attrs.get('name') == self.name or + self.id and attrs.get('id') == self.id or not (self.id or self.name)): in_form = True elif in_form: if tagname == 'input': - type = attrib.get('type') + type = attrs.get('type') if type in ('checkbox', 'radio'): - name = attrib.get('name') + name = attrs.get('name') if name: value = self.data.get(name) - declval = attrib.get('value') + declval = attrs.get('value') checked = False if isinstance(value, (list, tuple)): if declval: @@ -102,32 +102,34 @@ elif type == 'checkbox': checked = bool(value) if checked: - attrib.set('checked', 'checked') - else: - attrib.remove('checked') + attrs |= [('checked', 'checked')] + elif 'checked' in attrs: + attrs -= 'checked' elif type in (None, 'hidden', 'text'): - name = attrib.get('name') + name = attrs.get('name') if name: value = self.data.get(name) if isinstance(value, (list, tuple)): value = value[0] if value is not None: - attrib.set('value', unicode(value)) + attrs |= [('value', unicode(value))] elif tagname == 'select': - name = attrib.get('name') + name = attrs.get('name') select_value = self.data.get(name) in_select = True elif tagname == 'textarea': - name = attrib.get('name') + name = attrs.get('name') textarea_value = self.data.get(name) if isinstance(textarea_value, (list, tuple)): textarea_value = textarea_value[0] in_textarea = True elif in_select and tagname == 'option': option_start = kind, data, pos - option_value = attrib.get('value') + option_value = attrs.get('value') in_option = True continue + yield kind, (tag, attrs), pos + elif in_form and kind is TEXT: if in_select and in_option: @@ -137,6 +139,7 @@ continue elif in_textarea: continue + yield kind, data, pos elif in_form and kind is END: tagname = data.localname @@ -150,12 +153,12 @@ selected = option_value in select_value else: selected = option_value == select_value - attrib = option_start[1][1] + okind, (tag, attrs), opos = option_start if selected: - attrib.set('selected', 'selected') - else: - attrib.remove('selected') - yield option_start + attrs |= [('selected', 'selected')] + elif 'selected' in attrs: + attrs -= 'selected' + yield okind, (tag, attrs), opos if option_text: yield option_text in_option = False @@ -164,8 +167,10 @@ if textarea_value: yield TEXT, unicode(textarea_value), pos in_textarea = False + yield kind, data, pos - yield kind, data, pos + else: + yield kind, data, pos class HTMLSanitizer(object): @@ -234,13 +239,13 @@ if kind is START: if waiting_for: continue - tag, attrib = data + tag, attrs = data if tag not in self.safe_tags: waiting_for = tag continue - new_attrib = Attrs() - for attr, value in attrib: + new_attrs = [] + for attr, value in attrs: value = stripentities(value) if attr not in self.safe_attrs: continue @@ -264,9 +269,9 @@ if not decls: continue value = '; '.join(decls) - new_attrib.append((attr, value)) + new_attrs.append((attr, value)) - yield kind, (tag, new_attrib), pos + yield kind, (tag, Attrs(new_attrs)), pos elif kind is END: tag = data @@ -313,9 +318,9 @@ for kind, data, pos in stream: if kind is START and not in_fallback and data[0] in namespace: - tag, attrib = data + tag, attrs = data if tag.localname == 'include': - include_href = attrib.get('href') + include_href = attrs.get('href') elif tag.localname == 'fallback': in_fallback = True fallback_stream = []
--- a/genshi/output.py +++ b/genshi/output.py @@ -98,7 +98,7 @@ ns_attrib.append((QName('xmlns'), namespace)) buf = ['<', tagname] - for attr, value in attrib + ns_attrib: + for attr, value in attrib + tuple(ns_attrib): attrname = attr.localname if attr.namespace: prefix = ns_mapping.get(attr.namespace) @@ -183,7 +183,10 @@ _BOOLEAN_ATTRS = frozenset(['selected', 'checked', 'compact', 'declare', 'defer', 'disabled', 'ismap', 'multiple', 'nohref', 'noresize', 'noshade', 'nowrap']) - _PRESERVE_SPACE = frozenset([QName('pre'), QName('textarea')]) + _PRESERVE_SPACE = frozenset([ + QName('pre'), QName('http://www.w3.org/1999/xhtml}pre'), + QName('textarea'), QName('http://www.w3.org/1999/xhtml}textarea') + ]) def __call__(self, stream): namespace = self.NAMESPACE @@ -213,7 +216,7 @@ ns_attrib.append((QName('xmlns'), tagns)) buf = ['<', tagname] - for attr, value in attrib + ns_attrib: + for attr, value in chain(attrib, ns_attrib): attrname = attr.localname if attr.namespace: prefix = ns_mapping.get(attr.namespace) @@ -461,8 +464,8 @@ @param noescape: a set or sequence of tag names for which text content should not be escaped - Both the `preserve` and `noescape` sets are expected to refer to - elements that cannot contain further child elements. + The `noescape` set is expected to refer to elements that cannot contain + further child elements (such as <style> or <script> in HTML documents). """ if preserve is None: preserve = [] @@ -476,7 +479,7 @@ collapse_lines=re.compile('\n{2,}').sub): mjoin = Markup('').join preserve_elems = self.preserve - preserve = False + preserve = 0 noescape_elems = self.noescape noescape = False @@ -500,15 +503,17 @@ yield TEXT, Markup(text), pos if kind is START: - tag, attrib = data - if not preserve and (tag in preserve_elems or - attrib.get(space) == 'preserve'): - preserve = True + tag, attrs = data + if preserve or (tag in preserve_elems or + attrs.get(space) == 'preserve'): + preserve += 1 if not noescape and tag in noescape_elems: noescape = True elif kind is END: - preserve = noescape = False + noescape = False + if preserve: + preserve -= 1 elif kind is START_CDATA: noescape = True
--- a/genshi/template/core.py +++ b/genshi/template/core.py @@ -340,9 +340,9 @@ if kind is START and data[1]: # Attributes may still contain expressions in start tags at # this point, so do some evaluation - tag, attrib = data - new_attrib = [] - for name, substream in attrib: + tag, attrs = data + new_attrs = [] + for name, substream in attrs: if isinstance(substream, basestring): value = substream else: @@ -354,8 +354,8 @@ value = [x for x in values if x is not None] if not value: continue - new_attrib.append((name, u''.join(value))) - yield kind, (tag, Attrs(new_attrib)), pos + new_attrs.append((name, u''.join(value))) + yield kind, (tag, Attrs(new_attrs)), pos elif kind is EXPR: result = data.evaluate(ctxt)
--- a/genshi/template/directives.py +++ b/genshi/template/directives.py @@ -80,7 +80,6 @@ kind, (tag, attrib), pos = stream.next() attrs = self.expr.evaluate(ctxt) if attrs: - attrib = Attrs(attrib[:]) if isinstance(attrs, Stream): try: attrs = iter(attrs).next() @@ -88,11 +87,9 @@ attrs = [] elif not isinstance(attrs, list): # assume it's a dict attrs = attrs.items() - for name, value in attrs: - if value is None: - attrib.remove(name) - else: - attrib.set(name, unicode(value).strip()) + attrib -= [name for name, val in attrs if val is None] + attrib |= [(name, unicode(val).strip()) for name, val in attrs + if val is not None] yield kind, (tag, attrib), pos for event in stream: yield event
--- a/genshi/tests/builder.py +++ b/genshi/tests/builder.py @@ -16,7 +16,7 @@ import unittest from genshi.builder import Element, tag -from genshi.core import Stream +from genshi.core import Attrs, Stream class ElementFactoryTestCase(unittest.TestCase): @@ -24,7 +24,8 @@ def test_link(self): link = tag.a(href='#', title='Foo', accesskey=None)('Bar') bits = iter(link.generate()) - self.assertEqual((Stream.START, ('a', [('href', "#"), ('title', "Foo")]), + self.assertEqual((Stream.START, + ('a', Attrs([('href', "#"), ('title', "Foo")])), (None, -1, -1)), bits.next()) self.assertEqual((Stream.TEXT, u'Bar', (None, -1, -1)), bits.next()) self.assertEqual((Stream.END, 'a', (None, -1, -1)), bits.next()) @@ -36,7 +37,8 @@ generated. """ event = iter(tag.foo(id=3)).next() - self.assertEqual((Stream.START, ('foo', [('id', '3')]), (None, -1, -1)), + self.assertEqual((Stream.START, ('foo', Attrs([('id', '3')])), + (None, -1, -1)), event)
--- a/genshi/tests/output.py +++ b/genshi/tests/output.py @@ -141,6 +141,12 @@ output = stream.render(XHTMLSerializer) self.assertEqual('<textarea name="foo">%s</textarea>' % content, output) + def test_pre_whitespace(self): + content = '\nHey <em>there</em>. \n\n I am indented.\n' + stream = XML('<pre>%s</pre>' % content) + output = stream.render(XHTMLSerializer) + self.assertEqual('<pre>%s</pre>' % content, output) + def test_xml_space(self): text = '<foo xml:space="preserve"> Do not mess \n\n with me </foo>' output = XML(text).render(XHTMLSerializer) @@ -254,6 +260,18 @@ class HTMLSerializerTestCase(unittest.TestCase): + def test_textarea_whitespace(self): + content = '\nHey there. \n\n I am indented.\n' + stream = XML('<textarea name="foo">%s</textarea>' % content) + output = stream.render(HTMLSerializer) + self.assertEqual('<textarea name="foo">%s</textarea>' % content, output) + + def test_pre_whitespace(self): + content = '\nHey <em>there</em>. \n\n I am indented.\n' + stream = XML('<pre>%s</pre>' % content) + output = stream.render(HTMLSerializer) + self.assertEqual('<pre>%s</pre>' % content, output) + def test_xml_space(self): text = '<foo xml:space="preserve"> Do not mess \n\n with me </foo>' output = XML(text).render(HTMLSerializer)