portosaurus 1.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.
- package/LICENSE +674 -0
- package/README.md +116 -0
- package/bin/portosaurus.js +337 -0
- package/internal/notes/index.md +10 -0
- package/internal/sidebars.js +20 -0
- package/internal/src/components/AboutSection/index.js +67 -0
- package/internal/src/components/AboutSection/styles.module.css +492 -0
- package/internal/src/components/ContactSection/index.js +94 -0
- package/internal/src/components/ContactSection/styles.module.css +327 -0
- package/internal/src/components/ExperienceSection/index.js +25 -0
- package/internal/src/components/ExperienceSection/styles.module.css +180 -0
- package/internal/src/components/HeroSection/index.js +61 -0
- package/internal/src/components/HeroSection/styles.module.css +471 -0
- package/internal/src/components/NoteIndex/index.js +127 -0
- package/internal/src/components/NoteIndex/styles.module.css +143 -0
- package/internal/src/components/ProjectsSection/index.js +529 -0
- package/internal/src/components/ProjectsSection/styles.module.css +830 -0
- package/internal/src/components/ScrollToTop/index.js +98 -0
- package/internal/src/components/ScrollToTop/styles.module.css +96 -0
- package/internal/src/components/SocialLinks/index.js +129 -0
- package/internal/src/components/SocialLinks/styles.module.css +55 -0
- package/internal/src/components/Tooltip/index.js +30 -0
- package/internal/src/components/Tooltip/styles.module.css +92 -0
- package/internal/src/config/iconMappings.js +329 -0
- package/internal/src/config/metaTags.js +240 -0
- package/internal/src/config/prism.js +179 -0
- package/internal/src/config/sidebar.js +20 -0
- package/internal/src/css/bootstrap.css +6 -0
- package/internal/src/css/catppuccin.css +632 -0
- package/internal/src/css/custom.css +186 -0
- package/internal/src/css/tasks.css +868 -0
- package/internal/src/pages/index.js +98 -0
- package/internal/src/pages/notes.js +88 -0
- package/internal/src/pages/tasks.js +310 -0
- package/internal/src/utils/HashNavigation.js +250 -0
- package/internal/src/utils/appVersion.js +27 -0
- package/internal/src/utils/compileConfig.js +82 -0
- package/internal/src/utils/cssUtils.js +99 -0
- package/internal/src/utils/filterEnabledItems.js +21 -0
- package/internal/src/utils/generateFavicon.js +256 -0
- package/internal/src/utils/generateRobotsTxt.js +97 -0
- package/internal/src/utils/iconExtractor.js +159 -0
- package/internal/src/utils/imageDownloader.js +88 -0
- package/internal/src/utils/imageProcessor.js +134 -0
- package/internal/src/utils/linkShortner.js +0 -0
- package/internal/src/utils/updateTitle.js +107 -0
- package/package.json +51 -0
- package/template/.github/workflows/deploy.yml +57 -0
- package/template/README.md +70 -0
- package/template/blog/authors.yml +5 -0
- package/template/blog/welcome.md +10 -0
- package/template/config.js +233 -0
- package/template/notes/getting-started.md +7 -0
- package/template/static/README.md +33 -0
- package/utils/createConfig.js +227 -0
- package/utils/logger.js +19 -0
- package/utils/packageManager.js +88 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
function formatRules(rules) {
|
|
6
|
+
if (!Array.isArray(rules) || !rules.length) return '';
|
|
7
|
+
|
|
8
|
+
let content = '';
|
|
9
|
+
|
|
10
|
+
rules.forEach(rule => {
|
|
11
|
+
|
|
12
|
+
// Default userAgent to '*' if not specified
|
|
13
|
+
const userAgent = rule.userAgent || '*';
|
|
14
|
+
|
|
15
|
+
content += `User-agent: ${userAgent}\n`;
|
|
16
|
+
|
|
17
|
+
// Check if root path is explicitly disallowed
|
|
18
|
+
const disallowsRoot = Array.isArray(rule.disallow) &&
|
|
19
|
+
rule.disallow.some(path => path === '/' || path === '/*');
|
|
20
|
+
|
|
21
|
+
// Process allow directives
|
|
22
|
+
if (Array.isArray(rule.allow) && rule.allow.length > 0) {
|
|
23
|
+
|
|
24
|
+
// Use explicitly defined allow rules
|
|
25
|
+
for (const allowPath of rule.allow) {
|
|
26
|
+
content += `Allow: ${allowPath}\n`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
} else if (!disallowsRoot) {
|
|
30
|
+
content += `Allow: /\n`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Process disallow directives
|
|
34
|
+
if (Array.isArray(rule.disallow)) {
|
|
35
|
+
for (const disallowPath of rule.disallow) {
|
|
36
|
+
content += `Disallow: ${disallowPath}\n`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
content += '\n';
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return content;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
module.exports = function() {
|
|
48
|
+
return {
|
|
49
|
+
name: 'generate-robots-txt',
|
|
50
|
+
|
|
51
|
+
async postBuild({ outDir, siteConfig }) {
|
|
52
|
+
const robotsConfig = siteConfig.customFields?.robotsTxt;
|
|
53
|
+
|
|
54
|
+
// Early return if robots.txt is disabled
|
|
55
|
+
if (!robotsConfig || robotsConfig.enable === false) {
|
|
56
|
+
console.log('ℹ️ robots.txt generation is disabled in config');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const robotsTxtPath = path.join(outDir, 'robots.txt');
|
|
62
|
+
let content = '';
|
|
63
|
+
|
|
64
|
+
// Process the rules structure
|
|
65
|
+
if (robotsConfig.rules) {
|
|
66
|
+
content += formatRules(robotsConfig.rules);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Always include sitemap reference
|
|
70
|
+
content += `Sitemap: ${siteConfig.url}/sitemap.xml\n`;
|
|
71
|
+
|
|
72
|
+
if (Array.isArray(robotsConfig.customLines) && robotsConfig.customLines.length) {
|
|
73
|
+
|
|
74
|
+
const filteredCustomLines = robotsConfig.customLines.filter(
|
|
75
|
+
line => !line.trim().toLowerCase().startsWith('sitemap:')
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (filteredCustomLines.length > 0) {
|
|
79
|
+
content += filteredCustomLines.join('\n') + '\n';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Only write file if we have content
|
|
84
|
+
if (content) {
|
|
85
|
+
fs.writeFileSync(robotsTxtPath, content, 'utf8');
|
|
86
|
+
console.log('✅ robots.txt generated successfully');
|
|
87
|
+
|
|
88
|
+
} else {
|
|
89
|
+
console.warn('⚠️ Warning: No robots.txt rules defined in config');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('❌ Error generating robots.txt:', error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const React = require('react');
|
|
4
|
+
const ReactDOMServer = require('react-dom/server');
|
|
5
|
+
const { iconMap } = require('../config/iconMappings');
|
|
6
|
+
|
|
7
|
+
/// AI Generated
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Dynamically requires a React Icon
|
|
11
|
+
* @param {string} iconPath - Path in format "package/IconName" (e.g., "ai/AiFillAlert")
|
|
12
|
+
* @returns {Object|null} The icon component or null if not found
|
|
13
|
+
*/
|
|
14
|
+
function requireDynamicIcon(iconPath) {
|
|
15
|
+
try {
|
|
16
|
+
const [packageName, iconName] = iconPath.split('/');
|
|
17
|
+
|
|
18
|
+
if (packageName && iconName) {
|
|
19
|
+
const iconPackage = require(`react-icons/${packageName}`);
|
|
20
|
+
return iconPackage[iconName];
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(`Failed to import icon ${iconPath}:`, error.message);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Cleans SVG string to create a properly formatted standalone SVG file
|
|
31
|
+
* @param {string} svgString - The SVG string from ReactDOMServer
|
|
32
|
+
* @param {string} color - The color to apply to the SVG
|
|
33
|
+
* @returns {string} Clean SVG content
|
|
34
|
+
*/
|
|
35
|
+
function cleanSvgString(svgString, color) {
|
|
36
|
+
|
|
37
|
+
// Remove React-specific attributes and add proper XML declaration
|
|
38
|
+
let cleanedSvg = svgString
|
|
39
|
+
.replace(/<!--.*?-->/g, '') // Remove comments
|
|
40
|
+
.replace(/stroke="currentColor"/g, `stroke="${color}"`)
|
|
41
|
+
.replace(/fill="currentColor"/g, `fill="${color}"`);
|
|
42
|
+
|
|
43
|
+
// Add XML declaration if not present
|
|
44
|
+
if (!cleanedSvg.includes('<?xml')) {
|
|
45
|
+
cleanedSvg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n${cleanedSvg}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Add viewBox if not present
|
|
49
|
+
if (!cleanedSvg.includes('viewBox')) {
|
|
50
|
+
cleanedSvg = cleanedSvg.replace('<svg', '<svg viewBox="0 0 24 24"');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return cleanedSvg;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extracts SVG content from a React Icon component and saves it to a file
|
|
58
|
+
*
|
|
59
|
+
* @param {React.ComponentType|string} icon - Icon component, name from iconMap, or path like "ai/AiFillAlert"
|
|
60
|
+
* @param {string} outputPath - Path to save the SVG file, defaults to static/img/svg if not provided
|
|
61
|
+
* @param {object} options - Additional options
|
|
62
|
+
* @param {string} options.color - Icon color (default: 'white')
|
|
63
|
+
* @param {number} options.size - Icon size (default: 24)
|
|
64
|
+
* @param {string} options.filename - Override filename (default: icon-<name>.svg)
|
|
65
|
+
* @param {string} options.iconName - Specify icon name instead of using component name
|
|
66
|
+
* @returns {string} The path to the saved SVG file
|
|
67
|
+
*/
|
|
68
|
+
function extractSvg(icon, outputPath = null, options = {}) {
|
|
69
|
+
|
|
70
|
+
// Default output path
|
|
71
|
+
if (!outputPath) {
|
|
72
|
+
outputPath = path.join(path.resolve(__dirname, '../..'), 'static', 'img', 'svg');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Ensure the base output directory exists
|
|
76
|
+
const baseOutputDir = typeof outputPath === 'string' && !path.extname(outputPath)
|
|
77
|
+
? outputPath
|
|
78
|
+
: path.dirname(outputPath);
|
|
79
|
+
|
|
80
|
+
if (!fs.existsSync(baseOutputDir)) {
|
|
81
|
+
fs.mkdirSync(baseOutputDir, { recursive: true });
|
|
82
|
+
console.log(`[INFO] Created directory: ${baseOutputDir}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Resolve icon component
|
|
86
|
+
let IconComponent = icon;
|
|
87
|
+
let iconIdentifier = null;
|
|
88
|
+
|
|
89
|
+
if (typeof icon === 'string') {
|
|
90
|
+
|
|
91
|
+
if (icon.includes('/')) {
|
|
92
|
+
|
|
93
|
+
// ("ai/AiFillAlert")
|
|
94
|
+
IconComponent = requireDynamicIcon(icon);
|
|
95
|
+
iconIdentifier = icon.split('/')[1];
|
|
96
|
+
|
|
97
|
+
} else {
|
|
98
|
+
|
|
99
|
+
// Icon from the iconMap
|
|
100
|
+
IconComponent = iconMap[icon.toLowerCase()]?.icon;
|
|
101
|
+
iconIdentifier = icon;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!IconComponent) {
|
|
105
|
+
throw new Error(`Icon "${icon}" could not be resolved`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Use identifier if iconName not specified
|
|
109
|
+
if (!options.iconName && iconIdentifier) {
|
|
110
|
+
options.iconName = iconIdentifier;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { color = 'white', size = 24, filename = null, iconName = null } = options;
|
|
115
|
+
|
|
116
|
+
// Determine final output path
|
|
117
|
+
const pathStats = fs.existsSync(outputPath) ? fs.statSync(outputPath) : null;
|
|
118
|
+
const isDirectory = pathStats ? pathStats.isDirectory() : false;
|
|
119
|
+
|
|
120
|
+
if (isDirectory || filename) {
|
|
121
|
+
const baseDir = isDirectory ? outputPath : path.dirname(outputPath);
|
|
122
|
+
|
|
123
|
+
// Create directory if needed
|
|
124
|
+
if (!fs.existsSync(baseDir)) {
|
|
125
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Generate filename if not provided
|
|
129
|
+
const componentName = iconName || IconComponent.name;
|
|
130
|
+
const finalFilename = filename || `icon-${componentName}.svg`;
|
|
131
|
+
outputPath = path.join(baseDir, finalFilename);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Create and render the icon
|
|
135
|
+
const element = React.createElement(IconComponent, {
|
|
136
|
+
color,
|
|
137
|
+
size,
|
|
138
|
+
style: { color }
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Render the React element to an HTML string and clean it
|
|
142
|
+
const svgString = ReactDOMServer.renderToStaticMarkup(element);
|
|
143
|
+
const cleanedSvg = cleanSvgString(svgString, color);
|
|
144
|
+
|
|
145
|
+
// Ensure final output path exists
|
|
146
|
+
const finalDir = path.dirname(outputPath);
|
|
147
|
+
|
|
148
|
+
if (!fs.existsSync(finalDir)) {
|
|
149
|
+
fs.mkdirSync(finalDir, { recursive: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Write to file
|
|
153
|
+
fs.writeFileSync(outputPath, cleanedSvg);
|
|
154
|
+
console.log(`[INFO] Generated SVG icon: ${outputPath}`);
|
|
155
|
+
|
|
156
|
+
return outputPath;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { extractSvg, iconMap };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const https = require('https');
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
// AI GENERATED - Partially
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Downloads or copies an image from a URL or local file path to a specified directory
|
|
10
|
+
*
|
|
11
|
+
* @param {string} source - The URL or local file path of the image
|
|
12
|
+
* @param {string} outputDir - The directory where the image should be saved
|
|
13
|
+
* @param {string} [filename='temp_image.png'] - The filename to use for the saved image
|
|
14
|
+
* @returns {Promise<string>} - A promise that resolves with the path to the saved image
|
|
15
|
+
*/
|
|
16
|
+
async function downloadImage(source, outputDir, filename = 'temp_image.png') {
|
|
17
|
+
|
|
18
|
+
const tmpImagePath = path.resolve(outputDir, filename);
|
|
19
|
+
|
|
20
|
+
// Check if the source is a local file path or a URL
|
|
21
|
+
const isUrl = source.startsWith('http://') || source.startsWith('https://');
|
|
22
|
+
|
|
23
|
+
if (isUrl) {
|
|
24
|
+
console.log(`[INFO] Downloading remote image from: ${source}`);
|
|
25
|
+
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const file = fs.createWriteStream(tmpImagePath);
|
|
28
|
+
|
|
29
|
+
https.get(source, (response) => {
|
|
30
|
+
if (response.statusCode !== 200) {
|
|
31
|
+
reject(new Error(`Failed to download image, status code: ${response.statusCode}`));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
response.pipe(file);
|
|
36
|
+
|
|
37
|
+
file.on('finish', () => {
|
|
38
|
+
file.close();
|
|
39
|
+
resolve(tmpImagePath);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
file.on('error', (err) => {
|
|
43
|
+
fs.unlink(tmpImagePath, () => {});
|
|
44
|
+
reject(err);
|
|
45
|
+
});
|
|
46
|
+
}).on('error', (err) => {
|
|
47
|
+
fs.unlink(tmpImagePath, () => {});
|
|
48
|
+
reject(err);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
} else {
|
|
52
|
+
|
|
53
|
+
// local file path
|
|
54
|
+
console.log(`[INFO] Copying local image from: ${source}`);
|
|
55
|
+
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
|
|
58
|
+
// Check if source file exists
|
|
59
|
+
if (!fs.existsSync(source)) {
|
|
60
|
+
reject(new Error(`Source file does not exist: ${source}`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create a read stream from the source file
|
|
65
|
+
const readStream = fs.createReadStream(source);
|
|
66
|
+
const writeStream = fs.createWriteStream(tmpImagePath);
|
|
67
|
+
|
|
68
|
+
// Pipe the read stream to the write stream
|
|
69
|
+
readStream.pipe(writeStream);
|
|
70
|
+
|
|
71
|
+
writeStream.on('finish', () => {
|
|
72
|
+
resolve(tmpImagePath);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
writeStream.on('error', (err) => {
|
|
76
|
+
fs.unlink(tmpImagePath, () => {});
|
|
77
|
+
reject(err);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
readStream.on('error', (err) => {
|
|
81
|
+
fs.unlink(tmpImagePath, () => {});
|
|
82
|
+
reject(err);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = { downloadImage };
|