spec-up-t 1.2.8 → 1.3.0-beta

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 (38) hide show
  1. package/.github/copilot-instructions.md +3 -1
  2. package/assets/compiled/body.js +5 -4
  3. package/assets/compiled/head.css +1 -0
  4. package/assets/compiled/refs.json +1 -1
  5. package/assets/css/highlight-heading-plus-sibling-nodes.css +6 -0
  6. package/assets/css/index.css +9 -0
  7. package/assets/js/addAnchorsToTerms.js +13 -5
  8. package/assets/js/collapse-definitions.js +0 -6
  9. package/assets/js/fix-last-dd.js +6 -3
  10. package/assets/js/highlight-heading-plus-sibling-nodes.js +258 -0
  11. package/assets/js/insert-trefs.js +32 -28
  12. package/config/asset-map.json +2 -0
  13. package/gulpfile.js +8 -2
  14. package/index.js +45 -241
  15. package/package.json +2 -1
  16. package/sonar-project.properties +6 -0
  17. package/src/collect-external-references.js +22 -11
  18. package/src/collect-external-references.test.js +153 -2
  19. package/src/collectExternalReferences/fetchTermsFromIndex.js +65 -110
  20. package/src/collectExternalReferences/processXTrefsData.js +9 -11
  21. package/src/create-docx.js +332 -0
  22. package/src/create-pdf.js +243 -122
  23. package/src/escape-handler.js +67 -0
  24. package/src/fix-markdown-files.js +31 -34
  25. package/src/html-dom-processor.js +290 -0
  26. package/src/init.js +3 -0
  27. package/src/install-from-boilerplate/boilerplate/.github/workflows/menu.yml +4 -13
  28. package/src/install-from-boilerplate/boilerplate/spec/example-markup-in-markdown.md +0 -1
  29. package/src/install-from-boilerplate/boilerplate/spec/terms-and-definitions-intro.md +1 -5
  30. package/src/install-from-boilerplate/config-scripts-keys.js +4 -4
  31. package/src/install-from-boilerplate/menu.sh +6 -6
  32. package/src/markdown-it-extensions.js +60 -31
  33. package/src/references.js +18 -6
  34. package/templates/template.html +2 -0
  35. package/test-default-definitions.js +55 -0
  36. package/test-edge-cases.md +20 -0
  37. package/test-fix-markdown.js +11 -0
  38. package/test-no-def.md +22 -0
@@ -0,0 +1,6 @@
1
+ .highlight2 {
2
+ padding-left: 1em;
3
+ padding-right: 1em;
4
+ border: 1px dashed #71bbe6;
5
+ background: #a9dde03b;
6
+ }
@@ -3,6 +3,15 @@
3
3
  /* Matches sticky header height */
4
4
  }
5
5
 
6
+
7
+ /*
8
+ - When a URL fragment targets an element,
9
+ the `:target` pseudo-class triggers the corresponding CSS rules.
10
+ - If the body lacks the hashscroll attribute:
11
+ - The targeted element gets highlighted with an animation (highlight-target).
12
+ - Its parent <dt>(if applicable) gets highlighted with a different animation (highlight-target-parent-dt).
13
+ - These animations visually emphasize the targeted element and its parent for better user experience.
14
+ */
6
15
  body:not([hashscroll]) *:target {
7
16
  animation: highlight-target 3.5s 0.25s ease;
8
17
  }
@@ -1,9 +1,17 @@
1
1
  function addAnchorsToTerms() {
2
- // Function to find the deepest <span>
3
- // Spec-Up is generating nested spans. The deepest span is the main term, and that is what we need.
4
- function findDeepestSpan(element) {
2
+ // Function to find the appropriate span for anchor linking
3
+ // For external references (tref), we need to use the main term ID, not alias ID
4
+ function findMainTermSpan(element) {
5
+ // First, check if this is a transcluded external reference
6
+ const transcludedSpan = element.querySelector('span.transcluded-xref-term[id^="term:"]');
7
+ if (transcludedSpan) {
8
+ // For transcluded external references, always use the main term ID (outermost span)
9
+ // This ensures that anchor links work correctly with external content insertion
10
+ return transcludedSpan;
11
+ }
12
+
13
+ // For regular terms, find the deepest span
5
14
  let currentElement = element;
6
- // While there is a <span> child, keep going deeper
7
15
  while (currentElement.querySelector('span[id^="term:"]')) {
8
16
  currentElement = currentElement.querySelector('span[id^="term:"]');
9
17
  }
@@ -15,7 +23,7 @@ function addAnchorsToTerms() {
15
23
 
16
24
  dts.forEach(item => {
17
25
 
18
- const dt = findDeepestSpan(item);
26
+ const dt = findMainTermSpan(item);
19
27
  const id = dt.getAttribute('id');
20
28
  const a = document.createElement('a');
21
29
  a.setAttribute('href', `#${id}`);
@@ -227,12 +227,6 @@ function collapseDefinitions() {
227
227
  button.style.right = `${window.innerWidth - buttonRect.right}px`;
228
228
  button.style.zIndex = '1000';
229
229
 
230
- // Add highlight effect
231
- dtElement.classList.add('highlight');
232
- setTimeout(() => {
233
- dtElement.classList.remove('highlight');
234
- }, 0);
235
-
236
230
  // Toggle visibility which might change layout
237
231
  toggleVisibility();
238
232
 
@@ -22,6 +22,9 @@ function fixLastDd() {
22
22
 
23
23
  // Process each dd element
24
24
  ddElements.forEach((dd, index) => {
25
+ // Remove the 'last-dd' class if it exists, one of the ways it can be there is via the fetching of the external references html. We do not know what classes are in there.
26
+ dd.classList.remove('last-dd');
27
+
25
28
  // Get the next sibling element
26
29
  let nextSibling = dd.nextElementSibling;
27
30
 
@@ -37,8 +40,8 @@ function fixLastDd() {
37
40
  }
38
41
 
39
42
  /**
40
- * Initializes the function when the DOM content is fully loaded.
43
+ * Initializes the function when the custom event "trefs-inserted" is fired.
41
44
  */
42
- document.addEventListener("DOMContentLoaded", function () {
43
- fixLastDd();
45
+ document.addEventListener("trefs-inserted", function () {
46
+ fixLastDd();
44
47
  });
@@ -0,0 +1,258 @@
1
+ /**
2
+ * @fileoverview Provides functionality to highlight headings and their following sibling nodes
3
+ * when anchor links are clicked. This module uses event delegation to handle link clicks
4
+ * and dynamically wraps content sections with highlighting divs.
5
+ *
6
+ * @author Generated by AI Assistant
7
+ * @version 1.0.0
8
+ */
9
+
10
+ /**
11
+ * Highlights a heading and its following sibling nodes until the next heading of the same level.
12
+ * Only processes headings h2-h6, excluding h1 elements.
13
+ *
14
+ * @param {string} anchor - The anchor string (e.g., "#linking-to-this-glossary")
15
+ * @returns {boolean} True if highlighting was successful, false otherwise
16
+ *
17
+ * @example
18
+ * // Highlight heading with id "section-1" and its content
19
+ * highlightHeadingSection("#section-1");
20
+ *
21
+ * @example
22
+ * // Use with anchor from URL hash
23
+ * highlightHeadingSection(window.location.hash);
24
+ */
25
+ function highlightHeadingSection(anchor) {
26
+ // Validate input parameter
27
+ if (!anchor || typeof anchor !== 'string' || !anchor.startsWith('#')) {
28
+ console.warn('Invalid anchor provided:', anchor);
29
+ return false;
30
+ }
31
+
32
+ // Remove existing highlights before adding new ones
33
+ removeExistingHighlights();
34
+
35
+ // Extract the element ID from the anchor
36
+ const elementId = anchor.substring(1);
37
+ const targetElement = document.getElementById(elementId);
38
+
39
+ if (!targetElement) {
40
+ console.warn('Element with ID not found:', elementId);
41
+ return false;
42
+ }
43
+
44
+ // Check if the target element is a valid heading (h2-h6)
45
+ const headingLevel = getHeadingLevel(targetElement);
46
+ if (headingLevel === null) {
47
+ return false;
48
+ }
49
+
50
+ // Find all sibling nodes until the next heading of the same level
51
+ const nodesToHighlight = collectHeadingSiblings(targetElement, headingLevel);
52
+
53
+ // Wrap the collected nodes with highlight div
54
+ wrapNodesWithHighlight(nodesToHighlight);
55
+
56
+ return true;
57
+ }
58
+
59
+ /**
60
+ * Determines if an element is a heading and returns its level.
61
+ * Only considers h2-h6 elements as valid headings.
62
+ *
63
+ * @param {Element} element - The DOM element to check
64
+ * @returns {number|null} The heading level (2-6) or null if not a valid heading
65
+ *
66
+ * @example
67
+ * const level = getHeadingLevel(document.querySelector('h3')); // Returns 3
68
+ * const invalid = getHeadingLevel(document.querySelector('h1')); // Returns null
69
+ */
70
+ function getHeadingLevel(element) {
71
+ const tagName = element.tagName.toLowerCase();
72
+ const headingMatch = tagName.match(/^h([2-6])$/);
73
+ return headingMatch ? parseInt(headingMatch[1], 10) : null;
74
+ }
75
+
76
+ /**
77
+ * Collects a heading element and all its following sibling nodes until the next
78
+ * heading of the same or higher level in the hierarchy.
79
+ *
80
+ * @param {Element} headingElement - The heading element to start from
81
+ * @param {number} headingLevel - The level of the heading (2-6)
82
+ * @returns {Element[]} Array of elements to be highlighted
83
+ *
84
+ * @example
85
+ * const h3Element = document.querySelector('#my-section');
86
+ * const siblings = collectHeadingSiblings(h3Element, 3);
87
+ * // Returns [h3Element, ...following siblings until next h3 or higher]
88
+ */
89
+ function collectHeadingSiblings(headingElement, headingLevel) {
90
+ const nodesToHighlight = [headingElement];
91
+ let currentNode = headingElement.nextElementSibling;
92
+
93
+ while (currentNode) {
94
+ const currentHeadingLevel = getHeadingLevel(currentNode);
95
+
96
+ // Stop if we encounter a heading of the same or higher level
97
+ if (currentHeadingLevel !== null && currentHeadingLevel <= headingLevel) {
98
+ break;
99
+ }
100
+
101
+ nodesToHighlight.push(currentNode);
102
+ currentNode = currentNode.nextElementSibling;
103
+ }
104
+
105
+ return nodesToHighlight;
106
+ }
107
+
108
+ /**
109
+ * Wraps a collection of DOM nodes with a highlighting div element.
110
+ * Creates a div with class "highlight2" and moves all nodes inside it.
111
+ *
112
+ * @param {Element[]} nodes - Array of DOM elements to wrap
113
+ * @returns {Element|null} The created wrapper div or null if no nodes provided
114
+ *
115
+ * @example
116
+ * const nodes = [headingEl, paragraphEl, listEl];
117
+ * const wrapper = wrapNodesWithHighlight(nodes);
118
+ * // Creates: <div class="highlight2">...nodes...</div>
119
+ */
120
+ function wrapNodesWithHighlight(nodes) {
121
+ if (!nodes || nodes.length === 0) {
122
+ return null;
123
+ }
124
+
125
+ // Create the highlight wrapper div
126
+ const highlightDiv = document.createElement('div');
127
+ highlightDiv.className = 'highlight2';
128
+
129
+ // Insert the wrapper before the first node
130
+ const firstNode = nodes[0];
131
+ firstNode.parentNode.insertBefore(highlightDiv, firstNode);
132
+
133
+ // Move all nodes into the wrapper
134
+ nodes.forEach(node => {
135
+ highlightDiv.appendChild(node);
136
+ });
137
+
138
+ return highlightDiv;
139
+ }
140
+
141
+ /**
142
+ * Removes all existing highlight wrappers from the document.
143
+ * This ensures only one section is highlighted at a time.
144
+ *
145
+ * @returns {number} The number of highlight divs removed
146
+ *
147
+ * @example
148
+ * const removedCount = removeExistingHighlights();
149
+ * console.log(`Removed ${removedCount} existing highlights`);
150
+ */
151
+ function removeExistingHighlights() {
152
+ const existingHighlights = document.querySelectorAll('.highlight2');
153
+ let removedCount = 0;
154
+
155
+ existingHighlights.forEach(highlight => {
156
+ // Move all children back to the parent before removing the wrapper
157
+ const parent = highlight.parentNode;
158
+ while (highlight.firstChild) {
159
+ parent.insertBefore(highlight.firstChild, highlight);
160
+ }
161
+ parent.removeChild(highlight);
162
+ removedCount++;
163
+ });
164
+
165
+ return removedCount;
166
+ }
167
+
168
+ /**
169
+ * Handles click events on anchor links and triggers heading highlighting.
170
+ * Uses event delegation to handle dynamically added links.
171
+ *
172
+ * @param {Event} event - The click event object
173
+ * @returns {void}
174
+ *
175
+ * @example
176
+ * // This function is automatically called when anchor links are clicked
177
+ * // due to the event delegation setup in initializeAnchorHighlighting()
178
+ */
179
+ function handleAnchorClick(event) {
180
+ const target = event.target;
181
+
182
+ // Check if the clicked element is a link with an href containing a hash
183
+ if (target.tagName.toLowerCase() !== 'a') {
184
+ return;
185
+ }
186
+
187
+ const href = target.getAttribute('href');
188
+ if (!href || !href.includes('#')) {
189
+ return;
190
+ }
191
+
192
+ // Extract the anchor part from the href
193
+ const anchorPart = href.substring(href.indexOf('#'));
194
+
195
+ // Only process if it's an internal anchor (starts with #)
196
+ if (anchorPart.startsWith('#') && anchorPart.length > 1) {
197
+ // Small delay to allow browser navigation to complete
198
+ setTimeout(() => {
199
+ highlightHeadingSection(anchorPart);
200
+ }, 100);
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Initializes the anchor link highlighting functionality using event delegation.
206
+ * Sets up a single click event listener on the document that handles all anchor clicks.
207
+ * This approach is more efficient than attaching individual listeners to each link.
208
+ *
209
+ * @returns {void}
210
+ *
211
+ * @example
212
+ * // Initialize the highlighting system
213
+ * initializeAnchorHighlighting();
214
+ *
215
+ * // Now all anchor links will automatically trigger highlighting
216
+ * // when clicked, including dynamically added links
217
+ */
218
+ function initializeAnchorHighlighting() {
219
+ // Use event delegation to handle all anchor clicks
220
+ document.addEventListener('click', handleAnchorClick);
221
+
222
+ // Also handle direct navigation to anchors (e.g., page load with hash)
223
+ if (window.location.hash) {
224
+ // Delay to ensure DOM is fully loaded
225
+ setTimeout(() => {
226
+ highlightHeadingSection(window.location.hash);
227
+ }, 200);
228
+ }
229
+
230
+ // Handle browser back/forward navigation
231
+ window.addEventListener('hashchange', () => {
232
+ if (window.location.hash) {
233
+ highlightHeadingSection(window.location.hash);
234
+ } else {
235
+ removeExistingHighlights();
236
+ }
237
+ });
238
+ }
239
+
240
+ // Auto-initialize when DOM is ready
241
+ if (document.readyState === 'loading') {
242
+ document.addEventListener('DOMContentLoaded', initializeAnchorHighlighting);
243
+ } else {
244
+ // DOM is already loaded
245
+ initializeAnchorHighlighting();
246
+ }
247
+
248
+ // Export functions for potential external use
249
+ if (typeof module !== 'undefined' && module.exports) {
250
+ module.exports = {
251
+ highlightHeadingSection,
252
+ getHeadingLevel,
253
+ collectHeadingSiblings,
254
+ wrapNodesWithHighlight,
255
+ removeExistingHighlights,
256
+ initializeAnchorHighlighting
257
+ };
258
+ }
@@ -27,11 +27,11 @@ function insertTrefs(allXTrefs) {
27
27
  * @type {Array<{element: Element, textContent: string, dt: Element, parent: Element}>}
28
28
  */
29
29
  const allTerms = [];
30
- document.querySelectorAll('dt span.transcluded-xref-term').forEach(termElement => {
31
- const textContent = Array.from(termElement.childNodes)
32
- .filter(node => node.nodeType === Node.TEXT_NODE)
33
- .map(node => node.textContent.trim())
34
- .join('');
30
+
31
+ document.querySelectorAll('dl.terms-and-definitions-list dt span.transcluded-xref-term').forEach((termElement) => {
32
+ // Get the full text content including any nested spans (for aliases) of a term (dt)
33
+ // In case of `[[tref:toip1, agency, ag]]`, this will return `agency`
34
+ const textContent = termElement.textContent.trim();
35
35
 
36
36
  // Find the dt element once outside the loop
37
37
  const dt = termElement.closest('dt');
@@ -68,7 +68,7 @@ function insertTrefs(allXTrefs) {
68
68
 
69
69
  // Find the first matching xref to avoid duplicates
70
70
  const xref = xtrefsData.xtrefs.find(x => x.term === textContent);
71
-
71
+
72
72
  // Create a DocumentFragment to hold all new elements for this term
73
73
  const fragment = document.createDocumentFragment();
74
74
 
@@ -93,16 +93,22 @@ function insertTrefs(allXTrefs) {
93
93
  metaInfoEl.innerHTML = md.render(metaInfo);
94
94
  fragment.appendChild(metaInfoEl);
95
95
 
96
- // Clean up markdown content
96
+ // Clean up the markdown content in the term definition
97
+ // Part A: clean up via regex
97
98
  let content = xref.content
98
- .replace(/\[\[def:[^\]]*?\]\]/g, '') // Remove [[def: ...]] patterns regardless of trailing chars
99
99
  .split('\n')
100
100
  .map(line => line.replace(/^\s*~\s*/, '')) // Remove leading ~ and spaces
101
101
  .join('\n')
102
- .replace(/\[\[ref:/g, '') // Remove [[ref: ...]]
103
102
  .replace(/\]\]/g, '');
104
103
 
105
- // Parse the rendered HTML to check for dd elements
104
+ // Clean up the markdown content in the term definition
105
+ // Part B: Remove all <a> elements from the content via a temporary div and DOM manipulation
106
+ const tempDivForLinks = document.createElement('div');
107
+ tempDivForLinks.innerHTML = md.render(content);
108
+ tempDivForLinks.querySelectorAll('a').forEach(a => a.replaceWith(...a.childNodes));
109
+ content = tempDivForLinks.innerHTML;
110
+
111
+ // Parse the rendered HTML to check for dd elements. xref.content is a string that contains HTML, in the form of <dd>...</dd>'s
106
112
  const tempDiv = document.createElement('div');
107
113
  tempDiv.innerHTML = md.render(content);
108
114
 
@@ -119,14 +125,17 @@ function insertTrefs(allXTrefs) {
119
125
  fragment.appendChild(clonedDD);
120
126
  });
121
127
  } else {
122
- // No dd elements found, create one to hold the content
128
+ /*
129
+ No dd elements found, create one to hold the conten. Explanation: this is the content in case nothing was found:
130
+ `"content": "This term was not found in the external repository"`
131
+ */
123
132
  const contentEl = document.createElement('dd');
124
133
  contentEl.classList.add('transcluded-xref-term', 'transcluded-xref-term-embedded');
125
134
  contentEl.innerHTML = tempDiv.innerHTML;
126
135
  fragment.appendChild(contentEl);
127
136
  }
128
137
  } else {
129
- // Handle case where xref is not found
138
+ // When the [[tref]] is not valid, for example `[[tref: transferable, transferable]]`, where `transferable` is not an external repo in specs.json
130
139
  metaInfoEl.innerHTML = md.render(`
131
140
  | Property | Value |
132
141
  | -------- | ----- |
@@ -138,7 +147,7 @@ function insertTrefs(allXTrefs) {
138
147
 
139
148
  // Create not found message
140
149
  const notFoundEl = document.createElement('dd');
141
- notFoundEl.classList.add('transcluded-xref-term', 'transcluded-xref-term-embedded', 'last-dd');
150
+
142
151
  notFoundEl.innerHTML = '<p>This term was not found in the external repository.</p>';
143
152
  fragment.appendChild(notFoundEl);
144
153
  }
@@ -160,28 +169,20 @@ function insertTrefs(allXTrefs) {
160
169
  const { dt, parent, fragment } = change;
161
170
  parent.insertBefore(fragment, dt.nextSibling);
162
171
  });
163
-
172
+
164
173
  // Dispatch a custom event when all DOM modifications are complete
165
174
  // This allows other scripts to know exactly when our work is done
166
175
  /**
167
176
  * Dispatches a custom event to signal that trefs insertion is complete
168
177
  * @fires trefs-inserted
169
178
  */
170
- document.dispatchEvent(new CustomEvent('trefs-inserted', {
171
- detail: { count: domChanges.length }
179
+ document.dispatchEvent(new CustomEvent('trefs-inserted', {
180
+ detail: { count: domChanges.length }
172
181
  }));
173
182
  });
174
183
  }
175
184
 
176
- if (allXTrefs?.xtrefs) {
177
- processTerms(allXTrefs);
178
- } else {
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
- }));
184
- }
185
+ processTerms(allXTrefs);
185
186
  }
186
187
 
187
188
  /**
@@ -231,11 +232,14 @@ function initializeOnTrefsInserted(initCallback) {
231
232
  * @listens DOMContentLoaded
232
233
  */
233
234
  document.addEventListener('DOMContentLoaded', () => {
234
- // Check if allXTrefs is defined in the global scope
235
- if (typeof allXTrefs !== 'undefined') {
235
+ if (typeof allXTrefs !== 'undefined' && allXTrefs?.xtrefs) {
236
236
  insertTrefs(allXTrefs);
237
237
  } else {
238
- console.warn('allXTrefs is not available in the global scope. Transcluded references will not be inserted.');
238
+ console.error('allXTrefs is undefined or missing xtrefs property');
239
+ // Dispatch event even when there are no xrefs, so waiting code knows we're done
240
+ document.dispatchEvent(new CustomEvent('trefs-inserted', {
241
+ detail: { count: 0, error: 'Missing xtrefs data' }
242
+ }));
239
243
  }
240
244
  });
241
245
 
@@ -26,6 +26,7 @@
26
26
  "assets/css/image-full-size.css",
27
27
  "assets/css/add-bootstrap-classes-to-images.css",
28
28
  "assets/css/horizontal-scroll-hint.css",
29
+ "assets/css/highlight-heading-plus-sibling-nodes.css",
29
30
  "assets/css/index.css"
30
31
  ],
31
32
  "js": [
@@ -72,6 +73,7 @@
72
73
  "assets/js/horizontal-scroll-hint.js",
73
74
  "assets/js/add-bootstrap-classes-to-images.js",
74
75
  "assets/js/image-full-size.js",
76
+ "assets/js/highlight-heading-plus-sibling-nodes.js",
75
77
  "assets/js/bootstrap.bundle.min.js"
76
78
  ]
77
79
  }
package/gulpfile.js CHANGED
@@ -19,9 +19,15 @@ async function fetchSpecRefs() {
19
19
  return Promise.all([
20
20
  axios.get('https://raw.githubusercontent.com/tobie/specref/master/refs/ietf.json'),
21
21
  axios.get('https://raw.githubusercontent.com/tobie/specref/master/refs/w3c.json'),
22
- axios.get('https://raw.githubusercontent.com/tobie/specref/master/refs/whatwg.json')
22
+ axios.get('https://raw.githubusercontent.com/tobie/specref/master/refs/whatwg.json'),
23
+ axios.get('https://raw.githubusercontent.com/tobie/specref/master/refs/biblio.json')
23
24
  ]).then(async results => {
24
- let json = Object.assign(results[0].data, results[1].data, results[2].data);
25
+ let json = Object.assign(
26
+ results[0].data,// IETF
27
+ results[1].data,// W3C
28
+ results[2].data,// WHATWG
29
+ results[3].data// Biblio
30
+ );
25
31
  return fs.outputFile(compileLocation + '/refs.json', JSON.stringify(json));
26
32
  }).catch(e => console.log(e));
27
33
  }