blockquote.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. # Block quotes
  2. from __future__ import annotations
  3. import logging
  4. from ..common.utils import isStrSpace
  5. from .state_block import StateBlock
  6. LOGGER = logging.getLogger(__name__)
  7. def blockquote(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
  8. LOGGER.debug(
  9. "entering blockquote: %s, %s, %s, %s", state, startLine, endLine, silent
  10. )
  11. oldLineMax = state.lineMax
  12. pos = state.bMarks[startLine] + state.tShift[startLine]
  13. max = state.eMarks[startLine]
  14. if state.is_code_block(startLine):
  15. return False
  16. # check the block quote marker
  17. try:
  18. if state.src[pos] != ">":
  19. return False
  20. except IndexError:
  21. return False
  22. pos += 1
  23. # we know that it's going to be a valid blockquote,
  24. # so no point trying to find the end of it in silent mode
  25. if silent:
  26. return True
  27. # set offset past spaces and ">"
  28. initial = offset = state.sCount[startLine] + 1
  29. try:
  30. second_char: str | None = state.src[pos]
  31. except IndexError:
  32. second_char = None
  33. # skip one optional space after '>'
  34. if second_char == " ":
  35. # ' > test '
  36. # ^ -- position start of line here:
  37. pos += 1
  38. initial += 1
  39. offset += 1
  40. adjustTab = False
  41. spaceAfterMarker = True
  42. elif second_char == "\t":
  43. spaceAfterMarker = True
  44. if (state.bsCount[startLine] + offset) % 4 == 3:
  45. # ' >\t test '
  46. # ^ -- position start of line here (tab has width==1)
  47. pos += 1
  48. initial += 1
  49. offset += 1
  50. adjustTab = False
  51. else:
  52. # ' >\t test '
  53. # ^ -- position start of line here + shift bsCount slightly
  54. # to make extra space appear
  55. adjustTab = True
  56. else:
  57. spaceAfterMarker = False
  58. oldBMarks = [state.bMarks[startLine]]
  59. state.bMarks[startLine] = pos
  60. while pos < max:
  61. ch = state.src[pos]
  62. if isStrSpace(ch):
  63. if ch == "\t":
  64. offset += (
  65. 4
  66. - (offset + state.bsCount[startLine] + (1 if adjustTab else 0)) % 4
  67. )
  68. else:
  69. offset += 1
  70. else:
  71. break
  72. pos += 1
  73. oldBSCount = [state.bsCount[startLine]]
  74. state.bsCount[startLine] = (
  75. state.sCount[startLine] + 1 + (1 if spaceAfterMarker else 0)
  76. )
  77. lastLineEmpty = pos >= max
  78. oldSCount = [state.sCount[startLine]]
  79. state.sCount[startLine] = offset - initial
  80. oldTShift = [state.tShift[startLine]]
  81. state.tShift[startLine] = pos - state.bMarks[startLine]
  82. terminatorRules = state.md.block.ruler.getRules("blockquote")
  83. oldParentType = state.parentType
  84. state.parentType = "blockquote"
  85. # Search the end of the block
  86. #
  87. # Block ends with either:
  88. # 1. an empty line outside:
  89. # ```
  90. # > test
  91. #
  92. # ```
  93. # 2. an empty line inside:
  94. # ```
  95. # >
  96. # test
  97. # ```
  98. # 3. another tag:
  99. # ```
  100. # > test
  101. # - - -
  102. # ```
  103. # for (nextLine = startLine + 1; nextLine < endLine; nextLine++) {
  104. nextLine = startLine + 1
  105. while nextLine < endLine:
  106. # check if it's outdented, i.e. it's inside list item and indented
  107. # less than said list item:
  108. #
  109. # ```
  110. # 1. anything
  111. # > current blockquote
  112. # 2. checking this line
  113. # ```
  114. isOutdented = state.sCount[nextLine] < state.blkIndent
  115. pos = state.bMarks[nextLine] + state.tShift[nextLine]
  116. max = state.eMarks[nextLine]
  117. if pos >= max:
  118. # Case 1: line is not inside the blockquote, and this line is empty.
  119. break
  120. evaluatesTrue = state.src[pos] == ">" and not isOutdented
  121. pos += 1
  122. if evaluatesTrue:
  123. # This line is inside the blockquote.
  124. # set offset past spaces and ">"
  125. initial = offset = state.sCount[nextLine] + 1
  126. try:
  127. next_char: str | None = state.src[pos]
  128. except IndexError:
  129. next_char = None
  130. # skip one optional space after '>'
  131. if next_char == " ":
  132. # ' > test '
  133. # ^ -- position start of line here:
  134. pos += 1
  135. initial += 1
  136. offset += 1
  137. adjustTab = False
  138. spaceAfterMarker = True
  139. elif next_char == "\t":
  140. spaceAfterMarker = True
  141. if (state.bsCount[nextLine] + offset) % 4 == 3:
  142. # ' >\t test '
  143. # ^ -- position start of line here (tab has width==1)
  144. pos += 1
  145. initial += 1
  146. offset += 1
  147. adjustTab = False
  148. else:
  149. # ' >\t test '
  150. # ^ -- position start of line here + shift bsCount slightly
  151. # to make extra space appear
  152. adjustTab = True
  153. else:
  154. spaceAfterMarker = False
  155. oldBMarks.append(state.bMarks[nextLine])
  156. state.bMarks[nextLine] = pos
  157. while pos < max:
  158. ch = state.src[pos]
  159. if isStrSpace(ch):
  160. if ch == "\t":
  161. offset += (
  162. 4
  163. - (
  164. offset
  165. + state.bsCount[nextLine]
  166. + (1 if adjustTab else 0)
  167. )
  168. % 4
  169. )
  170. else:
  171. offset += 1
  172. else:
  173. break
  174. pos += 1
  175. lastLineEmpty = pos >= max
  176. oldBSCount.append(state.bsCount[nextLine])
  177. state.bsCount[nextLine] = (
  178. state.sCount[nextLine] + 1 + (1 if spaceAfterMarker else 0)
  179. )
  180. oldSCount.append(state.sCount[nextLine])
  181. state.sCount[nextLine] = offset - initial
  182. oldTShift.append(state.tShift[nextLine])
  183. state.tShift[nextLine] = pos - state.bMarks[nextLine]
  184. nextLine += 1
  185. continue
  186. # Case 2: line is not inside the blockquote, and the last line was empty.
  187. if lastLineEmpty:
  188. break
  189. # Case 3: another tag found.
  190. terminate = False
  191. for terminatorRule in terminatorRules:
  192. if terminatorRule(state, nextLine, endLine, True):
  193. terminate = True
  194. break
  195. if terminate:
  196. # Quirk to enforce "hard termination mode" for paragraphs;
  197. # normally if you call `tokenize(state, startLine, nextLine)`,
  198. # paragraphs will look below nextLine for paragraph continuation,
  199. # but if blockquote is terminated by another tag, they shouldn't
  200. state.lineMax = nextLine
  201. if state.blkIndent != 0:
  202. # state.blkIndent was non-zero, we now set it to zero,
  203. # so we need to re-calculate all offsets to appear as
  204. # if indent wasn't changed
  205. oldBMarks.append(state.bMarks[nextLine])
  206. oldBSCount.append(state.bsCount[nextLine])
  207. oldTShift.append(state.tShift[nextLine])
  208. oldSCount.append(state.sCount[nextLine])
  209. state.sCount[nextLine] -= state.blkIndent
  210. break
  211. oldBMarks.append(state.bMarks[nextLine])
  212. oldBSCount.append(state.bsCount[nextLine])
  213. oldTShift.append(state.tShift[nextLine])
  214. oldSCount.append(state.sCount[nextLine])
  215. # A negative indentation means that this is a paragraph continuation
  216. #
  217. state.sCount[nextLine] = -1
  218. nextLine += 1
  219. oldIndent = state.blkIndent
  220. state.blkIndent = 0
  221. token = state.push("blockquote_open", "blockquote", 1)
  222. token.markup = ">"
  223. token.map = lines = [startLine, 0]
  224. state.md.block.tokenize(state, startLine, nextLine)
  225. token = state.push("blockquote_close", "blockquote", -1)
  226. token.markup = ">"
  227. state.lineMax = oldLineMax
  228. state.parentType = oldParentType
  229. lines[1] = state.line
  230. # Restore original tShift; this might not be necessary since the parser
  231. # has already been here, but just to make sure we can do that.
  232. for i, item in enumerate(oldTShift):
  233. state.bMarks[i + startLine] = oldBMarks[i]
  234. state.tShift[i + startLine] = item
  235. state.sCount[i + startLine] = oldSCount[i]
  236. state.bsCount[i + startLine] = oldBSCount[i]
  237. state.blkIndent = oldIndent
  238. return True