mdld-parse 0.7.1 → 0.7.3
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/package.json +4 -1
- package/src/generate.js +2 -1
- package/src/index.js +1 -1
- package/src/merge.js +11 -3
- package/src/parse.js +246 -72
- package/src/render.js +345 -345
- package/src/shared.js +212 -0
- package/src/utils.js +2 -9
package/src/render.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { parse } from './parse.js';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
hash
|
|
3
|
+
DataFactory,
|
|
4
|
+
expandIRI,
|
|
5
|
+
shortenIRI,
|
|
6
|
+
parseSemanticBlock,
|
|
7
|
+
hash
|
|
9
8
|
} from './utils.js';
|
|
9
|
+
import { DEFAULT_CONTEXT } from './shared.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Render MD-LD to HTML+RDFa
|
|
@@ -19,472 +19,472 @@ import {
|
|
|
19
19
|
* @returns {Object} Render result with HTML and metadata
|
|
20
20
|
*/
|
|
21
21
|
export function render(mdld, options = {}) {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
// Phase 1: Parse MD-LD (reuse parser)
|
|
23
|
+
const parsed = parse(mdld, { context: options.context || {} });
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// Phase 2: Build render state
|
|
26
|
+
const state = buildRenderState(parsed, options, mdld);
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Phase 3: Render blocks to HTML
|
|
29
|
+
const html = renderBlocks(parsed.origin.blocks, state);
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// Phase 4: Wrap with RDFa context
|
|
32
|
+
const wrapped = wrapWithRDFaContext(html, state.ctx);
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
return {
|
|
35
|
+
html: wrapped,
|
|
36
|
+
context: state.ctx,
|
|
37
|
+
metadata: {
|
|
38
|
+
blockCount: parsed.origin.blocks.size,
|
|
39
|
+
quadCount: parsed.quads.length,
|
|
40
|
+
renderTime: Date.now()
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
46
|
* Build render state following parser pattern
|
|
47
47
|
*/
|
|
48
48
|
function buildRenderState(parsed, options, mdld) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
49
|
+
// Use the parser's context which already includes document prefixes
|
|
50
|
+
const ctx = parsed.context || { ...DEFAULT_CONTEXT, ...(options.context || {}) };
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
ctx,
|
|
54
|
+
df: options.dataFactory || DataFactory,
|
|
55
|
+
baseIRI: options.baseIRI || '',
|
|
56
|
+
sourceText: mdld, // Store original text for content extraction
|
|
57
|
+
output: [],
|
|
58
|
+
currentSubject: null,
|
|
59
|
+
documentSubject: null,
|
|
60
|
+
blockStack: [],
|
|
61
|
+
carrierStack: []
|
|
62
|
+
};
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Render blocks to HTML with RDFa annotations
|
|
67
67
|
*/
|
|
68
68
|
function renderBlocks(blocks, state) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
69
|
+
// Sort blocks by position
|
|
70
|
+
const sortedBlocks = Array.from(blocks.values()).sort((a, b) => {
|
|
71
|
+
return (a.range?.start || 0) - (b.range?.start || 0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Separate list blocks from other blocks
|
|
75
|
+
const listBlocks = sortedBlocks.filter(block => block.carrierType === 'list');
|
|
76
|
+
const otherBlocks = sortedBlocks.filter(block => block.carrierType !== 'list');
|
|
77
|
+
|
|
78
|
+
// Render non-list blocks normally
|
|
79
|
+
otherBlocks.forEach(block => {
|
|
80
|
+
renderBlock(block, state);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Render lists using Markdown approach with RDFa enrichment
|
|
84
|
+
if (listBlocks.length > 0) {
|
|
85
|
+
renderListsWithRDFa(listBlocks, state);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return state.output.join('');
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
* Render lists using Markdown structure with RDFa enrichment
|
|
93
93
|
*/
|
|
94
94
|
function renderListsWithRDFa(listBlocks, state) {
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
// Group list blocks by their context (consecutive blocks with similar context)
|
|
96
|
+
const listGroups = groupListBlocksByContext(listBlocks, state.sourceText);
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
listGroups.forEach(group => {
|
|
99
|
+
renderListGroup(group, state);
|
|
100
|
+
});
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
104
|
* Group list blocks by their structural hierarchy
|
|
105
105
|
*/
|
|
106
106
|
function groupListBlocksByContext(listBlocks, sourceText) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
107
|
+
const groups = [];
|
|
108
|
+
|
|
109
|
+
// Group consecutive list blocks
|
|
110
|
+
let currentGroup = null;
|
|
111
|
+
|
|
112
|
+
for (const block of listBlocks) {
|
|
113
|
+
// Start new group for each top-level item (indent 0)
|
|
114
|
+
const indent = getIndentLevel(block, sourceText);
|
|
115
|
+
|
|
116
|
+
if (indent === 0) {
|
|
117
|
+
// Close previous group
|
|
118
|
+
if (currentGroup) {
|
|
119
|
+
groups.push(currentGroup);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Start new group with a generic name
|
|
123
|
+
currentGroup = {
|
|
124
|
+
contextName: 'Items',
|
|
125
|
+
blocks: [block]
|
|
126
|
+
};
|
|
127
|
+
} else {
|
|
128
|
+
// Add nested items to current group
|
|
129
|
+
if (currentGroup) {
|
|
130
|
+
currentGroup.blocks.push(block);
|
|
131
|
+
} else {
|
|
132
|
+
// This shouldn't happen, but handle it
|
|
133
|
+
currentGroup = {
|
|
134
|
+
contextName: 'Items',
|
|
135
|
+
blocks: [block]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
115
140
|
|
|
116
|
-
if (
|
|
117
|
-
// Close previous group
|
|
118
|
-
if (currentGroup) {
|
|
141
|
+
if (currentGroup) {
|
|
119
142
|
groups.push(currentGroup);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Start new group with a generic name
|
|
123
|
-
currentGroup = {
|
|
124
|
-
contextName: 'Items',
|
|
125
|
-
blocks: [block]
|
|
126
|
-
};
|
|
127
|
-
} else {
|
|
128
|
-
// Add nested items to current group
|
|
129
|
-
if (currentGroup) {
|
|
130
|
-
currentGroup.blocks.push(block);
|
|
131
|
-
} else {
|
|
132
|
-
// This shouldn't happen, but handle it
|
|
133
|
-
currentGroup = {
|
|
134
|
-
contextName: 'Items',
|
|
135
|
-
blocks: [block]
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
143
|
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (currentGroup) {
|
|
142
|
-
groups.push(currentGroup);
|
|
143
|
-
}
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
return groups;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/**
|
|
149
149
|
* Render a list group with proper Markdown structure and RDFa enrichment
|
|
150
150
|
*/
|
|
151
151
|
function renderListGroup(group, state) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
// Extract the list anchor text from the first block's position
|
|
153
|
+
const firstBlock = group.blocks[0];
|
|
154
|
+
const listAnchorText = extractListAnchorText(firstBlock, state.sourceText);
|
|
155
155
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
// Render the list anchor as a paragraph if it exists
|
|
157
|
+
if (listAnchorText) {
|
|
158
|
+
state.output.push(`<p>${escapeHtml(listAnchorText)}</p>`);
|
|
159
|
+
}
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
// Render the list directly without the semantic-list wrapper
|
|
162
|
+
state.output.push(`<ul>`);
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
// Render list items preserving Markdown structure
|
|
165
|
+
const markdownList = group.blocks.map(block =>
|
|
166
|
+
state.sourceText.substring(block.range.start, block.range.end)
|
|
167
|
+
).join('\n');
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
// Parse markdown list and enrich with RDFa
|
|
170
|
+
const htmlList = parseMarkdownList(markdownList, group.blocks, state);
|
|
171
|
+
state.output.push(htmlList);
|
|
172
172
|
|
|
173
|
-
|
|
173
|
+
state.output.push(`</ul>`);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
/**
|
|
177
177
|
* Extract list anchor text (the paragraph before the list)
|
|
178
178
|
*/
|
|
179
179
|
function extractListAnchorText(firstBlock, sourceText) {
|
|
180
|
-
|
|
180
|
+
if (!firstBlock.range || !sourceText) return null;
|
|
181
181
|
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
// Look backwards from the first list item to find the list anchor
|
|
183
|
+
const startPos = firstBlock.range.start;
|
|
184
184
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
// Search backwards for a line that has semantic annotation but no value carrier
|
|
186
|
+
let searchPos = startPos;
|
|
187
|
+
let foundAnchor = null;
|
|
188
188
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
189
|
+
while (searchPos > 0 && !foundAnchor) {
|
|
190
|
+
// Find the start of the current line
|
|
191
|
+
let lineStart = searchPos - 1;
|
|
192
|
+
while (lineStart > 0 && sourceText[lineStart - 1] !== '\n') {
|
|
193
|
+
lineStart--;
|
|
194
|
+
}
|
|
195
195
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
196
|
+
// Extract the line
|
|
197
|
+
let lineEnd = searchPos;
|
|
198
|
+
while (lineEnd < sourceText.length && sourceText[lineEnd] !== '\n') {
|
|
199
|
+
lineEnd++;
|
|
200
|
+
}
|
|
201
201
|
|
|
202
|
-
|
|
202
|
+
const line = sourceText.substring(lineStart, lineEnd).trim();
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
204
|
+
// Check if this looks like a list anchor (has semantic annotation but no value carrier)
|
|
205
|
+
if (line.includes('{') && !line.match(/^-\s/)) {
|
|
206
|
+
foundAnchor = line;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
209
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
210
|
+
// Continue searching backwards
|
|
211
|
+
searchPos = lineStart - 1;
|
|
212
|
+
}
|
|
213
213
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
214
|
+
if (foundAnchor) {
|
|
215
|
+
// Clean the line by removing MD-LD annotations
|
|
216
|
+
const cleanLine = foundAnchor.replace(/\s*\{[^}]+\}\s*$/, '');
|
|
217
|
+
return cleanLine;
|
|
218
|
+
}
|
|
219
219
|
|
|
220
|
-
|
|
220
|
+
return null;
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
/**
|
|
224
224
|
* Parse markdown list and enrich with RDFa attributes
|
|
225
225
|
*/
|
|
226
226
|
function parseMarkdownList(markdownList, blocks, state) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
227
|
+
const lines = markdownList.split('\n').filter(line => line.trim());
|
|
228
|
+
let html = '';
|
|
229
|
+
let currentLevel = 0;
|
|
230
|
+
let openLi = false;
|
|
231
|
+
|
|
232
|
+
lines.forEach((line, index) => {
|
|
233
|
+
const indent = line.match(/^(\s*)/)[1].length;
|
|
234
|
+
const content = line.trim();
|
|
235
|
+
|
|
236
|
+
if (content.startsWith('-')) {
|
|
237
|
+
const level = Math.floor(indent / 2); // 2 spaces per level
|
|
238
|
+
const itemContent = content.substring(1).trim();
|
|
239
|
+
|
|
240
|
+
// Find corresponding block for RDFa attributes
|
|
241
|
+
// Try exact match first, then try without MD-LD annotations
|
|
242
|
+
const cleanLine = itemContent.replace(/\s*\{[^}]+\}\s*$/, '');
|
|
243
|
+
let block = blocks.find(b =>
|
|
244
|
+
b.range && state.sourceText.substring(b.range.start, b.range.end).trim() === line
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// If no exact match, try matching by clean content
|
|
248
|
+
if (!block) {
|
|
249
|
+
block = blocks.find(b => {
|
|
250
|
+
if (!b.range) return false;
|
|
251
|
+
const blockText = state.sourceText.substring(b.range.start, b.range.end).trim();
|
|
252
|
+
const blockCleanContent = blockText.replace(/^-\s*/, '').replace(/\s*\{[^}]+\}\s*$/, '');
|
|
253
|
+
return blockCleanContent === cleanLine;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Clean content by removing MD-LD annotations
|
|
258
|
+
const cleanContent = itemContent.replace(/\s*\{[^}]+\}\s*$/, '');
|
|
259
|
+
|
|
260
|
+
// Close lists if going to a higher level
|
|
261
|
+
while (currentLevel > level) {
|
|
262
|
+
if (openLi) {
|
|
263
|
+
html += '</li>';
|
|
264
|
+
openLi = false;
|
|
265
|
+
}
|
|
266
|
+
html += '</ul>';
|
|
267
|
+
currentLevel--;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Open lists if going deeper
|
|
271
|
+
while (currentLevel < level) {
|
|
272
|
+
if (openLi) {
|
|
273
|
+
html += '<ul>';
|
|
274
|
+
openLi = false;
|
|
275
|
+
} else {
|
|
276
|
+
html += '<ul>';
|
|
277
|
+
}
|
|
278
|
+
currentLevel++;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Close previous li if open
|
|
282
|
+
if (openLi) {
|
|
283
|
+
html += '</li>';
|
|
284
|
+
openLi = false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const attrs = block ? buildRDFaAttrsFromBlock(block, state.ctx) : '';
|
|
288
|
+
html += `<li${attrs}>${escapeHtml(cleanContent)}`;
|
|
289
|
+
openLi = true;
|
|
265
290
|
}
|
|
266
|
-
|
|
267
|
-
currentLevel--;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Open lists if going deeper
|
|
271
|
-
while (currentLevel < level) {
|
|
272
|
-
if (openLi) {
|
|
273
|
-
html += '<ul>';
|
|
274
|
-
openLi = false;
|
|
275
|
-
} else {
|
|
276
|
-
html += '<ul>';
|
|
277
|
-
}
|
|
278
|
-
currentLevel++;
|
|
279
|
-
}
|
|
291
|
+
});
|
|
280
292
|
|
|
281
|
-
|
|
282
|
-
|
|
293
|
+
// Close any remaining open li and lists
|
|
294
|
+
if (openLi) {
|
|
283
295
|
html += '</li>';
|
|
284
|
-
openLi = false;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const attrs = block ? buildRDFaAttrsFromBlock(block, state.ctx) : '';
|
|
288
|
-
html += `<li${attrs}>${escapeHtml(cleanContent)}`;
|
|
289
|
-
openLi = true;
|
|
290
296
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
while (currentLevel > 0) {
|
|
298
|
-
html += '</ul>';
|
|
299
|
-
currentLevel--;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return html;
|
|
297
|
+
while (currentLevel > 0) {
|
|
298
|
+
html += '</ul>';
|
|
299
|
+
currentLevel--;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return html;
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
/**
|
|
306
306
|
* Get indent level from source text
|
|
307
307
|
*/
|
|
308
308
|
function getIndentLevel(block, sourceText) {
|
|
309
|
-
|
|
309
|
+
if (!block.range || !sourceText) return 0;
|
|
310
310
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
311
|
+
const text = sourceText.substring(block.range.start, block.range.end);
|
|
312
|
+
const indentMatch = text.match(/^(\s*)/);
|
|
313
|
+
return indentMatch ? indentMatch[1].length : 0;
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
/**
|
|
317
317
|
* Render a single block
|
|
318
318
|
*/
|
|
319
319
|
function renderBlock(block, state) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
320
|
+
const attrs = buildRDFaAttrsFromBlock(block, state.ctx);
|
|
321
|
+
|
|
322
|
+
switch (block.type || block.carrierType) {
|
|
323
|
+
case 'heading':
|
|
324
|
+
const level = block.text ? block.text.match(/^#+/)?.[0]?.length || 1 : 1;
|
|
325
|
+
const tag = `h${level}`;
|
|
326
|
+
state.output.push(`<${tag}${attrs}>`);
|
|
327
|
+
renderBlockContent(block, state);
|
|
328
|
+
state.output.push(`</${tag}>`);
|
|
329
|
+
break;
|
|
330
|
+
|
|
331
|
+
case 'para':
|
|
332
|
+
state.output.push(`<p${attrs}>`);
|
|
333
|
+
renderBlockContent(block, state);
|
|
334
|
+
state.output.push(`</p>`);
|
|
335
|
+
break;
|
|
336
|
+
|
|
337
|
+
case 'list':
|
|
338
|
+
// List blocks are handled separately in renderListsWithRDFa
|
|
339
|
+
break;
|
|
340
|
+
|
|
341
|
+
case 'quote':
|
|
342
|
+
state.output.push(`<blockquote${attrs}>`);
|
|
343
|
+
renderBlockContent(block, state);
|
|
344
|
+
state.output.push(`</blockquote>`);
|
|
345
|
+
break;
|
|
346
|
+
|
|
347
|
+
case 'code':
|
|
348
|
+
const language = block.info || '';
|
|
349
|
+
state.output.push(`<pre><code${attrs}${language ? ` class="language-${escapeHtml(language)}"` : ''}>`);
|
|
350
|
+
state.output.push(escapeHtml(block.text || ''));
|
|
351
|
+
state.output.push(`</code></pre>`);
|
|
352
|
+
break;
|
|
353
|
+
|
|
354
|
+
default:
|
|
355
|
+
// Default rendering as paragraph
|
|
356
|
+
state.output.push(`<div${attrs}>`);
|
|
357
|
+
renderBlockContent(block, state);
|
|
358
|
+
state.output.push(`</div>`);
|
|
359
|
+
}
|
|
360
360
|
}
|
|
361
361
|
|
|
362
362
|
/**
|
|
363
363
|
* Render block content with inline carriers
|
|
364
364
|
*/
|
|
365
365
|
function renderBlockContent(block, state) {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
366
|
+
// Extract text from source using range information
|
|
367
|
+
if (block.range && state.sourceText) {
|
|
368
|
+
let text = state.sourceText.substring(block.range.start, block.range.end);
|
|
369
|
+
|
|
370
|
+
// Remove semantic block annotations from the text
|
|
371
|
+
if (block.attrsRange) {
|
|
372
|
+
const beforeAttrs = text.substring(0, block.attrsRange.start - block.range.start);
|
|
373
|
+
const afterAttrs = text.substring(block.attrsRange.end - block.range.start);
|
|
374
|
+
text = beforeAttrs + afterAttrs;
|
|
375
|
+
}
|
|
376
376
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
377
|
+
// For headings, extract text content from the heading
|
|
378
|
+
if (block.carrierType === 'heading') {
|
|
379
|
+
// Remove heading markers (#) and trim
|
|
380
|
+
const content = text.replace(/^#+\s*/, '').trim();
|
|
381
|
+
state.output.push(escapeHtml(content));
|
|
382
|
+
} else {
|
|
383
|
+
state.output.push(escapeHtml(text.trim()));
|
|
384
|
+
}
|
|
384
385
|
}
|
|
385
|
-
}
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
/**
|
|
389
389
|
* Build RDFa attributes from block
|
|
390
390
|
*/
|
|
391
391
|
function buildRDFaAttrsFromBlock(block, ctx) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
// Subject
|
|
395
|
-
if (block.subject && block.subject !== 'RESET' && !block.subject.startsWith('=#') && !block.subject.startsWith('+')) {
|
|
396
|
-
const expanded = expandIRI(block.subject, ctx);
|
|
397
|
-
const shortened = shortenIRI(expanded, ctx);
|
|
398
|
-
attrs.push(`about="${escapeHtml(shortened)}"`);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Types
|
|
402
|
-
if (block.types && block.types.length > 0) {
|
|
403
|
-
const types = block.types.map(t => {
|
|
404
|
-
const iri = typeof t === 'string' ? t : t.iri;
|
|
405
|
-
const expanded = expandIRI(iri, ctx);
|
|
406
|
-
return shortenIRI(expanded, ctx);
|
|
407
|
-
}).join(' ');
|
|
408
|
-
attrs.push(`typeof="${escapeHtml(types)}"`);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Predicates
|
|
412
|
-
if (block.predicates && block.predicates.length > 0) {
|
|
413
|
-
const literalProps = [];
|
|
414
|
-
const objectProps = [];
|
|
415
|
-
const reverseProps = [];
|
|
416
|
-
|
|
417
|
-
block.predicates.forEach(pred => {
|
|
418
|
-
const iri = typeof pred === 'string' ? pred : pred.iri;
|
|
419
|
-
const expanded = expandIRI(iri, ctx);
|
|
420
|
-
const shortened = shortenIRI(expanded, ctx);
|
|
421
|
-
const form = typeof pred === 'string' ? '' : (pred.form || '');
|
|
422
|
-
|
|
423
|
-
if (form === '!') {
|
|
424
|
-
reverseProps.push(shortened);
|
|
425
|
-
} else if (form === '?') {
|
|
426
|
-
objectProps.push(shortened);
|
|
427
|
-
} else {
|
|
428
|
-
literalProps.push(shortened);
|
|
429
|
-
}
|
|
430
|
-
});
|
|
392
|
+
const attrs = [];
|
|
431
393
|
|
|
432
|
-
|
|
433
|
-
|
|
394
|
+
// Subject
|
|
395
|
+
if (block.subject && block.subject !== 'RESET' && !block.subject.startsWith('=#') && !block.subject.startsWith('+')) {
|
|
396
|
+
const expanded = expandIRI(block.subject, ctx);
|
|
397
|
+
const shortened = shortenIRI(expanded, ctx);
|
|
398
|
+
attrs.push(`about="${escapeHtml(shortened)}"`);
|
|
434
399
|
}
|
|
435
|
-
|
|
436
|
-
|
|
400
|
+
|
|
401
|
+
// Types
|
|
402
|
+
if (block.types && block.types.length > 0) {
|
|
403
|
+
const types = block.types.map(t => {
|
|
404
|
+
const iri = typeof t === 'string' ? t : t.iri;
|
|
405
|
+
const expanded = expandIRI(iri, ctx);
|
|
406
|
+
return shortenIRI(expanded, ctx);
|
|
407
|
+
}).join(' ');
|
|
408
|
+
attrs.push(`typeof="${escapeHtml(types)}"`);
|
|
437
409
|
}
|
|
438
|
-
|
|
439
|
-
|
|
410
|
+
|
|
411
|
+
// Predicates
|
|
412
|
+
if (block.predicates && block.predicates.length > 0) {
|
|
413
|
+
const literalProps = [];
|
|
414
|
+
const objectProps = [];
|
|
415
|
+
const reverseProps = [];
|
|
416
|
+
|
|
417
|
+
block.predicates.forEach(pred => {
|
|
418
|
+
const iri = typeof pred === 'string' ? pred : pred.iri;
|
|
419
|
+
const expanded = expandIRI(iri, ctx);
|
|
420
|
+
const shortened = shortenIRI(expanded, ctx);
|
|
421
|
+
const form = typeof pred === 'string' ? '' : (pred.form || '');
|
|
422
|
+
|
|
423
|
+
if (form === '!') {
|
|
424
|
+
reverseProps.push(shortened);
|
|
425
|
+
} else if (form === '?') {
|
|
426
|
+
objectProps.push(shortened);
|
|
427
|
+
} else {
|
|
428
|
+
literalProps.push(shortened);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
if (literalProps.length > 0) {
|
|
433
|
+
attrs.push(`property="${escapeHtml(literalProps.join(' '))}"`);
|
|
434
|
+
}
|
|
435
|
+
if (objectProps.length > 0) {
|
|
436
|
+
attrs.push(`rel="${escapeHtml(objectProps.join(' '))}"`);
|
|
437
|
+
}
|
|
438
|
+
if (reverseProps.length > 0) {
|
|
439
|
+
attrs.push(`rev="${escapeHtml(reverseProps.join(' '))}"`);
|
|
440
|
+
}
|
|
440
441
|
}
|
|
441
|
-
}
|
|
442
442
|
|
|
443
|
-
|
|
443
|
+
return attrs.length > 0 ? ` ${attrs.join(' ')}` : '';
|
|
444
444
|
}
|
|
445
445
|
|
|
446
446
|
/**
|
|
447
447
|
* Generate prefix declarations for RDFa
|
|
448
448
|
*/
|
|
449
449
|
function generatePrefixDeclarations(ctx) {
|
|
450
|
-
|
|
450
|
+
const prefixes = [];
|
|
451
451
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
452
|
+
for (const [prefix, iri] of Object.entries(ctx)) {
|
|
453
|
+
if (prefix !== '@vocab') {
|
|
454
|
+
prefixes.push(`${prefix}: ${iri}`);
|
|
455
|
+
}
|
|
455
456
|
}
|
|
456
|
-
}
|
|
457
457
|
|
|
458
|
-
|
|
458
|
+
return prefixes.length > 0 ? ` prefix="${prefixes.join(' ')}"` : '';
|
|
459
459
|
}
|
|
460
460
|
|
|
461
461
|
/**
|
|
462
462
|
* Generate vocabulary declaration
|
|
463
463
|
*/
|
|
464
464
|
function generateVocabDeclaration(ctx) {
|
|
465
|
-
|
|
465
|
+
return ctx['@vocab'] ? ` vocab="${ctx['@vocab']}"` : '';
|
|
466
466
|
}
|
|
467
467
|
|
|
468
468
|
/**
|
|
469
469
|
* Wrap HTML with RDFa context declarations
|
|
470
470
|
*/
|
|
471
471
|
function wrapWithRDFaContext(html, ctx) {
|
|
472
|
-
|
|
473
|
-
|
|
472
|
+
const prefixDecl = generatePrefixDeclarations(ctx);
|
|
473
|
+
const vocabDecl = generateVocabDeclaration(ctx);
|
|
474
474
|
|
|
475
|
-
|
|
475
|
+
return `<div${prefixDecl}${vocabDecl}>${html}</div>`;
|
|
476
476
|
}
|
|
477
477
|
|
|
478
478
|
/**
|
|
479
479
|
* Escape HTML special characters
|
|
480
480
|
*/
|
|
481
481
|
function escapeHtml(text) {
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
482
|
+
const map = {
|
|
483
|
+
'&': '&',
|
|
484
|
+
'<': '<',
|
|
485
|
+
'>': '>',
|
|
486
|
+
'"': '"',
|
|
487
|
+
"'": '''
|
|
488
|
+
};
|
|
489
|
+
return String(text || '').replace(/[&<>"']/g, m => map[m]);
|
|
490
490
|
}
|