mirror of
https://github.com/zensical/zensical.git
synced 2026-10-08 05:41:24 +00:00
240 lines
8.1 KiB
Python
Vendored
240 lines
8.1 KiB
Python
Vendored
# Copyright (c) 2025-2026 Zensical and contributors
|
|
#
|
|
# SPDX-License-Identifier: MIT
|
|
# All contributions are certified under the DCO
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to
|
|
# deal in the Software without restriction, including without limitation the
|
|
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
# sell copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
# IN THE SOFTWARE.
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from markdown import Markdown
|
|
from markdown.postprocessors import Postprocessor
|
|
|
|
from tests.unit.extensions.conftest import soup
|
|
from zensical.extensions.glightbox import GlightboxExtension
|
|
from zensical.extensions.links import LinksExtension
|
|
from zensical.extensions.media import MediaExtension
|
|
|
|
|
|
def _markdown(**config: object) -> Markdown:
|
|
return Markdown(extensions=["attr_list", MediaExtension(**config)])
|
|
|
|
|
|
def test_default_video_and_audio_embeds() -> None:
|
|
md = _markdown()
|
|
page = soup(
|
|
md.convert(
|
|
"\n\n"
|
|
""
|
|
)
|
|
)
|
|
|
|
iframe = page.select_one(".video-container > iframe")
|
|
assert iframe is not None
|
|
assert iframe["src"] == "https://example.com/embed/1"
|
|
assert iframe["style"] == "position:relative;width:100%;height:22.172vw"
|
|
assert iframe["frameborder"] == "0"
|
|
assert iframe.has_attr("allowfullscreen")
|
|
|
|
audio = page.select_one(".audio-container > audio")
|
|
assert audio is not None
|
|
assert audio.has_attr("controls")
|
|
assert audio["style"] == "width:100%"
|
|
source = audio.find("source")
|
|
assert source is not None
|
|
assert source["src"] == "sound.mp3"
|
|
assert source["type"] == "audio/mp3"
|
|
|
|
|
|
@pytest.mark.parametrize("kind", ["video", "audio"])
|
|
def test_media_can_be_enabled_independently(kind: str) -> None:
|
|
config = {
|
|
"video": {"enabled": kind == "video"},
|
|
"audio": {"enabled": kind == "audio"},
|
|
}
|
|
page = soup(
|
|
_markdown(**config).convert(
|
|
"\n\n"
|
|
)
|
|
)
|
|
|
|
assert (page.select_one(".video-container") is not None) == (
|
|
kind == "video"
|
|
)
|
|
assert (page.select_one(".audio-container") is not None) == (
|
|
kind == "audio"
|
|
)
|
|
assert len(page.find_all("img")) == 1
|
|
|
|
|
|
def test_video_element_options_and_attribute_overrides() -> None:
|
|
md = _markdown(
|
|
video={
|
|
"is_video": True,
|
|
"video_type": " OGG ",
|
|
"video_muted": True,
|
|
"video_loop": True,
|
|
"video_controls": False,
|
|
"video_autoplay": True,
|
|
"css_style": {"width": "50%"},
|
|
},
|
|
audio={"enabled": False},
|
|
)
|
|
page = soup(
|
|
md.convert(
|
|
""
|
|
"{: src='other.ogg' style='width: 80%' .demo}"
|
|
)
|
|
)
|
|
|
|
video = page.select_one(".video-container > video.demo")
|
|
assert video is not None
|
|
assert video["style"] == "width: 80%"
|
|
assert video.has_attr("muted")
|
|
assert video.has_attr("loop")
|
|
assert video.has_attr("autoplay")
|
|
assert not video.has_attr("controls")
|
|
assert not video.has_attr("src")
|
|
assert video.source is not None
|
|
assert video.source["src"] == "other.ogg"
|
|
assert video.source["type"] == "video/ogg"
|
|
|
|
|
|
def test_audio_options_and_disabling_globals() -> None:
|
|
md = _markdown(
|
|
video={"enabled": False},
|
|
audio={
|
|
"mark": "sound",
|
|
"audio_type": " OGG ",
|
|
"audio_controls": False,
|
|
"audio_loop": True,
|
|
"audio_autoplay": True,
|
|
},
|
|
)
|
|
page = soup(
|
|
md.convert(
|
|
"\n\n"
|
|
"{: disable-global-config style='width: 60%'}"
|
|
)
|
|
)
|
|
|
|
first, second = page.select(".audio-container > audio")
|
|
assert not first.has_attr("controls")
|
|
assert first.has_attr("loop")
|
|
assert first.has_attr("autoplay")
|
|
assert first.source is not None
|
|
assert first.source["type"] == "audio/ogg"
|
|
assert second["style"] == "width: 60%"
|
|
assert not second.has_attr("loop")
|
|
assert not second.has_attr("autoplay")
|
|
assert not second.has_attr("disable-global-config")
|
|
|
|
|
|
def test_raw_html_and_markdown_media_precede_image_and_url_processing() -> None:
|
|
md = Markdown(
|
|
extensions=[
|
|
"attr_list",
|
|
GlightboxExtension(),
|
|
MediaExtension(),
|
|
LinksExtension(path="guide/page.md", use_directory_urls=True),
|
|
]
|
|
)
|
|
html = md.convert(
|
|
'<p><img alt="type:audio" src="sound.mp3"></p>\n\n'
|
|
"\n\n"
|
|
)
|
|
page = soup(html)
|
|
|
|
audio_source = page.select_one(".audio-container source")
|
|
video_frame = page.select_one(".video-container iframe")
|
|
ordinary_img = page.select_one("a.glightbox img")
|
|
assert audio_source is not None
|
|
assert video_frame is not None
|
|
assert ordinary_img is not None
|
|
assert audio_source["src"] == "../sound.mp3"
|
|
assert video_frame["src"] == "../movie.mp4"
|
|
assert len(page.select("a.glightbox")) == 1
|
|
assert ordinary_img["src"] == "../picture.png"
|
|
|
|
|
|
def test_raw_html_preserves_unrelated_markup_and_inline_text() -> None:
|
|
md = _markdown()
|
|
html = md.convert(
|
|
"<script>const example = "
|
|
"\"<img alt='type:video' src='x'>\";"
|
|
"</script>\n\n"
|
|
'<p class="demo">before <img alt="type:video" src="a>b.mp4"> after</p>'
|
|
)
|
|
assert (
|
|
"<script>const example = \"<img alt='type:video' src='x'>\";</script>"
|
|
in html
|
|
)
|
|
assert 'class="demo"' in html
|
|
assert "after</p>" in html
|
|
frame = soup(html).select_one(".video-container iframe")
|
|
assert frame is not None
|
|
assert frame["src"] == "a>b.mp4"
|
|
|
|
|
|
def test_tree_preserves_text_after_inline_image() -> None:
|
|
html = _markdown().convert("before  after")
|
|
assert "after</p>" in html
|
|
|
|
|
|
def test_source_url_restores_markdown_escapes() -> None:
|
|
html = _markdown().convert(r"")
|
|
source = soup(html).select_one(".audio-container source")
|
|
assert source is not None
|
|
assert source["src"] == "a_b.mp3?x=1&y=2"
|
|
|
|
|
|
def test_markdown_entities_match_media_markers_and_sources() -> None:
|
|
html = _markdown().convert("")
|
|
source = soup(html).select_one(".audio-container source")
|
|
assert source is not None
|
|
assert source["src"] == "a.mp3"
|
|
|
|
|
|
def test_raw_html_entities_are_decoded_only_once() -> None:
|
|
html = _markdown().convert('<img alt="type:audio" src="a&amp;b.mp3">')
|
|
source = soup(html).select_one(".audio-container source")
|
|
assert source is not None
|
|
assert source["src"] == "a&b.mp3"
|
|
|
|
|
|
def test_media_from_late_markdown_postprocessor() -> None:
|
|
class LateImage(Postprocessor):
|
|
def run(self, text: str) -> str:
|
|
return text + '<p><img alt="type:audio" src="late.mp3"></p>'
|
|
|
|
md = _markdown()
|
|
md.postprocessors.register(LateImage(md), "late_image", 25)
|
|
|
|
source = soup(md.convert("# Media")).select_one(".audio-container source")
|
|
assert source is not None
|
|
assert source["src"] == "late.mp3"
|
|
|
|
|
|
def test_markdown_instance_can_be_reset() -> None:
|
|
md = _markdown()
|
|
assert "audio-container" in md.convert('<img alt="type:audio" src="a.mp3">')
|
|
md.reset()
|
|
assert "audio-container" in md.convert('<img alt="type:audio" src="b.mp3">')
|