Mercurial > libervia-desktop-kivy
comparison cagou/core/simple_xhtml.py @ 312:772c170b47a9
Python3 port:
/!\ Cagou now runs with Python 3.6+
Port has been done in the same way as for backend (check backend commit b2d067339de3
message for details).
author | Goffi <goffi@goffi.org> |
---|---|
date | Tue, 13 Aug 2019 19:14:22 +0200 |
parents | 86b1cd8121dd |
children | 5868a5575e01 |
comparison
equal
deleted
inserted
replaced
311:a0d978d3ce84 | 312:772c170b47a9 |
---|---|
29 from sat_frontends.tools import css_color, strings as sat_strings | 29 from sat_frontends.tools import css_color, strings as sat_strings |
30 from cagou.core.image import AsyncImage | 30 from cagou.core.image import AsyncImage |
31 import webbrowser | 31 import webbrowser |
32 | 32 |
33 | 33 |
34 class Escape(unicode): | 34 class Escape(str): |
35 """Class used to mark that a message need to be escaped""" | 35 """Class used to mark that a message need to be escaped""" |
36 | |
37 def __init__(self, text): | |
38 super(Escape, self).__init__(text) | |
39 | 36 |
40 | 37 |
41 class SimpleXHTMLWidgetEscapedText(Label): | 38 class SimpleXHTMLWidgetEscapedText(Label): |
42 | 39 |
43 def on_parent(self, instance, parent): | 40 def on_parent(self, instance, parent): |
50 links = 0 | 47 links = 0 |
51 while True: | 48 while True: |
52 m = sat_strings.RE_URL.search(text[idx:]) | 49 m = sat_strings.RE_URL.search(text[idx:]) |
53 if m is not None: | 50 if m is not None: |
54 text_elts.append(escape_markup(m.string[0:m.start()])) | 51 text_elts.append(escape_markup(m.string[0:m.start()])) |
55 link_key = u'link_' + unicode(links) | 52 link_key = 'link_' + str(links) |
56 url = m.group() | 53 url = m.group() |
57 text_elts.append(u'[color=5500ff][ref={link}]{url}[/ref][/color]'.format( | 54 text_elts.append('[color=5500ff][ref={link}]{url}[/ref][/color]'.format( |
58 link = link_key, | 55 link = link_key, |
59 url = url | 56 url = url |
60 )) | 57 )) |
61 if not links: | 58 if not links: |
62 self.ref_urls = {link_key: url} | 59 self.ref_urls = {link_key: url} |
66 idx += m.end() | 63 idx += m.end() |
67 else: | 64 else: |
68 if links: | 65 if links: |
69 text_elts.append(escape_markup(text[idx:])) | 66 text_elts.append(escape_markup(text[idx:])) |
70 self.markup = True | 67 self.markup = True |
71 self.text = u''.join(text_elts) | 68 self.text = ''.join(text_elts) |
72 break | 69 break |
73 | 70 |
74 def on_text(self, instance, text): | 71 def on_text(self, instance, text): |
75 # do NOT call the method if self.markup is set | 72 # do NOT call the method if self.markup is set |
76 # this would result in infinite loop (because self.text | 73 # this would result in infinite loop (because self.text |
105 """ | 102 """ |
106 parent = self.parent | 103 parent = self.parent |
107 while parent and not isinstance(parent, SimpleXHTMLWidget): | 104 while parent and not isinstance(parent, SimpleXHTMLWidget): |
108 parent = parent.parent | 105 parent = parent.parent |
109 if parent is None: | 106 if parent is None: |
110 log.error(u"no SimpleXHTMLWidget parent found") | 107 log.error("no SimpleXHTMLWidget parent found") |
111 return parent | 108 return parent |
112 | 109 |
113 def _on_source_load(self, value): | 110 def _on_source_load(self, value): |
114 # this method is called when image is loaded | 111 # this method is called when image is loaded |
115 super(SimpleXHTMLWidgetImage, self)._on_source_load(value) | 112 super(SimpleXHTMLWidgetImage, self)._on_source_load(value) |
206 def _do_complexe_sizing(self, width): | 203 def _do_complexe_sizing(self, width): |
207 try: | 204 try: |
208 self.splitted | 205 self.splitted |
209 except AttributeError: | 206 except AttributeError: |
210 # XXX: to make things easier, we split labels in words | 207 # XXX: to make things easier, we split labels in words |
211 log.debug(u"split start") | 208 log.debug("split start") |
212 children = self.children[::-1] | 209 children = self.children[::-1] |
213 self.clear_widgets() | 210 self.clear_widgets() |
214 for child in children: | 211 for child in children: |
215 if isinstance(child, Label): | 212 if isinstance(child, Label): |
216 log.debug(u"label before split: {}".format(child.text)) | 213 log.debug("label before split: {}".format(child.text)) |
217 styles = [] | 214 styles = [] |
218 tag = False | 215 tag = False |
219 new_text = [] | 216 new_text = [] |
220 current_tag = [] | 217 current_tag = [] |
221 current_value = [] | 218 current_value = [] |
226 # on each new word (actually each space) | 223 # on each new word (actually each space) |
227 # FIXME: handle '\n' and other white chars | 224 # FIXME: handle '\n' and other white chars |
228 for c in child.text: | 225 for c in child.text: |
229 if tag: | 226 if tag: |
230 # we are parsing a markup tag | 227 # we are parsing a markup tag |
231 if c == u']': | 228 if c == ']': |
232 current_tag_s = u''.join(current_tag) | 229 current_tag_s = ''.join(current_tag) |
233 current_style = (current_tag_s, u''.join(current_value)) | 230 current_style = (current_tag_s, ''.join(current_value)) |
234 if close: | 231 if close: |
235 for idx, s in enumerate(reversed(styles)): | 232 for idx, s in enumerate(reversed(styles)): |
236 if s[0] == current_tag_s: | 233 if s[0] == current_tag_s: |
237 del styles[len(styles) - idx - 1] | 234 del styles[len(styles) - idx - 1] |
238 break | 235 break |
241 current_tag = [] | 238 current_tag = [] |
242 current_value = [] | 239 current_value = [] |
243 tag = False | 240 tag = False |
244 value = False | 241 value = False |
245 close = False | 242 close = False |
246 elif c == u'/': | 243 elif c == '/': |
247 close = True | 244 close = True |
248 elif c == u'=': | 245 elif c == '=': |
249 value = True | 246 value = True |
250 elif value: | 247 elif value: |
251 current_value.append(c) | 248 current_value.append(c) |
252 else: | 249 else: |
253 current_tag.append(c) | 250 current_tag.append(c) |
254 new_text.append(c) | 251 new_text.append(c) |
255 else: | 252 else: |
256 # we are parsing regular text | 253 # we are parsing regular text |
257 if c == u'[': | 254 if c == '[': |
258 new_text.append(c) | 255 new_text.append(c) |
259 tag = True | 256 tag = True |
260 elif c == u' ': | 257 elif c == ' ': |
261 # new word, we do a new widget | 258 # new word, we do a new widget |
262 new_text.append(u' ') | 259 new_text.append(' ') |
263 for t, v in reversed(styles): | 260 for t, v in reversed(styles): |
264 new_text.append(u'[/{}]'.format(t)) | 261 new_text.append('[/{}]'.format(t)) |
265 current_wid.text = u''.join(new_text) | 262 current_wid.text = ''.join(new_text) |
266 new_text = [] | 263 new_text = [] |
267 self.add_widget(current_wid) | 264 self.add_widget(current_wid) |
268 log.debug(u"new widget: {}".format(current_wid.text)) | 265 log.debug("new widget: {}".format(current_wid.text)) |
269 current_wid = self._createText() | 266 current_wid = self._createText() |
270 for t, v in styles: | 267 for t, v in styles: |
271 new_text.append(u'[{tag}{value}]'.format( | 268 new_text.append('[{tag}{value}]'.format( |
272 tag = t, | 269 tag = t, |
273 value = u'={}'.format(v) if v else u'')) | 270 value = '={}'.format(v) if v else '')) |
274 else: | 271 else: |
275 new_text.append(c) | 272 new_text.append(c) |
276 if current_wid.text: | 273 if current_wid.text: |
277 # we may have a remaining widget after the parsing | 274 # we may have a remaining widget after the parsing |
278 close_styles = [] | 275 close_styles = [] |
279 for t, v in reversed(styles): | 276 for t, v in reversed(styles): |
280 close_styles.append(u'[/{}]'.format(t)) | 277 close_styles.append('[/{}]'.format(t)) |
281 current_wid.text = u''.join(close_styles) | 278 current_wid.text = ''.join(close_styles) |
282 self.add_widget(current_wid) | 279 self.add_widget(current_wid) |
283 log.debug(u"new widget: {}".format(current_wid.text)) | 280 log.debug("new widget: {}".format(current_wid.text)) |
284 else: | 281 else: |
285 # non Label widgets, we just add them | 282 # non Label widgets, we just add them |
286 self.add_widget(child) | 283 self.add_widget(child) |
287 self.splitted = True | 284 self.splitted = True |
288 log.debug(u"split OK") | 285 log.debug("split OK") |
289 | 286 |
290 # we now set the content width | 287 # we now set the content width |
291 # FIXME: for now we just use the full width | 288 # FIXME: for now we just use the full width |
292 self.content_width = width | 289 self.content_width = width |
293 | 290 |
301 @param e(ET.Element): element to parse | 298 @param e(ET.Element): element to parse |
302 """ | 299 """ |
303 try: | 300 try: |
304 method = getattr(self, "xhtml_{}".format(e.tag)) | 301 method = getattr(self, "xhtml_{}".format(e.tag)) |
305 except AttributeError: | 302 except AttributeError: |
306 log.warning(u"Unhandled XHTML tag: {}".format(e.tag)) | 303 log.warning("Unhandled XHTML tag: {}".format(e.tag)) |
307 method = self.xhtml_generic | 304 method = self.xhtml_generic |
308 method(e) | 305 method(e) |
309 | 306 |
310 def _addStyle(self, tag, value=None, append_to_list=True): | 307 def _addStyle(self, tag, value=None, append_to_list=True): |
311 """add a markup style to label | 308 """add a markup style to label |
315 @param append_to_list(bool): if True style we be added to self.styles | 312 @param append_to_list(bool): if True style we be added to self.styles |
316 self.styles is needed to keep track of styles to remove | 313 self.styles is needed to keep track of styles to remove |
317 should most probably be set to True | 314 should most probably be set to True |
318 """ | 315 """ |
319 label = self._getLabel() | 316 label = self._getLabel() |
320 label.text += u'[{tag}{value}]'.format( | 317 label.text += '[{tag}{value}]'.format( |
321 tag = tag, | 318 tag = tag, |
322 value = u'={}'.format(value) if value else '' | 319 value = '={}'.format(value) if value else '' |
323 ) | 320 ) |
324 if append_to_list: | 321 if append_to_list: |
325 self.styles.append((tag, value)) | 322 self.styles.append((tag, value)) |
326 | 323 |
327 def _removeStyle(self, tag, remove_from_list=True): | 324 def _removeStyle(self, tag, remove_from_list=True): |
330 @param tag(unicode): markup tag to remove | 327 @param tag(unicode): markup tag to remove |
331 @param remove_from_list(bool): if True, remove from self.styles too | 328 @param remove_from_list(bool): if True, remove from self.styles too |
332 should most probably be set to True | 329 should most probably be set to True |
333 """ | 330 """ |
334 label = self._getLabel() | 331 label = self._getLabel() |
335 label.text += u'[/{tag}]'.format( | 332 label.text += '[/{tag}]'.format( |
336 tag = tag | 333 tag = tag |
337 ) | 334 ) |
338 if remove_from_list: | 335 if remove_from_list: |
339 for rev_idx, style in enumerate(reversed(self.styles)): | 336 for rev_idx, style in enumerate(reversed(self.styles)): |
340 if style[0] == tag: | 337 if style[0] == tag: |
380 | 377 |
381 self._css_styles will be created and contained markup styles added by this method | 378 self._css_styles will be created and contained markup styles added by this method |
382 @param e(ET.Element): element which may have a "style" attribute | 379 @param e(ET.Element): element which may have a "style" attribute |
383 """ | 380 """ |
384 styles_limit = len(self.styles) | 381 styles_limit = len(self.styles) |
385 styles = e.attrib['style'].split(u';') | 382 styles = e.attrib['style'].split(';') |
386 for style in styles: | 383 for style in styles: |
387 try: | 384 try: |
388 prop, value = style.split(u':') | 385 prop, value = style.split(':') |
389 except ValueError: | 386 except ValueError: |
390 log.warning(u"can't parse style: {}".format(style)) | 387 log.warning("can't parse style: {}".format(style)) |
391 continue | 388 continue |
392 prop = prop.strip().replace(u'-', '_') | 389 prop = prop.strip().replace('-', '_') |
393 value = value.strip() | 390 value = value.strip() |
394 try: | 391 try: |
395 method = getattr(self, "css_{}".format(prop)) | 392 method = getattr(self, "css_{}".format(prop)) |
396 except AttributeError: | 393 except AttributeError: |
397 log.warning(u"Unhandled CSS: {}".format(prop)) | 394 log.warning("Unhandled CSS: {}".format(prop)) |
398 else: | 395 else: |
399 method(e, value) | 396 method(e, value) |
400 self._css_styles = self.styles[styles_limit:] | 397 self._css_styles = self.styles[styles_limit:] |
401 | 398 |
402 def _closeCSS(self): | 399 def _closeCSS(self): |
417 @param style(bool): if True handle style attribute (CSS) | 414 @param style(bool): if True handle style attribute (CSS) |
418 @param markup(tuple[unicode, (unicode, None)], None): kivy markup to use | 415 @param markup(tuple[unicode, (unicode, None)], None): kivy markup to use |
419 """ | 416 """ |
420 # we first add markup and CSS style | 417 # we first add markup and CSS style |
421 if markup is not None: | 418 if markup is not None: |
422 if isinstance(markup, basestring): | 419 if isinstance(markup, str): |
423 tag, value = markup, None | 420 tag, value = markup, None |
424 else: | 421 else: |
425 tag, value = markup | 422 tag, value = markup |
426 self._addStyle(tag, value) | 423 self._addStyle(tag, value) |
427 style_ = 'style' in elem.attrib and style | 424 style_ = 'style' in elem.attrib and style |
458 | 455 |
459 def xhtml_img(self, elem): | 456 def xhtml_img(self, elem): |
460 try: | 457 try: |
461 src = elem.attrib['src'] | 458 src = elem.attrib['src'] |
462 except KeyError: | 459 except KeyError: |
463 log.warning(u"<img> element without src: {}".format(ET.tostring(elem))) | 460 log.warning("<img> element without src: {}".format(ET.tostring(elem))) |
464 return | 461 return |
465 try: | 462 try: |
466 target_height = int(elem.get(u'height', 0)) | 463 target_height = int(elem.get('height', 0)) |
467 except ValueError: | 464 except ValueError: |
468 log.warning(u"Can't parse image height: {}".format(elem.get(u'height'))) | 465 log.warning("Can't parse image height: {}".format(elem.get('height'))) |
469 target_height = 0 | 466 target_height = 0 |
470 try: | 467 try: |
471 target_width = int(elem.get(u'width', 0)) | 468 target_width = int(elem.get('width', 0)) |
472 except ValueError: | 469 except ValueError: |
473 log.warning(u"Can't parse image width: {}".format(elem.get(u'width'))) | 470 log.warning("Can't parse image width: {}".format(elem.get('width'))) |
474 target_width = 0 | 471 target_width = 0 |
475 | 472 |
476 img = SimpleXHTMLWidgetImage(source=src, target_height=target_height, target_width=target_width) | 473 img = SimpleXHTMLWidgetImage(source=src, target_height=target_height, target_width=target_width) |
477 self.current_wid = img | 474 self.current_wid = img |
478 self.add_widget(img) | 475 self.add_widget(img) |
489 self.xhtml_generic(elem, markup='b') | 486 self.xhtml_generic(elem, markup='b') |
490 | 487 |
491 # methods handling CSS properties | 488 # methods handling CSS properties |
492 | 489 |
493 def css_color(self, elem, value): | 490 def css_color(self, elem, value): |
494 self._addStyle(u"color", css_color.parse(value)) | 491 self._addStyle("color", css_color.parse(value)) |
495 | 492 |
496 def css_text_decoration(self, elem, value): | 493 def css_text_decoration(self, elem, value): |
497 if value == u'underline': | 494 if value == 'underline': |
498 self._addStyle('u') | 495 self._addStyle('u') |
499 elif value == u'line-through': | 496 elif value == 'line-through': |
500 self._addStyle('s') | 497 self._addStyle('s') |
501 else: | 498 else: |
502 log.warning(u"unhandled text decoration: {}".format(value)) | 499 log.warning("unhandled text decoration: {}".format(value)) |