@jxsuite/markup 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/package.json +43 -0
- package/src/html-to-jx.ts +121 -0
- package/src/index.ts +13 -0
- package/src/md-html.ts +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @jxsuite/markup
|
|
2
|
+
|
|
3
|
+
Browser-safe markup conversion utilities shared by the Jx core packages:
|
|
4
|
+
|
|
5
|
+
- `@jxsuite/markup/html-to-jx` — `htmlToJx(html)` converts an HTML string into
|
|
6
|
+
an array of Jx tree nodes (`JxElement | string`), mapping attributes through
|
|
7
|
+
`property-information` and inline `style` strings into style objects.
|
|
8
|
+
- `@jxsuite/markup/md-html` — `markdownToHtml(markdown)` renders untrusted
|
|
9
|
+
markdown (GFM tables/strikethrough/task lists) to a **sanitized** HTML
|
|
10
|
+
string: raw HTML is dropped, event handlers and `javascript:` URLs are
|
|
11
|
+
stripped.
|
|
12
|
+
|
|
13
|
+
Both pipelines are DOM-free and node-free — no `node:*` imports — so they are
|
|
14
|
+
safe for any bundle target (Jx Studio, Cloudflare Workers, node/Bun CLIs).
|
|
15
|
+
The package root re-exports both functions.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { htmlToJx, markdownToHtml } from "@jxsuite/markup";
|
|
19
|
+
|
|
20
|
+
htmlToJx('<p class="x">Hello</p>');
|
|
21
|
+
// → [{ tagName: "p", attributes: { class: "x" }, textContent: "Hello" }]
|
|
22
|
+
|
|
23
|
+
markdownToHtml("# Hi\n\n**bold**");
|
|
24
|
+
// → "<h1>Hi</h1>\n<p><strong>bold</strong></p>"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For markdown → Jx _document_ transpilation (frontmatter, directives, Jx
|
|
28
|
+
markdown), use `@jxsuite/parser` — this package deliberately stays a thin,
|
|
29
|
+
dependency-light conversion layer.
|
|
30
|
+
|
|
31
|
+
## Versioning
|
|
32
|
+
|
|
33
|
+
The types are published as TypeScript source (like every `@jxsuite` package)
|
|
34
|
+
and follow the monorepo's release train. Within the monorepo, packages depend
|
|
35
|
+
on it via `workspace:^`. Consumers install `@jxsuite/markup` and import its
|
|
36
|
+
`/html-to-jx` and `/md-html` entrypoints (or the root re-export); its only
|
|
37
|
+
`@jxsuite` runtime dependency is `@jxsuite/schema`.
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jxsuite/markup",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browser-safe markup conversion: HTML → Jx nodes, markdown → sanitized HTML",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/jxsuite/jx.git",
|
|
9
|
+
"directory": "packages/markup"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src/"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./src/index.ts",
|
|
17
|
+
"./html-to-jx": "./src/html-to-jx.ts",
|
|
18
|
+
"./md-html": "./src/md-html.ts"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"provenance": false
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "bun test --isolate",
|
|
25
|
+
"test:coverage": "bun test --isolate --coverage",
|
|
26
|
+
"upgrade": "bunx npm-check-updates -u && bun install"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@jxsuite/schema": "workspace:^",
|
|
30
|
+
"hast-util-from-html": "^2.0.3",
|
|
31
|
+
"hast-util-whitespace": "^3.0.0",
|
|
32
|
+
"property-information": "^7.2.0",
|
|
33
|
+
"rehype-sanitize": "^6.0.0",
|
|
34
|
+
"rehype-stringify": "^10.0.1",
|
|
35
|
+
"remark-gfm": "^4.0.1",
|
|
36
|
+
"remark-parse": "^11.0.0",
|
|
37
|
+
"remark-rehype": "^11.1.2",
|
|
38
|
+
"unified": "^11.0.5"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/hast": "3.0.5"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { fromHtml } from "hast-util-from-html";
|
|
2
|
+
import { whitespace } from "hast-util-whitespace";
|
|
3
|
+
import { find, html as htmlInfo } from "property-information";
|
|
4
|
+
|
|
5
|
+
import type { Nodes as HastNode } from "hast";
|
|
6
|
+
import type { JxElement } from "@jxsuite/schema/types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Convert an HTML string into an array of Jx tree nodes.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} htmlString
|
|
12
|
+
* @returns {(JxElement | string)[]}
|
|
13
|
+
*/
|
|
14
|
+
export function htmlToJx(htmlString: string) {
|
|
15
|
+
const hast = fromHtml(htmlString, { fragment: true });
|
|
16
|
+
return convertHastChildren(hast.children as HastNode[]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {HastNode[]} children
|
|
21
|
+
* @returns {(JxElement | string)[]}
|
|
22
|
+
*/
|
|
23
|
+
function convertHastChildren(children: HastNode[]) {
|
|
24
|
+
const result: (JxElement | string)[] = [];
|
|
25
|
+
for (const child of children) {
|
|
26
|
+
const converted = convertHastNode(child);
|
|
27
|
+
if (converted != null) {
|
|
28
|
+
result.push(converted);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {HastNode} node
|
|
36
|
+
* @returns {JxElement | string | null}
|
|
37
|
+
*/
|
|
38
|
+
function convertHastNode(node: HastNode) {
|
|
39
|
+
if (node.type === "text") {
|
|
40
|
+
if (whitespace(node as unknown as HastNode)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return node.value ?? null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (node.type === "element") {
|
|
47
|
+
const el: JxElement = { tagName: node.tagName };
|
|
48
|
+
|
|
49
|
+
if (node.properties && Object.keys(node.properties).length > 0) {
|
|
50
|
+
const { style, attrs } = hastPropsToJx(node.properties);
|
|
51
|
+
if (Object.keys(attrs).length > 0) {
|
|
52
|
+
el.attributes = attrs;
|
|
53
|
+
}
|
|
54
|
+
if (Object.keys(style).length > 0) {
|
|
55
|
+
el.style = style;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const kids = node.children ? convertHastChildren(node.children) : [];
|
|
60
|
+
|
|
61
|
+
if (kids.length === 1 && typeof kids[0] === "string") {
|
|
62
|
+
[el.textContent] = kids;
|
|
63
|
+
} else if (kids.length > 0) {
|
|
64
|
+
el.children = kids as JxElement[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return el;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {Record<string, unknown>} properties
|
|
75
|
+
* @returns {{ style: Record<string, string>; attrs: Record<string, string> }}
|
|
76
|
+
*/
|
|
77
|
+
function hastPropsToJx(properties: Record<string, unknown>) {
|
|
78
|
+
const attrs: Record<string, string> = {};
|
|
79
|
+
const style: Record<string, string> = {};
|
|
80
|
+
|
|
81
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
82
|
+
if (value === false || value === undefined || value === null) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const info = find(htmlInfo, key);
|
|
87
|
+
const name = info.attribute;
|
|
88
|
+
|
|
89
|
+
if (name === "style" && typeof value === "string") {
|
|
90
|
+
parseInlineStyle(value, style);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (value === true) {
|
|
95
|
+
attrs[name] = "";
|
|
96
|
+
} else if (Array.isArray(value)) {
|
|
97
|
+
attrs[name] = value.join(info.commaSeparated ? ", " : " ");
|
|
98
|
+
} else {
|
|
99
|
+
attrs[name] = String(value);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { attrs, style };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {string} styleStr
|
|
107
|
+
* @param {Record<string, string>} out
|
|
108
|
+
*/
|
|
109
|
+
function parseInlineStyle(styleStr: string, out: Record<string, string>) {
|
|
110
|
+
for (const decl of styleStr.split(";")) {
|
|
111
|
+
const colon = decl.indexOf(":");
|
|
112
|
+
if (colon === -1) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const prop = decl.slice(0, colon).trim();
|
|
116
|
+
const val = decl.slice(colon + 1).trim();
|
|
117
|
+
if (prop && val) {
|
|
118
|
+
out[prop] = val;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markup — browser-safe markup conversion utilities.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports the package's two pipelines: `htmlToJx` (HTML string → Jx tree nodes) and
|
|
5
|
+
* `markdownToHtml` (markdown → sanitized HTML string). Both are DOM-free and node-free, so
|
|
6
|
+
* they are safe for any bundle target (studio, workers, node).
|
|
7
|
+
*
|
|
8
|
+
* @module @jxsuite/markup
|
|
9
|
+
* @license MIT
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { htmlToJx } from "./html-to-jx";
|
|
13
|
+
export { markdownToHtml } from "./md-html";
|
package/src/md-html.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Md-html — browser-safe markdown → sanitized HTML string
|
|
3
|
+
*
|
|
4
|
+
* Small display pipeline for rendering untrusted markdown (e.g. LLM chat output) as
|
|
5
|
+
* HTML: remark-parse + remark-gfm → remark-rehype (raw HTML disabled) → rehype-sanitize
|
|
6
|
+
* (default schema: strips scripts, event handlers, javascript:/data: URLs) → stringify.
|
|
7
|
+
*
|
|
8
|
+
* Like everything in @jxsuite/markup this module has no node-only imports, so it is safe
|
|
9
|
+
* for the studio bundle. It intentionally produces an HTML *string*, not Jx nodes — use
|
|
10
|
+
* `@jxsuite/parser/markdown` for markdown → Jx document transpilation.
|
|
11
|
+
*
|
|
12
|
+
* @module @jxsuite/markup/md-html
|
|
13
|
+
* @license MIT
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import rehypeSanitize from "rehype-sanitize";
|
|
17
|
+
import rehypeStringify from "rehype-stringify";
|
|
18
|
+
import remarkGfm from "remark-gfm";
|
|
19
|
+
import remarkParse from "remark-parse";
|
|
20
|
+
import remarkRehype from "remark-rehype";
|
|
21
|
+
import { unified } from "unified";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Minimal structural surface of the unified processor used here — same rationale as
|
|
25
|
+
* `UnifiedProcessor` in @jxsuite/parser's types (oxlint sees the ESM-only unified types as `any`),
|
|
26
|
+
* plus `processSync` which that shared type doesn't carry.
|
|
27
|
+
*/
|
|
28
|
+
interface MdHtmlProcessor {
|
|
29
|
+
use: (plugin: unknown, ...options: unknown[]) => MdHtmlProcessor;
|
|
30
|
+
processSync: (source: string) => { toString: () => string };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const processor = (unified as unknown as () => MdHtmlProcessor)()
|
|
34
|
+
.use(remarkParse)
|
|
35
|
+
.use(remarkGfm)
|
|
36
|
+
.use(remarkRehype)
|
|
37
|
+
.use(rehypeSanitize)
|
|
38
|
+
.use(rehypeStringify);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convert markdown source to a sanitized HTML string (GFM tables/strikethrough/task lists
|
|
42
|
+
* supported). Raw HTML in the source is dropped, not passed through.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} markdown
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
export function markdownToHtml(markdown: string): string {
|
|
48
|
+
return String(processor.processSync(markdown));
|
|
49
|
+
}
|