975 lines
30 KiB
Python
975 lines
30 KiB
Python
### Copyright (C) 2005 Gustavo J. A. M. Carneiro
|
|
### Copyright (C) 2006 Santiago Gala
|
|
###
|
|
### 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
|
|
|
|
Starting with the version posted by Gustavo Carneiro,
|
|
I (Santiago Gala) am trying to make it more compatible
|
|
with the markup that docutils generate, and also more
|
|
modular.
|
|
|
|
"""
|
|
|
|
import gobject
|
|
import pango
|
|
import gtk
|
|
import xml.sax, xml.sax.handler
|
|
import re
|
|
import warnings
|
|
from cStringIO import StringIO
|
|
import urllib2
|
|
import operator
|
|
|
|
from common import gajim
|
|
#from common import i18n
|
|
|
|
|
|
import tooltips
|
|
|
|
|
|
__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()))
|
|
|
|
#embryo of CSS classes
|
|
classes = {
|
|
#'system-message':';display: none',
|
|
'problematic':';color: red',
|
|
}
|
|
|
|
#styles for elemens
|
|
element_styles = {
|
|
'u' : ';text-decoration: underline',
|
|
'em' : ';font-style: oblique',
|
|
'cite' : '; background-color:rgb(170,190,250); font-style: oblique',
|
|
'li' : '; margin-left: 1em; margin-right: 10%',
|
|
'strong' : ';font-weight: bold',
|
|
'pre' : '; background-color:rgb(190,190,190); font-family: monospace; white-space: pre; margin-left: 1em; margin-right: 10%',
|
|
'kbd' : ';background-color:rgb(210,210,210);font-family: monospace',
|
|
'blockquote': '; background-color:rgb(170,190,250); margin-left: 2em; margin-right: 10%',
|
|
'dt' : ';font-weight: bold; font-style: oblique',
|
|
'dd' : ';margin-left: 2em; font-style: oblique'
|
|
}
|
|
# no difference for the moment
|
|
element_styles['dfn'] = element_styles['em']
|
|
element_styles['var'] = element_styles['em']
|
|
# deprecated, legacy, presentational
|
|
element_styles['tt'] = element_styles['kbd']
|
|
element_styles['i'] = element_styles['em']
|
|
element_styles['b'] = element_styles['strong']
|
|
|
|
class_styles = {
|
|
}
|
|
|
|
"""
|
|
==========
|
|
JEP-0071
|
|
==========
|
|
|
|
This Integration Set includes a subset of the modules defined for
|
|
XHTML 1.0 but does not redefine any existing modules, nor
|
|
does it define any new modules. Specifically, it includes the
|
|
following modules only:
|
|
|
|
- Structure
|
|
- Text
|
|
|
|
* Block
|
|
|
|
phrasal
|
|
addr, blockquote, pre
|
|
Struc
|
|
div,p
|
|
Heading
|
|
h1, h2, h3, h4, h5, h6
|
|
|
|
* Inline
|
|
|
|
phrasal
|
|
abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var
|
|
structural
|
|
br, span
|
|
|
|
- Hypertext (a)
|
|
- List (ul, ol, dl)
|
|
- Image (img)
|
|
- Style Attribute
|
|
|
|
Therefore XHTML-IM uses the following content models:
|
|
|
|
Block.mix
|
|
Block-like elements, e.g., paragraphs
|
|
Flow.mix
|
|
Any block or inline elements
|
|
Inline.mix
|
|
Character-level elements
|
|
InlineNoAnchor.class
|
|
Anchor element
|
|
InlinePre.mix
|
|
Pre element
|
|
|
|
XHTML-IM also uses the following Attribute Groups:
|
|
|
|
Core.extra.attrib
|
|
TBD
|
|
I18n.extra.attrib
|
|
TBD
|
|
Common.extra
|
|
style
|
|
|
|
|
|
...
|
|
#block level:
|
|
#Heading h
|
|
# ( pres = h1 | h2 | h3 | h4 | h5 | h6 )
|
|
#Block ( phrasal = address | blockquote | pre )
|
|
#NOT ( presentational = hr )
|
|
# ( structural = div | p )
|
|
#other: section
|
|
#Inline ( phrasal = abbr | acronym | cite | code | dfn | em | kbd | q | samp | strong | var )
|
|
#NOT ( presentational = b | big | i | small | sub | sup | tt )
|
|
# ( structural = br | span )
|
|
#Param/Legacy param, font, basefont, center, s, strike, u, dir, menu, isindex
|
|
#
|
|
"""
|
|
|
|
BLOCK_HEAD = set(( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', ))
|
|
BLOCK_PHRASAL = set(( 'address', 'blockquote', 'pre', ))
|
|
BLOCK_PRES = set(( 'hr', )) #not in xhtml-im
|
|
BLOCK_STRUCT = set(( 'div', 'p', ))
|
|
BLOCK_HACKS = set(( 'table', 'tr' )) # at the very least, they will start line ;)
|
|
BLOCK = BLOCK_HEAD.union(BLOCK_PHRASAL).union(BLOCK_STRUCT).union(BLOCK_PRES).union(BLOCK_HACKS)
|
|
|
|
INLINE_PHRASAL = set('abbr, acronym, cite, code, dfn, em, kbd, q, samp, strong, var'.split(', '))
|
|
INLINE_PRES = set('b, i, u, tt'.split(', ')) #not in xhtml-im
|
|
INLINE_STRUCT = set('br, span'.split(', '))
|
|
INLINE = INLINE_PHRASAL.union(INLINE_PRES).union(INLINE_STRUCT)
|
|
|
|
LIST_ELEMS = set( 'dl, ol, ul'.split(', '))
|
|
|
|
for name in BLOCK_HEAD:
|
|
num = eval(name[1])
|
|
size = (num-1) // 2
|
|
weigth = (num - 1) % 2
|
|
element_styles[name] = '; font-size: %s; %s' % ( ('large', 'medium', 'small')[size],
|
|
('font-weight: bold', 'font-style: oblique')[weigth],
|
|
)
|
|
|
|
|
|
def build_patterns(view, config, interface):
|
|
#extra, rst does not mark _underline_ or /it/ up
|
|
#actually <b>, <i> or <u> are not in the JEP-0071, but are seen in the wild
|
|
basic_pattern = r'(?<!\w|\<|/|:)' r'/[^\s/]' r'([^/]*[^\s/])?' r'/(?!\w|/|:)|'\
|
|
r'(?<!\w)' r'_[^\s_]' r'([^_]*[^\s_])?' r'_(?!\w)'
|
|
view.basic_pattern_re = re.compile(basic_pattern)
|
|
#TODO: emoticons
|
|
emoticons_pattern = ''
|
|
if config.get('emoticons_theme'):
|
|
# When an emoticon is bordered by an alpha-numeric character it is NOT
|
|
# expanded. e.g., foo:) NO, foo :) YES, (brb) NO, (:)) YES, etc.
|
|
# We still allow multiple emoticons side-by-side like :P:P:P
|
|
# sort keys by length so :qwe emot is checked before :q
|
|
keys = interface.emoticons.keys()
|
|
keys.sort(interface.on_emoticon_sort)
|
|
emoticons_pattern_prematch = ''
|
|
emoticons_pattern_postmatch = ''
|
|
emoticon_length = 0
|
|
for emoticon in keys: # travel thru emoticons list
|
|
emoticon_escaped = re.escape(emoticon) # espace regexp metachars
|
|
emoticons_pattern += emoticon_escaped + '|'# | means or in regexp
|
|
if (emoticon_length != len(emoticon)):
|
|
# Build up expressions to match emoticons next to other emoticons
|
|
emoticons_pattern_prematch = emoticons_pattern_prematch[:-1] + ')|(?<='
|
|
emoticons_pattern_postmatch = emoticons_pattern_postmatch[:-1] + ')|(?='
|
|
emoticon_length = len(emoticon)
|
|
emoticons_pattern_prematch += emoticon_escaped + '|'
|
|
emoticons_pattern_postmatch += emoticon_escaped + '|'
|
|
# We match from our list of emoticons, but they must either have
|
|
# whitespace, or another emoticon next to it to match successfully
|
|
# [\w.] alphanumeric and dot (for not matching 8) in (2.8))
|
|
emoticons_pattern = '|' + \
|
|
'(?:(?<![\w.]' + emoticons_pattern_prematch[:-1] + '))' + \
|
|
'(?:' + emoticons_pattern[:-1] + ')' + \
|
|
'(?:(?![\w.]' + emoticons_pattern_postmatch[:-1] + '))'
|
|
|
|
# because emoticons match later (in the string) they need to be after
|
|
# basic matches that may occur earlier
|
|
emot_and_basic_pattern = basic_pattern + emoticons_pattern
|
|
view.emot_and_basic_re = re.compile(emot_and_basic_pattern, re.IGNORECASE)
|
|
|
|
|
|
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 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.starting=True
|
|
self.preserve = False
|
|
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_display(self, tag, value):
|
|
if value == 'none':
|
|
tag.set_property('invisible','true')
|
|
#Fixme: display: block, inline
|
|
|
|
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(self,length, tag, propname):
|
|
styles = self._get_style_tags()
|
|
if styles:
|
|
length += styles[-1].get_property(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)
|
|
|
|
def _parse_style_white_space(self, tag, value):
|
|
if value == 'pre':
|
|
tag.set_property("wrap_mode", gtk.WRAP_NONE)
|
|
elif value == 'normal':
|
|
tag.set_property("wrap_mode", gtk.WRAP_WORD)
|
|
elif value == 'nowrap':
|
|
tag.set_property("wrap_mode", gtk.WRAP_NONE)
|
|
|
|
|
|
# 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", "white-space", 'display' ]:
|
|
try:
|
|
method = locals()["_parse_style_%s" % style.replace('-', '_')]
|
|
except KeyError:
|
|
warnings.warn("Style attribute '%s' not yet implemented" % style)
|
|
else:
|
|
__style_methods[style] = method
|
|
del style
|
|
# --
|
|
|
|
def _get_style_tags(self):
|
|
return [tag for tag in self.styles if tag is not None]
|
|
|
|
def _create_url(self, href, title, type_, id_):
|
|
tag = self.textbuf.create_tag(id_)
|
|
if href and href[0] != '#':
|
|
tag.href = href
|
|
tag.type_ = type_ # to be used by the URL handler
|
|
tag.connect('event', self.textview.html_hyperlink_handler, 'url', href)
|
|
tag.set_property('foreground', '#0000ff')
|
|
tag.set_property('underline', pango.UNDERLINE_SINGLE)
|
|
tag.is_anchor = True
|
|
if title:
|
|
tag.title = title
|
|
return tag
|
|
|
|
|
|
def _begin_span(self, style, tag=None, id_=None):
|
|
if style is None:
|
|
self.styles.append(tag)
|
|
return None
|
|
if tag is None:
|
|
if id_:
|
|
tag = self.textbuf.create_tag(id_)
|
|
else:
|
|
tag = self.textbuf.create_tag() # we create anonymous tag
|
|
for attr, val in [item.split(':', 1) for item in style.split(';') if len(item.strip())]:
|
|
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()
|
|
|
|
def _jump_line(self):
|
|
self.textbuf.insert_with_tags_by_name(self.iter, '\n', 'eol')
|
|
self.starting = True
|
|
|
|
def _insert_text(self, text):
|
|
if self.starting and text != '\n':
|
|
self.starting = (text[-1] == '\n')
|
|
tags = self._get_style_tags()
|
|
if tags:
|
|
self.textbuf.insert_with_tags(self.iter, text, *tags)
|
|
else:
|
|
self.textbuf.insert(self.iter, text)
|
|
|
|
def _starts_line(self):
|
|
return self.starting or self.iter.starts_line()
|
|
|
|
def _flush_text(self):
|
|
if not self.text: return
|
|
text, self.text = self.text, ''
|
|
if not self.preserve:
|
|
text = text.replace('\n', ' ')
|
|
self.handle_specials(whitespace_rx.sub(' ', text))
|
|
else:
|
|
self._insert_text(text.strip("\n"))
|
|
|
|
def _anchor_event(self, tag, textview, event, iter, href, type_):
|
|
if event.type == gtk.gdk.BUTTON_PRESS:
|
|
self.textview.emit("url-clicked", href, type_)
|
|
return True
|
|
return False
|
|
|
|
def handle_specials(self, text):
|
|
index = 0
|
|
se = self.textview.config.get('show_ascii_formatting_chars')
|
|
if self.textview.config.get('emoticons_theme'):
|
|
iterator = self.textview.emot_and_basic_re.finditer(text)
|
|
else:
|
|
iterator = self.textview.basic_pattern_re.finditer(text)
|
|
for match in iterator:
|
|
start, end = match.span()
|
|
special_text = text[start:end]
|
|
if start != 0:
|
|
self._insert_text(text[index:start])
|
|
index = end # update index
|
|
#emoticons
|
|
possible_emot_ascii_caps = special_text.upper() # emoticons keys are CAPS
|
|
if self.textview.config.get('emoticons_theme') and \
|
|
possible_emot_ascii_caps in self.textview.interface.emoticons.keys():
|
|
#it's an emoticon
|
|
emot_ascii = possible_emot_ascii_caps
|
|
anchor = self.textbuf.create_child_anchor(self.iter)
|
|
img = gtk.Image()
|
|
img.set_from_file(self.textview.interface.emoticons[emot_ascii])
|
|
img.show()
|
|
# TODO: add alt/tooltip with the special_text (a11y)
|
|
self.textview.add_child_at_anchor(img, anchor)
|
|
else:
|
|
# now print it
|
|
if special_text.startswith('/'): # it's explicit italics
|
|
self.startElement('i', {})
|
|
elif special_text.startswith('_'): # it's explicit underline
|
|
self.startElement("u", {})
|
|
if se: self._insert_text(special_text[0])
|
|
self.handle_specials(special_text[1:-1])
|
|
if se: self._insert_text(special_text[0])
|
|
if special_text.startswith('_'): # it's explicit underline
|
|
self.endElement('u')
|
|
if special_text.startswith('/'): # it's explicit italics
|
|
self.endElement('i')
|
|
if index < len(text):
|
|
self._insert_text(text[index:])
|
|
|
|
def characters(self, content):
|
|
if self.preserve:
|
|
self.text += content
|
|
return
|
|
if allwhitespace_rx.match(content) is not None and self._starts_line():
|
|
return
|
|
self.text += content
|
|
self.starting = False
|
|
|
|
|
|
def startElement(self, name, attrs):
|
|
self._flush_text()
|
|
klass = [i for i in attrs.get('class',' ').split(' ') if i]
|
|
style = attrs.get('style','')
|
|
#Add styles defined for classes
|
|
#TODO: priority between class and style elements?
|
|
for k in klass:
|
|
if k in classes:
|
|
style += classes[k]
|
|
|
|
tag = None
|
|
#FIXME: if we want to use id, it needs to be unique across
|
|
# the whole textview, so we need to add something like the
|
|
# message-id to it.
|
|
#id_ = attrs.get('id',None)
|
|
id_ = None
|
|
if name == 'a':
|
|
#TODO: accesskey, charset, hreflang, rel, rev, tabindex, type
|
|
href = attrs.get('href', None)
|
|
if not href:
|
|
href = attrs.get('HREF', None)
|
|
# Gaim sends HREF instead of href
|
|
title = attrs.get('title', attrs.get('rel',href))
|
|
type_ = attrs.get('type', None)
|
|
tag = self._create_url(href, title, type_, id_)
|
|
elif name == 'blockquote':
|
|
cite = attrs.get('cite', None)
|
|
if cite:
|
|
tag = self.textbuf.create_tag(id_)
|
|
tag.title = title
|
|
tag.is_anchor = True
|
|
elif name in LIST_ELEMS:
|
|
style += ';margin-left: 2em'
|
|
if name in element_styles:
|
|
style += element_styles[name]
|
|
|
|
if style == '':
|
|
style = None
|
|
self._begin_span(style, tag, id_)
|
|
|
|
if name == 'br':
|
|
pass # handled in endElement
|
|
elif name == 'hr':
|
|
pass # handled in endElement
|
|
elif name in BLOCK:
|
|
if not self._starts_line():
|
|
self._jump_line()
|
|
if name == 'pre':
|
|
self.preserve = True
|
|
elif name == 'span':
|
|
pass
|
|
elif name in ('dl', 'ul'):
|
|
if not self._starts_line():
|
|
self._jump_line()
|
|
self.list_counters.append(None)
|
|
elif name == 'ol':
|
|
if not self._starts_line():
|
|
self._jump_line()
|
|
self.list_counters.append(0)
|
|
elif name == 'li':
|
|
if self.list_counters[-1] is None:
|
|
li_head = unichr(0x2022)
|
|
else:
|
|
self.list_counters[-1] += 1
|
|
li_head = "%i." % self.list_counters[-1]
|
|
self.text = ' '*len(self.list_counters)*4 + li_head + ' '
|
|
self._flush_text()
|
|
self.starting = True
|
|
elif name == 'dd':
|
|
self._jump_line()
|
|
elif name == 'dt':
|
|
if not self.starting:
|
|
self._jump_line()
|
|
elif name == 'img':
|
|
try:
|
|
# Max image size = 2 MB (to try to prevent DoS)
|
|
mem = urllib2.urlopen(attrs['src']).read(2*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:
|
|
gajim.log.debug(str('Error loading image'+ex))
|
|
pixbuf = None
|
|
alt = attrs.get('alt', "Broken image")
|
|
try:
|
|
loader.close()
|
|
except: pass
|
|
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' or name == 'html':
|
|
pass
|
|
elif name == 'a':
|
|
pass
|
|
elif name in INLINE:
|
|
pass
|
|
else:
|
|
warnings.warn("Unhandled element '%s'" % name)
|
|
|
|
def endElement(self, name):
|
|
endPreserving = False
|
|
newLine = False
|
|
if name == 'br':
|
|
newLine = True
|
|
elif name == 'hr':
|
|
#FIXME: plenty of unused attributes (width, height,...) :)
|
|
self._jump_line()
|
|
try:
|
|
self.textbuf.insert_pixbuf(self.iter, self.textview.focus_out_line_pixbuf)
|
|
#self._insert_text(u"\u2550"*40)
|
|
self._jump_line()
|
|
except Exception, e:
|
|
gajim.log.debug(str("Error in hr"+e))
|
|
elif name in LIST_ELEMS:
|
|
self.list_counters.pop()
|
|
elif name == 'li':
|
|
newLine = True
|
|
elif name == 'img':
|
|
pass
|
|
elif name == 'body' or name == 'html':
|
|
pass
|
|
elif name == 'a':
|
|
pass
|
|
elif name in INLINE:
|
|
pass
|
|
elif name in ('dd', 'dt', ):
|
|
pass
|
|
elif name in BLOCK:
|
|
if name == 'pre':
|
|
endPreserving = True
|
|
else:
|
|
warnings.warn("Unhandled element '%s'" % name)
|
|
self._flush_text()
|
|
if endPreserving:
|
|
self.preserve = False
|
|
if newLine:
|
|
self._jump_line()
|
|
self._end_span()
|
|
#if not self._starts_line():
|
|
# self.text = ' '
|
|
|
|
class HtmlTextView(gtk.TextView):
|
|
__gtype_name__ = 'HtmlTextView'
|
|
__gsignals__ = {
|
|
'url-clicked': (gobject.SIGNAL_RUN_LAST, None, (str, str)), # href, type
|
|
}
|
|
|
|
def __init__(self):
|
|
gobject.GObject.__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.get_buffer().create_tag('eol', scale = pango.SCALE_XX_SMALL)
|
|
self.tooltip = tooltips.BaseTooltip()
|
|
self.config = gajim.config
|
|
self.interface = gajim.interface
|
|
# end big hack
|
|
build_patterns(self,gajim.config,gajim.interface)
|
|
|
|
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 show_tooltip(self, tag):
|
|
if not self.tooltip.win:
|
|
# check if the current pointer is still over the line
|
|
x, y, _ = self.window.get_pointer()
|
|
x, y = self.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, x, y)
|
|
tags = self.get_iter_at_location(x, y).get_tags()
|
|
is_over_anchor = False
|
|
for tag_ in tags:
|
|
if getattr(tag_, 'is_anchor', False):
|
|
is_over_anchor = True
|
|
break
|
|
if not is_over_anchor:
|
|
return
|
|
text = getattr(tag, 'title', False)
|
|
if text:
|
|
pointer = self.get_pointer()
|
|
position = self.window.get_origin()
|
|
self.tooltip.show_tooltip(text, 8, position[1] + pointer[1])
|
|
|
|
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()
|
|
is_over_anchor = False
|
|
for tag in tags:
|
|
if getattr(tag, 'is_anchor', False):
|
|
is_over_anchor = True
|
|
break
|
|
if self.tooltip.timeout != 0:
|
|
# Check if we should hide the line tooltip
|
|
if not is_over_anchor:
|
|
self.tooltip.hide_tooltip()
|
|
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
|
|
self.tooltip.timeout = gobject.timeout_add(500, self.show_tooltip, tag)
|
|
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.setFeature(xml.sax.handler.feature_validation, True)
|
|
parser = xml.sax.make_parser()
|
|
parser.setContentHandler(HtmlHandler(self, eob))
|
|
parser.parse(StringIO(html))
|
|
|
|
#if not eob.starts_line():
|
|
# buffer.insert(eob, "\n")
|
|
|
|
if gobject.pygtk_version < (2, 8):
|
|
gobject.type_register(HtmlTextView)
|
|
|
|
change_cursor = None
|
|
|
|
if __name__ == '__main__':
|
|
|
|
htmlview = HtmlTextView()
|
|
|
|
tooltip = tooltips.BaseTooltip()
|
|
def on_textview_motion_notify_event(widget, event):
|
|
'''change the cursor to a hand when we are over a mail or an url'''
|
|
global change_cursor
|
|
pointer_x, pointer_y, spam = htmlview.window.get_pointer()
|
|
x, y = htmlview.window_to_buffer_coords(gtk.TEXT_WINDOW_TEXT, pointer_x,
|
|
pointer_y)
|
|
tags = htmlview.get_iter_at_location(x, y).get_tags()
|
|
if change_cursor:
|
|
htmlview.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
|
|
gtk.gdk.Cursor(gtk.gdk.XTERM))
|
|
change_cursor = None
|
|
tag_table = htmlview.get_buffer().get_tag_table()
|
|
over_line = False
|
|
for tag in tags:
|
|
try:
|
|
if tag.is_anchor:
|
|
htmlview.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
|
|
gtk.gdk.Cursor(gtk.gdk.HAND2))
|
|
change_cursor = tag
|
|
elif tag == tag_table.lookup('focus-out-line'):
|
|
over_line = True
|
|
except: pass
|
|
|
|
#if line_tooltip.timeout != 0:
|
|
# Check if we should hide the line tooltip
|
|
# if not over_line:
|
|
# line_tooltip.hide_tooltip()
|
|
#if over_line and not line_tooltip.win:
|
|
# line_tooltip.timeout = gobject.timeout_add(500,
|
|
# show_line_tooltip)
|
|
# htmlview.get_window(gtk.TEXT_WINDOW_TEXT).set_cursor(
|
|
# gtk.gdk.Cursor(gtk.gdk.LEFT_PTR))
|
|
# change_cursor = tag
|
|
|
|
htmlview.connect('motion_notify_event', on_textview_motion_notify_event)
|
|
|
|
def handler(texttag, widget, event, iter, kind, href):
|
|
if event.type == gtk.gdk.BUTTON_PRESS:
|
|
print href
|
|
|
|
htmlview.html_hyperlink_handler = handler
|
|
|
|
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("<hr />")
|
|
htmlview.display_html("""
|
|
<p style='font-size:large'>
|
|
<span style='font-style: italic'>O<span style='font-size:larger'>M</span>G</span>,
|
|
I'm <span style='color:green'>green</span>
|
|
with <span style='font-weight: bold'>envy</span>!
|
|
</p>
|
|
""")
|
|
htmlview.display_html("<hr />")
|
|
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%'>
|
|
"A foolish consistency is the hobgoblin of little minds."
|
|
</p>
|
|
</body>
|
|
""")
|
|
htmlview.display_html("<hr />")
|
|
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("<hr />")
|
|
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><hr /><pre style="background-color:rgb(120,120,120)">def fac(n):
|
|
def faciter(n,acc):
|
|
if n==0: return acc
|
|
return faciter(n-1, acc*n)
|
|
if n<0: raise ValueError("Must be non-negative")
|
|
return faciter(n,1)</pre>
|
|
</body>
|
|
""")
|
|
htmlview.display_html("<hr />")
|
|
htmlview.display_html("""
|
|
<body xmlns='http://www.w3.org/1999/xhtml'>
|
|
<ol style='background-color:rgb(120,140,100)'>
|
|
<li> One </li>
|
|
<li> Two is nested: <ul style='background-color:rgb(200,200,100)'>
|
|
<li> One </li>
|
|
<li> Two </li>
|
|
<li> Three </li>
|
|
</ul></li>
|
|
<li> Three </li></ol>
|
|
</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()
|