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 |