spec-up-t 1.4.1 → 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.
- package/assets/compiled/body.js +2 -2
- package/assets/compiled/head.css +2 -2
- package/assets/css/embedded-libraries/bootstrap.min.css +1 -1
- package/assets/css/header-navbar.css +4 -4
- package/assets/js/github-issues.js +3 -3
- package/examples/read-console-messages.js +102 -0
- package/gulpfile.js +42 -1
- package/index.js +49 -1
- package/package.json +2 -1
- package/src/health-check.js +47 -629
- package/src/install-from-boilerplate/config-scripts-keys.js +1 -1
- package/src/markdown-it/README.md +2 -14
- package/src/markdown-it/index.js +1 -7
- package/src/parsers/template-tag-parser.js +19 -3
- package/src/pipeline/postprocessing/definition-list-postprocessor.js +4 -2
- package/src/pipeline/references/collect-external-references.js +65 -8
- package/src/pipeline/references/external-references-service.js +55 -10
- package/src/pipeline/references/fetch-terms-from-index.js +62 -1
- package/src/pipeline/references/process-xtrefs-data.js +34 -0
- package/src/pipeline/references/xtref-utils.js +22 -3
- package/src/pipeline/rendering/render-spec-document.js +0 -1
- package/src/run-healthcheck.js +177 -0
- package/src/utils/logger.js +15 -1
- package/src/utils/message-collector.js +144 -0
- package/templates/template.html +6 -6
- package/test/message-collector.test.js +286 -0
- package/src/markdown-it/link-enhancement.js +0 -98
|
@@ -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
|
|
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. `
|
|
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
|
-
###
|
|
66
|
+
### 4. `index.js`
|
|
79
67
|
|
|
80
68
|
**Purpose**: Main orchestrator that applies all enhancements in the correct order.
|
|
81
69
|
|
package/src/markdown-it/index.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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="${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
...
|
|
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
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
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 ${
|
|
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:${
|
|
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({
|
|
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,
|