mirror of
https://github.com/morpheus65535/bazarr.git
synced 2026-07-31 10:07:37 -04:00
143 lines
4.4 KiB
Python
143 lines
4.4 KiB
Python
"""
|
|
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, not as nested HTML anchors. 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()
|