add
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"""
|
||||
OSC 8 hyperlink parsing and measurement.
|
||||
|
||||
.. versionadded:: 0.7.0
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# std imports
|
||||
import re
|
||||
|
||||
import typing
|
||||
|
||||
# local
|
||||
from ._width import width as _width
|
||||
from .escape_sequences import _SEQUENCE_CLASSIFY
|
||||
|
||||
HYPERLINK_OPEN_RE = re.compile(r'\x1b]8;([^;]*);([^\x07\x1b]*)(\x07|\x1b\\)')
|
||||
HYPERLINK_CLOSE_RE = re.compile(r'\x1b]8;;(\x07|\x1b\\)')
|
||||
|
||||
|
||||
class HyperlinkParams(typing.NamedTuple):
|
||||
r"""
|
||||
Parsed parameters from an OSC 8 hyperlink open sequence.
|
||||
|
||||
:param url: The hyperlink URL.
|
||||
:param params: Colon-separated metadata string (often empty).
|
||||
:param terminator: Sequence terminator (``\x07`` or ``\x1b\\``).
|
||||
"""
|
||||
|
||||
url: str
|
||||
params: str = ''
|
||||
terminator: str = '\x07'
|
||||
|
||||
@classmethod
|
||||
def parse(cls, seq: str) -> HyperlinkParams | None:
|
||||
r"""
|
||||
Parse an OSC 8 open sequence string.
|
||||
|
||||
Returns ``None`` if *seq* is not a valid OSC 8 open.
|
||||
|
||||
Example::
|
||||
|
||||
>>> HyperlinkParams.parse('\x1b]8;;http://example.com\x07')
|
||||
HyperlinkParams(url='http://example.com', params='', terminator='\x07')
|
||||
"""
|
||||
m = HYPERLINK_OPEN_RE.match(seq)
|
||||
if m is None:
|
||||
return None
|
||||
return cls(url=m.group(2), params=m.group(1), terminator=m.group(3))
|
||||
|
||||
def make_open(self) -> str:
|
||||
"""Generate the OSC 8 open escape sequence."""
|
||||
return f'\x1b]8;{self.params};{self.url}{self.terminator}'
|
||||
|
||||
def make_close(self) -> str:
|
||||
"""Generate the OSC 8 close escape sequence."""
|
||||
return f'\x1b]8;;{self.terminator}'
|
||||
|
||||
|
||||
class Hyperlink(typing.NamedTuple):
|
||||
"""
|
||||
A complete OSC 8 hyperlink with target and inner text.
|
||||
|
||||
:param params: Parsed open sequence parameters.
|
||||
:param text: Inner text between the open and close sequences.
|
||||
"""
|
||||
|
||||
params: HyperlinkParams
|
||||
text: str
|
||||
|
||||
@classmethod
|
||||
def find_close(cls, text: str, open_end: int) -> tuple[int, int]:
|
||||
"""
|
||||
Find the matching OSC 8 close sequence.
|
||||
|
||||
Searches 'text' starting at 'open_end', the position just past the open
|
||||
sequence. Returns position of close sequence ``(close_start,
|
||||
close_end)`` or ``(-1, -1)`` if not found.
|
||||
|
||||
Per the OSC 8 specification, terminal emulators treat hyperlinks as a
|
||||
state attribute. A close sequence closes
|
||||
the current hyperlink regardless of how many open sequences preceded it.
|
||||
"""
|
||||
m = HYPERLINK_CLOSE_RE.search(text, open_end)
|
||||
if m is None:
|
||||
return (-1, -1)
|
||||
return (m.start(), m.end())
|
||||
|
||||
def display_width(
|
||||
self,
|
||||
*,
|
||||
control_codes: typing.Literal['parse', 'strict', 'ignore'] = 'parse',
|
||||
tabsize: int = 8,
|
||||
ambiguous_width: int = 1,
|
||||
) -> int:
|
||||
r"""
|
||||
Measure the display width of the hyperlink's inner text.
|
||||
|
||||
Delegates to :func:`wcwidth.width` with the given parameters.
|
||||
|
||||
Example::
|
||||
|
||||
>>> hl = Hyperlink.parse('\x1b]8;;http://example.com\x07Hello\x1b]8;;\x07', 0)
|
||||
>>> hl.display_width()
|
||||
5
|
||||
"""
|
||||
return _width(
|
||||
self.text,
|
||||
control_codes=control_codes,
|
||||
tabsize=tabsize,
|
||||
ambiguous_width=ambiguous_width,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, text: str, start: int = 0) -> Hyperlink | None:
|
||||
r"""
|
||||
Parse a complete OSC 8 hyperlink unit from *text* at position *start*.
|
||||
|
||||
Locates the open sequence, finds the matching close, and returns a
|
||||
``Hyperlink`` containing the parsed parameters and inner text. Returns
|
||||
``None`` if the text at *start* is not a complete OSC 8 hyperlink.
|
||||
|
||||
Example::
|
||||
|
||||
>>> Hyperlink.parse('\x1b]8;;http://example.com\x07Hello\x1b]8;;\x07')
|
||||
Hyperlink(params=HyperlinkParams(url='http://example.com', ...), text='Hello')
|
||||
"""
|
||||
m = _SEQUENCE_CLASSIFY.match(text, start)
|
||||
if m is None:
|
||||
return None
|
||||
params = HyperlinkParams.parse(m.group())
|
||||
if params is None:
|
||||
return None
|
||||
close_start, close_end = cls.find_close(text, m.end())
|
||||
if (close_start, close_end) == (-1, -1):
|
||||
return None
|
||||
return cls(params=params, text=text[m.end():close_start])
|
||||
|
||||
def make_sequence(self) -> str:
|
||||
"""Rebuild the complete OSC 8 hyperlink escape sequence."""
|
||||
return self.params.make_open() + self.text + self.params.make_close()
|
||||
Reference in New Issue
Block a user