spec-up-t 1.2.9 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/.github/copilot-instructions.md +2 -1
  2. package/assets/compiled/body.js +5 -5
  3. package/assets/compiled/head.css +1 -0
  4. package/assets/css/counter.css +104 -0
  5. package/assets/js/addAnchorsToTerms.js +13 -5
  6. package/assets/js/collapse-definitions.js +0 -3
  7. package/assets/js/custom-elements.js +25 -27
  8. package/assets/js/fix-last-dd.js +6 -3
  9. package/assets/js/highlight-heading-plus-sibling-nodes.js +0 -1
  10. package/assets/js/highlight-heading-plus-sibling-nodes.test.js +120 -0
  11. package/assets/js/insert-trefs.js +32 -28
  12. package/config/asset-map.json +1 -0
  13. package/index.js +33 -227
  14. package/package.json +4 -2
  15. package/sonar-project.properties +7 -0
  16. package/src/collect-external-references.js +22 -11
  17. package/src/collect-external-references.test.js +153 -2
  18. package/src/collectExternalReferences/fetchTermsFromIndex.js +65 -110
  19. package/src/collectExternalReferences/processXTrefsData.js +9 -11
  20. package/src/create-docx.js +332 -0
  21. package/src/create-pdf.js +243 -122
  22. package/src/fix-markdown-files.js +31 -34
  23. package/src/html-dom-processor.js +290 -0
  24. package/src/init.js +3 -0
  25. package/src/install-from-boilerplate/boilerplate/.github/workflows/menu.yml +51 -36
  26. package/src/install-from-boilerplate/boilerplate/spec/example-markup-in-markdown.md +0 -1
  27. package/src/install-from-boilerplate/boilerplate/spec/terms-and-definitions-intro.md +1 -5
  28. package/src/install-from-boilerplate/config-scripts-keys.js +4 -4
  29. package/src/install-from-boilerplate/menu.sh +6 -6
  30. package/src/markdown-it-extensions.js +54 -33
  31. package/src/references.js +18 -6
  32. package/templates/template.html +2 -0
@@ -27,11 +27,11 @@ function insertTrefs(allXTrefs) {
27
27
  * @type {Array<{element: Element, textContent: string, dt: Element, parent: Element}>}
28
28
  */
29
29
  const allTerms = [];
30
- document.querySelectorAll('dt span.transcluded-xref-term').forEach(termElement => {
31
- const textContent = Array.from(termElement.childNodes)
32
- .filter(node => node.nodeType === Node.TEXT_NODE)
33
- .map(node => node.textContent.trim())
34
- .join('');
30
+
31
+ document.querySelectorAll('dl.terms-and-definitions-list dt span.transcluded-xref-term').forEach((termElement) => {
32
+ // Get the full text content including any nested spans (for aliases) of a term (dt)
33
+ // In case of `[[tref:toip1, agency, ag]]`, this will return `agency`
34
+ const textContent = termElement.textContent.trim();
35
35
 
36
36
  // Find the dt element once outside the loop
37
37
  const dt = termElement.closest('dt');
@@ -68,7 +68,7 @@ function insertTrefs(allXTrefs) {
68
68
 
69
69
  // Find the first matching xref to avoid duplicates
70
70
  const xref = xtrefsData.xtrefs.find(x => x.term === textContent);
71
-
71
+
72
72
  // Create a DocumentFragment to hold all new elements for this term
73
73
  const fragment = document.createDocumentFragment();
74
74
 
@@ -93,16 +93,22 @@ function insertTrefs(allXTrefs) {
93
93
  metaInfoEl.innerHTML = md.render(metaInfo);
94
94
  fragment.appendChild(metaInfoEl);
95
95
 
96
- // Clean up markdown content
96
+ // Clean up the markdown content in the term definition
97
+ // Part A: clean up via regex
97
98
  let content = xref.content
98
- .replace(/\[\[def:[^\]]*?\]\]/g, '') // Remove [[def: ...]] patterns regardless of trailing chars
99
99
  .split('\n')
100
100
  .map(line => line.replace(/^\s*~\s*/, '')) // Remove leading ~ and spaces
101
101
  .join('\n')
102
- .replace(/\[\[ref:/g, '') // Remove [[ref: ...]]
103
102
  .replace(/\]\]/g, '');
104
103
 
105
- // Parse the rendered HTML to check for dd elements
104
+ // Clean up the markdown content in the term definition
105
+ // Part B: Remove all <a> elements from the content via a temporary div and DOM manipulation
106
+ const tempDivForLinks = document.createElement('div');
107
+ tempDivForLinks.innerHTML = md.render(content);
108
+ tempDivForLinks.querySelectorAll('a').forEach(a => a.replaceWith(...a.childNodes));
109
+ content = tempDivForLinks.innerHTML;
110
+
111
+ // Parse the rendered HTML to check for dd elements. xref.content is a string that contains HTML, in the form of <dd>...</dd>'s
106
112
  const tempDiv = document.createElement('div');
107
113
  tempDiv.innerHTML = md.render(content);
108
114
 
@@ -119,14 +125,17 @@ function insertTrefs(allXTrefs) {
119
125
  fragment.appendChild(clonedDD);
120
126
  });
121
127
  } else {
122
- // No dd elements found, create one to hold the content
128
+ /*
129
+ No dd elements found, create one to hold the conten. Explanation: this is the content in case nothing was found:
130
+ `"content": "This term was not found in the external repository"`
131
+ */
123
132
  const contentEl = document.createElement('dd');
124
133
  contentEl.classList.add('transcluded-xref-term', 'transcluded-xref-term-embedded');
125
134
  contentEl.innerHTML = tempDiv.innerHTML;
126
135
  fragment.appendChild(contentEl);
127
136
  }
128
137
  } else {
129
- // Handle case where xref is not found
138
+ // When the [[tref]] is not valid, for example `[[tref: transferable, transferable]]`, where `transferable` is not an external repo in specs.json
130
139
  metaInfoEl.innerHTML = md.render(`
131
140
  | Property | Value |
132
141
  | -------- | ----- |
@@ -138,7 +147,7 @@ function insertTrefs(allXTrefs) {
138
147
 
139
148
  // Create not found message
140
149
  const notFoundEl = document.createElement('dd');
141
- notFoundEl.classList.add('transcluded-xref-term', 'transcluded-xref-term-embedded', 'last-dd');
150
+
142
151
  notFoundEl.innerHTML = '<p>This term was not found in the external repository.</p>';
143
152
  fragment.appendChild(notFoundEl);
144
153
  }
@@ -160,28 +169,20 @@ function insertTrefs(allXTrefs) {
160
169
  const { dt, parent, fragment } = change;
161
170
  parent.insertBefore(fragment, dt.nextSibling);
162
171
  });
163
-
172
+
164
173
  // Dispatch a custom event when all DOM modifications are complete
165
174
  // This allows other scripts to know exactly when our work is done
166
175
  /**
167
176
  * Dispatches a custom event to signal that trefs insertion is complete
168
177
  * @fires trefs-inserted
169
178
  */
170
- document.dispatchEvent(new CustomEvent('trefs-inserted', {
171
- detail: { count: domChanges.length }
179
+ document.dispatchEvent(new CustomEvent('trefs-inserted', {
180
+ detail: { count: domChanges.length }
172
181
  }));
173
182
  });
174
183
  }
175
184
 
176
- if (allXTrefs?.xtrefs) {
177
- processTerms(allXTrefs);
178
- } else {
179
- console.error('allXTrefs is undefined or missing xtrefs property');
180
- // Dispatch event even when there are no xrefs, so waiting code knows we're done
181
- document.dispatchEvent(new CustomEvent('trefs-inserted', {
182
- detail: { count: 0, error: 'Missing xtrefs data' }
183
- }));
184
- }
185
+ processTerms(allXTrefs);
185
186
  }
186
187
 
187
188
  /**
@@ -231,11 +232,14 @@ function initializeOnTrefsInserted(initCallback) {
231
232
  * @listens DOMContentLoaded
232
233
  */
233
234
  document.addEventListener('DOMContentLoaded', () => {
234
- // Check if allXTrefs is defined in the global scope
235
- if (typeof allXTrefs !== 'undefined') {
235
+ if (typeof allXTrefs !== 'undefined' && allXTrefs?.xtrefs) {
236
236
  insertTrefs(allXTrefs);
237
237
  } else {
238
- console.warn('allXTrefs is not available in the global scope. Transcluded references will not be inserted.');
238
+ console.error('allXTrefs is undefined or missing xtrefs property');
239
+ // Dispatch event even when there are no xrefs, so waiting code knows we're done
240
+ document.dispatchEvent(new CustomEvent('trefs-inserted', {
241
+ detail: { count: 0, error: 'Missing xtrefs data' }
242
+ }));
239
243
  }
240
244
  });
241
245
 
@@ -27,6 +27,7 @@
27
27
  "assets/css/add-bootstrap-classes-to-images.css",
28
28
  "assets/css/horizontal-scroll-hint.css",
29
29
  "assets/css/highlight-heading-plus-sibling-nodes.css",
30
+ "assets/css/counter.css",
30
31
  "assets/css/index.css"
31
32
  ],
32
33
  "js": [
package/index.js CHANGED
@@ -38,6 +38,7 @@ module.exports = async function (options = {}) {
38
38
 
39
39
  const { fixMarkdownFiles } = require('./src/fix-markdown-files.js');
40
40
  const { processEscapedTags, restoreEscapedTags } = require('./src/escape-mechanism.js');
41
+ const { sortDefinitionTermsInHtml, fixDefinitionListStructure } = require('./src/html-dom-processor.js');
41
42
 
42
43
  let template = fs.readFileSync(path.join(modulePath, 'templates/template.html'), 'utf8');
43
44
  let assets = fs.readJsonSync(modulePath + '/config/asset-map.json');
@@ -71,7 +72,13 @@ module.exports = async function (options = {}) {
71
72
  linkify: true,
72
73
  typographer: true
73
74
  })
75
+ /*
76
+ Configures a Markdown-it plugin by passing it an array of extension objects, each responsible for handling specific custom syntax in Markdown documents.
77
+ */
74
78
  .use(require('./src/markdown-it-extensions.js'), [
79
+ /*
80
+ The first extension (= the first configuration object = the first element of the array) focuses on terminology-related constructs, using a filter to match types against a regular expression (terminologyRegex).
81
+ */
75
82
  {
76
83
  filter: type => type.match(terminologyRegex),
77
84
  parse(token, type, primary) {
@@ -92,7 +99,20 @@ module.exports = async function (options = {}) {
92
99
  href="${url}#term:${term}">${token.info.args[1]}</a>`;
93
100
  }
94
101
  else if (type === 'tref') {
95
- return `<span class="transcluded-xref-term" id="term:${token.info.args[1]}">${token.info.args[1]}</span>`;
102
+ // Support tref with optional alias: [[tref: spec, term, alias]]
103
+ const termName = token.info.args[1];
104
+ const alias = token.info.args[2]; // Optional alias
105
+
106
+ // Create IDs for both the original term and the alias to enable referencing by either
107
+ const termId = `term:${termName.replace(spaceRegex, '-').toLowerCase()}`;
108
+ const aliasId = alias ? `term:${alias.replace(spaceRegex, '-').toLowerCase()}` : '';
109
+
110
+ // Return the term structure similar to def, so it can be processed by markdown-it's definition list parser
111
+ if (aliasId && alias !== termName) {
112
+ return `<span class="transcluded-xref-term" id="${termId}"><span id="${aliasId}">${termName}</span></span>`;
113
+ } else {
114
+ return `<span class="transcluded-xref-term" id="${termId}">${termName}</span>`;
115
+ }
96
116
  }
97
117
  else {
98
118
  references.push(primary);
@@ -100,6 +120,9 @@ module.exports = async function (options = {}) {
100
120
  }
101
121
  }
102
122
  },
123
+ /*
124
+ The second extension is designed for handling specification references.
125
+ */
103
126
  {
104
127
  filter: type => type.match(specNameRegex),
105
128
  parse(token, type, name) {
@@ -174,7 +197,7 @@ module.exports = async function (options = {}) {
174
197
  .use(require('@traptitech/markdown-it-katex'))
175
198
 
176
199
  const katexRules = ['math_block', 'math_inline'];
177
- const replacerRegex = /\[\[\s*([^\s\[\]:]+):?\s*([^\]\n]+)?\]\]/img;
200
+ const replacerRegex = /\[\[\s*([^\s[\]:]+):?\s*([^\]\n]+)?\]\]/img;
178
201
  const replacerArgsRegex = /\s*,+\s*/;
179
202
  const replacers = [
180
203
  {
@@ -183,29 +206,6 @@ module.exports = async function (options = {}) {
183
206
  if (!path) return '';
184
207
  return fs.readFileSync(path, 'utf8');
185
208
  }
186
- },
187
-
188
- /**
189
- * Custom replacer for tref tags that converts them directly to HTML definition term elements.
190
- *
191
- * This is a critical part of our solution for fixing transcluded terms in definition lists.
192
- * When a [[tref:spec,term]] tag is found in the markdown, this replacer transforms it into
193
- * a proper <dt> element with the appropriate structure before the markdown parser processes it.
194
- *
195
- * By directly generating the HTML structure (instead of letting the markdown-it parser
196
- * handle it later), we prevent the issue where transcluded terms break the definition list.
197
- *
198
- * @param {string} originalMatch - The original [[tref:spec,term]] tag found in the markdown
199
- * @param {string} type - The tag type ('tref')
200
- * @param {string} spec - The specification identifier (e.g., 'wot-1')
201
- * @param {string} term - The term to transclude (e.g., 'DAR')
202
- * @returns {string} - HTML representation of the term as a dt element
203
- */
204
- {
205
- test: 'tref',
206
- transform: function (originalMatch, type, spec, term) {
207
- return `<dt class="transcluded-xref-term"><span class="transcluded-xref-term" id="term:${term.replace(/\s+/g, '-').toLowerCase()}">${term}</span></dt>`;
208
- }
209
209
  }
210
210
  ];
211
211
 
@@ -295,208 +295,6 @@ module.exports = async function (options = {}) {
295
295
  throw Error("katex distribution could not be located");
296
296
  }
297
297
 
298
- function sortDefinitionTermsInHtml(html) {
299
- const { JSDOM } = require('jsdom');
300
- const dom = new JSDOM(html);
301
- const document = dom.window.document;
302
-
303
- // Find the terms and definitions list
304
- const dlElement = document.querySelector('.terms-and-definitions-list');
305
- if (!dlElement) return html; // If not found, return the original HTML
306
-
307
- // Collect all dt/dd pairs
308
- const pairs = [];
309
- let currentDt = null;
310
- let currentDds = [];
311
-
312
- // Process each child of the dl element
313
- Array.from(dlElement.children).forEach(child => {
314
- if (child.tagName === 'DT') {
315
- // If we already have a dt, save the current pair
316
- if (currentDt) {
317
- pairs.push({
318
- dt: currentDt,
319
- dds: [...currentDds],
320
- text: currentDt.textContent.trim().toLowerCase() // Use lowercase for sorting
321
- });
322
- currentDds = []; // Reset dds for the next dt
323
- }
324
- currentDt = child;
325
- } else if (child.tagName === 'DD' && currentDt) {
326
- currentDds.push(child);
327
- }
328
- });
329
-
330
- // Add the last pair if exists
331
- if (currentDt) {
332
- pairs.push({
333
- dt: currentDt,
334
- dds: [...currentDds],
335
- text: currentDt.textContent.trim().toLowerCase()
336
- });
337
- }
338
-
339
- // Sort pairs case-insensitively
340
- pairs.sort((a, b) => a.text.localeCompare(b.text));
341
-
342
- // Clear the dl element
343
- while (dlElement.firstChild) {
344
- dlElement.removeChild(dlElement.firstChild);
345
- }
346
-
347
- // Re-append elements in sorted order
348
- pairs.forEach(pair => {
349
- dlElement.appendChild(pair.dt);
350
- pair.dds.forEach(dd => {
351
- dlElement.appendChild(dd);
352
- });
353
- });
354
-
355
- // Return the modified HTML
356
- return dom.serialize();
357
- }
358
-
359
- // Function to fix broken definition list structures
360
- /**
361
- * This function repairs broken definition list (dl) structures in the HTML output.
362
- * Specifically, it addresses the issue where transcluded terms (tref tags) break
363
- * out of the definition list, creating separate lists instead of a continuous one.
364
- *
365
- * The strategy:
366
- * 1. Find all definition lists (dl elements) in the document
367
- * 2. Use the dl with class 'terms-and-definitions-list' as the main/target list
368
- * 3. Process each subsequent node after the this main dl:
369
- * - If another dl is found, merge all its children into the main dl
370
- * - If a standalone dt is found, move it into the main dl
371
- * - Remove any empty paragraphs that might be breaking the list continuity
372
- *
373
- * This ensures all terms appear in one continuous definition list,
374
- * regardless of how they were originally rendered in the markdown.
375
- *
376
- * @param {string} html - The HTML content to fix
377
- * @returns {string} - The fixed HTML content with merged definition lists
378
- */
379
- function fixDefinitionListStructure(html) {
380
- const { JSDOM } = require('jsdom');
381
- const dom = new JSDOM(html);
382
- const document = dom.window.document;
383
-
384
- // Find all dl elements first
385
- const allDls = Array.from(document.querySelectorAll('dl'));
386
-
387
- // Then filter to find the one with the terms-and-definitions-list class
388
- const dlElements = allDls.filter(dl => {
389
- return dl?.classList?.contains('terms-and-definitions-list');
390
- });
391
-
392
- // Find any transcluded term dt elements anywhere in the document
393
- const transcludedTerms = document.querySelectorAll('dt.transcluded-xref-term');
394
-
395
- let mainDl = null;
396
-
397
- // If we have an existing dl with the terms-and-definitions-list class, use it
398
- if (dlElements.length > 0) {
399
- mainDl = dlElements[0]; // Use the first one
400
- }
401
- // If we have transcluded terms but no main dl, we need to create one
402
- else if (transcludedTerms.length > 0) {
403
- // Create a new dl element with the right class
404
- mainDl = document.createElement('dl');
405
- mainDl.className = 'terms-and-definitions-list';
406
-
407
- // Look for the marker
408
- const marker = document.getElementById('terminology-section-start');
409
-
410
- if (marker) {
411
- // Insert the new dl right after the marker
412
- if (marker.nextSibling) {
413
- marker.parentNode.insertBefore(mainDl, marker.nextSibling);
414
- } else {
415
- marker.parentNode.appendChild(mainDl);
416
- }
417
- } else {
418
- // Fallback to the original approach if marker isn't found
419
- const firstTerm = transcludedTerms[0];
420
- const insertPoint = firstTerm.parentNode;
421
- insertPoint.parentNode.insertBefore(mainDl, insertPoint);
422
- }
423
- }
424
-
425
- // Safety check - if we still don't have a mainDl, exit early to avoid null reference errors
426
- if (!mainDl) {
427
- return html; // Return the original HTML without modifications
428
- }
429
-
430
- // Now process all transcluded terms and other dt elements
431
- transcludedTerms.forEach(dt => {
432
- // Check if this dt is not already inside our main dl
433
- if (dt.parentElement !== mainDl) {
434
- // Move it into the main dl
435
- const dtClone = dt.cloneNode(true);
436
- mainDl.appendChild(dtClone);
437
- dt.parentNode.removeChild(dt);
438
- }
439
- });
440
-
441
- // First special case - handle transcluded-xref-term dt that comes BEFORE the main dl
442
- const transcludedTermsBeforeMainDl = document.querySelectorAll('dt.transcluded-xref-term');
443
-
444
- // Special handling for transcluded terms that appear BEFORE the main dl
445
- transcludedTermsBeforeMainDl.forEach(dt => {
446
- // Check if this dt is not already inside our main list
447
- if (dt.parentElement !== mainDl) {
448
- // This is a dt outside our main list - move it into the main dl
449
- const dtClone = dt.cloneNode(true);
450
- mainDl.appendChild(dtClone);
451
- dt.parentNode.removeChild(dt);
452
- }
453
- });
454
-
455
- // Remove any empty dt elements that may exist
456
- const emptyDts = mainDl.querySelectorAll('dt:empty');
457
- emptyDts.forEach(emptyDt => {
458
- emptyDt.parentNode.removeChild(emptyDt);
459
- });
460
-
461
- // Process all subsequent content after the main dl
462
- let currentNode = mainDl.nextSibling;
463
-
464
- // Process all subsequent content
465
- while (currentNode) {
466
- // Save the next node before potentially modifying the DOM
467
- const nextNode = currentNode.nextSibling;
468
-
469
- // Handle different node types
470
- if (currentNode.nodeType === 1) { // 1 = Element node
471
- if (currentNode.tagName === 'DL') {
472
- // Found another definition list - move all its children to the main dl
473
- while (currentNode.firstChild) {
474
- mainDl.appendChild(currentNode.firstChild);
475
- }
476
- // Remove the now-empty dl element
477
- currentNode.parentNode.removeChild(currentNode);
478
- }
479
- else if (currentNode.tagName === 'DT') {
480
- // Found a standalone dt - move it into the main dl
481
- const dtClone = currentNode.cloneNode(true);
482
- mainDl.appendChild(dtClone);
483
- currentNode.parentNode.removeChild(currentNode);
484
- }
485
- else if (currentNode.tagName === 'P' &&
486
- (!currentNode.textContent || currentNode.textContent.trim() === '')) {
487
- // Remove empty paragraphs - these break the list structure
488
- currentNode.parentNode.removeChild(currentNode);
489
- }
490
- }
491
-
492
- // Move to the next node we saved earlier
493
- currentNode = nextNode;
494
- }
495
-
496
- // Return the fixed HTML
497
- return dom.serialize();
498
- }
499
-
500
298
  async function render(spec, assets) {
501
299
  try {
502
300
  noticeTitles = {};
@@ -507,6 +305,13 @@ module.exports = async function (options = {}) {
507
305
  return template.replace(/\${(.*?)}/g, (match, p1) => variables[p1.trim()]);
508
306
  }
509
307
 
308
+ // Add current date in 'DD Month YYYY' format for template injection
309
+ const date = new Date();
310
+ const day = String(date.getDate()).padStart(2, '0');
311
+ const month = date.toLocaleString('en-US', { month: 'long' });
312
+ const year = date.getFullYear();
313
+ const currentDate = `${day} ${month} ${year}`;
314
+
510
315
  const docs = await Promise.all(
511
316
  (spec.markdown_paths || ['spec.md']).map(_path =>
512
317
  fs.readFile(spec.spec_directory + _path, 'utf8')
@@ -572,6 +377,7 @@ module.exports = async function (options = {}) {
572
377
  specLogoLink: spec.logo_link,
573
378
  spec: JSON.stringify(spec),
574
379
  externalSpecsList: externalSpecsList,
380
+ currentDate: currentDate
575
381
  });
576
382
 
577
383
  const outputPath = path.join(spec.destination, 'index.html');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-up-t",
3
- "version": "1.2.9",
3
+ "version": "1.3.0",
4
4
  "description": "Technical specification drafting tool that generates rich specification documents from markdown. Forked from https://github.com/decentralized-identity/spec-up by Daniel Buchner (https://github.com/csuwildcat)",
5
5
  "main": "./index",
6
6
  "repository": {
@@ -28,6 +28,7 @@
28
28
  "axios": "^1.7.7",
29
29
  "dedent": "^1.5.3",
30
30
  "diff": "^7.0.0",
31
+ "docx": "^8.5.0",
31
32
  "dotenv": "^16.4.7",
32
33
  "find-pkg-dir": "^2.0.0",
33
34
  "fs-extra": "^11.3.0",
@@ -67,7 +68,8 @@
67
68
  "braces": ">=3.0.3"
68
69
  },
69
70
  "devDependencies": {
70
- "jest": "^29.7.0"
71
+ "jest": "^29.7.0",
72
+ "jest-environment-jsdom": "^30.0.5"
71
73
  },
72
74
  "scripts": {
73
75
  "test": "jest",
@@ -0,0 +1,7 @@
1
+ # Temporarily disabled for Automatic Analysis
2
+ # sonar.projectKey=blockchainbird_spec-up-t
3
+ # sonar.organization=blockchainbird
4
+ # sonar.host.url=https://sonarcloud.io
5
+ # sonar.token=SONAR_TOKEN
6
+ # sonar.exclusions=assets/compiled/refs.json
7
+ # sonar.cpd.exclusions=**/*.test.js
@@ -54,26 +54,37 @@ const readlineSync = require('readline-sync');
54
54
  * @returns {boolean} True if the xtref is found in the content
55
55
  */
56
56
  function isXTrefInMarkdown(xtref, markdownContent) {
57
- const regex = new RegExp(`\\[\\[(?:x|t)ref:${xtref.externalSpec},\\s*${xtref.term}\\]\\]`, 'g');
58
- return regex.test(markdownContent);
57
+ // Escape special regex characters in externalSpec and term
58
+ const escapedSpec = xtref.externalSpec.replace(/[.*+?^${}()|[\]\\-]/g, '\\$&');
59
+ const escapedTerm = xtref.term.replace(/[.*+?^${}()|[\]\\-]/g, '\\$&');
60
+
61
+ // Check for both the term and with any alias (accounting for spaces)
62
+ const regexTerm = new RegExp(`\\[\\[(?:x|t)ref:\\s*${escapedSpec},\\s*${escapedTerm}(?:,\\s*[^\\]]+)?\\]\\]`, 'g');
63
+ return regexTerm.test(markdownContent);
59
64
  }
60
65
 
61
66
  /**
62
67
  * Helper function to process an XTref string and return an object.
63
68
  *
64
69
  * @param {string} xtref - The xtref string to process
65
- * @returns {Object} An object with externalSpec and term properties
70
+ * @returns {Object} An object with externalSpec, term, and optional alias properties
66
71
  */
67
72
  function processXTref(xtref) {
68
- let [externalSpec, term] = xtref
73
+ const parts = xtref
69
74
  .replace(/\[\[(?:xref|tref):/, '')
70
75
  .replace(/\]\]/, '')
71
76
  .trim()
72
- .split(/,/, 2);
77
+ .split(/,/);
78
+
73
79
  const xtrefObject = {
74
- externalSpec: externalSpec.trim(),
75
- term: term.trim()
80
+ externalSpec: parts[0].trim(),
81
+ term: parts[1].trim()
76
82
  };
83
+
84
+ // Add alias if provided (third parameter)
85
+ if (parts.length > 2 && parts[2].trim()) {
86
+ xtrefObject.alias = parts[2].trim();
87
+ }
77
88
 
78
89
  return xtrefObject;
79
90
  }
@@ -129,6 +140,7 @@ function extendXTrefs(config, xtrefs) {
129
140
  xtref.owner = urlParts[1];
130
141
  xtref.repo = urlParts[2];
131
142
  xtref.avatarUrl = repo.avatar_url;
143
+ xtref.ghPageUrl = repo.gh_page; // Add GitHub Pages URL
132
144
  }
133
145
  });
134
146
 
@@ -149,9 +161,8 @@ function extendXTrefs(config, xtrefs) {
149
161
  *
150
162
  * @param {Object} config - The configuration object from specs.json
151
163
  * @param {string} GITHUB_API_TOKEN - The GitHub API token
152
- * @param {Object} options - Configuration options
153
164
  */
154
- function processExternalReferences(config, GITHUB_API_TOKEN, options) {
165
+ function processExternalReferences(config, GITHUB_API_TOKEN) {
155
166
  const { processXTrefsData } = require('./collectExternalReferences/processXTrefsData.js');
156
167
  const { doesUrlExist } = require('./utils/doesUrlExist.js');
157
168
  const externalSpecsRepos = config.specs[0].external_specs;
@@ -257,7 +268,7 @@ function processExternalReferences(config, GITHUB_API_TOKEN, options) {
257
268
  // }
258
269
  // ]
259
270
 
260
- processXTrefsData(allXTrefs, GITHUB_API_TOKEN, outputPathJSON, outputPathJS, outputPathJSTimeStamped, options);
271
+ processXTrefsData(allXTrefs, GITHUB_API_TOKEN, outputPathJSON, outputPathJS, outputPathJSTimeStamped);
261
272
  }
262
273
 
263
274
  /**
@@ -320,7 +331,7 @@ function collectExternalReferences(options = {}) {
320
331
  return;
321
332
  }
322
333
  } else {
323
- processExternalReferences(config, GITHUB_API_TOKEN, options);
334
+ processExternalReferences(config, GITHUB_API_TOKEN);
324
335
  }
325
336
  }
326
337