mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			222 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			222 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import time
 | 
						|
from pathlib import Path
 | 
						|
from typing import Any
 | 
						|
 | 
						|
import markdown
 | 
						|
import markdown.extensions.admonition
 | 
						|
import markdown.extensions.codehilite
 | 
						|
import markdown.extensions.extra
 | 
						|
import markdown.extensions.toc
 | 
						|
import orjson
 | 
						|
from django.conf import settings
 | 
						|
from django.contrib.staticfiles.storage import staticfiles_storage
 | 
						|
from django.template import Library, engines
 | 
						|
from django.template.backends.jinja2 import Jinja2
 | 
						|
from django.utils.safestring import mark_safe
 | 
						|
from markupsafe import Markup
 | 
						|
 | 
						|
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_settings_links
 | 
						|
import zerver.lib.markdown.include
 | 
						|
import zerver.lib.markdown.nested_code_blocks
 | 
						|
import zerver.lib.markdown.static
 | 
						|
import zerver.lib.markdown.tabbed_sections
 | 
						|
import zerver.openapi.markdown_extension
 | 
						|
from zerver.lib.cache import dict_to_items_tuple, ignore_unhashable_lru_cache, items_tuple_to_dict
 | 
						|
from zerver.openapi.merge_api_changelogs import (
 | 
						|
    get_feature_level,
 | 
						|
    get_unmerged_changelogs,
 | 
						|
    merge_changelogs,
 | 
						|
)
 | 
						|
 | 
						|
register = Library()
 | 
						|
 | 
						|
 | 
						|
def and_n_others(values: list[str | Markup], limit: int) -> str | Markup:
 | 
						|
    # A helper for the commonly appended "and N other(s)" string, with
 | 
						|
    # the appropriate pluralization.
 | 
						|
    return " and {} other{}".format(
 | 
						|
        len(values) - limit,
 | 
						|
        "" if len(values) == limit + 1 else "s",
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
def get_updated_changelog() -> str | None:
 | 
						|
    unmerged_changelogs = get_unmerged_changelogs(verbose=False)
 | 
						|
    if not unmerged_changelogs:
 | 
						|
        return None
 | 
						|
 | 
						|
    new_feature_level = get_feature_level(update_feature_level=False)
 | 
						|
    return merge_changelogs(unmerged_changelogs, new_feature_level, False)
 | 
						|
 | 
						|
 | 
						|
@register.filter(name="display_list", is_safe=True)
 | 
						|
def display_list(values: list[str | Markup], display_limit: int) -> str | Markup:
 | 
						|
    """
 | 
						|
    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
 | 
						|
    """
 | 
						|
    sep = Markup(", ") if any(isinstance(value, Markup) for value in values) else ", "
 | 
						|
    if len(values) == 1:
 | 
						|
        # One value, show it.
 | 
						|
        display_string = values[0]
 | 
						|
    elif len(values) <= display_limit:
 | 
						|
        # Fewer than `display_limit` values, show all of them.
 | 
						|
        display_string = sep.join(value for value in values[:-1])
 | 
						|
        display_string += " and " + values[-1]
 | 
						|
    else:
 | 
						|
        # More than `display_limit` values, only mention a few.
 | 
						|
        display_string = sep.join(value for value in values[:display_limit])
 | 
						|
        display_string += and_n_others(values, display_limit)
 | 
						|
 | 
						|
    return display_string
 | 
						|
 | 
						|
 | 
						|
md_extensions: list[markdown.Extension] | None = None
 | 
						|
md_macro_extension: markdown.Extension | None = None
 | 
						|
# 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 = [
 | 
						|
    "incoming-webhooks-walkthrough.md",
 | 
						|
]
 | 
						|
 | 
						|
 | 
						|
# 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
 | 
						|
@ignore_unhashable_lru_cache(512)
 | 
						|
@items_tuple_to_dict
 | 
						|
@register.filter(name="render_markdown_path", is_safe=True)
 | 
						|
def render_markdown_path(
 | 
						|
    markdown_file_path: str,
 | 
						|
    context: dict[str, Any] | None = None,
 | 
						|
    integration_doc: bool = False,
 | 
						|
    help_center: bool = False,
 | 
						|
) -> str:
 | 
						|
    """Given a path to a Markdown file, return the rendered HTML.
 | 
						|
 | 
						|
    Note that this assumes that any HTML in the Markdown file is
 | 
						|
    trusted; it is intended to be used for documentation, not user
 | 
						|
    data."""
 | 
						|
 | 
						|
    # We set this global hackishly
 | 
						|
    from zerver.lib.markdown.help_settings_links import set_relative_settings_links
 | 
						|
 | 
						|
    relative_links = bool(context is not None and context.get("html_settings_links"))
 | 
						|
    set_relative_settings_links(relative_links)
 | 
						|
 | 
						|
    global md_extensions, md_macro_extension
 | 
						|
    if md_extensions is None:
 | 
						|
        md_extensions = [
 | 
						|
            markdown.extensions.extra.makeExtension(),
 | 
						|
            markdown.extensions.toc.makeExtension(),
 | 
						|
            markdown.extensions.admonition.makeExtension(),
 | 
						|
            markdown.extensions.codehilite.makeExtension(
 | 
						|
                linenums=False,
 | 
						|
                guess_lang=False,
 | 
						|
            ),
 | 
						|
            zerver.lib.markdown.fenced_code.makeExtension(
 | 
						|
                run_content_validators=bool(
 | 
						|
                    context is not None and context.get("run_content_validators", False)
 | 
						|
                ),
 | 
						|
            ),
 | 
						|
            zerver.lib.markdown.api_arguments_table_generator.makeExtension(),
 | 
						|
            zerver.lib.markdown.api_return_values_table_generator.makeExtension(),
 | 
						|
            zerver.lib.markdown.nested_code_blocks.makeExtension(),
 | 
						|
            zerver.lib.markdown.tabbed_sections.makeExtension(),
 | 
						|
            zerver.lib.markdown.help_settings_links.makeExtension(),
 | 
						|
            zerver.lib.markdown.static.makeExtension(),
 | 
						|
        ]
 | 
						|
    if context is not None and "api_url" in context:
 | 
						|
        # 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.
 | 
						|
        extensions = [
 | 
						|
            zerver.openapi.markdown_extension.makeExtension(
 | 
						|
                api_url=context["api_url"],
 | 
						|
            ),
 | 
						|
            *md_extensions,
 | 
						|
        ]
 | 
						|
    else:
 | 
						|
        extensions = md_extensions
 | 
						|
 | 
						|
    if integration_doc:
 | 
						|
        md_macro_extension = zerver.lib.markdown.include.makeExtension(
 | 
						|
            base_path="templates/zerver/integrations/include/"
 | 
						|
        )
 | 
						|
    elif help_center:
 | 
						|
        md_macro_extension = zerver.lib.markdown.include.makeExtension(base_path="help/include/")
 | 
						|
    else:
 | 
						|
        md_macro_extension = zerver.lib.markdown.include.makeExtension(
 | 
						|
            base_path="api_docs/include/"
 | 
						|
        )
 | 
						|
    if not any(doc in markdown_file_path for doc in docs_without_macros):
 | 
						|
        extensions = [md_macro_extension, *extensions]
 | 
						|
 | 
						|
    md_engine = markdown.Markdown(extensions=extensions)
 | 
						|
    md_engine.reset()
 | 
						|
 | 
						|
    jinja = engines["Jinja2"]
 | 
						|
    assert isinstance(jinja, Jinja2)
 | 
						|
    if markdown_file_path.startswith("/"):
 | 
						|
        with open(markdown_file_path) as fp:
 | 
						|
            markdown_string = fp.read()
 | 
						|
    else:
 | 
						|
        markdown_string = jinja.env.loader.get_source(jinja.env, markdown_file_path)[0]
 | 
						|
 | 
						|
    if (
 | 
						|
        settings.DEVELOPMENT
 | 
						|
        and Path(markdown_file_path).resolve() == Path("api_docs/changelog.md").resolve()
 | 
						|
    ):
 | 
						|
        updated_changelog = get_updated_changelog()
 | 
						|
        if updated_changelog is not None:
 | 
						|
            markdown_string = updated_changelog
 | 
						|
 | 
						|
    API_ENDPOINT_NAME = context.get("API_ENDPOINT_NAME", "") if context is not None else ""
 | 
						|
    markdown_string = markdown_string.replace("API_ENDPOINT_NAME", API_ENDPOINT_NAME)
 | 
						|
 | 
						|
    html = md_engine.convert(markdown_string)
 | 
						|
    if context is None:
 | 
						|
        return mark_safe(html)  # noqa: S308
 | 
						|
 | 
						|
    return mark_safe(jinja.from_string(html).render(context))  # noqa: S308
 | 
						|
 | 
						|
 | 
						|
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"]
 | 
						|
        if not settings.DEBUG or status != "compile":
 | 
						|
            break
 | 
						|
        time.sleep(0.2)
 | 
						|
 | 
						|
    if status != "done":
 | 
						|
        raise RuntimeError("Webpack compilation was not successful")
 | 
						|
 | 
						|
    try:
 | 
						|
        files_from_entrypoints = [
 | 
						|
            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")
 | 
						|
        ]
 | 
						|
    except KeyError:
 | 
						|
        raise KeyError(
 | 
						|
            f"'{entrypoint}' entrypoint could not be found. Please define it in web/webpack.assets.json."
 | 
						|
        )
 | 
						|
 | 
						|
    return files_from_entrypoints
 |