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);
});