Mercurial > libervia-desktop-kivy
annotate cagou/core/simple_xhtml.py @ 335:597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
when an `aesgcm` scheme is found, the file is download/decrypted using fileDownload, then
opened.
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 20 Dec 2019 12:29:37 +0100 |
parents | 5868a5575e01 |
children | 83697218b9b2 |
rev | line source |
---|---|
22 | 1 #!/usr/bin/python |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # Cagou: desktop/mobile frontend for Salut à Toi XMPP client | |
282 | 5 # Copyright (C) 2016-2019 Jérôme Poisson (goffi@goffi.org) |
22 | 6 |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 | |
325 | 21 import webbrowser |
22 from xml.etree import ElementTree as ET | |
335
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
23 from urllib.parse import urlparse |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
24 from pathlib import Path |
57 | 25 from kivy.uix.stacklayout import StackLayout |
26 from kivy.uix.label import Label | |
27 from kivy.utils import escape_markup | |
325 | 28 from kivy.metrics import sp, dp |
22 | 29 from kivy import properties |
325 | 30 from sat.core import log as logging |
335
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
31 from sat.tools.common import files_utils |
106
9909ed7a7a20
moved SimpleXHTMLWidget to a dedicated module
Goffi <goffi@goffi.org>
parents:
105
diff
changeset
|
32 from sat_frontends.tools import css_color, strings as sat_strings |
335
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
33 from sat_frontends.quick_frontend.quick_widgets import QuickWidget |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
34 from cagou import G |
106
9909ed7a7a20
moved SimpleXHTMLWidget to a dedicated module
Goffi <goffi@goffi.org>
parents:
105
diff
changeset
|
35 from cagou.core.image import AsyncImage |
325 | 36 from cagou.core.constants import Const as C |
37 | |
38 | |
39 log = logging.getLogger(__name__) | |
22 | 40 |
41 | |
312 | 42 class Escape(str): |
57 | 43 """Class used to mark that a message need to be escaped""" |
44 | |
45 | |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
46 class SimpleXHTMLWidgetEscapedText(Label): |
100
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
47 |
185
ab3f5173ef5c
chat, simple XHTML: font size adjustement
Goffi <goffi@goffi.org>
parents:
126
diff
changeset
|
48 def on_parent(self, instance, parent): |
288
44752e8031f8
simple XHTML: fixed crash when parent is set to None + fixed bold restoration when escaped message is modified
Goffi <goffi@goffi.org>
parents:
284
diff
changeset
|
49 if parent is not None: |
44752e8031f8
simple XHTML: fixed crash when parent is set to None + fixed bold restoration when escaped message is modified
Goffi <goffi@goffi.org>
parents:
284
diff
changeset
|
50 self.font_size = parent.font_size |
185
ab3f5173ef5c
chat, simple XHTML: font size adjustement
Goffi <goffi@goffi.org>
parents:
126
diff
changeset
|
51 |
100
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
52 def _addUrlMarkup(self, text): |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
53 text_elts = [] |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
54 idx = 0 |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
55 links = 0 |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
56 while True: |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
57 m = sat_strings.RE_URL.search(text[idx:]) |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
58 if m is not None: |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
59 text_elts.append(escape_markup(m.string[0:m.start()])) |
312 | 60 link_key = 'link_' + str(links) |
100
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
61 url = m.group() |
325 | 62 escaped_url = escape_markup(url) |
63 text_elts.append( | |
64 f'[color=5500ff][ref={link_key}]{escaped_url}[/ref][/color]') | |
100
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
65 if not links: |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
66 self.ref_urls = {link_key: url} |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
67 else: |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
68 self.ref_urls[link_key] = url |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
69 links += 1 |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
70 idx += m.end() |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
71 else: |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
72 if links: |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
73 text_elts.append(escape_markup(text[idx:])) |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
74 self.markup = True |
312 | 75 self.text = ''.join(text_elts) |
100
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
76 break |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
77 |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
78 def on_text(self, instance, text): |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
79 # do NOT call the method if self.markup is set |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
80 # this would result in infinite loop (because self.text |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
81 # is changed if an URL is found, and in this case markup too) |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
82 if text and not self.markup: |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
83 self._addUrlMarkup(text) |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
84 |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
85 def on_ref_press(self, ref): |
d7447c585603
chat: added url detection on text messages
Goffi <goffi@goffi.org>
parents:
98
diff
changeset
|
86 url = self.ref_urls[ref] |
335
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
87 parsed_url = urlparse(url) |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
88 if parsed_url.scheme == "aesgcm": |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
89 # aesgcm files need to be decrypted first |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
90 # so we download them before opening |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
91 name = Path(parsed_url.path).name.strip() or "unnamed_file" |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
92 log.info(f"downloading/decrypting file {name!r}") |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
93 dest_path = files_utils.get_unique_name(Path(G.host.downloads_dir)/name) |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
94 quick_widget = G.host.getAncestorWidget(self, QuickWidget) |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
95 if quick_widget is None: |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
96 log.error("Can't find ancestor QuickWidget") |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
97 return |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
98 profile = quick_widget.profile |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
99 G.host.bridge.fileDownloadComplete( |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
100 url, |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
101 str(dest_path), |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
102 '', |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
103 profile, |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
104 callback=webbrowser.open, |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
105 errback=G.host.errback |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
106 ) |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
107 else: |
597cc207c8e7
core (simple_xhtml): handle `aesgcm` schemes:
Goffi <goffi@goffi.org>
parents:
325
diff
changeset
|
108 webbrowser.open(url) |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
109 |
59 | 110 |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
111 class SimpleXHTMLWidgetText(Label): |
185
ab3f5173ef5c
chat, simple XHTML: font size adjustement
Goffi <goffi@goffi.org>
parents:
126
diff
changeset
|
112 |
ab3f5173ef5c
chat, simple XHTML: font size adjustement
Goffi <goffi@goffi.org>
parents:
126
diff
changeset
|
113 def on_parent(self, instance, parent): |
325 | 114 if parent is not None: |
115 self.font_size = parent.font_size | |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
116 |
59 | 117 |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
118 class SimpleXHTMLWidgetImage(AsyncImage): |
59 | 119 # following properties are desired height/width |
120 # i.e. the ones specified in height/width attributes of <img> | |
121 # (or wanted for whatever reason) | |
325 | 122 # set to None to ignore them |
123 target_height = properties.NumericProperty(allownone=True) | |
124 target_width = properties.NumericProperty(allownone=True) | |
59 | 125 |
325 | 126 def __init__(self, **kwargs): |
127 # best calculated size | |
128 self._best_width = self._best_height = 100 | |
129 super().__init__(**kwargs) | |
59 | 130 |
325 | 131 def on_texture(self, instance, texture): |
132 """Adapt the size according to max size and target_*""" | |
133 if texture is None: | |
134 return | |
135 max_width, max_height = dp(C.IMG_MAX_WIDTH), dp(C.IMG_MAX_HEIGHT) | |
136 width, height = texture.size | |
137 if self.target_width: | |
138 width = min(width, self.target_width) | |
139 if width > max_width: | |
140 width = C.IMG_MAX_WIDTH | |
59 | 141 |
325 | 142 height = width / self.image_ratio |
143 | |
144 if self.target_height: | |
145 height = min(height, self.target_height) | |
146 | |
147 if height > max_height: | |
148 height = max_height | |
149 width = height * self.image_ratio | |
150 | |
151 self.width, self.height = self._best_width, self._best_height = width, height | |
59 | 152 |
153 def on_parent(self, instance, parent): | |
154 if parent is not None: | |
325 | 155 parent.bind(width=self.on_parent_width) |
156 | |
157 def on_parent_width(self, instance, width): | |
158 if self._best_width > width: | |
159 self.width = width | |
160 self.height = width / self.image_ratio | |
161 else: | |
162 self.width, self.height = self._best_width, self._best_height | |
59 | 163 |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
164 |
57 | 165 class SimpleXHTMLWidget(StackLayout): |
166 """widget handling simple XHTML parsing""" | |
167 xhtml = properties.StringProperty() | |
168 color = properties.ListProperty([1, 1, 1, 1]) | |
169 # XXX: bold is only used for escaped text | |
170 bold = properties.BooleanProperty(False) | |
185
ab3f5173ef5c
chat, simple XHTML: font size adjustement
Goffi <goffi@goffi.org>
parents:
126
diff
changeset
|
171 font_size = properties.NumericProperty(sp(14)) |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
172 |
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
173 # text/XHTML input |
57 | 174 |
175 def on_xhtml(self, instance, xhtml): | |
176 """parse xhtml and set content accordingly | |
177 | |
288
44752e8031f8
simple XHTML: fixed crash when parent is set to None + fixed bold restoration when escaped message is modified
Goffi <goffi@goffi.org>
parents:
284
diff
changeset
|
178 if xhtml is an instance of Escape, a Label with no markup will be used |
57 | 179 """ |
180 self.clear_widgets() | |
181 if isinstance(xhtml, Escape): | |
288
44752e8031f8
simple XHTML: fixed crash when parent is set to None + fixed bold restoration when escaped message is modified
Goffi <goffi@goffi.org>
parents:
284
diff
changeset
|
182 label = SimpleXHTMLWidgetEscapedText( |
44752e8031f8
simple XHTML: fixed crash when parent is set to None + fixed bold restoration when escaped message is modified
Goffi <goffi@goffi.org>
parents:
284
diff
changeset
|
183 text=xhtml, color=self.color, bold=self.bold) |
325 | 184 self.bind(font_size=label.setter('font_size')) |
57 | 185 self.bind(color=label.setter('color')) |
186 self.bind(bold=label.setter('bold')) | |
187 self.add_widget(label) | |
188 else: | |
325 | 189 xhtml = ET.fromstring(xhtml.encode()) |
57 | 190 self.current_wid = None |
191 self.styles = [] | |
192 self._callParseMethod(xhtml) | |
325 | 193 if len(self.children) > 1: |
194 self._do_split_labels() | |
57 | 195 |
196 def escape(self, text): | |
197 """mark that a text need to be escaped (i.e. no markup)""" | |
198 return Escape(text) | |
199 | |
325 | 200 def _do_split_labels(self): |
201 """Split labels so their content can flow with images""" | |
202 # XXX: to make things easier, we split labels in words | |
203 log.debug("labels splitting start") | |
204 children = self.children[::-1] | |
205 self.clear_widgets() | |
206 for child in children: | |
207 if isinstance(child, Label): | |
208 log.debug("label before split: {}".format(child.text)) | |
209 styles = [] | |
210 tag = False | |
211 new_text = [] | |
212 current_tag = [] | |
213 current_value = [] | |
214 current_wid = self._createText() | |
215 value = False | |
216 close = False | |
217 # we will parse the text and create a new widget | |
218 # on each new word (actually each space) | |
219 # FIXME: handle '\n' and other white chars | |
220 for c in child.text: | |
221 if tag: | |
222 # we are parsing a markup tag | |
223 if c == ']': | |
224 current_tag_s = ''.join(current_tag) | |
225 current_style = (current_tag_s, ''.join(current_value)) | |
226 if close: | |
227 for idx, s in enumerate(reversed(styles)): | |
228 if s[0] == current_tag_s: | |
229 del styles[len(styles) - idx - 1] | |
230 break | |
231 else: | |
232 styles.append(current_style) | |
233 current_tag = [] | |
234 current_value = [] | |
235 tag = False | |
236 value = False | |
237 close = False | |
238 elif c == '/': | |
239 close = True | |
240 elif c == '=': | |
241 value = True | |
242 elif value: | |
243 current_value.append(c) | |
244 else: | |
245 current_tag.append(c) | |
246 new_text.append(c) | |
247 else: | |
248 # we are parsing regular text | |
249 if c == '[': | |
250 new_text.append(c) | |
251 tag = True | |
252 elif c == ' ': | |
253 # new word, we do a new widget | |
254 new_text.append(' ') | |
255 for t, v in reversed(styles): | |
256 new_text.append('[/{}]'.format(t)) | |
257 current_wid.text = ''.join(new_text) | |
258 new_text = [] | |
259 self.add_widget(current_wid) | |
260 log.debug("new widget: {}".format(current_wid.text)) | |
261 current_wid = self._createText() | |
262 for t, v in styles: | |
263 new_text.append('[{tag}{value}]'.format( | |
264 tag = t, | |
265 value = '={}'.format(v) if v else '')) | |
266 else: | |
267 new_text.append(c) | |
268 if current_wid.text: | |
269 # we may have a remaining widget after the parsing | |
270 close_styles = [] | |
271 for t, v in reversed(styles): | |
272 close_styles.append('[/{}]'.format(t)) | |
273 current_wid.text = ''.join(close_styles) | |
274 self.add_widget(current_wid) | |
275 log.debug("new widget: {}".format(current_wid.text)) | |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
276 else: |
325 | 277 # non Label widgets, we just add them |
278 self.add_widget(child) | |
279 self.splitted = True | |
280 log.debug("split OK") | |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
281 |
57 | 282 # XHTML parsing methods |
283 | |
284 def _callParseMethod(self, e): | |
325 | 285 """Call the suitable method to parse the element |
57 | 286 |
287 self.xhtml_[tag] will be called if it exists, else | |
288 self.xhtml_generic will be used | |
289 @param e(ET.Element): element to parse | |
290 """ | |
291 try: | |
325 | 292 method = getattr(self, f"xhtml_{e.tag}") |
57 | 293 except AttributeError: |
325 | 294 log.warning(f"Unhandled XHTML tag: {e.tag}") |
57 | 295 method = self.xhtml_generic |
296 method(e) | |
297 | |
298 def _addStyle(self, tag, value=None, append_to_list=True): | |
299 """add a markup style to label | |
300 | |
301 @param tag(unicode): markup tag | |
302 @param value(unicode): markup value if suitable | |
303 @param append_to_list(bool): if True style we be added to self.styles | |
304 self.styles is needed to keep track of styles to remove | |
305 should most probably be set to True | |
306 """ | |
307 label = self._getLabel() | |
312 | 308 label.text += '[{tag}{value}]'.format( |
57 | 309 tag = tag, |
312 | 310 value = '={}'.format(value) if value else '' |
57 | 311 ) |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
312 if append_to_list: |
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
313 self.styles.append((tag, value)) |
57 | 314 |
315 def _removeStyle(self, tag, remove_from_list=True): | |
316 """remove a markup style from the label | |
317 | |
318 @param tag(unicode): markup tag to remove | |
319 @param remove_from_list(bool): if True, remove from self.styles too | |
320 should most probably be set to True | |
321 """ | |
322 label = self._getLabel() | |
312 | 323 label.text += '[/{tag}]'.format( |
57 | 324 tag = tag |
325 ) | |
326 if remove_from_list: | |
327 for rev_idx, style in enumerate(reversed(self.styles)): | |
328 if style[0] == tag: | |
329 tag_idx = len(self.styles) - 1 - rev_idx | |
330 del self.styles[tag_idx] | |
331 break | |
332 | |
333 def _getLabel(self): | |
334 """get current Label if it exists, or create a new one""" | |
335 if not isinstance(self.current_wid, Label): | |
336 self._addLabel() | |
337 return self.current_wid | |
338 | |
339 def _addLabel(self): | |
340 """add a new Label | |
341 | |
342 current styles will be closed and reopened if needed | |
343 """ | |
344 self._closeLabel() | |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
345 self.current_wid = self._createText() |
57 | 346 for tag, value in self.styles: |
347 self._addStyle(tag, value, append_to_list=False) | |
348 self.add_widget(self.current_wid) | |
349 | |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
350 def _createText(self): |
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
351 label = SimpleXHTMLWidgetText(color=self.color, markup=True) |
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
352 self.bind(color=label.setter('color')) |
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
353 label.bind(texture_size=label.setter('size')) |
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
354 return label |
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
355 |
57 | 356 def _closeLabel(self): |
357 """close current style tags in current label | |
358 | |
359 needed when you change label to keep style between | |
360 different widgets | |
361 """ | |
362 if isinstance(self.current_wid, Label): | |
363 for tag, value in reversed(self.styles): | |
364 self._removeStyle(tag, remove_from_list=False) | |
365 | |
366 def _parseCSS(self, e): | |
367 """parse CSS found in "style" attribute of element | |
368 | |
369 self._css_styles will be created and contained markup styles added by this method | |
370 @param e(ET.Element): element which may have a "style" attribute | |
371 """ | |
372 styles_limit = len(self.styles) | |
312 | 373 styles = e.attrib['style'].split(';') |
57 | 374 for style in styles: |
375 try: | |
312 | 376 prop, value = style.split(':') |
57 | 377 except ValueError: |
325 | 378 log.warning(f"can't parse style: {style}") |
57 | 379 continue |
312 | 380 prop = prop.strip().replace('-', '_') |
57 | 381 value = value.strip() |
382 try: | |
325 | 383 method = getattr(self, f"css_{prop}") |
57 | 384 except AttributeError: |
325 | 385 log.warning(f"Unhandled CSS: {prop}") |
57 | 386 else: |
387 method(e, value) | |
388 self._css_styles = self.styles[styles_limit:] | |
389 | |
390 def _closeCSS(self): | |
391 """removed CSS styles | |
392 | |
393 styles in self._css_styles will be removed | |
394 and the attribute will be deleted | |
395 """ | |
284 | 396 for tag, __ in reversed(self._css_styles): |
57 | 397 self._removeStyle(tag) |
398 del self._css_styles | |
399 | |
400 def xhtml_generic(self, elem, style=True, markup=None): | |
325 | 401 """Generic method for adding HTML elements |
57 | 402 |
403 this method handle content, style and children parsing | |
404 @param elem(ET.Element): element to add | |
405 @param style(bool): if True handle style attribute (CSS) | |
406 @param markup(tuple[unicode, (unicode, None)], None): kivy markup to use | |
407 """ | |
408 # we first add markup and CSS style | |
409 if markup is not None: | |
312 | 410 if isinstance(markup, str): |
57 | 411 tag, value = markup, None |
412 else: | |
413 tag, value = markup | |
414 self._addStyle(tag, value) | |
415 style_ = 'style' in elem.attrib and style | |
416 if style_: | |
417 self._parseCSS(elem) | |
418 | |
419 # then content | |
420 if elem.text: | |
421 self._getLabel().text += escape_markup(elem.text) | |
422 | |
423 # we parse the children | |
424 for child in elem: | |
425 self._callParseMethod(child) | |
426 | |
427 # closing CSS style and markup | |
428 if style_: | |
429 self._closeCSS() | |
430 if markup is not None: | |
431 self._removeStyle(tag) | |
432 | |
433 # and the tail, which is regular text | |
434 if elem.tail: | |
435 self._getLabel().text += escape_markup(elem.tail) | |
436 | |
437 # method handling XHTML elements | |
438 | |
439 def xhtml_br(self, elem): | |
440 label = self._getLabel() | |
441 label.text+='\n' | |
442 self.xhtml_generic(style=False) | |
443 | |
444 def xhtml_em(self, elem): | |
445 self.xhtml_generic(elem, markup='i') | |
446 | |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
447 def xhtml_img(self, elem): |
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
448 try: |
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
449 src = elem.attrib['src'] |
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
450 except KeyError: |
312 | 451 log.warning("<img> element without src: {}".format(ET.tostring(elem))) |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
452 return |
59 | 453 try: |
312 | 454 target_height = int(elem.get('height', 0)) |
59 | 455 except ValueError: |
325 | 456 log.warning(f"Can't parse image height: {elem.get('height')}") |
457 target_height = None | |
59 | 458 try: |
312 | 459 target_width = int(elem.get('width', 0)) |
59 | 460 except ValueError: |
325 | 461 log.warning(f"Can't parse image width: {elem.get('width')}") |
462 target_width = None | |
59 | 463 |
464 img = SimpleXHTMLWidgetImage(source=src, target_height=target_height, target_width=target_width) | |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
465 self.current_wid = img |
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
466 self.add_widget(img) |
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
467 |
57 | 468 def xhtml_p(self, elem): |
58
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
469 if isinstance(self.current_wid, Label): |
7aa2ffff9067
chat: <img/> tag handling first draft:
Goffi <goffi@goffi.org>
parents:
57
diff
changeset
|
470 self.current_wid.text+="\n\n" |
57 | 471 self.xhtml_generic(elem) |
472 | |
473 def xhtml_span(self, elem): | |
474 self.xhtml_generic(elem) | |
475 | |
476 def xhtml_strong(self, elem): | |
477 self.xhtml_generic(elem, markup='b') | |
478 | |
479 # methods handling CSS properties | |
480 | |
481 def css_color(self, elem, value): | |
312 | 482 self._addStyle("color", css_color.parse(value)) |
57 | 483 |
484 def css_text_decoration(self, elem, value): | |
312 | 485 if value == 'underline': |
299
86b1cd8121dd
core (simple_xhtml): activated underline and line-through which are now available
Goffi <goffi@goffi.org>
parents:
288
diff
changeset
|
486 self._addStyle('u') |
312 | 487 elif value == 'line-through': |
299
86b1cd8121dd
core (simple_xhtml): activated underline and line-through which are now available
Goffi <goffi@goffi.org>
parents:
288
diff
changeset
|
488 self._addStyle('s') |
57 | 489 else: |
312 | 490 log.warning("unhandled text decoration: {}".format(value)) |