@adminforth/agent 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 (66) hide show
  1. package/.woodpecker/buildRelease.sh +13 -0
  2. package/.woodpecker/buildSlackNotify.sh +46 -0
  3. package/.woodpecker/release.yml +57 -0
  4. package/agent/middleware/apiBasedTools.ts +109 -0
  5. package/agent/middleware/sequenceDebug.ts +302 -0
  6. package/agent/simpleAgent.ts +291 -0
  7. package/agent/skills/registry.ts +135 -0
  8. package/agent/systemPrompt.ts +69 -0
  9. package/agent/toolCallEvents.ts +17 -0
  10. package/agent/tools/apiTool.ts +99 -0
  11. package/agent/tools/fetchSkill.ts +58 -0
  12. package/agent/tools/fetchToolSchema.ts +50 -0
  13. package/agent/tools/index.ts +26 -0
  14. package/apiBasedTools.ts +625 -0
  15. package/build.log +30 -0
  16. package/custom/ChatSurface.vue +184 -0
  17. package/custom/ConversationArea.vue +175 -0
  18. package/custom/Message.vue +206 -0
  19. package/custom/SessionsHistory.vue +93 -0
  20. package/custom/ToolRenderer.vue +131 -0
  21. package/custom/ToolsGroup.vue +67 -0
  22. package/custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue +301 -0
  23. package/custom/incremark_code_renderers/incremarkCodeHighlight.ts +285 -0
  24. package/custom/incremark_code_renderers/incremarkRenderer.ts +653 -0
  25. package/custom/incremark_code_renderers/renderIncremarkMarkdown.ts +118 -0
  26. package/custom/package.json +26 -0
  27. package/custom/pnpm-lock.yaml +1467 -0
  28. package/custom/skills/fetch_data/SKILL.md +15 -0
  29. package/custom/skills/mutate_data/SKILL.md +108 -0
  30. package/custom/tsconfig.json +16 -0
  31. package/custom/types.ts +34 -0
  32. package/custom/useAgentStore.ts +349 -0
  33. package/dist/agent/middleware/apiBasedTools.js +91 -0
  34. package/dist/agent/middleware/sequenceDebug.js +210 -0
  35. package/dist/agent/simpleAgent.js +173 -0
  36. package/dist/agent/skills/registry.js +108 -0
  37. package/dist/agent/systemPrompt.js +64 -0
  38. package/dist/agent/toolCallEvents.js +1 -0
  39. package/dist/agent/tools/apiTool.js +93 -0
  40. package/dist/agent/tools/fetchSkill.js +51 -0
  41. package/dist/agent/tools/fetchToolSchema.js +36 -0
  42. package/dist/agent/tools/index.js +28 -0
  43. package/dist/apiBasedTools.js +412 -0
  44. package/dist/custom/ChatSurface.vue +184 -0
  45. package/dist/custom/ConversationArea.vue +175 -0
  46. package/dist/custom/Message.vue +206 -0
  47. package/dist/custom/SessionsHistory.vue +93 -0
  48. package/dist/custom/ToolRenderer.vue +131 -0
  49. package/dist/custom/ToolsGroup.vue +67 -0
  50. package/dist/custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue +301 -0
  51. package/dist/custom/incremark_code_renderers/incremarkCodeHighlight.ts +285 -0
  52. package/dist/custom/incremark_code_renderers/incremarkRenderer.ts +653 -0
  53. package/dist/custom/incremark_code_renderers/renderIncremarkMarkdown.ts +118 -0
  54. package/dist/custom/package.json +26 -0
  55. package/dist/custom/pnpm-lock.yaml +1467 -0
  56. package/dist/custom/skills/fetch_data/SKILL.md +15 -0
  57. package/dist/custom/skills/mutate_data/SKILL.md +108 -0
  58. package/dist/custom/tsconfig.json +16 -0
  59. package/dist/custom/types.ts +34 -0
  60. package/dist/custom/useAgentStore.ts +349 -0
  61. package/dist/index.js +415 -0
  62. package/dist/types.js +1 -0
  63. package/index.ts +457 -0
  64. package/package.json +58 -0
  65. package/tsconfig.json +13 -0
  66. package/types.ts +45 -0
@@ -0,0 +1,653 @@
1
+ import type { Root } from '@incremark/core';
2
+
3
+ type KatexRenderer = typeof import('katex')['default'];
4
+
5
+ interface NodeLike {
6
+ type?: unknown;
7
+ children?: unknown;
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ const SAFE_URL_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
12
+ const HTTP_URL_RE = /^https?:\/\//i;
13
+ const CLASS_TOKEN_INVALID_CHARS_RE = /[^a-z0-9_-]+/g;
14
+ const EDGE_HYPHENS_RE = /^-+|-+$/g;
15
+ const INCREMARK_BLOCK_MATH_PLACEHOLDER_RE = /<div class="incremark-block incremark-math incremark-math--pending" data-incremark-math="([\s\S]*?)" data-incremark-math-display="block"><pre><code>[\s\S]*?<\/code><\/pre><\/div>/g;
16
+ const INCREMARK_INLINE_MATH_PLACEHOLDER_RE = /<code class="incremark-inline-code incremark-inline-math incremark-inline-math--pending" data-incremark-math="([\s\S]*?)" data-incremark-math-display="inline">[\s\S]*?<\/code>/g;
17
+ const HTML_ENTITY_RE = /&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g;
18
+ const SAFE_HTML_TAGS = new Set([
19
+ 'a',
20
+ 'article',
21
+ 'b',
22
+ 'blockquote',
23
+ 'br',
24
+ 'code',
25
+ 'del',
26
+ 'details',
27
+ 'div',
28
+ 'em',
29
+ 'figcaption',
30
+ 'figure',
31
+ 'footer',
32
+ 'h1',
33
+ 'h2',
34
+ 'h3',
35
+ 'h4',
36
+ 'h5',
37
+ 'h6',
38
+ 'header',
39
+ 'hr',
40
+ 'i',
41
+ 'img',
42
+ 'kbd',
43
+ 'li',
44
+ 'ol',
45
+ 'p',
46
+ 'pre',
47
+ 's',
48
+ 'section',
49
+ 'span',
50
+ 'strong',
51
+ 'sub',
52
+ 'summary',
53
+ 'sup',
54
+ 'table',
55
+ 'tbody',
56
+ 'td',
57
+ 'th',
58
+ 'thead',
59
+ 'tr',
60
+ 'u',
61
+ 'ul'
62
+ ]);
63
+ const BLOCK_HTML_TAGS = new Set([
64
+ 'article',
65
+ 'blockquote',
66
+ 'details',
67
+ 'div',
68
+ 'figcaption',
69
+ 'figure',
70
+ 'footer',
71
+ 'h1',
72
+ 'h2',
73
+ 'h3',
74
+ 'h4',
75
+ 'h5',
76
+ 'h6',
77
+ 'header',
78
+ 'hr',
79
+ 'li',
80
+ 'ol',
81
+ 'p',
82
+ 'pre',
83
+ 'section',
84
+ 'table',
85
+ 'tbody',
86
+ 'td',
87
+ 'th',
88
+ 'thead',
89
+ 'tr',
90
+ 'ul'
91
+ ]);
92
+ const VOID_HTML_TAGS = new Set(['br', 'hr', 'img']);
93
+ const SAFE_HTML_ATTRIBUTES = new Set([
94
+ 'align',
95
+ 'alt',
96
+ 'class',
97
+ 'colspan',
98
+ 'height',
99
+ 'href',
100
+ 'id',
101
+ 'open',
102
+ 'rel',
103
+ 'rowspan',
104
+ 'src',
105
+ 'target',
106
+ 'title',
107
+ 'width'
108
+ ]);
109
+ const HEADING_SIZES: Record<number, string> = {
110
+ 1: '1.5rem',
111
+ 2: '1.2rem',
112
+ 3: '1.1rem',
113
+ 4: '1.05rem',
114
+ 5: '1rem',
115
+ 6: '0.94rem'
116
+ };
117
+ const NAMED_HTML_ENTITIES: Record<string, string> = {
118
+ amp: '&',
119
+ apos: "'",
120
+ gt: '>',
121
+ lt: '<',
122
+ quot: '"'
123
+ };
124
+ const CODE_COPY_BUTTON_HTML =
125
+ '<button type="button" class="incremark-code-copy" aria-label="Copy code" title="Copy code"></button>';
126
+
127
+ export function renderIncremarkAst(root: Root): string {
128
+ return root.children.map((child) => renderBlock(child as NodeLike)).join('');
129
+ }
130
+
131
+ export async function renderKatexIncremarkHtml(html: string): Promise<string> {
132
+ if (!html.includes('data-incremark-math=')) {
133
+ return html;
134
+ }
135
+
136
+ try {
137
+ const { default: katex } = await import('katex');
138
+ const withRenderedBlockMath = html.replace(
139
+ INCREMARK_BLOCK_MATH_PLACEHOLDER_RE,
140
+ (_match, encodedFormula: string) => renderKatexFormulaHtml(katex, decodeHtmlEntities(encodedFormula), true)
141
+ );
142
+
143
+ return withRenderedBlockMath.replace(
144
+ INCREMARK_INLINE_MATH_PLACEHOLDER_RE,
145
+ (_match, encodedFormula: string) => renderKatexFormulaHtml(katex, decodeHtmlEntities(encodedFormula), false)
146
+ );
147
+ } catch {
148
+ return html;
149
+ }
150
+ }
151
+
152
+ function renderBlock(node: NodeLike): string {
153
+ switch (getType(node)) {
154
+ case 'paragraph':
155
+ return wrapBlock('p', 'incremark-paragraph', renderInlineChildren(getChildren(node)));
156
+ case 'heading': {
157
+ const depth = clampHeadingDepth(getNumber(node, 'depth'));
158
+ const size = HEADING_SIZES[depth];
159
+ return `<h${depth} class="incremark-block incremark-heading incremark-heading-${depth}" style="font-size: ${size};">${renderInlineChildren(getChildren(node))}</h${depth}>`;
160
+ }
161
+ case 'code':
162
+ return renderCodeBlock(node);
163
+ case 'blockquote':
164
+ return wrapBlock('blockquote', 'incremark-blockquote', renderBlockChildren(getChildren(node)));
165
+ case 'list':
166
+ return renderList(node);
167
+ case 'listItem':
168
+ return renderListItem(node);
169
+ case 'thematicBreak':
170
+ return '<hr class="incremark-block incremark-thematic-break" />';
171
+ case 'table':
172
+ return renderTable(node);
173
+ case 'math':
174
+ return renderMathBlock(node);
175
+ case 'htmlElement':
176
+ return renderHtmlElement(node);
177
+ case 'containerDirective':
178
+ case 'leafDirective':
179
+ return renderDirectiveBlock(node);
180
+ case 'footnoteDefinition':
181
+ return renderFootnoteDefinition(node);
182
+ case 'definition':
183
+ return '';
184
+ case 'html':
185
+ return renderCodeShell(`<pre><code>${escapeHtml(getString(node, 'value'))}</code></pre>`);
186
+ default: {
187
+ const children = getChildren(node);
188
+ if (children.length > 0) {
189
+ return renderBlockChildren(children);
190
+ }
191
+
192
+ const value = getString(node, 'value');
193
+ return value ? wrapBlock('p', 'incremark-paragraph', escapeHtml(value)) : '';
194
+ }
195
+ }
196
+ }
197
+
198
+ function renderInline(node: NodeLike): string {
199
+ switch (getType(node)) {
200
+ case 'text':
201
+ return escapeHtml(getString(node, 'value'));
202
+ case 'strong':
203
+ return `<strong>${renderInlineChildren(getChildren(node))}</strong>`;
204
+ case 'emphasis':
205
+ return `<em>${renderInlineChildren(getChildren(node))}</em>`;
206
+ case 'delete':
207
+ return `<del>${renderInlineChildren(getChildren(node))}</del>`;
208
+ case 'inlineCode':
209
+ return `<code class="incremark-inline-code">${escapeHtml(getString(node, 'value'))}</code>`;
210
+ case 'link':
211
+ return renderLink(node);
212
+ case 'linkReference':
213
+ return renderInlineChildren(getChildren(node)) || escapeHtml(getString(node, 'label') || getString(node, 'identifier'));
214
+ case 'image':
215
+ return renderImage(node);
216
+ case 'imageReference':
217
+ return escapeHtml(getString(node, 'alt'));
218
+ case 'break':
219
+ return '<br />';
220
+ case 'inlineMath':
221
+ return renderInlineMath(node);
222
+ case 'footnoteReference':
223
+ return `<sup class="incremark-footnote-reference">[${escapeHtml(getString(node, 'identifier'))}]</sup>`;
224
+ case 'textDirective':
225
+ return renderDirectiveInline(node);
226
+ case 'htmlElement':
227
+ return renderHtmlElement(node);
228
+ case 'html':
229
+ return escapeHtml(getString(node, 'value'));
230
+ default: {
231
+ const children = getChildren(node);
232
+ if (children.length > 0) {
233
+ return renderInlineChildren(children);
234
+ }
235
+
236
+ return escapeHtml(getString(node, 'value'));
237
+ }
238
+ }
239
+ }
240
+
241
+ function renderBlockChildren(children: NodeLike[]): string {
242
+ return children.map((child) => renderBlock(child)).join('');
243
+ }
244
+
245
+ function renderInlineChildren(children: NodeLike[]): string {
246
+ return children.map((child) => renderInline(child)).join('');
247
+ }
248
+
249
+ function renderCodeShell(preHtml: string, headerHtml = ''): string {
250
+ return `<div class="incremark-block incremark-code"><div class="incremark-code-toolbar">${headerHtml}${CODE_COPY_BUTTON_HTML}</div>${preHtml}</div>`;
251
+ }
252
+
253
+ function renderCodeBlock(node: NodeLike): string {
254
+ const lang = sanitizeClassToken(getString(node, 'lang'));
255
+ const langLabel = lang ? `<div class="incremark-code-header">${escapeHtml(lang)}</div>` : '';
256
+ const languageClass = lang ? ` class="language-${escapeAttribute(lang)}"` : '';
257
+ const code = `<pre><code${languageClass}>${escapeHtml(getString(node, 'value'))}</code></pre>`;
258
+ return renderCodeShell(code, langLabel);
259
+ }
260
+
261
+ function renderMathBlock(node: NodeLike): string {
262
+ return renderKatexPlaceholder(getString(node, 'value'), true);
263
+ }
264
+
265
+ function renderInlineMath(node: NodeLike): string {
266
+ return renderKatexPlaceholder(getString(node, 'value'), false);
267
+ }
268
+
269
+ function renderList(node: NodeLike): string {
270
+ const ordered = getBoolean(node, 'ordered') === true;
271
+ const tag = ordered ? 'ol' : 'ul';
272
+ const classes = `incremark-block incremark-list incremark-list--${ordered ? 'ordered' : 'unordered'}`;
273
+ const start = ordered ? getNumber(node, 'start') : undefined;
274
+ const startAttr = ordered && typeof start === 'number' && start > 1 ? ` start="${start}"` : '';
275
+ return `<${tag} class="${classes}"${startAttr}>${getChildren(node).map((child) => renderListItem(child)).join('')}</${tag}>`;
276
+ }
277
+
278
+ function renderListItem(node: NodeLike): string {
279
+ const children = getChildren(node);
280
+ const checked = getBoolean(node, 'checked');
281
+
282
+ if (typeof checked === 'boolean' && children[0] && getType(children[0]) === 'paragraph') {
283
+ const [first, ...rest] = children;
284
+ const firstContent = renderInlineChildren(getChildren(first));
285
+ const restContent = rest.map((child) => renderBlock(child)).join('');
286
+ return `<li class="incremark-list-item incremark-list-item--task"><label class="incremark-task-item"><input class="incremark-task-checkbox" type="checkbox" disabled${checked ? ' checked' : ''} /><span class="incremark-task-content">${firstContent}</span></label>${restContent}</li>`;
287
+ }
288
+
289
+ const checkbox = typeof checked === 'boolean'
290
+ ? `<input class="incremark-task-checkbox" type="checkbox" disabled${checked ? ' checked' : ''} />`
291
+ : '';
292
+ return `<li class="incremark-list-item">${checkbox}${children.map((child) => renderBlock(child)).join('')}</li>`;
293
+ }
294
+
295
+ function renderTable(node: NodeLike): string {
296
+ const rows = getChildren(node);
297
+ if (rows.length === 0) {
298
+ return '';
299
+ }
300
+
301
+ const align = Array.isArray(node.align) ? node.align : [];
302
+ const [headerRow, ...bodyRows] = rows;
303
+ const thead = `<thead>${renderTableRow(headerRow, true, align)}</thead>`;
304
+ const tbody = bodyRows.length > 0
305
+ ? `<tbody>${bodyRows.map((row) => renderTableRow(row, false, align)).join('')}</tbody>`
306
+ : '';
307
+
308
+ return `<div class="incremark-block incremark-table-wrapper"><table class="incremark-table">${thead}${tbody}</table></div>`;
309
+ }
310
+
311
+ function renderTableRow(row: NodeLike, header: boolean, align: unknown[]): string {
312
+ const cells = getChildren(row);
313
+ const tag = header ? 'th' : 'td';
314
+ const className = header ? 'incremark-table-header' : 'incremark-table-cell';
315
+ const renderedCells = cells.map((cell, index) => {
316
+ const alignment = typeof align[index] === 'string' ? String(align[index]) : '';
317
+ const style = alignment ? ` style="text-align: ${escapeAttribute(alignment)};"` : '';
318
+ return `<${tag} class="${className}"${style}>${renderInlineChildren(getChildren(cell))}</${tag}>`;
319
+ }).join('');
320
+
321
+ return `<tr>${renderedCells}</tr>`;
322
+ }
323
+
324
+ function renderLink(node: NodeLike): string {
325
+ const href = sanitizeUrl(getString(node, 'url'));
326
+ const content = renderInlineChildren(getChildren(node)) || escapeHtml(getString(node, 'url'));
327
+ if (!href) {
328
+ return content;
329
+ }
330
+
331
+ const title = getString(node, 'title');
332
+ const titleAttr = title ? ` title="${escapeAttribute(title)}"` : '';
333
+ const relAttr = isExternalHttpUrl(href) ? ' rel="noreferrer"' : '';
334
+ return `<a class="incremark-link" href="${escapeAttribute(href)}"${titleAttr}${relAttr}>${content}</a>`;
335
+ }
336
+
337
+ function renderImage(node: NodeLike): string {
338
+ const src = sanitizeUrl(getString(node, 'url'));
339
+ if (!src) {
340
+ return escapeHtml(getString(node, 'alt'));
341
+ }
342
+
343
+ const alt = escapeAttribute(getString(node, 'alt'));
344
+ const title = getString(node, 'title');
345
+ const titleAttr = title ? ` title="${escapeAttribute(title)}"` : '';
346
+ return `<img class="incremark-image" src="${escapeAttribute(src)}" alt="${alt}"${titleAttr} />`;
347
+ }
348
+
349
+ function renderDirectiveBlock(node: NodeLike): string {
350
+ const name = sanitizeClassToken(getString(node, 'name')) || 'directive';
351
+ const content = renderBlockChildren(getChildren(node));
352
+ const attrs = renderHtmlAttributes(getAttributes(node, 'attributes'), [
353
+ 'incremark-block',
354
+ 'incremark-container',
355
+ `incremark-container--${name}`
356
+ ]);
357
+ return `<section${attrs}>${content}</section>`;
358
+ }
359
+
360
+ function renderDirectiveInline(node: NodeLike): string {
361
+ const name = sanitizeClassToken(getString(node, 'name')) || 'directive';
362
+ const content = renderInlineChildren(getChildren(node));
363
+ const attrs = renderHtmlAttributes(getAttributes(node, 'attributes'), [
364
+ 'incremark-inline-directive',
365
+ `incremark-inline-directive--${name}`
366
+ ]);
367
+ return `<span${attrs}>${content}</span>`;
368
+ }
369
+
370
+ function renderHtmlElement(node: NodeLike): string {
371
+ const tagName = getString(node, 'tagName').toLowerCase();
372
+ if (!SAFE_HTML_TAGS.has(tagName)) {
373
+ return renderBlockChildren(getChildren(node));
374
+ }
375
+
376
+ const extraClasses = [
377
+ 'incremark-html-element',
378
+ `incremark-html-element--${sanitizeClassToken(tagName)}`
379
+ ];
380
+
381
+ if (tagName === 'img') {
382
+ extraClasses.push('incremark-image');
383
+ }
384
+
385
+ if (BLOCK_HTML_TAGS.has(tagName)) {
386
+ extraClasses.unshift('incremark-block');
387
+ }
388
+
389
+ const attrs = renderHtmlAttributes(getAttributes(node, 'attrs'), extraClasses);
390
+ if (VOID_HTML_TAGS.has(tagName)) {
391
+ return `<${tagName}${attrs} />`;
392
+ }
393
+
394
+ const content = BLOCK_HTML_TAGS.has(tagName)
395
+ ? renderMixedChildren(getChildren(node))
396
+ : renderInlineChildren(getChildren(node));
397
+ return `<${tagName}${attrs}>${content}</${tagName}>`;
398
+ }
399
+
400
+ function renderFootnoteDefinition(node: NodeLike): string {
401
+ const identifier = sanitizeClassToken(getString(node, 'identifier'));
402
+ const idAttr = identifier ? ` id="footnote-${escapeAttribute(identifier)}"` : '';
403
+ return `<div class="incremark-block incremark-footnote-definition"${idAttr}>${renderBlockChildren(getChildren(node))}</div>`;
404
+ }
405
+
406
+ function renderMixedChildren(children: NodeLike[]): string {
407
+ return children.map((child) => renderAny(child)).join('');
408
+ }
409
+
410
+ function renderAny(node: NodeLike): string {
411
+ const type = getType(node);
412
+ if (type === 'textDirective') {
413
+ return renderDirectiveInline(node);
414
+ }
415
+
416
+ if (type === 'htmlElement') {
417
+ const tagName = getString(node, 'tagName').toLowerCase();
418
+ return BLOCK_HTML_TAGS.has(tagName) ? renderBlock(node) : renderInline(node);
419
+ }
420
+
421
+ if (isInlineType(type)) {
422
+ return renderInline(node);
423
+ }
424
+
425
+ return renderBlock(node);
426
+ }
427
+
428
+ function wrapBlock(tag: string, className: string, content: string): string {
429
+ return `<${tag} class="incremark-block ${className}">${content}</${tag}>`;
430
+ }
431
+
432
+ function renderKatexPlaceholder(formula: string, displayMode: boolean): string {
433
+ const trimmedFormula = formula.trim();
434
+ if (!trimmedFormula) {
435
+ return displayMode ? '' : '<span class="incremark-inline-math"></span>';
436
+ }
437
+
438
+ if (displayMode) {
439
+ return `<div class="incremark-block incremark-math incremark-math--pending" data-incremark-math="${escapeAttribute(trimmedFormula)}" data-incremark-math-display="block"><pre><code>${escapeHtml(trimmedFormula)}</code></pre></div>`;
440
+ }
441
+
442
+ return `<code class="incremark-inline-code incremark-inline-math incremark-inline-math--pending" data-incremark-math="${escapeAttribute(trimmedFormula)}" data-incremark-math-display="inline">${escapeHtml(trimmedFormula)}</code>`;
443
+ }
444
+
445
+ function renderKatexFormulaHtml(katex: KatexRenderer, formula: string, displayMode: boolean): string {
446
+ const trimmedFormula = formula.trim();
447
+ if (!trimmedFormula) {
448
+ return displayMode ? '' : '<span class="incremark-inline-math"></span>';
449
+ }
450
+
451
+ try {
452
+ const renderedFormula = katex.renderToString(trimmedFormula, {
453
+ displayMode,
454
+ output: 'htmlAndMathml',
455
+ throwOnError: false,
456
+ trust: false
457
+ });
458
+
459
+ if (displayMode) {
460
+ return `<div class="incremark-block incremark-math incremark-math--rendered">${renderedFormula}</div>`;
461
+ }
462
+
463
+ return `<span class="incremark-inline-math incremark-inline-math--rendered">${renderedFormula}</span>`;
464
+ } catch {
465
+ return renderKatexFallbackHtml(trimmedFormula, displayMode);
466
+ }
467
+ }
468
+
469
+ function renderKatexFallbackHtml(formula: string, displayMode: boolean): string {
470
+ if (displayMode) {
471
+ return `<div class="incremark-block incremark-math incremark-math--fallback"><pre><code>${escapeHtml(formula)}</code></pre></div>`;
472
+ }
473
+
474
+ return `<code class="incremark-inline-code incremark-inline-math incremark-inline-math--fallback">${escapeHtml(formula)}</code>`;
475
+ }
476
+
477
+ function renderHtmlAttributes(attributes: Record<string, string>, extraClasses: string[]): string {
478
+ const rendered: string[] = [];
479
+ const classNames = [...extraClasses];
480
+
481
+ for (const [name, rawValue] of Object.entries(attributes)) {
482
+ const normalizedName = name.toLowerCase();
483
+ if (normalizedName.startsWith('on')) {
484
+ continue;
485
+ }
486
+
487
+ if (normalizedName === 'class') {
488
+ if (rawValue.trim()) {
489
+ classNames.push(rawValue.trim());
490
+ }
491
+ continue;
492
+ }
493
+
494
+ if (normalizedName === 'style') {
495
+ continue;
496
+ }
497
+
498
+ if (!SAFE_HTML_ATTRIBUTES.has(normalizedName) && !normalizedName.startsWith('aria-') && !normalizedName.startsWith('data-')) {
499
+ continue;
500
+ }
501
+
502
+ if (normalizedName === 'href' || normalizedName === 'src') {
503
+ const safeUrl = sanitizeUrl(rawValue);
504
+ if (!safeUrl) {
505
+ continue;
506
+ }
507
+ rendered.push(` ${normalizedName}="${escapeAttribute(safeUrl)}"`);
508
+ continue;
509
+ }
510
+
511
+ if (normalizedName === 'open') {
512
+ if (rawValue !== 'false') {
513
+ rendered.push(' open');
514
+ }
515
+ continue;
516
+ }
517
+
518
+ rendered.push(` ${normalizedName}="${escapeAttribute(rawValue)}"`);
519
+ }
520
+
521
+ if (classNames.length > 0) {
522
+ rendered.unshift(` class="${escapeAttribute(classNames.join(' '))}"`);
523
+ }
524
+
525
+ return rendered.join('');
526
+ }
527
+
528
+ function getAttributes(node: NodeLike, key: string): Record<string, string> {
529
+ const value = node[key];
530
+ if (!value || typeof value !== 'object') {
531
+ return {};
532
+ }
533
+
534
+ return Object.fromEntries(
535
+ Object.entries(value as Record<string, unknown>).flatMap(([name, attrValue]) => {
536
+ if (typeof attrValue !== 'string') {
537
+ return [];
538
+ }
539
+
540
+ return [[name, attrValue]];
541
+ })
542
+ );
543
+ }
544
+
545
+ function getChildren(node: NodeLike): NodeLike[] {
546
+ return Array.isArray(node.children) ? (node.children as NodeLike[]) : [];
547
+ }
548
+
549
+ function getType(node: NodeLike): string {
550
+ return typeof node.type === 'string' ? node.type : '';
551
+ }
552
+
553
+ function getString(node: NodeLike, key: string): string {
554
+ const value = node[key];
555
+ return typeof value === 'string' ? value : '';
556
+ }
557
+
558
+ function getBoolean(node: NodeLike, key: string): boolean | undefined {
559
+ const value = node[key];
560
+ return typeof value === 'boolean' ? value : undefined;
561
+ }
562
+
563
+ function getNumber(node: NodeLike, key: string): number | undefined {
564
+ const value = node[key];
565
+ return typeof value === 'number' ? value : undefined;
566
+ }
567
+
568
+ function clampHeadingDepth(depth: number | undefined): 1 | 2 | 3 | 4 | 5 | 6 {
569
+ if (!depth || depth < 1) {
570
+ return 1;
571
+ }
572
+
573
+ if (depth > 6) {
574
+ return 6;
575
+ }
576
+
577
+ return depth as 1 | 2 | 3 | 4 | 5 | 6;
578
+ }
579
+
580
+ function sanitizeUrl(url: string): string | null {
581
+ const trimmed = url.trim();
582
+ if (!trimmed) {
583
+ return null;
584
+ }
585
+
586
+ if (trimmed.startsWith('#') || trimmed.startsWith('/') || trimmed.startsWith('./') || trimmed.startsWith('../')) {
587
+ return trimmed;
588
+ }
589
+
590
+ try {
591
+ const parsed = new URL(trimmed);
592
+ return SAFE_URL_PROTOCOLS.has(parsed.protocol) ? trimmed : null;
593
+ } catch {
594
+ return null;
595
+ }
596
+ }
597
+
598
+ function isExternalHttpUrl(url: string): boolean {
599
+ return HTTP_URL_RE.test(url);
600
+ }
601
+
602
+ function isInlineType(type: string): boolean {
603
+ return type === 'break'
604
+ || type === 'delete'
605
+ || type === 'emphasis'
606
+ || type === 'footnoteReference'
607
+ || type === 'html'
608
+ || type === 'image'
609
+ || type === 'imageReference'
610
+ || type === 'inlineCode'
611
+ || type === 'inlineMath'
612
+ || type === 'link'
613
+ || type === 'linkReference'
614
+ || type === 'strong'
615
+ || type === 'text';
616
+ }
617
+
618
+ function sanitizeClassToken(value: string): string {
619
+ return value
620
+ .trim()
621
+ .toLowerCase()
622
+ .replace(CLASS_TOKEN_INVALID_CHARS_RE, '-')
623
+ .replace(EDGE_HYPHENS_RE, '');
624
+ }
625
+
626
+ function escapeHtml(value: string): string {
627
+ return value
628
+ .replaceAll('&', '&amp;')
629
+ .replaceAll('<', '&lt;')
630
+ .replaceAll('>', '&gt;')
631
+ .replaceAll('"', '&quot;')
632
+ .replaceAll("'", '&#39;');
633
+ }
634
+
635
+ function escapeAttribute(value: string): string {
636
+ return escapeHtml(value);
637
+ }
638
+
639
+ function decodeHtmlEntities(value: string): string {
640
+ return value.replace(HTML_ENTITY_RE, (match, entity) => {
641
+ if (entity.startsWith('#x') || entity.startsWith('#X')) {
642
+ const codePoint = Number.parseInt(entity.slice(2), 16);
643
+ return Number.isNaN(codePoint) ? match : String.fromCodePoint(codePoint);
644
+ }
645
+
646
+ if (entity.startsWith('#')) {
647
+ const codePoint = Number.parseInt(entity.slice(1), 10);
648
+ return Number.isNaN(codePoint) ? match : String.fromCodePoint(codePoint);
649
+ }
650
+
651
+ return NAMED_HTML_ENTITIES[entity] ?? match;
652
+ });
653
+ }