Files

124 lines
5.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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