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/src/shared.js ADDED
@@ -0,0 +1,529 @@
1
+ import { DEFAULT_CONTEXT, STANDALONE_SUBJECT_REGEX, FENCE_REGEX, PREFIX_REGEX, HEADING_REGEX, UNORDERED_LIST_REGEX, BLOCKQUOTE_REGEX } from './constants.js';
2
+ import { parseSemanticBlock, expandIRI, shortenIRI } from './utils.js';
3
+
4
+ // Cache for fence regex patterns
5
+ export const FENCE_CLOSE_PATTERNS = new Map();
6
+
7
+ export function getFenceClosePattern(fenceChar) {
8
+ if (!FENCE_CLOSE_PATTERNS.has(fenceChar)) {
9
+ FENCE_CLOSE_PATTERNS.set(fenceChar, new RegExp(`^(${fenceChar}{3,})`));
10
+ }
11
+ return FENCE_CLOSE_PATTERNS.get(fenceChar);
12
+ }
13
+
14
+ // Range calculation utilities - shared between parser and renderer
15
+ export function calcRangeInfo(line, attrs, lineStart, prefixLength, valueLength) {
16
+ const wsLength = prefixLength < line.length && line[prefixLength] === ' ' ? 1 :
17
+ line.slice(prefixLength).match(/^\s+/)?.[0]?.length || 0;
18
+ const valueStartInLine = prefixLength + wsLength;
19
+ return {
20
+ valueRange: [lineStart + valueStartInLine, lineStart + valueStartInLine + valueLength],
21
+ attrsRange: calcAttrsRange(line, attrs, lineStart)
22
+ };
23
+ }
24
+
25
+ export function calcAttrsRange(line, attrs, lineStart) {
26
+ if (!attrs) return null;
27
+ const attrsStartInLine = line.lastIndexOf(attrs);
28
+ return attrsStartInLine >= 0 ? [lineStart + attrsStartInLine, lineStart + attrsStartInLine + attrs.length] : null;
29
+ }
30
+
31
+ // Token creation utilities - shared structure
32
+ export function createToken(type, range, text, attrs = null, attrsRange = null, valueRange = null, extra = {}) {
33
+ const token = { type, range, text, attrs, attrsRange, valueRange, ...extra };
34
+ Object.defineProperty(token, '_carriers', {
35
+ enumerable: false, writable: true, value: null
36
+ });
37
+ return token;
38
+ }
39
+
40
+ export function createCarrier(type, text, attrs, attrsRange, valueRange, range, pos, extra = {}) {
41
+ return { type, text, attrs, attrsRange, valueRange, range, pos, ...extra };
42
+ }
43
+
44
+ // List token creation - shared logic
45
+ export function createListToken(type, line, lineStart, pos, match) {
46
+ const attrs = match[4] || null;
47
+ const prefix = match[1].length + (match[2] ? match[2].length : 0);
48
+ const rangeInfo = calcRangeInfo(line, attrs, lineStart, prefix, match[3].length);
49
+ return createToken(type, [lineStart, pos - 1], match[3].trim(), attrs,
50
+ rangeInfo.attrsRange, rangeInfo.valueRange, { indent: match[1].length });
51
+ }
52
+
53
+ // Semantic block parsing - shared between parser and renderer
54
+ export const semCache = {};
55
+ export const EMPTY_SEM = Object.freeze({ predicates: [], types: [], subject: null });
56
+
57
+ export function parseSemCached(attrs) {
58
+ if (!attrs) return EMPTY_SEM;
59
+ let sem = semCache[attrs];
60
+ if (!sem) {
61
+ sem = Object.freeze(parseSemanticBlock(attrs));
62
+ semCache[attrs] = sem;
63
+ }
64
+ return sem;
65
+ }
66
+
67
+ // Indentation utilities - shared for list processing
68
+ export function getIndentLevel(block, sourceText) {
69
+ if (!block.range || !sourceText) return 0;
70
+
71
+ const text = sourceText.substring(block.range.start, block.range.end);
72
+ const indentMatch = text.match(/^(\s*)/);
73
+ const indentSpaces = indentMatch ? indentMatch[1].length : 0;
74
+
75
+ // CommonMark: 4 spaces or 1 tab = one level
76
+ // We'll use 2 spaces for better readability (configurable)
77
+ return Math.floor(indentSpaces / 2);
78
+ }
79
+
80
+ // Content extraction utilities - shared between parser and renderer
81
+ export function extractContentFromRange(sourceText, range, attrsRange = null) {
82
+ if (!range || !sourceText) return '';
83
+
84
+ let text = sourceText.substring(range[0], range[1]);
85
+
86
+ // Remove MD-LD annotations, preserve content
87
+ if (attrsRange) {
88
+ const beforeAttrs = text.substring(0, attrsRange[0] - range[0]);
89
+ const afterAttrs = text.substring(attrsRange[1] - range[0]);
90
+ text = beforeAttrs + afterAttrs;
91
+ }
92
+
93
+ return text.trim();
94
+ }
95
+
96
+ // List marker utilities - shared for advanced list processing
97
+ export function getListMarker(block, sourceText) {
98
+ if (!block.range) return null;
99
+
100
+ const text = sourceText.substring(block.range.start, block.range.end);
101
+ const markerMatch = text.match(/^(\s*)([-*+]|\d+\[\.|\])\s+/);
102
+
103
+ if (!markerMatch) return null;
104
+
105
+ return {
106
+ type: markerMatch[2].startsWith('-') ? 'dash' :
107
+ markerMatch[2].startsWith('*') ? 'asterisk' :
108
+ markerMatch[2].startsWith('+') ? 'plus' : 'ordered',
109
+ marker: markerMatch[2],
110
+ indent: markerMatch[1].length
111
+ };
112
+ }
113
+
114
+ // CommonMark line processors - shared between parser and renderer
115
+ export const PROCESSORS = [
116
+ { test: line => line.startsWith('```'), process: null }, // Code blocks handled separately
117
+ { test: line => line.startsWith('`'), process: null }, // Code spans handled separately
118
+ { test: line => PREFIX_REGEX.test(line), process: null }, // Prefixes handled separately
119
+ { test: line => HEADING_REGEX.test(line), process: null }, // Headings handled separately
120
+ { test: line => UNORDERED_LIST_REGEX.test(line), process: null }, // Lists handled separately
121
+ { test: line => BLOCKQUOTE_REGEX.test(line), process: null }, // Blockquotes handled separately
122
+ { test: line => STANDALONE_SUBJECT_REGEX.test(line), process: null }, // Standalone subjects handled separately
123
+ { test: line => line.trim() === '', process: null }, // Empty lines handled separately
124
+ { test: line => true, process: null } // Default: paragraph
125
+ ];
126
+
127
+ // Token scanning processors - shared between parser and renderer
128
+ export const TOKEN_PROCESSORS = [
129
+ { type: 'fence', test: line => FENCE_REGEX.test(line.trim()), process: null }, // Will be overridden in parse.js
130
+ { type: 'content', test: line => false, process: null }, // Will be overridden in parse.js
131
+ { type: 'prefix', test: line => PREFIX_REGEX.test(line), process: null }, // Will be overridden in parse.js
132
+ { type: 'heading', test: line => HEADING_REGEX.test(line), process: null }, // Will be overridden in parse.js
133
+ { type: 'list', test: line => UNORDERED_LIST_REGEX.test(line), process: null }, // Will be overridden in parse.js
134
+ { type: 'blockquote', test: line => BLOCKQUOTE_REGEX.test(line), process: null }, // Will be overridden in parse.js
135
+ { type: 'para', test: line => line.trim(), process: null } // Will be overridden in parse.js
136
+ ];
137
+
138
+ // Language and attributes parsing
139
+ export function parseLangAndAttrs(langAndAttrs) {
140
+ const spaceIndex = langAndAttrs.indexOf(' ');
141
+ const braceIndex = langAndAttrs.indexOf('{');
142
+ const langEnd = Math.min(
143
+ spaceIndex > -1 ? spaceIndex : Infinity,
144
+ braceIndex > -1 ? braceIndex : Infinity
145
+ );
146
+ return {
147
+ lang: langAndAttrs.substring(0, langEnd),
148
+ attrsText: langAndAttrs.substring(langEnd).match(/\{[^{}]*\}/)?.[0] || null
149
+ };
150
+ }
151
+
152
+ // Carrier extraction utilities
153
+ export function findMatchingBracket(text, bracketStart) {
154
+ let bracketDepth = 1;
155
+ let bracketEnd = bracketStart + 1;
156
+
157
+ while (bracketEnd < text.length && bracketDepth > 0) {
158
+ if (text[bracketEnd] === '[') bracketDepth++;
159
+ else if (text[bracketEnd] === ']') bracketDepth--;
160
+ bracketEnd++;
161
+ }
162
+
163
+ return bracketDepth > 0 ? null : bracketEnd;
164
+ }
165
+
166
+ export function extractUrlFromBrackets(text, bracketEnd) {
167
+ let url = null;
168
+ let spanEnd = bracketEnd;
169
+
170
+ if (text[spanEnd] === '(') {
171
+ const parenEnd = text.indexOf(')', spanEnd);
172
+ if (parenEnd !== -1) {
173
+ url = text.substring(spanEnd + 1, parenEnd);
174
+ spanEnd = parenEnd + 1;
175
+ }
176
+ }
177
+
178
+ return { url, spanEnd };
179
+ }
180
+
181
+ export function extractAttributesFromText(text, spanEnd, baseOffset) {
182
+ let attrs = null;
183
+ let attrsRange = null;
184
+ const remaining = text.substring(spanEnd);
185
+
186
+ const wsMatch = remaining.match(/^\s+/);
187
+ const attrsStart = wsMatch ? wsMatch[0].length : 0;
188
+
189
+ if (remaining[attrsStart] === '{') {
190
+ const braceEnd = remaining.indexOf('}', attrsStart);
191
+ if (braceEnd !== -1) {
192
+ attrs = remaining.substring(attrsStart, braceEnd + 1);
193
+ const absStart = baseOffset + spanEnd + attrsStart;
194
+ attrsRange = [absStart, absStart + attrs.length];
195
+ spanEnd += braceEnd + 1;
196
+ }
197
+ }
198
+
199
+ return { attrs, attrsRange, finalSpanEnd: spanEnd };
200
+ }
201
+
202
+ export function determineCarrierType(url) {
203
+ if (url && !url.startsWith('=')) {
204
+ return { carrierType: 'link', resourceIRI: url };
205
+ }
206
+ return { carrierType: 'span', resourceIRI: null };
207
+ }
208
+
209
+ export function calcCarrierRanges(match, baseOffset, matchStart) {
210
+ const valueStart = baseOffset + matchStart + match[0].indexOf(match[1]);
211
+ const valueEnd = valueStart + match[1].length;
212
+ const attrsStart = baseOffset + matchStart + match[0].indexOf('{');
213
+ const attrsEnd = attrsStart + match[2].length + 2; // +2 for { and }
214
+ return {
215
+ valueRange: [valueStart, valueEnd],
216
+ attrsRange: [attrsStart + 1, attrsEnd - 1], // Exclude braces
217
+ range: [baseOffset + matchStart, attrsEnd],
218
+ pos: matchStart + match[0].length // pos should be relative to current text, not document
219
+ };
220
+ }
221
+
222
+ // Clean text extraction utilities
223
+ export function extractCleanText(token) {
224
+ if (!token.text) return '';
225
+
226
+ let text = token.text;
227
+
228
+ // Remove semantic annotations
229
+ if (token.attrsRange) {
230
+ const beforeAttrs = text.substring(0, token.attrsRange[0] - (token.range?.[0] || 0));
231
+ const afterAttrs = text.substring(token.attrsRange[1] - (token.range?.[0] || 0));
232
+ text = beforeAttrs + afterAttrs;
233
+ }
234
+
235
+ // Clean based on token type
236
+ switch (token.type) {
237
+ case 'heading':
238
+ return text.replace(/^#+\s*/, '').trim();
239
+ case 'list':
240
+ return text.replace(/^[-*+]\s*/, '').trim();
241
+ case 'blockquote':
242
+ return text.replace(/^>\s*/, '').trim();
243
+ default:
244
+ return text.trim();
245
+ }
246
+ }
247
+
248
+ // Quad emission utilities
249
+ export const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
250
+ export const RDF_STATEMENT = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement';
251
+ export const RDF_SUBJECT = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#subject';
252
+ export const RDF_PREDICATE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#predicate';
253
+ export const RDF_OBJECT = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#object';
254
+
255
+ export function createLeanOriginEntry(block, subject, predicate, meta = null) {
256
+ return {
257
+ blockId: block.id,
258
+ range: block.range,
259
+ carrierType: block.carrierType,
260
+ subject: subject.value,
261
+ predicate: predicate.value,
262
+ context: block.context, // Direct reference instead of spread
263
+ polarity: meta?.remove ? '-' : '+',
264
+ value: block.text || ''
265
+ };
266
+ }
267
+
268
+ // Fragment resolution utilities
269
+ export function resolveFragment(fragment, currentSubject, dataFactory) {
270
+ if (!currentSubject) return null;
271
+ const subjectValue = currentSubject.value;
272
+ const hashIndex = subjectValue.indexOf('#');
273
+ const baseIRI = hashIndex > -1 ? subjectValue.slice(0, hashIndex) : subjectValue;
274
+ return dataFactory.namedNode(baseIRI + '#' + fragment);
275
+ }
276
+
277
+ export function resolveSubject(sem, state) {
278
+ if (!sem.subject) return null;
279
+ if (sem.subject === 'RESET') {
280
+ state.currentSubject = null;
281
+ return null;
282
+ }
283
+ if (sem.subject.startsWith('=#')) return resolveFragment(sem.subject.substring(2), state.currentSubject, state.df);
284
+ return state.df.namedNode(expandIRI(sem.subject, state.ctx));
285
+ }
286
+
287
+ export function resolveObject(sem, state) {
288
+ if (!sem.object) return null;
289
+ if (sem.object.startsWith('#')) return resolveFragment(sem.object.substring(1), state.currentSubject, state.df);
290
+ return state.df.namedNode(expandIRI(sem.object, state.ctx));
291
+ }
292
+
293
+ // HTML escaping - shared utility
294
+ export function escapeHtml(text) {
295
+ if (!text) return '';
296
+ return text
297
+ .replace(/&/g, '&amp;')
298
+ .replace(/</g, '&lt;')
299
+ .replace(/>/g, '&gt;')
300
+ .replace(/"/g, '&quot;')
301
+ .replace(/'/g, '&#x27;');
302
+ }
303
+
304
+ // RDF term type checking utilities - shared across modules
305
+ export function isLiteral(term) {
306
+ return term?.termType === 'Literal';
307
+ }
308
+
309
+ export function isNamedNode(term) {
310
+ return term?.termType === 'NamedNode';
311
+ }
312
+
313
+ export function isRdfType(term) {
314
+ return term?.value === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
315
+ }
316
+
317
+ // IRI prefix extraction utility
318
+ export function getPrefixFromIRI(iri, context) {
319
+ if (!iri) return null;
320
+ const shortened = shortenIRI(iri, context);
321
+ if (shortened.includes(':')) {
322
+ return shortened.split(':')[0];
323
+ }
324
+ return null;
325
+ }
326
+
327
+ // Prefix collection utility - used by generate.js
328
+ export function collectUsedPrefixes(subjectGroups, context) {
329
+ const usedPrefixes = new Set();
330
+
331
+ for (const subjectQuads of subjectGroups.values()) {
332
+ for (const quad of subjectQuads) {
333
+ // Check subject prefix
334
+ const subjectPrefix = getPrefixFromIRI(quad.subject.value, context);
335
+ if (subjectPrefix) usedPrefixes.add(subjectPrefix);
336
+
337
+ // Check predicate prefix
338
+ const predicatePrefix = getPrefixFromIRI(quad.predicate.value, context);
339
+ if (predicatePrefix) usedPrefixes.add(predicatePrefix);
340
+
341
+ // Check object prefix if it's a named node
342
+ if (isNamedNode(quad.object)) {
343
+ const objectPrefix = getPrefixFromIRI(quad.object.value, context);
344
+ if (objectPrefix) usedPrefixes.add(objectPrefix);
345
+ }
346
+
347
+ // Check datatype prefix if present
348
+ if (quad.object.datatype && quad.object.datatype.value) {
349
+ const datatypePrefix = getPrefixFromIRI(quad.object.datatype.value, context);
350
+ if (datatypePrefix) usedPrefixes.add(datatypePrefix);
351
+ }
352
+ }
353
+ }
354
+
355
+ return usedPrefixes;
356
+ }
357
+
358
+ // Token processing utility - eliminates duplication in TOKEN_PROCESSORS
359
+ export function processTokenWithBlockTracking(token, state, processAnnotations, createBlockEntry, additionalProcessors = []) {
360
+ const blockEntry = createBlockEntry(token, state);
361
+ state.currentBlock = blockEntry;
362
+ state.blockStack.push(blockEntry.id);
363
+
364
+ // Run any additional processors first
365
+ additionalProcessors.forEach(processor => processor(token, state));
366
+
367
+ // Process annotations
368
+ processAnnotations(token, state, token.type);
369
+
370
+ state.blockStack.pop();
371
+ state.currentBlock = state.blockStack.length > 0 ?
372
+ state.origin.blocks.get(state.blockStack[state.blockStack.length - 1]) : null;
373
+ }
374
+
375
+ // Quad key generation - shared between parser and renderer
376
+ export function quadIndexKey(subject, predicate, object) {
377
+ const datatype = object.datatype?.value || '';
378
+ const language = object.language || '';
379
+ return `${subject.value}|${predicate.value}|${object.value}|${datatype}|${language}`;
380
+ }
381
+
382
+ // IRI expansion and shortening - shared utilities
383
+ export function expandAndShortenIRI(iri, ctx) {
384
+ const expanded = expandIRI(iri, ctx);
385
+ return shortenIRI(expanded, ctx);
386
+ }
387
+
388
+ // Subject resolution utilities - shared between parser and renderer
389
+ export function resolveSubjectType(subjectDecl) {
390
+ if (!subjectDecl) return 'none';
391
+
392
+ if (subjectDecl.startsWith('=#')) {
393
+ return 'fragment';
394
+ }
395
+
396
+ if (subjectDecl.startsWith('+')) {
397
+ return 'soft-object';
398
+ }
399
+
400
+ if (subjectDecl === 'RESET') {
401
+ return 'reset';
402
+ }
403
+
404
+ return 'full-iri';
405
+ }
406
+
407
+ // Constants - shared across modules (bundle-size optimized)
408
+ export const XSD_STRING = 'http://www.w3.org/2001/XMLSchema#string';
409
+
410
+ // Optimized sorting utilities - inline for better minification
411
+ export function sortQuadsByPredicate(quads) {
412
+ return quads.sort((a, b) => a.predicate.value.localeCompare(b.predicate.value));
413
+ }
414
+
415
+ // Optimized text generation - template literals for smaller bundle
416
+ export const generatePrefixDeclaration = (prefix, namespace) => `[${prefix}] <${namespace}>\n`;
417
+
418
+ export function generateLiteralText(quad, context) {
419
+ const predShort = shortenIRI(quad.predicate.value, context);
420
+ let annotation = predShort;
421
+
422
+ if (quad.object.language) {
423
+ annotation += ` @${quad.object.language}`;
424
+ } else if (quad.object.datatype.value !== XSD_STRING) {
425
+ annotation += ` ^^${shortenIRI(quad.object.datatype.value, context)}`;
426
+ }
427
+
428
+ return `[${quad.object.value}] {${annotation}}\n`;
429
+ }
430
+
431
+ export const generateObjectText = (quad, context) => {
432
+ const objShort = shortenIRI(quad.object.value, context);
433
+ const predShort = shortenIRI(quad.predicate.value, context);
434
+ return `[${objShort}] {+${objShort} ?${predShort}}\n`;
435
+ };
436
+
437
+ // Optimized quad filtering - destructuring for smaller minified output
438
+ export function filterQuadsByType(subjectQuads) {
439
+ const types = [], literals = [], objects = [];
440
+ for (const q of subjectQuads) {
441
+ if (isRdfType(q.predicate)) {
442
+ types.push(q);
443
+ } else if (isLiteral(q.object)) {
444
+ literals.push(q);
445
+ } else if (isNamedNode(q.object)) {
446
+ objects.push(q);
447
+ }
448
+ }
449
+ return { types, literals, objects };
450
+ }
451
+
452
+ // Predicate processing utilities - common RDFa patterns
453
+ export function processPredicates(predicates, ctx) {
454
+ const literalProps = [];
455
+ const objectProps = [];
456
+ const reverseProps = [];
457
+
458
+ predicates.forEach(pred => {
459
+ const iri = typeof pred === 'string' ? pred : pred.iri;
460
+ const expanded = expandIRI(iri, ctx);
461
+ const shortened = shortenIRI(expanded, ctx);
462
+ const form = typeof pred === 'string' ? '' : (pred.form || '');
463
+
464
+ if (form === '!') {
465
+ reverseProps.push(shortened);
466
+ } else if (form === '?') {
467
+ objectProps.push(shortened);
468
+ } else {
469
+ literalProps.push(shortened);
470
+ }
471
+ });
472
+
473
+ return { literalProps, objectProps, reverseProps };
474
+ }
475
+
476
+ // Deterministic sorting utilities - ensure consistent output
477
+ export function sortDeterministic(array, keyFn) {
478
+ return array.sort((a, b) => {
479
+ const keyA = keyFn(a);
480
+ const keyB = keyFn(b);
481
+ return keyA.localeCompare(keyB);
482
+ });
483
+ }
484
+
485
+ export function sortQuadsDeterministically(quads) {
486
+ return quads.sort((a, b) => {
487
+ // Deterministic sorting: subject -> predicate -> object
488
+ const sComp = a.subject.value.localeCompare(b.subject.value);
489
+ if (sComp !== 0) return sComp;
490
+ const pComp = a.predicate.value.localeCompare(b.predicate.value);
491
+ if (pComp !== 0) return pComp;
492
+ const oA = isLiteral(a.object) ? a.object.value : a.object.value;
493
+ const oB = isLiteral(b.object) ? b.object.value : b.object.value;
494
+ return oA.localeCompare(oB);
495
+ });
496
+ }
497
+
498
+ // Optimized deterministic prefix generation
499
+ export function generateDeterministicPrefixes(context, usedPrefixes) {
500
+ const sortedEntries = Object.entries(context).sort(([a], [b]) => a.localeCompare(b));
501
+ let text = '';
502
+
503
+ for (const [prefix, namespace] of sortedEntries) {
504
+ if (prefix !== '@vocab' && !prefix.startsWith('@') && !DEFAULT_CONTEXT[prefix] && usedPrefixes.has(prefix)) {
505
+ text += generatePrefixDeclaration(prefix, namespace);
506
+ }
507
+ }
508
+
509
+ return text;
510
+ }
511
+
512
+ // Memory-efficient block creation
513
+ export function createOptimizedBlockEntry(token, state) {
514
+ const id = hash(`${token.range[0]}-${token.range[1]}-${token.text.slice(0, 50)}`);
515
+ const block = {
516
+ id,
517
+ type: token.type,
518
+ carrierType: token.type,
519
+ range: token.range,
520
+ text: token.text,
521
+ carriers: [],
522
+ predicates: [],
523
+ subject: state.currentSubject,
524
+ context: { ...state.ctx }
525
+ };
526
+
527
+ state.origin.blocks.set(id, block);
528
+ return block;
529
+ }
package/src/utils.js CHANGED
@@ -1,11 +1,4 @@
1
- export const DEFAULT_CONTEXT = {
2
- '@vocab': "http://www.w3.org/2000/01/rdf-schema#",
3
- rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
4
- rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
5
- xsd: 'http://www.w3.org/2001/XMLSchema#',
6
- sh: "http://www.w3.org/ns/shacl#",
7
- prov: 'http://www.w3.org/ns/prov#'
8
- };
1
+ import { URL_REGEX, DEFAULT_CONTEXT } from './constants.js';
9
2
 
10
3
  // Base Term class for RDF/JS compatibility
11
4
  export class Term {
@@ -258,7 +251,7 @@ export function expandIRI(term, ctx) {
258
251
  const t = raw.trim();
259
252
  let result;
260
253
 
261
- if (t.match(/^https?:/)) {
254
+ if (t.match(URL_REGEX)) {
262
255
  result = t;
263
256
  } else if (t.includes(':')) {
264
257
  const [prefix, ref] = t.split(':', 2);