2017-03-03 12:42:07 +01:00
|
|
|
import re
|
2020-06-11 00:54:34 +02:00
|
|
|
from typing import List, Match, Tuple
|
2017-03-03 12:42:07 +01:00
|
|
|
|
|
|
|
from bs4 import BeautifulSoup
|
|
|
|
|
2017-03-10 11:47:06 +01:00
|
|
|
# The phrases in this list will be ignored. The longest phrase is
|
|
|
|
# tried first; this removes the chance of smaller phrases changing
|
|
|
|
# the text before longer phrases are tried.
|
|
|
|
# The errors shown by `tools/check-capitalization` can be added to
|
|
|
|
# this list without any modification.
|
|
|
|
IGNORED_PHRASES = [
|
2017-03-03 12:42:07 +01:00
|
|
|
# Proper nouns and acronyms
|
|
|
|
r"API",
|
2017-07-07 19:43:02 +02:00
|
|
|
r"APNS",
|
2017-07-08 02:02:28 +02:00
|
|
|
r"Botserver",
|
2017-03-03 12:42:07 +01:00
|
|
|
r"Cookie Bot",
|
2020-07-21 22:23:58 +02:00
|
|
|
r"DevAuthBackend",
|
2019-02-08 19:11:55 +01:00
|
|
|
r"GCM",
|
2017-03-03 12:42:07 +01:00
|
|
|
r"GitHub",
|
2018-03-05 19:29:18 +01:00
|
|
|
r"Gravatar",
|
2018-12-17 21:44:44 +01:00
|
|
|
r"Help Center",
|
2017-03-03 12:42:07 +01:00
|
|
|
r"HTTP",
|
|
|
|
r"ID",
|
|
|
|
r"IDs",
|
2018-04-30 21:04:01 +02:00
|
|
|
r"IP",
|
2017-03-03 12:42:07 +01:00
|
|
|
r"JSON",
|
|
|
|
r"Kerberos",
|
2017-09-25 07:45:17 +02:00
|
|
|
r"LDAP",
|
2020-08-11 01:47:49 +02:00
|
|
|
r"Markdown",
|
2017-03-19 20:01:01 +01:00
|
|
|
r"OTP",
|
2017-03-03 12:42:07 +01:00
|
|
|
r"Pivotal",
|
2019-05-15 04:39:22 +02:00
|
|
|
r"PM",
|
|
|
|
r"PMs",
|
2021-02-12 08:20:45 +01:00
|
|
|
r"Slack",
|
2022-05-19 21:37:54 +02:00
|
|
|
r"Google",
|
2021-02-12 08:20:45 +01:00
|
|
|
r"Terms of Service",
|
|
|
|
r"Tuesday",
|
2017-03-03 12:42:07 +01:00
|
|
|
r"URL",
|
2021-12-22 14:37:12 +01:00
|
|
|
r"UUID",
|
2017-03-03 12:42:07 +01:00
|
|
|
r"Webathena",
|
|
|
|
r"WordPress",
|
|
|
|
r"Zephyr",
|
2018-12-28 20:45:54 +01:00
|
|
|
r"Zoom",
|
2017-03-03 12:42:07 +01:00
|
|
|
r"Zulip",
|
2022-07-31 19:05:40 +02:00
|
|
|
r"Zulip Server",
|
2018-12-18 08:59:56 +01:00
|
|
|
r"Zulip Account Security",
|
2018-12-20 08:57:49 +01:00
|
|
|
r"Zulip Security",
|
2022-02-05 08:29:54 +01:00
|
|
|
r"Zulip Cloud Standard",
|
2021-07-06 00:23:51 +02:00
|
|
|
r"BigBlueButton",
|
2017-03-03 12:42:07 +01:00
|
|
|
# Code things
|
2022-02-23 01:56:38 +01:00
|
|
|
r"\.zuliprc",
|
2022-03-19 00:41:10 +01:00
|
|
|
# BeautifulSoup will remove <z-user> which is horribly confusing,
|
|
|
|
# so we need more of the sentence.
|
2022-03-19 00:57:51 +01:00
|
|
|
r"<z-user></z-user> will have the same role",
|
2022-10-25 22:24:26 +02:00
|
|
|
r"<z-user></z-user> will have the same properties",
|
2017-03-03 12:42:07 +01:00
|
|
|
# Things using "I"
|
2021-10-28 06:25:23 +02:00
|
|
|
r"I understand",
|
2017-03-03 12:42:07 +01:00
|
|
|
r"I'm",
|
2021-10-03 08:53:35 +02:00
|
|
|
r"I've",
|
2017-03-03 12:42:07 +01:00
|
|
|
# Specific short words
|
2020-01-08 01:49:44 +01:00
|
|
|
r"beta",
|
2017-03-03 12:42:07 +01:00
|
|
|
r"and",
|
|
|
|
r"bot",
|
2022-02-23 01:56:38 +01:00
|
|
|
r"e\.g\.",
|
2017-11-16 02:38:56 +01:00
|
|
|
r"enabled",
|
2020-06-26 13:43:49 +02:00
|
|
|
r"signups",
|
2017-12-05 01:11:14 +01:00
|
|
|
# Placeholders
|
|
|
|
r"keyword",
|
|
|
|
r"streamname",
|
2022-02-23 01:56:38 +01:00
|
|
|
r"user@example\.com",
|
2017-03-03 12:42:07 +01:00
|
|
|
# Fragments of larger strings
|
2021-02-12 08:20:45 +01:00
|
|
|
(r"your subscriptions on your Streams page"),
|
2022-02-23 01:56:38 +01:00
|
|
|
r"Add global time<br />Everyone sees global times in their own time zone\.",
|
2018-08-21 08:14:46 +02:00
|
|
|
r"user",
|
2018-12-18 08:59:56 +01:00
|
|
|
r"an unknown operating system",
|
2019-02-05 19:28:56 +01:00
|
|
|
r"Go to Settings",
|
2017-03-03 12:42:07 +01:00
|
|
|
# SPECIAL CASES
|
|
|
|
# Because topics usually are lower-case, this would look weird if it were capitalized
|
|
|
|
r"more topics",
|
2022-05-19 21:37:54 +02:00
|
|
|
# Used alone in a parenthetical where capitalized looks worse.
|
|
|
|
r"^deprecated$",
|
2022-09-13 13:15:57 +02:00
|
|
|
# We want the similar text in the Private Messages section to have the same capitalization.
|
|
|
|
r"more conversations",
|
|
|
|
r"back to streams",
|
2018-02-07 01:13:11 +01:00
|
|
|
# Capital 'i' looks weird in reminders popover
|
|
|
|
r"in 1 hour",
|
|
|
|
r"in 20 minutes",
|
|
|
|
r"in 3 hours",
|
2019-07-11 23:05:38 +02:00
|
|
|
# these are used as topics
|
2021-02-12 08:20:45 +01:00
|
|
|
r"^new streams$",
|
|
|
|
r"^stream events$",
|
2018-03-02 20:54:39 +01:00
|
|
|
# These are used as example short names (e.g. an uncapitalized context):
|
|
|
|
r"^marketing$",
|
|
|
|
r"^cookie$",
|
2018-05-02 19:02:51 +02:00
|
|
|
# Used to refer custom time limits
|
|
|
|
r"\bN\b",
|
2019-02-23 00:46:51 +01:00
|
|
|
# Capital c feels obtrusive in clear status option
|
|
|
|
r"clear",
|
2022-02-23 01:56:38 +01:00
|
|
|
r"group private messages with \{recipient\}",
|
|
|
|
r"private messages with \{recipient\}",
|
2018-12-16 20:34:31 +01:00
|
|
|
r"private messages with yourself",
|
2018-08-04 11:03:37 +02:00
|
|
|
r"GIF",
|
|
|
|
# Emoji name placeholder
|
|
|
|
r"leafy green vegetable",
|
2018-08-25 14:06:17 +02:00
|
|
|
# Subdomain placeholder
|
|
|
|
r"your-organization-url",
|
2019-02-06 20:31:45 +01:00
|
|
|
# Used in invite modal
|
|
|
|
r"or",
|
2021-03-19 13:21:18 +01:00
|
|
|
# Used in GIPHY popover.
|
|
|
|
r"GIFs",
|
|
|
|
r"GIPHY",
|
2021-07-27 19:42:18 +02:00
|
|
|
# Used in our case studies
|
|
|
|
r"Technical University of Munich",
|
|
|
|
r"University of California San Diego",
|
2021-11-11 14:56:40 +01:00
|
|
|
# Used in stream creation form
|
|
|
|
r"email hidden",
|
2021-11-25 10:00:04 +01:00
|
|
|
# Use in compose box.
|
|
|
|
r"to send",
|
|
|
|
r"to add a new line",
|
2022-09-09 20:22:28 +02:00
|
|
|
# Used in showing Notification Bot read receipts message
|
|
|
|
"Notification Bot",
|
2022-09-24 12:40:36 +02:00
|
|
|
# Used in presence_enabled setting label
|
|
|
|
r"invisible mode off",
|
2022-10-03 20:05:09 +02:00
|
|
|
# Typeahead suggestions for "Pronouns" custom field type.
|
|
|
|
r"he/him",
|
|
|
|
r"she/her",
|
|
|
|
r"they/them",
|
2022-12-22 19:14:23 +01:00
|
|
|
# Used in message-move-time-limit setting label
|
|
|
|
r"does not apply to moderators and administrators",
|
2017-03-10 11:47:06 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
# Sort regexes in descending order of their lengths. As a result, the
|
|
|
|
# longer phrases will be ignored first.
|
|
|
|
IGNORED_PHRASES.sort(key=lambda regex: len(regex), reverse=True)
|
|
|
|
|
|
|
|
# Compile regexes to improve performance. This also extracts the
|
|
|
|
# text using BeautifulSoup and then removes extra whitespaces from
|
|
|
|
# it. This step enables us to add HTML in our regexes directly.
|
|
|
|
COMPILED_IGNORED_PHRASES = [
|
2021-02-12 08:20:45 +01:00
|
|
|
re.compile(" ".join(BeautifulSoup(regex, "lxml").text.split())) for regex in IGNORED_PHRASES
|
2017-03-10 11:47:06 +01:00
|
|
|
]
|
2017-03-03 12:42:07 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
SPLIT_BOUNDARY = "?.!" # Used to split string into sentences.
|
2022-02-15 23:45:41 +01:00
|
|
|
SPLIT_BOUNDARY_REGEX = re.compile(rf"[{SPLIT_BOUNDARY}]")
|
2017-03-03 12:42:07 +01:00
|
|
|
|
|
|
|
# Regexes which check capitalization in sentences.
|
2020-09-02 02:50:08 +02:00
|
|
|
DISALLOWED = [
|
2021-02-12 08:20:45 +01:00
|
|
|
r"^[a-z](?!\})", # Checks if the sentence starts with a lower case character.
|
|
|
|
r"^[A-Z][a-z]+[\sa-z0-9]+[A-Z]", # Checks if an upper case character exists
|
2017-03-03 12:42:07 +01:00
|
|
|
# after a lower case character when the first character is in upper case.
|
2020-09-02 02:50:08 +02:00
|
|
|
]
|
|
|
|
DISALLOWED_REGEX = re.compile(r"|".join(DISALLOWED))
|
2017-03-03 12:42:07 +01:00
|
|
|
|
2018-03-11 06:06:54 +01:00
|
|
|
BANNED_WORDS = {
|
2021-02-12 08:20:45 +01:00
|
|
|
"realm": "The term realm should not appear in user-facing strings. Use organization instead.",
|
2018-03-11 06:06:54 +01:00
|
|
|
}
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
python: Convert function type annotations to Python 3 style.
Generated by com2ann (slightly patched to avoid also converting
assignment type annotations, which require Python 3.6), followed by
some manual whitespace adjustment, and six fixes for runtime issues:
- def __init__(self, token: Token, parent: Optional[Node]) -> None:
+ def __init__(self, token: Token, parent: "Optional[Node]") -> None:
-def main(options: argparse.Namespace) -> NoReturn:
+def main(options: argparse.Namespace) -> "NoReturn":
-def fetch_request(url: str, callback: Any, **kwargs: Any) -> Generator[Callable[..., Any], Any, None]:
+def fetch_request(url: str, callback: Any, **kwargs: Any) -> "Generator[Callable[..., Any], Any, None]":
-def assert_server_running(server: subprocess.Popen[bytes], log_file: Optional[str]) -> None:
+def assert_server_running(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> None:
-def server_is_up(server: subprocess.Popen[bytes], log_file: Optional[str]) -> bool:
+def server_is_up(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> bool:
- method_kwarg_pairs: List[FuncKwargPair],
+ method_kwarg_pairs: "List[FuncKwargPair]",
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-19 03:48:37 +02:00
|
|
|
def get_safe_phrase(phrase: str) -> str:
|
2017-03-03 12:42:07 +01:00
|
|
|
"""
|
|
|
|
Safe phrase is in lower case and doesn't contain characters which can
|
|
|
|
conflict with split boundaries. All conflicting characters are replaced
|
|
|
|
with low dash (_).
|
|
|
|
"""
|
2021-02-12 08:20:45 +01:00
|
|
|
phrase = SPLIT_BOUNDARY_REGEX.sub("_", phrase)
|
2017-03-03 12:42:07 +01:00
|
|
|
return phrase.lower()
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
python: Convert function type annotations to Python 3 style.
Generated by com2ann (slightly patched to avoid also converting
assignment type annotations, which require Python 3.6), followed by
some manual whitespace adjustment, and six fixes for runtime issues:
- def __init__(self, token: Token, parent: Optional[Node]) -> None:
+ def __init__(self, token: Token, parent: "Optional[Node]") -> None:
-def main(options: argparse.Namespace) -> NoReturn:
+def main(options: argparse.Namespace) -> "NoReturn":
-def fetch_request(url: str, callback: Any, **kwargs: Any) -> Generator[Callable[..., Any], Any, None]:
+def fetch_request(url: str, callback: Any, **kwargs: Any) -> "Generator[Callable[..., Any], Any, None]":
-def assert_server_running(server: subprocess.Popen[bytes], log_file: Optional[str]) -> None:
+def assert_server_running(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> None:
-def server_is_up(server: subprocess.Popen[bytes], log_file: Optional[str]) -> bool:
+def server_is_up(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> bool:
- method_kwarg_pairs: List[FuncKwargPair],
+ method_kwarg_pairs: "List[FuncKwargPair]",
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-19 03:48:37 +02:00
|
|
|
def replace_with_safe_phrase(matchobj: Match[str]) -> str:
|
2017-03-03 12:42:07 +01:00
|
|
|
"""
|
|
|
|
The idea is to convert IGNORED_PHRASES into safe phrases, see
|
|
|
|
`get_safe_phrase()` function. The only exception is when the
|
|
|
|
IGNORED_PHRASE is at the start of the text or after a split
|
|
|
|
boundary; in this case, we change the first letter of the phrase
|
|
|
|
to upper case.
|
|
|
|
"""
|
|
|
|
ignored_phrase = matchobj.group(0)
|
|
|
|
safe_string = get_safe_phrase(ignored_phrase)
|
|
|
|
|
|
|
|
start_index = matchobj.start()
|
|
|
|
complete_string = matchobj.string
|
|
|
|
|
|
|
|
is_string_start = start_index == 0
|
|
|
|
# We expect that there will be one space between split boundary
|
|
|
|
# and the next word.
|
|
|
|
punctuation = complete_string[max(start_index - 2, 0)]
|
|
|
|
is_after_split_boundary = punctuation in SPLIT_BOUNDARY
|
|
|
|
if is_string_start or is_after_split_boundary:
|
|
|
|
return safe_string.capitalize()
|
|
|
|
|
|
|
|
return safe_string
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
python: Convert function type annotations to Python 3 style.
Generated by com2ann (slightly patched to avoid also converting
assignment type annotations, which require Python 3.6), followed by
some manual whitespace adjustment, and six fixes for runtime issues:
- def __init__(self, token: Token, parent: Optional[Node]) -> None:
+ def __init__(self, token: Token, parent: "Optional[Node]") -> None:
-def main(options: argparse.Namespace) -> NoReturn:
+def main(options: argparse.Namespace) -> "NoReturn":
-def fetch_request(url: str, callback: Any, **kwargs: Any) -> Generator[Callable[..., Any], Any, None]:
+def fetch_request(url: str, callback: Any, **kwargs: Any) -> "Generator[Callable[..., Any], Any, None]":
-def assert_server_running(server: subprocess.Popen[bytes], log_file: Optional[str]) -> None:
+def assert_server_running(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> None:
-def server_is_up(server: subprocess.Popen[bytes], log_file: Optional[str]) -> bool:
+def server_is_up(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> bool:
- method_kwarg_pairs: List[FuncKwargPair],
+ method_kwarg_pairs: "List[FuncKwargPair]",
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-19 03:48:37 +02:00
|
|
|
def get_safe_text(text: str) -> str:
|
2017-03-03 12:42:07 +01:00
|
|
|
"""
|
|
|
|
This returns text which is rendered by BeautifulSoup and is in the
|
|
|
|
form that can be split easily and has all IGNORED_PHRASES processed.
|
|
|
|
"""
|
2021-02-12 08:20:45 +01:00
|
|
|
soup = BeautifulSoup(text, "lxml")
|
|
|
|
text = " ".join(soup.text.split()) # Remove extra whitespaces.
|
2017-03-10 11:47:06 +01:00
|
|
|
for phrase_regex in COMPILED_IGNORED_PHRASES:
|
2017-03-03 12:42:07 +01:00
|
|
|
text = phrase_regex.sub(replace_with_safe_phrase, text)
|
|
|
|
|
|
|
|
return text
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
python: Convert function type annotations to Python 3 style.
Generated by com2ann (slightly patched to avoid also converting
assignment type annotations, which require Python 3.6), followed by
some manual whitespace adjustment, and six fixes for runtime issues:
- def __init__(self, token: Token, parent: Optional[Node]) -> None:
+ def __init__(self, token: Token, parent: "Optional[Node]") -> None:
-def main(options: argparse.Namespace) -> NoReturn:
+def main(options: argparse.Namespace) -> "NoReturn":
-def fetch_request(url: str, callback: Any, **kwargs: Any) -> Generator[Callable[..., Any], Any, None]:
+def fetch_request(url: str, callback: Any, **kwargs: Any) -> "Generator[Callable[..., Any], Any, None]":
-def assert_server_running(server: subprocess.Popen[bytes], log_file: Optional[str]) -> None:
+def assert_server_running(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> None:
-def server_is_up(server: subprocess.Popen[bytes], log_file: Optional[str]) -> bool:
+def server_is_up(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> bool:
- method_kwarg_pairs: List[FuncKwargPair],
+ method_kwarg_pairs: "List[FuncKwargPair]",
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-19 03:48:37 +02:00
|
|
|
def is_capitalized(safe_text: str) -> bool:
|
2017-03-03 12:42:07 +01:00
|
|
|
sentences = SPLIT_BOUNDARY_REGEX.split(safe_text)
|
2020-09-02 02:50:08 +02:00
|
|
|
return not any(DISALLOWED_REGEX.search(sentence.strip()) for sentence in sentences)
|
2017-03-03 12:42:07 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2018-03-11 06:06:54 +01:00
|
|
|
def check_banned_words(text: str) -> List[str]:
|
|
|
|
lower_cased_text = text.lower()
|
|
|
|
errors = []
|
|
|
|
for word, reason in BANNED_WORDS.items():
|
|
|
|
if word in lower_cased_text:
|
2018-03-17 00:41:21 +01:00
|
|
|
# Hack: Should move this into BANNED_WORDS framework; for
|
|
|
|
# now, just hand-code the skips:
|
2021-02-12 08:20:45 +01:00
|
|
|
if "realm_name" in lower_cased_text:
|
2018-03-17 00:41:21 +01:00
|
|
|
continue
|
2018-03-11 06:06:54 +01:00
|
|
|
kwargs = dict(word=word, text=text, reason=reason)
|
|
|
|
msg = "{word} found in '{text}'. {reason}".format(**kwargs)
|
|
|
|
errors.append(msg)
|
|
|
|
|
|
|
|
return errors
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
python: Convert function type annotations to Python 3 style.
Generated by com2ann (slightly patched to avoid also converting
assignment type annotations, which require Python 3.6), followed by
some manual whitespace adjustment, and six fixes for runtime issues:
- def __init__(self, token: Token, parent: Optional[Node]) -> None:
+ def __init__(self, token: Token, parent: "Optional[Node]") -> None:
-def main(options: argparse.Namespace) -> NoReturn:
+def main(options: argparse.Namespace) -> "NoReturn":
-def fetch_request(url: str, callback: Any, **kwargs: Any) -> Generator[Callable[..., Any], Any, None]:
+def fetch_request(url: str, callback: Any, **kwargs: Any) -> "Generator[Callable[..., Any], Any, None]":
-def assert_server_running(server: subprocess.Popen[bytes], log_file: Optional[str]) -> None:
+def assert_server_running(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> None:
-def server_is_up(server: subprocess.Popen[bytes], log_file: Optional[str]) -> bool:
+def server_is_up(server: "subprocess.Popen[bytes]", log_file: Optional[str]) -> bool:
- method_kwarg_pairs: List[FuncKwargPair],
+ method_kwarg_pairs: "List[FuncKwargPair]",
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-19 03:48:37 +02:00
|
|
|
def check_capitalization(strings: List[str]) -> Tuple[List[str], List[str], List[str]]:
|
2017-03-03 12:42:07 +01:00
|
|
|
errors = []
|
|
|
|
ignored = []
|
2018-03-11 06:06:54 +01:00
|
|
|
banned_word_errors = []
|
2017-03-03 12:42:07 +01:00
|
|
|
for text in strings:
|
2021-02-12 08:20:45 +01:00
|
|
|
text = " ".join(text.split()) # Remove extra whitespaces.
|
2017-03-03 12:42:07 +01:00
|
|
|
safe_text = get_safe_text(text)
|
|
|
|
has_ignored_phrase = text != safe_text
|
|
|
|
capitalized = is_capitalized(safe_text)
|
|
|
|
if not capitalized:
|
|
|
|
errors.append(text)
|
2022-06-01 00:09:19 +02:00
|
|
|
elif has_ignored_phrase:
|
2017-03-03 12:42:07 +01:00
|
|
|
ignored.append(text)
|
|
|
|
|
2018-03-11 06:06:54 +01:00
|
|
|
banned_word_errors.extend(check_banned_words(text))
|
|
|
|
|
|
|
|
return sorted(errors), sorted(ignored), sorted(banned_word_errors)
|