Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/markdown/extensions/attr_list.py: 99%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

102 statements  

1# Attribute List Extension for Python-Markdown 

2# ============================================ 

3 

4# Adds attribute list syntax. Inspired by 

5# [Maruku](http://maruku.rubyforge.org/proposal.html#attribute_lists)'s 

6# feature of the same name. 

7 

8# See https://Python-Markdown.github.io/extensions/attr_list 

9# for documentation. 

10 

11# Original code Copyright 2011 [Waylan Limberg](https://github.com/waylan). 

12 

13# All changes Copyright 2011-2014 The Python Markdown Project 

14 

15# License: [BSD](https://opensource.org/licenses/bsd-license.php) 

16 

17""" 

18Adds attribute list syntax to Python-Markdown. 

19Inspired by 

20[Maruku](http://maruku.rubyforge.org/proposal.html#attribute_lists)'s 

21feature of the same name. 

22 

23See the [documentation](https://Python-Markdown.github.io/extensions/attr_list) 

24for details. 

25""" 

26 

27from __future__ import annotations 

28from typing import TYPE_CHECKING 

29 

30from . import Extension 

31from ..treeprocessors import Treeprocessor 

32import re 

33 

34if TYPE_CHECKING: # pragma: no cover 

35 from xml.etree.ElementTree import Element 

36 

37 

38def _handle_double_quote(s, t): 

39 k, v = t.split('=', 1) 

40 return k, v.strip('"') 

41 

42 

43def _handle_single_quote(s, t): 

44 k, v = t.split('=', 1) 

45 return k, v.strip("'") 

46 

47 

48def _handle_key_value(s, t): 

49 return t.split('=', 1) 

50 

51 

52def _handle_word(s, t): 

53 if t.startswith('.'): 

54 return '.', t[1:] 

55 if t.startswith('#'): 

56 return 'id', t[1:] 

57 return t, t 

58 

59 

60_scanner = re.Scanner([ 

61 (r'[^ =}]+=".*?"', _handle_double_quote), 

62 (r"[^ =}]+='.*?'", _handle_single_quote), 

63 (r'[^ =}]+=[^ =}]+', _handle_key_value), 

64 (r'[^ =}]+', _handle_word), 

65 (r' ', None) 

66]) 

67 

68 

69def get_attrs_and_remainder(attrs_string: str) -> tuple[list[tuple[str, str]], str]: 

70 """ Parse attribute list and return a list of attribute tuples. 

71 

72 Additionally, return any text that remained after a curly brace. In typical cases, its presence 

73 should mean that the input does not match the intended attribute list syntax. 

74 """ 

75 attrs, remainder = _scanner.scan(attrs_string) 

76 # To keep historic behavior, discard all unparsable text prior to '}'. 

77 index = remainder.find('}') 

78 remainder = remainder[index:] if index != -1 else '' 

79 return attrs, remainder 

80 

81 

82def get_attrs(str: str) -> list[tuple[str, str]]: # pragma: no cover 

83 """ Soft-deprecated. Prefer `get_attrs_and_remainder`. """ 

84 return get_attrs_and_remainder(str)[0] 

85 

86 

87def isheader(elem: Element) -> bool: 

88 return elem.tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] 

89 

90 

91NAME_RE = re.compile(r'[^A-Z_a-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff' 

92 r'\u0370-\u037d\u037f-\u1fff\u200c-\u200d' 

93 r'\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff' 

94 r'\uf900-\ufdcf\ufdf0-\ufffd' 

95 r'\:\-\.0-9\u00b7\u0300-\u036f\u203f-\u2040]+') 

96 

97 

98def sanitize_name(name: str) -> str: 

99 """ 

100 Sanitize name as 'an XML Name, minus the `:`.' 

101 See <https://www.w3.org/TR/REC-xml-names/#NT-NCName>. 

102 """ 

103 return NAME_RE.sub('_', name) 

104 

105 

106class AttrListTreeprocessor(Treeprocessor): 

107 

108 BASE_RE = r'\{\:?[ ]*([^\}\n ][^\n]*)[ ]*\}' 

109 HEADER_RE = re.compile(r'[ ]+{}[ ]*$'.format(BASE_RE)) 

110 BLOCK_RE = re.compile(r'\n[ ]*{}[ ]*$'.format(BASE_RE)) 

111 INLINE_RE = re.compile(r'^{}'.format(BASE_RE)) 

112 NAME_RE = NAME_RE 

113 

114 def run(self, doc: Element) -> None: 

115 for elem in doc.iter(): 

116 if self.md.is_block_level(elem.tag): 

117 # Block level: check for `attrs` on last line of text 

118 RE = self.BLOCK_RE 

119 if isheader(elem) or elem.tag in ['dt', 'td', 'th']: 

120 # header, def-term, or table cell: check for attributes at end of element 

121 RE = self.HEADER_RE 

122 if len(elem) and elem.tag == 'li': 

123 # special case list items. children may include a `ul` or `ol`. 

124 pos = None 

125 # find the `ul` or `ol` position 

126 for i, child in enumerate(elem): 

127 if child.tag in ['ul', 'ol']: 

128 pos = i 

129 break 

130 if pos is None and elem[-1].tail: 

131 # use tail of last child. no `ul` or `ol`. 

132 m = RE.search(elem[-1].tail) 

133 if m: 

134 if not self.assign_attrs(elem, m.group(1), strict=True): 

135 elem[-1].tail = elem[-1].tail[:m.start()] 

136 elif pos is not None and pos > 0 and elem[pos-1].tail: 

137 # use tail of last child before `ul` or `ol` 

138 m = RE.search(elem[pos-1].tail) 

139 if m: 

140 if not self.assign_attrs(elem, m.group(1), strict=True): 

141 elem[pos-1].tail = elem[pos-1].tail[:m.start()] 

142 elif elem.text: 

143 # use text. `ul` is first child. 

144 m = RE.search(elem.text) 

145 if m: 

146 if not self.assign_attrs(elem, m.group(1), strict=True): 

147 elem.text = elem.text[:m.start()] 

148 elif len(elem) and elem[-1].tail: 

149 # has children. Get from tail of last child 

150 m = RE.search(elem[-1].tail) 

151 if m: 

152 if not self.assign_attrs(elem, m.group(1), strict=True): 

153 elem[-1].tail = elem[-1].tail[:m.start()] 

154 if isheader(elem): 

155 # clean up trailing #s 

156 elem[-1].tail = elem[-1].tail.rstrip('#').rstrip() 

157 elif elem.text: 

158 # no children. Get from text. 

159 m = RE.search(elem.text) 

160 if m: 

161 if not self.assign_attrs(elem, m.group(1), strict=True): 

162 elem.text = elem.text[:m.start()] 

163 if isheader(elem): 

164 # clean up trailing #s 

165 elem.text = elem.text.rstrip('#').rstrip() 

166 else: 

167 # inline: check for `attrs` at start of tail 

168 if elem.tail: 

169 m = self.INLINE_RE.match(elem.tail) 

170 if m: 

171 remainder = self.assign_attrs(elem, m.group(1)) 

172 elem.tail = elem.tail[m.end():] + remainder 

173 

174 def assign_attrs(self, elem: Element, attrs_string: str, *, strict: bool = False) -> str: 

175 """ Assign `attrs` to element. 

176 

177 If the `attrs_string` has an extra closing curly brace, the remaining text is returned. 

178 

179 The `strict` argument controls whether to still assign `attrs` if there is a remaining `}`. 

180 """ 

181 attrs, remainder = get_attrs_and_remainder(attrs_string) 

182 if strict and remainder: 

183 return remainder 

184 

185 for k, v in attrs: 

186 if k == '.': 

187 # add to class 

188 cls = elem.get('class') 

189 if cls: 

190 elem.set('class', '{} {}'.format(cls, v)) 

191 else: 

192 elem.set('class', v) 

193 else: 

194 # assign attribute `k` with `v` 

195 elem.set(self.sanitize_name(k), v) 

196 # The text that we initially over-matched will be put back. 

197 return remainder 

198 

199 def sanitize_name(self, name: str) -> str: 

200 """ 

201 Sanitize name as 'an XML Name, minus the `:`.' 

202 See <https://www.w3.org/TR/REC-xml-names/#NT-NCName>. 

203 """ 

204 return sanitize_name(name) 

205 

206 

207class AttrListExtension(Extension): 

208 """ Attribute List extension for Python-Markdown """ 

209 def extendMarkdown(self, md): 

210 """ 

211 Register the processor. 

212 

213 | Class Instance | Registry | Name | Priority | 

214 | --------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------ | :------: | 

215 | [`AttrListTreeprocessor`][markdown.extensions.attr_list.AttrListTreeprocessor] | [`treeprocessors`][markdown.treeprocessors.build_treeprocessors] | `attr_list` | `8` | 

216 

217 """ 

218 # flake8: noqa: E501 202-204 

219 md.treeprocessors.register(AttrListTreeprocessor(md), 'attr_list', 8) 

220 md.registerExtension(self) 

221 

222 

223def makeExtension(**kwargs): # pragma: no cover 

224 return AttrListExtension(**kwargs)