@particle-academy/fancy-code 0.4.6 → 0.5.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/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
1
+ import * as react from 'react';
2
2
  import { RefObject, ReactNode } from 'react';
3
+ import { InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
3
4
 
4
5
  interface ThemeColors {
5
6
  background: string;
@@ -114,6 +115,13 @@ interface CodeEditorContextValue {
114
115
  };
115
116
  /** Length of the current selection (0 if none) */
116
117
  selectionLength: number;
118
+ /**
119
+ * Ref to the underlying `<textarea>` element. Exposed so external
120
+ * widgets — e.g. `<InputTag>`'s `textareaAdapter` — can attach to
121
+ * the same input the editor renders into. Will be `null` before
122
+ * the editor has mounted.
123
+ */
124
+ textareaRef: RefObject<HTMLTextAreaElement | null>;
117
125
  /** Placeholder text */
118
126
  placeholder?: string;
119
127
  /** @internal */
@@ -135,27 +143,27 @@ interface CodeEditorStatusBarProps {
135
143
  className?: string;
136
144
  }
137
145
 
138
- declare function CodeEditorPanel({ className }: CodeEditorPanelProps): react_jsx_runtime.JSX.Element | null;
146
+ declare function CodeEditorPanel({ className }: CodeEditorPanelProps): react.JSX.Element | null;
139
147
  declare namespace CodeEditorPanel {
140
148
  var displayName: string;
141
149
  }
142
150
 
143
- declare function CodeEditorToolbar({ children, className }: CodeEditorToolbarProps): react_jsx_runtime.JSX.Element;
151
+ declare function CodeEditorToolbar({ children, className }: CodeEditorToolbarProps): react.JSX.Element;
144
152
  declare namespace CodeEditorToolbar {
145
153
  var displayName: string;
146
154
  }
147
155
 
148
- declare function CodeEditorToolbarSeparator(): react_jsx_runtime.JSX.Element;
156
+ declare function CodeEditorToolbarSeparator(): react.JSX.Element;
149
157
  declare namespace CodeEditorToolbarSeparator {
150
158
  var displayName: string;
151
159
  }
152
160
 
153
- declare function CodeEditorStatusBar({ children, className }: CodeEditorStatusBarProps): react_jsx_runtime.JSX.Element;
161
+ declare function CodeEditorStatusBar({ children, className }: CodeEditorStatusBarProps): react.JSX.Element;
154
162
  declare namespace CodeEditorStatusBar {
155
163
  var displayName: string;
156
164
  }
157
165
 
158
- 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_runtime.JSX.Element;
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;
159
167
  declare namespace CodeEditorRoot {
160
168
  var displayName: string;
161
169
  }
@@ -169,6 +177,64 @@ declare const CodeEditor: typeof CodeEditorRoot & {
169
177
 
170
178
  declare function useCodeEditor(): CodeEditorContextValue;
171
179
 
180
+ type MarkdownEditorMode = "split" | "edit" | "preview";
181
+ interface MarkdownEditorProps {
182
+ /** Controlled markdown value. */
183
+ value?: string;
184
+ /** Initial value (uncontrolled). */
185
+ defaultValue?: string;
186
+ /** Called with the new markdown whenever the document changes. */
187
+ onValueChange?: (markdown: string) => void;
188
+ /**
189
+ * Layout: `split` (editor + live preview side by side), `edit` (editor only),
190
+ * or `preview` (rendered only). Default `split`.
191
+ */
192
+ mode?: MarkdownEditorMode;
193
+ /** Editor theme — `"light" | "dark" | "auto"` or a registered name. Default `auto`. */
194
+ theme?: string;
195
+ /** Prevent editing. */
196
+ readOnly?: boolean;
197
+ /** Show line numbers in the editor (default false — prose reads better without). */
198
+ lineNumbers?: boolean;
199
+ /** Wrap long lines in the editor (default true for prose). */
200
+ wordWrap?: boolean;
201
+ /** Placeholder shown when empty. */
202
+ placeholder?: string;
203
+ /** Editor height bounds (px). */
204
+ minHeight?: number;
205
+ maxHeight?: number;
206
+ className?: string;
207
+ /**
208
+ * Swap the markdown → HTML renderer used by the preview pane (e.g. a full
209
+ * CommonMark library). Defaults to the built-in lightweight renderer.
210
+ */
211
+ renderPreview?: (markdown: string) => string;
212
+ }
213
+ /**
214
+ * A markdown-aware editor: a syntax-highlighted `CodeEditor` (the registered
215
+ * `markdown` language) with an optional live preview pane. Controlled via
216
+ * `value` + `onValueChange`. The preview renders with a small built-in
217
+ * dependency-free renderer; pass `renderPreview` to use your own.
218
+ *
219
+ * ```tsx
220
+ * <MarkdownEditor value={md} onValueChange={setMd} mode="split" minHeight={240} />
221
+ * ```
222
+ */
223
+ declare function MarkdownEditor({ value, defaultValue, onValueChange, mode, theme, readOnly, lineNumbers, wordWrap, placeholder, minHeight, maxHeight, className, renderPreview, }: MarkdownEditorProps): react.JSX.Element;
224
+ declare namespace MarkdownEditor {
225
+ var displayName: string;
226
+ }
227
+
228
+ /**
229
+ * Tiny, dependency-free Markdown → HTML renderer for the MarkdownEditor preview
230
+ * pane. Covers the common subset — headings, bold/italic, inline + fenced code,
231
+ * links, images, blockquotes, ordered/unordered lists, hr, and paragraphs. Input
232
+ * is HTML-escaped first, so raw HTML in the source is shown as text (safe by
233
+ * default). Not a full CommonMark implementation; swap in a real renderer
234
+ * downstream if you need one.
235
+ */
236
+ declare function renderMarkdown(markdown: string): string;
237
+
172
238
  type TokenType = "keyword" | "string" | "comment" | "number" | "operator" | "tag" | "attribute" | "attributeValue" | "punctuation" | "function" | "type" | "variable" | "plain";
173
239
  interface Token {
174
240
  type: TokenType;
@@ -177,6 +243,14 @@ interface Token {
177
243
  }
178
244
  type Tokenizer = (source: string) => Token[];
179
245
 
246
+ /**
247
+ * Minimal, dependency-free Markdown tokenizer for editor highlighting. Line-based
248
+ * for block constructs (fences, headings, blockquotes, list markers) plus a few
249
+ * safe, non-overlapping inline spans (code, bold/italic, links). Good enough to
250
+ * read structure at a glance — not a full CommonMark parse.
251
+ */
252
+ declare function tokenizeMarkdown(source: string): Token[];
253
+
180
254
  interface LanguageDefinition {
181
255
  /** Display name shown in UI (e.g., "JavaScript") */
182
256
  name: string;
@@ -194,4 +268,45 @@ declare function registerTheme(theme: ThemeDefinition): void;
194
268
  declare function getTheme(name: string): ThemeDefinition | undefined;
195
269
  declare function getRegisteredThemes(): string[];
196
270
 
197
- export { CodeEditor, type CodeEditorContextValue, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type LanguageDefinition, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, useCodeEditor };
271
+ /**
272
+ * Glue between `@particle-academy/react-fancy`'s `<InputTag>` and the
273
+ * `<CodeEditor>` from this package. `<CodeEditor>` renders a real DOM
274
+ * `<textarea>` under its overlay, so the existing `textareaAdapter`
275
+ * just works — this module hands it the right ref via the editor
276
+ * context.
277
+ *
278
+ * <CodeEditor value={code} onChange={setCode} language="markdown">
279
+ * <CodeEditor.Toolbar />
280
+ * <CodeEditor.Panel />
281
+ * <CodeEditorInputTag
282
+ * triggers={{
283
+ * "/": { items: commands, insert: (c) => `${c.name} ` },
284
+ * "@": { items: mentions, insert: (m) => `@${m.name} ` },
285
+ * }}
286
+ * />
287
+ * </CodeEditor>
288
+ *
289
+ * Must be rendered as a descendant of `<CodeEditor>` (it reads the
290
+ * editor's context). For ad-hoc wiring outside the component tree,
291
+ * pull the textarea ref off the context and call `textareaAdapter`
292
+ * yourself.
293
+ */
294
+ /**
295
+ * Hook form — returns an `InputTagAdapter` bound to the surrounding
296
+ * `<CodeEditor>`. Useful when you want to compose your own picker UI
297
+ * around the same adapter.
298
+ */
299
+ declare function useCodeEditorInputTagAdapter(): InputTagAdapter;
300
+ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
301
+ triggers: InputTagTriggers;
302
+ }
303
+ /**
304
+ * Convenience wrapper — renders `<InputTag>` already wired to the
305
+ * surrounding `<CodeEditor>`'s textarea. Equivalent to:
306
+ *
307
+ * const adapter = useCodeEditorInputTagAdapter();
308
+ * return <InputTag adapter={adapter} {...rest} />;
309
+ */
310
+ declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
311
+
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 };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
1
+ import * as react from 'react';
2
2
  import { RefObject, ReactNode } from 'react';
3
+ import { InputTagProps, InputTagTriggers, InputTagAdapter } from '@particle-academy/react-fancy';
3
4
 
4
5
  interface ThemeColors {
5
6
  background: string;
@@ -114,6 +115,13 @@ interface CodeEditorContextValue {
114
115
  };
115
116
  /** Length of the current selection (0 if none) */
116
117
  selectionLength: number;
118
+ /**
119
+ * Ref to the underlying `<textarea>` element. Exposed so external
120
+ * widgets — e.g. `<InputTag>`'s `textareaAdapter` — can attach to
121
+ * the same input the editor renders into. Will be `null` before
122
+ * the editor has mounted.
123
+ */
124
+ textareaRef: RefObject<HTMLTextAreaElement | null>;
117
125
  /** Placeholder text */
118
126
  placeholder?: string;
119
127
  /** @internal */
@@ -135,27 +143,27 @@ interface CodeEditorStatusBarProps {
135
143
  className?: string;
136
144
  }
137
145
 
138
- declare function CodeEditorPanel({ className }: CodeEditorPanelProps): react_jsx_runtime.JSX.Element | null;
146
+ declare function CodeEditorPanel({ className }: CodeEditorPanelProps): react.JSX.Element | null;
139
147
  declare namespace CodeEditorPanel {
140
148
  var displayName: string;
141
149
  }
142
150
 
143
- declare function CodeEditorToolbar({ children, className }: CodeEditorToolbarProps): react_jsx_runtime.JSX.Element;
151
+ declare function CodeEditorToolbar({ children, className }: CodeEditorToolbarProps): react.JSX.Element;
144
152
  declare namespace CodeEditorToolbar {
145
153
  var displayName: string;
146
154
  }
147
155
 
148
- declare function CodeEditorToolbarSeparator(): react_jsx_runtime.JSX.Element;
156
+ declare function CodeEditorToolbarSeparator(): react.JSX.Element;
149
157
  declare namespace CodeEditorToolbarSeparator {
150
158
  var displayName: string;
151
159
  }
152
160
 
153
- declare function CodeEditorStatusBar({ children, className }: CodeEditorStatusBarProps): react_jsx_runtime.JSX.Element;
161
+ declare function CodeEditorStatusBar({ children, className }: CodeEditorStatusBarProps): react.JSX.Element;
154
162
  declare namespace CodeEditorStatusBar {
155
163
  var displayName: string;
156
164
  }
157
165
 
158
- 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_runtime.JSX.Element;
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;
159
167
  declare namespace CodeEditorRoot {
160
168
  var displayName: string;
161
169
  }
@@ -169,6 +177,64 @@ declare const CodeEditor: typeof CodeEditorRoot & {
169
177
 
170
178
  declare function useCodeEditor(): CodeEditorContextValue;
171
179
 
180
+ type MarkdownEditorMode = "split" | "edit" | "preview";
181
+ interface MarkdownEditorProps {
182
+ /** Controlled markdown value. */
183
+ value?: string;
184
+ /** Initial value (uncontrolled). */
185
+ defaultValue?: string;
186
+ /** Called with the new markdown whenever the document changes. */
187
+ onValueChange?: (markdown: string) => void;
188
+ /**
189
+ * Layout: `split` (editor + live preview side by side), `edit` (editor only),
190
+ * or `preview` (rendered only). Default `split`.
191
+ */
192
+ mode?: MarkdownEditorMode;
193
+ /** Editor theme — `"light" | "dark" | "auto"` or a registered name. Default `auto`. */
194
+ theme?: string;
195
+ /** Prevent editing. */
196
+ readOnly?: boolean;
197
+ /** Show line numbers in the editor (default false — prose reads better without). */
198
+ lineNumbers?: boolean;
199
+ /** Wrap long lines in the editor (default true for prose). */
200
+ wordWrap?: boolean;
201
+ /** Placeholder shown when empty. */
202
+ placeholder?: string;
203
+ /** Editor height bounds (px). */
204
+ minHeight?: number;
205
+ maxHeight?: number;
206
+ className?: string;
207
+ /**
208
+ * Swap the markdown → HTML renderer used by the preview pane (e.g. a full
209
+ * CommonMark library). Defaults to the built-in lightweight renderer.
210
+ */
211
+ renderPreview?: (markdown: string) => string;
212
+ }
213
+ /**
214
+ * A markdown-aware editor: a syntax-highlighted `CodeEditor` (the registered
215
+ * `markdown` language) with an optional live preview pane. Controlled via
216
+ * `value` + `onValueChange`. The preview renders with a small built-in
217
+ * dependency-free renderer; pass `renderPreview` to use your own.
218
+ *
219
+ * ```tsx
220
+ * <MarkdownEditor value={md} onValueChange={setMd} mode="split" minHeight={240} />
221
+ * ```
222
+ */
223
+ declare function MarkdownEditor({ value, defaultValue, onValueChange, mode, theme, readOnly, lineNumbers, wordWrap, placeholder, minHeight, maxHeight, className, renderPreview, }: MarkdownEditorProps): react.JSX.Element;
224
+ declare namespace MarkdownEditor {
225
+ var displayName: string;
226
+ }
227
+
228
+ /**
229
+ * Tiny, dependency-free Markdown → HTML renderer for the MarkdownEditor preview
230
+ * pane. Covers the common subset — headings, bold/italic, inline + fenced code,
231
+ * links, images, blockquotes, ordered/unordered lists, hr, and paragraphs. Input
232
+ * is HTML-escaped first, so raw HTML in the source is shown as text (safe by
233
+ * default). Not a full CommonMark implementation; swap in a real renderer
234
+ * downstream if you need one.
235
+ */
236
+ declare function renderMarkdown(markdown: string): string;
237
+
172
238
  type TokenType = "keyword" | "string" | "comment" | "number" | "operator" | "tag" | "attribute" | "attributeValue" | "punctuation" | "function" | "type" | "variable" | "plain";
173
239
  interface Token {
174
240
  type: TokenType;
@@ -177,6 +243,14 @@ interface Token {
177
243
  }
178
244
  type Tokenizer = (source: string) => Token[];
179
245
 
246
+ /**
247
+ * Minimal, dependency-free Markdown tokenizer for editor highlighting. Line-based
248
+ * for block constructs (fences, headings, blockquotes, list markers) plus a few
249
+ * safe, non-overlapping inline spans (code, bold/italic, links). Good enough to
250
+ * read structure at a glance — not a full CommonMark parse.
251
+ */
252
+ declare function tokenizeMarkdown(source: string): Token[];
253
+
180
254
  interface LanguageDefinition {
181
255
  /** Display name shown in UI (e.g., "JavaScript") */
182
256
  name: string;
@@ -194,4 +268,45 @@ declare function registerTheme(theme: ThemeDefinition): void;
194
268
  declare function getTheme(name: string): ThemeDefinition | undefined;
195
269
  declare function getRegisteredThemes(): string[];
196
270
 
197
- export { CodeEditor, type CodeEditorContextValue, type CodeEditorPanelProps, type CodeEditorProps, type CodeEditorStatusBarProps, type CodeEditorToolbarProps, type LanguageDefinition, type ThemeColors, type ThemeDefinition, type Token, type TokenType, type Tokenizer, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, useCodeEditor };
271
+ /**
272
+ * Glue between `@particle-academy/react-fancy`'s `<InputTag>` and the
273
+ * `<CodeEditor>` from this package. `<CodeEditor>` renders a real DOM
274
+ * `<textarea>` under its overlay, so the existing `textareaAdapter`
275
+ * just works — this module hands it the right ref via the editor
276
+ * context.
277
+ *
278
+ * <CodeEditor value={code} onChange={setCode} language="markdown">
279
+ * <CodeEditor.Toolbar />
280
+ * <CodeEditor.Panel />
281
+ * <CodeEditorInputTag
282
+ * triggers={{
283
+ * "/": { items: commands, insert: (c) => `${c.name} ` },
284
+ * "@": { items: mentions, insert: (m) => `@${m.name} ` },
285
+ * }}
286
+ * />
287
+ * </CodeEditor>
288
+ *
289
+ * Must be rendered as a descendant of `<CodeEditor>` (it reads the
290
+ * editor's context). For ad-hoc wiring outside the component tree,
291
+ * pull the textarea ref off the context and call `textareaAdapter`
292
+ * yourself.
293
+ */
294
+ /**
295
+ * Hook form — returns an `InputTagAdapter` bound to the surrounding
296
+ * `<CodeEditor>`. Useful when you want to compose your own picker UI
297
+ * around the same adapter.
298
+ */
299
+ declare function useCodeEditorInputTagAdapter(): InputTagAdapter;
300
+ interface CodeEditorInputTagProps extends Omit<InputTagProps, "adapter"> {
301
+ triggers: InputTagTriggers;
302
+ }
303
+ /**
304
+ * Convenience wrapper — renders `<InputTag>` already wired to the
305
+ * surrounding `<CodeEditor>`'s textarea. Equivalent to:
306
+ *
307
+ * const adapter = useCodeEditorInputTagAdapter();
308
+ * return <InputTag adapter={adapter} {...rest} />;
309
+ */
310
+ declare function CodeEditorInputTag(props: CodeEditorInputTagProps): react.JSX.Element;
311
+
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 };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createContext, useState, useCallback, useRef, useMemo, useSyncExternalStore, useEffect, useContext } from 'react';
2
- import { cn, useControllableState } from '@particle-academy/react-fancy';
2
+ import { cn, useControllableState, 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
@@ -1035,7 +1035,79 @@ var tokenizeGo = (source) => {
1035
1035
  return tokens;
1036
1036
  };
1037
1037
 
1038
+ // src/engine/tokenizer.ts
1039
+ function tok(type, s, e) {
1040
+ return { type, start: s, end: e };
1041
+ }
1042
+
1043
+ // src/engine/tokenizers/markdown.ts
1044
+ function tokenizeMarkdown(source) {
1045
+ const tokens = [];
1046
+ const lines = source.split("\n");
1047
+ let offset = 0;
1048
+ let inFence = false;
1049
+ for (const line of lines) {
1050
+ const base = offset;
1051
+ const len = line.length;
1052
+ offset += len + 1;
1053
+ if (/^\s*(```|~~~)/.test(line)) {
1054
+ tokens.push(tok("string", base, base + len));
1055
+ inFence = !inFence;
1056
+ continue;
1057
+ }
1058
+ if (inFence) {
1059
+ tokens.push(tok("string", base, base + len));
1060
+ continue;
1061
+ }
1062
+ if (/^#{1,6}\s/.test(line)) {
1063
+ tokens.push(tok("keyword", base, base + len));
1064
+ continue;
1065
+ }
1066
+ if (/^\s*>/.test(line)) {
1067
+ tokens.push(tok("comment", base, base + len));
1068
+ continue;
1069
+ }
1070
+ const list = line.match(/^(\s*)([-*+]|\d+\.)\s/);
1071
+ if (list) {
1072
+ const start = base + list[1].length;
1073
+ tokens.push(tok("operator", start, start + list[2].length));
1074
+ }
1075
+ pushInline(line, base, tokens);
1076
+ }
1077
+ return tokens;
1078
+ }
1079
+ function pushInline(line, base, out) {
1080
+ const spans = [];
1081
+ const add = (type, start, end) => {
1082
+ spans.push(tok(type, base + start, base + end));
1083
+ };
1084
+ let m;
1085
+ const code = /`[^`]+`/g;
1086
+ while (m = code.exec(line)) add("string", m.index, m.index + m[0].length);
1087
+ const emph = /(\*\*|__)(?=\S)([\s\S]*?\S)\1|(\*|_)(?=\S)([\s\S]*?\S)\3/g;
1088
+ while (m = emph.exec(line)) add("type", m.index, m.index + m[0].length);
1089
+ const link = /!?\[[^\]]*\]\(([^)]+)\)/g;
1090
+ while (m = link.exec(line)) {
1091
+ add("tag", m.index, m.index + m[0].length - m[1].length - 1);
1092
+ const urlStart = m.index + m[0].length - m[1].length - 1;
1093
+ add("attributeValue", urlStart, urlStart + m[1].length);
1094
+ }
1095
+ spans.sort((a, b) => a.start - b.start || b.end - a.end);
1096
+ let lastEnd = -1;
1097
+ for (const s of spans) {
1098
+ if (s.start >= lastEnd) {
1099
+ out.push(s);
1100
+ lastEnd = s.end;
1101
+ }
1102
+ }
1103
+ }
1104
+
1038
1105
  // src/languages/builtin.ts
1106
+ registerLanguage({
1107
+ name: "Markdown",
1108
+ aliases: ["md", "markdown", "mkd"],
1109
+ tokenize: tokenizeMarkdown
1110
+ });
1039
1111
  registerLanguage({
1040
1112
  name: "JavaScript",
1041
1113
  aliases: ["js", "javascript", "jsx"],
@@ -1576,6 +1648,7 @@ function CodeEditorRoot({
1576
1648
  },
1577
1649
  cursorPosition,
1578
1650
  selectionLength,
1651
+ textareaRef: engineReturn.textareaRef,
1579
1652
  placeholder,
1580
1653
  _engineReturn: engineReturn,
1581
1654
  _minHeight: minHeight,
@@ -1605,6 +1678,164 @@ var CodeEditor = Object.assign(CodeEditorRoot, {
1605
1678
  StatusBar: CodeEditorStatusBar
1606
1679
  });
1607
1680
 
1608
- export { CodeEditor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, useCodeEditor };
1681
+ // src/engine/markdown-render.ts
1682
+ function escapeHtml2(s) {
1683
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1684
+ }
1685
+ function inline(text) {
1686
+ return text.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (_m, alt, src, title) => `<img src="${src}" alt="${alt}"${title ? ` title="${title}"` : ""}>`).replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)/g, (_m, t, href, title) => `<a href="${href}"${title ? ` title="${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>");
1687
+ }
1688
+ function renderMarkdown(markdown) {
1689
+ const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
1690
+ const out = [];
1691
+ let i = 0;
1692
+ let para = [];
1693
+ let listType = null;
1694
+ const flushPara = () => {
1695
+ if (para.length) {
1696
+ out.push(`<p>${inline(para.join(" "))}</p>`);
1697
+ para = [];
1698
+ }
1699
+ };
1700
+ const closeList = () => {
1701
+ if (listType) {
1702
+ out.push(`</${listType}>`);
1703
+ listType = null;
1704
+ }
1705
+ };
1706
+ while (i < lines.length) {
1707
+ const line = lines[i];
1708
+ const fence = line.match(/^\s*(```|~~~)(.*)$/);
1709
+ if (fence) {
1710
+ flushPara();
1711
+ closeList();
1712
+ const lang = fence[2].trim();
1713
+ const body = [];
1714
+ i++;
1715
+ while (i < lines.length && !/^\s*(```|~~~)/.test(lines[i])) {
1716
+ body.push(lines[i]);
1717
+ i++;
1718
+ }
1719
+ i++;
1720
+ out.push(`<pre><code${lang ? ` class="language-${escapeHtml2(lang)}"` : ""}>${escapeHtml2(body.join("\n"))}</code></pre>`);
1721
+ continue;
1722
+ }
1723
+ const esc = escapeHtml2(line);
1724
+ if (/^\s*$/.test(line)) {
1725
+ flushPara();
1726
+ closeList();
1727
+ i++;
1728
+ continue;
1729
+ }
1730
+ const heading = esc.match(/^(#{1,6})\s+(.*)$/);
1731
+ if (heading) {
1732
+ flushPara();
1733
+ closeList();
1734
+ const level = heading[1].length;
1735
+ out.push(`<h${level}>${inline(heading[2])}</h${level}>`);
1736
+ i++;
1737
+ continue;
1738
+ }
1739
+ if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
1740
+ flushPara();
1741
+ closeList();
1742
+ out.push("<hr>");
1743
+ i++;
1744
+ continue;
1745
+ }
1746
+ const quote = esc.match(/^\s*&gt;\s?(.*)$/);
1747
+ if (quote) {
1748
+ flushPara();
1749
+ closeList();
1750
+ out.push(`<blockquote>${inline(quote[1])}</blockquote>`);
1751
+ i++;
1752
+ continue;
1753
+ }
1754
+ const ol = esc.match(/^\s*\d+\.\s+(.*)$/);
1755
+ const ul = esc.match(/^\s*[-*+]\s+(.*)$/);
1756
+ if (ol || ul) {
1757
+ flushPara();
1758
+ const type = ol ? "ol" : "ul";
1759
+ if (listType !== type) {
1760
+ closeList();
1761
+ out.push(`<${type}>`);
1762
+ listType = type;
1763
+ }
1764
+ out.push(`<li>${inline((ol ?? ul)[1])}</li>`);
1765
+ i++;
1766
+ continue;
1767
+ }
1768
+ closeList();
1769
+ para.push(esc);
1770
+ i++;
1771
+ }
1772
+ flushPara();
1773
+ closeList();
1774
+ return out.join("\n");
1775
+ }
1776
+ function MarkdownEditor({
1777
+ value,
1778
+ defaultValue = "",
1779
+ onValueChange,
1780
+ mode = "split",
1781
+ theme = "auto",
1782
+ readOnly = false,
1783
+ lineNumbers = false,
1784
+ wordWrap = true,
1785
+ placeholder,
1786
+ minHeight,
1787
+ maxHeight,
1788
+ className,
1789
+ renderPreview = renderMarkdown
1790
+ }) {
1791
+ const [content, setContent] = useControllableState(value, defaultValue, onValueChange);
1792
+ const showEditor = mode !== "preview";
1793
+ const showPreview = mode !== "edit";
1794
+ return /* @__PURE__ */ jsxs(
1795
+ "div",
1796
+ {
1797
+ "data-fancy-markdown-editor": "",
1798
+ "data-mode": mode,
1799
+ className: cn("fancy-md-editor", className),
1800
+ children: [
1801
+ showEditor && /* @__PURE__ */ jsx("div", { className: "fancy-md-editor-pane", children: /* @__PURE__ */ jsx(
1802
+ CodeEditor,
1803
+ {
1804
+ value: content,
1805
+ onChange: setContent,
1806
+ language: "markdown",
1807
+ theme,
1808
+ readOnly,
1809
+ lineNumbers,
1810
+ wordWrap,
1811
+ placeholder,
1812
+ minHeight,
1813
+ maxHeight,
1814
+ children: /* @__PURE__ */ jsx(CodeEditor.Panel, {})
1815
+ }
1816
+ ) }),
1817
+ showPreview && /* @__PURE__ */ jsx(
1818
+ "div",
1819
+ {
1820
+ "data-fancy-markdown-preview": "",
1821
+ className: "fancy-md-preview",
1822
+ dangerouslySetInnerHTML: { __html: renderPreview(content) }
1823
+ }
1824
+ )
1825
+ ]
1826
+ }
1827
+ );
1828
+ }
1829
+ MarkdownEditor.displayName = "MarkdownEditor";
1830
+ function useCodeEditorInputTagAdapter() {
1831
+ const ctx = useCodeEditor();
1832
+ return useMemo(() => textareaAdapter(ctx.textareaRef), [ctx.textareaRef]);
1833
+ }
1834
+ function CodeEditorInputTag(props) {
1835
+ const adapter = useCodeEditorInputTagAdapter();
1836
+ return /* @__PURE__ */ jsx(InputTag, { adapter, ...props });
1837
+ }
1838
+
1839
+ export { CodeEditor, CodeEditorInputTag, MarkdownEditor, getLanguage, getRegisteredLanguages, getRegisteredThemes, getTheme, registerLanguage, registerTheme, renderMarkdown, tokenizeMarkdown, useCodeEditor, useCodeEditorInputTagAdapter };
1609
1840
  //# sourceMappingURL=index.js.map
1610
1841
  //# sourceMappingURL=index.js.map