From a78dcf3e9e37faf693ef4bdbddc393e246999b0d Mon Sep 17 00:00:00 2001 From: facelessuser Date: Wed, 22 Sep 2021 19:45:37 -0600 Subject: [PATCH 1/6] Experimental: Recognize audio/video links in img syntax --- pymdownx/magiclink.py | 131 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/pymdownx/magiclink.py b/pymdownx/magiclink.py index 61978b53b..135f24c36 100644 --- a/pymdownx/magiclink.py +++ b/pymdownx/magiclink.py @@ -30,6 +30,7 @@ from . import util import re from markdown.inlinepatterns import LinkInlineProcessor, InlineProcessor +import copy MAGIC_LINK = 1 MAGIC_AUTO_LINK = 2 @@ -217,6 +218,128 @@ } +class MagiclinkMediaTreeprocessor(Treeprocessor): + """ + Find video links and parse them up. + + We'll use the image link syntax to identify our links of interest. + """ + + # Current recognized file extensions + MIMES = re.compile(r'(?i).*?\.(mp3|ogg|wav|flac|mp4|webm)$') + + # Default MIME types, but can be overridden + MIMEMAP = { + 'mp4': 'video/mp4', + 'webm': 'video/webm', + 'mp3': 'audio/mpeg', + 'ogg': 'audio/ogg', + 'wav': 'audio/wav', + 'flac': 'audio/flac' + } + + def __init__(self, md, in_img, in_anchor): + """Initialize.""" + + self.in_img = in_img + self.in_anchor = in_anchor + + super().__init__(md) + + def run(self, root): + """Shorten popular git repository links.""" + + # Grab the elements of interest and form a parent mapping + links = [e for e in root.iter() if e.tag.lower() in ('img',)] + parent_map = {c: p for p in root.iter() for c in p} + + # Evaluate links + for link in reversed(links): + + # Save the attributes as we will reuse them + attrib = copy.copy(link.attrib) + + # See if source matches the audio or video mime type + src = attrib.get('src', '') + m = self.MIMES.match(src) + if m is None: + continue + + # Use whatever audio/video type specified or construct our own + # Reject any other types + mime = m.group(1).lower() + + # We don't know what case the attributes are in + # so take care to find them. Sort out source attributes + # and audio/video attributes. + stop = False + src_attrib = {'src': src} + del attrib['src'] + for k in list(attrib.keys()): + key = k.lower() + + if key == 'type': + v = attrib[k] + t = v.lower().split('/')[0] + if t in ('audio', 'video'): + src_attrib[key] = v + del attrib[k] + else: + stop = True + break + + elif key in ('srcset', 'sizes', 'media'): + src_attrib[key] = attrib[k] + del attrib[key] + + # We must have found an incompatible type + if stop: + break + + # No type found, set it from our mapping + if 'type' not in src_attrib: + src_attrib['type'] = self.MIMEMAP[mime] + + # Setup media controls + attrib['controls'] = '' + + # Build the source element and apply the right type + source = etree.Element('source', src_attrib) + + # Find the parent and check if the next sibling is already a media group + # that we can attach to. If so, the current link will become the primary + # source, and the existing will become the fallback. + parent = parent_map[link] + one_more = False + sibling = None + index = -1 + mtype = src_attrib['type'][:5].lower() + for i, c in enumerate(parent, 0): + if one_more: + # If there is another sibling, see if it is already a video container + if c.tag.lower() == mtype: + sibling = c + break + if c is link: + # Found where we live, now let's find our sibling + index = i + one_more = True + + # Attach the media source as the primary source, or construct a new group. + if sibling is not None: + sibling.insert(0, source) + sibling.attrib = attrib + else: + media = etree.Element(mtype, attrib) + media.append(source) + parent.insert(index, media) + + # Remove the old link + parent.remove(link) + + return root + + class _MagiclinkShorthandPattern(InlineProcessor): """Base shorthand link class.""" @@ -992,6 +1115,12 @@ def setup_shortener(self, md, base_url, base_user_url, config, repo_shortner, so shortener.config = config md.treeprocessors.register(shortener, "magic-repo-shortener", 9.9) + def setup_media(self, md, in_img, in_anchor): + """Setup media transformations.""" + + media = MagiclinkMediaTreeprocessor(md, in_img, in_anchor) + md.treeprocessors.register(media, 'magic-video', 7.9) + def get_base_urls(self, config): """Get base URLs.""" @@ -1053,6 +1182,8 @@ def extendMarkdown(self, md): base_url, base_user_url = self.get_base_urls(config) self.setup_shortener(md, base_url, base_user_url, config, self.repo_shortner, self.social_shortener) + self.setup_media(md, True, True) + def makeExtension(*args, **kwargs): """Return extension.""" From e8b360c176262edec116cf783a4ee8b043f2064d Mon Sep 17 00:00:00 2001 From: facelessuser Date: Wed, 22 Sep 2021 21:12:45 -0600 Subject: [PATCH 2/6] Provide a message and download as absolute last fallback Use alt as a description in the message Also, use sibling videos/audio if fallback is defined --- pymdownx/magiclink.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pymdownx/magiclink.py b/pymdownx/magiclink.py index 135f24c36..7023df620 100644 --- a/pymdownx/magiclink.py +++ b/pymdownx/magiclink.py @@ -238,6 +238,10 @@ class MagiclinkMediaTreeprocessor(Treeprocessor): 'flac': 'audio/flac' } + MEDIA_FAIL = "Sorry, your browser doesn't support embedded videos." + MEDIA_DOWNLOAD = "Try downloading a copy of the video." + MEDIA_DESCRIPTION = "Description: {}" + def __init__(self, md, in_img, in_anchor): """Initialize.""" @@ -275,10 +279,14 @@ def run(self, root): stop = False src_attrib = {'src': src} del attrib['src'] + alt = '' for k in list(attrib.keys()): key = k.lower() - if key == 'type': + if key == 'alt': + alt = attrib[k] + del attrib[k] + elif key == 'type': v = attrib[k] t = v.lower().split('/')[0] if t in ('audio', 'video'): @@ -317,7 +325,7 @@ def run(self, root): for i, c in enumerate(parent, 0): if one_more: # If there is another sibling, see if it is already a video container - if c.tag.lower() == mtype: + if c.tag.lower() == mtype and 'fallback' in c.attrib: sibling = c break if c is link: @@ -329,9 +337,17 @@ def run(self, root): if sibling is not None: sibling.insert(0, source) sibling.attrib = attrib + last = list(sibling)[-1] + last.attrib['href'] = src + last.tail = md_util.AtomicString(self.MEDIA_DESCRIPTION.format(alt) if alt else '') else: media = etree.Element(mtype, attrib) media.append(source) + source.tail = md_util.AtomicString(self.MEDIA_FAIL) + download = etree.SubElement(media, 'a', {"href": src, "download": ""}) + download.text = md_util.AtomicString(self.MEDIA_DOWNLOAD) + if alt: + download.tail = md_util.AtomicString(self.MEDIA_DESCRIPTION.format(alt)) parent.insert(index, media) # Remove the old link From 431924098cead2856c5fb14a5cf4fee62c08dbdd Mon Sep 17 00:00:00 2001 From: facelessuser Date: Thu, 23 Sep 2021 06:31:38 -0600 Subject: [PATCH 3/6] Allow global default attributes which can be overridden, also cleanup --- pymdownx/magiclink.py | 82 +++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/pymdownx/magiclink.py b/pymdownx/magiclink.py index 7023df620..8d9eda4e7 100644 --- a/pymdownx/magiclink.py +++ b/pymdownx/magiclink.py @@ -242,11 +242,11 @@ class MagiclinkMediaTreeprocessor(Treeprocessor): MEDIA_DOWNLOAD = "Try downloading a copy of the video." MEDIA_DESCRIPTION = "Description: {}" - def __init__(self, md, in_img, in_anchor): + def __init__(self, md, video_defaults, audio_defaults): """Initialize.""" - self.in_img = in_img - self.in_anchor = in_anchor + self.video_defaults = video_defaults + self.audio_defaults = audio_defaults super().__init__(md) @@ -273,44 +273,43 @@ def run(self, root): # Reject any other types mime = m.group(1).lower() - # We don't know what case the attributes are in - # so take care to find them. Sort out source attributes - # and audio/video attributes. - stop = False - src_attrib = {'src': src} + # We don't know what case the attributes are in, so normalize them. + keys = set([k.lower() for k in attrib.keys()]) + + # Identify whether we are working with audio or video and save MIME type + mtype = '' + if 'type' in keys: + v = attrib['type'] + t = v.lower().split('/')[0] + if t in ('audio', 'video'): + mtype = v + keys.remove('type') + else: + mtype = self.MIMEMAP.get(mime, '') + attrib['type'] = mtype + + # Doesn't look like audio/video + if not mtype: + continue + + # Setup attributess for `` element + vtype = mtype[:5].lower() + attrib = {**copy.deepcopy(self.video_defaults if vtype == 'video' else self.audio_defaults), **attrib} + src_attrib = {'src': src, 'type': mtype} del attrib['src'] + del attrib['type'] + + # Find any other `` specific attributes and check if there is an `alt` alt = '' - for k in list(attrib.keys()): + for k in keys: key = k.lower() - if key == 'alt': alt = attrib[k] del attrib[k] - elif key == 'type': - v = attrib[k] - t = v.lower().split('/')[0] - if t in ('audio', 'video'): - src_attrib[key] = v - del attrib[k] - else: - stop = True - break - elif key in ('srcset', 'sizes', 'media'): src_attrib[key] = attrib[k] del attrib[key] - # We must have found an incompatible type - if stop: - break - - # No type found, set it from our mapping - if 'type' not in src_attrib: - src_attrib['type'] = self.MIMEMAP[mime] - - # Setup media controls - attrib['controls'] = '' - # Build the source element and apply the right type source = etree.Element('source', src_attrib) @@ -336,7 +335,8 @@ def run(self, root): # Attach the media source as the primary source, or construct a new group. if sibling is not None: sibling.insert(0, source) - sibling.attrib = attrib + sibling.attrib.clear() + sibling.attrib.update(attrib) last = list(sibling)[-1] last.attrib['href'] = src last.tail = md_util.AtomicString(self.MEDIA_DESCRIPTION.format(alt) if alt else '') @@ -1054,6 +1054,14 @@ def __init__(self, *args, **kwargs): 'repo': [ '', 'The base repo to use - Default: ""' + ], + "video_defaults": [ + {'controls': '', 'preload': 'metadata'}, + "Video default attributes - Default: {}" + ], + "audio_defaults": [ + {'controls': '', 'preload': 'metadata'}, + "Video default attributes - Default: {}" ] } super(MagiclinkExtension, self).__init__(*args, **kwargs) @@ -1131,10 +1139,10 @@ def setup_shortener(self, md, base_url, base_user_url, config, repo_shortner, so shortener.config = config md.treeprocessors.register(shortener, "magic-repo-shortener", 9.9) - def setup_media(self, md, in_img, in_anchor): + def setup_media(self, md, video_defaults, audio_defaults): """Setup media transformations.""" - media = MagiclinkMediaTreeprocessor(md, in_img, in_anchor) + media = MagiclinkMediaTreeprocessor(md, video_defaults, audio_defaults) md.treeprocessors.register(media, 'magic-video', 7.9) def get_base_urls(self, config): @@ -1198,7 +1206,11 @@ def extendMarkdown(self, md): base_url, base_user_url = self.get_base_urls(config) self.setup_shortener(md, base_url, base_user_url, config, self.repo_shortner, self.social_shortener) - self.setup_media(md, True, True) + self.setup_media( + md, + config['video_defaults'], + config['audio_defaults'] + ) def makeExtension(*args, **kwargs): From 4bda1cf04f81033f76cbb670101d3de60702daf7 Mon Sep 17 00:00:00 2001 From: facelessuser Date: Thu, 23 Sep 2021 13:00:54 -0600 Subject: [PATCH 4/6] Move media embedding to a new Embed extension - Allow any extension as long as a MIME type of audio/video is provided - Remove fallback message. Only show a download link with alt text if provided an if not, the link will show the URL. --- docs/src/mkdocs.yml | 1 + mkdocs.yml | 1 + pymdownx/embed.py | 186 ++++++++++++++++++++++++++++++++++++++++++ pymdownx/magiclink.py | 159 ------------------------------------ 4 files changed, 188 insertions(+), 159 deletions(-) create mode 100644 pymdownx/embed.py diff --git a/docs/src/mkdocs.yml b/docs/src/mkdocs.yml index 2b33bfd8f..4473f7fb2 100644 --- a/docs/src/mkdocs.yml +++ b/docs/src/mkdocs.yml @@ -153,6 +153,7 @@ markdown_extensions: - pymdownx.tabbed: alternate_style: true - pymdownx.saneheaders: + - pymdownx.embed: extra: social: diff --git a/mkdocs.yml b/mkdocs.yml index 6b53a810c..9948d3183 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -153,6 +153,7 @@ markdown_extensions: - pymdownx.tabbed: alternate_style: true - pymdownx.saneheaders: + - pymdownx.embed: extra: social: diff --git a/pymdownx/embed.py b/pymdownx/embed.py new file mode 100644 index 000000000..ca3f82310 --- /dev/null +++ b/pymdownx/embed.py @@ -0,0 +1,186 @@ +"""Library for embedding media and such.""" +from markdown import Extension +from markdown.treeprocessors import Treeprocessor +from markdown import util as md_util +import xml.etree.ElementTree as etree +import re +import copy + + +class EmbedMediaTreeprocessor(Treeprocessor): + """ + Find video links and parse them up. + + We'll use the image link syntax to identify our links of interest. + """ + + # Current recognized file extensions + MIMES = re.compile(r'(?i).*?\.([a-z0-9]+)$') + + # Default MIME types, but can be overridden + # These are just MIME types we know, but some can be + # audio or video, and we cannot predict without a type + # in those cases. So any of these can be overridden if + # a type is provided, but if none is provided, we take + # our best guess. + MIMEMAP = { + 'mp4': 'video/mp4', + 'webm': 'video/webm', + 'mp3': 'audio/mpeg', + 'ogg': 'audio/ogg', + 'wav': 'audio/wav', + 'flac': 'audio/flac' + } + + def __init__(self, md, video_defaults, audio_defaults): + """Initialize.""" + + self.video_defaults = video_defaults + self.audio_defaults = audio_defaults + + super().__init__(md) + + def run(self, root): + """Shorten popular git repository links.""" + + # Grab the elements of interest and form a parent mapping + links = [e for e in root.iter() if e.tag.lower() in ('img',)] + parent_map = {c: p for p in root.iter() for c in p} + + # Evaluate links + for link in reversed(links): + + # Save the attributes as we will reuse them + attrib = copy.copy(link.attrib) + + # See if source matches the audio or video mime type + src = attrib.get('src', '') + m = self.MIMES.match(src) + if m is None: + continue + + # Use whatever audio/video type specified or construct our own + # Reject any other types + mime = m.group(1).lower() + + # We don't know what case the attributes are in, so normalize them. + keys = set([k.lower() for k in attrib.keys()]) + + # Identify whether we are working with audio or video and save MIME type + mtype = '' + if 'type' in keys: + v = attrib['type'] + t = v.lower().split('/')[0] + if t in ('audio', 'video'): + mtype = v + keys.remove('type') + else: + mtype = self.MIMEMAP.get(mime, '') + attrib['type'] = mtype + + # Doesn't look like audio/video + if not mtype: + continue + + # Setup attributess for `` element + vtype = mtype[:5].lower() + attrib = {**copy.deepcopy(self.video_defaults if vtype == 'video' else self.audio_defaults), **attrib} + src_attrib = {'src': src, 'type': mtype} + del attrib['src'] + del attrib['type'] + + # Find any other `` specific attributes and check if there is an `alt` + alt = '' + for k in keys: + key = k.lower() + if key == 'alt': + alt = attrib[k] + del attrib[k] + elif key in ('srcset', 'sizes', 'media'): + src_attrib[key] = attrib[k] + del attrib[key] + + # Build the source element and apply the right type + source = etree.Element('source', src_attrib) + + # Find the parent and check if the next sibling is already a media group + # that we can attach to. If so, the current link will become the primary + # source, and the existing will become the fallback. + parent = parent_map[link] + one_more = False + sibling = None + index = -1 + mtype = src_attrib['type'][:5].lower() + for i, c in enumerate(parent, 0): + if one_more: + # If there is another sibling, see if it is already a video container + if c.tag.lower() == mtype and 'fallback' in c.attrib: + sibling = c + break + if c is link: + # Found where we live, now let's find our sibling + index = i + one_more = True + + # Attach the media source as the primary source, or construct a new group. + if sibling is not None: + # Insert the source at the top + sibling.insert(0, source) + # Update container's attributes + sibling.attrib.clear() + sibling.attrib.update(attrib) + # Update fallback link + last = list(sibling)[-1] + last.attrib['href'] = src + last.text = md_util.AtomicString(alt if alt else src) + else: + # Create media container and insert source + media = etree.Element(mtype, attrib) + media.append(source) + # Just in case the browser doesn't support `