spec-up-t 1.2.5 → 1.2.7

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 (37) hide show
  1. package/assets/compiled/body.js +9 -9
  2. package/assets/compiled/head.css +6 -5
  3. package/assets/css/collapse-definitions.css +41 -0
  4. package/assets/css/create-pdf.css +339 -0
  5. package/assets/css/horizontal-scroll-hint.css +6 -0
  6. package/assets/css/image-full-size.css +1 -5
  7. package/assets/css/index.css +5 -0
  8. package/assets/css/search.css +8 -8
  9. package/assets/css/terms-and-definitions.css +3 -3
  10. package/assets/js/add-bootstrap-classes-to-images.js +2 -5
  11. package/assets/js/collapse-definitions.js +260 -34
  12. package/assets/js/collapse-meta-info.js +37 -10
  13. package/assets/js/collapsibleMenu.js +0 -24
  14. package/assets/js/create-term-filter.js +36 -18
  15. package/assets/js/hide-show-utility-container.js +0 -1
  16. package/assets/js/horizontal-scroll-hint.js +2 -2
  17. package/assets/js/image-full-size.js +0 -2
  18. package/assets/js/insert-trefs.js +135 -49
  19. package/assets/js/search.js +34 -20
  20. package/{src → config}/asset-map.json +1 -0
  21. package/gulpfile.js +1 -1
  22. package/index.js +3 -3
  23. package/jest.config.js +20 -0
  24. package/package.json +3 -2
  25. package/src/collectExternalReferences/fetchTermsFromIndex.1.js +340 -0
  26. package/src/collectExternalReferences/fetchTermsFromIndex.js +1 -1
  27. package/src/collectExternalReferences/processXTrefsData.js +1 -1
  28. package/src/create-pdf.js +330 -21
  29. package/src/health-check/{output-gitignore-checker.js → destination-gitignore-checker.js} +40 -25
  30. package/src/health-check.js +38 -38
  31. package/src/markdown-it-extensions.js +2 -2
  32. package/src/utils/README.md +35 -0
  33. package/src/utils/file-opener.js +89 -0
  34. package/assets/css/pdf-styles.css +0 -170
  35. package/branches.md +0 -17
  36. package/src/README.md +0 -98
  37. /package/{src/config → config}/paths.js +0 -0
@@ -1,21 +1,31 @@
1
1
  /**
2
- * @fileoverview Handles the insertion of transcluded external references (xrefs) into HTML documentation.
2
+ * @fileoverview Handles the insertion of transcluded external references (trefs) into HTML documentation.
3
3
  * This script processes terms marked with a specific class and adds their definitions from external sources.
4
+ * The terms come from external repositories and are inserted into the document as collapsible definitions.
4
5
  */
5
6
 
6
7
  /**
7
- * Inserts transcluded external references into the document.
8
+ * Inserts transcluded external references (trefs) into the document.
9
+ * This function processes the allXTrefs data and inserts definition content into the document
10
+ * for terms marked with the 'transcluded-xref-term' class.
8
11
  * @param {Object} allXTrefs - The object containing all external references data
9
12
  * @param {Array} allXTrefs.xtrefs - Array of external reference objects, each containing term definitions
10
13
  */
11
- function insertTrefs(allXTrefs) { // Pass allXTrefs as a parameter
14
+ function insertTrefs(allXTrefs) {
12
15
  /**
13
- * Processes all terms found in the document and inserts their corresponding content
16
+ * Processes all terms found in the document and collects DOM changes
17
+ * before applying them in batch to improve performance.
18
+ *
14
19
  * @param {Object} xtrefsData - The object containing xtrefs data
15
20
  * @param {Array} xtrefsData.xtrefs - Array of external reference objects
21
+ * @returns {void} - This function does not return a value but dispatches a 'trefs-inserted' event
22
+ * when all DOM modifications are complete
16
23
  */
17
24
  function processTerms(xtrefsData) {
18
- // First collect all terms to ensure consistent processing order
25
+ /**
26
+ * First collect all terms to ensure consistent processing order
27
+ * @type {Array<{element: Element, textContent: string, dt: Element, parent: Element}>}
28
+ */
19
29
  const allTerms = [];
20
30
  document.querySelectorAll('dt span.transcluded-xref-term').forEach(termElement => {
21
31
  const textContent = Array.from(termElement.childNodes)
@@ -23,22 +33,10 @@ function insertTrefs(allXTrefs) { // Pass allXTrefs as a parameter
23
33
  .map(node => node.textContent.trim())
24
34
  .join('');
25
35
 
26
- allTerms.push({
27
- element: termElement,
28
- textContent: textContent
29
- });
30
- });
31
-
32
- // Then process all terms in a consistent order (from the JS file order)
33
- allTerms.forEach(termData => {
34
- const termElement = termData.element;
35
- const textContent = termData.textContent;
36
-
37
- // Find the first matching xref to avoid duplicates
38
- const xref = xtrefsData.xtrefs.find(x => x.term === textContent);
39
-
40
- // Skip if we've already added content for this term (check for existing dd elements)
36
+ // Find the dt element once outside the loop
41
37
  const dt = termElement.closest('dt');
38
+
39
+ // Skip if the term has already been processed
42
40
  if (dt) {
43
41
  const nextElement = dt.nextElementSibling;
44
42
  if (nextElement?.classList.contains('transcluded-xref-term') &&
@@ -47,13 +45,38 @@ function insertTrefs(allXTrefs) { // Pass allXTrefs as a parameter
47
45
  }
48
46
  }
49
47
 
50
- if (xref) {
51
- const parent = dt.parentNode;
48
+ // Only add terms that haven't been processed yet
49
+ allTerms.push({
50
+ element: termElement,
51
+ textContent: textContent,
52
+ dt: dt,
53
+ parent: dt?.parentNode
54
+ });
55
+ });
56
+
57
+ /**
58
+ * Prepare all DOM changes first before making any insertions
59
+ * Each item in the array contains the elements needed for DOM insertion
60
+ * @type {Array<{dt: Element, parent: Element, fragment: DocumentFragment}>}
61
+ */
62
+ const domChanges = allTerms.map(termData => {
63
+ const { textContent, dt, parent } = termData;
64
+
65
+ if (!dt || !parent) {
66
+ return null; // Skip invalid entries
67
+ }
68
+
69
+ // Find the first matching xref to avoid duplicates
70
+ const xref = xtrefsData.xtrefs.find(x => x.term === textContent);
71
+
72
+ // Create a DocumentFragment to hold all new elements for this term
73
+ const fragment = document.createDocumentFragment();
52
74
 
53
- // Create and insert meta info element
54
- const metaInfoEl = document.createElement('dd');
55
- metaInfoEl.classList.add('transcluded-xref-term', 'meta-info-content-wrapper', 'collapsed');
75
+ // Create meta info element
76
+ const metaInfoEl = document.createElement('dd');
77
+ metaInfoEl.classList.add('transcluded-xref-term', 'meta-info-content-wrapper', 'collapsed');
56
78
 
79
+ if (xref) {
57
80
  // Generate meta info content
58
81
  const avatar = xref.avatarUrl ? `![avatar](${xref.avatarUrl})` : '';
59
82
  const owner = xref.owner || 'Unknown';
@@ -68,7 +91,7 @@ function insertTrefs(allXTrefs) { // Pass allXTrefs as a parameter
68
91
  | Commit hash | ${commitHash} |
69
92
  `;
70
93
  metaInfoEl.innerHTML = md.render(metaInfo);
71
- parent.insertBefore(metaInfoEl, dt.nextSibling);
94
+ fragment.appendChild(metaInfoEl);
72
95
 
73
96
  // Clean up markdown content
74
97
  let content = xref.content
@@ -83,67 +106,129 @@ function insertTrefs(allXTrefs) { // Pass allXTrefs as a parameter
83
106
  const tempDiv = document.createElement('div');
84
107
  tempDiv.innerHTML = md.render(content);
85
108
 
86
- // If there are dd elements in the rendered content, insert them directly
109
+ // If there are dd elements in the rendered content, prepare them for insertion
87
110
  const ddElements = tempDiv.querySelectorAll('dd');
88
111
  if (ddElements.length > 0) {
89
- // Insert each dd element in the correct order
90
- let insertPosition = metaInfoEl; // Start inserting after the meta info element
91
-
92
- // Convert NodeList to Array and insert in original order
112
+ // Add all dd elements to the fragment in original order
93
113
  Array.from(ddElements).forEach(dd => {
94
114
  // Clone the node to avoid removing it from tempDiv during insertion
95
115
  const clonedDD = dd.cloneNode(true);
96
116
  // Add necessary classes
97
117
  clonedDD.classList.add('transcluded-xref-term', 'transcluded-xref-term-embedded');
98
- // Insert after the previous element
99
- parent.insertBefore(clonedDD, insertPosition.nextSibling);
100
- // Update insertion position to be after this newly inserted element
101
- insertPosition = clonedDD;
118
+ // Add to fragment
119
+ fragment.appendChild(clonedDD);
102
120
  });
103
121
  } else {
104
122
  // No dd elements found, create one to hold the content
105
123
  const contentEl = document.createElement('dd');
106
124
  contentEl.classList.add('transcluded-xref-term', 'transcluded-xref-term-embedded');
107
125
  contentEl.innerHTML = tempDiv.innerHTML;
108
- parent.insertBefore(contentEl, metaInfoEl.nextSibling);
126
+ fragment.appendChild(contentEl);
109
127
  }
110
128
  } else {
111
129
  // Handle case where xref is not found
112
- const parent = dt.parentNode;
113
-
114
- // Create and insert meta info for not found case
115
- const metaInfoEl = document.createElement('dd');
116
- metaInfoEl.classList.add('transcluded-xref-term', 'meta-info-content-wrapper', 'collapsed');
117
- const metaInfo = `
130
+ metaInfoEl.innerHTML = md.render(`
118
131
  | Property | Value |
119
132
  | -------- | ----- |
120
133
  | Owner | Unknown |
121
134
  | Repo | Unknown |
122
135
  | Commit hash | not found |
123
- `;
124
- metaInfoEl.innerHTML = md.render(metaInfo);
125
- parent.insertBefore(metaInfoEl, dt.nextSibling);
136
+ `);
137
+ fragment.appendChild(metaInfoEl);
126
138
 
127
- // Create and insert not found message
139
+ // Create not found message
128
140
  const notFoundEl = document.createElement('dd');
129
141
  notFoundEl.classList.add('transcluded-xref-term', 'transcluded-xref-term-embedded', 'last-dd');
130
142
  notFoundEl.innerHTML = '<p>This term was not found in the external repository.</p>';
131
- parent.insertBefore(notFoundEl, metaInfoEl.nextSibling);
143
+ fragment.appendChild(notFoundEl);
132
144
  }
145
+
146
+ // Return all necessary information for DOM insertion
147
+ return {
148
+ dt: dt,
149
+ parent: parent,
150
+ fragment: fragment
151
+ };
152
+ }).filter(Boolean); // Remove null entries
153
+
154
+ /**
155
+ * Perform all DOM insertions in a single batch using requestAnimationFrame
156
+ * to optimize browser rendering and prevent layout thrashing
157
+ */
158
+ requestAnimationFrame(() => {
159
+ domChanges.forEach(change => {
160
+ const { dt, parent, fragment } = change;
161
+ parent.insertBefore(fragment, dt.nextSibling);
162
+ });
163
+
164
+ // Dispatch a custom event when all DOM modifications are complete
165
+ // This allows other scripts to know exactly when our work is done
166
+ /**
167
+ * Dispatches a custom event to signal that trefs insertion is complete
168
+ * @fires trefs-inserted
169
+ */
170
+ document.dispatchEvent(new CustomEvent('trefs-inserted', {
171
+ detail: { count: domChanges.length }
172
+ }));
133
173
  });
134
174
  }
135
-
136
175
 
137
176
  if (allXTrefs?.xtrefs) {
138
177
  processTerms(allXTrefs);
139
178
  } else {
140
179
  console.error('allXTrefs is undefined or missing xtrefs property');
180
+ // Dispatch event even when there are no xrefs, so waiting code knows we're done
181
+ document.dispatchEvent(new CustomEvent('trefs-inserted', {
182
+ detail: { count: 0, error: 'Missing xtrefs data' }
183
+ }));
141
184
  }
142
185
  }
143
186
 
187
+ /**
188
+ * Handles the initialization of collapsible definitions functionality.
189
+ * This function sets up event listeners for the 'trefs-inserted' event and
190
+ * provides a fallback initialization mechanism if the event is never fired.
191
+ * @param {Function} initCallback - The function to call when initialization should occur
192
+ * @returns {void} - This function does not return a value
193
+ */
194
+ function initializeOnTrefsInserted(initCallback) {
195
+ // Track initialization state
196
+ let hasInitialized = false;
197
+
198
+ // Set up the event listener for the custom event from insert-trefs.js
199
+ document.addEventListener('trefs-inserted', function (event) {
200
+ // Avoid double initialization
201
+ if (hasInitialized) return;
202
+
203
+ // Now we know for certain that insert-trefs.js has completed its work
204
+ hasInitialized = true;
205
+ initCallback();
206
+
207
+ // Log info about the completed xrefs insertion (useful for debugging)
208
+ if (event.detail) {
209
+ console.log(`Collapsible definitions initialized after ${event.detail.count} xrefs were inserted`);
210
+ }
211
+ });
212
+
213
+ // Fallback initialization in case insert-trefs.js doesn't run or doesn't emit the event
214
+ // This ensures our UI works even if there's an issue with xrefs processing
215
+ // or if the 'trefs-inserted' event is never dispatched for some reason
216
+
217
+ // Wait for a reasonable time, then check if we've initialized
218
+ setTimeout(() => {
219
+ if (!hasInitialized) {
220
+ console.warn('trefs-inserted event was not received, initializing collapsible definitions anyway');
221
+ initCallback();
222
+ hasInitialized = true;
223
+ }
224
+ }, 1000); // Longer timeout as this is just a fallback
225
+ }
226
+
144
227
  /**
145
228
  * Initialize the transcluded references when the DOM is fully loaded.
146
229
  * Checks for the global allXTrefs object and calls insertTrefs if available.
230
+ * This event listener ensures that the DOM is ready before attempting to process references.
231
+ * @listens DOMContentLoaded
147
232
  */
148
233
  document.addEventListener('DOMContentLoaded', () => {
149
234
  // Check if allXTrefs is defined in the global scope
@@ -152,4 +237,5 @@ document.addEventListener('DOMContentLoaded', () => {
152
237
  } else {
153
238
  console.warn('allXTrefs is not available in the global scope. Transcluded references will not be inserted.');
154
239
  }
155
- });
240
+ });
241
+
@@ -32,7 +32,7 @@ function inPageSearch() {
32
32
  const terminologySectionUtilityContainer = document.getElementById("terminology-section-utility-container");
33
33
 
34
34
  const matchesStyle = specConfig.searchHighlightStyle || 'ssi';
35
- const antiNameCollisions = 'search-h7vc6omi2hr2880';// random string to be added to classes, id's etc, to prevent name collisions in the global space
35
+ const antiNameCollisions = 'search';
36
36
  const debounceTime = 600;
37
37
  const matches = 'matches';// What text to display after the number of matches
38
38
  const searchBarPlaceholder = '🔍';
@@ -43,12 +43,12 @@ function inPageSearch() {
43
43
 
44
44
  // Styling of search matches. See styles in /assets/css/search.css
45
45
  const matchesStyleSelector = {
46
- dif: 'highlight-matches-DIF-search-h7vc6omi2hr2880',
47
- toip: 'highlight-matches-ToIP-search-h7vc6omi2hr2880',
48
- btc: 'highlight-matches-BTC-search-h7vc6omi2hr2880',
49
- keri: 'highlight-matches-KERI-search-h7vc6omi2hr2880',
50
- ssi: 'highlight-matches-SSI-search-h7vc6omi2hr2880',
51
- gleif: 'highlight-matches-GLEIF-search-h7vc6omi2hr2880'
46
+ dif: 'highlight-matches-DIF-search',
47
+ toip: 'highlight-matches-ToIP-search',
48
+ btc: 'highlight-matches-BTC-search',
49
+ keri: 'highlight-matches-KERI-search',
50
+ ssi: 'highlight-matches-SSI-search',
51
+ gleif: 'highlight-matches-GLEIF-search'
52
52
  };
53
53
 
54
54
 
@@ -113,13 +113,15 @@ function inPageSearch() {
113
113
 
114
114
  // Add an event listener to the input element
115
115
  searchInput.addEventListener("input", function () {
116
- debouncedSearchAndHighlight(searchInput.value);
116
+ const shouldScrollToFirstMatch = true; // Scroll to first match when user is actively searching
117
+ debouncedSearchAndHighlight(searchInput.value, shouldScrollToFirstMatch);
117
118
  });
118
119
 
119
120
  // The search will run when the user clicks the collapse button, so the search results are updated when the terms are collapsed or expanded. If the definitions are collapsed, the search results in the definitions will be removed.
120
121
  document.addEventListener('click', event => {
121
122
  if (event.target.classList.contains('collapse-all-defs-button')) {
122
- debouncedSearchAndHighlight(searchInput.value);
123
+ const shouldScrollToFirstMatch = true; // Scroll when updating after collapse/expand
124
+ debouncedSearchAndHighlight(searchInput.value, shouldScrollToFirstMatch);
123
125
  }
124
126
  });
125
127
 
@@ -192,7 +194,11 @@ function inPageSearch() {
192
194
  totalMatchesSpan.innerHTML = `${totalMatches} ${matches}`;
193
195
  }
194
196
 
195
- // Debounce search input. Prepare the debounced function outside the event listener
197
+ // Debounce search input to improve performance. This creates a function that will only execute
198
+ // after the user has stopped typing for the specified debounceTime.
199
+ // The function takes two parameters:
200
+ // - searchString: The text to search for
201
+ // - scrollToFirstMatch: Whether to automatically scroll to the first match after highlighting
196
202
  const debouncedSearchAndHighlight = debounce(search, debounceTime);
197
203
 
198
204
  goToPreviousMatchButton.addEventListener("click", function () {
@@ -243,7 +249,12 @@ function inPageSearch() {
243
249
  });
244
250
 
245
251
  // Runs after every search input (debounced)
246
- function search(searchString) {
252
+ /**
253
+ * Performs search and highlighting of matches in the document
254
+ * @param {string} searchString - The text to search for
255
+ * @param {boolean} scrollToFirstMatch - Whether to automatically scroll to the first match after highlighting
256
+ */
257
+ function search(searchString, scrollToFirstMatch) {
247
258
  // Start clean
248
259
  removeAllSpans();
249
260
  totalMatches = 0;
@@ -252,8 +263,8 @@ function inPageSearch() {
252
263
  // Remove outer quotes if present
253
264
  if (searchString.length >= 2) {
254
265
  if (
255
- (searchString[0] === '"' && searchString[searchString.length - 1] === '"') ||
256
- (searchString[0] === "'" && searchString[searchString.length - 1] === "'")
266
+ (searchString.startsWith('"') && searchString.endsWith('"')) ||
267
+ (searchString.startsWith("'") && searchString.endsWith("'"))
257
268
  ) {
258
269
  searchString = searchString.substring(1, searchString.length - 1);
259
270
  }
@@ -328,13 +339,6 @@ function inPageSearch() {
328
339
 
329
340
  searchNodes(searchableContent);
330
341
 
331
- // Scroll to the first match
332
- // Using querySelector instead of querySelectorAll because we only want to select the first element
333
- let firstHighlight = document.querySelector('.' + matchesStyleSelectorClassName);
334
- if (firstHighlight !== null) {
335
- scrollToElementCenter(firstHighlight);
336
- }
337
-
338
342
  // Update the total matches counter
339
343
  setTotalMatches();
340
344
 
@@ -343,6 +347,16 @@ function inPageSearch() {
343
347
 
344
348
  // Update the active match index
345
349
  activeMatchIndex = -1;
350
+
351
+ // Scroll to the first match
352
+ // Using querySelector instead of querySelectorAll because we only want to select the first element
353
+ if (scrollToFirstMatch) {
354
+ let firstHighlight = document.querySelector('.' + matchesStyleSelectorClassName);
355
+ if (firstHighlight !== null) {
356
+ scrollToElementCenter(firstHighlight);
357
+ }
358
+
359
+ }
346
360
  }
347
361
  }
348
362
 
@@ -25,6 +25,7 @@
25
25
  "assets/css/adjust-font-size.css",
26
26
  "assets/css/image-full-size.css",
27
27
  "assets/css/add-bootstrap-classes-to-images.css",
28
+ "assets/css/horizontal-scroll-hint.css",
28
29
  "assets/css/index.css"
29
30
  ],
30
31
  "js": [
package/gulpfile.js CHANGED
@@ -11,7 +11,7 @@ const terser = require('gulp-terser');
11
11
  const mergeStreams = require('merge-stream');
12
12
  const cleanCSS = require('gulp-clean-css');
13
13
  const axios = require('axios').default;
14
- const assets = fs.readJsonSync('./src/asset-map.json');
14
+ const assets = fs.readJsonSync('./config/asset-map.json');
15
15
 
16
16
  let compileLocation = 'assets/compiled';
17
17
 
package/index.js CHANGED
@@ -37,7 +37,7 @@ module.exports = async function (options = {}) {
37
37
  const { fixMarkdownFiles } = require('./src/fix-markdown-files.js');
38
38
 
39
39
  let template = fs.readFileSync(path.join(modulePath, 'templates/template.html'), 'utf8');
40
- let assets = fs.readJsonSync(modulePath + '/src/asset-map.json');
40
+ let assets = fs.readJsonSync(modulePath + '/config/asset-map.json');
41
41
  let externalReferences;
42
42
  let references = [];
43
43
  let definitions = [];
@@ -406,7 +406,7 @@ module.exports = async function (options = {}) {
406
406
  mainDl.className = 'terms-and-definitions-list';
407
407
 
408
408
  // Look for the marker
409
- const marker = document.getElementById('terminology-section-start-h7vc6omi2hr2880');
409
+ const marker = document.getElementById('terminology-section-start');
410
410
 
411
411
  if (marker) {
412
412
  // Insert the new dl right after the marker
@@ -523,7 +523,7 @@ module.exports = async function (options = {}) {
523
523
  const termsIndex = (spec.markdown_paths || ['spec.md']).indexOf('terms-and-definitions-intro.md');
524
524
  if (termsIndex !== -1) {
525
525
  // Append the HTML string to the content of terms-and-definitions-intro.md. This string is used to create a div that is used to insert an alphabet index, and a div that is used as the starting point of the terminology index. The newlines are essential for the correct rendering of the markdown.
526
- docs[termsIndex] += '\n\n<div id="terminology-section-start-h7vc6omi2hr2880"></div>\n\n';
526
+ docs[termsIndex] += '\n\n<div id="terminology-section-start"></div>\n\n';
527
527
  }
528
528
 
529
529
  let doc = docs.join("\n");
package/jest.config.js ADDED
@@ -0,0 +1,20 @@
1
+ module.exports = {
2
+ collectCoverageFrom: [
3
+ 'src/**/*.{js,jsx}',
4
+ '!src/**/*.test.{js,jsx}',
5
+ '!src/**/__tests__/**',
6
+ '!src/**/__mocks__/**',
7
+ ],
8
+ coverageDirectory: 'coverage',
9
+ coverageReporters: ['text', 'lcov', 'html'],
10
+ coverageThreshold: {
11
+ global: {
12
+ branches: 80,
13
+ functions: 80,
14
+ lines: 80,
15
+ statements: 80,
16
+ },
17
+ },
18
+ testEnvironment: 'node',
19
+ verbose: true,
20
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-up-t",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "Technical specification drafting tool that generates rich specification documents from markdown. Forked from https://github.com/decentralized-identity/spec-up by Daniel Buchner (https://github.com/csuwildcat)",
5
5
  "main": "./index",
6
6
  "repository": {
@@ -70,6 +70,7 @@
70
70
  "jest": "^29.7.0"
71
71
  },
72
72
  "scripts": {
73
- "test": "NODE_OPTIONS=--experimental-vm-modules jest"
73
+ "test": "jest",
74
+ "test:coverage": "jest --coverage"
74
75
  }
75
76
  }