2021-03-17 03:11:19 +01:00
|
|
|
import time
|
2020-06-13 03:34:01 +02:00
|
|
|
from typing import Any, List, Mapping, Optional
|
2016-11-09 02:00:28 +01:00
|
|
|
|
2016-11-09 21:52:42 +01:00
|
|
|
import markdown
|
2016-12-21 19:50:48 +01:00
|
|
|
import markdown.extensions.admonition
|
2016-11-10 06:32:15 +01:00
|
|
|
import markdown.extensions.codehilite
|
2017-11-16 00:49:39 +01:00
|
|
|
import markdown.extensions.extra
|
2016-11-10 06:32:15 +01:00
|
|
|
import markdown.extensions.toc
|
2021-03-17 03:11:19 +01:00
|
|
|
import orjson
|
|
|
|
from django.conf import settings
|
|
|
|
from django.contrib.staticfiles.storage import staticfiles_storage
|
2019-02-02 23:53:55 +01:00
|
|
|
from django.template import Library, engines
|
2017-11-16 00:49:39 +01:00
|
|
|
from django.utils.safestring import mark_safe
|
2018-08-09 02:46:32 +02:00
|
|
|
from jinja2.exceptions import TemplateNotFound
|
2017-11-16 00:49:39 +01:00
|
|
|
|
2020-06-25 15:00:33 +02:00
|
|
|
import zerver.lib.markdown.api_arguments_table_generator
|
|
|
|
import zerver.lib.markdown.api_return_values_table_generator
|
|
|
|
import zerver.lib.markdown.fenced_code
|
|
|
|
import zerver.lib.markdown.help_emoticon_translations_table
|
|
|
|
import zerver.lib.markdown.help_relative_links
|
|
|
|
import zerver.lib.markdown.help_settings_links
|
|
|
|
import zerver.lib.markdown.include
|
|
|
|
import zerver.lib.markdown.nested_code_blocks
|
|
|
|
import zerver.lib.markdown.tabbed_sections
|
2020-06-11 00:54:34 +02:00
|
|
|
import zerver.openapi.markdown_extension
|
|
|
|
from zerver.lib.cache import dict_to_items_tuple, ignore_unhashable_lru_cache, items_tuple_to_dict
|
2016-11-09 21:52:42 +01:00
|
|
|
|
2013-10-25 18:54:03 +02:00
|
|
|
register = Library()
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2018-03-12 04:12:03 +01:00
|
|
|
def and_n_others(values: List[str], limit: int) -> str:
|
2013-10-25 18:54:03 +02:00
|
|
|
# A helper for the commonly appended "and N other(s)" string, with
|
|
|
|
# the appropriate pluralization.
|
2020-06-13 08:59:37 +02:00
|
|
|
return " and {} other{}".format(
|
2021-02-12 08:19:30 +01:00
|
|
|
len(values) - limit,
|
|
|
|
"" if len(values) == limit + 1 else "s",
|
2020-06-13 08:59:37 +02:00
|
|
|
)
|
2013-10-25 18:54:03 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
@register.filter(name="display_list", is_safe=True)
|
2018-03-12 04:12:03 +01:00
|
|
|
def display_list(values: List[str], display_limit: int) -> str:
|
2013-10-25 18:54:03 +02:00
|
|
|
"""
|
|
|
|
Given a list of values, return a string nicely formatting those values,
|
|
|
|
summarizing when you have more than `display_limit`. Eg, for a
|
|
|
|
`display_limit` of 3 we get the following possible cases:
|
|
|
|
|
|
|
|
Jessica
|
|
|
|
Jessica and Waseem
|
|
|
|
Jessica, Waseem, and Tim
|
|
|
|
Jessica, Waseem, Tim, and 1 other
|
|
|
|
Jessica, Waseem, Tim, and 2 others
|
|
|
|
"""
|
|
|
|
if len(values) == 1:
|
|
|
|
# One value, show it.
|
2020-06-10 06:41:04 +02:00
|
|
|
display_string = f"{values[0]}"
|
2013-10-25 18:54:03 +02:00
|
|
|
elif len(values) <= display_limit:
|
|
|
|
# Fewer than `display_limit` values, show all of them.
|
2021-02-12 08:19:30 +01:00
|
|
|
display_string = ", ".join(f"{value}" for value in values[:-1])
|
2020-06-10 06:41:04 +02:00
|
|
|
display_string += f" and {values[-1]}"
|
2013-10-25 18:54:03 +02:00
|
|
|
else:
|
|
|
|
# More than `display_limit` values, only mention a few.
|
2021-02-12 08:19:30 +01:00
|
|
|
display_string = ", ".join(f"{value}" for value in values[:display_limit])
|
2013-10-25 18:54:03 +02:00
|
|
|
display_string += and_n_others(values, display_limit)
|
|
|
|
|
|
|
|
return display_string
|
2016-05-11 19:01:53 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
md_extensions: Optional[List[Any]] = None
|
|
|
|
md_macro_extension: Optional[Any] = None
|
2017-11-22 17:20:58 +01:00
|
|
|
# Prevent the automatic substitution of macros in these docs. If
|
|
|
|
# they contain a macro, it is always used literally for documenting
|
|
|
|
# the macro system.
|
|
|
|
docs_without_macros = [
|
2018-10-17 05:08:27 +02:00
|
|
|
"incoming-webhooks-walkthrough.md",
|
2017-11-22 17:20:58 +01:00
|
|
|
]
|
2016-11-10 06:32:15 +01:00
|
|
|
|
2019-04-18 04:35:14 +02:00
|
|
|
# render_markdown_path is passed a context dictionary (unhashable), which
|
|
|
|
# results in the calls not being cached. To work around this, we convert the
|
|
|
|
# dict to a tuple of dict items to cache the results.
|
|
|
|
@dict_to_items_tuple
|
2018-01-12 09:02:01 +01:00
|
|
|
@ignore_unhashable_lru_cache(512)
|
2019-04-18 04:35:14 +02:00
|
|
|
@items_tuple_to_dict
|
2021-02-12 08:20:45 +01:00
|
|
|
@register.filter(name="render_markdown_path", is_safe=True)
|
2021-02-12 08:19:30 +01:00
|
|
|
def render_markdown_path(
|
|
|
|
markdown_file_path: str, context: Mapping[str, Any] = {}, pure_markdown: bool = False
|
|
|
|
) -> str:
|
2020-08-11 01:47:54 +02:00
|
|
|
"""Given a path to a Markdown file, return the rendered HTML.
|
2016-11-09 01:19:53 +01:00
|
|
|
|
2020-08-11 01:47:49 +02:00
|
|
|
Note that this assumes that any HTML in the Markdown file is
|
2016-11-09 01:19:53 +01:00
|
|
|
trusted; it is intended to be used for documentation, not user
|
|
|
|
data."""
|
2018-04-18 04:31:57 +02:00
|
|
|
|
|
|
|
# We set this global hackishly
|
2020-06-25 15:00:33 +02:00
|
|
|
from zerver.lib.markdown.help_settings_links import set_relative_settings_links
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
set_relative_settings_links(bool(context.get("html_settings_links")))
|
2020-06-25 15:00:33 +02:00
|
|
|
from zerver.lib.markdown.help_relative_links import set_relative_help_links
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
set_relative_help_links(bool(context.get("html_settings_links")))
|
2018-04-18 04:31:57 +02:00
|
|
|
|
2016-12-16 10:07:08 +01:00
|
|
|
global md_extensions
|
2017-11-22 17:20:58 +01:00
|
|
|
global md_macro_extension
|
2016-12-16 10:07:08 +01:00
|
|
|
if md_extensions is None:
|
|
|
|
md_extensions = [
|
2017-07-25 02:35:59 +02:00
|
|
|
markdown.extensions.extra.makeExtension(),
|
2016-11-10 06:32:15 +01:00
|
|
|
markdown.extensions.toc.makeExtension(),
|
2016-12-21 19:50:48 +01:00
|
|
|
markdown.extensions.admonition.makeExtension(),
|
2016-11-10 06:32:15 +01:00
|
|
|
markdown.extensions.codehilite.makeExtension(
|
|
|
|
linenums=False,
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
guess_lang=False,
|
2016-11-10 06:32:15 +01:00
|
|
|
),
|
2020-06-25 15:00:33 +02:00
|
|
|
zerver.lib.markdown.fenced_code.makeExtension(
|
2021-02-12 08:20:45 +01:00
|
|
|
run_content_validators=context.get("run_content_validators", False),
|
2019-05-16 22:38:53 +02:00
|
|
|
),
|
2020-06-25 15:00:33 +02:00
|
|
|
zerver.lib.markdown.api_arguments_table_generator.makeExtension(
|
2021-02-12 08:20:45 +01:00
|
|
|
base_path="templates/zerver/api/"
|
2021-02-12 08:19:30 +01:00
|
|
|
),
|
2020-06-25 15:00:33 +02:00
|
|
|
zerver.lib.markdown.api_return_values_table_generator.makeExtension(
|
2021-02-12 08:20:45 +01:00
|
|
|
base_path="templates/zerver/api/"
|
2021-02-12 08:19:30 +01:00
|
|
|
),
|
2020-06-25 15:00:33 +02:00
|
|
|
zerver.lib.markdown.nested_code_blocks.makeExtension(),
|
|
|
|
zerver.lib.markdown.tabbed_sections.makeExtension(),
|
|
|
|
zerver.lib.markdown.help_settings_links.makeExtension(),
|
|
|
|
zerver.lib.markdown.help_relative_links.makeExtension(),
|
|
|
|
zerver.lib.markdown.help_emoticon_translations_table.makeExtension(),
|
2016-12-16 10:07:08 +01:00
|
|
|
]
|
2017-11-22 17:20:58 +01:00
|
|
|
if md_macro_extension is None:
|
2020-06-25 15:00:33 +02:00
|
|
|
md_macro_extension = zerver.lib.markdown.include.makeExtension(
|
2021-02-12 08:20:45 +01:00
|
|
|
base_path="templates/zerver/help/include/"
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2021-02-12 08:20:45 +01:00
|
|
|
if "api_url" in context:
|
2019-08-16 21:17:01 +02:00
|
|
|
# We need to generate the API code examples extension each
|
|
|
|
# time so the `api_url` config parameter can be set dynamically.
|
|
|
|
#
|
|
|
|
# TODO: Convert this to something more efficient involving
|
|
|
|
# passing the API URL as a direct parameter.
|
2021-02-12 08:19:30 +01:00
|
|
|
extensions = [
|
|
|
|
zerver.openapi.markdown_extension.makeExtension(
|
|
|
|
api_url=context["api_url"],
|
|
|
|
),
|
2021-06-02 18:36:25 +02:00
|
|
|
*md_extensions,
|
2021-02-12 08:19:30 +01:00
|
|
|
]
|
2021-06-02 18:36:25 +02:00
|
|
|
else:
|
|
|
|
extensions = md_extensions
|
|
|
|
|
2019-08-16 21:17:01 +02:00
|
|
|
if not any(doc in markdown_file_path for doc in docs_without_macros):
|
2020-09-02 06:59:07 +02:00
|
|
|
extensions = [md_macro_extension, *extensions]
|
2017-11-22 17:20:58 +01:00
|
|
|
|
2019-08-16 21:17:01 +02:00
|
|
|
md_engine = markdown.Markdown(extensions=extensions)
|
2016-12-16 10:07:08 +01:00
|
|
|
md_engine.reset()
|
2016-11-10 06:32:15 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
jinja = engines["Jinja2"]
|
2018-08-09 22:45:54 +02:00
|
|
|
try:
|
2020-08-11 01:47:49 +02:00
|
|
|
# By default, we do both Jinja2 templating and Markdown
|
2018-08-09 02:46:32 +02:00
|
|
|
# processing on the file, to make it easy to use both Jinja2
|
|
|
|
# context variables and markdown includes in the file.
|
|
|
|
markdown_string = jinja.env.loader.get_source(jinja.env, markdown_file_path)[0]
|
2018-08-09 22:45:54 +02:00
|
|
|
except TemplateNotFound as e:
|
|
|
|
if pure_markdown:
|
|
|
|
# For files such as /etc/zulip/terms.md where we don't intend
|
|
|
|
# to use Jinja2 template variables, we still try to load the
|
|
|
|
# template using Jinja2 (in case the file path isn't absolute
|
|
|
|
# and does happen to be in Jinja's recognized template
|
|
|
|
# directories), and if that fails, we try to load it directly
|
|
|
|
# from disk.
|
|
|
|
with open(markdown_file_path) as fp:
|
|
|
|
markdown_string = fp.read()
|
|
|
|
else:
|
|
|
|
raise e
|
|
|
|
|
2021-05-23 13:27:41 +02:00
|
|
|
API_ENDPOINT_NAME = context.get("API_ENDPOINT_NAME", "")
|
|
|
|
markdown_string = markdown_string.replace("API_ENDPOINT_NAME", API_ENDPOINT_NAME)
|
2018-08-09 22:45:54 +02:00
|
|
|
html = md_engine.convert(markdown_string)
|
|
|
|
rendered_html = jinja.from_string(html).render(context)
|
2018-08-09 02:46:32 +02:00
|
|
|
|
|
|
|
return mark_safe(rendered_html)
|
2021-03-17 03:11:19 +01:00
|
|
|
|
|
|
|
|
|
|
|
def webpack_entry(entrypoint: str) -> List[str]:
|
|
|
|
while True:
|
|
|
|
with open(settings.WEBPACK_STATS_FILE, "rb") as f:
|
|
|
|
stats = orjson.loads(f.read())
|
|
|
|
status = stats["status"]
|
2021-03-17 03:11:41 +01:00
|
|
|
if not settings.DEBUG or status != "compile":
|
2021-03-17 03:11:19 +01:00
|
|
|
break
|
|
|
|
time.sleep(0.2)
|
|
|
|
|
|
|
|
if status != "done":
|
|
|
|
raise RuntimeError("Webpack compilation was not successful")
|
|
|
|
|
|
|
|
return [
|
2021-03-17 03:11:41 +01:00
|
|
|
staticfiles_storage.url(settings.WEBPACK_BUNDLES + filename)
|
|
|
|
for filename in stats["chunks"][entrypoint]
|
|
|
|
if filename.endswith((".css", ".js")) and not filename.endswith(".hot-update.js")
|
2021-03-17 03:11:19 +01:00
|
|
|
]
|