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
-
+

-
+
## 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}` : ""}`;
+ return `
${caption ? `${caption}` : ""}`;
}
}
@@ -215,11 +214,11 @@ export class MarkdownRenderer {
// Images  — must be matched before links
out = out.replace(
- /!\[([^\]]*)\]\(([^)]+)\)/g,
- (_, alt, url) => {
- const safeUrl = MarkdownRenderer.#sanitizeUrl(url);
- if (!safeUrl) return MarkdownRenderer.#escape(alt);
- return `
`;
+ /!\[([^\]]*)\]\((.+?)\)/g,
+ (_, altText, rawMeta) => {
+ const { src, caption } = MarkdownRenderer.#parseImageMeta(altText, rawMeta);
+ if (!src) return MarkdownRenderer.#escape(altText);
+ return `
`;
}
);
@@ -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: 
+ * - 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;
+ }
}