portosaurus 0.14.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.

Potentially problematic release.


This version of portosaurus might be problematic. Click here for more details.

Files changed (64) hide show
  1. package/.vscode/snippets.code-snippets +79 -0
  2. package/AGENTS.md +37 -0
  3. package/GG/config.js +233 -0
  4. package/GG/package.json +14 -0
  5. package/GG/static/.nojekyll +0 -0
  6. package/GG/static/docusaurus-snippet.css +3 -0
  7. package/GG/static/img/icon-bg.png +0 -0
  8. package/GG/static/img/icon-old.png +0 -0
  9. package/GG/static/img/icon.png +0 -0
  10. package/GG/static/img/project-blank.png +0 -0
  11. package/GG/static/img/social-card.jpeg +0 -0
  12. package/LICENSE +674 -0
  13. package/README.md +57 -0
  14. package/bin/portosaurus.js +136 -0
  15. package/package.json +36 -0
  16. package/src/config/iconMappings.js +329 -0
  17. package/src/config/metaTags.js +240 -0
  18. package/src/config/prism.js +179 -0
  19. package/src/config/sidebar.js +20 -0
  20. package/src/configLoader.js +99 -0
  21. package/src/index.js +79 -0
  22. package/src/pages/index.js +98 -0
  23. package/src/pages/notes.js +88 -0
  24. package/src/pages/tasks.js +251 -0
  25. package/src/theme/components/AboutSection/index.js +67 -0
  26. package/src/theme/components/AboutSection/styles.module.css +492 -0
  27. package/src/theme/components/ContactSection/index.js +87 -0
  28. package/src/theme/components/ContactSection/styles.module.css +327 -0
  29. package/src/theme/components/ExperienceSection/index.js +25 -0
  30. package/src/theme/components/ExperienceSection/styles.module.css +180 -0
  31. package/src/theme/components/HeroSection/index.js +63 -0
  32. package/src/theme/components/HeroSection/styles.module.css +471 -0
  33. package/src/theme/components/NoteIndex/index.js +119 -0
  34. package/src/theme/components/NoteIndex/styles.module.css +143 -0
  35. package/src/theme/components/ProjectsSection/index.js +529 -0
  36. package/src/theme/components/ProjectsSection/styles.module.css +830 -0
  37. package/src/theme/components/ScrollToTop/index.js +98 -0
  38. package/src/theme/components/ScrollToTop/styles.module.css +96 -0
  39. package/src/theme/components/SocialLinks/index.js +129 -0
  40. package/src/theme/components/SocialLinks/styles.module.css +55 -0
  41. package/src/theme/components/Tooltip/index.js +30 -0
  42. package/src/theme/components/Tooltip/styles.module.css +92 -0
  43. package/src/theme/css/bootstrap.css +6 -0
  44. package/src/theme/css/catppuccin.css +632 -0
  45. package/src/theme/css/custom.css +186 -0
  46. package/src/theme/css/tasks.css +868 -0
  47. package/src/theme/staticLink/.nojekyll +0 -0
  48. package/src/theme/staticLink/docusaurus-snippet.css +3 -0
  49. package/src/theme/staticLink/img/icon-bg.png +0 -0
  50. package/src/theme/staticLink/img/icon-old.png +0 -0
  51. package/src/theme/staticLink/img/icon.png +0 -0
  52. package/src/theme/staticLink/img/project-blank.png +0 -0
  53. package/src/theme/staticLink/img/social-card.jpeg +0 -0
  54. package/src/utils/HashNavigation.js +250 -0
  55. package/src/utils/appVersion.js +27 -0
  56. package/src/utils/cssUtils.js +99 -0
  57. package/src/utils/filterEnabledItems.js +21 -0
  58. package/src/utils/generateFavicon.js +256 -0
  59. package/src/utils/generateRobotsTxt.js +97 -0
  60. package/src/utils/iconExtractor.js +159 -0
  61. package/src/utils/imageDownloader.js +88 -0
  62. package/src/utils/imageProcessor.js +134 -0
  63. package/src/utils/linkShortner.js +0 -0
  64. package/src/utils/updateTitle.js +107 -0
File without changes
@@ -0,0 +1,3 @@
1
+ .hide-lines {
2
+ display: none;
3
+ }
@@ -0,0 +1,250 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ // AI Generated (partially)
4
+
5
+ /**
6
+ * <HashNavigation/> Component to handle hash-based navigation with visual effects
7
+ * Should be added at the bottom of the page
8
+ *
9
+ * @param {Object} props Component props
10
+ * @param {string} props.elementPrefix Prefix for element IDs (default: 'card-')
11
+ * @param {string} props.elementSelector Selector for all elements in the group (default: '.content-card')
12
+ * @param {string} props.containerSelector Selector for the container element (default: '.container')
13
+ * @param {number} props.effectDuration Duration of the visual effect in ms (default: 6000)
14
+ * @param {number} props.scrollDelay Delay before scrolling in ms (default: 300)
15
+ * @param {Object} props.scrollOptions Options for scrollIntoView (default: { behavior: 'smooth', block: 'center' })
16
+ * @param {boolean} props.enabled Whether the component is enabled (default: true)
17
+ * @param {Object} props.styles Custom styling options
18
+ * @param {string} props.styles.overlayColor Background color for the overlay (default: 'rgba(var(--ifm-color-emphasis-200-rgb), 0.5)')
19
+ * @param {string} props.styles.highlightShadow Shadow for highlighted element (default: '0 0 30px 10px var(--ifm-color-primary)')
20
+ * @param {string} props.styles.highlightScale Scale for highlighted element (default: '1.05')
21
+ * @param {string} props.styles.blurAmount Blur amount for non-highlighted elements (default: '4px')
22
+ * @param {string} props.styles.blurOpacity Opacity for blurred elements (default: '0.3')
23
+ */
24
+ export default function HashNavigation({
25
+ elementPrefix = 'card-',
26
+ elementSelector = '.content-card',
27
+ containerSelector = '.container',
28
+ effectDuration = 6000,
29
+ scrollDelay = 300,
30
+ scrollOptions = { behavior: 'smooth', block: 'center' },
31
+ enabled = true,
32
+ styles = {}
33
+ }) {
34
+ const styleId = 'hash-navigation-styles';
35
+ const highlightClass = 'hash-nav-highlight';
36
+ const blurClass = 'hash-nav-blur';
37
+ const containerActiveClass = 'hash-nav-active';
38
+
39
+ // Default styles
40
+ const {
41
+ overlayColor = 'rgba(var(--ifm-color-emphasis-200-rgb), 0.5)',
42
+ highlightShadow = '0 0 30px 10px var(--ifm-color-primary)',
43
+ highlightScale = '1.05',
44
+ blurAmount = '4px',
45
+ blurOpacity = '0.3'
46
+ } = styles;
47
+
48
+ // Reference to track if styles have been injected
49
+ const stylesInjected = useRef(false);
50
+
51
+ // Inject the component styles
52
+ useEffect(() => {
53
+
54
+ // Don't inject styles if already present
55
+ if (document.getElementById(styleId) || stylesInjected.current) {
56
+ return;
57
+ }
58
+
59
+ const styleElement = document.createElement('style');
60
+
61
+ styleElement.id = styleId;
62
+
63
+ styleElement.innerHTML = `
64
+
65
+ ${containerSelector}.${containerActiveClass} {
66
+ position: relative;
67
+ }
68
+
69
+ ${containerSelector}.${containerActiveClass}::after {
70
+ content: '';
71
+ position: fixed;
72
+ top: 0;
73
+ left: 0;
74
+ width: 100%;
75
+ height: 100%;
76
+ background: ${overlayColor};
77
+ z-index: 10;
78
+ pointer-events: none;
79
+ }
80
+
81
+ .${highlightClass} {
82
+ position: relative;
83
+ z-index: 20;
84
+ transform: scale(${highlightScale});
85
+ box-shadow: ${highlightShadow};
86
+ transition: all 0.3s ease;
87
+ }
88
+
89
+ .${blurClass} {
90
+ filter: blur(${blurAmount}) grayscale(70%) brightness(0.8);
91
+ opacity: ${blurOpacity};
92
+ transition: all 0.3s ease;
93
+ background-color: rgba(var(--ifm-color-emphasis-200-rgb), 0.15);
94
+ pointer-events: none;
95
+ }
96
+
97
+ /* Clickable overlay for dismissing effects */
98
+ .hash-nav-overlay {
99
+ position: fixed;
100
+ top: 0;
101
+ left: 0;
102
+ width: 100%;
103
+ height: 100%;
104
+ z-index: 15;
105
+ cursor: pointer;
106
+ }
107
+
108
+ /* Reduced motion support */
109
+ @media (prefers-reduced-motion: reduce) {
110
+ .${highlightClass},
111
+ .${blurClass} {
112
+ transition: none !important;
113
+ }
114
+
115
+ .${highlightClass} {
116
+ transform: none !important;
117
+ }
118
+
119
+ .${blurClass} {
120
+ filter: opacity(${blurOpacity});
121
+ background-color: rgba(var(--ifm-color-emphasis-200-rgb), 0.15);
122
+ }
123
+ }
124
+ `;
125
+
126
+ document.head.appendChild(styleElement);
127
+ stylesInjected.current = true;
128
+
129
+ // Clean up on unmount
130
+ return () => {
131
+ const existingStyle = document.getElementById(styleId);
132
+ if (existingStyle) {
133
+ document.head.removeChild(existingStyle);
134
+ }
135
+ stylesInjected.current = false;
136
+ };
137
+ }, [containerSelector, overlayColor, highlightShadow, highlightScale, blurAmount, blurOpacity]);
138
+
139
+
140
+ // Main hash navigation logic
141
+ useEffect(() => {
142
+ if (!enabled) return;
143
+
144
+ if (window.location.hash) {
145
+ const hashValue = window.location.hash.substring(1);
146
+ const targetElement = document.getElementById(`${elementPrefix}${hashValue}`);
147
+
148
+ if (targetElement) {
149
+
150
+ // Wait a moment for the page to fully render
151
+ setTimeout(() => {
152
+
153
+ // Scroll to the element
154
+ targetElement.scrollIntoView(scrollOptions);
155
+
156
+ // Get the container to add the overlay effect
157
+ const container = document.querySelector(containerSelector);
158
+ if (container) {
159
+ container.classList.add(containerActiveClass);
160
+ }
161
+
162
+ // Add visual effects
163
+ const allElements = document.querySelectorAll(elementSelector);
164
+
165
+ // Add highlight class to the target element
166
+ targetElement.classList.add(highlightClass);
167
+
168
+ // Add blur to all other elements
169
+ allElements.forEach(element => {
170
+ if (element !== targetElement) {
171
+ element.classList.add(blurClass);
172
+ }
173
+ });
174
+
175
+ // Create clickable overlay for dismissing effects
176
+ const overlay = document.createElement('div');
177
+ overlay.className = 'hash-nav-overlay';
178
+
179
+ document.body.appendChild(overlay);
180
+
181
+ let effectTimeoutId = null;
182
+
183
+ // remove effects
184
+ const removeEffects = () => {
185
+
186
+ if (effectTimeoutId) {
187
+ clearTimeout(effectTimeoutId);
188
+ effectTimeoutId = null;
189
+ }
190
+
191
+ // Remove effects
192
+ allElements.forEach(element => {
193
+ element.classList.remove(blurClass);
194
+ });
195
+
196
+ targetElement.classList.remove(highlightClass);
197
+
198
+ // Remove the container overlay
199
+ if (container) {
200
+ container.classList.remove(containerActiveClass);
201
+ }
202
+
203
+ // Remove clickable overlay
204
+ if (overlay && overlay.parentNode) {
205
+ overlay.parentNode.removeChild(overlay);
206
+ }
207
+
208
+ // Remove event listeners after effects are cleared
209
+ overlay.removeEventListener('click', removeEffects);
210
+ overlay.removeEventListener('touchstart', removeEffects);
211
+ document.removeEventListener('keydown', handleKeyDown);
212
+ };
213
+
214
+ // Add keyboard escape handler
215
+ const handleKeyDown = (e) => {
216
+ if (e.key === 'Escape') {
217
+ removeEffects();
218
+ }
219
+ };
220
+
221
+ // Add event listeners to dismiss effects
222
+ overlay.addEventListener('click', removeEffects);
223
+ overlay.addEventListener('touchstart', removeEffects);
224
+ document.addEventListener('keydown', handleKeyDown);
225
+
226
+ // Set timeout to automatically remove effects after duration
227
+ effectTimeoutId = setTimeout(removeEffects, effectDuration);
228
+
229
+ }, scrollDelay);
230
+ }
231
+ }
232
+
233
+ // Cleanup
234
+ return () => {
235
+ const container = document.querySelector(containerSelector);
236
+ const overlay = document.querySelector('.hash-nav-overlay');
237
+
238
+ if (container) {
239
+ container.classList.remove(containerActiveClass);
240
+ }
241
+
242
+ // Remove any overlay element that might exist
243
+ if (overlay && overlay.parentNode) {
244
+ overlay.parentNode.removeChild(overlay);
245
+ }
246
+ };
247
+ }, [enabled, elementPrefix, elementSelector, containerSelector, effectDuration, scrollDelay, scrollOptions]);
248
+
249
+ return null;
250
+ }
@@ -0,0 +1,27 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ let cachedVersion = null;
5
+
6
+ function appVersion() {
7
+
8
+ if (cachedVersion) {
9
+ return cachedVersion;
10
+ }
11
+
12
+ try {
13
+ const pkgPath = path.resolve(__dirname, '../../package.json');
14
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
15
+
16
+ cachedVersion = pkg.version || '0.0.0';
17
+ } catch (err) {
18
+
19
+ cachedVersion = '0.0.0';
20
+ console.warn('Could not read package.json version:', err.message);
21
+ }
22
+
23
+ console.info('\n[INFO] App version:', cachedVersion);
24
+ return cachedVersion;
25
+ }
26
+
27
+ module.exports = { appVersion };
@@ -0,0 +1,99 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ // AI generated
5
+
6
+ // Cache for CSS content and parsed variables
7
+ const cssCache = new Map();
8
+ const varCache = new Map();
9
+
10
+ // Debug flag - set to true to enable debug logs
11
+ const DEBUG = false;
12
+
13
+ function getCssVar(varName) {
14
+ // Return cached value if exists
15
+ if (varCache.has(varName)) {
16
+ DEBUG && console.log(`[CSS-DEBUG] Using cached value for ${varName}: ${varCache.get(varName)}`);
17
+ return varCache.get(varName);
18
+ }
19
+
20
+ DEBUG && console.log(`[CSS-DEBUG] Looking for CSS variable: ${varName}`);
21
+
22
+ // Try to find variable in CSS files
23
+ const cssFiles = [
24
+ path.resolve(__dirname, '../css/custom.css'),
25
+ path.resolve(__dirname, '../css/catppuccin.css')
26
+ ];
27
+
28
+ for (const cssPath of cssFiles) {
29
+ try {
30
+ DEBUG && console.log(`[CSS-DEBUG] Checking file: ${cssPath}`);
31
+
32
+ if (!fs.existsSync(cssPath)) {
33
+ DEBUG && console.log(`[CSS-DEBUG] File not found: ${cssPath}`);
34
+ continue;
35
+ }
36
+
37
+ let cssContent = cssCache.get(cssPath);
38
+ if (!cssContent) {
39
+ cssContent = fs.readFileSync(cssPath, 'utf8');
40
+ cssCache.set(cssPath, cssContent);
41
+ DEBUG && console.log(`[CSS-DEBUG] Loaded CSS file: ${cssPath}`);
42
+ }
43
+
44
+ // Find all occurrences of the variable
45
+ const regex = new RegExp(`${varName}:\\s*([^;]+);`, 'g');
46
+ let lastValue = null;
47
+ let match;
48
+ let matchCount = 0;
49
+
50
+ // Find all matches and keep the last one
51
+ while ((match = regex.exec(cssContent)) !== null) {
52
+ matchCount++;
53
+ lastValue = match[1].replace(/!important/g, '').trim();
54
+ DEBUG && console.log(`[CSS-DEBUG] Found match #${matchCount} for ${varName}: ${lastValue}`);
55
+ }
56
+
57
+ if (matchCount > 0) {
58
+ DEBUG && console.log(`[CSS-DEBUG] Using last of ${matchCount} matches for ${varName}: ${lastValue}`);
59
+ }
60
+
61
+ // Process nested variables
62
+ if (lastValue && lastValue.startsWith('var(')) {
63
+ const nestedMatch = lastValue.match(/var\((--[^)]+)\)/);
64
+ if (nestedMatch) {
65
+ const nestedVar = nestedMatch[1];
66
+ DEBUG && console.log(`[CSS-DEBUG] Found nested variable in ${varName}: ${nestedVar}`);
67
+
68
+ try {
69
+ const resolvedValue = getCssVar(nestedVar);
70
+ DEBUG && console.log(`[CSS-DEBUG] Resolved nested ${nestedVar} to: ${resolvedValue}`);
71
+ varCache.set(varName, resolvedValue);
72
+ return resolvedValue;
73
+ } catch (err) {
74
+ DEBUG && console.log(`[CSS-DEBUG] Could not resolve nested variable: ${err.message}`);
75
+ throw new Error(`Failed to resolve nested variable ${nestedVar} in ${varName}: ${err.message}`);
76
+ }
77
+ }
78
+ }
79
+
80
+ if (lastValue) {
81
+ DEBUG && console.log(`[CSS-DEBUG] Caching and returning value for ${varName}: ${lastValue}`);
82
+ varCache.set(varName, lastValue);
83
+ return lastValue;
84
+ }
85
+ } catch (err) {
86
+ DEBUG && console.log(`[CSS-DEBUG] Error processing ${cssPath}: ${err.message}`);
87
+ if (err.message.includes('Failed to resolve nested variable')) {
88
+ throw err;
89
+ }
90
+ }
91
+ }
92
+
93
+ // Variable not found - throw error
94
+ const errorMsg = `CSS variable ${varName} not found in any CSS files`;
95
+ DEBUG && console.error(`[CSS-DEBUG] ${errorMsg}`);
96
+ throw new Error(errorMsg);
97
+ }
98
+
99
+ module.exports = { getCssVar };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Filters an array of items that can be either:
3
+ * 1. Regular items (included as-is)
4
+ * 2. Conditional items with enable/value properties (included only if enabled)
5
+ *
6
+ **/
7
+ export const useEnabled = (items) => {
8
+ if (!Array.isArray(items)) {
9
+ console.warn('useEnabled: Expected an array, received:', typeof items);
10
+ return [];
11
+ }
12
+
13
+ return items.flatMap(item => {
14
+ // If item has enable property, it's a conditional item
15
+ if (item && typeof item === 'object' && 'enable' in item && 'value' in item) {
16
+ return item.enable === true ? [item.value] : [];
17
+ }
18
+ // Otherwise it's a regular item that's always included
19
+ return [item];
20
+ });
21
+ };
@@ -0,0 +1,256 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const { favicons } = require('favicons');
5
+ const { downloadImage } = require('./imageDownloader');
6
+ const { reshapeImage } = require('./imageProcessor');
7
+ const { getCssVar } = require('./cssUtils');
8
+ const { extractSvg } = require('./iconExtractor');
9
+
10
+ function createDirectoryIfNotExists(dir) {
11
+ if (!fs.existsSync(dir)) {
12
+ fs.mkdirSync(dir, { recursive: true });
13
+ return true;
14
+ }
15
+ return false;
16
+ }
17
+
18
+ function cleanupFile(filePath) {
19
+ if (fs.existsSync(filePath)) {
20
+ try {
21
+ fs.unlinkSync(filePath);
22
+ return true;
23
+ } catch (e) {
24
+ console.warn(`[WARNING] Failed to clean up file ${filePath}:`, e.message);
25
+ }
26
+ }
27
+ return false;
28
+ }
29
+
30
+ function processManifest(manifestFile, outputDir, appVersion) {
31
+ try {
32
+ const manifest = JSON.parse(manifestFile.contents);
33
+ manifest.version = appVersion;
34
+
35
+ fs.writeFileSync(
36
+ path.join(outputDir, manifestFile.name),
37
+ JSON.stringify(manifest, null, 2)
38
+ );
39
+ return true;
40
+ } catch (err) {
41
+ console.error('[ERROR] Failed to process manifest:', err.message);
42
+ fs.writeFileSync(path.join(outputDir, manifestFile.name), manifestFile.contents);
43
+ return false;
44
+ }
45
+ }
46
+
47
+ async function generateFavicons(context, options = {}) {
48
+ console.log('\n[INFO] Generating favicons...');
49
+
50
+ const {siteConfig} = context;
51
+ const profilePicUrl = options.imagePath || siteConfig.customFields.heroSection.profilePic;
52
+ const appVersion = siteConfig.customFields.version || '1.0';
53
+ const circular = options.circular !== false;
54
+ const shape = options.shape || 'circle';
55
+
56
+ const staticBaseDir = path.resolve(context.siteDir, 'static');
57
+ const imgDir = path.join(staticBaseDir, 'img', 'svg');
58
+ const imgStaticPath = '/img/svg';
59
+ const outputDir = path.join(staticBaseDir, options.outputPath || 'favicon');
60
+
61
+ const tempDir = path.resolve(context.siteDir);
62
+ const reshapedImagePath = path.join(tempDir, 'temp_reshaped_pic.png');
63
+
64
+ const tempFiles = [];
65
+
66
+ try {
67
+ const iconColor = { color: getCssVar('--ifm-color-primary') };
68
+ const iconsToGenerate = ['note', 'blog'];
69
+
70
+ for (const icon of iconsToGenerate) {
71
+ await extractSvg(icon, imgDir, iconColor);
72
+ }
73
+
74
+ const configuration = {
75
+ path: `/${options.outputPath || 'favicon'}/`,
76
+ appName: siteConfig.title || 'Portfolio',
77
+ appDescription: siteConfig.tagline || 'Portfolio',
78
+ background: getCssVar('--ifm-background-color'),
79
+ theme_color: getCssVar('--ifm-color-primary'),
80
+ appleStatusBarStyle: 'black-translucent',
81
+ display: 'standalone',
82
+ scope: '/',
83
+ start_url: '/',
84
+ version: appVersion,
85
+ orientation: 'natural',
86
+ logging: false,
87
+ loadManifestWithCredentials: true,
88
+ manifestMaskable: true,
89
+ icons: {
90
+ android: {
91
+ offset: 0,
92
+ background: false,
93
+ mask: true,
94
+ overlayGlow: false,
95
+ androidPlayStore: true,
96
+ },
97
+ favicons: true,
98
+ appleIcon: true,
99
+ appleStartup: false,
100
+ windows: false,
101
+ yandex: false,
102
+ },
103
+ shortcuts: [
104
+ {
105
+ name: "Notes",
106
+ short_name: "Notes",
107
+ description: "View my collection of notes",
108
+ url: "/notes",
109
+ icons: [
110
+ {
111
+ src: `${imgStaticPath}/icon-note.svg`,
112
+ sizes: "any",
113
+ type: "image/svg+xml",
114
+ purpose: "any"
115
+ },
116
+ {
117
+ src: "/img/project-blank.png",
118
+ sizes: "192x192",
119
+ type: "image/png",
120
+ purpose: "any"
121
+ }
122
+ ]
123
+ },
124
+ {
125
+ name: "Blog",
126
+ short_name: "Blog",
127
+ description: "Read my latest blog posts",
128
+ url: "/blog",
129
+ icons: [
130
+ {
131
+ src: `${imgStaticPath}/icon-blog.svg`,
132
+ sizes: "any",
133
+ type: "image/svg+xml",
134
+ purpose: "any"
135
+ },
136
+ {
137
+ src: "/favicon/android-chrome-192x192.png",
138
+ sizes: "192x192",
139
+ type: "image/png",
140
+ purpose: "any"
141
+ }
142
+ ]
143
+ }
144
+ ]
145
+ };
146
+
147
+ const downloadedImage = await downloadImage(profilePicUrl, context.siteDir, 'temp_profile_pic.png');
148
+
149
+ tempFiles.push(downloadedImage);
150
+
151
+ let finalImagePath = downloadedImage;
152
+ if (circular) {
153
+ finalImagePath = await reshapeImage(downloadedImage, reshapedImagePath, shape);
154
+ tempFiles.push(finalImagePath);
155
+ cleanupFile(downloadedImage);
156
+ }
157
+
158
+ createDirectoryIfNotExists(outputDir);
159
+
160
+ console.log(`[INFO] Generating favicon assets from ${finalImagePath}`);
161
+ const response = await favicons(finalImagePath, configuration);
162
+
163
+ let imageCount = 0, fileCount = 0;
164
+
165
+ if (Array.isArray(response.images) && response.images.length > 0) {
166
+ for (const image of response.images) {
167
+ if (!image || !image.name || !image.contents) continue;
168
+
169
+ try {
170
+ fs.writeFileSync(path.join(outputDir, image.name), image.contents);
171
+ imageCount++;
172
+ } catch (err) {
173
+ console.error(`[ERROR] Failed to write image ${image.name}:`, err.message);
174
+ }
175
+ }
176
+ }
177
+
178
+ if (Array.isArray(response.files) && response.files.length > 0) {
179
+ for (const file of response.files) {
180
+ if (!file || !file.name || !file.contents) continue;
181
+
182
+ try {
183
+ if (file.name.includes('manifest')) {
184
+ processManifest(file, outputDir, appVersion);
185
+ } else {
186
+ fs.writeFileSync(path.join(outputDir, file.name), file.contents);
187
+ }
188
+ fileCount++;
189
+ } catch (err) {
190
+ console.error(`[ERROR] Failed to write file ${file.name}:`, err.message);
191
+ }
192
+ }
193
+ }
194
+
195
+ tempFiles.forEach(cleanupFile);
196
+
197
+ console.log(`[SUCCESS] Generated ${imageCount} favicon images and ${fileCount} support files\n`);
198
+ return true;
199
+
200
+ } catch (error) {
201
+ console.error('[ERROR] Error generating favicons:', error.message);
202
+ tempFiles.forEach(cleanupFile);
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ module.exports = {
208
+ generateFavicons,
209
+ default: function(context, options = {}) {
210
+ const {
211
+ imagePath = null,
212
+ outputPath = 'favicon',
213
+ circular = true,
214
+ shape = 'circle',
215
+ generateOnDev = true,
216
+ } = options;
217
+
218
+ return {
219
+ name: 'favicon-generator',
220
+
221
+ async loadContent() {
222
+ const shouldGenerate =
223
+ process.env.NODE_ENV === 'production' ||
224
+ process.env.GENERATE_FAVICONS ||
225
+ generateOnDev;
226
+
227
+ if (shouldGenerate) {
228
+ try {
229
+ await generateFavicons(context, {
230
+ imagePath,
231
+ outputPath,
232
+ circular,
233
+ shape,
234
+ });
235
+ } catch (error) {
236
+ console.error('[FATAL] Favicon generation failed:', error);
237
+ process.exit(1);
238
+ }
239
+ }
240
+ }
241
+ };
242
+ }
243
+ };
244
+
245
+ if (require.main === module) {
246
+ const siteDir = path.resolve(__dirname, '../..');
247
+ const siteConfig = require('../../docusaurus.config.js').default;
248
+
249
+ generateFavicons({
250
+ siteDir,
251
+ siteConfig
252
+ }).catch(error => {
253
+ console.error('[FATAL] Error in CLI mode:', error);
254
+ process.exit(1);
255
+ });
256
+ }