Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
File size: 5,249 Bytes
17d0a5b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | #!/usr/bin/env node
/**
* General-purpose screenshot tool for the blog article.
*
* Takes a screenshot of any section or element in the rendered blog post.
* Useful for agent debugging: visually verify how content renders.
*
* Usage:
* node scripts/screenshot.mjs [options]
*
* Options:
* --target <selector> CSS selector or #anchor to screenshot (default: viewport)
* --output <path> Output file path (default: ../../assets/screenshot.png)
* --width <px> Viewport width (default: 1400)
* --height <px> Viewport height (default: 900)
* --full-page Capture the full scrollable page
* --padding <px> Extra padding around the target element (default: 40)
* --url <url> Page URL (default: http://localhost:4321/)
* --wait <ms> Extra wait time after navigation in ms (default: 2000)
* --dark Use dark theme
*
* Examples:
* # Screenshot the full viewport (above the fold)
* node scripts/screenshot.mjs
*
* # Screenshot a specific section by heading anchor
* node scripts/screenshot.mjs --target "#experiments"
*
* # Screenshot a specific figure by its id
* node scripts/screenshot.mjs --target "#baselines-comparison"
*
* # Screenshot a CSS selector with extra padding
* node scripts/screenshot.mjs --target ".mermaid" --padding 80
*
* # Full-page screenshot in dark mode
* node scripts/screenshot.mjs --full-page --dark
*
* # Custom output path
* node scripts/screenshot.mjs --target "#infrastructure" --output ./infra-shot.png
*/
import { chromium } from 'playwright';
import { fileURLToPath } from 'url';
import { dirname, join, isAbsolute } from 'path';
import { parseArgs } from 'util';
const __dirname = dirname(fileURLToPath(import.meta.url));
const { values: args } = parseArgs({
options: {
target: { type: 'string', default: '' },
output: { type: 'string', default: '' },
width: { type: 'string', default: '1400' },
height: { type: 'string', default: '900' },
'full-page': { type: 'boolean', default: false },
padding: { type: 'string', default: '40' },
url: { type: 'string', default: 'http://localhost:4321/' },
wait: { type: 'string', default: '2000' },
dark: { type: 'boolean', default: false },
},
});
const target = args.target;
const width = parseInt(args.width, 10);
const height = parseInt(args.height, 10);
const fullPage = args['full-page'];
const padding = parseInt(args.padding, 10);
const url = args.url;
const waitMs = parseInt(args.wait, 10);
const dark = args.dark;
// Build output path: default uses target name or "viewport"
const defaultName = target
? `screenshot-${target.replace(/^#/, '').replace(/[^a-zA-Z0-9_-]/g, '_')}.png`
: 'screenshot.png';
const output = args.output
? (isAbsolute(args.output) ? args.output : join(process.cwd(), args.output))
: join(__dirname, '..', '..', 'assets', defaultName);
async function main() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setViewportSize({ width, height });
// Navigate
await page.goto(url, { waitUntil: 'domcontentloaded' });
// Apply dark theme if requested
if (dark) {
await page.evaluate(() => {
document.documentElement.setAttribute('data-theme', 'dark');
});
}
// Wait for async content (mermaid, embeds) to render
await page.waitForTimeout(waitMs);
if (fullPage) {
await page.screenshot({ path: output, fullPage: true });
console.log(`Full-page screenshot saved to: ${output}`);
await browser.close();
return;
}
if (!target) {
// No target: screenshot the current viewport
await page.screenshot({ path: output, fullPage: false });
console.log(`Viewport screenshot saved to: ${output}`);
await browser.close();
return;
}
// Resolve target: if it starts with #, try clicking a TOC link first for smooth scroll
if (target.startsWith('#')) {
const tocLink = page.locator(`a[href="${target}"]`).first();
if (await tocLink.count() > 0) {
await tocLink.click();
await page.waitForTimeout(500);
} else {
// Fallback: scroll the element into view directly
await page.evaluate((sel) => {
const el = document.querySelector(sel);
el?.scrollIntoView({ behavior: 'instant', block: 'start' });
}, target);
await page.waitForTimeout(300);
}
}
// Find the target element
const locator = page.locator(target).first();
const box = await locator.boundingBox();
if (box) {
const clip = {
x: Math.max(0, box.x - padding),
y: Math.max(0, box.y - padding),
width: Math.min(width, box.width + padding * 2),
height: box.height + padding * 2,
};
await page.screenshot({ path: output, clip, fullPage: false });
console.log(`Screenshot of "${target}" saved to: ${output}`);
} else {
console.warn(`Target "${target}" not found, falling back to viewport screenshot.`);
await page.screenshot({ path: output, fullPage: false });
console.log(`Viewport screenshot saved to: ${output}`);
}
await browser.close();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
|