%PDF- %PDF-
Direktori : /usr/lib/calibre/calibre/ebooks/textile/ |
Current File : //usr/lib/calibre/calibre/ebooks/textile/functions.py |
#!/usr/bin/env python3 """ PyTextile A Humane Web Text Generator """ # Last upstream version basis # __version__ = '2.1.4' # __date__ = '2009/12/04' __copyright__ = """ Copyright (c) 2011, Leigh Parry <leighparry@blueyonder.co.uk> Copyright (c) 2011, John Schember <john@nachtimwald.com> Copyright (c) 2009, Jason Samsa, http://jsamsa.com/ Copyright (c) 2004, Roberto A. F. De Almeida, http://dealmeida.net/ Copyright (c) 2003, Mark Pilgrim, http://diveintomark.org/ Original PHP Version: Copyright (c) 2003-2004, Dean Allen <dean@textism.com> All rights reserved. Thanks to Carlo Zottmann <carlo@g-blog.net> for refactoring Textile's procedural code into a class framework Additions and fixes Copyright (c) 2006 Alex Shiels http://thresholdstate.com/ """ __license__ = """ L I C E N S E ============= Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Textile nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ import re import uuid from calibre.utils.smartypants import smartyPants from polyglot.urllib import urlopen, urlparse def _normalize_newlines(string): out = re.sub(r'\r\n', '\n', string) out = re.sub(r'\n{3,}', '\n\n', out) out = re.sub(r'\n\s*\n', '\n\n', out) out = re.sub(r'"$', '" ', out) return out def getimagesize(url): """ Attempts to determine an image's width and height, and returns a string suitable for use in an <img> tag, or None in case of failure. Requires that PIL is installed. >>> getimagesize("http://www.google.com/intl/en_ALL/images/logo.gif") ... #doctest: +ELLIPSIS, +SKIP 'width="..." height="..."' """ from PIL import ImageFile try: p = ImageFile.Parser() f = urlopen(url) while True: s = f.read(1024) if not s: break p.feed(s) if p.image: return 'width="%i" height="%i"' % p.image.size except (OSError, ValueError): return None class Textile: hlgn = r'(?:\<(?!>)|(?<!<)\>|\<\>|\=|[()]+(?! ))' vlgn = r'[\-^~]' clas = r'(?:\([^)]+\))' lnge = r'(?:\[[^\]]+\])' styl = r'(?:\{[^}]+\})' cspn = r'(?:\\\d+)' rspn = r'(?:\/\d+)' a = fr'(?:{hlgn}|{vlgn})*' s = fr'(?:{cspn}|{rspn})*' c = r'(?:%s)*' % '|'.join([clas, styl, lnge, hlgn]) pnct = r'[-!"#$%&()*+,/:;<=>?@\'\[\\\]\.^_`{|}~]' # urlch = r'[\w"$\-_.+!*\'(),";/?:@=&%#{}|\\^~\[\]`]' urlch = r'[\w"$\-_.+*\'(),";\/?:@=&%#{}|\\^~\[\]`]' url_schemes = ('http', 'https', 'ftp', 'mailto') btag = ('bq', 'bc', 'notextile', 'pre', 'h[1-6]', r'fn\d+', 'p') btag_lite = ('bq', 'bc', 'p') macro_defaults = [ (re.compile(r'{(c\||\|c)}'), r'¢'), # cent (re.compile(r'{(L-|-L)}'), r'£'), # pound (re.compile(r'{(Y=|=Y)}'), r'¥'), # yen (re.compile(r'{\(c\)}'), r'©'), # copyright (re.compile(r'{\(r\)}'), r'®'), # registered (re.compile(r'{(\+_|_\+)}'), r'±'), # plus-minus (re.compile(r'{1/4}'), r'¼'), # quarter (re.compile(r'{1/2}'), r'½'), # half (re.compile(r'{3/4}'), r'¾'), # three-quarter (re.compile(r'{(A`|`A)}'), r'À'), # A-acute (re.compile(r'{(A\'|\'A)}'), r'Á'), # A-grave (re.compile(r'{(A\^|\^A)}'), r'Â'), # A-circumflex (re.compile(r'{(A~|~A)}'), r'Ã'), # A-tilde (re.compile(r'{(A\"|\"A)}'), r'Ä'), # A-diaeresis (re.compile(r'{(Ao|oA)}'), r'Å'), # A-ring (re.compile(r'{(AE)}'), r'Æ'), # AE (re.compile(r'{(C,|,C)}'), r'Ç'), # C-cedilla (re.compile(r'{(E`|`E)}'), r'È'), # E-acute (re.compile(r'{(E\'|\'E)}'), r'É'), # E-grave (re.compile(r'{(E\^|\^E)}'), r'Ê'), # E-circumflex (re.compile(r'{(E\"|\"E)}'), r'Ë'), # E-diaeresis (re.compile(r'{(I`|`I)}'), r'Ì'), # I-acute (re.compile(r'{(I\'|\'I)}'), r'Í'), # I-grave (re.compile(r'{(I\^|\^I)}'), r'Î'), # I-circumflex (re.compile(r'{(I\"|\"I)}'), r'Ï'), # I-diaeresis (re.compile(r'{(D-|-D)}'), r'Ð'), # ETH (re.compile(r'{(N~|~N)}'), r'Ñ'), # N-tilde (re.compile(r'{(O`|`O)}'), r'Ò'), # O-acute (re.compile(r'{(O\'|\'O)}'), r'Ó'), # O-grave (re.compile(r'{(O\^|\^O)}'), r'Ô'), # O-circumflex (re.compile(r'{(O~|~O)}'), r'Õ'), # O-tilde (re.compile(r'{(O\"|\"O)}'), r'Ö'), # O-diaeresis (re.compile(r'{x}'), r'×'), # dimension (re.compile(r'{(O\/|\/O)}'), r'Ø'), # O-slash (re.compile(r'{(U`|`U)}'), r'Ù'), # U-acute (re.compile(r'{(U\'|\'U)}'), r'Ú'), # U-grave (re.compile(r'{(U\^|\^U)}'), r'Û'), # U-circumflex (re.compile(r'{(U\"|\"U)}'), r'Ü'), # U-diaeresis (re.compile(r'{(Y\'|\'Y)}'), r'Ý'), # Y-grave (re.compile(r'{sz}'), r'ß'), # sharp-s (re.compile(r'{(a`|`a)}'), r'à'), # a-grave (re.compile(r'{(a\'|\'a)}'), r'á'), # a-acute (re.compile(r'{(a\^|\^a)}'), r'â'), # a-circumflex (re.compile(r'{(a~|~a)}'), r'ã'), # a-tilde (re.compile(r'{(a\"|\"a)}'), r'ä'), # a-diaeresis (re.compile(r'{(ao|oa)}'), r'å'), # a-ring (re.compile(r'{ae}'), r'æ'), # ae (re.compile(r'{(c,|,c)}'), r'ç'), # c-cedilla (re.compile(r'{(e`|`e)}'), r'è'), # e-grave (re.compile(r'{(e\'|\'e)}'), r'é'), # e-acute (re.compile(r'{(e\^|\^e)}'), r'ê'), # e-circumflex (re.compile(r'{(e\"|\"e)}'), r'ë'), # e-diaeresis (re.compile(r'{(i`|`i)}'), r'ì'), # i-grave (re.compile(r'{(i\'|\'i)}'), r'í'), # i-acute (re.compile(r'{(i\^|\^i)}'), r'î'), # i-circumflex (re.compile(r'{(i\"|\"i)}'), r'ï'), # i-diaeresis (re.compile(r'{(d-|-d)}'), r'ð'), # eth (re.compile(r'{(n~|~n)}'), r'ñ'), # n-tilde (re.compile(r'{(o`|`o)}'), r'ò'), # o-grave (re.compile(r'{(o\'|\'o)}'), r'ó'), # o-acute (re.compile(r'{(o\^|\^o)}'), r'ô'), # o-circumflex (re.compile(r'{(o~|~o)}'), r'õ'), # o-tilde (re.compile(r'{(o\"|\"o)}'), r'ö'), # o-diaeresis (re.compile(r'{(o\/|\/o)}'), r'ø'), # o-stroke (re.compile(r'{(u`|`u)}'), r'ù'), # u-grave (re.compile(r'{(u\'|\'u)}'), r'ú'), # u-acute (re.compile(r'{(u\^|\^u)}'), r'û'), # u-circumflex (re.compile(r'{(u\"|\"u)}'), r'ü'), # u-diaeresis (re.compile(r'{(y\'|\'y)}'), r'ý'), # y-acute (re.compile(r'{(y\"|\"y)}'), r'ÿ'), # y-diaeresis (re.compile(r'{(C\ˇ|\ˇC)}'), r'Č'), # C-caron (re.compile(r'{(c\ˇ|\ˇc)}'), r'č'), # c-caron (re.compile(r'{(D\ˇ|\ˇD)}'), r'Ď'), # D-caron (re.compile(r'{(d\ˇ|\ˇd)}'), r'ď'), # d-caron (re.compile(r'{(E\ˇ|\ˇE)}'), r'Ě'), # E-caron (re.compile(r'{(e\ˇ|\ˇe)}'), r'ě'), # e-caron (re.compile(r'{(L\'|\'L)}'), r'Ĺ'), # L-acute (re.compile(r'{(l\'|\'l)}'), r'ĺ'), # l-acute (re.compile(r'{(L\ˇ|\ˇL)}'), r'Ľ'), # L-caron (re.compile(r'{(l\ˇ|\ˇl)}'), r'ľ'), # l-caron (re.compile(r'{(N\ˇ|\ˇN)}'), r'Ň'), # N-caron (re.compile(r'{(n\ˇ|\ˇn)}'), r'ň'), # n-caron (re.compile(r'{OE}'), r'Œ'), # OE (re.compile(r'{oe}'), r'œ'), # oe (re.compile(r'{(R\'|\'R)}'), r'Ŕ'), # R-acute (re.compile(r'{(r\'|\'r)}'), r'ŕ'), # r-acute (re.compile(r'{(R\ˇ|\ˇR)}'), r'Ř'), # R-caron (re.compile(r'{(r\ˇ|\ˇr)}'), r'ř'), # r-caron (re.compile(r'{(S\^|\^S)}'), r'Ŝ'), # S-circumflex (re.compile(r'{(s\^|\^s)}'), r'ŝ'), # s-circumflex (re.compile(r'{(S\ˇ|\ˇS)}'), r'Š'), # S-caron (re.compile(r'{(s\ˇ|\ˇs)}'), r'š'), # s-caron (re.compile(r'{(T\ˇ|\ˇT)}'), r'Ť'), # T-caron (re.compile(r'{(t\ˇ|\ˇt)}'), r'ť'), # t-caron (re.compile(r'{(U\°|\°U)}'), r'Ů'), # U-ring (re.compile(r'{(u\°|\°u)}'), r'ů'), # u-ring (re.compile(r'{(Z\ˇ|\ˇZ)}'), r'Ž'), # Z-caron (re.compile(r'{(z\ˇ|\ˇz)}'), r'ž'), # z-caron (re.compile(r'{\*}'), r'•'), # bullet (re.compile(r'{Fr}'), r'₣'), # Franc (re.compile(r'{(L=|=L)}'), r'₤'), # Lira (re.compile(r'{Rs}'), r'₨'), # Rupee (re.compile(r'{(C=|=C)}'), r'€'), # euro (re.compile(r'{tm}'), r'™'), # trademark (re.compile(r'{spades?}'), r'♠'), # spade (re.compile(r'{clubs?}'), r'♣'), # club (re.compile(r'{hearts?}'), r'♥'), # heart (re.compile(r'{diam(onds?|s)}'), r'♦'), # diamond (re.compile(r'{"}'), r'"'), # double-quote (re.compile(r"{'}"), r'''), # single-quote (re.compile(r"{(’|'/|/')}"), r'’'), # closing-single-quote - apostrophe (re.compile(r"{(‘|\\'|'\\)}"), r'‘'), # opening-single-quote (re.compile(r'{(”|"/|/")}'), r'”'), # closing-double-quote (re.compile(r'{(“|\\"|"\\)}'), r'“'), # opening-double-quote ] glyph_defaults = [ (re.compile(r'(\d+\'?\"?)( ?)x( ?)(?=\d+)'), r'\1\2×\3'), # dimension sign (re.compile(r'(\d+)\'(\s)', re.I), r'\1′\2'), # prime (re.compile(r'(\d+)\"(\s)', re.I), r'\1″\2'), # prime-double (re.compile(r'\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])'), r'<acronym title="\2">\1</acronym>'), # 3+ uppercase acronym (re.compile(r'\b([A-Z][A-Z\'\-]+[A-Z])(?=[\s.,\)>])'), r'<span class="caps">\1</span>'), # 3+ uppercase (re.compile(r'\b(\s{0,1})?\.{3}'), r'\1…'), # ellipsis (re.compile(r'^[\*_-]{3,}$', re.M), r'<hr />'), # <hr> scene-break (re.compile(r'(^|[^-])--([^-]|$)'), r'\1—\2'), # em dash (re.compile(r'\s-(?:\s|$)'), r' – '), # en dash (re.compile(r'\b( ?)[([]TM[])]', re.I), r'\1™'), # trademark (re.compile(r'\b( ?)[([]R[])]', re.I), r'\1®'), # registered (re.compile(r'\b( ?)[([]C[])]', re.I), r'\1©'), # copyright ] def __init__(self, restricted=False, lite=False, noimage=False): """docstring for __init__""" self.restricted = restricted self.lite = lite self.noimage = noimage self.get_sizes = False self.fn = {} self.urlrefs = {} self.shelf = {} self.rel = '' self.html_type = 'xhtml' def textile(self, text, rel=None, head_offset=0, html_type='xhtml'): """ >>> import textile >>> textile.textile('some textile') u'\\t<p>some textile</p>' """ self.html_type = html_type # text = type(u'')(text) text = _normalize_newlines(text) if self.restricted: text = self.encode_html(text, quotes=False) if rel: self.rel = ' rel="%s"' % rel text = self.getRefs(text) text = self.block(text, int(head_offset)) text = self.retrieve(text) text = smartyPants(text, 'q') return text def pba(self, input, element=None): """ Parse block attributes. >>> t = Textile() >>> t.pba(r'\3') '' >>> t.pba(r'\\3', element='td') ' colspan="3"' >>> t.pba(r'/4', element='td') ' rowspan="4"' >>> t.pba(r'\\3/4', element='td') ' colspan="3" rowspan="4"' >>> t.vAlign('^') 'top' >>> t.pba('^', element='td') ' style="vertical-align:top;"' >>> t.pba('{line-height:18px}') ' style="line-height:18px;"' >>> t.pba('(foo-bar)') ' class="foo-bar"' >>> t.pba('(#myid)') ' id="myid"' >>> t.pba('(foo-bar#myid)') ' class="foo-bar" id="myid"' >>> t.pba('((((') ' style="padding-left:4em;"' >>> t.pba(')))') ' style="padding-right:3em;"' >>> t.pba('[fr]') ' lang="fr"' """ style = [] aclass = '' lang = '' colspan = '' rowspan = '' id = '' if not input: return '' matched = input if element == 'td': m = re.search(r'\\(\d+)', matched) if m: colspan = m.group(1) m = re.search(r'/(\d+)', matched) if m: rowspan = m.group(1) if element == 'td' or element == 'tr': m = re.search(r'(%s)' % self.vlgn, matched) if m: style.append("vertical-align:%s;" % self.vAlign(m.group(1))) m = re.search(r'\{([^}]*)\}', matched) if m: style.append(m.group(1).rstrip(';') + ';') matched = matched.replace(m.group(0), '') m = re.search(r'\[([^\]]+)\]', matched, re.U) if m: lang = m.group(1) matched = matched.replace(m.group(0), '') m = re.search(r'\(([^()]+)\)', matched, re.U) if m: aclass = m.group(1) matched = matched.replace(m.group(0), '') m = re.search(r'([(]+)', matched) if m: style.append("padding-left:%sem;" % len(m.group(1))) matched = matched.replace(m.group(0), '') m = re.search(r'([)]+)', matched) if m: style.append("padding-right:%sem;" % len(m.group(1))) matched = matched.replace(m.group(0), '') m = re.search(r'(%s)' % self.hlgn, matched) if m: style.append("text-align:%s;" % self.hAlign(m.group(1))) m = re.search(r'^(.*)#(.*)$', aclass) if m: id = m.group(2) aclass = m.group(1) if self.restricted: if lang: return ' lang="%s"' else: return '' result = [] if style: result.append(' style="%s"' % "".join(style)) if aclass: result.append(' class="%s"' % aclass) if lang: result.append(' lang="%s"' % lang) if id: result.append(' id="%s"' % id) if colspan: result.append(' colspan="%s"' % colspan) if rowspan: result.append(' rowspan="%s"' % rowspan) return ''.join(result) def hasRawText(self, text): """ checks whether the text has text not already enclosed by a block tag >>> t = Textile() >>> t.hasRawText('<p>foo bar biz baz</p>') False >>> t.hasRawText(' why yes, yes it does') True """ r = re.compile(r'<(p|blockquote|div|form|table|ul|ol|pre|h\d)[^>]*?>.*</\1>', re.S).sub('', text.strip()).strip() r = re.compile(r'<(hr|br)[^>]*?/>').sub('', r) return '' != r def table(self, text): r""" >>> t = Textile() >>> t.table('|one|two|three|\n|a|b|c|') '\t<table>\n\t\t<tr>\n\t\t\t<td>one</td>\n\t\t\t<td>two</td>\n\t\t\t<td>three</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td>a</td>\n\t\t\t<td>b</td>\n\t\t\t<td>c</td>\n\t\t</tr>\n\t</table>\n\n' """ text = text + "\n\n" pattern = re.compile(r'^(?:table(_?%(s)s%(a)s%(c)s)\. ?\n)?^(%(a)s%(c)s\.? ?\|.*\|)\n\n' % {'s':self.s, 'a':self.a, 'c':self.c}, re.S|re.M|re.U) return pattern.sub(self.fTable, text) def fTable(self, match): tatts = self.pba(match.group(1), 'table') rows = [] for row in [x for x in match.group(2).split('\n') if x]: rmtch = re.search(fr'^({self.a}{self.c}\. )(.*)', row.lstrip()) if rmtch: ratts = self.pba(rmtch.group(1), 'tr') row = rmtch.group(2) else: ratts = '' cells = [] for cell in row.split('|')[1:-1]: ctyp = 'd' if re.search(r'^_', cell): ctyp = "h" cmtch = re.search(fr'^(_?{self.s}{self.a}{self.c}\. )(.*)', cell) if cmtch: catts = self.pba(cmtch.group(1), 'td') cell = cmtch.group(2) else: catts = '' cell = self.graf(self.span(cell)) cells.append(f'\t\t\t<t{ctyp}{catts}>{cell}</t{ctyp}>') rows.append("\t\t<tr{}>\n{}\n\t\t</tr>".format(ratts, '\n'.join(cells))) cells = [] catts = None return "\t<table{}>\n{}\n\t</table>\n\n".format(tatts, '\n'.join(rows)) def lists(self, text): """ >>> t = Textile() >>> t.lists("* one\\n* two\\n* three") '\\t<ul>\\n\\t\\t<li>one</li>\\n\\t\\t<li>two</li>\\n\\t\\t<li>three</li>\\n\\t</ul>' """ pattern = re.compile(r'^([#*]+%s .*)$(?![^#*])' % self.c, re.U|re.M|re.S) return pattern.sub(self.fList, text) def fList(self, match): text = match.group(0).split("\n") result = [] lists = [] for i, line in enumerate(text): try: nextline = text[i+1] except IndexError: nextline = '' m = re.search(fr"^([#*]+)({self.a}{self.c}) (.*)$", line, re.S) if m: tl, atts, content = m.groups() nl = '' nm = re.search(r'^([#*]+)\s.*', nextline) if nm: nl = nm.group(1) if tl not in lists: lists.append(tl) atts = self.pba(atts) line = f"\t<{self.lT(tl)}l{atts}>\n\t\t<li>{self.graf(content)}" else: line = "\t\t<li>" + self.graf(content) if len(nl) <= len(tl): line = line + "</li>" for k in reversed(lists): if len(k) > len(nl): line = line + "\n\t</%sl>" % self.lT(k) if len(k) > 1: line = line + "</li>" lists.remove(k) result.append(line) return "\n".join(result) def lT(self, input): if re.search(r'^#+', input): return 'o' else: return 'u' def doPBr(self, in_): return re.compile(r'<(p)([^>]*?)>(.*)(</\1>)', re.S).sub(self.doBr, in_) def doBr(self, match): if self.html_type == 'html': content = re.sub(r'(.+)(?:(?<!<br>)|(?<!<br />))\n(?![#*\s|])', '\\1<br>', match.group(3)) else: content = re.sub(r'(.+)(?:(?<!<br>)|(?<!<br />))\n(?![#*\s|])', '\\1<br />', match.group(3)) return f'<{match.group(1)}{match.group(2)}>{content}{match.group(4)}' def block(self, text, head_offset=0): """ >>> t = Textile() >>> t.block('h1. foobar baby') '\\t<h1>foobar baby</h1>' """ if not self.lite: tre = '|'.join(self.btag) else: tre = '|'.join(self.btag_lite) text = text.split('\n\n') tag = 'p' atts = cite = graf = ext = c1 = '' out = [] anon = False for line in text: pattern = fr'^({tre})({self.a}{self.c})\.(\.?)(?::(\S+))? (.*)$' match = re.search(pattern, line, re.S) if match: if ext: out.append(out.pop() + c1) tag, atts, ext, cite, graf = match.groups() h_match = re.search(r'h([1-6])', tag) if h_match: head_level, = h_match.groups() tag = 'h%i' % max(1, min(int(head_level) + head_offset, 6)) o1, o2, content, c2, c1 = self.fBlock(tag, atts, ext, cite, graf) # leave off c1 if this block is extended, # we'll close it at the start of the next block if ext: line = f"{o1}{o2}{content}{c2}" else: line = f"{o1}{o2}{content}{c2}{c1}" else: anon = True if ext or not re.search(r'^\s', line): o1, o2, content, c2, c1 = self.fBlock(tag, atts, ext, cite, line) # skip $o1/$c1 because this is part of a continuing # extended block if tag == 'p' and not self.hasRawText(content): line = content else: line = f"{o2}{content}{c2}" else: line = self.graf(line) line = self.doPBr(line) if self.html_type == 'xhtml': line = re.sub(r'<br>', '<br />', line) if ext and anon: out.append(out.pop() + "\n" + line) else: out.append(line) if not ext: tag = 'p' atts = '' cite = '' graf = '' if ext: out.append(out.pop() + c1) return '\n\n'.join(out) def fBlock(self, tag, atts, ext, cite, content): """ >>> t = Textile() >>> t.fBlock("bq", "", None, "", "Hello BlockQuote") ('\\t<blockquote>\\n', '\\t\\t<p>', 'Hello BlockQuote', '</p>', '\\n\\t</blockquote>') >>> t.fBlock("bq", "", None, "http://google.com", "Hello BlockQuote") ('\\t<blockquote cite="http://google.com">\\n', '\\t\\t<p>', 'Hello BlockQuote', '</p>', '\\n\\t</blockquote>') >>> t.fBlock("bc", "", None, "", 'printf "Hello, World";') # doctest: +ELLIPSIS ('<pre>', '<code>', ..., '</code>', '</pre>') >>> t.fBlock("h1", "", None, "", "foobar") ('', '\\t<h1>', 'foobar', '</h1>', '') """ atts = self.pba(atts) o1 = o2 = c2 = c1 = '' m = re.search(r'fn(\d+)', tag) if m: tag = 'p' if m.group(1) in self.fn: fnid = self.fn[m.group(1)] else: fnid = m.group(1) atts = atts + ' id="fn%s"' % fnid if atts.find('class=') < 0: atts = atts + ' class="footnote"' content = ('<sup>%s</sup>' % m.group(1)) + content if tag == 'bq': cite = self.checkRefs(cite) if cite: cite = ' cite="%s"' % cite else: cite = '' o1 = f"\t<blockquote{cite}{atts}>\n" o2 = "\t\t<p%s>" % atts c2 = "</p>" c1 = "\n\t</blockquote>" elif tag == 'bc': o1 = "<pre%s>" % atts o2 = "<code%s>" % atts c2 = "</code>" c1 = "</pre>" content = self.shelve(self.encode_html(content.rstrip("\n") + "\n")) elif tag == 'notextile': content = self.shelve(content) o1 = o2 = '' c1 = c2 = '' elif tag == 'pre': content = self.shelve(self.encode_html(content.rstrip("\n") + "\n")) o1 = "<pre%s>" % atts o2 = c2 = '' c1 = '</pre>' else: o2 = f"\t<{tag}{atts}>" c2 = "</%s>" % tag content = self.graf(content) return o1, o2, content, c2, c1 def footnoteRef(self, text): """ >>> t = Textile() >>> t.footnoteRef('foo[1] ') # doctest: +ELLIPSIS 'foo<sup class="footnote"><a href="#fn...">1</a></sup> ' """ return re.sub(r'\b\[([0-9]+)\](\s)?', self.footnoteID, text) def footnoteID(self, match): id, t = match.groups() if id not in self.fn: self.fn[id] = str(uuid.uuid4()) fnid = self.fn[id] if not t: t = '' return f'<sup class="footnote"><a href="#fn{fnid}">{id}</a></sup>{t}' def glyphs(self, text): """ >>> t = Textile() >>> t.glyphs("apostrophe's") 'apostrophe’s' >>> t.glyphs("back in '88") 'back in ’88' >>> t.glyphs('foo ...') 'foo …' >>> t.glyphs('--') '—' >>> t.glyphs('FooBar[tm]') 'FooBar™' >>> t.glyphs("<p><cite>Cat's Cradle</cite> by Vonnegut</p>") '<p><cite>Cat’s Cradle</cite> by Vonnegut</p>' """ # fix: hackish text = re.sub(r'"\Z', '\" ', text) result = [] for line in re.compile(r'(<.*?>)', re.U).split(text): if not re.search(r'<.*>', line): rules = [] if re.search(r'{.+?}', line): rules = self.macro_defaults + self.glyph_defaults else: rules = self.glyph_defaults for s, r in rules: line = s.sub(r, line) result.append(line) return ''.join(result) def macros_only(self, text): # fix: hackish text = re.sub(r'"\Z', '\" ', text) result = [] for line in re.compile(r'(<.*?>)', re.U).split(text): if not re.search(r'<.*>', line): rules = [] if re.search(r'{.+?}', line): rules = self.macro_defaults for s, r in rules: line = s.sub(r, line) result.append(line) return ''.join(result) def vAlign(self, input): d = {'^':'top', '-':'middle', '~':'bottom'} return d.get(input, '') def hAlign(self, input): d = {'<':'left', '=':'center', '>':'right', '<>': 'justify'} return d.get(input, '') def getRefs(self, text): """ what is this for? """ pattern = re.compile(r'(?:(?<=^)|(?<=\s))\[(.+)\]((?:http(?:s?):\/\/|\/)\S+)(?=\s|$)', re.U) text = pattern.sub(self.refs, text) return text def refs(self, match): flag, url = match.groups() self.urlrefs[flag] = url return '' def checkRefs(self, url): return self.urlrefs.get(url, url) def isRelURL(self, url): """ Identify relative urls. >>> t = Textile() >>> t.isRelURL("http://www.google.com/") False >>> t.isRelURL("/foo") True """ (scheme, netloc) = urlparse(url)[0:2] return not scheme and not netloc def relURL(self, url): scheme = urlparse(url)[0] if self.restricted and scheme and scheme not in self.url_schemes: return '#' return url def shelve(self, text): id = str(uuid.uuid4()) + 'c' self.shelf[id] = text return id def retrieve(self, text): """ >>> t = Textile() >>> id = t.shelve("foobar") >>> t.retrieve(id) 'foobar' """ while True: old = text for k, v in self.shelf.items(): text = text.replace(k, v) if text == old: break return text def encode_html(self, text, quotes=True): a = ( ('&', '&'), ('<', '<'), ('>', '>') ) if quotes: a = a + ( ("'", '''), ('"', '"') ) for k, v in a: text = text.replace(k, v) return text def graf(self, text): if not self.lite: text = self.noTextile(text) text = self.code(text) text = self.links(text) if not self.noimage: text = self.image(text) if not self.lite: text = self.lists(text) text = self.table(text) text = self.span(text) text = self.footnoteRef(text) text = self.glyphs(text) return text.rstrip('\n') def links(self, text): """ >>> t = Textile() >>> t.links('fooobar "Google":http://google.com/foobar/ and hello world "flickr":http://flickr.com/photos/jsamsa/ ') # doctest: +ELLIPSIS 'fooobar ... and hello world ...' """ text = self.macros_only(text) punct = '!"#$%&\'*+,-./:;=?@\\^_`|~' pattern = r''' (?P<pre> [\s\[{{(]|[{}] )? " # start (?P<atts> {} ) (?P<text> [^"]+? ) \s? (?: \(([^)]+?)\)(?=") )? # $title ": (?P<url> (?:ftp|https?)? (?: :// )? [-A-Za-z0-9+&@#/?=~_()|!:,.;]*[-A-Za-z0-9+&@#/=~_()|] ) (?P<post> [^\w\/;]*? ) (?=<|\s|$) '''.format(re.escape(punct), self.c) text = re.compile(pattern, re.X).sub(self.fLink, text) return text def fLink(self, match): pre, atts, text, title, url, post = match.groups() if pre is None: pre = '' # assume ) at the end of the url is not actually part of the url # unless the url also contains a ( if url.endswith(')') and not url.find('(') > -1: post = url[-1] + post url = url[:-1] url = self.checkRefs(url) atts = self.pba(atts) if title: atts = atts + ' title="%s"' % self.encode_html(title) if not self.noimage: text = self.image(text) text = self.span(text) text = self.glyphs(text) url = self.relURL(url) out = f'<a href="{self.encode_html(url)}"{atts}{self.rel}>{text}</a>' out = self.shelve(out) return ''.join([pre, out, post]) def span(self, text): """ >>> t = Textile() >>> t.span(r"hello %(bob)span *strong* and **bold**% goodbye") 'hello <span class="bob">span <strong>strong</strong> and <b>bold</b></span> goodbye' """ qtags = (r'\*\*', r'\*', r'\?\?', r'\-', r'__', r'_', r'%', r'\+', r'~', r'\^') pnct = ".,\"'?!;:" for qtag in qtags: pattern = re.compile(r""" (?:^|(?<=[\s>%(pnct)s\(])|\[|([\]}])) (%(qtag)s)(?!%(qtag)s) (%(c)s) (?::(\S+))? ([^\s%(qtag)s]+|\S[^%(qtag)s\n]*[^\s%(qtag)s\n]) ([%(pnct)s]*) %(qtag)s (?:$|([\]}])|(?=%(selfpnct)s{1,2}|\s)) """ % {'qtag':qtag, 'c':self.c, 'pnct':pnct, 'selfpnct':self.pnct}, re.X) text = pattern.sub(self.fSpan, text) return text def fSpan(self, match): _, tag, atts, cite, content, end, _ = match.groups() qtags = { '*': 'strong', '**': 'b', '??': 'cite', '_' : 'em', '__': 'i', '-' : 'del', '%' : 'span', '+' : 'ins', '~' : 'sub', '^' : 'sup' } tag = qtags[tag] atts = self.pba(atts) if cite: atts = atts + 'cite="%s"' % cite content = self.span(content) out = f"<{tag}{atts}>{content}{end}</{tag}>" return out def image(self, text): """ >>> t = Textile() >>> t.image('!/imgs/myphoto.jpg!:http://jsamsa.com') '<a href="http://jsamsa.com"><img src="/imgs/myphoto.jpg" alt="" /></a>' """ pattern = re.compile(r""" (?:[\[{])? # pre \! # opening ! (%s) # optional style,class atts (?:\. )? # optional dot-space ([^\s(!]+) # presume this is the src \s? # optional space (?:\(([^\)]+)\))? # optional title \! # closing (?::(\S+))? # optional href (?:[\]}]|(?=\s|$)) # lookahead: space or end of string """ % self.c, re.U|re.X) return pattern.sub(self.fImage, text) def fImage(self, match): # (None, '', '/imgs/myphoto.jpg', None, None) atts, url, title, href = match.groups() atts = self.pba(atts) if title: atts = atts + f' title="{title}" alt="{title}"' else: atts = atts + ' alt=""' if not self.isRelURL(url) and self.get_sizes: size = getimagesize(url) if (size): atts += " %s" % size if href: href = self.checkRefs(href) url = self.checkRefs(url) url = self.relURL(url) out = [] if href: out.append('<a href="%s" class="img">' % href) if self.html_type == 'html': out.append(f'<img src="{url}"{atts}>') else: out.append(f'<img src="{url}"{atts} />') if href: out.append('</a>') return ''.join(out) def code(self, text): text = self.doSpecial(text, '<code>', '</code>', self.fCode) text = self.doSpecial(text, '@', '@', self.fCode) text = self.doSpecial(text, '<pre>', '</pre>', self.fPre) return text def fCode(self, match): before, text, after = match.groups() if after is None: after = '' # text needs to be escaped if not self.restricted: text = self.encode_html(text) return ''.join([before, self.shelve('<code>%s</code>' % text), after]) def fPre(self, match): before, text, after = match.groups() if after is None: after = '' # text needs to be escapedd if not self.restricted: text = self.encode_html(text) return ''.join([before, '<pre>', self.shelve(text), '</pre>', after]) def doSpecial(self, text, start, end, method=None): if method is None: method = self.fSpecial pattern = re.compile(fr'(^|\s|[\[({{>]){re.escape(start)}(.*?){re.escape(end)}(\s|$|[\])}}])?', re.M|re.S) return pattern.sub(method, text) def fSpecial(self, match): """ special blocks like notextile or code """ before, text, after = match.groups() if after is None: after = '' return ''.join([before, self.shelve(self.encode_html(text)), after]) def noTextile(self, text): text = self.doSpecial(text, '<notextile>', '</notextile>', self.fTextile) return self.doSpecial(text, '==', '==', self.fTextile) def fTextile(self, match): before, notextile, after = match.groups() if after is None: after = '' return ''.join([before, self.shelve(notextile), after]) def textile(text, head_offset=0, html_type='xhtml', encoding=None, output=None): """ this function takes additional parameters: head_offset - offset to apply to heading levels (default: 0) html_type - 'xhtml' or 'html' style tags (default: 'xhtml') """ return Textile().textile(text, head_offset=head_offset, html_type=html_type) def textile_restricted(text, lite=True, noimage=True, html_type='xhtml'): """ Restricted version of Textile designed for weblog comments and other untrusted input. Raw HTML is escaped. Style attributes are disabled. rel='nofollow' is added to external links. When lite=True is set (the default): Block tags are restricted to p, bq, and bc. Lists and tables are disabled. When noimage=True is set (the default): Image tags are disabled. """ return Textile(restricted=True, lite=lite, noimage=noimage).textile(text, rel='nofollow', html_type=html_type)