Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/markdown_it/rules_inline/state_inline.py: 93%

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

109 statements  

1from __future__ import annotations 

2 

3from dataclasses import dataclass 

4from typing import TYPE_CHECKING, Any, Literal, NamedTuple 

5 

6from ..common.utils import isMdAsciiPunct, isPunctChar, isWhiteSpace 

7from ..ruler import StateBase 

8from ..token import Token 

9from ..utils import EnvType 

10 

11if TYPE_CHECKING: 

12 from markdown_it import MarkdownIt 

13 

14 

15@dataclass(slots=True) 

16class Delimiter: 

17 # Char code of the starting marker (number). 

18 marker: int 

19 

20 # Total length of these series of delimiters. 

21 length: int 

22 

23 # A position of the token this delimiter corresponds to. 

24 token: int 

25 

26 # If this delimiter is matched as a valid opener, `end` will be 

27 # equal to its position, otherwise it's `-1`. 

28 end: int 

29 

30 # Boolean flags that determine if this delimiter could open or close 

31 # an emphasis. 

32 open: bool 

33 close: bool 

34 

35 level: bool | None = None 

36 

37 

38class Scanned(NamedTuple): 

39 can_open: bool 

40 can_close: bool 

41 length: int 

42 

43 

44class StateInline(StateBase): 

45 def __init__( 

46 self, src: str, md: MarkdownIt, env: EnvType, outTokens: list[Token] 

47 ) -> None: 

48 self.src = src 

49 self.env = env 

50 self.md = md 

51 self.tokens = outTokens 

52 self.tokens_meta: list[dict[str, Any] | None] = [None] * len(outTokens) 

53 

54 self.pos = 0 

55 self.posMax = len(self.src) 

56 self.level = 0 

57 # `pending` holds literal text not yet flushed to a token. It is 

58 # exposed as a plain `str` (see the property below), but is accumulated 

59 # through a list buffer so that appending one character at a time -- the 

60 # inline tokenizer's fallback path -- stays amortised O(1). Appending 

61 # to a `str` *attribute* cannot use CPython's in-place concatenation 

62 # optimisation (the attribute holds a second reference), so each `+=` 

63 # copies the whole string, making long runs of non-markup characters 

64 # quadratic. 

65 self._pending = "" 

66 self._pending_buffer: list[str] = [] 

67 self.pendingLevel = 0 

68 

69 # Stores { start: end } pairs. Useful for backtrack 

70 # optimization of pairs parse (emphasis, strikes). 

71 self.cache: dict[int, int] = {} 

72 

73 # List of emphasis-like delimiters for current tag 

74 self.delimiters: list[Delimiter] = [] 

75 

76 # Stack of delimiter lists for upper level tags 

77 self._prev_delimiters: list[list[Delimiter]] = [] 

78 

79 # backticklength => last seen position 

80 self.backticks: dict[int, int] = {} 

81 self.backticksScanned = False 

82 

83 # Counter used to disable inline linkify-it execution 

84 # inside <a> and markdown links 

85 self.linkLevel = 0 

86 

87 def __repr__(self) -> str: 

88 return ( 

89 f"{self.__class__.__name__}" 

90 f"(pos=[{self.pos} of {self.posMax}], token={len(self.tokens)})" 

91 ) 

92 

93 @property 

94 def pending(self) -> str: 

95 """Literal text accumulated so far, but not yet flushed to a token.""" 

96 buffer = self._pending_buffer 

97 if buffer: 

98 # Move the string into a local and drop the instance's reference 

99 # before concatenating. With a single reference left, CPython 

100 # resizes the string in place (amortised O(new chars)) rather 

101 # than copying it, so a rule that reads `pending` on every 

102 # character (e.g. an attribute-syntax plugin) stays linear. 

103 text = self._pending 

104 self._pending = "" 

105 text += "".join(buffer) 

106 buffer.clear() 

107 self._pending = text 

108 return self._pending 

109 

110 @pending.setter 

111 def pending(self, value: str) -> None: 

112 self._pending = value 

113 # Assign rather than `.clear()` so the setter also works on an 

114 # instance whose `__init__` has not run yet (subclasses that set 

115 # `pending` before calling `super().__init__()`), and so a copied 

116 # state never shares a buffer with its original. 

117 self._pending_buffer = [] 

118 

119 def __copy__(self) -> StateInline: 

120 """Shallow copy that does not share the pending text buffer.""" 

121 text = self.pending # materialise (and clear) our own buffer first 

122 new = self.__class__.__new__(self.__class__) 

123 new.__dict__.update(self.__dict__) 

124 new._pending = text 

125 new._pending_buffer = [] 

126 return new 

127 

128 def append_pending(self, text: str) -> None: 

129 """Append literal text to `pending`, in amortised O(1) time. 

130 

131 Prefer this to ``state.pending += text`` on hot paths: it buffers the 

132 fragment rather than rebuilding the whole ``pending`` string per call. 

133 """ 

134 self._pending_buffer.append(text) 

135 

136 def pushPending(self) -> Token: 

137 token = Token("text", "", 0) 

138 token.content = self.pending 

139 token.level = self.pendingLevel 

140 self.tokens.append(token) 

141 self.pending = "" 

142 return token 

143 

144 def push(self, ttype: str, tag: str, nesting: Literal[-1, 0, 1]) -> Token: 

145 """Push new token to "stream". 

146 If pending text exists - flush it as text token 

147 """ 

148 if self.pending: 

149 self.pushPending() 

150 

151 token = Token(ttype, tag, nesting) 

152 token_meta = None 

153 

154 if nesting < 0: 

155 # closing tag 

156 self.level -= 1 

157 self.delimiters = self._prev_delimiters.pop() 

158 

159 token.level = self.level 

160 

161 if nesting > 0: 

162 # opening tag 

163 self.level += 1 

164 self._prev_delimiters.append(self.delimiters) 

165 self.delimiters = [] 

166 token_meta = {"delimiters": self.delimiters} 

167 

168 self.pendingLevel = self.level 

169 self.tokens.append(token) 

170 self.tokens_meta.append(token_meta) 

171 return token 

172 

173 def scanDelims(self, start: int, canSplitWord: bool) -> Scanned: 

174 """ 

175 Scan a sequence of emphasis-like markers, and determine whether 

176 it can start an emphasis sequence or end an emphasis sequence. 

177 

178 - start - position to scan from (it should point at a valid marker); 

179 - canSplitWord - determine if these markers can be found inside a word 

180 

181 """ 

182 pos = start 

183 maximum = self.posMax 

184 marker = self.src[start] 

185 

186 # treat beginning of the line as a whitespace 

187 lastChar = self.src[start - 1] if start > 0 else " " 

188 

189 while pos < maximum and self.src[pos] == marker: 

190 pos += 1 

191 

192 count = pos - start 

193 

194 # treat end of the line as a whitespace 

195 nextChar = self.src[pos] if pos < maximum else " " 

196 

197 isLastPunctChar = isMdAsciiPunct(ord(lastChar)) or isPunctChar(lastChar) 

198 isNextPunctChar = isMdAsciiPunct(ord(nextChar)) or isPunctChar(nextChar) 

199 

200 isLastWhiteSpace = isWhiteSpace(ord(lastChar)) 

201 isNextWhiteSpace = isWhiteSpace(ord(nextChar)) 

202 

203 left_flanking = not ( 

204 isNextWhiteSpace 

205 or (isNextPunctChar and not (isLastWhiteSpace or isLastPunctChar)) 

206 ) 

207 right_flanking = not ( 

208 isLastWhiteSpace 

209 or (isLastPunctChar and not (isNextWhiteSpace or isNextPunctChar)) 

210 ) 

211 

212 can_open = left_flanking and ( 

213 canSplitWord or (not right_flanking) or isLastPunctChar 

214 ) 

215 can_close = right_flanking and ( 

216 canSplitWord or (not left_flanking) or isNextPunctChar 

217 ) 

218 

219 return Scanned(can_open, can_close, count)