OLD | NEW |
| (Empty) |
1 """ | |
2 Attribute List Extension for Python-Markdown | |
3 ============================================ | |
4 | |
5 Adds attribute list syntax. Inspired by | |
6 [maruku](http://maruku.rubyforge.org/proposal.html#attribute_lists)'s | |
7 feature of the same name. | |
8 | |
9 Copyright 2011 [Waylan Limberg](http://achinghead.com/). | |
10 | |
11 Contact: markdown@freewisdom.org | |
12 | |
13 License: BSD (see ../LICENSE.md for details) | |
14 | |
15 Dependencies: | |
16 * [Python 2.4+](http://python.org) | |
17 * [Markdown 2.1+](http://packages.python.org/Markdown/) | |
18 | |
19 """ | |
20 | |
21 from __future__ import absolute_import | |
22 from __future__ import unicode_literals | |
23 from . import Extension | |
24 from ..treeprocessors import Treeprocessor | |
25 from ..util import isBlockLevel | |
26 import re | |
27 | |
28 try: | |
29 Scanner = re.Scanner | |
30 except AttributeError: | |
31 # must be on Python 2.4 | |
32 from sre import Scanner | |
33 | |
34 def _handle_double_quote(s, t): | |
35 k, v = t.split('=') | |
36 return k, v.strip('"') | |
37 | |
38 def _handle_single_quote(s, t): | |
39 k, v = t.split('=') | |
40 return k, v.strip("'") | |
41 | |
42 def _handle_key_value(s, t): | |
43 return t.split('=') | |
44 | |
45 def _handle_word(s, t): | |
46 if t.startswith('.'): | |
47 return '.', t[1:] | |
48 if t.startswith('#'): | |
49 return 'id', t[1:] | |
50 return t, t | |
51 | |
52 _scanner = Scanner([ | |
53 (r'[^ ]+=".*?"', _handle_double_quote), | |
54 (r"[^ ]+='.*?'", _handle_single_quote), | |
55 (r'[^ ]+=[^ ]*', _handle_key_value), | |
56 (r'[^ ]+', _handle_word), | |
57 (r' ', None) | |
58 ]) | |
59 | |
60 def get_attrs(str): | |
61 """ Parse attribute list and return a list of attribute tuples. """ | |
62 return _scanner.scan(str)[0] | |
63 | |
64 def isheader(elem): | |
65 return elem.tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] | |
66 | |
67 class AttrListTreeprocessor(Treeprocessor): | |
68 | |
69 BASE_RE = r'\{\:?([^\}]*)\}' | |
70 HEADER_RE = re.compile(r'[ ]*%s[ ]*$' % BASE_RE) | |
71 BLOCK_RE = re.compile(r'\n[ ]*%s[ ]*$' % BASE_RE) | |
72 INLINE_RE = re.compile(r'^%s' % BASE_RE) | |
73 NAME_RE = re.compile(r'[^A-Z_a-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff\u037
0-\u037d' | |
74 r'\u037f-\u1fff\u200c-\u200d\u2070-\u218f\u2c00-\u2fef' | |
75 r'\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd' | |
76 r'\:\-\.0-9\u00b7\u0300-\u036f\u203f-\u2040]+') | |
77 | |
78 def run(self, doc): | |
79 for elem in doc.getiterator(): | |
80 if isBlockLevel(elem.tag): | |
81 # Block level: check for attrs on last line of text | |
82 RE = self.BLOCK_RE | |
83 if isheader(elem): | |
84 # header: check for attrs at end of line | |
85 RE = self.HEADER_RE | |
86 if len(elem) and elem[-1].tail: | |
87 # has children. Get from tail of last child | |
88 m = RE.search(elem[-1].tail) | |
89 if m: | |
90 self.assign_attrs(elem, m.group(1)) | |
91 elem[-1].tail = elem[-1].tail[:m.start()] | |
92 if isheader(elem): | |
93 # clean up trailing #s | |
94 elem[-1].tail = elem[-1].tail.rstrip('#').rstrip() | |
95 elif elem.text: | |
96 # no children. Get from text. | |
97 m = RE.search(elem.text) | |
98 if m: | |
99 self.assign_attrs(elem, m.group(1)) | |
100 elem.text = elem.text[:m.start()] | |
101 if isheader(elem): | |
102 # clean up trailing #s | |
103 elem.text = elem.text.rstrip('#').rstrip() | |
104 else: | |
105 # inline: check for attrs at start of tail | |
106 if elem.tail: | |
107 m = self.INLINE_RE.match(elem.tail) | |
108 if m: | |
109 self.assign_attrs(elem, m.group(1)) | |
110 elem.tail = elem.tail[m.end():] | |
111 | |
112 def assign_attrs(self, elem, attrs): | |
113 """ Assign attrs to element. """ | |
114 for k, v in get_attrs(attrs): | |
115 if k == '.': | |
116 # add to class | |
117 cls = elem.get('class') | |
118 if cls: | |
119 elem.set('class', '%s %s' % (cls, v)) | |
120 else: | |
121 elem.set('class', v) | |
122 else: | |
123 # assign attr k with v | |
124 elem.set(self.sanitize_name(k), v) | |
125 | |
126 def sanitize_name(self, name): | |
127 """ | |
128 Sanitize name as 'an XML Name, minus the ":"'. | |
129 See http://www.w3.org/TR/REC-xml-names/#NT-NCName | |
130 """ | |
131 return self.NAME_RE.sub('_', name) | |
132 | |
133 | |
134 class AttrListExtension(Extension): | |
135 def extendMarkdown(self, md, md_globals): | |
136 md.treeprocessors.add('attr_list', AttrListTreeprocessor(md), '>prettify
') | |
137 | |
138 | |
139 def makeExtension(configs={}): | |
140 return AttrListExtension(configs=configs) | |
OLD | NEW |