@particle-academy/fancy-code 0.10.0 → 0.12.0

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,127 @@
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.12.0] - 2026-10-17
18
+
19
+ ### Added
20
+
21
+ - **`<FileViewer line={42}>` lands on a line**: scrolled to the middle of the
22
+ view, caret placed, and highlighted — on mount and again whenever `line`
23
+ changes, so re-targeting an open file scrolls again. `column` places the caret
24
+ within it. Text files only; it does not steal focus.
25
+
26
+ Asked for by Genie 2, whose `openFileForUser(path, line)` now opens files in its
27
+ own FileViewer panel. `CodeEditor` could already reveal a line (`cursorLine`);
28
+ `FileViewer` never passed one through, and nothing marked the line on arrival.
29
+
30
+ - **`highlightLines` on `FileViewer` and `CodeEditor`** draws a full-width band
31
+ behind each listed line and bolds its gutter number. `FileViewer` highlights
32
+ `[line]` by default; pass a range or several hits to override, or `[]` to land
33
+ without a highlight. Lines outside the document are ignored. Measured in a real
34
+ browser: the band lines up with the gutter row to the pixel. Sized for
35
+ unwrapped lines, as the gutter is.
36
+
37
+ Handle: `data-fancy-code-highlight-line={n}`.
38
+
39
+ **What you must DO: nothing.** Both props are optional; without them the viewer
40
+ renders exactly as before.
41
+
42
+ ### Fixed
43
+
44
+ - **`CHANGELOG.md` is now in the published tarball.** `files` did not whitelist it, so npm never shipped it — and this package puts breaking changes in MINOR releases and tells you in the README to read the entry before taking one. The instruction existed for the author, who has the file, and not for the consumer, who is the only one being instructed. Nothing for you to do; the file simply arrives from this release on.
45
+
46
+ ### Security
47
+
48
+ - `source-map-js` is pinned forward to `^1.2.2` via `overrides`. Versions up to
49
+ 1.2.1 allow an event-loop denial of service through indexed source-map section
50
+ offsets, and it arrives here transitively through the build toolchain.
51
+ **Nothing for a consumer to do, and no runtime change**: an npm package does
52
+ not ship a lockfile, so this governs builds OF this repo, not anything
53
+ installed FROM it. Recorded rather than left silent because the override it
54
+ sits beside — `shell-quote` `^1.9.0`, added for an earlier advisory — was
55
+ carried with no note of why, and had drifted back inside the vulnerable range
56
+ before anyone looked.
57
+
58
+ ## [0.11.0] — 2026-08-14
59
+
60
+ ### Changed
61
+
62
+ - **The `MarkdownEditor` preview now renders through react-fancy's `<ContentRenderer format="markdown">`** — a full CommonMark + GFM parse — instead of the small built-in renderer.
63
+
64
+ Reported by a consumer reviewing documents through this surface, and measured before changing anything: a GFM table collapsed into a single run-on `<p>` (no table support at all), a hard-wrapped blockquote became one `<blockquote>` per source line, and a wrapped list item's continuation escaped the `<ul>` to become a `<p>`. On a document hard-wrapped at ~95 characters that degrades *everything*, and reads as though the author wrote it badly. Their most important block — a claim/evidence/status table — was the worst rendered.
65
+
66
+ The fix adds **no dependency**. `marked` was already in this tree as a runtime dependency of react-fancy, which fancy-code already requires, so the built-in renderer was a second, weaker markdown implementation sitting next to a complete one. Output is sanitised by default, as before.
67
+
68
+ **What you must do:** nothing to install, and nothing if you already pass `renderPreview` — that still wins and is still injected as-is. If you style the preview, note the rendered HTML is now marked's output: real `<table>`/`<thead>`/`<td>`, one `<blockquote>` per quote, and `<p>` inside `<blockquote>`. Selectors written against the old flattened markup may need updating.
69
+
70
+ `renderMarkdown` is unchanged and still exported for direct use; it is simply no longer the preview default.
71
+
72
+ ### Fixed
73
+
74
+ - **`maxHeight` now constrains the preview pane.** It was passed only to the editor, so a long document rendered at full height instead of scrolling. The preview gets `maxHeight` + `overflow-y: auto` when the prop is set.
75
+
76
+ ## [0.10.0] — 2026-08-07
77
+
78
+ ### Changed
79
+
80
+ - **BREAKING — Node 22 is now declared as the floor.** `engines.node` is `>=22`, where this package previously declared **nothing at all**.
81
+
82
+ Declaring nothing was not the same as supporting old Node: a consumer on 18 installed cleanly and found out at runtime.
83
+
84
+ **What you must do:** on Node 22 or newer, nothing. Note npm only *warns* on an `engines` mismatch while **pnpm fails the install**, so this surfaces differently depending on your package manager. Node 18 is end-of-life and 20 is maintenance-only.
85
+
86
+ - **BREAKING — React 18 is no longer supported.** `peerDependencies.react` / `react-dom` are now `^19.0.0`.
87
+
88
+ **What you must do:** on React 19, nothing. On React 18, stay on the previous release, or upgrade your app to 19 first.
89
+
90
+ React 18 support was a claim nothing tested — every build and test in this package ran against 19, so the 18 half of the old range was never executed. An untested compatibility claim is worse than an absent one, because it reads as support.
91
+
92
+ ### Why
93
+
94
+ These are the kit 0.5 platform floors, applied across every package at once so a consumer never has to resolve a mix. **No API changed, nothing was removed, nothing was renamed** — only what the package requires.
95
+
96
+
97
+ ## [0.9.1] — 2026-07-28
98
+
99
+ ### Fixed
100
+
101
+ - **`CodeEditorPanel` crashed when the editor engine arrived after first
102
+ render.** `if (!engine) return null` sat above the `useMemo` that builds the
103
+ gutter, so a render without an engine ran zero hooks and the next ran one —
104
+ React desyncs on the changed count and throws from inside itself.
105
+ `_engineReturn` is typed `| null` and starts null, so any host rendering the
106
+ panel before the engine settles hit this.
107
+
108
+ **No action needed** — the fix is internal and the API is unchanged.
109
+
110
+ ### Added
111
+
112
+ - `jsdom` as a devDependency, and the first component test in this package.
113
+ There was previously no way to render a component in a test here at all, which
114
+ is precisely how the bug above shipped.
115
+
116
+ ### Changed
117
+
118
+ - Widened the `@particle-academy/fancy-file-commons` requirement from `^0.2.0` to
119
+ `>=0.2 <2.0`, so a sibling minor release is an upgrade and not a resolver
120
+ conflict. **No action needed** — widening a range only adds candidates; the
121
+ version you have today still resolves.
122
+
123
+ A caret on a `0.x` range locks the MINOR, so this pinned a sibling at
124
+ whatever it happened to be on the day it was written, and each sibling
125
+ release then read as a conflict to the resolver rather than an upgrade.
126
+ Nothing here was using an API the newer minors removed — the range was the
127
+ whole problem.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Particle Academy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -12,7 +12,36 @@ npm install @particle-academy/fancy-code
12
12
  # or: yarn add @particle-academy/fancy-code
13
13
  ```
14
14
 
15
- **Peer dependencies:** `react >= 18`, `react-dom >= 18`, `@particle-academy/react-fancy >= 4.9` (the `FileViewer` delegates media to react-fancy's `<MediaViewer>`, added in 4.9)
15
+ **Peer dependencies:** `react ^19.0.0`, `react-dom ^19.0.0`, `@particle-academy/react-fancy >= 4.9` (the `FileViewer` delegates media to react-fancy's `<MediaViewer>`, added in 4.9)
16
+
17
+ ## Which version range to depend on
18
+
19
+ **Use a caret. `^0.11.0` is correct here, and it is deliberate.**
20
+
21
+ ```jsonc
22
+ "@particle-academy/fancy-code": "^0.11.0"
23
+ ```
24
+
25
+ This package is **pre-1.0, and breaking changes land in MINOR releases** — see
26
+ the note at the top of [`CHANGELOG.md`](./CHANGELOG.md). A caret on a `0.x` locks
27
+ the minor (npm reads `^0.11.0` as `>=0.11.0 <0.12.0`), so it gives you patches and
28
+ holds you at a surface you have already integrated against. That is the right
29
+ default when the next minor may change something under you.
30
+
31
+ **Moving up a minor is a deliberate act.** Read that version's `CHANGELOG.md`
32
+ entry before you do — breaking entries say what a consumer must actually DO, and
33
+ about half of what reads as breaking needs no action at all.
34
+
35
+ > **A note on the rest of the suite.** Some Fancy packages recommend an open
36
+ > `>=X <2.0` range instead. That is not an inconsistency: those packages carry a
37
+ > **runtime compatibility check** that fails loudly when a consumer is out of
38
+ > step — `fancy-connector-core`'s `CONNECTOR_API_VERSION` is the example — which
39
+ > is a stronger guarantee than a caret and makes the caret unnecessary. This
40
+ > package has no such check, so the caret is doing real work.
41
+ >
42
+ > The rule inside the suite differs again: first-party packages depend on each
43
+ > other with an open range because they are **released and tested together** at a
44
+ > kit version. A consumer is not, which is why the advice here is not the same.
16
45
 
17
46
  ## Setup
18
47
 
package/dist/index.cjs CHANGED
@@ -41,12 +41,14 @@ function CodeEditorPanel({ className }) {
41
41
  placeholder,
42
42
  _diffAnnotations: diffAnnotations,
43
43
  _maxHeight,
44
- _minHeight
44
+ _minHeight,
45
+ _highlightLines: highlightLines
45
46
  } = useCodeEditor();
46
47
  const lineNumberElements = react.useMemo(() => {
47
48
  if (!engine || !lineNumbers) return null;
48
49
  const { themeColors: themeColors2, lineCount: lineCount2, activeLine: activeLine2 } = engine;
49
50
  const removed = diffRemovedColor(themeColors2);
51
+ const highlighted = new Set(highlightLines ?? []);
50
52
  const lines = [];
51
53
  for (let i = 1; i <= lineCount2; i++) {
52
54
  const mark = gutterDiffMark(diffAnnotations, i);
@@ -58,8 +60,9 @@ function CodeEditorPanel({ className }) {
58
60
  "data-diff-deleted-above": mark?.deletedAbove ? "" : void 0,
59
61
  style: {
60
62
  position: "relative",
61
- color: i === activeLine2 ? themeColors2.foreground : themeColors2.gutterForeground,
62
- backgroundColor: i === activeLine2 ? themeColors2.activeLineBackground : void 0
63
+ color: i === activeLine2 || highlighted.has(i) ? themeColors2.foreground : themeColors2.gutterForeground,
64
+ backgroundColor: i === activeLine2 ? themeColors2.activeLineBackground : void 0,
65
+ fontWeight: highlighted.has(i) ? 600 : void 0
63
66
  },
64
67
  className: "pr-3 text-right text-[13px] leading-[1.5] select-none",
65
68
  children: [
@@ -119,7 +122,7 @@ function CodeEditorPanel({ className }) {
119
122
  );
120
123
  }
121
124
  return lines;
122
- }, [engine, lineNumbers, diffAnnotations]);
125
+ }, [engine, lineNumbers, diffAnnotations, highlightLines]);
123
126
  if (!engine) return null;
124
127
  const {
125
128
  textareaRef,
@@ -162,6 +165,21 @@ function CodeEditorPanel({ className }) {
162
165
  }
163
166
  ),
164
167
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: reactFancy.cn("relative flex-1", wordWrap && "min-w-0"), children: [
168
+ (highlightLines ?? []).filter((n) => Number.isInteger(n) && n >= 1 && n <= lineCount).map((n) => /* @__PURE__ */ jsxRuntime.jsx(
169
+ "div",
170
+ {
171
+ "data-fancy-code-highlight-line": n,
172
+ "aria-hidden": "true",
173
+ className: "pointer-events-none absolute inset-x-0 text-[13px] leading-[1.5]",
174
+ style: {
175
+ top: `calc(0.625rem + ${(n - 1) * 1.5}em)`,
176
+ height: "1.5em",
177
+ backgroundColor: themeColors.selectionBackground,
178
+ opacity: 0.55
179
+ }
180
+ },
181
+ `highlight-${n}`
182
+ )),
165
183
  /* @__PURE__ */ jsxRuntime.jsx(
166
184
  "pre",
167
185
  {
@@ -1517,7 +1535,8 @@ function CodeEditorRoot({
1517
1535
  maxHeight,
1518
1536
  cursorLine,
1519
1537
  cursorColumn,
1520
- diffBase
1538
+ diffBase,
1539
+ highlightLines
1521
1540
  }) {
1522
1541
  const [currentValue, setCurrentValue] = reactFancy.useControllableState(valueProp, defaultValue, onChange);
1523
1542
  const diffAnnotations = react.useMemo(
@@ -1610,9 +1629,10 @@ function CodeEditorRoot({
1610
1629
  _diffAnnotations: diffAnnotations,
1611
1630
  _engineReturn: engineReturn,
1612
1631
  _minHeight: minHeight,
1613
- _maxHeight: maxHeight
1632
+ _maxHeight: maxHeight,
1633
+ _highlightLines: highlightLines
1614
1634
  }),
1615
- [engineReturn, currentValue, currentLanguage, setLanguage, resolvedTheme, readOnly, showLineNumbers, isWordWrap, tabSizeProp, cursorPosition, selectionLength, setCurrentValue, placeholder, diffAnnotations, minHeight, maxHeight]
1635
+ [engineReturn, currentValue, currentLanguage, setLanguage, resolvedTheme, readOnly, showLineNumbers, isWordWrap, tabSizeProp, cursorPosition, selectionLength, setCurrentValue, placeholder, diffAnnotations, minHeight, maxHeight, highlightLines]
1616
1636
  );
1617
1637
  return /* @__PURE__ */ jsxRuntime.jsx(CodeEditorContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx(
1618
1638
  "div",
@@ -1635,105 +1655,6 @@ var CodeEditor = Object.assign(CodeEditorRoot, {
1635
1655
  Panel: CodeEditorPanel,
1636
1656
  StatusBar: CodeEditorStatusBar
1637
1657
  });
1638
-
1639
- // src/engine/markdown-render.ts
1640
- function escapeHtml(s) {
1641
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1642
- }
1643
- function escapeAttr(s) {
1644
- return s.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1645
- }
1646
- 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>");
1648
- }
1649
- function renderMarkdown(markdown) {
1650
- const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
1651
- const out = [];
1652
- let i = 0;
1653
- let para = [];
1654
- let listType = null;
1655
- const flushPara = () => {
1656
- if (para.length) {
1657
- out.push(`<p>${inline(para.join(" "))}</p>`);
1658
- para = [];
1659
- }
1660
- };
1661
- const closeList = () => {
1662
- if (listType) {
1663
- out.push(`</${listType}>`);
1664
- listType = null;
1665
- }
1666
- };
1667
- while (i < lines.length) {
1668
- const line = lines[i];
1669
- const fence = line.match(/^\s*(```|~~~)(.*)$/);
1670
- if (fence) {
1671
- flushPara();
1672
- closeList();
1673
- const lang = fence[2].trim();
1674
- const body = [];
1675
- i++;
1676
- while (i < lines.length && !/^\s*(```|~~~)/.test(lines[i])) {
1677
- body.push(lines[i]);
1678
- i++;
1679
- }
1680
- i++;
1681
- out.push(`<pre><code${lang ? ` class="language-${escapeAttr(escapeHtml(lang))}"` : ""}>${escapeHtml(body.join("\n"))}</code></pre>`);
1682
- continue;
1683
- }
1684
- const esc = escapeHtml(line);
1685
- if (/^\s*$/.test(line)) {
1686
- flushPara();
1687
- closeList();
1688
- i++;
1689
- continue;
1690
- }
1691
- const heading = esc.match(/^(#{1,6})\s+(.*)$/);
1692
- if (heading) {
1693
- flushPara();
1694
- closeList();
1695
- const level = heading[1].length;
1696
- out.push(`<h${level}>${inline(heading[2])}</h${level}>`);
1697
- i++;
1698
- continue;
1699
- }
1700
- if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
1701
- flushPara();
1702
- closeList();
1703
- out.push("<hr>");
1704
- i++;
1705
- continue;
1706
- }
1707
- const quote = esc.match(/^\s*&gt;\s?(.*)$/);
1708
- if (quote) {
1709
- flushPara();
1710
- closeList();
1711
- out.push(`<blockquote>${inline(quote[1])}</blockquote>`);
1712
- i++;
1713
- continue;
1714
- }
1715
- const ol = esc.match(/^\s*\d+\.\s+(.*)$/);
1716
- const ul = esc.match(/^\s*[-*+]\s+(.*)$/);
1717
- if (ol || ul) {
1718
- flushPara();
1719
- const type = ol ? "ol" : "ul";
1720
- if (listType !== type) {
1721
- closeList();
1722
- out.push(`<${type}>`);
1723
- listType = type;
1724
- }
1725
- out.push(`<li>${inline((ol ?? ul)[1])}</li>`);
1726
- i++;
1727
- continue;
1728
- }
1729
- closeList();
1730
- para.push(esc);
1731
- i++;
1732
- }
1733
- flushPara();
1734
- closeList();
1735
- return out.join("\n");
1736
- }
1737
1658
  function MarkdownEditor({
1738
1659
  value,
1739
1660
  defaultValue = "",
@@ -1747,7 +1668,7 @@ function MarkdownEditor({
1747
1668
  minHeight,
1748
1669
  maxHeight,
1749
1670
  className,
1750
- renderPreview = renderMarkdown
1671
+ renderPreview
1751
1672
  }) {
1752
1673
  const [content, setContent] = reactFancy.useControllableState(value, defaultValue, onValueChange);
1753
1674
  const showEditor = mode !== "preview";
@@ -1780,7 +1701,19 @@ function MarkdownEditor({
1780
1701
  {
1781
1702
  "data-fancy-markdown-preview": "",
1782
1703
  className: "fancy-md-preview",
1783
- dangerouslySetInnerHTML: { __html: renderPreview(content) }
1704
+ style: maxHeight ? { maxHeight, overflowY: "auto" } : void 0,
1705
+ children: renderPreview ? (
1706
+ // The host imposed its own pipeline; its HTML is injected as-is and
1707
+ // is its own responsibility to sanitise.
1708
+ /* @__PURE__ */ jsxRuntime.jsx("div", { dangerouslySetInnerHTML: { __html: renderPreview(content) } })
1709
+ ) : (
1710
+ // The kit's own renderer — a full CommonMark + GFM parse that
1711
+ // sanitises by default. `marked` already sits in this tree as a
1712
+ // react-fancy runtime dependency, so the built-in renderer this
1713
+ // replaced was a second, weaker markdown implementation next to a
1714
+ // complete one.
1715
+ /* @__PURE__ */ jsxRuntime.jsx(reactFancy.ContentRenderer, { value: content, format: "markdown" })
1716
+ )
1784
1717
  }
1785
1718
  )
1786
1719
  ]
@@ -1804,6 +1737,9 @@ function FileViewer({
1804
1737
  fileKind,
1805
1738
  lineNumbers = true,
1806
1739
  wordWrap = false,
1740
+ line,
1741
+ column,
1742
+ highlightLines,
1807
1743
  minHeight,
1808
1744
  maxHeight,
1809
1745
  mediaProps,
@@ -1855,6 +1791,9 @@ function FileViewer({
1855
1791
  wordWrap,
1856
1792
  minHeight,
1857
1793
  maxHeight,
1794
+ cursorLine: line,
1795
+ cursorColumn: column,
1796
+ highlightLines: highlightLines ?? (line != null ? [line] : void 0),
1858
1797
  children: /* @__PURE__ */ jsxRuntime.jsx(CodeEditor.Panel, {})
1859
1798
  }
1860
1799
  )
@@ -1862,6 +1801,105 @@ function FileViewer({
1862
1801
  );
1863
1802
  }
1864
1803
  FileViewer.displayName = "FileViewer";
1804
+
1805
+ // src/engine/markdown-render.ts
1806
+ function escapeHtml(s) {
1807
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1808
+ }
1809
+ function escapeAttr(s) {
1810
+ return s.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1811
+ }
1812
+ 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>");
1814
+ }
1815
+ function renderMarkdown(markdown) {
1816
+ const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
1817
+ const out = [];
1818
+ let i = 0;
1819
+ let para = [];
1820
+ let listType = null;
1821
+ const flushPara = () => {
1822
+ if (para.length) {
1823
+ out.push(`<p>${inline(para.join(" "))}</p>`);
1824
+ para = [];
1825
+ }
1826
+ };
1827
+ const closeList = () => {
1828
+ if (listType) {
1829
+ out.push(`</${listType}>`);
1830
+ listType = null;
1831
+ }
1832
+ };
1833
+ while (i < lines.length) {
1834
+ const line = lines[i];
1835
+ const fence = line.match(/^\s*(```|~~~)(.*)$/);
1836
+ if (fence) {
1837
+ flushPara();
1838
+ closeList();
1839
+ const lang = fence[2].trim();
1840
+ const body = [];
1841
+ i++;
1842
+ while (i < lines.length && !/^\s*(```|~~~)/.test(lines[i])) {
1843
+ body.push(lines[i]);
1844
+ i++;
1845
+ }
1846
+ i++;
1847
+ out.push(`<pre><code${lang ? ` class="language-${escapeAttr(escapeHtml(lang))}"` : ""}>${escapeHtml(body.join("\n"))}</code></pre>`);
1848
+ continue;
1849
+ }
1850
+ const esc = escapeHtml(line);
1851
+ if (/^\s*$/.test(line)) {
1852
+ flushPara();
1853
+ closeList();
1854
+ i++;
1855
+ continue;
1856
+ }
1857
+ const heading = esc.match(/^(#{1,6})\s+(.*)$/);
1858
+ if (heading) {
1859
+ flushPara();
1860
+ closeList();
1861
+ const level = heading[1].length;
1862
+ out.push(`<h${level}>${inline(heading[2])}</h${level}>`);
1863
+ i++;
1864
+ continue;
1865
+ }
1866
+ if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
1867
+ flushPara();
1868
+ closeList();
1869
+ out.push("<hr>");
1870
+ i++;
1871
+ continue;
1872
+ }
1873
+ const quote = esc.match(/^\s*&gt;\s?(.*)$/);
1874
+ if (quote) {
1875
+ flushPara();
1876
+ closeList();
1877
+ out.push(`<blockquote>${inline(quote[1])}</blockquote>`);
1878
+ i++;
1879
+ continue;
1880
+ }
1881
+ const ol = esc.match(/^\s*\d+\.\s+(.*)$/);
1882
+ const ul = esc.match(/^\s*[-*+]\s+(.*)$/);
1883
+ if (ol || ul) {
1884
+ flushPara();
1885
+ const type = ol ? "ol" : "ul";
1886
+ if (listType !== type) {
1887
+ closeList();
1888
+ out.push(`<${type}>`);
1889
+ listType = type;
1890
+ }
1891
+ out.push(`<li>${inline((ol ?? ul)[1])}</li>`);
1892
+ i++;
1893
+ continue;
1894
+ }
1895
+ closeList();
1896
+ para.push(esc);
1897
+ i++;
1898
+ }
1899
+ flushPara();
1900
+ closeList();
1901
+ return out.join("\n");
1902
+ }
1865
1903
  function useCodeEditorInputTagAdapter() {
1866
1904
  const ctx = useCodeEditor();
1867
1905
  return react.useMemo(() => reactFancy.textareaAdapter(ctx.textareaRef), [ctx.textareaRef]);