@particle-academy/fancy-code 0.5.1 → 0.7.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/README.md +28 -1
- package/dist/index.cjs +161 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +131 -4
- package/dist/index.d.ts +131 -4
- package/dist/index.js +161 -4
- package/dist/index.js.map +1 -1
- package/docs/FileViewer.md +78 -0
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { RefObject, ReactNode } from 'react';
|
|
3
|
-
import { InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
|
|
2
|
+
import { RefObject, ReactNode, CSSProperties } from 'react';
|
|
3
|
+
import { MediaKind, MediaViewerProps, InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
|
|
4
4
|
|
|
5
5
|
interface ThemeColors {
|
|
6
6
|
background: string;
|
|
@@ -43,6 +43,13 @@ interface UseEditorEngineReturn {
|
|
|
43
43
|
handleInput: () => void;
|
|
44
44
|
handleScroll: () => void;
|
|
45
45
|
handleSelect: () => void;
|
|
46
|
+
/**
|
|
47
|
+
* Place the caret on a 1-based `line` / `column` and scroll the editor so the
|
|
48
|
+
* line is centered in view. Out-of-range positions clamp to document bounds.
|
|
49
|
+
*/
|
|
50
|
+
revealLine: (line: number, column?: number, opts?: {
|
|
51
|
+
focus?: boolean;
|
|
52
|
+
}) => void;
|
|
46
53
|
scrollTop: number;
|
|
47
54
|
scrollLeft: number;
|
|
48
55
|
}
|
|
@@ -76,6 +83,16 @@ interface CodeEditorProps {
|
|
|
76
83
|
minHeight?: number;
|
|
77
84
|
/** Maximum height in px (scrolls beyond this) */
|
|
78
85
|
maxHeight?: number;
|
|
86
|
+
/**
|
|
87
|
+
* 1-based line to reveal (scroll to + place the caret on). Applied on mount
|
|
88
|
+
* and whenever it (or `cursorColumn`) changes — so opening a file *at* a line,
|
|
89
|
+
* or re-targeting an already-open file, scrolls the line into view. Does not
|
|
90
|
+
* steal focus; call the imperative `revealLine`/`setCursor` (or `focus`) for
|
|
91
|
+
* that. Out-of-range values clamp to document bounds.
|
|
92
|
+
*/
|
|
93
|
+
cursorLine?: number;
|
|
94
|
+
/** 1-based column for {@link cursorLine} (default 1). */
|
|
95
|
+
cursorColumn?: number;
|
|
79
96
|
}
|
|
80
97
|
interface CodeEditorContextValue {
|
|
81
98
|
/** Get the current document text */
|
|
@@ -88,6 +105,16 @@ interface CodeEditorContextValue {
|
|
|
88
105
|
replaceSelection: (text: string) => void;
|
|
89
106
|
/** Focus the editor */
|
|
90
107
|
focus: () => void;
|
|
108
|
+
/**
|
|
109
|
+
* Scroll to + place the caret on a 1-based `line` (optional 1-based `column`),
|
|
110
|
+
* focusing the editor. Out-of-range positions clamp to document bounds.
|
|
111
|
+
*/
|
|
112
|
+
revealLine: (line: number, column?: number) => void;
|
|
113
|
+
/** Place the caret at a 1-based `{ line, column }` and reveal it (focuses). */
|
|
114
|
+
setCursor: (pos: {
|
|
115
|
+
line: number;
|
|
116
|
+
column?: number;
|
|
117
|
+
}) => void;
|
|
91
118
|
/** Current language name */
|
|
92
119
|
language: string;
|
|
93
120
|
/** Change the active language */
|
|
@@ -163,7 +190,7 @@ declare namespace CodeEditorStatusBar {
|
|
|
163
190
|
var displayName: string;
|
|
164
191
|
}
|
|
165
192
|
|
|
166
|
-
declare function CodeEditorRoot({ children, className, value: valueProp, defaultValue, onChange, language: languageProp, onLanguageChange, theme, readOnly, lineNumbers: lineNumbersProp, wordWrap: wordWrapProp, tabSize: tabSizeProp, placeholder, minHeight, maxHeight, }: CodeEditorProps): react.JSX.Element;
|
|
193
|
+
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, }: CodeEditorProps): react.JSX.Element;
|
|
167
194
|
declare namespace CodeEditorRoot {
|
|
168
195
|
var displayName: string;
|
|
169
196
|
}
|
|
@@ -225,6 +252,106 @@ declare namespace MarkdownEditor {
|
|
|
225
252
|
var displayName: string;
|
|
226
253
|
}
|
|
227
254
|
|
|
255
|
+
/**
|
|
256
|
+
* The decision a file viewer makes about a file: render it as editable text, or
|
|
257
|
+
* hand it to a media viewer. `media` carries the resolved {@link MediaKind};
|
|
258
|
+
* `text` carries the editor language id.
|
|
259
|
+
*/
|
|
260
|
+
type FileKind = {
|
|
261
|
+
kind: "text";
|
|
262
|
+
language: string;
|
|
263
|
+
} | {
|
|
264
|
+
kind: "media";
|
|
265
|
+
mediaKind: MediaKind;
|
|
266
|
+
};
|
|
267
|
+
interface ResolveFileKindInput {
|
|
268
|
+
/** File name or path — sniffed for an extension (and used for the media check). */
|
|
269
|
+
filename?: string;
|
|
270
|
+
/** MIME type — preferred over the filename when known. */
|
|
271
|
+
mime?: string;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Resolve the fancy-code editor language for a filename. Returns `"plaintext"`
|
|
275
|
+
* (no syntax highlighting) for unknown or extension-less files.
|
|
276
|
+
*/
|
|
277
|
+
declare function languageFromFilename(filename?: string): string;
|
|
278
|
+
/**
|
|
279
|
+
* Decide whether a file is previewable media (image / video / audio / PDF) or
|
|
280
|
+
* text, from its MIME type (preferred) and/or filename. Media wins when the
|
|
281
|
+
* type resolves to one; otherwise it's text and the editor language is derived
|
|
282
|
+
* from the extension.
|
|
283
|
+
*
|
|
284
|
+
* Exposed so consumers can branch their own chrome (tabs, save buttons) the
|
|
285
|
+
* same way `<FileViewer>` does internally.
|
|
286
|
+
*
|
|
287
|
+
* ```ts
|
|
288
|
+
* resolveFileKind({ filename: "logo.png" }); // { kind: "media", mediaKind: "image" }
|
|
289
|
+
* resolveFileKind({ filename: "app.tsx" }); // { kind: "text", language: "typescript" }
|
|
290
|
+
* resolveFileKind({ mime: "application/pdf" }); // { kind: "media", mediaKind: "pdf" }
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
declare function resolveFileKind({ filename, mime }: ResolveFileKindInput): FileKind;
|
|
294
|
+
|
|
295
|
+
interface FileViewerProps {
|
|
296
|
+
/**
|
|
297
|
+
* File name or path — used to detect the file kind / editor language, and as
|
|
298
|
+
* a label / alt text. Pass `mime` too when you have it (it's preferred).
|
|
299
|
+
*/
|
|
300
|
+
filename?: string;
|
|
301
|
+
/** MIME type — preferred over `filename` for detection. */
|
|
302
|
+
mime?: string;
|
|
303
|
+
/**
|
|
304
|
+
* Source URL for **media** files (image / video / audio / PDF) —
|
|
305
|
+
* `http(s):` / `data:` / `blob:`. Required when the file resolves to media.
|
|
306
|
+
*/
|
|
307
|
+
src?: string;
|
|
308
|
+
/** Text content for **text** files. */
|
|
309
|
+
value?: string;
|
|
310
|
+
/** Called when text content changes (ignored when `readOnly`). */
|
|
311
|
+
onChange?: (value: string) => void;
|
|
312
|
+
/**
|
|
313
|
+
* Read-only. Defaults to `true` — `FileViewer` is a viewer. Media is always
|
|
314
|
+
* read-only regardless. Set `false` (with `onChange`) to edit text.
|
|
315
|
+
*/
|
|
316
|
+
readOnly?: boolean;
|
|
317
|
+
/** Editor theme for text: `"light" | "dark" | "auto"` or a registered name (default `"auto"`). */
|
|
318
|
+
theme?: string;
|
|
319
|
+
/** Force the file kind, bypassing detection. */
|
|
320
|
+
fileKind?: FileKind;
|
|
321
|
+
/** Show line numbers in the text editor (default `true`). */
|
|
322
|
+
lineNumbers?: boolean;
|
|
323
|
+
/** Wrap long lines in the text editor (default `false`). */
|
|
324
|
+
wordWrap?: boolean;
|
|
325
|
+
/** Editor height bounds (px). */
|
|
326
|
+
minHeight?: number;
|
|
327
|
+
maxHeight?: number;
|
|
328
|
+
/** Extra props forwarded to `<MediaViewer>` for media files. */
|
|
329
|
+
mediaProps?: Partial<Omit<MediaViewerProps, "src" | "mime" | "alt">>;
|
|
330
|
+
className?: string;
|
|
331
|
+
style?: CSSProperties;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* A unified file viewer: renders text files in a read-only `CodeEditor` (with
|
|
336
|
+
* syntax highlighting picked from the filename) and media files — image, video,
|
|
337
|
+
* audio, PDF — via react-fancy's `<MediaViewer>`. The text-vs-media decision is
|
|
338
|
+
* made by {@link resolveFileKind}; pass `fileKind` to override it, or call
|
|
339
|
+
* `resolveFileKind` yourself to branch your own chrome (tabs, save buttons).
|
|
340
|
+
*
|
|
341
|
+
* Give the container a height for media files (PDF/image/video fill their box).
|
|
342
|
+
*
|
|
343
|
+
* ```tsx
|
|
344
|
+
* // text
|
|
345
|
+
* <FileViewer filename="app.tsx" value={source} />
|
|
346
|
+
* // media
|
|
347
|
+
* <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, minHeight, maxHeight, mediaProps, className, style, }: FileViewerProps): react.JSX.Element;
|
|
351
|
+
declare namespace FileViewer {
|
|
352
|
+
var displayName: string;
|
|
353
|
+
}
|
|
354
|
+
|
|
228
355
|
/**
|
|
229
356
|
* Tiny, dependency-free Markdown → HTML renderer for the MarkdownEditor preview
|
|
230
357
|
* pane. Covers the common subset — headings, bold/italic, inline + fenced code,
|
|
@@ -309,4 +436,4 @@ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
|
|
|
309
436
|
*/
|
|
310
437
|
declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
|
|
311
438
|
|
|
312
|
-
export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, renderMarkdown, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
|
|
439
|
+
export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type FileKind, FileViewer, type FileViewerProps, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ResolveFileKindInput, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, languageFromFilename, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
|
-
import { RefObject, ReactNode } from 'react';
|
|
3
|
-
import { InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
|
|
2
|
+
import { RefObject, ReactNode, CSSProperties } from 'react';
|
|
3
|
+
import { MediaKind, MediaViewerProps, InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
|
|
4
4
|
|
|
5
5
|
interface ThemeColors {
|
|
6
6
|
background: string;
|
|
@@ -43,6 +43,13 @@ interface UseEditorEngineReturn {
|
|
|
43
43
|
handleInput: () => void;
|
|
44
44
|
handleScroll: () => void;
|
|
45
45
|
handleSelect: () => void;
|
|
46
|
+
/**
|
|
47
|
+
* Place the caret on a 1-based `line` / `column` and scroll the editor so the
|
|
48
|
+
* line is centered in view. Out-of-range positions clamp to document bounds.
|
|
49
|
+
*/
|
|
50
|
+
revealLine: (line: number, column?: number, opts?: {
|
|
51
|
+
focus?: boolean;
|
|
52
|
+
}) => void;
|
|
46
53
|
scrollTop: number;
|
|
47
54
|
scrollLeft: number;
|
|
48
55
|
}
|
|
@@ -76,6 +83,16 @@ interface CodeEditorProps {
|
|
|
76
83
|
minHeight?: number;
|
|
77
84
|
/** Maximum height in px (scrolls beyond this) */
|
|
78
85
|
maxHeight?: number;
|
|
86
|
+
/**
|
|
87
|
+
* 1-based line to reveal (scroll to + place the caret on). Applied on mount
|
|
88
|
+
* and whenever it (or `cursorColumn`) changes — so opening a file *at* a line,
|
|
89
|
+
* or re-targeting an already-open file, scrolls the line into view. Does not
|
|
90
|
+
* steal focus; call the imperative `revealLine`/`setCursor` (or `focus`) for
|
|
91
|
+
* that. Out-of-range values clamp to document bounds.
|
|
92
|
+
*/
|
|
93
|
+
cursorLine?: number;
|
|
94
|
+
/** 1-based column for {@link cursorLine} (default 1). */
|
|
95
|
+
cursorColumn?: number;
|
|
79
96
|
}
|
|
80
97
|
interface CodeEditorContextValue {
|
|
81
98
|
/** Get the current document text */
|
|
@@ -88,6 +105,16 @@ interface CodeEditorContextValue {
|
|
|
88
105
|
replaceSelection: (text: string) => void;
|
|
89
106
|
/** Focus the editor */
|
|
90
107
|
focus: () => void;
|
|
108
|
+
/**
|
|
109
|
+
* Scroll to + place the caret on a 1-based `line` (optional 1-based `column`),
|
|
110
|
+
* focusing the editor. Out-of-range positions clamp to document bounds.
|
|
111
|
+
*/
|
|
112
|
+
revealLine: (line: number, column?: number) => void;
|
|
113
|
+
/** Place the caret at a 1-based `{ line, column }` and reveal it (focuses). */
|
|
114
|
+
setCursor: (pos: {
|
|
115
|
+
line: number;
|
|
116
|
+
column?: number;
|
|
117
|
+
}) => void;
|
|
91
118
|
/** Current language name */
|
|
92
119
|
language: string;
|
|
93
120
|
/** Change the active language */
|
|
@@ -163,7 +190,7 @@ declare namespace CodeEditorStatusBar {
|
|
|
163
190
|
var displayName: string;
|
|
164
191
|
}
|
|
165
192
|
|
|
166
|
-
declare function CodeEditorRoot({ children, className, value: valueProp, defaultValue, onChange, language: languageProp, onLanguageChange, theme, readOnly, lineNumbers: lineNumbersProp, wordWrap: wordWrapProp, tabSize: tabSizeProp, placeholder, minHeight, maxHeight, }: CodeEditorProps): react.JSX.Element;
|
|
193
|
+
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, }: CodeEditorProps): react.JSX.Element;
|
|
167
194
|
declare namespace CodeEditorRoot {
|
|
168
195
|
var displayName: string;
|
|
169
196
|
}
|
|
@@ -225,6 +252,106 @@ declare namespace MarkdownEditor {
|
|
|
225
252
|
var displayName: string;
|
|
226
253
|
}
|
|
227
254
|
|
|
255
|
+
/**
|
|
256
|
+
* The decision a file viewer makes about a file: render it as editable text, or
|
|
257
|
+
* hand it to a media viewer. `media` carries the resolved {@link MediaKind};
|
|
258
|
+
* `text` carries the editor language id.
|
|
259
|
+
*/
|
|
260
|
+
type FileKind = {
|
|
261
|
+
kind: "text";
|
|
262
|
+
language: string;
|
|
263
|
+
} | {
|
|
264
|
+
kind: "media";
|
|
265
|
+
mediaKind: MediaKind;
|
|
266
|
+
};
|
|
267
|
+
interface ResolveFileKindInput {
|
|
268
|
+
/** File name or path — sniffed for an extension (and used for the media check). */
|
|
269
|
+
filename?: string;
|
|
270
|
+
/** MIME type — preferred over the filename when known. */
|
|
271
|
+
mime?: string;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Resolve the fancy-code editor language for a filename. Returns `"plaintext"`
|
|
275
|
+
* (no syntax highlighting) for unknown or extension-less files.
|
|
276
|
+
*/
|
|
277
|
+
declare function languageFromFilename(filename?: string): string;
|
|
278
|
+
/**
|
|
279
|
+
* Decide whether a file is previewable media (image / video / audio / PDF) or
|
|
280
|
+
* text, from its MIME type (preferred) and/or filename. Media wins when the
|
|
281
|
+
* type resolves to one; otherwise it's text and the editor language is derived
|
|
282
|
+
* from the extension.
|
|
283
|
+
*
|
|
284
|
+
* Exposed so consumers can branch their own chrome (tabs, save buttons) the
|
|
285
|
+
* same way `<FileViewer>` does internally.
|
|
286
|
+
*
|
|
287
|
+
* ```ts
|
|
288
|
+
* resolveFileKind({ filename: "logo.png" }); // { kind: "media", mediaKind: "image" }
|
|
289
|
+
* resolveFileKind({ filename: "app.tsx" }); // { kind: "text", language: "typescript" }
|
|
290
|
+
* resolveFileKind({ mime: "application/pdf" }); // { kind: "media", mediaKind: "pdf" }
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
declare function resolveFileKind({ filename, mime }: ResolveFileKindInput): FileKind;
|
|
294
|
+
|
|
295
|
+
interface FileViewerProps {
|
|
296
|
+
/**
|
|
297
|
+
* File name or path — used to detect the file kind / editor language, and as
|
|
298
|
+
* a label / alt text. Pass `mime` too when you have it (it's preferred).
|
|
299
|
+
*/
|
|
300
|
+
filename?: string;
|
|
301
|
+
/** MIME type — preferred over `filename` for detection. */
|
|
302
|
+
mime?: string;
|
|
303
|
+
/**
|
|
304
|
+
* Source URL for **media** files (image / video / audio / PDF) —
|
|
305
|
+
* `http(s):` / `data:` / `blob:`. Required when the file resolves to media.
|
|
306
|
+
*/
|
|
307
|
+
src?: string;
|
|
308
|
+
/** Text content for **text** files. */
|
|
309
|
+
value?: string;
|
|
310
|
+
/** Called when text content changes (ignored when `readOnly`). */
|
|
311
|
+
onChange?: (value: string) => void;
|
|
312
|
+
/**
|
|
313
|
+
* Read-only. Defaults to `true` — `FileViewer` is a viewer. Media is always
|
|
314
|
+
* read-only regardless. Set `false` (with `onChange`) to edit text.
|
|
315
|
+
*/
|
|
316
|
+
readOnly?: boolean;
|
|
317
|
+
/** Editor theme for text: `"light" | "dark" | "auto"` or a registered name (default `"auto"`). */
|
|
318
|
+
theme?: string;
|
|
319
|
+
/** Force the file kind, bypassing detection. */
|
|
320
|
+
fileKind?: FileKind;
|
|
321
|
+
/** Show line numbers in the text editor (default `true`). */
|
|
322
|
+
lineNumbers?: boolean;
|
|
323
|
+
/** Wrap long lines in the text editor (default `false`). */
|
|
324
|
+
wordWrap?: boolean;
|
|
325
|
+
/** Editor height bounds (px). */
|
|
326
|
+
minHeight?: number;
|
|
327
|
+
maxHeight?: number;
|
|
328
|
+
/** Extra props forwarded to `<MediaViewer>` for media files. */
|
|
329
|
+
mediaProps?: Partial<Omit<MediaViewerProps, "src" | "mime" | "alt">>;
|
|
330
|
+
className?: string;
|
|
331
|
+
style?: CSSProperties;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* A unified file viewer: renders text files in a read-only `CodeEditor` (with
|
|
336
|
+
* syntax highlighting picked from the filename) and media files — image, video,
|
|
337
|
+
* audio, PDF — via react-fancy's `<MediaViewer>`. The text-vs-media decision is
|
|
338
|
+
* made by {@link resolveFileKind}; pass `fileKind` to override it, or call
|
|
339
|
+
* `resolveFileKind` yourself to branch your own chrome (tabs, save buttons).
|
|
340
|
+
*
|
|
341
|
+
* Give the container a height for media files (PDF/image/video fill their box).
|
|
342
|
+
*
|
|
343
|
+
* ```tsx
|
|
344
|
+
* // text
|
|
345
|
+
* <FileViewer filename="app.tsx" value={source} />
|
|
346
|
+
* // media
|
|
347
|
+
* <FileViewer filename="logo.png" src={url} style={{ height: 360 }} />
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
declare function FileViewer({ filename, mime, src, value, onChange, readOnly, theme, fileKind, lineNumbers, wordWrap, minHeight, maxHeight, mediaProps, className, style, }: FileViewerProps): react.JSX.Element;
|
|
351
|
+
declare namespace FileViewer {
|
|
352
|
+
var displayName: string;
|
|
353
|
+
}
|
|
354
|
+
|
|
228
355
|
/**
|
|
229
356
|
* Tiny, dependency-free Markdown → HTML renderer for the MarkdownEditor preview
|
|
230
357
|
* pane. Covers the common subset — headings, bold/italic, inline + fenced code,
|
|
@@ -309,4 +436,4 @@ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
|
|
|
309
436
|
*/
|
|
310
437
|
declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
|
|
311
438
|
|
|
312
|
-
export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, renderMarkdown, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
|
|
439
|
+
export { CodeEditor, type CodeEditorContextValue, CodeEditorInputTag, type CodeEditorInputTagProps, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type FileKind, FileViewer, type FileViewerProps, type LanguageDefinition, MarkdownEditor, type MarkdownEditorMode, type MarkdownEditorProps, type ResolveFileKindInput, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, languageFromFilename, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createContext, useState, useCallback, useRef, useMemo, useSyncExternalStore,
|
|
2
|
-
import { cn, useControllableState, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
|
|
1
|
+
import { createContext, useState, useCallback, useRef, useEffect, useMemo, useSyncExternalStore, useContext } from 'react';
|
|
2
|
+
import { cn, useControllableState, resolveMediaType, MediaViewer, textareaAdapter, InputTag } from '@particle-academy/react-fancy';
|
|
3
3
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
4
|
|
|
5
5
|
// src/components/CodeEditor/CodeEditor.tsx
|
|
@@ -1344,6 +1344,19 @@ function highlightCode(source, tokens, colors) {
|
|
|
1344
1344
|
return parts.join("");
|
|
1345
1345
|
}
|
|
1346
1346
|
|
|
1347
|
+
// src/engine/position.ts
|
|
1348
|
+
function lineColumnToOffset(text, line, column = 1) {
|
|
1349
|
+
const lines = text.split("\n");
|
|
1350
|
+
const targetLine = Math.max(1, Math.min(Math.trunc(line) || 1, lines.length));
|
|
1351
|
+
let offset = 0;
|
|
1352
|
+
for (let i = 0; i < targetLine - 1; i++) {
|
|
1353
|
+
offset += lines[i].length + 1;
|
|
1354
|
+
}
|
|
1355
|
+
const lineText = lines[targetLine - 1] ?? "";
|
|
1356
|
+
const col = Math.max(1, Math.min(Math.trunc(column) || 1, lineText.length + 1));
|
|
1357
|
+
return offset + (col - 1);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1347
1360
|
// src/hooks/use-editor-engine.ts
|
|
1348
1361
|
var DEFAULT_COLORS = {
|
|
1349
1362
|
background: "#18181b",
|
|
@@ -1458,6 +1471,28 @@ function useEditorEngine({
|
|
|
1458
1471
|
setScrollTop(ta.scrollTop);
|
|
1459
1472
|
setScrollLeft(ta.scrollLeft);
|
|
1460
1473
|
}, []);
|
|
1474
|
+
const revealLine = useCallback(
|
|
1475
|
+
(line, column = 1, opts) => {
|
|
1476
|
+
const ta = textareaRef.current;
|
|
1477
|
+
if (!ta) return;
|
|
1478
|
+
const offset = lineColumnToOffset(ta.value, line, column);
|
|
1479
|
+
if (opts?.focus) ta.focus();
|
|
1480
|
+
ta.selectionStart = ta.selectionEnd = offset;
|
|
1481
|
+
updateCursorInfo();
|
|
1482
|
+
const panel = ta.closest("[data-fancy-code-panel]");
|
|
1483
|
+
if (!panel) return;
|
|
1484
|
+
const cs = getComputedStyle(ta);
|
|
1485
|
+
const lineHeight = parseFloat(cs.lineHeight) || 19.5;
|
|
1486
|
+
const paddingTop = parseFloat(cs.paddingTop) || 0;
|
|
1487
|
+
const lineIndex = (ta.value.slice(0, offset).match(/\n/g) || []).length;
|
|
1488
|
+
const textareaTopInPanel = ta.getBoundingClientRect().top - panel.getBoundingClientRect().top + panel.scrollTop;
|
|
1489
|
+
const lineTop = textareaTopInPanel + paddingTop + lineIndex * lineHeight;
|
|
1490
|
+
const target = lineTop - panel.clientHeight / 2 + lineHeight / 2;
|
|
1491
|
+
const maxScroll = panel.scrollHeight - panel.clientHeight;
|
|
1492
|
+
panel.scrollTop = Math.max(0, Math.min(target, maxScroll));
|
|
1493
|
+
},
|
|
1494
|
+
[updateCursorInfo]
|
|
1495
|
+
);
|
|
1461
1496
|
const handleKeyDown = useCallback(
|
|
1462
1497
|
(e) => {
|
|
1463
1498
|
if (readOnly) {
|
|
@@ -1544,6 +1579,7 @@ function useEditorEngine({
|
|
|
1544
1579
|
handleInput,
|
|
1545
1580
|
handleScroll,
|
|
1546
1581
|
handleSelect,
|
|
1582
|
+
revealLine,
|
|
1547
1583
|
scrollTop,
|
|
1548
1584
|
scrollLeft
|
|
1549
1585
|
};
|
|
@@ -1577,7 +1613,9 @@ function CodeEditorRoot({
|
|
|
1577
1613
|
tabSize: tabSizeProp = 2,
|
|
1578
1614
|
placeholder,
|
|
1579
1615
|
minHeight,
|
|
1580
|
-
maxHeight
|
|
1616
|
+
maxHeight,
|
|
1617
|
+
cursorLine,
|
|
1618
|
+
cursorColumn
|
|
1581
1619
|
}) {
|
|
1582
1620
|
const [currentValue, setCurrentValue] = useControllableState(valueProp, defaultValue, onChange);
|
|
1583
1621
|
const isDark = useDarkMode();
|
|
@@ -1613,6 +1651,11 @@ function CodeEditorRoot({
|
|
|
1613
1651
|
setSelectionLength(sel);
|
|
1614
1652
|
}
|
|
1615
1653
|
});
|
|
1654
|
+
const { revealLine: revealLineImpl } = engineReturn;
|
|
1655
|
+
useEffect(() => {
|
|
1656
|
+
if (cursorLine == null) return;
|
|
1657
|
+
revealLineImpl(cursorLine, cursorColumn ?? 1, { focus: false });
|
|
1658
|
+
}, [cursorLine, cursorColumn, revealLineImpl]);
|
|
1616
1659
|
const contextValue = useMemo(
|
|
1617
1660
|
() => ({
|
|
1618
1661
|
getValue: () => engineReturn.textareaRef.current?.value ?? currentValue,
|
|
@@ -1634,6 +1677,8 @@ function CodeEditorRoot({
|
|
|
1634
1677
|
setCurrentValue(ta.value);
|
|
1635
1678
|
},
|
|
1636
1679
|
focus: () => engineReturn.textareaRef.current?.focus(),
|
|
1680
|
+
revealLine: (line, column) => engineReturn.revealLine(line, column ?? 1, { focus: true }),
|
|
1681
|
+
setCursor: ({ line, column }) => engineReturn.revealLine(line, column ?? 1, { focus: true }),
|
|
1637
1682
|
language: currentLanguage,
|
|
1638
1683
|
setLanguage,
|
|
1639
1684
|
theme: resolvedTheme,
|
|
@@ -1831,6 +1876,118 @@ function MarkdownEditor({
|
|
|
1831
1876
|
);
|
|
1832
1877
|
}
|
|
1833
1878
|
MarkdownEditor.displayName = "MarkdownEditor";
|
|
1879
|
+
var EXT_LANGUAGE = {
|
|
1880
|
+
js: "javascript",
|
|
1881
|
+
mjs: "javascript",
|
|
1882
|
+
cjs: "javascript",
|
|
1883
|
+
jsx: "javascript",
|
|
1884
|
+
json: "javascript",
|
|
1885
|
+
// the JS tokenizer highlights JSON acceptably
|
|
1886
|
+
jsonc: "javascript",
|
|
1887
|
+
ts: "typescript",
|
|
1888
|
+
mts: "typescript",
|
|
1889
|
+
cts: "typescript",
|
|
1890
|
+
tsx: "typescript",
|
|
1891
|
+
html: "html",
|
|
1892
|
+
htm: "html",
|
|
1893
|
+
xml: "html",
|
|
1894
|
+
xhtml: "html",
|
|
1895
|
+
vue: "html",
|
|
1896
|
+
php: "php",
|
|
1897
|
+
phtml: "php",
|
|
1898
|
+
py: "python",
|
|
1899
|
+
pyw: "python",
|
|
1900
|
+
go: "go",
|
|
1901
|
+
md: "markdown",
|
|
1902
|
+
markdown: "markdown",
|
|
1903
|
+
mkd: "markdown",
|
|
1904
|
+
mdx: "markdown"
|
|
1905
|
+
};
|
|
1906
|
+
function basename(filename) {
|
|
1907
|
+
return filename.split(/[?#]/, 1)[0].split(/[\\/]/).pop() ?? "";
|
|
1908
|
+
}
|
|
1909
|
+
function languageFromFilename(filename) {
|
|
1910
|
+
if (!filename) return "plaintext";
|
|
1911
|
+
const seg = basename(filename);
|
|
1912
|
+
const dot = seg.lastIndexOf(".");
|
|
1913
|
+
if (dot <= 0) return "plaintext";
|
|
1914
|
+
const ext = seg.slice(dot + 1).toLowerCase();
|
|
1915
|
+
return EXT_LANGUAGE[ext] ?? "plaintext";
|
|
1916
|
+
}
|
|
1917
|
+
function resolveFileKind({ filename, mime }) {
|
|
1918
|
+
const media = resolveMediaType({ mime, src: filename });
|
|
1919
|
+
if (media !== "unknown") return { kind: "media", mediaKind: media };
|
|
1920
|
+
return { kind: "text", language: languageFromFilename(filename) };
|
|
1921
|
+
}
|
|
1922
|
+
function FileViewer({
|
|
1923
|
+
filename,
|
|
1924
|
+
mime,
|
|
1925
|
+
src,
|
|
1926
|
+
value = "",
|
|
1927
|
+
onChange,
|
|
1928
|
+
readOnly = true,
|
|
1929
|
+
theme = "auto",
|
|
1930
|
+
fileKind,
|
|
1931
|
+
lineNumbers = true,
|
|
1932
|
+
wordWrap = false,
|
|
1933
|
+
minHeight,
|
|
1934
|
+
maxHeight,
|
|
1935
|
+
mediaProps,
|
|
1936
|
+
className,
|
|
1937
|
+
style
|
|
1938
|
+
}) {
|
|
1939
|
+
const resolved = fileKind ?? resolveFileKind({ filename, mime });
|
|
1940
|
+
if (resolved.kind === "media") {
|
|
1941
|
+
const { className: mediaClassName, ...restMediaProps } = mediaProps ?? {};
|
|
1942
|
+
const fill = resolved.mediaKind !== "audio";
|
|
1943
|
+
return /* @__PURE__ */ jsx(
|
|
1944
|
+
"div",
|
|
1945
|
+
{
|
|
1946
|
+
"data-fancy-file-viewer": "",
|
|
1947
|
+
"data-kind": "media",
|
|
1948
|
+
"data-media-kind": resolved.mediaKind,
|
|
1949
|
+
className: cn("overflow-hidden", className),
|
|
1950
|
+
style,
|
|
1951
|
+
children: /* @__PURE__ */ jsx(
|
|
1952
|
+
MediaViewer,
|
|
1953
|
+
{
|
|
1954
|
+
src: src ?? "",
|
|
1955
|
+
mime,
|
|
1956
|
+
alt: filename,
|
|
1957
|
+
kind: resolved.mediaKind,
|
|
1958
|
+
className: cn(fill && "h-full w-full", mediaClassName),
|
|
1959
|
+
...restMediaProps
|
|
1960
|
+
}
|
|
1961
|
+
)
|
|
1962
|
+
}
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
return /* @__PURE__ */ jsx(
|
|
1966
|
+
"div",
|
|
1967
|
+
{
|
|
1968
|
+
"data-fancy-file-viewer": "",
|
|
1969
|
+
"data-kind": "text",
|
|
1970
|
+
className,
|
|
1971
|
+
style,
|
|
1972
|
+
children: /* @__PURE__ */ jsx(
|
|
1973
|
+
CodeEditor,
|
|
1974
|
+
{
|
|
1975
|
+
value,
|
|
1976
|
+
onChange: readOnly ? void 0 : onChange,
|
|
1977
|
+
language: resolved.language,
|
|
1978
|
+
theme,
|
|
1979
|
+
readOnly,
|
|
1980
|
+
lineNumbers,
|
|
1981
|
+
wordWrap,
|
|
1982
|
+
minHeight,
|
|
1983
|
+
maxHeight,
|
|
1984
|
+
children: /* @__PURE__ */ jsx(CodeEditor.Panel, {})
|
|
1985
|
+
}
|
|
1986
|
+
)
|
|
1987
|
+
}
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
FileViewer.displayName = "FileViewer";
|
|
1834
1991
|
function useCodeEditorInputTagAdapter() {
|
|
1835
1992
|
const ctx = useCodeEditor();
|
|
1836
1993
|
return useMemo(() => textareaAdapter(ctx.textareaRef), [ctx.textareaRef]);
|
|
@@ -1840,6 +1997,6 @@ function CodeEditorInputTag(props) {
|
|
|
1840
1997
|
return /* @__PURE__ */ jsx(InputTag, { adapter, ...props });
|
|
1841
1998
|
}
|
|
1842
1999
|
|
|
1843
|
-
export { CodeEditor, CodeEditorInputTag, MarkdownEditor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, renderMarkdown, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
|
|
2000
|
+
export { CodeEditor, CodeEditorInputTag, FileViewer, MarkdownEditor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, languageFromFilename, registerLanguage, registerTheme, renderMarkdown, resolveFileKind, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
|
|
1844
2001
|
//# sourceMappingURL=index.js.map
|
|
1845
2002
|
//# sourceMappingURL=index.js.map
|