import fs from 'node:fs'; import path from 'node:path'; export const BRAND_SPEC_REL = '.design_library/kole-ui/brand/brand-mark.json'; export const BRAND_DATA_REL = 'site/brand-mark.generated.json'; export const BRAND_ASSET_REL = 'site/assets/kole-mark.svg'; export const BRAND_MONO_REL = 'site/assets/kole-mark-mono.svg'; export const BRAND_SCHEMA_VERSION = 2; export function readBrandSpec(root) { const file = path.join(root, BRAND_SPEC_REL); return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); } export function validateBrandSpec(spec) { const errors = []; if (!spec || spec.schemaVersion !== BRAND_SCHEMA_VERSION) { errors.push(`schemaVersion 必须为 ${BRAND_SCHEMA_VERSION}`); } if (!spec || spec.viewBox !== '0 0 24 24') errors.push('viewBox 必须为 0 0 24 24'); const g = spec?.geometry || {}; const paths = g.paths; if (!Array.isArray(paths) || !paths.length || paths.some((d) => typeof d !== 'string' || !d.trim())) { errors.push('geometry.paths 必须包含至少一条非空路径'); } if (g.mode !== 'fill' && g.mode !== 'stroke') errors.push('geometry.mode 必须为 fill 或 stroke'); if (g.mode === 'stroke') { if (g.strokeWidth !== 1.5) errors.push('stroke 模式的 geometry.strokeWidth 必须为 1.5'); if (g.linecap !== 'round') errors.push('stroke 模式的 geometry.linecap 必须为 round'); if (g.linejoin !== 'round') errors.push('stroke 模式的 geometry.linejoin 必须为 round'); } if (g.opacities !== undefined) { if (!Array.isArray(g.opacities) || g.opacities.length !== (paths || []).length) { errors.push('geometry.opacities 必须与 paths 等长'); } else if (g.opacities.some((v) => typeof v !== 'number' || !(v > 0) || v > 1)) { errors.push('geometry.opacities 取值必须在 (0, 1] 区间'); } } if (!/^#[0-9A-Fa-f]{6}$/.test(spec?.favicon?.background || '')) errors.push('favicon.background 必须为六位十六进制颜色'); if (!/^#[0-9A-Fa-f]{6}$/.test(spec?.favicon?.foreground || '')) errors.push('favicon.foreground 必须为六位十六进制颜色'); if (spec?.favicon?.radius !== 6) errors.push('favicon.radius 必须为 6'); for (const [key, value] of Object.entries(spec?.sizes || {})) { if (!Number.isFinite(value) || value <= 0) errors.push(`sizes.${key} 必须为正数`); } return errors; } /** 逐路径生成 :实心模式按 opacities 给 fill-opacity;描边模式交给外层 统一给描边属性。 */ function pathsMarkup(spec) { const { paths, mode, opacities } = spec.geometry; return paths .map((d, i) => { const opacity = mode === 'fill' && opacities && opacities[i] != null ? ` fill-opacity="${opacities[i]}"` : ''; return ``; }) .join(''); } /** 图形的公共外层:实心走 fill,描边走 stroke;颜色由调用方决定。 */ function groupMarkup(spec, color, { mono }) { const { mode } = spec.geometry; const strokeAttrs = `fill="none" stroke="${mono ? '#000000' : color}" stroke-width="${spec.geometry.strokeWidth}"` + ` stroke-linecap="${spec.geometry.linecap}" stroke-linejoin="${spec.geometry.linejoin}"`; return mode === 'stroke' ? `${pathsMarkup(spec)}` : `${pathsMarkup(spec)}`; } export function renderFaviconSvg(spec) { return [ ``, ``, groupMarkup(spec, spec.favicon.foreground, { mono: false }), '', ].join(''); } export function renderMonoSvg(spec) { return [ ``, groupMarkup(spec, '#000000', { mono: true }), '', ].join(''); } export function generatedData(spec) { return { schemaVersion: spec.schemaVersion, name: spec.name, viewBox: spec.viewBox, geometry: { mode: spec.geometry.mode, paths: spec.geometry.paths, ...(spec.geometry.opacities ? { opacities: spec.geometry.opacities } : {}), ...(spec.geometry.mode === 'stroke' ? { strokeWidth: spec.geometry.strokeWidth, linecap: spec.geometry.linecap, linejoin: spec.geometry.linejoin, } : {}), }, favicon: { background: spec.favicon.background, foreground: spec.favicon.foreground, radius: spec.favicon.radius, }, sizes: spec.sizes, assets: { favicon: 'assets/kole-mark.svg', mono: 'assets/kole-mark-mono.svg', }, }; } export function atomicWrite(file, content) { fs.mkdirSync(path.dirname(file), { recursive: true }); const tmp = `${file}.tmp-${process.pid}`; fs.writeFileSync(tmp, content, 'utf8'); fs.renameSync(tmp, file); }