Files
upstream-zulip/zerver/lib/markdown/from_html.py
upstream-mirror 64898b2c79
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
2026-09-15 15:05:30 +05:00

75 lines
3.0 KiB
Python

import posixpath
import re
import sys
import warnings
from urllib.parse import urlsplit
import markdownify
from bs4 import MarkupResemblesLocatorWarning, Tag
from zerver.lib.markdown import get_markdown_link_for_url, sanitize_url
class ZulipMarkdownConverter(markdownify.MarkdownConverter):
"""HTML-to-Markdown converter adapted to Zulip's Markdown.
Zulip doesn't inline-render external images, so images become
`[label](src)` links instead of broken `![alt](src)`.
"""
def convert_img(self, el: Tag, text: str, parent_tags: set[str]) -> str:
src = el.get("src", "")
alt = el.get("alt", "")
# BeautifulSoup returns a list only for multi-valued attributes
# (e.g. class), never for src or alt.
assert isinstance(src, str)
assert isinstance(alt, str)
if not src:
return alt
url = sanitize_url(src)
if not url:
# Unlinkable src (e.g. data: URIs, unsafe schemes): emit just the
# alt text rather than a bogus link.
return alt
label = alt or posixpath.basename(urlsplit(url).path)
if not label:
# No alt text, and no filename in the path to fall back on, as in
# `<img src="https://example.com/?token=abc">`.
return url
if "a" in parent_tags:
# Markdown has no nested links, so hand the label up for the
# enclosing <a> to use as its text, with brackets stripped.
return re.sub(r"\[|\]", "", label)
return get_markdown_link_for_url(label, url)
def convert_title(self, el: Tag, text: str, parent_tags: set[str]) -> str:
return ""
def convert_html_to_markdown(
html: str, *, converter_class: type[ZulipMarkdownConverter] = ZulipMarkdownConverter
) -> str:
# A bare URL is expected input here (the text/html part of an incoming
# email), but BeautifulSoup warns about it ("looks like a URL, not markup").
# Suppress the warning so that expected input doesn't spam our logs.
with warnings.catch_warnings():
warnings.simplefilter("ignore", MarkupResemblesLocatorWarning)
converter = converter_class(
# Zulip's Markdown supports only ATX headings, not Setext headings.
heading_style="ATX",
# Zulip's Markdown has no backslash escaping; it renders "\" as a
# literal character.
escape_asterisks=False,
escape_underscores=False,
# HTML treats a newline as insignificant whitespace, but Zulip
# renders it as a line break. Collapse source newlines to spaces.
# <br>, <pre>, and block-level elements still produce line breaks.
wrap=True,
# An effectively infinite width disables column wrapping. Don't
# use None, which also disables wrapping but skips the pass that
# strips indentation from lines following a <br>.
wrap_width=sys.maxsize,
)
markdown: str = converter.convert(html).strip()
return markdown