codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import re4from typing import Protocol5 6from ..common.utils import arrayReplaceAt, isLinkClose, isLinkOpen7from ..token import Token8from .state_core import StateCore9 10HTTP_RE = re.compile(r"^http://")11MAILTO_RE = re.compile(r"^mailto:")12TEST_MAILTO_RE = re.compile(r"^mailto:", flags=re.IGNORECASE)13 14 15def linkify(state: StateCore) -> None:16 """Rule for identifying plain-text links."""17 if not state.md.options.linkify:18 return19 20 if not state.md.linkify:21 raise ModuleNotFoundError("Linkify enabled but not installed.")22 23 for inline_token in state.tokens:24 if inline_token.type != "inline" or not state.md.linkify.pretest(25 inline_token.content26 ):27 continue28 29 tokens = inline_token.children30 31 htmlLinkLevel = 032 33 # We scan from the end, to keep position when new tags added.34 # Use reversed logic in links start/end match35 assert tokens is not None36 i = len(tokens)37 while i >= 1:38 i -= 139 assert isinstance(tokens, list)40 currentToken = tokens[i]41 42 # Skip content of markdown links43 if currentToken.type == "link_close":44 i -= 145 while (46 tokens[i].level != currentToken.level47 and tokens[i].type != "link_open"48 ):49 i -= 150 continue51 52 # Skip content of html tag links53 if currentToken.type == "html_inline":54 if isLinkOpen(currentToken.content) and htmlLinkLevel > 0:55 htmlLinkLevel -= 156 if isLinkClose(currentToken.content):57 htmlLinkLevel += 158 if htmlLinkLevel > 0:59 continue60 61 if currentToken.type == "text" and state.md.linkify.test(62 currentToken.content63 ):64 text = currentToken.content65 links: list[_LinkType] = state.md.linkify.match(text) or []66 67 # Now split string to nodes68 nodes = []69 level = currentToken.level70 lastPos = 071 72 # forbid escape sequence at the start of the string,73 # this avoids http\://example.com/ from being linkified as74 # http:<a href="//example.com/">//example.com/</a>75 if (76 links77 and links[0].index == 078 and i > 079 and tokens[i - 1].type == "text_special"80 ):81 links = links[1:]82 83 for link in links:84 url = link.url85 fullUrl = state.md.normalizeLink(url)86 if not state.md.validateLink(fullUrl):87 continue88 89 urlText = link.text90 91 # Linkifier might send raw hostnames like "example.com", where url92 # starts with domain name. So we prepend http:// in those cases,93 # and remove it afterwards.94 if not link.schema:95 urlText = HTTP_RE.sub(96 "", state.md.normalizeLinkText("http://" + urlText)97 )98 elif link.schema == "mailto:" and TEST_MAILTO_RE.search(urlText):99 urlText = MAILTO_RE.sub(100 "", state.md.normalizeLinkText("mailto:" + urlText)101 )102 else:103 urlText = state.md.normalizeLinkText(urlText)104 105 pos = link.index106 107 if pos > lastPos:108 token = Token("text", "", 0)109 token.content = text[lastPos:pos]110 token.level = level111 nodes.append(token)112 113 token = Token("link_open", "a", 1)114 token.attrs = {"href": fullUrl}115 token.level = level116 level += 1117 token.markup = "linkify"118 token.info = "auto"119 nodes.append(token)120 121 token = Token("text", "", 0)122 token.content = urlText123 token.level = level124 nodes.append(token)125 126 token = Token("link_close", "a", -1)127 level -= 1128 token.level = level129 token.markup = "linkify"130 token.info = "auto"131 nodes.append(token)132 133 lastPos = link.last_index134 135 if lastPos < len(text):136 token = Token("text", "", 0)137 token.content = text[lastPos:]138 token.level = level139 nodes.append(token)140 141 inline_token.children = tokens = arrayReplaceAt(tokens, i, nodes)142 143 144class _LinkType(Protocol):145 url: str146 text: str147 index: int148 last_index: int149 schema: str | None150 