@theseus.run/jsx-md 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -204
- package/dist/index-8tdwjkh9.js +11 -0
- package/dist/index-8tdwjkh9.js.map +10 -0
- package/dist/index.js +435 -0
- package/dist/index.js.map +13 -0
- package/dist/jsx-dev-runtime.js +13 -0
- package/dist/jsx-dev-runtime.js.map +9 -0
- package/dist/jsx-runtime.js +13 -0
- package/dist/jsx-runtime.js.map +9 -0
- package/package.json +23 -8
- package/src/context.ts +47 -4
- package/src/escape.ts +69 -11
- package/src/index.ts +5 -1
- package/src/jsx-runtime.ts +36 -21
- package/src/primitives.tsx +140 -74
- package/src/render.ts +54 -19
- package/src/_render-registry.ts +0 -16
package/src/context.ts
CHANGED
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Synchronous context API for jsx-md — matches the React context shape.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* createContext(defaultValue) returns a Context object with a Provider
|
|
5
|
+
* component. Wrap a render() call with <Ctx.Provider value={...}> and read
|
|
6
|
+
* the value anywhere in the tree with useContext(Ctx).
|
|
7
|
+
*
|
|
8
|
+
* Uses a module-level stack map so values are available synchronously during
|
|
9
|
+
* the render() traversal. Not safe for async or concurrent rendering.
|
|
6
10
|
*/
|
|
7
11
|
|
|
12
|
+
import { render } from './render.ts';
|
|
13
|
+
import type { VNode } from './jsx-runtime.ts';
|
|
14
|
+
|
|
8
15
|
export interface Context<T> {
|
|
9
16
|
readonly _id: symbol;
|
|
10
17
|
readonly _default: T;
|
|
18
|
+
/**
|
|
19
|
+
* JSX provider component — identical usage to React's Context.Provider.
|
|
20
|
+
*
|
|
21
|
+
* Return type is `string` (not `VNode`) because `render()` always returns `string`
|
|
22
|
+
* and this is the honest, precise type. This means `Provider` is not directly
|
|
23
|
+
* assignable to `Component<P>` without a cast, even though `string extends VNode`.
|
|
24
|
+
*
|
|
25
|
+
* React solves this by typing `Provider` as `ProviderExoticComponent` — a distinct
|
|
26
|
+
* type with a `$$typeof: symbol` discriminant — rather than a plain function component.
|
|
27
|
+
* That approach deliberately prevents `Provider` from being used as a generic
|
|
28
|
+
* `Component` argument, and it returns `ReactNode` (broad) rather than a precise type.
|
|
29
|
+
*
|
|
30
|
+
* We keep `=> string` because: (a) no code in this codebase passes `Provider` as a
|
|
31
|
+
* `Component`-typed value, (b) precision is more useful than assignability here,
|
|
32
|
+
* and (c) the exotic-component indirection would be engineering for a non-problem.
|
|
33
|
+
* If that higher-order pattern ever becomes necessary, change this to `=> VNode`.
|
|
34
|
+
*/
|
|
35
|
+
readonly Provider: (props: { value: T; children?: VNode }) => string;
|
|
11
36
|
}
|
|
12
37
|
|
|
13
38
|
/**
|
|
@@ -18,7 +43,24 @@ export interface Context<T> {
|
|
|
18
43
|
const stack = new Map<symbol, unknown[]>();
|
|
19
44
|
|
|
20
45
|
export function createContext<T>(defaultValue: T): Context<T> {
|
|
21
|
-
|
|
46
|
+
const _id = Symbol();
|
|
47
|
+
|
|
48
|
+
function Provider({ value, children }: { value: T; children?: VNode }): string {
|
|
49
|
+
let s = stack.get(_id);
|
|
50
|
+
if (!s) {
|
|
51
|
+
s = [];
|
|
52
|
+
stack.set(_id, s);
|
|
53
|
+
}
|
|
54
|
+
s.push(value as unknown);
|
|
55
|
+
try {
|
|
56
|
+
return render(children ?? null);
|
|
57
|
+
} finally {
|
|
58
|
+
s.pop();
|
|
59
|
+
if (s.length === 0) stack.delete(_id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { _id, _default: defaultValue, Provider };
|
|
22
64
|
}
|
|
23
65
|
|
|
24
66
|
export function useContext<T>(ctx: Context<T>): T {
|
|
@@ -40,5 +82,6 @@ export function withContext<T>(ctx: Context<T>, value: T, fn: () => string): str
|
|
|
40
82
|
return fn();
|
|
41
83
|
} finally {
|
|
42
84
|
s.pop();
|
|
85
|
+
if (s.length === 0) stack.delete(ctx._id);
|
|
43
86
|
}
|
|
44
87
|
}
|
package/src/escape.ts
CHANGED
|
@@ -2,21 +2,35 @@
|
|
|
2
2
|
* Escapes `&`, `"`, `<`, `>` — for use in XML/HTML attribute values.
|
|
3
3
|
* Escapes for double-quoted HTML/XML attribute values. Single quotes are not
|
|
4
4
|
* escaped — do not use this in single-quoted attribute contexts.
|
|
5
|
+
*
|
|
6
|
+
* Single-pass via a lookup map — avoids four sequential .replace() calls.
|
|
5
7
|
*/
|
|
8
|
+
const HTML_ATTR_MAP: Record<string, string> = {
|
|
9
|
+
'&': '&',
|
|
10
|
+
'"': '"',
|
|
11
|
+
'<': '<',
|
|
12
|
+
'>': '>',
|
|
13
|
+
};
|
|
14
|
+
const HTML_ATTR_RE = /[&"<>]/g;
|
|
15
|
+
|
|
6
16
|
export function escapeHtmlAttr(s: string): string {
|
|
7
|
-
return s
|
|
8
|
-
.replace(/&/g, '&')
|
|
9
|
-
.replace(/"/g, '"')
|
|
10
|
-
.replace(/</g, '<')
|
|
11
|
-
.replace(/>/g, '>');
|
|
17
|
+
return s.replace(HTML_ATTR_RE, (c) => HTML_ATTR_MAP[c]!);
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Escapes `&`, `<`, `>` — for use in HTML/XML text content (not attributes).
|
|
22
|
+
*
|
|
23
|
+
* Single-pass via a lookup map.
|
|
24
|
+
*/
|
|
25
|
+
const HTML_CONTENT_MAP: Record<string, string> = {
|
|
26
|
+
'&': '&',
|
|
27
|
+
'<': '<',
|
|
28
|
+
'>': '>',
|
|
29
|
+
};
|
|
30
|
+
const HTML_CONTENT_RE = /[&<>]/g;
|
|
31
|
+
|
|
15
32
|
export function escapeHtmlContent(s: string): string {
|
|
16
|
-
return s
|
|
17
|
-
.replace(/&/g, '&')
|
|
18
|
-
.replace(/</g, '<')
|
|
19
|
-
.replace(/>/g, '>');
|
|
33
|
+
return s.replace(HTML_CONTENT_RE, (c) => HTML_CONTENT_MAP[c]!);
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
/**
|
|
@@ -24,7 +38,7 @@ export function escapeHtmlContent(s: string): string {
|
|
|
24
38
|
* Markdown link syntax `[text](url)`.
|
|
25
39
|
*/
|
|
26
40
|
export function encodeLinkUrl(url: string): string {
|
|
27
|
-
return url.replace(
|
|
41
|
+
return url.replace(/\(/g, '%28').replace(/\)/g, '%29');
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
/**
|
|
@@ -34,3 +48,47 @@ export function encodeLinkUrl(url: string): string {
|
|
|
34
48
|
export function encodeLinkLabel(text: string): string {
|
|
35
49
|
return text.replace(/[[\]]/g, (c) => encodeURIComponent(c));
|
|
36
50
|
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Escapes all CommonMark ASCII punctuation metacharacters in a string so that
|
|
54
|
+
* user-supplied content is treated as literal text by any markdown renderer.
|
|
55
|
+
*
|
|
56
|
+
* Useful when interpolating variable strings — filenames, user input, code
|
|
57
|
+
* identifiers, etc. — into markdown prose where unintended formatting must be
|
|
58
|
+
* suppressed:
|
|
59
|
+
*
|
|
60
|
+
* <P>File: <Escape>{untrustedFilename}</Escape></P>
|
|
61
|
+
* <P>File: {escapeMarkdown(untrustedFilename)}</P>
|
|
62
|
+
*
|
|
63
|
+
* Escaped characters: \ ` * _ [ ] ( ) # + - . ! | ~ < >
|
|
64
|
+
* These cover all CommonMark inline and block trigger characters.
|
|
65
|
+
* HTML entities (& etc.) are intentionally not escaped here — use
|
|
66
|
+
* escapeHtmlContent for HTML attribute / tag contexts.
|
|
67
|
+
*/
|
|
68
|
+
const MARKDOWN_ESCAPE_RE = /[\\`*_[\]()#+\-.!|~<>]/g;
|
|
69
|
+
|
|
70
|
+
export function escapeMarkdown(s: string): string {
|
|
71
|
+
return s.replace(MARKDOWN_ESCAPE_RE, '\\$&');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Returns the minimum backtick fence length needed to safely wrap `content`
|
|
76
|
+
* as a CommonMark inline code span or fenced code block.
|
|
77
|
+
*
|
|
78
|
+
* CommonMark rule: the fence must be a run of N backticks where N is strictly
|
|
79
|
+
* greater than the longest run of consecutive backticks in the content.
|
|
80
|
+
* Minimum is 1 for inline code, 3 for fenced code blocks.
|
|
81
|
+
*/
|
|
82
|
+
export function backtickFenceLength(content: string, minimum: number = 1): number {
|
|
83
|
+
let maxRun = 0;
|
|
84
|
+
let currentRun = 0;
|
|
85
|
+
for (const ch of content) {
|
|
86
|
+
if (ch === '`') {
|
|
87
|
+
currentRun++;
|
|
88
|
+
if (currentRun > maxRun) maxRun = currentRun;
|
|
89
|
+
} else {
|
|
90
|
+
currentRun = 0;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return Math.max(minimum, maxRun + 1);
|
|
94
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,9 @@ export { render } from './render.ts';
|
|
|
9
9
|
export { createContext, useContext, withContext } from './context.ts';
|
|
10
10
|
export type { Context } from './context.ts';
|
|
11
11
|
|
|
12
|
+
// Escape utilities
|
|
13
|
+
export { escapeMarkdown } from './escape.ts';
|
|
14
|
+
|
|
12
15
|
// Primitive components
|
|
13
16
|
export {
|
|
14
17
|
H1, H2, H3, H4, H5, H6,
|
|
@@ -18,6 +21,7 @@ export {
|
|
|
18
21
|
Blockquote,
|
|
19
22
|
Li, Ul, Ol,
|
|
20
23
|
Bold, Code, Italic, Strikethrough, Link, Img,
|
|
24
|
+
Br, Sup, Sub, Kbd, Escape,
|
|
21
25
|
Md,
|
|
22
26
|
Table, Tr, Th, Td,
|
|
23
27
|
TaskList, Task,
|
|
@@ -26,4 +30,4 @@ export {
|
|
|
26
30
|
Details,
|
|
27
31
|
} from './primitives.tsx';
|
|
28
32
|
|
|
29
|
-
export type { CalloutType } from './primitives.tsx';
|
|
33
|
+
export type { CalloutType, ColAlign } from './primitives.tsx';
|
package/src/jsx-runtime.ts
CHANGED
|
@@ -8,21 +8,34 @@
|
|
|
8
8
|
* Call `render(node)` from `./render.ts` to produce the final markdown string.
|
|
9
9
|
* All markdown primitives live in primitives.tsx as named components.
|
|
10
10
|
*
|
|
11
|
-
* Fragment
|
|
12
|
-
*
|
|
13
|
-
* shared side-channel: render.ts calls registerRender(render) on module init,
|
|
14
|
-
* Fragment defers to callRender(). Any entry point that imports render will
|
|
15
|
-
* register it before rendering starts.
|
|
11
|
+
* Fragment is a Symbol. render.ts imports this Symbol and handles it explicitly,
|
|
12
|
+
* keeping the dependency one-way: render.ts → jsx-runtime.ts (no cycle).
|
|
16
13
|
*/
|
|
17
14
|
|
|
18
|
-
import { callRender } from './_render-registry.ts';
|
|
19
|
-
|
|
20
15
|
// ---------------------------------------------------------------------------
|
|
21
16
|
// VNode types
|
|
22
17
|
// ---------------------------------------------------------------------------
|
|
23
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The concrete runtime shape of a JSX element — what the `jsx()` factory always produces.
|
|
21
|
+
*
|
|
22
|
+
* Useful for structural inspection of a VNode tree (e.g. testing whether a node is an
|
|
23
|
+
* element rather than a string, null, or array). The `isVNodeElement` predicate in
|
|
24
|
+
* render.ts narrows to this type.
|
|
25
|
+
*
|
|
26
|
+
* @remarks **Do not use as a component return-type annotation.** TypeScript infers
|
|
27
|
+
* `JSX.Element` (= `VNode`) as the return type of JSX expressions, not `VNodeElement`.
|
|
28
|
+
* Annotating a component as `(): VNodeElement` causes TS2322 because `VNode` (the
|
|
29
|
+
* inferred type) is not assignable to the narrower `VNodeElement`. Use `VNode` instead:
|
|
30
|
+
* ```ts
|
|
31
|
+
* // Wrong — TS2322
|
|
32
|
+
* function MyComp(): VNodeElement { return <P>hi</P>; }
|
|
33
|
+
* // Correct
|
|
34
|
+
* function MyComp(): VNode { return <P>hi</P>; }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
24
37
|
export type VNodeElement = {
|
|
25
|
-
readonly type: Component | string;
|
|
38
|
+
readonly type: Component | string | typeof Fragment;
|
|
26
39
|
readonly props: Record<string, unknown>;
|
|
27
40
|
};
|
|
28
41
|
|
|
@@ -63,12 +76,16 @@ export namespace JSX {
|
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
// ---------------------------------------------------------------------------
|
|
66
|
-
//
|
|
79
|
+
// Fragment symbol
|
|
67
80
|
// ---------------------------------------------------------------------------
|
|
68
81
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Fragment — a unique Symbol used as the `type` of JSX fragment VNodes.
|
|
84
|
+
* render.ts detects this Symbol and renders children directly, with no wrapper.
|
|
85
|
+
* Using a Symbol (rather than a function) eliminates the circular dependency
|
|
86
|
+
* that previously required _render-registry.ts.
|
|
87
|
+
*/
|
|
88
|
+
export const Fragment = Symbol('Fragment');
|
|
72
89
|
|
|
73
90
|
// ---------------------------------------------------------------------------
|
|
74
91
|
// JSX factory
|
|
@@ -77,18 +94,16 @@ export namespace JSX {
|
|
|
77
94
|
/**
|
|
78
95
|
* JSX factory — called by Bun's compiled JSX. Builds VNode tree; no evaluation.
|
|
79
96
|
*
|
|
80
|
-
* `type` is a function component
|
|
81
|
-
* as XML blocks by render.ts: `<tag attrs>\ncontent\n</tag>\n`
|
|
82
|
-
* when the inner content is empty).
|
|
97
|
+
* `type` is a function component, a string tag name, or the Fragment symbol.
|
|
98
|
+
* String tags are rendered as XML blocks by render.ts: `<tag attrs>\ncontent\n</tag>\n`
|
|
99
|
+
* (or self-closing when the inner content is empty).
|
|
83
100
|
*/
|
|
84
|
-
export function jsx(
|
|
101
|
+
export function jsx(
|
|
102
|
+
type: Component | string | typeof Fragment,
|
|
103
|
+
props: Record<string, unknown>,
|
|
104
|
+
): VNodeElement {
|
|
85
105
|
return { type, props };
|
|
86
106
|
}
|
|
87
107
|
|
|
88
108
|
/** jsxs — same as jsx, used when there are multiple children (children is array) */
|
|
89
109
|
export const jsxs = jsx;
|
|
90
|
-
|
|
91
|
-
/** Fragment — renders children. Defers to _render-registry to avoid circular imports. */
|
|
92
|
-
export function Fragment({ children }: { children?: VNode }): string {
|
|
93
|
-
return callRender(children ?? null);
|
|
94
|
-
}
|