@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 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
+ }