build: add precompiled CLI distribution

This commit is contained in:
2026-08-29 19:00:14 +08:00
parent d074681928
commit 4d1dc76e31
77 changed files with 3461 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
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