OLD | NEW |
(Empty) | |
| 1 # -*- coding: utf-8 -*- |
| 2 ''' |
| 3 Smarty extension for Python-Markdown |
| 4 ==================================== |
| 5 |
| 6 Adds conversion of ASCII dashes, quotes and ellipses to their HTML |
| 7 entity equivalents. |
| 8 |
| 9 See <https://pythonhosted.org/Markdown/extensions/smarty.html> |
| 10 for documentation. |
| 11 |
| 12 Author: 2013, Dmitry Shachnev <mitya57@gmail.com> |
| 13 |
| 14 All changes Copyright 2013-2014 The Python Markdown Project |
| 15 |
| 16 License: [BSD](http://www.opensource.org/licenses/bsd-license.php) |
| 17 |
| 18 SmartyPants license: |
| 19 |
| 20 Copyright (c) 2003 John Gruber <http://daringfireball.net/> |
| 21 All rights reserved. |
| 22 |
| 23 Redistribution and use in source and binary forms, with or without |
| 24 modification, are permitted provided that the following conditions are |
| 25 met: |
| 26 |
| 27 * Redistributions of source code must retain the above copyright |
| 28 notice, this list of conditions and the following disclaimer. |
| 29 |
| 30 * Redistributions in binary form must reproduce the above copyright |
| 31 notice, this list of conditions and the following disclaimer in |
| 32 the documentation and/or other materials provided with the |
| 33 distribution. |
| 34 |
| 35 * Neither the name "SmartyPants" nor the names of its contributors |
| 36 may be used to endorse or promote products derived from this |
| 37 software without specific prior written permission. |
| 38 |
| 39 This software is provided by the copyright holders and contributors "as |
| 40 is" and any express or implied warranties, including, but not limited |
| 41 to, the implied warranties of merchantability and fitness for a |
| 42 particular purpose are disclaimed. In no event shall the copyright |
| 43 owner or contributors be liable for any direct, indirect, incidental, |
| 44 special, exemplary, or consequential damages (including, but not |
| 45 limited to, procurement of substitute goods or services; loss of use, |
| 46 data, or profits; or business interruption) however caused and on any |
| 47 theory of liability, whether in contract, strict liability, or tort |
| 48 (including negligence or otherwise) arising in any way out of the use |
| 49 of this software, even if advised of the possibility of such damage. |
| 50 |
| 51 |
| 52 smartypants.py license: |
| 53 |
| 54 smartypants.py is a derivative work of SmartyPants. |
| 55 Copyright (c) 2004, 2007 Chad Miller <http://web.chad.org/> |
| 56 |
| 57 Redistribution and use in source and binary forms, with or without |
| 58 modification, are permitted provided that the following conditions are |
| 59 met: |
| 60 |
| 61 * Redistributions of source code must retain the above copyright |
| 62 notice, this list of conditions and the following disclaimer. |
| 63 |
| 64 * Redistributions in binary form must reproduce the above copyright |
| 65 notice, this list of conditions and the following disclaimer in |
| 66 the documentation and/or other materials provided with the |
| 67 distribution. |
| 68 |
| 69 This software is provided by the copyright holders and contributors "as |
| 70 is" and any express or implied warranties, including, but not limited |
| 71 to, the implied warranties of merchantability and fitness for a |
| 72 particular purpose are disclaimed. In no event shall the copyright |
| 73 owner or contributors be liable for any direct, indirect, incidental, |
| 74 special, exemplary, or consequential damages (including, but not |
| 75 limited to, procurement of substitute goods or services; loss of use, |
| 76 data, or profits; or business interruption) however caused and on any |
| 77 theory of liability, whether in contract, strict liability, or tort |
| 78 (including negligence or otherwise) arising in any way out of the use |
| 79 of this software, even if advised of the possibility of such damage. |
| 80 |
| 81 ''' |
| 82 |
| 83 |
| 84 from __future__ import unicode_literals |
| 85 from . import Extension |
| 86 from ..inlinepatterns import HtmlPattern |
| 87 from ..odict import OrderedDict |
| 88 from ..treeprocessors import InlineProcessor |
| 89 |
| 90 |
| 91 # Constants for quote education. |
| 92 punctClass = r"""[!"#\$\%'()*+,-.\/:;<=>?\@\[\\\]\^_`{|}~]""" |
| 93 endOfWordClass = r"[\s.,;:!?)]" |
| 94 closeClass = "[^\ \t\r\n\[\{\(\-\u0002\u0003]" |
| 95 |
| 96 openingQuotesBase = ( |
| 97 '(\s' # a whitespace char |
| 98 '| ' # or a non-breaking space entity |
| 99 '|--' # or dashes |
| 100 '|–|—' # or unicode |
| 101 '|&[mn]dash;' # or named dash entities |
| 102 '|–|—' # or decimal entities |
| 103 ')' |
| 104 ) |
| 105 |
| 106 substitutions = { |
| 107 'mdash': '—', |
| 108 'ndash': '–', |
| 109 'ellipsis': '…', |
| 110 'left-angle-quote': '«', |
| 111 'right-angle-quote': '»', |
| 112 'left-single-quote': '‘', |
| 113 'right-single-quote': '’', |
| 114 'left-double-quote': '“', |
| 115 'right-double-quote': '”', |
| 116 } |
| 117 |
| 118 |
| 119 # Special case if the very first character is a quote |
| 120 # followed by punctuation at a non-word-break. Close the quotes by brute force: |
| 121 singleQuoteStartRe = r"^'(?=%s\B)" % punctClass |
| 122 doubleQuoteStartRe = r'^"(?=%s\B)' % punctClass |
| 123 |
| 124 # Special case for double sets of quotes, e.g.: |
| 125 # <p>He said, "'Quoted' words in a larger quote."</p> |
| 126 doubleQuoteSetsRe = r""""'(?=\w)""" |
| 127 singleQuoteSetsRe = r"""'"(?=\w)""" |
| 128 |
| 129 # Special case for decade abbreviations (the '80s): |
| 130 decadeAbbrRe = r"(?<!\w)'(?=\d{2}s)" |
| 131 |
| 132 # Get most opening double quotes: |
| 133 openingDoubleQuotesRegex = r'%s"(?=\w)' % openingQuotesBase |
| 134 |
| 135 # Double closing quotes: |
| 136 closingDoubleQuotesRegex = r'"(?=\s)' |
| 137 closingDoubleQuotesRegex2 = '(?<=%s)"' % closeClass |
| 138 |
| 139 # Get most opening single quotes: |
| 140 openingSingleQuotesRegex = r"%s'(?=\w)" % openingQuotesBase |
| 141 |
| 142 # Single closing quotes: |
| 143 closingSingleQuotesRegex = r"(?<=%s)'(?!\s|s\b|\d)" % closeClass |
| 144 closingSingleQuotesRegex2 = r"(?<=%s)'(\s|s\b)" % closeClass |
| 145 |
| 146 # All remaining quotes should be opening ones |
| 147 remainingSingleQuotesRegex = "'" |
| 148 remainingDoubleQuotesRegex = '"' |
| 149 |
| 150 |
| 151 class SubstituteTextPattern(HtmlPattern): |
| 152 def __init__(self, pattern, replace, markdown_instance): |
| 153 """ Replaces matches with some text. """ |
| 154 HtmlPattern.__init__(self, pattern) |
| 155 self.replace = replace |
| 156 self.markdown = markdown_instance |
| 157 |
| 158 def handleMatch(self, m): |
| 159 result = '' |
| 160 for part in self.replace: |
| 161 if isinstance(part, int): |
| 162 result += m.group(part) |
| 163 else: |
| 164 result += self.markdown.htmlStash.store(part, safe=True) |
| 165 return result |
| 166 |
| 167 |
| 168 class SmartyExtension(Extension): |
| 169 def __init__(self, *args, **kwargs): |
| 170 self.config = { |
| 171 'smart_quotes': [True, 'Educate quotes'], |
| 172 'smart_angled_quotes': [False, 'Educate angled quotes'], |
| 173 'smart_dashes': [True, 'Educate dashes'], |
| 174 'smart_ellipses': [True, 'Educate ellipses'], |
| 175 'substitutions': [{}, 'Overwrite default substitutions'], |
| 176 } |
| 177 super(SmartyExtension, self).__init__(*args, **kwargs) |
| 178 self.substitutions = dict(substitutions) |
| 179 self.substitutions.update(self.getConfig('substitutions', default={})) |
| 180 |
| 181 def _addPatterns(self, md, patterns, serie): |
| 182 for ind, pattern in enumerate(patterns): |
| 183 pattern += (md,) |
| 184 pattern = SubstituteTextPattern(*pattern) |
| 185 after = ('>smarty-%s-%d' % (serie, ind - 1) if ind else '_begin') |
| 186 name = 'smarty-%s-%d' % (serie, ind) |
| 187 self.inlinePatterns.add(name, pattern, after) |
| 188 |
| 189 def educateDashes(self, md): |
| 190 emDashesPattern = SubstituteTextPattern( |
| 191 r'(?<!-)---(?!-)', (self.substitutions['mdash'],), md |
| 192 ) |
| 193 enDashesPattern = SubstituteTextPattern( |
| 194 r'(?<!-)--(?!-)', (self.substitutions['ndash'],), md |
| 195 ) |
| 196 self.inlinePatterns.add('smarty-em-dashes', emDashesPattern, '_begin') |
| 197 self.inlinePatterns.add( |
| 198 'smarty-en-dashes', enDashesPattern, '>smarty-em-dashes' |
| 199 ) |
| 200 |
| 201 def educateEllipses(self, md): |
| 202 ellipsesPattern = SubstituteTextPattern( |
| 203 r'(?<!\.)\.{3}(?!\.)', (self.substitutions['ellipsis'],), md |
| 204 ) |
| 205 self.inlinePatterns.add('smarty-ellipses', ellipsesPattern, '_begin') |
| 206 |
| 207 def educateAngledQuotes(self, md): |
| 208 leftAngledQuotePattern = SubstituteTextPattern( |
| 209 r'\<\<', (self.substitutions['left-angle-quote'],), md |
| 210 ) |
| 211 rightAngledQuotePattern = SubstituteTextPattern( |
| 212 r'\>\>', (self.substitutions['right-angle-quote'],), md |
| 213 ) |
| 214 self.inlinePatterns.add( |
| 215 'smarty-left-angle-quotes', leftAngledQuotePattern, '_begin' |
| 216 ) |
| 217 self.inlinePatterns.add( |
| 218 'smarty-right-angle-quotes', |
| 219 rightAngledQuotePattern, |
| 220 '>smarty-left-angle-quotes' |
| 221 ) |
| 222 |
| 223 def educateQuotes(self, md): |
| 224 lsquo = self.substitutions['left-single-quote'] |
| 225 rsquo = self.substitutions['right-single-quote'] |
| 226 ldquo = self.substitutions['left-double-quote'] |
| 227 rdquo = self.substitutions['right-double-quote'] |
| 228 patterns = ( |
| 229 (singleQuoteStartRe, (rsquo,)), |
| 230 (doubleQuoteStartRe, (rdquo,)), |
| 231 (doubleQuoteSetsRe, (ldquo + lsquo,)), |
| 232 (singleQuoteSetsRe, (lsquo + ldquo,)), |
| 233 (decadeAbbrRe, (rsquo,)), |
| 234 (openingSingleQuotesRegex, (2, lsquo)), |
| 235 (closingSingleQuotesRegex, (rsquo,)), |
| 236 (closingSingleQuotesRegex2, (rsquo, 2)), |
| 237 (remainingSingleQuotesRegex, (lsquo,)), |
| 238 (openingDoubleQuotesRegex, (2, ldquo)), |
| 239 (closingDoubleQuotesRegex, (rdquo,)), |
| 240 (closingDoubleQuotesRegex2, (rdquo,)), |
| 241 (remainingDoubleQuotesRegex, (ldquo,)) |
| 242 ) |
| 243 self._addPatterns(md, patterns, 'quotes') |
| 244 |
| 245 def extendMarkdown(self, md, md_globals): |
| 246 configs = self.getConfigs() |
| 247 self.inlinePatterns = OrderedDict() |
| 248 if configs['smart_ellipses']: |
| 249 self.educateEllipses(md) |
| 250 if configs['smart_quotes']: |
| 251 self.educateQuotes(md) |
| 252 if configs['smart_angled_quotes']: |
| 253 self.educateAngledQuotes(md) |
| 254 if configs['smart_dashes']: |
| 255 self.educateDashes(md) |
| 256 inlineProcessor = InlineProcessor(md) |
| 257 inlineProcessor.inlinePatterns = self.inlinePatterns |
| 258 md.treeprocessors.add('smarty', inlineProcessor, '_end') |
| 259 md.ESCAPED_CHARS.extend(['"', "'"]) |
| 260 |
| 261 |
| 262 def makeExtension(*args, **kwargs): |
| 263 return SmartyExtension(*args, **kwargs) |
OLD | NEW |