diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 38052a3b..f046dc04 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,16 +48,21 @@ jobs: run: | npm run build - - name: Build extension (if needed) + - name: Copy extension to dist for tests if: runner.os != 'Windows' shell: bash run: | - if [ -d "../sentience-chrome" ]; then - cd ../sentience-chrome && ./build.sh || echo "Extension build skipped (may not be available in CI)" + # Extension should be synced to src/extension/ by sync-extension workflow + # Copy it to dist/extension/ so tests can find it (browser.ts looks relative to __dirname) + if [ -d "src/extension" ] && [ -f "src/extension/manifest.json" ]; then + echo "Copying extension from src/extension to dist/extension..." + cp -r src/extension dist/ + echo "Extension files in dist/extension:" + ls -la dist/extension/ else - echo "Extension directory not found, skipping build" + echo "Warning: src/extension not found, tests may fail" fi - + - name: Run tests run: | npm test diff --git a/examples/show-grid-examples.ts b/examples/show-grid-examples.ts new file mode 100644 index 00000000..49203a4f --- /dev/null +++ b/examples/show-grid-examples.ts @@ -0,0 +1,134 @@ +/** + * Example: Grid Overlay Visualization + * + * Demonstrates how to use the grid overlay feature to visualize detected grids + * on a webpage, including highlighting specific grids and identifying the dominant group. + */ + +import { SentienceBrowser, snapshot, getGridBounds } from '../src/index'; + +async function main() { + // Get API key from environment variable (optional - uses free tier if not set) + const apiKey = process.env.SENTIENCE_API_KEY as string | undefined; + + const browser = new SentienceBrowser(apiKey, undefined, false); + + try { + await browser.start(); + + // Navigate to a page with grid layouts (e.g., product listings, article feeds) + const page = browser.getPage(); + if (!page) { + throw new Error('Failed to get page after browser.start()'); + } + await page.goto('https://google.com', { + waitUntil: 'domcontentloaded', + }); + await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait for page to fully load + + console.log('='.repeat(60)); + console.log('Example 1: Show all detected grids'); + console.log('='.repeat(60)); + // Show all grids (all in purple) + // Use local extension mode (use_api: false) to ensure layout data is computed + const snap = await snapshot(browser, { show_grid: true, use_api: false }); + console.log(`✅ Found ${snap.elements.length} elements`); + console.log(' Purple borders appear around all detected grids for 5 seconds'); + await new Promise((resolve) => setTimeout(resolve, 6000)); // Wait to see the overlay + + console.log('\n' + '='.repeat(60)); + console.log('Example 2: Highlight a specific grid in red'); + console.log('='.repeat(60)); + // Get grid information first + const grids = getGridBounds(snap); + if (grids.length > 0) { + console.log(`✅ Found ${grids.length} grids:`); + for (const grid of grids) { + console.log( + ` Grid ${grid.grid_id}: ${grid.item_count} items, ` + + `${grid.row_count}x${grid.col_count} rows/cols, ` + + `label: ${grid.label || 'none'}` + ); + } + + // Highlight the first grid in red + if (grids.length > 0) { + const targetGridId = grids[0].grid_id; + console.log(`\n Highlighting Grid ${targetGridId} in red...`); + await snapshot(browser, { + show_grid: true, + grid_id: targetGridId, // This grid will be highlighted in red + use_api: false, // Use local extension mode + }); + await new Promise((resolve) => setTimeout(resolve, 6000)); // Wait to see the overlay + } + } else { + console.log(' ⚠️ No grids detected on this page'); + } + + console.log('\n' + '='.repeat(60)); + console.log('Example 3: Highlight the dominant group'); + console.log('='.repeat(60)); + // Find and highlight the dominant grid + const allGrids = getGridBounds(snap); + const dominantGrid = allGrids.find((g) => g.is_dominant); + + if (dominantGrid) { + console.log(`✅ Dominant group detected: Grid ${dominantGrid.grid_id}`); + console.log(` Label: ${dominantGrid.label || 'none'}`); + console.log(` Items: ${dominantGrid.item_count}`); + console.log(` Size: ${dominantGrid.row_count}x${dominantGrid.col_count}`); + console.log(`\n Highlighting dominant grid in red...`); + await snapshot(browser, { + show_grid: true, + grid_id: dominantGrid.grid_id, // Highlight dominant grid in red + use_api: false, // Use local extension mode + }); + await new Promise((resolve) => setTimeout(resolve, 6000)); // Wait to see the overlay + } else { + console.log(' ⚠️ No dominant group detected'); + } + + console.log('\n' + '='.repeat(60)); + console.log('Example 4: Combine element overlay and grid overlay'); + console.log('='.repeat(60)); + // Show both element borders and grid borders simultaneously + await snapshot(browser, { + show_overlay: true, // Show element borders (green/blue/red) + show_grid: true, // Show grid borders (purple/orange/red) + use_api: false, // Use local extension mode + }); + console.log('✅ Both overlays are now visible:'); + console.log(' - Element borders: Green (regular), Blue (primary), Red (target)'); + console.log(' - Grid borders: Purple (regular), Orange (dominant), Red (target)'); + await new Promise((resolve) => setTimeout(resolve, 6000)); // Wait to see the overlay + + console.log('\n' + '='.repeat(60)); + console.log('Example 5: Grid information analysis'); + console.log('='.repeat(60)); + // Analyze grid structure + const allGridsForAnalysis = getGridBounds(snap); + console.log(`✅ Grid Analysis:`); + for (const grid of allGridsForAnalysis) { + const dominantIndicator = grid.is_dominant ? '⭐ DOMINANT' : ''; + console.log(`\n Grid ${grid.grid_id} ${dominantIndicator}:`); + console.log(` Label: ${grid.label || 'none'}`); + console.log(` Items: ${grid.item_count}`); + console.log(` Size: ${grid.row_count} rows × ${grid.col_count} cols`); + console.log( + ` BBox: (${grid.bbox.x.toFixed(0)}, ${grid.bbox.y.toFixed(0)}) ` + + `${grid.bbox.width.toFixed(0)}×${grid.bbox.height.toFixed(0)}` + ); + console.log(` Confidence: ${grid.confidence}`); + } + + console.log('\n✅ All examples completed!'); + } catch (e: any) { + console.error(`❌ Error: ${e.message}`); + console.error(e.stack); + } finally { + await browser.close(); + } +} + +main().catch(console.error); diff --git a/package-lock.json b/package-lock.json index 4aa0fb61..6180bdda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,17 @@ { "name": "sentienceapi", - "version": "0.92.3", + "version": "0.94.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sentienceapi", - "version": "0.92.3", + "version": "0.94.0", "license": "(MIT OR Apache-2.0)", "dependencies": { + "canvas": "^3.2.1", "playwright": "^1.40.0", + "sharp": "^0.34.5", "turndown": "^7.2.2", "uuid": "^9.0.0", "zod": "^3.22.0" @@ -600,6 +602,16 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -813,6 +825,471 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1988,6 +2465,26 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.9.11", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", @@ -1998,6 +2495,17 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2079,6 +2587,30 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -2148,6 +2680,20 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.1.tgz", + "integrity": "sha512-ej1sPFR5+0YWtaVp6S1N1FVz69TQCqmrkGeRvQxZeAB1nAIcjNTHVwrZtYtWFFBmQsF40/uDLehsW5KuYC99mg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2175,6 +2721,12 @@ "node": ">=10" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -2436,6 +2988,21 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", @@ -2451,6 +3018,15 @@ } } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2478,6 +3054,15 @@ "node": ">=0.4.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2560,6 +3145,15 @@ "dev": true, "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -3015,6 +3609,15 @@ "node": ">= 0.8.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", @@ -3200,6 +3803,12 @@ "node": ">= 14" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3327,6 +3936,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3512,6 +4127,26 @@ "url": "https://github.com/sponsors/typicode" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -3595,7 +4230,12 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/is-arrayish": { @@ -5017,6 +5657,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5034,12 +5686,17 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5060,6 +5717,12 @@ "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -5074,6 +5737,36 @@ "dev": true, "license": "MIT" }, + "node_modules/node-abi": { + "version": "3.85.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", + "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -5157,7 +5850,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -5469,6 +6161,32 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5557,6 +6275,16 @@ "license": "MIT", "optional": true }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5584,6 +6312,30 @@ ], "license": "MIT" }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -5591,6 +6343,20 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5726,8 +6492,7 @@ "url": "https://feross.org/support" } ], - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", @@ -5739,6 +6504,62 @@ "semver": "bin/semver.js" } }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5769,6 +6590,51 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -5873,6 +6739,15 @@ "node": ">=10" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -6000,6 +6875,34 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -6226,6 +7129,25 @@ } } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/turndown": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz", @@ -6371,6 +7293,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -6499,7 +7427,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { diff --git a/package.json b/package.json index 17b21b4f..fdabb912 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,9 @@ "sentience": "./dist/cli.js" }, "dependencies": { + "canvas": "^3.2.1", "playwright": "^1.40.0", + "sharp": "^0.34.5", "turndown": "^7.2.2", "uuid": "^9.0.0", "zod": "^3.22.0" diff --git a/src/backends/snapshot.ts b/src/backends/snapshot.ts index d5dccb5f..33cf7e53 100644 --- a/src/backends/snapshot.ts +++ b/src/backends/snapshot.ts @@ -98,6 +98,10 @@ export interface SnapshotOptions { filter?: SnapshotFilter; /** Show visual overlay on page */ showOverlay?: boolean; + /** Show visual overlay highlighting detected grids */ + showGrid?: boolean; + /** Optional grid ID to show specific grid (only used if showGrid=true) */ + gridId?: number | null; /** Use server-side API (Pro/Enterprise tier) */ useApi?: boolean; /** API key for server-side processing */ @@ -368,6 +372,26 @@ async function snapshotViaExtension( } } + // Show grid overlay if requested + if (options.showGrid) { + const { getGridBounds } = await import('../utils/grid-utils'); + // Get all grids (don't filter by gridId here - we want to show all but highlight the target) + const grids = getGridBounds(result, undefined); + if (grids.length > 0) { + // Pass gridId as targetGridId to highlight it in red + const targetGridId = options.gridId ?? null; + await backend.eval(` + (() => { + if (window.sentience && window.sentience.showGrid) { + window.sentience.showGrid(${JSON.stringify(grids)}, ${targetGridId !== null ? targetGridId : 'null'}); + } else { + console.warn('[SDK] showGrid not available in extension'); + } + })() + `); + } + } + return result; } diff --git a/src/index.ts b/src/index.ts index 68713998..74eb3c36 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ export { showOverlay, clearOverlay } from './overlay'; export { findTextRect } from './textSearch'; export * from './types'; export { saveStorageState } from './utils'; +export { getGridBounds } from './utils/grid-utils'; // Agent Layer (v0.2.0+) export { diff --git a/src/snapshot.ts b/src/snapshot.ts index 6a24d6b7..053852d1 100644 --- a/src/snapshot.ts +++ b/src/snapshot.ts @@ -25,6 +25,8 @@ export interface SnapshotOptions { trace_path?: string; // Path to save trace file (default: "trace_{timestamp}.json") goal?: string; // Optional goal/task description for the snapshot show_overlay?: boolean; // Show visual overlay highlighting elements in browser + show_grid?: boolean; // Show visual overlay highlighting detected grids + grid_id?: number | null; // Optional grid ID to show specific grid (only used if show_grid=true) } /** @@ -130,6 +132,13 @@ async function snapshotViaExtension( _saveTraceToFile(result.raw_elements, options.trace_path); } + // Basic validation + if (result.status !== 'success' && result.status !== 'error') { + throw new Error(`Invalid snapshot status: ${result.status}`); + } + + const snapshot = result as Snapshot; + // Show visual overlay if requested if (options.show_overlay && result.raw_elements) { await BrowserEvaluator.evaluate( @@ -143,12 +152,45 @@ async function snapshotViaExtension( ); } - // Basic validation - if (result.status !== 'success' && result.status !== 'error') { - throw new Error(`Invalid snapshot status: ${result.status}`); + // Show grid overlay if requested + if (options.show_grid) { + const { getGridBounds } = await import('./utils/grid-utils'); + // Get all grids (don't filter by grid_id here - we want to show all but highlight the target) + const grids = getGridBounds(snapshot, undefined); + + // Debug: Check if elements have layout data + const elementsWithLayout = snapshot.elements.filter(e => e.layout?.grid_id != null).length; + if (grids.length === 0 && elementsWithLayout === 0) { + console.warn( + '[SDK] No grids detected. Elements may not have layout data. ' + + 'Ensure you are using use_api: false or that the API returns layout data.' + ); + } + + if (grids.length > 0) { + // Pass grid_id as targetGridId to highlight it in red + const targetGridId = options.grid_id ?? null; + await BrowserEvaluator.evaluate( + page, + (args: any) => { + if ((window as any).sentience && (window as any).sentience.showGrid) { + (window as any).sentience.showGrid(args.grids, args.targetGridId); + } else { + console.warn( + '[SDK] showGrid not available in extension. Make sure the extension is loaded.' + ); + } + }, + { grids, targetGridId } + ); + } else { + console.warn( + `[SDK] No grids to display. Found ${elementsWithLayout} elements with layout data out of ${snapshot.elements.length} total elements.` + ); + } } - return result as Snapshot; + return snapshot; } async function snapshotViaApi( @@ -280,6 +322,28 @@ async function snapshotViaApi( ); } + // Show grid overlay if requested + if (options.show_grid) { + const { getGridBounds } = await import('./utils/grid-utils'); + // Get all grids (don't filter by grid_id here - we want to show all but highlight the target) + const grids = getGridBounds(snapshotData, undefined); + if (grids.length > 0 && page) { + // Pass grid_id as targetGridId to highlight it in red + const targetGridId = options.grid_id ?? null; + await BrowserEvaluator.evaluate( + page, + (args: any) => { + if ((window as any).sentience && (window as any).sentience.showGrid) { + (window as any).sentience.showGrid(args.grids, args.targetGridId); + } else { + console.warn('[SDK] showGrid not available in extension'); + } + }, + { grids, targetGridId } + ); + } + } + return snapshotData; } catch (e: any) { throw new Error(`API request failed: ${e.message}`); diff --git a/src/types.ts b/src/types.ts index d9f19b79..0ce8e589 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,6 +54,61 @@ export interface Element { // This field is computed by the gateway so downstream consumers don't need to // implement fuzzy matching logic themselves. in_dominant_group?: boolean; + + // Layout-derived metadata (internal-only in v0, not exposed in API responses) + // Per ChatGPT feedback: explicitly optional to prevent users assuming layout is always present + // Note: This field is marked with skip_serializing_if in Rust, so it won't appear in API responses + layout?: LayoutHints; +} + +export interface GridPosition { + /** 0-based row index */ + row_index: number; + /** 0-based column index */ + col_index: number; + /** ID of the row cluster (for distinguishing separate grids) */ + cluster_id: number; +} + +export interface LayoutHints { + /** Grid ID (maps to GridInfo.grid_id) - distinguishes multiple grids on same page */ + /** Per feedback: Add grid_id to distinguish main feed + sidebar lists + nav links */ + grid_id?: number | null; + /** Grid position within the grid (row_index, col_index) */ + grid_pos?: GridPosition | null; + /** Inferred parent index in the original elements slice */ + parent_index?: number | null; + /** Indices of children elements (optional to avoid payload bloat - container elements can have hundreds) */ + /** Per feedback: Make optional/capped to prevent serializing large arrays */ + children_indices?: number[] | null; + /** Confidence score for grid position assignment (0.0-1.0) */ + grid_confidence: number; + /** Confidence score for parent-child containment (0.0-1.0) */ + parent_confidence: number; + /** Optional: Page region (header/nav/main/aside/footer) - killer signal for ordinality + dominant group */ + /** Per feedback: Optional but very useful for region detection */ + region?: 'header' | 'nav' | 'main' | 'aside' | 'footer' | null; + /** Confidence score for region assignment (0.0-1.0) */ + region_confidence: number; +} + +export interface GridInfo { + /** The grid ID (matches grid_id in LayoutHints) */ + grid_id: number; + /** Bounding box: x, y, width, height (document coordinates) */ + bbox: BBox; + /** Number of rows in the grid */ + row_count: number; + /** Number of columns in the grid */ + col_count: number; + /** Total number of items in the grid */ + item_count: number; + /** Confidence score (currently 1.0) */ + confidence: number; + /** Optional inferred label (e.g., "product_grid", "search_results", "navigation") - best-effort heuristic, may be null */ + label?: string | null; + /** Whether this grid is the dominant group (main content area) */ + is_dominant?: boolean; } export interface Snapshot { diff --git a/src/utils/grid-utils.ts b/src/utils/grid-utils.ts new file mode 100644 index 00000000..41125ce4 --- /dev/null +++ b/src/utils/grid-utils.ts @@ -0,0 +1,328 @@ +/** + * Utility functions for working with grid layout data in snapshots. + */ + +import type { Snapshot, GridInfo, Element } from '../types'; + +/** + * Get grid coordinates (bounding boxes) for detected grids. + * + * Groups elements by grid_id and computes the overall bounding box, + * row/column counts, and item count for each grid. + * + * @param snapshot - The snapshot containing elements with layout data + * @param gridId - Optional grid ID to filter by. If undefined, returns all grids. + * @returns Array of GridInfo objects, one per detected grid, sorted by grid_id. + * Each GridInfo contains: + * - grid_id: The grid identifier + * - bbox: Bounding box (x, y, width, height) in document coordinates + * - row_count: Number of rows in the grid + * - col_count: Number of columns in the grid + * - item_count: Total number of items in the grid + * - confidence: Confidence score (currently 1.0) + * - label: Optional inferred label (e.g., "product_grid", "search_results", "navigation") + * Note: Label inference is best-effort and may not always be accurate + * + * @example + * ```typescript + * const snapshot = await browser.snapshot(); + * // Get all grids + * const allGrids = getGridBounds(snapshot); + * // Get specific grid + * const mainGrid = getGridBounds(snapshot, 0); + * if (mainGrid.length > 0) { + * console.log(`Grid 0: ${mainGrid[0].item_count} items at (${mainGrid[0].bbox.x}, ${mainGrid[0].bbox.y})`); + * } + * ``` + */ +export function getGridBounds(snapshot: Snapshot, gridId?: number): GridInfo[] { + // Group elements by grid_id + const gridElements: Map = new Map(); + + for (const elem of snapshot.elements) { + if (elem.layout?.grid_id != null) { + const gid = elem.layout.grid_id; + if (!gridElements.has(gid)) { + gridElements.set(gid, []); + } + gridElements.get(gid)!.push(elem); + } + } + + // Filter by gridId if specified + if (gridId !== undefined) { + if (!gridElements.has(gridId)) { + return []; + } + const filtered = new Map([[gridId, gridElements.get(gridId)!]]); + gridElements.clear(); + filtered.forEach((v, k) => gridElements.set(k, v)); + } + + const gridInfos: GridInfo[] = []; + const gridDominantCounts = new Map(); + + // Sort by grid_id for consistent output + const sortedGridIds = Array.from(gridElements.keys()).sort((a, b) => a - b); + + // First pass: compute all grid infos and count dominant group elements + for (const gid of sortedGridIds) { + const elementsInGrid = gridElements.get(gid)!; + if (elementsInGrid.length === 0) { + continue; + } + + // Count dominant group elements in this grid + const dominantCount = elementsInGrid.filter(e => e.in_dominant_group === true).length; + gridDominantCounts.set(gid, { + dominant: dominantCount, + total: elementsInGrid.length, + }); + + // Compute bounding box + let minX = Infinity; + let minY = Infinity; + let maxX = -Infinity; + let maxY = -Infinity; + + // Count rows and columns + const rowIndices = new Set(); + const colIndices = new Set(); + + for (const elem of elementsInGrid) { + const bbox = elem.bbox; + minX = Math.min(minX, bbox.x); + minY = Math.min(minY, bbox.y); + maxX = Math.max(maxX, bbox.x + bbox.width); + maxY = Math.max(maxY, bbox.y + bbox.height); + + if (elem.layout?.grid_pos) { + rowIndices.add(elem.layout.grid_pos.row_index); + colIndices.add(elem.layout.grid_pos.col_index); + } + } + + // Infer grid label from element patterns (best-effort heuristic) + const label = inferGridLabel(elementsInGrid); + + gridInfos.push({ + grid_id: gid, + bbox: { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }, + row_count: rowIndices.size, + col_count: colIndices.size, + item_count: elementsInGrid.length, + confidence: 1.0, + label: label, + is_dominant: false, // Will be set below + }); + } + + // Second pass: identify dominant grid + // The grid with the highest count (or highest percentage >= 50%) of dominant group elements + if (gridDominantCounts.size > 0) { + // Find grid with highest absolute count + let maxDominantCount = 0; + for (const { dominant } of gridDominantCounts.values()) { + maxDominantCount = Math.max(maxDominantCount, dominant); + } + + if (maxDominantCount > 0) { + // Find grid(s) with highest count + const dominantGrids: number[] = []; + for (const [gid, counts] of gridDominantCounts.entries()) { + if (counts.dominant === maxDominantCount) { + dominantGrids.push(gid); + } + } + + // If multiple grids tie, prefer the one with highest percentage + dominantGrids.sort((a, b) => { + const aCounts = gridDominantCounts.get(a)!; + const bCounts = gridDominantCounts.get(b)!; + const aPct = aCounts.total > 0 ? aCounts.dominant / aCounts.total : 0; + const bPct = bCounts.total > 0 ? bCounts.dominant / bCounts.total : 0; + return bPct - aPct; + }); + + // Mark the dominant grid + const dominantGid = dominantGrids[0]; + const counts = gridDominantCounts.get(dominantGid)!; + // Only mark as dominant if it has >= 50% dominant group elements or >= 3 elements + if (counts.dominant >= 3 || (counts.total > 0 && counts.dominant / counts.total >= 0.5)) { + const gridInfo = gridInfos.find(g => g.grid_id === dominantGid); + if (gridInfo) { + gridInfo.is_dominant = true; + } + } + } + } + + return gridInfos; +} + +/** + * Infer grid label from element patterns using text fingerprinting (best-effort heuristic). + * + * Uses patterns similar to dominant_group.rs content filtering logic, inverted to detect + * semantic grid types. Analyzes first 5 items as a "bag of features". + * + * Returns null if label cannot be reliably determined. + * This is a simple heuristic and may not always be accurate. + */ +function inferGridLabel(elements: Element[]): string | null { + if (elements.length === 0) { + return null; + } + + // Sample first 5 items for fingerprinting (as suggested in feedback) + const sampleElements = elements.slice(0, 5); + const elementTexts = sampleElements.map(e => (e.text || '').trim()).filter(t => t.length > 0); + + if (elementTexts.length === 0) { + return null; + } + + // Collect text patterns + const allText = elementTexts.map(t => t.toLowerCase()).join(' '); + const hrefs = sampleElements.filter(e => e.href).map(e => (e.href || '').toLowerCase()); + + // ========================================================================= + // 1. PRODUCT GRID: Currency symbols, action verbs, ratings + // ========================================================================= + // Currency patterns: $, €, £, or price patterns like "19.99", "$50", "€30" + const currencyPattern = /[$€£¥]\s*\d+|\d+\.\d{2}/.test(allText); + const productActionVerbs = [ + 'add to cart', + 'buy now', + 'shop now', + 'purchase', + 'out of stock', + 'in stock', + ]; + const hasProductActions = productActionVerbs.some(verb => allText.includes(verb)); + + // Ratings pattern: "4.5 stars", "(120 reviews)", "4.5/5" + const ratingPattern = /\d+\.?\d*\s*(stars?|reviews?|\/5|\/10)/i.test(allText); + + // Product URL patterns + const productUrlPatterns = ['/product/', '/item/', '/dp/', '/p/', '/products/']; + const hasProductUrls = hrefs.some(href => + productUrlPatterns.some(pattern => href.includes(pattern)) + ); + + if ( + (currencyPattern || hasProductActions || ratingPattern) && + (hasProductUrls || + elementTexts.filter(t => /[$€£¥]\s*\d+|\d+\.\d{2}/.test(t.toLowerCase())).length >= 2) + ) { + return 'product_grid'; + } + + // ========================================================================= + // 2. ARTICLE/NEWS FEED: Timestamps, bylines, reading time + // ========================================================================= + // Timestamp patterns (reusing logic from dominant_group.rs) + // "2 hours ago", "3 days ago", "5 minutes ago", "1 second ago", "2 ago" + const timestampPatterns = [ + /\d+\s+(hour|day|minute|second)s?\s+ago/i, + /\d+\s+ago/i, // Short form: "2 ago" + /\d{1,2}\s+(hour|day|minute|second)\s+ago/i, // Singular + ]; + const hasTimestamps = timestampPatterns.some(pattern => pattern.test(allText)); + + // Date patterns: "Aug 21, 2024", "2024-01-13", "Jan 15" + const datePatterns = [ + /\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{1,2},?\s+\d{4}/i, + /\d{4}-\d{2}-\d{2}/, + /\d{1,2}\/\d{1,2}\/\d{4}/, + ]; + const hasDates = datePatterns.some(pattern => pattern.test(allText)); + + // Bylines: "By [Name]", "Author:", "Written by" + const bylinePatterns = ['by ', 'author:', 'written by', 'posted by']; + const hasBylines = bylinePatterns.some(pattern => allText.includes(pattern)); + + // Reading time: "5 min read", "10 min", "read more" + const readingTimePattern = /\d+\s*(min|minute)s?\s*(read)?/i.test(allText); + + if (hasTimestamps || (hasDates && hasBylines) || readingTimePattern) { + return 'article_feed'; + } + + // ========================================================================= + // 3. SEARCH RESULTS: Snippets, metadata, ellipses + // ========================================================================= + const searchKeywords = ['result', 'search', 'found', 'showing', 'results 1-', 'sponsored']; + const hasSearchMetadata = searchKeywords.some(keyword => allText.includes(keyword)); + + // Snippet indicators: ellipses, "match found", truncated text + const hasEllipses = + allText.includes('...') || elementTexts.some(t => t.length > 100 && t.includes('...')); + + // Check if many elements are links (typical for search results) + const linkCount = sampleElements.filter(e => e.role === 'link' || e.href).length; + const isMostlyLinks = linkCount >= sampleElements.length * 0.7; // 70%+ are links + + if ((hasSearchMetadata || hasEllipses) && isMostlyLinks) { + return 'search_results'; + } + + // ========================================================================= + // 4. NAVIGATION: Short length, homogeneity, common nav terms + // ========================================================================= + // Calculate average text length and variance + const textLengths = elementTexts.map(t => t.length); + if (textLengths.length > 0) { + const avgLength = textLengths.reduce((sum, len) => sum + len, 0) / textLengths.length; + // Low variance = homogeneous (typical of navigation) + const variance = + textLengths.length > 1 + ? textLengths.reduce((sum, len) => sum + Math.pow(len - avgLength, 2), 0) / + textLengths.length + : 0; + + const navKeywords = [ + 'home', + 'about', + 'contact', + 'menu', + 'login', + 'sign in', + 'profile', + 'settings', + ]; + const hasNavKeywords = navKeywords.some(keyword => allText.includes(keyword)); + + // Navigation: short average length (< 15 chars) AND low variance OR nav keywords + if (avgLength < 15 && (variance < 20 || hasNavKeywords)) { + // Also check if all are links + if (sampleElements.every(e => e.role === 'link' || e.href)) { + return 'navigation'; + } + } + } + + // ========================================================================= + // 5. BUTTON GRID: All buttons + // ========================================================================= + if (sampleElements.every(e => e.role === 'button')) { + return 'button_grid'; + } + + // ========================================================================= + // 6. LINK LIST: Mostly links but not navigation + // ========================================================================= + const linkListCount = sampleElements.filter(e => e.role === 'link' || e.href).length; + if (linkListCount >= sampleElements.length * 0.8) { + // 80%+ are links + return 'link_list'; + } + + // Unknown/unclear + return null; +} diff --git a/tests/actions.test.ts b/tests/actions.test.ts index 7bfeb8fe..d04c9aa3 100644 --- a/tests/actions.test.ts +++ b/tests/actions.test.ts @@ -12,6 +12,7 @@ import { snapshot, find, BBox, + Element, } from '../src'; import { createTestBrowser, getPageOrThrow } from './test-utils'; @@ -26,6 +27,7 @@ describe('Actions', () => { await page.waitForLoadState('networkidle', { timeout: 10000 }); const snap = await snapshot(browser); + const link = find(snap, 'role=link'); if (link) { @@ -140,7 +142,7 @@ describe('Actions', () => { const snap = await snapshot(browser); // Find an element to scroll to - const elements = snap.elements.filter(el => el.role === 'link'); + const elements = snap.elements.filter((el: Element) => el.role === 'link'); if (elements.length > 0) { // Get the last element which might be out of viewport @@ -164,7 +166,7 @@ describe('Actions', () => { await page.waitForLoadState('networkidle', { timeout: 10000 }); const snap = await snapshot(browser); - const elements = snap.elements.filter(el => el.role === 'link'); + const elements = snap.elements.filter((el: Element) => el.role === 'link'); if (elements.length > 0) { const element = elements[0]; @@ -186,7 +188,7 @@ describe('Actions', () => { await page.waitForLoadState('networkidle', { timeout: 10000 }); const snap = await snapshot(browser); - const elements = snap.elements.filter(el => el.role === 'link'); + const elements = snap.elements.filter((el: Element) => el.role === 'link'); if (elements.length > 0) { const element = elements[0]; @@ -339,6 +341,9 @@ describe('Actions', () => { await page.goto('https://example.com'); await page.waitForLoadState('networkidle', { timeout: 10000 }); + // Check if extension is available by trying to take a snapshot + const snap = await snapshot(browser); + const result = await clickRect(browser, { x: 100, y: 100, w: 50, h: 30 }, true, 2.0, true); expect(result.success).toBe(true); expect(result.snapshot_after).toBeDefined(); diff --git a/tests/grid-bounds.test.ts b/tests/grid-bounds.test.ts new file mode 100644 index 00000000..d1e2e444 --- /dev/null +++ b/tests/grid-bounds.test.ts @@ -0,0 +1,314 @@ +/** + * Tests for getGridBounds functionality + */ + +import { getGridBounds, Snapshot, Element, BBox, LayoutHints, GridPosition } from '../src'; + +/** + * Helper to create test elements with layout data + */ +function createTestElement( + elementId: number, + x: number, + y: number, + width: number, + height: number, + gridId?: number | null, + rowIndex?: number | null, + colIndex?: number | null, + text?: string | null, + href?: string | null +): Element { + let layout: LayoutHints | undefined = undefined; + if (gridId != null) { + let gridPos: GridPosition | undefined = undefined; + if (rowIndex != null && colIndex != null) { + gridPos = { + row_index: rowIndex, + col_index: colIndex, + cluster_id: gridId, + }; + } + layout = { + grid_id: gridId, + grid_pos: gridPos, + grid_confidence: 1.0, + parent_confidence: 1.0, + region_confidence: 1.0, + }; + } + + return { + id: elementId, + role: 'link', + text: text || `Element ${elementId}`, + importance: 100, + bbox: { x, y, width, height }, + visual_cues: { + is_primary: false, + background_color_name: null, + is_clickable: true, + }, + in_viewport: true, + is_occluded: false, + z_index: 0, + layout, + href: href || undefined, + }; +} + +describe('getGridBounds', () => { + it('should return empty array for empty snapshot', () => { + const snapshot: Snapshot = { + status: 'success', + url: 'https://example.com', + elements: [], + }; + + const result = getGridBounds(snapshot); + expect(result).toEqual([]); + }); + + it('should return empty array when no layout data', () => { + const snapshot: Snapshot = { + status: 'success', + url: 'https://example.com', + elements: [createTestElement(1, 10, 20, 100, 50), createTestElement(2, 120, 20, 100, 50)], + }; + + const result = getGridBounds(snapshot); + expect(result).toEqual([]); + }); + + it('should compute bounds for single 2x2 grid', () => { + const elements: Element[] = [ + createTestElement(1, 10, 20, 100, 50, 0, 0, 0), + createTestElement(2, 120, 20, 100, 50, 0, 0, 1), + createTestElement(3, 10, 80, 100, 50, 0, 1, 0), + createTestElement(4, 120, 80, 100, 50, 0, 1, 1), + ]; + + const snapshot: Snapshot = { + status: 'success', + url: 'https://example.com', + elements, + }; + + const result = getGridBounds(snapshot); + expect(result.length).toBe(1); + + const grid = result[0]; + expect(grid.grid_id).toBe(0); + expect(grid.bbox.x).toBe(10); + expect(grid.bbox.y).toBe(20); + expect(grid.bbox.width).toBe(210); // max_x (120+100) - min_x (10) + expect(grid.bbox.height).toBe(110); // max_y (80+50) - min_y (20) + expect(grid.row_count).toBe(2); + expect(grid.col_count).toBe(2); + expect(grid.item_count).toBe(4); + expect(grid.confidence).toBe(1.0); + }); + + it('should handle multiple distinct grids', () => { + // Grid 0: 2x1 at top + const grid0Elements: Element[] = [ + createTestElement(1, 10, 20, 100, 50, 0, 0, 0), + createTestElement(2, 120, 20, 100, 50, 0, 0, 1), + ]; + // Grid 1: 1x3 at bottom + const grid1Elements: Element[] = [ + createTestElement(3, 10, 200, 100, 50, 1, 0, 0), + createTestElement(4, 10, 260, 100, 50, 1, 1, 0), + createTestElement(5, 10, 320, 100, 50, 1, 2, 0), + ]; + + const snapshot: Snapshot = { + status: 'success', + url: 'https://example.com', + elements: [...grid0Elements, ...grid1Elements], + }; + + const result = getGridBounds(snapshot); + expect(result.length).toBe(2); + + // Check grid 0 + const grid0 = result[0]; + expect(grid0.grid_id).toBe(0); + expect(grid0.bbox.x).toBe(10); + expect(grid0.bbox.y).toBe(20); + expect(grid0.bbox.width).toBe(210); + expect(grid0.bbox.height).toBe(50); + expect(grid0.row_count).toBe(1); + expect(grid0.col_count).toBe(2); + expect(grid0.item_count).toBe(2); + + // Check grid 1 + const grid1 = result[1]; + expect(grid1.grid_id).toBe(1); + expect(grid1.bbox.x).toBe(10); + expect(grid1.bbox.y).toBe(200); + expect(grid1.bbox.width).toBe(100); + expect(grid1.bbox.height).toBe(170); // max_y (320+50) - min_y (200) + expect(grid1.row_count).toBe(3); + expect(grid1.col_count).toBe(1); + expect(grid1.item_count).toBe(3); + }); + + it('should filter by specific grid_id', () => { + const elements: Element[] = [ + createTestElement(1, 10, 20, 100, 50, 0, 0, 0), + createTestElement(2, 120, 20, 100, 50, 0, 0, 1), + createTestElement(3, 10, 200, 100, 50, 1, 0, 0), + ]; + + const snapshot: Snapshot = { + status: 'success', + url: 'https://example.com', + elements, + }; + + // Get only grid 0 + let result = getGridBounds(snapshot, 0); + expect(result.length).toBe(1); + expect(result[0].grid_id).toBe(0); + expect(result[0].item_count).toBe(2); + + // Get only grid 1 + result = getGridBounds(snapshot, 1); + expect(result.length).toBe(1); + expect(result[0].grid_id).toBe(1); + expect(result[0].item_count).toBe(1); + + // Get non-existent grid + result = getGridBounds(snapshot, 99); + expect(result).toEqual([]); + }); + + it('should handle grid elements without grid_pos', () => { + // Elements with grid_id but no grid_pos (should still be counted) + const elements: Element[] = [ + createTestElement(1, 10, 20, 100, 50, 0, null, null), + createTestElement(2, 120, 20, 100, 50, 0, null, null), + ]; + + const snapshot: Snapshot = { + status: 'success', + url: 'https://example.com', + elements, + }; + + const result = getGridBounds(snapshot); + expect(result.length).toBe(1); + const grid = result[0]; + expect(grid.grid_id).toBe(0); + expect(grid.item_count).toBe(2); + expect(grid.row_count).toBe(0); // No grid_pos means no rows/cols counted + expect(grid.col_count).toBe(0); + }); + + it('should infer product_grid label', () => { + const elements: Element[] = [ + createTestElement( + 1, + 10, + 20, + 100, + 50, + 0, + 0, + 0, + 'Wireless Headphones $50', + 'https://example.com/product/headphones' + ), + createTestElement( + 2, + 120, + 20, + 100, + 50, + 0, + 0, + 1, + 'Bluetooth Speaker $30', + 'https://example.com/product/speaker' + ), + createTestElement( + 3, + 10, + 80, + 100, + 50, + 0, + 1, + 0, + 'USB-C Cable $10', + 'https://example.com/product/cable' + ), + ]; + + const snapshot: Snapshot = { + status: 'success', + url: 'https://example.com', + elements, + }; + + const result = getGridBounds(snapshot); + expect(result.length).toBe(1); + expect(result[0].label).toBe('product_grid'); + }); + + it('should infer article_feed label', () => { + const elements: Element[] = [ + createTestElement(1, 10, 20, 100, 50, 0, 0, 0, 'Breaking News 2 hours ago'), + createTestElement(2, 10, 80, 100, 50, 0, 1, 0, 'Tech Update 3 days ago'), + ]; + + const snapshot: Snapshot = { + status: 'success', + url: 'https://example.com', + elements, + }; + + const result = getGridBounds(snapshot); + expect(result.length).toBe(1); + expect(result[0].label).toBe('article_feed'); + }); + + it('should infer navigation label', () => { + const elements: Element[] = [ + createTestElement(1, 10, 20, 80, 30, 0, 0, 0, 'Home'), + createTestElement(2, 100, 20, 80, 30, 0, 0, 1, 'About'), + createTestElement(3, 190, 20, 80, 30, 0, 0, 2, 'Contact'), + ]; + + const snapshot: Snapshot = { + status: 'success', + url: 'https://example.com', + elements, + }; + + const result = getGridBounds(snapshot); + expect(result.length).toBe(1); + expect(result[0].label).toBe('navigation'); + }); + + it('should sort results by grid_id', () => { + const elements: Element[] = [ + createTestElement(1, 10, 20, 100, 50, 2, 0, 0), + createTestElement(2, 10, 200, 100, 50, 0, 0, 0), + createTestElement(3, 10, 380, 100, 50, 1, 0, 0), + ]; + + const snapshot: Snapshot = { + status: 'success', + url: 'https://example.com', + elements, + }; + + const result = getGridBounds(snapshot); + expect(result.length).toBe(3); + expect(result[0].grid_id).toBe(0); + expect(result[1].grid_id).toBe(1); + expect(result[2].grid_id).toBe(2); + }); +});