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