Mercurial > repos > shellac > guppy_basecaller
comparison env/lib/python3.7/site-packages/lxml/html/clean.py @ 0:26e78fe6e8c4 draft
"planemo upload commit c699937486c35866861690329de38ec1a5d9f783"
author | shellac |
---|---|
date | Sat, 02 May 2020 07:14:21 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:26e78fe6e8c4 |
---|---|
1 # cython: language_level=2 | |
2 | |
3 """A cleanup tool for HTML. | |
4 | |
5 Removes unwanted tags and content. See the `Cleaner` class for | |
6 details. | |
7 """ | |
8 | |
9 from __future__ import absolute_import | |
10 | |
11 import re | |
12 import copy | |
13 try: | |
14 from urlparse import urlsplit | |
15 from urllib import unquote_plus | |
16 except ImportError: | |
17 # Python 3 | |
18 from urllib.parse import urlsplit, unquote_plus | |
19 from lxml import etree | |
20 from lxml.html import defs | |
21 from lxml.html import fromstring, XHTML_NAMESPACE | |
22 from lxml.html import xhtml_to_html, _transform_result | |
23 | |
24 try: | |
25 unichr | |
26 except NameError: | |
27 # Python 3 | |
28 unichr = chr | |
29 try: | |
30 unicode | |
31 except NameError: | |
32 # Python 3 | |
33 unicode = str | |
34 try: | |
35 basestring | |
36 except NameError: | |
37 basestring = (str, bytes) | |
38 | |
39 | |
40 __all__ = ['clean_html', 'clean', 'Cleaner', 'autolink', 'autolink_html', | |
41 'word_break', 'word_break_html'] | |
42 | |
43 # Look at http://code.sixapart.com/trac/livejournal/browser/trunk/cgi-bin/cleanhtml.pl | |
44 # Particularly the CSS cleaning; most of the tag cleaning is integrated now | |
45 # I have multiple kinds of schemes searched; but should schemes be | |
46 # whitelisted instead? | |
47 # max height? | |
48 # remove images? Also in CSS? background attribute? | |
49 # Some way to whitelist object, iframe, etc (e.g., if you want to | |
50 # allow *just* embedded YouTube movies) | |
51 # Log what was deleted and why? | |
52 # style="behavior: ..." might be bad in IE? | |
53 # Should we have something for just <meta http-equiv>? That's the worst of the | |
54 # metas. | |
55 # UTF-7 detections? Example: | |
56 # <HEAD><META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=UTF-7"> </HEAD>+ADw-SCRIPT+AD4-alert('XSS');+ADw-/SCRIPT+AD4- | |
57 # you don't always have to have the charset set, if the page has no charset | |
58 # and there's UTF7-like code in it. | |
59 # Look at these tests: http://htmlpurifier.org/live/smoketests/xssAttacks.php | |
60 | |
61 | |
62 # This is an IE-specific construct you can have in a stylesheet to | |
63 # run some Javascript: | |
64 _css_javascript_re = re.compile( | |
65 r'expression\s*\(.*?\)', re.S|re.I) | |
66 | |
67 # Do I have to worry about @\nimport? | |
68 _css_import_re = re.compile( | |
69 r'@\s*import', re.I) | |
70 | |
71 # All kinds of schemes besides just javascript: that can cause | |
72 # execution: | |
73 _is_image_dataurl = re.compile( | |
74 r'^data:image/.+;base64', re.I).search | |
75 _is_possibly_malicious_scheme = re.compile( | |
76 r'(?:javascript|jscript|livescript|vbscript|data|about|mocha):', | |
77 re.I).search | |
78 def _is_javascript_scheme(s): | |
79 if _is_image_dataurl(s): | |
80 return None | |
81 return _is_possibly_malicious_scheme(s) | |
82 | |
83 _substitute_whitespace = re.compile(r'[\s\x00-\x08\x0B\x0C\x0E-\x19]+').sub | |
84 # FIXME: should data: be blocked? | |
85 | |
86 # FIXME: check against: http://msdn2.microsoft.com/en-us/library/ms537512.aspx | |
87 _conditional_comment_re = re.compile( | |
88 r'\[if[\s\n\r]+.*?][\s\n\r]*>', re.I|re.S) | |
89 | |
90 _find_styled_elements = etree.XPath( | |
91 "descendant-or-self::*[@style]") | |
92 | |
93 _find_external_links = etree.XPath( | |
94 ("descendant-or-self::a [normalize-space(@href) and substring(normalize-space(@href),1,1) != '#'] |" | |
95 "descendant-or-self::x:a[normalize-space(@href) and substring(normalize-space(@href),1,1) != '#']"), | |
96 namespaces={'x':XHTML_NAMESPACE}) | |
97 | |
98 | |
99 class Cleaner(object): | |
100 """ | |
101 Instances cleans the document of each of the possible offending | |
102 elements. The cleaning is controlled by attributes; you can | |
103 override attributes in a subclass, or set them in the constructor. | |
104 | |
105 ``scripts``: | |
106 Removes any ``<script>`` tags. | |
107 | |
108 ``javascript``: | |
109 Removes any Javascript, like an ``onclick`` attribute. Also removes stylesheets | |
110 as they could contain Javascript. | |
111 | |
112 ``comments``: | |
113 Removes any comments. | |
114 | |
115 ``style``: | |
116 Removes any style tags. | |
117 | |
118 ``inline_style`` | |
119 Removes any style attributes. Defaults to the value of the ``style`` option. | |
120 | |
121 ``links``: | |
122 Removes any ``<link>`` tags | |
123 | |
124 ``meta``: | |
125 Removes any ``<meta>`` tags | |
126 | |
127 ``page_structure``: | |
128 Structural parts of a page: ``<head>``, ``<html>``, ``<title>``. | |
129 | |
130 ``processing_instructions``: | |
131 Removes any processing instructions. | |
132 | |
133 ``embedded``: | |
134 Removes any embedded objects (flash, iframes) | |
135 | |
136 ``frames``: | |
137 Removes any frame-related tags | |
138 | |
139 ``forms``: | |
140 Removes any form tags | |
141 | |
142 ``annoying_tags``: | |
143 Tags that aren't *wrong*, but are annoying. ``<blink>`` and ``<marquee>`` | |
144 | |
145 ``remove_tags``: | |
146 A list of tags to remove. Only the tags will be removed, | |
147 their content will get pulled up into the parent tag. | |
148 | |
149 ``kill_tags``: | |
150 A list of tags to kill. Killing also removes the tag's content, | |
151 i.e. the whole subtree, not just the tag itself. | |
152 | |
153 ``allow_tags``: | |
154 A list of tags to include (default include all). | |
155 | |
156 ``remove_unknown_tags``: | |
157 Remove any tags that aren't standard parts of HTML. | |
158 | |
159 ``safe_attrs_only``: | |
160 If true, only include 'safe' attributes (specifically the list | |
161 from the feedparser HTML sanitisation web site). | |
162 | |
163 ``safe_attrs``: | |
164 A set of attribute names to override the default list of attributes | |
165 considered 'safe' (when safe_attrs_only=True). | |
166 | |
167 ``add_nofollow``: | |
168 If true, then any <a> tags will have ``rel="nofollow"`` added to them. | |
169 | |
170 ``host_whitelist``: | |
171 A list or set of hosts that you can use for embedded content | |
172 (for content like ``<object>``, ``<link rel="stylesheet">``, etc). | |
173 You can also implement/override the method | |
174 ``allow_embedded_url(el, url)`` or ``allow_element(el)`` to | |
175 implement more complex rules for what can be embedded. | |
176 Anything that passes this test will be shown, regardless of | |
177 the value of (for instance) ``embedded``. | |
178 | |
179 Note that this parameter might not work as intended if you do not | |
180 make the links absolute before doing the cleaning. | |
181 | |
182 Note that you may also need to set ``whitelist_tags``. | |
183 | |
184 ``whitelist_tags``: | |
185 A set of tags that can be included with ``host_whitelist``. | |
186 The default is ``iframe`` and ``embed``; you may wish to | |
187 include other tags like ``script``, or you may want to | |
188 implement ``allow_embedded_url`` for more control. Set to None to | |
189 include all tags. | |
190 | |
191 This modifies the document *in place*. | |
192 """ | |
193 | |
194 scripts = True | |
195 javascript = True | |
196 comments = True | |
197 style = False | |
198 inline_style = None | |
199 links = True | |
200 meta = True | |
201 page_structure = True | |
202 processing_instructions = True | |
203 embedded = True | |
204 frames = True | |
205 forms = True | |
206 annoying_tags = True | |
207 remove_tags = None | |
208 allow_tags = None | |
209 kill_tags = None | |
210 remove_unknown_tags = True | |
211 safe_attrs_only = True | |
212 safe_attrs = defs.safe_attrs | |
213 add_nofollow = False | |
214 host_whitelist = () | |
215 whitelist_tags = {'iframe', 'embed'} | |
216 | |
217 def __init__(self, **kw): | |
218 for name, value in kw.items(): | |
219 if not hasattr(self, name): | |
220 raise TypeError( | |
221 "Unknown parameter: %s=%r" % (name, value)) | |
222 setattr(self, name, value) | |
223 if self.inline_style is None and 'inline_style' not in kw: | |
224 self.inline_style = self.style | |
225 | |
226 # Used to lookup the primary URL for a given tag that is up for | |
227 # removal: | |
228 _tag_link_attrs = dict( | |
229 script='src', | |
230 link='href', | |
231 # From: http://java.sun.com/j2se/1.4.2/docs/guide/misc/applet.html | |
232 # From what I can tell, both attributes can contain a link: | |
233 applet=['code', 'object'], | |
234 iframe='src', | |
235 embed='src', | |
236 layer='src', | |
237 # FIXME: there doesn't really seem like a general way to figure out what | |
238 # links an <object> tag uses; links often go in <param> tags with values | |
239 # that we don't really know. You'd have to have knowledge about specific | |
240 # kinds of plugins (probably keyed off classid), and match against those. | |
241 ##object=?, | |
242 # FIXME: not looking at the action currently, because it is more complex | |
243 # than than -- if you keep the form, you should keep the form controls. | |
244 ##form='action', | |
245 a='href', | |
246 ) | |
247 | |
248 def __call__(self, doc): | |
249 """ | |
250 Cleans the document. | |
251 """ | |
252 if hasattr(doc, 'getroot'): | |
253 # ElementTree instance, instead of an element | |
254 doc = doc.getroot() | |
255 # convert XHTML to HTML | |
256 xhtml_to_html(doc) | |
257 # Normalize a case that IE treats <image> like <img>, and that | |
258 # can confuse either this step or later steps. | |
259 for el in doc.iter('image'): | |
260 el.tag = 'img' | |
261 if not self.comments: | |
262 # Of course, if we were going to kill comments anyway, we don't | |
263 # need to worry about this | |
264 self.kill_conditional_comments(doc) | |
265 | |
266 kill_tags = set(self.kill_tags or ()) | |
267 remove_tags = set(self.remove_tags or ()) | |
268 allow_tags = set(self.allow_tags or ()) | |
269 | |
270 if self.scripts: | |
271 kill_tags.add('script') | |
272 if self.safe_attrs_only: | |
273 safe_attrs = set(self.safe_attrs) | |
274 for el in doc.iter(etree.Element): | |
275 attrib = el.attrib | |
276 for aname in attrib.keys(): | |
277 if aname not in safe_attrs: | |
278 del attrib[aname] | |
279 if self.javascript: | |
280 if not (self.safe_attrs_only and | |
281 self.safe_attrs == defs.safe_attrs): | |
282 # safe_attrs handles events attributes itself | |
283 for el in doc.iter(etree.Element): | |
284 attrib = el.attrib | |
285 for aname in attrib.keys(): | |
286 if aname.startswith('on'): | |
287 del attrib[aname] | |
288 doc.rewrite_links(self._remove_javascript_link, | |
289 resolve_base_href=False) | |
290 # If we're deleting style then we don't have to remove JS links | |
291 # from styles, otherwise... | |
292 if not self.inline_style: | |
293 for el in _find_styled_elements(doc): | |
294 old = el.get('style') | |
295 new = _css_javascript_re.sub('', old) | |
296 new = _css_import_re.sub('', new) | |
297 if self._has_sneaky_javascript(new): | |
298 # Something tricky is going on... | |
299 del el.attrib['style'] | |
300 elif new != old: | |
301 el.set('style', new) | |
302 if not self.style: | |
303 for el in list(doc.iter('style')): | |
304 if el.get('type', '').lower().strip() == 'text/javascript': | |
305 el.drop_tree() | |
306 continue | |
307 old = el.text or '' | |
308 new = _css_javascript_re.sub('', old) | |
309 # The imported CSS can do anything; we just can't allow: | |
310 new = _css_import_re.sub('', old) | |
311 if self._has_sneaky_javascript(new): | |
312 # Something tricky is going on... | |
313 el.text = '/* deleted */' | |
314 elif new != old: | |
315 el.text = new | |
316 if self.comments or self.processing_instructions: | |
317 # FIXME: why either? I feel like there's some obscure reason | |
318 # because you can put PIs in comments...? But I've already | |
319 # forgotten it | |
320 kill_tags.add(etree.Comment) | |
321 if self.processing_instructions: | |
322 kill_tags.add(etree.ProcessingInstruction) | |
323 if self.style: | |
324 kill_tags.add('style') | |
325 if self.inline_style: | |
326 etree.strip_attributes(doc, 'style') | |
327 if self.links: | |
328 kill_tags.add('link') | |
329 elif self.style or self.javascript: | |
330 # We must get rid of included stylesheets if Javascript is not | |
331 # allowed, as you can put Javascript in them | |
332 for el in list(doc.iter('link')): | |
333 if 'stylesheet' in el.get('rel', '').lower(): | |
334 # Note this kills alternate stylesheets as well | |
335 if not self.allow_element(el): | |
336 el.drop_tree() | |
337 if self.meta: | |
338 kill_tags.add('meta') | |
339 if self.page_structure: | |
340 remove_tags.update(('head', 'html', 'title')) | |
341 if self.embedded: | |
342 # FIXME: is <layer> really embedded? | |
343 # We should get rid of any <param> tags not inside <applet>; | |
344 # These are not really valid anyway. | |
345 for el in list(doc.iter('param')): | |
346 found_parent = False | |
347 parent = el.getparent() | |
348 while parent is not None and parent.tag not in ('applet', 'object'): | |
349 parent = parent.getparent() | |
350 if parent is None: | |
351 el.drop_tree() | |
352 kill_tags.update(('applet',)) | |
353 # The alternate contents that are in an iframe are a good fallback: | |
354 remove_tags.update(('iframe', 'embed', 'layer', 'object', 'param')) | |
355 if self.frames: | |
356 # FIXME: ideally we should look at the frame links, but | |
357 # generally frames don't mix properly with an HTML | |
358 # fragment anyway. | |
359 kill_tags.update(defs.frame_tags) | |
360 if self.forms: | |
361 remove_tags.add('form') | |
362 kill_tags.update(('button', 'input', 'select', 'textarea')) | |
363 if self.annoying_tags: | |
364 remove_tags.update(('blink', 'marquee')) | |
365 | |
366 _remove = [] | |
367 _kill = [] | |
368 for el in doc.iter(): | |
369 if el.tag in kill_tags: | |
370 if self.allow_element(el): | |
371 continue | |
372 _kill.append(el) | |
373 elif el.tag in remove_tags: | |
374 if self.allow_element(el): | |
375 continue | |
376 _remove.append(el) | |
377 | |
378 if _remove and _remove[0] == doc: | |
379 # We have to drop the parent-most tag, which we can't | |
380 # do. Instead we'll rewrite it: | |
381 el = _remove.pop(0) | |
382 el.tag = 'div' | |
383 el.attrib.clear() | |
384 elif _kill and _kill[0] == doc: | |
385 # We have to drop the parent-most element, which we can't | |
386 # do. Instead we'll clear it: | |
387 el = _kill.pop(0) | |
388 if el.tag != 'html': | |
389 el.tag = 'div' | |
390 el.clear() | |
391 | |
392 _kill.reverse() # start with innermost tags | |
393 for el in _kill: | |
394 el.drop_tree() | |
395 for el in _remove: | |
396 el.drop_tag() | |
397 | |
398 if self.remove_unknown_tags: | |
399 if allow_tags: | |
400 raise ValueError( | |
401 "It does not make sense to pass in both allow_tags and remove_unknown_tags") | |
402 allow_tags = set(defs.tags) | |
403 if allow_tags: | |
404 bad = [] | |
405 for el in doc.iter(): | |
406 if el.tag not in allow_tags: | |
407 bad.append(el) | |
408 if bad: | |
409 if bad[0] is doc: | |
410 el = bad.pop(0) | |
411 el.tag = 'div' | |
412 el.attrib.clear() | |
413 for el in bad: | |
414 el.drop_tag() | |
415 if self.add_nofollow: | |
416 for el in _find_external_links(doc): | |
417 if not self.allow_follow(el): | |
418 rel = el.get('rel') | |
419 if rel: | |
420 if ('nofollow' in rel | |
421 and ' nofollow ' in (' %s ' % rel)): | |
422 continue | |
423 rel = '%s nofollow' % rel | |
424 else: | |
425 rel = 'nofollow' | |
426 el.set('rel', rel) | |
427 | |
428 def allow_follow(self, anchor): | |
429 """ | |
430 Override to suppress rel="nofollow" on some anchors. | |
431 """ | |
432 return False | |
433 | |
434 def allow_element(self, el): | |
435 """ | |
436 Decide whether an element is configured to be accepted or rejected. | |
437 | |
438 :param el: an element. | |
439 :return: true to accept the element or false to reject/discard it. | |
440 """ | |
441 if el.tag not in self._tag_link_attrs: | |
442 return False | |
443 attr = self._tag_link_attrs[el.tag] | |
444 if isinstance(attr, (list, tuple)): | |
445 for one_attr in attr: | |
446 url = el.get(one_attr) | |
447 if not url: | |
448 return False | |
449 if not self.allow_embedded_url(el, url): | |
450 return False | |
451 return True | |
452 else: | |
453 url = el.get(attr) | |
454 if not url: | |
455 return False | |
456 return self.allow_embedded_url(el, url) | |
457 | |
458 def allow_embedded_url(self, el, url): | |
459 """ | |
460 Decide whether a URL that was found in an element's attributes or text | |
461 if configured to be accepted or rejected. | |
462 | |
463 :param el: an element. | |
464 :param url: a URL found on the element. | |
465 :return: true to accept the URL and false to reject it. | |
466 """ | |
467 if self.whitelist_tags is not None and el.tag not in self.whitelist_tags: | |
468 return False | |
469 scheme, netloc, path, query, fragment = urlsplit(url) | |
470 netloc = netloc.lower().split(':', 1)[0] | |
471 if scheme not in ('http', 'https'): | |
472 return False | |
473 if netloc in self.host_whitelist: | |
474 return True | |
475 return False | |
476 | |
477 def kill_conditional_comments(self, doc): | |
478 """ | |
479 IE conditional comments basically embed HTML that the parser | |
480 doesn't normally see. We can't allow anything like that, so | |
481 we'll kill any comments that could be conditional. | |
482 """ | |
483 bad = [] | |
484 self._kill_elements( | |
485 doc, lambda el: _conditional_comment_re.search(el.text), | |
486 etree.Comment) | |
487 | |
488 def _kill_elements(self, doc, condition, iterate=None): | |
489 bad = [] | |
490 for el in doc.iter(iterate): | |
491 if condition(el): | |
492 bad.append(el) | |
493 for el in bad: | |
494 el.drop_tree() | |
495 | |
496 def _remove_javascript_link(self, link): | |
497 # links like "j a v a s c r i p t:" might be interpreted in IE | |
498 new = _substitute_whitespace('', unquote_plus(link)) | |
499 if _is_javascript_scheme(new): | |
500 # FIXME: should this be None to delete? | |
501 return '' | |
502 return link | |
503 | |
504 _substitute_comments = re.compile(r'/\*.*?\*/', re.S).sub | |
505 | |
506 def _has_sneaky_javascript(self, style): | |
507 """ | |
508 Depending on the browser, stuff like ``e x p r e s s i o n(...)`` | |
509 can get interpreted, or ``expre/* stuff */ssion(...)``. This | |
510 checks for attempt to do stuff like this. | |
511 | |
512 Typically the response will be to kill the entire style; if you | |
513 have just a bit of Javascript in the style another rule will catch | |
514 that and remove only the Javascript from the style; this catches | |
515 more sneaky attempts. | |
516 """ | |
517 style = self._substitute_comments('', style) | |
518 style = style.replace('\\', '') | |
519 style = _substitute_whitespace('', style) | |
520 style = style.lower() | |
521 if 'javascript:' in style: | |
522 return True | |
523 if 'expression(' in style: | |
524 return True | |
525 return False | |
526 | |
527 def clean_html(self, html): | |
528 result_type = type(html) | |
529 if isinstance(html, basestring): | |
530 doc = fromstring(html) | |
531 else: | |
532 doc = copy.deepcopy(html) | |
533 self(doc) | |
534 return _transform_result(result_type, doc) | |
535 | |
536 clean = Cleaner() | |
537 clean_html = clean.clean_html | |
538 | |
539 ############################################################ | |
540 ## Autolinking | |
541 ############################################################ | |
542 | |
543 _link_regexes = [ | |
544 re.compile(r'(?P<body>https?://(?P<host>[a-z0-9._-]+)(?:/[/\-_.,a-z0-9%&?;=~]*)?(?:\([/\-_.,a-z0-9%&?;=~]*\))?)', re.I), | |
545 # This is conservative, but autolinking can be a bit conservative: | |
546 re.compile(r'mailto:(?P<body>[a-z0-9._-]+@(?P<host>[a-z0-9_.-]+[a-z]))', re.I), | |
547 ] | |
548 | |
549 _avoid_elements = ['textarea', 'pre', 'code', 'head', 'select', 'a'] | |
550 | |
551 _avoid_hosts = [ | |
552 re.compile(r'^localhost', re.I), | |
553 re.compile(r'\bexample\.(?:com|org|net)$', re.I), | |
554 re.compile(r'^127\.0\.0\.1$'), | |
555 ] | |
556 | |
557 _avoid_classes = ['nolink'] | |
558 | |
559 def autolink(el, link_regexes=_link_regexes, | |
560 avoid_elements=_avoid_elements, | |
561 avoid_hosts=_avoid_hosts, | |
562 avoid_classes=_avoid_classes): | |
563 """ | |
564 Turn any URLs into links. | |
565 | |
566 It will search for links identified by the given regular | |
567 expressions (by default mailto and http(s) links). | |
568 | |
569 It won't link text in an element in avoid_elements, or an element | |
570 with a class in avoid_classes. It won't link to anything with a | |
571 host that matches one of the regular expressions in avoid_hosts | |
572 (default localhost and 127.0.0.1). | |
573 | |
574 If you pass in an element, the element's tail will not be | |
575 substituted, only the contents of the element. | |
576 """ | |
577 if el.tag in avoid_elements: | |
578 return | |
579 class_name = el.get('class') | |
580 if class_name: | |
581 class_name = class_name.split() | |
582 for match_class in avoid_classes: | |
583 if match_class in class_name: | |
584 return | |
585 for child in list(el): | |
586 autolink(child, link_regexes=link_regexes, | |
587 avoid_elements=avoid_elements, | |
588 avoid_hosts=avoid_hosts, | |
589 avoid_classes=avoid_classes) | |
590 if child.tail: | |
591 text, tail_children = _link_text( | |
592 child.tail, link_regexes, avoid_hosts, factory=el.makeelement) | |
593 if tail_children: | |
594 child.tail = text | |
595 index = el.index(child) | |
596 el[index+1:index+1] = tail_children | |
597 if el.text: | |
598 text, pre_children = _link_text( | |
599 el.text, link_regexes, avoid_hosts, factory=el.makeelement) | |
600 if pre_children: | |
601 el.text = text | |
602 el[:0] = pre_children | |
603 | |
604 def _link_text(text, link_regexes, avoid_hosts, factory): | |
605 leading_text = '' | |
606 links = [] | |
607 last_pos = 0 | |
608 while 1: | |
609 best_match, best_pos = None, None | |
610 for regex in link_regexes: | |
611 regex_pos = last_pos | |
612 while 1: | |
613 match = regex.search(text, pos=regex_pos) | |
614 if match is None: | |
615 break | |
616 host = match.group('host') | |
617 for host_regex in avoid_hosts: | |
618 if host_regex.search(host): | |
619 regex_pos = match.end() | |
620 break | |
621 else: | |
622 break | |
623 if match is None: | |
624 continue | |
625 if best_pos is None or match.start() < best_pos: | |
626 best_match = match | |
627 best_pos = match.start() | |
628 if best_match is None: | |
629 # No more matches | |
630 if links: | |
631 assert not links[-1].tail | |
632 links[-1].tail = text | |
633 else: | |
634 assert not leading_text | |
635 leading_text = text | |
636 break | |
637 link = best_match.group(0) | |
638 end = best_match.end() | |
639 if link.endswith('.') or link.endswith(','): | |
640 # These punctuation marks shouldn't end a link | |
641 end -= 1 | |
642 link = link[:-1] | |
643 prev_text = text[:best_match.start()] | |
644 if links: | |
645 assert not links[-1].tail | |
646 links[-1].tail = prev_text | |
647 else: | |
648 assert not leading_text | |
649 leading_text = prev_text | |
650 anchor = factory('a') | |
651 anchor.set('href', link) | |
652 body = best_match.group('body') | |
653 if not body: | |
654 body = link | |
655 if body.endswith('.') or body.endswith(','): | |
656 body = body[:-1] | |
657 anchor.text = body | |
658 links.append(anchor) | |
659 text = text[end:] | |
660 return leading_text, links | |
661 | |
662 def autolink_html(html, *args, **kw): | |
663 result_type = type(html) | |
664 if isinstance(html, basestring): | |
665 doc = fromstring(html) | |
666 else: | |
667 doc = copy.deepcopy(html) | |
668 autolink(doc, *args, **kw) | |
669 return _transform_result(result_type, doc) | |
670 | |
671 autolink_html.__doc__ = autolink.__doc__ | |
672 | |
673 ############################################################ | |
674 ## Word wrapping | |
675 ############################################################ | |
676 | |
677 _avoid_word_break_elements = ['pre', 'textarea', 'code'] | |
678 _avoid_word_break_classes = ['nobreak'] | |
679 | |
680 def word_break(el, max_width=40, | |
681 avoid_elements=_avoid_word_break_elements, | |
682 avoid_classes=_avoid_word_break_classes, | |
683 break_character=unichr(0x200b)): | |
684 """ | |
685 Breaks any long words found in the body of the text (not attributes). | |
686 | |
687 Doesn't effect any of the tags in avoid_elements, by default | |
688 ``<textarea>`` and ``<pre>`` | |
689 | |
690 Breaks words by inserting ​, which is a unicode character | |
691 for Zero Width Space character. This generally takes up no space | |
692 in rendering, but does copy as a space, and in monospace contexts | |
693 usually takes up space. | |
694 | |
695 See http://www.cs.tut.fi/~jkorpela/html/nobr.html for a discussion | |
696 """ | |
697 # Character suggestion of ​ comes from: | |
698 # http://www.cs.tut.fi/~jkorpela/html/nobr.html | |
699 if el.tag in _avoid_word_break_elements: | |
700 return | |
701 class_name = el.get('class') | |
702 if class_name: | |
703 dont_break = False | |
704 class_name = class_name.split() | |
705 for avoid in avoid_classes: | |
706 if avoid in class_name: | |
707 dont_break = True | |
708 break | |
709 if dont_break: | |
710 return | |
711 if el.text: | |
712 el.text = _break_text(el.text, max_width, break_character) | |
713 for child in el: | |
714 word_break(child, max_width=max_width, | |
715 avoid_elements=avoid_elements, | |
716 avoid_classes=avoid_classes, | |
717 break_character=break_character) | |
718 if child.tail: | |
719 child.tail = _break_text(child.tail, max_width, break_character) | |
720 | |
721 def word_break_html(html, *args, **kw): | |
722 result_type = type(html) | |
723 doc = fromstring(html) | |
724 word_break(doc, *args, **kw) | |
725 return _transform_result(result_type, doc) | |
726 | |
727 def _break_text(text, max_width, break_character): | |
728 words = text.split() | |
729 for word in words: | |
730 if len(word) > max_width: | |
731 replacement = _insert_break(word, max_width, break_character) | |
732 text = text.replace(word, replacement) | |
733 return text | |
734 | |
735 _break_prefer_re = re.compile(r'[^a-z]', re.I) | |
736 | |
737 def _insert_break(word, width, break_character): | |
738 orig_word = word | |
739 result = '' | |
740 while len(word) > width: | |
741 start = word[:width] | |
742 breaks = list(_break_prefer_re.finditer(start)) | |
743 if breaks: | |
744 last_break = breaks[-1] | |
745 # Only walk back up to 10 characters to find a nice break: | |
746 if last_break.end() > width-10: | |
747 # FIXME: should the break character be at the end of the | |
748 # chunk, or the beginning of the next chunk? | |
749 start = word[:last_break.end()] | |
750 result += start + break_character | |
751 word = word[len(start):] | |
752 result += word | |
753 return result | |
754 |