mdld-parse 0.7.2 → 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/src/render.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { parse } from './parse.js';
2
2
  import {
3
- DEFAULT_CONTEXT,
4
- DataFactory,
5
- expandIRI,
6
- shortenIRI,
7
- parseSemanticBlock,
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
- // Phase 1: Parse MD-LD (reuse parser)
23
- const parsed = parse(mdld, { context: options.context || {} });
22
+ // Phase 1: Parse MD-LD (reuse parser)
23
+ const parsed = parse(mdld, { context: options.context || {} });
24
24
 
25
- // Phase 2: Build render state
26
- const state = buildRenderState(parsed, options, mdld);
25
+ // Phase 2: Build render state
26
+ const state = buildRenderState(parsed, options, mdld);
27
27
 
28
- // Phase 3: Render blocks to HTML
29
- const html = renderBlocks(parsed.origin.blocks, state);
28
+ // Phase 3: Render blocks to HTML
29
+ const html = renderBlocks(parsed.origin.blocks, state);
30
30
 
31
- // Phase 4: Wrap with RDFa context
32
- const wrapped = wrapWithRDFaContext(html, state.ctx);
31
+ // Phase 4: Wrap with RDFa context
32
+ const wrapped = wrapWithRDFaContext(html, state.ctx);
33
33
 
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
- };
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
- // 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
- };
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
- // 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('');
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
- // Group list blocks by their context (consecutive blocks with similar context)
96
- const listGroups = groupListBlocksByContext(listBlocks, state.sourceText);
95
+ // Group list blocks by their context (consecutive blocks with similar context)
96
+ const listGroups = groupListBlocksByContext(listBlocks, state.sourceText);
97
97
 
98
- listGroups.forEach(group => {
99
- renderListGroup(group, state);
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
- 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);
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 (indent === 0) {
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
- return groups;
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
- // Extract the list anchor text from the first block's position
153
- const firstBlock = group.blocks[0];
154
- const listAnchorText = extractListAnchorText(firstBlock, state.sourceText);
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
- // Render the list anchor as a paragraph if it exists
157
- if (listAnchorText) {
158
- state.output.push(`<p>${escapeHtml(listAnchorText)}</p>`);
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
- // Render the list directly without the semantic-list wrapper
162
- state.output.push(`<ul>`);
161
+ // Render the list directly without the semantic-list wrapper
162
+ state.output.push(`<ul>`);
163
163
 
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');
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
- // Parse markdown list and enrich with RDFa
170
- const htmlList = parseMarkdownList(markdownList, group.blocks, state);
171
- state.output.push(htmlList);
169
+ // Parse markdown list and enrich with RDFa
170
+ const htmlList = parseMarkdownList(markdownList, group.blocks, state);
171
+ state.output.push(htmlList);
172
172
 
173
- state.output.push(`</ul>`);
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
- if (!firstBlock.range || !sourceText) return null;
180
+ if (!firstBlock.range || !sourceText) return null;
181
181
 
182
- // Look backwards from the first list item to find the list anchor
183
- const startPos = firstBlock.range.start;
182
+ // Look backwards from the first list item to find the list anchor
183
+ const startPos = firstBlock.range.start;
184
184
 
185
- // Search backwards for a line that has semantic annotation but no value carrier
186
- let searchPos = startPos;
187
- let foundAnchor = null;
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
- 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
- }
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
- // Extract the line
197
- let lineEnd = searchPos;
198
- while (lineEnd < sourceText.length && sourceText[lineEnd] !== '\n') {
199
- lineEnd++;
200
- }
196
+ // Extract the line
197
+ let lineEnd = searchPos;
198
+ while (lineEnd < sourceText.length && sourceText[lineEnd] !== '\n') {
199
+ lineEnd++;
200
+ }
201
201
 
202
- const line = sourceText.substring(lineStart, lineEnd).trim();
202
+ const line = sourceText.substring(lineStart, lineEnd).trim();
203
203
 
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
- }
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
- // Continue searching backwards
211
- searchPos = lineStart - 1;
212
- }
210
+ // Continue searching backwards
211
+ searchPos = lineStart - 1;
212
+ }
213
213
 
214
- if (foundAnchor) {
215
- // Clean the line by removing MD-LD annotations
216
- const cleanLine = foundAnchor.replace(/\s*\{[^}]+\}\s*$/, '');
217
- return cleanLine;
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
- return null;
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
- 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;
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
- 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
- }
291
+ });
280
292
 
281
- // Close previous li if open
282
- if (openLi) {
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
- // Close any remaining open li and lists
294
- if (openLi) {
295
- html += '</li>';
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
- if (!block.range || !sourceText) return 0;
309
+ if (!block.range || !sourceText) return 0;
310
310
 
311
- const text = sourceText.substring(block.range.start, block.range.end);
312
- const indentMatch = text.match(/^(\s*)/);
313
- return indentMatch ? indentMatch[1].length : 0;
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
- 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
- }
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
- // 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
- }
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
- // 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()));
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
- const attrs = [];
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
- if (literalProps.length > 0) {
433
- attrs.push(`property="${escapeHtml(literalProps.join(' '))}"`);
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
- if (objectProps.length > 0) {
436
- attrs.push(`rel="${escapeHtml(objectProps.join(' '))}"`);
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
- if (reverseProps.length > 0) {
439
- attrs.push(`rev="${escapeHtml(reverseProps.join(' '))}"`);
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
- return attrs.length > 0 ? ` ${attrs.join(' ')}` : '';
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
- const prefixes = [];
450
+ const prefixes = [];
451
451
 
452
- for (const [prefix, iri] of Object.entries(ctx)) {
453
- if (prefix !== '@vocab') {
454
- prefixes.push(`${prefix}: ${iri}`);
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
- return prefixes.length > 0 ? ` prefix="${prefixes.join(' ')}"` : '';
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
- return ctx['@vocab'] ? ` vocab="${ctx['@vocab']}"` : '';
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
- const prefixDecl = generatePrefixDeclarations(ctx);
473
- const vocabDecl = generateVocabDeclaration(ctx);
472
+ const prefixDecl = generatePrefixDeclarations(ctx);
473
+ const vocabDecl = generateVocabDeclaration(ctx);
474
474
 
475
- return `<div${prefixDecl}${vocabDecl}>${html}</div>`;
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
- const map = {
483
- '&': '&amp;',
484
- '<': '&lt;',
485
- '>': '&gt;',
486
- '"': '&quot;',
487
- "'": '&#39;'
488
- };
489
- return String(text || '').replace(/[&<>"']/g, m => map[m]);
482
+ const map = {
483
+ '&': '&amp;',
484
+ '<': '&lt;',
485
+ '>': '&gt;',
486
+ '"': '&quot;',
487
+ "'": '&#39;'
488
+ };
489
+ return String(text || '').replace(/[&<>"']/g, m => map[m]);
490
490
  }