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.
- package/assets/compiled/body.js +3 -2
- package/assets/compiled/head.css +5 -5
- package/assets/css/embedded-libraries/bootstrap.min.css +1 -1
- package/assets/css/header-navbar.css +4 -4
- package/assets/css/index.css +5 -4
- package/assets/css/refs.css +30 -0
- package/assets/css/terms-and-definitions.css +89 -1
- package/assets/js/github-issues.js +3 -3
- package/assets/js/insert-irefs.js +214 -0
- package/config/asset-map.json +2 -1
- package/examples/read-console-messages.js +102 -0
- package/gulpfile.js +42 -1
- package/index.js +49 -1
- package/package.json +2 -1
- package/src/create-docx.js +13 -6
- package/src/create-pdf.js +22 -18
- package/src/health-check.js +47 -629
- package/src/init.js +7 -3
- package/src/install-from-boilerplate/config-scripts-keys.js +1 -1
- package/src/markdown-it/README.md +2 -14
- package/src/markdown-it/index.js +1 -7
- package/src/parsers/template-tag-parser.js +42 -4
- package/src/pipeline/postprocessing/definition-list-postprocessor.js +4 -2
- package/src/pipeline/references/collect-external-references.js +101 -17
- package/src/pipeline/references/external-references-service.js +102 -21
- package/src/pipeline/references/fetch-terms-from-index.js +62 -1
- package/src/pipeline/references/process-xtrefs-data.js +67 -9
- package/src/pipeline/references/xtref-utils.js +22 -3
- package/src/pipeline/rendering/render-spec-document.js +0 -1
- package/src/run-healthcheck.js +177 -0
- package/src/utils/logger.js +129 -8
- package/src/utils/message-collector.js +144 -0
- package/src/utils/regex-patterns.js +3 -1
- package/templates/template.html +6 -6
- package/test/logger.test.js +290 -0
- package/test/message-collector.test.js +286 -0
- package/assets/css/insert-trefs.css +0 -1
- package/src/markdown-it/link-enhancement.js +0 -98
- 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.
|
|
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": {
|
package/src/create-docx.js
CHANGED
|
@@ -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(
|
|
266
|
-
|
|
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
|
-
|
|
331
|
+
Logger.success('DOCX generated successfully! Find the DOCX file in the docs directory.');
|
|
329
332
|
} catch (error) {
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
678
|
+
Logger.success('PDF generated successfully! Find the PDF in the docs directory.');
|
|
679
679
|
} catch (error) {
|
|
680
|
-
Logger.error('Error generating PDF
|
|
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
|
})();
|