@particle-academy/fancy-code 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -14,6 +14,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
14
14
 
15
15
  ## [Unreleased]
16
16
 
17
+ ## [0.12.1] - 2026-10-18
18
+
19
+ ### Security
20
+
21
+ - **`<MarkdownEditor>`'s preview no longer renders a link or image whose URL
22
+ can run script.** The preview is built as an HTML string and inserted with
23
+ `innerHTML`, where React's `javascript:` guard does not apply. URLs were kept
24
+ inside their attribute but their scheme was never checked, so
25
+ `[x](javascript:...)` became a live script link in the preview, in the page's
26
+ own origin. A link now needs a relative URL or `http`, `https`, `mailto` or
27
+ `tel`; an image needs a relative URL, `http(s)`, or a raster
28
+ `data:image/...`. Anything else renders as its text (a link) or its alt text
29
+ (an image). The scheme is read the way a browser reads it: case-insensitive,
30
+ with leading control characters and embedded tabs and newlines ignored.
31
+ **Upgrade if any markdown you preview was written by someone else**, an agent
32
+ included. Nothing else changes. Found while auditing the viewers fancy-labs
33
+ uses to show agent-written documents.
34
+
17
35
  ## [0.12.0] - 2026-10-17
18
36
 
19
37
  ### Added
package/dist/index.cjs CHANGED
@@ -1809,8 +1809,27 @@ function escapeHtml(s) {
1809
1809
  function escapeAttr(s) {
1810
1810
  return s.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1811
1811
  }
1812
+ var LINK_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
1813
+ function schemeOf(url) {
1814
+ let s = "";
1815
+ for (const ch of url) {
1816
+ const code = ch.codePointAt(0) ?? 0;
1817
+ if (code === 9 || code === 10 || code === 13) continue;
1818
+ if (s === "" && code <= 32) continue;
1819
+ s += ch;
1820
+ }
1821
+ const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(s);
1822
+ return m ? m[1].toLowerCase() : null;
1823
+ }
1824
+ function safeUrl(url, kind) {
1825
+ const scheme = schemeOf(url);
1826
+ if (scheme === null) return true;
1827
+ if (scheme === "http" || scheme === "https") return true;
1828
+ if (kind === "link") return LINK_SCHEMES.has(scheme);
1829
+ return scheme === "data" && /^data:image\/(png|jpe?g|gif|webp|avif|bmp);/i.test(url.trim());
1830
+ }
1812
1831
  function inline(text) {
1813
- return text.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (_m, alt, src, title) => `<img src="${escapeAttr(src)}" alt="${escapeAttr(alt)}"${title ? ` title="${escapeAttr(title)}"` : ""}>`).replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (_m, t, href, title) => `<a href="${escapeAttr(href)}"${title ? ` title="${escapeAttr(title)}"` : ""} rel="noopener">${t}</a>`).replace(/`([^`]+)`/g, (_m, c) => `<code>${c}</code>`).replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, "<strong>$2</strong>").replace(/(\*|_)(?=\S)([\s\S]*?\S)\1/g, "<em>$2</em>");
1832
+ return text.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (_m, alt, src, title) => safeUrl(src, "image") ? `<img src="${escapeAttr(src)}" alt="${escapeAttr(alt)}"${title ? ` title="${escapeAttr(title)}"` : ""}>` : alt).replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (_m, t, href, title) => safeUrl(href, "link") ? `<a href="${escapeAttr(href)}"${title ? ` title="${escapeAttr(title)}"` : ""} rel="noopener">${t}</a>` : t).replace(/`([^`]+)`/g, (_m, c) => `<code>${c}</code>`).replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, "<strong>$2</strong>").replace(/(\*|_)(?=\S)([\s\S]*?\S)\1/g, "<em>$2</em>");
1814
1833
  }
1815
1834
  function renderMarkdown(markdown) {
1816
1835
  const lines = markdown.replace(/\r\n?/g, "\n").split("\n");