diff --git a/website/data/blog/markdown-test.md b/website/data/blog/markdown-test.md index 3e603d7..72157d2 100644 --- a/website/data/blog/markdown-test.md +++ b/website/data/blog/markdown-test.md @@ -102,11 +102,11 @@ const obj = { ## Images -![1.00](data/blog/media/test.gif "Caption Test") +![0.50](data/blog/media/test.gif "Caption Test") ![0.26](data/blog/media/paste-1778404389024-jnpo3.jpeg) -![1.00](data/blog/media/paste-1778404389025-wzt8y.png) +![1.00](blob:vscode-webview://1b6n822od8b3q8ir5r6it0k34211vkpefa12tlnhqbm9ukcvq8kt/f5da78e4-e0e9-4fd8-b889-6286ac0769f8 "Caption without resize") ## Mixed Content diff --git a/website/scripts/MarkdownRenderer.js b/website/scripts/MarkdownRenderer.js index 04e5898..bd7037d 100644 --- a/website/scripts/MarkdownRenderer.js +++ b/website/scripts/MarkdownRenderer.js @@ -108,12 +108,11 @@ export class MarkdownRenderer { } // Standalone image - const imgBlockMatch = block.match(/^!\[([^\]]*)\]\(([^)]+)\)$/); + const imgBlockMatch = block.match(/^!\[([^\]]*)\]\((.+)\)$/); if (imgBlockMatch) { - const alt = MarkdownRenderer.#escape(imgBlockMatch[1]); - const src = MarkdownRenderer.#sanitizeUrl(imgBlockMatch[2]); + const { src, caption, imageStyle } = MarkdownRenderer.#parseImageMeta(imgBlockMatch[1], imgBlockMatch[2]); if (src) { - return `
${alt}${alt ? `
${alt}
` : ""}
`; + return `
${caption}${caption ? `
${caption}
` : ""}
`; } } @@ -215,11 +214,11 @@ export class MarkdownRenderer { // Images ![alt](url) — must be matched before links out = out.replace( - /!\[([^\]]*)\]\(([^)]+)\)/g, - (_, alt, url) => { - const safeUrl = MarkdownRenderer.#sanitizeUrl(url); - if (!safeUrl) return MarkdownRenderer.#escape(alt); - return `${MarkdownRenderer.#escape(alt)}`; + /!\[([^\]]*)\]\((.+?)\)/g, + (_, altText, rawMeta) => { + const { src, caption } = MarkdownRenderer.#parseImageMeta(altText, rawMeta); + if (!src) return MarkdownRenderer.#escape(altText); + return `${caption}`; } ); @@ -273,4 +272,54 @@ export class MarkdownRenderer { if (/^[a-zA-Z0-9_\-][a-zA-Z0-9_.\-\/]*$/.test(trimmed)) return trimmed; return null; } + + /** + * Parses markdown image metadata including optional title and size token. + * + * Supported format: ![alt](url "title") + * - Numeric alt-only values (e.g. 0.50) are treated as width scale metadata. + * - Title text is used as the visible caption when present. + * + * @param {string} rawAlt + * @param {string} rawMeta + * @returns {{ src: string | null, caption: string, imageStyle: string }} + */ + static #parseImageMeta(rawAlt, rawMeta) { + const meta = rawMeta.trim(); + const titleMatch = meta.match(/^(\S+)(?:\s+"([\s\S]*?)")?$/); + if (!titleMatch) { + return { + src: null, + caption: MarkdownRenderer.#escape(rawAlt), + imageStyle: "", + }; + } + + const src = MarkdownRenderer.#sanitizeUrl(titleMatch[1]); + const titleCaption = titleMatch[2] ? MarkdownRenderer.#escape(titleMatch[2]) : ""; + const trimmedAlt = rawAlt.trim(); + const sizeScale = MarkdownRenderer.#parseImageSizeScale(trimmedAlt); + const caption = titleCaption || (sizeScale === null ? MarkdownRenderer.#escape(trimmedAlt) : ""); + const imageStyle = sizeScale !== null ? ` style="width: ${sizeScale * 100}%;"` : ""; + + return { + src, + caption, + imageStyle, + }; + } + + /** + * Parses a numeric image scale token from markdown alt text. + * + * @param {string} rawAlt + * @returns {number | null} + */ + static #parseImageSizeScale(rawAlt) { + if (!/^\d*\.?\d+$/.test(rawAlt)) return null; + const value = parseFloat(rawAlt); + if (!Number.isFinite(value)) return null; + if (value <= 0 || value > 1) return null; + return value; + } }