Skip to content

Commit 7783522

Browse files
committed
Fix schema validation, dark mode guard, and path traversal check
Schema: add if/then conditional rules to interactionStep requiring selector for click/hover/wait/scroll and script for eval actions. Capture: add null guard for defaults.dark.ready in switchToDark and switchToLight with descriptive error messages. Serve: fix path traversal prefix check to prevent sibling directory matching (e.g., C:\foo matching C:\foobar). Docs: remove leftover "pre-rendered site path" reference in SKILL.md, fix "Three source types" → "Two source types" in manifest-schema.md.
1 parent 494ab42 commit 7783522

5 files changed

Lines changed: 21 additions & 4 deletions

File tree

.claude/skills/screenshot/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Gather these parameters (ask about unknowns, infer from context when obvious):
4444
| Parameter | Values / Notes |
4545
|-----------|---------------|
4646
| Source type | `url` (live site), `example` (Quarto project — render then serve) |
47-
| Source detail | URL, example project path (create minimal project if needed), or pre-rendered site path |
47+
| Source detail | URL or example project path (create minimal project if needed) |
4848
| Viewport | navbar=1440x400, sidebar=992x600, about=1200x900, full page=1440x900 |
4949
| Zoom | Default 1.0; use 1.15 for about pages or excess internal padding |
5050
| Element | CSS selector if capturing a specific element; omit for full viewport |

.claude/skills/screenshot/manifest-schema.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Each entry in the `screenshots` array defines one screenshot to capture.
6363

6464
### `source`
6565

66-
Three source types:
66+
Two source types:
6767

6868
**`example`** — a Quarto project that capture.js will **render then serve**:
6969
```json

tools/screenshots/capture.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,9 @@ function darkOutputPath(outputPath) {
275275
// Switch to dark mode via JS (avoids toggle visibility issues at narrow viewports)
276276
async function switchToDark(page) {
277277
const darkConfig = manifest.defaults.dark;
278+
if (!darkConfig?.ready) {
279+
throw new Error('defaults.dark.ready must be configured when screenshots use "dark": true');
280+
}
278281
await page.evaluate(() => {
279282
if (typeof window.quartoToggleColorScheme !== 'function') {
280283
throw new Error('quartoToggleColorScheme not found — page may not support dark mode');
@@ -290,6 +293,9 @@ async function switchToDark(page) {
290293
// Switch back to light mode
291294
async function switchToLight(page) {
292295
const darkConfig = manifest.defaults.dark;
296+
if (!darkConfig?.ready) {
297+
throw new Error('defaults.dark.ready must be configured when switching back to light');
298+
}
293299
await page.evaluate(() => {
294300
if (typeof window.quartoToggleColorScheme !== 'function') {
295301
throw new Error('quartoToggleColorScheme not found — page may not support dark mode');

tools/screenshots/manifest-schema.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,17 @@
6767
"type": "string",
6868
"description": "JavaScript for eval action."
6969
}
70-
}
70+
},
71+
"allOf": [
72+
{
73+
"if": { "properties": { "action": { "enum": ["click", "hover", "wait", "scroll"] } } },
74+
"then": { "required": ["selector"] }
75+
},
76+
{
77+
"if": { "properties": { "action": { "const": "eval" } } },
78+
"then": { "required": ["script"] }
79+
}
80+
]
7181
},
7282
"spotlightConfig": {
7383
"type": "object",

tools/screenshots/scripts/serve.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ const server = http.createServer((req, res) => {
2323
const pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname);
2424
let filePath = path.join(dir, pathname);
2525
const resolved = path.resolve(filePath);
26-
if (!resolved.startsWith(dir + path.sep) && resolved !== dir) {
26+
const dirPrefix = dir.endsWith(path.sep) ? dir : dir + path.sep;
27+
if (!resolved.startsWith(dirPrefix) && resolved !== dir) {
2728
res.writeHead(403);
2829
res.end('Forbidden');
2930
return;

0 commit comments

Comments
 (0)