@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 +127 -0
- package/LICENSE +21 -0
- package/README.md +30 -1
- package/dist/index.cjs +146 -108
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -7
- package/dist/index.d.ts +37 -7
- package/dist/index.js +147 -109
- package/dist/index.js.map +1 -1
- package/package.json +12 -8
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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1642
|
-
}
|
|
1643
|
-
function escapeAttr(s) {
|
|
1644
|
-
return s.replace(/"/g, """).replace(/'/g, "'");
|
|
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*>\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
|
|
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
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1808
|
+
}
|
|
1809
|
+
function escapeAttr(s) {
|
|
1810
|
+
return s.replace(/"/g, """).replace(/'/g, "'");
|
|
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*>\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]);
|