File size: 4,314 Bytes
e1ae2c6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | const { escapeHtml } = require('../utils/html');
/**
* 将 CSS 文本安全嵌入到 <style> 中,避免出现 `</style>` 结束标签导致样式块被提前终止。
* @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 `<link rel="stylesheet" href="${safeHref}">`;
return [
`<link rel="preload" href="${safeHref}" as="style" onload="this.onload=null;this.rel='stylesheet'">`,
`<noscript><link rel="stylesheet" href="${safeHref}"></noscript>`,
].join('\n');
}
// 生成字体相关 <link>
function generateFontLinks(config) {
const fonts = getNormalizedFontsConfig(config);
const links = [];
// 全站基础字体:按配置加载
if (fonts.source === 'css' && fonts.cssUrl) {
const origin = tryGetUrlOrigin(fonts.cssUrl);
if (origin) {
links.push(`<link rel="preconnect" href="${escapeHtml(origin)}" crossorigin>`);
}
links.push(buildStylesheetLinkTag(fonts.cssUrl, fonts.preload));
}
if (fonts.source === 'google' && fonts.family) {
links.push('<link rel="preconnect" href="https://fonts.googleapis.com">');
links.push('<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>');
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,
};
|