234 lines
7.0 KiB
JavaScript
234 lines
7.0 KiB
JavaScript
const fs = require('fs');
|
||
const path = require('path');
|
||
const { getSubmenuForNavItem, assignCategorySlugs } = require('../config');
|
||
const {
|
||
tryLoadArticlesFeedCache,
|
||
buildArticlesCategoriesByPageCategories,
|
||
} = require('../cache/articles');
|
||
const {
|
||
tryLoadProjectsRepoCache,
|
||
tryLoadProjectsHeatmapCache,
|
||
applyRepoMetaToCategories,
|
||
buildProjectsMeta,
|
||
} = require('../cache/projects');
|
||
const { getPageConfigUpdatedAtMeta } = require('../utils/pageMeta');
|
||
const { createLogger, isVerbose } = require('../utils/logger');
|
||
const { ConfigError } = require('../utils/errors');
|
||
const { renderMarkdownToHtml } = require('./markdown');
|
||
|
||
const log = createLogger('render');
|
||
|
||
function prepareNavigationData(pageId, config) {
|
||
if (!Array.isArray(config.navigation)) {
|
||
log.warn('config.navigation 不是数组,已降级为空数组');
|
||
return [];
|
||
}
|
||
|
||
return config.navigation.map((nav) => {
|
||
const navItem = {
|
||
...nav,
|
||
isActive: nav.id === pageId,
|
||
active: nav.id === pageId,
|
||
};
|
||
|
||
const submenu = getSubmenuForNavItem(navItem, config);
|
||
if (submenu) {
|
||
navItem.submenu = submenu;
|
||
}
|
||
|
||
return navItem;
|
||
});
|
||
}
|
||
|
||
function resolveTemplateName(pageId, data) {
|
||
const explicitTemplate = typeof data.template === 'string' ? data.template.trim() : '';
|
||
let templateName = explicitTemplate || pageId;
|
||
|
||
if (!explicitTemplate) {
|
||
const inferredTemplatePath = path.join(
|
||
process.cwd(),
|
||
'templates',
|
||
'pages',
|
||
`${templateName}.hbs`
|
||
);
|
||
if (!fs.existsSync(inferredTemplatePath)) {
|
||
templateName = 'page';
|
||
}
|
||
}
|
||
|
||
return templateName;
|
||
}
|
||
|
||
function applyProjectsData(data, pageId, config) {
|
||
data.siteCardStyle = 'repo';
|
||
data.projectsMeta = buildProjectsMeta(config);
|
||
|
||
const heatmapCache = tryLoadProjectsHeatmapCache(pageId, config);
|
||
if (data.projectsMeta && data.projectsMeta.heatmap && heatmapCache) {
|
||
data.projectsMeta.heatmap.html = heatmapCache.html;
|
||
data.projectsMeta.heatmap.generatedAt = heatmapCache.meta.generatedAt;
|
||
data.projectsMeta.heatmap.sourceUrl = heatmapCache.meta.sourceUrl;
|
||
}
|
||
|
||
if (Array.isArray(data.categories)) {
|
||
const repoCache = tryLoadProjectsRepoCache(pageId, config);
|
||
if (repoCache && repoCache.map) {
|
||
applyRepoMetaToCategories(data.categories, repoCache.map);
|
||
}
|
||
}
|
||
}
|
||
|
||
function applyArticlesData(data, pageId, config) {
|
||
const cache = tryLoadArticlesFeedCache(pageId, config);
|
||
data.articlesItems = cache && Array.isArray(cache.items) ? cache.items : [];
|
||
data.articlesMeta = cache ? cache.meta : null;
|
||
data.articlesCategories = data.articlesItems.length
|
||
? buildArticlesCategoriesByPageCategories(data.categories, data.articlesItems)
|
||
: [];
|
||
}
|
||
|
||
function applyBookmarksData(data, pageId) {
|
||
const updatedAtMeta = getPageConfigUpdatedAtMeta(pageId);
|
||
if (updatedAtMeta) {
|
||
data.pageMeta = { ...updatedAtMeta };
|
||
}
|
||
}
|
||
|
||
function applyContentData(data, pageId, config) {
|
||
const content = data && data.content && typeof data.content === 'object' ? data.content : null;
|
||
const file = content && content.file ? String(content.file).trim() : '';
|
||
if (!file) {
|
||
throw new ConfigError(`内容页缺少 content.file:${pageId}`, [
|
||
`请在 config/*/pages/${pageId}.yml 中配置:`,
|
||
'template: content',
|
||
'content:',
|
||
' file: path/to/file.md',
|
||
]);
|
||
}
|
||
|
||
// 本期仅支持读取本地 markdown 文件
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
|
||
// 允许用户以相对 repo root 的路径引用(推荐约定:content/*.md)
|
||
// 同时允许绝对路径(便于本地调试/临时文件),但不会自动复制资源到 dist。
|
||
const normalized = file.replace(/\\/g, '/');
|
||
const absPath = path.isAbsolute(normalized)
|
||
? path.normalize(normalized)
|
||
: path.join(process.cwd(), normalized.replace(/^\//, ''));
|
||
if (!fs.existsSync(absPath)) {
|
||
throw new ConfigError(`内容页 markdown 文件不存在:${pageId}`, [
|
||
`检查路径是否正确:${file}`,
|
||
'提示:路径相对于仓库根目录(process.cwd())解析',
|
||
]);
|
||
}
|
||
|
||
const markdownText = fs.readFileSync(absPath, 'utf8');
|
||
const allowedSchemes =
|
||
config &&
|
||
config.site &&
|
||
config.site.security &&
|
||
Array.isArray(config.site.security.allowedSchemes)
|
||
? config.site.security.allowedSchemes
|
||
: null;
|
||
|
||
data.contentFile = normalized;
|
||
data.contentHtml = renderMarkdownToHtml(markdownText, { allowedSchemes });
|
||
}
|
||
|
||
function convertTopLevelSitesToCategory(data, pageId, templateName) {
|
||
const isFriendsPage = pageId === 'friends' || templateName === 'friends';
|
||
const isArticlesPage = pageId === 'articles' || templateName === 'articles';
|
||
|
||
if (
|
||
(isFriendsPage || isArticlesPage) &&
|
||
(!Array.isArray(data.categories) || data.categories.length === 0) &&
|
||
Array.isArray(data.sites) &&
|
||
data.sites.length > 0
|
||
) {
|
||
const implicitName = isFriendsPage ? '全部友链' : '全部来源';
|
||
data.categories = [
|
||
{
|
||
name: implicitName,
|
||
icon: 'fas fa-link',
|
||
sites: data.sites,
|
||
},
|
||
];
|
||
}
|
||
}
|
||
|
||
function applyHomePageTitles(data, pageId, config) {
|
||
const homePageId =
|
||
config.homePageId ||
|
||
(Array.isArray(config.navigation) && config.navigation[0] ? config.navigation[0].id : null) ||
|
||
'home';
|
||
|
||
data.homePageId = homePageId;
|
||
|
||
if (pageId === homePageId && config.profile) {
|
||
if (config.profile.title !== undefined) data.title = config.profile.title;
|
||
if (config.profile.subtitle !== undefined) data.subtitle = config.profile.subtitle;
|
||
}
|
||
}
|
||
|
||
function preparePageData(pageId, config) {
|
||
const data = {
|
||
...(config || {}),
|
||
currentPage: pageId,
|
||
pageId,
|
||
};
|
||
|
||
data.navigation = prepareNavigationData(pageId, config);
|
||
data.socialLinks = Array.isArray(config.social) ? config.social : [];
|
||
data.navigationData = data.navigation;
|
||
|
||
if (config[pageId]) {
|
||
Object.assign(data, config[pageId]);
|
||
}
|
||
|
||
if (data.title === undefined) {
|
||
const navItem = Array.isArray(config.navigation)
|
||
? config.navigation.find((nav) => nav.id === pageId)
|
||
: null;
|
||
if (navItem && navItem.name !== undefined) data.title = navItem.name;
|
||
}
|
||
if (data.subtitle === undefined) data.subtitle = '';
|
||
if (!Array.isArray(data.categories)) data.categories = [];
|
||
|
||
const templateName = resolveTemplateName(pageId, data);
|
||
|
||
if (templateName === 'projects') {
|
||
applyProjectsData(data, pageId, config);
|
||
}
|
||
|
||
convertTopLevelSitesToCategory(data, pageId, templateName);
|
||
|
||
if (templateName === 'articles') {
|
||
applyArticlesData(data, pageId, config);
|
||
}
|
||
|
||
if (templateName === 'bookmarks') {
|
||
applyBookmarksData(data, pageId);
|
||
}
|
||
|
||
if (templateName === 'content') {
|
||
applyContentData(data, pageId, config);
|
||
}
|
||
|
||
applyHomePageTitles(data, pageId, config);
|
||
|
||
if (Array.isArray(data.categories) && data.categories.length > 0) {
|
||
assignCategorySlugs(data.categories, new Map());
|
||
}
|
||
|
||
if (config[pageId] && config[pageId].template) {
|
||
if (isVerbose()) log.info(`页面 ${pageId} 使用指定模板`, { template: templateName });
|
||
}
|
||
|
||
return { data, templateName };
|
||
}
|
||
|
||
module.exports = {
|
||
preparePageData,
|
||
};
|