|
129 | 129 | - Total reset: |
130 | 130 | This will reset all previously applied formatting codes. |
131 | 131 | - `[_]` |
| 132 | +- Hyperlinks: |
| 133 | + Create a clickable hyperlink using the `link:` prefix followed by any URL. |
| 134 | + Auto-reset braces are required to define the visible, clickable text. |
| 135 | + Examples: |
| 136 | + - `[link:file:///C:/path/to/file.txt](open file)` |
| 137 | + - `[link:https://example.com|br:blue](click here)` |
132 | 138 |
|
133 | 139 | ------------------------------------------------------------------------------------------------------------------------------------ |
134 | 140 | #### Additional Formatting Codes when a `default_color` is set |
|
192 | 198 | _PATTERNS = LazyRegex( |
193 | 199 | star_reset=r"\[\s*([^]_]*?)\s*\*\s*([^]_]*?)\]", |
194 | 200 | star_reset_inside=r"([^|]*?)\s*\*\s*([^|]*)", |
195 | | - ansi_seq=ANSI.CHAR + r"(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])", |
| 201 | + ansi_seq=ANSI.CHAR + r"(?:\].*?(?:\x1b\\|\x07)|\[[0-?]*[ -/]*[@-~]|[@-Z\\-_])", |
| 202 | + link=r"(?i)^\s*link\s*:\s*(.+?)\s*$", |
196 | 203 | formatting=( |
197 | 204 | Regex.brackets("[", "]", is_group=True, ignore_in_strings=False) + r"(?:([/\\]?)" |
198 | 205 | + Regex.brackets("(", ")", is_group=True, strip_spaces=False, ignore_in_strings=False) + r")?" |
@@ -633,8 +640,15 @@ def __call__(self, match: _rx.Match[str], /) -> str: |
633 | 640 | else: |
634 | 641 | _formats = _PATTERNS.star_reset_inside.sub(r"\1_\2", formats) |
635 | 642 |
|
636 | | - if all(self.cls._get_replacement(format_key, self.default_color) != format_key |
637 | | - for format_key in self.cls._formats_to_keys(_formats)): |
| 643 | + has_link = False |
| 644 | + has_invalid_key = False |
| 645 | + for format_key in self.cls._formats_to_keys(_formats): |
| 646 | + if _PATTERNS.link.match(format_key): |
| 647 | + has_link = True |
| 648 | + elif self.cls._get_replacement(format_key, self.default_color) == format_key: |
| 649 | + has_invalid_key = True |
| 650 | + |
| 651 | + if has_link or not has_invalid_key: |
638 | 652 | # ESCAPE THE FORMATTING CODE |
639 | 653 | escaped = f"[{self.escape_char}{formats}]" |
640 | 654 | if auto_reset_txt: |
@@ -702,14 +716,48 @@ def __call__(self, match: _rx.Match[str], /) -> str: |
702 | 716 | if self.formats_escaped: |
703 | 717 | self.original_formats = self.formats = _PATTERNS.escape_char.sub(r"\1", self.formats) |
704 | 718 |
|
| 719 | + # HANDLE HYPERLINK FORMAT |
| 720 | + all_keys = self.cls._formats_to_keys(self.formats) |
| 721 | + if (result := self.handle_link(match, all_keys)) is not None: |
| 722 | + return result |
| 723 | + |
705 | 724 | self.process_formats_and_auto_reset() |
706 | 725 |
|
| 726 | + # IF THERE ARE NO FORMATS OR ALL FORMATS ARE INVALID, RETURN THE ORIGINAL STRING |
707 | 727 | if not self.formats: |
708 | 728 | return match.group(0) |
709 | 729 |
|
710 | 730 | self.convert_to_ansi() |
711 | 731 | return self.build_output(match) |
712 | 732 |
|
| 733 | + def handle_link(self, match: _rx.Match[str], all_keys: list[str], /) -> Optional[str]: |
| 734 | + """Handle a hyperlink format code, returning the OSC 8 sequence or None if not a link.""" |
| 735 | + link_key = next((k for k in all_keys if _PATTERNS.link.match(k)), None) |
| 736 | + |
| 737 | + if link_key is None: |
| 738 | + return None |
| 739 | + if self.auto_reset_txt is None: |
| 740 | + return match.group(0) # LINK WITHOUT DISPLAY BRACES IS INVALID |
| 741 | + if self.formats_escaped: |
| 742 | + return f"[{self.original_formats}]({self.auto_reset_txt})" |
| 743 | + |
| 744 | + link_url = _PATTERNS.link.match(link_key).group(1) # type: ignore[union-attr] |
| 745 | + display = self.auto_reset_txt |
| 746 | + |
| 747 | + if other_keys := [k for k in all_keys if k != link_key]: |
| 748 | + # APPLY REMAINING FORMAT CODES TO DISPLAY TEXT WITH AUTO-RESET |
| 749 | + display = "[{}]({})".format("|".join(other_keys), display) |
| 750 | + if other_keys or ("[" in display and "]" in display): |
| 751 | + display = self.cls.to_ansi( |
| 752 | + display, |
| 753 | + self.default_color, |
| 754 | + self.brightness_steps, |
| 755 | + _default_start=False, |
| 756 | + _validate_default=False, |
| 757 | + ) |
| 758 | + |
| 759 | + return ANSI.SEQ_LINK_OPEN.format(link_url) + display + ANSI.SEQ_LINK_CLOSE |
| 760 | + |
713 | 761 | def process_formats_and_auto_reset(self) -> None: |
714 | 762 | """Process nested formatting in both formats and auto-reset text.""" |
715 | 763 | # PROCESS AUTO-RESET TEXT IF IT CONTAINS NESTED FORMATTING |
|
0 commit comments