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.
Files changed (37) hide show
  1. package/assets/compiled/body.js +9 -9
  2. package/assets/compiled/head.css +6 -5
  3. package/assets/css/collapse-definitions.css +41 -0
  4. package/assets/css/create-pdf.css +339 -0
  5. package/assets/css/horizontal-scroll-hint.css +6 -0
  6. package/assets/css/image-full-size.css +1 -5
  7. package/assets/css/index.css +5 -0
  8. package/assets/css/search.css +8 -8
  9. package/assets/css/terms-and-definitions.css +3 -3
  10. package/assets/js/add-bootstrap-classes-to-images.js +2 -5
  11. package/assets/js/collapse-definitions.js +260 -34
  12. package/assets/js/collapse-meta-info.js +37 -10
  13. package/assets/js/collapsibleMenu.js +0 -24
  14. package/assets/js/create-term-filter.js +36 -18
  15. package/assets/js/hide-show-utility-container.js +0 -1
  16. package/assets/js/horizontal-scroll-hint.js +2 -2
  17. package/assets/js/image-full-size.js +0 -2
  18. package/assets/js/insert-trefs.js +135 -49
  19. package/assets/js/search.js +34 -20
  20. package/{src → config}/asset-map.json +1 -0
  21. package/gulpfile.js +1 -1
  22. package/index.js +3 -3
  23. package/jest.config.js +20 -0
  24. package/package.json +3 -2
  25. package/src/collectExternalReferences/fetchTermsFromIndex.1.js +340 -0
  26. package/src/collectExternalReferences/fetchTermsFromIndex.js +1 -1
  27. package/src/collectExternalReferences/processXTrefsData.js +1 -1
  28. package/src/create-pdf.js +330 -21
  29. package/src/health-check/{output-gitignore-checker.js → destination-gitignore-checker.js} +40 -25
  30. package/src/health-check.js +38 -38
  31. package/src/markdown-it-extensions.js +2 -2
  32. package/src/utils/README.md +35 -0
  33. package/src/utils/file-opener.js +89 -0
  34. package/assets/css/pdf-styles.css +0 -170
  35. package/branches.md +0 -17
  36. package/src/README.md +0 -98
  37. /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('../config/paths');
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('../config/paths');
4
+ const { addPath, getPath, getAllPaths } = require('../../config/paths');
5
5
  const path = require('path');
6
6
 
7
7
  // Directory to store cached files