@particle-academy/fancy-code 0.9.1 → 0.9.3

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 ADDED
@@ -0,0 +1,68 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@particle-academy/fancy-code` are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
+
7
+ > **Pre-1.0:** breaking changes land in MINOR releases. Until 1.0 the minor
8
+ > number is not a compatibility promise — read the entry, not the version.
9
+
10
+ > This file starts here. Earlier releases predate it and were never written up;
11
+ > `git log` is the record for those. It is not backfilled rather than
12
+ > guessed-at, because a changelog that invents its own history is worse than one
13
+ > that admits where it begins.
14
+
15
+ ## [Unreleased]
16
+
17
+ ## [0.9.3] — 2026-10-18
18
+
19
+ The 0.9.2 fix below, actually published. **0.9.2 was tagged but never reached
20
+ npm**: the publish workflow's install step crashed on the runner's npm 10 (an
21
+ arborist peer-set bug). It now installs with npm 11, the version it already
22
+ published with. This line also publishes under the `maint-0.4` dist-tag, so
23
+ `latest` stays on the current line.
24
+
25
+ ## [0.9.2] — 2026-10-18 (tagged, never published)
26
+
27
+ ### Security
28
+
29
+ - **`<MarkdownEditor>`'s preview no longer renders a link or image whose URL
30
+ can run script.** Backported from 0.12.1. The preview is inserted with
31
+ `innerHTML`, where React's `javascript:` guard does not apply, and URL schemes
32
+ were never checked, so `[x](javascript:...)` was a live script link. A link
33
+ now needs a relative URL or `http`, `https`, `mailto` or `tel`; an image a
34
+ relative URL, `http(s)` or a raster `data:image/...`. Anything else renders as
35
+ its text or alt text. **Upgrade if any markdown you preview was written by
36
+ someone else.** Nothing else changes.
37
+
38
+ ## [0.9.1] — 2026-07-28
39
+
40
+ ### Fixed
41
+
42
+ - **`CodeEditorPanel` crashed when the editor engine arrived after first
43
+ render.** `if (!engine) return null` sat above the `useMemo` that builds the
44
+ gutter, so a render without an engine ran zero hooks and the next ran one —
45
+ React desyncs on the changed count and throws from inside itself.
46
+ `_engineReturn` is typed `| null` and starts null, so any host rendering the
47
+ panel before the engine settles hit this.
48
+
49
+ **No action needed** — the fix is internal and the API is unchanged.
50
+
51
+ ### Added
52
+
53
+ - `jsdom` as a devDependency, and the first component test in this package.
54
+ There was previously no way to render a component in a test here at all, which
55
+ is precisely how the bug above shipped.
56
+
57
+ ### Changed
58
+
59
+ - Widened the `@particle-academy/fancy-file-commons` requirement from `^0.2.0` to
60
+ `>=0.2 <2.0`, so a sibling minor release is an upgrade and not a resolver
61
+ conflict. **No action needed** — widening a range only adds candidates; the
62
+ version you have today still resolves.
63
+
64
+ A caret on a `0.x` range locks the MINOR, so this pinned a sibling at
65
+ whatever it happened to be on the day it was written, and each sibling
66
+ release then read as a conflict to the resolver rather than an upgrade.
67
+ Nothing here was using an API the newer minors removed — the range was the
68
+ whole problem.
package/dist/index.cjs CHANGED
@@ -1643,8 +1643,27 @@ function escapeHtml(s) {
1643
1643
  function escapeAttr(s) {
1644
1644
  return s.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1645
1645
  }
1646
+ var LINK_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
1647
+ function schemeOf(url) {
1648
+ let s = "";
1649
+ for (const ch of url) {
1650
+ const code = ch.codePointAt(0) ?? 0;
1651
+ if (code === 9 || code === 10 || code === 13) continue;
1652
+ if (s === "" && code <= 32) continue;
1653
+ s += ch;
1654
+ }
1655
+ const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(s);
1656
+ return m ? m[1].toLowerCase() : null;
1657
+ }
1658
+ function safeUrl(url, kind) {
1659
+ const scheme = schemeOf(url);
1660
+ if (scheme === null) return true;
1661
+ if (scheme === "http" || scheme === "https") return true;
1662
+ if (kind === "link") return LINK_SCHEMES.has(scheme);
1663
+ return scheme === "data" && /^data:image\/(png|jpe?g|gif|webp|avif|bmp);/i.test(url.trim());
1664
+ }
1646
1665
  function inline(text) {
1647
- 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>");
1666
+ 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>");
1648
1667
  }
1649
1668
  function renderMarkdown(markdown) {
1650
1669
  const lines = markdown.replace(/\r\n?/g, "\n").split("\n");