spec-up-t 1.4.1-beta.3 → 1.5.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.
@@ -11,7 +11,7 @@ const configScriptsKeys = {
11
11
  "menu": "bash ./node_modules/spec-up-t/src/install-from-boilerplate/menu.sh",
12
12
  "addremovexrefsource": "node --no-warnings -e \"require('spec-up-t/src/add-remove-xref-source.js')\"",
13
13
  "configure": "node --no-warnings -e \"require('spec-up-t/src/configure.js')\"",
14
- "healthCheck": "node --no-warnings -e \"require('spec-up-t/src/health-check.js')\"",
14
+ "healthCheck": "node --no-warnings ./node_modules/spec-up-t/src/health-check.js",
15
15
  "custom-update": "npm update && node -e \"require('spec-up-t/src/install-from-boilerplate/custom-update.js')\""
16
16
  };
17
17
 
@@ -46,19 +46,7 @@ Our extensions override default renderer rules and add custom inline parsing rul
46
46
  - Creates template tokens with parsed information
47
47
  - Provides a renderer rule to convert template tokens to HTML
48
48
 
49
- ### 3. `link-enhancement.js`
50
-
51
- **Purpose**: Adds path-based attributes to links for CSS styling and JavaScript targeting.
52
-
53
- **Features**:
54
-
55
- - Extracts domain and path segments from URLs
56
- - Adds `path-0`, `path-1`, etc. attributes to anchor tags
57
- - Special handling for auto-detected links (linkify)
58
-
59
- **How it works**: Overrides `link_open` and `link_close` renderer rules.
60
-
61
- ### 4. `definition-lists.js`
49
+ ### 3. `definition-lists.js`
62
50
 
63
51
  **Purpose**: Advanced processing of definition lists for terminology and reference management.
64
52
 
@@ -75,7 +63,7 @@ Our extensions override default renderer rules and add custom inline parsing rul
75
63
  - Uses helper functions to analyze token structure and content
76
64
  - Applies CSS classes based on term types and context
77
65
 
78
- ### 5. `index.js`
66
+ ### 4. `index.js`
79
67
 
80
68
  **Purpose**: Main orchestrator that applies all enhancements in the correct order.
81
69
 
@@ -12,7 +12,6 @@
12
12
  *
13
13
  * - TABLE ENHANCEMENT: Bootstrap styling and responsive wrappers
14
14
  * - TEMPLATE-TAG SYNTAX: Custom [[template-tag:args]] syntax processing
15
- * - LINK ENHANCEMENT: Path-based attributes for links
16
15
  * - DEFINITION LISTS: Advanced terminology and reference list handling
17
16
  *
18
17
  * This modular approach makes the code more maintainable and easier to
@@ -22,7 +21,6 @@
22
21
  // Import all the specialized enhancement modules
23
22
  const applyTableEnhancements = require('./table-enhancement');
24
23
  const applyTemplateTagSyntax = require('./template-tag-syntax');
25
- const applyLinkEnhancements = require('./link-enhancement');
26
24
  const applyDefinitionListEnhancements = require('./definition-lists');
27
25
 
28
26
  /**
@@ -64,10 +62,7 @@ function applyMarkdownItExtensions(md, templates = []) {
64
62
  // 2. Template-tag syntax - should be applied early as other modules may depend on it
65
63
  applyTemplateTagSyntax(md, templates);
66
64
 
67
- // 3. Link enhancements - independent, can be applied anytime
68
- applyLinkEnhancements(md);
69
-
70
- // 4. Definition lists - depends on template-tag syntax for term type detection
65
+ // 3. Definition lists - depends on template-tag syntax for term type detection
71
66
  applyDefinitionListEnhancements(md);
72
67
 
73
68
  // The markdown-it instance is now fully enhanced and ready for use
@@ -79,5 +74,4 @@ module.exports = applyMarkdownItExtensions;
79
74
  // Also export individual modules for fine-grained control if needed
80
75
  module.exports.tableEnhancements = applyTableEnhancements;
81
76
  module.exports.templateTagSyntax = applyTemplateTagSyntax;
82
- module.exports.linkEnhancements = applyLinkEnhancements;
83
77
  module.exports.definitionLists = applyDefinitionListEnhancements;
@@ -14,6 +14,7 @@
14
14
  const { findExternalSpecByKey } = require('../pipeline/references/external-references-service.js');
15
15
  const { lookupXrefTerm } = require('../pipeline/rendering/render-utils.js');
16
16
  const { whitespace, htmlComments, contentCleaning, externalReferences } = require('../utils/regex-patterns');
17
+ const Logger = require('../utils/logger.js');
17
18
 
18
19
  /**
19
20
  * Extracts the current file from token content for source tracking
@@ -135,11 +136,16 @@ function parseRef(globalState, primary) {
135
136
  * Uses primaryDisplayTerm concept: shows first alias if available, otherwise shows the term itself
136
137
  * @param {Object} config - Configuration containing external specs
137
138
  * @param {Object} token - The markdown-it token
138
- * @returns {string} HTML anchor element linking to external term
139
+ * @returns {string} HTML anchor element linking to external term or error span if unresolved
139
140
  */
140
141
  function parseXref(config, token) {
141
142
  const externalSpec = findExternalSpecByKey(config, token.info.args[0]);
142
- const url = externalSpec?.gh_page || '#';
143
+
144
+ // If external spec cannot be found, return error indicator
145
+ if (!externalSpec) {
146
+ return `<span class="no-xref-found-message" title="External spec '${token.info.args[0]}' not found in configuration">xref cannot be resolved</span>`;
147
+ }
148
+
143
149
  const termName = token.info.args[1];
144
150
  const aliases = token.info.args.slice(2).filter(Boolean); // Get all aliases after the term
145
151
  const term = termName.replace(whitespace.oneOrMore, '-').toLowerCase();
@@ -148,8 +154,11 @@ function parseXref(config, token) {
148
154
  // Determine the primary display term (first alias if available, otherwise original term)
149
155
  const primaryDisplayTerm = aliases.length > 0 ? aliases[0] : termName;
150
156
 
157
+ // Build the href attribute using the external spec's gh_page
158
+ const href = `${externalSpec.gh_page}#term:${term}`;
159
+
151
160
  // Build link attributes with both local and external href capabilities
152
- let linkAttributes = `class="x-term-reference term-reference" data-local-href="#term:${token.info.args[0]}:${term}" href="${url}#term:${term}"`;
161
+ let linkAttributes = `class="x-term-reference term-reference" data-local-href="#term:${token.info.args[0]}:${term}" href="${href}"`;
153
162
 
154
163
  // Add tooltip content if term definition is available
155
164
  if (xrefTerm && xrefTerm.content) {
@@ -262,6 +271,13 @@ function processXTrefObject(xtref) {
262
271
  if (allAliases.length > 0) {
263
272
  xtrefObject.firstXrefAlias = allAliases[0];
264
273
  }
274
+
275
+ // Log error if xref has more than one alias
276
+ // xref should only have 0 or 1 alias, unlike tref which supports multiple aliases
277
+ if (allAliases.length > 1) {
278
+ const extraAliases = allAliases.slice(1).join(', ');
279
+ Logger.error(`Invalid xref syntax: [[xref: ${xtrefObject.externalSpec}, ${xtrefObject.term}, ${allAliases.join(', ')}]] has ${allAliases.length} aliases. Only the first alias "${allAliases[0]}" will be used. Extra aliases ignored: ${extraAliases}.`);
280
+ }
265
281
  }
266
282
 
267
283
  return xtrefObject;
@@ -65,7 +65,8 @@ function sortDefinitionTermsInHtml(html) {
65
65
  });
66
66
 
67
67
  // Return the modified HTML
68
- return dom.serialize();
68
+ // Extract only the body's innerHTML to avoid wrapping in <html><head></head><body> tags
69
+ return dom.window.document.body.innerHTML;
69
70
  }
70
71
 
71
72
  /**
@@ -340,7 +341,8 @@ function fixDefinitionListStructure(html) {
340
341
  }
341
342
 
342
343
  // Return the fixed HTML
343
- return dom.serialize();
344
+ // Extract only the body's innerHTML to avoid wrapping in <html><head></head><body> tags
345
+ return dom.window.document.body.innerHTML;
344
346
  }
345
347
 
346
348
  module.exports = {
@@ -13,6 +13,7 @@ const fs = require('fs-extra');
13
13
  const readlineSync = require('readline-sync');
14
14
 
15
15
  const Logger = require('../../utils/logger');
16
+ const messageCollector = require('../../utils/message-collector');
16
17
  const { shouldProcessFile } = require('../../utils/file-filter');
17
18
  const { getCurrentBranch } = require('../../utils/git-info');
18
19
  const { addNewXTrefsFromMarkdown, isXTrefInAnyFile } = require('./xtref-utils');
@@ -24,7 +25,8 @@ const { processXTrefObject } = require('../../parsers/template-tag-parser');
24
25
  * automated callers of `collectExternalReferences` continue to receive a fully rendered spec.
25
26
  */
26
27
  function renderSpecification() {
27
- require('../../../index.js')({ nowatch: true });
28
+ // Pass skipClear to preserve messages from collectExternalReferences
29
+ require('../../../index.js')({ nowatch: true, skipClear: true });
28
30
  }
29
31
 
30
32
  /**
@@ -232,7 +234,7 @@ function processExternalReferences(config, GITHUB_API_TOKEN) {
232
234
  );
233
235
 
234
236
  fileContents.forEach((content, filename) => {
235
- addNewXTrefsFromMarkdown(content, allXTrefs, filename, processXTrefObject);
237
+ addNewXTrefsFromMarkdown(content, allXTrefs, filename, processXTrefObject, externalSpecsRepos);
236
238
  });
237
239
 
238
240
  extendXTrefs(config, allXTrefs.xtrefs);
@@ -242,9 +244,17 @@ function processExternalReferences(config, GITHUB_API_TOKEN) {
242
244
  /**
243
245
  * Public entry point for the external reference collection stage.
244
246
  *
245
- * @param {{ pat?: string }} options - Optional overrides (GitHub PAT).
247
+ * @param {{ pat?: string, collectMessages?: boolean }} options - Optional overrides (GitHub PAT, message collection).
246
248
  */
247
249
  function collectExternalReferences(options = {}) {
250
+ // Start collecting messages if requested
251
+ const shouldCollectMessages = options.collectMessages !== false; // Collect by default
252
+
253
+ if (shouldCollectMessages) {
254
+ messageCollector.clearMessages();
255
+ messageCollector.startCollecting('collectExternalReferences');
256
+ }
257
+
248
258
  const config = fs.readJsonSync('specs.json');
249
259
  const normalizedConfig = normalizeSpecConfiguration(config, {
250
260
  noSpecsMessage: 'No specs defined in specs.json. Nothing to collect.'
@@ -252,6 +262,10 @@ function collectExternalReferences(options = {}) {
252
262
 
253
263
  // Bail out immediately if the specs.json file lacks the required specs collection.
254
264
  if (!normalizedConfig) {
265
+ if (shouldCollectMessages) {
266
+ messageCollector.stopCollecting();
267
+ messageCollector.saveMessages();
268
+ }
255
269
  return;
256
270
  }
257
271
 
@@ -268,7 +282,16 @@ function collectExternalReferences(options = {}) {
268
282
  Logger.info(
269
283
  'No external_specs array found on the first spec entry in specs.json. External reference collection is skipped.'
270
284
  );
271
- renderSpecification();
285
+
286
+ if (shouldCollectMessages) {
287
+ messageCollector.stopCollecting();
288
+ messageCollector.saveMessages().then(path => {
289
+ Logger.success(`Console messages saved to: ${path}`);
290
+ renderSpecification();
291
+ });
292
+ } else {
293
+ renderSpecification();
294
+ }
272
295
  return;
273
296
  }
274
297
 
@@ -277,7 +300,16 @@ function collectExternalReferences(options = {}) {
277
300
  Logger.info(
278
301
  'The external_specs array in specs.json is empty. Add external repositories to collect external references.'
279
302
  );
280
- renderSpecification();
303
+
304
+ if (shouldCollectMessages) {
305
+ messageCollector.stopCollecting();
306
+ messageCollector.saveMessages().then(path => {
307
+ Logger.success(`Console messages saved to: ${path}`);
308
+ renderSpecification();
309
+ });
310
+ } else {
311
+ renderSpecification();
312
+ }
281
313
  return;
282
314
  }
283
315
 
@@ -287,15 +319,40 @@ function collectExternalReferences(options = {}) {
287
319
  if (pipeline && typeof pipeline.then === 'function') {
288
320
  return pipeline
289
321
  .then(result => {
290
- renderSpecification();
291
- return result;
322
+ if (shouldCollectMessages) {
323
+ messageCollector.stopCollecting();
324
+ return messageCollector.saveMessages().then(path => {
325
+ Logger.success(`Console messages saved to: ${path}`);
326
+ renderSpecification();
327
+ return result;
328
+ });
329
+ } else {
330
+ renderSpecification();
331
+ return result;
332
+ }
292
333
  })
293
334
  .catch(error => {
294
335
  Logger.error('Rendering failed after collecting external references.', error);
336
+
337
+ if (shouldCollectMessages) {
338
+ messageCollector.stopCollecting();
339
+ messageCollector.saveMessages().catch(() => {
340
+ // Silent fail on save error
341
+ });
342
+ }
343
+
295
344
  throw error;
296
345
  });
297
346
  } else {
298
- renderSpecification();
347
+ if (shouldCollectMessages) {
348
+ messageCollector.stopCollecting();
349
+ messageCollector.saveMessages().then(path => {
350
+ Logger.success(`Console messages saved to: ${path}`);
351
+ renderSpecification();
352
+ });
353
+ } else {
354
+ renderSpecification();
355
+ }
299
356
  return pipeline;
300
357
  }
301
358
  }
@@ -17,7 +17,7 @@ function validateReferences(references, definitions, render) {
17
17
  }
18
18
  );
19
19
  if (unresolvedRefs.length > 0) {
20
- Logger.info('Unresolved References:', unresolvedRefs);
20
+ Logger.warn(`Unresolved References: ${unresolvedRefs.join(',')}`);
21
21
  }
22
22
 
23
23
  const danglingDefs = [];
@@ -38,7 +38,7 @@ function validateReferences(references, definitions, render) {
38
38
  }
39
39
  })
40
40
  if (danglingDefs.length > 0) {
41
- Logger.info('Dangling Definitions:', danglingDefs);
41
+ Logger.warn(`Dangling Definitions: ${danglingDefs.join(',')}`);
42
42
  }
43
43
  }
44
44
 
@@ -117,6 +117,10 @@ async function mergeXrefTermsIntoAllXTrefs(xrefTerms, outputPathJSON, outputPath
117
117
 
118
118
  // Add xref terms to the allXTrefs structure
119
119
  // Mark them with source: 'xref' to distinguish from tref entries
120
+ // Track how many terms were matched vs skipped for logging purposes
121
+ let matchedCount = 0;
122
+ let skippedCount = 0;
123
+
120
124
  xrefTerms.forEach(xrefTerm => {
121
125
  // Check if this term already exists (match by externalSpec and term only)
122
126
  // Don't filter by source because entries from markdown scanning don't have source field
@@ -126,20 +130,54 @@ async function mergeXrefTermsIntoAllXTrefs(xrefTerms, outputPathJSON, outputPath
126
130
  );
127
131
 
128
132
  if (existingIndex >= 0) {
133
+ // Get the existing entry to check if it's a tref
134
+ const existingXtref = allXTrefs.xtrefs[existingIndex];
135
+
129
136
  // Update existing entry - preserve the existing metadata but add/update content
130
137
  allXTrefs.xtrefs[existingIndex] = {
131
- ...allXTrefs.xtrefs[existingIndex],
138
+ ...existingXtref,
132
139
  content: xrefTerm.content, // Update the content from fetched HTML
140
+ classes: xrefTerm.classes || [], // Update classes from dt element
133
141
  source: xrefTerm.source, // Add source field
134
142
  termId: xrefTerm.termId, // Add termId if not present
135
143
  lastUpdated: new Date().toISOString()
136
144
  };
145
+
146
+ // Check if this is a tref to an external tref (nested tref)
147
+ // A term with 'term-external' class means it's transcluded from another spec
148
+ const isExternalTref = xrefTerm.classes && xrefTerm.classes.includes('term-external');
149
+ const isTref = existingXtref.sourceFiles && existingXtref.sourceFiles.some(sf => sf.type === 'tref');
150
+ const isXref = existingXtref.sourceFiles && existingXtref.sourceFiles.some(sf => sf.type === 'xref');
151
+
152
+ if (isExternalTref && isTref) {
153
+ // Build a readable list of source files for the error message
154
+ const sourceFilesList = existingXtref.sourceFile
155
+ ? existingXtref.sourceFile
156
+ : (existingXtref.sourceFiles || []).map(sf => sf.file).join(', ');
157
+
158
+ // Construct the external repository URL
159
+ 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.`);
162
+ }
163
+
164
+ if (isExternalTref && isXref) {
165
+ // Build a readable list of source files for the warning message
166
+ const sourceFilesList = existingXtref.sourceFile
167
+ ? existingXtref.sourceFile
168
+ : (existingXtref.sourceFiles || []).map(sf => sf.file).join(', ');
169
+
170
+ // Construct the external repository URL
171
+ 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})`);
174
+ }
175
+
176
+ matchedCount++;
137
177
  } else {
138
- // Add new entry (this term wasn't referenced in the markdown)
139
- allXTrefs.xtrefs.push({
140
- ...xrefTerm,
141
- lastUpdated: new Date().toISOString()
142
- });
178
+ // Skip terms that are not referenced in the local markdown files
179
+ // This prevents bloating the xtrefs-data.json with unreferenced terms
180
+ skippedCount++;
143
181
  }
144
182
  });
145
183
 
@@ -150,7 +188,7 @@ async function mergeXrefTermsIntoAllXTrefs(xrefTerms, outputPathJSON, outputPath
150
188
  const stringReadyForFileWrite = `const allXTrefs = ${allXTrefsStr};`;
151
189
  fs.writeFileSync(outputPathJS, stringReadyForFileWrite, 'utf8');
152
190
 
153
- Logger.success(`Merged ${xrefTerms.length} xref terms into allXTrefs. Total entries: ${allXTrefs.xtrefs.length}`);
191
+ Logger.success(`Merged xref terms: ${matchedCount} matched, ${skippedCount} skipped (not referenced). Total entries: ${allXTrefs.xtrefs.length}`);
154
192
 
155
193
  } catch (error) {
156
194
  Logger.error('Error merging xref terms into allXTrefs:', error.message);
@@ -208,6 +246,12 @@ function extractTermsFromHtml(externalSpec, html) {
208
246
  if (termIds.length === 0) {
209
247
  return;
210
248
  }
249
+
250
+ // Extract classes from the <dt> element to determine if it's a local or external term.
251
+ // This helps identify if a tref to an external resource is itself a tref (term-external).
252
+ const dtClasses = $termElement.attr('class');
253
+ const classArray = dtClasses ? dtClasses.split(/\s+/).filter(Boolean) : [];
254
+ const termClasses = classArray.filter(cls => cls === 'term-local' || cls === 'term-external');
211
255
 
212
256
  const dd = $termElement.next('dd');
213
257
 
@@ -221,9 +265,10 @@ function extractTermsFromHtml(externalSpec, html) {
221
265
  externalSpec: externalSpec,
222
266
  term: termName,
223
267
  content: ddContent,
268
+ classes: termClasses, // CSS classes from dt element (term-local or term-external)
224
269
  // Add metadata for consistency with tref structure
225
270
  source: 'xref', // Distinguish from tref entries
226
- termId: `term:${externalSpec}:${termName}`, // Fully qualified term ID
271
+ termId: `term:${termName}`, // Term ID matches the actual HTML anchor format
227
272
  };
228
273
 
229
274
  terms.push(termObj);
@@ -12,6 +12,16 @@ const Logger = require('../../utils/logger');
12
12
 
13
13
  const CACHE_DIR = getPath('githubcache');
14
14
 
15
+ /**
16
+ * Retrieves the latest commit hash for a specific file in a GitHub repository.
17
+ *
18
+ * @param {string} token - GitHub API token for authentication
19
+ * @param {string} owner - Repository owner
20
+ * @param {string} repo - Repository name
21
+ * @param {string} filePath - Path to the file within the repository
22
+ * @param {Object} headers - HTTP headers for the request
23
+ * @returns {Promise<string|null>} The commit SHA or null if not found
24
+ */
15
25
  async function getFileCommitHash(token, owner, repo, filePath, headers) {
16
26
  try {
17
27
  const normalizedPath = filePath.replace(/^\//, '');
@@ -31,6 +41,27 @@ async function getFileCommitHash(token, owner, repo, filePath, headers) {
31
41
  }
32
42
  }
33
43
 
44
+ /**
45
+ * Fetches all term definitions from an external specification repository.
46
+ * Extracts terms from the generated index.html file and includes CSS classes
47
+ * to identify whether terms are local definitions or external references.
48
+ *
49
+ * @param {string} token - GitHub API token for authentication
50
+ * @param {string} owner - Repository owner
51
+ * @param {string} repo - Repository name
52
+ * @param {Object} [options={}] - Additional options
53
+ * @param {string} [options.ghPageUrl] - GitHub Pages URL for the repository
54
+ * @returns {Promise<Object|null>} Object containing:
55
+ * - {number} timestamp - Unix timestamp when terms were fetched
56
+ * - {string} repository - Full repository path (owner/repo)
57
+ * - {Array<Object>} terms - Array of term objects, each containing:
58
+ * - {string} term - The term identifier
59
+ * - {string} definition - HTML definition content
60
+ * - {Array<string>} classes - CSS classes from the dt element ('term-local' or 'term-external')
61
+ * - {string|null} sha - Commit hash
62
+ * - {string|null} avatarUrl - Avatar URL
63
+ * - {string} outputFileName - Name of the cached file
64
+ */
34
65
  async function fetchAllTermsFromIndex(token, owner, repo, options = {}) {
35
66
  try {
36
67
  const headers = token ? { Authorization: `token ${token}` } : {};
@@ -118,6 +149,12 @@ async function fetchAllTermsFromIndex(token, owner, repo, options = {}) {
118
149
  return;
119
150
  }
120
151
 
152
+ // Extract classes from the <dt> element to determine if it's a local or external term.
153
+ // This helps identify if a tref to an external resource is itself a tref (term-external)
154
+ // or a local definition (term-local).
155
+ const dtClasses = dt.className ? dt.className.split(/\s+/).filter(Boolean) : [];
156
+ const termClasses = dtClasses.filter(cls => cls === 'term-local' || cls === 'term-external');
157
+
121
158
  const definitions = [];
122
159
  let pointer = dt.nextElementSibling;
123
160
  while (pointer && pointer.tagName.toLowerCase() === 'dd') {
@@ -125,7 +162,11 @@ async function fetchAllTermsFromIndex(token, owner, repo, options = {}) {
125
162
  pointer = pointer.nextElementSibling;
126
163
  }
127
164
 
128
- terms.push({ term: termText, definition: definitions.join('\n') });
165
+ terms.push({
166
+ term: termText,
167
+ definition: definitions.join('\n'),
168
+ classes: termClasses
169
+ });
129
170
  });
130
171
 
131
172
  const timestamp = Date.now();
@@ -163,6 +204,25 @@ async function fetchAllTermsFromIndex(token, owner, repo, options = {}) {
163
204
  }
164
205
  }
165
206
 
207
+ /**
208
+ * Fetches a specific term definition from an external specification repository.
209
+ * This is a convenience wrapper around fetchAllTermsFromIndex that returns
210
+ * only a single matching term.
211
+ *
212
+ * @param {string} token - GitHub API token for authentication
213
+ * @param {string} term - The specific term to fetch
214
+ * @param {string} owner - Repository owner
215
+ * @param {string} repo - Repository name
216
+ * @param {string} termsDir - Terms directory (currently unused but kept for compatibility)
217
+ * @param {Object} [options={}] - Additional options
218
+ * @param {string} [options.ghPageUrl] - GitHub Pages URL for the repository
219
+ * @returns {Promise<Object|null>} Object containing:
220
+ * - {string} term - The term identifier
221
+ * - {string} content - HTML definition content
222
+ * - {Array<string>} classes - CSS classes ('term-local' or 'term-external')
223
+ * - {string|null} sha - Commit hash
224
+ * - {Object} repository - Repository metadata
225
+ */
166
226
  async function fetchTermsFromIndex(token, term, owner, repo, termsDir, options = {}) {
167
227
  const allTermsData = await fetchAllTermsFromIndex(token, owner, repo, options);
168
228
  if (!allTermsData || !Array.isArray(allTermsData.terms)) {
@@ -179,6 +239,7 @@ async function fetchTermsFromIndex(token, term, owner, repo, termsDir, options =
179
239
  return {
180
240
  term: foundTerm.term,
181
241
  content: foundTerm.definition,
242
+ classes: foundTerm.classes || [],
182
243
  sha: allTermsData.sha,
183
244
  repository: {
184
245
  owner: {
@@ -69,6 +69,40 @@ async function processXTrefsData(allXTrefs, GITHUB_API_TOKEN, outputPathJSON, ou
69
69
  xtref.commitHash = allTermsData.sha;
70
70
  xtref.content = foundTerm.definition;
71
71
  xtref.avatarUrl = allTermsData.avatarUrl;
72
+ // Copy the classes array from the foundTerm to identify if this is a local or external term.
73
+ // This helps determine if a tref to an external resource is itself a tref (term-external).
74
+ xtref.classes = foundTerm.classes || [];
75
+
76
+ // Check if this is a tref to an external tref (nested tref)
77
+ // A term with 'term-external' class means it's transcluded from another spec
78
+ const isExternalTref = foundTerm.classes && foundTerm.classes.includes('term-external');
79
+ const isTref = xtref.sourceFiles && xtref.sourceFiles.some(sf => sf.type === 'tref');
80
+ const isXref = xtref.sourceFiles && xtref.sourceFiles.some(sf => sf.type === 'xref');
81
+
82
+ if (isExternalTref && isTref) {
83
+ // Build a readable list of source files for the error message
84
+ const sourceFilesList = xtref.sourceFile
85
+ ? xtref.sourceFile
86
+ : (xtref.sourceFiles || []).map(sf => sf.file).join(', ');
87
+
88
+ // Construct the external repository URL
89
+ const externalRepoUrl = xtref.ghPageUrl || xtref.repoUrl || `https://github.com/${xtref.owner}/${xtref.repo}`;
90
+
91
+ Logger.error(`Origin: ${sourceFilesList} 👉 NESTED TREF DETECTED: Term "${xtref.term}" in ${xtref.externalSpec} is itself a tref (transcluded from another spec). This creates a chain of external references.`);
92
+ }
93
+
94
+ if (isExternalTref && isXref) {
95
+ // Build a readable list of source files for the warning message
96
+ const sourceFilesList = xtref.sourceFile
97
+ ? xtref.sourceFile
98
+ : (xtref.sourceFiles || []).map(sf => sf.file).join(', ');
99
+
100
+ // Construct the external repository URL
101
+ const externalRepoUrl = xtref.ghPageUrl || xtref.repoUrl || `https://github.com/${xtref.owner}/${xtref.repo}`;
102
+
103
+ Logger.error(`Origin: ${sourceFilesList} 👉 NESTED XREF DETECTED: Term "${xtref.term}" in ${xtref.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})`);
104
+ }
105
+
72
106
  Logger.success(`Match found for term: ${xtref.term} in ${xtref.externalSpec}`);
73
107
  } else {
74
108
  xtref.commitHash = 'not found';
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  const { externalReferences, utils } = require('../../utils/regex-patterns');
10
+ const Logger = require('../../utils/logger');
10
11
 
11
12
  /**
12
13
  * Checks if a specific xtref is present in the markdown content.
@@ -43,13 +44,30 @@ function isXTrefInAnyFile(xtref, fileContents) {
43
44
  * @param {object} xtrefObject - Pre-parsed xtref object from template-tag-parser
44
45
  * @param {{ xtrefs: Array<object> }} allXTrefs - Aggregated reference collection
45
46
  * @param {string|null} filename - Originating filename for bookkeeping
47
+ * @param {Array<object>|null} externalSpecs - Array of external_specs from specs.json for validation
46
48
  * @returns {{ xtrefs: Array<object> }} Updated reference collection
47
49
  */
48
- function addXtrefToCollection(xtrefObject, allXTrefs, filename = null) {
50
+ function addXtrefToCollection(xtrefObject, allXTrefs, filename = null, externalSpecs = null) {
49
51
  const referenceType = xtrefObject.referenceType;
50
52
  const cleanXTrefObj = { ...xtrefObject };
51
53
  delete cleanXTrefObj.referenceType;
52
54
 
55
+ // Validate that the external spec exists in specs.json configuration
56
+ if (externalSpecs && Array.isArray(externalSpecs)) {
57
+ const externalSpecExists = externalSpecs.some(
58
+ spec => spec.external_spec === cleanXTrefObj.externalSpec
59
+ );
60
+
61
+ if (!externalSpecExists) {
62
+ const availableSpecs = externalSpecs.map(s => s.external_spec).join(', ');
63
+ Logger.error(
64
+ `External spec "${cleanXTrefObj.externalSpec}" not found in specs.json configuration. ` +
65
+ `Available external specs: ${availableSpecs}. ` +
66
+ `Check [[${referenceType}: ${cleanXTrefObj.externalSpec}, ${cleanXTrefObj.term}]] in ${filename || 'unknown file'}`
67
+ );
68
+ }
69
+ }
70
+
53
71
  const existingIndex = allXTrefs?.xtrefs?.findIndex(existingXTref =>
54
72
  existingXTref.term === cleanXTrefObj.term &&
55
73
  existingXTref.externalSpec === cleanXTrefObj.externalSpec
@@ -135,9 +153,10 @@ function addXtrefToCollection(xtrefObject, allXTrefs, filename = null) {
135
153
  * @param {{ xtrefs: Array<object> }} allXTrefs - Aggregated reference collection.
136
154
  * @param {string|null} filename - Originating filename for bookkeeping.
137
155
  * @param {function} processXTrefObject - Parsing function for xtref strings.
156
+ * @param {Array<object>|null} externalSpecs - Array of external_specs from specs.json for validation.
138
157
  * @returns {{ xtrefs: Array<object> }} Updated reference collection.
139
158
  */
140
- function addNewXTrefsFromMarkdown(markdownContent, allXTrefs, filename = null, processXTrefObject) {
159
+ function addNewXTrefsFromMarkdown(markdownContent, allXTrefs, filename = null, processXTrefObject, externalSpecs = null) {
141
160
  if (!processXTrefObject) {
142
161
  throw new Error('processXTrefObject function is required. Import from template-tag-parser.');
143
162
  }
@@ -152,7 +171,7 @@ function addNewXTrefsFromMarkdown(markdownContent, allXTrefs, filename = null, p
152
171
 
153
172
  xtrefs.forEach(rawXtref => {
154
173
  const xtrefObject = processXTrefObject(rawXtref);
155
- addXtrefToCollection(xtrefObject, allXTrefs, filename);
174
+ addXtrefToCollection(xtrefObject, allXTrefs, filename, externalSpecs);
156
175
  });
157
176
 
158
177
  return allXTrefs;
@@ -111,7 +111,6 @@ async function render(spec, assets, sharedVars, config, template, assetsGlobal,
111
111
  assetsHead: assets.head,
112
112
  assetsBody: assets.body,
113
113
  assetsSvg: assets.svg,
114
- features: Object.keys(features).join(' '),
115
114
  externalReferences: '', // No longer inject DOM HTML - xrefs are in allXTrefs
116
115
  xtrefsData: createScriptElementWithXTrefDataForEmbeddingInHtml(),
117
116
  specLogo: spec.logo,