snapshot of https://github.com/ueberdosis/tiptap.git (main, 2026-09-15; blobs > 131072B omitted)
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
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
This commit is contained in:
295
packages/extension-twitch/src/utils.ts
Normal file
295
packages/extension-twitch/src/utils.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* Regex patterns for matching Twitch URLs
|
||||
* Matches:
|
||||
* - https://twitch.tv/videos/1234567890
|
||||
* - https://www.twitch.tv/videos/1234567890
|
||||
* - https://twitch.tv/examplechannel/clip/ClipName-123
|
||||
* - https://www.twitch.tv/examplechannel/clip/ClipName-123
|
||||
* - https://clips.twitch.tv/ClipName
|
||||
* - https://www.clips.twitch.tv/ClipName
|
||||
* - https://twitch.tv/examplechannel (channel)
|
||||
* - https://www.twitch.tv/examplechannel
|
||||
*/
|
||||
export const TWITCH_REGEX =
|
||||
/^(https?:\/\/)?(www\.)?(twitch\.tv|clips\.twitch\.tv)\/(?:videos\/(\d+)|(\w+)\/clip\/([\w-]+)|([\w-]+)(?:\/)?)?(\?.*)?$/
|
||||
|
||||
export const TWITCH_REGEX_GLOBAL =
|
||||
/^(https?:\/\/)?(www\.)?(twitch\.tv|clips\.twitch\.tv)\/(?:videos\/(\d+)|(\w+)\/clip\/([\w-]+)|([\w-]+)(?:\/)?)?(\?.*)?$/g
|
||||
|
||||
/**
|
||||
* Validates if a URL is a valid Twitch video, clip or channel URL
|
||||
*
|
||||
* @param url - The URL to validate
|
||||
* @returns true if the URL is a valid Twitch URL
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* isValidTwitchUrl('https://www.twitch.tv/videos/1234567890') // true
|
||||
* isValidTwitchUrl('https://www.twitch.tv/examplechannel/clip/ExampleClipName') // true
|
||||
* isValidTwitchUrl('https://www.twitch.tv/examplechannel') // true
|
||||
* isValidTwitchUrl('https://clips.twitch.tv/ExampleClipName') // true
|
||||
* isValidTwitchUrl('invalid') // false
|
||||
* ```
|
||||
*/
|
||||
export const isValidTwitchUrl = (url: string) => {
|
||||
return url.match(TWITCH_REGEX)
|
||||
}
|
||||
|
||||
export interface GetEmbedUrlOptions {
|
||||
url: string
|
||||
allowFullscreen?: boolean
|
||||
autoplay?: boolean
|
||||
muted?: boolean
|
||||
time?: string
|
||||
parent?: string
|
||||
}
|
||||
|
||||
export interface TwitchEmbedAttributes {
|
||||
src: string
|
||||
autoplay?: boolean
|
||||
muted?: boolean
|
||||
time?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the video, clip or channel identifier from a Twitch URL
|
||||
*
|
||||
* @param url - The Twitch URL
|
||||
* @returns Object containing type ('video', 'clip' or 'channel') and ID, or null if invalid
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* getTwitchIdentifier('https://www.twitch.tv/videos/1234567890')
|
||||
* // { type: 'video', id: '1234567890' }
|
||||
*
|
||||
* getTwitchIdentifier('https://www.twitch.tv/examplechannel/clip/ExampleClipName-ABC123')
|
||||
* // { type: 'clip', id: 'ExampleClipName-ABC123' }
|
||||
*
|
||||
* getTwitchIdentifier('https://www.twitch.tv/examplechannel')
|
||||
* // { type: 'channel', id: 'examplechannel' }
|
||||
* ```
|
||||
*/
|
||||
export const getTwitchIdentifier = (
|
||||
url: string,
|
||||
): { type: 'video' | 'clip' | 'channel'; id: string } | null => {
|
||||
if (!isValidTwitchUrl(url)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Remove query parameters
|
||||
const cleanUrl = url.split('?')[0]
|
||||
|
||||
// Handle clip URLs from clips.twitch.tv
|
||||
if (cleanUrl.includes('clips.twitch.tv/')) {
|
||||
const clipRegex = /clips\.twitch\.tv\/([\w-]+)/
|
||||
const match = cleanUrl.match(clipRegex)
|
||||
return match ? { type: 'clip', id: match[1] } : null
|
||||
}
|
||||
|
||||
// Handle twitch.tv URLs
|
||||
if (cleanUrl.includes('twitch.tv/')) {
|
||||
// Check if it's a video URL (videos/ID)
|
||||
const videoRegex = /twitch\.tv\/videos\/(\d+)/
|
||||
const videoMatch = cleanUrl.match(videoRegex)
|
||||
if (videoMatch) {
|
||||
return { type: 'video', id: videoMatch[1] }
|
||||
}
|
||||
|
||||
// Check if it's a clip URL (channel/clip/clipname)
|
||||
const channelClipRegex = /twitch\.tv\/([\w-]+)\/clip\/([\w-]+)/
|
||||
const clipMatch = cleanUrl.match(channelClipRegex)
|
||||
if (clipMatch) {
|
||||
return { type: 'clip', id: clipMatch[2] }
|
||||
}
|
||||
|
||||
// Otherwise it's a channel URL
|
||||
const channelRegex = /twitch\.tv\/([\w-]+)(?:\/)?$/
|
||||
const channelMatch = cleanUrl.match(channelRegex)
|
||||
if (channelMatch) {
|
||||
return { type: 'channel', id: channelMatch[1] }
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an embed URL from a Twitch video, clip or channel URL with optional parameters
|
||||
*
|
||||
* @param options - The embed URL options
|
||||
* @returns The embed URL or null if invalid
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* getEmbedUrlFromTwitchUrl({
|
||||
* url: 'https://www.twitch.tv/videos/1234567890',
|
||||
* parent: 'example.com',
|
||||
* muted: true,
|
||||
* time: '1h2m3s'
|
||||
* })
|
||||
* // Returns: https://player.twitch.tv/?video=1234567890&parent=example.com&muted=true&time=1h2m3s
|
||||
*
|
||||
* getEmbedUrlFromTwitchUrl({
|
||||
* url: 'https://www.twitch.tv/examplechannel/clip/ExampleClipName-ABC123',
|
||||
* parent: 'example.com',
|
||||
* autoplay: true,
|
||||
* muted: true
|
||||
* })
|
||||
* // Returns: https://clips.twitch.tv/embed?clip=ExampleClipName-ABC123&parent=example.com&autoplay=true&muted=true
|
||||
*
|
||||
* getEmbedUrlFromTwitchUrl({
|
||||
* url: 'https://www.twitch.tv/examplechannel',
|
||||
* parent: 'example.com'
|
||||
* })
|
||||
* // Returns: https://player.twitch.tv/?channel=examplechannel&parent=example.com
|
||||
* ```
|
||||
*/
|
||||
export const getEmbedUrlFromTwitchUrl = (options: GetEmbedUrlOptions): string | null => {
|
||||
const { url, allowFullscreen = true, autoplay = false, muted = false, time, parent } = options
|
||||
|
||||
const identifier = getTwitchIdentifier(url)
|
||||
|
||||
if (!identifier) {
|
||||
return null
|
||||
}
|
||||
|
||||
const parentDomain = parent || 'localhost'
|
||||
|
||||
// Handle clip URLs
|
||||
if (identifier.type === 'clip') {
|
||||
const clipUrl = new URL('https://clips.twitch.tv/embed')
|
||||
clipUrl.searchParams.set('clip', identifier.id)
|
||||
clipUrl.searchParams.set('parent', parentDomain)
|
||||
|
||||
if (autoplay) {
|
||||
clipUrl.searchParams.set('autoplay', 'true')
|
||||
}
|
||||
|
||||
if (muted) {
|
||||
clipUrl.searchParams.set('muted', 'true')
|
||||
}
|
||||
|
||||
return clipUrl.toString()
|
||||
}
|
||||
|
||||
// Handle video URLs
|
||||
if (identifier.type === 'video') {
|
||||
const videoUrl = new URL('https://player.twitch.tv/')
|
||||
videoUrl.searchParams.set('video', identifier.id)
|
||||
videoUrl.searchParams.set('parent', parentDomain)
|
||||
|
||||
if (allowFullscreen) {
|
||||
videoUrl.searchParams.set('allowfullscreen', 'true')
|
||||
}
|
||||
|
||||
if (autoplay) {
|
||||
videoUrl.searchParams.set('autoplay', 'true')
|
||||
}
|
||||
|
||||
if (muted) {
|
||||
videoUrl.searchParams.set('muted', 'true')
|
||||
}
|
||||
|
||||
if (time) {
|
||||
videoUrl.searchParams.set('time', time)
|
||||
}
|
||||
|
||||
return videoUrl.toString()
|
||||
}
|
||||
|
||||
// Handle channel URLs
|
||||
if (identifier.type === 'channel') {
|
||||
const channelUrl = new URL('https://player.twitch.tv/')
|
||||
channelUrl.searchParams.set('channel', identifier.id)
|
||||
channelUrl.searchParams.set('parent', parentDomain)
|
||||
|
||||
if (allowFullscreen) {
|
||||
channelUrl.searchParams.set('allowfullscreen', 'true')
|
||||
}
|
||||
|
||||
if (autoplay) {
|
||||
channelUrl.searchParams.set('autoplay', 'true')
|
||||
}
|
||||
|
||||
if (muted) {
|
||||
channelUrl.searchParams.set('muted', 'true')
|
||||
}
|
||||
|
||||
return channelUrl.toString()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a Twitch embed URL into canonical Twitch node attributes.
|
||||
*
|
||||
* @param url - The Twitch embed URL
|
||||
* @returns Canonical node attributes or null if the URL is not a supported Twitch embed URL
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* getAttributesFromTwitchEmbedUrl('https://player.twitch.tv/?video=1234567890&parent=example.com&muted=true')
|
||||
* // Returns: { src: 'https://www.twitch.tv/videos/1234567890', muted: true }
|
||||
*
|
||||
* getAttributesFromTwitchEmbedUrl('https://clips.twitch.tv/embed?clip=ExampleClipName-ABC123&parent=example.com')
|
||||
* // Returns: { src: 'https://clips.twitch.tv/ExampleClipName-ABC123' }
|
||||
* ```
|
||||
*/
|
||||
export const getAttributesFromTwitchEmbedUrl = (url: string): TwitchEmbedAttributes | null => {
|
||||
let parsedUrl: URL
|
||||
|
||||
try {
|
||||
parsedUrl = new URL(url)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
const hostname = parsedUrl.hostname.replace(/^www\./, '')
|
||||
const result: TwitchEmbedAttributes = {
|
||||
src: '',
|
||||
}
|
||||
|
||||
if (hostname === 'player.twitch.tv') {
|
||||
const videoId = parsedUrl.searchParams.get('video')
|
||||
const channelName = parsedUrl.searchParams.get('channel')
|
||||
|
||||
if (videoId) {
|
||||
result.src = `https://www.twitch.tv/videos/${videoId}`
|
||||
} else if (channelName) {
|
||||
result.src = `https://www.twitch.tv/${channelName}`
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} else if (hostname === 'clips.twitch.tv' && parsedUrl.pathname === '/embed') {
|
||||
const clipId = parsedUrl.searchParams.get('clip')
|
||||
|
||||
if (!clipId) {
|
||||
return null
|
||||
}
|
||||
|
||||
result.src = `https://clips.twitch.tv/${clipId}`
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
if (parsedUrl.searchParams.get('autoplay') === 'true') {
|
||||
result.autoplay = true
|
||||
}
|
||||
|
||||
if (parsedUrl.searchParams.get('muted') === 'true') {
|
||||
result.muted = true
|
||||
}
|
||||
|
||||
const time = parsedUrl.searchParams.get('time')
|
||||
|
||||
if (time) {
|
||||
result.time = time
|
||||
}
|
||||
|
||||
if (!isValidTwitchUrl(result.src)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user