slides-grab 1.0.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -5
- package/bin/ppt-agent.js +46 -2
- package/convert.cjs +47 -13
- package/package.json +21 -4
- package/scripts/build-viewer.js +349 -0
- package/scripts/editor-server.js +7 -1
- package/scripts/figma-export.js +148 -0
- package/scripts/html2pdf.js +419 -32
- package/scripts/html2pptx.js +135 -0
- package/scripts/install-codex-skills.js +119 -0
- package/scripts/validate-slides.js +159 -371
- package/skills/{ppt-presentation-skill → slides-grab}/SKILL.md +16 -13
- package/skills/{ppt-design-skill → slides-grab-design}/SKILL.md +12 -5
- package/skills/{ppt-pptx-skill → slides-grab-export}/SKILL.md +7 -6
- package/skills/{ppt-plan-skill → slides-grab-plan}/SKILL.md +2 -2
- package/src/editor/codex-edit.js +136 -1
- package/src/editor/js/editor-init.js +10 -3
- package/src/figma.js +63 -0
- package/src/html2pptx.cjs +1166 -0
- package/src/image-contract.js +222 -0
- package/src/validation/cli.js +97 -0
- package/src/validation/core.js +688 -0
- package/templates/split-layout.html +3 -1
- package/AGENTS.md +0 -80
- package/PROGRESS.md +0 -39
- package/SETUP.md +0 -51
- package/prd.json +0 -135
- package/prd.md +0 -104
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { cp, mkdir, readdir, rm, access } from 'node:fs/promises';
|
|
4
|
+
import { constants as fsConstants } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
const packageRoot = resolve(__dirname, '..');
|
|
12
|
+
const sourceRoot = join(packageRoot, 'skills');
|
|
13
|
+
|
|
14
|
+
function parseArgs(argv) {
|
|
15
|
+
const opts = {
|
|
16
|
+
force: false,
|
|
17
|
+
dryRun: false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
for (const arg of argv) {
|
|
21
|
+
if (arg === '--force') {
|
|
22
|
+
opts.force = true;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (arg === '--dry-run') {
|
|
26
|
+
opts.dryRun = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (arg === '-h' || arg === '--help') {
|
|
30
|
+
opts.help = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return opts;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function pathExists(path) {
|
|
40
|
+
try {
|
|
41
|
+
await access(path, fsConstants.F_OK);
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function usage() {
|
|
49
|
+
process.stdout.write('Usage: node scripts/install-codex-skills.js [--force] [--dry-run]\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function listSkillDirs() {
|
|
53
|
+
const entries = await readdir(sourceRoot, { withFileTypes: true });
|
|
54
|
+
const names = [];
|
|
55
|
+
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (!entry.isDirectory()) continue;
|
|
58
|
+
const skillPath = join(sourceRoot, entry.name, 'SKILL.md');
|
|
59
|
+
if (await pathExists(skillPath)) {
|
|
60
|
+
names.push(entry.name);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return names.sort((a, b) => a.localeCompare(b));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function main() {
|
|
68
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
69
|
+
if (opts.help) {
|
|
70
|
+
usage();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const codexHome = process.env.CODEX_HOME || join(homedir(), '.codex');
|
|
75
|
+
const targetRoot = join(codexHome, 'skills');
|
|
76
|
+
|
|
77
|
+
const skillDirs = await listSkillDirs();
|
|
78
|
+
if (skillDirs.length === 0) {
|
|
79
|
+
process.stdout.write('[codex-skills] No skills found under ./skills\n');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
process.stdout.write(`[codex-skills] Source: ${sourceRoot}\n`);
|
|
84
|
+
process.stdout.write(`[codex-skills] Target: ${targetRoot}\n`);
|
|
85
|
+
|
|
86
|
+
if (opts.dryRun) {
|
|
87
|
+
for (const name of skillDirs) {
|
|
88
|
+
process.stdout.write(`[dry-run] would install: ${name}\n`);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await mkdir(targetRoot, { recursive: true });
|
|
94
|
+
|
|
95
|
+
for (const name of skillDirs) {
|
|
96
|
+
const src = join(sourceRoot, name);
|
|
97
|
+
const dest = join(targetRoot, name);
|
|
98
|
+
|
|
99
|
+
const exists = await pathExists(dest);
|
|
100
|
+
if (exists && !opts.force) {
|
|
101
|
+
process.stdout.write(`[skip] ${name} already exists (use --force to overwrite)\n`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (exists && opts.force) {
|
|
106
|
+
await rm(dest, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
await cp(src, dest, { recursive: true });
|
|
110
|
+
process.stdout.write(`[install] ${name}\n`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
process.stdout.write('[codex-skills] Done. Restart Codex to pick up new skills.\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
main().catch((error) => {
|
|
117
|
+
process.stderr.write(`[codex-skills] ${error.message}\n`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
});
|
|
@@ -1,340 +1,167 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
6
5
|
import { chromium } from 'playwright';
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_SLIDES_DIR,
|
|
9
|
+
DEFAULT_VALIDATE_FORMAT,
|
|
10
|
+
getValidateUsage,
|
|
11
|
+
parseValidateCliArgs,
|
|
12
|
+
} from '../src/validation/cli.js';
|
|
13
|
+
import {
|
|
14
|
+
createValidationFailure,
|
|
15
|
+
createValidationResult,
|
|
16
|
+
ensureSlidesPassValidation,
|
|
17
|
+
findSlideFiles,
|
|
18
|
+
formatValidationFailureForExport,
|
|
19
|
+
scanSlides,
|
|
20
|
+
selectSlideFiles,
|
|
21
|
+
} from '../src/validation/core.js';
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
DEFAULT_SLIDES_DIR,
|
|
25
|
+
ensureSlidesPassValidation,
|
|
26
|
+
findSlideFiles,
|
|
27
|
+
formatValidationFailureForExport,
|
|
28
|
+
parseValidateCliArgs as parseCliArgs,
|
|
13
29
|
};
|
|
14
|
-
const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
|
|
15
|
-
const TEXT_SELECTOR = 'p,h1,h2,h3,h4,h5,h6,li';
|
|
16
|
-
const TOLERANCE_PX = 0.5;
|
|
17
|
-
const DEFAULT_SLIDES_DIR = 'slides';
|
|
18
30
|
|
|
19
|
-
function
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
31
|
+
export function flattenValidationResult(result) {
|
|
32
|
+
const diagnostics = [];
|
|
33
|
+
|
|
34
|
+
for (const slide of result.slides || []) {
|
|
35
|
+
for (const issue of slide.critical || []) {
|
|
36
|
+
diagnostics.push({
|
|
37
|
+
slide: slide.slide,
|
|
38
|
+
severity: 'error',
|
|
39
|
+
code: issue.code,
|
|
40
|
+
message: issue.message,
|
|
41
|
+
location: issue.element || issue.parent || undefined,
|
|
42
|
+
related: Array.isArray(issue.elements) ? issue.elements : undefined,
|
|
43
|
+
source: issue.source,
|
|
44
|
+
assetPath: issue.assetPath,
|
|
45
|
+
detail: issue.detail,
|
|
46
|
+
metrics: issue.metrics,
|
|
47
|
+
bbox: issue.bbox
|
|
48
|
+
? {
|
|
49
|
+
x: issue.bbox.x,
|
|
50
|
+
y: issue.bbox.y,
|
|
51
|
+
width: issue.bbox.width,
|
|
52
|
+
height: issue.bbox.height,
|
|
53
|
+
}
|
|
54
|
+
: undefined,
|
|
55
|
+
intersection: issue.intersection,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
43
58
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
59
|
+
for (const issue of slide.warning || []) {
|
|
60
|
+
diagnostics.push({
|
|
61
|
+
slide: slide.slide,
|
|
62
|
+
severity: 'warning',
|
|
63
|
+
code: issue.code,
|
|
64
|
+
message: issue.message,
|
|
65
|
+
location: issue.element || issue.parent || undefined,
|
|
66
|
+
related: Array.isArray(issue.elements) ? issue.elements : undefined,
|
|
67
|
+
source: issue.source,
|
|
68
|
+
assetPath: issue.assetPath,
|
|
69
|
+
detail: issue.detail,
|
|
70
|
+
metrics: issue.metrics,
|
|
71
|
+
bbox: issue.bbox
|
|
72
|
+
? {
|
|
73
|
+
x: issue.bbox.x,
|
|
74
|
+
y: issue.bbox.y,
|
|
75
|
+
width: issue.bbox.width,
|
|
76
|
+
height: issue.bbox.height,
|
|
77
|
+
}
|
|
78
|
+
: undefined,
|
|
79
|
+
intersection: issue.intersection,
|
|
80
|
+
});
|
|
49
81
|
}
|
|
50
|
-
summary.criticalIssues += slide.summary.criticalCount;
|
|
51
|
-
summary.warnings += slide.summary.warningCount;
|
|
52
82
|
}
|
|
53
83
|
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
process.stdout.write('\n');
|
|
84
|
+
return {
|
|
85
|
+
schemaVersion: 1,
|
|
86
|
+
generatedAt: result.generatedAt,
|
|
87
|
+
summary: {
|
|
88
|
+
totalSlides: result.summary?.totalSlides ?? 0,
|
|
89
|
+
passedSlides: result.summary?.passedSlides ?? 0,
|
|
90
|
+
failedSlides: result.summary?.failedSlides ?? 0,
|
|
91
|
+
errors: result.summary?.criticalIssues ?? 0,
|
|
92
|
+
warnings: result.summary?.warnings ?? 0,
|
|
93
|
+
},
|
|
94
|
+
diagnostics,
|
|
95
|
+
...(result.error ? { error: result.error } : {}),
|
|
96
|
+
};
|
|
68
97
|
}
|
|
69
98
|
|
|
70
|
-
function
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
99
|
+
function formatDiagnosticLine(diagnostic) {
|
|
100
|
+
const target = diagnostic.location
|
|
101
|
+
|| (diagnostic.related && diagnostic.related.length > 0 ? diagnostic.related.join(' <> ') : '')
|
|
102
|
+
|| diagnostic.source
|
|
103
|
+
|| '';
|
|
104
|
+
|
|
105
|
+
const extra = [];
|
|
106
|
+
if (diagnostic.source) extra.push(`source=${diagnostic.source}`);
|
|
107
|
+
if (diagnostic.assetPath) extra.push(`assetPath=${diagnostic.assetPath}`);
|
|
108
|
+
if (diagnostic.metrics) {
|
|
109
|
+
extra.push(
|
|
110
|
+
Object.entries(diagnostic.metrics)
|
|
111
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
112
|
+
.join(' '),
|
|
113
|
+
);
|
|
74
114
|
}
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function parseCliArgs(args) {
|
|
79
|
-
const options = {
|
|
80
|
-
slidesDir: DEFAULT_SLIDES_DIR,
|
|
81
|
-
help: false,
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
for (let i = 0; i < args.length; i += 1) {
|
|
85
|
-
const arg = args[i];
|
|
86
|
-
|
|
87
|
-
if (arg === '-h' || arg === '--help') {
|
|
88
|
-
options.help = true;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (arg === '--slides-dir') {
|
|
93
|
-
options.slidesDir = readOptionValue(args, i, '--slides-dir');
|
|
94
|
-
i += 1;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (arg.startsWith('--slides-dir=')) {
|
|
99
|
-
options.slidesDir = arg.slice('--slides-dir='.length);
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
throw new Error(`Unknown option: ${arg}`);
|
|
115
|
+
if (diagnostic.bbox) {
|
|
116
|
+
extra.push(`bbox=${diagnostic.bbox.x},${diagnostic.bbox.y} ${diagnostic.bbox.width}x${diagnostic.bbox.height}`);
|
|
104
117
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
118
|
+
if (diagnostic.intersection) {
|
|
119
|
+
extra.push(
|
|
120
|
+
`intersection=${diagnostic.intersection.x},${diagnostic.intersection.y} ${diagnostic.intersection.width}x${diagnostic.intersection.height}`,
|
|
121
|
+
);
|
|
108
122
|
}
|
|
123
|
+
if (diagnostic.detail) extra.push(`detail=${diagnostic.detail}`);
|
|
109
124
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function findSlideFiles(slidesDir) {
|
|
115
|
-
const entries = await readdir(slidesDir, { withFileTypes: true });
|
|
116
|
-
return entries
|
|
117
|
-
.filter((entry) => entry.isFile() && SLIDE_FILE_PATTERN.test(entry.name))
|
|
118
|
-
.map((entry) => entry.name)
|
|
119
|
-
.sort(sortSlideFiles);
|
|
125
|
+
const targetSuffix = target ? ` ${target}` : '';
|
|
126
|
+
const extraSuffix = extra.length > 0 ? ` (${extra.join('; ')})` : '';
|
|
127
|
+
return `${diagnostic.slide}:${diagnostic.severity}[${diagnostic.code}]${targetSuffix} - ${diagnostic.message}${extraSuffix}`;
|
|
120
128
|
}
|
|
121
129
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
await page.goto(slideUrl, { waitUntil: 'load' });
|
|
127
|
-
await page.evaluate(async () => {
|
|
128
|
-
if (document.fonts?.ready) {
|
|
129
|
-
await document.fonts.ready;
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const inspection = await page.evaluate(
|
|
134
|
-
({ framePx, textSelector, tolerancePx }) => {
|
|
135
|
-
const skipTags = new Set(['SCRIPT', 'STYLE', 'META', 'LINK', 'HEAD', 'TITLE', 'NOSCRIPT']);
|
|
136
|
-
const critical = [];
|
|
137
|
-
const warning = [];
|
|
138
|
-
const seenOverlaps = new Set();
|
|
139
|
-
|
|
140
|
-
const round = (value) => Number(value.toFixed(2));
|
|
141
|
-
|
|
142
|
-
const normalizeRect = (rect) => {
|
|
143
|
-
const left = rect.left ?? rect.x ?? 0;
|
|
144
|
-
const top = rect.top ?? rect.y ?? 0;
|
|
145
|
-
const width = rect.width ?? (rect.right - left) ?? 0;
|
|
146
|
-
const height = rect.height ?? (rect.bottom - top) ?? 0;
|
|
147
|
-
const right = rect.right ?? (left + width);
|
|
148
|
-
const bottom = rect.bottom ?? (top + height);
|
|
149
|
-
return {
|
|
150
|
-
x: round(left),
|
|
151
|
-
y: round(top),
|
|
152
|
-
width: round(width),
|
|
153
|
-
height: round(height),
|
|
154
|
-
left: round(left),
|
|
155
|
-
top: round(top),
|
|
156
|
-
right: round(right),
|
|
157
|
-
bottom: round(bottom)
|
|
158
|
-
};
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const elementPath = (element) => {
|
|
162
|
-
if (!element || element.nodeType !== Node.ELEMENT_NODE) return '';
|
|
163
|
-
if (element === document.body) return 'body';
|
|
164
|
-
|
|
165
|
-
const parts = [];
|
|
166
|
-
let current = element;
|
|
167
|
-
|
|
168
|
-
while (current && current.nodeType === Node.ELEMENT_NODE && current !== document.body) {
|
|
169
|
-
let part = current.tagName.toLowerCase();
|
|
170
|
-
if (current.id) {
|
|
171
|
-
part += `#${current.id}`;
|
|
172
|
-
parts.unshift(part);
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const classNames = Array.from(current.classList).slice(0, 2);
|
|
177
|
-
if (classNames.length > 0) {
|
|
178
|
-
part += `.${classNames.join('.')}`;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (current.parentElement) {
|
|
182
|
-
const siblingsOfSameTag = Array.from(current.parentElement.children)
|
|
183
|
-
.filter((sibling) => sibling.tagName === current.tagName);
|
|
184
|
-
if (siblingsOfSameTag.length > 1) {
|
|
185
|
-
const index = siblingsOfSameTag.indexOf(current);
|
|
186
|
-
part += `:nth-of-type(${index + 1})`;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
parts.unshift(part);
|
|
191
|
-
current = current.parentElement;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return `body > ${parts.join(' > ')}`;
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const isVisible = (element) => {
|
|
198
|
-
if (skipTags.has(element.tagName)) return false;
|
|
199
|
-
const style = window.getComputedStyle(element);
|
|
200
|
-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) {
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
const rect = element.getBoundingClientRect();
|
|
204
|
-
return rect.width > 0 && rect.height > 0;
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
const bodyRect = document.body.getBoundingClientRect();
|
|
208
|
-
const frameRect = {
|
|
209
|
-
left: bodyRect.left,
|
|
210
|
-
top: bodyRect.top,
|
|
211
|
-
right: bodyRect.left + (bodyRect.width || framePx.width),
|
|
212
|
-
bottom: bodyRect.top + (bodyRect.height || framePx.height),
|
|
213
|
-
width: bodyRect.width || framePx.width,
|
|
214
|
-
height: bodyRect.height || framePx.height
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const allVisibleElements = Array.from(document.body.querySelectorAll('*')).filter(isVisible);
|
|
218
|
-
const visibleSet = new Set(allVisibleElements);
|
|
219
|
-
|
|
220
|
-
for (const element of allVisibleElements) {
|
|
221
|
-
const rect = element.getBoundingClientRect();
|
|
222
|
-
const outsideFrame = (
|
|
223
|
-
rect.left < frameRect.left - tolerancePx ||
|
|
224
|
-
rect.top < frameRect.top - tolerancePx ||
|
|
225
|
-
rect.right > frameRect.right + tolerancePx ||
|
|
226
|
-
rect.bottom > frameRect.bottom + tolerancePx
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
if (outsideFrame) {
|
|
230
|
-
critical.push({
|
|
231
|
-
code: 'overflow-outside-frame',
|
|
232
|
-
message: 'Element exceeds the 720pt x 405pt slide frame.',
|
|
233
|
-
element: elementPath(element),
|
|
234
|
-
bbox: normalizeRect(rect),
|
|
235
|
-
frame: normalizeRect(frameRect)
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const textElements = Array.from(document.querySelectorAll(textSelector));
|
|
241
|
-
for (const element of textElements) {
|
|
242
|
-
if (!isVisible(element)) continue;
|
|
243
|
-
const content = (element.textContent || '').trim();
|
|
244
|
-
if (!content) continue;
|
|
245
|
-
|
|
246
|
-
const clipped = element.scrollHeight > element.clientHeight;
|
|
247
|
-
if (!clipped) continue;
|
|
248
|
-
|
|
249
|
-
critical.push({
|
|
250
|
-
code: 'text-clipped',
|
|
251
|
-
message: 'Text element is clipped because scrollHeight is larger than clientHeight.',
|
|
252
|
-
element: elementPath(element),
|
|
253
|
-
metrics: {
|
|
254
|
-
scrollHeight: element.scrollHeight,
|
|
255
|
-
clientHeight: element.clientHeight
|
|
256
|
-
},
|
|
257
|
-
bbox: normalizeRect(element.getBoundingClientRect())
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const parents = [document.body, ...allVisibleElements];
|
|
262
|
-
for (const parent of parents) {
|
|
263
|
-
const children = Array.from(parent.children).filter((child) => visibleSet.has(child));
|
|
264
|
-
if (children.length < 2) continue;
|
|
265
|
-
|
|
266
|
-
for (let i = 0; i < children.length; i += 1) {
|
|
267
|
-
for (let j = i + 1; j < children.length; j += 1) {
|
|
268
|
-
const first = children[i];
|
|
269
|
-
const second = children[j];
|
|
270
|
-
|
|
271
|
-
const rectA = first.getBoundingClientRect();
|
|
272
|
-
const rectB = second.getBoundingClientRect();
|
|
273
|
-
|
|
274
|
-
const overlapWidth = Math.min(rectA.right, rectB.right) - Math.max(rectA.left, rectB.left);
|
|
275
|
-
const overlapHeight = Math.min(rectA.bottom, rectB.bottom) - Math.max(rectA.top, rectB.top);
|
|
276
|
-
|
|
277
|
-
if (overlapWidth <= tolerancePx || overlapHeight <= tolerancePx) continue;
|
|
278
|
-
|
|
279
|
-
const firstPath = elementPath(first);
|
|
280
|
-
const secondPath = elementPath(second);
|
|
281
|
-
const overlapKey = [firstPath, secondPath].sort().join('::');
|
|
282
|
-
|
|
283
|
-
if (seenOverlaps.has(overlapKey)) continue;
|
|
284
|
-
seenOverlaps.add(overlapKey);
|
|
130
|
+
export function formatValidationResult(result, format = DEFAULT_VALIDATE_FORMAT) {
|
|
131
|
+
if (format === 'json-full') {
|
|
132
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
133
|
+
}
|
|
285
134
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
elements: [firstPath, secondPath],
|
|
291
|
-
intersection: {
|
|
292
|
-
x: round(Math.max(rectA.left, rectB.left)),
|
|
293
|
-
y: round(Math.max(rectA.top, rectB.top)),
|
|
294
|
-
width: round(overlapWidth),
|
|
295
|
-
height: round(overlapHeight)
|
|
296
|
-
},
|
|
297
|
-
boxes: [normalizeRect(rectA), normalizeRect(rectB)]
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
135
|
+
const flattened = flattenValidationResult(result);
|
|
136
|
+
if (format === 'json') {
|
|
137
|
+
return `${JSON.stringify(flattened, null, 2)}\n`;
|
|
138
|
+
}
|
|
302
139
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
{
|
|
309
|
-
framePx: FRAME_PX,
|
|
310
|
-
textSelector: TEXT_SELECTOR,
|
|
311
|
-
tolerancePx: TOLERANCE_PX
|
|
312
|
-
}
|
|
140
|
+
const lines = flattened.diagnostics.map(formatDiagnosticLine);
|
|
141
|
+
if (flattened.error) {
|
|
142
|
+
lines.push(`validation:error[validation-failed] - ${flattened.error}`);
|
|
143
|
+
}
|
|
144
|
+
lines.push(
|
|
145
|
+
`summary: ${flattened.summary.totalSlides} slide(s) checked, ${flattened.summary.passedSlides} passed, ${flattened.summary.failedSlides} failed, ${flattened.summary.errors} error(s), ${flattened.summary.warnings} warning(s)`,
|
|
313
146
|
);
|
|
314
|
-
|
|
315
|
-
const summary = {
|
|
316
|
-
criticalCount: inspection.critical.length,
|
|
317
|
-
warningCount: inspection.warning.length
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
return {
|
|
321
|
-
slide: fileName,
|
|
322
|
-
status: summary.criticalCount > 0 ? 'fail' : 'pass',
|
|
323
|
-
critical: inspection.critical,
|
|
324
|
-
warning: inspection.warning,
|
|
325
|
-
summary
|
|
326
|
-
};
|
|
147
|
+
return `${lines.join('\n')}\n`;
|
|
327
148
|
}
|
|
328
149
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
150
|
+
function peekValidateFormat(args = []) {
|
|
151
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
152
|
+
const arg = args[i];
|
|
153
|
+
if (arg === '--format') {
|
|
154
|
+
return args[i + 1] || DEFAULT_VALIDATE_FORMAT;
|
|
155
|
+
}
|
|
156
|
+
if (arg.startsWith('--format=')) {
|
|
157
|
+
return arg.slice('--format='.length) || DEFAULT_VALIDATE_FORMAT;
|
|
158
|
+
}
|
|
334
159
|
}
|
|
160
|
+
return DEFAULT_VALIDATE_FORMAT;
|
|
161
|
+
}
|
|
335
162
|
|
|
336
|
-
|
|
337
|
-
const slideFiles = await findSlideFiles(slidesDir);
|
|
163
|
+
export async function validateSlides(slidesDir, { selectedSlides = [] } = {}) {
|
|
164
|
+
const slideFiles = selectSlideFiles(await findSlideFiles(slidesDir), selectedSlides, slidesDir);
|
|
338
165
|
if (slideFiles.length === 0) {
|
|
339
166
|
throw new Error(`No slide-*.html files found in: ${slidesDir}`);
|
|
340
167
|
}
|
|
@@ -343,74 +170,35 @@ async function main() {
|
|
|
343
170
|
const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
|
|
344
171
|
const page = await context.newPage();
|
|
345
172
|
|
|
346
|
-
const slides = [];
|
|
347
173
|
try {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const result = await inspectSlide(page, slideFile, slidesDir);
|
|
351
|
-
slides.push(result);
|
|
352
|
-
} catch (error) {
|
|
353
|
-
slides.push({
|
|
354
|
-
slide: slideFile,
|
|
355
|
-
status: 'fail',
|
|
356
|
-
critical: [
|
|
357
|
-
buildIssue(
|
|
358
|
-
'slide-validation-error',
|
|
359
|
-
'Slide validation failed before checks could complete.',
|
|
360
|
-
{ detail: error instanceof Error ? error.message : String(error) }
|
|
361
|
-
)
|
|
362
|
-
],
|
|
363
|
-
warning: [],
|
|
364
|
-
summary: {
|
|
365
|
-
criticalCount: 1,
|
|
366
|
-
warningCount: 0
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
}
|
|
174
|
+
const slides = await scanSlides(page, slidesDir, slideFiles);
|
|
175
|
+
return createValidationResult(slides);
|
|
371
176
|
} finally {
|
|
372
177
|
await browser.close();
|
|
373
178
|
}
|
|
179
|
+
}
|
|
374
180
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
heightPx: FRAME_PX.height
|
|
382
|
-
},
|
|
383
|
-
slides,
|
|
384
|
-
summary: summarizeSlides(slides)
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
181
|
+
export async function main(args = process.argv.slice(2)) {
|
|
182
|
+
const options = parseValidateCliArgs(args);
|
|
183
|
+
if (options.help) {
|
|
184
|
+
process.stdout.write(`${getValidateUsage()}\n`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
388
187
|
|
|
188
|
+
const slidesDir = resolve(process.cwd(), options.slidesDir);
|
|
189
|
+
const result = await validateSlides(slidesDir, { selectedSlides: options.slides });
|
|
190
|
+
process.stdout.write(formatValidationResult(result, options.format));
|
|
389
191
|
if (result.summary.failedSlides > 0) {
|
|
390
192
|
process.exitCode = 1;
|
|
391
193
|
}
|
|
392
194
|
}
|
|
393
195
|
|
|
394
|
-
|
|
395
|
-
const failure = {
|
|
396
|
-
generatedAt: new Date().toISOString(),
|
|
397
|
-
frame: {
|
|
398
|
-
widthPt: FRAME_PT.width,
|
|
399
|
-
heightPt: FRAME_PT.height,
|
|
400
|
-
widthPx: FRAME_PX.width,
|
|
401
|
-
heightPx: FRAME_PX.height
|
|
402
|
-
},
|
|
403
|
-
slides: [],
|
|
404
|
-
summary: {
|
|
405
|
-
totalSlides: 0,
|
|
406
|
-
passedSlides: 0,
|
|
407
|
-
failedSlides: 0,
|
|
408
|
-
criticalIssues: 1,
|
|
409
|
-
warnings: 0
|
|
410
|
-
},
|
|
411
|
-
error: error instanceof Error ? error.message : String(error)
|
|
412
|
-
};
|
|
196
|
+
const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
413
197
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
198
|
+
if (isMain) {
|
|
199
|
+
main().catch((error) => {
|
|
200
|
+
const failure = createValidationFailure(error);
|
|
201
|
+
process.stdout.write(formatValidationResult(failure, peekValidateFormat(process.argv.slice(2))));
|
|
202
|
+
process.exit(1);
|
|
203
|
+
});
|
|
204
|
+
}
|