Custom Markdown
VitePress uses Markdown IT for markdown rendering and it allows many Markdown IT extensions
Here is an example of how to embed a YouTube video using custom markdown. Use the following syntax: @[youtube](YOUR_VIDEO_ID)
. This will ensure the video is scaled correctly.
- Create markdown extension
/apps/wiki/.vitepress/markdown-it-video.ts
ts
// Process @[youtube](youtubeVideoID)
// Process @[vimeo](vimeoVideoID)
// Process @[vine](vineVideoID)
// Process @[prezi](preziID)
// Process @[osf](guid)
const ytRegex = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/
function youtubeParser(url) {
const match = url.match(ytRegex)
return match && match[7].length === 11 ? match[7] : url
}
const vimeoRegex =
/https?:\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|)(\d+)(?:$|\/|\?)/
function vimeoParser(url) {
const match = url.match(vimeoRegex)
return match && typeof match[3] === 'string' ? match[3] : url
}
const vineRegex = /^http(?:s?):\/\/(?:www\.)?vine\.co\/v\/([a-zA-Z0-9]{1,13}).*/
function vineParser(url) {
const match = url.match(vineRegex)
return match && match[1].length === 11 ? match[1] : url
}
const preziRegex = /^https:\/\/prezi.com\/(.[^/]+)/
function preziParser(url) {
const match = url.match(preziRegex)
return match ? match[1] : url
}
// TODO: Write regex for staging and local servers.
const mfrRegex =
/^http(?:s?):\/\/(?:www\.)?mfr\.osf\.io\/render\?url=http(?:s?):\/\/osf\.io\/([a-zA-Z0-9]{1,5})\/\?action=download/
function mfrParser(url) {
const match = url.match(mfrRegex)
return match ? match[1] : url
}
const EMBED_REGEX = /@\[([a-zA-Z].+)]\([\s]*(.*?)[\s]*[)]/im
function videoEmbed(md, options) {
function videoReturn(state, silent) {
let token
let videoID
const theState = state
const oldPos = state.pos
if (
state.src.charCodeAt(oldPos) !== 0x40 /* @ */ ||
state.src.charCodeAt(oldPos + 1) !== 0x5b /* [ */
) {
return false
}
const match = EMBED_REGEX.exec(state.src.slice(state.pos, state.src.length))
if (!match || match.length < 3) {
return false
}
const service = match[1]
videoID = match[2]
const serviceLower = service.toLowerCase()
if (serviceLower === 'youtube') {
videoID = youtubeParser(videoID)
} else if (serviceLower === 'vimeo') {
videoID = vimeoParser(videoID)
} else if (serviceLower === 'vine') {
videoID = vineParser(videoID)
} else if (serviceLower === 'prezi') {
videoID = preziParser(videoID)
} else if (serviceLower === 'osf') {
videoID = mfrParser(videoID)
} else if (!options[serviceLower]) {
return false
}
// If the videoID field is empty, regex currently make it the close parenthesis.
if (videoID === ')') {
videoID = ''
}
const serviceStart = oldPos + 2
const serviceEnd = md.helpers.parseLinkLabel(state, oldPos + 1, false)
//
// 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.
//
if (!silent) {
theState.pos = serviceStart
theState.service = theState.src.slice(serviceStart, serviceEnd)
const newState = new theState.md.inline.State(service, theState.md, theState.env, [])
newState.md.inline.tokenize(newState)
token = theState.push('video', '')
token.videoID = videoID
token.service = service
token.url = match[2]
token.level = theState.level
}
theState.pos += theState.src.indexOf(')', theState.pos)
return true
}
return videoReturn
}
function extractVideoParameters(url) {
const parameterMap = new Map()
const params = url.replace(/&/gi, '&').split(/[#?&]/)
if (params.length > 1) {
for (let i = 1; i < params.length; i += 1) {
const keyValue = params[i].split('=')
if (keyValue.length > 1) parameterMap.set(keyValue[0], keyValue[1])
}
}
return parameterMap
}
function videoUrl(service, videoID, url, options) {
switch (service) {
case 'youtube': {
const parameters = extractVideoParameters(url)
if (options.youtube.parameters) {
Object.keys(options.youtube.parameters).forEach((key) => {
parameters.set(key, options.youtube.parameters[key])
})
}
// Start time parameter can have the format t=0m10s or t=<time_in_seconds> in share URLs,
// but in embed URLs the parameter must be called 'start' and time must be in seconds
const timeParameter = parameters.get('t')
if (timeParameter !== undefined) {
let startTime = 0
const timeParts = timeParameter.match(/[0-9]+/g)
let j = 0
while (timeParts.length > 0) {
startTime += Number(timeParts.pop()) * Math.pow(60, j)
j += 1
}
parameters.set('start', startTime)
parameters.delete('t')
}
parameters.delete('v')
parameters.delete('feature')
parameters.delete('origin')
const parameterArray = Array.from(parameters, (p) => p.join('='))
const parameterPos = videoID.indexOf('?')
let finalUrl = 'https://www.youtube'
if (options.youtube.nocookie || url.indexOf('youtube-nocookie.com') > -1)
finalUrl += '-nocookie'
finalUrl += '.com/embed/' + (parameterPos > -1 ? videoID.substr(0, parameterPos) : videoID)
if (parameterArray.length > 0) finalUrl += '?' + parameterArray.join('&')
return finalUrl
}
case 'vimeo':
return 'https://player.vimeo.com/video/' + videoID
case 'vine':
return 'https://vine.co/v/' + videoID + '/embed/' + options.vine.embed
case 'prezi':
return (
'https://prezi.com/embed/' +
videoID +
'/?bgcolor=ffffff&lock_to_path=0&autoplay=0&autohide_ctrls=0&' +
'landing_data=bHVZZmNaNDBIWnNjdEVENDRhZDFNZGNIUE43MHdLNWpsdFJLb2ZHanI5N1lQVHkxSHFxazZ0UUNCRHloSXZROHh3PT0&' +
'landing_sign=1kD6c0N6aYpMUS0wxnQjxzSqZlEB8qNFdxtdjYhwSuI'
)
case 'osf':
return 'https://mfr.osf.io/render?url=https://osf.io/' + videoID + '/?action=download'
default:
return service
}
}
function tokenizeVideo(md, options) {
function tokenizeReturn(tokens, idx) {
const videoID = md.utils.escapeHtml(tokens[idx].videoID)
const service = md.utils.escapeHtml(tokens[idx].service).toLowerCase()
const checkUrl =
/http(?:s?):\/\/(?:www\.)?[a-zA-Z0-9-:.]{1,}\/render(?:\/)?[a-zA-Z0-9.&;?=:%]{1,}url=http(?:s?):\/\/[a-zA-Z0-9 -:.]{1,}\/[a-zA-Z0-9]{1,5}\/\?[a-zA-Z0-9.=:%]{1,}/
let num
if (service === 'osf' && videoID) {
num = Math.random() * 0x10000
if (videoID.match(checkUrl)) {
return (
'<div id="' +
num +
'" class="mfr mfr-file"></div><script>' +
'$(document).ready(function () {new mfr.Render("' +
num +
'", "' +
videoID +
'");' +
' }); </script>'
)
}
return (
'<div id="' +
num +
'" class="mfr mfr-file"></div><script>' +
'$(document).ready(function () {new mfr.Render("' +
num +
'", "https://mfr.osf.io/' +
'render?url=https://osf.io/' +
videoID +
'/?action=download%26mode=render");' +
' }); </script>'
)
}
return videoID === ''
? ''
: '<span class="embed-responsive embed-responsive-16by9"><iframe class="embed-responsive-item ' +
service +
'-player" type="text/html" width="' +
options[service].width +
'" height="' +
options[service].height +
'" src="' +
options.url(service, videoID, tokens[idx].url, options) +
'"></iframe></span>'
}
return tokenizeReturn
}
const defaults = {
url: videoUrl,
youtube: { width: '100%', height: '100%', nocookie: false },
vimeo: { width: 500, height: 281 },
vine: { width: 600, height: 600, embed: 'simple' },
prezi: { width: 550, height: 400 },
osf: { width: '100%', height: '100%' },
}
export default function (md, options) {
let theOptions = options
const theMd = md
if (theOptions) {
Object.keys(defaults).forEach(function checkForKeys(key) {
if (typeof theOptions[key] === 'undefined') {
theOptions[key] = defaults[key]
}
})
} else {
theOptions = defaults
}
theMd.renderer.rules.video = tokenizeVideo(theMd, theOptions)
theMd.inline.ruler.before('emphasis', 'video', videoEmbed(theMd, theOptions))
}
- Append classes to
/apps/wiki/.vitepress/theme/style.css
css
.embed-responsive-16by9 {
padding-top: 56.25%;
position: relative;
display: block;
}
.embed-responsive-item {
position: absolute;
top: 0;
left: 0;
border: 0px;
}
- Include extension in
/apps/wiki/.vitepress/config.ts
ts
import { defineConfig } from "vitepress"
import videoPlugin from "./markdown-it-video"
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "My site",
description: "My site",
markdown: {
config: (md) => {
// use more markdown-it plugins!
md.use(videoPlugin)
},
},
})
- Enjoy