const { escapeHtml } = require('../utils/html'); /** * 将 CSS 文本安全嵌入到 ` 结束标签导致样式块被提前终止。 * @param {string} cssText CSS 文本 * @returns {string} 安全的 CSS 文本 */ function makeCssSafeForHtmlStyleTag(cssText) { if (typeof cssText !== 'string') { return ''; } return cssText.replace(/<\/style/gi, '<\\/style'); } function normalizeFontWeight(input) { if (input === undefined || input === null) return 'normal'; if (typeof input === 'number' && Number.isFinite(input)) { return String(input); } const raw = String(input).trim(); if (!raw) return 'normal'; if (/^(normal|bold|bolder|lighter)$/i.test(raw)) return raw.toLowerCase(); if (/^[1-9]00$/.test(raw)) return raw; return raw; } function normalizeFontFamilyForCss(input) { const raw = String(input || '').trim(); if (!raw) return ''; const generics = new Set([ 'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'system-ui', 'ui-serif', 'ui-sans-serif', 'ui-monospace', 'ui-rounded', 'emoji', 'math', 'fangsong', ]); return raw .split(',') .map((part) => part.trim()) .filter(Boolean) .map((part) => { const unquoted = part.replace(/^['"]|['"]$/g, '').trim(); if (!unquoted) return ''; if (generics.has(unquoted)) return unquoted; const needsQuotes = /\s/.test(unquoted); if (!needsQuotes) return unquoted; return `"${unquoted.replace(/"/g, '\\"')}"`; }) .filter(Boolean) .join(', '); } function normalizeFontSource(input) { const raw = String(input || '') .trim() .toLowerCase(); if (raw === 'css' || raw === 'google' || raw === 'system') return raw; return 'system'; } function getNormalizedFontsConfig(config) { const fonts = config && config.fonts && typeof config.fonts === 'object' ? config.fonts : {}; return { source: normalizeFontSource(fonts.source), family: normalizeFontFamilyForCss(fonts.family), weight: normalizeFontWeight(fonts.weight), cssUrl: String(fonts.cssUrl || fonts.href || '').trim(), preload: Boolean(fonts.preload), }; } function tryGetUrlOrigin(input) { const raw = String(input || '').trim(); if (!raw) return ''; try { return new URL(raw).origin; } catch { return ''; } } function buildStylesheetLinkTag(href, preload) { const safeHref = escapeHtml(href); if (!preload) return ``; return [ ``, ``, ].join('\n'); } // 生成字体相关 function generateFontLinks(config) { const fonts = getNormalizedFontsConfig(config); const links = []; // 全站基础字体:按配置加载 if (fonts.source === 'css' && fonts.cssUrl) { const origin = tryGetUrlOrigin(fonts.cssUrl); if (origin) { links.push(``); } links.push(buildStylesheetLinkTag(fonts.cssUrl, fonts.preload)); } if (fonts.source === 'google' && fonts.family) { links.push(''); links.push(''); const familyNoQuotes = fonts.family.replace(/[\"']/g, '').split(',')[0].trim(); const weight = /^[1-9]00$/.test(fonts.weight) ? fonts.weight : '400'; const familyParam = encodeURIComponent(familyNoQuotes).replace(/%20/g, '+'); links.push( buildStylesheetLinkTag( `https://fonts.googleapis.com/css2?family=${familyParam}:wght@${weight}&display=swap`, fonts.preload ) ); } return links.join('\n'); } // 生成字体 CSS 变量(单一字体配置) function generateFontCss(config) { const fonts = getNormalizedFontsConfig(config); const family = fonts.family || 'system-ui, sans-serif'; const weight = fonts.weight || 'normal'; const css = `:root {\n --font-body: ${family};\n --font-weight-body: ${weight};\n}\n`; return makeCssSafeForHtmlStyleTag(css); } module.exports = { generateFontLinks, generateFontCss, };