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,
};