|
2 | 2 | UI to render a Zulip message for display, and respond contextually to actions
|
3 | 3 | """
|
4 | 4 |
|
| 5 | +import re |
5 | 6 | import typing
|
6 | 7 | from collections import defaultdict
|
7 | 8 | from datetime import date, datetime
|
8 | 9 | from time import time
|
9 |
| -from typing import Any, Dict, List, NamedTuple, Optional, Tuple, Union |
| 10 | +from typing import Any, Dict, List, Match, NamedTuple, Optional, Tuple, Union |
10 | 11 | from urllib.parse import urljoin, urlparse
|
11 | 12 |
|
12 | 13 | import dateutil.parser
|
@@ -390,6 +391,9 @@ def soup2markup(
|
390 | 391 | metadata["bq_len"] -= 1
|
391 | 392 | continue
|
392 | 393 | markup.append(element)
|
| 394 | + elif tag == "span" and "alert-word" in element.get("class", []): |
| 395 | + if tag_text: |
| 396 | + markup.append(("msg_code", tag_text)) |
393 | 397 | elif tag == "div" and (set(tag_classes) & set(unrendered_div_classes)):
|
394 | 398 | # UNRENDERED DIV CLASSES
|
395 | 399 | # NOTE: Though `matches` is generalized for multiple
|
@@ -629,6 +633,8 @@ def main_view(self) -> List[Any]:
|
629 | 633 | else:
|
630 | 634 | recipient_header = None
|
631 | 635 |
|
| 636 | + self.alerted_words: Optional[Any] = self.model.get_alert_words() |
| 637 | + |
632 | 638 | # Content Header
|
633 | 639 | message = {
|
634 | 640 | key: {
|
@@ -717,7 +723,11 @@ def main_view(self) -> List[Any]:
|
717 | 723 |
|
718 | 724 | # Transform raw message content into markup (As needed by urwid.Text)
|
719 | 725 | content, self.message_links, self.time_mentions = self.transform_content(
|
720 |
| - self.message["content"], self.model.server_url |
| 726 | + self.message["content"], |
| 727 | + self.model.server_url, |
| 728 | + self.alerted_words, |
| 729 | + "has_alert_word" in self.message["flags"] |
| 730 | + # self.message["flags"], |
721 | 731 | )
|
722 | 732 | self.content.set_text(content)
|
723 | 733 |
|
@@ -798,14 +808,59 @@ def update_message_author_status(self) -> bool:
|
798 | 808 |
|
799 | 809 | return author_is_present
|
800 | 810 |
|
| 811 | + @staticmethod |
| 812 | + def transform_content_alert_words(content: str, alerted_list: List[str]) -> Any: |
| 813 | + alert_regex_replacements = { |
| 814 | + "&": "&", |
| 815 | + "<": "<", |
| 816 | + ">": ">", |
| 817 | + # Accept quotes with or without HTML escaping |
| 818 | + '"': r"(?:\"|")", |
| 819 | + "'": r"(?:\'|')", |
| 820 | + } |
| 821 | + |
| 822 | + before_punctuation = r"\s|^|>|[\\(\".,';\\[]" |
| 823 | + |
| 824 | + after_punctuation = r"(?=\s|$|<|[\\)\"?!:.,';\]!])" |
| 825 | + |
| 826 | + clean: str |
| 827 | + |
| 828 | + def replace_callback(match: Match[Union[str, str, str]]) -> str: |
| 829 | + before = match.group(1) |
| 830 | + word = match.group(2) |
| 831 | + after = match.group(3) |
| 832 | + offset = match.start() |
| 833 | + matched_content = match.string |
| 834 | + pre_match = matched_content[:offset] |
| 835 | + check_string = pre_match + match.group() |
| 836 | + in_tag = check_string.rfind("<") > check_string.rfind(">") |
| 837 | + if in_tag: |
| 838 | + return f"{before}{word}{after}" |
| 839 | + return f"{before}<span class='alert-word'>{word}</span>{after}" |
| 840 | + |
| 841 | + for word in alerted_list: |
| 842 | + clean = "".join(alert_regex_replacements.get(c, c) for c in word) |
| 843 | + regex = f"({before_punctuation})({clean})({after_punctuation})" |
| 844 | + regex1 = re.compile(regex, re.IGNORECASE) |
| 845 | + content = re.sub(regex1, replace_callback, content) |
| 846 | + |
| 847 | + return content |
| 848 | + |
801 | 849 | @classmethod
|
802 | 850 | def transform_content(
|
803 |
| - cls, content: Any, server_url: str |
| 851 | + cls, |
| 852 | + content: Any, |
| 853 | + server_url: str, |
| 854 | + alerted_list: List[str] = list(), |
| 855 | + alert_word_present: bool = False, |
804 | 856 | ) -> Tuple[
|
805 | 857 | Tuple[None, Any],
|
806 | 858 | Dict[str, Tuple[str, int, bool]],
|
807 | 859 | List[Tuple[str, str]],
|
808 | 860 | ]:
|
| 861 | + if alert_word_present: |
| 862 | + content = cls.transform_content_alert_words(content, alerted_list) |
| 863 | + |
809 | 864 | soup = BeautifulSoup(content, "lxml")
|
810 | 865 | body = soup.find(name="body")
|
811 | 866 |
|
|
0 commit comments