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
75 lines
3.0 KiB
Python
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 ``.
|
|
"""
|
|
|
|
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
|