spec-up-t 1.3.0 → 1.4.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/.github/copilot-instructions.md +13 -0
- package/assets/compiled/body.js +18 -12
- package/assets/compiled/head.css +8 -6
- package/assets/css/collapse-definitions.css +0 -1
- package/assets/css/counter.css +10 -22
- package/assets/css/create-pdf.css +4 -2
- package/assets/css/create-term-filter.css +4 -4
- package/assets/css/definition-buttons-container.css +60 -0
- package/assets/css/{pdf-download.css → download-pdf-docx.css} +9 -5
- package/assets/css/insert-trefs.css +7 -0
- package/assets/css/sidebar-toc.css +2 -1
- package/assets/css/terms-and-definitions.css +73 -22
- package/assets/js/add-href-to-snapshot-link.js +16 -9
- package/assets/js/addAnchorsToTerms.js +2 -2
- package/assets/js/charts.js +10 -0
- package/assets/js/collapse-definitions.js +13 -2
- package/assets/js/collapse-meta-info.js +11 -9
- package/assets/js/definition-button-container-utils.js +82 -0
- package/assets/js/download-pdf-docx.js +68 -0
- package/assets/js/edit-term-buttons.js +77 -20
- package/assets/js/github-issues.js +35 -0
- package/assets/js/github-repo-info.js +144 -0
- package/assets/js/highlight-heading-plus-sibling-nodes.test.js +18 -0
- package/assets/js/insert-trefs.js +62 -13
- package/assets/js/mermaid-diagrams.js +11 -0
- package/assets/js/terminology-section-utility-container/README.md +107 -0
- package/assets/js/terminology-section-utility-container/create-alphabet-index.js +17 -0
- package/assets/js/{create-term-filter.js → terminology-section-utility-container/create-term-filter.js} +11 -44
- package/assets/js/terminology-section-utility-container/hide-show-utility-container.js +21 -0
- package/assets/js/terminology-section-utility-container/search.js +203 -0
- package/assets/js/terminology-section-utility-container.js +203 -0
- package/assets/js/tooltips.js +283 -0
- package/config/asset-map.json +26 -18
- package/index.js +57 -390
- package/package.json +5 -2
- package/src/add-remove-xref-source.js +20 -21
- package/src/collect-external-references.js +8 -337
- package/src/collect-external-references.test.js +440 -33
- package/src/configure.js +8 -109
- package/src/create-docx.js +7 -6
- package/src/create-pdf.js +15 -14
- package/src/freeze-spec-data.js +46 -0
- package/src/git-info.test.js +76 -0
- package/src/health-check/destination-gitignore-checker.js +5 -3
- package/src/health-check/external-specs-checker.js +5 -4
- package/src/health-check/specs-configuration-checker.js +2 -1
- package/src/health-check/term-references-checker.js +5 -3
- package/src/health-check/terms-intro-checker.js +2 -1
- package/src/health-check/tref-term-checker.js +8 -7
- package/src/health-check.js +8 -7
- package/src/init.js +3 -2
- package/src/install-from-boilerplate/add-gitignore-entries.js +3 -2
- package/src/install-from-boilerplate/add-scripts-keys.js +5 -4
- package/src/install-from-boilerplate/boilerplate/.github/workflows/menu.yml +74 -97
- package/src/install-from-boilerplate/boilerplate/README.md +1 -1
- package/src/install-from-boilerplate/boilerplate/spec/example-markup-in-markdown.md +1 -1
- package/src/install-from-boilerplate/boilerplate/spec/spec-head.md +2 -2
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/composability.md +3 -0
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/compost.md +3 -0
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/fertilizer.md +3 -0
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/mulch.md +3 -0
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/pruning.md +3 -0
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/seedling.md +3 -0
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/soil.md +11 -0
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/watering.md +3 -0
- package/src/install-from-boilerplate/boilerplate/specs.json +24 -10
- package/src/install-from-boilerplate/config-scripts-keys.js +3 -3
- package/src/install-from-boilerplate/config-system-files.js +0 -1
- package/src/install-from-boilerplate/copy-boilerplate.js +2 -1
- package/src/install-from-boilerplate/copy-system-files.js +4 -3
- package/src/install-from-boilerplate/custom-update.js +12 -1
- package/src/install-from-boilerplate/help.txt +1 -1
- package/src/install-from-boilerplate/menu.sh +6 -6
- package/src/json-key-validator.js +17 -11
- package/src/markdown-it/README.md +207 -0
- package/src/markdown-it/definition-lists.js +397 -0
- package/src/markdown-it/index.js +83 -0
- package/src/markdown-it/link-enhancement.js +98 -0
- package/src/markdown-it/plugins.js +118 -0
- package/src/markdown-it/table-enhancement.js +97 -0
- package/src/markdown-it/template-tag-syntax.js +152 -0
- package/src/parsers/index.js +16 -0
- package/src/parsers/spec-parser.js +152 -0
- package/src/parsers/spec-parser.test.js +109 -0
- package/src/parsers/template-tag-parser.js +277 -0
- package/src/parsers/template-tag-parser.test.js +107 -0
- package/src/pipeline/configuration/configure-starterpack.js +200 -0
- package/src/{create-external-specs-list.js → pipeline/configuration/create-external-specs-list.js} +13 -12
- package/src/{create-term-index.js → pipeline/configuration/create-term-index.js} +19 -18
- package/src/{create-versions-index.js → pipeline/configuration/create-versions-index.js} +4 -3
- package/src/{insert-term-index.js → pipeline/configuration/insert-term-index.js} +2 -2
- package/src/pipeline/configuration/prepare-spec-configuration.js +70 -0
- package/src/pipeline/parsing/apply-markdown-it-extensions.js +35 -0
- package/src/pipeline/parsing/create-markdown-parser.js +94 -0
- package/src/pipeline/parsing/create-markdown-parser.test.js +49 -0
- package/src/{html-dom-processor.js → pipeline/postprocessing/definition-list-postprocessor.js} +69 -10
- package/src/{escape-handler.js → pipeline/preprocessing/escape-processor.js} +3 -1
- package/src/{fix-markdown-files.js → pipeline/preprocessing/normalize-terminology-markdown.js} +41 -31
- package/src/pipeline/references/collect-external-references.js +307 -0
- package/src/pipeline/references/external-references-service.js +231 -0
- package/src/pipeline/references/fetch-terms-from-index.js +198 -0
- package/src/pipeline/references/match-term.js +34 -0
- package/src/{collectExternalReferences/matchTerm.test.js → pipeline/references/match-term.test.js} +8 -2
- package/src/pipeline/references/process-xtrefs-data.js +94 -0
- package/src/pipeline/references/xtref-utils.js +166 -0
- package/src/pipeline/rendering/render-spec-document.js +146 -0
- package/src/pipeline/rendering/render-utils.js +154 -0
- package/src/utils/LOGGER.md +81 -0
- package/src/utils/{doesUrlExist.js → does-url-exist.js} +4 -3
- package/src/utils/fetch.js +5 -4
- package/src/utils/file-opener.js +3 -2
- package/src/utils/git-info.js +77 -0
- package/src/utils/logger.js +74 -0
- package/src/utils/regex-patterns.js +471 -0
- package/src/utils/regex-patterns.test.js +281 -0
- package/templates/template.html +56 -21
- package/assets/js/create-alphabet-index.js +0 -60
- package/assets/js/hide-show-utility-container.js +0 -16
- package/assets/js/index.js +0 -87
- package/assets/js/pdf-download.js +0 -46
- package/assets/js/search.js +0 -365
- package/src/collectExternalReferences/fetchTermsFromIndex.js +0 -284
- package/src/collectExternalReferences/matchTerm.js +0 -32
- package/src/collectExternalReferences/processXTrefsData.js +0 -108
- package/src/freeze.js +0 -90
- package/src/install-from-boilerplate/boilerplate/.github/workflows/fetch-and-push-xrefs.yml.old +0 -42
- package/src/install-from-boilerplate/boilerplate/.github/workflows/render-specs.yml +0 -47
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/term-1.md +0 -13
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/term-2.md +0 -3
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/term-3.md +0 -3
- package/src/install-from-boilerplate/boilerplate/spec/terms-definitions/term-4.md +0 -3
- package/src/markdown-it-extensions.js +0 -395
- package/src/references.js +0 -114
- /package/assets/css/{bootstrap.min.css → embedded-libraries/bootstrap.min.css} +0 -0
- /package/assets/css/{prism.css → embedded-libraries/prism.css} +0 -0
- /package/assets/css/{prism.dark.css → embedded-libraries/prism.dark.css} +0 -0
- /package/assets/css/{prism.default.css → embedded-libraries/prism.default.css} +0 -0
- /package/assets/js/{bootstrap.bundle.min.js → embedded-libraries/bootstrap.bundle.min.js} +0 -0
- /package/assets/js/{chart.js → embedded-libraries/chart.js} +0 -0
- /package/assets/js/{diff.min.js → embedded-libraries/diff.min.js} +0 -0
- /package/assets/js/{font-awesome.js → embedded-libraries/font-awesome.js} +0 -0
- /package/assets/js/{mermaid.js → embedded-libraries/mermaid.js} +0 -0
- /package/assets/js/{notyf.js → embedded-libraries/notyf.js} +0 -0
- /package/assets/js/{popper.js → embedded-libraries/popper.js} +0 -0
- /package/assets/js/{prism.dark.js → embedded-libraries/prism.dark.js} +0 -0
- /package/assets/js/{prism.default.js → embedded-libraries/prism.default.js} +0 -0
- /package/assets/js/{prism.js → embedded-libraries/prism.js} +0 -0
- /package/assets/js/{tippy.js → embedded-libraries/tippy.js} +0 -0
- /package/src/{escape-mechanism.js → pipeline/preprocessing/escape-placeholder-utils.js} +0 -0
- /package/src/utils/{isLineWithDefinition.js → is-line-with-definition.js} +0 -0
package/src/{fix-markdown-files.js → pipeline/preprocessing/normalize-terminology-markdown.js}
RENAMED
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { shouldProcessFile } = require('
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Checks if a term has a definition starting from the given line index
|
|
7
|
-
* @param {string[]} lines - Array of file lines
|
|
8
|
-
* @param {number} startIndex - Index to start checking from
|
|
9
|
-
* @returns {boolean} - True if definition exists, false otherwise
|
|
10
|
-
*/
|
|
11
|
-
function hasDefinition(lines, startIndex) {
|
|
12
|
-
for (let i = startIndex; i < lines.length; i++) {
|
|
13
|
-
const line = lines[i].trim();
|
|
14
|
-
if (line === '') continue; // Skip empty lines
|
|
15
|
-
if (line.startsWith('~')) return true; // Found definition
|
|
16
|
-
if (line.startsWith('[[def:') || line.startsWith('[[tref:')) return false; // Found next term
|
|
17
|
-
if (line.length > 0) return false; // Found other content
|
|
18
|
-
}
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
3
|
+
const { shouldProcessFile } = require('../../utils/file-filter.js');
|
|
4
|
+
const Logger = require('../../utils/logger.js');
|
|
21
5
|
|
|
22
6
|
/**
|
|
23
7
|
* Handles specific functionality for `[[def:` and `[[tref:` lines
|
|
@@ -31,19 +15,41 @@ function processDefLines(lines) {
|
|
|
31
15
|
for (let i = 0; i < result.length; i++) {
|
|
32
16
|
if (result[i].startsWith('[[def:') || result[i].startsWith('[[tref:')) {
|
|
33
17
|
let insertIndex = i + 1;
|
|
34
|
-
|
|
18
|
+
|
|
35
19
|
// Ensure a blank line immediately follows `[[def:` and `[[tref:` lines
|
|
36
20
|
if (insertIndex < result.length && result[insertIndex].trim() !== '') {
|
|
37
21
|
result.splice(insertIndex, 0, ''); // Insert blank line
|
|
38
22
|
insertIndex++;
|
|
39
23
|
modified = true;
|
|
40
24
|
}
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
25
|
+
|
|
26
|
+
// No additional content needed - both [[def: and [[tref: lines can exist standalone
|
|
27
|
+
// The HTML post-processing will handle the proper structure
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { lines: result, modified };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Prepends `~ ` to appropriate lines
|
|
36
|
+
* @param {string[]} lines - Array of file lines
|
|
37
|
+
* @returns {object} - Object containing modified lines and modification status
|
|
38
|
+
*/
|
|
39
|
+
function prependTildeToLines(lines) {
|
|
40
|
+
const result = [...lines];
|
|
41
|
+
let modified = false;
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < result.length; i++) {
|
|
44
|
+
if (
|
|
45
|
+
!result[i].startsWith('[[def:') &&
|
|
46
|
+
!result[i].startsWith('[[tref:') &&
|
|
47
|
+
result[i].trim() !== '' &&
|
|
48
|
+
!result[i].startsWith('~ ') &&
|
|
49
|
+
!result[i].trim().startsWith('<!--')
|
|
50
|
+
) {
|
|
51
|
+
result[i] = `~ ${result[i]}`;
|
|
52
|
+
modified = true;
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -117,6 +123,10 @@ function processMarkdownFile(filePath, fileName) {
|
|
|
117
123
|
lines = spacingResult.lines;
|
|
118
124
|
modified = modified || spacingResult.modified;
|
|
119
125
|
|
|
126
|
+
const tildeResult = prependTildeToLines(lines);
|
|
127
|
+
lines = tildeResult.lines;
|
|
128
|
+
modified = modified || tildeResult.modified;
|
|
129
|
+
|
|
120
130
|
const newlineResult = ensureTrailingNewline(lines);
|
|
121
131
|
lines = newlineResult.lines;
|
|
122
132
|
modified = modified || newlineResult.modified;
|
|
@@ -127,7 +137,7 @@ function processMarkdownFile(filePath, fileName) {
|
|
|
127
137
|
fs.writeFileSync(filePath, newData, 'utf8');
|
|
128
138
|
}
|
|
129
139
|
} catch (err) {
|
|
130
|
-
|
|
140
|
+
Logger.error('Error while trying to fix the markdown in file %s: %o', fileName, err);
|
|
131
141
|
}
|
|
132
142
|
}
|
|
133
143
|
|
|
@@ -136,7 +146,7 @@ function processMarkdownFile(filePath, fileName) {
|
|
|
136
146
|
* @param {string} directory - The directory to process
|
|
137
147
|
* @returns {void}
|
|
138
148
|
*/
|
|
139
|
-
function
|
|
149
|
+
function normalizeTerminologyMarkdown(directory) {
|
|
140
150
|
try {
|
|
141
151
|
// Read the contents of the directory synchronously
|
|
142
152
|
const items = fs.readdirSync(directory, { withFileTypes: true });
|
|
@@ -145,17 +155,17 @@ function fixMarkdownFiles(directory) {
|
|
|
145
155
|
items.forEach(item => {
|
|
146
156
|
const itemPath = path.join(directory, item.name);
|
|
147
157
|
if (item.isDirectory()) {
|
|
148
|
-
// If the item is a directory, call
|
|
149
|
-
|
|
158
|
+
// If the item is a directory, call normalizeTerminologyMarkdown recursively
|
|
159
|
+
normalizeTerminologyMarkdown(itemPath);
|
|
150
160
|
} else if (item.isFile() && shouldProcessFile(item.name)) {
|
|
151
161
|
processMarkdownFile(itemPath, item.name);
|
|
152
162
|
}
|
|
153
163
|
});
|
|
154
164
|
} catch (err) {
|
|
155
|
-
|
|
165
|
+
Logger.error('Error reading directory: %o', err);
|
|
156
166
|
}
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
module.exports = {
|
|
160
|
-
|
|
170
|
+
normalizeTerminologyMarkdown
|
|
161
171
|
};
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Orchestrates external reference collection and enrichment within the pipeline.
|
|
3
|
+
*
|
|
4
|
+
* This module coordinates three stages:
|
|
5
|
+
* 1. Scan local markdown for `[[xref:...]]` / `[[tref:...]]` references.
|
|
6
|
+
* 2. Enrich references with metadata derived from `specs.json`.
|
|
7
|
+
* 3. Persist the combined dataset for downstream consumers.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
require('dotenv').config();
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs-extra');
|
|
13
|
+
const readlineSync = require('readline-sync');
|
|
14
|
+
|
|
15
|
+
const Logger = require('../../utils/logger');
|
|
16
|
+
const { shouldProcessFile } = require('../../utils/file-filter');
|
|
17
|
+
const { getCurrentBranch } = require('../../utils/git-info');
|
|
18
|
+
const { addNewXTrefsFromMarkdown, isXTrefInAnyFile } = require('./xtref-utils');
|
|
19
|
+
const { processXTrefObject } = require('../../parsers/template-tag-parser');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reuses the main rendering entry point once reference collection has refreshed the cache.
|
|
23
|
+
* Keeping this invocation on the Node side (instead of the menu script) guarantees that
|
|
24
|
+
* automated callers of `collectExternalReferences` continue to receive a fully rendered spec.
|
|
25
|
+
*/
|
|
26
|
+
function renderSpecification() {
|
|
27
|
+
require('../../../index.js')({ nowatch: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Normalizes the specs structure pulled from specs.json so callers can rely on predictable shapes.
|
|
32
|
+
*
|
|
33
|
+
* The helper guarantees:
|
|
34
|
+
* - the returned `specs` array is always an array;
|
|
35
|
+
* - callers get the first spec entry (or an empty object) as `primarySpec`;
|
|
36
|
+
* - callers receive a safe `externalSpecsRepos` array for follow-up validation or iteration;
|
|
37
|
+
* - when the specs array is empty, an explanatory error is logged and `null` is returned to signal abortion.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} config - Parsed specs configuration.
|
|
40
|
+
* @param {{ noSpecsMessage: string }} options - Allows callers to tailor the abort message for their context.
|
|
41
|
+
* @returns {{ specs: Array<object>, primarySpec: object, externalSpecsRepos: Array<object>, hasExternalSpecsField: boolean } | null}
|
|
42
|
+
*/
|
|
43
|
+
function normalizeSpecConfiguration(config, { noSpecsMessage }) {
|
|
44
|
+
const specs = Array.isArray(config?.specs) ? config.specs : [];
|
|
45
|
+
|
|
46
|
+
if (specs.length === 0) {
|
|
47
|
+
Logger.error(noSpecsMessage);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const primarySpec = specs[0] ?? {};
|
|
52
|
+
const hasExternalSpecsField = Array.isArray(primarySpec.external_specs);
|
|
53
|
+
const externalSpecsRepos = hasExternalSpecsField ? primarySpec.external_specs : [];
|
|
54
|
+
|
|
55
|
+
return { specs, primarySpec, externalSpecsRepos, hasExternalSpecsField };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Augments reference records with repository metadata pulled from `specs.json`.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} config - Parsed specs configuration.
|
|
62
|
+
* @param {Array<object>} xtrefs - Reference entries collected from markdown.
|
|
63
|
+
*/
|
|
64
|
+
function extendXTrefs(config, xtrefs) {
|
|
65
|
+
if (config.specs[0].external_specs_repos) {
|
|
66
|
+
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');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const repoLookup = new Map();
|
|
71
|
+
const siteLookup = new Map();
|
|
72
|
+
|
|
73
|
+
config.specs.forEach(spec => {
|
|
74
|
+
spec.external_specs.forEach(repo => {
|
|
75
|
+
if (repo.external_spec) {
|
|
76
|
+
repoLookup.set(repo.external_spec, repo);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
spec.external_specs
|
|
81
|
+
.filter(externalSpec => typeof externalSpec === 'object' && externalSpec !== null)
|
|
82
|
+
.forEach(externalSpec => {
|
|
83
|
+
const key = Object.keys(externalSpec)[0];
|
|
84
|
+
siteLookup.set(key, externalSpec[key]);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
xtrefs.forEach(xtref => {
|
|
89
|
+
xtref.repoUrl = null;
|
|
90
|
+
xtref.terms_dir = null;
|
|
91
|
+
xtref.owner = null;
|
|
92
|
+
xtref.repo = null;
|
|
93
|
+
xtref.site = null;
|
|
94
|
+
xtref.branch = null;
|
|
95
|
+
|
|
96
|
+
const repo = repoLookup.get(xtref.externalSpec);
|
|
97
|
+
if (repo) {
|
|
98
|
+
xtref.repoUrl = repo.url;
|
|
99
|
+
xtref.terms_dir = repo.terms_dir;
|
|
100
|
+
|
|
101
|
+
if (xtref.repoUrl) {
|
|
102
|
+
const urlParts = new URL(xtref.repoUrl).pathname.split('/');
|
|
103
|
+
xtref.owner = urlParts[1];
|
|
104
|
+
xtref.repo = urlParts[2];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
xtref.avatarUrl = repo.avatar_url;
|
|
108
|
+
xtref.ghPageUrl = repo.gh_page;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const site = siteLookup.get(xtref.externalSpec);
|
|
112
|
+
if (site) {
|
|
113
|
+
xtref.site = site;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
xtref.branch = getCurrentBranch();
|
|
118
|
+
} catch (error) {
|
|
119
|
+
Logger.warn(`Could not get current branch for xtref ${xtref.externalSpec}:${xtref.term}: ${error.message}`);
|
|
120
|
+
xtref.branch = 'main';
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Executes the full collection pipeline once pre-flight checks pass.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} config - Parsed specs configuration.
|
|
129
|
+
* @param {string} GITHUB_API_TOKEN - GitHub PAT used for API calls.
|
|
130
|
+
*/
|
|
131
|
+
function processExternalReferences(config, GITHUB_API_TOKEN) {
|
|
132
|
+
const { processXTrefsData } = require('./process-xtrefs-data');
|
|
133
|
+
const { doesUrlExist } = require('../../utils/does-url-exist');
|
|
134
|
+
|
|
135
|
+
const normalizedConfig = normalizeSpecConfiguration(config, {
|
|
136
|
+
noSpecsMessage: 'No specs defined in specs.json. Skipping external reference collection.'
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Abort collection when the configuration is missing mandatory specs definitions.
|
|
140
|
+
if (!normalizedConfig) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const { specs, externalSpecsRepos } = normalizedConfig;
|
|
145
|
+
|
|
146
|
+
externalSpecsRepos.forEach(repo => {
|
|
147
|
+
doesUrlExist(repo.url)
|
|
148
|
+
.then(exists => {
|
|
149
|
+
if (exists) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const userInput = readlineSync.question(
|
|
154
|
+
`❌ This external reference is not a valid URL:
|
|
155
|
+
|
|
156
|
+
Repository: ${repo.url},
|
|
157
|
+
|
|
158
|
+
Terms directory: ${repo.terms_dir}
|
|
159
|
+
|
|
160
|
+
Please fix the external references in the specs.json file that you will find at the root of your project.
|
|
161
|
+
|
|
162
|
+
Do you want to stop? (yes/no): `);
|
|
163
|
+
|
|
164
|
+
if (userInput.toLowerCase() === 'yes' || userInput.toLowerCase() === 'y') {
|
|
165
|
+
Logger.info('Stopping...');
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
.catch(error => {
|
|
170
|
+
Logger.error('Error checking URL existence:', error);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const outputDir = '.cache';
|
|
175
|
+
const xtrefsHistoryDir = path.join(outputDir, 'xtrefs-history');
|
|
176
|
+
const outputPathJSON = path.join(outputDir, 'xtrefs-data.json');
|
|
177
|
+
const outputPathJS = path.join(outputDir, 'xtrefs-data.js');
|
|
178
|
+
const outputPathJSTimeStamped = path.join(xtrefsHistoryDir, `xtrefs-data-${Date.now()}.js`);
|
|
179
|
+
|
|
180
|
+
fs.ensureDirSync(outputDir);
|
|
181
|
+
fs.ensureDirSync(xtrefsHistoryDir);
|
|
182
|
+
|
|
183
|
+
let allXTrefs = { xtrefs: [] };
|
|
184
|
+
if (fs.existsSync(outputPathJSON)) {
|
|
185
|
+
const existingXTrefs = fs.readJsonSync(outputPathJSON);
|
|
186
|
+
if (existingXTrefs?.xtrefs) {
|
|
187
|
+
allXTrefs = existingXTrefs;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const specTermsDirectories = specs.reduce((directories, spec) => {
|
|
192
|
+
const specDir = spec?.spec_directory;
|
|
193
|
+
const termsDir = spec?.spec_terms_directory;
|
|
194
|
+
|
|
195
|
+
if (!specDir || !termsDir) {
|
|
196
|
+
Logger.warn(`Spec entry is missing spec_directory or spec_terms_directory: ${JSON.stringify(spec)}`);
|
|
197
|
+
return directories;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const resolvedDir = path.join(specDir, termsDir);
|
|
201
|
+
|
|
202
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
203
|
+
Logger.warn(`Spec terms directory does not exist: ${resolvedDir}`);
|
|
204
|
+
return directories;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
directories.push(resolvedDir);
|
|
208
|
+
return directories;
|
|
209
|
+
}, []);
|
|
210
|
+
|
|
211
|
+
if (specTermsDirectories.length === 0) {
|
|
212
|
+
Logger.warn('No spec terms directories found. Skipping external reference collection.');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const fileContents = new Map();
|
|
217
|
+
|
|
218
|
+
specTermsDirectories.forEach(specDirectory => {
|
|
219
|
+
fs.readdirSync(specDirectory).forEach(file => {
|
|
220
|
+
if (!shouldProcessFile(file)) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const filePath = path.join(specDirectory, file);
|
|
225
|
+
const markdown = fs.readFileSync(filePath, 'utf8');
|
|
226
|
+
fileContents.set(file, markdown);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
allXTrefs.xtrefs = allXTrefs.xtrefs.filter(existingXTref =>
|
|
231
|
+
isXTrefInAnyFile(existingXTref, fileContents)
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
fileContents.forEach((content, filename) => {
|
|
235
|
+
addNewXTrefsFromMarkdown(content, allXTrefs, filename, processXTrefObject);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
extendXTrefs(config, allXTrefs.xtrefs);
|
|
239
|
+
return processXTrefsData(allXTrefs, GITHUB_API_TOKEN, outputPathJSON, outputPathJS, outputPathJSTimeStamped);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Public entry point for the external reference collection stage.
|
|
244
|
+
*
|
|
245
|
+
* @param {{ pat?: string }} options - Optional overrides (GitHub PAT).
|
|
246
|
+
*/
|
|
247
|
+
function collectExternalReferences(options = {}) {
|
|
248
|
+
const config = fs.readJsonSync('specs.json');
|
|
249
|
+
const normalizedConfig = normalizeSpecConfiguration(config, {
|
|
250
|
+
noSpecsMessage: 'No specs defined in specs.json. Nothing to collect.'
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Bail out immediately if the specs.json file lacks the required specs collection.
|
|
254
|
+
if (!normalizedConfig) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const { externalSpecsRepos, hasExternalSpecsField } = normalizedConfig;
|
|
259
|
+
const GITHUB_API_TOKEN = options.pat || process.env.GITHUB_API_TOKEN;
|
|
260
|
+
|
|
261
|
+
if (!GITHUB_API_TOKEN) {
|
|
262
|
+
Logger.warn('No GitHub Personal Access Token (PAT) found. Running without authentication (may hit rate limits).');
|
|
263
|
+
Logger.info('For better performance, set up a PAT: https://trustoverip.github.io/spec-up-t-website/docs/getting-started/github-token\n');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Communicate that the expected external_specs array is missing entirely.
|
|
267
|
+
if (!hasExternalSpecsField) {
|
|
268
|
+
Logger.info(
|
|
269
|
+
'No external_specs array found on the first spec entry in specs.json. External reference collection is skipped.'
|
|
270
|
+
);
|
|
271
|
+
renderSpecification();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Let the user know the array exists but contains no repositories, making collection pointless.
|
|
276
|
+
if (externalSpecsRepos.length === 0) {
|
|
277
|
+
Logger.info(
|
|
278
|
+
'The external_specs array in specs.json is empty. Add external repositories to collect external references.'
|
|
279
|
+
);
|
|
280
|
+
renderSpecification();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const pipeline = processExternalReferences(config, GITHUB_API_TOKEN);
|
|
285
|
+
|
|
286
|
+
// If the pipeline short-circuited (e.g. missing configuration), render immediately and return its value.
|
|
287
|
+
if (pipeline && typeof pipeline.then === 'function') {
|
|
288
|
+
return pipeline
|
|
289
|
+
.then(result => {
|
|
290
|
+
renderSpecification();
|
|
291
|
+
return result;
|
|
292
|
+
})
|
|
293
|
+
.catch(error => {
|
|
294
|
+
Logger.error('Rendering failed after collecting external references.', error);
|
|
295
|
+
throw error;
|
|
296
|
+
});
|
|
297
|
+
} else {
|
|
298
|
+
renderSpecification();
|
|
299
|
+
return pipeline;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = {
|
|
304
|
+
collectExternalReferences,
|
|
305
|
+
extendXTrefs,
|
|
306
|
+
processExternalReferences
|
|
307
|
+
};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
const cheerio = require("cheerio");
|
|
2
|
+
const axios = require('axios').default;
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const Logger = require('../../utils/logger.js');
|
|
5
|
+
|
|
6
|
+
const spaceRegex = /\s+/g;
|
|
7
|
+
|
|
8
|
+
function validateReferences(references, definitions, render) {
|
|
9
|
+
const unresolvedRefs = [];
|
|
10
|
+
[...new Set(references)].forEach(
|
|
11
|
+
ref => {
|
|
12
|
+
if (render.includes(`id="term:${ref.replace(spaceRegex, '-').toLowerCase()}"`)) {
|
|
13
|
+
// Reference is resolved
|
|
14
|
+
} else {
|
|
15
|
+
unresolvedRefs.push(ref);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
if (unresolvedRefs.length > 0) {
|
|
20
|
+
Logger.info('Unresolved References:', unresolvedRefs);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const danglingDefs = [];
|
|
24
|
+
definitions.forEach(def => {
|
|
25
|
+
// Handle both old array format and new object format
|
|
26
|
+
if (Array.isArray(def)) {
|
|
27
|
+
let found = def.some(term => render.includes(`href="#term:${term.replace(spaceRegex, '-').toLowerCase()}"`))
|
|
28
|
+
if (!found) {
|
|
29
|
+
danglingDefs.push(def[0]);
|
|
30
|
+
}
|
|
31
|
+
} else if (def.term) {
|
|
32
|
+
// New object format
|
|
33
|
+
const terms = [def.term, def.alias].filter(Boolean);
|
|
34
|
+
let found = terms.some(term => render.includes(`href="#term:${term.replace(spaceRegex, '-').toLowerCase()}"`))
|
|
35
|
+
if (!found) {
|
|
36
|
+
danglingDefs.push(def.term);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
if (danglingDefs.length > 0) {
|
|
41
|
+
Logger.info('Dangling Definitions:', danglingDefs);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function findExternalSpecByKey(config, key) {
|
|
46
|
+
if (!config || !config.specs) return null;
|
|
47
|
+
for (const spec of config.specs) {
|
|
48
|
+
if (spec.external_specs) {
|
|
49
|
+
for (const externalSpec of spec.external_specs) {
|
|
50
|
+
if (externalSpec.external_spec === key) {
|
|
51
|
+
return externalSpec;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function fetchExternalSpecs(spec) {
|
|
60
|
+
try {
|
|
61
|
+
let results = await Promise.all(
|
|
62
|
+
spec.external_specs.map(s => {
|
|
63
|
+
const url = s["gh_page"];
|
|
64
|
+
return axios.get(url).catch(error => ({ error, url }));
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const failed = results.filter(r => r && r.error);
|
|
69
|
+
if (failed.length > 0) {
|
|
70
|
+
failed.forEach(f => {
|
|
71
|
+
const msg = f.error.response
|
|
72
|
+
? `HTTP ${f.error.response.status} for ${f.url}`
|
|
73
|
+
: `Network error for ${f.url}: ${f.error.message}`;
|
|
74
|
+
Logger.error("External spec fetch failed:", msg);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Map results to extract terms instead of creating DOM HTML
|
|
79
|
+
const extractedTerms = [];
|
|
80
|
+
|
|
81
|
+
results
|
|
82
|
+
.map((r, index) =>
|
|
83
|
+
r && r.status === 200
|
|
84
|
+
? { externalSpec: spec.external_specs[index].external_spec, data: r.data }
|
|
85
|
+
: null
|
|
86
|
+
)
|
|
87
|
+
.filter(r => r) // Remove null values (failed fetches)
|
|
88
|
+
.forEach(r => {
|
|
89
|
+
// Extract terms from each external spec's HTML
|
|
90
|
+
const termsFromSpec = extractTermsFromHtml(r.externalSpec, r.data);
|
|
91
|
+
extractedTerms.push(...termsFromSpec);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return extractedTerms;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
Logger.error("Unexpected error in fetchExternalSpecs:", e);
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Merges xref terms from external specs into the allXTrefs structure
|
|
104
|
+
* @param {Array} xrefTerms - Array of xref term objects from fetchExternalSpecs
|
|
105
|
+
* @param {string} outputPathJSON - Path to the xtrefs-data.json file
|
|
106
|
+
* @param {string} outputPathJS - Path to the xtrefs-data.js file
|
|
107
|
+
* @returns {Promise<void>}
|
|
108
|
+
*/
|
|
109
|
+
async function mergeXrefTermsIntoAllXTrefs(xrefTerms, outputPathJSON, outputPathJS) {
|
|
110
|
+
try {
|
|
111
|
+
let allXTrefs = { xtrefs: [] };
|
|
112
|
+
|
|
113
|
+
// Load existing xtrefs data if it exists
|
|
114
|
+
if (fs.existsSync(outputPathJSON)) {
|
|
115
|
+
allXTrefs = fs.readJsonSync(outputPathJSON);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Add xref terms to the allXTrefs structure
|
|
119
|
+
// Mark them with source: 'xref' to distinguish from tref entries
|
|
120
|
+
xrefTerms.forEach(xrefTerm => {
|
|
121
|
+
// Check if this term already exists (avoid duplicates)
|
|
122
|
+
const existingIndex = allXTrefs.xtrefs.findIndex(existing =>
|
|
123
|
+
existing.externalSpec === xrefTerm.externalSpec &&
|
|
124
|
+
existing.term === xrefTerm.term &&
|
|
125
|
+
existing.source === 'xref'
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (existingIndex >= 0) {
|
|
129
|
+
// Update existing entry
|
|
130
|
+
allXTrefs.xtrefs[existingIndex] = {
|
|
131
|
+
...allXTrefs.xtrefs[existingIndex],
|
|
132
|
+
...xrefTerm,
|
|
133
|
+
lastUpdated: new Date().toISOString()
|
|
134
|
+
};
|
|
135
|
+
} else {
|
|
136
|
+
// Add new entry
|
|
137
|
+
allXTrefs.xtrefs.push({
|
|
138
|
+
...xrefTerm,
|
|
139
|
+
lastUpdated: new Date().toISOString()
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Write the updated data back to files
|
|
145
|
+
const allXTrefsStr = JSON.stringify(allXTrefs, null, 2);
|
|
146
|
+
fs.writeFileSync(outputPathJSON, allXTrefsStr, 'utf8');
|
|
147
|
+
|
|
148
|
+
const stringReadyForFileWrite = `const allXTrefs = ${allXTrefsStr};`;
|
|
149
|
+
fs.writeFileSync(outputPathJS, stringReadyForFileWrite, 'utf8');
|
|
150
|
+
|
|
151
|
+
Logger.success(`Merged ${xrefTerms.length} xref terms into allXTrefs. Total entries: ${allXTrefs.xtrefs.length}`);
|
|
152
|
+
|
|
153
|
+
} catch (error) {
|
|
154
|
+
Logger.error('Error merging xref terms into allXTrefs:', error.message);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Extracts terms and their definitions from HTML and returns them as structured data
|
|
160
|
+
* @param {string} externalSpec - The external spec identifier
|
|
161
|
+
* @param {string} html - The HTML content to parse
|
|
162
|
+
* @returns {Array} Array of term objects suitable for the allXTrefs structure
|
|
163
|
+
*/
|
|
164
|
+
function extractTermsFromHtml(externalSpec, html) {
|
|
165
|
+
try {
|
|
166
|
+
const $ = cheerio.load(html);
|
|
167
|
+
const terms = [];
|
|
168
|
+
|
|
169
|
+
const termElements = $('dl.terms-and-definitions-list dt');
|
|
170
|
+
Logger.highlight(`Found ${termElements.length} term elements in ${externalSpec} (HTML size: ${Math.round(html.length / 1024)}KB)`);
|
|
171
|
+
|
|
172
|
+
// Process terms in batches to prevent stack overflow with large datasets
|
|
173
|
+
const BATCH_SIZE = 100;
|
|
174
|
+
const totalElements = termElements.length;
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < totalElements; i += BATCH_SIZE) {
|
|
177
|
+
const batch = termElements.slice(i, i + BATCH_SIZE);
|
|
178
|
+
|
|
179
|
+
batch.each((index, termElement) => {
|
|
180
|
+
try {
|
|
181
|
+
const $termElement = $(termElement);
|
|
182
|
+
const termId = $termElement.attr('id');
|
|
183
|
+
|
|
184
|
+
// Skip elements without an id attribute or with invalid id format
|
|
185
|
+
if (!termId || !termId.includes('term:')) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const termName = termId.replace('term:', '');
|
|
190
|
+
const dd = $termElement.next('dd');
|
|
191
|
+
|
|
192
|
+
if (dd.length > 0) {
|
|
193
|
+
// Create term object compatible with allXTrefs structure
|
|
194
|
+
const termObj = {
|
|
195
|
+
externalSpec: externalSpec,
|
|
196
|
+
term: termName,
|
|
197
|
+
content: $.html(dd), // Store the complete DD content
|
|
198
|
+
// Add metadata for consistency with tref structure
|
|
199
|
+
source: 'xref', // Distinguish from tref entries
|
|
200
|
+
termId: `term:${externalSpec}:${termName}`, // Fully qualified term ID
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
terms.push(termObj);
|
|
204
|
+
}
|
|
205
|
+
} catch (termError) {
|
|
206
|
+
Logger.warn(`Error processing term in ${externalSpec}:`, termError.message);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Log progress for very large datasets
|
|
211
|
+
if (totalElements > 1000 && i % (BATCH_SIZE * 10) === 0) {
|
|
212
|
+
Logger.progress(Math.min(i + BATCH_SIZE, totalElements), totalElements, `Processing terms from ${externalSpec}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
Logger.success(`Extracted ${terms.length} terms from external spec: ${externalSpec}`);
|
|
217
|
+
return terms;
|
|
218
|
+
|
|
219
|
+
} catch (error) {
|
|
220
|
+
Logger.error(`Error extracting terms from external spec '${externalSpec}':`, error.message);
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
findExternalSpecByKey,
|
|
227
|
+
validateReferences,
|
|
228
|
+
fetchExternalSpecs,
|
|
229
|
+
extractTermsFromHtml,
|
|
230
|
+
mergeXrefTermsIntoAllXTrefs
|
|
231
|
+
}
|