Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/mdit_py_plugins/attrs/index.py: 39%

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

114 statements  

1from typing import List, Optional, Sequence 

2 

3from markdown_it import MarkdownIt 

4from markdown_it.rules_block import StateBlock 

5from markdown_it.rules_core import StateCore 

6from markdown_it.rules_inline import StateInline 

7from markdown_it.token import Token 

8 

9from mdit_py_plugins.utils import is_code_block 

10 

11from .parse import ParseError, parse 

12 

13 

14def attrs_plugin( 

15 md: MarkdownIt, 

16 *, 

17 after: Sequence[str] = ("image", "code_inline", "link_close", "span_close"), 

18 spans: bool = False, 

19 span_after: str = "link", 

20) -> None: 

21 """Parse inline attributes that immediately follow certain inline elements:: 

22 

23 ![alt](https://image.com){#id .a b=c} 

24 

25 This syntax is inspired by 

26 `Djot spans 

27 <https://htmlpreview.github.io/?https://github.com/jgm/djot/blob/master/doc/syntax.html#inline-attributes>`_. 

28 

29 Inside the curly braces, the following syntax is possible: 

30 

31 - `.foo` specifies foo as a class. 

32 Multiple classes may be given in this way; they will be combined. 

33 - `#foo` specifies foo as an identifier. 

34 An element may have only one identifier; 

35 if multiple identifiers are given, the last one is used. 

36 - `key="value"` or `key=value` specifies a key-value attribute. 

37 Quotes are not needed when the value consists entirely of 

38 ASCII alphanumeric characters or `_` or `:` or `-`. 

39 Backslash escapes may be used inside quoted values. 

40 - `%` begins a comment, which ends with the next `%` or the end of the attribute (`}`). 

41 

42 Multiple attribute blocks are merged. 

43 

44 :param md: The MarkdownIt instance to modify. 

45 :param after: The names of inline elements after which attributes may be specified. 

46 This plugin does not support attributes after emphasis, strikethrough or text elements, 

47 which all require post-parse processing. 

48 :param spans: If True, also parse attributes after spans of text, encapsulated by `[]`. 

49 Note Markdown link references take precedence over this syntax. 

50 :param span_after: The name of an inline rule after which spans may be specified. 

51 """ 

52 

53 def _attr_inline_rule(state: StateInline, silent: bool) -> bool: 

54 if state.pending or not state.tokens: 

55 return False 

56 token = state.tokens[-1] 

57 if token.type not in after: 

58 return False 

59 try: 

60 new_pos, attrs = parse(state.src[state.pos :]) 

61 except ParseError: 

62 return False 

63 token_index = _find_opening(state.tokens, len(state.tokens) - 1) 

64 if token_index is None: 

65 return False 

66 state.pos += new_pos + 1 

67 if not silent: 

68 attr_token = state.tokens[token_index] 

69 if "class" in attrs and "class" in token.attrs: 

70 attrs["class"] = f"{attr_token.attrs['class']} {attrs['class']}" 

71 attr_token.attrs.update(attrs) 

72 return True 

73 

74 if spans: 

75 md.inline.ruler.after(span_after, "span", _span_rule) 

76 if after: 

77 md.inline.ruler.push("attr", _attr_inline_rule) 

78 

79 

80def attrs_block_plugin(md: MarkdownIt) -> None: 

81 """Parse block attributes. 

82 

83 Block attributes are attributes on a single line, with no other content. 

84 They attach the specified attributes to the block below them:: 

85 

86 {.a #b c=1} 

87 A paragraph, that will be assigned the class ``a`` and the identifier ``b``. 

88 

89 Attributes can be stacked, with classes accumulating and lower attributes overriding higher:: 

90 

91 {#a .a c=1} 

92 {#b .b c=2} 

93 A paragraph, that will be assigned the class ``a b c``, and the identifier ``b``. 

94 

95 This syntax is inspired by Djot block attributes. 

96 """ 

97 md.block.ruler.before("fence", "attr", _attr_block_rule) 

98 md.core.ruler.after("block", "attr", _attr_resolve_block_rule) 

99 

100 

101def _find_opening(tokens: List[Token], index: int) -> Optional[int]: 

102 """Find the opening token index, if the token is closing.""" 

103 if tokens[index].nesting != -1: 

104 return index 

105 level = 0 

106 while index >= 0: 

107 level += tokens[index].nesting 

108 if level == 0: 

109 return index 

110 index -= 1 

111 return None 

112 

113 

114def _span_rule(state: StateInline, silent: bool) -> bool: 

115 if state.src[state.pos] != "[": 

116 return False 

117 

118 maximum = state.posMax 

119 labelStart = state.pos + 1 

120 labelEnd = state.md.helpers.parseLinkLabel(state, state.pos, False) 

121 

122 # parser failed to find ']', so it's not a valid span 

123 if labelEnd < 0: 

124 return False 

125 

126 pos = labelEnd + 1 

127 

128 # check not at end of inline 

129 if pos >= maximum: 

130 return False 

131 

132 try: 

133 new_pos, attrs = parse(state.src[pos:]) 

134 except ParseError: 

135 return False 

136 

137 pos += new_pos + 1 

138 

139 if not silent: 

140 state.pos = labelStart 

141 state.posMax = labelEnd 

142 token = state.push("span_open", "span", 1) 

143 token.attrs = attrs # type: ignore[assignment] 

144 state.md.inline.tokenize(state) 

145 token = state.push("span_close", "span", -1) 

146 

147 state.pos = pos 

148 state.posMax = maximum 

149 return True 

150 

151 

152def _attr_block_rule( 

153 state: StateBlock, startLine: int, endLine: int, silent: bool 

154) -> bool: 

155 """Find a block of attributes. 

156 

157 The block must be a single line that begins with a `{`, after three or less spaces, 

158 and end with a `}` followed by any number if spaces. 

159 """ 

160 if is_code_block(state, startLine): 

161 return False 

162 

163 pos = state.bMarks[startLine] + state.tShift[startLine] 

164 maximum = state.eMarks[startLine] 

165 

166 # if it doesn't start with a {, it's not an attribute block 

167 if state.src[pos] != "{": 

168 return False 

169 

170 # find first non-space character from the right 

171 while maximum > pos and state.src[maximum - 1] in (" ", "\t"): 

172 maximum -= 1 

173 # if it doesn't end with a }, it's not an attribute block 

174 if maximum <= pos: 

175 return False 

176 if state.src[maximum - 1] != "}": 

177 return False 

178 

179 try: 

180 new_pos, attrs = parse(state.src[pos:maximum]) 

181 except ParseError: 

182 return False 

183 

184 # if the block was resolved earlier than expected, it's not an attribute block 

185 # TODO this was not working in some instances, so I disabled it 

186 # if (maximum - 1) != new_pos: 

187 # return False 

188 

189 if silent: 

190 return True 

191 

192 token = state.push("attrs_block", "", 0) 

193 token.attrs = attrs # type: ignore[assignment] 

194 token.map = [startLine, startLine + 1] 

195 

196 state.line = startLine + 1 

197 return True 

198 

199 

200def _attr_resolve_block_rule(state: StateCore) -> None: 

201 """Find attribute block then move its attributes to the next block.""" 

202 i = 0 

203 len_tokens = len(state.tokens) 

204 while i < len_tokens: 

205 if state.tokens[i].type != "attrs_block": 

206 i += 1 

207 continue 

208 

209 if i + 1 < len_tokens: 

210 next_token = state.tokens[i + 1] 

211 

212 # classes are appended 

213 if "class" in state.tokens[i].attrs and "class" in next_token.attrs: 

214 state.tokens[i].attrs["class"] = ( 

215 f"{state.tokens[i].attrs['class']} {next_token.attrs['class']}" 

216 ) 

217 

218 if next_token.type == "attrs_block": 

219 # subsequent attribute blocks take precedence, when merging 

220 for key, value in state.tokens[i].attrs.items(): 

221 if key == "class" or key not in next_token.attrs: 

222 next_token.attrs[key] = value 

223 else: 

224 # attribute block takes precedence over attributes in other blocks 

225 next_token.attrs.update(state.tokens[i].attrs) 

226 

227 state.tokens.pop(i) 

228 len_tokens -= 1