Files
upstream-tiptap/packages/extension-youtube/src/utils.ts
upstream-mirror 083a46e93b
Some checks failed
Verify / Install node dependencies (push) Failing after 2s
Verify / Build packages (push) Has been skipped
Verify / Check linting & formatting (push) Has been skipped
Verify / Run unit tests (push) Has been skipped
Verify / Run e2e tests (shard 1/4) (push) Has been skipped
Verify / Run e2e tests (shard 2/4) (push) Has been skipped
Verify / Run e2e tests (shard 3/4) (push) Has been skipped
Verify / Run e2e tests (shard 4/4) (push) Has been skipped
Publish / Resolve publish config (push) Failing after 2s
Publish / Send notification to Slack (push) Has been skipped
Publish / Send failure notification to Slack (push) Failing after 8s
Verify / Merge e2e reports (push) Has been skipped
Publish / Build (push) Has been skipped
Publish / Bump Versions OR Release to Registry (push) Has been skipped
snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:11:02 +05:00

235 lines
4.9 KiB
TypeScript

export const YOUTUBE_REGEX =
/^((?:https?:)?\/\/)?((?:www|m|music)\.)?((?:youtube\.com|youtu\.be|youtube-nocookie\.com))(\/(?:[\w-]+\?v=|embed\/|v\/)?)([\w-]+)(\S+)?$/
export const YOUTUBE_REGEX_GLOBAL =
/^((?:https?:)?\/\/)?((?:www|m|music)\.)?((?:youtube\.com|youtu\.be|youtube-nocookie\.com))(\/(?:[\w-]+\?v=|embed\/|v\/)?)([\w-]+)(\S+)?$/g
/**
* Checks whether a url is a youtube url that the extension can embed.
*
* The `src` attribute of a youtube node is declared with `default: null`, so a node
* parsed from an iframe without a `src` attribute legitimately carries no url. Guard
* against that here instead of letting the caller dereference it.
*/
export const isValidYoutubeUrl = (url: string | null | undefined) => {
if (!url) {
return null
}
return url.match(YOUTUBE_REGEX)
}
export interface GetEmbedUrlOptions {
url: string
allowFullscreen?: boolean
autoplay?: boolean
ccLanguage?: string
ccLoadPolicy?: boolean
controls?: boolean
disableKBcontrols?: boolean
enableIFrameApi?: boolean
endTime?: number
interfaceLanguage?: string
ivLoadPolicy?: number
loop?: boolean
modestBranding?: boolean
nocookie?: boolean
origin?: string
playlist?: string
progressBarColor?: string
startAt?: number
rel?: number
}
export interface YoutubeEmbedAttributes {
src: string
start?: number
}
export const getYoutubeEmbedUrl = (nocookie?: boolean, isPlaylist?: boolean) => {
if (isPlaylist) {
return 'https://www.youtube-nocookie.com/embed/videoseries?list='
}
return nocookie ? 'https://www.youtube-nocookie.com/embed/' : 'https://www.youtube.com/embed/'
}
export const getEmbedUrlFromYoutubeUrl = (options: GetEmbedUrlOptions) => {
const {
url,
allowFullscreen,
autoplay,
ccLanguage,
ccLoadPolicy,
controls,
disableKBcontrols,
enableIFrameApi,
endTime,
interfaceLanguage,
ivLoadPolicy,
loop,
modestBranding,
nocookie,
origin,
playlist,
progressBarColor,
startAt,
rel,
} = options
if (!isValidYoutubeUrl(url)) {
return null
}
// if is already an embed url, return it
if (url.includes('/embed/')) {
return url
}
// if is a youtu.be url, get the id after the /
if (url.includes('youtu.be')) {
const id = url.split('/').pop()
if (!id) {
return null
}
return `${getYoutubeEmbedUrl(nocookie)}${id}`
}
const videoIdRegex = /(?:(v|list)=|\/(?:shorts|live)\/)([-\w]+)/gm
const matches = videoIdRegex.exec(url)
if (!matches || !matches[2]) {
return null
}
let outputUrl = `${getYoutubeEmbedUrl(nocookie, matches[1] === 'list')}${matches[2]}`
const params = []
if (allowFullscreen === false) {
params.push('fs=0')
}
if (autoplay) {
params.push('autoplay=1')
}
if (ccLanguage) {
params.push(`cc_lang_pref=${ccLanguage}`)
}
if (ccLoadPolicy) {
params.push('cc_load_policy=1')
}
if (!controls) {
params.push('controls=0')
}
if (disableKBcontrols) {
params.push('disablekb=1')
}
if (enableIFrameApi) {
params.push('enablejsapi=1')
}
if (endTime) {
params.push(`end=${endTime}`)
}
if (interfaceLanguage) {
params.push(`hl=${interfaceLanguage}`)
}
if (ivLoadPolicy) {
params.push(`iv_load_policy=${ivLoadPolicy}`)
}
if (loop) {
params.push('loop=1')
}
if (modestBranding) {
params.push('modestbranding=1')
}
if (origin) {
params.push(`origin=${origin}`)
}
if (playlist) {
params.push(`playlist=${playlist}`)
}
if (startAt) {
params.push(`start=${startAt}`)
}
if (progressBarColor) {
params.push(`color=${progressBarColor}`)
}
if (rel !== undefined) {
params.push(`rel=${rel}`)
}
if (params.length) {
outputUrl += `${matches[1] === 'list' ? '&' : '?'}${params.join('&')}`
}
return outputUrl
}
export const getAttributesFromYoutubeEmbedUrl = (url: string): YoutubeEmbedAttributes | null => {
let parsedUrl: URL
try {
parsedUrl = new URL(url)
} catch {
return null
}
const hostname = parsedUrl.hostname.replace(/^www\./, '')
if (hostname !== 'youtube.com' && hostname !== 'youtube-nocookie.com') {
return null
}
let src: string | null = null
if (parsedUrl.pathname === '/embed/videoseries') {
const list = parsedUrl.searchParams.get('list')
if (!list) {
return null
}
src = `https://www.youtube.com/playlist?list=${list}`
} else {
const matches = parsedUrl.pathname.match(/^\/embed\/([\w-]+)$/)
if (!matches?.[1]) {
return null
}
src = `https://www.youtube.com/watch?v=${matches[1]}`
}
if (!isValidYoutubeUrl(src)) {
return null
}
const attributes: YoutubeEmbedAttributes = { src }
const start = parsedUrl.searchParams.get('start')
if (start) {
const parsedStart = Number.parseInt(start, 10)
if (!Number.isNaN(parsedStart)) {
attributes.start = parsedStart
}
}
return attributes
}