@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.
- package/dist/browser/index.js +79 -0
- package/dist/browser/transform-engine.js +314 -0
- package/dist/browser/transform-engine.markdown.js +138 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +79 -0
- package/dist/node/index.js +79 -0
- package/dist/node/transform-engine.js +314 -0
- package/dist/node/transform-engine.markdown.js +138 -0
- package/dist/transform-engine.d.ts +27 -0
- package/dist/transform-engine.js +315 -0
- package/dist/transform-engine.markdown.d.ts +4 -0
- package/dist/transform-engine.markdown.js +139 -0
- package/dist/transform-engine.test.d.ts +1 -0
- package/package.json +35 -5
package/dist/browser/index.js
CHANGED
|
@@ -517,9 +517,88 @@ function withPresentationMetroAliases(config, opts = {}) {
|
|
|
517
517
|
};
|
|
518
518
|
return config;
|
|
519
519
|
}
|
|
520
|
+
var tech_presentation_runtime_DocBlocks = [
|
|
521
|
+
{
|
|
522
|
+
id: "docs.tech.presentation-runtime",
|
|
523
|
+
title: "Presentation Runtime",
|
|
524
|
+
summary: "Cross-platform runtime for list pages, presentation flows, and headless ContractSpec tables.",
|
|
525
|
+
kind: "reference",
|
|
526
|
+
visibility: "public",
|
|
527
|
+
route: "/docs/tech/presentation-runtime",
|
|
528
|
+
tags: ["tech", "presentation-runtime"],
|
|
529
|
+
body: `## Presentation Runtime
|
|
530
|
+
|
|
531
|
+
Cross-platform runtime for list pages, presentation flows, and headless ContractSpec tables.
|
|
532
|
+
|
|
533
|
+
### Packages
|
|
534
|
+
|
|
535
|
+
- \`@contractspec/lib.presentation-runtime-core\`: shared state/types for URL state, list coordination, and headless table controllers
|
|
536
|
+
- \`@contractspec/lib.presentation-runtime-react\`: React hooks (web/native-compatible API)
|
|
537
|
+
- \`@contractspec/lib.presentation-runtime-react-native\`: Native entrypoint (re-exports the shared React table hooks and keeps native form/list helpers)
|
|
538
|
+
|
|
539
|
+
### Next.js config helper
|
|
540
|
+
|
|
541
|
+
\`\`\`ts
|
|
542
|
+
// next.config.mjs
|
|
543
|
+
import { withPresentationNextAliases } from '@contractspec/lib.presentation-runtime-core/next';
|
|
544
|
+
|
|
545
|
+
const nextConfig = {
|
|
546
|
+
webpack: (config) => withPresentationNextAliases(config),
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
export default nextConfig;
|
|
550
|
+
\`\`\`
|
|
551
|
+
|
|
552
|
+
### Metro config helper
|
|
553
|
+
|
|
554
|
+
\`\`\`js
|
|
555
|
+
// metro.config.js (CJS)
|
|
556
|
+
const { getDefaultConfig } = require('expo/metro-config');
|
|
557
|
+
const {
|
|
558
|
+
withPresentationMetroAliases,
|
|
559
|
+
} = require('@contractspec/lib.presentation-runtime-core/src/metro.cjs');
|
|
560
|
+
|
|
561
|
+
const projectRoot = __dirname;
|
|
562
|
+
const config = getDefaultConfig(projectRoot);
|
|
563
|
+
|
|
564
|
+
module.exports = withPresentationMetroAliases(config);
|
|
565
|
+
\`\`\`
|
|
566
|
+
|
|
567
|
+
### React hooks
|
|
568
|
+
|
|
569
|
+
- \`useListCoordinator\`: URL + RHF + derived variables (no fetching)
|
|
570
|
+
- \`usePresentationController\`: Same plus \`fetcher\` integration
|
|
571
|
+
- \`useContractTable\`: headless TanStack-backed controller for sorting, pagination, selection, visibility, pinning, resizing, and expansion
|
|
572
|
+
- \`useDataViewTable\`: adapter that maps \`DataViewSpec\` table contracts onto the generic controller
|
|
573
|
+
- \`DataViewRenderer\` (design-system): render \`DataViewSpec\` projections (\`list\`, \`table\`, \`detail\`, \`grid\`) using shared UI atoms
|
|
574
|
+
|
|
575
|
+
Both list hooks accept a \`useUrlState\` adapter. On web, use \`useListUrlState\` (design-system) or a Next adapter.
|
|
576
|
+
|
|
577
|
+
### Table layering
|
|
578
|
+
|
|
579
|
+
- \`contracts-spec\`: declarative table contract (\`DataViewTableConfig\`)
|
|
580
|
+
- \`presentation-runtime-*\`: headless controller state and TanStack integration
|
|
581
|
+
- \`ui-kit\` / \`ui-kit-web\`: platform renderers that consume controller output
|
|
582
|
+
- \`design-system\`: opinionated \`DataTable\`, \`DataViewTable\`, \`ApprovalQueue\`, and \`ListTablePage\`
|
|
583
|
+
|
|
584
|
+
### KYC molecules (bundle)
|
|
585
|
+
|
|
586
|
+
- \`ComplianceBadge\` in \`@contractspec/bundle.strit/presentation/components/kyc\` renders a status badge for KYC/compliance snapshots. It accepts a \`state\` (missing_core | incomplete | complete | expiring | unknown) and optional localized \`labels\`. Prefer consuming apps to pass translated labels (e.g., via \`useT('appPlatformAdmin')\`).
|
|
587
|
+
|
|
588
|
+
### Markdown routes and llms.txt
|
|
589
|
+
|
|
590
|
+
- Each web app exposes \`/llms\` (and \`/llms.txt\`, \`/llms.md\`) via rewrites. See [llmstxt.org](https://llmstxt.org/).
|
|
591
|
+
- Catch‑all markdown handler lives at \`app/[...slug].md/route.ts\`. It resolves a page descriptor from \`app/.presentations.manifest.json\` and renders via the \`presentations.v2\` engine (target: \`markdown\`).
|
|
592
|
+
- Per‑page companion convention: add \`app/<route>/ai.ts\` exporting a \`PresentationSpec\`.
|
|
593
|
+
- Build‑time tool: \`tools/generate-presentations-manifest.mjs <app-root>\` populates the manifest.
|
|
594
|
+
- CI check: \`bunllms:check\` verifies coverage (% of pages with descriptors) and fails if below threshold.
|
|
595
|
+
`
|
|
596
|
+
}
|
|
597
|
+
];
|
|
520
598
|
export {
|
|
521
599
|
withPresentationNextAliases,
|
|
522
600
|
withPresentationMetroAliases,
|
|
601
|
+
tech_presentation_runtime_DocBlocks,
|
|
523
602
|
formatVisualizationValue,
|
|
524
603
|
createVisualizationModel,
|
|
525
604
|
createEmptyTableState,
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// src/transform-engine.markdown.ts
|
|
2
|
+
import TurndownService from "turndown";
|
|
3
|
+
function renderTextNode(node) {
|
|
4
|
+
const text = node.text ?? "";
|
|
5
|
+
if (!node.marks || node.marks.length === 0)
|
|
6
|
+
return text;
|
|
7
|
+
return node.marks.reduce((acc, mark) => {
|
|
8
|
+
switch (mark.type) {
|
|
9
|
+
case "bold":
|
|
10
|
+
return `**${acc}**`;
|
|
11
|
+
case "italic":
|
|
12
|
+
return `*${acc}*`;
|
|
13
|
+
case "underline":
|
|
14
|
+
return `__${acc}__`;
|
|
15
|
+
case "strike":
|
|
16
|
+
return `~~${acc}~~`;
|
|
17
|
+
case "code":
|
|
18
|
+
return `\`${acc}\``;
|
|
19
|
+
case "link": {
|
|
20
|
+
const href = mark.attrs?.href ?? "";
|
|
21
|
+
return href ? `[${acc}](${href})` : acc;
|
|
22
|
+
}
|
|
23
|
+
default:
|
|
24
|
+
return acc;
|
|
25
|
+
}
|
|
26
|
+
}, text);
|
|
27
|
+
}
|
|
28
|
+
function renderInline(nodes) {
|
|
29
|
+
if (!nodes?.length)
|
|
30
|
+
return "";
|
|
31
|
+
return nodes.map((child) => renderNode(child)).join("");
|
|
32
|
+
}
|
|
33
|
+
function renderList(nodes, ordered = false) {
|
|
34
|
+
if (!nodes?.length)
|
|
35
|
+
return "";
|
|
36
|
+
let counter = 1;
|
|
37
|
+
return nodes.map((item) => {
|
|
38
|
+
const body = renderInline(item.content ?? []);
|
|
39
|
+
if (!body)
|
|
40
|
+
return "";
|
|
41
|
+
const prefix = ordered ? `${counter++}. ` : "- ";
|
|
42
|
+
return `${prefix}${body}`;
|
|
43
|
+
}).filter(Boolean).join(`
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
function renderNode(node) {
|
|
47
|
+
switch (node.type) {
|
|
48
|
+
case "doc":
|
|
49
|
+
return renderInline(node.content);
|
|
50
|
+
case "paragraph": {
|
|
51
|
+
const text = renderInline(node.content);
|
|
52
|
+
return text.trim().length ? text : "";
|
|
53
|
+
}
|
|
54
|
+
case "heading": {
|
|
55
|
+
const levelAttr = node.attrs?.level;
|
|
56
|
+
const levelVal = typeof levelAttr === "number" ? levelAttr : 1;
|
|
57
|
+
const level = Math.min(Math.max(levelVal, 1), 6);
|
|
58
|
+
return `${"#".repeat(level)} ${renderInline(node.content)}`.trim();
|
|
59
|
+
}
|
|
60
|
+
case "bullet_list":
|
|
61
|
+
return renderList(node.content, false);
|
|
62
|
+
case "ordered_list":
|
|
63
|
+
return renderList(node.content, true);
|
|
64
|
+
case "list_item":
|
|
65
|
+
return renderInline(node.content);
|
|
66
|
+
case "blockquote": {
|
|
67
|
+
const body = renderInline(node.content);
|
|
68
|
+
return body.split(`
|
|
69
|
+
`).map((line) => `> ${line}`).join(`
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
case "code_block": {
|
|
73
|
+
const body = renderInline(node.content);
|
|
74
|
+
return body ? `\`\`\`
|
|
75
|
+
${body}
|
|
76
|
+
\`\`\`` : "";
|
|
77
|
+
}
|
|
78
|
+
case "horizontal_rule":
|
|
79
|
+
return "---";
|
|
80
|
+
case "hard_break":
|
|
81
|
+
return `
|
|
82
|
+
`;
|
|
83
|
+
case "text":
|
|
84
|
+
return renderTextNode(node);
|
|
85
|
+
default:
|
|
86
|
+
if (node.text)
|
|
87
|
+
return renderTextNode(node);
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function createMarkdownTurndownService() {
|
|
92
|
+
const turndownService = new TurndownService({
|
|
93
|
+
headingStyle: "atx",
|
|
94
|
+
codeBlockStyle: "fenced",
|
|
95
|
+
bulletListMarker: "-"
|
|
96
|
+
});
|
|
97
|
+
turndownService.addRule("link", {
|
|
98
|
+
filter: "a",
|
|
99
|
+
replacement: (content, node) => {
|
|
100
|
+
const candidate = node;
|
|
101
|
+
const href = candidate.getAttribute?.("href") ?? (typeof candidate.href === "string" ? candidate.href : "");
|
|
102
|
+
if (href && content) {
|
|
103
|
+
return `[${content}](${href})`;
|
|
104
|
+
}
|
|
105
|
+
return content || "";
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return turndownService;
|
|
109
|
+
}
|
|
110
|
+
var defaultTurndown = createMarkdownTurndownService();
|
|
111
|
+
function htmlToMarkdown(html) {
|
|
112
|
+
return defaultTurndown.turndown(html);
|
|
113
|
+
}
|
|
114
|
+
function blockNoteToMarkdown(docJson) {
|
|
115
|
+
if (typeof docJson === "string")
|
|
116
|
+
return docJson;
|
|
117
|
+
if (docJson && typeof docJson === "object" && "html" in docJson) {
|
|
118
|
+
const html = String(docJson.html);
|
|
119
|
+
return htmlToMarkdown(html);
|
|
120
|
+
}
|
|
121
|
+
const root = docJson;
|
|
122
|
+
if (root?.type === "doc" || root?.content) {
|
|
123
|
+
const blocks = (root.content ?? []).map((node) => renderNode(node)).filter(Boolean);
|
|
124
|
+
return blocks.join(`
|
|
125
|
+
|
|
126
|
+
`).trim();
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
return JSON.stringify(docJson, null, 2);
|
|
130
|
+
} catch {
|
|
131
|
+
return String(docJson);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/transform-engine.ts
|
|
136
|
+
import { schemaToMarkdown } from "@contractspec/lib.contracts-spec/schema-to-markdown";
|
|
137
|
+
function applyPii(desc, obj) {
|
|
138
|
+
let clone;
|
|
139
|
+
try {
|
|
140
|
+
clone = JSON.parse(JSON.stringify(obj));
|
|
141
|
+
} catch {
|
|
142
|
+
clone = obj;
|
|
143
|
+
}
|
|
144
|
+
const paths = desc.policy?.pii ?? [];
|
|
145
|
+
const setAtPath = (root, path) => {
|
|
146
|
+
const segments = path.replace(/^\//, "").replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
|
|
147
|
+
let current = root;
|
|
148
|
+
for (let i = 0;i < segments.length - 1; i++) {
|
|
149
|
+
const key = segments[i];
|
|
150
|
+
if (!key || !current || typeof current !== "object" || !(key in current)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
current = current[key];
|
|
154
|
+
}
|
|
155
|
+
const last = segments[segments.length - 1];
|
|
156
|
+
if (current && typeof current === "object" && last && last in current) {
|
|
157
|
+
current[last] = "[REDACTED]";
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
for (const path of paths) {
|
|
161
|
+
setAtPath(clone, path);
|
|
162
|
+
}
|
|
163
|
+
return clone;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
class TransformEngine {
|
|
167
|
+
renderers = new Map;
|
|
168
|
+
validators = [];
|
|
169
|
+
register(renderer) {
|
|
170
|
+
const renderers = this.renderers.get(renderer.target) ?? [];
|
|
171
|
+
renderers.push(renderer);
|
|
172
|
+
this.renderers.set(renderer.target, renderers);
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
175
|
+
prependRegister(renderer) {
|
|
176
|
+
const renderers = this.renderers.get(renderer.target) ?? [];
|
|
177
|
+
renderers.unshift(renderer);
|
|
178
|
+
this.renderers.set(renderer.target, renderers);
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
addValidator(validator) {
|
|
182
|
+
this.validators.push(validator);
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
async render(target, desc, ctx) {
|
|
186
|
+
if (!desc.targets.includes(target)) {
|
|
187
|
+
throw new Error(`Target ${target} not declared for ${desc.meta.key}.v${desc.meta.version}`);
|
|
188
|
+
}
|
|
189
|
+
for (const validator of this.validators) {
|
|
190
|
+
await validator.validate(desc, target, ctx);
|
|
191
|
+
}
|
|
192
|
+
const renderers = this.renderers.get(target) ?? [];
|
|
193
|
+
for (const renderer of renderers) {
|
|
194
|
+
try {
|
|
195
|
+
return await renderer.render(desc, ctx);
|
|
196
|
+
} catch {}
|
|
197
|
+
}
|
|
198
|
+
throw new Error(`No renderer available for ${target}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function createDefaultTransformEngine() {
|
|
202
|
+
const engine = new TransformEngine;
|
|
203
|
+
engine.register({
|
|
204
|
+
target: "markdown",
|
|
205
|
+
async render(desc, ctx) {
|
|
206
|
+
let data = ctx?.data;
|
|
207
|
+
if (!data && ctx?.fetchData) {
|
|
208
|
+
data = await ctx.fetchData();
|
|
209
|
+
}
|
|
210
|
+
if (desc.source.type === "component" && desc.source.props && data !== undefined) {
|
|
211
|
+
const isArray = Array.isArray(data);
|
|
212
|
+
const isSimpleObject = !isArray && typeof data === "object" && data !== null && !Object.values(data).some((value) => Array.isArray(value) || typeof value === "object" && value !== null);
|
|
213
|
+
if (isArray || isSimpleObject) {
|
|
214
|
+
return {
|
|
215
|
+
mimeType: "text/markdown",
|
|
216
|
+
body: schemaToMarkdown(desc.source.props, data, {
|
|
217
|
+
title: desc.meta.description ?? desc.meta.key,
|
|
218
|
+
description: `${desc.meta.key} v${desc.meta.version}`
|
|
219
|
+
})
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
throw new Error(`Complex data structure for ${desc.meta.key} - expecting custom renderer`);
|
|
223
|
+
}
|
|
224
|
+
if (desc.source.type === "blocknotejs") {
|
|
225
|
+
const redacted = applyPii(desc, {
|
|
226
|
+
text: blockNoteToMarkdown(desc.source.docJson)
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
mimeType: "text/markdown",
|
|
230
|
+
body: String(redacted.text ?? "")
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
if (desc.source.type === "component" && data !== undefined) {
|
|
234
|
+
throw new Error(`No schema (source.props) available for ${desc.meta.key} - expecting custom renderer`);
|
|
235
|
+
}
|
|
236
|
+
if (desc.source.type !== "component") {
|
|
237
|
+
throw new Error("unsupported");
|
|
238
|
+
}
|
|
239
|
+
const header = `# ${desc.meta.key} v${desc.meta.version}`;
|
|
240
|
+
const about = desc.meta.description ? `
|
|
241
|
+
|
|
242
|
+
${desc.meta.description}` : "";
|
|
243
|
+
const tags = desc.meta.tags && desc.meta.tags.length ? `
|
|
244
|
+
|
|
245
|
+
Tags: ${desc.meta.tags.join(", ")}` : "";
|
|
246
|
+
const owners = desc.meta.owners && desc.meta.owners.length ? `
|
|
247
|
+
|
|
248
|
+
Owners: ${desc.meta.owners.join(", ")}` : "";
|
|
249
|
+
const component = `
|
|
250
|
+
|
|
251
|
+
Component: \`${desc.source.componentKey}\``;
|
|
252
|
+
const policy = desc.policy?.pii?.length ? `
|
|
253
|
+
|
|
254
|
+
Redacted paths: ${desc.policy.pii.map((path) => `\`${path}\``).join(", ")}` : "";
|
|
255
|
+
return {
|
|
256
|
+
mimeType: "text/markdown",
|
|
257
|
+
body: `${header}${about}${tags}${owners}${component}${policy}`
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
engine.register({
|
|
262
|
+
target: "application/json",
|
|
263
|
+
async render(desc) {
|
|
264
|
+
const payload = applyPii(desc, { meta: desc.meta, source: desc.source });
|
|
265
|
+
let body;
|
|
266
|
+
try {
|
|
267
|
+
body = JSON.stringify(payload, null, 2);
|
|
268
|
+
} catch {
|
|
269
|
+
body = JSON.stringify({
|
|
270
|
+
meta: { key: desc.meta.key, version: desc.meta.version },
|
|
271
|
+
source: "[non-serializable]"
|
|
272
|
+
}, null, 2);
|
|
273
|
+
}
|
|
274
|
+
return { mimeType: "application/json", body };
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
engine.register({
|
|
278
|
+
target: "application/xml",
|
|
279
|
+
async render(desc) {
|
|
280
|
+
const payload = applyPii(desc, { meta: desc.meta, source: desc.source });
|
|
281
|
+
let json;
|
|
282
|
+
try {
|
|
283
|
+
json = JSON.stringify(payload);
|
|
284
|
+
} catch {
|
|
285
|
+
json = JSON.stringify({
|
|
286
|
+
meta: { key: desc.meta.key, version: desc.meta.version },
|
|
287
|
+
source: "[non-serializable]"
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
mimeType: "application/xml",
|
|
292
|
+
body: `<presentation name="${desc.meta.key}" version="${desc.meta.version}"><json>${encodeURIComponent(json)}</json></presentation>`
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
return engine;
|
|
297
|
+
}
|
|
298
|
+
function registerBasicValidation(engine) {
|
|
299
|
+
engine.addValidator({
|
|
300
|
+
validate(desc) {
|
|
301
|
+
if (!desc.meta.description || desc.meta.description.length < 3) {
|
|
302
|
+
throw new Error(`Presentation ${desc.meta.key}.v${desc.meta.version} missing meta.description`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
return engine;
|
|
307
|
+
}
|
|
308
|
+
export {
|
|
309
|
+
registerBasicValidation,
|
|
310
|
+
htmlToMarkdown,
|
|
311
|
+
createDefaultTransformEngine,
|
|
312
|
+
blockNoteToMarkdown,
|
|
313
|
+
TransformEngine
|
|
314
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// src/transform-engine.markdown.ts
|
|
2
|
+
import TurndownService from "turndown";
|
|
3
|
+
function renderTextNode(node) {
|
|
4
|
+
const text = node.text ?? "";
|
|
5
|
+
if (!node.marks || node.marks.length === 0)
|
|
6
|
+
return text;
|
|
7
|
+
return node.marks.reduce((acc, mark) => {
|
|
8
|
+
switch (mark.type) {
|
|
9
|
+
case "bold":
|
|
10
|
+
return `**${acc}**`;
|
|
11
|
+
case "italic":
|
|
12
|
+
return `*${acc}*`;
|
|
13
|
+
case "underline":
|
|
14
|
+
return `__${acc}__`;
|
|
15
|
+
case "strike":
|
|
16
|
+
return `~~${acc}~~`;
|
|
17
|
+
case "code":
|
|
18
|
+
return `\`${acc}\``;
|
|
19
|
+
case "link": {
|
|
20
|
+
const href = mark.attrs?.href ?? "";
|
|
21
|
+
return href ? `[${acc}](${href})` : acc;
|
|
22
|
+
}
|
|
23
|
+
default:
|
|
24
|
+
return acc;
|
|
25
|
+
}
|
|
26
|
+
}, text);
|
|
27
|
+
}
|
|
28
|
+
function renderInline(nodes) {
|
|
29
|
+
if (!nodes?.length)
|
|
30
|
+
return "";
|
|
31
|
+
return nodes.map((child) => renderNode(child)).join("");
|
|
32
|
+
}
|
|
33
|
+
function renderList(nodes, ordered = false) {
|
|
34
|
+
if (!nodes?.length)
|
|
35
|
+
return "";
|
|
36
|
+
let counter = 1;
|
|
37
|
+
return nodes.map((item) => {
|
|
38
|
+
const body = renderInline(item.content ?? []);
|
|
39
|
+
if (!body)
|
|
40
|
+
return "";
|
|
41
|
+
const prefix = ordered ? `${counter++}. ` : "- ";
|
|
42
|
+
return `${prefix}${body}`;
|
|
43
|
+
}).filter(Boolean).join(`
|
|
44
|
+
`);
|
|
45
|
+
}
|
|
46
|
+
function renderNode(node) {
|
|
47
|
+
switch (node.type) {
|
|
48
|
+
case "doc":
|
|
49
|
+
return renderInline(node.content);
|
|
50
|
+
case "paragraph": {
|
|
51
|
+
const text = renderInline(node.content);
|
|
52
|
+
return text.trim().length ? text : "";
|
|
53
|
+
}
|
|
54
|
+
case "heading": {
|
|
55
|
+
const levelAttr = node.attrs?.level;
|
|
56
|
+
const levelVal = typeof levelAttr === "number" ? levelAttr : 1;
|
|
57
|
+
const level = Math.min(Math.max(levelVal, 1), 6);
|
|
58
|
+
return `${"#".repeat(level)} ${renderInline(node.content)}`.trim();
|
|
59
|
+
}
|
|
60
|
+
case "bullet_list":
|
|
61
|
+
return renderList(node.content, false);
|
|
62
|
+
case "ordered_list":
|
|
63
|
+
return renderList(node.content, true);
|
|
64
|
+
case "list_item":
|
|
65
|
+
return renderInline(node.content);
|
|
66
|
+
case "blockquote": {
|
|
67
|
+
const body = renderInline(node.content);
|
|
68
|
+
return body.split(`
|
|
69
|
+
`).map((line) => `> ${line}`).join(`
|
|
70
|
+
`);
|
|
71
|
+
}
|
|
72
|
+
case "code_block": {
|
|
73
|
+
const body = renderInline(node.content);
|
|
74
|
+
return body ? `\`\`\`
|
|
75
|
+
${body}
|
|
76
|
+
\`\`\`` : "";
|
|
77
|
+
}
|
|
78
|
+
case "horizontal_rule":
|
|
79
|
+
return "---";
|
|
80
|
+
case "hard_break":
|
|
81
|
+
return `
|
|
82
|
+
`;
|
|
83
|
+
case "text":
|
|
84
|
+
return renderTextNode(node);
|
|
85
|
+
default:
|
|
86
|
+
if (node.text)
|
|
87
|
+
return renderTextNode(node);
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function createMarkdownTurndownService() {
|
|
92
|
+
const turndownService = new TurndownService({
|
|
93
|
+
headingStyle: "atx",
|
|
94
|
+
codeBlockStyle: "fenced",
|
|
95
|
+
bulletListMarker: "-"
|
|
96
|
+
});
|
|
97
|
+
turndownService.addRule("link", {
|
|
98
|
+
filter: "a",
|
|
99
|
+
replacement: (content, node) => {
|
|
100
|
+
const candidate = node;
|
|
101
|
+
const href = candidate.getAttribute?.("href") ?? (typeof candidate.href === "string" ? candidate.href : "");
|
|
102
|
+
if (href && content) {
|
|
103
|
+
return `[${content}](${href})`;
|
|
104
|
+
}
|
|
105
|
+
return content || "";
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return turndownService;
|
|
109
|
+
}
|
|
110
|
+
var defaultTurndown = createMarkdownTurndownService();
|
|
111
|
+
function htmlToMarkdown(html) {
|
|
112
|
+
return defaultTurndown.turndown(html);
|
|
113
|
+
}
|
|
114
|
+
function blockNoteToMarkdown(docJson) {
|
|
115
|
+
if (typeof docJson === "string")
|
|
116
|
+
return docJson;
|
|
117
|
+
if (docJson && typeof docJson === "object" && "html" in docJson) {
|
|
118
|
+
const html = String(docJson.html);
|
|
119
|
+
return htmlToMarkdown(html);
|
|
120
|
+
}
|
|
121
|
+
const root = docJson;
|
|
122
|
+
if (root?.type === "doc" || root?.content) {
|
|
123
|
+
const blocks = (root.content ?? []).map((node) => renderNode(node)).filter(Boolean);
|
|
124
|
+
return blocks.join(`
|
|
125
|
+
|
|
126
|
+
`).trim();
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
return JSON.stringify(docJson, null, 2);
|
|
130
|
+
} catch {
|
|
131
|
+
return String(docJson);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export {
|
|
135
|
+
htmlToMarkdown,
|
|
136
|
+
createMarkdownTurndownService,
|
|
137
|
+
blockNoteToMarkdown
|
|
138
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DocBlock } from '@contractspec/lib.contracts-spec/docs';
|
|
1
2
|
export interface ListState<TFilters extends Record<string, unknown>> {
|
|
2
3
|
q: string;
|
|
3
4
|
page: number;
|
|
@@ -26,3 +27,4 @@ export type MetroAliasOptions = NextAliasOptions & {
|
|
|
26
27
|
monorepoRoot?: string;
|
|
27
28
|
};
|
|
28
29
|
export declare function withPresentationMetroAliases(config: any, opts?: MetroAliasOptions): any;
|
|
30
|
+
export declare const tech_presentation_runtime_DocBlocks: DocBlock[];
|
package/dist/index.js
CHANGED
|
@@ -518,9 +518,88 @@ function withPresentationMetroAliases(config, opts = {}) {
|
|
|
518
518
|
};
|
|
519
519
|
return config;
|
|
520
520
|
}
|
|
521
|
+
var tech_presentation_runtime_DocBlocks = [
|
|
522
|
+
{
|
|
523
|
+
id: "docs.tech.presentation-runtime",
|
|
524
|
+
title: "Presentation Runtime",
|
|
525
|
+
summary: "Cross-platform runtime for list pages, presentation flows, and headless ContractSpec tables.",
|
|
526
|
+
kind: "reference",
|
|
527
|
+
visibility: "public",
|
|
528
|
+
route: "/docs/tech/presentation-runtime",
|
|
529
|
+
tags: ["tech", "presentation-runtime"],
|
|
530
|
+
body: `## Presentation Runtime
|
|
531
|
+
|
|
532
|
+
Cross-platform runtime for list pages, presentation flows, and headless ContractSpec tables.
|
|
533
|
+
|
|
534
|
+
### Packages
|
|
535
|
+
|
|
536
|
+
- \`@contractspec/lib.presentation-runtime-core\`: shared state/types for URL state, list coordination, and headless table controllers
|
|
537
|
+
- \`@contractspec/lib.presentation-runtime-react\`: React hooks (web/native-compatible API)
|
|
538
|
+
- \`@contractspec/lib.presentation-runtime-react-native\`: Native entrypoint (re-exports the shared React table hooks and keeps native form/list helpers)
|
|
539
|
+
|
|
540
|
+
### Next.js config helper
|
|
541
|
+
|
|
542
|
+
\`\`\`ts
|
|
543
|
+
// next.config.mjs
|
|
544
|
+
import { withPresentationNextAliases } from '@contractspec/lib.presentation-runtime-core/next';
|
|
545
|
+
|
|
546
|
+
const nextConfig = {
|
|
547
|
+
webpack: (config) => withPresentationNextAliases(config),
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
export default nextConfig;
|
|
551
|
+
\`\`\`
|
|
552
|
+
|
|
553
|
+
### Metro config helper
|
|
554
|
+
|
|
555
|
+
\`\`\`js
|
|
556
|
+
// metro.config.js (CJS)
|
|
557
|
+
const { getDefaultConfig } = require('expo/metro-config');
|
|
558
|
+
const {
|
|
559
|
+
withPresentationMetroAliases,
|
|
560
|
+
} = require('@contractspec/lib.presentation-runtime-core/src/metro.cjs');
|
|
561
|
+
|
|
562
|
+
const projectRoot = __dirname;
|
|
563
|
+
const config = getDefaultConfig(projectRoot);
|
|
564
|
+
|
|
565
|
+
module.exports = withPresentationMetroAliases(config);
|
|
566
|
+
\`\`\`
|
|
567
|
+
|
|
568
|
+
### React hooks
|
|
569
|
+
|
|
570
|
+
- \`useListCoordinator\`: URL + RHF + derived variables (no fetching)
|
|
571
|
+
- \`usePresentationController\`: Same plus \`fetcher\` integration
|
|
572
|
+
- \`useContractTable\`: headless TanStack-backed controller for sorting, pagination, selection, visibility, pinning, resizing, and expansion
|
|
573
|
+
- \`useDataViewTable\`: adapter that maps \`DataViewSpec\` table contracts onto the generic controller
|
|
574
|
+
- \`DataViewRenderer\` (design-system): render \`DataViewSpec\` projections (\`list\`, \`table\`, \`detail\`, \`grid\`) using shared UI atoms
|
|
575
|
+
|
|
576
|
+
Both list hooks accept a \`useUrlState\` adapter. On web, use \`useListUrlState\` (design-system) or a Next adapter.
|
|
577
|
+
|
|
578
|
+
### Table layering
|
|
579
|
+
|
|
580
|
+
- \`contracts-spec\`: declarative table contract (\`DataViewTableConfig\`)
|
|
581
|
+
- \`presentation-runtime-*\`: headless controller state and TanStack integration
|
|
582
|
+
- \`ui-kit\` / \`ui-kit-web\`: platform renderers that consume controller output
|
|
583
|
+
- \`design-system\`: opinionated \`DataTable\`, \`DataViewTable\`, \`ApprovalQueue\`, and \`ListTablePage\`
|
|
584
|
+
|
|
585
|
+
### KYC molecules (bundle)
|
|
586
|
+
|
|
587
|
+
- \`ComplianceBadge\` in \`@contractspec/bundle.strit/presentation/components/kyc\` renders a status badge for KYC/compliance snapshots. It accepts a \`state\` (missing_core | incomplete | complete | expiring | unknown) and optional localized \`labels\`. Prefer consuming apps to pass translated labels (e.g., via \`useT('appPlatformAdmin')\`).
|
|
588
|
+
|
|
589
|
+
### Markdown routes and llms.txt
|
|
590
|
+
|
|
591
|
+
- Each web app exposes \`/llms\` (and \`/llms.txt\`, \`/llms.md\`) via rewrites. See [llmstxt.org](https://llmstxt.org/).
|
|
592
|
+
- Catch\u2011all markdown handler lives at \`app/[...slug].md/route.ts\`. It resolves a page descriptor from \`app/.presentations.manifest.json\` and renders via the \`presentations.v2\` engine (target: \`markdown\`).
|
|
593
|
+
- Per\u2011page companion convention: add \`app/<route>/ai.ts\` exporting a \`PresentationSpec\`.
|
|
594
|
+
- Build\u2011time tool: \`tools/generate-presentations-manifest.mjs <app-root>\` populates the manifest.
|
|
595
|
+
- CI check: \`bunllms:check\` verifies coverage (% of pages with descriptors) and fails if below threshold.
|
|
596
|
+
`
|
|
597
|
+
}
|
|
598
|
+
];
|
|
521
599
|
export {
|
|
522
600
|
withPresentationNextAliases,
|
|
523
601
|
withPresentationMetroAliases,
|
|
602
|
+
tech_presentation_runtime_DocBlocks,
|
|
524
603
|
formatVisualizationValue,
|
|
525
604
|
createVisualizationModel,
|
|
526
605
|
createEmptyTableState,
|