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
|
@@ -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 };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const sharp = require('sharp');
|
|
2
|
+
|
|
3
|
+
// AI GENERATED
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reshapes an image to various predefined shapes
|
|
7
|
+
*
|
|
8
|
+
* @param {string} imagePath - Path to the input image
|
|
9
|
+
* @param {string} outputPath - Path to save the processed image
|
|
10
|
+
* @param {string} [shape='circle'] - Shape to apply ('circle', 'square', 'rounded', 'hexagon', 'shield')
|
|
11
|
+
* @param {number} [roundedCornerRadius=50] - Corner radius for rounded shape (percentage)
|
|
12
|
+
* @returns {Promise<string>} - A promise that resolves with the path to the processed image
|
|
13
|
+
*/
|
|
14
|
+
async function reshapeImage(imagePath, outputPath, shape = 'circle', roundedCornerRadius = 50) {
|
|
15
|
+
console.log(`[INFO] Processing image to ${shape} shape...`);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Get image dimensions
|
|
19
|
+
const metadata = await sharp(imagePath).metadata();
|
|
20
|
+
const size = Math.min(metadata.width, metadata.height);
|
|
21
|
+
|
|
22
|
+
let shapeMask;
|
|
23
|
+
|
|
24
|
+
switch (shape.toLowerCase()) {
|
|
25
|
+
case 'circle':
|
|
26
|
+
shapeMask = Buffer.from(
|
|
27
|
+
`<svg><circle cx="${size/2}" cy="${size/2}" r="${size/2}" fill="white"/></svg>`
|
|
28
|
+
);
|
|
29
|
+
break;
|
|
30
|
+
|
|
31
|
+
case 'square':
|
|
32
|
+
// No mask needed for square, just resize to equal width/height
|
|
33
|
+
await sharp(imagePath)
|
|
34
|
+
.resize(size, size, { fit: 'cover' })
|
|
35
|
+
.toFile(outputPath);
|
|
36
|
+
return outputPath;
|
|
37
|
+
|
|
38
|
+
case 'rounded':
|
|
39
|
+
// Convert percentage to actual pixels (e.g., 50% becomes size/4)
|
|
40
|
+
const radius = Math.min(100, Math.max(0, roundedCornerRadius)) * size / 200;
|
|
41
|
+
shapeMask = Buffer.from(
|
|
42
|
+
`<svg><rect x="0" y="0" width="${size}" height="${size}" rx="${radius}" ry="${radius}" fill="white"/></svg>`
|
|
43
|
+
);
|
|
44
|
+
break;
|
|
45
|
+
|
|
46
|
+
case 'hexagon':
|
|
47
|
+
// Calculate hexagon points
|
|
48
|
+
const centerX = size / 2;
|
|
49
|
+
const centerY = size / 2;
|
|
50
|
+
const hexRadius = size / 2;
|
|
51
|
+
let points = '';
|
|
52
|
+
for (let i = 0; i < 6; i++) {
|
|
53
|
+
const angleDeg = 60 * i - 30;
|
|
54
|
+
const angleRad = Math.PI / 180 * angleDeg;
|
|
55
|
+
const x = centerX + hexRadius * Math.cos(angleRad);
|
|
56
|
+
const y = centerY + hexRadius * Math.sin(angleRad);
|
|
57
|
+
points += `${x},${y} `;
|
|
58
|
+
}
|
|
59
|
+
shapeMask = Buffer.from(
|
|
60
|
+
`<svg><polygon points="${points}" fill="white"/></svg>`
|
|
61
|
+
);
|
|
62
|
+
break;
|
|
63
|
+
|
|
64
|
+
case 'shield':
|
|
65
|
+
const shieldWidth = size;
|
|
66
|
+
const shieldHeight = size;
|
|
67
|
+
const bottomCurve = size / 3;
|
|
68
|
+
shapeMask = Buffer.from(
|
|
69
|
+
`<svg>
|
|
70
|
+
<path d="M ${shieldWidth/2},0
|
|
71
|
+
L ${shieldWidth},${shieldHeight/3}
|
|
72
|
+
C ${shieldWidth},${shieldHeight-bottomCurve} ${shieldWidth/2},${shieldHeight} ${shieldWidth/2},${shieldHeight}
|
|
73
|
+
C ${shieldWidth/2},${shieldHeight} 0,${shieldHeight-bottomCurve} 0,${shieldHeight/3}
|
|
74
|
+
L ${shieldWidth/2},0 Z"
|
|
75
|
+
fill="white"/>
|
|
76
|
+
</svg>`
|
|
77
|
+
);
|
|
78
|
+
break;
|
|
79
|
+
|
|
80
|
+
default:
|
|
81
|
+
// Default to circle if shape is not recognized
|
|
82
|
+
console.warn(`Shape "${shape}" not recognized, defaulting to circle`);
|
|
83
|
+
shapeMask = Buffer.from(
|
|
84
|
+
`<svg><circle cx="${size/2}" cy="${size/2}" r="${size/2}" fill="white"/></svg>`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Apply the mask and save the final image
|
|
89
|
+
await sharp(imagePath)
|
|
90
|
+
.resize(size, size, { fit: 'cover' })
|
|
91
|
+
.composite([
|
|
92
|
+
{
|
|
93
|
+
input: shapeMask,
|
|
94
|
+
blend: 'dest-in'
|
|
95
|
+
}
|
|
96
|
+
])
|
|
97
|
+
.png()
|
|
98
|
+
.toFile(outputPath);
|
|
99
|
+
|
|
100
|
+
return outputPath;
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(`Error reshaping image to ${shape}:`, error);
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resizes an image while maintaining aspect ratio
|
|
109
|
+
*
|
|
110
|
+
* @param {string} imagePath - Path to the input image
|
|
111
|
+
* @param {string} outputPath - Path to save the processed image
|
|
112
|
+
* @param {number} maxWidth - Maximum width of the output image
|
|
113
|
+
* @param {number} maxHeight - Maximum height of the output image
|
|
114
|
+
* @returns {Promise<string>} - A promise that resolves with the path to the processed image
|
|
115
|
+
*/
|
|
116
|
+
async function resizeImage(imagePath, outputPath, maxWidth, maxHeight) {
|
|
117
|
+
console.log(`Resizing image to max ${maxWidth}x${maxHeight}...`);
|
|
118
|
+
|
|
119
|
+
await sharp(imagePath)
|
|
120
|
+
.resize(maxWidth, maxHeight, {
|
|
121
|
+
fit: 'inside',
|
|
122
|
+
withoutEnlargement: true
|
|
123
|
+
})
|
|
124
|
+
.toFile(outputPath);
|
|
125
|
+
|
|
126
|
+
return outputPath;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
module.exports = {
|
|
132
|
+
reshapeImage,
|
|
133
|
+
resizeImage,
|
|
134
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useLocation } from '@docusaurus/router';
|
|
3
|
+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
4
|
+
|
|
5
|
+
// AI Generated
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* UpdateTitle component that changes the page title based on the section in view
|
|
9
|
+
* @param {Object} props - Component props
|
|
10
|
+
* @param {Object} props.sections - Map of section IDs to their corresponding titles
|
|
11
|
+
* @param {string} props.defaultTitle - Default title to use when no section is prominently visible
|
|
12
|
+
* @param {boolean} props.enabled - Whether the dynamic title functionality is enabled
|
|
13
|
+
* @returns {null} This component doesn't render anything visible
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export default function UpdateTitle({
|
|
18
|
+
sections = {},
|
|
19
|
+
defaultTitle = null,
|
|
20
|
+
enabled = true
|
|
21
|
+
}) {
|
|
22
|
+
const location = useLocation();
|
|
23
|
+
const { siteConfig } = useDocusaurusContext();
|
|
24
|
+
const [currentTitle, setCurrentTitle] = useState(null);
|
|
25
|
+
|
|
26
|
+
// Use the provided default title or fall back to site title
|
|
27
|
+
const effectiveDefaultTitle = defaultTitle || siteConfig.title;
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
|
|
31
|
+
// Only run if enabled
|
|
32
|
+
if (!enabled) return;
|
|
33
|
+
|
|
34
|
+
// Use provided sections or default to empty object
|
|
35
|
+
const sectionTitles = Object.keys(sections).length > 0
|
|
36
|
+
? sections
|
|
37
|
+
: {};
|
|
38
|
+
|
|
39
|
+
const updateTitle = () => {
|
|
40
|
+
// Get all sections we want to track
|
|
41
|
+
const sectionsToTrack = Object.keys(sectionTitles)
|
|
42
|
+
.map(id => document.getElementById(id))
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
|
|
45
|
+
if (sectionsToTrack.length === 0) {
|
|
46
|
+
// No sections found, use default title
|
|
47
|
+
setCurrentTitle(effectiveDefaultTitle);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Calculate which section is most visible
|
|
52
|
+
const viewportHeight = window.innerHeight;
|
|
53
|
+
let maxVisibleSection = null;
|
|
54
|
+
let maxVisibleArea = 0;
|
|
55
|
+
|
|
56
|
+
sectionsToTrack.forEach(section => {
|
|
57
|
+
const rect = section.getBoundingClientRect();
|
|
58
|
+
const visibleTop = Math.max(0, rect.top);
|
|
59
|
+
const visibleBottom = Math.min(viewportHeight, rect.bottom);
|
|
60
|
+
const visibleArea = Math.max(0, visibleBottom - visibleTop);
|
|
61
|
+
|
|
62
|
+
if (visibleArea > maxVisibleArea) {
|
|
63
|
+
maxVisibleArea = visibleArea;
|
|
64
|
+
maxVisibleSection = section.id;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Update title state based on visible section
|
|
69
|
+
if (maxVisibleSection && sectionTitles[maxVisibleSection]) {
|
|
70
|
+
setCurrentTitle(sectionTitles[maxVisibleSection]);
|
|
71
|
+
} else {
|
|
72
|
+
setCurrentTitle(effectiveDefaultTitle);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Add scroll event listener with throttling
|
|
77
|
+
let isScrolling = false;
|
|
78
|
+
const handleScroll = () => {
|
|
79
|
+
if (!isScrolling) {
|
|
80
|
+
window.requestAnimationFrame(() => {
|
|
81
|
+
updateTitle();
|
|
82
|
+
isScrolling = false;
|
|
83
|
+
});
|
|
84
|
+
isScrolling = true;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
window.addEventListener('scroll', handleScroll);
|
|
89
|
+
|
|
90
|
+
// Initial call
|
|
91
|
+
updateTitle();
|
|
92
|
+
|
|
93
|
+
// Clean up
|
|
94
|
+
return () => {
|
|
95
|
+
window.removeEventListener('scroll', handleScroll);
|
|
96
|
+
};
|
|
97
|
+
}, [location.pathname, sections, effectiveDefaultTitle, enabled]);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (currentTitle) {
|
|
101
|
+
document.title = currentTitle;
|
|
102
|
+
}
|
|
103
|
+
}, [currentTitle]);
|
|
104
|
+
|
|
105
|
+
// Component doesn't render anything visible
|
|
106
|
+
return null;
|
|
107
|
+
}
|