diff --git a/src/hooks/useMarkdown.hook.ts b/src/hooks/useMarkdown.hook.ts
index 45ad824..56bec3c 100644
--- a/src/hooks/useMarkdown.hook.ts
+++ b/src/hooks/useMarkdown.hook.ts
@@ -4,7 +4,7 @@ import blockEmbedPlugin from 'markdown-it-block-embed'
import markdownItCheckbox from 'markdown-it-checkbox'
import markdownItSvgCodeCopy from 'markdown-it-svg-code-copy'
import markdownItFootnote from 'markdown-it-footnote'
-import htmlMedia from 'markdown-it-html5-media'
+import { html5Media } from '@/utils/markdown/markdown-html5-media'
const md = new MarkdownIt({
typographer: true,
@@ -19,6 +19,7 @@ const md = new MarkdownIt({
h6: ['title', 'is-6'],
table: ['table', 'is-fullwidth']
})
+ .use(html5Media)
.use(blockEmbedPlugin, {
youtube: {
width: '100%'
@@ -34,7 +35,6 @@ const md = new MarkdownIt({
buttonClass: 'button is-light'
})
.use(markdownItFootnote)
- .use(htmlMedia.html5Media)
export const useMarkdown = (defaultPrefix?: string) => {
return {
diff --git a/src/utils/markdown/markdown-html5-media.ts b/src/utils/markdown/markdown-html5-media.ts
new file mode 100644
index 0000000..b979ecd
--- /dev/null
+++ b/src/utils/markdown/markdown-html5-media.ts
@@ -0,0 +1,388 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+// We can only detect video/audio files from the extension in the URL.
+// We ignore MP1 and MP2 (not in active use) and default to video for ambiguous
+
+import MarkdownIt from 'markdown-it'
+
+// extensions (MPG, MP4)
+const validAudioExtensions = ['aac', 'm4a', 'mp3', 'oga', 'ogg', 'wav']
+const validVideoExtensions = ['mp4', 'm4v', 'ogv', 'webm', 'mpg', 'mpeg']
+
+/**
+ * @property {Object} messages
+ * @property {Object} messages.languageCode
+ * a set of messages identified with a language code, typically an ISO639 code
+ * @property {String} messages.languageCode.messageKey
+ * an individual translation of a message to that language, identified with a
+ * message key
+ * @typedef {Object} MessagesObj
+ */
+let messages: { [key: string]: any } = {
+ en: {
+ 'html5 video not supported':
+ 'Your browser does not support playing HTML5 video.',
+ 'html5 audio not supported':
+ 'Your browser does not support playing HTML5 audio.',
+ 'html5 media fallback link':
+ 'You can download the file instead.',
+ 'html5 media description': 'Here is a description of the content: %s'
+ }
+}
+
+let translate = (
+ language: string,
+ messageKey: string,
+ messageParams?: unknown[]
+): string => {
+ // Revert back to English default if no message object, or no translation
+ // for this language
+ if (!messages[language] || !messages[language][messageKey]) {
+ language = 'en'
+ }
+
+ if (!messages[language]) {
+ return ''
+ }
+
+ let message = messages[language][messageKey] || ''
+
+ if (messageParams)
+ for (const param of messageParams) {
+ message = message.replace('%s', param)
+ }
+
+ return message
+}
+
+/**
+ * A fork of the built-in image tokenizer which guesses video/audio files based
+ * on their extension, and tokenizes them accordingly.
+ *
+ * @param {Object} state
+ * Markdown-It state
+ * @param {Boolean} silent
+ * if true, only validate, don't tokenize
+ * @param {MarkdownIt} md
+ * instance of Markdown-It used for utility functions
+ * @returns {Boolean}
+ * @memberof HTML5Media
+ */
+function tokenizeImagesAndMedia(
+ state: {
+ pos: number
+ posMax: number
+ src: string
+ md: {
+ helpers: {
+ parseLinkLabel: (arg0: any, arg1: any, arg2?: boolean) => any
+ parseLinkDestination: (arg0: any, arg1: any, arg2: any) => any
+ parseLinkTitle: (arg0: any, arg1: any, arg2: any) => any
+ }
+ normalizeLink: (arg0: any) => string
+ validateLink: (arg0: string) => any
+ inline: {
+ parse: (arg0: any, arg1: any, arg2: any, arg3: never[]) => void
+ }
+ }
+ env: { references: { [x: string]: any } }
+ push: (arg0: string, arg1: string, arg2: number) => any
+ },
+ silent: any,
+ md: {
+ utils: {
+ isSpace: (arg0: any) => any
+ normalizeReference: (arg0: any) => string | number
+ }
+ }
+): boolean {
+ let attrs, code, label, pos, ref, res, title, tokens: never[], start
+ let href = ''
+ const oldPos = state.pos
+ const max = state.posMax
+
+ // Exclamation mark followed by open square bracket - ![ - otherwise abort
+ if (
+ state.src.charCodeAt(state.pos) !== 0x21 ||
+ state.src.charCodeAt(state.pos + 1) !== 0x5b
+ )
+ return false
+
+ const labelStart = state.pos + 2
+ const labelEnd = state.md.helpers.parseLinkLabel(state, state.pos + 1, false)
+
+ // Parser failed to find ']', so it's not a valid link
+ if (labelEnd < 0) return false
+
+ pos = labelEnd + 1
+ if (pos < max && state.src.charCodeAt(pos) === 0x28) {
+ // Parenthesis: (
+ //
+ // Inline link
+ //
+
+ // [link]( "title" )
+ // ^^ skipping these spaces
+ pos++
+ for (; pos < max; pos++) {
+ code = state.src.charCodeAt(pos)
+ if (!md.utils.isSpace(code) && code !== 0x0a)
+ // LF \n
+ break
+ }
+ if (pos >= max) return false
+
+ // [link]( "title" )
+ // ^^^^^^ parsing link destination
+ start = pos
+ res = state.md.helpers.parseLinkDestination(state.src, pos, state.posMax)
+ if (res.ok) {
+ href = state.md.normalizeLink(res.str)
+ if (state.md.validateLink(href)) {
+ pos = res.pos
+ } else {
+ href = ''
+ }
+ }
+
+ // [link]( "title" )
+ // ^^ skipping these spaces
+ start = pos
+ for (; pos < max; pos++) {
+ code = state.src.charCodeAt(pos)
+ if (!md.utils.isSpace(code) && code !== 0x0a) break
+ }
+
+ // [link]( "title" )
+ // ^^^^^^^ parsing link title
+ res = state.md.helpers.parseLinkTitle(state.src, pos, state.posMax)
+ if (pos < max && start !== pos && res.ok) {
+ title = res.str
+ pos = res.pos
+
+ // [link]( "title" )
+ // ^^ skipping these spaces
+ for (; pos < max; pos++) {
+ code = state.src.charCodeAt(pos)
+ if (!md.utils.isSpace(code) && code !== 0x0a) break
+ }
+ } else {
+ title = ''
+ }
+
+ if (pos >= max || state.src.charCodeAt(pos) !== 0x29) {
+ // Parenthesis: )
+ state.pos = oldPos
+ return false
+ }
+ pos++
+ } else {
+ //
+ // Link reference
+ //
+ if (typeof state.env.references === 'undefined') return false
+
+ if (pos < max && state.src.charCodeAt(pos) === 0x5b) {
+ // Bracket: [
+ start = pos + 1
+ pos = state.md.helpers.parseLinkLabel(state, pos)
+ if (pos >= 0) {
+ label = state.src.slice(start, pos++)
+ } else {
+ pos = labelEnd + 1
+ }
+ } else {
+ pos = labelEnd + 1
+ }
+
+ // covers label === '' and label === undefined
+ // (collapsed reference link and shortcut reference link respectively)
+ if (!label) label = state.src.slice(labelStart, labelEnd)
+
+ ref = state.env.references[md.utils.normalizeReference(label)]
+ if (!ref) {
+ state.pos = oldPos
+ return false
+ }
+ href = ref.href
+ title = ref.title
+ }
+
+ state.pos = pos
+ state.posMax = max
+
+ if (silent) return true
+
+ // We found the end of the link, and know for a fact it's a valid link;
+ // so all that's left to do is to call tokenizer.
+ const content = state.src.slice(labelStart, labelEnd)
+
+ state.md.inline.parse(content, state.md, state.env, (tokens = []))
+
+ const mediaType = guessMediaType(href)
+ const tag = mediaType == 'image' ? 'img' : mediaType
+
+ const token = state.push(mediaType, tag, 0)
+ token.attrs = attrs = [['src', href]]
+ if (mediaType == 'image') attrs.push(['alt', ''])
+ token.children = tokens
+ token.content = content
+
+ if (title) attrs.push(['title', title])
+
+ state.pos = pos
+ state.posMax = max
+ return true
+}
+
+/**
+ * Guess the media type represented by a URL based on the file extension,
+ * if any
+ *
+ * @param {String} url
+ * any valid URL
+ * @returns {String}
+ * a type identifier: 'image' (default for all unrecognized URLs), 'audio'
+ * or 'video'
+ * @memberof HTML5Media
+ */
+function guessMediaType(url: string): string {
+ const extensionMatch = url.match(/\.([^/.]+)$/)
+ if (extensionMatch === null) return 'image'
+ const extension = extensionMatch[1]
+ if (validAudioExtensions.indexOf(extension.toLowerCase()) != -1)
+ return 'audio'
+ else if (validVideoExtensions.indexOf(extension.toLowerCase()) != -1)
+ return 'video'
+ else return 'image'
+}
+
+/**
+ * Render tokens of the video/audio type to HTML5 tags
+ *
+ * @param {Object} tokens
+ * token stream
+ * @param {Number} idx
+ * which token are we rendering
+ * @param {Object} options
+ * Markdown-It options, including this plugin's settings
+ * @param {Object} env
+ * Markdown-It environment, potentially including language setting
+ * @param {MarkdownIt} md
+ * instance used for utilities access
+ * @returns {String}
+ * rendered token
+ * @memberof HTML5Media
+ */
+function renderMedia(
+ tokens: { [x: string]: any },
+ idx: string | number,
+ options: { html5Media: { [x: string]: string } },
+ env: { language: any },
+ md: { utils: { escapeHtml: (arg0: any) => any } }
+): string {
+ const token = tokens[idx]
+ const type = token.type
+
+ if (!token.attrs || (type !== 'video' && type !== 'audio')) {
+ return ''
+ }
+
+ let attrs = options.html5Media[`${type}Attrs`].trim()
+ if (attrs) {
+ attrs = ' ' + attrs
+ }
+
+ // We'll always have a URL for non-image media: they are detected by URL
+ const url = token.attrs[token.attrIndex('src')][1]
+
+ // Title is set like this: 
+ const title =
+ token.attrIndex('title') != -1
+ ? ` title="${md.utils.escapeHtml(
+ token.attrs[token.attrIndex('title')][1]
+ )}"`
+ : ''
+
+ const fallbackText =
+ translate(env.language, `html5 ${type} not supported`) +
+ '\n' +
+ translate(env.language, 'html5 media fallback link', [url])
+
+ const description = token.content
+ ? '\n' +
+ translate(env.language, 'html5 media description', [
+ md.utils.escapeHtml(token.content)
+ ])
+ : ''
+
+ return (
+ `<${type} src="${url}"${title}${attrs}>\n` +
+ `${fallbackText}${description}\n` +
+ `${type}>`
+ )
+}
+
+/**
+ * The main plugin function, exported as module.exports
+ *
+ * @param {MarkdownIt} md
+ * instance, automatically passed by md.use
+ * @param {Object} [options]
+ * configuration
+ * @param {String} [options.videoAttrs='controls class="html5-video-player"']
+ * attributes to include inside `