// Embed expansion for Journal HTML. The Blog Editor (Journal plugin,
// assets/src/blogeditor/embed.ts) never stores an iframe: it writes the
// placeholder , and the
// plugin's bluemonday policy (classes/format_html.go) keeps exactly that, with
// data-embed matching ^youtube$ and data-id matching ^[A-Za-z0-9_-]{11}$.
// htmlGate.ts mirrors those two rules, so the placeholder survives the layer's
// sanitizer too.
//
// expandEmbeds is the very last step before v-html, after sanitizeJournalHtml:
// its input is always sanitizer output, and its output is never sanitized
// again (the sanitizer strips every iframe, including the one built here). The
// regex matches the serialization sanitize-html gives the placeholder:
// attribute order kept, double quotes, empty figure. Only a known provider
// with a valid id becomes an iframe, whose src is a fixed host plus that id;
// anything else is returned unchanged and stays an inert empty figure.
/** One allowlisted embed provider. */
export type EmbedProvider = {
/** The id a placeholder must match to expand. */
id: RegExp
/** The iframe src for a validated id. */
src: (id: string) => string
/** The iframe's accessible title. */
title: string
}
/** Providers a placeholder may name. YouTube only, on its privacy-enhanced host. */
export const EMBED_PROVIDERS: Readonly> = Object.freeze({
youtube: {
id: /^[A-Za-z0-9_-]{11}$/,
src: (id: string) => `https://www.youtube-nocookie.com/embed/${id}`,
title: 'YouTube video',
},
})
/**
* The placeholder exactly as the editor writes it and sanitize-html
* serializes it. Group 1 is the provider, group 2 the id; neither may hold a
* quote or an angle bracket.
*/
export const EMBED_PLACEHOLDER = /]*)" data-id="([^"<>]*)"><\/figure>/g
/**
* Replaces every placeholder whose provider is in EMBED_PROVIDERS and whose
* id passes the provider's pattern with a figure holding the provider's
* iframe. Every other match is returned unchanged. Empty input gives "".
* Call it on sanitizeJournalHtml output only, and never sanitize its result.
*/
export function expandEmbeds(html: string | null | undefined): string {
if (!html) return ''
return html.replace(EMBED_PLACEHOLDER, (match: string, provider: string, id: string) => {
if (!Object.hasOwn(EMBED_PROVIDERS, provider)) return match
const p = EMBED_PROVIDERS[provider]!
if (!p.id.test(id)) return match
return (
`` +
`' +
''
)
})
}