Skip to content

Commit

Permalink
Make vtt subtitles work and alter syntax of vid services to use ![]()
Browse files Browse the repository at this point in the history
  • Loading branch information
facelessuser committed Nov 10, 2021
1 parent e33d122 commit 5e8dc6c
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 107 deletions.
3 changes: 3 additions & 0 deletions docs/src/dictionary/en-custom.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ CommonMark
CriticMarkup
Ctrl
DOM
Dailymotion
Dicts
Donath
ElementTree
Expand Down Expand Up @@ -110,6 +111,7 @@ UTF
Uml
Unescape
Validators
Vimeo
Virtualenv
Waylan
YAML
Expand Down Expand Up @@ -142,6 +144,7 @@ emojione
escaper
eslint
facelessuser
fallbacks
formatter
formatter's
formatters
Expand Down
282 changes: 175 additions & 107 deletions pymdownx/embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"""
)

SOURCE_ATTR = ('srcset', 'sizes', 'media')
TRACK_ATTR = ('default', 'kind', 'label', 'srclang')


def youtube(url):
"""Match YouTube source and return a suitable URL."""
Expand Down Expand Up @@ -65,15 +68,27 @@ def vimeo(url):
SERVICES = {
"youtube": {
"handler": youtube,
"defaults": {"allowfullscreen": '', "frameborder": '0'}
"defaults": {
"allowfullscreen": '',
"frameborder": '0',
"title": "YouTube video player"
}
},
"dailymotion": {
"handler": dailymotion,
"defaults": {"allowfullscreen": '', "frameborder": '0'}
"defaults": {
"allowfullscreen": '',
"frameborder": '0',
"title": "Dailymotion video player"
}
},
"vimeo": {
"handler": vimeo,
"defaults": {"allowfullscreen": '', "frameborder": '0'}
"defaults": {
"allowfullscreen": '',
"frameborder": '0',
"title": "Vimeo video player"
}
}
}

Expand Down Expand Up @@ -117,30 +132,45 @@ def __init__(self, md, video_defaults, audio_defaults, services):

super().__init__(md)

def process_embedded_media(self, links, parent_map):
"""Process embedded video and audio files."""
def convert_to_anchor(self, link, parent_map):
"""Convert to a normal link."""

# Evaluate links
for link in reversed(links):
parent = parent_map[link]
el = None
index = -1
for index, c in enumerate(list(parent), 0):
if c is link:
src = link.attrib['src']
el = etree.Element('a', {'href': src, 'download': ''})
el.text = md_util.AtomicString(src)
break

# Save the attributes as we will reuse them
attrib = copy.copy(link.attrib)
parent.insert(index, el)
parent.remove(link)

# See if source matches the audio or video mime type
src = attrib.get('src', '')
m = self.MIMES.match(src)
if m is None:
continue
def process_embedded_media(self, link, parent_map):
"""Process embedded video and audio files."""

# Use whatever audio/video type specified or construct our own
# Reject any other types
mime = m.group(1).lower()
# Save the attributes as we will reuse them
attrib = copy.copy(link.attrib)

# We don't know what case the attributes are in, so normalize them.
keys = set([k.lower() for k in attrib.keys()])
# See if source matches the audio or video mime type
src = attrib.get('src', '')
m = self.MIMES.match(src)
if m is None:
return

# Identify whether we are working with audio or video and save MIME type
mtype = ''
# Use whatever audio/video type specified or construct our own
# Reject any other types
mime = m.group(1).lower()
is_vtt = mime == 'vtt'

# 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 not is_vtt:
if 'type' in keys:
v = attrib['type']
t = v.lower().split('/')[0]
Expand All @@ -153,107 +183,145 @@ def process_embedded_media(self, links, parent_map):

# Doesn't look like audio/video
if not mtype:
continue
return

# Setup attributess for `<source>` element
# Setup attributes for `<source>` 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']
else:
# We need `<track>` elements to be fallbacks.
if 'fallback' not in keys:
self.convert_to_anchor(link, parent_map)
return
# Setup attributes for `<track>` element.
src_attrib = {'src': src}
del attrib['src']

# Find any other `<source>` 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 `<video>` or `<audio>`
download = etree.SubElement(media, 'a', {"href": src, "download": ""})
download.text = md_util.AtomicString(alt if alt else src)
# Insert media where the old link was
parent.insert(index, media)

# Remove the old link
parent.remove(link)

def process_embedded_media_service(self, links, parent_map):
# Find any other `<source>` specific attributes and check if there is an `alt`
# Also handle any for `<track>`.
alt = ''
title = ''
fallback = False
for k in keys:
key = k.lower()
if key == 'alt':
alt = attrib[k]
del attrib[k]
elif key == 'title':
title = attrib[k]
elif key == 'fallback':
fallback = True
del attrib[k]
elif not is_vtt and key in SOURCE_ATTR:
src_attrib[key] = attrib[k]
del attrib[k]
elif is_vtt and key in TRACK_ATTR:
src_attrib[key] = attrib[k]
del attrib[k]
elif key in SOURCE_ATTR or key in TRACK_ATTR:
del attrib[k]

# Use the title as `alt` if none was provided.
# As a last resort, we'll just use the `src` as the `alt`
if not alt:
alt = title
if not alt:
alt = src

# 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]
index = -1
mtype = src_attrib['type'][:5].lower() if not is_vtt else 'track'
prev = None
for i, c in enumerate(parent, 0):
if c is link:
# Found where we live, now let's find our sibling
fallback = fallback and (prev and (mtype == prev.tag.lower() or mtype == 'track'))
index = len(list(prev)) - 1 if fallback else i
break
prev = c

# Track with no parent
if mtype == 'track' and not fallback:
self.convert_to_anchor(link, parent_map)
return

# Build the source element and apply the right type
source = etree.Element('source' if not is_vtt else 'track', src_attrib)

# Attach the media source as the primary source, or construct a new group.
if fallback:
# Insert the source at the top
prev.insert(index, source)
else:
# Create media container and insert source
media = etree.Element(mtype, attrib)
media.append(source)
# Just in case the browser doesn't support `<video>` or `<audio>`
download = etree.SubElement(media, 'a', {"href": src, "download": ""})
download.text = md_util.AtomicString(alt)
# Insert media where the old link was
parent.insert(index, media)

# Remove the old link
parent.remove(link)

def process_embedded_media_service(self, link, parent_map):
"""Process links to embedded media services like YouTube, etc."""

for link in reversed(links):
for service in self.services.values():
src = service['handler'](link.attrib.get('href', ''))
if src:
orig = copy.copy(link.attrib)
orig['src'] = src
del orig['href']
attrib = {**copy.copy(service['defaults']), **orig}
el = etree.Element('iframe', attrib)
parent = parent_map[link]
for index, c in enumerate(list(parent), 0):
if c is el:
break
parent.insert(index, el)
parent.remove(link)
processed = False
for service in self.services.values():
src = service['handler'](link.attrib.get('src', ''))
if src:
orig = copy.copy(link.attrib)
orig['src'] = src

alt = ''
title = ''
# We don't know what case the attributes are in, so normalize them.
keys = set([k.lower() for k in orig.keys()])
for k in keys:
key = k.lower()
if key == 'alt':
alt = orig[k]
del orig[k]
elif key == 'title':
title = orig[k]
del orig[k]

if not title:
title = alt

if title:
orig['title'] = title

attrib = {**copy.copy(service['defaults']), **orig}
el = etree.Element('iframe', attrib)
parent = parent_map[link]
for index, c in enumerate(list(parent), 0):
if c is el:
break
parent.insert(index, el)
parent.remove(link)
processed = True
break
return processed

def run(self, root):
"""Shorten popular git repository links."""

# Grab the elements of interest and form a parent mapping
images = []
anchors = []
parent_map = {c: p for p in root.iter() for c in p}
for e in root.iter():
tag = e.tag.lower()
if tag == 'img':
images.append(e)
elif tag == 'a':
anchors.append(e)

self.process_embedded_media(images, parent_map)
self.process_embedded_media_service(anchors, parent_map)
links = [e for e in root.iter() if e.tag.lower() == 'img']

for link in links:
if not self.process_embedded_media_service(link, parent_map):
self.process_embedded_media(link, parent_map)

return root

Expand Down
3 changes: 3 additions & 0 deletions pymdownx/magiclink.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ def get_provider(self, match):
"""Get the provider and hash size."""

# Set provider specific variables
provider = ''
if match.group('github'):
provider = 'github'
elif match.group('bitbucket'):
Expand All @@ -526,13 +527,15 @@ def get_provider(self, match):
def get_social_provider(self, match):
"""Get social provider."""

provider = ''
if match.group('twitter'):
provider = 'twitter'
return provider

def get_type(self, provider, match):
"""Get the link type."""

value = link_type = None
try:
# Gather info about link type
if match.group(provider + '_diff1') is not None:
Expand Down

0 comments on commit 5e8dc6c

Please sign in to comment.