2021-03-17 03:11:19 +01:00
|
|
|
import time
|
2022-07-21 21:37:10 +02:00
|
|
|
from typing import Any, Dict, List, 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
|
2022-07-22 02:19:49 +02:00
|
|
|
from django.template.backends.jinja2 import Jinja2
|
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
|
|
|
|
2022-06-26 03:37:51 +02:00
|
|
|
md_extensions: Optional[List[markdown.Extension]] = None
|
|
|
|
md_macro_extension: Optional[markdown.Extension] = 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(
|
2022-07-21 21:37:10 +02:00
|
|
|
markdown_file_path: str, context: Optional[Dict[str, Any]] = None, pure_markdown: bool = False
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> 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."""
|
2022-07-21 21:37:10 +02:00
|
|
|
if context is None:
|
|
|
|
context = {}
|
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.
|
2022-07-22 02:19:49 +02:00
|
|
|
assert isinstance(jinja, Jinja2)
|
2018-08-09 02:46:32 +02:00
|
|
|
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
|
|
|
]
|