@sigx/lynx-markdown 0.4.5 → 0.4.7

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
@@ -1,26 +1,17 @@
1
1
  # @sigx/lynx-markdown
2
2
 
3
- Typed SignalX wrapper around Lynx's `<x-markdown>` XElement.
3
+ A **SignalX-native, streaming-aware markdown renderer** for Lynx.
4
4
 
5
- ## Status
5
+ It parses markdown in JavaScript (zero dependencies) and renders to native Lynx
6
+ `<view>`/`<text>`/`<image>` primitives — so it renders **identically on every
7
+ platform** (iOS, Android, Harmony) and is fully controllable from JS. Built for
8
+ AI output: as the source string grows token-by-token, finalized blocks keep a
9
+ stable identity and are never re-parsed or remounted, so completed content
10
+ doesn't flicker or reflow while new tokens stream in.
6
11
 
7
- **Pre-release** the wrapper compiles and types cleanly today, but the
8
- underlying native element ships per-platform on different schedules:
9
-
10
- | Platform | First version | Notes |
11
- | -------- | -------------- | ---------------------------------------------- |
12
- | Harmony | Lynx 3.7.0 | Stable. |
13
- | Android | Lynx 3.8.0-rc.0 | Ships as `org.lynxsdk.lynx:lynx_xelement_markdown`. |
14
- | iOS | (post-3.8.0) | Currently only on the upstream `main` branch. |
15
-
16
- On platforms where `<x-markdown>` is not yet registered, the component
17
- renders nothing at runtime. This package exists so that app code can
18
- already adopt the typed API surface — once you bump SignalX's Lynx pins
19
- to a release that includes the native element on your target platforms,
20
- it starts rendering without code changes.
21
-
22
- When the iOS/Android pods land in stable, this README will be updated
23
- with the matching native dependency wiring (Podfile + gradle).
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.
24
15
 
25
16
  ## Install
26
17
 
@@ -31,41 +22,120 @@ pnpm add @sigx/lynx-markdown
31
22
  ## Use
32
23
 
33
24
  ```tsx
34
- import { Markdown } from '@sigx/lynx-markdown';
25
+ import { MarkdownView } from '@sigx/lynx-markdown';
35
26
 
36
27
  export default function ArticleScreen() {
37
28
  return (
38
- <Markdown
39
- value={"# Hello\n\nThis is **markdown**."}
40
- effect="typewriter"
41
- onLink={(e) => console.log('tapped', e.detail.url)}
42
- onParseEnd={() => console.log('parsed')}
29
+ <MarkdownView
30
+ value={'# Hello\n\nThis is **markdown** with a [link](https://signalx.dev).'}
31
+ onLink={(href) => openUrl(href)}
43
32
  />
44
33
  );
45
34
  }
46
35
  ```
47
36
 
48
- ## Props
37
+ ### Streaming AI output
38
+
39
+ `createMarkdownStream()` bridges a token loop to `<MarkdownView>` in one line. It
40
+ owns a reactive `value` signal and coalesces bursts of tokens into a bounded
41
+ number of re-renders.
42
+
43
+ ```tsx
44
+ import { MarkdownView, createMarkdownStream } from '@sigx/lynx-markdown';
45
+
46
+ const md = createMarkdownStream({ flushIntervalMs: 16 }); // ~60fps cap
47
+
48
+ // producer — your AI completion loop
49
+ for await (const token of completion) md.append(token);
50
+ md.done();
51
+
52
+ // consumer
53
+ <MarkdownView value={md.value.value} />;
54
+ ```
55
+
56
+ `MarkdownStream` API:
57
+
58
+ | Member | Description |
59
+ | ------------------ | ------------------------------------------------------- |
60
+ | `value` | Reactive accumulated source (`PrimitiveSignal<string>`).|
61
+ | `finished` | Reactive flag, set by `done()`. |
62
+ | `append(chunk)` | Append a token/chunk; buffered + coalesced into `value`.|
63
+ | `done()` | Flush the buffer and mark complete. |
64
+ | `reset()` | Clear buffer/`value`/`finished` (e.g. for a regenerate).|
65
+
66
+ ## Supported syntax
67
+
68
+ Core CommonMark + GFM:
69
+
70
+ - Headings (`#`…`######`)
71
+ - Paragraphs with soft-wrap joining
72
+ - **Bold**, _italic_, ~~strikethrough~~, `inline code`
73
+ - Links, images, angle (`<url>`) and bare (`https://…`, `www.…`) autolinks
74
+ - Ordered and unordered nested lists, GFM task lists (`- [ ]` / `- [x]`)
75
+ - Blockquotes (with nested blocks)
76
+ - Fenced code blocks (with language label); unterminated fences render while
77
+ streaming and close cleanly when the final fence arrives
78
+ - Thematic breaks (`---`, `***`, `___`)
79
+ - GFM tables with per-column alignment
80
+
81
+ Not supported (renders as literal text): raw HTML, reference-style links,
82
+ setext headings, syntax highlighting. Link hrefs are sanitized — only
83
+ `http(s):`, `mailto:`, `tel:` and scheme-less links are exposed to `onLink`;
84
+ `javascript:`/`data:` collapse to `#`.
85
+
86
+ ### `<MarkdownView>` props
87
+
88
+ | Prop | Type | Description |
89
+ | ------------- | ----------------------------- | -------------------------------------------- |
90
+ | `value` | `string` | Markdown source (reactive). |
91
+ | `onLink` | `(href: string) => void` | Fired when a link/autolink is tapped. |
92
+ | `onImageTap` | `(src: string) => void` | Fired when an image is tapped. |
93
+ | `components` | `Partial<MarkdownComponents>` | Per-node-type render-function overrides. |
94
+
95
+ ### Theming with `components`
96
+
97
+ `MarkdownView` is **design-system agnostic**: it ships neutral, theme-agnostic
98
+ defaults and walks the AST calling a render function per node type. Override any
99
+ subset to control the element, styling, and layout — the engine pre-renders each
100
+ node's `children` and keeps the stable streaming keys regardless of what you
101
+ return.
49
102
 
50
- | Prop | Type | Notes |
51
- | ------------- | --------------------------------------- | --------------------------------------------------- |
52
- | `value` | `string` | Markdown source passed as a raw-text child. |
53
- | `effect` | `'typewriter' \| 'none' \| string` | Maps to the `markdown-effect` attribute. |
54
- | `attachments` | `ReadonlyArray<unknown>` | Maps to `text-mark-attachments`. Engine-defined. |
55
- | `class` | `string` | |
56
- | `style` | `string \| Record<string, ...>` | |
57
- | `onLink` | `(e: MarkdownLinkEvent) => void` | Underlying `bindlink`. |
58
- | `onImageTap` | `(e: MarkdownImageTapEvent) => void` | Underlying `bindimageTap`. |
59
- | `onParseEnd` | `(e: MarkdownParseEndEvent) => void` | Underlying `bindparseEnd`. |
103
+ ```tsx
104
+ <MarkdownView
105
+ value={src}
106
+ components={{
107
+ heading: ({ level, children }) => <Heading level={level}>{children}</Heading>,
108
+ strong: ({ children }) => <text class="font-bold">{children}</text>,
109
+ }}
110
+ />
111
+ ```
112
+
113
+ For a ready-made daisyUI mapping, pass `markdownComponents` from
114
+ `@sigx/lynx-daisyui`:
115
+
116
+ ```tsx
117
+ import { MarkdownView } from '@sigx/lynx-markdown';
118
+ import { markdownComponents } from '@sigx/lynx-daisyui';
119
+
120
+ <MarkdownView value={src} components={markdownComponents} />;
121
+ ```
122
+
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):
60
127
 
61
- ## Native element methods
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. |
62
133
 
63
- The underlying `<x-markdown>` exposes UI methods you can invoke via a
64
- `main-thread:ref`:
134
+ On platforms where `<x-markdown>` is not registered, it renders nothing. Prefer
135
+ `MarkdownView` for cross-platform, streaming, and customizable rendering.
65
136
 
66
- - `getContent`, `getParseResult`, `getImages`
67
- - `pauseAnimation`, `resumeAnimation`, `clearStatus`
68
- - `getTextBoundingRect`, `setTextSelection`, `getSelectedText`
137
+ ```tsx
138
+ import { XMarkdown } from '@sigx/lynx-markdown';
69
139
 
70
- These are not yet wrapped by the component; drop down to the intrinsic
71
- `<x-markdown>` element with a ref to call them directly.
140
+ <XMarkdown value={'# Hello'} effect="typewriter" onLink={(e) => open(e.detail.url)} />;
141
+ ```
@@ -0,0 +1,36 @@
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, {}>;
@@ -2,7 +2,13 @@ import { jsx as _jsx } from "@sigx/lynx/jsx-runtime";
2
2
  import { component } from '@sigx/lynx';
3
3
  import './jsx-augment.js';
4
4
  /**
5
- * Render a markdown document using Lynx's `<x-markdown>` XElement.
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.
6
12
  *
7
13
  * The markdown source is passed via the `value` prop; it is delivered to the
8
14
  * native element as a raw-text child (per the 3.7.0 "raw-text node
@@ -12,7 +18,7 @@ import './jsx-augment.js';
12
18
  *
13
19
  * @example
14
20
  * ```tsx
15
- * <Markdown
21
+ * <XMarkdown
16
22
  * value={"# Hello\n\nThis is **markdown**."}
17
23
  * effect="typewriter"
18
24
  * onLink={(e) => console.log('tapped', e.detail.url)}
@@ -25,6 +31,6 @@ import './jsx-augment.js';
25
31
  * native element is not registered, the engine logs a warning and renders
26
32
  * no view; there is no JS-side feature gate.
27
33
  */
28
- export const Markdown = component(({ props }) => {
34
+ export const XMarkdown = component(({ props }) => {
29
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 ?? '' }));
30
36
  });
package/dist/ast.d.ts ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * AST node types for the SignalX-native markdown renderer.
3
+ *
4
+ * The tree is intentionally small and flat: the parser produces it, the render
5
+ * layer maps it to Lynx `<view>`/`<text>`/`<image>` intrinsics, and the
6
+ * incremental engine memoizes finalized {@link BlockNode}s by reference. Every
7
+ * block carries a `key` (assigned by the incremental layer for stable
8
+ * reconciliation) and `raw` (the exact source slice, used for memo equality and
9
+ * introspection).
10
+ */
11
+ export type InlineNode = InlineText | InlineStrong | InlineEm | InlineDel | InlineCodeSpan | InlineLink | InlineImage | InlineAutolink | InlineBreak;
12
+ /** A run of literal text. */
13
+ export interface InlineText {
14
+ type: 'text';
15
+ value: string;
16
+ }
17
+ /** `**bold**` / `__bold__`. */
18
+ export interface InlineStrong {
19
+ type: 'strong';
20
+ children: InlineNode[];
21
+ }
22
+ /** `*italic*` / `_italic_`. */
23
+ export interface InlineEm {
24
+ type: 'em';
25
+ children: InlineNode[];
26
+ }
27
+ /** GFM `~~strikethrough~~`. */
28
+ export interface InlineDel {
29
+ type: 'del';
30
+ children: InlineNode[];
31
+ }
32
+ /** `` `code` `` — content is literal (no nested inline parsing). */
33
+ export interface InlineCodeSpan {
34
+ type: 'codeSpan';
35
+ value: string;
36
+ }
37
+ /** `[text](href "title")`. */
38
+ export interface InlineLink {
39
+ type: 'link';
40
+ href: string;
41
+ title?: string;
42
+ children: InlineNode[];
43
+ }
44
+ /** `![alt](src "title")`. */
45
+ export interface InlineImage {
46
+ type: 'image';
47
+ src: string;
48
+ alt: string;
49
+ title?: string;
50
+ }
51
+ /** `<https://…>` / `<email>` / GFM bare-URL autolink. */
52
+ export interface InlineAutolink {
53
+ type: 'autolink';
54
+ href: string;
55
+ value: string;
56
+ }
57
+ /** A hard line break (two trailing spaces or a trailing backslash). */
58
+ export interface InlineBreak {
59
+ type: 'br';
60
+ }
61
+ export interface BlockBase {
62
+ /** Stable reconciliation key, assigned by the incremental engine. */
63
+ key: string;
64
+ /** Exact source slice this block was parsed from. */
65
+ raw: string;
66
+ }
67
+ export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
68
+ export type TableAlign = 'left' | 'center' | 'right';
69
+ export type BlockNode = HeadingBlock | ParagraphBlock | BlockquoteBlock | ListBlock | CodeBlock | ThematicBreakBlock | TableBlock;
70
+ export interface HeadingBlock extends BlockBase {
71
+ type: 'heading';
72
+ level: HeadingLevel;
73
+ children: InlineNode[];
74
+ }
75
+ export interface ParagraphBlock extends BlockBase {
76
+ type: 'paragraph';
77
+ children: InlineNode[];
78
+ }
79
+ export interface BlockquoteBlock extends BlockBase {
80
+ type: 'blockquote';
81
+ children: BlockNode[];
82
+ }
83
+ export interface ListBlock extends BlockBase {
84
+ type: 'list';
85
+ ordered: boolean;
86
+ /** Starting number for ordered lists (1 for unordered). */
87
+ start: number;
88
+ /** Tight lists have no blank lines between items → tighter spacing. */
89
+ tight: boolean;
90
+ items: ListItem[];
91
+ }
92
+ export interface ListItem {
93
+ key: string;
94
+ /** GFM task list state: `true`/`false`, or `null` when not a task item. */
95
+ checked: boolean | null;
96
+ children: BlockNode[];
97
+ }
98
+ export interface CodeBlock extends BlockBase {
99
+ type: 'codeBlock';
100
+ lang?: string;
101
+ value: string;
102
+ /** `false` while the fence is still open (streaming, or unterminated). */
103
+ closed: boolean;
104
+ }
105
+ export interface ThematicBreakBlock extends BlockBase {
106
+ type: 'thematicBreak';
107
+ }
108
+ export interface TableBlock extends BlockBase {
109
+ type: 'table';
110
+ /** Per-column alignment; `null` = default (left). */
111
+ align: (TableAlign | null)[];
112
+ header: TableCell[];
113
+ rows: TableCell[][];
114
+ }
115
+ export interface TableCell {
116
+ children: InlineNode[];
117
+ }
package/dist/ast.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * AST node types for the SignalX-native markdown renderer.
3
+ *
4
+ * The tree is intentionally small and flat: the parser produces it, the render
5
+ * layer maps it to Lynx `<view>`/`<text>`/`<image>` intrinsics, and the
6
+ * incremental engine memoizes finalized {@link BlockNode}s by reference. Every
7
+ * block carries a `key` (assigned by the incremental layer for stable
8
+ * reconciliation) and `raw` (the exact source slice, used for memo equality and
9
+ * introspection).
10
+ */
11
+ export {};
package/dist/index.d.ts CHANGED
@@ -1,4 +1,15 @@
1
1
  import './jsx-augment.js';
2
- export { Markdown } from './Markdown.js';
3
- export type { MarkdownProps, MarkdownEffect } from './Markdown.js';
2
+ export { MarkdownView } from './render/MarkdownView.js';
3
+ export type { MarkdownViewProps } from './render/MarkdownView.js';
4
+ export { defaultComponents } from './render/components.js';
5
+ 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';
6
+ export { createMarkdownStream } from './stream.js';
7
+ export type { MarkdownStream, CreateMarkdownStreamOptions } from './stream.js';
8
+ export { XMarkdown } from './XMarkdown.js';
9
+ export type { XMarkdownProps, XMarkdownEffect } from './XMarkdown.js';
10
+ export { createIncrementalEngine } from './parser/incremental.js';
11
+ export type { IncrementalEngine } from './parser/incremental.js';
12
+ export { parseBlocks } from './parser/blocks.js';
13
+ export { parseInline } from './parser/inline.js';
14
+ export type * from './ast.js';
4
15
  export type { XMarkdownAttributes, MarkdownLinkEvent, MarkdownLinkEventDetail, MarkdownImageTapEvent, MarkdownImageTapEventDetail, MarkdownParseEndEvent, MarkdownParseEndEventDetail, } from './jsx-augment.js';
package/dist/index.js CHANGED
@@ -1,2 +1,14 @@
1
1
  import './jsx-augment.js';
2
- export { Markdown } from './Markdown.js';
2
+ // Primary: the SignalX-native streaming renderer. (An editable `MarkdownEditor`
3
+ // is planned as a sibling export.)
4
+ export { MarkdownView } from './render/MarkdownView.js';
5
+ // Generic render-function override API (design systems plug in here).
6
+ export { defaultComponents } from './render/components.js';
7
+ // Streaming controller for AI token loops.
8
+ export { createMarkdownStream } from './stream.js';
9
+ // Native `<x-markdown>` wrapper, preserved for platforms that ship the element.
10
+ export { XMarkdown } from './XMarkdown.js';
11
+ // Parser primitives (for advanced consumers / testing).
12
+ export { createIncrementalEngine } from './parser/incremental.js';
13
+ export { parseBlocks } from './parser/blocks.js';
14
+ export { parseInline } from './parser/inline.js';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Block-level parser: source → `BlockNode[]`.
3
+ *
4
+ * Line-oriented with a small open-container model. The key streaming-safety
5
+ * rule lives here: a blank line only separates blocks at the top level and
6
+ * *outside* an open fenced code block — a blank line inside ``` does not close
7
+ * it. An unterminated fence parses as a `codeBlock` with `closed: false` and is
8
+ * still rendered, so streaming code dumps don't flicker when the closing fence
9
+ * finally arrives.
10
+ *
11
+ * Keys are assigned by the caller through a `KeyGen` so nested blocks (list
12
+ * items, blockquote contents) get stable, unique keys for reconciliation.
13
+ */
14
+ import type { BlockNode } from '../ast.js';
15
+ /** Monotonic key generator so every block (incl. nested) gets a unique key. */
16
+ export interface KeyGen {
17
+ next(): string;
18
+ }
19
+ export declare function makeKeyGen(prefix: string): KeyGen;
20
+ /** Parse a complete (or partial) source string into block nodes. */
21
+ export declare function parseBlocks(src: string, keys?: KeyGen): BlockNode[];