diff --git a/app/static/css/global.css b/app/static/css/global.css index e90351e..e96db7f 100644 --- a/app/static/css/global.css +++ b/app/static/css/global.css @@ -120,15 +120,15 @@ input[type="submit"]:focus { .bg-warn:active { background: var(--warn-active); } -img.align-left { +.align-left { text-align: left; } -img.align-center { +.align-center { display: block; margin-left: auto; margin-right: auto; } -img.align-right { +.align-right { display: block; margin-left: auto; } diff --git a/app/static/less/global.less b/app/static/less/global.less index 65ccd18..cfbf990 100644 --- a/app/static/less/global.less +++ b/app/static/less/global.less @@ -117,15 +117,15 @@ section { } } -img.align-left { +.align-left { text-align: left; } -img.align-center { +.align-center { display: block; margin-left: auto; margin-right: auto; } -img.align-right { +.align-right { display: block; margin-left: auto; } diff --git a/app/utils/bleach_allowlist.py b/app/utils/bleach_allowlist.py index 5aaf75b..8a2883e 100644 --- a/app/utils/bleach_allowlist.py +++ b/app/utils/bleach_allowlist.py @@ -10,7 +10,8 @@ markdown_tags = [ "sub", "sup", "table", "thead", "tbody", "tr", "th", "td", "form", "fieldset", "input", "textarea", - "label", "progress" + "label", "progress", + "video", "source", "iframe", ] markdown_attrs = { @@ -21,4 +22,7 @@ markdown_attrs = { "input": ["id", "name", "type", "value"], "label": ["for"], "progress": ["value", "min", "max"], + "video": ["controls", "width", "height"], + "source": ["src"], + "iframe": ["src", "width", "height", "frameborder", "allowfullscreen"], } diff --git a/app/utils/markdown_extensions/media.py b/app/utils/markdown_extensions/media.py index 8d0c170..1e21d4e 100644 --- a/app/utils/markdown_extensions/media.py +++ b/app/utils/markdown_extensions/media.py @@ -1,6 +1,8 @@ from markdown.extensions import Extension from markdown.inlinepatterns import LinkInlineProcessor import xml.etree.ElementTree as etree +import urllib +import re class MediaExtension(Extension): def __init__(self, **kwargs): @@ -49,6 +51,19 @@ class AttrDict: except ValueError: return None, None + def getAlignmentClass(self): + if self.has("left"): + return "align-left" + if self.has("center"): + return "align-center" + elif self.has("right"): + return "align-right" + elif self.has("float-left"): + return "float-left" + elif self.has("float-right"): + return "float-right" + return "" + class AttributeLinkInlineProcessor(LinkInlineProcessor): """ A LinkInlineProcessor which additionally supports attributes after links, @@ -100,8 +115,11 @@ class MediaInlineProcessor(AttributeLinkInlineProcessor): """ Return a media element from the given match. """ def isVideo(self, url): - # TODO: YouTube integration - return url.endswith(".mp4") or url.endswith(".webm") + if url.endswith(".mp4") or url.endswith(".webm"): + return True + url = urllib.parse.urlparse(url) + # TODO: Better detect YouTube URLs + return url.hostname in ["youtu.be", "www.youtube.com"] def isAudio(self, url): return url.endswith(".mp3") or url.endswith(".ogg") @@ -120,29 +138,20 @@ class MediaInlineProcessor(AttributeLinkInlineProcessor): return None, None, None kind = "image" - if attrs.has("audio") or self.isAudio(src): + if attrs.has("image"): + kind = "image" + elif attrs.has("audio") or self.isAudio(src): kind = "audio" elif attrs.has("video") or self.isVideo(src): kind = "video" - # Images - if kind == "image": w, h = attrs.getSize("size") class_ = "" # TODO: Media converter: Find a way to clear atfer a float if attrs.has("pixelated"): class_ += " pixelated" - if attrs.has("left"): - class_ += " align-left" - if attrs.has("center"): - class_ += " align-center" - elif attrs.has("right"): - class_ += " align-right" - elif attrs.has("float-left"): - class_ += " float-left" - elif attrs.has("float-right"): - class_ += " float-right" + class_ += " " + attrs.getAlignmentClass() el = etree.Element("img") el.set("src", src) @@ -160,4 +169,52 @@ class MediaInlineProcessor(AttributeLinkInlineProcessor): el.set('alt', self.unescape(text)) return el, m.start(0), index + elif kind == "audio": + # TODO: Media converter: support audio files + pass + + elif kind == "video": + w, h = attrs.getSize("size") + class_ = attrs.getAlignmentClass() + url = urllib.parse.urlparse(src) + args = urllib.parse.parse_qs(url.query) + youtube_source = None + + if url.hostname == "youtu.be" and \ + re.fullmatch(r'\/[a-zA-Z0-9_-]+', url.path): + youtube_source = url.path[1:] + elif url.hostname == "www.youtube.com" and "v" in args and \ + re.fullmatch(r'[a-zA-Z0-9_-]+', args["v"][0]): + youtube_source = args["v"][0] + + if youtube_source: + if w is None and h is None: + w, h = (470, 300) if attrs.has("tiny") else (560, 340) + + el = etree.Element("iframe") + el.set("src",f"https://www.youtube.com/embed/{youtube_source}") + el.set("frameborder", "0") + el.set("allowfullscreen", "") + # + pass + else: + el = etree.Element("video") + el.set("controls", "") + source = etree.Element("source") + source.set("src", src) + el.append(source) + # + + el.set("class", class_) + if w is not None: + el.set("width", str(min(w, 560))) + if h is not None: + el.set("height", str(min(h, 340))) + + return el, m.start(0), index + return None, None, None