diff --git a/apps/svelte.dev/src/lib/utils/escape.js b/apps/svelte.dev/src/lib/utils/escape.js index d399dab5b5..47725b5de5 100644 --- a/apps/svelte.dev/src/lib/utils/escape.js +++ b/apps/svelte.dev/src/lib/utils/escape.js @@ -2,10 +2,28 @@ const chars = { '&': '&', '<': '<', - '>': '>' + '>': '>', + '"': '"', + "'": ''' }; +const encoded_chars = Object.fromEntries(Object.entries(chars).map(([key, value]) => [value, key])); + +const symbols_regex = new RegExp(`[${Object.keys(chars).join('')}]`, 'g'); + +const encoded_symbols_regex = new RegExp( + `&(?:${Object.values(chars) + .map((entity) => entity.slice(1, entity.length - 1)) + .join('|')});`, + 'g' +); + /** @param {string} html */ export function escape_html(html) { - return html.replace(/[&<>]/g, (c) => chars[c]); + return html.replace(symbols_regex, (c) => chars[c] ?? c); +} + +/** @param {string} html */ +export function decode_html(html) { + return html.replace(encoded_symbols_regex, (c) => encoded_chars[c] ?? c); } diff --git a/apps/svelte.dev/src/routes/blog/[slug]/card.png/+server.ts b/apps/svelte.dev/src/routes/blog/[slug]/card.png/+server.ts index 50c54f34a9..d175aee7e9 100644 --- a/apps/svelte.dev/src/routes/blog/[slug]/card.png/+server.ts +++ b/apps/svelte.dev/src/routes/blog/[slug]/card.png/+server.ts @@ -8,6 +8,7 @@ import Card from './Card.svelte'; import DMSerifDisplay from '$lib/fonts/DMSerifDisplay-Regular.ttf?url'; import FiraSans from '$lib/fonts/FiraSans-Regular.ttf?url'; import { blog_posts } from '$lib/server/content'; +import { decode_html } from '$lib/utils/escape'; import type { ServerlessConfig } from '@sveltejs/adapter-vercel'; export const config: ServerlessConfig = { @@ -22,6 +23,34 @@ export function entries() { })); } +type VNode = ReturnType; + +/** + * Decodes the XML-ified parts of string subnodes + * to revert to the original content + * + * @param node the original node + * @returns the node with replaced leaves + */ +function decode_vnode(node: VNode): VNode { + const children = node.props.children; + if (!children) return node; + + const decodedChildren = + typeof children === 'string' + ? decode_html(children) + : Array.isArray(children) + ? children.map(decode_vnode) + : decode_vnode(children); + + if (decodedChildren === children) return node; + + return { + ...node, + props: { ...node.props, children: decodedChildren } + }; +} + const height = 630; const width = 1200; const dm_serif_display = await read(DMSerifDisplay).arrayBuffer(); @@ -35,7 +64,7 @@ export async function GET({ params }) { const result = render(Card, { props: { title: post.metadata.title, date: post.date_formatted } }); const element = toReactNode(`${result.head}${result.body}`); - const svg = await satori(element, { + const svg = await satori(decode_vnode(element), { fonts: [ { name: 'DMSerif Display', diff --git a/apps/svelte.dev/src/routes/docs/[topic]/[...path]/card.png/+server.ts b/apps/svelte.dev/src/routes/docs/[topic]/[...path]/card.png/+server.ts index 0a9c22a75f..650a772c93 100644 --- a/apps/svelte.dev/src/routes/docs/[topic]/[...path]/card.png/+server.ts +++ b/apps/svelte.dev/src/routes/docs/[topic]/[...path]/card.png/+server.ts @@ -8,6 +8,7 @@ import Card from './Card.svelte'; import DMSerifDisplay from '$lib/fonts/DMSerifDisplay-Regular.ttf?url'; import FiraSans from '$lib/fonts/FiraSans-Regular.ttf?url'; import { docs } from '$lib/server/content'; +import { decode_html } from '$lib/utils/escape'; import type { ServerlessConfig } from '@sveltejs/adapter-vercel'; export const config: ServerlessConfig = { @@ -27,6 +28,34 @@ export function entries() { }); } +type VNode = ReturnType; + +/** + * Decodes the XML-ified parts of string subnodes + * to revert to the original content + * + * @param node the original node + * @returns the node with replaced leaves + */ +function decode_vnode(node: VNode): VNode { + const children = node.props.children; + if (!children) return node; + + const decodedChildren = + typeof children === 'string' + ? decode_html(children) + : Array.isArray(children) + ? children.map(decode_vnode) + : decode_vnode(children); + + if (decodedChildren === children) return node; + + return { + ...node, + props: { ...node.props, children: decodedChildren } + }; +} + const height = 630; const width = 1200; const dm_serif_display = await read(DMSerifDisplay).arrayBuffer(); @@ -42,7 +71,7 @@ export async function GET({ params }) { }); const element = toReactNode(`${result.head}${result.body}`); - const svg = await satori(element, { + const svg = await satori(decode_vnode(element), { fonts: [ { name: 'DMSerif Display',