@sigx/lynx-markdown 0.4.6 → 0.4.8

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 CHANGED
@@ -9,10 +9,6 @@ AI output: as the source string grows token-by-token, finalized blocks keep a
9
9
  stable identity and are never re-parsed or remounted, so completed content
10
10
  doesn't flicker or reflow while new tokens stream in.
11
11
 
12
- The package also ships [`XMarkdown`](#xmarkdown--native-element-wrapper), the
13
- thin wrapper around Lynx's native `<x-markdown>` element, for cases where you
14
- want the platform's built-in renderer.
15
-
16
12
  ## Install
17
13
 
18
14
  ```
@@ -120,22 +116,38 @@ import { markdownComponents } from '@sigx/lynx-daisyui';
120
116
  <MarkdownView value={src} components={markdownComponents} />;
121
117
  ```
122
118
 
123
- ## `XMarkdown` — native element wrapper
124
-
125
- `XMarkdown` wraps Lynx's native `<x-markdown>` XElement. It's fast where
126
- available but platform-gated and opaque (the engine owns parsing and styling):
127
119
 
128
- | Platform | First version | Notes |
129
- | -------- | --------------- | --------------------------------------------------- |
130
- | Harmony | Lynx 3.7.0 | Stable. |
131
- | Android | Lynx 3.8.0-rc.0 | Ships as `org.lynxsdk.lynx:lynx_xelement_markdown`. |
132
- | iOS | (post-3.8.0) | Currently only on the upstream `main` branch. |
120
+ ## `MarkdownEditor` true-WYSIWYG editing
133
121
 
134
- On platforms where `<x-markdown>` is not registered, it renders nothing. Prefer
135
- `MarkdownView` for cross-platform, streaming, and customizable rendering.
122
+ `MarkdownEditor` provides WYSIWYG markdown editing on the native
123
+ [`@sigx/lynx-richtext`](../lynx-richtext) element (optional peer dependency
124
+ bold is bold *inside* the input, not `**` markers). The external contract stays
125
+ markdown: `value` in, `onChange(markdown)` out.
136
126
 
137
127
  ```tsx
138
- import { XMarkdown } from '@sigx/lynx-markdown';
139
-
140
- <XMarkdown value={'# Hello'} effect="typewriter" onLink={(e) => open(e.detail.url)} />;
128
+ import { MarkdownEditor, type MarkdownEditorController } from '@sigx/lynx-markdown';
129
+
130
+ let ctrl: MarkdownEditorController | null = null;
131
+
132
+ <MarkdownEditor
133
+ value={draft}
134
+ placeholder="Message…"
135
+ minLines={1}
136
+ maxLines={4} // chat-style: grow 1→4 lines, then scroll
137
+ mode="auto" // 'auto' | 'fixed' | 'fullscreen'
138
+ confirmType="send"
139
+ onChange={(md) => { draft = md; }}
140
+ onSelectionChange={(sel) => toolbarState(sel.activeFormats)}
141
+ controllerRef={(c) => { ctrl = c; }}
142
+ />;
143
+
144
+ // Imperative commands (what a toolbar drives):
145
+ ctrl?.toggleBold();
146
+ ctrl?.setHeading(2);
147
+ ctrl?.clear(); // chat send
141
148
  ```
149
+
150
+ v1 models paragraphs, headings, and bold/italic/strike/code/link in-field;
151
+ everything else (lists, tables, code fences) round-trips **losslessly** as raw
152
+ markdown source via `raw` blocks until later phases model them. Conversion
153
+ helpers `mdToDoc`/`docToMd` are exported for advanced use.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * `<MarkdownEditor>` — true-WYSIWYG markdown editing on the native
3
+ * `<sigx-richtext>` element.
4
+ *
5
+ * The external contract is **markdown**: `value` in, `onChange(markdown)` out.
6
+ * Internally the editor converts markdown ↔ the element's `RichDoc` span model
7
+ * (`convert/mdToDoc`, `convert/docToMd`) and drives formatting through
8
+ * fire-and-forget commands; the element is the single source of truth for live
9
+ * text and selection (lightly-controlled — keystrokes are never echoed back).
10
+ *
11
+ * ### Echo / IME rules (JS side)
12
+ * - An incoming `value` identical to the last markdown we emitted is our own
13
+ * echo → ignored (string compare; exact).
14
+ * - Otherwise it's compared structurally against the element's last document —
15
+ * only genuinely different content is pushed via `setDocument`.
16
+ * - While the IME is composing, external values are buffered and applied on
17
+ * the composition-end change; `onChange` is also suppressed mid-composition.
18
+ *
19
+ * Sizing: `minLines`/`maxLines` × line height drive the element's auto-grow
20
+ * window (`mode="auto"`, chat-style 1 → N lines then internal scroll);
21
+ * `mode="fixed"` pins the height at `maxLines`; `mode="fullscreen"` fills the
22
+ * parent.
23
+ */
24
+ import { type Define } from '@sigx/lynx';
25
+ import { type SelectionState } from '@sigx/lynx-richtext';
26
+ export type MarkdownEditorMode = 'auto' | 'fixed' | 'fullscreen';
27
+ /** Imperative command surface — what toolbars and plugins drive. */
28
+ export interface MarkdownEditorController {
29
+ toggleBold(): void;
30
+ toggleItalic(): void;
31
+ toggleStrike(): void;
32
+ toggleCode(): void;
33
+ /** 1–6 sets a heading; 0 reverts to paragraph. */
34
+ setHeading(level: 0 | 1 | 2 | 3 | 4 | 5 | 6): void;
35
+ insertText(text: string): void;
36
+ /** Clear the document (chat send). */
37
+ clear(): void;
38
+ focus(): void;
39
+ blur(): void;
40
+ /** The current markdown (as of the last element change). */
41
+ getMarkdown(): string;
42
+ /** The current selection state (as of the last selection event). */
43
+ getSelection(): SelectionState | null;
44
+ }
45
+ export type MarkdownEditorProps = Define.Prop<'value', string, false> & Define.Prop<'placeholder', string, false> & Define.Prop<'minLines', number, false> & Define.Prop<'maxLines', number, false> & Define.Prop<'mode', MarkdownEditorMode, false> & Define.Prop<'fontSize', number, false> & Define.Prop<'textColor', string, false> & Define.Prop<'accentColor', string, false> & Define.Prop<'placeholderColor', string, false> & Define.Prop<'confirmType', 'send' | 'search' | 'next' | 'go' | 'done', false> & Define.Prop<'autoFocus', boolean, false> & Define.Prop<'disabled', boolean, false> & Define.Prop<'class', string, false> & Define.Prop<'onChange', (markdown: string) => void, false> & Define.Prop<'onSelectionChange', (sel: SelectionState) => void, false> & Define.Prop<'onFocus', () => void, false> & Define.Prop<'onBlur', () => void, false>
46
+ /** Receives the imperative controller once on mount. */
47
+ & Define.Prop<'controllerRef', (ctrl: MarkdownEditorController) => void, false>;
48
+ export declare const MarkdownEditor: import("@sigx/runtime-core").ComponentFactory<MarkdownEditorProps, void, {}>;
@@ -0,0 +1,127 @@
1
+ import { jsx as _jsx } from "@sigx/lynx/jsx-runtime";
2
+ /**
3
+ * `<MarkdownEditor>` — true-WYSIWYG markdown editing on the native
4
+ * `<sigx-richtext>` element.
5
+ *
6
+ * The external contract is **markdown**: `value` in, `onChange(markdown)` out.
7
+ * Internally the editor converts markdown ↔ the element's `RichDoc` span model
8
+ * (`convert/mdToDoc`, `convert/docToMd`) and drives formatting through
9
+ * fire-and-forget commands; the element is the single source of truth for live
10
+ * text and selection (lightly-controlled — keystrokes are never echoed back).
11
+ *
12
+ * ### Echo / IME rules (JS side)
13
+ * - An incoming `value` identical to the last markdown we emitted is our own
14
+ * echo → ignored (string compare; exact).
15
+ * - Otherwise it's compared structurally against the element's last document —
16
+ * only genuinely different content is pushed via `setDocument`.
17
+ * - While the IME is composing, external values are buffered and applied on
18
+ * the composition-end change; `onChange` is also suppressed mid-composition.
19
+ *
20
+ * Sizing: `minLines`/`maxLines` × line height drive the element's auto-grow
21
+ * window (`mode="auto"`, chat-style 1 → N lines then internal scroll);
22
+ * `mode="fixed"` pins the height at `maxLines`; `mode="fullscreen"` fills the
23
+ * parent.
24
+ */
25
+ import { component, signal, watch } from '@sigx/lynx';
26
+ import { RichTextInput, RichTextMethods, docEquals, normalizeDoc, emptyDoc, } from '@sigx/lynx-richtext';
27
+ import { mdToDoc } from './convert/mdToDoc.js';
28
+ import { docToMd } from './convert/docToMd.js';
29
+ const DEFAULT_FONT_SIZE = 16;
30
+ /** Vertical padding the element applies internally (8 top + 8 bottom). */
31
+ const ELEMENT_PADDING = 16;
32
+ export const MarkdownEditor = component(({ props }) => {
33
+ let el = null;
34
+ // --- sync state (see module docs) ---
35
+ const initialMd = typeof props.value === 'string' ? props.value : '';
36
+ let lastEmittedMd = initialMd;
37
+ let lastDocFromElement = normalizeDoc(mdToDoc(initialMd));
38
+ let lastSeenVersion = 0;
39
+ let composing = false;
40
+ let pendingExternal = null;
41
+ let currentSel = null;
42
+ // Auto-grow: the native element reports its (clamped) content height and
43
+ // the editor feeds it back as the element's layout height — Lynx layout
44
+ // sizes views from styles, never from native intrinsic content.
45
+ const reportedHeight = signal(0);
46
+ const applyExternal = (md) => {
47
+ if (md === lastEmittedMd)
48
+ return; // our own echo
49
+ if (composing) {
50
+ pendingExternal = md;
51
+ return;
52
+ }
53
+ const doc = mdToDoc(md, lastSeenVersion);
54
+ if (docEquals(normalizeDoc(doc), lastDocFromElement)) {
55
+ lastEmittedMd = md; // same content, different markdown spelling
56
+ return;
57
+ }
58
+ RichTextMethods.setDocument(el, doc);
59
+ };
60
+ watch(() => props.value, (next) => {
61
+ if (typeof next === 'string')
62
+ applyExternal(next);
63
+ });
64
+ const handleChange = (doc, isComposing) => {
65
+ composing = isComposing;
66
+ lastSeenVersion = doc.v;
67
+ lastDocFromElement = normalizeDoc(doc);
68
+ if (isComposing)
69
+ return;
70
+ const md = docToMd(doc);
71
+ if (md !== lastEmittedMd) {
72
+ lastEmittedMd = md;
73
+ props.onChange?.(md);
74
+ }
75
+ if (pendingExternal !== null) {
76
+ const pending = pendingExternal;
77
+ pendingExternal = null;
78
+ applyExternal(pending);
79
+ }
80
+ };
81
+ const controller = {
82
+ toggleBold: () => RichTextMethods.toggleFormat(el, 'bold'),
83
+ toggleItalic: () => RichTextMethods.toggleFormat(el, 'italic'),
84
+ toggleStrike: () => RichTextMethods.toggleFormat(el, 'strike'),
85
+ toggleCode: () => RichTextMethods.toggleFormat(el, 'code'),
86
+ setHeading: (level) => {
87
+ if (level === 0)
88
+ RichTextMethods.setBlockType(el, 'paragraph');
89
+ else
90
+ RichTextMethods.setBlockType(el, 'heading', level);
91
+ },
92
+ insertText: (text) => RichTextMethods.insertText(el, text),
93
+ clear: () => RichTextMethods.setDocument(el, emptyDoc(lastSeenVersion)),
94
+ focus: () => RichTextMethods.focus(el),
95
+ blur: () => RichTextMethods.blur(el),
96
+ getMarkdown: () => lastEmittedMd ?? '',
97
+ getSelection: () => currentSel,
98
+ };
99
+ props.controllerRef?.(controller);
100
+ return () => {
101
+ const fontSize = props.fontSize ?? DEFAULT_FONT_SIZE;
102
+ const lineHeight = Math.round(fontSize * 1.5);
103
+ const mode = props.mode ?? 'auto';
104
+ const minLines = Math.max(1, props.minLines ?? 1);
105
+ const maxLines = Math.max(minLines, props.maxLines ?? 4);
106
+ let minHeight = minLines * lineHeight + ELEMENT_PADDING;
107
+ let maxHeight = maxLines * lineHeight + ELEMENT_PADDING;
108
+ if (mode === 'fixed')
109
+ minHeight = maxHeight;
110
+ if (mode === 'fullscreen')
111
+ maxHeight = 0; // unbounded; element fills parent
112
+ return (_jsx("view", { class: props.class, style: {
113
+ display: 'flex',
114
+ flexDirection: 'column',
115
+ ...(mode === 'fullscreen' ? { flexGrow: 1, flexShrink: 1 } : {}),
116
+ }, children: _jsx(RichTextInput, { value: mdToDoc(initialMd), placeholder: props.placeholder, editable: props.disabled ? false : undefined, minHeight: minHeight, maxHeight: maxHeight, fontSize: fontSize, textColor: props.textColor, accentColor: props.accentColor, placeholderColor: props.placeholderColor, confirmType: props.confirmType, autoFocus: props.autoFocus, style: mode === 'fullscreen'
117
+ ? { flexGrow: 1 }
118
+ : { height: Math.max(minHeight, Math.min(reportedHeight.value || minHeight, maxHeight)) }, onElement: (handle) => {
119
+ el = handle;
120
+ }, onHeightChange: (height) => {
121
+ reportedHeight.value = height;
122
+ }, onChange: handleChange, onSelection: (sel) => {
123
+ currentSel = sel;
124
+ props.onSelectionChange?.(sel);
125
+ }, onFocus: () => props.onFocus?.(), onBlur: () => props.onBlur?.() }) }));
126
+ };
127
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * {@link RichDoc} → markdown — the serializer (inverse of `mdToDoc`).
3
+ *
4
+ * Segments the flat text by `blocks[]` (raw chunks verbatim, heading lines
5
+ * prefixed, remaining lines as paragraphs), then serializes inline spans per
6
+ * segment via elementary runs + a close/reopen delimiter stack (valid nesting
7
+ * from arbitrarily overlapping spans).
8
+ *
9
+ * Round-trip contract: `mdToDoc(docToMd(doc))` is structurally equal to a
10
+ * *normalized* `doc` — emphasis markers, hard breaks (→ paragraph breaks), and
11
+ * blank lines normalize; `raw` blocks round-trip byte-for-byte.
12
+ */
13
+ import type { RichDoc } from '@sigx/lynx-richtext';
14
+ export declare function docToMd(doc: RichDoc): string;
@@ -0,0 +1,201 @@
1
+ /**
2
+ * {@link RichDoc} → markdown — the serializer (inverse of `mdToDoc`).
3
+ *
4
+ * Segments the flat text by `blocks[]` (raw chunks verbatim, heading lines
5
+ * prefixed, remaining lines as paragraphs), then serializes inline spans per
6
+ * segment via elementary runs + a close/reopen delimiter stack (valid nesting
7
+ * from arbitrarily overlapping spans).
8
+ *
9
+ * Round-trip contract: `mdToDoc(docToMd(doc))` is structurally equal to a
10
+ * *normalized* `doc` — emphasis markers, hard breaks (→ paragraph breaks), and
11
+ * blank lines normalize; `raw` blocks round-trip byte-for-byte.
12
+ */
13
+ import { computeRuns, markPriority } from './overlap.js';
14
+ export function docToMd(doc) {
15
+ const parts = [];
16
+ for (const seg of segmentize(doc)) {
17
+ if (seg.type === 'raw') {
18
+ parts.push(doc.text.slice(seg.start, seg.end));
19
+ continue;
20
+ }
21
+ const inline = serializeInline(doc, seg.start, seg.end, seg.type === 'paragraph');
22
+ if (seg.type === 'heading') {
23
+ const level = Math.min(6, Math.max(1, seg.level ?? 1));
24
+ parts.push(`${'#'.repeat(level)} ${inline}`);
25
+ }
26
+ else if (inline !== '') {
27
+ parts.push(inline);
28
+ }
29
+ // Empty paragraph lines are visual spacing only — they normalize away.
30
+ }
31
+ return parts.join('\n\n');
32
+ }
33
+ /** Cover the text with block segments; uncovered regions split per line into paragraphs. */
34
+ function segmentize(doc) {
35
+ const segments = [];
36
+ const text = doc.text;
37
+ const blocks = [...doc.blocks]
38
+ .map((b) => ({
39
+ ...b,
40
+ start: Math.max(0, Math.min(b.start, text.length)),
41
+ end: Math.max(0, Math.min(b.end, text.length)),
42
+ }))
43
+ .filter((b) => b.end >= b.start)
44
+ .sort((a, b) => a.start - b.start);
45
+ /**
46
+ * Split an uncovered region `[from, to)` into per-line paragraph segments.
47
+ * A `\n` ends the line before it; a region ending exactly on a separator
48
+ * `\n` emits no phantom trailing line.
49
+ */
50
+ const emitLines = (from, to) => {
51
+ let lineStart = from;
52
+ for (let i = from; i < to; i++) {
53
+ if (text[i] === '\n') {
54
+ segments.push({ start: lineStart, end: i, type: 'paragraph' });
55
+ lineStart = i + 1;
56
+ }
57
+ }
58
+ if (lineStart < to)
59
+ segments.push({ start: lineStart, end: to, type: 'paragraph' });
60
+ };
61
+ let cursor = 0;
62
+ for (const block of blocks) {
63
+ const start = Math.max(cursor, block.start);
64
+ if (start > cursor)
65
+ emitLines(cursor, start);
66
+ const end = Math.max(start, block.end);
67
+ // Block ranges include their trailing newline (paragraphRange
68
+ // semantics) — content excludes it.
69
+ const contentEnd = end > start && text[end - 1] === '\n' ? end - 1 : end;
70
+ if (block.type === 'heading') {
71
+ segments.push({ start, end: contentEnd, type: 'heading', level: block.level });
72
+ }
73
+ else if (block.type === 'raw') {
74
+ segments.push({ start, end: contentEnd, type: 'raw' });
75
+ }
76
+ else {
77
+ // Unknown/unstyled block types degrade to paragraphs (per line).
78
+ emitLines(start, contentEnd);
79
+ }
80
+ cursor = end;
81
+ }
82
+ if (cursor < text.length)
83
+ emitLines(cursor, text.length);
84
+ return segments;
85
+ }
86
+ /** Serialize one segment's text + spans into markdown inline syntax. */
87
+ function serializeInline(doc, start, end, escapeLineStart) {
88
+ const runs = computeRuns(doc.spans, start, end);
89
+ if (runs.length === 0) {
90
+ return escapeText(doc.text.slice(start, end), escapeLineStart);
91
+ }
92
+ // Extent-aware nesting (the ProseMirror-serializer trick): per run, order
93
+ // marks so the one that stays active the LONGEST sits outermost. A shorter
94
+ // mark is closed-and-reopened inside it, which avoids the unserializable
95
+ // adjacent-delimiter runs (`***`) that naive close/reopen produces.
96
+ const contEnd = [];
97
+ for (let i = runs.length - 1; i >= 0; i--) {
98
+ const map = new Map();
99
+ for (const mark of runs[i].active) {
100
+ const next = i + 1 < runs.length ? contEnd[i + 1].get(mark.key) : undefined;
101
+ map.set(mark.key, next !== undefined && runs[i + 1].start === runs[i].end ? next : runs[i].end);
102
+ }
103
+ contEnd[i] = map;
104
+ }
105
+ let out = '';
106
+ const open = [];
107
+ let first = true;
108
+ for (let i = 0; i < runs.length; i++) {
109
+ const run = runs[i];
110
+ const desired = [...run.active].sort((a, b) => {
111
+ const ea = contEnd[i].get(a.key) ?? run.end;
112
+ const eb = contEnd[i].get(b.key) ?? run.end;
113
+ return eb - ea || markPriority(a) - markPriority(b);
114
+ });
115
+ // Keep the common open-stack prefix; close the rest (innermost first).
116
+ let keep = 0;
117
+ while (keep < open.length && keep < desired.length && open[keep].key === desired[keep].key)
118
+ keep++;
119
+ for (let j = open.length - 1; j >= keep; j--)
120
+ out += delimiter(open[j], doc, 'close');
121
+ open.length = keep;
122
+ for (let j = keep; j < desired.length; j++) {
123
+ out += delimiter(desired[j], doc, 'open');
124
+ open.push(desired[j]);
125
+ }
126
+ const slice = doc.text.slice(run.start, run.end);
127
+ out += open.some((m) => m.type === 'code')
128
+ ? slice
129
+ : escapeText(slice, escapeLineStart && first && open.length === 0);
130
+ first = false;
131
+ }
132
+ for (let j = open.length - 1; j >= 0; j--)
133
+ out += delimiter(open[j], doc, 'close');
134
+ return out;
135
+ }
136
+ function delimiter(mark, doc, side) {
137
+ switch (mark.type) {
138
+ case 'bold':
139
+ return '**';
140
+ case 'italic':
141
+ // `_` (not `*`) so a bold/italic split never emits an ambiguous
142
+ // `***` delimiter run (`**a*b***c*` parses wrong; `**a_b_**_c_`
143
+ // can't collide). Trade-off: mid-word italic won't re-parse
144
+ // (intraword `_` rule) — documented normalization.
145
+ return '_';
146
+ case 'strike':
147
+ return '~~';
148
+ case 'code': {
149
+ // Widen the fence beyond any backtick run in the content.
150
+ const fence = '`'.repeat(maxBacktickRun(doc.text) + 1);
151
+ return fence.length > 1 ? fence : '`';
152
+ }
153
+ case 'link':
154
+ return side === 'open' ? '[' : `](${escapeLinkDest(String(mark.attrs?.href ?? ''))})`;
155
+ case 'mention':
156
+ // P3 — until the mention plugin lands, serialize as plain label.
157
+ return '';
158
+ default:
159
+ return '';
160
+ }
161
+ }
162
+ /**
163
+ * Escape a link destination so it re-parses to the same href (mirrors
164
+ * `scanLinkDest`'s two accepted forms):
165
+ *
166
+ * - bare form: backslash-escape `\`, `(`, `)`, `<` (a leading `<` would
167
+ * otherwise flip the parser into the angle branch) — valid as long as
168
+ * the destination has no whitespace;
169
+ * - whitespace anywhere → angle form `<…>`, whose only terminators are `>`
170
+ * and newline; those get percent-encoded (they're invalid raw in URLs
171
+ * anyway, so this is normalization, not loss).
172
+ */
173
+ function escapeLinkDest(href) {
174
+ if (/\s/.test(href)) {
175
+ return `<${href.replace(/>/g, '%3E').replace(/\n/g, '%0A')}>`;
176
+ }
177
+ return href.replace(/[\\()<]/g, (c) => `\\${c}`);
178
+ }
179
+ function maxBacktickRun(text) {
180
+ let max = 0;
181
+ let run = 0;
182
+ for (const ch of text) {
183
+ run = ch === '`' ? run + 1 : 0;
184
+ if (run > max)
185
+ max = run;
186
+ }
187
+ return max;
188
+ }
189
+ /** Escape markdown-significant characters in literal text. */
190
+ function escapeText(text, atLineStart) {
191
+ let out = text.replace(/([\\`*_~[\]])/g, '\\$1');
192
+ if (atLineStart) {
193
+ // Constructs that only bind at the start of a line.
194
+ out = out
195
+ .replace(/^(#{1,6})(\s)/, '\\$1$2')
196
+ .replace(/^>/, '\\>')
197
+ .replace(/^([-+])(\s)/, '\\$1$2')
198
+ .replace(/^(\d+)([.)])(\s)/, '$1\\$2$3');
199
+ }
200
+ return out;
201
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * markdown → {@link RichDoc} — flatten the parsed AST into the rich-text
3
+ * element's flat text+spans+blocks model.
4
+ *
5
+ * v1 models paragraphs, headings, and the inline set
6
+ * (bold/italic/strike/code/link). Everything else — lists, blockquotes, code
7
+ * blocks, tables, thematic breaks, and any paragraph containing an
8
+ * unrepresentable inline (images) — becomes a **`raw` block**: the original
9
+ * markdown source verbatim, edited as source and serialized back
10
+ * byte-for-byte. That's the lossless escape hatch that makes the round trip
11
+ * safe long before every node type is WYSIWYG-editable.
12
+ *
13
+ * Line convention (chat-style): every `\n` in `doc.text` is a paragraph
14
+ * boundary. Markdown hard breaks split into separate doc lines; `docToMd`
15
+ * serializes doc lines back as blank-line-separated paragraphs (hard breaks
16
+ * normalize to paragraph breaks — documented).
17
+ */
18
+ import type { RichDoc } from '@sigx/lynx-richtext';
19
+ export declare function mdToDoc(markdown: string, v?: number): RichDoc;
@@ -0,0 +1,187 @@
1
+ /**
2
+ * markdown → {@link RichDoc} — flatten the parsed AST into the rich-text
3
+ * element's flat text+spans+blocks model.
4
+ *
5
+ * v1 models paragraphs, headings, and the inline set
6
+ * (bold/italic/strike/code/link). Everything else — lists, blockquotes, code
7
+ * blocks, tables, thematic breaks, and any paragraph containing an
8
+ * unrepresentable inline (images) — becomes a **`raw` block**: the original
9
+ * markdown source verbatim, edited as source and serialized back
10
+ * byte-for-byte. That's the lossless escape hatch that makes the round trip
11
+ * safe long before every node type is WYSIWYG-editable.
12
+ *
13
+ * Line convention (chat-style): every `\n` in `doc.text` is a paragraph
14
+ * boundary. Markdown hard breaks split into separate doc lines; `docToMd`
15
+ * serializes doc lines back as blank-line-separated paragraphs (hard breaks
16
+ * normalize to paragraph breaks — documented).
17
+ */
18
+ import { parseBlocks } from '../../parser/blocks.js';
19
+ export function mdToDoc(markdown, v = 0) {
20
+ const ast = parseBlocks(markdown ?? '');
21
+ let text = '';
22
+ const spans = [];
23
+ const blocks = [];
24
+ /** Append one doc line (or multi-line raw chunk) plus its block attr. */
25
+ const push = (chunk, attr) => {
26
+ // Raw chunks may carry trailing blank lines consumed by the block
27
+ // parser (loose lists) — the inter-block separator is reconstructed
28
+ // by the serializer's join, so strip them here for stable round trips.
29
+ if (attr?.type === 'raw')
30
+ chunk = chunk.replace(/\n+$/, '');
31
+ const start = text.length;
32
+ text += chunk;
33
+ if (attr) {
34
+ // Range includes the trailing newline once it exists — matching how
35
+ // native paragraph ranges read back (paragraphRange semantics).
36
+ blocks.push({ start, end: text.length + 1, ...attr });
37
+ }
38
+ text += '\n';
39
+ };
40
+ for (const block of ast) {
41
+ switch (block.type) {
42
+ case 'paragraph': {
43
+ if (!inlineRepresentable(block.children)) {
44
+ push(block.raw, { type: 'raw' });
45
+ break;
46
+ }
47
+ for (const line of splitOnBreaks(block.children)) {
48
+ const flat = flattenInline(line, text.length);
49
+ spans.push(...flat.spans);
50
+ push(flat.text);
51
+ }
52
+ break;
53
+ }
54
+ case 'heading': {
55
+ if (!inlineRepresentable(block.children)) {
56
+ push(block.raw, { type: 'raw' });
57
+ break;
58
+ }
59
+ const flat = flattenInline(block.children, text.length);
60
+ spans.push(...flat.spans);
61
+ push(flat.text, { type: 'heading', level: block.level });
62
+ break;
63
+ }
64
+ default:
65
+ push(block.raw, { type: 'raw' });
66
+ }
67
+ }
68
+ // Drop the final separator newline; clamp any block range that reached
69
+ // past it (the "+1 for trailing newline" of the last block).
70
+ if (text.endsWith('\n'))
71
+ text = text.slice(0, -1);
72
+ for (const b of blocks) {
73
+ if (b.end > text.length)
74
+ b.end = text.length;
75
+ }
76
+ return { text, spans, blocks, v };
77
+ }
78
+ /** Inline node types the editor can model in-field. */
79
+ function inlineRepresentable(nodes) {
80
+ for (const node of nodes) {
81
+ switch (node.type) {
82
+ case 'text':
83
+ case 'br':
84
+ case 'codeSpan':
85
+ case 'autolink':
86
+ break;
87
+ case 'strong':
88
+ case 'em':
89
+ case 'del':
90
+ if (!inlineRepresentable(node.children))
91
+ return false;
92
+ break;
93
+ case 'link':
94
+ if (!inlineRepresentable(node.children))
95
+ return false;
96
+ break;
97
+ default:
98
+ return false; // image, future extension nodes
99
+ }
100
+ }
101
+ return true;
102
+ }
103
+ /** Split a paragraph's inline children into visual lines at top-level hard breaks. */
104
+ function splitOnBreaks(nodes) {
105
+ const lines = [];
106
+ let current = [];
107
+ for (const node of nodes) {
108
+ if (node.type === 'br') {
109
+ lines.push(current);
110
+ current = [];
111
+ }
112
+ else {
113
+ current.push(node);
114
+ }
115
+ }
116
+ lines.push(current);
117
+ return lines;
118
+ }
119
+ /** Depth-first flatten of an inline tree into text + overlapping spans. */
120
+ function flattenInline(nodes, base) {
121
+ let text = '';
122
+ const spans = [];
123
+ const walk = (list) => {
124
+ for (const node of list) {
125
+ switch (node.type) {
126
+ case 'text':
127
+ text += node.value;
128
+ break;
129
+ case 'br':
130
+ // Nested hard break (inside emphasis) — degrade to a space.
131
+ text += ' ';
132
+ break;
133
+ case 'codeSpan': {
134
+ const start = base + text.length;
135
+ text += node.value;
136
+ spans.push({ start, end: base + text.length, type: 'code' });
137
+ break;
138
+ }
139
+ case 'strong': {
140
+ const start = base + text.length;
141
+ walk(node.children);
142
+ spans.push({ start, end: base + text.length, type: 'bold' });
143
+ break;
144
+ }
145
+ case 'em': {
146
+ const start = base + text.length;
147
+ walk(node.children);
148
+ spans.push({ start, end: base + text.length, type: 'italic' });
149
+ break;
150
+ }
151
+ case 'del': {
152
+ const start = base + text.length;
153
+ walk(node.children);
154
+ spans.push({ start, end: base + text.length, type: 'strike' });
155
+ break;
156
+ }
157
+ case 'link': {
158
+ const start = base + text.length;
159
+ walk(node.children);
160
+ spans.push({
161
+ start,
162
+ end: base + text.length,
163
+ type: 'link',
164
+ attrs: { href: node.href },
165
+ });
166
+ break;
167
+ }
168
+ case 'autolink': {
169
+ const start = base + text.length;
170
+ text += node.value;
171
+ spans.push({
172
+ start,
173
+ end: base + text.length,
174
+ type: 'link',
175
+ attrs: { href: node.href },
176
+ });
177
+ break;
178
+ }
179
+ default:
180
+ break; // unreachable — filtered by inlineRepresentable
181
+ }
182
+ }
183
+ };
184
+ walk(nodes);
185
+ // Zero-length spans (empty emphasis) carry no information — drop them.
186
+ return { text, spans: spans.filter((s) => s.end > s.start) };
187
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Elementary-run computation for inline serialization.
3
+ *
4
+ * The doc model permits arbitrarily overlapping spans; markdown needs properly
5
+ * nested delimiters. Step one of the serializer is to slice a text range into
6
+ * **elementary runs** — maximal intervals over which the set of active marks
7
+ * is constant. The emitter (docToMd) then walks the runs with a close/reopen
8
+ * stack, which is guaranteed to produce valid nesting.
9
+ */
10
+ import type { InlineSpan, InlineSpanType } from '@sigx/lynx-richtext';
11
+ export interface ActiveMark {
12
+ type: InlineSpanType;
13
+ /** Identity key — distinguishes links by href so adjacent different links don't merge. */
14
+ key: string;
15
+ attrs?: Record<string, string>;
16
+ }
17
+ export interface Run {
18
+ start: number;
19
+ end: number;
20
+ /** Marks active over the whole run. */
21
+ active: ActiveMark[];
22
+ }
23
+ export declare function markPriority(mark: ActiveMark): number;
24
+ /**
25
+ * Slice `[start, end)` into elementary runs for the given spans.
26
+ *
27
+ * `code` is terminal: within a code span every other mark except an enclosing
28
+ * `link` is suppressed (markdown cannot style inside code spans, but
29
+ * `[`code`](href)` is valid).
30
+ */
31
+ export declare function computeRuns(spans: InlineSpan[], start: number, end: number): Run[];
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Elementary-run computation for inline serialization.
3
+ *
4
+ * The doc model permits arbitrarily overlapping spans; markdown needs properly
5
+ * nested delimiters. Step one of the serializer is to slice a text range into
6
+ * **elementary runs** — maximal intervals over which the set of active marks
7
+ * is constant. The emitter (docToMd) then walks the runs with a close/reopen
8
+ * stack, which is guaranteed to produce valid nesting.
9
+ */
10
+ /** Outer-to-inner nesting priority (lower index opens first / sits outermost). */
11
+ const PRIORITY = { link: 0, code: 1, bold: 2, italic: 3, strike: 4 };
12
+ export function markPriority(mark) {
13
+ return PRIORITY[mark.type] ?? 99;
14
+ }
15
+ /**
16
+ * Slice `[start, end)` into elementary runs for the given spans.
17
+ *
18
+ * `code` is terminal: within a code span every other mark except an enclosing
19
+ * `link` is suppressed (markdown cannot style inside code spans, but
20
+ * `[`code`](href)` is valid).
21
+ */
22
+ export function computeRuns(spans, start, end) {
23
+ if (end <= start)
24
+ return [];
25
+ const relevant = spans
26
+ .filter((s) => s.end > start && s.start < end && s.end > s.start)
27
+ .map((s) => ({
28
+ start: Math.max(s.start, start),
29
+ end: Math.min(s.end, end),
30
+ mark: toMark(s),
31
+ }));
32
+ const boundaries = new Set([start, end]);
33
+ for (const s of relevant) {
34
+ boundaries.add(s.start);
35
+ boundaries.add(s.end);
36
+ }
37
+ const points = [...boundaries].sort((a, b) => a - b);
38
+ const runs = [];
39
+ for (let i = 0; i < points.length - 1; i++) {
40
+ const a = points[i];
41
+ const b = points[i + 1];
42
+ if (b <= a)
43
+ continue;
44
+ let active = relevant
45
+ .filter((s) => s.start <= a && s.end >= b)
46
+ .map((s) => s.mark);
47
+ active = dedupe(active);
48
+ if (active.some((m) => m.type === 'code')) {
49
+ active = active.filter((m) => m.type === 'code' || m.type === 'link');
50
+ }
51
+ active.sort((x, y) => markPriority(x) - markPriority(y) || x.key.localeCompare(y.key));
52
+ runs.push({ start: a, end: b, active });
53
+ }
54
+ return runs;
55
+ }
56
+ function toMark(span) {
57
+ const key = span.type === 'link' ? `link:${span.attrs?.href ?? ''}` : span.type;
58
+ return { type: span.type, key, ...(span.attrs ? { attrs: span.attrs } : {}) };
59
+ }
60
+ function dedupe(marks) {
61
+ const seen = new Set();
62
+ const out = [];
63
+ for (const m of marks) {
64
+ if (!seen.has(m.key)) {
65
+ seen.add(m.key);
66
+ out.push(m);
67
+ }
68
+ }
69
+ return out;
70
+ }
package/dist/index.d.ts CHANGED
@@ -1,15 +1,15 @@
1
- import './jsx-augment.js';
2
1
  export { MarkdownView } from './render/MarkdownView.js';
3
2
  export type { MarkdownViewProps } from './render/MarkdownView.js';
4
3
  export { defaultComponents } from './render/components.js';
5
4
  export type { MarkdownComponents, MarkdownChild, RootProps, HeadingProps, ParagraphProps, BlockquoteProps, ListProps, ListItemProps, CodeProps, ThematicBreakProps, TableProps, TableRowProps, TableCellProps, StrongProps, EmProps, DelProps, CodeSpanProps, LinkProps, AutolinkProps, ImageProps, } from './render/components.js';
5
+ export { MarkdownEditor } from './editor/MarkdownEditor.js';
6
+ export type { MarkdownEditorProps, MarkdownEditorController, MarkdownEditorMode, } from './editor/MarkdownEditor.js';
7
+ export { mdToDoc } from './editor/convert/mdToDoc.js';
8
+ export { docToMd } from './editor/convert/docToMd.js';
6
9
  export { createMarkdownStream } from './stream.js';
7
10
  export type { MarkdownStream, CreateMarkdownStreamOptions } from './stream.js';
8
- export { XMarkdown } from './XMarkdown.js';
9
- export type { XMarkdownProps, XMarkdownEffect } from './XMarkdown.js';
10
11
  export { createIncrementalEngine } from './parser/incremental.js';
11
12
  export type { IncrementalEngine } from './parser/incremental.js';
12
13
  export { parseBlocks } from './parser/blocks.js';
13
14
  export { parseInline } from './parser/inline.js';
14
15
  export type * from './ast.js';
15
- export type { XMarkdownAttributes, MarkdownLinkEvent, MarkdownLinkEventDetail, MarkdownImageTapEvent, MarkdownImageTapEventDetail, MarkdownParseEndEvent, MarkdownParseEndEventDetail, } from './jsx-augment.js';
package/dist/index.js CHANGED
@@ -1,13 +1,15 @@
1
- import './jsx-augment.js';
2
1
  // Primary: the SignalX-native streaming renderer. (An editable `MarkdownEditor`
3
2
  // is planned as a sibling export.)
4
3
  export { MarkdownView } from './render/MarkdownView.js';
5
4
  // Generic render-function override API (design systems plug in here).
6
5
  export { defaultComponents } from './render/components.js';
6
+ // True-WYSIWYG editor on the native <sigx-richtext> element
7
+ // (requires the optional @sigx/lynx-richtext peer).
8
+ export { MarkdownEditor } from './editor/MarkdownEditor.js';
9
+ export { mdToDoc } from './editor/convert/mdToDoc.js';
10
+ export { docToMd } from './editor/convert/docToMd.js';
7
11
  // Streaming controller for AI token loops.
8
12
  export { createMarkdownStream } from './stream.js';
9
- // Native `<x-markdown>` wrapper, preserved for platforms that ship the element.
10
- export { XMarkdown } from './XMarkdown.js';
11
13
  // Parser primitives (for advanced consumers / testing).
12
14
  export { createIncrementalEngine } from './parser/incremental.js';
13
15
  export { parseBlocks } from './parser/blocks.js';
@@ -2,9 +2,8 @@
2
2
  * `<MarkdownView>` — a SignalX-native, streaming-aware markdown renderer.
3
3
  *
4
4
  * Parses markdown in JS (zero dependencies) and renders to Lynx primitives, so
5
- * it works identically on every platform unlike {@link XMarkdown}, which wraps
6
- * the platform-gated native `<x-markdown>` element. For an editable counterpart,
7
- * see `MarkdownEditor`.
5
+ * it works identically on every platform. For an editable counterpart, see
6
+ * `MarkdownEditor`.
8
7
  *
9
8
  * Rendering is **generic**: the package ships neutral, theme-agnostic defaults
10
9
  * and exposes a `components` map so any design system can fully control the look
@@ -2,9 +2,8 @@
2
2
  * `<MarkdownView>` — a SignalX-native, streaming-aware markdown renderer.
3
3
  *
4
4
  * Parses markdown in JS (zero dependencies) and renders to Lynx primitives, so
5
- * it works identically on every platform unlike {@link XMarkdown}, which wraps
6
- * the platform-gated native `<x-markdown>` element. For an editable counterpart,
7
- * see `MarkdownEditor`.
5
+ * it works identically on every platform. For an editable counterpart, see
6
+ * `MarkdownEditor`.
8
7
  *
9
8
  * Rendering is **generic**: the package ships neutral, theme-agnostic defaults
10
9
  * and exposes a `components` map so any design system can fully control the look
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sigx/lynx-markdown",
3
- "version": "0.4.6",
4
- "description": "SignalX-native streaming markdown renderer for Lynx. Parses markdown in JS (zero deps) and renders to native <view>/<text> primitives, with incremental streaming for AI output. Also ships XMarkdown, the native <x-markdown> wrapper.",
3
+ "version": "0.4.8",
4
+ "description": "SignalX-native streaming markdown renderer for Lynx. Parses markdown in JS (zero deps) and renders to native <view>/<text> primitives, with incremental streaming for AI output.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -19,13 +19,20 @@
19
19
  "LICENSE"
20
20
  ],
21
21
  "peerDependencies": {
22
- "@sigx/lynx": "^0.4.6"
22
+ "@sigx/lynx": "^0.4.8",
23
+ "@sigx/lynx-richtext": "^0.4.8"
24
+ },
25
+ "peerDependenciesMeta": {
26
+ "@sigx/lynx-richtext": {
27
+ "optional": true
28
+ }
23
29
  },
24
30
  "devDependencies": {
25
31
  "@typescript/native-preview": "7.0.0-dev.20260521.1",
26
32
  "typescript": "^6.0.3",
27
- "@sigx/lynx": "^0.4.6",
28
- "@sigx/lynx-testing": "^0.4.6"
33
+ "@sigx/lynx": "^0.4.8",
34
+ "@sigx/lynx-richtext": "^0.4.8",
35
+ "@sigx/lynx-testing": "^0.4.8"
29
36
  },
30
37
  "author": "Andreas Ekdahl",
31
38
  "license": "MIT",
@@ -48,8 +55,7 @@
48
55
  "markdown",
49
56
  "native",
50
57
  "streaming",
51
- "ai",
52
- "x-markdown"
58
+ "ai"
53
59
  ],
54
60
  "scripts": {
55
61
  "build": "node ../../scripts/clean.mjs dist && tsgo",
@@ -1,36 +0,0 @@
1
- import { type Define } from '@sigx/lynx';
2
- import './jsx-augment.js';
3
- import type { MarkdownLinkEvent, MarkdownImageTapEvent, MarkdownParseEndEvent } from './jsx-augment.js';
4
- export type XMarkdownEffect = 'typewriter' | 'none' | (string & {});
5
- export type XMarkdownProps = Define.Prop<'value', string, false> & Define.Prop<'effect', XMarkdownEffect, false> & Define.Prop<'attachments', ReadonlyArray<unknown>, false> & Define.Prop<'class', string, false> & Define.Prop<'style', string | Record<string, string | number>, false> & Define.Prop<'onLink', (e: MarkdownLinkEvent) => void, false> & Define.Prop<'onImageTap', (e: MarkdownImageTapEvent) => void, false> & Define.Prop<'onParseEnd', (e: MarkdownParseEndEvent) => void, false>;
6
- /**
7
- * Render a markdown document using Lynx's native `<x-markdown>` XElement.
8
- *
9
- * This is the thin wrapper over the platform's native markdown element. It is
10
- * fast where available but platform-gated (Harmony 3.7.0+, Android 3.8.0-rc.0+,
11
- * iOS not yet in a tagged release) and opaque — the engine owns parsing and
12
- * styling. For a cross-platform, fully-controllable, streaming-aware renderer
13
- * built on Lynx `<view>`/`<text>` primitives, use {@link Markdown} instead.
14
- *
15
- * The markdown source is passed via the `value` prop; it is delivered to the
16
- * native element as a raw-text child (per the 3.7.0 "raw-text node
17
- * optimization" path). Event props use signalx's automatic
18
- * `onLink`→`bindlink` mapping in `nodeOps.parseEventProp`, so handlers wire
19
- * up without any per-event glue.
20
- *
21
- * @example
22
- * ```tsx
23
- * <XMarkdown
24
- * value={"# Hello\n\nThis is **markdown**."}
25
- * effect="typewriter"
26
- * onLink={(e) => console.log('tapped', e.detail.url)}
27
- * />
28
- * ```
29
- *
30
- * @remarks
31
- * Availability of the `<x-markdown>` element is platform-dependent — see
32
- * `jsx-augment.ts` for the per-platform schedule. On platforms where the
33
- * native element is not registered, the engine logs a warning and renders
34
- * no view; there is no JS-side feature gate.
35
- */
36
- export declare const XMarkdown: import("@sigx/runtime-core").ComponentFactory<XMarkdownProps, void, {}>;
package/dist/XMarkdown.js DELETED
@@ -1,36 +0,0 @@
1
- import { jsx as _jsx } from "@sigx/lynx/jsx-runtime";
2
- import { component } from '@sigx/lynx';
3
- import './jsx-augment.js';
4
- /**
5
- * Render a markdown document using Lynx's native `<x-markdown>` XElement.
6
- *
7
- * This is the thin wrapper over the platform's native markdown element. It is
8
- * fast where available but platform-gated (Harmony 3.7.0+, Android 3.8.0-rc.0+,
9
- * iOS not yet in a tagged release) and opaque — the engine owns parsing and
10
- * styling. For a cross-platform, fully-controllable, streaming-aware renderer
11
- * built on Lynx `<view>`/`<text>` primitives, use {@link Markdown} instead.
12
- *
13
- * The markdown source is passed via the `value` prop; it is delivered to the
14
- * native element as a raw-text child (per the 3.7.0 "raw-text node
15
- * optimization" path). Event props use signalx's automatic
16
- * `onLink`→`bindlink` mapping in `nodeOps.parseEventProp`, so handlers wire
17
- * up without any per-event glue.
18
- *
19
- * @example
20
- * ```tsx
21
- * <XMarkdown
22
- * value={"# Hello\n\nThis is **markdown**."}
23
- * effect="typewriter"
24
- * onLink={(e) => console.log('tapped', e.detail.url)}
25
- * />
26
- * ```
27
- *
28
- * @remarks
29
- * Availability of the `<x-markdown>` element is platform-dependent — see
30
- * `jsx-augment.ts` for the per-platform schedule. On platforms where the
31
- * native element is not registered, the engine logs a warning and renders
32
- * no view; there is no JS-side feature gate.
33
- */
34
- export const XMarkdown = component(({ props }) => {
35
- return () => (_jsx("x-markdown", { "markdown-effect": props.effect, "text-mark-attachments": props.attachments, class: props.class, style: props.style, bindlink: props.onLink, bindimageTap: props.onImageTap, bindparseEnd: props.onParseEnd, children: props.value ?? '' }));
36
- });
@@ -1,83 +0,0 @@
1
- /**
2
- * JSX intrinsic type augmentation for Lynx's `<x-markdown>` XElement.
3
- *
4
- * Importing this module registers `'x-markdown'` as a valid JSX intrinsic
5
- * with its 3.7.0+ attributes and events. Pulled in automatically by
6
- * `@sigx/lynx-markdown`'s entry point so consumers do not need to import
7
- * it directly.
8
- *
9
- * The native element ships per-platform on different schedules:
10
- * - Harmony: available since 3.7.0
11
- * - Android: available since 3.8.0-rc.0 (artifact `lynx_xelement_markdown`)
12
- * - iOS: not yet in any tagged release; lands on the main branch
13
- * post-3.8.0
14
- *
15
- * `<x-markdown>` props in JSX still type-check on every platform. At
16
- * runtime the SignalX renderer issues a `__CreateElement('x-markdown')`
17
- * op unconditionally; on platforms where the native element is not
18
- * registered, the underlying engine handles the unknown tag (today it
19
- * logs a warning and emits no view). There is no JS-side feature gate
20
- * in this package — once you upgrade to a Lynx release that ships the
21
- * element on your target platforms, rendering activates automatically.
22
- */
23
- import type { LynxCommonAttributes, LynxEventHandler } from '@sigx/lynx-runtime';
24
- /** Detail payload of `bindlink` — the engine ships `url` plus optional fields. */
25
- export interface MarkdownLinkEventDetail {
26
- url: string;
27
- [k: string]: unknown;
28
- }
29
- export interface MarkdownLinkEvent {
30
- type: 'link';
31
- detail: MarkdownLinkEventDetail;
32
- }
33
- /** Detail payload of `bindimageTap`. */
34
- export interface MarkdownImageTapEventDetail {
35
- src: string;
36
- [k: string]: unknown;
37
- }
38
- export interface MarkdownImageTapEvent {
39
- type: 'imageTap';
40
- detail: MarkdownImageTapEventDetail;
41
- }
42
- /** Detail payload of `bindparseEnd`. */
43
- export interface MarkdownParseEndEventDetail {
44
- [k: string]: unknown;
45
- }
46
- export interface MarkdownParseEndEvent {
47
- type: 'parseEnd';
48
- detail: MarkdownParseEndEventDetail;
49
- }
50
- export interface XMarkdownAttributes extends LynxCommonAttributes {
51
- /**
52
- * Raw markdown source. Lynx parses the first text child of
53
- * `<x-markdown>` per the 3.7.0 raw-text-node optimization. Passing a
54
- * single string here is the common path; JSX expressions resolving to
55
- * a string also work.
56
- */
57
- children?: any;
58
- /**
59
- * Render-time effect applied to the parsed markdown output.
60
- * Known values: `'typewriter'`, `'none'`. The engine treats unknown
61
- * strings as `'none'`.
62
- */
63
- 'markdown-effect'?: string;
64
- /**
65
- * Inline view attachments referenced by markdown text marks. Shape is
66
- * engine-defined; passed through as-is.
67
- */
68
- 'text-mark-attachments'?: ReadonlyArray<unknown>;
69
- /** Fires when the user taps an `[anchor](url)` link. */
70
- bindlink?: LynxEventHandler<MarkdownLinkEvent>;
71
- /** Fires when the user taps an inline image. */
72
- bindimageTap?: LynxEventHandler<MarkdownImageTapEvent>;
73
- /** Fires once the engine finishes parsing the source. */
74
- bindparseEnd?: LynxEventHandler<MarkdownParseEndEvent>;
75
- }
76
- declare global {
77
- namespace JSX {
78
- interface IntrinsicElements {
79
- 'x-markdown': XMarkdownAttributes;
80
- }
81
- }
82
- }
83
- export {};
@@ -1 +0,0 @@
1
- export {};