spec-up-t 1.5.0 → 1.6.0-beta.2

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/create-pdf.js CHANGED
@@ -198,7 +198,7 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
198
198
  // Insert the TOC after the title section
199
199
  document.body.insertBefore(tocContainer, titleWrapper.nextSibling);
200
200
 
201
- console.log('Generated a Table of Contents with ' + headings.length + ' entries.');
201
+ console.log('Generated a Table of Contents with ' + headings.length + ' entries.');
202
202
  }
203
203
  }
204
204
  }, logo, logoLink, title, description);
@@ -254,7 +254,7 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
254
254
 
255
255
  // If TOC doesn't exist, we'll need to create one
256
256
  if (!document.getElementById('toc')) {
257
- console.log('No TOC found in the document. Will create one.');
257
+ console.log('No TOC found in the document. Will create one.');
258
258
  }
259
259
  });
260
260
 
@@ -508,7 +508,7 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
508
508
  }
509
509
  });
510
510
 
511
- Logger.process('Generating PDF with proper TOC page numbers...');
511
+ Logger.process('Generating PDF with proper TOC page numbers...');
512
512
 
513
513
  // First, generate a draft PDF to calculate the page positions of each heading
514
514
  const draftPdfBuffer = await page.pdf({
@@ -530,28 +530,28 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
530
530
  // Find the PDF TOC
531
531
  const pdfToc = document.getElementById('pdf-toc');
532
532
  if (!pdfToc) return;
533
-
533
+
534
534
  const tocEntries = pdfToc.querySelectorAll('.toc-page-number');
535
535
  const originalToc = document.getElementById('toc');
536
-
536
+
537
537
  if (originalToc) {
538
538
  // Get all links from the original TOC that have tooltip data
539
539
  const originalLinks = originalToc.querySelectorAll('a[title], a[data-bs-title]');
540
-
540
+
541
541
  // Create a mapping from heading IDs to page numbers based on tooltips
542
542
  const idToPageMap = {};
543
-
543
+
544
544
  originalLinks.forEach(link => {
545
545
  // Extract the heading ID from href
546
546
  const href = link.getAttribute('href');
547
547
  if (!href || !href.startsWith('#')) return;
548
-
548
+
549
549
  const headingId = href.substring(1);
550
-
550
+
551
551
  // Extract page number from tooltip text (e.g., "Go to page 5")
552
552
  const tooltipText = link.getAttribute('title') || link.getAttribute('data-bs-title');
553
553
  if (!tooltipText) return;
554
-
554
+
555
555
  const pageNumberMatch = tooltipText.match(/Go to page (\d+)/i);
556
556
  if (pageNumberMatch && pageNumberMatch[1]) {
557
557
  const pageNumber = parseInt(pageNumberMatch[1], 10);
@@ -560,7 +560,7 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
560
560
  }
561
561
  }
562
562
  });
563
-
563
+
564
564
  // Now update the TOC page numbers using the extracted values
565
565
  tocEntries.forEach(entry => {
566
566
  const targetId = entry.getAttribute('data-for-id');
@@ -578,10 +578,10 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
578
578
  } else {
579
579
  // Fallback to old estimation method if original TOC is not available
580
580
  console.log('Original TOC not found, using page number estimation method');
581
-
581
+
582
582
  // Find all headings with IDs (potential TOC targets)
583
583
  const headingsWithIds = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')).filter(h => h.id);
584
-
584
+
585
585
  // Use real offsets for more accurate page numbers
586
586
  const idToPosition = {};
587
587
 
@@ -643,12 +643,12 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
643
643
 
644
644
  await browser.close();
645
645
 
646
- Logger.success('PDF generated by Puppeteer. Processing with pdf-lib for ISO compliance...');
646
+ Logger.success('PDF generated by Puppeteer. Processing with pdf-lib for ISO compliance...');
647
647
 
648
648
  // Optimize PDF with pdf-lib for ISO compliance
649
649
  try {
650
650
  const pdfDoc = await pdfLib.PDFDocument.load(pdfBuffer);
651
-
651
+
652
652
  // Set ISO-compliant metadata (this is safer than XMP embedding)
653
653
  pdfDoc.setTitle(metadata.title);
654
654
  pdfDoc.setAuthor(metadata.author);
@@ -666,7 +666,7 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
666
666
  useObjectStreams: false, // Required for PDF/A compliance
667
667
  addDefaultPage: false
668
668
  });
669
-
669
+
670
670
  fs.writeFileSync('docs/index.pdf', optimizedPdfBytes);
671
671
  Logger.success('PDF saved with ISO compliance features.');
672
672
  } catch (pdfError) {
@@ -675,8 +675,12 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
675
675
  fs.writeFileSync('docs/index.pdf', pdfBuffer);
676
676
  }
677
677
 
678
- Logger.success('PDF generated successfully! Find the PDF in the docs directory.');
678
+ Logger.success('PDF generated successfully! Find the PDF in the docs directory.');
679
679
  } catch (error) {
680
- Logger.error('Error generating PDF: %o', error);
680
+ Logger.error('Error generating PDF', {
681
+ context: 'Failed during PDF document generation',
682
+ hint: 'Ensure the HTML file exists (run "npm run render" first), Puppeteer is installed, and you have write permissions',
683
+ details: error.message || error
684
+ });
681
685
  }
682
686
  })();
package/src/init.js CHANGED
@@ -8,14 +8,14 @@ async function initialize() {
8
8
  try {
9
9
  // Ensure the .cache directory exists
10
10
  await fs.ensureDir(outputDir);
11
-
11
+
12
12
  // Check if the init script has already run
13
13
  if (await fs.pathExists(initFlagPath)) {
14
14
  return;
15
15
  }
16
16
 
17
17
  // Place the init script here
18
-
18
+
19
19
  // End of the init script
20
20
 
21
21
  // Create the init flag file
@@ -23,7 +23,11 @@ async function initialize() {
23
23
 
24
24
  Logger.success('Initialization complete.');
25
25
  } catch (error) {
26
- Logger.error(`Initialization failed: ${error.message}`);
26
+ Logger.error('Initialization failed', {
27
+ context: 'Failed to set up spec-up-t boilerplate files',
28
+ hint: 'Ensure you have write permissions in the current directory. Try running with appropriate permissions or in a different directory',
29
+ details: error.message
30
+ });
27
31
  }
28
32
  }
29
33
 
@@ -1 +1 @@
1
- [//]: # (This file, named “terms-and-definitions-intro.md” is mandatory and should not be deleted.)
1
+ [//]: # (This file, named “terms-and-definitions-intro.md” is mandatory and should not be deleted. However, you can safely delete this comment and replace it with text of your choice.)
@@ -13,7 +13,7 @@
13
13
 
14
14
  const { findExternalSpecByKey } = require('../pipeline/references/external-references-service.js');
15
15
  const { lookupXrefTerm } = require('../pipeline/rendering/render-utils.js');
16
- const { whitespace, htmlComments, contentCleaning, externalReferences } = require('../utils/regex-patterns');
16
+ const { whitespace, htmlComments, contentCleaning, externalReferences, utils } = require('../utils/regex-patterns');
17
17
  const Logger = require('../utils/logger.js');
18
18
 
19
19
  /**
@@ -33,7 +33,7 @@ function extractCurrentFile(token, globalState) {
33
33
  * @param {Object} config - Configuration object containing specs and settings
34
34
  * @param {Object} globalState - Global state object containing definitions, references, etc.
35
35
  * @param {Object} token - The markdown-it token being processed
36
- * @param {string} type - The type of construct (def, ref, xref, tref)
36
+ * @param {string} type - The type of construct (def, ref, iref, xref, tref)
37
37
  * @param {string} primary - The primary content/term
38
38
  * @returns {string} The rendered HTML for the construct
39
39
  */
@@ -45,6 +45,8 @@ function parseTemplateTag(config, globalState, token, type, primary) {
45
45
  switch (type) {
46
46
  case 'def':
47
47
  return parseDef(globalState, token, primary, currentFile);
48
+ case 'iref':
49
+ return parseIref(globalState, primary);
48
50
  case 'xref':
49
51
  return parseXref(config, token);
50
52
  case 'tref':
@@ -109,7 +111,10 @@ function parseDef(globalState, token, primary, currentFile) {
109
111
  // IDs stay intact - we create an ID for the original term and each alias
110
112
  return token.info.args.reduce((acc, syn) => {
111
113
  // Generate a unique term ID by normalizing the synonym: replace whitespace with hyphens and convert to lowercase. The ID is used for fragment identifier (hash) in the URL, which in turn can be used for an anchor in a web page.
112
- const termId = `term:${syn.replace(whitespace.oneOrMore, '-').toLowerCase()}`;
114
+ // Apply sanitization to remove special characters that would break CSS selectors
115
+ const normalizedSyn = syn.replace(whitespace.oneOrMore, '-').toLowerCase();
116
+ const sanitizedSyn = utils.sanitizeTermId(normalizedSyn);
117
+ const termId = `term:${sanitizedSyn}`;
113
118
  return `<span id="${termId}">${acc}</span>`;
114
119
  }, displayText);
115
120
  }
@@ -130,6 +135,25 @@ function parseRef(globalState, primary) {
130
135
  return `<a class="term-reference" href="#term:${termId}">${primary}</a>`;
131
136
  }
132
137
 
138
+ /**
139
+ * Processes [[iref: term]] constructs
140
+ * Creates a placeholder that will be replaced client-side with a copy of the term definition
141
+ * This allows inline copying of existing term definitions from the terms-and-definitions-list
142
+ * @param {Object} globalState - Global state to track inline references
143
+ * @param {string} primary - The term to inline copy
144
+ * @returns {string} HTML placeholder element that will be replaced by client-side script
145
+ */
146
+ function parseIref(globalState, primary) {
147
+ // Track this inline reference for validation purposes
148
+ globalState.references.push(primary);
149
+
150
+ // Create a placeholder span with data attribute containing the term to copy
151
+ // The client-side script (insert-irefs.js) will find this and replace it with
152
+ // a copy of the actual <dt> and <dd> elements from the terms-and-definitions-list
153
+ const termId = primary.replace(whitespace.oneOrMore, '-').toLowerCase();
154
+ return `<span class="iref-placeholder" data-iref-term="${termId}" data-iref-original="${primary}"></span>`;
155
+ }
156
+
133
157
  /**
134
158
  * Processes [[xref: spec, term, alias, ...]] constructs
135
159
  * Creates links to external specification terms with tooltips
@@ -217,7 +241,10 @@ function parseTref(token) {
217
241
 
218
242
  return termsAndAliases.reduce((acc, syn, index) => {
219
243
  // Generate a unique term ID by normalizing the synonym: replace whitespace with hyphens and convert to lowercase
220
- const termId = `term:${syn.replace(whitespace.oneOrMore, '-').toLowerCase()}`;
244
+ // Apply sanitization to remove special characters that would break CSS selectors
245
+ const normalizedSyn = syn.replace(whitespace.oneOrMore, '-').toLowerCase();
246
+ const sanitizedSyn = utils.sanitizeTermId(normalizedSyn);
247
+ const termId = `term:${sanitizedSyn}`;
221
248
  // Add title attribute to the innermost span (first in the array, which wraps the display text directly)
222
249
  // This provides a tooltip showing which external term this alias refers to
223
250
  const titleAttr = index === 0 && aliases.length > 0 ? ` title="Externally defined as ${termName}"` : '';
@@ -298,6 +325,7 @@ module.exports = {
298
325
  createTemplateTagParser,
299
326
  // Export individual functions for testing purposes
300
327
  parseDef,
328
+ parseIref,
301
329
  parseXref,
302
330
  parseTref,
303
331
  parseRef,
@@ -46,7 +46,11 @@ function normalizeSpecConfiguration(config, { noSpecsMessage }) {
46
46
  const specs = Array.isArray(config?.specs) ? config.specs : [];
47
47
 
48
48
  if (specs.length === 0) {
49
- Logger.error(noSpecsMessage);
49
+ Logger.error(noSpecsMessage, {
50
+ context: 'specs.json is missing or has no specs array',
51
+ hint: 'Create a valid specs.json file in your project root. Run "npm run init" or copy from: https://github.com/trustoverip/spec-up-t-starter-pack',
52
+ details: 'The specs array is required to configure your specification'
53
+ });
50
54
  return null;
51
55
  }
52
56
 
@@ -65,7 +69,11 @@ function normalizeSpecConfiguration(config, { noSpecsMessage }) {
65
69
  */
66
70
  function extendXTrefs(config, xtrefs) {
67
71
  if (config.specs[0].external_specs_repos) {
68
- Logger.warn('Your specs.json file uses an outdated structure. Update it using: https://github.com/trustoverip/spec-up-t/blob/master/src/install-from-boilerplate/boilerplate/specs.json');
72
+ Logger.warn('Your specs.json file uses an outdated structure', {
73
+ context: 'The "external_specs_repos" field is deprecated',
74
+ hint: 'Update to the new structure using "external_specs" instead. See: https://github.com/trustoverip/spec-up-t/blob/master/src/install-from-boilerplate/boilerplate/specs.json',
75
+ details: 'External references may not work correctly with the old structure'
76
+ });
69
77
  return;
70
78
  }
71
79
 
@@ -118,7 +126,11 @@ function extendXTrefs(config, xtrefs) {
118
126
  try {
119
127
  xtref.branch = getCurrentBranch();
120
128
  } catch (error) {
121
- Logger.warn(`Could not get current branch for xtref ${xtref.externalSpec}:${xtref.term}: ${error.message}`);
129
+ Logger.warn(`Could not get current branch for xtref ${xtref.externalSpec}:${xtref.term}`, {
130
+ context: 'Git branch detection failed for external reference',
131
+ hint: 'Ensure you\'re in a git repository, or specify github_repo_branch in specs.json external_specs configuration',
132
+ details: `${error.message}. Using default: main`
133
+ });
122
134
  xtref.branch = 'main';
123
135
  }
124
136
  });
@@ -153,7 +165,7 @@ function processExternalReferences(config, GITHUB_API_TOKEN) {
153
165
  }
154
166
 
155
167
  const userInput = readlineSync.question(
156
- `❌ This external reference is not a valid URL:
168
+ `❌ This external reference is not a valid URL:
157
169
 
158
170
  Repository: ${repo.url},
159
171
 
@@ -195,14 +207,22 @@ function processExternalReferences(config, GITHUB_API_TOKEN) {
195
207
  const termsDir = spec?.spec_terms_directory;
196
208
 
197
209
  if (!specDir || !termsDir) {
198
- Logger.warn(`Spec entry is missing spec_directory or spec_terms_directory: ${JSON.stringify(spec)}`);
210
+ Logger.warn(`Spec entry is missing spec_directory or spec_terms_directory`, {
211
+ context: 'Invalid specs.json configuration',
212
+ hint: 'Ensure each spec in specs.json has both "spec_directory" and "spec_terms_directory" fields',
213
+ details: `Incomplete spec entry: ${JSON.stringify(spec)}`
214
+ });
199
215
  return directories;
200
216
  }
201
217
 
202
218
  const resolvedDir = path.join(specDir, termsDir);
203
219
 
204
220
  if (!fs.existsSync(resolvedDir)) {
205
- Logger.warn(`Spec terms directory does not exist: ${resolvedDir}`);
221
+ Logger.warn(`Spec terms directory does not exist: ${resolvedDir}`, {
222
+ context: 'Directory specified in specs.json not found',
223
+ hint: 'Create the directory or update the path in specs.json. Typically this should be "spec/term-definitions" or similar',
224
+ details: `Expected path: ${resolvedDir}`
225
+ });
206
226
  return directories;
207
227
  }
208
228
 
@@ -211,7 +231,11 @@ function processExternalReferences(config, GITHUB_API_TOKEN) {
211
231
  }, []);
212
232
 
213
233
  if (specTermsDirectories.length === 0) {
214
- Logger.warn('No spec terms directories found. Skipping external reference collection.');
234
+ Logger.warn('No spec terms directories found. Skipping external reference collection', {
235
+ context: 'Cannot collect external references without valid terminology directories',
236
+ hint: 'Check specs.json configuration. Ensure spec_directory and spec_terms_directory point to existing directories',
237
+ details: 'External trefs and xrefs will not be processed'
238
+ });
215
239
  return;
216
240
  }
217
241
 
@@ -249,7 +273,7 @@ function processExternalReferences(config, GITHUB_API_TOKEN) {
249
273
  function collectExternalReferences(options = {}) {
250
274
  // Start collecting messages if requested
251
275
  const shouldCollectMessages = options.collectMessages !== false; // Collect by default
252
-
276
+
253
277
  if (shouldCollectMessages) {
254
278
  messageCollector.clearMessages();
255
279
  messageCollector.startCollecting('collectExternalReferences');
@@ -273,8 +297,11 @@ function collectExternalReferences(options = {}) {
273
297
  const GITHUB_API_TOKEN = options.pat || process.env.GITHUB_API_TOKEN;
274
298
 
275
299
  if (!GITHUB_API_TOKEN) {
276
- Logger.warn('No GitHub Personal Access Token (PAT) found. Running without authentication (may hit rate limits).');
277
- Logger.info('For better performance, set up a PAT: https://trustoverip.github.io/spec-up-t-website/docs/getting-started/github-token\n');
300
+ Logger.warn('No GitHub Personal Access Token (PAT) found. Running without authentication', {
301
+ context: 'GitHub API requests will use unauthenticated access',
302
+ hint: 'Set GITHUB_PAT environment variable to increase rate limit from 60 to 5000 requests/hour. See: https://trustoverip.github.io/spec-up-t-website/docs/getting-started/github-token',
303
+ details: 'You may hit rate limits when fetching many external references'
304
+ });
278
305
  }
279
306
 
280
307
  // Communicate that the expected external_specs array is missing entirely.
@@ -282,7 +309,7 @@ function collectExternalReferences(options = {}) {
282
309
  Logger.info(
283
310
  'No external_specs array found on the first spec entry in specs.json. External reference collection is skipped.'
284
311
  );
285
-
312
+
286
313
  if (shouldCollectMessages) {
287
314
  messageCollector.stopCollecting();
288
315
  messageCollector.saveMessages().then(path => {
@@ -300,7 +327,7 @@ function collectExternalReferences(options = {}) {
300
327
  Logger.info(
301
328
  'The external_specs array in specs.json is empty. Add external repositories to collect external references.'
302
329
  );
303
-
330
+
304
331
  if (shouldCollectMessages) {
305
332
  messageCollector.stopCollecting();
306
333
  messageCollector.saveMessages().then(path => {
@@ -333,14 +360,14 @@ function collectExternalReferences(options = {}) {
333
360
  })
334
361
  .catch(error => {
335
362
  Logger.error('Rendering failed after collecting external references.', error);
336
-
363
+
337
364
  if (shouldCollectMessages) {
338
365
  messageCollector.stopCollecting();
339
366
  messageCollector.saveMessages().catch(() => {
340
367
  // Silent fail on save error
341
368
  });
342
369
  }
343
-
370
+
344
371
  throw error;
345
372
  });
346
373
  } else {
@@ -17,7 +17,11 @@ function validateReferences(references, definitions, render) {
17
17
  }
18
18
  );
19
19
  if (unresolvedRefs.length > 0) {
20
- Logger.warn(`Unresolved References: ${unresolvedRefs.join(',')}`);
20
+ Logger.warn(`Unresolved References: ${unresolvedRefs.join(',')}`, {
21
+ context: 'These terms are referenced in your spec but not defined',
22
+ hint: 'Add [[def: term]] definitions for these terms in your terminology files, or check for typos in [[ref: term]] references',
23
+ details: `Count: ${unresolvedRefs.length} unresolved term(s)`
24
+ });
21
25
  }
22
26
 
23
27
  const danglingDefs = [];
@@ -38,7 +42,11 @@ function validateReferences(references, definitions, render) {
38
42
  }
39
43
  })
40
44
  if (danglingDefs.length > 0) {
41
- Logger.warn(`Dangling Definitions: ${danglingDefs.join(',')}`);
45
+ Logger.warn(`Dangling Definitions: ${danglingDefs.join(',')}`, {
46
+ context: 'These terms are defined but never referenced in your spec',
47
+ hint: 'Add [[ref: term]] references where needed.',
48
+ details: `Count: ${danglingDefs.length} unused definition(s)`
49
+ });
42
50
  }
43
51
  }
44
52
 
@@ -71,7 +79,11 @@ async function fetchExternalSpecs(spec) {
71
79
  const msg = f.error.response
72
80
  ? `HTTP ${f.error.response.status} for ${f.url}`
73
81
  : `Network error for ${f.url}: ${f.error.message}`;
74
- Logger.error("External spec fetch failed:", msg);
82
+ Logger.error("External spec fetch failed", {
83
+ context: `Attempting to fetch external terminology from: ${f.url}`,
84
+ hint: 'Verify the URL is correct in specs.json under external_specs[].gh_page. Ensure the external spec has published their GitHub Pages site',
85
+ details: msg
86
+ });
75
87
  });
76
88
  }
77
89
 
@@ -93,7 +105,11 @@ async function fetchExternalSpecs(spec) {
93
105
 
94
106
  return extractedTerms;
95
107
  } catch (e) {
96
- Logger.error("Unexpected error in fetchExternalSpecs:", e);
108
+ Logger.error("Unexpected error in fetchExternalSpecs", {
109
+ context: 'Failed while fetching terms from external specifications',
110
+ hint: 'Check your internet connection and verify all external_specs URLs in specs.json are valid. Run with GITHUB_PAT set if accessing private repos',
111
+ details: e.message
112
+ });
97
113
  return [];
98
114
  }
99
115
  }
@@ -132,7 +148,7 @@ async function mergeXrefTermsIntoAllXTrefs(xrefTerms, outputPathJSON, outputPath
132
148
  if (existingIndex >= 0) {
133
149
  // Get the existing entry to check if it's a tref
134
150
  const existingXtref = allXTrefs.xtrefs[existingIndex];
135
-
151
+
136
152
  // Update existing entry - preserve the existing metadata but add/update content
137
153
  allXTrefs.xtrefs[existingIndex] = {
138
154
  ...existingXtref,
@@ -151,26 +167,34 @@ async function mergeXrefTermsIntoAllXTrefs(xrefTerms, outputPathJSON, outputPath
151
167
 
152
168
  if (isExternalTref && isTref) {
153
169
  // Build a readable list of source files for the error message
154
- const sourceFilesList = existingXtref.sourceFile
155
- ? existingXtref.sourceFile
170
+ const sourceFilesList = existingXtref.sourceFile
171
+ ? existingXtref.sourceFile
156
172
  : (existingXtref.sourceFiles || []).map(sf => sf.file).join(', ');
157
-
173
+
158
174
  // Construct the external repository URL
159
175
  const externalRepoUrl = existingXtref.ghPageUrl || existingXtref.repoUrl || `https://github.com/${existingXtref.owner}/${existingXtref.repo}`;
160
-
161
- Logger.error(`Origin: ${sourceFilesList} 👉 NESTED TREF DETECTED: Term "${existingXtref.term}" in ${existingXtref.externalSpec} is itself a tref (transcluded from another spec). This creates a chain of external references.`);
176
+
177
+ Logger.error(`NESTED TREF DETECTED: Term "${existingXtref.term}" in ${existingXtref.externalSpec}`, {
178
+ context: `Origin: ${sourceFilesList} - This term is itself transcluded from another spec`,
179
+ hint: 'Avoid chaining external references (tref → tref). Reference the original source spec directly, or define the term locally in your spec',
180
+ details: `This creates a complex dependency chain. Repository: ${externalRepoUrl}`
181
+ });
162
182
  }
163
183
 
164
184
  if (isExternalTref && isXref) {
165
185
  // Build a readable list of source files for the warning message
166
- const sourceFilesList = existingXtref.sourceFile
167
- ? existingXtref.sourceFile
186
+ const sourceFilesList = existingXtref.sourceFile
187
+ ? existingXtref.sourceFile
168
188
  : (existingXtref.sourceFiles || []).map(sf => sf.file).join(', ');
169
-
189
+
170
190
  // Construct the external repository URL
171
191
  const externalRepoUrl = existingXtref.ghPageUrl || existingXtref.repoUrl || `https://github.com/${existingXtref.owner}/${existingXtref.repo}`;
172
-
173
- Logger.error(`Origin: ${sourceFilesList} 👉 NESTED XREF DETECTED: Term "${existingXtref.term}" in ${existingXtref.externalSpec} is itself a tref (transcluded from another spec). This xref points to a term that is already transcluded from elsewhere, creating a chain of external references. (${externalRepoUrl})`);
192
+
193
+ Logger.error(`NESTED XREF DETECTED: Term "${existingXtref.term}" in ${existingXtref.externalSpec}`, {
194
+ context: `Origin: ${sourceFilesList} - This xref points to a term that is already transcluded from elsewhere`,
195
+ hint: 'Use [[xref]] only for terms directly defined in the external spec. For nested references, either reference the original source or define the term locally',
196
+ details: `This creates a chain of external references. Repository: ${externalRepoUrl}`
197
+ });
174
198
  }
175
199
 
176
200
  matchedCount++;
@@ -191,7 +215,11 @@ async function mergeXrefTermsIntoAllXTrefs(xrefTerms, outputPathJSON, outputPath
191
215
  Logger.success(`Merged xref terms: ${matchedCount} matched, ${skippedCount} skipped (not referenced). Total entries: ${allXTrefs.xtrefs.length}`);
192
216
 
193
217
  } catch (error) {
194
- Logger.error('Error merging xref terms into allXTrefs:', error.message);
218
+ Logger.error('Error merging xref terms into allXTrefs', {
219
+ context: 'Failed while merging external reference terms into the xtrefs cache',
220
+ hint: 'This may indicate corrupted cache files in .cache/xtrefs/. Try deleting the .cache directory and running the build again',
221
+ details: error.message
222
+ });
195
223
  }
196
224
  }
197
225
 
@@ -219,18 +247,18 @@ function extractTermsFromHtml(externalSpec, html) {
219
247
  batch.each((index, termElement) => {
220
248
  try {
221
249
  const $termElement = $(termElement);
222
-
250
+
223
251
  // The id can be on the dt element itself OR on span(s) inside it
224
252
  // Some terms have multiple nested spans with different IDs (e.g., aliases)
225
253
  // We need to extract ALL term IDs from the dt element
226
254
  const termIds = [];
227
-
255
+
228
256
  // First check if dt itself has an id
229
257
  const dtId = $termElement.attr('id');
230
258
  if (dtId && dtId.includes('term:')) {
231
259
  termIds.push(dtId.replace('term:', ''));
232
260
  }
233
-
261
+
234
262
  // Then find all spans with ids that contain 'term:'
235
263
  $termElement.find('span[id*="term:"]').each((i, span) => {
236
264
  const spanId = $(span).attr('id');
@@ -241,7 +269,7 @@ function extractTermsFromHtml(externalSpec, html) {
241
269
  }
242
270
  }
243
271
  });
244
-
272
+
245
273
  // Skip if no valid term IDs found
246
274
  if (termIds.length === 0) {
247
275
  return;
@@ -252,12 +280,12 @@ function extractTermsFromHtml(externalSpec, html) {
252
280
  const dtClasses = $termElement.attr('class');
253
281
  const classArray = dtClasses ? dtClasses.split(/\s+/).filter(Boolean) : [];
254
282
  const termClasses = classArray.filter(cls => cls === 'term-local' || cls === 'term-external');
255
-
283
+
256
284
  const dd = $termElement.next('dd');
257
285
 
258
286
  if (dd.length > 0) {
259
287
  const ddContent = $.html(dd); // Store the complete DD content once
260
-
288
+
261
289
  // Create a term object for each ID found in this dt element
262
290
  // This handles cases where one term definition has multiple aliases
263
291
  termIds.forEach(termName => {
@@ -275,7 +303,11 @@ function extractTermsFromHtml(externalSpec, html) {
275
303
  });
276
304
  }
277
305
  } catch (termError) {
278
- Logger.warn(`Error processing term in ${externalSpec}:`, termError.message);
306
+ Logger.warn(`Error processing term in ${externalSpec}`, {
307
+ context: 'Failed to extract a specific term from external spec HTML',
308
+ hint: 'The external spec may have malformed HTML or non-standard term definitions. Contact the spec maintainer if this persists',
309
+ details: termError.message
310
+ });
279
311
  }
280
312
  });
281
313
 
@@ -289,7 +321,11 @@ function extractTermsFromHtml(externalSpec, html) {
289
321
  return terms;
290
322
 
291
323
  } catch (error) {
292
- Logger.error(`Error extracting terms from external spec '${externalSpec}':`, error.message);
324
+ Logger.error(`Error extracting terms from external spec '${externalSpec}'`, {
325
+ context: 'Failed while parsing HTML from external spec to extract term definitions',
326
+ hint: 'Verify that the external spec is a valid spec-up-t generated document with a proper terms-and-definitions section',
327
+ details: error.message
328
+ });
293
329
  return [];
294
330
  }
295
331
  }