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