spec-up-t 1.2.9 → 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.
@@ -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 cached files
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
- // Get the specs.json content from the repository to find the output_path
138
- const specsJsonUrl = `https://api.github.com/repos/${owner}/${repo}/contents/specs.json`;
139
- console.log(`Fetching specs.json from: ${specsJsonUrl}`);
69
+ // Use GitHub Pages URL if provided in options, otherwise fallback to raw repository
70
+ let indexHtmlUrl;
71
+ let commitHash = null;
140
72
 
141
- // Fetch specs.json content
142
- const specsJsonResponse = await axios.get(specsJsonUrl, { headers });
143
- if (specsJsonResponse.status !== 200) {
144
- console.log(`❌ Could not find specs.json in repository ${owner}/${repo}`);
145
- return null;
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
- // Decode specs.json content from base64
149
- const specsJsonContent = Buffer.from(specsJsonResponse.data.content, 'base64').toString('utf8');
150
- const specsJson = JSON.parse(specsJsonContent);
151
-
152
- // Get the output_path from specs.json
153
- const outputPath = specsJson.specs[0].output_path;
154
- if (!outputPath) {
155
- console.log(`❌ No output_path found in specs.json for repository ${owner}/${repo}`);
156
- return null;
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
- // Fix: Properly normalize the output path to ensure it doesn't have leading "./" or trailing "/"
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 cached files
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, options) {
10
+ async function processXTrefsData(allXTrefs, GITHUB_API_TOKEN, outputPathJSON, outputPathJS, outputPathJSTimeStamped) {
11
11
  try {
12
- // Clear the cache (remove the cache directory) if the cache option is set to false
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
- options
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
+ })();