import { access, readFile } from 'node:fs/promises'; import path from 'node:path'; import fg from 'fast-glob'; import matter from 'gray-matter'; const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; const allowedFits = new Set(['contain', 'cover', 'fill', 'scale-down', 'none']); export async function discoverDecks(cwd = process.cwd(), slidesDir = 'slides') { const entries = await fg(`${slidesDir.replaceAll('\\', '/')}/*/slides.md`, { cwd, absolute: true, onlyFiles: true }); const decks = await Promise.all(entries.map(entry => readDeck(entry))); return decks.sort((a, b) => compareDecks(a, b)); } export async function readDeck(entry) { const source = await readFile(entry, 'utf8'); const parsed = matter(source); const directory = path.dirname(entry); const slug = path.basename(directory); const tags = Array.isArray(parsed.data.tags) ? parsed.data.tags.map((tag) => String(tag).trim()).filter(Boolean) : []; return { slug, entry, directory, title: String(parsed.data.title ?? '').trim(), description: String(parsed.data.description ?? '').trim(), author: String(parsed.data.author ?? '').trim(), date: normalizeDate(parsed.data.date), tags, cover: parsed.data.cover ? String(parsed.data.cover).trim() : undefined, draft: parsed.data.draft === true, }; } export async function resolveDeck(input, cwd = process.cwd(), slidesDir = 'slides') { const decks = await discoverDecks(cwd, slidesDir); if (!input) return decks.find(deck => !deck.draft) ?? decks[0]; const direct = path.resolve(cwd, input); const bySlug = decks.find(deck => deck.slug === input); const byPath = decks.find(deck => path.resolve(deck.entry) === direct); const deck = bySlug ?? byPath; if (!deck) throw new Error(`找不到演示“${input}”。可用 slug:${decks.map(item => item.slug).join(', ') || '无'}`); return deck; } export async function validateDeck(deck) { const issues = []; const source = await readFile(deck.entry, 'utf8'); const parsed = matter(source); if (!slugPattern.test(deck.slug)) issues.push(error(deck.entry, '目录名必须是小写 kebab-case slug')); if (!deck.title) issues.push(error(deck.entry, 'frontmatter 缺少必填 title')); if (deck.date && Number.isNaN(Date.parse(deck.date))) issues.push(error(deck.entry, `date 不是有效日期:${deck.date}`)); if (parsed.data.tags !== undefined && !Array.isArray(parsed.data.tags)) issues.push(error(deck.entry, 'tags 必须是数组')); if (parsed.data.theme && parsed.data.theme !== 'easy-jyy') issues.push(warning(deck.entry, `当前主题为 ${parsed.data.theme},easy-jyy 主题能力可能不会生效`)); if (deck.cover && !isRemote(deck.cover)) { const coverPath = path.resolve(deck.directory, deck.cover); if (!(await exists(coverPath))) issues.push(error(deck.entry, `封面不存在:${deck.cover}`)); } for (const image of extractMarkdownImages(parsed.content)) { if (!isRemote(image.source) && !image.source.startsWith('/') && !(await exists(path.resolve(deck.directory, image.source)))) issues.push(error(deck.entry, `图片不存在:${image.source}`)); if (image.attributes.fit && !allowedFits.has(image.attributes.fit)) issues.push(error(deck.entry, `图片 fit 不受支持:${image.attributes.fit}`)); if (image.attributes['max-height'] && !isSafeCssSize(image.attributes['max-height'])) issues.push(error(deck.entry, `图片 max-height 不合法:${image.attributes['max-height']}`)); } return issues; } export function extractMarkdownImages(markdown) { const images = []; const pattern = /!\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)\s*(?:\{([^}]*)\})?/g; for (const match of markdown.matchAll(pattern)) { images.push({ source: match[1], attributes: parseAttributes(match[2] ?? ''), }); } return images; } export function parseAttributes(input) { const attributes = {}; const pattern = /([\w-]+)=(?:"([^"]*)"|'([^']*)'|([^\s]+))/g; for (const match of input.matchAll(pattern)) attributes[match[1]] = match[2] ?? match[3] ?? match[4] ?? ''; return attributes; } function normalizeDate(value) { if (!value) return ''; if (value instanceof Date) return value.toISOString().slice(0, 10); return String(value).trim(); } function compareDecks(a, b) { const byDate = (Date.parse(b.date || '1970-01-01') || 0) - (Date.parse(a.date || '1970-01-01') || 0); return byDate || a.title.localeCompare(b.title, 'zh-CN'); } function isRemote(value) { return /^(?:https?:)?\/\//.test(value) || value.startsWith('data:'); } function isSafeCssSize(value) { return /^(?:\d+(?:\.\d+)?)(?:px|rem|em|vh|vw|%|vmin|vmax)$/.test(value); } async function exists(file) { try { await access(file); return true; } catch { return false; } } function error(file, message) { return { level: 'error', file, message }; } function warning(file, message) { return { level: 'warning', file, message }; } //# sourceMappingURL=content.js.map