- preservationExtensions: raw HTML cards for unmodelled blocks (shown as text), figure with figcaption, the YouTube embed placeholder node, span marks, a global attribute map (never on* handlers), tables without colgroup or min-width, inline images without base64 - serializeDocument keeps the original bytes of every top-level block still equal to the loaded one, so editing one paragraph changes only that paragraph; tables serialize with thead and bare cell text - Links accept only http, https, mailto and relative URLs (isPolicyUrl, the rule both sanitizer policies apply) and gain no target, rel or class - Toolbar with headings, marks, lists, links, blockquote, table editing, images, embeds, raw HTML block, undo and redo; en and pl labels - Images from the picker or a drop go out as summer-upload and are inserted on summer-upload-result with alt text and an optional caption - editor-corpus.json: the editor own output, rebuilt and checked by corpus.test.ts (UPDATE_CORPUS=1 rewrites it) for the policy tests - README Blog Editor section
60 lines
2.1 KiB
TypeScript
60 lines
2.1 KiB
TypeScript
/**
|
|
* Embed providers (D-05). Stored posts hold only the placeholder
|
|
* <figure data-embed="PROVIDER" data-id="ID"></figure>; the layer turns it
|
|
* into the real iframe. Adding a provider is one entry here and one in the
|
|
* layer's table.
|
|
*/
|
|
export interface EmbedProvider {
|
|
/** The name shown on the editor card. */
|
|
name: string
|
|
/** The id the placeholder may hold. */
|
|
idPattern: RegExp
|
|
/** Returns the id from a provider URL, or null. */
|
|
idFromUrl: (url: URL) => string | null
|
|
}
|
|
|
|
const YOUTUBE_HOSTS = new Set(['youtube.com', 'www.youtube.com', 'm.youtube.com', 'music.youtube.com', 'youtube-nocookie.com', 'www.youtube-nocookie.com'])
|
|
|
|
export const EMBED_PROVIDERS = {
|
|
youtube: {
|
|
name: 'YouTube',
|
|
idPattern: /^[A-Za-z0-9_-]{11}$/,
|
|
idFromUrl(url: URL): string | null {
|
|
const host = url.hostname.toLowerCase()
|
|
if (host === 'youtu.be') {
|
|
return url.pathname.split('/')[1] ?? null
|
|
}
|
|
if (!YOUTUBE_HOSTS.has(host)) return null
|
|
if (url.pathname === '/watch') return url.searchParams.get('v')
|
|
const match = /^\/(?:shorts|embed)\/([^/]+)\/?$/.exec(url.pathname)
|
|
return match ? match[1]! : null
|
|
},
|
|
},
|
|
} as const satisfies Record<string, EmbedProvider>
|
|
|
|
export type EmbedProviderName = keyof typeof EMBED_PROVIDERS
|
|
|
|
export function isEmbedProvider(name: string): name is EmbedProviderName {
|
|
return Object.prototype.hasOwnProperty.call(EMBED_PROVIDERS, name)
|
|
}
|
|
|
|
/**
|
|
* parseEmbedUrl returns the provider and id for a supported video URL
|
|
* (youtube.com/watch?v=, youtu.be/, youtube.com/shorts/ and
|
|
* youtube.com/embed/ for YouTube), or null for anything else.
|
|
*/
|
|
export function parseEmbedUrl(input: string): { provider: EmbedProviderName; id: string } | null {
|
|
let url: URL
|
|
try {
|
|
url = new URL(input.trim())
|
|
} catch {
|
|
return null
|
|
}
|
|
if (url.protocol !== 'https:' && url.protocol !== 'http:') return null
|
|
for (const provider of Object.keys(EMBED_PROVIDERS) as EmbedProviderName[]) {
|
|
const id = EMBED_PROVIDERS[provider].idFromUrl(url)
|
|
if (id && EMBED_PROVIDERS[provider].idPattern.test(id)) return { provider, id }
|
|
}
|
|
return null
|
|
}
|