codekingpro/portable-devtools
115k
1"""2OSC 8 hyperlink parsing and measurement.3 4.. versionadded:: 0.7.05"""6 7from __future__ import annotations8 9# std imports10import re11 12import typing13 14# local15from ._width import width as _width16from .escape_sequences import _SEQUENCE_CLASSIFY17 18HYPERLINK_OPEN_RE = re.compile(r'\x1b]8;([^;]*);([^\x07\x1b]*)(\x07|\x1b\\)')19HYPERLINK_CLOSE_RE = re.compile(r'\x1b]8;;(\x07|\x1b\\)')20 21 22class HyperlinkParams(typing.NamedTuple):23 r"""24 Parsed parameters from an OSC 8 hyperlink open sequence.25 26 :param url: The hyperlink URL.27 :param params: Colon-separated metadata string (often empty).28 :param terminator: Sequence terminator (``\x07`` or ``\x1b\\``).29 """30 31 url: str32 params: str = ''33 terminator: str = '\x07'34 35 @classmethod36 def parse(cls, seq: str) -> HyperlinkParams | None:37 r"""38 Parse an OSC 8 open sequence string.39 40 Returns ``None`` if *seq* is not a valid OSC 8 open.41 42 Example::43 44 >>> HyperlinkParams.parse('\x1b]8;;http://example.com\x07')45 HyperlinkParams(url='http://example.com', params='', terminator='\\x07')46 """47 m = HYPERLINK_OPEN_RE.match(seq)48 if m is None:49 return None50 return cls(url=m.group(2), params=m.group(1), terminator=m.group(3))51 52 def make_open(self) -> str:53 """Generate the OSC 8 open escape sequence."""54 return f'\x1b]8;{self.params};{self.url}{self.terminator}'55 56 def make_close(self) -> str:57 """Generate the OSC 8 close escape sequence."""58 return f'\x1b]8;;{self.terminator}'59 60 61class Hyperlink(typing.NamedTuple):62 """63 A complete OSC 8 hyperlink with target and inner text.64 65 :param params: Parsed open sequence parameters.66 :param text: Inner text between the open and close sequences.67 """68 69 params: HyperlinkParams70 text: str71 72 @classmethod73 def find_close(cls, text: str, open_end: int) -> tuple[int, int]:74 """75 Find the matching OSC 8 close sequence.76 77 Searches 'text' starting at 'open_end', the position just past the open78 sequence. Returns position of close sequence ``(close_start,79 close_end)`` or ``(-1, -1)`` if not found.80 81 Per the OSC 8 specification, terminal emulators treat hyperlinks as a82 state attribute, not as nested HTML anchors. A close sequence closes83 the current hyperlink regardless of how many open sequences preceded it.84 """85 m = HYPERLINK_CLOSE_RE.search(text, open_end)86 if m is None:87 return (-1, -1)88 return (m.start(), m.end())89 90 def display_width(91 self,92 *,93 control_codes: typing.Literal['parse', 'strict', 'ignore'] = 'parse',94 tabsize: int = 8,95 ambiguous_width: int = 1,96 ) -> int:97 r"""98 Measure the display width of the hyperlink's inner text.99 100 Delegates to :func:`wcwidth.width` with the given parameters.101 102 Example::103 104 >>> hl = Hyperlink.parse('\x1b]8;;http://example.com\x07Hello\x1b]8;;\x07', 0)105 >>> hl.display_width()106 5107 """108 return _width(109 self.text,110 control_codes=control_codes,111 tabsize=tabsize,112 ambiguous_width=ambiguous_width,113 )114 115 @classmethod116 def parse(cls, text: str, start: int = 0) -> Hyperlink | None:117 r"""118 Parse a complete OSC 8 hyperlink unit from *text* at position *start*.119 120 Locates the open sequence, finds the matching close, and returns a121 ``Hyperlink`` containing the parsed parameters and inner text. Returns122 ``None`` if the text at *start* is not a complete OSC 8 hyperlink.123 124 Example::125 126 >>> Hyperlink.parse('\x1b]8;;http://example.com\x07Hello\x1b]8;;\x07')127 Hyperlink(params=HyperlinkParams(url='http://example.com', ...), text='Hello')128 """129 m = _SEQUENCE_CLASSIFY.match(text, start)130 if m is None:131 return None132 params = HyperlinkParams.parse(m.group())133 if params is None:134 return None135 close_start, close_end = cls.find_close(text, m.end())136 if (close_start, close_end) == (-1, -1):137 return None138 return cls(params=params, text=text[m.end():close_start])139 140 def make_sequence(self) -> str:141 """Rebuild the complete OSC 8 hyperlink escape sequence."""142 return self.params.make_open() + self.text + self.params.make_close()143 