codekingpro/portable-devtools
114k
1from __future__ import annotations2 3from typing import TYPE_CHECKING, Literal4 5from ..common.utils import isStrSpace6from ..ruler import StateBase7from ..token import Token8from ..utils import EnvType9 10if TYPE_CHECKING:11 from markdown_it.main import MarkdownIt12 13 14class StateBlock(StateBase):15 def __init__(16 self, src: str, md: MarkdownIt, env: EnvType, tokens: list[Token]17 ) -> None:18 self.src = src19 20 # link to parser instance21 self.md = md22 23 self.env = env24 25 #26 # Internal state variables27 #28 29 self.tokens = tokens30 31 self.bMarks: list[int] = [] # line begin offsets for fast jumps32 self.eMarks: list[int] = [] # line end offsets for fast jumps33 # offsets of the first non-space characters (tabs not expanded)34 self.tShift: list[int] = []35 self.sCount: list[int] = [] # indents for each line (tabs expanded)36 37 # An amount of virtual spaces (tabs expanded) between beginning38 # of each line (bMarks) and real beginning of that line.39 #40 # It exists only as a hack because blockquotes override bMarks41 # losing information in the process.42 #43 # It's used only when expanding tabs, you can think about it as44 # an initial tab length, e.g. bsCount=21 applied to string `\t123`45 # means first tab should be expanded to 4-21%4 === 3 spaces.46 #47 self.bsCount: list[int] = []48 49 # block parser variables50 self.blkIndent = 0 # required block content indent (for example, if we are51 # inside a list, it would be positioned after list marker)52 self.line = 0 # line index in src53 self.lineMax = 0 # lines count54 self.tight = False # loose/tight mode for lists55 self.ddIndent = -1 # indent of the current dd block (-1 if there isn't any)56 self.listIndent = -1 # indent of the current list block (-1 if there isn't any)57 58 # can be 'blockquote', 'list', 'root', 'paragraph' or 'reference'59 # used in lists to determine if they interrupt a paragraph60 self.parentType = "root"61 62 self.level = 063 64 # renderer65 self.result = ""66 67 # Create caches68 # Generate markers.69 indent_found = False70 71 start = pos = indent = offset = 072 length = len(self.src)73 74 for pos, character in enumerate(self.src):75 if not indent_found:76 if isStrSpace(character):77 indent += 178 79 if character == "\t":80 offset += 4 - offset % 481 else:82 offset += 183 continue84 else:85 indent_found = True86 87 if character == "\n" or pos == length - 1:88 if character != "\n":89 pos += 190 self.bMarks.append(start)91 self.eMarks.append(pos)92 self.tShift.append(indent)93 self.sCount.append(offset)94 self.bsCount.append(0)95 96 indent_found = False97 indent = 098 offset = 099 start = pos + 1100 101 # Push fake entry to simplify cache bounds checks102 self.bMarks.append(length)103 self.eMarks.append(length)104 self.tShift.append(0)105 self.sCount.append(0)106 self.bsCount.append(0)107 108 self.lineMax = len(self.bMarks) - 1 # don't count last fake line109 110 # pre-check if code blocks are enabled, to speed up is_code_block method111 self._code_enabled = "code" in self.md["block"].ruler.get_active_rules()112 113 def __repr__(self) -> str:114 return (115 f"{self.__class__.__name__}"116 f"(line={self.line},level={self.level},tokens={len(self.tokens)})"117 )118 119 def push(self, ttype: str, tag: str, nesting: Literal[-1, 0, 1]) -> Token:120 """Push new token to "stream"."""121 token = Token(ttype, tag, nesting)122 token.block = True123 if nesting < 0:124 self.level -= 1 # closing tag125 token.level = self.level126 if nesting > 0:127 self.level += 1 # opening tag128 self.tokens.append(token)129 return token130 131 def isEmpty(self, line: int) -> bool:132 """."""133 return (self.bMarks[line] + self.tShift[line]) >= self.eMarks[line]134 135 def skipEmptyLines(self, from_pos: int) -> int:136 """."""137 while from_pos < self.lineMax:138 try:139 if (self.bMarks[from_pos] + self.tShift[from_pos]) < self.eMarks[140 from_pos141 ]:142 break143 except IndexError:144 pass145 from_pos += 1146 return from_pos147 148 def skipSpaces(self, pos: int) -> int:149 """Skip spaces from given position."""150 while True:151 try:152 current = self.src[pos]153 except IndexError:154 break155 if not isStrSpace(current):156 break157 pos += 1158 return pos159 160 def skipSpacesBack(self, pos: int, minimum: int) -> int:161 """Skip spaces from given position in reverse."""162 if pos <= minimum:163 return pos164 while pos > minimum:165 pos -= 1166 if not isStrSpace(self.src[pos]):167 return pos + 1168 return pos169 170 def skipChars(self, pos: int, code: int) -> int:171 """Skip character code from given position."""172 while True:173 try:174 current = self.srcCharCode[pos]175 except IndexError:176 break177 if current != code:178 break179 pos += 1180 return pos181 182 def skipCharsStr(self, pos: int, ch: str) -> int:183 """Skip character string from given position."""184 while True:185 try:186 current = self.src[pos]187 except IndexError:188 break189 if current != ch:190 break191 pos += 1192 return pos193 194 def skipCharsBack(self, pos: int, code: int, minimum: int) -> int:195 """Skip character code reverse from given position - 1."""196 if pos <= minimum:197 return pos198 while pos > minimum:199 pos -= 1200 if code != self.srcCharCode[pos]:201 return pos + 1202 return pos203 204 def skipCharsStrBack(self, pos: int, ch: str, minimum: int) -> int:205 """Skip character string reverse from given position - 1."""206 if pos <= minimum:207 return pos208 while pos > minimum:209 pos -= 1210 if ch != self.src[pos]:211 return pos + 1212 return pos213 214 def getLines(self, begin: int, end: int, indent: int, keepLastLF: bool) -> str:215 """Cut lines range from source."""216 line = begin217 if begin >= end:218 return ""219 220 queue = [""] * (end - begin)221 222 i = 1223 while line < end:224 lineIndent = 0225 lineStart = first = self.bMarks[line]226 last = (227 self.eMarks[line] + 1228 if line + 1 < end or keepLastLF229 else self.eMarks[line]230 )231 232 while (first < last) and (lineIndent < indent):233 ch = self.src[first]234 if isStrSpace(ch):235 if ch == "\t":236 lineIndent += 4 - (lineIndent + self.bsCount[line]) % 4237 else:238 lineIndent += 1239 elif first - lineStart < self.tShift[line]:240 lineIndent += 1241 else:242 break243 first += 1244 245 if lineIndent > indent:246 # partially expanding tabs in code blocks, e.g '\t\tfoobar'247 # with indent=2 becomes ' \tfoobar'248 queue[i - 1] = (" " * (lineIndent - indent)) + self.src[first:last]249 else:250 queue[i - 1] = self.src[first:last]251 252 line += 1253 i += 1254 255 return "".join(queue)256 257 def is_code_block(self, line: int) -> bool:258 """Check if line is a code block,259 i.e. the code block rule is enabled and text is indented by more than 3 spaces.260 """261 return self._code_enabled and (self.sCount[line] - self.blkIndent) >= 4262 