@contractspec/lib.presentation-runtime-core 3.8.3 → 3.9.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.
@@ -0,0 +1,315 @@
1
+ // @bun
2
+ // src/transform-engine.markdown.ts
3
+ import TurndownService from "turndown";
4
+ function renderTextNode(node) {
5
+ const text = node.text ?? "";
6
+ if (!node.marks || node.marks.length === 0)
7
+ return text;
8
+ return node.marks.reduce((acc, mark) => {
9
+ switch (mark.type) {
10
+ case "bold":
11
+ return `**${acc}**`;
12
+ case "italic":
13
+ return `*${acc}*`;
14
+ case "underline":
15
+ return `__${acc}__`;
16
+ case "strike":
17
+ return `~~${acc}~~`;
18
+ case "code":
19
+ return `\`${acc}\``;
20
+ case "link": {
21
+ const href = mark.attrs?.href ?? "";
22
+ return href ? `[${acc}](${href})` : acc;
23
+ }
24
+ default:
25
+ return acc;
26
+ }
27
+ }, text);
28
+ }
29
+ function renderInline(nodes) {
30
+ if (!nodes?.length)
31
+ return "";
32
+ return nodes.map((child) => renderNode(child)).join("");
33
+ }
34
+ function renderList(nodes, ordered = false) {
35
+ if (!nodes?.length)
36
+ return "";
37
+ let counter = 1;
38
+ return nodes.map((item) => {
39
+ const body = renderInline(item.content ?? []);
40
+ if (!body)
41
+ return "";
42
+ const prefix = ordered ? `${counter++}. ` : "- ";
43
+ return `${prefix}${body}`;
44
+ }).filter(Boolean).join(`
45
+ `);
46
+ }
47
+ function renderNode(node) {
48
+ switch (node.type) {
49
+ case "doc":
50
+ return renderInline(node.content);
51
+ case "paragraph": {
52
+ const text = renderInline(node.content);
53
+ return text.trim().length ? text : "";
54
+ }
55
+ case "heading": {
56
+ const levelAttr = node.attrs?.level;
57
+ const levelVal = typeof levelAttr === "number" ? levelAttr : 1;
58
+ const level = Math.min(Math.max(levelVal, 1), 6);
59
+ return `${"#".repeat(level)} ${renderInline(node.content)}`.trim();
60
+ }
61
+ case "bullet_list":
62
+ return renderList(node.content, false);
63
+ case "ordered_list":
64
+ return renderList(node.content, true);
65
+ case "list_item":
66
+ return renderInline(node.content);
67
+ case "blockquote": {
68
+ const body = renderInline(node.content);
69
+ return body.split(`
70
+ `).map((line) => `> ${line}`).join(`
71
+ `);
72
+ }
73
+ case "code_block": {
74
+ const body = renderInline(node.content);
75
+ return body ? `\`\`\`
76
+ ${body}
77
+ \`\`\`` : "";
78
+ }
79
+ case "horizontal_rule":
80
+ return "---";
81
+ case "hard_break":
82
+ return `
83
+ `;
84
+ case "text":
85
+ return renderTextNode(node);
86
+ default:
87
+ if (node.text)
88
+ return renderTextNode(node);
89
+ return "";
90
+ }
91
+ }
92
+ function createMarkdownTurndownService() {
93
+ const turndownService = new TurndownService({
94
+ headingStyle: "atx",
95
+ codeBlockStyle: "fenced",
96
+ bulletListMarker: "-"
97
+ });
98
+ turndownService.addRule("link", {
99
+ filter: "a",
100
+ replacement: (content, node) => {
101
+ const candidate = node;
102
+ const href = candidate.getAttribute?.("href") ?? (typeof candidate.href === "string" ? candidate.href : "");
103
+ if (href && content) {
104
+ return `[${content}](${href})`;
105
+ }
106
+ return content || "";
107
+ }
108
+ });
109
+ return turndownService;
110
+ }
111
+ var defaultTurndown = createMarkdownTurndownService();
112
+ function htmlToMarkdown(html) {
113
+ return defaultTurndown.turndown(html);
114
+ }
115
+ function blockNoteToMarkdown(docJson) {
116
+ if (typeof docJson === "string")
117
+ return docJson;
118
+ if (docJson && typeof docJson === "object" && "html" in docJson) {
119
+ const html = String(docJson.html);
120
+ return htmlToMarkdown(html);
121
+ }
122
+ const root = docJson;
123
+ if (root?.type === "doc" || root?.content) {
124
+ const blocks = (root.content ?? []).map((node) => renderNode(node)).filter(Boolean);
125
+ return blocks.join(`
126
+
127
+ `).trim();
128
+ }
129
+ try {
130
+ return JSON.stringify(docJson, null, 2);
131
+ } catch {
132
+ return String(docJson);
133
+ }
134
+ }
135
+
136
+ // src/transform-engine.ts
137
+ import { schemaToMarkdown } from "@contractspec/lib.contracts-spec/schema-to-markdown";
138
+ function applyPii(desc, obj) {
139
+ let clone;
140
+ try {
141
+ clone = JSON.parse(JSON.stringify(obj));
142
+ } catch {
143
+ clone = obj;
144
+ }
145
+ const paths = desc.policy?.pii ?? [];
146
+ const setAtPath = (root, path) => {
147
+ const segments = path.replace(/^\//, "").replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
148
+ let current = root;
149
+ for (let i = 0;i < segments.length - 1; i++) {
150
+ const key = segments[i];
151
+ if (!key || !current || typeof current !== "object" || !(key in current)) {
152
+ return;
153
+ }
154
+ current = current[key];
155
+ }
156
+ const last = segments[segments.length - 1];
157
+ if (current && typeof current === "object" && last && last in current) {
158
+ current[last] = "[REDACTED]";
159
+ }
160
+ };
161
+ for (const path of paths) {
162
+ setAtPath(clone, path);
163
+ }
164
+ return clone;
165
+ }
166
+
167
+ class TransformEngine {
168
+ renderers = new Map;
169
+ validators = [];
170
+ register(renderer) {
171
+ const renderers = this.renderers.get(renderer.target) ?? [];
172
+ renderers.push(renderer);
173
+ this.renderers.set(renderer.target, renderers);
174
+ return this;
175
+ }
176
+ prependRegister(renderer) {
177
+ const renderers = this.renderers.get(renderer.target) ?? [];
178
+ renderers.unshift(renderer);
179
+ this.renderers.set(renderer.target, renderers);
180
+ return this;
181
+ }
182
+ addValidator(validator) {
183
+ this.validators.push(validator);
184
+ return this;
185
+ }
186
+ async render(target, desc, ctx) {
187
+ if (!desc.targets.includes(target)) {
188
+ throw new Error(`Target ${target} not declared for ${desc.meta.key}.v${desc.meta.version}`);
189
+ }
190
+ for (const validator of this.validators) {
191
+ await validator.validate(desc, target, ctx);
192
+ }
193
+ const renderers = this.renderers.get(target) ?? [];
194
+ for (const renderer of renderers) {
195
+ try {
196
+ return await renderer.render(desc, ctx);
197
+ } catch {}
198
+ }
199
+ throw new Error(`No renderer available for ${target}`);
200
+ }
201
+ }
202
+ function createDefaultTransformEngine() {
203
+ const engine = new TransformEngine;
204
+ engine.register({
205
+ target: "markdown",
206
+ async render(desc, ctx) {
207
+ let data = ctx?.data;
208
+ if (!data && ctx?.fetchData) {
209
+ data = await ctx.fetchData();
210
+ }
211
+ if (desc.source.type === "component" && desc.source.props && data !== undefined) {
212
+ const isArray = Array.isArray(data);
213
+ const isSimpleObject = !isArray && typeof data === "object" && data !== null && !Object.values(data).some((value) => Array.isArray(value) || typeof value === "object" && value !== null);
214
+ if (isArray || isSimpleObject) {
215
+ return {
216
+ mimeType: "text/markdown",
217
+ body: schemaToMarkdown(desc.source.props, data, {
218
+ title: desc.meta.description ?? desc.meta.key,
219
+ description: `${desc.meta.key} v${desc.meta.version}`
220
+ })
221
+ };
222
+ }
223
+ throw new Error(`Complex data structure for ${desc.meta.key} - expecting custom renderer`);
224
+ }
225
+ if (desc.source.type === "blocknotejs") {
226
+ const redacted = applyPii(desc, {
227
+ text: blockNoteToMarkdown(desc.source.docJson)
228
+ });
229
+ return {
230
+ mimeType: "text/markdown",
231
+ body: String(redacted.text ?? "")
232
+ };
233
+ }
234
+ if (desc.source.type === "component" && data !== undefined) {
235
+ throw new Error(`No schema (source.props) available for ${desc.meta.key} - expecting custom renderer`);
236
+ }
237
+ if (desc.source.type !== "component") {
238
+ throw new Error("unsupported");
239
+ }
240
+ const header = `# ${desc.meta.key} v${desc.meta.version}`;
241
+ const about = desc.meta.description ? `
242
+
243
+ ${desc.meta.description}` : "";
244
+ const tags = desc.meta.tags && desc.meta.tags.length ? `
245
+
246
+ Tags: ${desc.meta.tags.join(", ")}` : "";
247
+ const owners = desc.meta.owners && desc.meta.owners.length ? `
248
+
249
+ Owners: ${desc.meta.owners.join(", ")}` : "";
250
+ const component = `
251
+
252
+ Component: \`${desc.source.componentKey}\``;
253
+ const policy = desc.policy?.pii?.length ? `
254
+
255
+ Redacted paths: ${desc.policy.pii.map((path) => `\`${path}\``).join(", ")}` : "";
256
+ return {
257
+ mimeType: "text/markdown",
258
+ body: `${header}${about}${tags}${owners}${component}${policy}`
259
+ };
260
+ }
261
+ });
262
+ engine.register({
263
+ target: "application/json",
264
+ async render(desc) {
265
+ const payload = applyPii(desc, { meta: desc.meta, source: desc.source });
266
+ let body;
267
+ try {
268
+ body = JSON.stringify(payload, null, 2);
269
+ } catch {
270
+ body = JSON.stringify({
271
+ meta: { key: desc.meta.key, version: desc.meta.version },
272
+ source: "[non-serializable]"
273
+ }, null, 2);
274
+ }
275
+ return { mimeType: "application/json", body };
276
+ }
277
+ });
278
+ engine.register({
279
+ target: "application/xml",
280
+ async render(desc) {
281
+ const payload = applyPii(desc, { meta: desc.meta, source: desc.source });
282
+ let json;
283
+ try {
284
+ json = JSON.stringify(payload);
285
+ } catch {
286
+ json = JSON.stringify({
287
+ meta: { key: desc.meta.key, version: desc.meta.version },
288
+ source: "[non-serializable]"
289
+ });
290
+ }
291
+ return {
292
+ mimeType: "application/xml",
293
+ body: `<presentation name="${desc.meta.key}" version="${desc.meta.version}"><json>${encodeURIComponent(json)}</json></presentation>`
294
+ };
295
+ }
296
+ });
297
+ return engine;
298
+ }
299
+ function registerBasicValidation(engine) {
300
+ engine.addValidator({
301
+ validate(desc) {
302
+ if (!desc.meta.description || desc.meta.description.length < 3) {
303
+ throw new Error(`Presentation ${desc.meta.key}.v${desc.meta.version} missing meta.description`);
304
+ }
305
+ }
306
+ });
307
+ return engine;
308
+ }
309
+ export {
310
+ registerBasicValidation,
311
+ htmlToMarkdown,
312
+ createDefaultTransformEngine,
313
+ blockNoteToMarkdown,
314
+ TransformEngine
315
+ };
@@ -0,0 +1,4 @@
1
+ import TurndownService from 'turndown';
2
+ export declare function createMarkdownTurndownService(): TurndownService;
3
+ export declare function htmlToMarkdown(html: string): string;
4
+ export declare function blockNoteToMarkdown(docJson: unknown): string;
@@ -0,0 +1,139 @@
1
+ // @bun
2
+ // src/transform-engine.markdown.ts
3
+ import TurndownService from "turndown";
4
+ function renderTextNode(node) {
5
+ const text = node.text ?? "";
6
+ if (!node.marks || node.marks.length === 0)
7
+ return text;
8
+ return node.marks.reduce((acc, mark) => {
9
+ switch (mark.type) {
10
+ case "bold":
11
+ return `**${acc}**`;
12
+ case "italic":
13
+ return `*${acc}*`;
14
+ case "underline":
15
+ return `__${acc}__`;
16
+ case "strike":
17
+ return `~~${acc}~~`;
18
+ case "code":
19
+ return `\`${acc}\``;
20
+ case "link": {
21
+ const href = mark.attrs?.href ?? "";
22
+ return href ? `[${acc}](${href})` : acc;
23
+ }
24
+ default:
25
+ return acc;
26
+ }
27
+ }, text);
28
+ }
29
+ function renderInline(nodes) {
30
+ if (!nodes?.length)
31
+ return "";
32
+ return nodes.map((child) => renderNode(child)).join("");
33
+ }
34
+ function renderList(nodes, ordered = false) {
35
+ if (!nodes?.length)
36
+ return "";
37
+ let counter = 1;
38
+ return nodes.map((item) => {
39
+ const body = renderInline(item.content ?? []);
40
+ if (!body)
41
+ return "";
42
+ const prefix = ordered ? `${counter++}. ` : "- ";
43
+ return `${prefix}${body}`;
44
+ }).filter(Boolean).join(`
45
+ `);
46
+ }
47
+ function renderNode(node) {
48
+ switch (node.type) {
49
+ case "doc":
50
+ return renderInline(node.content);
51
+ case "paragraph": {
52
+ const text = renderInline(node.content);
53
+ return text.trim().length ? text : "";
54
+ }
55
+ case "heading": {
56
+ const levelAttr = node.attrs?.level;
57
+ const levelVal = typeof levelAttr === "number" ? levelAttr : 1;
58
+ const level = Math.min(Math.max(levelVal, 1), 6);
59
+ return `${"#".repeat(level)} ${renderInline(node.content)}`.trim();
60
+ }
61
+ case "bullet_list":
62
+ return renderList(node.content, false);
63
+ case "ordered_list":
64
+ return renderList(node.content, true);
65
+ case "list_item":
66
+ return renderInline(node.content);
67
+ case "blockquote": {
68
+ const body = renderInline(node.content);
69
+ return body.split(`
70
+ `).map((line) => `> ${line}`).join(`
71
+ `);
72
+ }
73
+ case "code_block": {
74
+ const body = renderInline(node.content);
75
+ return body ? `\`\`\`
76
+ ${body}
77
+ \`\`\`` : "";
78
+ }
79
+ case "horizontal_rule":
80
+ return "---";
81
+ case "hard_break":
82
+ return `
83
+ `;
84
+ case "text":
85
+ return renderTextNode(node);
86
+ default:
87
+ if (node.text)
88
+ return renderTextNode(node);
89
+ return "";
90
+ }
91
+ }
92
+ function createMarkdownTurndownService() {
93
+ const turndownService = new TurndownService({
94
+ headingStyle: "atx",
95
+ codeBlockStyle: "fenced",
96
+ bulletListMarker: "-"
97
+ });
98
+ turndownService.addRule("link", {
99
+ filter: "a",
100
+ replacement: (content, node) => {
101
+ const candidate = node;
102
+ const href = candidate.getAttribute?.("href") ?? (typeof candidate.href === "string" ? candidate.href : "");
103
+ if (href && content) {
104
+ return `[${content}](${href})`;
105
+ }
106
+ return content || "";
107
+ }
108
+ });
109
+ return turndownService;
110
+ }
111
+ var defaultTurndown = createMarkdownTurndownService();
112
+ function htmlToMarkdown(html) {
113
+ return defaultTurndown.turndown(html);
114
+ }
115
+ function blockNoteToMarkdown(docJson) {
116
+ if (typeof docJson === "string")
117
+ return docJson;
118
+ if (docJson && typeof docJson === "object" && "html" in docJson) {
119
+ const html = String(docJson.html);
120
+ return htmlToMarkdown(html);
121
+ }
122
+ const root = docJson;
123
+ if (root?.type === "doc" || root?.content) {
124
+ const blocks = (root.content ?? []).map((node) => renderNode(node)).filter(Boolean);
125
+ return blocks.join(`
126
+
127
+ `).trim();
128
+ }
129
+ try {
130
+ return JSON.stringify(docJson, null, 2);
131
+ } catch {
132
+ return String(docJson);
133
+ }
134
+ }
135
+ export {
136
+ htmlToMarkdown,
137
+ createMarkdownTurndownService,
138
+ blockNoteToMarkdown
139
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contractspec/lib.presentation-runtime-core",
3
- "version": "3.8.3",
3
+ "version": "3.9.0",
4
4
  "description": "Core presentation runtime for contract-driven UIs",
5
5
  "keywords": [
6
6
  "contractspec",
@@ -31,13 +31,15 @@
31
31
  "typescript": "^5.9.0"
32
32
  },
33
33
  "dependencies": {
34
- "@contractspec/lib.contracts-spec": "4.1.2",
35
- "echarts": "^6.0.0"
34
+ "@contractspec/lib.contracts-spec": "5.0.0",
35
+ "echarts": "^6.0.0",
36
+ "turndown": "^7.2.2"
36
37
  },
37
38
  "devDependencies": {
38
- "@contractspec/tool.typescript": "3.7.8",
39
+ "@contractspec/tool.typescript": "3.7.9",
39
40
  "typescript": "^5.9.3",
40
- "@contractspec/tool.bun": "3.7.8"
41
+ "@contractspec/tool.bun": "3.7.9",
42
+ "@types/turndown": "^5.0.6"
41
43
  },
42
44
  "files": [
43
45
  "dist",
@@ -58,6 +60,20 @@
58
60
  "node": "./dist/node/table.js",
59
61
  "default": "./dist/table.js"
60
62
  },
63
+ "./transform-engine": {
64
+ "types": "./dist/transform-engine.d.ts",
65
+ "browser": "./dist/browser/transform-engine.js",
66
+ "bun": "./dist/transform-engine.js",
67
+ "node": "./dist/node/transform-engine.js",
68
+ "default": "./dist/transform-engine.js"
69
+ },
70
+ "./transform-engine.markdown": {
71
+ "types": "./dist/transform-engine.markdown.d.ts",
72
+ "browser": "./dist/browser/transform-engine.markdown.js",
73
+ "bun": "./dist/transform-engine.markdown.js",
74
+ "node": "./dist/node/transform-engine.markdown.js",
75
+ "default": "./dist/transform-engine.markdown.js"
76
+ },
61
77
  "./visualization": {
62
78
  "types": "./dist/visualization.d.ts",
63
79
  "browser": "./dist/browser/visualization.js",
@@ -125,6 +141,20 @@
125
141
  "node": "./dist/node/table.js",
126
142
  "default": "./dist/table.js"
127
143
  },
144
+ "./transform-engine": {
145
+ "types": "./dist/transform-engine.d.ts",
146
+ "browser": "./dist/browser/transform-engine.js",
147
+ "bun": "./dist/transform-engine.js",
148
+ "node": "./dist/node/transform-engine.js",
149
+ "default": "./dist/transform-engine.js"
150
+ },
151
+ "./transform-engine.markdown": {
152
+ "types": "./dist/transform-engine.markdown.d.ts",
153
+ "browser": "./dist/browser/transform-engine.markdown.js",
154
+ "bun": "./dist/transform-engine.markdown.js",
155
+ "node": "./dist/node/transform-engine.markdown.js",
156
+ "default": "./dist/transform-engine.markdown.js"
157
+ },
128
158
  "./visualization": {
129
159
  "types": "./dist/visualization.d.ts",
130
160
  "browser": "./dist/browser/visualization.js",