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