spec-up-t 1.4.1 → 1.6.0-beta.1

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 (39) hide show
  1. package/assets/compiled/body.js +3 -2
  2. package/assets/compiled/head.css +5 -5
  3. package/assets/css/embedded-libraries/bootstrap.min.css +1 -1
  4. package/assets/css/header-navbar.css +4 -4
  5. package/assets/css/index.css +5 -4
  6. package/assets/css/refs.css +30 -0
  7. package/assets/css/terms-and-definitions.css +89 -1
  8. package/assets/js/github-issues.js +3 -3
  9. package/assets/js/insert-irefs.js +214 -0
  10. package/config/asset-map.json +2 -1
  11. package/examples/read-console-messages.js +102 -0
  12. package/gulpfile.js +42 -1
  13. package/index.js +49 -1
  14. package/package.json +2 -1
  15. package/src/create-docx.js +13 -6
  16. package/src/create-pdf.js +22 -18
  17. package/src/health-check.js +47 -629
  18. package/src/init.js +7 -3
  19. package/src/install-from-boilerplate/config-scripts-keys.js +1 -1
  20. package/src/markdown-it/README.md +2 -14
  21. package/src/markdown-it/index.js +1 -7
  22. package/src/parsers/template-tag-parser.js +42 -4
  23. package/src/pipeline/postprocessing/definition-list-postprocessor.js +4 -2
  24. package/src/pipeline/references/collect-external-references.js +101 -17
  25. package/src/pipeline/references/external-references-service.js +102 -21
  26. package/src/pipeline/references/fetch-terms-from-index.js +62 -1
  27. package/src/pipeline/references/process-xtrefs-data.js +67 -9
  28. package/src/pipeline/references/xtref-utils.js +22 -3
  29. package/src/pipeline/rendering/render-spec-document.js +0 -1
  30. package/src/run-healthcheck.js +177 -0
  31. package/src/utils/logger.js +129 -8
  32. package/src/utils/message-collector.js +144 -0
  33. package/src/utils/regex-patterns.js +3 -1
  34. package/templates/template.html +6 -6
  35. package/test/logger.test.js +290 -0
  36. package/test/message-collector.test.js +286 -0
  37. package/assets/css/insert-trefs.css +0 -1
  38. package/src/markdown-it/link-enhancement.js +0 -98
  39. package/src/utils/LOGGER.md +0 -81
package/gulpfile.js CHANGED
@@ -32,6 +32,40 @@ async function fetchSpecRefs() {
32
32
  }).catch(e => console.log(e));
33
33
  }
34
34
 
35
+ /**
36
+ * Removes forbidden control characters from compiled JavaScript files
37
+ * to ensure W3C HTML validation compliance.
38
+ *
39
+ * Specifically removes:
40
+ * - U+0001 (SOH - Start of Heading)
41
+ * - U+001B (ESC - Escape, used in ANSI color codes)
42
+ *
43
+ * These characters appear in third-party libraries (mermaid.js, dagre-d3)
44
+ * and cause W3C validation errors when embedded in HTML.
45
+ *
46
+ * @param {string} filePath - Absolute path to the JavaScript file to clean
47
+ * @returns {Promise<void>}
48
+ */
49
+ async function removeForbiddenControlCharacters(filePath) {
50
+ try {
51
+ let content = await fs.readFile(filePath, 'utf8');
52
+
53
+ // Remove U+0001 (SOH - Start of Heading)
54
+ // This appears in dagre-d3 library as a null character separator
55
+ content = content.replace(/\x01/g, '');
56
+
57
+ // Remove U+001B (ESC - Escape)
58
+ // This appears in ANSI color codes like [35m, [31m, etc.
59
+ content = content.replace(/\x1b/g, '');
60
+
61
+ await fs.writeFile(filePath, content, 'utf8');
62
+ console.log(`✓ Cleaned forbidden characters from ${filePath}`);
63
+ } catch (error) {
64
+ console.error(`✗ Error cleaning ${filePath}:`, error.message);
65
+ throw error;
66
+ }
67
+ }
68
+
35
69
  async function compileAssets() {
36
70
  await fs.ensureDir(compileLocation);
37
71
  return new Promise(resolve => {
@@ -48,7 +82,14 @@ async function compileAssets() {
48
82
  .pipe(terser())
49
83
  .pipe(concat('body.js'))
50
84
  .pipe(gulp.dest(compileLocation))
51
- ).on('finish', function () {
85
+ ).on('finish', async function () {
86
+ // Post-process compiled JavaScript files to remove forbidden control characters
87
+ try {
88
+ await removeForbiddenControlCharacters(`${compileLocation}/head.js`);
89
+ await removeForbiddenControlCharacters(`${compileLocation}/body.js`);
90
+ } catch (error) {
91
+ console.error('Failed to clean compiled files:', error);
92
+ }
52
93
  resolve();
53
94
  })
54
95
  });
package/index.js CHANGED
@@ -1,7 +1,20 @@
1
1
  const { initialize } = require('./src/init');
2
2
  const Logger = require('./src/utils/logger');
3
+ const messageCollector = require('./src/utils/message-collector');
3
4
 
4
5
  module.exports = async function (options = {}) {
6
+ // Start collecting messages if requested
7
+ const shouldCollectMessages = options.collectMessages !== false; // Collect by default
8
+
9
+ if (shouldCollectMessages) {
10
+ // Only clear messages if not called from another operation (like collectExternalReferences)
11
+ // If skipClear is true, we're continuing from a previous operation
12
+ if (!options.skipClear) {
13
+ messageCollector.clearMessages();
14
+ }
15
+ messageCollector.startCollecting('render');
16
+ }
17
+
5
18
  try {
6
19
  const { initializeConfig } = require('./src/pipeline/configuration/prepare-spec-configuration.js');
7
20
  let toc = '';
@@ -137,8 +150,16 @@ module.exports = async function (options = {}) {
137
150
 
138
151
  // Run render and wait for it
139
152
  render(spec, assetTags, { externalReferences, references, definitions, specGroups, noticeTitles }, config, template, assets, Logger, md, externalSpecsList)
140
- .then(() => {
153
+ .then(async () => {
141
154
  Logger.info('Render completed for:', spec.destination);
155
+
156
+ // Save collected messages
157
+ if (shouldCollectMessages) {
158
+ messageCollector.stopCollecting();
159
+ const messagePath = await messageCollector.saveMessages();
160
+ Logger.success(`Console messages saved to: ${messagePath}`);
161
+ }
162
+
142
163
  if (options.nowatch) {
143
164
  Logger.info('Exiting with nowatch');
144
165
  process.exit(0);
@@ -146,6 +167,15 @@ module.exports = async function (options = {}) {
146
167
  })
147
168
  .catch((e) => {
148
169
  Logger.error('Render failed:', e.message);
170
+
171
+ // Save messages even on failure
172
+ if (shouldCollectMessages) {
173
+ messageCollector.stopCollecting();
174
+ messageCollector.saveMessages().catch(() => {
175
+ // Silent fail on save error
176
+ });
177
+ }
178
+
149
179
  process.exit(1);
150
180
  });
151
181
 
@@ -159,10 +189,28 @@ module.exports = async function (options = {}) {
159
189
  });
160
190
  } catch (error) {
161
191
  Logger.error(`Error during initialization or module execution: ${error.message}`);
192
+
193
+ // Save messages even on error
194
+ if (shouldCollectMessages) {
195
+ messageCollector.stopCollecting();
196
+ await messageCollector.saveMessages().catch(() => {
197
+ // Silent fail on save error
198
+ });
199
+ }
200
+
162
201
  throw error; // Re-throw to let the caller handle the error
163
202
  }
164
203
  } catch (error) {
165
204
  Logger.error(`Error during initialization: ${error.message}`);
205
+
206
+ // Save messages even on error
207
+ if (shouldCollectMessages) {
208
+ messageCollector.stopCollecting();
209
+ await messageCollector.saveMessages().catch(() => {
210
+ // Silent fail on save error
211
+ });
212
+ }
213
+
166
214
  throw error; // Re-throw to let the caller handle the error
167
215
  }
168
216
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-up-t",
3
- "version": "1.4.1",
3
+ "version": "1.6.0-beta.1",
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": {
@@ -64,6 +64,7 @@
64
64
  "prismjs": "^1.24.0",
65
65
  "puppeteer": "^23.2.1",
66
66
  "readline-sync": "^1.4.10",
67
+ "spec-up-t-healthcheck": "^1.0.0",
67
68
  "yargs": "^17.7.2"
68
69
  },
69
70
  "overrides": {
@@ -173,7 +173,7 @@ function processInlineElements(node, textRuns) {
173
173
  } else if (child.nodeType === 1) { // Element node
174
174
  const tagName = child.tagName.toLowerCase();
175
175
  const text = child.textContent.trim();
176
-
176
+
177
177
  if (text) {
178
178
  switch (tagName) {
179
179
  case 'strong':
@@ -262,8 +262,11 @@ function createTitlePage(config) {
262
262
 
263
263
  // Check if HTML file exists
264
264
  if (!fs.existsSync(filePath)) {
265
- Logger.error(`HTML file not found at ${filePath}`);
266
- Logger.info('Please run "npm run render" first to generate the HTML file.');
265
+ Logger.error('HTML file not found', {
266
+ context: 'Cannot generate DOCX without rendered HTML',
267
+ hint: 'Run "npm run render" first to generate the HTML file, then run "npm run docx"',
268
+ details: `Expected file: ${filePath}`
269
+ });
267
270
  return;
268
271
  }
269
272
 
@@ -325,9 +328,13 @@ function createTitlePage(config) {
325
328
  // Write the DOCX file
326
329
  fs.writeFileSync(docxPath, buffer);
327
330
 
328
- Logger.success('DOCX generated successfully! Find the DOCX file in the docs directory.');
331
+ Logger.success('DOCX generated successfully! Find the DOCX file in the docs directory.');
329
332
  } catch (error) {
330
- Logger.error('Error generating DOCX:', error);
331
- process.exit(1);
333
+ Logger.error('Error generating DOCX', {
334
+ context: 'Failed during DOCX document generation',
335
+ hint: 'Ensure the HTML file is valid and all dependencies are installed. Check that you have write permissions for the output directory',
336
+ details: error.message
337
+ });
338
+ process.exit(1);
332
339
  }
333
340
  })();
package/src/create-pdf.js CHANGED
@@ -198,7 +198,7 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
198
198
  // Insert the TOC after the title section
199
199
  document.body.insertBefore(tocContainer, titleWrapper.nextSibling);
200
200
 
201
- console.log('Generated a Table of Contents with ' + headings.length + ' entries.');
201
+ console.log('Generated a Table of Contents with ' + headings.length + ' entries.');
202
202
  }
203
203
  }
204
204
  }, logo, logoLink, title, description);
@@ -254,7 +254,7 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
254
254
 
255
255
  // If TOC doesn't exist, we'll need to create one
256
256
  if (!document.getElementById('toc')) {
257
- console.log('No TOC found in the document. Will create one.');
257
+ console.log('No TOC found in the document. Will create one.');
258
258
  }
259
259
  });
260
260
 
@@ -508,7 +508,7 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
508
508
  }
509
509
  });
510
510
 
511
- Logger.process('Generating PDF with proper TOC page numbers...');
511
+ Logger.process('Generating PDF with proper TOC page numbers...');
512
512
 
513
513
  // First, generate a draft PDF to calculate the page positions of each heading
514
514
  const draftPdfBuffer = await page.pdf({
@@ -530,28 +530,28 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
530
530
  // Find the PDF TOC
531
531
  const pdfToc = document.getElementById('pdf-toc');
532
532
  if (!pdfToc) return;
533
-
533
+
534
534
  const tocEntries = pdfToc.querySelectorAll('.toc-page-number');
535
535
  const originalToc = document.getElementById('toc');
536
-
536
+
537
537
  if (originalToc) {
538
538
  // Get all links from the original TOC that have tooltip data
539
539
  const originalLinks = originalToc.querySelectorAll('a[title], a[data-bs-title]');
540
-
540
+
541
541
  // Create a mapping from heading IDs to page numbers based on tooltips
542
542
  const idToPageMap = {};
543
-
543
+
544
544
  originalLinks.forEach(link => {
545
545
  // Extract the heading ID from href
546
546
  const href = link.getAttribute('href');
547
547
  if (!href || !href.startsWith('#')) return;
548
-
548
+
549
549
  const headingId = href.substring(1);
550
-
550
+
551
551
  // Extract page number from tooltip text (e.g., "Go to page 5")
552
552
  const tooltipText = link.getAttribute('title') || link.getAttribute('data-bs-title');
553
553
  if (!tooltipText) return;
554
-
554
+
555
555
  const pageNumberMatch = tooltipText.match(/Go to page (\d+)/i);
556
556
  if (pageNumberMatch && pageNumberMatch[1]) {
557
557
  const pageNumber = parseInt(pageNumberMatch[1], 10);
@@ -560,7 +560,7 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
560
560
  }
561
561
  }
562
562
  });
563
-
563
+
564
564
  // Now update the TOC page numbers using the extracted values
565
565
  tocEntries.forEach(entry => {
566
566
  const targetId = entry.getAttribute('data-for-id');
@@ -578,10 +578,10 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
578
578
  } else {
579
579
  // Fallback to old estimation method if original TOC is not available
580
580
  console.log('Original TOC not found, using page number estimation method');
581
-
581
+
582
582
  // Find all headings with IDs (potential TOC targets)
583
583
  const headingsWithIds = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6')).filter(h => h.id);
584
-
584
+
585
585
  // Use real offsets for more accurate page numbers
586
586
  const idToPosition = {};
587
587
 
@@ -643,12 +643,12 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
643
643
 
644
644
  await browser.close();
645
645
 
646
- Logger.success('PDF generated by Puppeteer. Processing with pdf-lib for ISO compliance...');
646
+ Logger.success('PDF generated by Puppeteer. Processing with pdf-lib for ISO compliance...');
647
647
 
648
648
  // Optimize PDF with pdf-lib for ISO compliance
649
649
  try {
650
650
  const pdfDoc = await pdfLib.PDFDocument.load(pdfBuffer);
651
-
651
+
652
652
  // Set ISO-compliant metadata (this is safer than XMP embedding)
653
653
  pdfDoc.setTitle(metadata.title);
654
654
  pdfDoc.setAuthor(metadata.author);
@@ -666,7 +666,7 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
666
666
  useObjectStreams: false, // Required for PDF/A compliance
667
667
  addDefaultPage: false
668
668
  });
669
-
669
+
670
670
  fs.writeFileSync('docs/index.pdf', optimizedPdfBytes);
671
671
  Logger.success('PDF saved with ISO compliance features.');
672
672
  } catch (pdfError) {
@@ -675,8 +675,12 @@ async function createTOCIfNeeded(page, logo, logoLink, title, description) {
675
675
  fs.writeFileSync('docs/index.pdf', pdfBuffer);
676
676
  }
677
677
 
678
- Logger.success('PDF generated successfully! Find the PDF in the docs directory.');
678
+ Logger.success('PDF generated successfully! Find the PDF in the docs directory.');
679
679
  } catch (error) {
680
- Logger.error('Error generating PDF: %o', error);
680
+ Logger.error('Error generating PDF', {
681
+ context: 'Failed during PDF document generation',
682
+ hint: 'Ensure the HTML file exists (run "npm run render" first), Puppeteer is installed, and you have write permissions',
683
+ details: error.message || error
684
+ });
681
685
  }
682
686
  })();