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
+313
View File
@@ -0,0 +1,313 @@
import { readFile } from 'node:fs/promises';
import { parseSync } from '@slidev/parser';
const canvasHeight = 768;
const canvasWidth = 1024;
const slidePaddingX = 52;
const slidePaddingY = 42;
const contentHeight = canvasHeight - slidePaddingY * 2;
const contentWidth = canvasWidth - slidePaddingX * 2;
const twoColumnGap = 46;
const baseFontSize = 32;
const baseLineHeight = baseFontSize * 1.42;
const verticalRiskTolerance = 1.08;
const codeHeightPresets = new Map([
['easy-code-height-sm', 180],
['easy-code-height-md', 300],
['easy-code-height-lg', 420],
]);
export class StaticOverflowAuditor {
cache = new Map();
onAnalyzeSlide;
constructor(options = {}) {
this.onAnalyzeSlide = options.onAnalyzeSlide;
}
async audit(decks) {
const result = { errors: [], issues: [] };
for (const deck of decks) {
try {
result.issues.push(...await this.auditDeck(deck));
}
catch (error) {
result.errors.push({
deck,
message: error instanceof Error ? error.message : String(error),
});
}
}
return result;
}
async auditDeck(deck) {
const source = await readFile(deck.entry, 'utf8');
const parsed = parseSync(source, deck.entry);
const previous = this.cache.get(deck.entry) ?? new Map();
const next = new Map();
const issues = [];
parsed.slides.forEach((slide, index) => {
const number = index + 1;
const fingerprint = slide.revision ?? slide.raw;
const cached = previous.get(number);
if (cached?.fingerprint === fingerprint) {
next.set(number, cached);
issues.push(...cached.issues);
return;
}
this.onAnalyzeSlide?.(deck, number);
const slideIssues = analyzeSlide(deck, number, slide);
next.set(number, { fingerprint, issues: slideIssues });
issues.push(...slideIssues);
});
this.cache.set(deck.entry, next);
return issues;
}
}
export async function auditStaticDecks(decks) {
return await new StaticOverflowAuditor().audit(decks);
}
function analyzeSlide(deck, number, slide) {
const content = stripComments(slide.content);
const layout = String(slide.frontmatter.layout ?? (number === 1 ? 'cover' : 'default'));
const classNames = normalizeClasses(slide.frontmatter.class);
const heading = slide.title?.trim() || extractHeading(content);
const estimates = estimateSlide(content, layout, classNames, String(slide.frontmatter.imagePlacement ?? 'auto'));
const directions = [];
if (estimates.some(estimate => estimate.height > contentHeight * verticalRiskTolerance))
directions.push('vertical');
if (estimates.some(estimate => estimate.horizontalRisk))
directions.push('horizontal');
if (!directions.length)
return [];
const reason = directions.length === 2
? '估算内容高度超过画布容量,且存在无法可靠换行的宽内容'
: directions[0] === 'vertical'
? '估算内容高度超过 easy-jyy 画布容量'
: '存在无法可靠换行的宽内容';
return [{ deck, directions, heading, reason, slide: number }];
}
function estimateSlide(content, layout, classNames, imagePlacement) {
if (layout === 'two-cols') {
const slots = splitTwoColumnContent(content);
const headingEstimate = estimateRegion(slots.before, contentWidth, layout, classNames);
const columnWidth = (contentWidth - twoColumnGap) / 2;
const left = estimateRegion(slots.left, columnWidth, layout, classNames);
const right = estimateRegion(slots.right, columnWidth, layout, classNames);
return [
{
height: headingEstimate.height + Math.max(left.height, right.height),
horizontalRisk: headingEstimate.horizontalRisk || left.horizontalRisk || right.horizontalRisk,
},
];
}
const splitImageLayout = ['image-left', 'image-right'].includes(layout)
|| (layout === 'image-auto' && ['left', 'right'].includes(imagePlacement));
const width = splitImageLayout ? (contentWidth - 42) / 2 : contentWidth;
return [estimateRegion(content, width, layout, classNames)];
}
function splitTwoColumnContent(content) {
const leftMarker = /^::left::\s*$/m;
const rightMarker = /^::right::\s*$/m;
const leftMatch = leftMarker.exec(content);
const rightMatch = rightMarker.exec(content);
if (!leftMatch && !rightMatch)
return { before: content, left: '', right: '' };
const firstIndex = Math.min(leftMatch?.index ?? Number.POSITIVE_INFINITY, rightMatch?.index ?? Number.POSITIVE_INFINITY);
const before = content.slice(0, firstIndex);
const left = leftMatch
? content.slice(leftMatch.index + leftMatch[0].length, rightMatch && rightMatch.index > leftMatch.index ? rightMatch.index : undefined)
: '';
const right = rightMatch
? content.slice(rightMatch.index + rightMatch[0].length, leftMatch && leftMatch.index > rightMatch.index ? leftMatch.index : undefined)
: '';
return { before, left, right };
}
function estimateRegion(markdown, width, layout, classNames) {
const lines = markdown.replace(/\r/g, '').split('\n');
let height = 0;
let horizontalRisk = false;
for (let index = 0; index < lines.length;) {
const raw = lines[index];
const line = raw.trim();
if (!line || isStructuralLine(line)) {
index++;
continue;
}
const fence = /^(?:`{3,}|~{3,})([^\s{]*)/.exec(line);
if (fence) {
const marker = line.startsWith('`') ? '`' : '~';
const markerLength = line.match(new RegExp(`^\\${marker}+`))?.[0].length ?? 3;
const language = fence[1].toLowerCase();
let end = index + 1;
while (end < lines.length && !new RegExp(`^\\s*\\${marker}{${markerLength},}`).test(lines[end]))
end++;
if (language === 'mermaid')
height += 180;
else
height += explicitCodeHeight(lines, index, classNames);
index = Math.min(lines.length, end + 1);
continue;
}
const heading = /^(#{1,4})\s+(.+)$/.exec(line);
if (heading) {
const level = heading[1].length;
const fontSize = level === 1 ? (layout === 'cover' ? 68 : 60) : level === 2 ? 48 : level === 3 ? 38 : 32;
const wraps = wrappedLines(heading[2], width, fontSize);
const margins = level === 1 ? 26 : level === 2 ? 39 : level === 3 ? 34 : 24;
height += wraps * fontSize * 1.15 + margins;
horizontalRisk ||= hasHorizontalRisk(heading[2], width, fontSize);
index++;
continue;
}
if (line === '$$' || line.startsWith('$$')) {
let end = index + 1;
while (end < lines.length && !lines[end].includes('$$'))
end++;
height += 82 + Math.max(0, end - index - 2) * 38;
index = Math.min(lines.length, end + 1);
continue;
}
if (isStandaloneImage(line)) {
height += canvasHeight * 0.46 + 16;
index++;
continue;
}
if (isTableRow(line) && isTableSeparator(lines[index + 1] ?? '')) {
const tableLines = [line];
index += 2;
while (index < lines.length && isTableRow(lines[index].trim())) {
tableLines.push(lines[index].trim());
index++;
}
const columnCount = Math.max(1, splitTableRow(tableLines[0]).length);
const cellWidth = width / columnCount - 28;
for (const row of tableLines) {
const rowWraps = Math.max(...splitTableRow(row).map(cell => wrappedLines(cell, cellWidth, 30)), 1);
height += rowWraps * 30 * 1.42 + 20;
}
continue;
}
if (isListLine(line)) {
const items = [];
while (index < lines.length && (isListLine(lines[index].trim()) || /^\s{2,}\S/.test(lines[index]))) {
if (isListLine(lines[index].trim()))
items.push(lines[index].trim().replace(/^(?:[-+*]|\d+[.)])\s+/, ''));
index++;
}
const itemHeight = items.reduce((sum, item) => sum + wrappedLines(item, width - baseFontSize * 1.2, baseFontSize) * baseLineHeight, 0);
height += itemHeight + Math.max(0, items.length - 1) * 10 + 28;
horizontalRisk ||= items.some(item => hasHorizontalRisk(item, width - baseFontSize * 1.2, baseFontSize));
continue;
}
if (line.startsWith('>')) {
const quote = [];
while (index < lines.length && lines[index].trim().startsWith('>')) {
quote.push(lines[index].trim().replace(/^>\s?/, ''));
index++;
}
const fontSize = layout === 'quote' ? 46 : baseFontSize;
height += wrappedLines(quote.join(' '), width - 29, fontSize) * fontSize * (layout === 'quote' ? 1.35 : 1.42) + 48;
horizontalRisk ||= hasHorizontalRisk(quote.join(' '), width - 29, fontSize);
continue;
}
const paragraph = [];
while (index < lines.length && !isBlockStart(lines, index)) {
const text = stripMarkup(lines[index]);
if (text)
paragraph.push(text);
index++;
}
if (paragraph.length) {
const text = paragraph.join(' ');
height += wrappedLines(text, width, baseFontSize) * baseLineHeight + 28;
horizontalRisk ||= hasHorizontalRisk(text, width, baseFontSize);
}
else {
index++;
}
}
horizontalRisk ||= explicitWidthRisk(markdown, width);
return { height, horizontalRisk };
}
function isBlockStart(lines, index) {
const line = lines[index].trim();
if (!line || isStructuralLine(line) || /^(?:`{3,}|~{3,}|#{1,4}\s|\$\$|>)/.test(line))
return true;
if (isStandaloneImage(line) || isListLine(line))
return true;
return isTableRow(line) && isTableSeparator(lines[index + 1] ?? '');
}
function explicitCodeHeight(lines, fenceIndex, classNames) {
const nearby = `${classNames.join(' ')} ${lines.slice(Math.max(0, fenceIndex - 8), fenceIndex + 1).join(' ')}`;
for (const [className, height] of codeHeightPresets) {
if (new RegExp(`(?:^|\\s|["'])${className}(?:$|\\s|["'])`).test(nearby))
return height;
}
const inlineHeight = /(?:height|max-height)\s*:\s*(\d+(?:\.\d+)?)px/i.exec(nearby);
return inlineHeight ? Number.parseFloat(inlineHeight[1]) : 0;
}
function wrappedLines(value, width, fontSize) {
const available = Math.max(1, width / fontSize);
return Math.max(1, Math.ceil(displayWidth(stripMarkup(value)) / available));
}
function displayWidth(value) {
let width = 0;
for (const character of value) {
if (/\s/.test(character))
width += 0.33;
else if (/[\u1100-\u115f\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe6f\uff01-\uff60\uffe0-\uffe6]/.test(character))
width += 1;
else
width += 0.56;
}
return width;
}
function hasHorizontalRisk(value, width, fontSize) {
const available = width / fontSize;
return stripMarkup(value)
.split(/\s+/)
.some(token => !/[\u2e80-\u9fff]/.test(token) && displayWidth(token) > available * 1.05);
}
function explicitWidthRisk(markdown, width) {
for (const match of markdown.matchAll(/(?:width\s*:\s*|width\s*=\s*["']?)(\d+(?:\.\d+)?)px/gi)) {
if (Number.parseFloat(match[1]) > width)
return true;
}
return false;
}
function normalizeClasses(value) {
if (Array.isArray(value))
return value.flatMap(item => String(item).split(/\s+/)).filter(Boolean);
return String(value ?? '').split(/\s+/).filter(Boolean);
}
function extractHeading(markdown) {
return /^(?:#{1,4})\s+(.+)$/m.exec(markdown)?.[1].trim() ?? '';
}
function stripComments(markdown) {
return markdown.replace(/<!--[\s\S]*?-->/g, '');
}
function stripMarkup(value) {
return value
.replace(/!\[[^\]]*\]\([^)]*\)(?:\{[^}]*\})?/g, '')
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
.replace(/<[^>]+>/g, ' ')
.replace(/[`*_~]/g, '')
.trim();
}
function isStructuralLine(line) {
return /^::(?:left|right|image)::$/i.test(line)
|| /^<\/?(?:v-clicks|div|span|template|slot)(?:\s[^>]*)?>$/i.test(line);
}
function isStandaloneImage(line) {
return /^!\[[^\]]*\]\([^)]*\)(?:\{[^}]*\})?$/.test(line);
}
function isListLine(line) {
return /^(?:[-+*]|\d+[.)])\s+/.test(line);
}
function isTableRow(line) {
return line.includes('|');
}
function isTableSeparator(line) {
return /^\s*\|?\s*:?-{3,}:?(?:\s*\|\s*:?-{3,}:?)+\s*\|?\s*$/.test(line);
}
function splitTableRow(line) {
return line.replace(/^\s*\||\|\s*$/g, '').split('|').map(cell => cell.trim());
}
//# sourceMappingURL=static-overflow.js.map