@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/dist/index.d.cts
CHANGED
|
@@ -73,6 +73,12 @@ interface CodeEditorProps {
|
|
|
73
73
|
cursorLine?: number;
|
|
74
74
|
/** 1-based column for {@link cursorLine} (default 1). */
|
|
75
75
|
cursorColumn?: number;
|
|
76
|
+
/**
|
|
77
|
+
* 1-based lines to mark with a full-width band — "here is the line you were
|
|
78
|
+
* sent to". Purely visual and independent of the caret; lines outside the
|
|
79
|
+
* document are ignored. Sized for unwrapped lines, as the gutter is.
|
|
80
|
+
*/
|
|
81
|
+
highlightLines?: readonly number[];
|
|
76
82
|
/**
|
|
77
83
|
* Baseline document for the diff gutter. When set, the line-number column
|
|
78
84
|
* shows per-line change marks of the CURRENT value against this baseline —
|
|
@@ -158,6 +164,8 @@ interface CodeEditorContextValue {
|
|
|
158
164
|
_minHeight?: number;
|
|
159
165
|
/** @internal */
|
|
160
166
|
_maxHeight?: number;
|
|
167
|
+
/** @internal — lines to band; see {@link CodeEditorProps.highlightLines}. */
|
|
168
|
+
_highlightLines?: readonly number[];
|
|
161
169
|
}
|
|
162
170
|
interface CodeEditorToolbarProps {
|
|
163
171
|
children?: ReactNode;
|
|
@@ -191,7 +199,7 @@ declare namespace CodeEditorStatusBar {
|
|
|
191
199
|
var displayName: string;
|
|
192
200
|
}
|
|
193
201
|
|
|
194
|
-
declare function CodeEditorRoot({ children, className, value: valueProp, defaultValue, onChange, language: languageProp, onLanguageChange, theme, readOnly, lineNumbers: lineNumbersProp, wordWrap: wordWrapProp, tabSize: tabSizeProp, placeholder, minHeight, maxHeight, cursorLine, cursorColumn, diffBase, }: CodeEditorProps): react.JSX.Element;
|
|
202
|
+
declare function CodeEditorRoot({ children, className, value: valueProp, defaultValue, onChange, language: languageProp, onLanguageChange, theme, readOnly, lineNumbers: lineNumbersProp, wordWrap: wordWrapProp, tabSize: tabSizeProp, placeholder, minHeight, maxHeight, cursorLine, cursorColumn, diffBase, highlightLines, }: CodeEditorProps): react.JSX.Element;
|
|
195
203
|
declare namespace CodeEditorRoot {
|
|
196
204
|
var displayName: string;
|
|
197
205
|
}
|
|
@@ -228,21 +236,30 @@ interface MarkdownEditorProps {
|
|
|
228
236
|
wordWrap?: boolean;
|
|
229
237
|
/** Placeholder shown when empty. */
|
|
230
238
|
placeholder?: string;
|
|
231
|
-
/**
|
|
239
|
+
/**
|
|
240
|
+
* Height bounds (px). `maxHeight` constrains the PREVIEW pane as well as the
|
|
241
|
+
* editor — it previously reached only the editor, so a long document rendered
|
|
242
|
+
* at full height instead of scrolling.
|
|
243
|
+
*/
|
|
232
244
|
minHeight?: number;
|
|
233
245
|
maxHeight?: number;
|
|
234
246
|
className?: string;
|
|
235
247
|
/**
|
|
236
|
-
* Swap the markdown → HTML renderer used by the preview pane
|
|
237
|
-
*
|
|
248
|
+
* Swap the markdown → HTML renderer used by the preview pane.
|
|
249
|
+
*
|
|
250
|
+
* Rarely needed now: the preview renders through react-fancy's
|
|
251
|
+
* `<ContentRenderer format="markdown">`, which is a full CommonMark + GFM
|
|
252
|
+
* parse. Supply this only to impose your own pipeline — the returned HTML is
|
|
253
|
+
* injected as-is, so it must already be sanitised.
|
|
238
254
|
*/
|
|
239
255
|
renderPreview?: (markdown: string) => string;
|
|
240
256
|
}
|
|
241
257
|
/**
|
|
242
258
|
* A markdown-aware editor: a syntax-highlighted `CodeEditor` (the registered
|
|
243
259
|
* `markdown` language) with an optional live preview pane. Controlled via
|
|
244
|
-
* `value` + `onValueChange`. The preview renders
|
|
245
|
-
*
|
|
260
|
+
* `value` + `onValueChange`. The preview renders through react-fancy's
|
|
261
|
+
* `<ContentRenderer format="markdown">` — a full CommonMark + GFM parse,
|
|
262
|
+
* sanitised by default. Pass `renderPreview` to impose your own pipeline.
|
|
246
263
|
*
|
|
247
264
|
* ```tsx
|
|
248
265
|
* <MarkdownEditor value={md} onValueChange={setMd} mode="split" minHeight={240} />
|
|
@@ -319,6 +336,19 @@ interface FileViewerProps {
|
|
|
319
336
|
lineNumbers?: boolean;
|
|
320
337
|
/** Wrap long lines in the text editor (default `false`). */
|
|
321
338
|
wordWrap?: boolean;
|
|
339
|
+
/**
|
|
340
|
+
* 1-based line to land on: scrolled to the middle of the view, caret placed,
|
|
341
|
+
* and highlighted. Applied on mount and again whenever it changes, so
|
|
342
|
+
* re-targeting an open file scrolls again. Text files only; does not steal focus.
|
|
343
|
+
*/
|
|
344
|
+
line?: number;
|
|
345
|
+
/** 1-based column for {@link line} (default 1). */
|
|
346
|
+
column?: number;
|
|
347
|
+
/**
|
|
348
|
+
* Lines to highlight. Defaults to `[line]` when `line` is set; pass your own
|
|
349
|
+
* (a range, several hits) to override, or `[]` to land without highlighting.
|
|
350
|
+
*/
|
|
351
|
+
highlightLines?: readonly number[];
|
|
322
352
|
/** Editor height bounds (px). */
|
|
323
353
|
minHeight?: number;
|
|
324
354
|
maxHeight?: number;
|
|
@@ -344,7 +374,7 @@ interface FileViewerProps {
|
|
|
344
374
|
* <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
|
|
345
375
|
* ```
|
|
346
376
|
*/
|
|
347
|
-
declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, minHeight, maxHeight, mediaProps, className, style, }: FileViewerProps): react.JSX.Element;
|
|
377
|
+
declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, line, column, highlightLines, minHeight, maxHeight, mediaProps, className, style, }: FileViewerProps): react.JSX.Element;
|
|
348
378
|
declare namespace FileViewer {
|
|
349
379
|
var displayName: string;
|
|
350
380
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -73,6 +73,12 @@ interface CodeEditorProps {
|
|
|
73
73
|
cursorLine?: number;
|
|
74
74
|
/** 1-based column for {@link cursorLine} (default 1). */
|
|
75
75
|
cursorColumn?: number;
|
|
76
|
+
/**
|
|
77
|
+
* 1-based lines to mark with a full-width band — "here is the line you were
|
|
78
|
+
* sent to". Purely visual and independent of the caret; lines outside the
|
|
79
|
+
* document are ignored. Sized for unwrapped lines, as the gutter is.
|
|
80
|
+
*/
|
|
81
|
+
highlightLines?: readonly number[];
|
|
76
82
|
/**
|
|
77
83
|
* Baseline document for the diff gutter. When set, the line-number column
|
|
78
84
|
* shows per-line change marks of the CURRENT value against this baseline —
|
|
@@ -158,6 +164,8 @@ interface CodeEditorContextValue {
|
|
|
158
164
|
_minHeight?: number;
|
|
159
165
|
/** @internal */
|
|
160
166
|
_maxHeight?: number;
|
|
167
|
+
/** @internal — lines to band; see {@link CodeEditorProps.highlightLines}. */
|
|
168
|
+
_highlightLines?: readonly number[];
|
|
161
169
|
}
|
|
162
170
|
interface CodeEditorToolbarProps {
|
|
163
171
|
children?: ReactNode;
|
|
@@ -191,7 +199,7 @@ declare namespace CodeEditorStatusBar {
|
|
|
191
199
|
var displayName: string;
|
|
192
200
|
}
|
|
193
201
|
|
|
194
|
-
declare function CodeEditorRoot({ children, className, value: valueProp, defaultValue, onChange, language: languageProp, onLanguageChange, theme, readOnly, lineNumbers: lineNumbersProp, wordWrap: wordWrapProp, tabSize: tabSizeProp, placeholder, minHeight, maxHeight, cursorLine, cursorColumn, diffBase, }: CodeEditorProps): react.JSX.Element;
|
|
202
|
+
declare function CodeEditorRoot({ children, className, value: valueProp, defaultValue, onChange, language: languageProp, onLanguageChange, theme, readOnly, lineNumbers: lineNumbersProp, wordWrap: wordWrapProp, tabSize: tabSizeProp, placeholder, minHeight, maxHeight, cursorLine, cursorColumn, diffBase, highlightLines, }: CodeEditorProps): react.JSX.Element;
|
|
195
203
|
declare namespace CodeEditorRoot {
|
|
196
204
|
var displayName: string;
|
|
197
205
|
}
|
|
@@ -228,21 +236,30 @@ interface MarkdownEditorProps {
|
|
|
228
236
|
wordWrap?: boolean;
|
|
229
237
|
/** Placeholder shown when empty. */
|
|
230
238
|
placeholder?: string;
|
|
231
|
-
/**
|
|
239
|
+
/**
|
|
240
|
+
* Height bounds (px). `maxHeight` constrains the PREVIEW pane as well as the
|
|
241
|
+
* editor — it previously reached only the editor, so a long document rendered
|
|
242
|
+
* at full height instead of scrolling.
|
|
243
|
+
*/
|
|
232
244
|
minHeight?: number;
|
|
233
245
|
maxHeight?: number;
|
|
234
246
|
className?: string;
|
|
235
247
|
/**
|
|
236
|
-
* Swap the markdown → HTML renderer used by the preview pane
|
|
237
|
-
*
|
|
248
|
+
* Swap the markdown → HTML renderer used by the preview pane.
|
|
249
|
+
*
|
|
250
|
+
* Rarely needed now: the preview renders through react-fancy's
|
|
251
|
+
* `<ContentRenderer format="markdown">`, which is a full CommonMark + GFM
|
|
252
|
+
* parse. Supply this only to impose your own pipeline — the returned HTML is
|
|
253
|
+
* injected as-is, so it must already be sanitised.
|
|
238
254
|
*/
|
|
239
255
|
renderPreview?: (markdown: string) => string;
|
|
240
256
|
}
|
|
241
257
|
/**
|
|
242
258
|
* A markdown-aware editor: a syntax-highlighted `CodeEditor` (the registered
|
|
243
259
|
* `markdown` language) with an optional live preview pane. Controlled via
|
|
244
|
-
* `value` + `onValueChange`. The preview renders
|
|
245
|
-
*
|
|
260
|
+
* `value` + `onValueChange`. The preview renders through react-fancy's
|
|
261
|
+
* `<ContentRenderer format="markdown">` — a full CommonMark + GFM parse,
|
|
262
|
+
* sanitised by default. Pass `renderPreview` to impose your own pipeline.
|
|
246
263
|
*
|
|
247
264
|
* ```tsx
|
|
248
265
|
* <MarkdownEditor value={md} onValueChange={setMd} mode="split" minHeight={240} />
|
|
@@ -319,6 +336,19 @@ interface FileViewerProps {
|
|
|
319
336
|
lineNumbers?: boolean;
|
|
320
337
|
/** Wrap long lines in the text editor (default `false`). */
|
|
321
338
|
wordWrap?: boolean;
|
|
339
|
+
/**
|
|
340
|
+
* 1-based line to land on: scrolled to the middle of the view, caret placed,
|
|
341
|
+
* and highlighted. Applied on mount and again whenever it changes, so
|
|
342
|
+
* re-targeting an open file scrolls again. Text files only; does not steal focus.
|
|
343
|
+
*/
|
|
344
|
+
line?: number;
|
|
345
|
+
/** 1-based column for {@link line} (default 1). */
|
|
346
|
+
column?: number;
|
|
347
|
+
/**
|
|
348
|
+
* Lines to highlight. Defaults to `[line]` when `line` is set; pass your own
|
|
349
|
+
* (a range, several hits) to override, or `[]` to land without highlighting.
|
|
350
|
+
*/
|
|
351
|
+
highlightLines?: readonly number[];
|
|
322
352
|
/** Editor height bounds (px). */
|
|
323
353
|
minHeight?: number;
|
|
324
354
|
maxHeight?: number;
|
|
@@ -344,7 +374,7 @@ interface FileViewerProps {
|
|
|
344
374
|
* <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
|
|
345
375
|
* ```
|
|
346
376
|
*/
|
|
347
|
-
declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, minHeight, maxHeight, mediaProps, className, style, }: FileViewerProps): react.JSX.Element;
|
|
377
|
+
declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, line, column, highlightLines, minHeight, maxHeight, mediaProps, className, style, }: FileViewerProps): react.JSX.Element;
|
|
348
378
|
declare namespace FileViewer {
|
|
349
379
|
var displayName: string;
|
|
350
380
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createContext, useMemo, useState, useCallback, useRef, useEffect, useSyncExternalStore, useContext } from 'react';
|
|
2
|
-
import { cn, useControllableState, resolveMediaType, MediaViewer, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
|
|
2
|
+
import { cn, useControllableState, ContentRenderer, resolveMediaType, MediaViewer, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
|
|
3
3
|
import { annotateLines, highlightCode, tok, tokenizeHtml, LIGHT_COLORS, DARK_COLORS, languageFromFilename } from '@particle-academy/fancy-file-commons';
|
|
4
4
|
export { languageFromFilename } from '@particle-academy/fancy-file-commons';
|
|
5
5
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
@@ -40,12 +40,14 @@ function CodeEditorPanel({ className }) {
|
|
|
40
40
|
placeholder,
|
|
41
41
|
_diffAnnotations: diffAnnotations,
|
|
42
42
|
_maxHeight,
|
|
43
|
-
_minHeight
|
|
43
|
+
_minHeight,
|
|
44
|
+
_highlightLines: highlightLines
|
|
44
45
|
} = useCodeEditor();
|
|
45
46
|
const lineNumberElements = useMemo(() => {
|
|
46
47
|
if (!engine || !lineNumbers) return null;
|
|
47
48
|
const { themeColors: themeColors2, lineCount: lineCount2, activeLine: activeLine2 } = engine;
|
|
48
49
|
const removed = diffRemovedColor(themeColors2);
|
|
50
|
+
const highlighted = new Set(highlightLines ?? []);
|
|
49
51
|
const lines = [];
|
|
50
52
|
for (let i = 1; i <= lineCount2; i++) {
|
|
51
53
|
const mark = gutterDiffMark(diffAnnotations, i);
|
|
@@ -57,8 +59,9 @@ function CodeEditorPanel({ className }) {
|
|
|
57
59
|
"data-diff-deleted-above": mark?.deletedAbove ? "" : void 0,
|
|
58
60
|
style: {
|
|
59
61
|
position: "relative",
|
|
60
|
-
color: i === activeLine2 ? themeColors2.foreground : themeColors2.gutterForeground,
|
|
61
|
-
backgroundColor: i === activeLine2 ? themeColors2.activeLineBackground : void 0
|
|
62
|
+
color: i === activeLine2 || highlighted.has(i) ? themeColors2.foreground : themeColors2.gutterForeground,
|
|
63
|
+
backgroundColor: i === activeLine2 ? themeColors2.activeLineBackground : void 0,
|
|
64
|
+
fontWeight: highlighted.has(i) ? 600 : void 0
|
|
62
65
|
},
|
|
63
66
|
className: "pr-3 text-right text-[13px] leading-[1.5] select-none",
|
|
64
67
|
children: [
|
|
@@ -118,7 +121,7 @@ function CodeEditorPanel({ className }) {
|
|
|
118
121
|
);
|
|
119
122
|
}
|
|
120
123
|
return lines;
|
|
121
|
-
}, [engine, lineNumbers, diffAnnotations]);
|
|
124
|
+
}, [engine, lineNumbers, diffAnnotations, highlightLines]);
|
|
122
125
|
if (!engine) return null;
|
|
123
126
|
const {
|
|
124
127
|
textareaRef,
|
|
@@ -161,6 +164,21 @@ function CodeEditorPanel({ className }) {
|
|
|
161
164
|
}
|
|
162
165
|
),
|
|
163
166
|
/* @__PURE__ */ jsxs("div", { className: cn("relative flex-1", wordWrap && "min-w-0"), children: [
|
|
167
|
+
(highlightLines ?? []).filter((n) => Number.isInteger(n) && n >= 1 && n <= lineCount).map((n) => /* @__PURE__ */ jsx(
|
|
168
|
+
"div",
|
|
169
|
+
{
|
|
170
|
+
"data-fancy-code-highlight-line": n,
|
|
171
|
+
"aria-hidden": "true",
|
|
172
|
+
className: "pointer-events-none absolute inset-x-0 text-[13px] leading-[1.5]",
|
|
173
|
+
style: {
|
|
174
|
+
top: `calc(0.625rem + ${(n - 1) * 1.5}em)`,
|
|
175
|
+
height: "1.5em",
|
|
176
|
+
backgroundColor: themeColors.selectionBackground,
|
|
177
|
+
opacity: 0.55
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
`highlight-${n}`
|
|
181
|
+
)),
|
|
164
182
|
/* @__PURE__ */ jsx(
|
|
165
183
|
"pre",
|
|
166
184
|
{
|
|
@@ -1516,7 +1534,8 @@ function CodeEditorRoot({
|
|
|
1516
1534
|
maxHeight,
|
|
1517
1535
|
cursorLine,
|
|
1518
1536
|
cursorColumn,
|
|
1519
|
-
diffBase
|
|
1537
|
+
diffBase,
|
|
1538
|
+
highlightLines
|
|
1520
1539
|
}) {
|
|
1521
1540
|
const [currentValue, setCurrentValue] = useControllableState(valueProp, defaultValue, onChange);
|
|
1522
1541
|
const diffAnnotations = useMemo(
|
|
@@ -1609,9 +1628,10 @@ function CodeEditorRoot({
|
|
|
1609
1628
|
_diffAnnotations: diffAnnotations,
|
|
1610
1629
|
_engineReturn: engineReturn,
|
|
1611
1630
|
_minHeight: minHeight,
|
|
1612
|
-
_maxHeight: maxHeight
|
|
1631
|
+
_maxHeight: maxHeight,
|
|
1632
|
+
_highlightLines: highlightLines
|
|
1613
1633
|
}),
|
|
1614
|
-
[engineReturn, currentValue, currentLanguage, setLanguage, resolvedTheme, readOnly, showLineNumbers, isWordWrap, tabSizeProp, cursorPosition, selectionLength, setCurrentValue, placeholder, diffAnnotations, minHeight, maxHeight]
|
|
1634
|
+
[engineReturn, currentValue, currentLanguage, setLanguage, resolvedTheme, readOnly, showLineNumbers, isWordWrap, tabSizeProp, cursorPosition, selectionLength, setCurrentValue, placeholder, diffAnnotations, minHeight, maxHeight, highlightLines]
|
|
1615
1635
|
);
|
|
1616
1636
|
return /* @__PURE__ */ jsx(CodeEditorContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
|
|
1617
1637
|
"div",
|
|
@@ -1634,105 +1654,6 @@ var CodeEditor = Object.assign(CodeEditorRoot, {
|
|
|
1634
1654
|
Panel: CodeEditorPanel,
|
|
1635
1655
|
StatusBar: CodeEditorStatusBar
|
|
1636
1656
|
});
|
|
1637
|
-
|
|
1638
|
-
// src/engine/markdown-render.ts
|
|
1639
|
-
function escapeHtml(s) {
|
|
1640
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1641
|
-
}
|
|
1642
|
-
function escapeAttr(s) {
|
|
1643
|
-
return s.replace(/"/g, """).replace(/'/g, "'");
|
|
1644
|
-
}
|
|
1645
|
-
function inline(text) {
|
|
1646
|
-
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>");
|
|
1647
|
-
}
|
|
1648
|
-
function renderMarkdown(markdown) {
|
|
1649
|
-
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
1650
|
-
const out = [];
|
|
1651
|
-
let i = 0;
|
|
1652
|
-
let para = [];
|
|
1653
|
-
let listType = null;
|
|
1654
|
-
const flushPara = () => {
|
|
1655
|
-
if (para.length) {
|
|
1656
|
-
out.push(`<p>${inline(para.join(" "))}</p>`);
|
|
1657
|
-
para = [];
|
|
1658
|
-
}
|
|
1659
|
-
};
|
|
1660
|
-
const closeList = () => {
|
|
1661
|
-
if (listType) {
|
|
1662
|
-
out.push(`</${listType}>`);
|
|
1663
|
-
listType = null;
|
|
1664
|
-
}
|
|
1665
|
-
};
|
|
1666
|
-
while (i < lines.length) {
|
|
1667
|
-
const line = lines[i];
|
|
1668
|
-
const fence = line.match(/^\s*(```|~~~)(.*)$/);
|
|
1669
|
-
if (fence) {
|
|
1670
|
-
flushPara();
|
|
1671
|
-
closeList();
|
|
1672
|
-
const lang = fence[2].trim();
|
|
1673
|
-
const body = [];
|
|
1674
|
-
i++;
|
|
1675
|
-
while (i < lines.length && !/^\s*(```|~~~)/.test(lines[i])) {
|
|
1676
|
-
body.push(lines[i]);
|
|
1677
|
-
i++;
|
|
1678
|
-
}
|
|
1679
|
-
i++;
|
|
1680
|
-
out.push(`<pre><code${lang ? ` class="language-${escapeAttr(escapeHtml(lang))}"` : ""}>${escapeHtml(body.join("\n"))}</code></pre>`);
|
|
1681
|
-
continue;
|
|
1682
|
-
}
|
|
1683
|
-
const esc = escapeHtml(line);
|
|
1684
|
-
if (/^\s*$/.test(line)) {
|
|
1685
|
-
flushPara();
|
|
1686
|
-
closeList();
|
|
1687
|
-
i++;
|
|
1688
|
-
continue;
|
|
1689
|
-
}
|
|
1690
|
-
const heading = esc.match(/^(#{1,6})\s+(.*)$/);
|
|
1691
|
-
if (heading) {
|
|
1692
|
-
flushPara();
|
|
1693
|
-
closeList();
|
|
1694
|
-
const level = heading[1].length;
|
|
1695
|
-
out.push(`<h${level}>${inline(heading[2])}</h${level}>`);
|
|
1696
|
-
i++;
|
|
1697
|
-
continue;
|
|
1698
|
-
}
|
|
1699
|
-
if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
|
|
1700
|
-
flushPara();
|
|
1701
|
-
closeList();
|
|
1702
|
-
out.push("<hr>");
|
|
1703
|
-
i++;
|
|
1704
|
-
continue;
|
|
1705
|
-
}
|
|
1706
|
-
const quote = esc.match(/^\s*>\s?(.*)$/);
|
|
1707
|
-
if (quote) {
|
|
1708
|
-
flushPara();
|
|
1709
|
-
closeList();
|
|
1710
|
-
out.push(`<blockquote>${inline(quote[1])}</blockquote>`);
|
|
1711
|
-
i++;
|
|
1712
|
-
continue;
|
|
1713
|
-
}
|
|
1714
|
-
const ol = esc.match(/^\s*\d+\.\s+(.*)$/);
|
|
1715
|
-
const ul = esc.match(/^\s*[-*+]\s+(.*)$/);
|
|
1716
|
-
if (ol || ul) {
|
|
1717
|
-
flushPara();
|
|
1718
|
-
const type = ol ? "ol" : "ul";
|
|
1719
|
-
if (listType !== type) {
|
|
1720
|
-
closeList();
|
|
1721
|
-
out.push(`<${type}>`);
|
|
1722
|
-
listType = type;
|
|
1723
|
-
}
|
|
1724
|
-
out.push(`<li>${inline((ol ?? ul)[1])}</li>`);
|
|
1725
|
-
i++;
|
|
1726
|
-
continue;
|
|
1727
|
-
}
|
|
1728
|
-
closeList();
|
|
1729
|
-
para.push(esc);
|
|
1730
|
-
i++;
|
|
1731
|
-
}
|
|
1732
|
-
flushPara();
|
|
1733
|
-
closeList();
|
|
1734
|
-
return out.join("\n");
|
|
1735
|
-
}
|
|
1736
1657
|
function MarkdownEditor({
|
|
1737
1658
|
value,
|
|
1738
1659
|
defaultValue = "",
|
|
@@ -1746,7 +1667,7 @@ function MarkdownEditor({
|
|
|
1746
1667
|
minHeight,
|
|
1747
1668
|
maxHeight,
|
|
1748
1669
|
className,
|
|
1749
|
-
renderPreview
|
|
1670
|
+
renderPreview
|
|
1750
1671
|
}) {
|
|
1751
1672
|
const [content, setContent] = useControllableState(value, defaultValue, onValueChange);
|
|
1752
1673
|
const showEditor = mode !== "preview";
|
|
@@ -1779,7 +1700,19 @@ function MarkdownEditor({
|
|
|
1779
1700
|
{
|
|
1780
1701
|
"data-fancy-markdown-preview": "",
|
|
1781
1702
|
className: "fancy-md-preview",
|
|
1782
|
-
|
|
1703
|
+
style: maxHeight ? { maxHeight, overflowY: "auto" } : void 0,
|
|
1704
|
+
children: renderPreview ? (
|
|
1705
|
+
// The host imposed its own pipeline; its HTML is injected as-is and
|
|
1706
|
+
// is its own responsibility to sanitise.
|
|
1707
|
+
/* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderPreview(content) } })
|
|
1708
|
+
) : (
|
|
1709
|
+
// The kit's own renderer — a full CommonMark + GFM parse that
|
|
1710
|
+
// sanitises by default. `marked` already sits in this tree as a
|
|
1711
|
+
// react-fancy runtime dependency, so the built-in renderer this
|
|
1712
|
+
// replaced was a second, weaker markdown implementation next to a
|
|
1713
|
+
// complete one.
|
|
1714
|
+
/* @__PURE__ */ jsx(ContentRenderer, { value: content, format: "markdown" })
|
|
1715
|
+
)
|
|
1783
1716
|
}
|
|
1784
1717
|
)
|
|
1785
1718
|
]
|
|
@@ -1803,6 +1736,9 @@ function FileViewer({
|
|
|
1803
1736
|
fileKind,
|
|
1804
1737
|
lineNumbers = true,
|
|
1805
1738
|
wordWrap = false,
|
|
1739
|
+
line,
|
|
1740
|
+
column,
|
|
1741
|
+
highlightLines,
|
|
1806
1742
|
minHeight,
|
|
1807
1743
|
maxHeight,
|
|
1808
1744
|
mediaProps,
|
|
@@ -1854,6 +1790,9 @@ function FileViewer({
|
|
|
1854
1790
|
wordWrap,
|
|
1855
1791
|
minHeight,
|
|
1856
1792
|
maxHeight,
|
|
1793
|
+
cursorLine: line,
|
|
1794
|
+
cursorColumn: column,
|
|
1795
|
+
highlightLines: highlightLines ?? (line != null ? [line] : void 0),
|
|
1857
1796
|
children: /* @__PURE__ */ jsx(CodeEditor.Panel, {})
|
|
1858
1797
|
}
|
|
1859
1798
|
)
|
|
@@ -1861,6 +1800,105 @@ function FileViewer({
|
|
|
1861
1800
|
);
|
|
1862
1801
|
}
|
|
1863
1802
|
FileViewer.displayName = "FileViewer";
|
|
1803
|
+
|
|
1804
|
+
// src/engine/markdown-render.ts
|
|
1805
|
+
function escapeHtml(s) {
|
|
1806
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1807
|
+
}
|
|
1808
|
+
function escapeAttr(s) {
|
|
1809
|
+
return s.replace(/"/g, """).replace(/'/g, "'");
|
|
1810
|
+
}
|
|
1811
|
+
function inline(text) {
|
|
1812
|
+
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>");
|
|
1813
|
+
}
|
|
1814
|
+
function renderMarkdown(markdown) {
|
|
1815
|
+
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|
|
1816
|
+
const out = [];
|
|
1817
|
+
let i = 0;
|
|
1818
|
+
let para = [];
|
|
1819
|
+
let listType = null;
|
|
1820
|
+
const flushPara = () => {
|
|
1821
|
+
if (para.length) {
|
|
1822
|
+
out.push(`<p>${inline(para.join(" "))}</p>`);
|
|
1823
|
+
para = [];
|
|
1824
|
+
}
|
|
1825
|
+
};
|
|
1826
|
+
const closeList = () => {
|
|
1827
|
+
if (listType) {
|
|
1828
|
+
out.push(`</${listType}>`);
|
|
1829
|
+
listType = null;
|
|
1830
|
+
}
|
|
1831
|
+
};
|
|
1832
|
+
while (i < lines.length) {
|
|
1833
|
+
const line = lines[i];
|
|
1834
|
+
const fence = line.match(/^\s*(```|~~~)(.*)$/);
|
|
1835
|
+
if (fence) {
|
|
1836
|
+
flushPara();
|
|
1837
|
+
closeList();
|
|
1838
|
+
const lang = fence[2].trim();
|
|
1839
|
+
const body = [];
|
|
1840
|
+
i++;
|
|
1841
|
+
while (i < lines.length && !/^\s*(```|~~~)/.test(lines[i])) {
|
|
1842
|
+
body.push(lines[i]);
|
|
1843
|
+
i++;
|
|
1844
|
+
}
|
|
1845
|
+
i++;
|
|
1846
|
+
out.push(`<pre><code${lang ? ` class="language-${escapeAttr(escapeHtml(lang))}"` : ""}>${escapeHtml(body.join("\n"))}</code></pre>`);
|
|
1847
|
+
continue;
|
|
1848
|
+
}
|
|
1849
|
+
const esc = escapeHtml(line);
|
|
1850
|
+
if (/^\s*$/.test(line)) {
|
|
1851
|
+
flushPara();
|
|
1852
|
+
closeList();
|
|
1853
|
+
i++;
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
const heading = esc.match(/^(#{1,6})\s+(.*)$/);
|
|
1857
|
+
if (heading) {
|
|
1858
|
+
flushPara();
|
|
1859
|
+
closeList();
|
|
1860
|
+
const level = heading[1].length;
|
|
1861
|
+
out.push(`<h${level}>${inline(heading[2])}</h${level}>`);
|
|
1862
|
+
i++;
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
|
|
1866
|
+
flushPara();
|
|
1867
|
+
closeList();
|
|
1868
|
+
out.push("<hr>");
|
|
1869
|
+
i++;
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
const quote = esc.match(/^\s*>\s?(.*)$/);
|
|
1873
|
+
if (quote) {
|
|
1874
|
+
flushPara();
|
|
1875
|
+
closeList();
|
|
1876
|
+
out.push(`<blockquote>${inline(quote[1])}</blockquote>`);
|
|
1877
|
+
i++;
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
const ol = esc.match(/^\s*\d+\.\s+(.*)$/);
|
|
1881
|
+
const ul = esc.match(/^\s*[-*+]\s+(.*)$/);
|
|
1882
|
+
if (ol || ul) {
|
|
1883
|
+
flushPara();
|
|
1884
|
+
const type = ol ? "ol" : "ul";
|
|
1885
|
+
if (listType !== type) {
|
|
1886
|
+
closeList();
|
|
1887
|
+
out.push(`<${type}>`);
|
|
1888
|
+
listType = type;
|
|
1889
|
+
}
|
|
1890
|
+
out.push(`<li>${inline((ol ?? ul)[1])}</li>`);
|
|
1891
|
+
i++;
|
|
1892
|
+
continue;
|
|
1893
|
+
}
|
|
1894
|
+
closeList();
|
|
1895
|
+
para.push(esc);
|
|
1896
|
+
i++;
|
|
1897
|
+
}
|
|
1898
|
+
flushPara();
|
|
1899
|
+
closeList();
|
|
1900
|
+
return out.join("\n");
|
|
1901
|
+
}
|
|
1864
1902
|
function useCodeEditorInputTagAdapter() {
|
|
1865
1903
|
const ctx = useCodeEditor();
|
|
1866
1904
|
return useMemo(() => textareaAdapter(ctx.textareaRef), [ctx.textareaRef]);
|