reference.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import logging
  2. from ..common.utils import charCodeAt, isSpace, normalizeReference
  3. from .state_block import StateBlock
  4. LOGGER = logging.getLogger(__name__)
  5. def reference(state: StateBlock, startLine: int, _endLine: int, silent: bool) -> bool:
  6. LOGGER.debug(
  7. "entering reference: %s, %s, %s, %s", state, startLine, _endLine, silent
  8. )
  9. lines = 0
  10. pos = state.bMarks[startLine] + state.tShift[startLine]
  11. maximum = state.eMarks[startLine]
  12. nextLine = startLine + 1
  13. if state.is_code_block(startLine):
  14. return False
  15. if state.src[pos] != "[":
  16. return False
  17. # Simple check to quickly interrupt scan on [link](url) at the start of line.
  18. # Can be useful on practice: https:#github.com/markdown-it/markdown-it/issues/54
  19. while pos < maximum:
  20. # /* ] */ /* \ */ /* : */
  21. if state.src[pos] == "]" and state.src[pos - 1] != "\\":
  22. if pos + 1 == maximum:
  23. return False
  24. if state.src[pos + 1] != ":":
  25. return False
  26. break
  27. pos += 1
  28. endLine = state.lineMax
  29. # jump line-by-line until empty one or EOF
  30. terminatorRules = state.md.block.ruler.getRules("reference")
  31. oldParentType = state.parentType
  32. state.parentType = "reference"
  33. while nextLine < endLine and not state.isEmpty(nextLine):
  34. # this would be a code block normally, but after paragraph
  35. # it's considered a lazy continuation regardless of what's there
  36. if state.sCount[nextLine] - state.blkIndent > 3:
  37. nextLine += 1
  38. continue
  39. # quirk for blockquotes, this line should already be checked by that rule
  40. if state.sCount[nextLine] < 0:
  41. nextLine += 1
  42. continue
  43. # Some tags can terminate paragraph without empty line.
  44. terminate = False
  45. for terminatorRule in terminatorRules:
  46. if terminatorRule(state, nextLine, endLine, True):
  47. terminate = True
  48. break
  49. if terminate:
  50. break
  51. nextLine += 1
  52. string = state.getLines(startLine, nextLine, state.blkIndent, False).strip()
  53. maximum = len(string)
  54. labelEnd = None
  55. pos = 1
  56. while pos < maximum:
  57. ch = charCodeAt(string, pos)
  58. if ch == 0x5B: # /* [ */
  59. return False
  60. elif ch == 0x5D: # /* ] */
  61. labelEnd = pos
  62. break
  63. elif ch == 0x0A: # /* \n */
  64. lines += 1
  65. elif ch == 0x5C: # /* \ */
  66. pos += 1
  67. if pos < maximum and charCodeAt(string, pos) == 0x0A:
  68. lines += 1
  69. pos += 1
  70. if (
  71. labelEnd is None or labelEnd < 0 or charCodeAt(string, labelEnd + 1) != 0x3A
  72. ): # /* : */
  73. return False
  74. # [label]: destination 'title'
  75. # ^^^ skip optional whitespace here
  76. pos = labelEnd + 2
  77. while pos < maximum:
  78. ch = charCodeAt(string, pos)
  79. if ch == 0x0A:
  80. lines += 1
  81. elif isSpace(ch):
  82. pass
  83. else:
  84. break
  85. pos += 1
  86. # [label]: destination 'title'
  87. # ^^^^^^^^^^^ parse this
  88. res = state.md.helpers.parseLinkDestination(string, pos, maximum)
  89. if not res.ok:
  90. return False
  91. href = state.md.normalizeLink(res.str)
  92. if not state.md.validateLink(href):
  93. return False
  94. pos = res.pos
  95. lines += res.lines
  96. # save cursor state, we could require to rollback later
  97. destEndPos = pos
  98. destEndLineNo = lines
  99. # [label]: destination 'title'
  100. # ^^^ skipping those spaces
  101. start = pos
  102. while pos < maximum:
  103. ch = charCodeAt(string, pos)
  104. if ch == 0x0A:
  105. lines += 1
  106. elif isSpace(ch):
  107. pass
  108. else:
  109. break
  110. pos += 1
  111. # [label]: destination 'title'
  112. # ^^^^^^^ parse this
  113. res = state.md.helpers.parseLinkTitle(string, pos, maximum)
  114. if pos < maximum and start != pos and res.ok:
  115. title = res.str
  116. pos = res.pos
  117. lines += res.lines
  118. else:
  119. title = ""
  120. pos = destEndPos
  121. lines = destEndLineNo
  122. # skip trailing spaces until the rest of the line
  123. while pos < maximum:
  124. ch = charCodeAt(string, pos)
  125. if not isSpace(ch):
  126. break
  127. pos += 1
  128. if pos < maximum and charCodeAt(string, pos) != 0x0A and title:
  129. # garbage at the end of the line after title,
  130. # but it could still be a valid reference if we roll back
  131. title = ""
  132. pos = destEndPos
  133. lines = destEndLineNo
  134. while pos < maximum:
  135. ch = charCodeAt(string, pos)
  136. if not isSpace(ch):
  137. break
  138. pos += 1
  139. if pos < maximum and charCodeAt(string, pos) != 0x0A:
  140. # garbage at the end of the line
  141. return False
  142. label = normalizeReference(string[1:labelEnd])
  143. if not label:
  144. # CommonMark 0.20 disallows empty labels
  145. return False
  146. # Reference can not terminate anything. This check is for safety only.
  147. if silent:
  148. return True
  149. if "references" not in state.env:
  150. state.env["references"] = {}
  151. state.line = startLine + lines + 1
  152. # note, this is not part of markdown-it JS, but is useful for renderers
  153. if state.md.options.get("inline_definitions", False):
  154. token = state.push("definition", "", 0)
  155. token.meta = {
  156. "id": label,
  157. "title": title,
  158. "url": href,
  159. "label": string[1:labelEnd],
  160. }
  161. token.map = [startLine, state.line]
  162. if label not in state.env["references"]:
  163. state.env["references"][label] = {
  164. "title": title,
  165. "href": href,
  166. "map": [startLine, state.line],
  167. }
  168. else:
  169. state.env.setdefault("duplicate_refs", []).append(
  170. {
  171. "title": title,
  172. "href": href,
  173. "label": label,
  174. "map": [startLine, state.line],
  175. }
  176. )
  177. state.parentType = oldParentType
  178. return True