spec-up-t 1.2.8 → 1.2.9

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.
@@ -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
  }
@@ -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
 
@@ -0,0 +1,259 @@
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
+ console.info('Target element is not a valid heading (h2-h6):', elementId);
48
+ return false;
49
+ }
50
+
51
+ // Find all sibling nodes until the next heading of the same level
52
+ const nodesToHighlight = collectHeadingSiblings(targetElement, headingLevel);
53
+
54
+ // Wrap the collected nodes with highlight div
55
+ wrapNodesWithHighlight(nodesToHighlight);
56
+
57
+ return true;
58
+ }
59
+
60
+ /**
61
+ * Determines if an element is a heading and returns its level.
62
+ * Only considers h2-h6 elements as valid headings.
63
+ *
64
+ * @param {Element} element - The DOM element to check
65
+ * @returns {number|null} The heading level (2-6) or null if not a valid heading
66
+ *
67
+ * @example
68
+ * const level = getHeadingLevel(document.querySelector('h3')); // Returns 3
69
+ * const invalid = getHeadingLevel(document.querySelector('h1')); // Returns null
70
+ */
71
+ function getHeadingLevel(element) {
72
+ const tagName = element.tagName.toLowerCase();
73
+ const headingMatch = tagName.match(/^h([2-6])$/);
74
+ return headingMatch ? parseInt(headingMatch[1], 10) : null;
75
+ }
76
+
77
+ /**
78
+ * Collects a heading element and all its following sibling nodes until the next
79
+ * heading of the same or higher level in the hierarchy.
80
+ *
81
+ * @param {Element} headingElement - The heading element to start from
82
+ * @param {number} headingLevel - The level of the heading (2-6)
83
+ * @returns {Element[]} Array of elements to be highlighted
84
+ *
85
+ * @example
86
+ * const h3Element = document.querySelector('#my-section');
87
+ * const siblings = collectHeadingSiblings(h3Element, 3);
88
+ * // Returns [h3Element, ...following siblings until next h3 or higher]
89
+ */
90
+ function collectHeadingSiblings(headingElement, headingLevel) {
91
+ const nodesToHighlight = [headingElement];
92
+ let currentNode = headingElement.nextElementSibling;
93
+
94
+ while (currentNode) {
95
+ const currentHeadingLevel = getHeadingLevel(currentNode);
96
+
97
+ // Stop if we encounter a heading of the same or higher level
98
+ if (currentHeadingLevel !== null && currentHeadingLevel <= headingLevel) {
99
+ break;
100
+ }
101
+
102
+ nodesToHighlight.push(currentNode);
103
+ currentNode = currentNode.nextElementSibling;
104
+ }
105
+
106
+ return nodesToHighlight;
107
+ }
108
+
109
+ /**
110
+ * Wraps a collection of DOM nodes with a highlighting div element.
111
+ * Creates a div with class "highlight2" and moves all nodes inside it.
112
+ *
113
+ * @param {Element[]} nodes - Array of DOM elements to wrap
114
+ * @returns {Element|null} The created wrapper div or null if no nodes provided
115
+ *
116
+ * @example
117
+ * const nodes = [headingEl, paragraphEl, listEl];
118
+ * const wrapper = wrapNodesWithHighlight(nodes);
119
+ * // Creates: <div class="highlight2">...nodes...</div>
120
+ */
121
+ function wrapNodesWithHighlight(nodes) {
122
+ if (!nodes || nodes.length === 0) {
123
+ return null;
124
+ }
125
+
126
+ // Create the highlight wrapper div
127
+ const highlightDiv = document.createElement('div');
128
+ highlightDiv.className = 'highlight2';
129
+
130
+ // Insert the wrapper before the first node
131
+ const firstNode = nodes[0];
132
+ firstNode.parentNode.insertBefore(highlightDiv, firstNode);
133
+
134
+ // Move all nodes into the wrapper
135
+ nodes.forEach(node => {
136
+ highlightDiv.appendChild(node);
137
+ });
138
+
139
+ return highlightDiv;
140
+ }
141
+
142
+ /**
143
+ * Removes all existing highlight wrappers from the document.
144
+ * This ensures only one section is highlighted at a time.
145
+ *
146
+ * @returns {number} The number of highlight divs removed
147
+ *
148
+ * @example
149
+ * const removedCount = removeExistingHighlights();
150
+ * console.log(`Removed ${removedCount} existing highlights`);
151
+ */
152
+ function removeExistingHighlights() {
153
+ const existingHighlights = document.querySelectorAll('.highlight2');
154
+ let removedCount = 0;
155
+
156
+ existingHighlights.forEach(highlight => {
157
+ // Move all children back to the parent before removing the wrapper
158
+ const parent = highlight.parentNode;
159
+ while (highlight.firstChild) {
160
+ parent.insertBefore(highlight.firstChild, highlight);
161
+ }
162
+ parent.removeChild(highlight);
163
+ removedCount++;
164
+ });
165
+
166
+ return removedCount;
167
+ }
168
+
169
+ /**
170
+ * Handles click events on anchor links and triggers heading highlighting.
171
+ * Uses event delegation to handle dynamically added links.
172
+ *
173
+ * @param {Event} event - The click event object
174
+ * @returns {void}
175
+ *
176
+ * @example
177
+ * // This function is automatically called when anchor links are clicked
178
+ * // due to the event delegation setup in initializeAnchorHighlighting()
179
+ */
180
+ function handleAnchorClick(event) {
181
+ const target = event.target;
182
+
183
+ // Check if the clicked element is a link with an href containing a hash
184
+ if (target.tagName.toLowerCase() !== 'a') {
185
+ return;
186
+ }
187
+
188
+ const href = target.getAttribute('href');
189
+ if (!href || !href.includes('#')) {
190
+ return;
191
+ }
192
+
193
+ // Extract the anchor part from the href
194
+ const anchorPart = href.substring(href.indexOf('#'));
195
+
196
+ // Only process if it's an internal anchor (starts with #)
197
+ if (anchorPart.startsWith('#') && anchorPart.length > 1) {
198
+ // Small delay to allow browser navigation to complete
199
+ setTimeout(() => {
200
+ highlightHeadingSection(anchorPart);
201
+ }, 100);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Initializes the anchor link highlighting functionality using event delegation.
207
+ * Sets up a single click event listener on the document that handles all anchor clicks.
208
+ * This approach is more efficient than attaching individual listeners to each link.
209
+ *
210
+ * @returns {void}
211
+ *
212
+ * @example
213
+ * // Initialize the highlighting system
214
+ * initializeAnchorHighlighting();
215
+ *
216
+ * // Now all anchor links will automatically trigger highlighting
217
+ * // when clicked, including dynamically added links
218
+ */
219
+ function initializeAnchorHighlighting() {
220
+ // Use event delegation to handle all anchor clicks
221
+ document.addEventListener('click', handleAnchorClick);
222
+
223
+ // Also handle direct navigation to anchors (e.g., page load with hash)
224
+ if (window.location.hash) {
225
+ // Delay to ensure DOM is fully loaded
226
+ setTimeout(() => {
227
+ highlightHeadingSection(window.location.hash);
228
+ }, 200);
229
+ }
230
+
231
+ // Handle browser back/forward navigation
232
+ window.addEventListener('hashchange', () => {
233
+ if (window.location.hash) {
234
+ highlightHeadingSection(window.location.hash);
235
+ } else {
236
+ removeExistingHighlights();
237
+ }
238
+ });
239
+ }
240
+
241
+ // Auto-initialize when DOM is ready
242
+ if (document.readyState === 'loading') {
243
+ document.addEventListener('DOMContentLoaded', initializeAnchorHighlighting);
244
+ } else {
245
+ // DOM is already loaded
246
+ initializeAnchorHighlighting();
247
+ }
248
+
249
+ // Export functions for potential external use
250
+ if (typeof module !== 'undefined' && module.exports) {
251
+ module.exports = {
252
+ highlightHeadingSection,
253
+ getHeadingLevel,
254
+ collectHeadingSiblings,
255
+ wrapNodesWithHighlight,
256
+ removeExistingHighlights,
257
+ initializeAnchorHighlighting
258
+ };
259
+ }
@@ -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
  }
package/index.js CHANGED
@@ -20,6 +20,8 @@ module.exports = async function (options = {}) {
20
20
  const { createTermIndex } = require('./src/create-term-index.js');
21
21
  createTermIndex();
22
22
 
23
+ const { processWithEscapes } = require('./src/escape-handler.js');
24
+
23
25
  const { insertTermIndex } = require('./src/insert-term-index.js');
24
26
  insertTermIndex();
25
27
 
@@ -182,14 +184,7 @@ module.exports = async function (options = {}) {
182
184
  return fs.readFileSync(path, 'utf8');
183
185
  }
184
186
  },
185
- {
186
- test: 'spec',
187
- transform: function (originalMatch, type, name) {
188
- // Simply return an empty string or special marker that won't be treated as a definition term
189
- // The actual rendering will be handled by the markdown-it extension
190
- return `<span class="spec-marker" data-spec="${name}"></span>`;
191
- }
192
- },
187
+
193
188
  /**
194
189
  * Custom replacer for tref tags that converts them directly to HTML definition term elements.
195
190
  *
@@ -245,13 +240,16 @@ module.exports = async function (options = {}) {
245
240
  * @returns {string} - The processed document with tags replaced by their HTML equivalents
246
241
  */
247
242
  function applyReplacers(doc) {
248
- return doc.replace(replacerRegex, function (match, type, args) {
249
- let replacer = replacers.find(r => type.trim().match(r.test));
250
- if (replacer) {
251
- let argsArray = args ? args.trim().split(replacerArgsRegex) : [];
252
- return replacer.transform(match, type, ...argsArray);
253
- }
254
- return match;
243
+ // Use the escape handler for three-phase processing
244
+ return processWithEscapes(doc, function(content) {
245
+ return content.replace(replacerRegex, function (match, type, args) {
246
+ let replacer = replacers.find(r => type.trim().match(r.test));
247
+ if (replacer) {
248
+ let argsArray = args ? args.trim().split(replacerArgsRegex) : [];
249
+ return replacer.transform(match, type, ...argsArray);
250
+ }
251
+ return match;
252
+ });
255
253
  });
256
254
  }
257
255
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-up-t",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
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": {
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Escape handler for substitution tags in Spec-Up
3
+ * Provides mechanism to display literal [[tag]] syntax without processing
4
+ *
5
+ * Implements a three-phase approach:
6
+ * 1. Pre-processing: Convert escaped sequences to temporary placeholders
7
+ * 2. Tag Processing: Existing substitution logic runs normally (placeholders ignored)
8
+ * 3. Post-processing: Restore escaped sequences as literals
9
+ */
10
+
11
+ const ESCAPED_PLACEHOLDER = '__SPEC_UP_ESCAPED_TAG__';
12
+
13
+ /**
14
+ * Pre-processes text to handle escaped substitution tags
15
+ * @param {string} text - Input text with potential escaped tags
16
+ * @returns {string} Text with escaped tags replaced by placeholders
17
+ */
18
+ function preProcessEscapes(text) {
19
+ if (!text || typeof text !== 'string') {
20
+ return text;
21
+ }
22
+
23
+ // Handle double backslash first: \\[[ → \__PLACEHOLDER__
24
+ let processed = text.replace(/\\\\(\[\[)/g, `\\${ESCAPED_PLACEHOLDER}`);
25
+
26
+ // Handle single backslash: \[[ → __PLACEHOLDER__
27
+ processed = processed.replace(/\\(\[\[)/g, ESCAPED_PLACEHOLDER);
28
+
29
+ return processed;
30
+ }
31
+
32
+ /**
33
+ * Post-processes text to restore escaped tags as literals
34
+ * @param {string} text - Text after substitution processing
35
+ * @returns {string} Text with placeholders restored to literal tags
36
+ */
37
+ function postProcessEscapes(text) {
38
+ if (!text || typeof text !== 'string') {
39
+ return text;
40
+ }
41
+
42
+ // Restore placeholders to literal [[
43
+ return text.replace(new RegExp(ESCAPED_PLACEHOLDER, 'g'), '[[');
44
+ }
45
+
46
+ /**
47
+ * Main processing function that wraps existing substitution logic
48
+ * @param {string} content - Raw markdown content
49
+ * @param {Function} processSubstitutions - Existing substitution processor
50
+ * @returns {string} Processed content with escape handling
51
+ */
52
+ function processWithEscapes(content, processSubstitutions) {
53
+ if (!content || typeof content !== 'string') {
54
+ return content;
55
+ }
56
+
57
+ const preProcessed = preProcessEscapes(content);
58
+ const substituted = processSubstitutions(preProcessed);
59
+ return postProcessEscapes(substituted);
60
+ }
61
+
62
+ module.exports = {
63
+ preProcessEscapes,
64
+ postProcessEscapes,
65
+ processWithEscapes,
66
+ ESCAPED_PLACEHOLDER
67
+ };
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { ESCAPED_PLACEHOLDER } = require('./escape-handler');
4
+
3
5
  /**
4
6
  * Configuration for custom template syntax [[example]] used throughout the markdown parsing
5
7
  * These constants define how template markers are identified and processed
@@ -64,6 +66,12 @@ module.exports = function (md, templates = {}) {
64
66
  md.inline.ruler.after('emphasis', 'templates', function templates_ruler(state, silent) {
65
67
  // Get the current parsing position
66
68
  var start = state.pos;
69
+
70
+ // Check if we're at an escaped placeholder - if so, skip processing
71
+ if (state.src.slice(start, start + ESCAPED_PLACEHOLDER.length) === ESCAPED_PLACEHOLDER) {
72
+ return false;
73
+ }
74
+
67
75
  // Check if we're at a template opening marker
68
76
  let prefix = state.src.slice(start, start + levels);
69
77
  if (prefix !== openString) return false;