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.
- package/.vscode/snippets.code-snippets +79 -0
- package/AGENTS.md +37 -0
- package/GG/config.js +233 -0
- package/GG/package.json +14 -0
- package/GG/static/.nojekyll +0 -0
- package/GG/static/docusaurus-snippet.css +3 -0
- package/GG/static/img/icon-bg.png +0 -0
- package/GG/static/img/icon-old.png +0 -0
- package/GG/static/img/icon.png +0 -0
- package/GG/static/img/project-blank.png +0 -0
- package/GG/static/img/social-card.jpeg +0 -0
- package/LICENSE +674 -0
- package/README.md +57 -0
- package/bin/portosaurus.js +136 -0
- package/package.json +36 -0
- package/src/config/iconMappings.js +329 -0
- package/src/config/metaTags.js +240 -0
- package/src/config/prism.js +179 -0
- package/src/config/sidebar.js +20 -0
- package/src/configLoader.js +99 -0
- package/src/index.js +79 -0
- package/src/pages/index.js +98 -0
- package/src/pages/notes.js +88 -0
- package/src/pages/tasks.js +251 -0
- package/src/theme/components/AboutSection/index.js +67 -0
- package/src/theme/components/AboutSection/styles.module.css +492 -0
- package/src/theme/components/ContactSection/index.js +87 -0
- package/src/theme/components/ContactSection/styles.module.css +327 -0
- package/src/theme/components/ExperienceSection/index.js +25 -0
- package/src/theme/components/ExperienceSection/styles.module.css +180 -0
- package/src/theme/components/HeroSection/index.js +63 -0
- package/src/theme/components/HeroSection/styles.module.css +471 -0
- package/src/theme/components/NoteIndex/index.js +119 -0
- package/src/theme/components/NoteIndex/styles.module.css +143 -0
- package/src/theme/components/ProjectsSection/index.js +529 -0
- package/src/theme/components/ProjectsSection/styles.module.css +830 -0
- package/src/theme/components/ScrollToTop/index.js +98 -0
- package/src/theme/components/ScrollToTop/styles.module.css +96 -0
- package/src/theme/components/SocialLinks/index.js +129 -0
- package/src/theme/components/SocialLinks/styles.module.css +55 -0
- package/src/theme/components/Tooltip/index.js +30 -0
- package/src/theme/components/Tooltip/styles.module.css +92 -0
- package/src/theme/css/bootstrap.css +6 -0
- package/src/theme/css/catppuccin.css +632 -0
- package/src/theme/css/custom.css +186 -0
- package/src/theme/css/tasks.css +868 -0
- package/src/theme/staticLink/.nojekyll +0 -0
- package/src/theme/staticLink/docusaurus-snippet.css +3 -0
- package/src/theme/staticLink/img/icon-bg.png +0 -0
- package/src/theme/staticLink/img/icon-old.png +0 -0
- package/src/theme/staticLink/img/icon.png +0 -0
- package/src/theme/staticLink/img/project-blank.png +0 -0
- package/src/theme/staticLink/img/social-card.jpeg +0 -0
- package/src/utils/HashNavigation.js +250 -0
- package/src/utils/appVersion.js +27 -0
- package/src/utils/cssUtils.js +99 -0
- package/src/utils/filterEnabledItems.js +21 -0
- package/src/utils/generateFavicon.js +256 -0
- package/src/utils/generateRobotsTxt.js +97 -0
- package/src/utils/iconExtractor.js +159 -0
- package/src/utils/imageDownloader.js +88 -0
- package/src/utils/imageProcessor.js +134 -0
- package/src/utils/linkShortner.js +0 -0
- package/src/utils/updateTitle.js +107 -0
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|