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.
- package/.github/copilot-instructions.md +3 -1
- package/assets/compiled/body.js +5 -4
- package/assets/compiled/head.css +1 -0
- package/assets/compiled/refs.json +1 -1
- package/assets/css/highlight-heading-plus-sibling-nodes.css +6 -0
- package/assets/css/index.css +9 -0
- package/assets/js/addAnchorsToTerms.js +13 -5
- package/assets/js/collapse-definitions.js +0 -6
- package/assets/js/fix-last-dd.js +6 -3
- package/assets/js/highlight-heading-plus-sibling-nodes.js +258 -0
- package/assets/js/insert-trefs.js +32 -28
- package/config/asset-map.json +2 -0
- package/gulpfile.js +8 -2
- package/index.js +45 -241
- package/package.json +2 -1
- package/sonar-project.properties +6 -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/escape-handler.js +67 -0
- 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 +4 -13
- 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 +60 -31
- package/src/references.js +18 -6
- package/templates/template.html +2 -0
- package/test-default-definitions.js +55 -0
- package/test-edge-cases.md +20 -0
- package/test-fix-markdown.js +11 -0
- package/test-no-def.md +22 -0
|
@@ -11,67 +11,11 @@ const path = require('path');
|
|
|
11
11
|
const { JSDOM } = require('jsdom');
|
|
12
12
|
const axios = require('axios');
|
|
13
13
|
const { addPath, getPath, getAllPaths } = require('../../config/paths');
|
|
14
|
-
const crypto = require('crypto');
|
|
15
14
|
|
|
16
|
-
// Directory to store
|
|
15
|
+
// Directory to store fetched data files
|
|
17
16
|
const CACHE_DIR = getPath('githubcache');
|
|
18
17
|
|
|
19
|
-
/**
|
|
20
|
-
* Generates a cache key based on repository information
|
|
21
|
-
* @param {string} owner - Repository owner
|
|
22
|
-
* @param {string} repo - Repository name
|
|
23
|
-
* @returns {string} - Cache key
|
|
24
|
-
*/
|
|
25
|
-
function generateCacheKey(owner, repo) {
|
|
26
|
-
const input = `${owner}-${repo}-index`;
|
|
27
|
-
return crypto.createHash('md5').update(input).digest('hex');
|
|
28
|
-
}
|
|
29
18
|
|
|
30
|
-
/**
|
|
31
|
-
* Checks if a cached version exists and is valid
|
|
32
|
-
* @param {string} cacheKey - Cache key
|
|
33
|
-
* @param {object} options - Options object
|
|
34
|
-
* @param {number} options.cacheTTL - Time-to-live for cache in milliseconds (default: 24 hours)
|
|
35
|
-
* @returns {object|null} - Cached data or null if not found or expired
|
|
36
|
-
* @example
|
|
37
|
-
* const cacheTTL = options.cacheTTL || 24 * 60 * 60 * 1000; // Default: 24 hours
|
|
38
|
-
*/
|
|
39
|
-
function getFromCache(cacheKey, options = {}) {
|
|
40
|
-
const cachePath = path.join(CACHE_DIR, `${cacheKey}.json`);
|
|
41
|
-
const cacheTTL = 0;
|
|
42
|
-
|
|
43
|
-
if (!fs.existsSync(cachePath)) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const cacheData = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
48
|
-
const cacheTime = new Date(cacheData.timestamp).getTime();
|
|
49
|
-
const currentTime = new Date().getTime();
|
|
50
|
-
|
|
51
|
-
// Check if cache is expired
|
|
52
|
-
if (currentTime - cacheTime > cacheTTL) {
|
|
53
|
-
console.log(`Cache expired for key: ${cacheKey}`);
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
console.log(`Using cached data for key: ${cacheKey}`);
|
|
58
|
-
return cacheData;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Saves data to cache
|
|
63
|
-
* @param {string} cacheKey - Cache key
|
|
64
|
-
* @param {object} data - Data to cache
|
|
65
|
-
*/
|
|
66
|
-
function saveToCache(cacheKey, data) {
|
|
67
|
-
const cachePath = path.join(CACHE_DIR, `${cacheKey}.json`);
|
|
68
|
-
const cacheData = {
|
|
69
|
-
timestamp: new Date().toISOString(),
|
|
70
|
-
...data
|
|
71
|
-
};
|
|
72
|
-
fs.writeFileSync(cachePath, JSON.stringify(cacheData, null, 2));
|
|
73
|
-
console.log(`Saved to cache: ${cacheKey}`);
|
|
74
|
-
}
|
|
75
19
|
|
|
76
20
|
/**
|
|
77
21
|
* Fetches the latest commit hash for a specific file in a repository
|
|
@@ -107,77 +51,93 @@ async function getFileCommitHash(token, owner, repo, filePath, headers) {
|
|
|
107
51
|
}
|
|
108
52
|
|
|
109
53
|
/**
|
|
110
|
-
* Fetches all terms and definitions from a repository's index.html
|
|
54
|
+
* Fetches all terms and definitions from a repository's GitHub Pages index.html
|
|
111
55
|
* @param {string} token - GitHub API Token
|
|
112
56
|
* @param {string} owner - Repository owner
|
|
113
57
|
* @param {string} repo - Repository name
|
|
114
|
-
* @param {object} options - Additional options
|
|
58
|
+
* @param {object} options - Additional options including ghPageUrl
|
|
115
59
|
* @returns {object|null} - Object containing all terms or null if error
|
|
116
60
|
*/
|
|
117
61
|
async function fetchAllTermsFromIndex(token, owner, repo, options = {}) {
|
|
118
62
|
try {
|
|
119
|
-
// Generate cache key based on repo information
|
|
120
|
-
const cacheKey = generateCacheKey(owner, repo);
|
|
121
|
-
let cachedData = null;
|
|
122
|
-
|
|
123
|
-
// Check cache first if caching is enabled
|
|
124
|
-
if (options.cache !== false) {
|
|
125
|
-
cachedData = getFromCache(cacheKey, options);
|
|
126
|
-
if (cachedData) {
|
|
127
|
-
return cachedData;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
63
|
// Configure headers for GitHub API
|
|
132
64
|
const headers = {};
|
|
133
65
|
if (token) {
|
|
134
66
|
headers['Authorization'] = `token ${token}`;
|
|
135
67
|
}
|
|
136
68
|
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
69
|
+
// Use GitHub Pages URL if provided in options, otherwise fallback to raw repository
|
|
70
|
+
let indexHtmlUrl;
|
|
71
|
+
let commitHash = null;
|
|
140
72
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
73
|
+
if (options.ghPageUrl) {
|
|
74
|
+
// Fetch from GitHub Pages (deployed HTML)
|
|
75
|
+
indexHtmlUrl = options.ghPageUrl.endsWith('/') ?
|
|
76
|
+
`${options.ghPageUrl}index.html` :
|
|
77
|
+
`${options.ghPageUrl}/index.html`;
|
|
78
|
+
console.log(`Fetching index.html from GitHub Pages: ${indexHtmlUrl}`);
|
|
79
|
+
|
|
80
|
+
// For GitHub Pages, we'll try to get the commit hash from the main branch
|
|
81
|
+
try {
|
|
82
|
+
const mainBranchUrl = `https://api.github.com/repos/${owner}/${repo}/branches/main`;
|
|
83
|
+
const branchResponse = await axios.get(mainBranchUrl, { headers });
|
|
84
|
+
if (branchResponse.status === 200) {
|
|
85
|
+
commitHash = branchResponse.data.commit.sha;
|
|
86
|
+
console.log(`✅ Got commit hash from main branch: ${commitHash}`);
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.log(`⚠️ Could not get commit hash from main branch: ${error.message}`);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
// Fallback to raw repository method
|
|
93
|
+
console.log(`⚠️ No GitHub Pages URL provided, falling back to repository method`);
|
|
94
|
+
|
|
95
|
+
// Get the specs.json content from the repository to find the output_path
|
|
96
|
+
const specsJsonUrl = `https://api.github.com/repos/${owner}/${repo}/contents/specs.json`;
|
|
97
|
+
console.log(`Fetching specs.json from: ${specsJsonUrl}`);
|
|
147
98
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
99
|
+
// Fetch specs.json content
|
|
100
|
+
const specsJsonResponse = await axios.get(specsJsonUrl, { headers });
|
|
101
|
+
if (specsJsonResponse.status !== 200) {
|
|
102
|
+
console.log(`❌ Could not find specs.json in repository ${owner}/${repo}`);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Decode specs.json content from base64
|
|
107
|
+
const specsJsonContent = Buffer.from(specsJsonResponse.data.content, 'base64').toString('utf8');
|
|
108
|
+
const specsJson = JSON.parse(specsJsonContent);
|
|
109
|
+
|
|
110
|
+
// Get the output_path from specs.json
|
|
111
|
+
const outputPath = specsJson.specs[0].output_path;
|
|
112
|
+
if (!outputPath) {
|
|
113
|
+
console.log(`❌ No output_path found in specs.json for repository ${owner}/${repo}`);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fix: Properly normalize the output path to ensure it doesn't have leading "./" or trailing "/"
|
|
118
|
+
const normalizedOutputPath = outputPath.replace(/^\.\//, '').replace(/\/$/, '');
|
|
119
|
+
|
|
120
|
+
// Create the path to the index.html file
|
|
121
|
+
const indexHtmlPath = `${normalizedOutputPath}/index.html`;
|
|
122
|
+
|
|
123
|
+
// Fetch the index.html content with properly constructed URL
|
|
124
|
+
indexHtmlUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/${indexHtmlPath}`;
|
|
125
|
+
console.log(`Fetching index.html from raw repository: ${indexHtmlUrl}`);
|
|
126
|
+
|
|
127
|
+
// Get the commit hash for the index.html file
|
|
128
|
+
commitHash = await getFileCommitHash(token, owner, repo, indexHtmlPath, headers);
|
|
129
|
+
if (!commitHash) {
|
|
130
|
+
console.log(`⚠️ Could not get commit hash for index.html, continuing without it`);
|
|
131
|
+
}
|
|
157
132
|
}
|
|
158
133
|
|
|
159
|
-
//
|
|
160
|
-
const normalizedOutputPath = outputPath.replace(/^\.\//, '').replace(/\/$/, '');
|
|
161
|
-
|
|
162
|
-
// Create the path to the index.html file
|
|
163
|
-
const indexHtmlPath = `${normalizedOutputPath}/index.html`;
|
|
164
|
-
|
|
165
|
-
// Fetch the index.html content with properly constructed URL
|
|
166
|
-
const indexHtmlUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/${indexHtmlPath}`;
|
|
167
|
-
console.log(`Fetching index.html from: ${indexHtmlUrl}`);
|
|
168
|
-
|
|
134
|
+
// Fetch the index.html content
|
|
169
135
|
const indexHtmlResponse = await axios.get(indexHtmlUrl, { headers });
|
|
170
136
|
if (indexHtmlResponse.status !== 200) {
|
|
171
137
|
console.log(`❌ Could not find index.html at ${indexHtmlUrl}`);
|
|
172
138
|
return null;
|
|
173
139
|
}
|
|
174
140
|
|
|
175
|
-
// Get the commit hash for the index.html file
|
|
176
|
-
const commitHash = await getFileCommitHash(token, owner, repo, indexHtmlPath, headers);
|
|
177
|
-
if (!commitHash) {
|
|
178
|
-
console.log(`⚠️ Could not get commit hash for index.html, continuing without it`);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
141
|
const htmlContent = indexHtmlResponse.data;
|
|
182
142
|
|
|
183
143
|
// Parse HTML using JSDOM
|
|
@@ -257,11 +217,6 @@ async function fetchAllTermsFromIndex(token, owner, repo, options = {}) {
|
|
|
257
217
|
fs.writeFileSync(outputFilePath, JSON.stringify(result, null, 2));
|
|
258
218
|
console.log(`✅ Saved ${terms.length} terms to ${outputFilePath}`);
|
|
259
219
|
|
|
260
|
-
// Save to cache if enabled
|
|
261
|
-
if (options.cache !== false) {
|
|
262
|
-
saveToCache(cacheKey, result);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
220
|
return result;
|
|
266
221
|
|
|
267
222
|
} catch (error) {
|
|
@@ -4,19 +4,12 @@ const { matchTerm } = require('./matchTerm.js');
|
|
|
4
4
|
const { addPath, getPath, getAllPaths } = require('../../config/paths');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
|
-
// Directory to store
|
|
7
|
+
// Directory to store fetched data files
|
|
8
8
|
const CACHE_DIR = getPath('githubcache');
|
|
9
9
|
|
|
10
|
-
async function processXTrefsData(allXTrefs, GITHUB_API_TOKEN, outputPathJSON, outputPathJS, outputPathJSTimeStamped
|
|
10
|
+
async function processXTrefsData(allXTrefs, GITHUB_API_TOKEN, outputPathJSON, outputPathJS, outputPathJSTimeStamped) {
|
|
11
11
|
try {
|
|
12
|
-
//
|
|
13
|
-
if (options.cache === false) {
|
|
14
|
-
if (fs.existsSync(CACHE_DIR)) {
|
|
15
|
-
fs.rmdirSync(CACHE_DIR, { recursive: true });
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Ensure the cache directory exists, so that we can store the fetched data
|
|
12
|
+
// Ensure the directory exists, so that we can store the fetched data
|
|
20
13
|
if (!fs.existsSync(CACHE_DIR)) {
|
|
21
14
|
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
22
15
|
}
|
|
@@ -51,12 +44,17 @@ async function processXTrefsData(allXTrefs, GITHUB_API_TOKEN, outputPathJSON, ou
|
|
|
51
44
|
const repoGroup = xrefsByRepo[repoKey];
|
|
52
45
|
console.log(`Processing repository: ${repoKey} (${repoGroup.xtrefs.length} terms)`);
|
|
53
46
|
|
|
47
|
+
// Get the GitHub Pages URL from the first xtref in this repo group
|
|
48
|
+
const ghPageUrl = repoGroup.xtrefs[0]?.ghPageUrl;
|
|
49
|
+
|
|
54
50
|
// First, fetch all terms from this repository
|
|
55
51
|
const allTermsData = await fetchAllTermsFromIndex(
|
|
56
52
|
GITHUB_API_TOKEN,
|
|
57
53
|
repoGroup.owner,
|
|
58
54
|
repoGroup.repo,
|
|
59
|
-
|
|
55
|
+
{
|
|
56
|
+
ghPageUrl: ghPageUrl // Pass the GitHub Pages URL
|
|
57
|
+
}
|
|
60
58
|
);
|
|
61
59
|
|
|
62
60
|
if (!allTermsData) {
|
|
@@ -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: tagName === 'ul' ? { level: 0 } : { 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
|
+
})();
|