spec-up-t 1.3.1 → 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.
Files changed (130) hide show
  1. package/.github/copilot-instructions.md +13 -0
  2. package/assets/compiled/body.js +17 -11
  3. package/assets/compiled/head.css +6 -4
  4. package/assets/css/collapse-definitions.css +0 -1
  5. package/assets/css/create-pdf.css +4 -2
  6. package/assets/css/create-term-filter.css +4 -4
  7. package/assets/css/definition-buttons-container.css +60 -0
  8. package/assets/css/insert-trefs.css +7 -0
  9. package/assets/css/sidebar-toc.css +2 -1
  10. package/assets/css/terms-and-definitions.css +73 -22
  11. package/assets/js/add-href-to-snapshot-link.js +16 -9
  12. package/assets/js/addAnchorsToTerms.js +1 -1
  13. package/assets/js/charts.js +10 -0
  14. package/assets/js/collapse-definitions.js +13 -2
  15. package/assets/js/collapse-meta-info.js +11 -9
  16. package/assets/js/definition-button-container-utils.js +82 -0
  17. package/assets/js/edit-term-buttons.js +77 -20
  18. package/assets/js/github-issues.js +35 -0
  19. package/assets/js/github-repo-info.js +144 -0
  20. package/assets/js/highlight-heading-plus-sibling-nodes.test.js +18 -0
  21. package/assets/js/insert-trefs.js +62 -13
  22. package/assets/js/mermaid-diagrams.js +11 -0
  23. package/assets/js/terminology-section-utility-container/README.md +107 -0
  24. package/assets/js/terminology-section-utility-container/create-alphabet-index.js +17 -0
  25. package/assets/js/{create-term-filter.js → terminology-section-utility-container/create-term-filter.js} +11 -44
  26. package/assets/js/terminology-section-utility-container/hide-show-utility-container.js +21 -0
  27. package/assets/js/terminology-section-utility-container/search.js +203 -0
  28. package/assets/js/terminology-section-utility-container.js +203 -0
  29. package/assets/js/tooltips.js +283 -0
  30. package/config/asset-map.json +24 -16
  31. package/index.js +57 -390
  32. package/package.json +5 -2
  33. package/src/add-remove-xref-source.js +20 -21
  34. package/src/collect-external-references.js +8 -337
  35. package/src/collect-external-references.test.js +440 -33
  36. package/src/configure.js +8 -109
  37. package/src/create-docx.js +7 -6
  38. package/src/create-pdf.js +15 -14
  39. package/src/freeze-spec-data.js +46 -0
  40. package/src/git-info.test.js +76 -0
  41. package/src/health-check/destination-gitignore-checker.js +5 -3
  42. package/src/health-check/external-specs-checker.js +5 -4
  43. package/src/health-check/specs-configuration-checker.js +2 -1
  44. package/src/health-check/term-references-checker.js +5 -3
  45. package/src/health-check/terms-intro-checker.js +2 -1
  46. package/src/health-check/tref-term-checker.js +8 -7
  47. package/src/health-check.js +8 -7
  48. package/src/init.js +3 -2
  49. package/src/install-from-boilerplate/add-gitignore-entries.js +3 -2
  50. package/src/install-from-boilerplate/add-scripts-keys.js +5 -4
  51. package/src/install-from-boilerplate/boilerplate/README.md +1 -1
  52. package/src/install-from-boilerplate/boilerplate/spec/example-markup-in-markdown.md +1 -1
  53. package/src/install-from-boilerplate/boilerplate/spec/spec-head.md +1 -1
  54. package/src/install-from-boilerplate/boilerplate/specs.json +2 -1
  55. package/src/install-from-boilerplate/config-scripts-keys.js +3 -3
  56. package/src/install-from-boilerplate/copy-boilerplate.js +2 -1
  57. package/src/install-from-boilerplate/copy-system-files.js +4 -3
  58. package/src/install-from-boilerplate/custom-update.js +12 -1
  59. package/src/install-from-boilerplate/help.txt +1 -1
  60. package/src/install-from-boilerplate/menu.sh +6 -6
  61. package/src/json-key-validator.js +17 -11
  62. package/src/markdown-it/README.md +207 -0
  63. package/src/markdown-it/definition-lists.js +397 -0
  64. package/src/markdown-it/index.js +83 -0
  65. package/src/markdown-it/link-enhancement.js +98 -0
  66. package/src/markdown-it/plugins.js +118 -0
  67. package/src/markdown-it/table-enhancement.js +97 -0
  68. package/src/markdown-it/template-tag-syntax.js +152 -0
  69. package/src/parsers/index.js +16 -0
  70. package/src/parsers/spec-parser.js +152 -0
  71. package/src/parsers/spec-parser.test.js +109 -0
  72. package/src/parsers/template-tag-parser.js +277 -0
  73. package/src/parsers/template-tag-parser.test.js +107 -0
  74. package/src/pipeline/configuration/configure-starterpack.js +200 -0
  75. package/src/{create-external-specs-list.js → pipeline/configuration/create-external-specs-list.js} +13 -12
  76. package/src/{create-term-index.js → pipeline/configuration/create-term-index.js} +19 -18
  77. package/src/{create-versions-index.js → pipeline/configuration/create-versions-index.js} +4 -3
  78. package/src/{insert-term-index.js → pipeline/configuration/insert-term-index.js} +2 -2
  79. package/src/pipeline/configuration/prepare-spec-configuration.js +70 -0
  80. package/src/pipeline/parsing/apply-markdown-it-extensions.js +35 -0
  81. package/src/pipeline/parsing/create-markdown-parser.js +94 -0
  82. package/src/pipeline/parsing/create-markdown-parser.test.js +49 -0
  83. package/src/{html-dom-processor.js → pipeline/postprocessing/definition-list-postprocessor.js} +69 -10
  84. package/src/{escape-handler.js → pipeline/preprocessing/escape-processor.js} +3 -1
  85. package/src/{fix-markdown-files.js → pipeline/preprocessing/normalize-terminology-markdown.js} +41 -31
  86. package/src/pipeline/references/collect-external-references.js +307 -0
  87. package/src/pipeline/references/external-references-service.js +231 -0
  88. package/src/pipeline/references/fetch-terms-from-index.js +198 -0
  89. package/src/pipeline/references/match-term.js +34 -0
  90. package/src/{collectExternalReferences/matchTerm.test.js → pipeline/references/match-term.test.js} +8 -2
  91. package/src/pipeline/references/process-xtrefs-data.js +94 -0
  92. package/src/pipeline/references/xtref-utils.js +166 -0
  93. package/src/pipeline/rendering/render-spec-document.js +146 -0
  94. package/src/pipeline/rendering/render-utils.js +154 -0
  95. package/src/utils/LOGGER.md +81 -0
  96. package/src/utils/{doesUrlExist.js → does-url-exist.js} +4 -3
  97. package/src/utils/fetch.js +5 -4
  98. package/src/utils/file-opener.js +3 -2
  99. package/src/utils/git-info.js +77 -0
  100. package/src/utils/logger.js +74 -0
  101. package/src/utils/regex-patterns.js +471 -0
  102. package/src/utils/regex-patterns.test.js +281 -0
  103. package/templates/template.html +56 -21
  104. package/assets/js/create-alphabet-index.js +0 -60
  105. package/assets/js/hide-show-utility-container.js +0 -16
  106. package/assets/js/index.js +0 -87
  107. package/assets/js/search.js +0 -365
  108. package/src/collectExternalReferences/fetchTermsFromIndex.js +0 -284
  109. package/src/collectExternalReferences/matchTerm.js +0 -32
  110. package/src/collectExternalReferences/processXTrefsData.js +0 -108
  111. package/src/freeze.js +0 -90
  112. package/src/markdown-it-extensions.js +0 -395
  113. package/src/references.js +0 -114
  114. /package/assets/css/{bootstrap.min.css → embedded-libraries/bootstrap.min.css} +0 -0
  115. /package/assets/css/{prism.css → embedded-libraries/prism.css} +0 -0
  116. /package/assets/css/{prism.dark.css → embedded-libraries/prism.dark.css} +0 -0
  117. /package/assets/css/{prism.default.css → embedded-libraries/prism.default.css} +0 -0
  118. /package/assets/js/{bootstrap.bundle.min.js → embedded-libraries/bootstrap.bundle.min.js} +0 -0
  119. /package/assets/js/{chart.js → embedded-libraries/chart.js} +0 -0
  120. /package/assets/js/{diff.min.js → embedded-libraries/diff.min.js} +0 -0
  121. /package/assets/js/{font-awesome.js → embedded-libraries/font-awesome.js} +0 -0
  122. /package/assets/js/{mermaid.js → embedded-libraries/mermaid.js} +0 -0
  123. /package/assets/js/{notyf.js → embedded-libraries/notyf.js} +0 -0
  124. /package/assets/js/{popper.js → embedded-libraries/popper.js} +0 -0
  125. /package/assets/js/{prism.dark.js → embedded-libraries/prism.dark.js} +0 -0
  126. /package/assets/js/{prism.default.js → embedded-libraries/prism.default.js} +0 -0
  127. /package/assets/js/{prism.js → embedded-libraries/prism.js} +0 -0
  128. /package/assets/js/{tippy.js → embedded-libraries/tippy.js} +0 -0
  129. /package/src/{escape-mechanism.js → pipeline/preprocessing/escape-placeholder-utils.js} +0 -0
  130. /package/src/utils/{isLineWithDefinition.js → is-line-with-definition.js} +0 -0
@@ -1,23 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { shouldProcessFile } = require('./utils/file-filter');
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
- // Check if term has a definition
43
- if (!hasDefinition(result, insertIndex)) {
44
- result.splice(insertIndex, 0, '', '~ No local definition found.', '');
45
- modified = true;
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
- console.error(`❌ Error while trying to fix the markdown in file ${fileName}: ${err}`);
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 fixMarkdownFiles(directory) {
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 fixMarkdownFiles recursively
149
- fixMarkdownFiles(itemPath);
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
- console.error(`❌ Error reading directory: ${err}`);
165
+ Logger.error('Error reading directory: %o', err);
156
166
  }
157
167
  }
158
168
 
159
169
  module.exports = {
160
- fixMarkdownFiles
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
+ }