@oml/markdown 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/out/index.d.ts +2 -0
- package/out/index.js +4 -0
- package/out/index.js.map +1 -0
- package/out/md/index.d.ts +6 -0
- package/out/md/index.js +8 -0
- package/out/md/index.js.map +1 -0
- package/out/md/md-execution.d.ts +33 -0
- package/out/md/md-execution.js +3 -0
- package/out/md/md-execution.js.map +1 -0
- package/out/md/md-executor.d.ts +21 -0
- package/out/md/md-executor.js +498 -0
- package/out/md/md-executor.js.map +1 -0
- package/out/md/md-frontmatter.d.ts +4 -0
- package/out/md/md-frontmatter.js +48 -0
- package/out/md/md-frontmatter.js.map +1 -0
- package/out/md/md-registry.d.ts +7 -0
- package/out/md/md-registry.js +19 -0
- package/out/md/md-registry.js.map +1 -0
- package/out/md/md-runtime.d.ts +10 -0
- package/out/md/md-runtime.js +166 -0
- package/out/md/md-runtime.js.map +1 -0
- package/out/md/md-types.d.ts +40 -0
- package/out/md/md-types.js +3 -0
- package/out/md/md-types.js.map +1 -0
- package/out/md/md-yaml.d.ts +1 -0
- package/out/md/md-yaml.js +15 -0
- package/out/md/md-yaml.js.map +1 -0
- package/out/renderers/chart-renderer.d.ts +6 -0
- package/out/renderers/chart-renderer.js +392 -0
- package/out/renderers/chart-renderer.js.map +1 -0
- package/out/renderers/diagram-renderer.d.ts +7 -0
- package/out/renderers/diagram-renderer.js +2354 -0
- package/out/renderers/diagram-renderer.js.map +1 -0
- package/out/renderers/graph-renderer.d.ts +6 -0
- package/out/renderers/graph-renderer.js +1384 -0
- package/out/renderers/graph-renderer.js.map +1 -0
- package/out/renderers/index.d.ts +14 -0
- package/out/renderers/index.js +16 -0
- package/out/renderers/index.js.map +1 -0
- package/out/renderers/list-renderer.d.ts +6 -0
- package/out/renderers/list-renderer.js +252 -0
- package/out/renderers/list-renderer.js.map +1 -0
- package/out/renderers/matrix-renderer.d.ts +14 -0
- package/out/renderers/matrix-renderer.js +498 -0
- package/out/renderers/matrix-renderer.js.map +1 -0
- package/out/renderers/message-renderer.d.ts +6 -0
- package/out/renderers/message-renderer.js +14 -0
- package/out/renderers/message-renderer.js.map +1 -0
- package/out/renderers/registry.d.ts +9 -0
- package/out/renderers/registry.js +41 -0
- package/out/renderers/registry.js.map +1 -0
- package/out/renderers/renderer.d.ts +28 -0
- package/out/renderers/renderer.js +61 -0
- package/out/renderers/renderer.js.map +1 -0
- package/out/renderers/table-editor-renderer.d.ts +4 -0
- package/out/renderers/table-editor-renderer.js +9 -0
- package/out/renderers/table-editor-renderer.js.map +1 -0
- package/out/renderers/table-renderer.d.ts +95 -0
- package/out/renderers/table-renderer.js +1571 -0
- package/out/renderers/table-renderer.js.map +1 -0
- package/out/renderers/text-renderer.d.ts +7 -0
- package/out/renderers/text-renderer.js +219 -0
- package/out/renderers/text-renderer.js.map +1 -0
- package/out/renderers/tree-renderer.d.ts +4 -0
- package/out/renderers/tree-renderer.js +9 -0
- package/out/renderers/tree-renderer.js.map +1 -0
- package/out/renderers/types.d.ts +18 -0
- package/out/renderers/types.js +3 -0
- package/out/renderers/types.js.map +1 -0
- package/out/renderers/wikilink-utils.d.ts +6 -0
- package/out/renderers/wikilink-utils.js +100 -0
- package/out/renderers/wikilink-utils.js.map +1 -0
- package/out/static/browser-runtime.bundle.js +74155 -0
- package/out/static/browser-runtime.bundle.js.map +7 -0
- package/out/static/browser-runtime.d.ts +1 -0
- package/out/static/browser-runtime.js +218 -0
- package/out/static/browser-runtime.js.map +1 -0
- package/out/static/index.d.ts +1 -0
- package/out/static/index.js +3 -0
- package/out/static/index.js.map +1 -0
- package/out/static/runtime-assets.d.ts +2 -0
- package/out/static/runtime-assets.js +174 -0
- package/out/static/runtime-assets.js.map +1 -0
- package/package.json +74 -0
- package/src/index.ts +4 -0
- package/src/md/index.ts +8 -0
- package/src/md/md-execution.ts +51 -0
- package/src/md/md-executor.ts +598 -0
- package/src/md/md-frontmatter.ts +53 -0
- package/src/md/md-registry.ts +22 -0
- package/src/md/md-runtime.ts +191 -0
- package/src/md/md-types.ts +48 -0
- package/src/md/md-yaml.ts +17 -0
- package/src/renderers/chart-renderer.ts +473 -0
- package/src/renderers/diagram-renderer.ts +2520 -0
- package/src/renderers/graph-renderer.ts +1653 -0
- package/src/renderers/index.ts +16 -0
- package/src/renderers/list-renderer.ts +289 -0
- package/src/renderers/matrix-renderer.ts +616 -0
- package/src/renderers/message-renderer.ts +18 -0
- package/src/renderers/registry.ts +45 -0
- package/src/renderers/renderer.ts +84 -0
- package/src/renderers/table-editor-renderer.ts +8 -0
- package/src/renderers/table-renderer.ts +1868 -0
- package/src/renderers/text-renderer.ts +252 -0
- package/src/renderers/tree-renderer.ts +7 -0
- package/src/renderers/types.ts +22 -0
- package/src/renderers/wikilink-utils.ts +108 -0
- package/src/static/browser-runtime.ts +249 -0
- package/src/static/index.ts +3 -0
- package/src/static/runtime-assets.ts +175 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import { QueryMarkdownBlockRenderer } from './renderer.js';
|
|
4
|
+
import type { MdBlockExecutionResult } from './types.js';
|
|
5
|
+
import MarkdownIt from 'markdown-it';
|
|
6
|
+
|
|
7
|
+
type TextStyleSelectorKind = 'paragraph';
|
|
8
|
+
type TextStyleTarget = 'value';
|
|
9
|
+
|
|
10
|
+
type CompiledTextStyleRule = {
|
|
11
|
+
selectors: Array<{ kind: TextStyleSelectorKind; condition?: string }>;
|
|
12
|
+
target: TextStyleTarget;
|
|
13
|
+
style: Record<string, string>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type TextSelectorContext = {
|
|
17
|
+
kind: 'paragraph';
|
|
18
|
+
context: {
|
|
19
|
+
index: number;
|
|
20
|
+
value: string;
|
|
21
|
+
row: { get: (key?: unknown) => { value: string } | undefined };
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export class TextMarkdownBlockRenderer extends QueryMarkdownBlockRenderer {
|
|
26
|
+
private readonly markdown = new MarkdownIt({
|
|
27
|
+
html: false,
|
|
28
|
+
linkify: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
canRender(result: MdBlockExecutionResult): boolean {
|
|
32
|
+
return result.status === 'ok'
|
|
33
|
+
&& result.kind === 'text'
|
|
34
|
+
&& result.format === 'table'
|
|
35
|
+
&& !!result.payload;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
render(result: MdBlockExecutionResult): HTMLElement {
|
|
39
|
+
const container = this.createResultContainer(result.status);
|
|
40
|
+
container.classList.add('oml-md-result-plain');
|
|
41
|
+
const payload = result.payload;
|
|
42
|
+
if (!payload || payload.rows.length === 0) {
|
|
43
|
+
container.appendChild(this.createMessageContainer('No results.'));
|
|
44
|
+
return container;
|
|
45
|
+
}
|
|
46
|
+
const stylesheet = compileTextStylesheet(result.options);
|
|
47
|
+
|
|
48
|
+
const wrapper = document.createElement('div');
|
|
49
|
+
wrapper.className = 'oml-md-result-text';
|
|
50
|
+
payload.rows.forEach((row, index) => {
|
|
51
|
+
const paragraph = document.createElement('p');
|
|
52
|
+
const valueSpan = document.createElement('span');
|
|
53
|
+
valueSpan.className = 'oml-md-result-text-value';
|
|
54
|
+
valueSpan.innerHTML = this.markdown.renderInline(row[0] ?? '');
|
|
55
|
+
paragraph.appendChild(valueSpan);
|
|
56
|
+
|
|
57
|
+
const context = createParagraphSelectorContext(index, row[0] ?? '', payload.columns, row);
|
|
58
|
+
for (const rule of stylesheet) {
|
|
59
|
+
if (!matchesTextRule(rule, [context])) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
applyTextValueStyles(valueSpan, rule.style);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
wrapper.appendChild(paragraph);
|
|
66
|
+
});
|
|
67
|
+
container.appendChild(wrapper);
|
|
68
|
+
return container;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function compileTextStylesheet(options: Record<string, unknown> | undefined): CompiledTextStyleRule[] {
|
|
73
|
+
const rawStylesheet = options?.stylesheet;
|
|
74
|
+
if (!Array.isArray(rawStylesheet)) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const compiled: CompiledTextStyleRule[] = [];
|
|
78
|
+
for (const rawRule of rawStylesheet) {
|
|
79
|
+
if (!isRecord(rawRule)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const selectors = parseTextSelectors(rawRule.selector);
|
|
83
|
+
if (selectors.length === 0) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const style = normalizeStyle(rawRule.style);
|
|
87
|
+
if (Object.keys(style).length === 0) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
compiled.push({ selectors, target: 'value', style });
|
|
91
|
+
}
|
|
92
|
+
return compiled;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createParagraphSelectorContext(index: number, value: string, columns: string[], row: string[]): TextSelectorContext {
|
|
96
|
+
return { kind: 'paragraph', context: { index, value, row: createRowAccessor(columns, row) } };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function matchesTextRule(rule: CompiledTextStyleRule, contexts: ReadonlyArray<TextSelectorContext>): boolean {
|
|
100
|
+
return rule.selectors.some((selector) => {
|
|
101
|
+
const contextEntry = contexts.find((entry) => entry.kind === selector.kind);
|
|
102
|
+
if (!contextEntry) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return evaluateCondition(selector.condition, contextEntry.context);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function applyTextValueStyles(target: HTMLElement, style: Record<string, string>): void {
|
|
110
|
+
for (const [property, cssValue] of Object.entries(style)) {
|
|
111
|
+
target.style.setProperty(property, cssValue);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseTextSelectors(rawSelector: unknown): Array<{ kind: TextStyleSelectorKind; condition?: string }> {
|
|
116
|
+
if (typeof rawSelector !== 'string') {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const parts = splitSelectors(rawSelector);
|
|
120
|
+
const selectors: Array<{ kind: TextStyleSelectorKind; condition?: string }> = [];
|
|
121
|
+
for (const part of parts) {
|
|
122
|
+
const parsed = parseTextSelector(part);
|
|
123
|
+
if (parsed) {
|
|
124
|
+
selectors.push(parsed);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return selectors;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function splitSelectors(rawSelector: string): string[] {
|
|
131
|
+
const parts: string[] = [];
|
|
132
|
+
let current = '';
|
|
133
|
+
let bracketDepth = 0;
|
|
134
|
+
let quote: '"' | "'" | undefined;
|
|
135
|
+
for (let index = 0; index < rawSelector.length; index += 1) {
|
|
136
|
+
const char = rawSelector[index];
|
|
137
|
+
if (quote) {
|
|
138
|
+
current += char;
|
|
139
|
+
if (char === quote && rawSelector[index - 1] !== '\\') {
|
|
140
|
+
quote = undefined;
|
|
141
|
+
}
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (char === '"' || char === "'") {
|
|
145
|
+
quote = char;
|
|
146
|
+
current += char;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (char === '[') {
|
|
150
|
+
bracketDepth += 1;
|
|
151
|
+
current += char;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (char === ']' && bracketDepth > 0) {
|
|
155
|
+
bracketDepth -= 1;
|
|
156
|
+
current += char;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (char === ',' && bracketDepth === 0) {
|
|
160
|
+
parts.push(current.trim());
|
|
161
|
+
current = '';
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
current += char;
|
|
165
|
+
}
|
|
166
|
+
if (current.trim()) {
|
|
167
|
+
parts.push(current.trim());
|
|
168
|
+
}
|
|
169
|
+
return parts;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function parseTextSelector(selector: string): { kind: TextStyleSelectorKind; condition?: string } | undefined {
|
|
173
|
+
const trimmed = selector.trim();
|
|
174
|
+
if (!trimmed) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
const bracketStart = trimmed.indexOf('[');
|
|
178
|
+
if (bracketStart === -1) {
|
|
179
|
+
return toTextSelector(trimmed, undefined);
|
|
180
|
+
}
|
|
181
|
+
if (!trimmed.endsWith(']')) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const kind = trimmed.slice(0, bracketStart).trim();
|
|
185
|
+
const condition = trimmed.slice(bracketStart + 1, -1).trim();
|
|
186
|
+
return toTextSelector(kind, condition || undefined);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function toTextSelector(kind: string, condition: string | undefined): { kind: TextStyleSelectorKind; condition?: string } | undefined {
|
|
190
|
+
const normalized = kind.trim().toLowerCase();
|
|
191
|
+
if (normalized !== 'paragraph') {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
return { kind: normalized, condition };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeStyle(rawStyle: unknown): Record<string, string> {
|
|
198
|
+
if (!isRecord(rawStyle)) {
|
|
199
|
+
return {};
|
|
200
|
+
}
|
|
201
|
+
const style: Record<string, string> = {};
|
|
202
|
+
for (const [property, rawValue] of Object.entries(rawStyle)) {
|
|
203
|
+
if (!property.trim()) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (rawValue === undefined || rawValue === null) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
style[property] = String(rawValue);
|
|
210
|
+
}
|
|
211
|
+
return style;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function createRowAccessor(columns: string[], row: string[]): { get: (key?: unknown) => { value: string } | undefined } {
|
|
215
|
+
return {
|
|
216
|
+
get: (key?: unknown) => {
|
|
217
|
+
if (typeof key === 'number' && Number.isInteger(key)) {
|
|
218
|
+
const value = row[key];
|
|
219
|
+
return value === undefined ? undefined : { value };
|
|
220
|
+
}
|
|
221
|
+
if (typeof key === 'string') {
|
|
222
|
+
const index = columns.indexOf(key);
|
|
223
|
+
if (index < 0) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
const value = row[index];
|
|
227
|
+
return value === undefined ? undefined : { value };
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function evaluateCondition(condition: string | undefined, context: object): boolean {
|
|
235
|
+
if (!condition) {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const scope = Object.assign(Object.create(null), context) as Record<string, unknown>;
|
|
240
|
+
const keys = Object.keys(scope);
|
|
241
|
+
const values = keys.map((key) => scope[key]);
|
|
242
|
+
// eslint-disable-next-line no-new-func
|
|
243
|
+
const evaluator = new Function(...keys, `"use strict"; return (${condition});`);
|
|
244
|
+
return Boolean(evaluator(...values));
|
|
245
|
+
} catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
251
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
252
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import { TableMarkdownBlockRenderer } from './table-renderer.js';
|
|
4
|
+
|
|
5
|
+
export class TreeMarkdownBlockRenderer extends TableMarkdownBlockRenderer {
|
|
6
|
+
protected override readonly tableKinds = ['tree'] as const;
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
export type MdExecutionStatus = 'ok' | 'error' | 'unimplemented';
|
|
4
|
+
|
|
5
|
+
export interface MdTablePayload {
|
|
6
|
+
columns: string[];
|
|
7
|
+
rows: string[][];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface MdBlockExecutionResult {
|
|
11
|
+
blockId: string;
|
|
12
|
+
kind: string;
|
|
13
|
+
status: MdExecutionStatus;
|
|
14
|
+
format: 'table' | 'message';
|
|
15
|
+
blockSource?: string;
|
|
16
|
+
blockMeta?: string;
|
|
17
|
+
blockLineStart?: number;
|
|
18
|
+
blockLineEnd?: number;
|
|
19
|
+
options?: Record<string, unknown>;
|
|
20
|
+
payload?: MdTablePayload;
|
|
21
|
+
message?: string;
|
|
22
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
export function appendInlineValue(container: HTMLElement, value: string): void {
|
|
4
|
+
const wikilinkPattern = /\[\[([^\]]+)\]\]/g;
|
|
5
|
+
let cursor = 0;
|
|
6
|
+
for (const match of value.matchAll(wikilinkPattern)) {
|
|
7
|
+
const index = match.index ?? 0;
|
|
8
|
+
if (index > cursor) {
|
|
9
|
+
appendPlainSegment(container, value.slice(cursor, index));
|
|
10
|
+
}
|
|
11
|
+
const iri = (match[1] ?? '').trim();
|
|
12
|
+
if (iri.length > 0) {
|
|
13
|
+
container.appendChild(createWikiLinkElement(iri, shortLabelFromIri(iri)));
|
|
14
|
+
} else {
|
|
15
|
+
container.appendChild(document.createTextNode(match[0] ?? ''));
|
|
16
|
+
}
|
|
17
|
+
cursor = index + (match[0]?.length ?? 0);
|
|
18
|
+
}
|
|
19
|
+
if (cursor < value.length) {
|
|
20
|
+
appendPlainSegment(container, value.slice(cursor));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function appendTokenizedValueParts(container: HTMLElement, raw: string, partClass: string): void {
|
|
25
|
+
const tokens = splitTokenizedValueParts(raw);
|
|
26
|
+
if (tokens.length === 0) {
|
|
27
|
+
container.textContent = raw;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const token of tokens) {
|
|
32
|
+
const fragment = document.createElement('span');
|
|
33
|
+
fragment.className = partClass;
|
|
34
|
+
|
|
35
|
+
const wikilinkIri = parseWikilinkToken(token);
|
|
36
|
+
if (wikilinkIri) {
|
|
37
|
+
fragment.dataset.value = wikilinkIri;
|
|
38
|
+
fragment.appendChild(createWikiLinkElement(wikilinkIri, shortLabelFromIri(wikilinkIri)));
|
|
39
|
+
container.appendChild(fragment);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fragment.dataset.value = token;
|
|
44
|
+
if (isIriValue(token)) {
|
|
45
|
+
fragment.appendChild(createWikiLinkElement(token, shortLabelFromIri(token)));
|
|
46
|
+
} else {
|
|
47
|
+
fragment.textContent = token;
|
|
48
|
+
}
|
|
49
|
+
container.appendChild(fragment);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function splitTokenizedValueParts(raw: string): string[] {
|
|
54
|
+
return raw
|
|
55
|
+
.split(/\r?\n|,\s*/)
|
|
56
|
+
.map((entry) => entry.trim())
|
|
57
|
+
.filter((entry) => entry.length > 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function shortLabelFromIri(iri: string): string {
|
|
61
|
+
const hash = iri.lastIndexOf('#');
|
|
62
|
+
if (hash >= 0 && hash < iri.length - 1) {
|
|
63
|
+
return iri.slice(hash + 1);
|
|
64
|
+
}
|
|
65
|
+
const slash = iri.lastIndexOf('/');
|
|
66
|
+
if (slash >= 0 && slash < iri.length - 1) {
|
|
67
|
+
return iri.slice(slash + 1);
|
|
68
|
+
}
|
|
69
|
+
return iri;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isIriValue(value: string): boolean {
|
|
73
|
+
return /^[a-z][a-z0-9+.-]*:[^\s]+$/i.test(value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createWikiLinkElement(iri: string, label: string): HTMLAnchorElement {
|
|
77
|
+
const link = document.createElement('a');
|
|
78
|
+
link.className = 'wikilink';
|
|
79
|
+
link.href = '#';
|
|
80
|
+
link.setAttribute('iri', iri);
|
|
81
|
+
link.title = iri;
|
|
82
|
+
link.textContent = label;
|
|
83
|
+
return link;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function appendPlainSegment(container: HTMLElement, value: string): void {
|
|
87
|
+
const parts = value.split(/([\s,]+)/).filter((part) => part.length > 0);
|
|
88
|
+
for (const part of parts) {
|
|
89
|
+
if (/^[\s,]+$/.test(part)) {
|
|
90
|
+
container.appendChild(document.createTextNode(part));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!isIriValue(part)) {
|
|
94
|
+
container.appendChild(document.createTextNode(part));
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
container.appendChild(createWikiLinkElement(part, shortLabelFromIri(part)));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseWikilinkToken(value: string): string | undefined {
|
|
102
|
+
const match = /^\[\[([^\]]+)\]\]$/.exec(value);
|
|
103
|
+
if (!match) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
const iri = (match[1] ?? '').trim();
|
|
107
|
+
return iri.length > 0 ? iri : undefined;
|
|
108
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Copyright (c) 2026 Modelware. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import { createMarkdownRendererRegistry } from '../renderers/registry.js';
|
|
4
|
+
|
|
5
|
+
const SUPPORTED = new Set(['table', 'tree', 'graph', 'chart', 'diagram', 'list', 'text', 'matrix', 'table-editor']);
|
|
6
|
+
|
|
7
|
+
type ManifestEntry = { blockId: string; path: string };
|
|
8
|
+
type WikilinkConfig = { linkingEnabled?: boolean };
|
|
9
|
+
|
|
10
|
+
function parseJsonNode<T>(id: string, fallback: T): T {
|
|
11
|
+
const node = document.getElementById(id);
|
|
12
|
+
if (!node) {
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(node.textContent ?? '') as T;
|
|
17
|
+
} catch {
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function safeDecode(value: string): string {
|
|
23
|
+
try {
|
|
24
|
+
return decodeURIComponent(value);
|
|
25
|
+
} catch {
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeWikiPathKey(raw: string): string {
|
|
31
|
+
return String(raw || '')
|
|
32
|
+
.replace(/\\/g, '/')
|
|
33
|
+
.replace(/^\/+/, '')
|
|
34
|
+
.replace(/[/]+/g, '/')
|
|
35
|
+
.replace(/\.html$/i, '')
|
|
36
|
+
.replace(/^\.\//, '')
|
|
37
|
+
.toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveWikiHref(
|
|
41
|
+
iri: string,
|
|
42
|
+
wikilinkIndex: Record<string, string>,
|
|
43
|
+
iriAliasIndex: Record<string, string>
|
|
44
|
+
): string | undefined {
|
|
45
|
+
const text = String(iri || '').trim();
|
|
46
|
+
if (!text) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const alias =
|
|
50
|
+
iriAliasIndex[text]
|
|
51
|
+
|| iriAliasIndex[text.replace(/[.,;:!?]+$/, '')]
|
|
52
|
+
|| iriAliasIndex[safeDecode(text)];
|
|
53
|
+
if (typeof alias === 'string' && alias.length > 0) {
|
|
54
|
+
return alias;
|
|
55
|
+
}
|
|
56
|
+
const hashIndex = text.indexOf('#');
|
|
57
|
+
const fragment = hashIndex >= 0 ? text.slice(hashIndex) : '';
|
|
58
|
+
const iriWithoutFragment = hashIndex >= 0 ? text.slice(0, hashIndex) : text;
|
|
59
|
+
let parsed: URL;
|
|
60
|
+
try {
|
|
61
|
+
parsed = new URL(iriWithoutFragment);
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
const rawPath = safeDecode((parsed.pathname || '').replace(/^\/+/, ''));
|
|
66
|
+
const hostPath = `${(parsed.hostname || '').toLowerCase()}/${rawPath}`.replace(/^\/+/, '');
|
|
67
|
+
const candidates = [normalizeWikiPathKey(hostPath), normalizeWikiPathKey(rawPath)]
|
|
68
|
+
.filter((candidate, index, all) => candidate && all.indexOf(candidate) === index);
|
|
69
|
+
for (const candidate of candidates) {
|
|
70
|
+
const direct = wikilinkIndex[candidate];
|
|
71
|
+
if (typeof direct === 'string' && direct.length > 0) {
|
|
72
|
+
return direct + fragment;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let bestMatch: { key: string; href: string } | undefined;
|
|
76
|
+
for (const candidate of candidates) {
|
|
77
|
+
for (const [key, href] of Object.entries(wikilinkIndex)) {
|
|
78
|
+
if (!candidate.endsWith('/' + key) && candidate !== key && !key.endsWith('/' + candidate)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!bestMatch || key.length > bestMatch.key.length) {
|
|
82
|
+
bestMatch = { key, href };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (bestMatch && bestMatch.href.length > 0) {
|
|
87
|
+
return bestMatch.href + fragment;
|
|
88
|
+
}
|
|
89
|
+
if (fragment) {
|
|
90
|
+
const currentPath = normalizeWikiPathKey((window.location && window.location.pathname) || '');
|
|
91
|
+
if (currentPath && candidates.some((candidate) => candidate === currentPath)) {
|
|
92
|
+
return fragment;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function applyWikilinks(
|
|
99
|
+
scope: ParentNode,
|
|
100
|
+
wikilinkIndex: Record<string, string>,
|
|
101
|
+
iriAliasIndex: Record<string, string>,
|
|
102
|
+
linkingEnabled: boolean
|
|
103
|
+
): void {
|
|
104
|
+
const links = Array.from(scope.querySelectorAll<HTMLAnchorElement>('a.wikilink[iri]'));
|
|
105
|
+
for (const link of links) {
|
|
106
|
+
const iri = link.getAttribute('iri') || '';
|
|
107
|
+
const label = link.textContent || iri;
|
|
108
|
+
if (!linkingEnabled) {
|
|
109
|
+
const span = document.createElement('span');
|
|
110
|
+
span.className = 'wikilink';
|
|
111
|
+
span.setAttribute('iri', iri);
|
|
112
|
+
span.title = iri;
|
|
113
|
+
span.textContent = label;
|
|
114
|
+
link.replaceWith(span);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const href = resolveWikiHref(iri, wikilinkIndex, iriAliasIndex);
|
|
118
|
+
if (href) {
|
|
119
|
+
link.href = href;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const span = document.createElement('span');
|
|
123
|
+
span.className = 'wikilink';
|
|
124
|
+
span.setAttribute('iri', iri);
|
|
125
|
+
span.title = iri;
|
|
126
|
+
span.textContent = label;
|
|
127
|
+
link.replaceWith(span);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function installWikilinkObserver(
|
|
132
|
+
wikilinkIndex: Record<string, string>,
|
|
133
|
+
iriAliasIndex: Record<string, string>,
|
|
134
|
+
linkingEnabled: boolean
|
|
135
|
+
): void {
|
|
136
|
+
const observer = new MutationObserver((mutations) => {
|
|
137
|
+
for (const mutation of mutations) {
|
|
138
|
+
for (const added of Array.from(mutation.addedNodes)) {
|
|
139
|
+
if (!(added instanceof Element)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (added.matches('a.wikilink[iri]')) {
|
|
143
|
+
applyWikilinks(added.parentElement ?? document, wikilinkIndex, iriAliasIndex, linkingEnabled);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (added.querySelector('a.wikilink[iri]')) {
|
|
147
|
+
applyWikilinks(added, wikilinkIndex, iriAliasIndex, linkingEnabled);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function setupDownloadHandler(): void {
|
|
156
|
+
window.addEventListener('md-download-file', (event: Event) => {
|
|
157
|
+
const detail = (event as CustomEvent<{ filename?: string; content?: string }>).detail;
|
|
158
|
+
if (!detail || typeof detail.filename !== 'string') {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const blob = new Blob([detail.content ?? ''], { type: 'text/plain;charset=utf-8' });
|
|
162
|
+
const anchor = document.createElement('a');
|
|
163
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
164
|
+
anchor.href = objectUrl;
|
|
165
|
+
anchor.download = detail.filename;
|
|
166
|
+
anchor.style.display = 'none';
|
|
167
|
+
document.body.appendChild(anchor);
|
|
168
|
+
anchor.click();
|
|
169
|
+
anchor.remove();
|
|
170
|
+
URL.revokeObjectURL(objectUrl);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getMdKindFromCodeElement(code: Element): string | undefined {
|
|
175
|
+
for (const className of Array.from(code.classList)) {
|
|
176
|
+
if (!className.startsWith('language-')) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const language = className.slice('language-'.length).toLowerCase();
|
|
180
|
+
if (SUPPORTED.has(language)) {
|
|
181
|
+
return language;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function loadResult(
|
|
188
|
+
manifestEntry: ManifestEntry,
|
|
189
|
+
inlineResults: Record<string, unknown>
|
|
190
|
+
): Promise<unknown> {
|
|
191
|
+
if (manifestEntry.blockId && Object.prototype.hasOwnProperty.call(inlineResults, manifestEntry.blockId)) {
|
|
192
|
+
return inlineResults[manifestEntry.blockId];
|
|
193
|
+
}
|
|
194
|
+
const response = await fetch(manifestEntry.path);
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
return { status: 'error', message: `Failed to load block result: ${manifestEntry.path}` };
|
|
197
|
+
}
|
|
198
|
+
return await response.json();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function applyExecutionResults(): Promise<void> {
|
|
202
|
+
const inlineResults = parseJsonNode<Record<string, unknown>>('oml-md-block-inline-results', {});
|
|
203
|
+
const wikilinkIndex = parseJsonNode<Record<string, string>>('oml-md-wikilink-index', {});
|
|
204
|
+
const iriAliasIndex = parseJsonNode<Record<string, string>>('oml-md-wikilink-iri-aliases', {});
|
|
205
|
+
const wikilinkConfig = parseJsonNode<WikilinkConfig>('oml-md-wikilink-config', { linkingEnabled: false });
|
|
206
|
+
const linkingEnabled = Boolean(wikilinkConfig?.linkingEnabled);
|
|
207
|
+
// Resolve wikilinks in regular markdown content (outside code-block panels).
|
|
208
|
+
applyWikilinks(document, wikilinkIndex, iriAliasIndex, linkingEnabled);
|
|
209
|
+
// Re-apply wikilinks for dynamic renderer re-renders (tables, filters, paging, etc.).
|
|
210
|
+
installWikilinkObserver(wikilinkIndex, iriAliasIndex, linkingEnabled);
|
|
211
|
+
|
|
212
|
+
const manifest = parseJsonNode<ManifestEntry[]>('oml-md-block-manifest', []);
|
|
213
|
+
if (!Array.isArray(manifest) || manifest.length === 0) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const codeNodes = Array.from(document.querySelectorAll('pre > code'))
|
|
218
|
+
.filter((code) => getMdKindFromCodeElement(code) !== undefined);
|
|
219
|
+
if (codeNodes.length === 0) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const registry = createMarkdownRendererRegistry();
|
|
224
|
+
for (let index = 0; index < codeNodes.length && index < manifest.length; index += 1) {
|
|
225
|
+
const code = codeNodes[index];
|
|
226
|
+
const pre = code.parentElement;
|
|
227
|
+
if (!pre) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const result = await loadResult(manifest[index], inlineResults);
|
|
232
|
+
const panel = registry.render(result as any);
|
|
233
|
+
// Resolve wikilinks newly introduced by dynamic block renderers.
|
|
234
|
+
applyWikilinks(panel, wikilinkIndex, iriAliasIndex, linkingEnabled);
|
|
235
|
+
pre.replaceWith(panel);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
const panel = document.createElement('div');
|
|
238
|
+
panel.className = 'oml-md-result oml-md-result-error';
|
|
239
|
+
const message = document.createElement('div');
|
|
240
|
+
message.className = 'oml-md-result-message';
|
|
241
|
+
message.textContent = `Render error: ${error instanceof Error ? error.message : String(error)}`;
|
|
242
|
+
panel.appendChild(message);
|
|
243
|
+
pre.replaceWith(panel);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
setupDownloadHandler();
|
|
249
|
+
void applyExecutionResults();
|