@dotit/core 1.0.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +229 -0
  3. package/dist/aliases.d.ts +1 -0
  4. package/dist/aliases.js +8 -0
  5. package/dist/ask.d.ts +7 -0
  6. package/dist/ask.js +55 -0
  7. package/dist/browser.d.ts +12 -0
  8. package/dist/browser.js +32 -0
  9. package/dist/diff.d.ts +17 -0
  10. package/dist/diff.js +179 -0
  11. package/dist/document-css.d.ts +1 -0
  12. package/dist/document-css.js +290 -0
  13. package/dist/executor.d.ts +40 -0
  14. package/dist/executor.js +501 -0
  15. package/dist/history.d.ts +10 -0
  16. package/dist/history.js +297 -0
  17. package/dist/html-to-it.d.ts +1 -0
  18. package/dist/html-to-it.js +288 -0
  19. package/dist/index-builder.d.ts +62 -0
  20. package/dist/index-builder.js +228 -0
  21. package/dist/index.d.ts +39 -0
  22. package/dist/index.js +94 -0
  23. package/dist/language-registry.d.ts +39 -0
  24. package/dist/language-registry.js +530 -0
  25. package/dist/markdown.d.ts +1 -0
  26. package/dist/markdown.js +123 -0
  27. package/dist/merge.d.ts +6 -0
  28. package/dist/merge.js +255 -0
  29. package/dist/parser.d.ts +29 -0
  30. package/dist/parser.js +1562 -0
  31. package/dist/query.d.ts +32 -0
  32. package/dist/query.js +293 -0
  33. package/dist/renderer.d.ts +16 -0
  34. package/dist/renderer.js +1286 -0
  35. package/dist/schema.d.ts +47 -0
  36. package/dist/schema.js +574 -0
  37. package/dist/source.d.ts +3 -0
  38. package/dist/source.js +223 -0
  39. package/dist/theme.d.ts +49 -0
  40. package/dist/theme.js +113 -0
  41. package/dist/themes/corporate.json +86 -0
  42. package/dist/themes/dark.json +64 -0
  43. package/dist/themes/editorial.json +54 -0
  44. package/dist/themes/legal.json +57 -0
  45. package/dist/themes/minimal.json +50 -0
  46. package/dist/themes/print.json +54 -0
  47. package/dist/themes/technical.json +59 -0
  48. package/dist/themes/warm.json +53 -0
  49. package/dist/trust.d.ts +66 -0
  50. package/dist/trust.js +200 -0
  51. package/dist/types.d.ts +234 -0
  52. package/dist/types.js +19 -0
  53. package/dist/utils.d.ts +2 -0
  54. package/dist/utils.js +13 -0
  55. package/dist/validate.d.ts +13 -0
  56. package/dist/validate.js +711 -0
  57. package/dist/workflow.d.ts +18 -0
  58. package/dist/workflow.js +160 -0
  59. package/package.json +51 -0
@@ -0,0 +1,1286 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DOC_STYLE_TARGETS = void 0;
4
+ exports.collectPrintLayout = collectPrintLayout;
5
+ exports.cssContentValue = cssContentValue;
6
+ exports.collectDocumentStyles = collectDocumentStyles;
7
+ exports.documentStyleCSS = documentStyleCSS;
8
+ exports.renderHTML = renderHTML;
9
+ exports.renderPrint = renderPrint;
10
+ const document_css_1 = require("./document-css");
11
+ const theme_1 = require("./theme");
12
+ function resolveThemeSync(ref) {
13
+ if (ref && typeof ref === "object")
14
+ return ref;
15
+ if (typeof ref === "string") {
16
+ const found = (0, theme_1.getBuiltinTheme)(ref);
17
+ if (found)
18
+ return found;
19
+ }
20
+ return (0, theme_1.getBuiltinTheme)("corporate");
21
+ }
22
+ const PAPER_SIZES = {
23
+ A4: "A4",
24
+ A5: "A5",
25
+ A3: "297mm 420mm",
26
+ Letter: "Letter",
27
+ Legal: "8.5in 14in",
28
+ };
29
+ function collectPrintLayout(doc) {
30
+ const allBlocks = doc.blocks.flatMap(function collect(b) {
31
+ return [b, ...(b.children ?? []).flatMap(collect)];
32
+ });
33
+ return {
34
+ page: allBlocks.find((b) => b.type === "page"),
35
+ header: allBlocks.filter((b) => b.type === "header").pop(),
36
+ footer: allBlocks.filter((b) => b.type === "footer").pop(),
37
+ watermark: allBlocks.filter((b) => b.type === "watermark").pop(),
38
+ breaks: allBlocks.filter((b) => b.type === "break" && (b.properties?.before || b.properties?.keep)),
39
+ };
40
+ }
41
+ function escapeHtml(text) {
42
+ return text
43
+ .replace(/&/g, "&")
44
+ .replace(/</g, "&lt;")
45
+ .replace(/>/g, "&gt;")
46
+ .replace(/\"/g, "&quot;")
47
+ .replace(/'/g, "&#39;");
48
+ }
49
+ function cssContentValue(text) {
50
+ if (!text)
51
+ return '""';
52
+ const parts = String(text)
53
+ .split(/(\{\{\s*pages?\s*\}\})/g)
54
+ .filter((p) => p !== "");
55
+ return (parts
56
+ .map((p) => {
57
+ if (/^\{\{\s*page\s*\}\}$/.test(p))
58
+ return "counter(page)";
59
+ if (/^\{\{\s*pages\s*\}\}$/.test(p))
60
+ return "counter(pages)";
61
+ return ('"' +
62
+ p
63
+ .replace(/\\/g, "\\\\")
64
+ .replace(/"/g, '\\"')
65
+ .replace(/[\r\n]+/g, " ") +
66
+ '"');
67
+ })
68
+ .join(" ") || '""');
69
+ }
70
+ function formatTrustDate(isoStr) {
71
+ try {
72
+ const d = new Date(isoStr);
73
+ if (isNaN(d.getTime()))
74
+ return escapeHtml(isoStr);
75
+ const months = [
76
+ "January",
77
+ "February",
78
+ "March",
79
+ "April",
80
+ "May",
81
+ "June",
82
+ "July",
83
+ "August",
84
+ "September",
85
+ "October",
86
+ "November",
87
+ "December",
88
+ ];
89
+ const day = d.getUTCDate();
90
+ const month = months[d.getUTCMonth()];
91
+ const year = d.getUTCFullYear();
92
+ const hours = String(d.getUTCHours()).padStart(2, "0");
93
+ const minutes = String(d.getUTCMinutes()).padStart(2, "0");
94
+ return `${day} ${month} ${year}, ${hours}:${minutes} UTC`;
95
+ }
96
+ catch {
97
+ return escapeHtml(isoStr);
98
+ }
99
+ }
100
+ function sanitizeUrl(url) {
101
+ const trimmed = url.trim();
102
+ if (trimmed === "")
103
+ return "";
104
+ if (trimmed.startsWith("/") ||
105
+ trimmed.startsWith("./") ||
106
+ trimmed.startsWith("../") ||
107
+ trimmed.startsWith("#")) {
108
+ return trimmed;
109
+ }
110
+ const lower = trimmed.toLowerCase();
111
+ if (lower.startsWith("javascript:") ||
112
+ lower.startsWith("vbscript:") ||
113
+ lower.startsWith("data:")) {
114
+ return "#";
115
+ }
116
+ if (!lower.includes(":") && !lower.startsWith("//")) {
117
+ return trimmed;
118
+ }
119
+ if (lower.startsWith("http://") ||
120
+ lower.startsWith("https://") ||
121
+ lower.startsWith("mailto:") ||
122
+ lower.startsWith("tel:")) {
123
+ return trimmed;
124
+ }
125
+ return "#";
126
+ }
127
+ function applyInlineFormatting(content, inline, originalContent) {
128
+ if (inline && inline.length > 0) {
129
+ return inline
130
+ .map((node) => {
131
+ switch (node.type) {
132
+ case "text":
133
+ return escapeHtml(node.value);
134
+ case "bold":
135
+ return `<strong>${escapeHtml(node.value)}</strong>`;
136
+ case "italic":
137
+ return `<em>${escapeHtml(node.value)}</em>`;
138
+ case "strike":
139
+ return `<del>${escapeHtml(node.value)}</del>`;
140
+ case "inline-quote":
141
+ return `<q class="intent-inline-quote">${escapeHtml(node.value)}</q>`;
142
+ case "highlight":
143
+ return `<mark class="intent-inline-highlight">${escapeHtml(node.value)}</mark>`;
144
+ case "code":
145
+ return `<code>${escapeHtml(node.value)}</code>`;
146
+ case "styled": {
147
+ const css = extractInlineStyles(node.props);
148
+ return css
149
+ ? `<span style="${css}">${escapeHtml(node.value)}</span>`
150
+ : escapeHtml(node.value);
151
+ }
152
+ case "inline-note":
153
+ return `<span class="intent-inline-note">${escapeHtml(node.value)}</span>`;
154
+ case "date":
155
+ return `<time class="intent-inline-date" datetime="${escapeHtml(node.iso)}">${escapeHtml(node.value)}</time>`;
156
+ case "mention":
157
+ return `<span class="intent-inline-mention">@${escapeHtml(node.value)}</span>`;
158
+ case "tag":
159
+ return `<span class="intent-inline-tag">#${escapeHtml(node.value)}</span>`;
160
+ case "link":
161
+ return `<a href="${escapeHtml(sanitizeUrl(node.href))}" class="intent-inline-link">${escapeHtml(node.value)}</a>`;
162
+ case "footnote-ref":
163
+ return `<sup class="it-fn-ref"><a href="#fn-${escapeHtml(node.value)}">${escapeHtml(node.value)}</a></sup>`;
164
+ case "label":
165
+ return `<span class="it-label">${escapeHtml(node.value)}</span>`;
166
+ default:
167
+ return escapeHtml(node.value);
168
+ }
169
+ })
170
+ .join("");
171
+ }
172
+ return escapeHtml(originalContent || content);
173
+ }
174
+ function getAlignmentClass(props) {
175
+ const raw = String(props.align || "")
176
+ .toLowerCase()
177
+ .trim();
178
+ if (raw === "center")
179
+ return " intent-align-center";
180
+ if (raw === "right")
181
+ return " intent-align-right";
182
+ if (raw === "justify")
183
+ return " intent-align-justify";
184
+ return "";
185
+ }
186
+ const STYLE_PROPERTIES = {
187
+ color: "color",
188
+ size: "font-size",
189
+ family: "font-family",
190
+ weight: "font-weight",
191
+ align: "text-align",
192
+ bg: "background-color",
193
+ indent: "padding-left",
194
+ opacity: "opacity",
195
+ italic: "font-style",
196
+ border: "border",
197
+ underline: "text-decoration",
198
+ strike: "text-decoration",
199
+ valign: "vertical-align",
200
+ };
201
+ function extractInlineStyles(properties, context = "attr") {
202
+ const styles = [];
203
+ const decorations = [];
204
+ for (const [prop, css] of Object.entries(STYLE_PROPERTIES)) {
205
+ const value = properties[prop];
206
+ if (value === undefined || value === "")
207
+ continue;
208
+ const strValue = context === "attr"
209
+ ? escapeHtml(String(value).replace(/[;{}]/g, ""))
210
+ : String(value).replace(/[<>{};]/g, "");
211
+ if (prop === "border" && strValue === "true") {
212
+ styles.push("border: 1px solid currentColor");
213
+ }
214
+ else if (prop === "italic" && strValue === "true") {
215
+ styles.push("font-style: italic");
216
+ }
217
+ else if (prop === "underline" && strValue === "true") {
218
+ decorations.push("underline");
219
+ }
220
+ else if (prop === "strike" && strValue === "true") {
221
+ decorations.push("line-through");
222
+ }
223
+ else if (prop !== "underline" && prop !== "strike") {
224
+ styles.push(`${css}: ${strValue}`);
225
+ }
226
+ }
227
+ if (decorations.length)
228
+ styles.push(`text-decoration: ${decorations.join(" ")}`);
229
+ return styles.join("; ");
230
+ }
231
+ exports.DOC_STYLE_TARGETS = {
232
+ title: [".intent-title"],
233
+ summary: [".intent-summary"],
234
+ section: [".intent-section"],
235
+ sub: [".intent-sub"],
236
+ text: [".intent-text"],
237
+ quote: [".intent-quote"],
238
+ callout: [".intent-callout"],
239
+ info: [".intent-callout"],
240
+ table: [".intent-table-th", ".intent-table-td"],
241
+ "table-header": [".intent-table-th"],
242
+ metric: [".it-metric-row"],
243
+ contact: [".it-contact"],
244
+ divider: [".intent-divider"],
245
+ };
246
+ function collectDocumentStyles(doc) {
247
+ const rules = [];
248
+ const walk = (blocks) => {
249
+ for (const b of blocks) {
250
+ if (b.type === "style" && b.content) {
251
+ const target = String(b.content).trim().toLowerCase();
252
+ if (exports.DOC_STYLE_TARGETS[target]) {
253
+ const declarations = extractInlineStyles(b.properties || {}, "stylesheet");
254
+ if (declarations)
255
+ rules.push({ target, declarations });
256
+ }
257
+ }
258
+ if (b.children)
259
+ walk(b.children);
260
+ }
261
+ };
262
+ walk(doc.blocks);
263
+ return rules;
264
+ }
265
+ function documentStyleCSS(doc, selectorMap = exports.DOC_STYLE_TARGETS, selectorPrefix = "") {
266
+ return collectDocumentStyles(doc)
267
+ .map((rule) => {
268
+ const sels = selectorMap[rule.target];
269
+ if (!sels || sels.length === 0)
270
+ return "";
271
+ const scoped = sels.map((s) => `${selectorPrefix}${s}`).join(",");
272
+ return `${scoped}{${rule.declarations};}`;
273
+ })
274
+ .filter(Boolean)
275
+ .join("\n");
276
+ }
277
+ function renderBlock(block) {
278
+ if (block.type === "agent" ||
279
+ block.type === "model" ||
280
+ block.type === "style") {
281
+ return "";
282
+ }
283
+ const content = applyInlineFormatting(block.content, block.inline, block.originalContent);
284
+ const props = block.properties || {};
285
+ const alignClass = getAlignmentClass(props);
286
+ const inlineStyle = extractInlineStyles(props);
287
+ const styleAttr = inlineStyle ? ` style="${inlineStyle}"` : "";
288
+ switch (block.type) {
289
+ case "title":
290
+ return `<h1 class="intent-title${alignClass}"${styleAttr}>${content}</h1>`;
291
+ case "summary":
292
+ return `<div class="intent-summary${alignClass}"${styleAttr}>${content}</div>`;
293
+ case "section":
294
+ return `<h2 id="${slugify(block.content)}" class="intent-section${alignClass}"${styleAttr}>${content}</h2>`;
295
+ case "sub":
296
+ return `<h3 id="${slugify(block.content)}" class="intent-sub${alignClass}"${styleAttr}>${content}</h3>`;
297
+ case "divider":
298
+ const dividerStyle = props.style
299
+ ? escapeHtml(String(props.style).replace(/[;{}"]/g, ""))
300
+ : "solid";
301
+ const label = content
302
+ ? `<span class="intent-divider-label">${content}</span>`
303
+ : "";
304
+ return `<div class="intent-divider">
305
+ <hr class="it-divider" style="border-style: ${dividerStyle}" />
306
+ ${label}
307
+ </div>`;
308
+ case "text":
309
+ return `<p class="intent-text${alignClass}"${styleAttr}>${content}</p>`;
310
+ case "body-text":
311
+ return `<p class="intent-prose${alignClass}"${styleAttr}>${content}</p>`;
312
+ case "info": {
313
+ const CALLOUT_VARIANTS = {
314
+ info: "Note",
315
+ warning: "Caution",
316
+ danger: "Danger",
317
+ tip: "Tip",
318
+ success: "Done",
319
+ };
320
+ const subtype = props.type || "info";
321
+ const variant = subtype in CALLOUT_VARIANTS ? subtype : "info";
322
+ const label = CALLOUT_VARIANTS[variant];
323
+ if (variant === "danger") {
324
+ return `<div class="it-callout it-danger" role="alert"${styleAttr}><span class="it-callout-icon" aria-hidden="true">⛔</span><div class="it-callout-body">${content}</div></div>`;
325
+ }
326
+ return `<div class="intent-callout intent-${variant}"${styleAttr}><span class="intent-callout-label">${label}</span><div class="intent-callout-content">${content}</div></div>`;
327
+ }
328
+ case "task":
329
+ case "done": {
330
+ const isDone = props.status === "done" || block.type === "done";
331
+ return `<div class="intent-task${isDone ? " intent-task-done" : ""}">
332
+ <input class="intent-task-checkbox" type="checkbox"${isDone ? " checked" : ""} />
333
+ <span class="intent-task-text${isDone ? " intent-task-text-done" : ""}">${content}</span>
334
+ <span class="intent-task-meta">
335
+ ${props.owner ? `<span class="intent-task-owner">${escapeHtml(String(props.owner))}</span>` : ""}
336
+ ${props.due ? `<span class="intent-task-due">${escapeHtml(String(props.due))}</span>` : ""}
337
+ ${props.time ? `<span class="intent-task-time">${escapeHtml(String(props.time))}</span>` : ""}
338
+ </span>
339
+ </div>`;
340
+ }
341
+ case "ask":
342
+ return `<div class="intent-ask"><span class="intent-ask-label">Query</span><div class="intent-ask-content">${content}</div></div>`;
343
+ case "quote": {
344
+ const attribution = props.by
345
+ ? `<cite class="intent-quote-cite">— ${escapeHtml(String(props.by))}</cite>`
346
+ : "";
347
+ return `<blockquote class="intent-quote"><p>${content}</p>${attribution}</blockquote>`;
348
+ }
349
+ case "cite": {
350
+ const citeAuthor = props.author
351
+ ? `<span class="it-cite-author">${escapeHtml(String(props.author))}</span>`
352
+ : "";
353
+ const citeDate = props.date
354
+ ? `<span class="it-cite-date">${escapeHtml(String(props.date))}</span>`
355
+ : "";
356
+ const citeUrl = props.url
357
+ ? ` href="${escapeHtml(sanitizeUrl(String(props.url)))}"`
358
+ : "";
359
+ const citeTitle = citeUrl
360
+ ? `<a class="it-cite-title"${citeUrl} target="_blank" rel="noopener noreferrer">${content}</a>`
361
+ : `<span class="it-cite-title">${content}</span>`;
362
+ return `<div class="it-cite">${citeTitle}${citeAuthor ? ` — ${citeAuthor}` : ""}${citeDate ? `, ${citeDate}` : ""}</div>`;
363
+ }
364
+ case "image":
365
+ const imgSrc = escapeHtml(sanitizeUrl(String(props.src ?? props.at ?? "")) ||
366
+ String(props.src ?? props.at ?? content));
367
+ const imgAlt = content;
368
+ return `<figure class="intent-image">
369
+ <img class="intent-image-img" src="${imgSrc}" alt="${imgAlt}" />
370
+ ${props.caption ? `<figcaption class="intent-image-caption">${escapeHtml(String(props.caption))}</figcaption>` : ""}
371
+ </figure>`;
372
+ case "link":
373
+ const href = escapeHtml(sanitizeUrl(String(props.to || content)));
374
+ const titleAttr = props.title
375
+ ? `title="${escapeHtml(String(props.title))}"`
376
+ : "";
377
+ return `<p class="intent-link"><a href="${href}" ${titleAttr}>${content}</a></p>`;
378
+ case "ref": {
379
+ const refFile = props.file ? String(props.file) : "";
380
+ const refUrl = props.url ? String(props.url) : "";
381
+ const refRel = props.rel ? escapeHtml(String(props.rel)) : "";
382
+ const refHref = refFile
383
+ ? escapeHtml(sanitizeUrl(refFile))
384
+ : refUrl
385
+ ? escapeHtml(sanitizeUrl(refUrl))
386
+ : "";
387
+ const refName = escapeHtml(block.content || refFile || refUrl);
388
+ const relBadge = refRel
389
+ ? `<span class="it-ref-rel">${refRel}</span>`
390
+ : "";
391
+ const linkEl = refHref
392
+ ? `<a href="${refHref}" class="it-ref-link">${refName}</a>`
393
+ : `<span class="it-ref-name">${refName}</span>`;
394
+ return `<div class="it-ref-card">
395
+ <span class="it-ref-icon">📎</span>
396
+ ${linkEl}
397
+ ${relBadge}
398
+ </div>`;
399
+ }
400
+ case "embed": {
401
+ const embedType = props.type || "iframe";
402
+ const src = String(props.src || "");
403
+ const embedContent = String(props.content || "");
404
+ switch (embedType) {
405
+ case "iframe":
406
+ return `<div class="intent-embed"><iframe src="${escapeHtml(sanitizeUrl(src))}" frameborder="0" loading="lazy" style="width:100%;min-height:400px;border-radius:8px;"></iframe></div>`;
407
+ case "mermaid":
408
+ return `<div class="intent-embed mermaid">${embedContent}</div>`;
409
+ case "svg":
410
+ return `<div class="intent-embed svg">${embedContent}</div>`;
411
+ case "video":
412
+ return `<div class="intent-embed video"><video src="${escapeHtml(sanitizeUrl(src))}" controls style="max-width:100%;border-radius:8px;"></video></div>`;
413
+ case "audio":
414
+ return `<div class="intent-embed audio"><audio src="${escapeHtml(sanitizeUrl(src))}" controls style="width:100%;"></audio></div>`;
415
+ default:
416
+ return `<div class="intent-embed unknown">${escapeHtml(embedContent || src)}</div>`;
417
+ }
418
+ }
419
+ case "code":
420
+ return `<pre class="intent-code"><code>${escapeHtml(block.content)}</code></pre>`;
421
+ case "table": {
422
+ const headers = block.table?.headers;
423
+ const rows = block.table?.rows || [];
424
+ const thead = headers
425
+ ? `<thead><tr>${headers
426
+ .map((h) => `<th class="intent-table-th">${escapeHtml(h)}</th>`)
427
+ .join("")}</tr></thead>`
428
+ : "";
429
+ const tbody = `<tbody>${rows
430
+ .map((row) => `<tr class="intent-row">${row
431
+ .map((c) => `<td class="intent-table-td">${escapeHtml(c)}</td>`)
432
+ .join("")}</tr>`)
433
+ .join("")}</tbody>`;
434
+ return `<table class="intent-table">${thead}${tbody}</table>`;
435
+ }
436
+ case "list-item": {
437
+ const listItemProps = block.properties || {};
438
+ const listItemMeta = [
439
+ listItemProps.owner &&
440
+ `<span class="intent-task-owner">${escapeHtml(String(listItemProps.owner))}</span>`,
441
+ listItemProps.due &&
442
+ `<span class="intent-task-due">${escapeHtml(String(listItemProps.due))}</span>`,
443
+ ]
444
+ .filter(Boolean)
445
+ .join(" ");
446
+ return `<li class="intent-list-item">${content}${listItemMeta ? ` <span class="intent-task-meta">${listItemMeta}</span>` : ""}</li>`;
447
+ }
448
+ case "step-item":
449
+ return `<li class="intent-step-item">${content}</li>`;
450
+ case "step": {
451
+ const statusVal = String(props.status || "pending");
452
+ const statusClass = `intent-status-${statusVal}`;
453
+ const toolBadge = props.tool
454
+ ? `<span class="intent-badge intent-badge-tool">${escapeHtml(String(props.tool))}</span>`
455
+ : "";
456
+ const statusBadge = `<span class="intent-badge ${statusClass}">${escapeHtml(statusVal)}</span>`;
457
+ const dependsArrow = props.depends
458
+ ? `<span class="intent-step-depends">⤷ ${escapeHtml(String(props.depends))}</span>`
459
+ : "";
460
+ const stepId = props.id
461
+ ? `<span class="intent-step-id">${escapeHtml(String(props.id))}</span>`
462
+ : "";
463
+ return `<div class="intent-step">
464
+ <span class="intent-step-icon">▶</span>
465
+ <span class="intent-step-content">${content}</span>
466
+ <span class="intent-step-meta">${stepId}${toolBadge}${statusBadge}${dependsArrow}</span>
467
+ </div>`;
468
+ }
469
+ case "decision": {
470
+ const ifExpr = props.if ? escapeHtml(String(props.if)) : "";
471
+ const thenTarget = props.then ? escapeHtml(String(props.then)) : "";
472
+ const elseTarget = props.else ? escapeHtml(String(props.else)) : "";
473
+ return `<div class="intent-decision">
474
+ <div class="intent-decision-diamond"></div>
475
+ <div class="intent-decision-body">
476
+ <div class="intent-decision-label">${content}</div>
477
+ ${ifExpr ? `<div class="intent-decision-condition"><strong>if</strong> ${ifExpr}</div>` : ""}
478
+ <div class="intent-decision-branches">
479
+ ${thenTarget ? `<span class="intent-decision-then">✓ then → ${thenTarget}</span>` : ""}
480
+ ${elseTarget ? `<span class="intent-decision-else">✗ else → ${elseTarget}</span>` : ""}
481
+ </div>
482
+ </div>
483
+ </div>`;
484
+ }
485
+ case "trigger": {
486
+ const eventBadge = props.event
487
+ ? `<span class="intent-badge intent-badge-event">${escapeHtml(String(props.event))}</span>`
488
+ : "";
489
+ return `<div class="intent-trigger">
490
+ <span class="intent-trigger-icon">⚡</span>
491
+ <span class="intent-trigger-content">${content}</span>
492
+ ${eventBadge}
493
+ </div>`;
494
+ }
495
+ case "loop": {
496
+ const overVal = props.over ? escapeHtml(String(props.over)) : "";
497
+ const doVal = props.do ? escapeHtml(String(props.do)) : "";
498
+ return `<div class="intent-loop">
499
+ <span class="intent-loop-icon">🔁</span>
500
+ <span class="intent-loop-content">${content}</span>
501
+ ${overVal ? `<span class="intent-loop-over">over: ${overVal}</span>` : ""}
502
+ ${doVal ? `<span class="intent-loop-do">do: ${doVal}</span>` : ""}
503
+ </div>`;
504
+ }
505
+ case "checkpoint":
506
+ return `<div class="intent-checkpoint">
507
+ <span class="intent-checkpoint-flag">🚩</span>
508
+ <span class="intent-checkpoint-label">${content}</span>
509
+ <hr class="intent-checkpoint-line" />
510
+ </div>`;
511
+ case "audit":
512
+ return `<div class="intent-audit"><span class="intent-audit-prefix">audit:</span> ${content}${props.by ? ` | by: ${escapeHtml(String(props.by))}` : ""}${props.at ? ` | at: ${escapeHtml(String(props.at))}` : ""}</div>`;
513
+ case "error": {
514
+ const fallbackInfo = props.fallback
515
+ ? `<span class="intent-error-fallback">fallback → ${escapeHtml(String(props.fallback))}</span>`
516
+ : "";
517
+ const notifyInfo = props.notify
518
+ ? `<span class="intent-error-notify">notify: ${escapeHtml(String(props.notify))}</span>`
519
+ : "";
520
+ return `<div class="intent-callout intent-error-block"><span class="intent-callout-label">Error</span><div class="intent-callout-content">${content} ${fallbackInfo} ${notifyInfo}</div></div>`;
521
+ }
522
+ case "context": {
523
+ const ctxEntries = Object.entries(props);
524
+ if (ctxEntries.length === 0) {
525
+ return `<div class="intent-context"><code>${content}</code></div>`;
526
+ }
527
+ const ctxRows = ctxEntries
528
+ .map(([k, v]) => `<tr><td class="intent-context-key">${escapeHtml(k)}</td><td class="intent-context-val">${escapeHtml(String(v))}</td></tr>`)
529
+ .join("");
530
+ return `<table class="intent-context-table"><tbody>${ctxRows}</tbody></table>`;
531
+ }
532
+ case "progress": {
533
+ let value = 0;
534
+ let total = 100;
535
+ const progressMatch = block.content.match(/(\d+)\s*\/\s*(\d+)/);
536
+ if (progressMatch) {
537
+ value = parseInt(progressMatch[1], 10);
538
+ total = parseInt(progressMatch[2], 10);
539
+ }
540
+ if (props.value)
541
+ value = Number(props.value);
542
+ if (props.total)
543
+ total = Number(props.total);
544
+ const pct = total > 0 ? Math.round((value / total) * 100) : 0;
545
+ return `<div class="intent-progress">
546
+ <span class="intent-progress-label">${content}</span>
547
+ <div class="intent-progress-bar"><div class="intent-progress-fill" style="width:${pct}%"></div></div>
548
+ <span class="intent-progress-pct">${pct}%</span>
549
+ </div>`;
550
+ }
551
+ case "import": {
552
+ const asAlias = props.as ? ` as ${escapeHtml(String(props.as))}` : "";
553
+ return `<div class="intent-file-ref intent-import"><span class="intent-file-ref-icon">📥</span> import: ${content}${asAlias}</div>`;
554
+ }
555
+ case "export": {
556
+ const fmt = props.format ? ` (${escapeHtml(String(props.format))})` : "";
557
+ return `<div class="intent-file-ref intent-export"><span class="intent-file-ref-icon">📤</span> export: ${content}${fmt}</div>`;
558
+ }
559
+ case "signal": {
560
+ const phase = props.phase
561
+ ? `<span class="intent-signal-phase">${escapeHtml(String(props.phase))}</span>`
562
+ : "";
563
+ const levelBadge = props.level
564
+ ? `<span class="intent-badge intent-badge-level">${escapeHtml(String(props.level))}</span>`
565
+ : "";
566
+ return `<div class="intent-signal-block">
567
+ <span class="intent-signal-icon">📡</span>
568
+ <span class="intent-signal-content">${content}</span>
569
+ <span class="intent-signal-meta">${phase}${levelBadge}</span>
570
+ </div>`;
571
+ }
572
+ case "result": {
573
+ const statusVal = String(props.status || "success");
574
+ const statusClass = `intent-result-${statusVal}`;
575
+ const codeBadge = props.code
576
+ ? `<span class="intent-badge intent-badge-code">${escapeHtml(String(props.code))}</span>`
577
+ : "";
578
+ const dataInfo = props.data
579
+ ? `<div class="intent-result-data"><code>${escapeHtml(String(props.data))}</code></div>`
580
+ : "";
581
+ return `<div class="intent-result ${statusClass}">
582
+ <span class="intent-result-icon">✅</span>
583
+ <span class="intent-result-content">${content}</span>
584
+ ${codeBadge}
585
+ ${dataInfo}
586
+ </div>`;
587
+ }
588
+ case "handoff": {
589
+ const fromAgent = props.from
590
+ ? `<span class="intent-handoff-agent intent-handoff-from">${escapeHtml(String(props.from))}</span>`
591
+ : "";
592
+ const toAgent = props.to
593
+ ? `<span class="intent-handoff-agent intent-handoff-to">${escapeHtml(String(props.to))}</span>`
594
+ : "";
595
+ const arrow = fromAgent || toAgent
596
+ ? `<span class="intent-handoff-arrow">${fromAgent} → ${toAgent}</span>`
597
+ : "";
598
+ return `<div class="intent-handoff">
599
+ <span class="intent-handoff-icon">🤝</span>
600
+ <span class="intent-handoff-content">${content}</span>
601
+ ${arrow}
602
+ </div>`;
603
+ }
604
+ case "wait": {
605
+ const onVal = props.on
606
+ ? `<span class="intent-badge intent-badge-event">${escapeHtml(String(props.on))}</span>`
607
+ : "";
608
+ const timeoutVal = props.timeout
609
+ ? `<span class="intent-badge intent-badge-timeout">${escapeHtml(String(props.timeout))}</span>`
610
+ : "";
611
+ const fallbackVal = props.fallback
612
+ ? `<span class="intent-wait-fallback">fallback → ${escapeHtml(String(props.fallback))}</span>`
613
+ : "";
614
+ return `<div class="intent-wait">
615
+ <span class="intent-wait-icon">⏳</span>
616
+ <span class="intent-wait-content">${content}</span>
617
+ <span class="intent-wait-meta">${onVal}${timeoutVal}${fallbackVal}</span>
618
+ </div>`;
619
+ }
620
+ case "parallel": {
621
+ const stepsVal = props.steps
622
+ ? String(props.steps)
623
+ .split(",")
624
+ .map((s) => s.trim())
625
+ : [];
626
+ const stepBadges = stepsVal.length > 0
627
+ ? stepsVal
628
+ .map((s) => `<span class="intent-badge intent-badge-parallel-step">${escapeHtml(s)}</span>`)
629
+ .join("")
630
+ : "";
631
+ const joinBadge = props.join
632
+ ? `<span class="intent-badge intent-badge-join">join: ${escapeHtml(String(props.join))}</span>`
633
+ : "";
634
+ return `<div class="intent-parallel">
635
+ <span class="intent-parallel-icon">⏩</span>
636
+ <span class="intent-parallel-content">${content}</span>
637
+ <span class="intent-parallel-steps">${stepBadges}${joinBadge}</span>
638
+ </div>`;
639
+ }
640
+ case "retry": {
641
+ const maxVal = props.max
642
+ ? `<span class="intent-badge intent-badge-retry-max">max: ${escapeHtml(String(props.max))}</span>`
643
+ : "";
644
+ const delayVal = props.delay
645
+ ? `<span class="intent-badge intent-badge-retry-delay">delay: ${escapeHtml(String(props.delay))}ms</span>`
646
+ : "";
647
+ const backoffVal = props.backoff
648
+ ? `<span class="intent-badge intent-badge-retry-backoff">${escapeHtml(String(props.backoff))}</span>`
649
+ : "";
650
+ return `<div class="intent-retry">
651
+ <span class="intent-retry-icon">🔄</span>
652
+ <span class="intent-retry-content">${content}</span>
653
+ <span class="intent-retry-meta">${maxVal}${delayVal}${backoffVal}</span>
654
+ </div>`;
655
+ }
656
+ case "gate": {
657
+ const approverBadge = props.approver
658
+ ? `<span class="intent-badge intent-badge-approver">${escapeHtml(String(props.approver))}</span>`
659
+ : "";
660
+ const timeoutBadge = props.timeout
661
+ ? `<span class="intent-badge intent-badge-timeout">${escapeHtml(String(props.timeout))}</span>`
662
+ : "";
663
+ const fallbackInfo = props.fallback
664
+ ? `<span class="intent-gate-fallback">fallback → ${escapeHtml(String(props.fallback))}</span>`
665
+ : "";
666
+ return `<div class="intent-gate">
667
+ <div class="intent-gate-icon">🛑</div>
668
+ <div class="intent-gate-body">
669
+ <div class="intent-gate-label">${content}</div>
670
+ <div class="intent-gate-meta">${approverBadge}${timeoutBadge}${fallbackInfo}</div>
671
+ </div>
672
+ </div>`;
673
+ }
674
+ case "call": {
675
+ const inputVal = props.input
676
+ ? `<span class="intent-call-input">input: ${escapeHtml(String(props.input))}</span>`
677
+ : "";
678
+ const outputVal = props.output
679
+ ? `<span class="intent-call-output">output: ${escapeHtml(String(props.output))}</span>`
680
+ : "";
681
+ return `<div class="intent-call">
682
+ <span class="intent-call-icon">📞</span>
683
+ <span class="intent-call-content">${content}</span>
684
+ <span class="intent-call-meta">${inputVal}${outputVal}</span>
685
+ </div>`;
686
+ }
687
+ case "policy": {
688
+ const conditions = [];
689
+ if (props.if)
690
+ conditions.push(`<span class="it-policy-condition">if ${escapeHtml(String(props.if))}</span>`);
691
+ if (props.always)
692
+ conditions.push(`<span class="it-policy-always">always: ${escapeHtml(String(props.always))}</span>`);
693
+ if (props.never)
694
+ conditions.push(`<span class="it-policy-never">never: ${escapeHtml(String(props.never))}</span>`);
695
+ if (props.action)
696
+ conditions.push(`<span class="it-policy-action">→ ${escapeHtml(String(props.action))}</span>`);
697
+ if (props.requires)
698
+ conditions.push(`<span class="it-policy-requires">requires: ${escapeHtml(String(props.requires))}</span>`);
699
+ if (props.notify)
700
+ conditions.push(`<span class="it-policy-notify">notify: ${escapeHtml(String(props.notify))}</span>`);
701
+ return `<div class="it-block it-policy">
702
+ <div class="it-policy-name">${content}</div>
703
+ <div class="it-policy-rules">${conditions.join(" ")}</div>
704
+ </div>`;
705
+ }
706
+ case "input": {
707
+ const inputType = props.type ? escapeHtml(String(props.type)) : "string";
708
+ const inputRequired = props.required === "true" || String(props.required) === "true";
709
+ const inputDef = props.default != null
710
+ ? `<span class="it-input-default">= ${escapeHtml(String(props.default))}</span>`
711
+ : "";
712
+ return `<div class="it-input"><span class="it-input-name">${content}</span><span class="it-input-type">${inputType}</span>${inputRequired ? '<span class="it-input-required">required</span>' : ""}${inputDef}</div>`;
713
+ }
714
+ case "output": {
715
+ const outputType = props.type ? escapeHtml(String(props.type)) : "any";
716
+ const outputFormat = props.format
717
+ ? `<span class="it-output-format">${escapeHtml(String(props.format))}</span>`
718
+ : "";
719
+ return `<div class="it-output"><span class="it-output-name">${content}</span><span class="it-output-type">${outputType}</span>${outputFormat}</div>`;
720
+ }
721
+ case "tool": {
722
+ const toolApi = props.api
723
+ ? `<code class="it-tool-api">${escapeHtml(String(props.api))}</code>`
724
+ : "";
725
+ const toolMethod = props.method
726
+ ? `<span class="it-tool-method">${escapeHtml(String(props.method))}</span>`
727
+ : "";
728
+ return `<div class="it-tool"><span class="it-tool-name">${content}</span>${toolApi}${toolMethod}</div>`;
729
+ }
730
+ case "prompt": {
731
+ const promptModel = props.model
732
+ ? `<span class="it-prompt-model">${escapeHtml(String(props.model))}</span>`
733
+ : "";
734
+ return `<div class="it-prompt">${promptModel}<div class="it-prompt-content">${content}</div></div>`;
735
+ }
736
+ case "memory": {
737
+ const memoryScope = props.scope
738
+ ? escapeHtml(String(props.scope))
739
+ : "session";
740
+ return `<div class="it-memory"><span class="it-memory-scope">${memoryScope}</span><span class="it-memory-content">${content}</span></div>`;
741
+ }
742
+ case "assert": {
743
+ const expectExpr = props.expect
744
+ ? `<code class="it-assert-expect">${escapeHtml(String(props.expect))}</code>`
745
+ : "";
746
+ const severity = props.severity
747
+ ? escapeHtml(String(props.severity))
748
+ : "error";
749
+ return `<div class="it-assert it-assert-${severity}"><span class="it-assert-label">ASSERT</span><span class="it-assert-content">${content}</span>${expectExpr}</div>`;
750
+ }
751
+ case "secret":
752
+ return `<div class="it-secret"><span class="it-secret-label">SECRET</span><span class="it-secret-value">${"\u2022".repeat(8)}</span></div>`;
753
+ case "font":
754
+ return "";
755
+ case "page":
756
+ return "";
757
+ case "break":
758
+ return `<div class="it-page-break" aria-hidden="true" style="display:none"></div>`;
759
+ case "history":
760
+ return "";
761
+ case "byline": {
762
+ const author = content;
763
+ const date = props.date ? escapeHtml(String(props.date)) : "";
764
+ const publication = props.publication
765
+ ? escapeHtml(String(props.publication))
766
+ : "";
767
+ const role = props.role ? escapeHtml(String(props.role)) : "";
768
+ const metaParts = [role, date, publication].filter(Boolean);
769
+ return `<div class="it-byline"><span class="it-byline-author">${author}</span>${metaParts.length > 0 ? `<span class="it-byline-meta">${metaParts.join(" · ")}</span>` : ""}</div>`;
770
+ }
771
+ case "epigraph": {
772
+ const by = props.by
773
+ ? `<span class="it-epigraph-by">— ${escapeHtml(String(props.by))}</span>`
774
+ : "";
775
+ return `<blockquote class="it-epigraph"><p>${content}</p>${by}</blockquote>`;
776
+ }
777
+ case "caption":
778
+ return `<figcaption class="it-caption">${content}</figcaption>`;
779
+ case "footnote":
780
+ return "";
781
+ case "toc":
782
+ return "";
783
+ case "dedication":
784
+ return `<div class="it-dedication">${content}</div>`;
785
+ case "track":
786
+ return "";
787
+ case "meta":
788
+ return "";
789
+ case "header":
790
+ return "";
791
+ case "footer":
792
+ return "";
793
+ case "watermark":
794
+ return "";
795
+ case "approve": {
796
+ const approveBy = props.by ? escapeHtml(String(props.by)) : "Unknown";
797
+ const approveRole = props.role ? escapeHtml(String(props.role)) : "";
798
+ const approveAt = props.at ? formatTrustDate(String(props.at)) : "";
799
+ return `<div class="it-approval">
800
+ <span class="it-approval__icon">✓</span>
801
+ <div class="it-approval__body">
802
+ <span class="it-approval__label">APPROVED</span>
803
+ <span class="it-approval__who">${approveBy}${approveRole ? ` — ${approveRole}` : ""}</span>
804
+ ${approveAt ? `<span class="it-approval__date">${approveAt}</span>` : ""}
805
+ </div>
806
+ </div>`;
807
+ }
808
+ case "sign": {
809
+ const signerName = escapeHtml(block.content);
810
+ const signRole = props.role ? escapeHtml(String(props.role)) : "";
811
+ const signAt = props.at ? formatTrustDate(String(props.at)) : "";
812
+ const signValid = props.hash ? true : false;
813
+ return `<div class="it-signature${signValid ? " it-signature--valid" : " it-signature--invalid"}">
814
+ <span class="it-signature__name">${signerName}</span>
815
+ ${signRole ? `<span class="it-signature__role">${signRole}</span>` : ""}
816
+ ${signAt ? `<span class="it-signature__date">${signAt}</span>` : ""}
817
+ <span class="it-signature__status">${signValid ? "✅ Verified" : "❌ Invalid"}</span>
818
+ </div>`;
819
+ }
820
+ case "freeze": {
821
+ const freezeAt = props.at ? formatTrustDate(String(props.at)) : "";
822
+ const freezeHash = props.hash
823
+ ? escapeHtml(String(props.hash)).slice(0, 20) + "..."
824
+ : "";
825
+ return `<div class="it-sealed-banner">
826
+ <span class="it-sealed-banner__icon">🔒</span>
827
+ <span class="it-sealed-banner__text">Sealed Document</span>
828
+ ${freezeAt ? `<span class="it-sealed-banner__date">${freezeAt}</span>` : ""}
829
+ ${freezeHash ? `<span class="it-sealed-banner__hash">${freezeHash}</span>` : ""}
830
+ </div>`;
831
+ }
832
+ case "revision":
833
+ return "";
834
+ case "def": {
835
+ const meaning = props.meaning ? escapeHtml(String(props.meaning)) : "";
836
+ const abbr = props.abbr
837
+ ? ` <span class="it-def-abbr">(${escapeHtml(String(props.abbr))})</span>`
838
+ : "";
839
+ return `<div class="it-def">
840
+ <dt class="it-def-term">${content}${abbr}</dt>
841
+ <dd class="it-def-meaning">${meaning}</dd>
842
+ </div>`;
843
+ }
844
+ case "metric": {
845
+ const val = props.value != null ? escapeHtml(String(props.value)) : "";
846
+ const unit = props.unit ? escapeHtml(String(props.unit)) : "";
847
+ const target = props.target != null ? String(props.target) : "";
848
+ const trend = props.trend ? String(props.trend) : "";
849
+ const period = props.period ? escapeHtml(String(props.period)) : "";
850
+ if (!target && !trend && !period) {
851
+ const isTotal = /\b(total|balance due|amount due|grand)\b/i.test(String(block.content || ""));
852
+ const valueText = [val, unit].filter(Boolean).join(" ");
853
+ return `<div class="it-metric-row${isTotal ? " it-metric-row--total" : ""}">
854
+ <span class="it-metric-row__label">${content}</span>
855
+ <span class="it-metric-row__value">${valueText}</span>
856
+ </div>`;
857
+ }
858
+ const trendIcon = trend === "up"
859
+ ? "↑"
860
+ : trend === "down"
861
+ ? "↓"
862
+ : trend === "stable"
863
+ ? "→"
864
+ : "";
865
+ let colorClass = "it-metric-neutral";
866
+ if (target && val) {
867
+ colorClass =
868
+ Number(val) >= Number(target) ? "it-metric-green" : "it-metric-red";
869
+ }
870
+ return `<div class="it-metric ${colorClass}">
871
+ <div class="it-metric-name">${content}</div>
872
+ <div class="it-metric-value">${val}<span class="it-metric-unit">${unit}</span></div>
873
+ ${target ? `<div class="it-metric-target">Target: ${escapeHtml(target)}</div>` : ""}
874
+ ${trendIcon ? `<div class="it-metric-trend">${trendIcon}</div>` : ""}
875
+ ${period ? `<div class="it-metric-period">${period}</div>` : ""}
876
+ </div>`;
877
+ }
878
+ case "amendment": {
879
+ const amendRef = props.ref ? escapeHtml(String(props.ref)) : "";
880
+ const amendSection = props.section
881
+ ? escapeHtml(String(props.section))
882
+ : "";
883
+ const amendWas = props.was ? escapeHtml(String(props.was)) : "";
884
+ const amendNow = props.now ? escapeHtml(String(props.now)) : "";
885
+ const amendBy = props.by ? escapeHtml(String(props.by)) : "";
886
+ const amendAt = props.at ? formatTrustDate(String(props.at)) : "";
887
+ return `<div class="it-amendment">
888
+ <div class="it-amendment-header">
889
+ <span class="it-amendment-icon">✏️</span>
890
+ <span class="it-amendment-ref">${amendRef}</span>
891
+ <span class="it-amendment-title">${content}</span>
892
+ </div>
893
+ ${amendSection ? `<div class="it-amendment-section">Section: ${amendSection}</div>` : ""}
894
+ ${amendWas ? `<div class="it-amendment-was">Was: ${amendWas}</div>` : ""}
895
+ ${amendNow ? `<div class="it-amendment-now">Now: ${amendNow}</div>` : ""}
896
+ <div class="it-amendment-meta">
897
+ ${amendBy ? `<span class="it-amendment-by">${amendBy}</span>` : ""}
898
+ ${amendAt ? `<span class="it-amendment-at">${amendAt}</span>` : ""}
899
+ </div>
900
+ </div>`;
901
+ }
902
+ case "figure": {
903
+ const figSrc = props.src
904
+ ? escapeHtml(sanitizeUrl(String(props.src)))
905
+ : "";
906
+ const figCaption = props.caption ? escapeHtml(String(props.caption)) : "";
907
+ const figNum = props.num ? escapeHtml(String(props.num)) : "";
908
+ const figWidth = props.width
909
+ ? `width:${escapeHtml(String(props.width))};`
910
+ : "";
911
+ const figAlign = props.align ? String(props.align) : "center";
912
+ const figAlt = props.alt
913
+ ? escapeHtml(String(props.alt))
914
+ : escapeHtml(block.content);
915
+ const numPrefix = figNum ? `Figure ${figNum}: ` : "";
916
+ return `<figure class="it-figure" style="text-align:${escapeHtml(figAlign)};">
917
+ ${figSrc ? `<img src="${figSrc}" alt="${figAlt}" style="${figWidth}max-width:100%;" />` : ""}
918
+ <figcaption class="it-figure-caption">${numPrefix}${figCaption}</figcaption>
919
+ </figure>`;
920
+ }
921
+ case "signline": {
922
+ const sigLabel = props.label
923
+ ? escapeHtml(String(props.label))
924
+ : "Signature";
925
+ const sigRole = props.role ? escapeHtml(String(props.role)) : "";
926
+ const sigDateLine = String(props["date-line"]) === "true";
927
+ const sigWidth = props.width ? escapeHtml(String(props.width)) : "60%";
928
+ return `<div class="it-signline" style="width:${sigWidth};">
929
+ <div class="it-signline-label">${sigLabel}</div>
930
+ <div class="it-signline-rule"></div>
931
+ <div class="it-signline-name">${content}</div>
932
+ ${sigRole ? `<div class="it-signline-role">${sigRole}</div>` : ""}
933
+ ${sigDateLine ? `<div class="it-signline-date">Date: _______________</div>` : ""}
934
+ </div>`;
935
+ }
936
+ case "contact": {
937
+ const cRole = props.role ? escapeHtml(String(props.role)) : "";
938
+ const cEmail = props.email ? String(props.email) : "";
939
+ const cPhone = props.phone ? String(props.phone) : "";
940
+ const cOrg = props.org ? escapeHtml(String(props.org)) : "";
941
+ const cUrl2 = props.url ? String(props.url) : "";
942
+ return `<div class="it-contact">
943
+ <div class="it-contact-name">${content}</div>
944
+ ${cRole ? `<div class="it-contact-role">${cRole}</div>` : ""}
945
+ ${cOrg ? `<div class="it-contact-org">${cOrg}</div>` : ""}
946
+ ${cEmail ? `<div class="it-contact-email"><a href="mailto:${escapeHtml(cEmail)}">${escapeHtml(cEmail)}</a></div>` : ""}
947
+ ${cPhone ? `<div class="it-contact-phone"><a href="tel:${escapeHtml(cPhone)}">${escapeHtml(cPhone)}</a></div>` : ""}
948
+ ${cUrl2 ? `<div class="it-contact-url"><a href="${escapeHtml(sanitizeUrl(cUrl2))}">${escapeHtml(cUrl2)}</a></div>` : ""}
949
+ </div>`;
950
+ }
951
+ case "deadline": {
952
+ const dlDate = props.date ? String(props.date) : "";
953
+ const dlConsequence = props.consequence
954
+ ? escapeHtml(String(props.consequence))
955
+ : "";
956
+ const dlAuthority = props.authority
957
+ ? escapeHtml(String(props.authority))
958
+ : "";
959
+ const dlOwner = props.owner ? escapeHtml(String(props.owner)) : "";
960
+ let dlColorClass = "it-deadline-green";
961
+ if (dlDate) {
962
+ const dlDateObj = new Date(dlDate);
963
+ if (!isNaN(dlDateObj.getTime())) {
964
+ const daysUntil = (dlDateObj.getTime() - Date.now()) / (1000 * 60 * 60 * 24);
965
+ if (daysUntil < 7)
966
+ dlColorClass = "it-deadline-red";
967
+ else if (daysUntil < 30)
968
+ dlColorClass = "it-deadline-amber";
969
+ }
970
+ }
971
+ return `<div class="it-deadline ${dlColorClass}">
972
+ <div class="it-deadline-name">${content}</div>
973
+ ${dlDate ? `<div class="it-deadline-date">${escapeHtml(dlDate)}</div>` : ""}
974
+ ${dlConsequence ? `<div class="it-deadline-consequence">${dlConsequence}</div>` : ""}
975
+ ${dlOwner ? `<div class="it-deadline-owner">${dlOwner}</div>` : ""}
976
+ ${dlAuthority ? `<div class="it-deadline-authority">${dlAuthority}</div>` : ""}
977
+ </div>`;
978
+ }
979
+ case "extension": {
980
+ const xType = props["x-type"]
981
+ ? escapeHtml(String(props["x-type"]))
982
+ : "unknown";
983
+ const xNs = props["x-ns"] ? escapeHtml(String(props["x-ns"])) : "ext";
984
+ return `<div class="it-extension it-ext-${xNs} it-ext-${xType}" data-x-type="${xType}" data-x-ns="${xNs}"${styleAttr}>${content}</div>`;
985
+ }
986
+ default:
987
+ return `<div class="intent-unknown">
988
+ <small class="intent-unknown-type">[${block.type}]</small> ${content}
989
+ </div>`;
990
+ }
991
+ }
992
+ function slugify(text) {
993
+ return text
994
+ .toLowerCase()
995
+ .replace(/[^a-z0-9]+/g, "-")
996
+ .replace(/^-|-$/g, "");
997
+ }
998
+ function collectSections(blocks, depth) {
999
+ const entries = [];
1000
+ for (const block of blocks) {
1001
+ if (block.type === "section") {
1002
+ entries.push({
1003
+ level: 1,
1004
+ content: block.content,
1005
+ slug: slugify(block.content),
1006
+ });
1007
+ if (depth >= 2 && block.children) {
1008
+ for (const child of block.children) {
1009
+ if (child.type === "sub") {
1010
+ entries.push({
1011
+ level: 2,
1012
+ content: child.content,
1013
+ slug: slugify(child.content),
1014
+ });
1015
+ }
1016
+ }
1017
+ }
1018
+ }
1019
+ }
1020
+ return entries;
1021
+ }
1022
+ function collectFootnotes(blocks) {
1023
+ const footnotes = [];
1024
+ for (const block of blocks) {
1025
+ if (block.type === "footnote")
1026
+ footnotes.push(block);
1027
+ if (block.children)
1028
+ footnotes.push(...collectFootnotes(block.children));
1029
+ }
1030
+ return footnotes;
1031
+ }
1032
+ function renderBlocks(blocks, allBlocks) {
1033
+ const topBlocks = allBlocks || blocks;
1034
+ let html = "";
1035
+ let i = 0;
1036
+ while (i < blocks.length) {
1037
+ const block = blocks[i];
1038
+ if (block.type === "toc") {
1039
+ const depth = Number(block.properties?.depth || 2);
1040
+ const title = String(block.properties?.title || "Contents");
1041
+ const entries = collectSections(topBlocks, depth);
1042
+ let tocHtml = `<nav class="it-toc"><h2 class="it-toc-title">${escapeHtml(title)}</h2><ol>`;
1043
+ for (const entry of entries) {
1044
+ const indent = entry.level === 2 ? ' class="it-toc-sub"' : "";
1045
+ tocHtml += `<li${indent}><a href="#${entry.slug}">${escapeHtml(entry.content)}</a></li>`;
1046
+ }
1047
+ tocHtml += `</ol></nav>`;
1048
+ html += tocHtml;
1049
+ i++;
1050
+ continue;
1051
+ }
1052
+ if (block.type === "list-item") {
1053
+ html += '<ul class="intent-list">';
1054
+ while (i < blocks.length && blocks[i].type === "list-item") {
1055
+ html += renderBlock(blocks[i]);
1056
+ i++;
1057
+ }
1058
+ html += "</ul>";
1059
+ continue;
1060
+ }
1061
+ if (block.type === "step-item") {
1062
+ html += '<ol class="intent-list">';
1063
+ while (i < blocks.length && blocks[i].type === "step-item") {
1064
+ html += renderBlock(blocks[i]);
1065
+ i++;
1066
+ }
1067
+ html += "</ol>";
1068
+ continue;
1069
+ }
1070
+ html += renderBlock(block);
1071
+ if ((block.type === "section" || block.type === "sub") &&
1072
+ block.children &&
1073
+ block.children.length > 0) {
1074
+ html += renderBlocks(block.children, topBlocks);
1075
+ }
1076
+ i++;
1077
+ }
1078
+ return html;
1079
+ }
1080
+ function renderHTML(document, options) {
1081
+ if (!document || !document.blocks)
1082
+ return "";
1083
+ const bodyHtml = renderBlocks(document.blocks);
1084
+ const footnotes = collectFootnotes(document.blocks);
1085
+ let footnotesHtml = "";
1086
+ if (footnotes.length > 0) {
1087
+ const items = footnotes
1088
+ .map((fn) => {
1089
+ const num = escapeHtml(fn.content);
1090
+ const text = fn.properties?.text
1091
+ ? escapeHtml(String(fn.properties.text))
1092
+ : escapeHtml(fn.content);
1093
+ return `<li id="fn-${num}" value="${num}">${text}</li>`;
1094
+ })
1095
+ .join("");
1096
+ footnotesHtml = `<div class="it-footnotes"><ol>${items}</ol></div>`;
1097
+ }
1098
+ const html = bodyHtml + footnotesHtml;
1099
+ const themeRef = options?.theme ?? document.metadata?.meta?.theme ?? undefined;
1100
+ const theme = resolveThemeSync(themeRef);
1101
+ const themeCSS = (0, theme_1.generateThemeCSS)(theme, "web");
1102
+ const docStyleCSS = documentStyleCSS(document);
1103
+ const direction = document.metadata?.language === "rtl" ? 'dir="rtl"' : 'dir="ltr"';
1104
+ return `<div class="intent-document" ${direction}>
1105
+ <style>
1106
+ ${document_css_1.DOCUMENT_CSS}${themeCSS}
1107
+ ${docStyleCSS}
1108
+ </style>
1109
+ ${html}
1110
+ </div>`;
1111
+ }
1112
+ function buildDynamicCSS(doc) {
1113
+ const fontBlock = doc.blocks.find((b) => b.type === "font");
1114
+ const pageBlock = doc.blocks.find((b) => b.type === "page");
1115
+ const fontFamily = String(fontBlock?.properties?.family || "Georgia, serif");
1116
+ const fontSize = String(fontBlock?.properties?.size || "12pt");
1117
+ const leading = String(fontBlock?.properties?.leading || "1.6");
1118
+ const rawSize = String(pageBlock?.properties?.size || "A4");
1119
+ let pageSize;
1120
+ let widthHint = rawSize;
1121
+ if (rawSize === "custom") {
1122
+ const w = String(pageBlock?.properties?.width || "210mm");
1123
+ const h = String(pageBlock?.properties?.height || "297mm");
1124
+ widthHint = w;
1125
+ pageSize = `${escapeHtml(w)} ${escapeHtml(h)}`;
1126
+ }
1127
+ else {
1128
+ pageSize = escapeHtml(PAPER_SIZES[rawSize] || rawSize);
1129
+ }
1130
+ const explicitMargin = pageBlock?.properties?.margin ?? pageBlock?.properties?.margins;
1131
+ const widthMatch = /(\d+(?:\.\d+)?)\s*mm/.exec(widthHint);
1132
+ const widthMm = widthMatch ? parseFloat(widthMatch[1]) : Infinity;
1133
+ const margins = String(explicitMargin ?? (widthMm <= 120 ? "4mm" : "20mm"));
1134
+ return `@page{size:${pageSize};margin:${escapeHtml(margins)};}body.it-print{font-family:${escapeHtml(fontFamily)};font-size:${escapeHtml(fontSize)};line-height:${escapeHtml(leading)};}`;
1135
+ }
1136
+ function renderPrint(doc, options) {
1137
+ if (!doc || !doc.blocks)
1138
+ return "";
1139
+ const bodyHtml = renderBlocks(doc.blocks);
1140
+ const footnotes = collectFootnotes(doc.blocks);
1141
+ let footnotesHtml = "";
1142
+ if (footnotes.length > 0) {
1143
+ const items = footnotes
1144
+ .map((fn) => {
1145
+ const num = escapeHtml(fn.content);
1146
+ const text = fn.properties?.text
1147
+ ? escapeHtml(String(fn.properties.text))
1148
+ : escapeHtml(fn.content);
1149
+ return `<li id="fn-${num}" value="${num}">${text}</li>`;
1150
+ })
1151
+ .join("");
1152
+ footnotesHtml = `<div class="it-footnotes"><ol>${items}</ol></div>`;
1153
+ }
1154
+ const html = bodyHtml + footnotesHtml;
1155
+ const dynamicCSS = buildDynamicCSS(doc);
1156
+ const direction = doc.metadata?.language === "rtl" ? 'dir="rtl"' : 'dir="ltr"';
1157
+ const themeRef = options?.theme ?? doc.metadata?.meta?.theme ?? undefined;
1158
+ const theme = resolveThemeSync(themeRef);
1159
+ const themeCSS = (0, theme_1.generateThemeCSS)(theme, "print");
1160
+ const layout = collectPrintLayout(doc);
1161
+ let headerFooterCSS = "";
1162
+ if (layout.header) {
1163
+ const hp = layout.header.properties || {};
1164
+ const left = cssContentValue(String(hp.left ?? ""));
1165
+ const center = cssContentValue(String(hp.center ?? ""));
1166
+ const right = cssContentValue(String(hp.right ?? ""));
1167
+ headerFooterCSS += `@page{@top-left{content:${left};}@top-center{content:${center};}@top-right{content:${right};}}`;
1168
+ if (String(hp["skip-first"]) === "true") {
1169
+ headerFooterCSS += `@page:first{@top-left{content:"";}@top-center{content:"";}@top-right{content:"";}}`;
1170
+ }
1171
+ }
1172
+ if (layout.footer) {
1173
+ const fp = layout.footer.properties || {};
1174
+ const left = cssContentValue(String(fp.left ?? ""));
1175
+ const center = cssContentValue(String(fp.center ?? ""));
1176
+ const right = cssContentValue(String(fp.right ?? ""));
1177
+ headerFooterCSS += `@page{@bottom-left{content:${left};}@bottom-center{content:${center};}@bottom-right{content:${right};}}`;
1178
+ if (String(fp["skip-first"]) === "true") {
1179
+ headerFooterCSS += `@page:first{@bottom-left{content:"";}@bottom-center{content:"";}@bottom-right{content:"";}}`;
1180
+ }
1181
+ }
1182
+ let breakCSS = "";
1183
+ for (const br of layout.breaks) {
1184
+ const before = br.properties?.before ? String(br.properties.before) : "";
1185
+ const keep = br.properties?.keep ? String(br.properties.keep) : "";
1186
+ if (before) {
1187
+ breakCSS += `.it-${escapeHtml(before)}{page-break-before:always;}`;
1188
+ }
1189
+ if (keep) {
1190
+ breakCSS += `.it-${escapeHtml(keep)}{break-inside:avoid;}`;
1191
+ }
1192
+ }
1193
+ let watermarkHtml = "";
1194
+ if (layout.watermark && layout.watermark.content) {
1195
+ const wp = layout.watermark.properties || {};
1196
+ const color = wp.color ? String(wp.color) : "rgba(0,0,0,0.08)";
1197
+ const angle = wp.angle ? String(wp.angle) : "-45";
1198
+ const size = wp.size ? String(wp.size) : "80pt";
1199
+ watermarkHtml = `<div class="it-watermark" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%) rotate(${escapeHtml(angle)}deg);font-size:${escapeHtml(size)};color:${escapeHtml(color)};z-index:-1;pointer-events:none;white-space:nowrap;">${escapeHtml(layout.watermark.content)}</div>`;
1200
+ }
1201
+ const pageBlock = layout.page;
1202
+ const printMode = pageBlock?.properties?.["print-mode"]
1203
+ ? String(pageBlock.properties["print-mode"])
1204
+ : "full";
1205
+ const bodyClass = printMode === "minimal-ink" ? "it-print it-print-minimal" : "it-print";
1206
+ let backwardCompatCSS = "";
1207
+ if (!layout.header && pageBlock?.properties?.header) {
1208
+ const h = cssContentValue(String(pageBlock.properties.header));
1209
+ backwardCompatCSS += `@page{@top-center{content:${h};}}`;
1210
+ }
1211
+ if (!layout.footer && pageBlock?.properties?.footer) {
1212
+ const f = cssContentValue(String(pageBlock.properties.footer));
1213
+ backwardCompatCSS += `@page{@bottom-center{content:${f};}}`;
1214
+ }
1215
+ const minimalInkCSS = printMode === "minimal-ink"
1216
+ ? `
1217
+ @media print{.it-print-minimal *{background-color:transparent !important;color:black !important;}.it-print-minimal strong,.it-print-minimal b{font-weight:bold;color:black !important;}.it-print-minimal em,.it-print-minimal i{font-style:italic;color:black !important;}.it-print-minimal .it-border{border:1px solid black !important;}}`
1218
+ : "";
1219
+ return `<!DOCTYPE html><html ${direction}><head><meta charset="utf-8"><style>
1220
+ ${dynamicCSS}
1221
+ ${document_css_1.DOCUMENT_CSS}
1222
+ ${themeCSS}
1223
+ ${documentStyleCSS(doc)}
1224
+ /* Print: the page box is handled by @page margins, so neutralise the screen
1225
+ document container's own max-width/centering/padding. */
1226
+ .intent-document{max-width:none;margin:0;padding:0;}
1227
+ ${headerFooterCSS}
1228
+ ${backwardCompatCSS}
1229
+ ${breakCSS}
1230
+ ${minimalInkCSS}
1231
+ @page{counter-increment:page;}
1232
+ @media print{body{margin:0;}.it-page-break{page-break-after:always;}.it-no-print{display:none;}a{text-decoration:none;color:inherit;}}
1233
+ body.it-print{color:#000;background:#fff;}
1234
+ body.it-print h1{font-size:1.8em;margin-bottom:0.3em;}
1235
+ body.it-print h2{font-size:1.3em;margin-top:1.5em;}
1236
+ body.it-print h3{font-size:1.1em;}
1237
+ body.it-print p{margin:0 0 0.8em 0;orphans:3;widows:3;}
1238
+ body.it-print table{width:100%;border-collapse:collapse;margin:1em 0;}
1239
+ body.it-print th{border-bottom:2px solid #000;padding:4pt 8pt;text-align:left;}
1240
+ body.it-print td{border-bottom:1px solid #ccc;padding:4pt 8pt;}
1241
+ /* Keep table rows whole across page breaks so they aren't clipped behind the
1242
+ running footer/header, and repeat the table header on every page. */
1243
+ body.it-print tr{break-inside:avoid;page-break-inside:avoid;}
1244
+ body.it-print thead{display:table-header-group;}
1245
+ body.it-print tfoot{display:table-footer-group;}
1246
+ /* Sections may legitimately span pages; only avoid splitting their headings. */
1247
+ body.it-print section{break-inside:auto;}
1248
+ body.it-print h1,body.it-print h2,body.it-print h3{break-after:avoid;page-break-after:avoid;}
1249
+ body.it-print .intent-callout{border-left:3pt solid #000;padding-left:10pt;margin:1em 0;}
1250
+ body.it-print .intent-quote{font-style:italic;margin:1em 2em;}
1251
+ body.it-print .it-byline{font-size:0.9em;color:#333;margin-bottom:1.5em;}
1252
+ body.it-print .it-byline .it-byline-author{font-weight:bold;display:block;}
1253
+ body.it-print .it-byline .it-byline-meta{font-size:0.85em;color:#666;}
1254
+ body.it-print .it-epigraph{font-style:italic;text-align:center;margin:2em 3em;border:none;padding:0;}
1255
+ body.it-print .it-epigraph .it-epigraph-by{display:block;text-align:right;font-size:0.9em;margin-top:0.5em;}
1256
+ body.it-print .it-caption{font-size:0.85em;font-style:italic;text-align:center;color:#444;margin-top:0.3em;margin-bottom:1em;}
1257
+ body.it-print .it-dedication{font-style:italic;text-align:center;margin:4em auto;page-break-after:always;}
1258
+ body.it-print .it-toc{margin:2em 0;}
1259
+ body.it-print .it-toc ol{list-style:none;padding:0;}
1260
+ body.it-print .it-toc li{margin:0.3em 0;}
1261
+ body.it-print .it-footnotes{border-top:1pt solid #ccc;margin-top:2em;padding-top:0.5em;font-size:0.85em;}
1262
+ body.it-print .it-footnotes ol{padding-left:1.5em;margin:0;}
1263
+ body.it-print .it-footnotes li{margin:0.3em 0;}
1264
+ body.it-print sup.it-fn-ref{font-size:0.7em;vertical-align:super;}
1265
+ body.it-print .it-page-break{page-break-after:always;break-after:page;height:0;}
1266
+ body.it-print .intent-task-checkbox{display:none;}
1267
+ body.it-print .intent-task::before{content:"\\2610 ";margin-right:4pt;}
1268
+ body.it-print .intent-task-done::before{content:"\\2611 ";}
1269
+ /* ── v2.11 Print ───────────────────────────────────────── */
1270
+ body.it-print .it-ref-card{border:none;padding:0;margin:0.5em 0;font-style:italic;}
1271
+ body.it-print .it-def{margin:0.3em 0;}
1272
+ body.it-print .it-def-term{font-weight:bold;}
1273
+ body.it-print .it-def-meaning{padding-left:1.5em;}
1274
+ body.it-print .it-metric{border:1pt solid #ccc;padding:6pt 10pt;display:inline-block;min-width:100pt;margin:4pt;vertical-align:top;}
1275
+ body.it-print .it-amendment{border:2pt solid #000;padding:8pt 12pt;margin:1em 0;}
1276
+ body.it-print .it-amendment-ref{border:1pt solid #000;color:#000;}
1277
+ body.it-print .it-figure{margin:1em 0;text-align:center;}
1278
+ body.it-print .it-figure img{max-width:100%;border:1pt solid #ccc;}
1279
+ body.it-print .it-figure-caption{font-size:0.85em;font-style:italic;text-align:center;margin-top:0.3em;}
1280
+ body.it-print .it-signline{display:inline-block;width:45%;margin:2em 2%;vertical-align:top;}
1281
+ body.it-print .it-signline-rule{border-bottom:1pt solid #000;margin-bottom:4pt;}
1282
+ body.it-print .it-contact{border:none;padding:0;margin:0.3em 0;}
1283
+ body.it-print .it-deadline{border-left:3pt solid #000;padding-left:8pt;margin:0.5em 0;}
1284
+ body.it-print .it-deadline-date{font-weight:bold;text-decoration:underline;}
1285
+ </style></head><body class="${bodyClass}"><div class="intent-document">${watermarkHtml}${html}</div></body></html>`;
1286
+ }