spec-up-t 1.2.9 → 1.3.0
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/.github/copilot-instructions.md +2 -1
- package/assets/compiled/body.js +5 -5
- package/assets/compiled/head.css +1 -0
- package/assets/css/counter.css +104 -0
- package/assets/js/addAnchorsToTerms.js +13 -5
- package/assets/js/collapse-definitions.js +0 -3
- package/assets/js/custom-elements.js +25 -27
- package/assets/js/fix-last-dd.js +6 -3
- package/assets/js/highlight-heading-plus-sibling-nodes.js +0 -1
- package/assets/js/highlight-heading-plus-sibling-nodes.test.js +120 -0
- package/assets/js/insert-trefs.js +32 -28
- package/config/asset-map.json +1 -0
- package/index.js +33 -227
- package/package.json +4 -2
- package/sonar-project.properties +7 -0
- package/src/collect-external-references.js +22 -11
- package/src/collect-external-references.test.js +153 -2
- package/src/collectExternalReferences/fetchTermsFromIndex.js +65 -110
- package/src/collectExternalReferences/processXTrefsData.js +9 -11
- package/src/create-docx.js +332 -0
- package/src/create-pdf.js +243 -122
- package/src/fix-markdown-files.js +31 -34
- package/src/html-dom-processor.js +290 -0
- package/src/init.js +3 -0
- package/src/install-from-boilerplate/boilerplate/.github/workflows/menu.yml +51 -36
- package/src/install-from-boilerplate/boilerplate/spec/example-markup-in-markdown.md +0 -1
- package/src/install-from-boilerplate/boilerplate/spec/terms-and-definitions-intro.md +1 -5
- package/src/install-from-boilerplate/config-scripts-keys.js +4 -4
- package/src/install-from-boilerplate/menu.sh +6 -6
- package/src/markdown-it-extensions.js +54 -33
- package/src/references.js +18 -6
- package/templates/template.html +2 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { JSDOM } = require('jsdom');
|
|
4
|
+
const { Document, Packer, Paragraph, TextRun, HeadingLevel, TableOfContents, Table, TableRow, TableCell, WidthType, AlignmentType } = require('docx');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates DOCX metadata from config
|
|
8
|
+
*/
|
|
9
|
+
function createDocxMetadata(config) {
|
|
10
|
+
return {
|
|
11
|
+
title: config.specs[0].title || 'Untitled Document',
|
|
12
|
+
subject: config.specs[0].description || '',
|
|
13
|
+
creator: config.specs[0].author || '',
|
|
14
|
+
keywords: config.specs[0].keywords || [],
|
|
15
|
+
description: config.specs[0].description || '',
|
|
16
|
+
lastModifiedBy: 'Spec-Up DOCX Generator',
|
|
17
|
+
revision: 1,
|
|
18
|
+
createdAt: new Date(),
|
|
19
|
+
modifiedAt: new Date()
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Converts HTML heading to DOCX heading level
|
|
25
|
+
*/
|
|
26
|
+
function getHeadingLevel(tagName) {
|
|
27
|
+
const levels = {
|
|
28
|
+
'h1': HeadingLevel.HEADING_1,
|
|
29
|
+
'h2': HeadingLevel.HEADING_2,
|
|
30
|
+
'h3': HeadingLevel.HEADING_3,
|
|
31
|
+
'h4': HeadingLevel.HEADING_4,
|
|
32
|
+
'h5': HeadingLevel.HEADING_5,
|
|
33
|
+
'h6': HeadingLevel.HEADING_6
|
|
34
|
+
};
|
|
35
|
+
return levels[tagName.toLowerCase()] || HeadingLevel.HEADING_1;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Processes HTML node and converts to DOCX paragraphs
|
|
40
|
+
*/
|
|
41
|
+
function processNode(node, elements = []) {
|
|
42
|
+
if (node.nodeType === 3) { // Text node
|
|
43
|
+
const text = node.textContent.trim();
|
|
44
|
+
if (text) {
|
|
45
|
+
elements.push(new Paragraph({
|
|
46
|
+
children: [new TextRun(text)]
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
return elements;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (node.nodeType !== 1) return elements; // Skip non-element nodes
|
|
53
|
+
|
|
54
|
+
const tagName = node.tagName.toLowerCase();
|
|
55
|
+
|
|
56
|
+
switch (tagName) {
|
|
57
|
+
case 'h1':
|
|
58
|
+
case 'h2':
|
|
59
|
+
case 'h3':
|
|
60
|
+
case 'h4':
|
|
61
|
+
case 'h5':
|
|
62
|
+
case 'h6':
|
|
63
|
+
elements.push(new Paragraph({
|
|
64
|
+
text: node.textContent.trim(),
|
|
65
|
+
heading: getHeadingLevel(tagName)
|
|
66
|
+
}));
|
|
67
|
+
break;
|
|
68
|
+
|
|
69
|
+
case 'p':
|
|
70
|
+
const textRuns = [];
|
|
71
|
+
processInlineElements(node, textRuns);
|
|
72
|
+
if (textRuns.length > 0) {
|
|
73
|
+
elements.push(new Paragraph({
|
|
74
|
+
children: textRuns
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
|
|
79
|
+
case 'ul':
|
|
80
|
+
case 'ol':
|
|
81
|
+
const listItems = Array.from(node.children);
|
|
82
|
+
listItems.forEach(li => {
|
|
83
|
+
if (li.tagName.toLowerCase() === 'li') {
|
|
84
|
+
elements.push(new Paragraph({
|
|
85
|
+
text: li.textContent.trim(),
|
|
86
|
+
bullet: { level: 0 }
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
break;
|
|
91
|
+
|
|
92
|
+
case 'table':
|
|
93
|
+
const tableRows = Array.from(node.querySelectorAll('tr'));
|
|
94
|
+
if (tableRows.length > 0) {
|
|
95
|
+
const docxRows = tableRows.map(row => {
|
|
96
|
+
const cells = Array.from(row.querySelectorAll('td, th'));
|
|
97
|
+
return new TableRow({
|
|
98
|
+
children: cells.map(cell => new TableCell({
|
|
99
|
+
children: [new Paragraph({
|
|
100
|
+
text: cell.textContent.trim()
|
|
101
|
+
})],
|
|
102
|
+
width: {
|
|
103
|
+
size: 100 / cells.length,
|
|
104
|
+
type: WidthType.PERCENTAGE
|
|
105
|
+
}
|
|
106
|
+
}))
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
elements.push(new Table({
|
|
111
|
+
rows: docxRows,
|
|
112
|
+
width: {
|
|
113
|
+
size: 100,
|
|
114
|
+
type: WidthType.PERCENTAGE
|
|
115
|
+
}
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case 'blockquote':
|
|
121
|
+
elements.push(new Paragraph({
|
|
122
|
+
text: node.textContent.trim(),
|
|
123
|
+
indent: {
|
|
124
|
+
left: 720 // 0.5 inch in twips
|
|
125
|
+
}
|
|
126
|
+
}));
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case 'dl':
|
|
130
|
+
// Process definition lists
|
|
131
|
+
const dlItems = Array.from(node.children);
|
|
132
|
+
dlItems.forEach(item => {
|
|
133
|
+
if (item.tagName.toLowerCase() === 'dt') {
|
|
134
|
+
elements.push(new Paragraph({
|
|
135
|
+
children: [new TextRun({
|
|
136
|
+
text: item.textContent.trim(),
|
|
137
|
+
bold: true
|
|
138
|
+
})]
|
|
139
|
+
}));
|
|
140
|
+
} else if (item.tagName.toLowerCase() === 'dd') {
|
|
141
|
+
elements.push(new Paragraph({
|
|
142
|
+
text: item.textContent.trim(),
|
|
143
|
+
indent: {
|
|
144
|
+
left: 360 // 0.25 inch in twips
|
|
145
|
+
}
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
break;
|
|
150
|
+
|
|
151
|
+
default:
|
|
152
|
+
// For other elements, process their children
|
|
153
|
+
Array.from(node.childNodes).forEach(child => {
|
|
154
|
+
processNode(child, elements);
|
|
155
|
+
});
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return elements;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Processes inline elements within paragraphs
|
|
164
|
+
*/
|
|
165
|
+
function processInlineElements(node, textRuns) {
|
|
166
|
+
Array.from(node.childNodes).forEach(child => {
|
|
167
|
+
if (child.nodeType === 3) { // Text node
|
|
168
|
+
const text = child.textContent;
|
|
169
|
+
if (text.trim()) {
|
|
170
|
+
textRuns.push(new TextRun(text));
|
|
171
|
+
}
|
|
172
|
+
} else if (child.nodeType === 1) { // Element node
|
|
173
|
+
const tagName = child.tagName.toLowerCase();
|
|
174
|
+
const text = child.textContent.trim();
|
|
175
|
+
|
|
176
|
+
if (text) {
|
|
177
|
+
switch (tagName) {
|
|
178
|
+
case 'strong':
|
|
179
|
+
case 'b':
|
|
180
|
+
textRuns.push(new TextRun({
|
|
181
|
+
text: text,
|
|
182
|
+
bold: true
|
|
183
|
+
}));
|
|
184
|
+
break;
|
|
185
|
+
case 'em':
|
|
186
|
+
case 'i':
|
|
187
|
+
textRuns.push(new TextRun({
|
|
188
|
+
text: text,
|
|
189
|
+
italics: true
|
|
190
|
+
}));
|
|
191
|
+
break;
|
|
192
|
+
case 'code':
|
|
193
|
+
textRuns.push(new TextRun({
|
|
194
|
+
text: text,
|
|
195
|
+
font: 'Courier New'
|
|
196
|
+
}));
|
|
197
|
+
break;
|
|
198
|
+
case 'a':
|
|
199
|
+
textRuns.push(new TextRun({
|
|
200
|
+
text: text,
|
|
201
|
+
color: '0000FF',
|
|
202
|
+
underline: {}
|
|
203
|
+
}));
|
|
204
|
+
break;
|
|
205
|
+
default:
|
|
206
|
+
textRuns.push(new TextRun(text));
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Creates a title page
|
|
216
|
+
*/
|
|
217
|
+
function createTitlePage(config) {
|
|
218
|
+
const elements = [];
|
|
219
|
+
const spec = config.specs[0];
|
|
220
|
+
|
|
221
|
+
if (spec.title) {
|
|
222
|
+
elements.push(new Paragraph({
|
|
223
|
+
text: spec.title,
|
|
224
|
+
heading: HeadingLevel.TITLE,
|
|
225
|
+
alignment: AlignmentType.CENTER
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (spec.description) {
|
|
230
|
+
elements.push(new Paragraph({
|
|
231
|
+
text: spec.description,
|
|
232
|
+
alignment: AlignmentType.CENTER
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (spec.author) {
|
|
237
|
+
elements.push(new Paragraph({
|
|
238
|
+
text: `Author: ${spec.author}`,
|
|
239
|
+
alignment: AlignmentType.CENTER
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Add some spacing
|
|
244
|
+
elements.push(new Paragraph({ text: '' }));
|
|
245
|
+
elements.push(new Paragraph({ text: '' }));
|
|
246
|
+
|
|
247
|
+
return elements;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
(async () => {
|
|
251
|
+
try {
|
|
252
|
+
console.log('Starting DOCX generation...');
|
|
253
|
+
|
|
254
|
+
// Read and parse the specs.json file
|
|
255
|
+
const config = fs.readJsonSync('specs.json');
|
|
256
|
+
const metadata = createDocxMetadata(config);
|
|
257
|
+
|
|
258
|
+
// Extract configuration details
|
|
259
|
+
const outputPath = config.specs[0].output_path;
|
|
260
|
+
const filePath = path.resolve(process.cwd(), outputPath, 'index.html');
|
|
261
|
+
|
|
262
|
+
// Check if HTML file exists
|
|
263
|
+
if (!fs.existsSync(filePath)) {
|
|
264
|
+
console.error(`❌ HTML file not found at ${filePath}`);
|
|
265
|
+
console.log('Please run "npm run render" first to generate the HTML file.');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Read and parse the HTML file
|
|
270
|
+
const htmlContent = fs.readFileSync(filePath, 'utf8');
|
|
271
|
+
const dom = new JSDOM(htmlContent);
|
|
272
|
+
const document = dom.window.document;
|
|
273
|
+
|
|
274
|
+
// Remove unnecessary elements
|
|
275
|
+
document.querySelectorAll('script, style, .d-print-none, [style*="display: none"]').forEach(el => el.remove());
|
|
276
|
+
|
|
277
|
+
// Start building the DOCX document
|
|
278
|
+
const docElements = [];
|
|
279
|
+
|
|
280
|
+
// Add title page
|
|
281
|
+
docElements.push(...createTitlePage(config));
|
|
282
|
+
|
|
283
|
+
// Add table of contents placeholder
|
|
284
|
+
docElements.push(new TableOfContents('Table of Contents', {
|
|
285
|
+
hyperlink: true,
|
|
286
|
+
headingStyleRange: '1-6'
|
|
287
|
+
}));
|
|
288
|
+
|
|
289
|
+
// Add page break after TOC
|
|
290
|
+
docElements.push(new Paragraph({
|
|
291
|
+
text: '',
|
|
292
|
+
pageBreakBefore: true
|
|
293
|
+
}));
|
|
294
|
+
|
|
295
|
+
// Process the main content
|
|
296
|
+
const mainContent = document.querySelector('main') || document.body;
|
|
297
|
+
processNode(mainContent, docElements);
|
|
298
|
+
|
|
299
|
+
// Create the DOCX document
|
|
300
|
+
const doc = new Document({
|
|
301
|
+
properties: {
|
|
302
|
+
title: metadata.title,
|
|
303
|
+
subject: metadata.subject,
|
|
304
|
+
creator: metadata.creator,
|
|
305
|
+
keywords: metadata.keywords.join(', '),
|
|
306
|
+
description: metadata.description,
|
|
307
|
+
lastModifiedBy: metadata.lastModifiedBy,
|
|
308
|
+
revision: metadata.revision,
|
|
309
|
+
createdAt: metadata.createdAt,
|
|
310
|
+
modifiedAt: metadata.modifiedAt
|
|
311
|
+
},
|
|
312
|
+
sections: [{
|
|
313
|
+
children: docElements
|
|
314
|
+
}]
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Generate the DOCX file
|
|
318
|
+
const buffer = await Packer.toBuffer(doc);
|
|
319
|
+
const docxPath = path.resolve(process.cwd(), 'docs/index.docx');
|
|
320
|
+
|
|
321
|
+
// Ensure docs directory exists
|
|
322
|
+
fs.ensureDirSync(path.dirname(docxPath));
|
|
323
|
+
|
|
324
|
+
// Write the DOCX file
|
|
325
|
+
fs.writeFileSync(docxPath, buffer);
|
|
326
|
+
|
|
327
|
+
console.log('✅ DOCX generated successfully! Find the DOCX file in the docs directory.');
|
|
328
|
+
} catch (error) {
|
|
329
|
+
console.error('❌ Error generating DOCX:', error);
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
})();
|