@particle-academy/fancy-code 0.11.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 +145 -0
- package/LICENSE +21 -0
- package/README.md +30 -1
- package/dist/index.cjs +53 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -2
- package/dist/index.d.ts +23 -2
- package/dist/index.js +53 -8
- package/dist/index.js.map +1 -1
- package/package.json +8 -4
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
|
}
|
|
@@ -328,6 +336,19 @@ interface FileViewerProps {
|
|
|
328
336
|
lineNumbers?: boolean;
|
|
329
337
|
/** Wrap long lines in the text editor (default `false`). */
|
|
330
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[];
|
|
331
352
|
/** Editor height bounds (px). */
|
|
332
353
|
minHeight?: number;
|
|
333
354
|
maxHeight?: number;
|
|
@@ -353,7 +374,7 @@ interface FileViewerProps {
|
|
|
353
374
|
* <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
|
|
354
375
|
* ```
|
|
355
376
|
*/
|
|
356
|
-
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;
|
|
357
378
|
declare namespace FileViewer {
|
|
358
379
|
var displayName: string;
|
|
359
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
|
}
|
|
@@ -328,6 +336,19 @@ interface FileViewerProps {
|
|
|
328
336
|
lineNumbers?: boolean;
|
|
329
337
|
/** Wrap long lines in the text editor (default `false`). */
|
|
330
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[];
|
|
331
352
|
/** Editor height bounds (px). */
|
|
332
353
|
minHeight?: number;
|
|
333
354
|
maxHeight?: number;
|
|
@@ -353,7 +374,7 @@ interface FileViewerProps {
|
|
|
353
374
|
* <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
|
|
354
375
|
* ```
|
|
355
376
|
*/
|
|
356
|
-
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;
|
|
357
378
|
declare namespace FileViewer {
|
|
358
379
|
var displayName: string;
|
|
359
380
|
}
|
package/dist/index.js
CHANGED
|
@@ -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",
|
|
@@ -1716,6 +1736,9 @@ function FileViewer({
|
|
|
1716
1736
|
fileKind,
|
|
1717
1737
|
lineNumbers = true,
|
|
1718
1738
|
wordWrap = false,
|
|
1739
|
+
line,
|
|
1740
|
+
column,
|
|
1741
|
+
highlightLines,
|
|
1719
1742
|
minHeight,
|
|
1720
1743
|
maxHeight,
|
|
1721
1744
|
mediaProps,
|
|
@@ -1767,6 +1790,9 @@ function FileViewer({
|
|
|
1767
1790
|
wordWrap,
|
|
1768
1791
|
minHeight,
|
|
1769
1792
|
maxHeight,
|
|
1793
|
+
cursorLine: line,
|
|
1794
|
+
cursorColumn: column,
|
|
1795
|
+
highlightLines: highlightLines ?? (line != null ? [line] : void 0),
|
|
1770
1796
|
children: /* @__PURE__ */ jsx(CodeEditor.Panel, {})
|
|
1771
1797
|
}
|
|
1772
1798
|
)
|
|
@@ -1782,8 +1808,27 @@ function escapeHtml(s) {
|
|
|
1782
1808
|
function escapeAttr(s) {
|
|
1783
1809
|
return s.replace(/"/g, """).replace(/'/g, "'");
|
|
1784
1810
|
}
|
|
1811
|
+
var LINK_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
|
|
1812
|
+
function schemeOf(url) {
|
|
1813
|
+
let s = "";
|
|
1814
|
+
for (const ch of url) {
|
|
1815
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
1816
|
+
if (code === 9 || code === 10 || code === 13) continue;
|
|
1817
|
+
if (s === "" && code <= 32) continue;
|
|
1818
|
+
s += ch;
|
|
1819
|
+
}
|
|
1820
|
+
const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(s);
|
|
1821
|
+
return m ? m[1].toLowerCase() : null;
|
|
1822
|
+
}
|
|
1823
|
+
function safeUrl(url, kind) {
|
|
1824
|
+
const scheme = schemeOf(url);
|
|
1825
|
+
if (scheme === null) return true;
|
|
1826
|
+
if (scheme === "http" || scheme === "https") return true;
|
|
1827
|
+
if (kind === "link") return LINK_SCHEMES.has(scheme);
|
|
1828
|
+
return scheme === "data" && /^data:image\/(png|jpe?g|gif|webp|avif|bmp);/i.test(url.trim());
|
|
1829
|
+
}
|
|
1785
1830
|
function inline(text) {
|
|
1786
|
-
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>");
|
|
1831
|
+
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>");
|
|
1787
1832
|
}
|
|
1788
1833
|
function renderMarkdown(markdown) {
|
|
1789
1834
|
const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
|