123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- import logging
- from ..common.utils import charCodeAt, isSpace, normalizeReference
- from .state_block import StateBlock
- LOGGER = logging.getLogger(__name__)
- def reference(state: StateBlock, startLine: int, _endLine: int, silent: bool) -> bool:
- LOGGER.debug(
- "entering reference: %s, %s, %s, %s", state, startLine, _endLine, silent
- )
- lines = 0
- pos = state.bMarks[startLine] + state.tShift[startLine]
- maximum = state.eMarks[startLine]
- nextLine = startLine + 1
- if state.is_code_block(startLine):
- return False
- if state.src[pos] != "[":
- return False
- # Simple check to quickly interrupt scan on [link](url) at the start of line.
- # Can be useful on practice: https:#github.com/markdown-it/markdown-it/issues/54
- while pos < maximum:
- # /* ] */ /* \ */ /* : */
- if state.src[pos] == "]" and state.src[pos - 1] != "\\":
- if pos + 1 == maximum:
- return False
- if state.src[pos + 1] != ":":
- return False
- break
- pos += 1
- endLine = state.lineMax
- # jump line-by-line until empty one or EOF
- terminatorRules = state.md.block.ruler.getRules("reference")
- oldParentType = state.parentType
- state.parentType = "reference"
- while nextLine < endLine and not state.isEmpty(nextLine):
- # this would be a code block normally, but after paragraph
- # it's considered a lazy continuation regardless of what's there
- if state.sCount[nextLine] - state.blkIndent > 3:
- nextLine += 1
- continue
- # quirk for blockquotes, this line should already be checked by that rule
- if state.sCount[nextLine] < 0:
- nextLine += 1
- continue
- # Some tags can terminate paragraph without empty line.
- terminate = False
- for terminatorRule in terminatorRules:
- if terminatorRule(state, nextLine, endLine, True):
- terminate = True
- break
- if terminate:
- break
- nextLine += 1
- string = state.getLines(startLine, nextLine, state.blkIndent, False).strip()
- maximum = len(string)
- labelEnd = None
- pos = 1
- while pos < maximum:
- ch = charCodeAt(string, pos)
- if ch == 0x5B: # /* [ */
- return False
- elif ch == 0x5D: # /* ] */
- labelEnd = pos
- break
- elif ch == 0x0A: # /* \n */
- lines += 1
- elif ch == 0x5C: # /* \ */
- pos += 1
- if pos < maximum and charCodeAt(string, pos) == 0x0A:
- lines += 1
- pos += 1
- if (
- labelEnd is None or labelEnd < 0 or charCodeAt(string, labelEnd + 1) != 0x3A
- ): # /* : */
- return False
- # [label]: destination 'title'
- # ^^^ skip optional whitespace here
- pos = labelEnd + 2
- while pos < maximum:
- ch = charCodeAt(string, pos)
- if ch == 0x0A:
- lines += 1
- elif isSpace(ch):
- pass
- else:
- break
- pos += 1
- # [label]: destination 'title'
- # ^^^^^^^^^^^ parse this
- res = state.md.helpers.parseLinkDestination(string, pos, maximum)
- if not res.ok:
- return False
- href = state.md.normalizeLink(res.str)
- if not state.md.validateLink(href):
- return False
- pos = res.pos
- lines += res.lines
- # save cursor state, we could require to rollback later
- destEndPos = pos
- destEndLineNo = lines
- # [label]: destination 'title'
- # ^^^ skipping those spaces
- start = pos
- while pos < maximum:
- ch = charCodeAt(string, pos)
- if ch == 0x0A:
- lines += 1
- elif isSpace(ch):
- pass
- else:
- break
- pos += 1
- # [label]: destination 'title'
- # ^^^^^^^ parse this
- res = state.md.helpers.parseLinkTitle(string, pos, maximum)
- if pos < maximum and start != pos and res.ok:
- title = res.str
- pos = res.pos
- lines += res.lines
- else:
- title = ""
- pos = destEndPos
- lines = destEndLineNo
- # skip trailing spaces until the rest of the line
- while pos < maximum:
- ch = charCodeAt(string, pos)
- if not isSpace(ch):
- break
- pos += 1
- if pos < maximum and charCodeAt(string, pos) != 0x0A and title:
- # garbage at the end of the line after title,
- # but it could still be a valid reference if we roll back
- title = ""
- pos = destEndPos
- lines = destEndLineNo
- while pos < maximum:
- ch = charCodeAt(string, pos)
- if not isSpace(ch):
- break
- pos += 1
- if pos < maximum and charCodeAt(string, pos) != 0x0A:
- # garbage at the end of the line
- return False
- label = normalizeReference(string[1:labelEnd])
- if not label:
- # CommonMark 0.20 disallows empty labels
- return False
- # Reference can not terminate anything. This check is for safety only.
- if silent:
- return True
- if "references" not in state.env:
- state.env["references"] = {}
- state.line = startLine + lines + 1
- # note, this is not part of markdown-it JS, but is useful for renderers
- if state.md.options.get("inline_definitions", False):
- token = state.push("definition", "", 0)
- token.meta = {
- "id": label,
- "title": title,
- "url": href,
- "label": string[1:labelEnd],
- }
- token.map = [startLine, state.line]
- if label not in state.env["references"]:
- state.env["references"][label] = {
- "title": title,
- "href": href,
- "map": [startLine, state.line],
- }
- else:
- state.env.setdefault("duplicate_refs", []).append(
- {
- "title": title,
- "href": href,
- "label": label,
- "map": [startLine, state.line],
- }
- )
- state.parentType = oldParentType
- return True
|