Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/lib/htmltextview.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/htmltextview.py')
-rw-r--r--lib/htmltextview.py591
1 files changed, 591 insertions, 0 deletions
diff --git a/lib/htmltextview.py b/lib/htmltextview.py
new file mode 100644
index 0000000..a717ea9
--- /dev/null
+++ b/lib/htmltextview.py
@@ -0,0 +1,591 @@
+### Copyright (C) 2005-2007 Gustavo J. A. M. Carneiro
+###
+### This library is free software; you can redistribute it and/or
+### modify it under the terms of the GNU Lesser General Public
+### License as published by the Free Software Foundation; either
+### version 2 of the License, or (at your option) any later version.
+###
+### This library is distributed in the hope that it will be useful,
+### but WITHOUT ANY WARRANTY; without even the implied warranty of
+### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+### Lesser General Public License for more details.
+###
+### You should have received a copy of the GNU Lesser General Public
+### License along with this library; if not, write to the
+### Free Software Foundation, Inc., 59 Temple Place - Suite 330,
+### Boston, MA 02111-1307, USA.
+
+'''
+A gtk.TextView-based renderer for XHTML-IM, as described in:
+ http://www.jabber.org/jeps/jep-0071.html
+'''
+import gobject
+import pango
+import gtk
+import xml.sax, xml.sax.handler
+import re
+import warnings
+from cStringIO import StringIO
+import urllib2
+import operator
+
+__all__ = ['HtmlTextView']
+
+whitespace_rx = re.compile("\\s+")
+allwhitespace_rx = re.compile("^\\s*$")
+
+## pixels = points * display_resolution
+display_resolution = 0.3514598*(gtk.gdk.screen_height() /
+ float(gtk.gdk.screen_height_mm()))
+
+
+def _parse_css_color(color):
+ '''_parse_css_color(css_color) -> gtk.gdk.Color'''
+ if color.startswith("rgb(") and color.endswith(')'):
+ r, g, b = [int(c)*257 for c in color[4:-1].split(',')]
+ return gtk.gdk.Color(r, g, b)
+ else:
+ return gtk.gdk.color_parse(color)
+
+
+# class HtmlEntityResolver(xml.sax.handler.EntityResolver):
+# def resolveEntity(publicId, systemId):
+# pass
+
+class HtmlHandler(xml.sax.handler.ContentHandler):
+
+ def __init__(self, textview, startiter):
+ xml.sax.handler.ContentHandler.__init__(self)
+ self.textbuf = textview.get_buffer()
+ self.textview = textview
+ self.iter = startiter
+ self.text = ''
+ self.styles = [] # a gtk.TextTag or None, for each span level
+ self.list_counters = [] # stack (top at head) of list
+ # counters, or None for unordered list
+
+ def _parse_style_color(self, tag, value):
+ color = _parse_css_color(value)
+ tag.set_property("foreground-gdk", color)
+
+ def _parse_style_background_color(self, tag, value):
+ color = _parse_css_color(value)
+ tag.set_property("background-gdk", color)
+ if gtk.gtk_version >= (2, 8):
+ tag.set_property("paragraph-background-gdk", color)
+
+
+ if gtk.gtk_version >= (2, 8, 5) or gobject.pygtk_version >= (2, 8, 1):
+
+ def _get_current_attributes(self):
+ attrs = self.textview.get_default_attributes()
+ self.iter.backward_char()
+ self.iter.get_attributes(attrs)
+ self.iter.forward_char()
+ return attrs
+
+ else:
+
+ ## Workaround http://bugzilla.gnome.org/show_bug.cgi?id=317455
+ def _get_current_style_attr(self, propname, comb_oper=None):
+ tags = [tag for tag in self.styles if tag is not None]
+ tags.reverse()
+ is_set_name = propname + "-set"
+ value = None
+ for tag in tags:
+ if tag.get_property(is_set_name):
+ if value is None:
+ value = tag.get_property(propname)
+ if comb_oper is None:
+ return value
+ else:
+ value = comb_oper(value, tag.get_property(propname))
+ return value
+
+ class _FakeAttrs(object):
+ __slots__ = ("font", "font_scale")
+
+ def _get_current_attributes(self):
+ attrs = self._FakeAttrs()
+ attrs.font_scale = self._get_current_style_attr("scale",
+ operator.mul)
+ if attrs.font_scale is None:
+ attrs.font_scale = 1.0
+ attrs.font = self._get_current_style_attr("font-desc")
+ if attrs.font is None:
+ attrs.font = self.textview.style.font_desc
+ return attrs
+
+
+ def __parse_length_frac_size_allocate(self, textview, allocation,
+ frac, callback, args):
+ callback(allocation.width*frac, *args)
+
+ def _parse_length(self, value, font_relative, callback, *args):
+ '''Parse/calc length, converting to pixels, calls callback(length, *args)
+ when the length is first computed or changes'''
+ if value.endswith('%'):
+ frac = float(value[:-1])/100
+ if font_relative:
+ attrs = self._get_current_attributes()
+ font_size = attrs.font.get_size() / pango.SCALE
+ callback(frac*display_resolution*font_size, *args)
+ else:
+ ## CSS says "Percentage values: refer to width of the closest
+ ## block-level ancestor"
+ ## This is difficult/impossible to implement, so we use
+ ## textview width instead; a reasonable approximation..
+ alloc = self.textview.get_allocation()
+ self.__parse_length_frac_size_allocate(self.textview, alloc,
+ frac, callback, args)
+ self.textview.connect("size-allocate",
+ self.__parse_length_frac_size_allocate,
+ frac, callback, args)
+
+ elif value.endswith('pt'): # points
+ callback(float(value[:-2])*display_resolution, *args)
+
+ elif value.endswith('em'): # ems, the height of the element's font
+ attrs = self._get_current_attributes()
+ font_size = attrs.font.get_size() / pango.SCALE
+ callback(float(value[:-2])*display_resolution*font_size, *args)
+
+ elif value.endswith('ex'): # x-height, ~ the height of the letter 'x'
+ ## FIXME: figure out how to calculate this correctly
+ ## for now 'em' size is used as approximation
+ attrs = self._get_current_attributes()
+ font_size = attrs.font.get_size() / pango.SCALE
+ callback(float(value[:-2])*display_resolution*font_size, *args)
+
+ elif value.endswith('px'): # pixels
+ callback(int(value[:-2]), *args)
+
+ else:
+ warnings.warn("Unable to parse length value '%s'" % value)
+
+ def __parse_font_size_cb(length, tag):
+ tag.set_property("size-points", length/display_resolution)
+ __parse_font_size_cb = staticmethod(__parse_font_size_cb)
+
+ def _parse_style_font_size(self, tag, value):
+ try:
+ scale = {
+ "xx-small": pango.SCALE_XX_SMALL,
+ "x-small": pango.SCALE_X_SMALL,
+ "small": pango.SCALE_SMALL,
+ "medium": pango.SCALE_MEDIUM,
+ "large": pango.SCALE_LARGE,
+ "x-large": pango.SCALE_X_LARGE,
+ "xx-large": pango.SCALE_XX_LARGE,
+ } [value]
+ except KeyError:
+ pass
+ else:
+ attrs = self._get_current_attributes()
+ tag.set_property("scale", scale / attrs.font_scale)
+ return
+ if value == 'smaller':
+ tag.set_property("scale", pango.SCALE_SMALL)
+ return
+ if value == 'larger':
+ tag.set_property("scale", pango.SCALE_LARGE)
+ return
+ self._parse_length(value, True, self.__parse_font_size_cb, tag)
+
+ def _parse_style_font_style(self, tag, value):
+ try:
+ style = {
+ "normal": pango.STYLE_NORMAL,
+ "italic": pango.STYLE_ITALIC,
+ "oblique": pango.STYLE_OBLIQUE,
+ } [value]
+ except KeyError:
+ warnings.warn("unknown font-style %s" % value)
+ else:
+ tag.set_property("style", style)
+
+ def __frac_length_tag_cb(length, tag, propname):
+ tag.set_property(propname, length)
+ __frac_length_tag_cb = staticmethod(__frac_length_tag_cb)
+
+ def _parse_style_margin_left(self, tag, value):
+ self._parse_length(value, False, self.__frac_length_tag_cb,
+ tag, "left-margin")
+
+ def _parse_style_margin_right(self, tag, value):
+ self._parse_length(value, False, self.__frac_length_tag_cb,
+ tag, "right-margin")
+
+ def _parse_style_font_weight(self, tag, value):
+ ## TODO: missing 'bolder' and 'lighter'
+ try:
+ weight = {
+ '100': pango.WEIGHT_ULTRALIGHT,
+ '200': pango.WEIGHT_ULTRALIGHT,
+ '300': pango.WEIGHT_LIGHT,
+ '400': pango.WEIGHT_NORMAL,
+ '500': pango.WEIGHT_NORMAL,
+ '600': pango.WEIGHT_BOLD,
+ '700': pango.WEIGHT_BOLD,
+ '800': pango.WEIGHT_ULTRABOLD,
+ '900': pango.WEIGHT_HEAVY,
+ 'normal': pango.WEIGHT_NORMAL,
+ 'bold': pango.WEIGHT_BOLD,
+ } [value]
+ except KeyError:
+ warnings.warn("unknown font-style %s" % value)
+ else:
+ tag.set_property("weight", weight)
+
+ def _parse_style_font_family(self, tag, value):
+ tag.set_property("family", value)
+
+ def _parse_style_text_align(self, tag, value):
+ try:
+ align = {
+ 'left': gtk.JUSTIFY_LEFT,
+ 'right': gtk.JUSTIFY_RIGHT,
+ 'center': gtk.JUSTIFY_CENTER,
+ 'justify': gtk.JUSTIFY_FILL,
+ } [value]
+ except KeyError:
+ warnings.warn("Invalid text-align:%s requested" % value)
+ else:
+ tag.set_property("justification", align)
+
+ def _parse_style_text_decoration(self, tag, value):
+ if value == "none":
+ tag.set_property("underline", pango.UNDERLINE_NONE)
+ tag.set_property("strikethrough", False)
+ elif value == "underline":
+ tag.set_property("underline", pango.UNDERLINE_SINGLE)
+ tag.set_property("strikethrough", False)
+ elif value == "overline":
+ warnings.warn("text-decoration:overline not implemented")
+ tag.set_property("underline", pango.UNDERLINE_NONE)
+ tag.set_property("strikethrough", False)
+ elif value == "line-through":
+ tag.set_property("underline", pango.UNDERLINE_NONE)
+ tag.set_property("strikethrough", True)
+ elif value == "blink":
+ warnings.warn("text-decoration:blink not implemented")
+ else:
+ warnings.warn("text-decoration:%s not implemented" % value)
+
+
+ ## build a dictionary mapping styles to methods, for greater speed
+ __style_methods = dict()
+ for style in ["background-color", "color", "font-family", "font-size",
+ "font-style", "font-weight", "margin-left", "margin-right",
+ "text-align", "text-decoration"]:
+ try:
+ method = locals()["_parse_style_%s" % style.replace('-', '_')]
+ except KeyError:
+ warnings.warn("Style attribute '%s' not yet implemented" % style)
+ else:
+ __style_methods[style] = method
+
+ def _get_style_tags(self):
+ return [tag for tag in self.styles if tag is not None]
+
+ def _begin_span(self, style, tag=None):
+ if style is None:
+ self.styles.append(tag)
+ return None
+ if tag is None:
+ tag = self.textbuf.create_tag()
+ for attr, val in [item.split(':', 1) for item in style.split(';')]:
+ attr = attr.strip().lower()
+ val = val.strip()
+ try:
+ method = self.__style_methods[attr]
+ except KeyError:
+ warnings.warn("Style attribute '%s' requested "
+ "but not yet implemented" % attr)
+ else:
+ method(self, tag, val)
+ self.styles.append(tag)
+
+ def _end_span(self):
+ self.styles.pop(-1)
+
+ def _insert_text(self, text):
+ tags = self._get_style_tags()
+ if tags:
+ self.textbuf.insert_with_tags(self.iter, text, *tags)
+ else:
+ self.textbuf.insert(self.iter, text)
+
+ def _flush_text(self):
+ if not self.text:
+ return
+ self._insert_text(self.text.replace('\n', ''))
+ self.text = ''
+
+ def _anchor_event(self, tag, textview, event, iter, href, type_):
+ if event.type == gtk.gdk.BUTTON_PRESS and event.button == 1:
+ self.textview.emit("url-clicked", href, type_)
+ return True
+ return False
+
+ def characters(self, content):
+ if allwhitespace_rx.match(content) is not None:
+ return
+ #if self.text:
+ # self.text += ' '
+ self.text += whitespace_rx.sub(' ', content)
+
+ def startElement(self, name, attrs):
+ self._flush_text()
+ try:
+ style = attrs['style']
+ except KeyError:
+ style = None
+
+ tag = None
+ if name == 'a':
+ tag = self.textbuf.create_tag()
+ tag.set_property('foreground', '#0000ff')
+ tag.set_property('underline', pango.UNDERLINE_SINGLE)
+ try:
+ type_ = attrs['type']
+ except KeyError:
+ type_ = None
+ tag.connect('event', self._anchor_event, attrs['href'], type_)
+ tag.is_anchor = True
+
+ self._begin_span(style, tag)
+
+ if name == 'br':
+ pass # handled in endElement
+ elif name == 'p':
+ if not self.iter.starts_line():
+ self._insert_text("\n")
+ elif name == 'div':
+ if not self.iter.starts_line():
+ self._insert_text("\n")
+ elif name == 'span':
+ pass
+ elif name == 'ul':
+ if not self.iter.starts_line():
+ self._insert_text("\n")
+ self.list_counters.insert(0, None)
+ elif name == 'ol':
+ if not self.iter.starts_line():
+ self._insert_text("\n")
+ self.list_counters.insert(0, 0)
+ elif name == 'li':
+ if self.list_counters[0] is None:
+ li_head = unichr(0x2022)
+ else:
+ self.list_counters[0] += 1
+ li_head = "%i." % self.list_counters[0]
+ self.text = ' '*len(self.list_counters)*4 + li_head + ' '
+ elif name == 'img':
+ try:
+ ## Max image size = 10 MB (to try to prevent DoS)
+ mem = urllib2.urlopen(attrs['src']).read(10*1024*1024)
+ ## Caveat: GdkPixbuf is known not to be safe to load
+ ## images from network... this program is now potentially
+ ## hackable ;)
+ loader = gtk.gdk.PixbufLoader()
+ loader.write(mem); loader.close()
+ pixbuf = loader.get_pixbuf()
+ except Exception, ex:
+ pixbuf = None
+ try:
+ alt = attrs['alt']
+ except KeyError:
+ alt = "Broken image"
+ if pixbuf is not None:
+ tags = self._get_style_tags()
+ if tags:
+ tmpmark = self.textbuf.create_mark(None, self.iter, True)
+
+ self.textbuf.insert_pixbuf(self.iter, pixbuf)
+
+ if tags:
+ start = self.textbuf.get_iter_at_mark(tmpmark)
+ for tag in tags:
+ self.textbuf.apply_tag(tag, start, self.iter)
+ self.textbuf.delete_mark(tmpmark)
+ else:
+ self._insert_text("[IMG: %s]" % alt)
+ elif name == 'body':
+ pass
+ elif name == 'a':
+ pass
+ else:
+ warnings.warn("Unhandled element '%s'" % name)
+
+ def endElement(self, name):
+ self._flush_text()
+ if name == 'p':
+ if not self.iter.starts_line():
+ self._insert_text("\n")
+ elif name == 'div':
+ if not self.iter.starts_line():
+ self._insert_text("\n")
+ elif name == 'span':
+ pass
+ elif name == 'br':
+ self._insert_text("\n")
+ elif name == 'ul':
+ self.list_counters.pop()
+ elif name == 'ol':
+ self.list_counters.pop()
+ elif name == 'li':
+ self._insert_text("\n")
+ elif name == 'img':
+ pass
+ elif name == 'body':
+ pass
+ elif name == 'a':
+ pass
+ else:
+ warnings.warn("Unhandled element '%s'" % name)
+ self._end_span()
+
+
+class HtmlTextView(gtk.TextView):
+ __gtype_name__ = 'HtmlTextView'
+ __gsignals__ = {
+ 'url-clicked': (gobject.SIGNAL_RUN_LAST, None, (str, str)), # href, type
+ }
+
+ def __init__(self):
+ gtk.TextView.__init__(self)
+ self.set_wrap_mode(gtk.WRAP_CHAR)
+ self.set_editable(False)
+ self._changed_cursor = False
+ self.connect("motion-notify-event", self.__motion_notify_event)
+ self.connect("leave-notify-event", self.__leave_event)
+ self.connect("enter-notify-event", self.__motion_notify_event)
+ self.set_pixels_above_lines(3)
+ self.set_pixels_below_lines(3)
+
+ def __leave_event(self, widget, event):
+ if self._changed_cursor:
+ window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
+ window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
+ self._changed_cursor = False
+
+ def __motion_notify_event(self, widget, event):
+ x, y, _ = widget.window.get_pointer()
+ x, y = widget.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, x, y)
+ tags = widget.get_iter_at_location(x, y).get_tags()
+ for tag in tags:
+ if getattr(tag, 'is_anchor', False):
+ is_over_anchor = True
+ break
+ else:
+ is_over_anchor = False
+ if not self._changed_cursor and is_over_anchor:
+ window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
+ window.set_cursor(gtk.gdk.Cursor(gtk.gdk.HAND2))
+ self._changed_cursor = True
+ elif self._changed_cursor and not is_over_anchor:
+ window = widget.get_window(gtk.TEXT_WINDOW_TEXT)
+ window.set_cursor(gtk.gdk.Cursor(gtk.gdk.XTERM))
+ self._changed_cursor = False
+ return False
+
+ def display_html(self, html):
+ buffer = self.get_buffer()
+ eob = buffer.get_end_iter()
+ ## this works too if libxml2 is not available
+ #parser = xml.sax.make_parser(['drv_libxml2'])
+ parser = xml.sax.make_parser()
+ # parser.setFeature(xml.sax.handler.feature_validation, True)
+ parser.setContentHandler(HtmlHandler(self, eob))
+ #parser.setEntityResolver(HtmlEntityResolver())
+ parser.parse(StringIO(html))
+
+ if not eob.starts_line():
+ buffer.insert(eob, "\n")
+
+if gobject.pygtk_version < (2, 8):
+ gobject.type_register(HtmlTextView)
+
+
+if __name__ == '__main__':
+ htmlview = HtmlTextView()
+ def url_cb(view, url, type_):
+ print "url-clicked", url, type_
+ htmlview.connect("url-clicked", url_cb)
+ htmlview.display_html('<div><span style="color: red; text-decoration:underline">Hello</span><br/>\n'
+ ' <img src="http://images.slashdot.org/topics/topicsoftware.gif"/><br/>\n'
+ ' <span style="font-size: 500%; font-family: serif">World</span>\n'
+ '</div>\n')
+ htmlview.display_html("<br/>")
+ htmlview.display_html("""
+ <p style='font-size:large'>
+ <span style='font-style: italic'>O<span style='font-size:larger'>M</span>G</span>,
+ I&apos;m <span style='color:green'>green</span>
+ with <span style='font-weight: bold'>envy</span>!
+ </p>
+ """)
+ htmlview.display_html("<br/>")
+ htmlview.display_html("""
+ <body xmlns='http://www.w3.org/1999/xhtml'>
+ <p>As Emerson said in his essay <span style='font-style: italic; background-color:cyan'>Self-Reliance</span>:</p>
+ <p style='margin-left: 5px; margin-right: 2%'>
+ &quot;A foolish consistency is the hobgoblin of little minds.&quot;
+ </p>
+ </body>
+ """)
+ htmlview.display_html("<br/>")
+ htmlview.display_html("""
+ <body xmlns='http://www.w3.org/1999/xhtml'>
+ <p style='text-align:center'>Hey, are you licensed to <a href='http://www.jabber.org/'>Jabber</a>?</p>
+ <p style='text-align:right'><img src='http://www.jabber.org/images/psa-license.jpg'
+ alt='A License to Jabber'
+ height='261'
+ width='537'/></p>
+ </body>
+ """)
+
+ htmlview.display_html("""
+ <body xmlns='http://www.w3.org/1999/xhtml'>
+ <ul style='background-color:rgb(120,140,100)'>
+ <li> One </li>
+ <li> Two </li>
+ <li> Three </li>
+ </ul>
+ </body>
+ """)
+ htmlview.display_html("""
+ <body xmlns='http://www.w3.org/1999/xhtml'>
+ <ol>
+ <li> One </li>
+ <li> Two </li>
+ <li> Three </li>
+ </ol>
+ </body>
+ """)
+
+ htmlview.display_html("""
+<p>
+Can we add button to return in project root directory in filemanager ?
+</p>
+ """)
+
+ htmlview.display_html('<body><span style="font-family: monospace">|&#xA0;&#xA0;&#xA0;|</span></body>')
+
+ htmlview.show()
+ sw = gtk.ScrolledWindow()
+ sw.set_property("hscrollbar-policy", gtk.POLICY_AUTOMATIC)
+ sw.set_property("vscrollbar-policy", gtk.POLICY_AUTOMATIC)
+ sw.set_property("border-width", 0)
+ sw.add(htmlview)
+ sw.show()
+ frame = gtk.Frame()
+ frame.set_shadow_type(gtk.SHADOW_IN)
+ frame.show()
+ frame.add(sw)
+ w = gtk.Window()
+ w.add(frame)
+ w.set_default_size(400, 300)
+ w.show_all()
+ w.connect("destroy", lambda w: gtk.main_quit())
+ gtk.main()