整理项目结构

This commit is contained in:
Zuoling Rong
2025-05-01 03:18:17 +08:00
parent 3600e83b54
commit 7f82481076
14 changed files with 251 additions and 809 deletions

407
src/generator.js Normal file
View File

@@ -0,0 +1,407 @@
const fs = require('fs');
const yaml = require('js-yaml');
const path = require('path');
// HTML转义函数防止XSS攻击
function escapeHtml(unsafe) {
if (unsafe === undefined || unsafe === null) {
return '';
}
return String(unsafe)
.replace(/&/g, "&")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// 读取配置文件
function loadConfig() {
let config = {};
// 读取默认配置
try {
const defaultConfigFile = fs.readFileSync('config.yml', 'utf8');
config = yaml.load(defaultConfigFile);
} catch (e) {
console.error('Error loading default config file:', e);
process.exit(1);
}
// 尝试读取用户配置并合并
try {
if (fs.existsSync('config.user.yml')) {
const userConfigFile = fs.readFileSync('config.user.yml', 'utf8');
const userConfig = yaml.load(userConfigFile);
// 深度合并配置,用户配置优先
config = mergeConfigs(config, userConfig);
console.log('Using user configuration from config.user.yml');
} else {
console.log('No user configuration found, using default config.yml');
}
} catch (e) {
console.error('Error loading user config file:', e);
console.log('Falling back to default configuration');
}
return config;
}
// 深度合并配置对象
function mergeConfigs(defaultConfig, userConfig) {
if (!userConfig) return defaultConfig;
const merged = { ...defaultConfig };
for (const key in userConfig) {
if (typeof userConfig[key] === 'object' && !Array.isArray(userConfig[key])) {
merged[key] = mergeConfigs(defaultConfig[key] || {}, userConfig[key]);
} else {
merged[key] = userConfig[key];
}
}
return merged;
}
// 生成导航菜单
function generateNavigation(navigation) {
return navigation.map(nav => `
<a href="#" class="nav-item${nav.active ? ' active' : ''}" data-page="${escapeHtml(nav.id)}">
<div class="icon-container">
<i class="${escapeHtml(nav.icon)}"></i>
</div>
<span class="nav-text">${escapeHtml(nav.name)}</span>
</a>`).join('\n');
}
// 生成网站卡片HTML
function generateSiteCards(sites) {
return sites.map(site => `
<a href="${escapeHtml(site.url)}" class="site-card">
<i class="${escapeHtml(site.icon)}"></i>
<h3>${escapeHtml(site.name)}</h3>
<p>${escapeHtml(site.description)}</p>
</a>`).join('\n');
}
// 生成分类HTML
function generateCategories(categories) {
return categories.map(category => `
<section class="category">
<h2><i class="${escapeHtml(category.icon)}"></i> ${escapeHtml(category.name)}</h2>
<div class="sites-grid">
${generateSiteCards(category.sites)}
</div>
</section>`).join('\n');
}
// 生成社交链接HTML
function generateSocialLinks(social) {
return social.map(link => `
<a href="${escapeHtml(link.url)}" class="nav-item" target="_blank">
<div class="icon-container">
<i class="${escapeHtml(link.icon)}"></i>
</div>
<span class="nav-text">${escapeHtml(link.name)}</span>
<i class="fas fa-external-link-alt external-icon"></i>
</a>`).join('\n');
}
// 生成欢迎区域和首页内容
function generateHomeContent(config) {
return `
<div class="welcome-section">
<h2>${escapeHtml(config.profile.title)}</h2>
<h3>${escapeHtml(config.profile.subtitle)}</h3>
<p class="subtitle">${escapeHtml(config.profile.description)}</p>
</div>
${generateCategories(config.categories)}`;
}
// 生成页面内容
function generatePageContent(pageId, data) {
return `
<div class="welcome-section">
<h2>${escapeHtml(data.title)}</h2>
<p class="subtitle">${escapeHtml(data.subtitle)}</p>
</div>
${generateCategories(data.categories)}`;
}
// 生成搜索结果页面
function generateSearchResultsPage(config) {
// 获取所有导航页面ID
const pageIds = config.navigation.map(nav => nav.id);
// 生成所有页面的搜索结果区域
const searchSections = pageIds.map(pageId => {
// 根据页面ID获取对应的图标和名称
const navItem = config.navigation.find(nav => nav.id === pageId);
const icon = navItem ? navItem.icon : 'fas fa-file';
const name = navItem ? navItem.name : pageId;
return `
<section class="category search-section" data-section="${escapeHtml(pageId)}" style="display: none;">
<h2><i class="${escapeHtml(icon)}"></i> ${escapeHtml(name)}匹配项</h2>
<div class="sites-grid"></div>
</section>`;
}).join('\n');
return `
<!-- 搜索结果页 -->
<div class="page" id="search-results">
<div class="welcome-section">
<h2>搜索结果</h2>
<p class="subtitle">在所有页面中找到的匹配项</p>
</div>
${searchSections}
</div>`;
}
// 生成Google Fonts链接
function generateGoogleFontsLink(config) {
const fonts = config.fonts;
const googleFonts = [];
// 收集需要加载的Google字体
Object.values(fonts).forEach(font => {
if (font.source === 'google') {
const fontName = font.family.replace(/["']/g, '');
const fontWeight = font.weight || 400;
googleFonts.push(`family=${fontName}:wght@${fontWeight}`);
}
});
return googleFonts.length > 0
? `<link href="https://fonts.googleapis.com/css2?${googleFonts.join('&')}&display=swap" rel="stylesheet">`
: '';
}
// 生成字体CSS变量
function generateFontVariables(config) {
const fonts = config.fonts;
let css = ':root {\n';
Object.entries(fonts).forEach(([key, font]) => {
css += ` --font-${key}: ${font.family};\n`;
if (font.weight) {
css += ` --font-weight-${key}: ${font.weight};\n`;
}
});
css += '}';
return css;
}
// 生成完整的HTML
function generateHTML(config) {
const googleFontsLink = generateGoogleFontsLink(config);
const fontVariables = generateFontVariables(config);
const currentYear = new Date().getFullYear();
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${escapeHtml(config.site.title)}</title>
<link rel="icon" href="./favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
${googleFontsLink}
<style>
${fontVariables}
</style>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body class="loading">
<div class="layout">
<!-- 移动端按钮 -->
<div class="mobile-buttons">
<button class="menu-toggle" aria-label="切换菜单">
<i class="fas fa-bars"></i>
</button>
<button class="search-toggle" aria-label="切换搜索">
<i class="fas fa-search"></i>
</button>
</div>
<!-- 遮罩层 -->
<div class="overlay"></div>
<!-- 左侧导航 -->
<nav class="sidebar">
<div class="logo">
<h1>导航站</h1>
</div>
<div class="nav-section">
${generateNavigation(config.navigation)}
</div>
<div class="nav-section">
<div class="section-title">
<i class="fas fa-link"></i>
</div>
${generateSocialLinks(config.social)}
</div>
<div class="copyright">
<p>© ${currentYear} <a href="https://github.com/rbetree/menav" target="_blank">MeNav</a></p>
<p>by <a href="https://github.com/rbetree" target="_blank">rbetree</a></p>
</div>
</nav>
<!-- 右侧内容区 -->
<main class="content">
<!-- 搜索框容器 -->
<div class="search-container">
<div class="search-box">
<input type="text" id="search" placeholder="搜索...">
<i class="fas fa-search"></i>
</div>
</div>
<!-- 首页 -->
<div class="page active" id="home">
<div class="welcome-section">
<h2>${escapeHtml(config.profile.title)}</h2>
<h3>${escapeHtml(config.profile.subtitle)}</h3>
<p class="subtitle">${escapeHtml(config.profile.description)}</p>
</div>
${generateCategories(config.categories)}
</div>
<!-- 项目页 -->
<div class="page" id="projects">
${generatePageContent('projects', config.projects)}
</div>
<!-- 文章页 -->
<div class="page" id="articles">
${generatePageContent('articles', config.articles)}
</div>
<!-- 朋友页 -->
<div class="page" id="friends">
${generatePageContent('friends', config.friends)}
</div>
${generateSearchResultsPage(config)}
</main>
</div>
<script src="script.js"></script>
</body>
</html>`;
}
// 复制静态文件
function copyStaticFiles(config) {
// 确保dist目录存在
if (!fs.existsSync('dist')) {
fs.mkdirSync('dist', { recursive: true });
}
// 复制CSS文件
try {
fs.copyFileSync('assets/style.css', 'dist/style.css');
console.log('Copied style.css to dist/');
} catch (e) {
console.error('Error copying style.css:', e);
}
// 复制JavaScript文件
try {
fs.copyFileSync('src/script.js', 'dist/script.js');
console.log('Copied script.js to dist/');
} catch (e) {
console.error('Error copying script.js:', e);
}
// 如果配置了favicon确保文件存在并复制
if (config.site.favicon) {
try {
if (fs.existsSync(`assets/${config.site.favicon}`)) {
fs.copyFileSync(`assets/${config.site.favicon}`, `dist/${path.basename(config.site.favicon)}`);
console.log(`Copied favicon: ${config.site.favicon} to dist/`);
} else if (fs.existsSync(config.site.favicon)) {
fs.copyFileSync(config.site.favicon, `dist/${path.basename(config.site.favicon)}`);
console.log(`Copied favicon: ${config.site.favicon} to dist/`);
} else {
console.warn(`Warning: Favicon file not found: ${config.site.favicon}`);
}
} catch (e) {
console.error('Error copying favicon:', e);
}
}
}
// 处理模板文件,替换占位符
function processTemplate(template, config) {
const currentYear = new Date().getFullYear();
const googleFontsLink = generateGoogleFontsLink(config);
const fontVariables = generateFontVariables(config);
// 创建替换映射
const replacements = {
'{{SITE_TITLE}}': escapeHtml(config.site.title),
'{{SITE_LOGO_TEXT}}': escapeHtml(config.site.logo_text || '导航站'), // 从配置中获取,如果不存在则使用默认值
'{{GOOGLE_FONTS}}': googleFontsLink,
'{{{FONT_VARIABLES}}}': fontVariables,
'{{NAVIGATION}}': generateNavigation(config.navigation),
'{{SOCIAL_LINKS}}': generateSocialLinks(config.social),
'{{CURRENT_YEAR}}': currentYear,
'{{HOME_CONTENT}}': generateHomeContent(config),
'{{PROJECTS_CONTENT}}': generatePageContent('projects', config.projects),
'{{ARTICLES_CONTENT}}': generatePageContent('articles', config.articles),
'{{FRIENDS_CONTENT}}': generatePageContent('friends', config.friends),
'{{SEARCH_RESULTS}}': generateSearchResultsPage(config)
};
// 执行替换
let processedTemplate = template;
for (const [placeholder, value] of Object.entries(replacements)) {
processedTemplate = processedTemplate.replace(placeholder, value);
}
return processedTemplate;
}
// 主函数
function main() {
const config = loadConfig();
try {
// 确保dist目录存在
if (!fs.existsSync('dist')) {
fs.mkdirSync('dist', { recursive: true });
}
// 读取模板文件
const templatePath = 'templates/index.html';
let htmlContent = '';
if (fs.existsSync(templatePath)) {
// 读取模板并处理
const template = fs.readFileSync(templatePath, 'utf8');
htmlContent = processTemplate(template, config);
console.log(`Using template from ${templatePath} and injecting content`);
} else {
// 如果没有模板文件使用生成的HTML
htmlContent = generateHTML(config);
console.log('No template file found, using generated HTML');
}
// 生成HTML
fs.writeFileSync('dist/index.html', htmlContent);
console.log('Successfully generated dist/index.html');
// 复制静态文件
copyStaticFiles(config);
} catch (e) {
console.error('Error in main function:', e);
process.exit(1);
}
}
main();