spec-up-t 1.2.5 → 1.2.7
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 +9 -9
- package/assets/compiled/head.css +6 -5
- package/assets/css/collapse-definitions.css +41 -0
- package/assets/css/create-pdf.css +339 -0
- package/assets/css/horizontal-scroll-hint.css +6 -0
- package/assets/css/image-full-size.css +1 -5
- package/assets/css/index.css +5 -0
- package/assets/css/search.css +8 -8
- package/assets/css/terms-and-definitions.css +3 -3
- package/assets/js/add-bootstrap-classes-to-images.js +2 -5
- package/assets/js/collapse-definitions.js +260 -34
- package/assets/js/collapse-meta-info.js +37 -10
- package/assets/js/collapsibleMenu.js +0 -24
- package/assets/js/create-term-filter.js +36 -18
- package/assets/js/hide-show-utility-container.js +0 -1
- package/assets/js/horizontal-scroll-hint.js +2 -2
- package/assets/js/image-full-size.js +0 -2
- package/assets/js/insert-trefs.js +135 -49
- package/assets/js/search.js +34 -20
- package/{src → config}/asset-map.json +1 -0
- package/gulpfile.js +1 -1
- package/index.js +3 -3
- package/jest.config.js +20 -0
- package/package.json +3 -2
- package/src/collectExternalReferences/fetchTermsFromIndex.1.js +340 -0
- package/src/collectExternalReferences/fetchTermsFromIndex.js +1 -1
- package/src/collectExternalReferences/processXTrefsData.js +1 -1
- package/src/create-pdf.js +330 -21
- package/src/health-check/{output-gitignore-checker.js → destination-gitignore-checker.js} +40 -25
- package/src/health-check.js +38 -38
- package/src/markdown-it-extensions.js +2 -2
- package/src/utils/README.md +35 -0
- package/src/utils/file-opener.js +89 -0
- package/assets/css/pdf-styles.css +0 -170
- package/branches.md +0 -17
- package/src/README.md +0 -98
- /package/{src/config → config}/paths.js +0 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file fetchTermsFromIndex.js
|
|
3
|
+
* @description Fetches terms and definitions from external repository's index.html
|
|
4
|
+
* @author Generated with assistance from GitHub Copilot
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
* @since 2025-04-15
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { JSDOM } = require('jsdom');
|
|
12
|
+
const axios = require('axios');
|
|
13
|
+
const { addPath, getPath, getAllPaths } = require('../../config/paths');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
// Directory to store cached files
|
|
17
|
+
const CACHE_DIR = getPath('githubcache');
|
|
18
|
+
|
|
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
|
+
|
|
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
|
+
// Use default 24h TTL instead of 0
|
|
42
|
+
const cacheTTL = options.cacheTTL || 24 * 60 * 60 * 1000;
|
|
43
|
+
|
|
44
|
+
// Early return if file doesn't exist
|
|
45
|
+
if (!fs.existsSync(cachePath)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Try to read and parse the cache file
|
|
50
|
+
let cacheData;
|
|
51
|
+
try {
|
|
52
|
+
cacheData = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.log(`Error reading cache for key: ${cacheKey}`);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check if cache has timestamp and is not expired
|
|
59
|
+
const cacheTime = new Date(cacheData.timestamp).getTime();
|
|
60
|
+
const currentTime = new Date().getTime();
|
|
61
|
+
const isExpired = currentTime - cacheTime > cacheTTL;
|
|
62
|
+
|
|
63
|
+
if (isExpired) {
|
|
64
|
+
console.log(`Cache expired for key: ${cacheKey}`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(`Using cached data for key: ${cacheKey}`);
|
|
69
|
+
return cacheData;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Saves data to cache
|
|
74
|
+
* @param {string} cacheKey - Cache key
|
|
75
|
+
* @param {object} data - Data to cache
|
|
76
|
+
*/
|
|
77
|
+
function saveToCache(cacheKey, data) {
|
|
78
|
+
const cachePath = path.join(CACHE_DIR, `${cacheKey}.json`);
|
|
79
|
+
const cacheData = {
|
|
80
|
+
timestamp: new Date().toISOString(),
|
|
81
|
+
...data
|
|
82
|
+
};
|
|
83
|
+
fs.writeFileSync(cachePath, JSON.stringify(cacheData, null, 2));
|
|
84
|
+
console.log(`Saved to cache: ${cacheKey}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Fetches the latest commit hash for a specific file in a repository
|
|
89
|
+
* @param {string} token - GitHub API Token
|
|
90
|
+
* @param {string} owner - Repository owner
|
|
91
|
+
* @param {string} repo - Repository name
|
|
92
|
+
* @param {string} filePath - Path to the file in the repository
|
|
93
|
+
* @param {object} headers - Request headers
|
|
94
|
+
* @returns {string|null} - Latest commit hash or null if error
|
|
95
|
+
*/
|
|
96
|
+
async function getFileCommitHash(token, owner, repo, filePath, headers) {
|
|
97
|
+
try {
|
|
98
|
+
// Normalize the file path to ensure it doesn't have leading slash
|
|
99
|
+
const normalizedPath = filePath.replace(/^\//, '');
|
|
100
|
+
|
|
101
|
+
// Construct API URL to get commits for the specific file
|
|
102
|
+
const commitsUrl = `https://api.github.com/repos/${owner}/${repo}/commits?path=${normalizedPath}&per_page=1`;
|
|
103
|
+
console.log(`Fetching latest commit for file: ${commitsUrl}`);
|
|
104
|
+
|
|
105
|
+
const response = await axios.get(commitsUrl, { headers });
|
|
106
|
+
|
|
107
|
+
if (response.status !== 200 || !response.data || response.data.length === 0) {
|
|
108
|
+
console.log(`❌ Could not find commit information for ${filePath}`);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Return the SHA of the latest commit
|
|
113
|
+
return response.data[0].sha;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(`❌ Error fetching commit hash: ${error.message}`);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Fetches all terms and definitions from a repository's index.html
|
|
122
|
+
* @param {string} token - GitHub API Token
|
|
123
|
+
* @param {string} owner - Repository owner
|
|
124
|
+
* @param {string} repo - Repository name
|
|
125
|
+
* @param {object} options - Additional options
|
|
126
|
+
* @returns {object|null} - Object containing all terms or null if error
|
|
127
|
+
*/
|
|
128
|
+
async function fetchAllTermsFromIndex(token, owner, repo, options = {}) {
|
|
129
|
+
try {
|
|
130
|
+
// Generate cache key based on repo information
|
|
131
|
+
const cacheKey = generateCacheKey(owner, repo);
|
|
132
|
+
let cachedData = null;
|
|
133
|
+
|
|
134
|
+
// Check cache first if caching is enabled
|
|
135
|
+
if (options.cache !== false) {
|
|
136
|
+
cachedData = getFromCache(cacheKey, options);
|
|
137
|
+
if (cachedData) {
|
|
138
|
+
return cachedData;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Configure headers for GitHub API
|
|
143
|
+
const headers = {};
|
|
144
|
+
if (token) {
|
|
145
|
+
headers['Authorization'] = `token ${token}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Get the specs.json content from the repository to find the output_path
|
|
149
|
+
const specsJsonUrl = `https://api.github.com/repos/${owner}/${repo}/contents/specs.json`;
|
|
150
|
+
console.log(`Fetching specs.json from: ${specsJsonUrl}`);
|
|
151
|
+
|
|
152
|
+
// Fetch specs.json content
|
|
153
|
+
const specsJsonResponse = await axios.get(specsJsonUrl, { headers });
|
|
154
|
+
if (specsJsonResponse.status !== 200) {
|
|
155
|
+
console.log(`❌ Could not find specs.json in repository ${owner}/${repo}`);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Decode specs.json content from base64
|
|
160
|
+
const specsJsonContent = Buffer.from(specsJsonResponse.data.content, 'base64').toString('utf8');
|
|
161
|
+
const specsJson = JSON.parse(specsJsonContent);
|
|
162
|
+
|
|
163
|
+
// Get the output_path from specs.json
|
|
164
|
+
const outputPath = specsJson.specs[0].output_path;
|
|
165
|
+
if (!outputPath) {
|
|
166
|
+
console.log(`❌ No output_path found in specs.json for repository ${owner}/${repo}`);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Fix: Properly normalize the output path to ensure it doesn't have leading "./" or trailing "/"
|
|
171
|
+
const normalizedOutputPath = outputPath.replace(/^\.\//, '').replace(/\/$/, '');
|
|
172
|
+
|
|
173
|
+
// Create the path to the index.html file
|
|
174
|
+
const indexHtmlPath = `${normalizedOutputPath}/index.html`;
|
|
175
|
+
|
|
176
|
+
// Fetch the index.html content with properly constructed URL
|
|
177
|
+
const indexHtmlUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/${indexHtmlPath}`;
|
|
178
|
+
console.log(`Fetching index.html from: ${indexHtmlUrl}`);
|
|
179
|
+
|
|
180
|
+
const indexHtmlResponse = await axios.get(indexHtmlUrl, { headers });
|
|
181
|
+
if (indexHtmlResponse.status !== 200) {
|
|
182
|
+
console.log(`❌ Could not find index.html at ${indexHtmlUrl}`);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Get the commit hash for the index.html file
|
|
187
|
+
const commitHash = await getFileCommitHash(token, owner, repo, indexHtmlPath, headers);
|
|
188
|
+
if (!commitHash) {
|
|
189
|
+
console.log(`⚠️ Could not get commit hash for index.html, continuing without it`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const htmlContent = indexHtmlResponse.data;
|
|
193
|
+
|
|
194
|
+
// Parse HTML using JSDOM
|
|
195
|
+
const dom = new JSDOM(htmlContent);
|
|
196
|
+
const document = dom.window.document;
|
|
197
|
+
|
|
198
|
+
// Find all term definition lists with class "terms-and-definitions-list"
|
|
199
|
+
const termDlList = document.querySelector('dl.terms-and-definitions-list');
|
|
200
|
+
if (!termDlList) {
|
|
201
|
+
console.log(`❌ No terms-and-definitions-list found in ${indexHtmlUrl}`);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Extract all terms and definitions
|
|
206
|
+
const terms = [];
|
|
207
|
+
let dtElements = termDlList.querySelectorAll('dt');
|
|
208
|
+
|
|
209
|
+
dtElements.forEach(dt => {
|
|
210
|
+
// Find the term span that starts with id="term:
|
|
211
|
+
const termSpan = dt.querySelector('span[id^="term:"]');
|
|
212
|
+
if (!termSpan) return;
|
|
213
|
+
|
|
214
|
+
// Get the term text (all text content, excluding nested spans)
|
|
215
|
+
let termText = '';
|
|
216
|
+
for (let node of termSpan.childNodes) {
|
|
217
|
+
if (node.nodeType === dom.window.Node.TEXT_NODE) {
|
|
218
|
+
termText += node.textContent.trim();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// If no text found, try to get the full text content
|
|
223
|
+
if (!termText) {
|
|
224
|
+
termText = termSpan.textContent.trim();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Skip empty terms
|
|
228
|
+
if (!termText) return;
|
|
229
|
+
|
|
230
|
+
// Find all corresponding definition elements
|
|
231
|
+
let dds = [];
|
|
232
|
+
let nextElement = dt.nextElementSibling;
|
|
233
|
+
|
|
234
|
+
// Collect all consecutive <dd> elements until we reach another <dt>
|
|
235
|
+
while (nextElement && nextElement.tagName.toLowerCase() === 'dd') {
|
|
236
|
+
dds.push(nextElement.outerHTML);
|
|
237
|
+
nextElement = nextElement.nextElementSibling;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
terms.push({
|
|
241
|
+
term: termText,
|
|
242
|
+
definition: dds.join('\n')
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Store all terms in a JSON file with timestamp
|
|
247
|
+
const timestamp = Date.now();
|
|
248
|
+
const outputDir = path.join(CACHE_DIR);
|
|
249
|
+
if (!fs.existsSync(outputDir)) {
|
|
250
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Create output filename with timestamp
|
|
254
|
+
const outputFileName = `${timestamp}-${owner}-${repo}-terms.json`;
|
|
255
|
+
const outputFilePath = path.join(outputDir, outputFileName);
|
|
256
|
+
|
|
257
|
+
// Create the result object
|
|
258
|
+
const result = {
|
|
259
|
+
timestamp,
|
|
260
|
+
repository: `${owner}/${repo}`,
|
|
261
|
+
terms,
|
|
262
|
+
sha: commitHash, // Use the commit hash of the index.html file
|
|
263
|
+
avatarUrl: null,
|
|
264
|
+
outputFileName
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Save all terms to file
|
|
268
|
+
fs.writeFileSync(outputFilePath, JSON.stringify(result, null, 2));
|
|
269
|
+
console.log(`✅ Saved ${terms.length} terms to ${outputFilePath}`);
|
|
270
|
+
|
|
271
|
+
// Save to cache if enabled
|
|
272
|
+
if (options.cache !== false) {
|
|
273
|
+
saveToCache(cacheKey, result);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return result;
|
|
277
|
+
|
|
278
|
+
} catch (error) {
|
|
279
|
+
if (error.response) {
|
|
280
|
+
if (error.response.status === 404) {
|
|
281
|
+
console.log(`❌ Resource not found: ${error.config.url}`);
|
|
282
|
+
} else if (error.response.status === 403 && error.response.headers['x-ratelimit-remaining'] === '0') {
|
|
283
|
+
const resetTime = new Date(parseInt(error.response.headers['x-ratelimit-reset']) * 1000);
|
|
284
|
+
console.error(`❌ GitHub API rate limit exceeded. Try again after ${resetTime.toLocaleString()}`);
|
|
285
|
+
} else {
|
|
286
|
+
console.error(`❌ Error fetching data: ${error.response.status} ${error.response.statusText}`);
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
console.error(`❌ Error fetching term: ${error.message}`);
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Fetches a specific term from repository's index.html
|
|
297
|
+
* This is a wrapper that uses fetchAllTermsFromIndex for efficiency
|
|
298
|
+
* @param {string} token - GitHub API Token
|
|
299
|
+
* @param {string} term - The specific term to look for
|
|
300
|
+
* @param {string} owner - Repository owner
|
|
301
|
+
* @param {string} repo - Repository name
|
|
302
|
+
* @param {string} termsDir - Directory containing term definitions (not used in this implementation)
|
|
303
|
+
* @param {object} options - Additional options
|
|
304
|
+
* @returns {object|null} - Found term data or null if not found
|
|
305
|
+
*/
|
|
306
|
+
async function fetchTermsFromIndex(token, term, owner, repo, termsDir, options = {}) {
|
|
307
|
+
// First get all terms from the repository (which is cached)
|
|
308
|
+
const allTermsData = await fetchAllTermsFromIndex(token, owner, repo, options);
|
|
309
|
+
|
|
310
|
+
if (!allTermsData || !allTermsData.terms) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Find the specific term
|
|
315
|
+
const foundTerm = allTermsData.terms.find(t => t.term.toLowerCase() === term.toLowerCase());
|
|
316
|
+
|
|
317
|
+
if (foundTerm) {
|
|
318
|
+
console.log(`Found term '${term}' in repository ${owner}/${repo}`);
|
|
319
|
+
return {
|
|
320
|
+
term: foundTerm.term,
|
|
321
|
+
content: foundTerm.definition,
|
|
322
|
+
sha: allTermsData.sha,
|
|
323
|
+
repository: {
|
|
324
|
+
owner: {
|
|
325
|
+
login: owner,
|
|
326
|
+
avatar_url: allTermsData.avatarUrl
|
|
327
|
+
},
|
|
328
|
+
name: repo
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
} else {
|
|
332
|
+
console.log(`❌ Term "${term}" not found in repository ${owner}/${repo}`);
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
module.exports = {
|
|
338
|
+
fetchTermsFromIndex,
|
|
339
|
+
fetchAllTermsFromIndex // Export the function to fetch all terms as well
|
|
340
|
+
};
|
|
@@ -10,7 +10,7 @@ const fs = require('fs');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const { JSDOM } = require('jsdom');
|
|
12
12
|
const axios = require('axios');
|
|
13
|
-
const { addPath, getPath, getAllPaths } = require('
|
|
13
|
+
const { addPath, getPath, getAllPaths } = require('../../config/paths');
|
|
14
14
|
const crypto = require('crypto');
|
|
15
15
|
|
|
16
16
|
// Directory to store cached files
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const { fetchTermsFromIndex, fetchAllTermsFromIndex } = require('./fetchTermsFromIndex.js');
|
|
3
3
|
const { matchTerm } = require('./matchTerm.js');
|
|
4
|
-
const { addPath, getPath, getAllPaths } = require('
|
|
4
|
+
const { addPath, getPath, getAllPaths } = require('../../config/paths');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
7
|
// Directory to store cached files
|