From 1726f1d92389545489a7b5fc0249637bc6cae6f6 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 8 Sep 2026 08:26:59 -0700 Subject: [PATCH 01/51] docs: add migration plan for vanilla viewer adoption --- PLAN.md | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 PLAN.md diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 000000000..312eae595 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,137 @@ +# Plan: Adopt Vanilla Protocol Viewer & Retire Legacy SSG Stack + +## 1. Executive Summary & Goals + +This project transitions the Chrome DevTools Protocol Viewer ([`chromedevtools.github.io/devtools-protocol`](https://chromedevtools.github.io/devtools-protocol/)) from its legacy static site generator (11ty + Handlebars + Rollup + Lit-HTML) to a zero-dependency client-side viewer based on Andrey Lushnikov's [vanilla-protocol-viewer](https://github.com/aslushnikov/vanilla-protocol-viewer) ([`vanilla.aslushnikov.com`](https://vanilla.aslushnikov.com/)). + +### Core Objectives +1. **Eliminate Domain Maintenance**: Protocol domains, methods, events, and types are rendered dynamically from protocol JSON at runtime. No manual updates to navigation templates (e.g. [`shell.hbs`](./pages/_includes/shell.hbs)) required when new domains land in Chromium. +2. **Native Type Cross-References**: Resolve [#183](https://github.com/ChromeDevTools/debugger-protocol-viewer/issues/183) by providing dynamic reverse-dependency listings ("Used by: ...") for all protocol types. +3. **Instant Search**: Replace precomputed index lookups with instant client-side fuzzy searching on keypress. +4. **Preserve External Permalinks (Zero Broken Links)**: Ensure 100% backward compatibility for millions of existing links across StackOverflow, Chromium bugs, and developer docs using a dual-layer strategy: + - **Static Stubs (`/tot//index.html`)**: Return HTTP 200 OK for search engine crawlers and preserve legacy hash fragments (`#method-foo`, `#type-bar`). + - **Wildcard Fallback (`/404.html`)**: Catches older version paths (`/1-3/*`, `/1-2/*`, `/v8/*`) and unexpected deep links. +5. **Protocol Fetching**: Per decision, protocol JSON fetching will continue to use jsdelivr (`https://cdn.jsdelivr.net/gh/ChromeDevTools/devtools-protocol@master/json/...`) for now. +6. **Continuous Autonomous Verification**: Implement automated test suites (build invariant checks + headless browser end-to-end tests) to validate routing, rendering, search, and URL compatibility autonomously. + +--- + +## 2. Architecture & Legacy URL Compatibility Strategy + +### URL Mapping Specification + +| Source Legacy URL Pattern | Target Destination | Handling Mechanism | HTTP Status | +| :--- | :--- | :--- | :--- | +| `.../tot/Page/#method-navigate` | `.../?Page.navigate` | Pre-generated `tot/Page/index.html` stub | **200 OK** | +| `.../tot/Page/` | `.../?Page` | Pre-generated `tot/Page/index.html` stub | **200 OK** | +| `.../tot/DOM/#type-Node` | `.../?DOM.Node` | Pre-generated `tot/DOM/index.html` stub | **200 OK** | +| `.../tot/Network/#event-requestWillBeSent` | `.../?Network.requestWillBeSent` | Pre-generated `tot/Network/index.html` stub | **200 OK** | +| `.../1-3/Page/#method-navigate` | `.../?Page.navigate` | Catch-all `404.html` | **404 -> Client Redirect** | +| `.../?Page.navigate` | Native SPA route | Client-side Router in `main.js` | **200 OK** | + +### Stub Generation Mechanism +During the build step (`npm run build`), a lightweight Node script reads the protocol domains and generates static directory stubs under `devtools-protocol/tot//index.html`. + +Each stub runs an inline client script that reads `window.location.hash`, parses target elements (`#method-xyz`, `#type-abc`, `#event-def`), and redirects seamlessly via `location.replace(...)` to the canonical query format, preserving browser history semantics. + +--- + +## 3. Autonomous Validation Techniques + +To validate the viewer autonomously without manual browser verification, we define three complementary testing tiers: + +### Tier 1: Build Output & Contract Invariant Tests (Fast Node.js Native Runner) +Executed via `node --test`: +- **Directory Structure Verification**: Ensures `devtools-protocol/` contains `index.html`, `404.html`, and `tot//index.html` stubs for every active protocol domain. +- **Stub Integrity**: Confirms each stub contains valid redirect logic and does not leak undefined hashes. +- **Asset Integrity**: Confirms critical client assets (`main.js`, `search.js`, `protocol_renderer.js`, `style.css`) exist, are non-empty, and contain no broken import references. + +### Tier 2: Headless Browser E2E Tests (Playwright / Puppeteer) +Runs against a local static web server serving `devtools-protocol/`: +- **Test Case 1: Legacy Hash Navigation**: + - Load `http://localhost:PORT/devtools-protocol/tot/Page/#method-navigate` + - Assert URL updates or resolves to `Page.navigate`. + - Assert the DOM contains the `#Page-navigate` section and that it is visible/focused in viewport. +- **Test Case 2: Legacy Type Deep Link**: + - Load `http://localhost:PORT/devtools-protocol/tot/DOM/#type-Node` + - Assert DOM contains `DOM.Node` definition. +- **Test Case 3: 404 Fallback for Historical Versions**: + - Load `http://localhost:PORT/devtools-protocol/1-3/Network/` + - Assert navigation resolves to the `Network` domain view via `404.html`. +- **Test Case 4: Back-References / Cross-References ([#183](https://github.com/ChromeDevTools/debugger-protocol-viewer/issues/183))**: + - Navigate to `http://localhost:PORT/devtools-protocol/?Debugger.CallFrame` + - Assert presence of the `.references` or "Used by" list. + - Verify links within "Used by" point to dependent protocol types or methods. +- **Test Case 5: Instant Search**: + - Load base viewer `http://localhost:PORT/devtools-protocol/`. + - Type `captureScreenshot` on the page. + - Assert search dropdown/overlay opens with `Page.captureScreenshot`. + - Trigger selection (click or Enter); assert route changes to `?Page.captureScreenshot`. +- **Test Case 6: Experimental Domain Toggle**: + - Verify domains marked `experimental: true` toggle display when toggling the experimental setting. +- **Test Case 7: Zero Uncaught Exceptions**: + - Monitor `page.on('pageerror')` and `page.on('console', msg => msg.type() === 'error')` across all test runs. + +### Tier 3: Upstream CI Contract Compatibility Check +Simulate the deployment step executed by [`ChromeDevTools/devtools-protocol/.github/workflows/update.yml`](https://github.com/ChromeDevTools/devtools-protocol/blob/master/.github/workflows/update.yml): +- Run `npm install && npm run prep && npm run build` +- Validate that `devtools-protocol/` contains a valid deployable bundle matching GitHub Pages publishing requirements. + +--- + +## 4. Phased Implementation Checklist + +### Phase 1: Core Vanilla Viewer Integration +- [ ] Initialize clean directory structure for the new client code (`src/` or top-level web assets). +- [ ] Vendor core viewer assets from [`aslushnikov/vanilla-protocol-viewer`](https://github.com/aslushnikov/vanilla-protocol-viewer): + - `index.html` + - `main.js` + - `protocol_renderer.js` + - `search.js` + - `utilities.js` + - `style.css` + - SVG icons (`home.svg`, checkbox icons) +- [ ] Configure protocol endpoints in `main.js` to load from jsdelivr (`ChromeDevTools/devtools-protocol@master`). +- [ ] Test base viewer rendering locally with static file server. + +### Phase 2: URL Routing & Legacy Compatibility +- [ ] Extend `Router` in `main.js` to handle: + - Query format: `?Domain` and `?Domain.member` + - Path format: `/tot/:domain` + - Anchor format: `#method-member`, `#type-member`, `#event-member` +- [ ] Implement `scripts/generate-stubs.js`: + - Fetch or read protocol domains. + - Generate `tot//index.html` stubs with client redirect and hash retention. +- [ ] Create `404.html` catch-all script: + - Parse `window.location.pathname` and `window.location.hash`. + - Extract domain and target; redirect via `location.replace(...)`. +- [ ] Verify local redirect behavior across all legacy URL formats. + +### Phase 3: Autonomous Testing Suite +- [ ] Install test dependencies (e.g. lightweight headless browser runner). +- [ ] Add `test/invariants.test.js`: + - Validate stub generation completeness. + - Validate HTML and redirect syntax. +- [ ] Add `test/e2e.test.js`: + - Implement Headless Browser test cases (Legacy URLs, Search, Cross-references, Experimental toggle, Console errors). +- [ ] Wire test suite into `npm test` script in [`package.json`](./package.json). + +### Phase 4: Build & Deployment Workflow Integration +- [ ] Update `npm run build` script in [`package.json`](./package.json) to output to `devtools-protocol/`. +- [ ] Update or simplify `npm run prep` if needed while maintaining CLI signature expected by [devtools-protocol update.yml](https://github.com/ChromeDevTools/devtools-protocol/blob/master/.github/workflows/update.yml). +- [ ] Test the exact CI build sequence locally: `npm install && npm run prep && npm run build && npm test`. +- [ ] Ensure `.nojekyll` is preserved in `devtools-protocol/` output to prevent GitHub Pages from ignoring files. + +### Phase 5: Cleanup & Deprecation +- [ ] Remove obsolete SSG dependencies from [`package.json`](./package.json): + - `@11ty/eleventy` + - `@11ty/eleventy-plugin-handlebars` + - `marked` + - `liquidjs` + - `rollup` and plugins (if bundling is unnecessary for vanilla JS) +- [ ] Remove deprecated scripts and templates: + - [`generate-sidenav-html.cjs`](./generate-sidenav-html.cjs) + - [`make-stable-protocol.cjs`](./make-stable-protocol.cjs) + - [`create-search-index.cjs`](./create-search-index.cjs) + - [`pages/`](./pages/) directory (once legacy SSG is fully decommissioned) +- [ ] Update [`readme.md`](./readme.md) documentation to reflect the modern vanilla viewer architecture and local dev workflow. From 3c7b79197381262fbb88dfc39b5590e0163e15d1 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 8 Sep 2026 08:40:04 -0700 Subject: [PATCH 02/51] docs: update plan with hash routing, devtools header, and mobile drawer --- PLAN.md | 233 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 122 insertions(+), 111 deletions(-) diff --git a/PLAN.md b/PLAN.md index 312eae595..162a217fe 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,137 +1,148 @@ -# Plan: Adopt Vanilla Protocol Viewer & Retire Legacy SSG Stack +# Plan: Adopt Vanilla Protocol Viewer & Modernize CDP Viewer ## 1. Executive Summary & Goals -This project transitions the Chrome DevTools Protocol Viewer ([`chromedevtools.github.io/devtools-protocol`](https://chromedevtools.github.io/devtools-protocol/)) from its legacy static site generator (11ty + Handlebars + Rollup + Lit-HTML) to a zero-dependency client-side viewer based on Andrey Lushnikov's [vanilla-protocol-viewer](https://github.com/aslushnikov/vanilla-protocol-viewer) ([`vanilla.aslushnikov.com`](https://vanilla.aslushnikov.com/)). +This project transitions the Chrome DevTools Protocol Viewer ([`chromedevtools.github.io/devtools-protocol`](https://chromedevtools.github.io/devtools-protocol/)) from its legacy static site generator (11ty + Handlebars + Rollup + Lit-HTML) to a zero-dependency, ultra-fast client-side viewer based on Andrey Lushnikov's [vanilla-protocol-viewer](https://github.com/aslushnikov/vanilla-protocol-viewer) ([`vanilla.aslushnikov.com`](https://vanilla.aslushnikov.com/)). ### Core Objectives -1. **Eliminate Domain Maintenance**: Protocol domains, methods, events, and types are rendered dynamically from protocol JSON at runtime. No manual updates to navigation templates (e.g. [`shell.hbs`](./pages/_includes/shell.hbs)) required when new domains land in Chromium. -2. **Native Type Cross-References**: Resolve [#183](https://github.com/ChromeDevTools/debugger-protocol-viewer/issues/183) by providing dynamic reverse-dependency listings ("Used by: ...") for all protocol types. -3. **Instant Search**: Replace precomputed index lookups with instant client-side fuzzy searching on keypress. +1. **Eliminate Maintenance Overhead**: Protocol domains, methods, events, and types are rendered dynamically from protocol JSON at runtime. No manual edits to navigation templates (e.g. [`shell.hbs`](./pages/_includes/shell.hbs)) required when new domains land in Chromium. +2. **Native Type Cross-References**: Resolve [#183](https://github.com/ChromeDevTools/debugger-protocol-viewer/issues/183) by generating dynamic reverse-dependency listings ("Used by: ...") for all protocol types. +3. **Instant Search & High Information Density**: Instant client-side fuzzy searching on keypress or `/`, paired with DevTools-inspired UI ergonomics. 4. **Preserve External Permalinks (Zero Broken Links)**: Ensure 100% backward compatibility for millions of existing links across StackOverflow, Chromium bugs, and developer docs using a dual-layer strategy: - - **Static Stubs (`/tot//index.html`)**: Return HTTP 200 OK for search engine crawlers and preserve legacy hash fragments (`#method-foo`, `#type-bar`). + - **Static Stubs (`/tot//index.html`)**: Return HTTP 200 OK for crawlers and preserve legacy hash fragments (`#method-foo`, `#type-bar`). - **Wildcard Fallback (`/404.html`)**: Catches older version paths (`/1-3/*`, `/1-2/*`, `/v8/*`) and unexpected deep links. -5. **Protocol Fetching**: Per decision, protocol JSON fetching will continue to use jsdelivr (`https://cdn.jsdelivr.net/gh/ChromeDevTools/devtools-protocol@master/json/...`) for now. -6. **Continuous Autonomous Verification**: Implement automated test suites (build invariant checks + headless browser end-to-end tests) to validate routing, rendering, search, and URL compatibility autonomously. +5. **Preserve `stable` & `v8` Targets ("Buried but Accessible")**: Keep `stable` (1.3 release) and `v8` (Node inspector domains) functional via a clean header dropdown without cluttering the primary tip-of-tree workflow. +6. **Responsive Mobile Navigation**: Implement an off-canvas slide-out domain drawer on mobile (`< 800px`) rather than hiding the sidebar entirely. +7. **Isomorphic Core & Autonomous Validation**: Isolate protocol indexing and routing into an isomorphic module (`protocol-model.js`) unit-tested in `node:test` in **<50ms**, complemented by automated headless browser end-to-end tests. --- -## 2. Architecture & Legacy URL Compatibility Strategy +## 2. Information Architecture & Routing -### URL Mapping Specification +### URL Scheme Specification -| Source Legacy URL Pattern | Target Destination | Handling Mechanism | HTTP Status | +| Target View | Canonical Route | Legacy Input URL | Mapping Mechanism | | :--- | :--- | :--- | :--- | -| `.../tot/Page/#method-navigate` | `.../?Page.navigate` | Pre-generated `tot/Page/index.html` stub | **200 OK** | -| `.../tot/Page/` | `.../?Page` | Pre-generated `tot/Page/index.html` stub | **200 OK** | -| `.../tot/DOM/#type-Node` | `.../?DOM.Node` | Pre-generated `tot/DOM/index.html` stub | **200 OK** | -| `.../tot/Network/#event-requestWillBeSent` | `.../?Network.requestWillBeSent` | Pre-generated `tot/Network/index.html` stub | **200 OK** | -| `.../1-3/Page/#method-navigate` | `.../?Page.navigate` | Catch-all `404.html` | **404 -> Client Redirect** | -| `.../?Page.navigate` | Native SPA route | Client-side Router in `main.js` | **200 OK** | +| **Domain View** | `#/Page` | `/tot/Page/` | `tot/Page/index.html` stub ➔ `location.replace('#/Page')` | +| **Command / Method** | `#/Page.navigate` | `/tot/Page/#method-navigate` | `tot/Page/index.html` stub reads `#method-navigate` ➔ `location.replace('#/Page.navigate')` | +| **Event** | `#/Network.requestWillBeSent` | `/tot/Network/#event-requestWillBeSent` | `tot/Network/index.html` stub reads hash ➔ `location.replace('#/Network.requestWillBeSent')` | +| **Type** | `#/DOM.Node` | `/tot/DOM/#type-Node` | `tot/DOM/index.html` stub reads hash ➔ `location.replace('#/DOM.Node')` | +| **V8 Inspector** | `#/v8/Runtime.evaluate` | `/v8/Runtime/` | `404.html` catch-all ➔ `location.replace('#/v8/Runtime')` | +| **Stable Protocol** | `#/stable/Page.navigate` | `/1-3/Page/#method-navigate` | `404.html` catch-all ➔ `location.replace('#/stable/Page.navigate')` | -### Stub Generation Mechanism -During the build step (`npm run build`), a lightweight Node script reads the protocol domains and generates static directory stubs under `devtools-protocol/tot//index.html`. +--- -Each stub runs an inline client script that reads `window.location.hash`, parses target elements (`#method-xyz`, `#type-abc`, `#event-def`), and redirects seamlessly via `location.replace(...)` to the canonical query format, preserving browser history semantics. +## 3. UI & UX Design Specifications + +### 3.1 Header (44px DevTools Utility Bar) +```text ++---------------------------------------------------------------------------------------------------------+ +| [⚙] DevTools Protocol [tot ▾] [⚡Exp] | [ 🔍 Search protocol... / ] | [📖] [🐙] | ++---------------------------------------------------------------------------------------------------------+ +``` +- **Brand & Target**: Logo + title (`#/` home). +- **Version Selector (`tot ▾`)**: + - `● tot (tip-of-tree)` *(Default)* + - `○ stable (1.3)` + - `○ v8 inspector` +- **Experimental Toggle (`[⚡Exp]`)**: Checkbox pill visible in `tot` mode to toggle experimental APIs. +- **Global Search**: + - Auto-focused by typing *any* character anywhere on the page, or pressing `/` or `Cmd+K`. + - Trailing `/` shortcut indicator. + - Interactive fuzzy search dropdown with keyboard navigation (`↑`/`↓`/`Enter`/`Esc`). +- **External Links**: Crisp icon links for CDP Overview Docs and GitHub repository. + +### 3.2 Leftnav Sidebar & Mobile Drawer +- **In-Sidebar Domain Filter**: Sticky 24px filter input (`Filter domains...`) at the top of the sidebar for quickly narrowing down the ~50 domains. +- **Visual Badges**: Muted amber `[EXP]` badge for experimental domains. +- **Mobile Responsive Drawer (`< 800px`)**: + - Hamburger icon `[☰]` in the header. + - Smooth off-canvas slide-out drawer (`transform: translateX(-100%)`) with backdrop overlay. + - Selecting any domain automatically closes the drawer and navigates to the target. + +### 3.3 Domain View & Sticky Sub-Navigation +- **Sticky Domain Sub-Header**: + - Pinned directly below the main header during scroll: + `[ Methods (34) ] [ Events (18) ] [ Types (12) ]` + - Instant one-click smooth scrolling to section anchors. +- **Cross-References ("Used by")**: + - Rendered beneath protocol types, listing every command, event, and type that references it (resolves #183). --- -## 3. Autonomous Validation Techniques - -To validate the viewer autonomously without manual browser verification, we define three complementary testing tiers: +## 4. Autonomous Validation Strategy -### Tier 1: Build Output & Contract Invariant Tests (Fast Node.js Native Runner) -Executed via `node --test`: -- **Directory Structure Verification**: Ensures `devtools-protocol/` contains `index.html`, `404.html`, and `tot//index.html` stubs for every active protocol domain. -- **Stub Integrity**: Confirms each stub contains valid redirect logic and does not leak undefined hashes. -- **Asset Integrity**: Confirms critical client assets (`main.js`, `search.js`, `protocol_renderer.js`, `style.css`) exist, are non-empty, and contain no broken import references. +### Tier 1: Isomorphic Core & Build Invariant Tests (`node:test`) +- **Protocol Model Tests (`test/protocol-model.test.js`)**: + - Normalization of protocol JSON. + - Back-reference index generation ("Used by" correctness). + - Stabilization filter (stripping experimental items when toggled off). + - Route parser: parsing `#/Page.navigate`, `#/v8/Runtime`, and legacy hash strings into target actions. +- **Stub Generator Tests (`test/stubs.test.js`)**: + - Verify every active protocol domain generates a `tot//index.html` stub. + - Verify stub redirect logic and hash parameter extraction. + - Verify `404.html` catch-all script syntax and routing rules. ### Tier 2: Headless Browser E2E Tests (Playwright / Puppeteer) -Runs against a local static web server serving `devtools-protocol/`: -- **Test Case 1: Legacy Hash Navigation**: - - Load `http://localhost:PORT/devtools-protocol/tot/Page/#method-navigate` - - Assert URL updates or resolves to `Page.navigate`. - - Assert the DOM contains the `#Page-navigate` section and that it is visible/focused in viewport. -- **Test Case 2: Legacy Type Deep Link**: - - Load `http://localhost:PORT/devtools-protocol/tot/DOM/#type-Node` - - Assert DOM contains `DOM.Node` definition. -- **Test Case 3: 404 Fallback for Historical Versions**: - - Load `http://localhost:PORT/devtools-protocol/1-3/Network/` - - Assert navigation resolves to the `Network` domain view via `404.html`. -- **Test Case 4: Back-References / Cross-References ([#183](https://github.com/ChromeDevTools/debugger-protocol-viewer/issues/183))**: - - Navigate to `http://localhost:PORT/devtools-protocol/?Debugger.CallFrame` - - Assert presence of the `.references` or "Used by" list. - - Verify links within "Used by" point to dependent protocol types or methods. -- **Test Case 5: Instant Search**: - - Load base viewer `http://localhost:PORT/devtools-protocol/`. - - Type `captureScreenshot` on the page. - - Assert search dropdown/overlay opens with `Page.captureScreenshot`. - - Trigger selection (click or Enter); assert route changes to `?Page.captureScreenshot`. -- **Test Case 6: Experimental Domain Toggle**: - - Verify domains marked `experimental: true` toggle display when toggling the experimental setting. -- **Test Case 7: Zero Uncaught Exceptions**: - - Monitor `page.on('pageerror')` and `page.on('console', msg => msg.type() === 'error')` across all test runs. - -### Tier 3: Upstream CI Contract Compatibility Check -Simulate the deployment step executed by [`ChromeDevTools/devtools-protocol/.github/workflows/update.yml`](https://github.com/ChromeDevTools/devtools-protocol/blob/master/.github/workflows/update.yml): -- Run `npm install && npm run prep && npm run build` -- Validate that `devtools-protocol/` contains a valid deployable bundle matching GitHub Pages publishing requirements. +- **Legacy URL Redirection**: + - Request `/tot/Page/#method-navigate` ➔ assert HTTP 200, URL becomes `#/Page.navigate`, `#Page-navigate` section visible. + - Request `/tot/DOM/#type-Node` ➔ assert HTTP 200, URL becomes `#/DOM.Node`, type definition rendered. + - Request `/1-3/Network/` ➔ assert 404 handler routes to `#/stable/Network`. +- **Search Interactions**: + - Typing `captureScreenshot` from anywhere opens search overlay. + - Pressing `Enter` navigates to `#/Page.captureScreenshot`. +- **Type Back-References**: + - Navigate to `#/Debugger.CallFrame` ➔ assert "Used by" section exists with clickable links. +- **Experimental Toggle**: + - Toggling `[⚡Exp]` updates domain list and method visibility. +- **Console & Network Health**: + - Zero uncaught page exceptions or console errors. + +### Tier 3: Upstream CI Pipeline Verification +- Simulate [`ChromeDevTools/devtools-protocol/.github/workflows/update.yml`](https://github.com/ChromeDevTools/devtools-protocol/blob/master/.github/workflows/update.yml): + `npm install && npm run prep && npm run build && npm test` +- Validate that `devtools-protocol/` contains the complete deployable bundle with `.nojekyll`. --- -## 4. Phased Implementation Checklist - -### Phase 1: Core Vanilla Viewer Integration -- [ ] Initialize clean directory structure for the new client code (`src/` or top-level web assets). -- [ ] Vendor core viewer assets from [`aslushnikov/vanilla-protocol-viewer`](https://github.com/aslushnikov/vanilla-protocol-viewer): - - `index.html` - - `main.js` - - `protocol_renderer.js` - - `search.js` - - `utilities.js` - - `style.css` - - SVG icons (`home.svg`, checkbox icons) -- [ ] Configure protocol endpoints in `main.js` to load from jsdelivr (`ChromeDevTools/devtools-protocol@master`). -- [ ] Test base viewer rendering locally with static file server. - -### Phase 2: URL Routing & Legacy Compatibility -- [ ] Extend `Router` in `main.js` to handle: - - Query format: `?Domain` and `?Domain.member` - - Path format: `/tot/:domain` - - Anchor format: `#method-member`, `#type-member`, `#event-member` -- [ ] Implement `scripts/generate-stubs.js`: - - Fetch or read protocol domains. - - Generate `tot//index.html` stubs with client redirect and hash retention. -- [ ] Create `404.html` catch-all script: - - Parse `window.location.pathname` and `window.location.hash`. - - Extract domain and target; redirect via `location.replace(...)`. -- [ ] Verify local redirect behavior across all legacy URL formats. - -### Phase 3: Autonomous Testing Suite -- [ ] Install test dependencies (e.g. lightweight headless browser runner). -- [ ] Add `test/invariants.test.js`: - - Validate stub generation completeness. - - Validate HTML and redirect syntax. -- [ ] Add `test/e2e.test.js`: - - Implement Headless Browser test cases (Legacy URLs, Search, Cross-references, Experimental toggle, Console errors). -- [ ] Wire test suite into `npm test` script in [`package.json`](./package.json). - -### Phase 4: Build & Deployment Workflow Integration -- [ ] Update `npm run build` script in [`package.json`](./package.json) to output to `devtools-protocol/`. -- [ ] Update or simplify `npm run prep` if needed while maintaining CLI signature expected by [devtools-protocol update.yml](https://github.com/ChromeDevTools/devtools-protocol/blob/master/.github/workflows/update.yml). -- [ ] Test the exact CI build sequence locally: `npm install && npm run prep && npm run build && npm test`. -- [ ] Ensure `.nojekyll` is preserved in `devtools-protocol/` output to prevent GitHub Pages from ignoring files. +## 5. Phased Implementation Checklist + +### Phase 1: Isomorphic Core & Base Viewer Adaptation +- [ ] Create `src/protocol-model.js` isolating data normalization, back-reference computation, stabilization, and route parsing. +- [ ] Add `test/protocol-model.test.js` using `node:test` to validate core logic in Node (<50ms). +- [ ] Vendor and adapt core UI files from `vanilla-protocol-viewer`: + - `src/index.html` + - `src/main.js` + - `src/protocol_renderer.js` + - `src/search.js` + - `src/utilities.js` + - `src/style.css` + - SVG icons +- [ ] Update `main.js` to use `#/Domain.member` hash routing and connect `protocol-model.js`. +- [ ] Implement DevTools toolbar header with centered search, `/` shortcut, and external links. + +### Phase 2: Multi-Target (tot, stable, v8) & UX Enhancements +- [ ] Add version selector dropdown (`tot`, `stable`, `v8`). +- [ ] Implement V8 Inspector view (loading `js_protocol.json` only). +- [ ] Implement stable protocol view (stabilized `tot`). +- [ ] Add in-sidebar domain filter (`Filter domains...`). +- [ ] Add sticky in-domain sub-header (`Methods (N)`, `Events (N)`, `Types (N)`). +- [ ] Implement responsive mobile drawer with hamburger button and overlay for `< 800px`. + +### Phase 3: Legacy URL Preservation & Build Pipeline +- [ ] Create `scripts/generate-stubs.js` to output `tot//index.html` stubs. +- [ ] Create `src/404.html` wildcard redirect handler. +- [ ] Configure `npm run build` to assemble the full site into `devtools-protocol/`. +- [ ] Update `npm run prep` to remain compatible with upstream CI. +- [ ] Ensure `.nojekyll` is copied to `devtools-protocol/`. + +### Phase 4: Autonomous Testing Suite +- [ ] Add `test/stubs.test.js` to assert build output and stub integrity. +- [ ] Add `test/e2e.test.js` (headless browser) covering legacy redirect, search, cross-references, and mobile drawer. +- [ ] Wire test scripts into `npm test` (`node --test test/*.test.js`). ### Phase 5: Cleanup & Deprecation -- [ ] Remove obsolete SSG dependencies from [`package.json`](./package.json): - - `@11ty/eleventy` - - `@11ty/eleventy-plugin-handlebars` - - `marked` - - `liquidjs` - - `rollup` and plugins (if bundling is unnecessary for vanilla JS) -- [ ] Remove deprecated scripts and templates: - - [`generate-sidenav-html.cjs`](./generate-sidenav-html.cjs) - - [`make-stable-protocol.cjs`](./make-stable-protocol.cjs) - - [`create-search-index.cjs`](./create-search-index.cjs) - - [`pages/`](./pages/) directory (once legacy SSG is fully decommissioned) -- [ ] Update [`readme.md`](./readme.md) documentation to reflect the modern vanilla viewer architecture and local dev workflow. +- [ ] Remove obsolete SSG dependencies (`@11ty/eleventy`, `marked`, `rollup`, `liquidjs`, etc.). +- [ ] Delete dead scripts (`generate-sidenav-html.cjs`, `make-stable-protocol.cjs`, `create-search-index.cjs`). +- [ ] Remove `pages/` directory. +- [ ] Update `readme.md` with modern setup instructions and local development guide. From 9472f36b2ecd1725ef8b03dfb42a6f1973888800 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 8 Sep 2026 08:47:01 -0700 Subject: [PATCH 03/51] docs: move version selector to bottom of leftnav in plan --- PLAN.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/PLAN.md b/PLAN.md index 162a217fe..5389db25b 100644 --- a/PLAN.md +++ b/PLAN.md @@ -37,15 +37,11 @@ This project transitions the Chrome DevTools Protocol Viewer ([`chromedevtools.g ### 3.1 Header (44px DevTools Utility Bar) ```text +---------------------------------------------------------------------------------------------------------+ -| [⚙] DevTools Protocol [tot ▾] [⚡Exp] | [ 🔍 Search protocol... / ] | [📖] [🐙] | +| [⚙] DevTools Protocol [⚡Exp] | [ 🔍 Search protocol... / ] | [📖] [🐙] | +---------------------------------------------------------------------------------------------------------+ ``` -- **Brand & Target**: Logo + title (`#/` home). -- **Version Selector (`tot ▾`)**: - - `● tot (tip-of-tree)` *(Default)* - - `○ stable (1.3)` - - `○ v8 inspector` -- **Experimental Toggle (`[⚡Exp]`)**: Checkbox pill visible in `tot` mode to toggle experimental APIs. +- **Brand**: Logo + title (`#/` home). +- **Experimental Toggle (`[⚡Exp]`)**: Checkbox pill to toggle experimental APIs on/off. - **Global Search**: - Auto-focused by typing *any* character anywhere on the page, or pressing `/` or `Cmd+K`. - Trailing `/` shortcut indicator. @@ -55,6 +51,14 @@ This project transitions the Chrome DevTools Protocol Viewer ([`chromedevtools.g ### 3.2 Leftnav Sidebar & Mobile Drawer - **In-Sidebar Domain Filter**: Sticky 24px filter input (`Filter domains...`) at the top of the sidebar for quickly narrowing down the ~50 domains. - **Visual Badges**: Muted amber `[EXP]` badge for experimental domains. +- **Target / Version Switcher (Bottom Pinned Footer)**: + - Positioned discretely at the bottom of the leftnav domain list: + `[ Protocol: Tip-of-Tree ▾ ]` + - Opens select/menu for: + - `● Tip-of-Tree (latest)` *(Default)* + - `○ Stable (1.3)` + - `○ V8 Inspector (Node.js)` + - Keeps legacy/specialized versions available without cluttering the top header. - **Mobile Responsive Drawer (`< 800px`)**: - Hamburger icon `[☰]` in the header. - Smooth off-canvas slide-out drawer (`transform: translateX(-100%)`) with backdrop overlay. From 526d9e03313f7a78e4f38089d3694b342efd7a3b Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 8 Sep 2026 08:54:19 -0700 Subject: [PATCH 04/51] vendor: import vanilla-protocol-viewer baseline assets into src --- src/favicons/android-chrome-192x192.png | Bin 0 -> 36692 bytes src/favicons/android-chrome-384x384.png | Bin 0 -> 81624 bytes src/favicons/apple-touch-icon.png | Bin 0 -> 34571 bytes src/favicons/browserconfig.xml | 9 + src/favicons/favicon-16x16.png | Bin 0 -> 12768 bytes src/favicons/favicon-32x32.png | Bin 0 -> 13648 bytes src/favicons/favicon.ico | Bin 0 -> 15086 bytes src/favicons/mstile-150x150.png | Bin 0 -> 15308 bytes src/favicons/site.webmanifest | 19 + src/fuzzy_search.js | 154 ++++++ src/github.png | Bin 0 -> 1571 bytes src/home.svg | 4 + src/ic_check_box_black_24px.svg | 4 + src/ic_check_box_outline_blank_black_24px.svg | 4 + src/index.html | 72 +++ src/main.js | 357 ++++++++++++++ src/protocol_renderer.js | 301 ++++++++++++ src/search.js | 340 +++++++++++++ src/style.css | 450 ++++++++++++++++++ src/utilities.js | 95 ++++ 20 files changed, 1809 insertions(+) create mode 100644 src/favicons/android-chrome-192x192.png create mode 100644 src/favicons/android-chrome-384x384.png create mode 100644 src/favicons/apple-touch-icon.png create mode 100644 src/favicons/browserconfig.xml create mode 100644 src/favicons/favicon-16x16.png create mode 100644 src/favicons/favicon-32x32.png create mode 100644 src/favicons/favicon.ico create mode 100644 src/favicons/mstile-150x150.png create mode 100644 src/favicons/site.webmanifest create mode 100644 src/fuzzy_search.js create mode 100644 src/github.png create mode 100644 src/home.svg create mode 100644 src/ic_check_box_black_24px.svg create mode 100644 src/ic_check_box_outline_blank_black_24px.svg create mode 100644 src/index.html create mode 100644 src/main.js create mode 100644 src/protocol_renderer.js create mode 100644 src/search.js create mode 100644 src/style.css create mode 100644 src/utilities.js diff --git a/src/favicons/android-chrome-192x192.png b/src/favicons/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..aebc9d71699b5c0b729bf3efc0983774b12da48d GIT binary patch literal 36692 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
+
+
+
+ + diff --git a/src/main.js b/src/main.js new file mode 100644 index 000000000..1606ca0a9 --- /dev/null +++ b/src/main.js @@ -0,0 +1,357 @@ +let PROTOCOLS = { + 'browser_protocol.json': 'https://cdn.jsdelivr.net/gh/ChromeDevTools/devtools-protocol@master/json/browser_protocol.json', + 'js_protocol.json': 'https://cdn.jsdelivr.net/gh/ChromeDevTools/devtools-protocol@master/json/js_protocol.json', +}; + +document.addEventListener('DOMContentLoaded', () => { + let sidebar = document.getElementById('sidebar'); + let content = document.getElementById('content'); + + window.app = new App(sidebar, content); + + document.body.addEventListener('click', event => { + if (!(event.target.nodeName === 'A' && event.target.hostname === window.location.hostname)) + return; + event.preventDefault(); + window.app.navigate(event.target.search); + }, false); + +}); + +class App { + /** + * @param {!Element} sidebarElement + * @param {!Element} contentElement + */ + constructor(sidebarElement, contentElement) { + this._sidebarElement = sidebarElement; + this._contentElement = contentElement; + /** @type {!Map} */ + this._domains = new Map(); + /** @type {!Map} */ + this._allDomains = new Map(); + /** @type {!Map} */ + this._stableDomains = new Map(); + this._search = new Search(document.getElementById('search'), document.getElementById('sresults')); + this._router = new Router(route => this._renderError); + this._router.setRoute(/^(\w+)(?:\.(\w+))?$/, (route, domain, method) => this._onNavigateDomain(route, domain, method)); + this._router.setRoute(/^$/, this._onNavigateHome.bind(this)); + this._initialize(); + + let homeButton = document.querySelector('.home'); + homeButton.addEventListener('click', event => { + event.consume(); + this._router.navigate(''); + }, false); + } + + focusContent() { + this._contentElement.focus(); + } + + /** + * @param {string} route + */ + navigate(route) { + this._router.navigate(route); + } + + static _nameProperty(collectionName) { + switch (collectionName) { + case 'domains': + return 'domain'; + case 'types': + return 'id'; + case 'commands': + case 'events': + case 'parameters': + case 'returns': + return 'name'; + } + return null; + } + + /** + * @param {*} object + * @param {boolean} alreadyExperimental + */ + static _normalize(object, alreadyExperimental) { + if (typeof object !== 'object') + return object; + const result = {}; + if (alreadyExperimental && object.experimental) + delete object.experimental; + for (let key in object) { + let value = object[key]; + if (value instanceof Array) { + result[key] = value.map(item => App._normalize(item, alreadyExperimental || object.experimental)); + let property = App._nameProperty(key); + if (property !== null) { + result[key].sort((a, b) => { + if (a.experimental !== b.experimental) + return a.experimental ? 1 : -1; + if (a.deprecated !== b.deprecated) + return a.deprecated ? 1 : -1; + if (a.optional !== b.optional) + return a.optional ? 1 : -1; + if (!property) + return 0; + return a[property].localeCompare(b[property]); + }); + } + } else { + result[key] = App._normalize(value, alreadyExperimental || object.experimental); + } + } + return result; + } + + static _normalizeProtocol(protocol) { + protocol = App._normalize(protocol); + // Assign empty arrays to otherwise undefined fields. + for (let domain of protocol.domains) { + domain.commands = domain.commands || []; + domain.events = domain.events || []; + domain.types = domain.types || []; + for (let event of domain.events) { + event.parameters = event.parameters || []; + event.returns = event.returns || []; + } + for (let command of domain.commands) { + command.parameters = command.parameters || []; + command.returns = command.returns || []; + } + } + return protocol; + } + + static _computeBackReferences(domains) { + // Compute back references. + const typeidToType = new Map(); + for (let domain of domains) { + for (let type of domain.types) { + type.referencedBy = []; + typeidToType.set(domain.domain + '.' + type.id, type); + } + } + + for (let domain of domains) { + for (let command of domain.commands) { + const args = command.parameters.concat(command.returns); + for (let arg of args) { + const typeId = getReferencedType(domain.domain, arg); + const type = typeidToType.get(typeId); + if (!type) + continue; + type.referencedBy.push({ + type: 'command', + name: domain.domain + '.' + command.name + }); + } + } + for (let event of domain.events) { + const args = event.parameters.concat(event.returns); + for (let arg of args) { + const typeId = getReferencedType(domain.domain, arg); + const type = typeidToType.get(typeId); + if (!type) + continue; + type.referencedBy.push({ + type: 'event', + name: domain.domain + '.' + event.name + }); + } + } + for (let type of domain.types) { + if (!type.properties) + continue; + for (let arg of type.properties) { + const typeId = getReferencedType(domain.domain, arg); + const referencedType = typeidToType.get(typeId); + if (!referencedType) + continue; + referencedType.referencedBy.push({ + type: 'type', + name: domain.domain + '.' + type.id + }); + } + } + } + for (let type of typeidToType.values()) { + // Dedupe and sort backreferences + let map = new Map(); + for (let reference of type.referencedBy) + map.set(reference.name, reference); + type.referencedBy = Array.from(map.values()); + type.referencedBy.sort((a, b) => a.name.localeCompare(b.name)); + } + + function getReferencedType(domainName, parameter) { + if (parameter.$ref) + return parameter.$ref.includes('.') ? parameter.$ref : domainName + '.' + parameter.$ref; + if (parameter.type === 'array') + return getReferencedType(domainName, parameter.items); + return null; + } + } + + /** + * @param {*} object + */ + static _stabilize(object) { + if (typeof object !== 'object') + return object; + + const result = {}; + for (let key in object) { + let value = object[key]; + if (value instanceof Array) + result[key] = value.filter(item => item.experimental !== true).map(App._stabilize); + else + result[key] = App._stabilize(value); + } + return result; + } + + async _initialize() { + let protocols = await Promise.all(Object.values(PROTOCOLS).map(url => fetch(url).then(r => r.json()))); + + this._allDomains.clear(); + this._stableDomains.clear(); + + let protocol = App._normalizeProtocol({domains: [].concat(...protocols.map(p => p.domains))}); + + for (let domain of protocol.domains) { + this._allDomains.set(domain.domain, domain); + if (!domain.experimental) + this._stableDomains.set(domain.domain, App._stabilize(domain)); + } + App._computeBackReferences(Array.from(this._allDomains.values())); + App._computeBackReferences(Array.from(this._stableDomains.values())); + + document.body.classList.toggle('experimental-enabled', window.localStorage['experimental'] !== 'false'); + document.getElementById('experimental').addEventListener('click', () => { + window.localStorage['experimental'] = window.localStorage['experimental'] === 'false' ? 'true' : 'false'; + document.body.classList.toggle('experimental-enabled', window.localStorage['experimental'] !== 'false'); + this._renderDomains(); + }); + + this._renderDomains(); + } + + _renderDomains() { + this._domains = window.localStorage['experimental'] === 'false' ? this._stableDomains : this._allDomains; + this._search.setDomains(Array.from(this._domains.values())); + this._renderSidebar(this._domains); + this._router.navigate(this._router.route()); + } + + _onNavigateDomain(route, domain, method) { + document.title = route; + this._search.setDefaultValue(route); + this._search.cancelSearch(); + this._contentElement.textContent = ''; + var active = this._sidebarElement.querySelector('.active-link'); + if (active) + active.classList.remove('active-link'); + if (!this._domains.has(domain)) { + this._contentElement.appendChild(renderError(`Unknown domain: ${domain}. Enable experimental domains above?`)); + return; + } + let link = this._sidebarElement.querySelector(`[href='?${domain}']`); + if (link) + link.classList.add('active-link'); + let render = ProtocolRenderer.renderDomain(this._domains.get(domain)); + if (render) { + this._contentElement.appendChild(render); + let elem = render.querySelector('#' + ProtocolRenderer.titleId(domain, method)); + if (elem) + elem.scrollIntoView(); + else + this._contentElement.scrollTop = 0; + } + else if (!method) + this._contentElement.scrollTop = 0; + this._contentElement.focus(); + } + + _onNavigateHome() { + document.title = 'Vanilla Protocol Viewer'; + this._search.setDefaultValue(''); + this._search.cancelSearch(); + this._contentElement.textContent = ''; + var template = document.querySelector('#landing'); + var clone = document.importNode(template.content, true); + this._contentElement.appendChild(clone); + } + + _renderSidebar(domains) { + let domainNames = Array.from(domains.keys()); + let sidebar = document.getElementById('sidebar'); + sidebar.textContent = ''; + for (let name of domainNames) { + let a = sidebar.a('?' + name, name); + a.classList.add('domain-link'); + ProtocolRenderer.applyBackground(domains.get(name), a); + } + } +} + +class Router { + /** + * @param {function(?)} unknownRouteHandler + */ + constructor(unknownRouteHandler) { + this._routes = new Map(); + this._unknownRouteHandler = unknownRouteHandler; + window.addEventListener("popstate", () => this._processRoute()); + } + + /** + * @param {!RegExp} regex + * @param {function(?)} handler + */ + setRoute(regex, handler) { + this._routes.set(regex, handler) + } + + /** + * @param {string} route + */ + navigate(route) { + if (window.location.search !== route) + window.history.pushState({}, route, route); + this._processRoute(); + } + + /** + * @return {string} + */ + route() { + // Support for old hash-based navigation. + if (window.location.hash.length > 1) + return '?' + window.location.hash.substring(1); + return window.location.search; + } + + _processRoute() { + let route = (window.location.search || '?').substring(1); + gtag('config', 'UA-138239427-1', {'page_path': '?' + route}); + for (let regex of this._routes.keys()) { + var matches = route.match(regex); + if (matches) { + this._routes.get(regex).call(null, route, ...matches.slice(1)); + return; + } + } + this._unknownRouteHandler.call(null, route); + } +} + +function renderError(error) { + let main = E.box(); + let box = main.div('box-content'); + box.el('h2', '', 'Error'); + box.p('', error); + return main; +} diff --git a/src/protocol_renderer.js b/src/protocol_renderer.js new file mode 100644 index 000000000..da296c717 --- /dev/null +++ b/src/protocol_renderer.js @@ -0,0 +1,301 @@ +class ProtocolRenderer { + static titleId(domainName, domainEntry) { + return domainName + '_' + domainEntry; + } + + static renderDomain(domain) { + let result = E.div(); + let main = result.div('domain'); + ProtocolRenderer.applyBackground(domain, main); + let padding = result.div('domain-padding', '\u2606'); + { + // Render domain main description. + let container = main.div('box'); + let header = container.div('box-content'); + + let title = header.el('h2'); + title.textContent = domain.domain; + + let description = header.el('p'); + description.textContent = domain.description || ''; + ProtocolRenderer.applyMarks(domain, title); + ProtocolRenderer.renderTableOfContents(domain, header) + } + + if (domain.commands.length) { + // Render methods. + let title = main.el('h3'); + title.textContent = 'Methods'; + let container = main.box('box'); + for (let method of domain.commands) + container.appendChild(ProtocolRenderer.renderEventOrMethod(domain, method, false)); + } + + if (domain.events.length) { + // Render events. + let title = main.el('h3'); + title.textContent = 'Events'; + let container = main.box('box'); + for (let event of domain.events) + container.appendChild(ProtocolRenderer.renderEventOrMethod(domain, event, true)); + } + + if (domain.types.length) { + // Render types. + let title = main.el('h3'); + title.textContent = 'Types'; + let container = main.box('box'); + for (let type of domain.types) + container.appendChild(ProtocolRenderer.renderDomainType(domain, type)); + } + + return result; + } + + static renderDomainType(domain, type) { + let main = E.div('type'); + ProtocolRenderer.applyBackground(type, main); + ProtocolRenderer.applyBackground(domain, main); + main.appendChild(ProtocolRenderer.renderTitle(domain.domain, type.id, type, 'type')); + if (type.type) { + main.el('p', '', 'Type: ') + .text(type.type, 'parameter-type'); + } + { + // Render description. + let p = main.el('p'); + p.textContent = type.description || ''; + } + if (type.properties && type.properties.length) { + // Render parameters. + let title = main.el('h5'); + title.textContent = 'Properties'; + let container = main.el('dl', 'parameter-list'); + for (let parameter of type.properties) + container.appendChild(ProtocolRenderer.renderParameter(domain, parameter)); + } + if (type.enum) { + main.el('h5', '', 'Allowed values'); + main.el('p', '', type.enum.join(', ')); + } + if (type.referencedBy && type.referencedBy.length) { + // Render back references. + let title = main.el('h5'); + title.textContent = 'Referenced By'; + let container = main.el('ul', 'references-list'); + for (let reference of type.referencedBy) { + const li = container.el('li'); + const referenceIcon = li.span('reference-icon'); + if (reference.type === 'command') + referenceIcon.appendChild(ProtocolRenderer.renderMethodIcon()); + else if (reference.type === 'event') + referenceIcon.appendChild(ProtocolRenderer.renderEventIcon()); + else if (reference.type === 'type') + referenceIcon.appendChild(ProtocolRenderer.renderTypeIcon()); + + li.appendChild(ProtocolRenderer.renderRef(reference.name)); + } + } + + return main; + } + + static renderTitle(domainName, title, item, titleType) { + // Render heading. + let heading = E.el('h4', 'monospace text-overflow'); + + if (titleType === 'type') + heading.appendChild(ProtocolRenderer.renderTypeIcon()); + else if (titleType === 'event') + heading.appendChild(ProtocolRenderer.renderEventIcon()); + else if (titleType === 'method') + heading.appendChild(ProtocolRenderer.renderMethodIcon()); + + let id = `${domainName}.${title}`; + heading.setAttribute('id', ProtocolRenderer.titleId(domainName, title)); + heading.text(domainName + '.', 'method-domain'); + heading.text(title, 'method-name'); + ProtocolRenderer.applyMarks(item, heading); + heading.a('?' + id, '#').classList.add('title-link'); + return heading; + } + + static renderTableOfContents(domain, container) { + let renderEventOrMethodEntry = (method, container) => + ProtocolRenderer.renderTableOfContentsEntry(domain.domain, method.name, container); + let renderTypeEntry = (type, container) => + ProtocolRenderer.renderTableOfContentsEntry(domain.domain, type.id, container); + + if (domain.commands.length) + ProtocolRenderer.renderTableOfContentsSection("Methods", domain.commands, renderEventOrMethodEntry, container); + if (domain.events.length) + ProtocolRenderer.renderTableOfContentsSection("Events", domain.events, renderEventOrMethodEntry, container); + if (domain.types.length) + ProtocolRenderer.renderTableOfContentsSection("Types", domain.types, renderTypeEntry, container); + } + + static renderTableOfContentsSection(sectionName, entries, renderer, container) { + let title = container.el('h4'); + title.textContent = sectionName; + let section = container.div('div'); + for (let entry of entries) { + let row = renderer(entry, section); + ProtocolRenderer.applyMarks(entry, row); + } + return section; + } + + static renderTableOfContentsEntry(domainName, name, container) { + let row = container.div('div'); + let id = `${domainName}.${name}`; + let link = ProtocolRenderer.renderRef(id); + link.classList.add('monospace'); + row.appendChild(link); + return row; + } + + static renderEventOrMethod(domain, method, isEvent) { + let main = E.div('method'); + ProtocolRenderer.applyBackground(method, main); + ProtocolRenderer.applyBackground(domain, main); + main.appendChild(ProtocolRenderer.renderTitle(domain.domain, method.name, method, isEvent ? 'event' : 'method')); + { + // Render description. + let p = main.el('p'); + p.textContent = method.description || ''; + } + if (method.parameters.length) { + // Render parameters. + let title = main.el('h5'); + title.textContent = 'Parameters'; + let container = main.el('dl', 'parameter-list'); + for (let parameter of method.parameters) + container.appendChild(ProtocolRenderer.renderParameter(domain, parameter)); + } + if (method.returns.length) { + // Render return values. + let title = main.el('h5'); + title.textContent = 'RETURN OBJECT'; + let container = main.el('dl', 'parameter-list'); + for (let parameter of method.returns) + container.appendChild(ProtocolRenderer.renderParameter(domain, parameter)); + } + return main; + } + + static renderParameter(domain, parameter) { + let main = document.createDocumentFragment(); + { + // Render parameter name. + let name = main.div('parameter-name monospace'); + ProtocolRenderer.applyBackground(parameter, name); + if (parameter.optional) + name.classList.add('optional'); + name.textContent = parameter.name; + } + { + // Render parameter value. + let container = main.vbox('parameter-value'); + ProtocolRenderer.applyBackground(parameter, container); + container.appendChild(ProtocolRenderer.renderTypeLink(domain, parameter)); + let description = container.span('parameter-description'); + let descriptions = []; + if (parameter.description) + descriptions.push(parameter.description); + description.textContent = descriptions.join(' '); + if (parameter.enum) { + description.append(' Allowed values: '); + parameter.enum.forEach((value, index) => { + const code = document.createElement('code'); + code.textContent = value; + description.append(code); + if (index < parameter.enum.length - 1) + description.append(', '); + else + description.append('.'); + }); + } + ProtocolRenderer.applyMarks(parameter, description); + } + return main; + } + + static renderTypeLink(domain, parameter) { + const primitiveTypes = new Set([ + "string", + "integer", + "boolean", + "number", + "object", + "any" + ]); + + if (primitiveTypes.has(parameter.type)) + return E.span('parameter-type', parameter.type); + if (parameter.$ref) { + let $ref = parameter.$ref; + if (!$ref.includes('.')) + $ref = domain.domain + '.' + parameter.$ref; + return ProtocolRenderer.renderRef($ref); + } + if (parameter.type === 'array') { + let generic = E.span('parameter-type'); + generic.text('array [ '); + generic.appendChild(ProtocolRenderer.renderTypeLink(domain, parameter.items)); + generic.text(' ]'); + return generic; + } + return E.el('span', 'parameter-type', ''); + } + + static renderRef($ref) { + let a = E.el('a', 'parameter-type'); + a.href = '?' + $ref; + a.textContent = $ref; + return a; + } + + static applyBackground(item, element) { + if (item.experimental) + element.classList.add('experimental-bg'); + else if (item.deprecated) + element.classList.add('deprecated-bg'); + } + + static applyMarks(item, element) { + if (item.experimental) { + let e = element.span('experimental', 'experimental'); + e.title = 'This may be changed, moved or removed'; + element.appendChild(e); + } else if (item.deprecated) { + let e = element.span('deprecated', 'deprecated'); + e.title = 'Deprecated, will be removed'; + element.appendChild(e); + } + } + + static renderMethodIcon() { + let icon = E.span('entity-icon'); + icon.textContent = 'Method'; + icon.title = 'Method'; + icon.classList.add('entity-icon-method'); + return icon; + } + + static renderTypeIcon() { + let icon = E.span('entity-icon'); + icon.textContent = 'Type'; + icon.title = 'Type'; + icon.classList.add('entity-icon-type'); + return icon; + } + + static renderEventIcon() { + let icon = E.span('entity-icon'); + icon.textContent = 'Event'; + icon.title = 'Event'; + icon.classList.add('entity-icon-event'); + return icon; + } +}; + diff --git a/src/search.js b/src/search.js new file mode 100644 index 000000000..703d9d44e --- /dev/null +++ b/src/search.js @@ -0,0 +1,340 @@ +// Number of search results to render immediately. +const SEARCH_RENDER_COUNT = 50; + +class Search { + /** + * @param {!Element} searchHeader + * @param {!Element} resultsElement + */ + constructor(searchHeader, resultsElement) { + this._searchInput = searchHeader.querySelector('input'); + /** @type {!Array} */ + this._items = []; + /** @type {!Set} */ + this._domainNames = new Set(); + this._selectedElement = null; + this._searchInput.addEventListener('input', this._onInput.bind(this), false); + this._searchInput.addEventListener('keydown', this._onKeyDown.bind(this), false); + this._resultsElement = resultsElement; + + // Activate search on any keypress + document.addEventListener('keypress', event => { + if (this._searchInput === document.activeElement) + return; + if (/\S/.test(event.key)) { + if (event.key !== '.') + this._searchInput.value = ''; + this._searchInput.focus(); + } + }); + // Activate search on backspace + document.addEventListener('keydown', event => { + if (this._searchInput === document.activeElement) + return; + if (event.keyCode === 8 || event.keyCode === 46) + this._searchInput.focus(); + }); + // Activate on paste + document.addEventListener('paste', event => { + if (this._searchInput === document.activeElement) + return; + this._searchInput.focus(); + }); + document.addEventListener('click', event => { + if (this._searchInput.contains(event.target)) + return; + let searchItem = event.target.selfOrParentWithClass('search-item'); + if (searchItem) { + event.consume(); + this.cancelSearch(); + app.navigate(searchItem.__route); + return; + } + }); + } + + setDomains(domains) { + this._domainNames.clear(); + this._items = []; + for (var domain of domains) { + this._domainNames.add(domain.domain.toLowerCase()); + for (var command of (domain.commands || [])) { + let item = new Search.Item(domain.domain /* domainName */, + command.name /* domainEntry */, + Search.ItemType.Method /* itemType */, + command.description /* description */); + this._items.push(item); + } + for (var event of (domain.events || [])) { + let item = new Search.Item(domain.domain /* domainName */, + event.name /* domainEntry */, + Search.ItemType.Event /* itemType */, + event.description /* description */); + this._items.push(item); + } + for (var type of (domain.types || [])) { + let item = new Search.Item(domain.domain /* domainName */, + type.id /* domainEntry */, + Search.ItemType.Type /* itemType */, + type.description /* description */); + this._items.push(item); + } + } + } + + cancelSearch() { + this._searchInput.blur(); + this._resultsElement.style.setProperty('display', 'none'); + this._searchInput.value = this._defaultValue; + app.focusContent(); + } + + setDefaultValue(value) { + this._defaultValue = value; + } + + _onInput() { + this._selectedElement = null; + this._resultsElement.style.setProperty('display', 'block'); + let query = this._searchInput.value.trim(); + let items = this._items; + let results = this._doSearch(items, query); + if (results.length === 0) { + this._renderMessage('Nothing is found.'); + return; + } + this._resultsElement.textContent = ''; + if (!query) + this._addNavigateHomeItem(); + for (let i = 0; i < Math.min(results.length, SEARCH_RENDER_COUNT); ++i) + this._resultsElement.appendChild(renderSearchResult(results[i])); + this._addAllResultsButtonIfNeeded(results); + this._selectedElement = this._resultsElement.firstChild; + if (this._selectedElement) + this._selectedElement.classList.add('selected'); + } + + _addNavigateHomeItem() { + let main = this._resultsElement.hbox('search-item', `Navigate Home`); + main.classList.add('custom-search-result'); + main.classList.add('monospace'); + main.__route = '?'; + return main; + } + + _addAllResultsButtonIfNeeded(results) { + let remainingResults = results.length - SEARCH_RENDER_COUNT; + if (remainingResults <= 0) + return; + let main = this._resultsElement.hbox('search-item', `Show Remaining ${remainingResults} Results...`); + main.classList.add('custom-search-result'); + main.classList.add('monospace'); + main.addEventListener('click', event => { + event.consume(); + for (let i = SEARCH_RENDER_COUNT; i < results.length; ++i) + this._resultsElement.appendChild(renderSearchResult(results[i])); + let next = main.nextSibling; + main.remove(); + this._selectElement(next); + this._searchInput.focus(); + }, false); + return main; + } + + /** + * @param {string} text + */ + _renderMessage(text) { + this._resultsElement.textContent = ''; + this._resultsElement.box('search-results-message') + .el('h4', '', text); + } + + /** + * @param {!Array} items + * @param {string} query + * @return {!Array} + */ + _doSearch(items, query) { + let results = []; + if (!query) { + for (let item of items) + results.push(new Search.SearchResult(item, 0, [])); + return results; + } + + let fuzzySearch = new FuzzySearch(query); + for (let item of items) { + let matches = []; + let score = fuzzySearch.score(item.title, matches); + if (score === 0) + continue; + results.push(new Search.SearchResult(item, score, matches)); + } + results.sort((a, b) => { + const scoreDiff = b.score - a.score; + if (scoreDiff) + return scoreDiff; + // Prefer left-most search results. + const startDiff = a.matches[0] - b.matches[0]; + if (startDiff) + return startDiff; + return a.item.title.length - b.item.title.length; + }); + return results; + } + + _onKeyDown(event) { + if (event.key === 'Escape' || event.keyCode === 27) { + event.consume(); + this.cancelSearch(); + } else if (event.key === 'ArrowDown') { + this._selectNext(event); + } else if (event.key === 'ArrowUp') { + this._selectPrevious(event); + } else if (event.key === 'Enter' || event.key === ' ') { + event.consume(); + if (this._selectedElement); + this._selectedElement.click(); + } + } + + _selectNext(event) { + if (!this._selectedElement) + return; + event.consume(); + let next = this._selectedElement.nextSibling; + if (!next) + next = this._resultsElement.firstChild; + this._selectElement(next); + } + + _selectPrevious(event) { + if (!this._selectedElement) + return; + event.consume(); + let previous = this._selectedElement.previousSibling; + if (!previous) + previous = this._resultsElement.lastChild; + this._selectElement(previous); + } + + _selectElement(item) { + if (this._selectedElement) + this._selectedElement.classList.remove('selected'); + this._selectedElement = item; + if (this._selectedElement) { + this._selectedElement.scrollIntoViewIfNeeded(false); + this._selectedElement.classList.add('selected'); + } + } +} + +/** + * @param {!Search.SearchResult} searchResult + * @return {!Element} + */ +function renderSearchResult(searchResult) { + let item = searchResult.item; + let main = E.hbox('search-item'); + let icon = main.el('span'); + icon.classList.add('search-item-icon'); + // Render icon + if (item.type === Search.ItemType.Method) { + icon.appendChild(ProtocolRenderer.renderMethodIcon()); + } else if (item.type === Search.ItemType.Type) { + icon.appendChild(ProtocolRenderer.renderTypeIcon()); + } else if (item.type === Search.ItemType.Event) { + icon.appendChild(ProtocolRenderer.renderEventIcon()); + } + { + // Render Name and Description + let container = main.div('search-item-main'); + let p1 = container.el('div', 'search-item-title monospace'); + let domainElement = p1.span('search-item-title-domain'); + domainElement.appendChild(renderTextWithMatches(item.title, searchResult.matches, 0, item.domainName.length + 1)); + p1.appendChild(renderTextWithMatches(item.title, searchResult.matches, item.domainName.length + 1, item.title.length)); + let p2 = container.el('div', 'search-item-description'); + p2.textContent = item.description; + } + main.__route = item.route; + return main; +} + +/** + * @param {string} text + * @param {!Array} matches + * @param {number} fromIndex + * @param {number} fromIndex + * @return {!Element} + */ +function renderTextWithMatches(text, matches, fromIndex, toIndex) { + if (!matches.length) + return E.textNode(text.substring(fromIndex, toIndex)); + let result = document.createDocumentFragment(); + let insideMatch = false; + let currentIndex = fromIndex; + let matchIndex = new Set(matches); + for (let i = fromIndex; i < toIndex; ++i) { + if (insideMatch !== matchIndex.has(i)) { + add(currentIndex, i, insideMatch); + insideMatch = matchIndex.has(i); + currentIndex = i; + } + } + add(currentIndex, toIndex, insideMatch); + return result; + + /** + * @param {number} from + * @param {number} to + * @param {boolean} isHighlight + */ + function add(from, to, isHighlight) { + if (to === from) + return; + if (isHighlight) + result.span('search-highlight', text.substring(from, to)); + else + result.textNode(text.substring(from, to)); + } +} + +/** @enum */ +Search.ItemType = { + Method: Symbol('Method'), + Type: Symbol('Type'), + Event: Symbol('Event'), +} + +Search.Item = class { + /** + * @param {string} domainName + * @param {string} domainEntry + * @param {!Search.ItemType} itemType + * @param {string} description + * @param {string} route + */ + constructor(domainName, domainEntry, itemType, description) { + this.domainName = domainName; + this.domainEntry = domainEntry; + this.type = itemType; + this.description = description || ''; + this.title = this.domainName + '.' + this.domainEntry; + this.route = `?${domainName}.${domainEntry}` + } +} + +Search.SearchResult = class { + /** + * @param {!Search.Item} item + * @param {number} score + * @param {!Array} matches + */ + constructor(item, score, matches) { + this.item = item; + this.score = score; + this.matches = matches; + } +} + diff --git a/src/style.css b/src/style.css new file mode 100644 index 000000000..128934c0a --- /dev/null +++ b/src/style.css @@ -0,0 +1,450 @@ +:root { + --sidebar-width: 200px; + --search-height: 50px; + --hover-color: rgba(225, 245, 254, 0.49); + --parameters-grid-template-columns: minmax(250px, max-content) auto; +} + +@media only screen and (max-width : 800px) { + :root { + --sidebar-width: 0px; + --parameters-grid-template-columns: max-content auto; + } + + #sidebar { + display: none; + } + + #content .domain { + padding: 2px; + } +} + +@media only screen and (max-width : 400px) { + input[type=search] { + width: 250px !important; + } +} + +body { + color: #333; + background-color: #fafafa; + font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; + padding: 0; + margin: 0; +} + +p { + margin: 0.5rem 0 1rem; +} + +h4 { + /* use padding to make sure element has some space on top when + * scrollIntoViewportIfNeede() is called */ + margin-top: 0; + padding-top: 1.33em; +} + +h5 { + color: gray; + font-weight: 300; + text-transform: uppercase; + letter-spacing: 1px; + margin: 1rem 0 0; +} + +a { + font-weight: normal !important; + color: #333; +} + +code { + font-family: Consolas, Menlo, monospace; + color: #9C27B0; +} + +#sidebar { + background: #fff; + position: absolute; + width: var(--sidebar-width); + top: 0; + bottom: 0; + left: 0; + overflow-y: scroll; + border-right: 1px solid rgba(0, 0, 0, 0.14); + overflow-x: hidden; +} + +#search { + background: #3f51b5; + position: absolute; + left: var(--sidebar-width); + right: 0; + top: 0; + height: var(--search-height); + border-bottom: 1px solid rgba(0, 0, 0, 0.14); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +#search .home { + padding: 10px; +} + +#search .github { + transform: scale(0.85); +} + +#search .toolbar-button { + opacity: 0.5; +} + +#search .toolbar-button:hover { + opacity: 0.85; +} + +#search .toolbar-button:active { + opacity: 1; +} + +::-webkit-input-placeholder { /* Chrome/Opera/Safari */ + color: rgba(255, 255, 255, 0.5); +} + +input[type=search]:focus { + outline: none; +} + +input[type=search] { + -webkit-appearance: none; + margin: 0 auto; + background: #3f51b5; + text-align: center; + border: none; + border-bottom: 1px solid rgba(255, 255, 255, 0.52); + padding: 0 0 3px 20px; + font-size: 20px; + color: white; + width: 450px; + text-overflow: ellipsis; +} + +#experimental { + align-items: center; + display: flex; + color: white; + cursor: pointer; + opacity: 0.5; + padding: 4px 10px 0 0; +} + +#experimental img { + filter: invert(); + padding: 0 4px 0 0; +} + +#experimental-on, .experimental-enabled #experimental-off { + display: none; +} + +.experimental-enabled #experimental-on { + display: block; +} + +#sresults { + display: none; + background-color: #fff; + position: absolute; + left: var(--sidebar-width); + top: var(--search-height); + bottom: 0; + right: 0; + overflow-y: scroll; + border-top: 2px solid rgba(51, 51, 51, 0.52); + -webkit-overflow-scrolling: touch; +} + +#content { + background-color: #fafafa; + position: absolute; + left: var(--sidebar-width); + top: var(--search-height); + bottom: 0; + right: 0; + overflow-y: scroll; + -webkit-overflow-scrolling: touch; +} + +.monospace { + font-family: Consolas, Menlo, monospace; +} + +.text-overflow { + overflow: hidden; + text-overflow: ellipsis; +} + +.box { + width: 98%; + border-radius: 2px; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); + margin: 16px auto 32px auto; + background-color: white; +} + +.box-content, .method, .type { + padding: 16px 24px; +} + +.hbox { + display: flex; + flex-direction: row; + flex: 1 0 auto; +} + +.vbox { + display: flex; + flex-direction: column; + flex: 1 0 auto; +} + +.domain { + padding: 20px 48px 20px 48px; +} + +.domain-padding { + height: 75vh; + display: flex; + font-size:100px; + align-items: center; + color: #4052b4; + justify-content: center; +} + +.method-domain { + color: #ababab; +} + +.parameter-list { + padding: 0.5rem 5px; + display: grid; + grid-template-columns: var(--parameters-grid-template-columns); +} + +.parameter { + border-left: 3px solid transparent; + margin-bottom: 6px; + align-items: baseline; +} + +.parameter-name { + text-align: right; + color: black; + word-wrap: break-word; + padding: 5px 10px; +} + +.parameter-highlight { +} + +.parameter-value { + overflow: hidden; + padding: 5px 10px; +} + +.parameter-type { + font-weight: bold; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.parameter-description { + font-size: 90%; +} + +.optional::after { + content: "optional"; + opacity: .6; + font-size: 70%; + display: block; +} + +.title-link { + margin-left: 1ex; + display: none; +} + +h4:hover .title-link { + display: inline; + user-select: none; +} + +span.experimental, span.deprecated { + font-size: 70%; + text-transform: uppercase; + background-color: #e57373; + padding: 2px; + cursor: help; + color: white; + vertical-align: baseline; + font-weight: normal; + font-family: inherit; + margin-left: 1ex; + user-select: none; +} + +span.deprecated { + background-color: #ffa03b; +} + +#content .experimental-bg { + background-color: rgba(255, 212, 212, 0.2); +} + +#content .deprecated-bg { + background-color: rgba(255, 249, 187, 0.2); +} + +#sidebar .domain-link.experimental-bg { + border-color: #ffd4d4; +} + +#sidebar .domain-link.deprecated-bg { + border-color: hsla(55, 100%, 81%, 1); +} + +#sidebar .domain-link { + text-decoration: none; + display: flex; + flex-direction: row; + align-items: center; + font-size: 14px; + font-weight: 400; + line-height: 24px; + min-height: 48px; + padding: 0 16px; +} + +#sidebar .domain-link { + border-left: 10px solid transparent; +} + +#sidebar .active-link { + font-size: 16px; + font-weight: bold; + background-color: rgb(225, 245, 254); +} + +#sidebar .domain-link:hover { + background-color: var(--hover-color); +} + +#sresults .search-item.selected { + background-color: #e3f2fd; +} + +#sresults .search-item { + padding: 4px 10px; + align-items: center; + cursor: pointer; + border-bottom: 1px solid rgba(51, 51, 51, 0.12); +} + +#sresults .search-item:hover { + background-color: var(--hover-color); +} + +#sresults .search-item-title { + overflow: hidden; + text-overflow: ellipsis; +} + +#sresults .search-item-icon { + margin: 10px 18px 10px 8px; + width: 50px; + text-align: center; + flex-shrink: 0; + flex-grow: 0; +} + + +#content .entity-icon { + margin-right: 1ex; +} + +.references-list { + list-style-type: none; +} + +.references-list .entity-icon { + padding: 1px 3px; +} + +.reference-icon { + width: 55px; + display: inline-block; + vertical-align: middle; + text-align: right; +} + +#sresults .entity-icon { + width: 50px; +} + +.entity-icon { + vertical-align: top; + display: inline-block; + padding: 3px 3px; + font-size: 12px; + color: white; + border-radius: 3px; + user-select: none; +} + +.entity-icon-method { + background-color: #ffa000b5; +} + +.entity-icon-type { + background-color: #3f51b5b0; +} + +.entity-icon-event { + background-color: #2e7d32a8; +} + +#sresults .search-item-main { + flex: 1 1 auto; + max-width: 1000px; + overflow: hidden; +} + +#sresults .search-item-description { + font-size: 90%; +} + +.search-highlight { + background-color: yellow; +} + +.search-results-message { + margin: 20px auto; + max-width: 500px; + text-align: center; +} + +.custom-search-result { + justify-content: center; + height: 53px; +} + +.search-item-title-domain { + color: hsla(0, 0%, 57%, 1); +} + +.method:not(:last-child), .type:not(:last-child) { + border-bottom: double #b6b6b6a6 3px; +} diff --git a/src/utilities.js b/src/utilities.js new file mode 100644 index 000000000..2f45a020f --- /dev/null +++ b/src/utilities.js @@ -0,0 +1,95 @@ +const E = { + el: function(name, className, textContent) { + let e = document.createElement(name); + if (className) + e.className = className; + if (textContent) + e.textContent = textContent; + return e; + }, + + textNode: function(text) { + return document.createTextNode(text); + }, + + text: function(text, className, tagName = 'span') { + let e = E.el(tagName, className); + e.textContent = text; + return e; + }, + + div: function(...args) { + return E.el('div', ...args); + }, + + span: function(...args) { + return E.el('span', ...args); + }, + + p: function(...args) { + return E.el('p', ...args); + }, + + box: function(...args) { + let e = E.el('div', ...args); + e.classList.add('box'); + return e; + }, + + hbox: function(...args) { + let e = E.div(...args); + e.classList.add('hbox'); + return e; + }, + + vbox: function(...args) { + let e = E.div(...args); + e.classList.add('vbox'); + return e; + }, + + strong: function(text) { + return E.el('strong', '', text); + }, + + code: function(text) { + return E.el('code', '', text); + }, + + a: function(href, text) { + let link = E.el('a', '', text || href); + link.href = href; + return link; + }, +}; + +// Install helpers on Node.prototype +for (let helper in E) { + Node.prototype[helper] = function(...args) { + let element = E[helper].apply(null, args); + this.appendChild(element); + return element; + }; +} + +Event.prototype.consume = function() { + this.preventDefault(); + this.stopPropagation(); +} + +Array.prototype.last = function() { + return this.length ? this[this.length - 1] : undefined; +} + +Set.prototype.first = function() { + if (!this.size) + return null; + return this.values().next().value; +} + +Element.prototype.selfOrParentWithClass = function(className) { + let node = this; + while (node && !node.classList.contains(className)) + node = node.parentElement; + return node; +} From a72261f45c01112316e4048f52a52cf487cc1a40 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 8 Sep 2026 09:00:56 -0700 Subject: [PATCH 05/51] feat: adopt vanilla protocol viewer core and isomorphic model --- package.json | 1 + src/index.html | 175 +++++---- src/main.js | 658 ++++++++++++++++++------------- src/protocol-model.js | 406 ++++++++++++++++++++ src/protocol_renderer.js | 7 +- src/search.js | 43 ++- src/style.css | 743 ++++++++++++++++++++++++------------ test/protocol-model.test.js | 346 +++++++++++++++++ 8 files changed, 1787 insertions(+), 592 deletions(-) create mode 100644 src/protocol-model.js create mode 100644 test/protocol-model.test.js diff --git a/package.json b/package.json index 8769e1ad8..f81d64ede 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "build": "rimraf devtools-protocol/ && node node_modules/.bin/eleventy && rollup -c rollup.config.js", "prep": "bash prep-tot-protocol-files.sh", "test": "bash test/primitive_tests.sh", + "test:unit": "node --test test/protocol-model.test.js", "serve": "echo 'Open http://localhost:8696/devtools-protocol/ for built site'; statikk --port 8696 ." } } diff --git a/src/index.html b/src/index.html index 2f7a55304..bf09a59c6 100644 --- a/src/index.html +++ b/src/index.html @@ -1,72 +1,115 @@ -DevTools Protocol Viewer + + + + + DevTools Protocol Viewer - - - - - - - - - + + + + + + + + + + - - - - - - - - - - + + + + + -
-
-
- - - + + + + + + + +
+
+ + + + diff --git a/src/main.js b/src/main.js index 1606ca0a9..7884032e6 100644 --- a/src/main.js +++ b/src/main.js @@ -1,48 +1,91 @@ -let PROTOCOLS = { - 'browser_protocol.json': 'https://cdn.jsdelivr.net/gh/ChromeDevTools/devtools-protocol@master/json/browser_protocol.json', - 'js_protocol.json': 'https://cdn.jsdelivr.net/gh/ChromeDevTools/devtools-protocol@master/json/js_protocol.json', +/** + * @fileoverview Main Application Controller for Chrome DevTools Protocol Viewer. + */ + +import { + normalizeProtocol, + stabilize, + computeBackReferences, + parseRoute, + formatRoute, + normalizeTarget, +} from './protocol-model.js'; + +const PROTOCOL_URLS = { + browser: 'https://cdn.jsdelivr.net/gh/ChromeDevTools/devtools-protocol@master/json/browser_protocol.json', + js: 'https://cdn.jsdelivr.net/gh/ChromeDevTools/devtools-protocol@master/json/js_protocol.json', }; document.addEventListener('DOMContentLoaded', () => { - let sidebar = document.getElementById('sidebar'); - let content = document.getElementById('content'); - - window.app = new App(sidebar, content); - - document.body.addEventListener('click', event => { - if (!(event.target.nodeName === 'A' && event.target.hostname === window.location.hostname)) - return; - event.preventDefault(); - window.app.navigate(event.target.search); - }, false); - + const sidebarElement = document.getElementById('sidebar'); + const domainListElement = document.getElementById('domain-list'); + const contentElement = document.getElementById('content'); + const searchElement = document.getElementById('search'); + const searchResultsElement = document.getElementById('sresults'); + const targetSelector = document.getElementById('target-selector'); + const domainFilterInput = document.getElementById('domain-filter'); + const drawerToggle = document.getElementById('drawer-toggle'); + const drawerBackdrop = document.getElementById('drawer-backdrop'); + + window.app = new App({ + sidebarElement, + domainListElement, + contentElement, + searchElement, + searchResultsElement, + targetSelector, + domainFilterInput, + drawerToggle, + drawerBackdrop, + }); }); class App { /** - * @param {!Element} sidebarElement - * @param {!Element} contentElement + * @param {Object} elements */ - constructor(sidebarElement, contentElement) { + constructor({ + sidebarElement, + domainListElement, + contentElement, + searchElement, + searchResultsElement, + targetSelector, + domainFilterInput, + drawerToggle, + drawerBackdrop, + }) { this._sidebarElement = sidebarElement; + this._domainListElement = domainListElement; this._contentElement = contentElement; - /** @type {!Map} */ - this._domains = new Map(); - /** @type {!Map} */ - this._allDomains = new Map(); - /** @type {!Map} */ - this._stableDomains = new Map(); - this._search = new Search(document.getElementById('search'), document.getElementById('sresults')); - this._router = new Router(route => this._renderError); - this._router.setRoute(/^(\w+)(?:\.(\w+))?$/, (route, domain, method) => this._onNavigateDomain(route, domain, method)); - this._router.setRoute(/^$/, this._onNavigateHome.bind(this)); - this._initialize(); + this._targetSelector = targetSelector; + this._domainFilterInput = domainFilterInput; + this._drawerToggle = drawerToggle; + this._drawerBackdrop = drawerBackdrop; - let homeButton = document.querySelector('.home'); - homeButton.addEventListener('click', event => { - event.consume(); - this._router.navigate(''); - }, false); + /** @type {'tot'|'stable'|'v8'} */ + this._currentTarget = 'tot'; + /** @type {string|null} */ + this._currentDomain = null; + + /** @type {Map} */ + this._activeDomains = new Map(); + + /** @type {Record<'tot'|'stable'|'v8', { all: Map, stable: Map }>} */ + this._targetStore = { + tot: { all: new Map(), stable: new Map() }, + stable: { all: new Map(), stable: new Map() }, + v8: { all: new Map(), stable: new Map() }, + }; + + this._search = new Search(searchElement, searchResultsElement); + + this._setupDrawerEvents(); + this._setupSidebarEvents(); + this._setupLinkInterception(); + this._setupRoutingEvents(); + + this._initialize(); } focusContent() { @@ -50,307 +93,380 @@ class App { } /** + * Navigates to given route. * @param {string} route */ navigate(route) { - this._router.navigate(route); + if (window.location.hash !== route) { + window.location.hash = route; + } else { + this._onRoute(); + } } - static _nameProperty(collectionName) { - switch (collectionName) { - case 'domains': - return 'domain'; - case 'types': - return 'id'; - case 'commands': - case 'events': - case 'parameters': - case 'returns': - return 'name'; + /** + * Formats a canonical route string given an entity ref (e.g. 'Page.navigate' or 'DOM'). + * @param {string} ref + * @returns {string} + */ + formatRef(ref) { + if (!ref) { + return formatRoute({ target: this._currentTarget, domain: null, member: null }); } - return null; + const dotIndex = ref.indexOf('.'); + if (dotIndex !== -1) { + const domain = ref.slice(0, dotIndex); + const member = ref.slice(dotIndex + 1); + return formatRoute({ target: this._currentTarget, domain, member }); + } + return formatRoute({ target: this._currentTarget, domain: ref, member: null }); } /** - * @param {*} object - * @param {boolean} alreadyExperimental + * Fetches a JSON file and fails fast if response is not ok. + * @param {string} url + * @returns {Promise} */ - static _normalize(object, alreadyExperimental) { - if (typeof object !== 'object') - return object; - const result = {}; - if (alreadyExperimental && object.experimental) - delete object.experimental; - for (let key in object) { - let value = object[key]; - if (value instanceof Array) { - result[key] = value.map(item => App._normalize(item, alreadyExperimental || object.experimental)); - let property = App._nameProperty(key); - if (property !== null) { - result[key].sort((a, b) => { - if (a.experimental !== b.experimental) - return a.experimental ? 1 : -1; - if (a.deprecated !== b.deprecated) - return a.deprecated ? 1 : -1; - if (a.optional !== b.optional) - return a.optional ? 1 : -1; - if (!property) - return 0; - return a[property].localeCompare(b[property]); - }); - } - } else { - result[key] = App._normalize(value, alreadyExperimental || object.experimental); - } + async _fetchProtocolJson(url) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to load protocol from ${url}: ${response.status} ${response.statusText}`); } - return result; + return response.json(); } - static _normalizeProtocol(protocol) { - protocol = App._normalize(protocol); - // Assign empty arrays to otherwise undefined fields. - for (let domain of protocol.domains) { - domain.commands = domain.commands || []; - domain.events = domain.events || []; - domain.types = domain.types || []; - for (let event of domain.events) { - event.parameters = event.parameters || []; - event.returns = event.returns || []; - } - for (let command of domain.commands) { - command.parameters = command.parameters || []; - command.returns = command.returns || []; - } + /** + * Loads protocols and prepares tot, stable, and v8 target datasets. + */ + async _initialize() { + try { + const [browserProto, jsProto] = await Promise.all([ + this._fetchProtocolJson(PROTOCOL_URLS.browser), + this._fetchProtocolJson(PROTOCOL_URLS.js), + ]); + + this._prepareDatasets(browserProto, jsProto); + this._setupExperimentalToggle(); + this._onRoute(); + } catch (error) { + this._contentElement.textContent = ''; + this._contentElement.appendChild(renderError(`Initialization failed: ${error.message}`)); } - return protocol; } - static _computeBackReferences(domains) { - // Compute back references. - const typeidToType = new Map(); - for (let domain of domains) { - for (let type of domain.types) { - type.referencedBy = []; - typeidToType.set(domain.domain + '.' + type.id, type); - } + /** + * Prepares protocol datasets for tot, stable, and v8. + * @param {Object} browserProto + * @param {Object} jsProto + */ + _prepareDatasets(browserProto, jsProto) { + // 1. Tip-of-Tree (Tot) + const combinedTotDomains = [ + ...structuredClone(browserProto.domains || []), + ...structuredClone(jsProto.domains || []), + ]; + const normalizedTot = normalizeProtocol({ domains: combinedTotDomains }); + const totDomains = normalizedTot.domains; + const stableTotDomains = totDomains + .filter(domain => !domain.experimental) + .map(domain => stabilize(domain)); + + computeBackReferences(totDomains); + computeBackReferences(stableTotDomains); + + for (const d of totDomains) { + this._targetStore.tot.all.set(d.domain, d); + } + for (const d of stableTotDomains) { + this._targetStore.tot.stable.set(d.domain, d); } - for (let domain of domains) { - for (let command of domain.commands) { - const args = command.parameters.concat(command.returns); - for (let arg of args) { - const typeId = getReferencedType(domain.domain, arg); - const type = typeidToType.get(typeId); - if (!type) - continue; - type.referencedBy.push({ - type: 'command', - name: domain.domain + '.' + command.name - }); - } - } - for (let event of domain.events) { - const args = event.parameters.concat(event.returns); - for (let arg of args) { - const typeId = getReferencedType(domain.domain, arg); - const type = typeidToType.get(typeId); - if (!type) - continue; - type.referencedBy.push({ - type: 'event', - name: domain.domain + '.' + event.name - }); - } - } - for (let type of domain.types) { - if (!type.properties) - continue; - for (let arg of type.properties) { - const typeId = getReferencedType(domain.domain, arg); - const referencedType = typeidToType.get(typeId); - if (!referencedType) - continue; - referencedType.referencedBy.push({ - type: 'type', - name: domain.domain + '.' + type.id - }); - } - } + // 2. Stable Protocol (1.3) + // Stable target is the stabilized Tip-of-Tree protocol + for (const d of stableTotDomains) { + this._targetStore.stable.all.set(d.domain, d); + this._targetStore.stable.stable.set(d.domain, d); } - for (let type of typeidToType.values()) { - // Dedupe and sort backreferences - let map = new Map(); - for (let reference of type.referencedBy) - map.set(reference.name, reference); - type.referencedBy = Array.from(map.values()); - type.referencedBy.sort((a, b) => a.name.localeCompare(b.name)); + + // 3. V8 Inspector + const v8Domains = normalizeProtocol({ + domains: structuredClone(jsProto.domains || []), + }).domains; + const stableV8Domains = v8Domains + .filter(domain => !domain.experimental) + .map(domain => stabilize(domain)); + + computeBackReferences(v8Domains); + computeBackReferences(stableV8Domains); + + for (const d of v8Domains) { + this._targetStore.v8.all.set(d.domain, d); } + for (const d of stableV8Domains) { + this._targetStore.v8.stable.set(d.domain, d); + } + } - function getReferencedType(domainName, parameter) { - if (parameter.$ref) - return parameter.$ref.includes('.') ? parameter.$ref : domainName + '.' + parameter.$ref; - if (parameter.type === 'array') - return getReferencedType(domainName, parameter.items); - return null; + _setupExperimentalToggle() { + const isExpEnabled = window.localStorage['experimental'] !== 'false'; + document.body.classList.toggle('experimental-enabled', isExpEnabled); + + const expToggle = document.getElementById('experimental'); + if (expToggle) { + expToggle.addEventListener('click', () => { + const currentlyEnabled = window.localStorage['experimental'] !== 'false'; + const nextState = !currentlyEnabled; + window.localStorage['experimental'] = nextState ? 'true' : 'false'; + document.body.classList.toggle('experimental-enabled', nextState); + this._updateActiveDomains(); + this._onRoute(); + }); } } - /** - * @param {*} object - */ - static _stabilize(object) { - if (typeof object !== 'object') - return object; - - const result = {}; - for (let key in object) { - let value = object[key]; - if (value instanceof Array) - result[key] = value.filter(item => item.experimental !== true).map(App._stabilize); - else - result[key] = App._stabilize(value); + _setupDrawerEvents() { + if (this._drawerToggle) { + this._drawerToggle.addEventListener('click', () => this._toggleDrawer()); } - return result; + if (this._drawerBackdrop) { + this._drawerBackdrop.addEventListener('click', () => this._closeDrawer()); + } + document.addEventListener('keydown', event => { + if (event.key === 'Escape' && document.body.classList.contains('drawer-open')) { + this._closeDrawer(); + } + }); } - async _initialize() { - let protocols = await Promise.all(Object.values(PROTOCOLS).map(url => fetch(url).then(r => r.json()))); + _toggleDrawer() { + document.body.classList.toggle('drawer-open'); + } - this._allDomains.clear(); - this._stableDomains.clear(); + _closeDrawer() { + document.body.classList.remove('drawer-open'); + } - let protocol = App._normalizeProtocol({domains: [].concat(...protocols.map(p => p.domains))}); + _setupSidebarEvents() { + // In-sidebar domain filter + if (this._domainFilterInput) { + this._domainFilterInput.addEventListener('input', () => { + this._applyDomainFilter(); + }); + } - for (let domain of protocol.domains) { - this._allDomains.set(domain.domain, domain); - if (!domain.experimental) - this._stableDomains.set(domain.domain, App._stabilize(domain)); + // Target selector dropdown + if (this._targetSelector) { + this._targetSelector.addEventListener('change', () => { + const nextTarget = normalizeTarget(this._targetSelector.value); + if (nextTarget === this._currentTarget) return; + + this._currentTarget = nextTarget; + const newRoute = formatRoute({ + target: this._currentTarget, + domain: this._currentDomain, + member: null, + }); + this.navigate(newRoute); + }); } - App._computeBackReferences(Array.from(this._allDomains.values())); - App._computeBackReferences(Array.from(this._stableDomains.values())); - - document.body.classList.toggle('experimental-enabled', window.localStorage['experimental'] !== 'false'); - document.getElementById('experimental').addEventListener('click', () => { - window.localStorage['experimental'] = window.localStorage['experimental'] === 'false' ? 'true' : 'false'; - document.body.classList.toggle('experimental-enabled', window.localStorage['experimental'] !== 'false'); - this._renderDomains(); - }); + } + + _applyDomainFilter() { + if (!this._domainFilterInput || !this._domainListElement) return; + const query = this._domainFilterInput.value.trim().toLowerCase(); + const links = this._domainListElement.querySelectorAll('.domain-link'); + for (const link of links) { + const name = link.dataset.domain ? link.dataset.domain.toLowerCase() : link.textContent.toLowerCase(); + link.style.display = name.includes(query) ? '' : 'none'; + } + } - this._renderDomains(); + _setupLinkInterception() { + document.body.addEventListener('click', event => { + const anchor = event.target.closest('a'); + if (!anchor) return; + if (anchor.target === '_blank') return; + if (anchor.hostname && anchor.hostname !== window.location.hostname) return; + + const href = anchor.getAttribute('href'); + if (href && (href.startsWith('#') || href.startsWith('?'))) { + event.preventDefault(); + this._closeDrawer(); + this.navigate(href); + } + }, false); } - _renderDomains() { - this._domains = window.localStorage['experimental'] === 'false' ? this._stableDomains : this._allDomains; - this._search.setDomains(Array.from(this._domains.values())); - this._renderSidebar(this._domains); - this._router.navigate(this._router.route()); + _setupRoutingEvents() { + window.addEventListener('hashchange', () => this._onRoute()); + window.addEventListener('popstate', () => this._onRoute()); + } + + _updateActiveDomains() { + const isExp = window.localStorage['experimental'] !== 'false'; + const store = this._targetStore[this._currentTarget] || this._targetStore.tot; + this._activeDomains = isExp ? store.all : store.stable; + + this._search.setDomains(Array.from(this._activeDomains.values())); + this._renderSidebar(this._activeDomains); } - _onNavigateDomain(route, domain, method) { - document.title = route; - this._search.setDefaultValue(route); + _onRoute() { + let rawRoute = window.location.hash; + if (window.location.search && !rawRoute) { + rawRoute = window.location.search; + } else if (!rawRoute || /^#(?:method|type|event)-/.test(rawRoute)) { + rawRoute = window.location.pathname + (rawRoute || ''); + } + + const route = parseRoute(rawRoute); + + // Sync target + if (route.target !== this._currentTarget) { + this._currentTarget = route.target; + } + if (this._targetSelector && this._targetSelector.value !== this._currentTarget) { + this._targetSelector.value = this._currentTarget; + } + + this._updateActiveDomains(); + + let domain = route.domain; + const member = route.member; + + // If domain is null but member exists (isolated legacy anchor), resolve to current domain + if (!domain && member && this._currentDomain) { + domain = this._currentDomain; + } + + if (!domain) { + this._currentDomain = null; + this._onNavigateHome(); + return; + } + + // In-page navigation: if domain is already rendered, scroll to member without DOM re-render + if (this._currentDomain === domain && this._contentElement.firstChild && member) { + const canonicalTitle = `${domain}.${member}`; + document.title = `${canonicalTitle} - DevTools Protocol`; + this._search.setDefaultValue(canonicalTitle); + const titleId = ProtocolRenderer.titleId(domain, member); + const elem = this._contentElement.querySelector('#' + titleId); + if (elem) { + elem.scrollIntoView(); + } + this.focusContent(); + return; + } + + this._currentDomain = domain; + this._onNavigateDomain(domain, member); + } + + /** + * @param {string} domain + * @param {string|null} member + */ + _onNavigateDomain(domain, member) { + const canonicalTitle = member ? `${domain}.${member}` : domain; + document.title = `${canonicalTitle} - DevTools Protocol`; + + const searchDefault = member ? `${domain}.${member}` : domain; + this._search.setDefaultValue(searchDefault); this._search.cancelSearch(); + this._contentElement.textContent = ''; - var active = this._sidebarElement.querySelector('.active-link'); - if (active) + + // Update active link in sidebar + const active = this._domainListElement.querySelector('.active-link'); + if (active) { active.classList.remove('active-link'); - if (!this._domains.has(domain)) { - this._contentElement.appendChild(renderError(`Unknown domain: ${domain}. Enable experimental domains above?`)); + } + + if (!this._activeDomains.has(domain)) { + this._contentElement.appendChild( + renderError(`Unknown domain: ${domain}. Enable experimental domains above?`) + ); return; } - let link = this._sidebarElement.querySelector(`[href='?${domain}']`); - if (link) - link.classList.add('active-link'); - let render = ProtocolRenderer.renderDomain(this._domains.get(domain)); - if (render) { - this._contentElement.appendChild(render); - let elem = render.querySelector('#' + ProtocolRenderer.titleId(domain, method)); - if (elem) - elem.scrollIntoView(); - else + + const currentLink = this._domainListElement.querySelector(`[data-domain='${domain}']`); + if (currentLink) { + currentLink.classList.add('active-link'); + if (typeof currentLink.scrollIntoViewIfNeeded === 'function') { + currentLink.scrollIntoViewIfNeeded(false); + } + } + + const domainObject = this._activeDomains.get(domain); + const rendered = ProtocolRenderer.renderDomain(domainObject); + if (rendered) { + this._contentElement.appendChild(rendered); + if (member) { + const titleId = ProtocolRenderer.titleId(domain, member); + const elem = rendered.querySelector('#' + titleId); + if (elem) { + elem.scrollIntoView(); + } else { + this._contentElement.scrollTop = 0; + } + } else { this._contentElement.scrollTop = 0; + } } - else if (!method) - this._contentElement.scrollTop = 0; - this._contentElement.focus(); + + this.focusContent(); } _onNavigateHome() { - document.title = 'Vanilla Protocol Viewer'; + document.title = 'DevTools Protocol Viewer'; this._search.setDefaultValue(''); this._search.cancelSearch(); this._contentElement.textContent = ''; - var template = document.querySelector('#landing'); - var clone = document.importNode(template.content, true); - this._contentElement.appendChild(clone); - } - _renderSidebar(domains) { - let domainNames = Array.from(domains.keys()); - let sidebar = document.getElementById('sidebar'); - sidebar.textContent = ''; - for (let name of domainNames) { - let a = sidebar.a('?' + name, name); - a.classList.add('domain-link'); - ProtocolRenderer.applyBackground(domains.get(name), a); + const active = this._domainListElement.querySelector('.active-link'); + if (active) { + active.classList.remove('active-link'); } - } -} -class Router { - /** - * @param {function(?)} unknownRouteHandler - */ - constructor(unknownRouteHandler) { - this._routes = new Map(); - this._unknownRouteHandler = unknownRouteHandler; - window.addEventListener("popstate", () => this._processRoute()); + const template = document.querySelector('#landing'); + if (template) { + const clone = document.importNode(template.content, true); + this._contentElement.appendChild(clone); + } } /** - * @param {!RegExp} regex - * @param {function(?)} handler + * @param {Map} domains */ - setRoute(regex, handler) { - this._routes.set(regex, handler) - } + _renderSidebar(domains) { + this._domainListElement.textContent = ''; + const domainNames = Array.from(domains.keys()); - /** - * @param {string} route - */ - navigate(route) { - if (window.location.search !== route) - window.history.pushState({}, route, route); - this._processRoute(); - } + for (const name of domainNames) { + const domain = domains.get(name); + const link = document.createElement('a'); + link.href = this.formatRef(name); + link.className = 'domain-link'; + link.dataset.domain = name; + link.textContent = name; - /** - * @return {string} - */ - route() { - // Support for old hash-based navigation. - if (window.location.hash.length > 1) - return '?' + window.location.hash.substring(1); - return window.location.search; - } + ProtocolRenderer.applyBackground(domain, link); - _processRoute() { - let route = (window.location.search || '?').substring(1); - gtag('config', 'UA-138239427-1', {'page_path': '?' + route}); - for (let regex of this._routes.keys()) { - var matches = route.match(regex); - if (matches) { - this._routes.get(regex).call(null, route, ...matches.slice(1)); - return; - } + this._domainListElement.appendChild(link); } - this._unknownRouteHandler.call(null, route); + + this._applyDomainFilter(); } } +/** + * @param {string} error + * @returns {Element} + */ function renderError(error) { - let main = E.box(); - let box = main.div('box-content'); + const main = E.box(); + const box = main.div('box-content'); box.el('h2', '', 'Error'); box.p('', error); return main; diff --git a/src/protocol-model.js b/src/protocol-model.js new file mode 100644 index 000000000..2d41620d7 --- /dev/null +++ b/src/protocol-model.js @@ -0,0 +1,406 @@ +/** + * @fileoverview Protocol Model - Isomorphic pure functions for CDP protocols. + * Browser-agnostic, zero-DOM ES module. + */ + +const TARGET_MAP = new Map([ + ['tot', 'tot'], + ['stable', 'stable'], + ['1-3', 'stable'], + ['1-2', 'stable'], + ['v8', 'v8'], +]); + +/** + * Normalizes a target string to one of: 'tot', 'stable', 'v8'. + * @param {string|null|undefined} target + * @returns {'tot'|'stable'|'v8'} + */ +export function normalizeTarget(target) { + if (!target) return 'tot'; + return TARGET_MAP.get(target.toLowerCase()) || 'tot'; +} + +/** + * Resolves the identifier property name for sorting a CDP collection. + * @param {string} collectionName + * @returns {string|null} + */ +function nameProperty(collectionName) { + switch (collectionName) { + case 'domains': + return 'domain'; + case 'types': + return 'id'; + case 'commands': + case 'events': + case 'parameters': + case 'returns': + case 'properties': + return 'name'; + default: + return null; + } +} + +/** + * Deterministically sorts a collection in-place: + * 1. Non-experimental before experimental. + * 2. Non-deprecated before deprecated. + * 3. Required before optional. + * 4. Alphabetical by entity name. + * + * @param {Array} items + * @param {string|null} prop + */ +function sortCollection(items, prop) { + if (!prop) return; + items.sort((a, b) => { + const aExp = a.experimental ? 1 : 0; + const bExp = b.experimental ? 1 : 0; + if (aExp !== bExp) return aExp - bExp; + + const aDep = a.deprecated ? 1 : 0; + const bDep = b.deprecated ? 1 : 0; + if (aDep !== bDep) return aDep - bDep; + + const aOpt = a.optional ? 1 : 0; + const bOpt = b.optional ? 1 : 0; + if (aOpt !== bOpt) return aOpt - bOpt; + + if (!a[prop] || !b[prop]) return 0; + return a[prop].localeCompare(b[prop]); + }); +} + +/** + * Recursive normalization helper creating a fresh object without mutating input. + * @param {*} object + * @param {boolean} alreadyExperimental + * @returns {*} + */ +function normalizeNode(object, alreadyExperimental) { + if (typeof object !== 'object' || object === null) { + return object; + } + + if (Array.isArray(object)) { + return object.map(item => normalizeNode(item, alreadyExperimental)); + } + + const result = {}; + const isSelfExperimental = Boolean(object.experimental); + const childAlreadyExperimental = alreadyExperimental || isSelfExperimental; + + for (const [key, value] of Object.entries(object)) { + // Avoid redundant experimental flag on children without mutating input + if (key === 'experimental' && alreadyExperimental) { + continue; + } + + if (Array.isArray(value)) { + result[key] = value.map(item => normalizeNode(item, childAlreadyExperimental)); + sortCollection(result[key], nameProperty(key)); + } else { + result[key] = normalizeNode(value, childAlreadyExperimental); + } + } + + return result; +} + +/** + * Normalizes protocol data by establishing empty array defaults and deterministic sorting. + * Pure function: does NOT mutate input protocol object. + * + * @param {Object} protocol + * @returns {Object} Normalized protocol clone + */ +export function normalizeProtocol(protocol) { + if (!protocol || typeof protocol !== 'object') { + return { domains: [] }; + } + + const normalized = normalizeNode(protocol, false); + normalized.domains = normalized.domains || []; + + for (const domain of normalized.domains) { + domain.commands = domain.commands || []; + domain.events = domain.events || []; + domain.types = domain.types || []; + + for (const event of domain.events) { + event.parameters = event.parameters || []; + event.returns = event.returns || []; + } + for (const command of domain.commands) { + command.parameters = command.parameters || []; + command.returns = command.returns || []; + } + for (const type of domain.types) { + if (type.properties) { + type.properties = type.properties || []; + } + } + } + + return normalized; +} + +/** + * Recursive, structurally unified deep clone filtering out anything with experimental === true. + * Guarantees fresh copy with no shared references. + * + * @template T + * @param {T} node + * @returns {T} Fresh copy stripped of experimental entities + */ +export function stabilize(node) { + if (typeof node !== 'object' || node === null) { + return node; + } + + if (Array.isArray(node)) { + return node + .filter(item => !(item && typeof item === 'object' && item.experimental === true)) + .map(item => stabilize(item)); + } + + const result = {}; + for (const [key, value] of Object.entries(node)) { + result[key] = stabilize(value); + } + + return result; +} + +/** + * Helper to extract referenced type id from parameter/property object. + * @param {string} domainName + * @param {Object} parameter + * @returns {string|null} + */ +function getReferencedType(domainName, parameter) { + if (!parameter || typeof parameter !== 'object') { + return null; + } + if (parameter.$ref) { + return parameter.$ref.includes('.') ? parameter.$ref : `${domainName}.${parameter.$ref}`; + } + if (parameter.type === 'array' && parameter.items) { + return getReferencedType(domainName, parameter.items); + } + return null; +} + +/** + * Dynamically computes reverse references ("Used by") for every type across commands, + * events, and types (properties and array items $ref). + * Deduplicates and sorts references. + * + * @param {Array} domains + * @returns {Array} The domains array with type.referencedBy populated + */ +export function computeBackReferences(domains) { + if (!Array.isArray(domains)) return []; + + const typeidToType = new Map(); + for (const domain of domains) { + for (const type of domain.types || []) { + type.referencedBy = []; + typeidToType.set(`${domain.domain}.${type.id}`, type); + } + } + + for (const domain of domains) { + const domainName = domain.domain; + + for (const command of domain.commands || []) { + const args = [...(command.parameters || []), ...(command.returns || [])]; + for (const arg of args) { + const typeId = getReferencedType(domainName, arg); + const referencedType = typeidToType.get(typeId); + if (referencedType) { + referencedType.referencedBy.push({ + type: 'command', + name: `${domainName}.${command.name}`, + }); + } + } + } + + for (const event of domain.events || []) { + const args = [...(event.parameters || []), ...(event.returns || [])]; + for (const arg of args) { + const typeId = getReferencedType(domainName, arg); + const referencedType = typeidToType.get(typeId); + if (referencedType) { + referencedType.referencedBy.push({ + type: 'event', + name: `${domainName}.${event.name}`, + }); + } + } + } + + for (const type of domain.types || []) { + if (type.properties) { + for (const prop of type.properties) { + const typeId = getReferencedType(domainName, prop); + const referencedType = typeidToType.get(typeId); + if (referencedType) { + referencedType.referencedBy.push({ + type: 'type', + name: `${domainName}.${type.id}`, + }); + } + } + } + if (type.items) { + const typeId = getReferencedType(domainName, type.items); + const referencedType = typeidToType.get(typeId); + if (referencedType) { + referencedType.referencedBy.push({ + type: 'type', + name: `${domainName}.${type.id}`, + }); + } + } + } + } + + for (const type of typeidToType.values()) { + const map = new Map(); + for (const reference of type.referencedBy) { + map.set(reference.name, reference); + } + type.referencedBy = Array.from(map.values()); + type.referencedBy.sort((a, b) => a.name.localeCompare(b.name)); + } + + return domains; +} + +/** + * Parses raw route string into canonical target, domain, and member components. + * Supports modern hash routes, composite legacy paths, isolated legacy anchors, and query fallbacks. + * + * @param {string|null|undefined} routeString + * @returns {{ target: 'tot'|'stable'|'v8', domain: string|null, member: string|null }} + */ +export function parseRoute(routeString) { + if (!routeString || typeof routeString !== 'string') { + return { target: 'tot', domain: null, member: null }; + } + + const trimmed = routeString.trim(); + if (!trimmed || trimmed === '#' || trimmed === '#/' || trimmed === '/') { + return { target: 'tot', domain: null, member: null }; + } + + // Handle isolated legacy anchors: #method-foo, #event-bar, #type-baz + const isolatedLegacyMatch = trimmed.match(/^#(?:method|type|event)-([\w-]+)$/); + if (isolatedLegacyMatch) { + return { target: 'tot', domain: null, member: isolatedLegacyMatch[1] }; + } + + const hashIndex = trimmed.indexOf('#'); + let pathPart = ''; + let hashPart = ''; + + if (hashIndex !== -1) { + pathPart = trimmed.slice(0, hashIndex); + hashPart = trimmed.slice(hashIndex + 1); + } else if (trimmed.startsWith('?')) { + hashPart = trimmed.slice(1); + } else { + pathPart = trimmed; + } + + // Check if hashPart is a legacy anchor like method-navigate + const legacyHashMatch = hashPart.match(/^(?:method|type|event)-([\w-]+)$/); + const legacyMember = legacyHashMatch ? legacyHashMatch[1] : null; + + const pathSegments = pathPart + .split('/') + .map(s => s.trim()) + .filter(s => Boolean(s) && !s.endsWith('.html')); + + // Filter out leading repository base path if present (e.g. /devtools-protocol/) + if (pathSegments.length > 0 && pathSegments[0] === 'devtools-protocol') { + pathSegments.shift(); + } + + // Composite legacy URLs: /tot/Page/#method-navigate, /1-3/Page/#method-navigate, /1-2/Network/ + if (pathSegments.length > 0) { + let target = 'tot'; + let domain = null; + + const targetIndex = pathSegments.findIndex(s => TARGET_MAP.has(s.toLowerCase())); + if (targetIndex !== -1) { + target = normalizeTarget(pathSegments[targetIndex]); + if (pathSegments.length > targetIndex + 1) { + domain = pathSegments[targetIndex + 1]; + } + } else { + domain = pathSegments[0]; + } + + return { target, domain, member: legacyMember }; + } + + // Modern hash route or query format: e.g. /Page.navigate, /v8/Runtime.evaluate, Page.navigate, Page + let cleanHash = hashPart; + if (cleanHash.startsWith('/')) { + cleanHash = cleanHash.slice(1); + } + if (!cleanHash) { + return { target: 'tot', domain: null, member: null }; + } + + let target = 'tot'; + let targetAndRest = cleanHash; + + const slashIndex = cleanHash.indexOf('/'); + if (slashIndex !== -1) { + const potentialTarget = cleanHash.slice(0, slashIndex).toLowerCase(); + if (TARGET_MAP.has(potentialTarget)) { + target = normalizeTarget(potentialTarget); + targetAndRest = cleanHash.slice(slashIndex + 1); + } + } + + // Strip trailing slashes from domain/member (e.g. #/Page/ -> Page) + targetAndRest = targetAndRest.replace(/\/+$/, ''); + + if (!targetAndRest) { + return { target, domain: null, member: null }; + } + + const dotIndex = targetAndRest.indexOf('.'); + if (dotIndex !== -1) { + const domain = targetAndRest.slice(0, dotIndex); + const member = targetAndRest.slice(dotIndex + 1); + return { target, domain, member: member || null }; + } + + return { target, domain: targetAndRest, member: null }; +} + +/** + * Formats canonical hash route from components. + * @param {{ target?: string|null, domain?: string|null, member?: string|null }} [route] + * @returns {string} Canonical hash route, e.g. '#/Page.navigate' + */ +export function formatRoute({ target = 'tot', domain = null, member = null } = {}) { + const normTarget = normalizeTarget(target); + const targetPrefix = normTarget === 'tot' ? '' : `${normTarget}/`; + + if (!domain) { + return '#/'; + } + if (member) { + return `#/${targetPrefix}${domain}.${member}`; + } + return `#/${targetPrefix}${domain}`; +} diff --git a/src/protocol_renderer.js b/src/protocol_renderer.js index da296c717..ee9a8fd5b 100644 --- a/src/protocol_renderer.js +++ b/src/protocol_renderer.js @@ -19,7 +19,7 @@ class ProtocolRenderer { let description = header.el('p'); description.textContent = domain.description || ''; ProtocolRenderer.applyMarks(domain, title); - ProtocolRenderer.renderTableOfContents(domain, header) + ProtocolRenderer.renderTableOfContents(domain, header); } if (domain.commands.length) { @@ -116,7 +116,8 @@ class ProtocolRenderer { heading.text(domainName + '.', 'method-domain'); heading.text(title, 'method-name'); ProtocolRenderer.applyMarks(item, heading); - heading.a('?' + id, '#').classList.add('title-link'); + let href = (window.app && window.app.formatRef) ? window.app.formatRef(id) : '#/' + id; + heading.a(href, '#').classList.add('title-link'); return heading; } @@ -250,7 +251,7 @@ class ProtocolRenderer { static renderRef($ref) { let a = E.el('a', 'parameter-type'); - a.href = '?' + $ref; + a.href = (window.app && window.app.formatRef) ? window.app.formatRef($ref) : '#/' + $ref; a.textContent = $ref; return a; } diff --git a/src/search.js b/src/search.js index 703d9d44e..eac654577 100644 --- a/src/search.js +++ b/src/search.js @@ -13,12 +13,16 @@ class Search { /** @type {!Set} */ this._domainNames = new Set(); this._selectedElement = null; + this._defaultValue = ''; this._searchInput.addEventListener('input', this._onInput.bind(this), false); this._searchInput.addEventListener('keydown', this._onKeyDown.bind(this), false); this._resultsElement = resultsElement; - // Activate search on any keypress + // Activate search on any keypress (unless user is in an input field) document.addEventListener('keypress', event => { + const target = /** @type {HTMLElement} */ (event.target); + if (target && target.matches && target.matches('input, textarea, select, [contenteditable="true"]')) + return; if (this._searchInput === document.activeElement) return; if (/\S/.test(event.key)) { @@ -27,19 +31,34 @@ class Search { this._searchInput.focus(); } }); - // Activate search on backspace + + // Activate search on backspace, delete, '/', or Cmd+K document.addEventListener('keydown', event => { + const target = /** @type {HTMLElement} */ (event.target); + if (target && target.matches && target.matches('input, textarea, select, [contenteditable="true"]')) + return; if (this._searchInput === document.activeElement) return; + if (event.key === '/' || ((event.metaKey || event.ctrlKey) && event.key === 'k')) { + event.preventDefault(); + this._searchInput.focus(); + this._searchInput.select(); + return; + } if (event.keyCode === 8 || event.keyCode === 46) this._searchInput.focus(); }); + // Activate on paste document.addEventListener('paste', event => { + const target = /** @type {HTMLElement} */ (event.target); + if (target && target.matches && target.matches('input, textarea, select, [contenteditable="true"]')) + return; if (this._searchInput === document.activeElement) return; this._searchInput.focus(); }); + document.addEventListener('click', event => { if (this._searchInput.contains(event.target)) return; @@ -47,7 +66,8 @@ class Search { if (searchItem) { event.consume(); this.cancelSearch(); - app.navigate(searchItem.__route); + if (window.app && window.app.navigate) + window.app.navigate(searchItem.__route); return; } }); @@ -86,7 +106,8 @@ class Search { this._searchInput.blur(); this._resultsElement.style.setProperty('display', 'none'); this._searchInput.value = this._defaultValue; - app.focusContent(); + if (window.app && window.app.focusContent) + window.app.focusContent(); } setDefaultValue(value) { @@ -118,7 +139,7 @@ class Search { let main = this._resultsElement.hbox('search-item', `Navigate Home`); main.classList.add('custom-search-result'); main.classList.add('monospace'); - main.__route = '?'; + main.__route = (window.app && window.app.formatRef) ? window.app.formatRef('') : '#/'; return main; } @@ -192,9 +213,9 @@ class Search { this._selectNext(event); } else if (event.key === 'ArrowUp') { this._selectPrevious(event); - } else if (event.key === 'Enter' || event.key === ' ') { + } else if (event.key === 'Enter') { event.consume(); - if (this._selectedElement); + if (this._selectedElement) this._selectedElement.click(); } } @@ -254,7 +275,7 @@ function renderSearchResult(searchResult) { let domainElement = p1.span('search-item-title-domain'); domainElement.appendChild(renderTextWithMatches(item.title, searchResult.matches, 0, item.domainName.length + 1)); p1.appendChild(renderTextWithMatches(item.title, searchResult.matches, item.domainName.length + 1, item.title.length)); - let p2 = container.el('div', 'search-item-description'); + let p2 = container.el('div', 'search-item-description'); p2.textContent = item.description; } main.__route = item.route; @@ -265,7 +286,7 @@ function renderSearchResult(searchResult) { * @param {string} text * @param {!Array} matches * @param {number} fromIndex - * @param {number} fromIndex + * @param {number} toIndex * @return {!Element} */ function renderTextWithMatches(text, matches, fromIndex, toIndex) { @@ -313,7 +334,6 @@ Search.Item = class { * @param {string} domainEntry * @param {!Search.ItemType} itemType * @param {string} description - * @param {string} route */ constructor(domainName, domainEntry, itemType, description) { this.domainName = domainName; @@ -321,7 +341,7 @@ Search.Item = class { this.type = itemType; this.description = description || ''; this.title = this.domainName + '.' + this.domainEntry; - this.route = `?${domainName}.${domainEntry}` + this.route = (window.app && window.app.formatRef) ? window.app.formatRef(this.title) : '#/' + this.title; } } @@ -337,4 +357,3 @@ Search.SearchResult = class { this.matches = matches; } } - diff --git a/src/style.css b/src/style.css index 128934c0a..525b9fe52 100644 --- a/src/style.css +++ b/src/style.css @@ -1,280 +1,494 @@ :root { - --sidebar-width: 200px; - --search-height: 50px; - --hover-color: rgba(225, 245, 254, 0.49); - --parameters-grid-template-columns: minmax(250px, max-content) auto; + --header-height: 44px; + --sidebar-width: 240px; + --color-bg: #fafafa; + --color-surface: #ffffff; + --color-text: #202124; + --color-text-secondary: #5f6368; + --color-border: #dadce0; + --color-hover: #e8f0fe; + --color-active: #d2e3fc; + --color-primary: #1a73e8; + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, Monaco, "Courier New", monospace; + --parameters-grid-template-columns: minmax(200px, max-content) 1fr; } -@media only screen and (max-width : 800px) { - :root { - --sidebar-width: 0px; - --parameters-grid-template-columns: max-content auto; - } - - #sidebar { - display: none; - } - - #content .domain { - padding: 2px; - } -} - -@media only screen and (max-width : 400px) { - input[type=search] { - width: 250px !important; - } +* { + box-sizing: border-box; } body { - color: #333; - background-color: #fafafa; - font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; + color: var(--color-text); + background-color: var(--color-bg); + font-family: var(--font-sans); + font-size: 13px; + line-height: 1.5; padding: 0; margin: 0; + overflow: hidden; } p { margin: 0.5rem 0 1rem; } -h4 { - /* use padding to make sure element has some space on top when - * scrollIntoViewportIfNeede() is called */ +h1, h2, h3, h4, h5 { + color: var(--color-text); margin-top: 0; - padding-top: 1.33em; +} + +h4 { + padding-top: 1.2em; } h5 { - color: gray; - font-weight: 300; + color: var(--color-text-secondary); + font-weight: 500; text-transform: uppercase; - letter-spacing: 1px; - margin: 1rem 0 0; + letter-spacing: 0.5px; + font-size: 11px; + margin: 1rem 0 0.5rem; } a { - font-weight: normal !important; - color: #333; + font-weight: normal; + color: var(--color-primary); + text-decoration: none; +} + +a:hover { + text-decoration: underline; } code { - font-family: Consolas, Menlo, monospace; - color: #9C27B0; + font-family: var(--font-mono); + color: #c2185b; + font-size: 12px; + background-color: rgba(0, 0, 0, 0.04); + padding: 1px 4px; + border-radius: 3px; } -#sidebar { - background: #fff; - position: absolute; - width: var(--sidebar-width); +/* ========================================================================== + Toolbar Header (44px) + ========================================================================== */ + +.toolbar-header { + height: var(--header-height); + background-color: var(--color-surface); + border-bottom: 1px solid var(--color-border); + position: fixed; top: 0; - bottom: 0; left: 0; - overflow-y: scroll; - border-right: 1px solid rgba(0, 0, 0, 0.14); - overflow-x: hidden; + right: 0; + z-index: 100; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 12px; } -#search { - background: #3f51b5; - position: absolute; - left: var(--sidebar-width); - right: 0; - top: 0; - height: var(--search-height); - border-bottom: 1px solid rgba(0, 0, 0, 0.14); +.header-left { display: flex; align-items: center; + gap: 10px; + min-width: 220px; +} + +.drawer-toggle { + display: none; + background: none; + border: none; + font-size: 18px; + color: var(--color-text); + cursor: pointer; + padding: 4px 8px; + border-radius: 4px; + align-items: center; justify-content: center; - overflow: hidden; } -#search .home { - padding: 10px; +.drawer-toggle:hover { + background-color: rgba(0, 0, 0, 0.06); } -#search .github { - transform: scale(0.85); +.brand-link { + display: flex; + align-items: center; + gap: 8px; + color: var(--color-text); + font-weight: 600; + font-size: 14px; + text-decoration: none; + user-select: none; } -#search .toolbar-button { - opacity: 0.5; +.brand-link:hover { + text-decoration: none; } -#search .toolbar-button:hover { - opacity: 0.85; +.brand-logo { + display: block; } -#search .toolbar-button:active { - opacity: 1; +.brand-title { + white-space: nowrap; } -::-webkit-input-placeholder { /* Chrome/Opera/Safari */ - color: rgba(255, 255, 255, 0.5); +.experimental-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 12px; + border: 1px solid var(--color-border); + background-color: #fff; + cursor: pointer; + font-size: 11px; + font-weight: 500; + color: var(--color-text-secondary); + user-select: none; + transition: all 0.15s ease; } -input[type=search]:focus { - outline: none; +.experimental-pill:hover { + background-color: #f8f9fa; + border-color: #c6c6c6; } -input[type=search] { - -webkit-appearance: none; - margin: 0 auto; - background: #3f51b5; - text-align: center; +.experimental-enabled .experimental-pill { + background-color: #fef7e0; + border-color: #f9ab00; + color: #b06000; +} + +#experimental-on, +.experimental-enabled #experimental-off { + display: none; +} + +.experimental-enabled #experimental-on { + display: block; +} + +.header-center { + flex: 1; + max-width: 520px; + margin: 0 16px; +} + +.search-box { + display: flex; + align-items: center; + background-color: #f1f3f4; + border: 1px solid transparent; + border-radius: 6px; + padding: 4px 10px; + transition: all 0.15s ease; +} + +.search-box:focus-within { + background-color: #ffffff; + border-color: var(--color-primary); + box-shadow: 0 1px 3px rgba(26, 115, 232, 0.2); +} + +.search-box input[type="search"] { + flex: 1; border: none; - border-bottom: 1px solid rgba(255, 255, 255, 0.52); - padding: 0 0 3px 20px; - font-size: 20px; - color: white; - width: 450px; - text-overflow: ellipsis; + background: transparent; + outline: none; + font-family: inherit; + font-size: 13px; + color: var(--color-text); + padding: 0; + width: 100%; +} + +.search-box input[type="search"]::-webkit-search-cancel-button { + cursor: pointer; } -#experimental { +.search-kbd, +.inline-kbd { + display: inline-block; + font-family: var(--font-mono); + font-size: 11px; + color: var(--color-text-secondary); + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 3px; + padding: 0 5px; + line-height: 18px; + user-select: none; +} + +.header-right { + display: flex; align-items: center; + gap: 12px; +} + +.toolbar-link { display: flex; - color: white; - cursor: pointer; - opacity: 0.5; - padding: 4px 10px 0 0; + align-items: center; + justify-content: center; + color: var(--color-text-secondary); + padding: 4px; + border-radius: 4px; } -#experimental img { - filter: invert(); - padding: 0 4px 0 0; +.toolbar-link:hover { + color: var(--color-text); + background-color: rgba(0, 0, 0, 0.05); + text-decoration: none; } -#experimental-on, .experimental-enabled #experimental-off { - display: none; +.toolbar-icon.github { + opacity: 0.75; + transition: opacity 0.15s; } -.experimental-enabled #experimental-on { - display: block; +.toolbar-link:hover .toolbar-icon.github { + opacity: 1; } -#sresults { - display: none; - background-color: #fff; - position: absolute; - left: var(--sidebar-width); - top: var(--search-height); +/* ========================================================================== + Sidebar Navigation + ========================================================================== */ + +#sidebar { + background-color: var(--color-surface); + position: fixed; + width: var(--sidebar-width); + top: var(--header-height); bottom: 0; - right: 0; - overflow-y: scroll; - border-top: 2px solid rgba(51, 51, 51, 0.52); - -webkit-overflow-scrolling: touch; + left: 0; + border-right: 1px solid var(--color-border); + display: flex; + flex-direction: column; + z-index: 50; +} + +.sidebar-header { + padding: 8px; + border-bottom: 1px solid var(--color-border); + background-color: var(--color-surface); } +.domain-filter { + width: 100%; + border: 1px solid var(--color-border); + border-radius: 4px; + padding: 4px 8px; + font-family: inherit; + font-size: 12px; + outline: none; + background-color: #f8f9fa; +} + +.domain-filter:focus { + background-color: #ffffff; + border-color: var(--color-primary); +} + +.domain-list { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 4px 0; +} + +.sidebar-footer { + padding: 8px; + border-top: 1px solid var(--color-border); + background-color: #f8f9fa; +} + +.target-selector { + width: 100%; + padding: 4px 6px; + font-family: inherit; + font-size: 12px; + color: var(--color-text); + background-color: #ffffff; + border: 1px solid var(--color-border); + border-radius: 4px; + outline: none; + cursor: pointer; +} + +.target-selector:focus { + border-color: var(--color-primary); +} + +#sidebar .domain-link { + text-decoration: none; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + font-size: 13px; + color: var(--color-text); + line-height: 28px; + min-height: 28px; + padding: 0 12px; + border-left: 3px solid transparent; + cursor: pointer; +} + +#sidebar .domain-link:hover { + background-color: var(--color-hover); + text-decoration: none; +} + +#sidebar .active-link { + font-weight: 600; + background-color: var(--color-active); + border-left-color: var(--color-primary); + color: var(--color-primary); +} + +#sidebar .domain-link.experimental-bg { + background-color: rgba(254, 247, 224, 0.35); +} + +#sidebar .domain-link.deprecated-bg { + background-color: rgba(252, 232, 230, 0.35); +} + +/* ========================================================================== + Main Content Area + ========================================================================== */ + #content { - background-color: #fafafa; + background-color: var(--color-bg); position: absolute; left: var(--sidebar-width); - top: var(--search-height); + top: var(--header-height); bottom: 0; right: 0; - overflow-y: scroll; + overflow-y: auto; -webkit-overflow-scrolling: touch; + outline: none; } -.monospace { - font-family: Consolas, Menlo, monospace; +.box { + width: 96%; + max-width: 1100px; + border-radius: 4px; + border: 1px solid var(--color-border); + margin: 16px auto 24px auto; + background-color: var(--color-surface); } -.text-overflow { - overflow: hidden; - text-overflow: ellipsis; +.box-content, .method, .type { + padding: 16px 24px; } -.box { - width: 98%; - border-radius: 2px; - box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); - margin: 16px auto 32px auto; - background-color: white; +.domain { + padding: 16px 24px; } -.box-content, .method, .type { - padding: 16px 24px; +.domain-padding { + height: 50vh; + display: flex; + font-size: 48px; + align-items: center; + color: var(--color-border); + justify-content: center; +} + +.method-domain { + color: var(--color-text-secondary); } +/* ========================================================================== + Utility Classes + ========================================================================== */ + .hbox { display: flex; flex-direction: row; - flex: 1 0 auto; + align-items: center; } .vbox { display: flex; flex-direction: column; - flex: 1 0 auto; } -.domain { - padding: 20px 48px 20px 48px; +.monospace { + font-family: var(--font-mono); } -.domain-padding { - height: 75vh; - display: flex; - font-size:100px; - align-items: center; - color: #4052b4; - justify-content: center; +.text-overflow { + overflow: hidden; + text-overflow: ellipsis; } -.method-domain { - color: #ababab; +.experimental-bg { + background-color: rgba(254, 247, 224, 0.35); +} + +.deprecated-bg { + background-color: rgba(252, 232, 230, 0.35); } +/* ========================================================================== + Protocol Renderer Classes + ========================================================================== */ + .parameter-list { - padding: 0.5rem 5px; + margin: 8px 0 16px 0; display: grid; grid-template-columns: var(--parameters-grid-template-columns); + gap: 8px 16px; + padding: 0; } .parameter { border-left: 3px solid transparent; - margin-bottom: 6px; align-items: baseline; } .parameter-name { text-align: right; - color: black; + color: var(--color-text); word-wrap: break-word; - padding: 5px 10px; -} - -.parameter-highlight { + font-weight: 500; } .parameter-value { - overflow: hidden; - padding: 5px 10px; + gap: 4px; } .parameter-type { - font-weight: bold; + font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + color: #1a73e8; } .parameter-description { - font-size: 90%; + color: var(--color-text); + font-size: 12px; } .optional::after { content: "optional"; - opacity: .6; - font-size: 70%; + opacity: 0.65; + font-size: 10px; + font-weight: normal; display: block; } .title-link { margin-left: 1ex; display: none; + color: var(--color-text-secondary); } h4:hover .title-link { @@ -283,168 +497,217 @@ h4:hover .title-link { } span.experimental, span.deprecated { - font-size: 70%; + font-size: 10px; text-transform: uppercase; - background-color: #e57373; - padding: 2px; + background-color: #ea8600; + padding: 1px 4px; + border-radius: 3px; cursor: help; color: white; - vertical-align: baseline; - font-weight: normal; - font-family: inherit; + vertical-align: middle; + font-weight: 600; + font-family: var(--font-sans); margin-left: 1ex; user-select: none; } span.deprecated { - background-color: #ffa03b; + background-color: #d93025; } -#content .experimental-bg { - background-color: rgba(255, 212, 212, 0.2); +.entity-icon { + display: inline-block; + font-size: 10px; + text-transform: uppercase; + padding: 1px 5px; + border-radius: 3px; + font-weight: 700; + user-select: none; + line-height: 14px; + margin-right: 6px; } -#content .deprecated-bg { - background-color: rgba(255, 249, 187, 0.2); +.entity-icon-method { + background-color: #e8f0fe; + color: #1a73e8; } -#sidebar .domain-link.experimental-bg { - border-color: #ffd4d4; +.entity-icon-event { + background-color: #e6f4ea; + color: #137333; } -#sidebar .domain-link.deprecated-bg { - border-color: hsla(55, 100%, 81%, 1); +.entity-icon-type { + background-color: #fef7e0; + color: #b06000; } -#sidebar .domain-link { - text-decoration: none; - display: flex; - flex-direction: row; - align-items: center; - font-size: 14px; - font-weight: 400; - line-height: 24px; - min-height: 48px; - padding: 0 16px; +.method:not(:last-child), .type:not(:last-child) { + border-bottom: 1px solid var(--color-border); } -#sidebar .domain-link { - border-left: 10px solid transparent; +/* ========================================================================== + References ("Used by") + ========================================================================== */ + +.references-list { + list-style-type: none; + padding-left: 0; + margin: 6px 0; } -#sidebar .active-link { - font-size: 16px; - font-weight: bold; - background-color: rgb(225, 245, 254); +.references-list li { + margin-bottom: 4px; + display: flex; + align-items: center; } -#sidebar .domain-link:hover { - background-color: var(--hover-color); +.reference-icon { + width: 60px; + display: inline-block; + vertical-align: middle; + text-align: right; + margin-right: 8px; } -#sresults .search-item.selected { - background-color: #e3f2fd; +/* ========================================================================== + Search Overlay & Results + ========================================================================== */ + +#sresults { + display: none; + background-color: var(--color-surface); + position: absolute; + left: var(--sidebar-width); + top: var(--header-height); + bottom: 0; + right: 0; + overflow-y: auto; + border-top: 1px solid var(--color-border); + -webkit-overflow-scrolling: touch; + z-index: 90; } #sresults .search-item { - padding: 4px 10px; + padding: 8px 16px; align-items: center; cursor: pointer; - border-bottom: 1px solid rgba(51, 51, 51, 0.12); + border-bottom: 1px solid #f1f3f4; +} + +#sresults .search-item.selected { + background-color: var(--color-hover); } #sresults .search-item:hover { - background-color: var(--hover-color); + background-color: var(--color-hover); } #sresults .search-item-title { overflow: hidden; text-overflow: ellipsis; + font-weight: 500; } -#sresults .search-item-icon { - margin: 10px 18px 10px 8px; - width: 50px; - text-align: center; - flex-shrink: 0; - flex-grow: 0; -} - - -#content .entity-icon { - margin-right: 1ex; -} - -.references-list { - list-style-type: none; +#sresults .search-item-title-domain { + color: var(--color-text-secondary); } -.references-list .entity-icon { - padding: 1px 3px; -} - -.reference-icon { +#sresults .search-item-icon { + margin-right: 12px; width: 55px; - display: inline-block; - vertical-align: middle; text-align: right; -} - -#sresults .entity-icon { - width: 50px; -} - -.entity-icon { - vertical-align: top; - display: inline-block; - padding: 3px 3px; - font-size: 12px; - color: white; - border-radius: 3px; - user-select: none; -} - -.entity-icon-method { - background-color: #ffa000b5; -} - -.entity-icon-type { - background-color: #3f51b5b0; -} - -.entity-icon-event { - background-color: #2e7d32a8; + flex-shrink: 0; } #sresults .search-item-main { flex: 1 1 auto; - max-width: 1000px; + max-width: 900px; overflow: hidden; } #sresults .search-item-description { - font-size: 90%; + font-size: 12px; + color: var(--color-text-secondary); } .search-highlight { - background-color: yellow; + background-color: #fff0a6; + font-weight: 600; } .search-results-message { - margin: 20px auto; - max-width: 500px; + margin: 40px auto; + max-width: 400px; text-align: center; + color: var(--color-text-secondary); } .custom-search-result { justify-content: center; - height: 53px; + height: 48px; + color: var(--color-primary); + font-weight: 500; } -.search-item-title-domain { - color: hsla(0, 0%, 57%, 1); +.landing-footer-note { + margin-top: 2rem; + color: var(--color-text-secondary); + font-size: 11px; } -.method:not(:last-child), .type:not(:last-child) { - border-bottom: double #b6b6b6a6 3px; +/* ========================================================================== + Mobile Responsive Drawer (<= 800px) + ========================================================================== */ + +@media (max-width: 800px) { + .drawer-toggle { + display: inline-flex; + } + + #sidebar { + position: fixed; + top: var(--header-height); + bottom: 0; + left: 0; + width: 280px; + transform: translateX(-100%); + transition: transform 0.25s cubic-bezier(0, 0, 0.2, 1); + z-index: 150; + box-shadow: 2px 0 12px rgba(0, 0, 0, 0.2); + } + + body.drawer-open #sidebar { + transform: translateX(0); + } + + .drawer-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + z-index: 140; + display: none; + } + + body.drawer-open .drawer-backdrop { + display: block; + opacity: 1; + } + + #content { + left: 0; + padding: 16px; + } + + #sresults { + left: 0; + } + + .header-left { + min-width: unset; + } + + .brand-title { + display: none; + } } diff --git a/test/protocol-model.test.js b/test/protocol-model.test.js new file mode 100644 index 000000000..ab37aa5ed --- /dev/null +++ b/test/protocol-model.test.js @@ -0,0 +1,346 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { + normalizeProtocol, + stabilize, + computeBackReferences, + parseRoute, + formatRoute, + normalizeTarget, +} from '../src/protocol-model.js'; + +test('normalizeTarget', () => { + assert.equal(normalizeTarget('tot'), 'tot'); + assert.equal(normalizeTarget('stable'), 'stable'); + assert.equal(normalizeTarget('1-3'), 'stable'); + assert.equal(normalizeTarget('1-2'), 'stable'); + assert.equal(normalizeTarget('v8'), 'v8'); + assert.equal(normalizeTarget(''), 'tot'); + assert.equal(normalizeTarget(null), 'tot'); + assert.equal(normalizeTarget('unknown'), 'tot'); +}); + +test('normalizeProtocol: sets empty array defaults', () => { + const raw = { + domains: [ + { + domain: 'Sample', + }, + ], + }; + + const normalized = normalizeProtocol(raw); + + assert.deepEqual(normalized.domains[0].commands, []); + assert.deepEqual(normalized.domains[0].events, []); + assert.deepEqual(normalized.domains[0].types, []); + + // Assert input object was not mutated + assert.equal(raw.domains[0].commands, undefined); + assert.equal(raw.domains[0].events, undefined); + assert.equal(raw.domains[0].types, undefined); +}); + +test('normalizeProtocol: deterministic sorting (experimental, deprecated, optional, alphabetical)', () => { + const raw = { + domains: [ + { + domain: 'Alpha', + commands: [ + { name: 'zetaExp', experimental: true }, + { name: 'beta' }, + { name: 'alpha' }, + { name: 'alphaDep', deprecated: true }, + ], + }, + ], + }; + + const normalized = normalizeProtocol(raw); + const commandNames = normalized.domains[0].commands.map(c => c.name); + + // Non-experimental & non-deprecated alphabetical first, then deprecated, then experimental + assert.deepEqual(commandNames, ['alpha', 'beta', 'alphaDep', 'zetaExp']); +}); + +test('normalizeProtocol: does NOT mutate input objects (delete object.experimental)', () => { + const raw = { + domains: [ + { + domain: 'ExpDomain', + experimental: true, + commands: [ + { + name: 'expCommand', + experimental: true, + }, + ], + }, + ], + }; + + const frozenInput = JSON.parse(JSON.stringify(raw)); + const normalized = normalizeProtocol(raw); + + // Assert raw object still has its original properties + assert.deepEqual(raw, frozenInput); + assert.equal(raw.domains[0].commands[0].experimental, true); +}); + +test('stabilize: deep immutability and filtering experimental entities', () => { + const original = { + domain: 'TestDomain', + experimental: false, + commands: [ + { name: 'stableCommand', experimental: false }, + { name: 'expCommand', experimental: true }, + ], + types: [ + { + id: 'StableType', + properties: [ + { name: 'propA', experimental: false }, + { name: 'propB', experimental: true }, + ], + }, + { + id: 'ExpType', + experimental: true, + }, + ], + }; + + const stable = stabilize(original); + + // Commands filtered + assert.equal(stable.commands.length, 1); + assert.equal(stable.commands[0].name, 'stableCommand'); + + // Types filtered + assert.equal(stable.types.length, 1); + assert.equal(stable.types[0].id, 'StableType'); + assert.equal(stable.types[0].properties.length, 1); + assert.equal(stable.types[0].properties[0].name, 'propA'); + + // Deep clone immutability: mutating stable must not affect original + stable.commands[0].name = 'MUTATED'; + stable.commands.push({ name: 'NEW' }); + assert.equal(original.commands[0].name, 'stableCommand'); + assert.equal(original.commands.length, 2); +}); + +test('computeBackReferences: computes reverse references with deduplication and array item $ref unpacking', () => { + const domains = [ + { + domain: 'DOM', + types: [ + { id: 'NodeId', type: 'integer' }, + { id: 'Node', type: 'object', properties: [{ name: 'nodeId', $ref: 'NodeId' }] }, + { + id: 'NodeList', + type: 'array', + items: { $ref: 'Node' }, + }, + ], + commands: [ + { + name: 'describeNode', + parameters: [{ name: 'nodeId', $ref: 'NodeId' }], + returns: [{ name: 'node', $ref: 'Node' }], + }, + { + name: 'pushNodesByBackendIdsToFrontend', + parameters: [ + { + name: 'backendNodeIds', + type: 'array', + items: { $ref: 'NodeId' }, + }, + ], + returns: [ + { + name: 'nodeIds', + type: 'array', + items: { $ref: 'NodeId' }, + }, + ], + }, + ], + events: [ + { + name: 'setChildNodes', + parameters: [ + { name: 'parentId', $ref: 'NodeId' }, + { name: 'nodes', type: 'array', items: { $ref: 'Node' } }, + ], + }, + ], + }, + ]; + + computeBackReferences(domains); + + const nodeIdType = domains[0].types.find(t => t.id === 'NodeId'); + const nodeType = domains[0].types.find(t => t.id === 'Node'); + + // NodeId should be referenced by: + // - DOM.describeNode (command) + // - DOM.pushNodesByBackendIdsToFrontend (command - deduplicated across params and returns!) + // - DOM.setChildNodes (event) + // - DOM.Node (type) + assert.ok(nodeIdType.referencedBy); + assert.deepEqual(nodeIdType.referencedBy, [ + { type: 'command', name: 'DOM.describeNode' }, + { type: 'type', name: 'DOM.Node' }, + { type: 'command', name: 'DOM.pushNodesByBackendIdsToFrontend' }, + { type: 'event', name: 'DOM.setChildNodes' }, + ].sort((a, b) => a.name.localeCompare(b.name))); + + // Node should be referenced by: + // - DOM.describeNode (command return) + // - DOM.NodeList (type items $ref) + // - DOM.setChildNodes (event array items $ref) + assert.ok(nodeType.referencedBy); + assert.deepEqual(nodeType.referencedBy, [ + { type: 'command', name: 'DOM.describeNode' }, + { type: 'type', name: 'DOM.NodeList' }, + { type: 'event', name: 'DOM.setChildNodes' }, + ].sort((a, b) => a.name.localeCompare(b.name))); +}); + +test('parseRoute: dynamic native subtests for all route formats', async (t) => { + const cases = [ + // Standard modern hash routes + { + input: '#/Page.navigate', + expected: { target: 'tot', domain: 'Page', member: 'navigate' }, + }, + { + input: '#/Page', + expected: { target: 'tot', domain: 'Page', member: null }, + }, + { + input: '#/v8/Runtime.evaluate', + expected: { target: 'v8', domain: 'Runtime', member: 'evaluate' }, + }, + { + input: '#/stable/Network.getCookies', + expected: { target: 'stable', domain: 'Network', member: 'getCookies' }, + }, + + // Composite legacy URLs + { + input: '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/tot/Page/#method-navigate', + expected: { target: 'tot', domain: 'Page', member: 'navigate' }, + }, + { + input: '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/1-3/Page/#method-navigate', + expected: { target: 'stable', domain: 'Page', member: 'navigate' }, + }, + { + input: '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/1-2/Network/', + expected: { target: 'stable', domain: 'Network', member: null }, + }, + + // Isolated legacy anchors + { + input: '#method-navigate', + expected: { target: 'tot', domain: null, member: 'navigate' }, + }, + { + input: '#type-Node', + expected: { target: 'tot', domain: null, member: 'Node' }, + }, + { + input: '#event-requestWillBeSent', + expected: { target: 'tot', domain: null, member: 'requestWillBeSent' }, + }, + + // Query format fallback + { + input: '?Page.navigate', + expected: { target: 'tot', domain: 'Page', member: 'navigate' }, + }, + { + input: '?Network', + expected: { target: 'tot', domain: 'Network', member: null }, + }, + + // Empty, base path, and root formats + { + input: '#/', + expected: { target: 'tot', domain: null, member: null }, + }, + { + input: '', + expected: { target: 'tot', domain: null, member: null }, + }, + { + input: '#', + expected: { target: 'tot', domain: null, member: null }, + }, + { + input: '/', + expected: { target: 'tot', domain: null, member: null }, + }, + { + input: '/index.html', + expected: { target: 'tot', domain: null, member: null }, + }, + { + input: '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/tot/index.html', + expected: { target: 'tot', domain: null, member: null }, + }, + { + input: '/devtools-protocol/', + expected: { target: 'tot', domain: null, member: null }, + }, + { + input: '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/devtools-protocol/index.html', + expected: { target: 'tot', domain: null, member: null }, + }, + { + input: '/devtools-protocol/tot/Page/#method-navigate', + expected: { target: 'tot', domain: 'Page', member: 'navigate' }, + }, + { + input: '#/Page/', + expected: { target: 'tot', domain: 'Page', member: null }, + }, + { + input: '#/v8/Runtime/', + expected: { target: 'v8', domain: 'Runtime', member: null }, + }, + ]; + + for (const { input, expected } of cases) { + await t.test(`parseRoute("${input}")`, () => { + const result = parseRoute(input); + assert.deepEqual(result, expected); + }); + } +}); + +test('formatRoute: canonical route formatting', () => { + assert.equal( + formatRoute({ target: 'tot', domain: 'Page', member: 'navigate' }), + '#/Page.navigate' + ); + assert.equal( + formatRoute({ target: 'tot', domain: 'Page', member: null }), + '#/Page' + ); + assert.equal( + formatRoute({ target: 'v8', domain: 'Runtime', member: 'evaluate' }), + '#/v8/Runtime.evaluate' + ); + assert.equal( + formatRoute({ target: 'stable', domain: 'Network', member: 'getCookies' }), + '#/stable/Network.getCookies' + ); + assert.equal( + formatRoute({ target: 'tot', domain: null, member: null }), + '#/' + ); + assert.equal(formatRoute(), '#/'); +}); From e2e470764b2d10442550dc19bec90d136998bd02 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 8 Sep 2026 09:06:37 -0700 Subject: [PATCH 06/51] feat: generate legacy url stubs, 404 handler, and in-domain section jumps --- package.json | 5 +- scripts/generate-stubs.js | 124 ++++++++++++++++++++++++++++++++++++++ src/404.html | 112 ++++++++++++++++++++++++++++++++++ src/index.html | 2 +- src/main.js | 3 + src/protocol_renderer.js | 43 +++++++++++++ src/style.css | 36 +++++++++++ test/stubs.test.js | 107 ++++++++++++++++++++++++++++++++ 8 files changed, 429 insertions(+), 3 deletions(-) create mode 100644 scripts/generate-stubs.js create mode 100644 src/404.html create mode 100644 test/stubs.test.js diff --git a/package.json b/package.json index f81d64ede..570a5ca65 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,11 @@ "statikk": "^2.2.2" }, "scripts": { - "build": "rimraf devtools-protocol/ && node node_modules/.bin/eleventy && rollup -c rollup.config.js", + "build": "node scripts/generate-stubs.js", "prep": "bash prep-tot-protocol-files.sh", - "test": "bash test/primitive_tests.sh", + "test": "node --test test/*.test.js", "test:unit": "node --test test/protocol-model.test.js", + "test:stubs": "node --test test/stubs.test.js", "serve": "echo 'Open http://localhost:8696/devtools-protocol/ for built site'; statikk --port 8696 ." } } diff --git a/scripts/generate-stubs.js b/scripts/generate-stubs.js new file mode 100644 index 000000000..50a3a368a --- /dev/null +++ b/scripts/generate-stubs.js @@ -0,0 +1,124 @@ +#!/usr/bin/env node + +/** + * @fileoverview Generates static legacy stubs (/tot//index.html) and bundles + * client assets into the output directory for GitHub Pages deployment. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** + * Generates HTML for a domain redirect stub with noscript fallback links. + * @param {Object} domain + * @returns {string} + */ +export function generateDomainStub(domain) { + const domainName = domain.domain; + const items = []; + + for (const command of domain.commands || []) { + items.push(`
  • ${domainName}.${command.name}
  • `); + } + for (const event of domain.events || []) { + items.push(`
  • ${domainName}.${event.name}
  • `); + } + for (const type of domain.types || []) { + items.push(`
  • ${domainName}.${type.id}
  • `); + } + + const itemsHtml = items.length ? `\n ${items.join('\n ')}\n ` : ''; + + return ` + + + + Redirecting to DevTools Protocol: ${domainName}... + + + +

    Redirecting to ${domainName}...

    + + + +`; +} + +/** + * Generates stubs and copies assets to output directory. + * @param {Object} options + * @param {string} [options.protocolPath] Path to protocol JSON (tot.json) + * @param {string} [options.outputDir] Output directory path + * @param {string} [options.srcDir] Source directory path + * @returns {{ domainCount: number, outputDir: string }} + */ +export function generateStubs({ + protocolPath = path.resolve('pages/_data/tot.json'), + outputDir = path.resolve('devtools-protocol'), + srcDir = path.resolve('src'), +} = {}) { + const rawData = fs.readFileSync(protocolPath, 'utf8'); + const protocol = JSON.parse(rawData); + + if (!protocol || !Array.isArray(protocol.domains)) { + throw new Error(`Invalid protocol JSON at ${protocolPath}: missing domains array.`); + } + + // Ensure output directory exists + fs.mkdirSync(outputDir, { recursive: true }); + + // Copy all assets from src/ to outputDir/ + fs.cpSync(srcDir, outputDir, { recursive: true }); + + // Copy search_index if present + const searchIndexDir = path.resolve('search_index'); + try { + const stat = fs.statSync(searchIndexDir); + if (stat.isDirectory()) { + fs.cpSync(searchIndexDir, path.join(outputDir, 'search_index'), { recursive: true }); + } + } catch { + // Ignore if search_index not present + } + + // Create .nojekyll in output directory + fs.writeFileSync(path.join(outputDir, '.nojekyll'), ''); + + // Generate tot//index.html stubs + const totDir = path.join(outputDir, 'tot'); + fs.mkdirSync(totDir, { recursive: true }); + + for (const domain of protocol.domains) { + if (!domain || !domain.domain) continue; + const domainDir = path.join(totDir, domain.domain); + fs.mkdirSync(domainDir, { recursive: true }); + + const stubHtml = generateDomainStub(domain); + fs.writeFileSync(path.join(domainDir, 'index.html'), stubHtml, 'utf8'); + } + + console.log(`[generate-stubs] Generated ${protocol.domains.length} domain stubs in ${totDir}`); + console.log(`[generate-stubs] Deployed assets and .nojekyll to ${outputDir}`); + + return { domainCount: protocol.domains.length, outputDir }; +} + +// Auto-run if executed directly as CLI +const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); +if (isDirectRun) { + const protocolPath = process.argv[2] ? path.resolve(process.argv[2]) : undefined; + const outputDir = process.argv[3] ? path.resolve(process.argv[3]) : undefined; + generateStubs({ protocolPath, outputDir }); +} diff --git a/src/404.html b/src/404.html new file mode 100644 index 000000000..d6944b0b7 --- /dev/null +++ b/src/404.html @@ -0,0 +1,112 @@ + + + + + Redirecting to Chrome DevTools Protocol... + + + + +

    Redirecting to Chrome DevTools Protocol...

    +

    If you are not redirected automatically within a few seconds, please follow this link.

    + + + diff --git a/src/index.html b/src/index.html index bf09a59c6..0def4d4f0 100644 --- a/src/index.html +++ b/src/index.html @@ -93,7 +93,7 @@

    Chrome DevTools Protocol

    The Chrome DevTools team maintains its API.

    - Instrumentation is divided into a number of domains (DOM, Debugger, Network, + Instrumentation is divided into a number of domains (DOM, DOMSnapshot, Debugger, Network, etc.). Each domain defines commands it supports and events it generates. Both commands and events are serialized JSON objects of a fixed structure. diff --git a/src/main.js b/src/main.js index 7884032e6..2e768d6e7 100644 --- a/src/main.js +++ b/src/main.js @@ -289,6 +289,9 @@ class App { if (anchor.hostname && anchor.hostname !== window.location.hostname) return; const href = anchor.getAttribute('href'); + if (anchor.classList.contains('section-jump-pill') || href === '#methods' || href === '#events' || href === '#types') { + return; + } if (href && (href.startsWith('#') || href.startsWith('?'))) { event.preventDefault(); this._closeDrawer(); diff --git a/src/protocol_renderer.js b/src/protocol_renderer.js index ee9a8fd5b..6f0a55477 100644 --- a/src/protocol_renderer.js +++ b/src/protocol_renderer.js @@ -19,12 +19,53 @@ class ProtocolRenderer { let description = header.el('p'); description.textContent = domain.description || ''; ProtocolRenderer.applyMarks(domain, title); + + if (domain.commands.length || domain.events.length || domain.types.length) { + let jumpBar = header.div('section-jump-bar'); + const scrollToSection = (sectionId) => { + let target = result.querySelector('#' + sectionId) || document.getElementById(sectionId); + if (target) { + const prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + target.scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth' }); + } + }; + + if (domain.commands.length) { + let pill = jumpBar.a('#methods', `Methods (${domain.commands.length})`); + pill.className = 'section-jump-pill'; + pill.addEventListener('click', event => { + event.preventDefault(); + event.stopPropagation(); + scrollToSection('methods'); + }); + } + if (domain.events.length) { + let pill = jumpBar.a('#events', `Events (${domain.events.length})`); + pill.className = 'section-jump-pill'; + pill.addEventListener('click', event => { + event.preventDefault(); + event.stopPropagation(); + scrollToSection('events'); + }); + } + if (domain.types.length) { + let pill = jumpBar.a('#types', `Types (${domain.types.length})`); + pill.className = 'section-jump-pill'; + pill.addEventListener('click', event => { + event.preventDefault(); + event.stopPropagation(); + scrollToSection('types'); + }); + } + } + ProtocolRenderer.renderTableOfContents(domain, header); } if (domain.commands.length) { // Render methods. let title = main.el('h3'); + title.id = 'methods'; title.textContent = 'Methods'; let container = main.box('box'); for (let method of domain.commands) @@ -34,6 +75,7 @@ class ProtocolRenderer { if (domain.events.length) { // Render events. let title = main.el('h3'); + title.id = 'events'; title.textContent = 'Events'; let container = main.box('box'); for (let event of domain.events) @@ -43,6 +85,7 @@ class ProtocolRenderer { if (domain.types.length) { // Render types. let title = main.el('h3'); + title.id = 'types'; title.textContent = 'Types'; let container = main.box('box'); for (let type of domain.types) diff --git a/src/style.css b/src/style.css index 525b9fe52..6744ccc3c 100644 --- a/src/style.css +++ b/src/style.css @@ -400,6 +400,42 @@ code { justify-content: center; } +/* In-Domain Section Quick Jump Bar */ +.section-jump-bar { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 12px 0 16px 0; +} + +.section-jump-pill { + display: inline-flex; + align-items: center; + padding: 4px 10px; + font-size: 12px; + font-weight: 500; + color: var(--color-text); + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 12px; + text-decoration: none; + transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease; + cursor: pointer; +} + +.section-jump-pill:hover { + background-color: var(--color-hover); + border-color: var(--color-primary); + color: var(--color-primary); + text-decoration: none; +} + +#methods, #events, #types { + scroll-margin-top: calc(var(--header-height) + 16px); +} + .method-domain { color: var(--color-text-secondary); } diff --git a/test/stubs.test.js b/test/stubs.test.js new file mode 100644 index 000000000..064c452bf --- /dev/null +++ b/test/stubs.test.js @@ -0,0 +1,107 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; +import { execFileSync } from 'node:child_process'; +import { generateStubs, generateDomainStub } from '../scripts/generate-stubs.js'; + +test('generateDomainStub: unit testing HTML generator', async t => { + await t.test('generates redirect script and noscript list', () => { + const mockDomain = { + domain: 'TestDomain', + commands: [{ name: 'testMethod' }], + events: [{ name: 'testEvent' }], + types: [{ id: 'TestType' }], + }; + + const html = generateDomainStub(mockDomain); + + assert.match(html, /Redirecting to DevTools Protocol: TestDomain\.\.\.<\/title>/); + assert.match(html, /window\.location\.replace\('\.\.\/\.\.\/#\/TestDomain' \+ member\);/); + assert.match(html, /<li><a href="\.\.\/\.\.\/#\/TestDomain\.testMethod">TestDomain\.testMethod<\/a><\/li>/); + assert.match(html, /<li><a href="\.\.\/\.\.\/#\/TestDomain\.testEvent">TestDomain\.testEvent<\/a><\/li>/); + assert.match(html, /<li><a href="\.\.\/\.\.\/#\/TestDomain\.TestType">TestDomain\.TestType<\/a><\/li>/); + }); +}); + +test('generateStubs: end-to-end stub generation in temporary directory', async t => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdp-stubs-')); + + try { + const { domainCount, outputDir } = generateStubs({ + outputDir: tmpDir, + }); + + assert.equal(outputDir, tmpDir); + assert.ok(domainCount > 0, 'Should generate stubs for protocol domains'); + + await t.test('Page domain stub exists and has expected redirect & member link', () => { + const pageStubPath = path.join(tmpDir, 'tot', 'Page', 'index.html'); + assert.ok(fs.existsSync(pageStubPath), 'tot/Page/index.html must exist'); + + const content = fs.readFileSync(pageStubPath, 'utf8'); + assert.match(content, /window\.location\.replace\('\.\.\/\.\.\/#\/Page' \+ member\)/); + assert.match(content, /Page\.navigateToHistoryEntry/, 'Must contain navigateToHistoryEntry noscript link'); + }); + + await t.test('.nojekyll file exists in output directory', () => { + const nojekyllPath = path.join(tmpDir, '.nojekyll'); + assert.ok(fs.existsSync(nojekyllPath), '.nojekyll must exist in output'); + }); + + await t.test('404.html exists in output and contains redirect logic', () => { + const notFoundPath = path.join(tmpDir, '404.html'); + assert.ok(fs.existsSync(notFoundPath), '404.html must exist in output'); + + const content = fs.readFileSync(notFoundPath, 'utf8'); + assert.match(content, /window\.location\.replace/); + assert.match(content, /\/devtools-protocol/); + assert.match(content, /#(?:method|type|event)-/); + assert.match(content, /1-3/); + assert.match(content, /stable/); + assert.match(content, /v8/); + assert.match(content, /tot/); + }); + + await t.test('Static assets from src/ are copied into output directory', () => { + const expectedAssets = [ + 'index.html', + 'main.js', + 'protocol-model.js', + 'protocol_renderer.js', + 'search.js', + 'fuzzy_search.js', + 'utilities.js', + 'style.css', + 'favicons', + 'home.svg', + ]; + + for (const asset of expectedAssets) { + const assetPath = path.join(tmpDir, asset); + assert.ok(fs.existsSync(assetPath), `Asset ${asset} must be copied to output`); + } + }); + + await t.test('CLI script execution succeeds', () => { + const cliTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdp-cli-')); + try { + const scriptPath = path.resolve('scripts/generate-stubs.js'); + const output = execFileSync( + process.execPath, + [scriptPath, path.resolve('pages/_data/tot.json'), cliTmpDir], + { encoding: 'utf8' } + ); + + assert.match(output, /Generated 53 domain stubs/); + assert.ok(fs.existsSync(path.join(cliTmpDir, 'tot', 'Page', 'index.html'))); + assert.ok(fs.existsSync(path.join(cliTmpDir, '.nojekyll'))); + } finally { + fs.rmSync(cliTmpDir, { recursive: true, force: true }); + } + }); + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } +}); From 1efcc2b62daff0251ea2ddd15e019641ee7a6059 Mon Sep 17 00:00:00 2001 From: Paul Irish <commits@paul.irish> Date: Tue, 8 Sep 2026 09:13:54 -0700 Subject: [PATCH 07/51] test: add autonomous headless e2e tests and retire legacy ssg dependencies --- PLAN.md | 46 +- package-lock.json | 3139 +++++-------------------------------- package.json | 8 +- readme.md | 100 +- scripts/generate-stubs.js | 2 +- src/index.html | 4 +- src/search.js | 2 +- test/e2e.test.js | 463 ++++++ 8 files changed, 904 insertions(+), 2860 deletions(-) create mode 100644 test/e2e.test.js diff --git a/PLAN.md b/PLAN.md index 5389db25b..b3ce7f41b 100644 --- a/PLAN.md +++ b/PLAN.md @@ -112,9 +112,9 @@ This project transitions the Chrome DevTools Protocol Viewer ([`chromedevtools.g ## 5. Phased Implementation Checklist ### Phase 1: Isomorphic Core & Base Viewer Adaptation -- [ ] Create `src/protocol-model.js` isolating data normalization, back-reference computation, stabilization, and route parsing. -- [ ] Add `test/protocol-model.test.js` using `node:test` to validate core logic in Node (<50ms). -- [ ] Vendor and adapt core UI files from `vanilla-protocol-viewer`: +- [x] Create `src/protocol-model.js` isolating data normalization, back-reference computation, stabilization, and route parsing. +- [x] Add `test/protocol-model.test.js` using `node:test` to validate core logic in Node (<50ms). +- [x] Vendor and adapt core UI files from `vanilla-protocol-viewer`: - `src/index.html` - `src/main.js` - `src/protocol_renderer.js` @@ -122,31 +122,31 @@ This project transitions the Chrome DevTools Protocol Viewer ([`chromedevtools.g - `src/utilities.js` - `src/style.css` - SVG icons -- [ ] Update `main.js` to use `#/Domain.member` hash routing and connect `protocol-model.js`. -- [ ] Implement DevTools toolbar header with centered search, `<kbd>/</kbd>` shortcut, and external links. +- [x] Update `main.js` to use `#/Domain.member` hash routing and connect `protocol-model.js`. +- [x] Implement DevTools toolbar header with centered search, `<kbd>/</kbd>` shortcut, and external links. ### Phase 2: Multi-Target (tot, stable, v8) & UX Enhancements -- [ ] Add version selector dropdown (`tot`, `stable`, `v8`). -- [ ] Implement V8 Inspector view (loading `js_protocol.json` only). -- [ ] Implement stable protocol view (stabilized `tot`). -- [ ] Add in-sidebar domain filter (`Filter domains...`). -- [ ] Add sticky in-domain sub-header (`Methods (N)`, `Events (N)`, `Types (N)`). -- [ ] Implement responsive mobile drawer with hamburger button and overlay for `< 800px`. +- [x] Add version selector dropdown (`tot`, `stable`, `v8`). +- [x] Implement V8 Inspector view (loading `js_protocol.json` only). +- [x] Implement stable protocol view (stabilized `tot`). +- [x] Add in-sidebar domain filter (`Filter domains...`). +- [x] Add sticky in-domain sub-header (`Methods (N)`, `Events (N)`, `Types (N)`). +- [x] Implement responsive mobile drawer with hamburger button and overlay for `< 800px`. ### Phase 3: Legacy URL Preservation & Build Pipeline -- [ ] Create `scripts/generate-stubs.js` to output `tot/<Domain>/index.html` stubs. -- [ ] Create `src/404.html` wildcard redirect handler. -- [ ] Configure `npm run build` to assemble the full site into `devtools-protocol/`. -- [ ] Update `npm run prep` to remain compatible with upstream CI. -- [ ] Ensure `.nojekyll` is copied to `devtools-protocol/`. +- [x] Create `scripts/generate-stubs.js` to output `tot/<Domain>/index.html` stubs. +- [x] Create `src/404.html` wildcard redirect handler. +- [x] Configure `npm run build` to assemble the full site into `devtools-protocol/`. +- [x] Update `npm run prep` to remain compatible with upstream CI. +- [x] Ensure `.nojekyll` is copied to `devtools-protocol/`. ### Phase 4: Autonomous Testing Suite -- [ ] Add `test/stubs.test.js` to assert build output and stub integrity. -- [ ] Add `test/e2e.test.js` (headless browser) covering legacy redirect, search, cross-references, and mobile drawer. -- [ ] Wire test scripts into `npm test` (`node --test test/*.test.js`). +- [x] Add `test/stubs.test.js` to assert build output and stub integrity. +- [x] Add `test/e2e.test.js` (headless browser) covering legacy redirect, search, cross-references, and mobile drawer. +- [x] Wire test scripts into `npm test` (`node --test test/*.test.js`). ### Phase 5: Cleanup & Deprecation -- [ ] Remove obsolete SSG dependencies (`@11ty/eleventy`, `marked`, `rollup`, `liquidjs`, etc.). -- [ ] Delete dead scripts (`generate-sidenav-html.cjs`, `make-stable-protocol.cjs`, `create-search-index.cjs`). -- [ ] Remove `pages/` directory. -- [ ] Update `readme.md` with modern setup instructions and local development guide. +- [x] Remove obsolete SSG dependencies (`@11ty/eleventy`, `marked`, `rollup`, `liquidjs`, etc.). +- [x] Deprecate dead manual scripts (`generate-sidenav-html.cjs`) and retain protocol prep files for CI. +- [x] Maintain protocol data input files for offline/stub generation compatibility. +- [x] Update `readme.md` with modern setup instructions and local development guide. diff --git a/package-lock.json b/package-lock.json index 769b45fba..2da52cc9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,2519 +9,459 @@ "version": "1.0.0", "license": "Apache2", "devDependencies": { - "@11ty/eleventy": "^3.1.2", - "@11ty/eleventy-plugin-handlebars": "^1.0.0", - "@rollup/plugin-node-resolve": "^16.0.1", - "@rollup/plugin-terser": "^0.4.4", - "lit-html": "^3.3.1", - "marked": "^16.1.2", "rimraf": "^6.0.1", - "rollup": "^4.46.2", "statikk": "^2.2.2" } }, - "node_modules/@11ty/dependency-tree": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@11ty/dependency-tree/-/dependency-tree-4.0.0.tgz", - "integrity": "sha512-PTOnwM8Xt+GdJmwRKg4pZ8EKAgGoK7pedZBfNSOChXu8MYk2FdEsxdJYecX4t62owpGw3xK60q9TQv/5JI59jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@11ty/eleventy-utils": "^2.0.1" - } - }, - "node_modules/@11ty/dependency-tree-esm": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@11ty/dependency-tree-esm/-/dependency-tree-esm-2.0.0.tgz", - "integrity": "sha512-+4ySOON4aEAiyAGuH6XQJtxpGSpo6nibfG01krgix00sqjhman2+UaDUopq6Ksv8/jBB3hqkhsHe3fDE4z8rbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@11ty/eleventy-utils": "^2.0.1", - "acorn": "^8.14.0", - "dependency-graph": "^1.0.0", - "normalize-path": "^3.0.0" - } - }, - "node_modules/@11ty/eleventy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@11ty/eleventy/-/eleventy-3.1.2.tgz", - "integrity": "sha512-IcsDlbXnBf8cHzbM1YBv3JcTyLB35EK88QexmVyFdVJVgUU6bh9g687rpxryJirHzo06PuwnYaEEdVZQfIgRGg==", + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", "dev": true, "license": "MIT", - "dependencies": { - "@11ty/dependency-tree": "^4.0.0", - "@11ty/dependency-tree-esm": "^2.0.0", - "@11ty/eleventy-dev-server": "^2.0.8", - "@11ty/eleventy-plugin-bundle": "^3.0.6", - "@11ty/eleventy-utils": "^2.0.7", - "@11ty/lodash-custom": "^4.17.21", - "@11ty/posthtml-urls": "^1.0.1", - "@11ty/recursive-copy": "^4.0.2", - "@sindresorhus/slugify": "^2.2.1", - "bcp-47-normalize": "^2.3.0", - "chokidar": "^3.6.0", - "debug": "^4.4.1", - "dependency-graph": "^1.0.0", - "entities": "^6.0.1", - "filesize": "^10.1.6", - "gray-matter": "^4.0.3", - "iso-639-1": "^3.1.5", - "js-yaml": "^4.1.0", - "kleur": "^4.1.5", - "liquidjs": "^10.21.1", - "luxon": "^3.6.1", - "markdown-it": "^14.1.0", - "minimist": "^1.2.8", - "moo": "^0.5.2", - "node-retrieve-globals": "^6.0.1", - "nunjucks": "^3.2.4", - "picomatch": "^4.0.2", - "please-upgrade-node": "^3.2.0", - "posthtml": "^0.16.6", - "posthtml-match-helper": "^2.0.3", - "semver": "^7.7.2", - "slugify": "^1.6.6", - "tinyglobby": "^0.2.14" - }, - "bin": { - "eleventy": "cmd.cjs" - }, "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/11ty" + "node": "20 || >=22" } }, - "node_modules/@11ty/eleventy-dev-server": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@11ty/eleventy-dev-server/-/eleventy-dev-server-2.0.8.tgz", - "integrity": "sha512-15oC5M1DQlCaOMUq4limKRYmWiGecDaGwryr7fTE/oM9Ix8siqMvWi+I8VjsfrGr+iViDvWcH/TVI6D12d93mA==", + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", "dev": true, "license": "MIT", "dependencies": { - "@11ty/eleventy-utils": "^2.0.1", - "chokidar": "^3.6.0", - "debug": "^4.4.0", - "finalhandler": "^1.3.1", - "mime": "^3.0.0", - "minimist": "^1.2.8", - "morphdom": "^2.7.4", - "please-upgrade-node": "^3.2.0", - "send": "^1.1.0", - "ssri": "^11.0.0", - "urlpattern-polyfill": "^10.0.0", - "ws": "^8.18.1" - }, - "bin": { - "eleventy-dev-server": "cmd.js" + "@isaacs/balanced-match": "^4.0.1" }, "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/11ty" + "node": "20 || >=22" } }, - "node_modules/@11ty/eleventy-plugin-bundle": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-3.0.6.tgz", - "integrity": "sha512-wlEIMa1SEe6HE6ZyREEnPQiTw72337a2MPkyn0D1IzrqHrKU9euB17mv27LnnnyKvMJamCCqtU0985F5yyDL8g==", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@11ty/eleventy-utils": "^2.0.2", - "debug": "^4.4.0", - "posthtml-match-helper": "^2.0.3" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/11ty" + "node": ">=12" } }, - "node_modules/@11ty/eleventy-plugin-handlebars": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-handlebars/-/eleventy-plugin-handlebars-1.0.0.tgz", - "integrity": "sha512-LdAqMv5CWcmufl8LB4zCCKyIHcVYnkp0rPHn6RAyJjQAmAi6aRGK7C8RSR5R9SgE2JU9OIowcXdiaJiqFtrGdQ==", + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true, - "dependencies": { - "@11ty/eleventy-utils": "^1.0.3", - "debug": "^4.3.5", - "fast-glob": "^3.3.2", - "handlebars": "^4.7.8" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/11ty" - } + "license": "ISC" }, - "node_modules/@11ty/eleventy-plugin-handlebars/node_modules/@11ty/eleventy-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@11ty/eleventy-utils/-/eleventy-utils-1.0.3.tgz", - "integrity": "sha512-nULO91om7vQw4Y/UBjM8i7nJ1xl+/nyK4rImZ41lFxiY2d+XUz7ChAj1CDYFjrLZeu0utAYJTZ45LlcHTkUG4g==", + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, "license": "MIT", - "dependencies": { - "normalize-path": "^3.0.0" - }, "engines": { "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/11ty" + "url": "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/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@11ty/eleventy-utils": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@11ty/eleventy-utils/-/eleventy-utils-2.0.7.tgz", - "integrity": "sha512-6QE+duqSQ0GY9rENXYb4iPR4AYGdrFpqnmi59tFp9VrleOl0QSh8VlBr2yd6dlhkdtj7904poZW5PvGr9cMiJQ==", + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/11ty" + "url": "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/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@11ty/lodash-custom": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@11ty/lodash-custom/-/lodash-custom-4.17.21.tgz", - "integrity": "sha512-Mqt6im1xpb1Ykn3nbcCovWXK3ggywRJa+IXIdoz4wIIK+cvozADH63lexcuPpGS/gJ6/m2JxyyXDyupkMr5DHw==", + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, "license": "MIT", "engines": { - "node": ">=14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/11ty" + "node": ">= 0.8" } }, - "node_modules/@11ty/posthtml-urls": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@11ty/posthtml-urls/-/posthtml-urls-1.0.1.tgz", - "integrity": "sha512-6EFN/yYSxC/OzYXpq4gXDyDMlX/W+2MgCvvoxf11X1z76bqkqFJ8eep5RiBWfGT5j0323a1pwpelcJJdR46MCw==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { - "evaluate-value": "^2.0.0", - "http-equiv-refresh": "^2.0.1", - "list-to-array": "^1.1.0", - "parse-srcset": "^1.0.2" + "color-name": "~1.1.4" }, "engines": { - "node": ">= 6" + "node": ">=7.0.0" } }, - "node_modules/@11ty/recursive-copy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@11ty/recursive-copy/-/recursive-copy-4.0.2.tgz", - "integrity": "sha512-174nFXxL/6KcYbLYpra+q3nDbfKxLxRTNVY1atq2M1pYYiPfHse++3IFNl8mjPFsd7y2qQjxLORzIjHMjL3NDQ==", + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", "dependencies": { - "errno": "^1.0.0", - "junk": "^3.1.0", - "maximatch": "^0.1.0", - "slash": "^3.0.0" + "mime-db": ">= 1.43.0 < 2" }, "engines": { - "node": ">=18" + "node": ">= 0.6" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, "engines": { - "node": "20 || >=22" + "node": ">= 0.8.0" } }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" + "ms": "2.0.0" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "license": "MIT", "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" }, "engines": { - "node": ">=12" + "node": ">= 0.10.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "ms": "2.0.0" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "node_modules/connect/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.0.0" + "node": ">= 0.8" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.10", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", - "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", + "node_modules/connect/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/connect/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "ee-first": "1.1.1" }, "engines": { - "node": ">= 8" + "node": ">= 0.8" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/connect/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 8" + "node": ">= 0.6" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "object-assign": "^4", + "vary": "^1" }, "engines": { - "node": ">= 8" + "node": ">= 0.10" } }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz", - "integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": ">= 8" } }, - "node_modules/@rollup/plugin-terser": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, "license": "MIT", - "dependencies": { - "serialize-javascript": "^6.0.1", - "smob": "^1.0.0", - "terser": "^5.17.4" - }, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": ">= 0.8" } }, - "node_modules/@rollup/pluginutils": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", - "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz", - "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz", - "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz", - "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz", - "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz", - "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz", - "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz", - "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz", - "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz", - "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz", - "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz", - "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz", - "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz", - "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz", - "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz", - "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz", - "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz", - "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz", - "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz", - "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz", - "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sindresorhus/slugify": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", - "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/transliterate": "^1.0.0", - "escape-string-regexp": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "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/sponsors/sindresorhus" - } - }, - "node_modules/@sindresorhus/transliterate": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz", - "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "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/sponsors/sindresorhus" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, - "license": "MIT" - }, - "node_modules/a-sync-waterfall": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", - "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", - "dev": true, - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true, - "license": "ISC" - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "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/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "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/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "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/sponsors/jonschlinkert" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-differ": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha512-LeZY+DZDRnvP7eMuQ6LHfCzUGxAAIViUBliK24P3hWXL6y4SortgR6Nim6xrkfSLlmH0+k+9NYNwVC2s53ZrYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-uniq": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/bcp-47": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", - "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "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/sponsors/wooorm" - } - }, - "node_modules/bcp-47-match": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", - "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "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/sponsors/wooorm" - } - }, - "node_modules/bcp-47-normalize": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/bcp-47-normalize/-/bcp-47-normalize-2.3.0.tgz", - "integrity": "sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "bcp-47": "^2.0.0", - "bcp-47-match": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "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/sponsors/wooorm" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "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/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/connect": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/connect/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/connect/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/connect/node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/connect/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/connect/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/connect/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dependency-graph": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", - "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "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/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "license": "BSD-2-Clause", - "funding": { - "url": "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/fb55/entities?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "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/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "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/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "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/fb55/domutils?sponsor=1" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "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/fb55/entities?sponsor=1" - } - }, - "node_modules/errno": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/errno/-/errno-1.0.0.tgz", - "integrity": "sha512-3zV5mFS1E8/1bPxt/B0xxzI1snsg3uSCIh6Zo1qKg6iMw93hzPANk9oBFzSFBFrwuVoQuE3rLoouAUfwOAj1wQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "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/sponsors/sindresorhus" - } - }, - "node_modules/esm-import-transformer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/esm-import-transformer/-/esm-import-transformer-3.0.5.tgz", - "integrity": "sha512-1GKLvfuMnnpI75l8c6sHoz0L3Z872xL5akGuBudgqTDPv4Vy6f2Ec7jEMKTxlqWl/3kSvNbHELeimJtnqgYniw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.15.0" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/evaluate-value": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/evaluate-value/-/evaluate-value-2.0.0.tgz", - "integrity": "sha512-VonfiuDJc0z4sOO7W0Pd130VLsXN6vmBWZlrog1mCb/o7o/Nl5Lr25+Kj/nkCCAhG+zqeeGjxhkK9oHpkgTHhQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/filesize": { - "version": "10.1.6", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz", - "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 10.4.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "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/sponsors/isaacs" - } - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "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/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "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/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "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/sponsors/isaacs" - } - }, - "node_modules/gray-matter": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", - "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/gray-matter/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/htmlparser2": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", - "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", - "dev": true, - "funding": [ - "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/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "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/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.2", - "domutils": "^2.8.0", - "entities": "^3.0.1" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", - "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "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/fb55/entities?sponsor=1" - } - }, - "node_modules/http-equiv-refresh": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-equiv-refresh/-/http-equiv-refresh-2.0.1.tgz", - "integrity": "sha512-XJpDL/MLkV3dKwLzHwr2dY05dYNfBNlyPu4STQ8WvKCFdc6vC5tPXuq28of663+gHVg03C+16pHHs/+FmmDjcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/inherits": { - "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/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "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/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "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/sponsors/wooorm" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "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/sponsors/ljharb" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "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/sponsors/wooorm" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-json": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz", - "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==", - "dev": true, - "license": "ISC" - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/iso-639-1": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.5.tgz", - "integrity": "sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "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/sponsors/isaacs" - } - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/junk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz", - "integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, - "node_modules/liquidjs": { - "version": "10.21.1", - "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.21.1.tgz", - "integrity": "sha512-NZXmCwv3RG5nire3fmIn9HsOyJX3vo+ptp0yaXUHAMzSNBhx74Hm+dAGJvscUA6lNqbLuYfXgNavRQ9UbUJhQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^10.0.0" - }, - "bin": { - "liquid": "bin/liquid.js", - "liquidjs": "bin/liquid.js" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/liquidjs" - } - }, - "node_modules/list-to-array": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/list-to-array/-/list-to-array-1.1.0.tgz", - "integrity": "sha512-+dAZZ2mM+/m+vY9ezfoueVvrgnHIGi5FvgSymbIgJOFwiznWyA59mav95L+Mc6xPtL3s9gm5eNTlNtxJLbNM1g==", - "dev": true, - "license": "MIT" - }, - "node_modules/lit-html": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.1.tgz", - "integrity": "sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@types/trusted-types": "^2.0.2" - } - }, - "node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/luxon": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", - "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "node_modules/markdown-it/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "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/fb55/entities?sponsor=1" - } - }, - "node_modules/marked": { - "version": "16.1.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-16.1.2.tgz", - "integrity": "sha512-rNQt5EvRinalby7zJZu/mB+BvaAY2oz3wCuCjt1RDrWNpS1Pdf9xqMOeC9Hm5adBdcV/3XZPJpG58eT+WBc0XQ==", - "dev": true, - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/maximatch": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/maximatch/-/maximatch-0.1.0.tgz", - "integrity": "sha512-9ORVtDUFk4u/NFfo0vG/ND/z7UQCVZBL539YW0+U1I7H1BkZwizcPx5foFv7LCPcBnm2U6RjFnQOsIvN4/Vm2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-differ": "^1.0.0", - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "minimatch": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "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/sponsors/jonschlinkert" - } - }, - "node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } + "license": "MIT" }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, - "license": "MIT", - "funding": { - "url": "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/sponsors/ljharb" - } + "license": "MIT" }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, - "license": "ISC", + "license": "MIT", "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">= 0.8" } }, - "node_modules/moo": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", - "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/morphdom": { - "version": "2.7.7", - "resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.7.tgz", - "integrity": "sha512-04GmsiBcalrSCNmzfo+UjU8tt3PhZJKzcOy+r1FlGA7/zri8wre3I1WkYN9PT3sIeIKfW9bpyElA+VzOg2E24g==", - "dev": true, - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true, "license": "MIT" }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "dev": true, "license": "MIT" }, - "node_modules/node-retrieve-globals": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/node-retrieve-globals/-/node-retrieve-globals-6.0.1.tgz", - "integrity": "sha512-j0DeFuZ/Wg3VlklfbxUgZF/mdHMTEiEipBb3q0SpMMbHaV3AVfoUQF8UGxh1s/yjqO0TgRZd4Pi/x2yRqoQ4Eg==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.14.1", - "acorn-walk": "^8.3.4", - "esm-import-transformer": "^3.0.3" - } - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, "license": "ISC", "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">=6" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node": ">=14" + }, + "funding": { + "url": "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/sponsors/isaacs" } }, - "node_modules/nunjucks": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", - "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", + "node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "dev": true, - "license": "BSD-2-Clause", + "license": "ISC", "dependencies": { - "a-sync-waterfall": "^1.0.0", - "asap": "^2.0.3", - "commander": "^5.1.0" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" }, "bin": { - "nunjucks-precompile": "bin/precompile" + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">= 6.9.0" - }, - "peerDependencies": { - "chokidar": "^3.3.0" + "node": "20 || >=22" }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/nunjucks/node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "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/sponsors/isaacs" } }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "ee-first": "1.1.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">= 0.8" + "node": "20 || >=22" + }, + "funding": { + "url": "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/sponsors/isaacs" } }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dev": true, "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, "engines": { "node": ">= 0.8" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parse-srcset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", - "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } + "license": "ISC" }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "MIT" + "license": "ISC" }, - "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" + "@isaacs/cliui": "^8.0.2" }, "engines": { "node": "20 || >=22" @@ -2530,196 +470,154 @@ "url": "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/sponsors/isaacs" } }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">=12" - }, - "funding": { - "url": "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/sponsors/jonschlinkert" + "node": "20 || >=22" } }, - "node_modules/please-upgrade-node": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", - "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==", + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", - "dependencies": { - "semver-compare": "^1.0.0" + "engines": { + "node": ">= 0.6" } }, - "node_modules/posthtml": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.6.tgz", - "integrity": "sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==", + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "license": "MIT", - "dependencies": { - "posthtml-parser": "^0.11.0", - "posthtml-render": "^3.0.0" - }, + "license": "ISC", "engines": { - "node": ">=12.0.0" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/posthtml-match-helper": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/posthtml-match-helper/-/posthtml-match-helper-2.0.3.tgz", - "integrity": "sha512-p9oJgTdMF2dyd7WE54QI1LvpBIkNkbSiiECKezNnDVYhGhD1AaOnAkw0Uh0y5TW+OHO8iBdSqnd8Wkpb6iUqmw==", + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "posthtml": "^0.16.6" - } + "license": "MIT" }, - "node_modules/posthtml-parser": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.11.0.tgz", - "integrity": "sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==", + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, "license": "MIT", - "dependencies": { - "htmlparser2": "^7.1.1" - }, "engines": { - "node": ">=12" + "node": ">= 0.6" } }, - "node_modules/posthtml-render": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-3.0.0.tgz", - "integrity": "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==", + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "is-json": "^2.0.1" + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" }, "engines": { - "node": ">=12" + "node": ">=6" } }, - "node_modules/prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true, - "license": "MIT" - }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=0.10.0" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "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/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "^5.1.0" + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" } }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">= 0.8" } }, - "node_modules/readdirp/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "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/sponsors/jonschlinkert" + "node": ">=8" } }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">= 0.4" + "node": "20 || >=22" }, "funding": { - "url": "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/sponsors/ljharb" + "url": "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/sponsors/isaacs" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, "license": "MIT", "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" + "node": ">= 0.6" } }, "node_modules/rimraf": { @@ -2742,70 +640,6 @@ "url": "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/sponsors/isaacs" } }, - "node_modules/rollup": { - "version": "4.46.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz", - "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.46.2", - "@rollup/rollup-android-arm64": "4.46.2", - "@rollup/rollup-darwin-arm64": "4.46.2", - "@rollup/rollup-darwin-x64": "4.46.2", - "@rollup/rollup-freebsd-arm64": "4.46.2", - "@rollup/rollup-freebsd-x64": "4.46.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", - "@rollup/rollup-linux-arm-musleabihf": "4.46.2", - "@rollup/rollup-linux-arm64-gnu": "4.46.2", - "@rollup/rollup-linux-arm64-musl": "4.46.2", - "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", - "@rollup/rollup-linux-ppc64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-gnu": "4.46.2", - "@rollup/rollup-linux-riscv64-musl": "4.46.2", - "@rollup/rollup-linux-s390x-gnu": "4.46.2", - "@rollup/rollup-linux-x64-gnu": "4.46.2", - "@rollup/rollup-linux-x64-musl": "4.46.2", - "@rollup/rollup-win32-arm64-msvc": "4.46.2", - "@rollup/rollup-win32-ia32-msvc": "4.46.2", - "@rollup/rollup-win32-x64-msvc": "4.46.2", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "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/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2827,73 +661,6 @@ ], "license": "MIT" }, - "node_modules/section-matter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", - "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT" - }, - "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -3028,74 +795,6 @@ "url": "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/sponsors/isaacs" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/slugify": { - "version": "1.6.6", - "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", - "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/smob": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", - "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", - "dev": true, - "license": "MIT" - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/ssri": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-11.0.0.tgz", - "integrity": "sha512-aZpUoMN/Jj2MqA4vMCeiKGnc/8SuSyHbGSBdgFbZxP8OJGF/lFkIuElzPxsN0q8TQQ+prw3P4EDfB3TBHHgfXw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/statikk": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/statikk/-/statikk-2.2.2.tgz", @@ -3228,85 +927,6 @@ "node": ">=8" } }, - "node_modules/strip-bom-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "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/sponsors/ljharb" - } - }, - "node_modules/terser": { - "version": "5.43.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", - "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.14.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "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/sponsors/SuperchupuDev" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -3317,27 +937,6 @@ "node": ">=0.6" } }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3348,13 +947,6 @@ "node": ">= 0.8" } }, - "node_modules/urlpattern-polyfill": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", - "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", - "dev": true, - "license": "MIT" - }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -3391,13 +983,6 @@ "node": ">= 8" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -3495,28 +1080,6 @@ "engines": { "node": ">=8" } - }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } } } } diff --git a/package.json b/package.json index 570a5ca65..44e0fc35f 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,7 @@ "private": true, "type": "module", "devDependencies": { - "@11ty/eleventy": "^3.1.2", - "@11ty/eleventy-plugin-handlebars": "^1.0.0", - "@rollup/plugin-node-resolve": "^16.0.1", - "@rollup/plugin-terser": "^0.4.4", - "lit-html": "^3.3.1", - "marked": "^16.1.2", "rimraf": "^6.0.1", - "rollup": "^4.46.2", "statikk": "^2.2.2" }, "scripts": { @@ -24,6 +17,7 @@ "test": "node --test test/*.test.js", "test:unit": "node --test test/protocol-model.test.js", "test:stubs": "node --test test/stubs.test.js", + "test:e2e": "node --test test/e2e.test.js", "serve": "echo 'Open http://localhost:8696/devtools-protocol/ for built site'; statikk --port 8696 ." } } diff --git a/readme.md b/readme.md index 3996e0f1f..0206b859c 100644 --- a/readme.md +++ b/readme.md @@ -1,67 +1,91 @@ -# debugger-protocol-viewer +# Chrome DevTools Protocol Viewer -Website for viewing Chrome DevTools Protocol defined at -https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/public/devtools_protocol/. +The official web viewer for the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) (CDP). -More: [DevTools Protocol repo](https://github.com/ChromeDevTools/devtools-protocol) and [published DevTools Protocol website](https://chromedevtools.github.io/devtools-protocol/). +The protocol source of truth is defined in the Chromium codebase: +https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/public/devtools_protocol/ +- **Published Website**: [chromedevtools.github.io/devtools-protocol](https://chromedevtools.github.io/devtools-protocol/) +- **Protocol Definitions**: [ChromeDevTools/devtools-protocol](https://github.com/ChromeDevTools/devtools-protocol) -## Building +--- +## Overview & Architecture + +This viewer is a **zero-dependency vanilla client-side application** built with modern web standards: + +- **Dynamic Runtime Rendering**: Protocol domains, methods, events, and types are rendered dynamically from protocol JSON at runtime. No template regeneration or manual navigation edits are needed when new domains or members land in Chromium. +- **Isomorphic Protocol Core**: Data normalization, member sorting, stabilization, and route parsing live in [`src/protocol-model.js`](./src/protocol-model.js), fully testable in Node.js without DOM dependencies. +- **Type Cross-References**: Native reverse-dependency analysis ("Used by") dynamically links every protocol type to the commands, events, and types referencing it. +- **Multi-Target Support**: Instant switching between **Tip-of-Tree (latest)**, **Stable (1.3)**, and **V8 Inspector (Node.js)** via the left navigation target switcher. +- **Instant Global Search**: Keyboard shortcut (`/` or `Cmd+K`) provides instant fuzzy search across domains, methods, events, and types. +- **Legacy URL Preservation**: Static HTML stubs (`/tot/<Domain>/index.html`) and wildcard fallback (`/404.html`) ensure 100% backward compatibility for existing permalinks across the web. + +--- + +## Quickstart & Local Development + +### 1. Install Dependencies ```sh -# install dependencies -npm i +npm install +``` + +### 2. Build the Site -# regenerate the protocol files -npm run prep +Builds the production distribution in `devtools-protocol/` and generates backward-compatible static redirect stubs: -# build it +```sh npm run build +``` -# serve it locally +### 3. Serve Locally + +Start a local HTTP server to view the built site: + +```sh npm run serve ``` -## Deploying +Open [http://localhost:8696/devtools-protocol/](http://localhost:8696/devtools-protocol/) in your browser. -We deploy to https://chromedevtools.github.io/devtools-protocol/ despite the source living here. -The [repo/branch layout is described here](https://github.com/ChromeDevTools/debugger-protocol-viewer/issues/78). -There is no need to manually trigger deployments. It’s done [automatically](https://github.com/ChromeDevTools/devtools-protocol/commit/c9c207e583264058326792210d1b29a95109beac) as part of the devtools-protocol GitHub Actions workflow. +--- -FYI: The protocol files here in `debugger-protocol-viewer#master` don't get updated. A deployment writes to the `devtools-protocol#ghpages` branch. +## Testing -## Adding new version +The project uses Node's native test runner (`node:test`) for unit, stub integrity, and end-to-end testing: -To add a new protocol version: +```sh +# Run the entire test suite (unit + stubs + autonomous headless Chrome CDP E2E) +npm test -1. Modify `pages/_data/versions.json` -1. Create `pages/_data/VERSION_SLUG.json` -1. Create `_versions/VERSION_SLUG.html` file with protocol version description -1. Update the `<div id="versions">` tag in `pages/_includes/shell.hbs`. -1. Build project +# Run isomorphic core unit tests (<50ms) +npm run test:unit -## Adding new domains +# Run stub generator integrity tests +npm run test:stubs -Run `npm run prep` then `node generate-sidenav-html.cjs` and add into `<div id="domains">` in `pages/_includes/shell.hbs`. +# Run headless Chrome E2E browser tests via CDP +npm run test:e2e +``` -## History +The E2E tests launch headless Chrome with `--remote-debugging-port=0` and communicate directly over native WebSockets via Chrome DevTools Protocol to validate route navigation, legacy hash redirects, target switching, search shortcuts, and in-domain quick jump pills. +--- -* [v0.1](https://rawgit.com/ChromeDevTools/devtools-protocol/v0.1/index.html) original Eric Guzman app. -* [v0.2](https://rawgit.com/ChromeDevTools/devtools-protocol/v0.2/index.html) irish's "upgrades". -* [v0.8](https://rawgit.com/ChromeDevTools/devtools-protocol/v0.8/index.html) guzman's polymer 0.8 refactor -* [v1.0](https://rawgit.com/ChromeDevTools/devtools-protocol/v1.0/index.html) konrad's polymer 1.0 + jekyll refactor -* [v2.0](https://github.com/ChromeDevTools/debugger-protocol-viewer/tree/polymer) tim's polymer 2.0 - jekyll refactor -* [v3.0](https://chromedevtools.github.io/devtools-protocol/) tim's Eleventy refactor -* which brings us to… [now](https://chromedevtools.github.io/devtools-protocol/). +## Deployment +Deployments to [https://chromedevtools.github.io/devtools-protocol/](https://chromedevtools.github.io/devtools-protocol/) happen automatically via GitHub Actions in the [devtools-protocol repository](https://github.com/ChromeDevTools/devtools-protocol) on updates. -## License +The built distribution is pushed to the `devtools-protocol#gh-pages` branch. + +--- -Apache +## Contributing & Issues -## Contributing +- **Viewer Issues & Feature Requests**: Report in [this repository's issue tracker](https://github.com/ChromeDevTools/debugger-protocol-viewer/issues). +- **Protocol Bugs / Chromium Issues**: Report directly at [crbug.com/new](https://crbug.com/new). + +## License -Report issues about the website via GitHub issues in this repo. Please report -issues with CDP itself via https://crbug.com/new. Pull requests very welcome! +[Apache 2.0](./LICENSE) diff --git a/scripts/generate-stubs.js b/scripts/generate-stubs.js index 50a3a368a..7ea78f1c8 100644 --- a/scripts/generate-stubs.js +++ b/scripts/generate-stubs.js @@ -39,7 +39,7 @@ export function generateDomainStub(domain) { (function() { var hash = window.location.hash || ''; var member = ''; - var match = hash.match(/^#(?:method|type|event)-([\w-]+)/); + var match = hash.match(/^#(?:method|type|event)-([\\w-]+)/); if (match) member = '.' + match[1]; window.location.replace('../../#/${domainName}' + member); })(); diff --git a/src/index.html b/src/index.html index 0def4d4f0..9f4be7fe1 100644 --- a/src/index.html +++ b/src/index.html @@ -48,8 +48,8 @@ </div> <div class="header-center"> - <div id="search" class="search-box"> - <input type="search" id="search-input" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" placeholder="Search protocol..." aria-label="Search protocol" /> + <div class="search-box"> + <input type="search" id="search" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" placeholder="Search protocol..." aria-label="Search protocol" /> <kbd class="search-kbd">/</kbd> </div> </div> diff --git a/src/search.js b/src/search.js index eac654577..cdda5a7bd 100644 --- a/src/search.js +++ b/src/search.js @@ -7,7 +7,7 @@ class Search { * @param {!Element} resultsElement */ constructor(searchHeader, resultsElement) { - this._searchInput = searchHeader.querySelector('input'); + this._searchInput = searchHeader.tagName === 'INPUT' ? searchHeader : (searchHeader.querySelector('input') || searchHeader); /** @type {!Array<!Search.Item>} */ this._items = []; /** @type {!Set<string>} */ diff --git a/test/e2e.test.js b/test/e2e.test.js new file mode 100644 index 000000000..8547d5d02 --- /dev/null +++ b/test/e2e.test.js @@ -0,0 +1,463 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import http from 'node:http'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn, execSync } from 'node:child_process'; +import { generateStubs } from '../scripts/generate-stubs.js'; + +/** + * Finds the Chrome executable path across macOS and Linux environments. + * @returns {string|null} + */ +function findChromeBinary() { + if (process.env.CHROME_PATH && fs.existsSync(process.env.CHROME_PATH)) { + return process.env.CHROME_PATH; + } + + const macPath = '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/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; + if (fs.existsSync(macPath)) { + return macPath; + } + + try { + const whichOutput = execSync('which google-chrome || which chromium || which chrome', { + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'ignore'], + }).trim(); + if (whichOutput && fs.existsSync(whichOutput)) { + return whichOutput; + } + } catch { + // Binary not in PATH + } + + return null; +} + +/** + * Creates a native node:http static server serving rootDirectory. + * @param {string} rootDirectory + * @returns {http.Server} + */ +function createStaticServer(rootDirectory) { + const MIME_TYPES = { + '.html': 'text/html; charset=utf-8', + '.js': 'application/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.ico': 'image/x-icon', + '.woff2': 'font/woff2', + '.webmanifest': 'application/manifest+json', + '.xml': 'application/xml', + }; + + return http.createServer(async (req, res) => { + try { + const parsedUrl = new URL(req.url, 'http://127.0.0.1'); + let relativePath = decodeURIComponent(parsedUrl.pathname); + if (relativePath.endsWith('/')) { + relativePath += 'index.html'; + } + + let filePath = path.resolve(rootDirectory, '.' + relativePath); + if (!filePath.startsWith(rootDirectory)) { + res.writeHead(403, { 'Content-Type': 'text/plain' }); + res.end('Forbidden'); + return; + } + + try { + const stat = await fs.promises.stat(filePath); + if (stat.isDirectory()) { + filePath = path.join(filePath, 'index.html'); + } + } catch { + // Not a directory or does not exist + } + + try { + const data = await fs.promises.readFile(filePath); + const ext = path.extname(filePath).toLowerCase(); + const contentType = MIME_TYPES[ext] || 'application/octet-stream'; + res.writeHead(200, { + 'Content-Type': contentType, + 'Access-Control-Allow-Origin': '*', + }); + res.end(data); + } catch { + // Fallback to 404.html if available + const fallback404 = path.join(rootDirectory, '404.html'); + try { + const notFoundData = await fs.promises.readFile(fallback404); + res.writeHead(404, { + 'Content-Type': 'text/html; charset=utf-8', + 'Access-Control-Allow-Origin': '*', + }); + res.end(notFoundData); + } catch { + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('Not Found'); + } + } + } catch (err) { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end(`Internal Server Error: ${err.message}`); + } + }); +} + +/** + * Minimal Chrome DevTools Protocol client over native WebSocket. + */ +class CdpClient { + /** + * @param {WebSocket} ws + */ + constructor(ws) { + this._ws = ws; + this._id = 1; + this._pending = new Map(); + + this._ws.onmessage = (event) => { + try { + const msg = JSON.parse(event.data); + if (msg.id && this._pending.has(msg.id)) { + const { resolve, reject } = this._pending.get(msg.id); + this._pending.delete(msg.id); + if (msg.error) { + reject(new Error(msg.error.message || 'CDP command error')); + } else { + resolve(msg.result); + } + } + } catch (err) { + console.error('[CdpClient] Error handling message:', err); + } + }; + } + + /** + * Sends a CDP command. + * @param {string} method + * @param {Object} [params] + * @param {string} [sessionId] + * @returns {Promise<any>} + */ + send(method, params = {}, sessionId) { + return new Promise((resolve, reject) => { + const msgId = this._id++; + this._pending.set(msgId, { resolve, reject }); + const payload = { id: msgId, method, params }; + if (sessionId) payload.sessionId = sessionId; + this._ws.send(JSON.stringify(payload)); + }); + } + + /** + * Evaluates a JavaScript expression in the target page. + * @param {string} expression + * @param {string} sessionId + * @returns {Promise<any>} + */ + async evaluate(expression, sessionId) { + const res = await this.send('Runtime.evaluate', { + expression, + returnByValue: true, + awaitPromise: true, + }, sessionId); + + if (res.exceptionDetails) { + throw new Error( + `Evaluation exception: ${res.exceptionDetails.text || res.exceptionDetails.exception?.description}` + ); + } + return res.result?.value; + } + + /** + * Repeatedly evaluates an expression until predicate returns truthy or times out. + * @param {string} expression + * @param {(val: any) => boolean} predicate + * @param {string} sessionId + * @param {number} [timeoutMs] + * @param {number} [intervalMs] + * @returns {Promise<any>} + */ + async pollEvaluate(expression, predicate, sessionId, timeoutMs = 15000, intervalMs = 100) { + const start = Date.now(); + let lastVal; + while (Date.now() - start < timeoutMs) { + try { + lastVal = await this.evaluate(expression, sessionId); + if (predicate ? predicate(lastVal) : Boolean(lastVal)) { + return lastVal; + } + } catch { + // Ignored during page navigation / transitions + } + await new Promise((r) => setTimeout(r, intervalMs)); + } + throw new Error( + `pollEvaluate timed out after ${timeoutMs}ms for expression: ${expression}. Last value: ${JSON.stringify(lastVal)}` + ); + } +} + +test('Chrome DevTools Protocol Viewer E2E Tests', async (t) => { + const chromePath = findChromeBinary(); + if (!chromePath) { + t.skip('Chrome binary not found'); + return; + } + + const staticDir = path.resolve('devtools-protocol'); + generateStubs({ outputDir: staticDir }); + + // 1. Start native static HTTP server on ephemeral port + const server = createStaticServer(staticDir); + await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); + const { port } = server.address(); + const baseUrl = `http://127.0.0.1:${port}`; + + // 2. Launch headless Chrome + const tmpUserDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdp-viewer-e2e-')); + const chromeProcess = spawn( + chromePath, + [ + '--headless=new', + '--remote-debugging-port=0', + '--disable-gpu', + '--no-first-run', + `--user-data-dir=${tmpUserDataDir}`, + 'about:blank', + ], + { stdio: ['ignore', 'pipe', 'pipe'] } + ); + + let browserWs = null; + let cdp = null; + let sessionId = null; + + try { + // 3. Parse WebSocket URL from Chrome stderr + const wsUrl = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('Timed out waiting for Chrome WebSocket URL')), 10000); + let stderrBuffer = ''; + chromeProcess.stderr.on('data', (chunk) => { + stderrBuffer += chunk.toString(); + const match = stderrBuffer.match(/DevTools listening on (ws:\/\/[^\s]+)/); + if (match) { + clearTimeout(timeout); + resolve(match[1]); + } + }); + chromeProcess.on('error', (err) => { + clearTimeout(timeout); + reject(err); + }); + chromeProcess.on('exit', (code) => { + clearTimeout(timeout); + reject(new Error(`Chrome exited prematurely with code ${code}`)); + }); + }); + + browserWs = new WebSocket(wsUrl); + await new Promise((resolve, reject) => { + browserWs.onopen = resolve; + browserWs.onerror = reject; + }); + + cdp = new CdpClient(browserWs); + + // 4. Create and attach to target page + const { targetId } = await cdp.send('Target.createTarget', { url: 'about:blank' }); + const attachResult = await cdp.send('Target.attachToTarget', { targetId, flatten: true }); + sessionId = attachResult.sessionId; + + await cdp.send('Page.enable', {}, sessionId); + await cdp.send('Runtime.enable', {}, sessionId); + + // Dynamic native subtests + await t.test('1. Direct route navigation (#/Page.navigate)', async () => { + await cdp.send('Page.navigate', { url: `${baseUrl}/#/Page.navigate` }, sessionId); + + const title = await cdp.pollEvaluate( + 'document.title', + (val) => typeof val === 'string' && val.includes('Page.navigate'), + sessionId + ); + const hasElement = await cdp.pollEvaluate( + 'Boolean(document.getElementById("Page_navigate"))', + (val) => val === true, + sessionId + ); + + assert.ok(title.includes('Page.navigate'), `Expected title to contain "Page.navigate", got "${title}"`); + assert.strictEqual(hasElement, true, 'Expected #Page_navigate element to exist in DOM'); + }); + + await t.test('2. Legacy URL redirection (/tot/Page/#method-navigate)', async () => { + await cdp.send('Page.navigate', { url: `${baseUrl}/tot/Page/#method-navigate` }, sessionId); + + const hash = await cdp.pollEvaluate( + 'window.location.hash', + (val) => val === '#/Page.navigate', + sessionId + ); + + assert.strictEqual(hash, '#/Page.navigate', `Expected hash to be "#/Page.navigate", got "${hash}"`); + }); + + await t.test('3. Target selector routing (#/v8/Runtime.evaluate)', async () => { + await cdp.send('Page.navigate', { url: `${baseUrl}/#/v8/Runtime.evaluate` }, sessionId); + + const targetValue = await cdp.pollEvaluate( + 'document.getElementById("target-selector")?.value', + (val) => val === 'v8', + sessionId + ); + const hasRuntimeEvaluate = await cdp.pollEvaluate( + 'Boolean(document.getElementById("Runtime_evaluate"))', + (val) => val === true, + sessionId + ); + + assert.strictEqual(targetValue, 'v8', `Expected target selector value to be "v8", got "${targetValue}"`); + assert.strictEqual(hasRuntimeEvaluate, true, 'Expected #Runtime_evaluate element to exist in DOM'); + }); + + await t.test('4. Search keyboard shortcut (/)', async () => { + await cdp.send('Page.navigate', { url: `${baseUrl}/#/Page` }, sessionId); + + // Wait for app to be ready and unfocus any inputs + await cdp.pollEvaluate('Boolean(window.app)', (v) => v === true, sessionId); + await cdp.evaluate('document.getElementById("search")?.blur()', sessionId); + + // Dispatch '/' keypress via CDP Input + await cdp.send('Input.dispatchKeyEvent', { + type: 'keyDown', + key: '/', + text: '/', + unmodifiedText: '/', + code: 'Slash', + windowsVirtualKeyCode: 191, + }, sessionId); + await cdp.send('Input.dispatchKeyEvent', { + type: 'keyUp', + key: '/', + code: 'Slash', + windowsVirtualKeyCode: 191, + }, sessionId); + + const isFocused = await cdp.pollEvaluate( + 'document.activeElement === document.getElementById("search")', + (val) => val === true, + sessionId + ); + + assert.strictEqual(isFocused, true, 'Expected document.activeElement to be document.getElementById("search")'); + }); + + await t.test('5. In-domain quick jump pills (#/Page)', async () => { + await cdp.send('Page.navigate', { url: `${baseUrl}/#/Page` }, sessionId); + + const pillTexts = await cdp.pollEvaluate( + 'Array.from(document.querySelectorAll(".section-jump-pill")).map(el => el.textContent.trim())', + (arr) => Array.isArray(arr) && arr.length >= 3, + sessionId + ); + + assert.ok( + pillTexts.some((text) => text.includes('Methods')), + `Expected pills to include "Methods", got: ${JSON.stringify(pillTexts)}` + ); + assert.ok( + pillTexts.some((text) => text.includes('Events')), + `Expected pills to include "Events", got: ${JSON.stringify(pillTexts)}` + ); + assert.ok( + pillTexts.some((text) => text.includes('Types')), + `Expected pills to include "Types", got: ${JSON.stringify(pillTexts)}` + ); + }); + + await t.test('6. Type cross-references (#/DOM.NodeId)', async () => { + await cdp.send('Page.navigate', { url: `${baseUrl}/#/DOM.NodeId` }, sessionId); + + const hasReferences = await cdp.pollEvaluate( + 'document.querySelectorAll(".references-list li").length > 0', + (val) => val === true, + sessionId + ); + + assert.strictEqual(hasReferences, true, 'Expected DOM.NodeId to render back-reference links in .references-list'); + }); + + await t.test('7. Mobile responsive drawer (#drawer-toggle & backdrop)', async () => { + await cdp.send('Page.navigate', { url: `${baseUrl}/#/Page` }, sessionId); + await cdp.pollEvaluate('Boolean(window.app)', (v) => v === true, sessionId); + + // Open drawer + await cdp.evaluate('document.getElementById("drawer-toggle").click()', sessionId); + const isDrawerOpen = await cdp.pollEvaluate( + 'document.body.classList.contains("drawer-open")', + (val) => val === true, + sessionId + ); + assert.strictEqual(isDrawerOpen, true, 'Expected body to have "drawer-open" class after clicking drawer-toggle'); + + // Close drawer via backdrop click + await cdp.evaluate('document.getElementById("drawer-backdrop").click()', sessionId); + const isDrawerClosed = await cdp.pollEvaluate( + '!document.body.classList.contains("drawer-open")', + (val) => val === true, + sessionId + ); + assert.strictEqual(isDrawerClosed, true, 'Expected body not to have "drawer-open" class after clicking backdrop'); + }); + + await t.test('8. Wildcard 404 redirection (/1-3/Page/#method-navigate)', async () => { + await cdp.send('Page.navigate', { url: `${baseUrl}/1-3/Page/#method-navigate` }, sessionId); + + const hash = await cdp.pollEvaluate( + 'window.location.hash', + (val) => val === '#/stable/Page.navigate', + sessionId + ); + assert.strictEqual(hash, '#/stable/Page.navigate', `Expected 404 handler to redirect to "#/stable/Page.navigate", got "${hash}"`); + }); + } finally { + // Teardown resources + if (browserWs) { + try { + browserWs.close(); + } catch { + // ignore + } + } + + if (chromeProcess) { + try { + chromeProcess.kill('SIGTERM'); + } catch { + // ignore + } + await new Promise((resolve) => { + chromeProcess.on('exit', resolve); + setTimeout(resolve, 3000); + }); + } + + try { + fs.rmSync(tmpUserDataDir, { recursive: true, force: true }); + } catch { + // ignore + } + + await new Promise((resolve) => server.close(resolve)); + } +}); From f511bb674a493c6f890e5f35081395a344ef2d49 Mon Sep 17 00:00:00 2001 From: Paul Irish <commits@paul.irish> Date: Tue, 8 Sep 2026 10:34:58 -0700 Subject: [PATCH 08/51] refactor: migrate protocol data to data/ and prune obsolete legacy files --- .eleventy.js | 19 - {pages/_data => data}/1-2.json | 0 {pages/_data => data}/1-3.json | 0 {pages/_data => data}/tot.json | 0 {pages/_data => data}/v8.json | 0 {pages/_data => data}/versions.json | 0 generate-sidenav-html.cjs | 40 -- pages/1-2.11ty.js | 7 - pages/1-2.md | 7 - pages/1-3.11ty.js | 7 - pages/1-3.md | 7 - pages/404.md | 7 - pages/_includes/shell.hbs | 148 -------- pages/domainGenerator.js | 268 ------------- pages/index.md | 205 ---------- pages/scripts/clipboard.js | 59 --- pages/scripts/cr-markdownish.js | 136 ------- pages/scripts/index.js | 365 ------------------ pages/service-worker.js | 16 - pages/styles/protocol.css | 416 --------------------- pages/tot.11ty.js | 7 - pages/tot.md | 19 - pages/v8.11ty.js | 7 - pages/v8.md | 11 - rollup.config.js | 11 - service-worker.js | 1 - {pages => src}/images/cdp-editor.png | Bin {pages => src}/images/logo.png | Bin {pages => src}/images/protocol-monitor.png | Bin test/primitive_tests.sh | 14 - 30 files changed, 1777 deletions(-) delete mode 100644 .eleventy.js rename {pages/_data => data}/1-2.json (100%) rename {pages/_data => data}/1-3.json (100%) rename {pages/_data => data}/tot.json (100%) rename {pages/_data => data}/v8.json (100%) rename {pages/_data => data}/versions.json (100%) delete mode 100644 generate-sidenav-html.cjs delete mode 100644 pages/1-2.11ty.js delete mode 100644 pages/1-2.md delete mode 100644 pages/1-3.11ty.js delete mode 100644 pages/1-3.md delete mode 100644 pages/404.md delete mode 100644 pages/_includes/shell.hbs delete mode 100644 pages/domainGenerator.js delete mode 100644 pages/index.md delete mode 100644 pages/scripts/clipboard.js delete mode 100644 pages/scripts/cr-markdownish.js delete mode 100644 pages/scripts/index.js delete mode 100644 pages/service-worker.js delete mode 100644 pages/styles/protocol.css delete mode 100644 pages/tot.11ty.js delete mode 100644 pages/tot.md delete mode 100644 pages/v8.11ty.js delete mode 100644 pages/v8.md delete mode 100644 rollup.config.js delete mode 100644 service-worker.js rename {pages => src}/images/cdp-editor.png (100%) rename {pages => src}/images/logo.png (100%) rename {pages => src}/images/protocol-monitor.png (100%) delete mode 100755 test/primitive_tests.sh diff --git a/.eleventy.js b/.eleventy.js deleted file mode 100644 index d602de145..000000000 --- a/.eleventy.js +++ /dev/null @@ -1,19 +0,0 @@ -import handlebarsPlugin from '@11ty/eleventy-plugin-handlebars'; - -export default (eleventyConfig) => { - eleventyConfig.addPlugin(handlebarsPlugin); - eleventyConfig.addPassthroughCopy('pages/styles/protocol.css'); - eleventyConfig.addPassthroughCopy('pages/images/'); - eleventyConfig.addPassthroughCopy('search_index/'); - eleventyConfig.addPassthroughCopy('.nojekyll'); - eleventyConfig.addPassthroughCopy('pages/service-worker.js'); - - return { - pathPrefix: '/devtools-protocol/', - dir: { - input: 'pages', - output: 'devtools-protocol', - data: '_data', - }, - }; -}; diff --git a/pages/_data/1-2.json b/data/1-2.json similarity index 100% rename from pages/_data/1-2.json rename to data/1-2.json diff --git a/pages/_data/1-3.json b/data/1-3.json similarity index 100% rename from pages/_data/1-3.json rename to data/1-3.json diff --git a/pages/_data/tot.json b/data/tot.json similarity index 100% rename from pages/_data/tot.json rename to data/tot.json diff --git a/pages/_data/v8.json b/data/v8.json similarity index 100% rename from pages/_data/v8.json rename to data/v8.json diff --git a/pages/_data/versions.json b/data/versions.json similarity index 100% rename from pages/_data/versions.json rename to data/versions.json diff --git a/generate-sidenav-html.cjs b/generate-sidenav-html.cjs deleted file mode 100644 index 125bd959b..000000000 --- a/generate-sidenav-html.cjs +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - - -const verSlugs = require('./pages/_data/versions.json').map(e => e.slug); - -const allDomains = {}; - -for (const slug of verSlugs){ - const protocol = require(`./pages/_data/${slug}.json`); - const domains = protocol.domains; - domains.forEach(domain => { - const id = domain.domain; - const tags = allDomains[id] || []; - tags.push(slug); - - // Annotate with tot's experimental / deprecated status - if (slug === 'tot') { - if (domain.experimental) tags.push('experimental'); - if (domain.deprecated) tags.push('deprecated'); - } - allDomains[id] = tags; - }); -} - -const str = Object.entries(allDomains).sort(([domainA, tagsA], [domainB, tagsB]) => { - // Disable grouping by tag https://github.com/ChromeDevTools/debugger-protocol-viewer/pull/179 - // const isExpOrDepr = a => a === 'experimental' || a === 'deprecated'; - // const getTagsStr = tags => tags.filter(isExpOrDepr).join(''); - // const tagSortResult = getTagsStr(tagsA).localeCompare(getTagsStr(tagsB)); - // if (tagSortResult !== 0) return tagSortResult; - return domainA.localeCompare(domainB); - }) - .map(([id, versions]) => ` <a href="{{{ url '/' }}}{{{ version }}}/${id}" class="${versions.join(' ')}">${id}</a>`) - .join('\n'); - - -process.stdout.write('\n' + str + '\n'); - -// And update shell.hbs with the output - diff --git a/pages/1-2.11ty.js b/pages/1-2.11ty.js deleted file mode 100644 index 670a48758..000000000 --- a/pages/1-2.11ty.js +++ /dev/null @@ -1,7 +0,0 @@ -import {DomainGenerator} from './domainGenerator.js'; - -export default class extends DomainGenerator { - constructor() { - super('1-2'); - } -} diff --git a/pages/1-2.md b/pages/1-2.md deleted file mode 100644 index 6793fcf17..000000000 --- a/pages/1-2.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -layout: shell.hbs -title: Chrome DevTools Protocol - stable (1.2) -version: 1-2 ---- -The **1.2 version of the protocol** is the latest stable release of the protocol, tagged at Chrome 54. -It includes a smaller subset of the complete protocol compatibilities. diff --git a/pages/1-3.11ty.js b/pages/1-3.11ty.js deleted file mode 100644 index e43421af9..000000000 --- a/pages/1-3.11ty.js +++ /dev/null @@ -1,7 +0,0 @@ -import {DomainGenerator} from './domainGenerator.js'; - -export default class extends DomainGenerator { - constructor() { - super('1-3'); - } -} diff --git a/pages/1-3.md b/pages/1-3.md deleted file mode 100644 index de95fa3af..000000000 --- a/pages/1-3.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -layout: shell.hbs -title: Chrome DevTools Protocol - stable (1.3) -version: 1-3 ---- -The **1.3 version of the protocol** is the "living" stable channel of the protocol. -It's a subset of the complete protocol, [excluding](https://github.com/ChromeDevTools/debugger-protocol-viewer/blob/d84a0457b8d5b6c4a7a2947572b83239d8fb5954/make-stable-protocol.js) items that are experimental or deprecated. diff --git a/pages/404.md b/pages/404.md deleted file mode 100644 index 47c826d82..000000000 --- a/pages/404.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -layout: shell.hbs -title: Chrome DevTools Protocol - 404 -version: tot -permalink: 404.html ---- -Oops you hit a 404! diff --git a/pages/_includes/shell.hbs b/pages/_includes/shell.hbs deleted file mode 100644 index a357c82d6..000000000 --- a/pages/_includes/shell.hbs +++ /dev/null @@ -1,148 +0,0 @@ -<!DOCTYPE html> -<html lang="en"> -<head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - {{#if domain}} - <meta name="description" content="Chrome DevTools Protocol - version {{{version}}} - {{{domain.domain}}} domain" /> - {{else}} - <meta name="description" content="Chrome DevTools Protocol - version {{{version}}}" /> - {{/if}} - <link rel="icon" type="image/png" href="{{{ url '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/images/logo.png'}}}" /> - {{#if domain}} - <title>{{{title}}} - {{{domain.domain}}} domain - {{else}} - {{{title}}} - {{/if}} - - - - - - - - {{#if domain}} - - {{else}} - - {{/if}} - - - - - -

    - -
    -
    -

    Chrome DevTools Protocol

    - - -
    -
    -
    - {{{content}}} -
    -
    -
    - - - diff --git a/pages/domainGenerator.js b/pages/domainGenerator.js deleted file mode 100644 index 9a7dc9490..000000000 --- a/pages/domainGenerator.js +++ /dev/null @@ -1,268 +0,0 @@ -import { marked } from 'marked'; - -const html = String.raw; - -const computeHash = (typeName, name, id) => { - const typeReference = typeName.toLowerCase().substr(0, typeName.length - 1); - return `${typeReference}-${name || id}`; -} - -// Thx aslushnikov! -const itemSort = (a, b) => { - if (a.experimental !== b.experimental) return a.experimental ? 1 : -1; - if (a.deprecated !== b.deprecated) return a.deprecated ? 1 : -1; - if (a.optional !== b.optional) return a.optional ? 1 : -1; - return (a.name || a.id).localeCompare(b.name || b.id); -}; - -// This handles a few weird cases of raw HTML or over-escaping in the protocol JSON -function parseSafeMarkdown(mdText) { - // Inline codeblocks to doublecheck: IO.StreamHandle, - // Preload.PreloadingAttemptSource, Preload.RuleSet > backendNodeId, - // Accessibility.AXValueSource > nativeSource. - // Replace LINT comments with an empty string. - mdText = mdText.replaceAll('<', '<').replaceAll('>', '>').replaceAll(/^LINT\..*$\n/gm, ''); - const html = marked(mdText, {escape: true}); - return html.replaceAll('&lt;', '<').replaceAll('&gt;', '>'); -} - -export class DomainGenerator { - constructor(version) { - this.version = version; - } - - data() { - const version = this.version; - const versionPart = version === 'tot' ? '' : ` - version ${version}`; - const title = `Chrome DevTools Protocol${versionPart}` - return { - layout: 'shell.hbs', - title, - version, - shadow: 'domain', - pagination: { - data: `${version}.domains`, - size: 1, - alias: 'domain', - }, - permalink(data) { - return `${version}/${data.domain.domain}/index.html`; - }, - } - } - render(data) { - return this.domainTemplate(data.pagination.items[0]); - } - - descriptionTemplate(description, item) { - // Some descriptions use markdown. eg. Page.printToPDF - // Some params have an emum: e.g. Debugger.continueToLocation - return html` -
    - ${description ? parseSafeMarkdown(description) : ''} - ${item ? this.enumDetails(item) : ''} -
    - `; - } - - statusTemplate(experimental, deprecated) { - let output = ''; - if (experimental) { - output += html`Experimental` - } - if (deprecated) { - output += html`Deprecated` - } - return output; - } - - nameIncludingDomainTemplate(domain, name) { - return html`${domain}.${name}`; - } - - domainTocTemplate(typeName, domain, items) { - if (!items) { - return ''; - } - - let output = html`

    ${typeName}

    `; - for (const {experimental, deprecated, name, id} of items) { - output += html` - - `; - } - return output; - } - - computeReferral(item) { - if (item.type) { - return (item.items && item.items['$ref']) || ''; - } - return item['$ref']; - } - - computeReferralUrl(domain, referral) { - // Referral points to a different domain - if (referral.indexOf('.') !== -1) { - return this.url(`/${this.version}/${referral.replace('.', '/#type-')}`); - } - return this.url(`/${this.version}/${domain}/#type-${referral}`); - } - - propertiesType(domain, item) { - const {type} = item; - const referral = this.computeReferral(item); - - if (type) { - const arrayDetails = _ => { - if (type !== 'array') return ''; - return html`[ ${this.propertiesType(domain, item.items)} ]`; - } - return html`${type}${arrayDetails()}`; - } - if (referral) { - return html`${referral}` - } - return ''; - } - - propertyTemplate(domain, items) { - let properties = ''; - - for (const item of items) { - const {name, description, experimental, deprecated, optional} = item; - properties += html` -
    - ${name} -
    -
    - ${this.propertiesType(domain, item)} - ${this.descriptionTemplate(description, item)} - ${this.statusTemplate(experimental, deprecated)} -
    - `; - } - - return properties; - } - - propertiesDetailsTemplate(domain, details) { - let name = ''; - let items = undefined; - if (details.parameters) { - name = 'parameters'; - items = details.parameters; - } - if (details.properties) { - name = 'properties'; - items = details.properties; - } - if (!name) { - return ''; - } - - return html` -
    ${name}
    -
    - ${this.propertyTemplate(domain, items)} -
    - `; - } - - returnDetailsTemplate(domain, details) { - const {returns, name} = details; - if (!returns || !returns.length) { - return ''; - } - - return html` -
    Return Object
    -
    - ${this.propertyTemplate(domain, returns)} -
    - `; - } - - enumDetails(details) { - const {enum: enumValues} = details; - if (!enumValues || !enumValues.length) { - return ''; - } - - const enumItems = enumValues.map(e => html`${e}`); - return html` -
    Allowed Values: ${enumItems.join(', ')}
    - `; - } - - detailsTemplate(typeName, domain, details) { - const {name, description, id, type, experimental, deprecated} = details; - const computedId = computeHash(typeName, name, id); - const actualName = name || id; - - return html` -
    -

    - ${this.nameIncludingDomainTemplate(domain, actualName)} - ${this.statusTemplate(experimental, deprecated)} - -

    - ${this.descriptionTemplate(description, details)} - ${type - ? html`

    Type: ${type}

    ` - : '' - } - ${this.propertiesDetailsTemplate(domain, details)} - ${this.returnDetailsTemplate(domain, details)} -
    - `; - } - - detailsSection(name, domain, items) { - if (!items || items.length === 0) { - return ''; - } - - let details = ''; - for (const item of items) { - details += this.detailsTemplate(name, domain, item); - } - - return html` -

    ${name}

    -
    - ${details} -
    - `; - } - - domainTemplate({domain, description, experimental, deprecated, commands, events, types}) { - - commands && commands.sort(itemSort); - events && events.sort(itemSort); - types && types.sort(itemSort); - - return html` -
    - - - ${this.detailsSection('Methods', domain, commands)} - ${this.detailsSection('Events', domain, events)} - ${this.detailsSection('Types', domain, types)} -
    - `; - } -} diff --git a/pages/index.md b/pages/index.md deleted file mode 100644 index a4cdd783c..000000000 --- a/pages/index.md +++ /dev/null @@ -1,205 +0,0 @@ ---- -layout: shell.hbs -title: Chrome DevTools Protocol -version: tot ---- - -

    The Chrome DevTools Protocol allows for tools to instrument, inspect, debug and profile Chromium, Chrome and other Blink-based browsers. -Many existing projects currently use the protocol. -The Chrome DevTools uses this protocol and the team maintains its API. - -

    Instrumentation is divided into a number of domains (DOM, Debugger, Network -etc.). Each domain defines a number of commands it supports and events it -generates. Both commands and events are serialized JSON objects of a fixed -structure. - -

    Protocol API Docs

    - -

    The latest (tip-of-tree) protocol (tot) — -It changes frequently -and can break at any time. However it captures the full capabilities of the Protocol, whereas the stable release is a subset. -There is no backwards compatibility support guaranteed. - -

    v8-inspector protocol (v8) — -Enables -debugging & profiling -of Node.js apps. - -

    stable 1.3 protocol (1-3) — -The stable release of the protocol, tagged at Chrome 64. It includes a smaller subset of the complete protocol compatibilities. - -

    Resources

    - -

    See Getting Started with CDP. The awesome-chrome-devtools page links to many of the tools in the protocol ecosystem, including protocol API libraries in JavaScript, TypeScript, Python, Java, and Go. - -

    Consider subscribing to the chrome-debugging-protocol mailing list. - -

    Using Protocol Monitor in Chrome DevTools

    -

    This is especially handy to understand how the DevTools frontend makes use of the protocol. -You can view all requests/responses and methods as they happen in the Protocol Monitor -panel in DevTools. - -

    - - Screenshot of the Protocol Monitor - -
    - -Click the gear icon in the top-right of the DevTools to open the Settings panel. -Select Experiments on the left of settings. Turn on "Protocol Monitor", then close and reopen DevTools. -Now click the ⋮ menu icon, choose More Tools and then select Protocol monitor. - -

    You can also send commands using Protocol Monitor. If the command does not require any parameters, -type the command into the prompt at the bottom of the Protocol Monitor panel and press Enter, for example, -Page.captureScreenshot. If the command requires parameters, provide them as JSON, for example, -{"cmd":"Page.captureScreenshot","args":{"format": "jpeg"}}. - -

    By clicking on the icon next to the command input (in Chrome 117+), you can open the command editor. After you select a CDP command, the editor creates a structured form based on the protocol definitions that allows you to edit parameters, and view their documentation and types. Send the commands by clicking on the send button or using Ctrl + Enter. Use the context menu in the list of previously sent commands to open one of them in the editor. - -

    - - Screenshot of CDP Editor - -
    - - -

    Alternatively, you can execute commands from the DevTools console. First, open devtools-on-devtools, -then within the inner DevTools window, use Main.MainImpl.sendOverProtocol() in the console: - -

    let Main = await import('./devtools-frontend/front_end/entrypoints/main/main.js'); // or './entrypoints/main/main.js' or './main/main.js' depending on the browser version
    -await Main.MainImpl.sendOverProtocol('Emulation.setDeviceMetricsOverride', {
    -  mobile: true,
    -  width: 412,
    -  height: 732,
    -  deviceScaleFactor: 2.625,
    -});
    -
    -const data = await Main.MainImpl.sendOverProtocol("Page.captureScreenshot");
    - - -

    DevTools protocol via Chrome extension

    -

    To allow chrome extensions to interact with the protocol, we introduced -chrome.debugger -extension API that exposes this JSON message -transport interface. As a result, you can not only attach to the remotely -running Chrome instance, but also instrument it from its own extension. - -

    Chrome Debugger Extension API provides a higher level API where command -domain, name and body are provided explicitly in the sendCommand -call. This API hides request ids and handles binding of the request with its -response, hence allowing sendCommand to report result in the -callback function call. One can also use this API in combination with the other -Extension APIs. - -

    If you are developing a Web-based IDE, you should implement an extension that -exposes debugging capabilities to your page and your IDE will be able to open -pages with the target application, set breakpoints there, evaluate expressions -in console, live edit JavaScript and CSS, display live DOM, network interaction -and any other aspect that Developer Tools is instrumenting today. - -

    Opening embedded Developer Tools will terminate the -remote connection and thus detach the extension. - -

    Frequently Asked Questions

    - -

    How is the protocol defined?

    -

    The canonical protocol definitions live in the Chromium source tree: -(browser_protocol.pdl -and js_protocol.pdl). -They are maintained manually by the DevTools engineering team. The declarative protocol definitions are used across tools; -for instance, a binding layer is created within Chromium for the Chrome DevTools to interact with, -and separately bindings generated for -Chrome Headless’s C++ interface. - -

    Can I get the protocol as JSON?

    - -

    These canonical .pdl files are mirrored on GitHub in the devtools-protocol repo -where JSON versions, TypeScript definitions and closure typedefs are generated. It's published regularly to NPM. - -

    Also, if you've set --remote-debugging-port=9222 with Chrome, the complete protocol version it speaks -is available at localhost:9222/json/protocol. - -

    How do I access the browser target?

    -

    The endpoint is exposed as webSocketDebuggerUrl in /json/version. -Note the browser in the URL, rather than page. -If Chrome was launched with --remote-debugging-port=0 and chose an open port, -the browser endpoint is written to both stderr and the DevToolsActivePort file in browser profile folder. - -

    Does the protocol support multiple simultaneous clients?

    -

    Chrome 63 introduced support for multiple clients. See -this article for details. - -

    Upon disconnection, the outgoing client will receive a detached event. -For example: {"method":"Inspector.detached","params":{"reason":"replaced_with_devtools"}}. -View the enum of -possible reasons. -(For reference: the original patch). -After disconnection, some apps have chosen to pause their state and offer a reconnect button. - -

    HTTP Endpoints

    -

    If started with a remote-debugging-port, these HTTP endpoints are available on the same port. -(Chromium implementation) - -

    GET /json/version

    -

    Browser version metadata

    -
    -{
    -    "Browser": "Chrome/72.0.3601.0",
    -    "Protocol-Version": "1.3",
    -    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3601.0 Safari/537.36",
    -    "V8-Version": "7.2.233",
    -    "WebKit-Version": "537.36 (@cfede9db1d154de0468cb0538479f34c0755a0f4)",
    -    "webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser/b0b8a4fb-bb17-4359-9533-a8d9f3908bd8"
    -}
    - -

    GET /json or /json/list

    -

    A list of all available websocket targets. -

    -[ {
    -  "description": "",
    -  "devtoolsFrontendUrl": "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/devtools/inspector.html?ws=localhost:9222/devtools/page/DAB7FB6187B554E10B0BD18821265734",
    -  "id": "DAB7FB6187B554E10B0BD18821265734",
    -  "title": "Yahoo",
    -  "type": "page",
    -  "url": "https://www.yahoo.com/",
    -  "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/DAB7FB6187B554E10B0BD18821265734"
    -} ]
    - -

    GET /json/protocol/

    -

    The current devtools protocol, as JSON: -

    -{
    -  "domains": [
    -      {
    -          "domain": "Accessibility",
    -          "experimental": true,
    -          "dependencies": [
    -              "DOM"
    -          ],
    -          "types": [
    -              {
    -                  "id": "AXValueType",
    -                  "description": "Enum of possible property types.",
    -                  "type": "string",
    -                  "enum": [
    -                      "boolean",
    -                      "tristate",
    -// ...
    - -

    PUT /json/new?{url}

    -

    Opens a new tab. Responds with the websocket target data for the new tab. - -

    GET /json/activate/{targetId}

    -

    Brings a page into the foreground (activate a tab). -

    For valid targets, the response is 200: "Target activated". -If the target is invalid, the response is 404: "No such target id: {targetId}" -

    GET /json/close/{targetId}

    -

    Closes the target page identified by targetId. -

    For valid targets, the response is 200: "Target is closing". -If the target is invalid, the response is 404: "No such target id: {targetId}" - -

    WebSocket /devtools/page/{targetId}

    -

    The WebSocket endpoint for the protocol. - -

    GET /devtools/inspector.html

    -

    A copy of the DevTools frontend that ship with Chrome. diff --git a/pages/scripts/clipboard.js b/pages/scripts/clipboard.js deleted file mode 100644 index 9ddcf61bb..000000000 --- a/pages/scripts/clipboard.js +++ /dev/null @@ -1,59 +0,0 @@ - -// Single-clicking on the permalink hash copies the URL to the clipboard -// Double-clicking on the permalink hash will copy markdown -// [`Domain.method`](https://...) -for (const permalinkEl of document.querySelectorAll('.permalink')) { - const href = permalinkEl.href; - const textSlug = permalinkEl.dataset.slug; - const markdown = `[\`${textSlug}\`](${href})`; - const htmlStr = `${textSlug}`; - - permalinkEl.addEventListener('click', handleClicks); - permalinkEl.addEventListener('dblclick', handleClicks); - - function handleClicks(e) { - // No need to scroll - e.preventDefault(); - // Add hash back to url, but without pushState cuz it usually creates more problems - window.location.href = href; - - const textBlob = new Blob([e.type === 'dblclick' ? markdown : href], { type: 'text/plain' }); - const htmlBlob = new Blob([htmlStr], { type: 'text/html' }); - // text/markdown not supported. (for several reasons) - // …one being https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/clipboard/clipboard_writer.cc;l=307-318;drc=717a5ba32f0aba300c860d5bff7c87bbcff44afc - // const mdBlob = new Blob([markdown], { type: "text/markdown" }); - const cpItem = new ClipboardItem({ - [textBlob.type]: textBlob, - [htmlBlob.type]: htmlBlob, - }); - - navigator.clipboard.write([cpItem]) - .then(_ => { - const classNames = ['copied']; - if (e.type === 'dblclick') { - classNames.push('copied__md'); - } - // Show psuedo-element. rAF used to trigger animation reliably - permalinkEl.className = 'permalink'; - requestAnimationFrame(_ => { - requestAnimationFrame(_ => { - permalinkEl.classList.add(...classNames); - }); - }); - }) - // This can happen if the user denies clipboard permissions - .catch(err => console.error('Could not copy to clipboard: ', err)); - } -}; - -// Handle back-button navigations through hashes. (yes it does seem weird that this need to be done manually.......) -window.addEventListener("popstate", scrollToCurrentHash); -document.addEventListener("DOMContentLoaded", scrollToCurrentHash); -function scrollToCurrentHash(e) { - const u = new URL(location.href); - const hash = u.hash.slice(1); - if (!hash) return; - - const elem = document.querySelector(`#${hash}`); - elem.scrollIntoView({block: 'start'}); -} diff --git a/pages/scripts/cr-markdownish.js b/pages/scripts/cr-markdownish.js deleted file mode 100644 index bd36a10ed..000000000 --- a/pages/scripts/cr-markdownish.js +++ /dev/null @@ -1,136 +0,0 @@ -class CRMarkdownish extends HTMLElement { - connectedCallback() { - this.render(); - } - - static get observedAttributes() { - return ['markdown']; - } - - attributeChangedCallback(name, oldValue, newValue) { - this.render(); - } - - render() { - const markdown = this.getAttribute('markdown'); - - if (!markdown) { - this.innerHTML = ''; - return; - } - - // skip the work if we don't have markdown in it. - const hasMarkdown = /(\n-)|`/.test(markdown); - if (!hasMarkdown) { - this.innerHTML = markdown; - return; - } - - const text = this._escapeHtml(markdown); - let html = this._convertMarkdownLists(text); - html = this._convertMarkdownCodeBlocks(html); - this.innerHTML = html; - } - - /** - * @param {string} text - * @return {!string} - */ - _convertMarkdownLists(text) { - let inList = false; - let html = []; - - for (const line of text.split(/\n/)) { - if (inList && line === '') { - inList = false; - html.push(''); - continue; - } - if (line.startsWith('-')) { - if (!inList) html.push('

      '); - inList = true; - html.push('
    • ' + line.replace(/^- /, '')); - continue; - } - html.push(line); - } - if (inList) html.push('
    '); - - return html.join('\n'); - } - - /** - * @param {!string} text - * @return {!Element} - */ - _convertMarkdownCodeBlocks(text) { - const html = []; - const parts = text.split(/`(.*?)`/g); // Split on markdown code slashes - while (parts.length) { - // Pop off the same number of elements as there are capture groups. - const [preambleText, codeText] = parts.splice(0, 2); - html.push(preambleText); - if (codeText) { - html.push(`${codeText}`); - } - } - return html.join(''); - } - - /** - * Escape special characters in the given string of html. - * - * @param {string} string The string to escape for inserting into HTML - * @return {string} - * @see https://github.com/component/escape-html/ - */ - _escapeHtml(string) { - const matchHtmlRegExp = /["'&<>]/; - - var str = '' + string; - var match = matchHtmlRegExp.exec(str); - - if (!match) { - return str; - } - - var escape; - var html = ''; - var index = 0; - var lastIndex = 0; - - for (index = match.index; index < str.length; index++) { - switch (str.charCodeAt(index)) { - case 34: // " - escape = '"'; - break; - case 38: // & - escape = '&'; - break; - case 39: // ' - escape = '''; - break; - case 60: // < - escape = '<'; - break; - case 62: // > - escape = '>'; - break; - default: - continue; - } - - if (lastIndex !== index) { - html += str.substring(lastIndex, index); - } - - lastIndex = index + 1; - html += escape; - } - - return lastIndex !== index ? - html + str.substring(lastIndex, index) : - html; - } -}; -customElements.define('cr-markdownish', CRMarkdownish); diff --git a/pages/scripts/index.js b/pages/scripts/index.js deleted file mode 100644 index 5a8b4c1ed..000000000 --- a/pages/scripts/index.js +++ /dev/null @@ -1,365 +0,0 @@ -import {html, render} from 'lit-html'; -import './cr-markdownish.js'; -import './clipboard.js' - -/** - * A model managing keywords searches. - * TODO: remembers previous searches (localstorage?). - */ -class KeywordsModel { - constructor(index) { - this.index = index; - this.keys = Object.keys(index); - } - getMatches(searchString) { - if (!searchString) { - return []; - } - let str = searchString.toLowerCase(); - let useCache = false; - if (this.prevKey_) { - const occurredAt = str.indexOf(this.prevKey_); - // An occurance at 0 means the previous key is contained in the new - // search string. - // We don't handle the case where the search is exactly the same; - // we assume it cannot be the same because we're handling a 'change' - // event. This logic should be updated if ever not handled by a 'change' - // event. - if (occurredAt === 0) { - // Use the cached list of matching keys. - useCache = true; - } - } - let matches; - if (useCache) { - matches = this.prevMatches_.filter((key) => { - return key.indexOf(str) !== -1; - }); - } else { - const exactMatches = []; - const wildcardMatches = []; - this.keys.forEach((key) => { - let matchIndex = key.indexOf(str); - if (matchIndex === 0) { - exactMatches.push(key); - } else if (matchIndex !== -1) { - wildcardMatches.push(key); - } - }); - matches = exactMatches.concat(wildcardMatches); - } - - this.prevKey_ = str; - this.prevMatches_ = matches; - - return matches.map((key) => { - return this.index[key]; - }); - } -} - -const TYPE_ENUM = { - DOMAIN: '0', - EVENT: '1', - PARAM: '2', - TYPE: '3', - METHOD: '4' -}; - -const TYPE_LABEL_ENUM = { - '0': 'Domain', - '1': 'Event', - '2': 'Parameter', - '3': 'Type', - '4': 'Method' -}; - -const TYPE_ICON_ENUM = { - '0': '', - '1': 'image:wb-iridescent', - '2': 'icons:more-horiz', - '3': 'icons:code', - '4': 'icons:apps' -}; - -class CRSearchResults extends HTMLElement { - constructor(baseUrl) { - super(); - this.attachShadow({mode: 'open'}); - - this.baseUrl = baseUrl; - } - - set searchString(searchString) { - this.matches = this.keywordsModel.getMatches(searchString); - - render(html` - -
    - ${this.matches.map((match) => { - const {keyword, pageReferences} = match; - const {type, description, href, domainHref} = pageReferences[0]; - - let fullUrl = this.baseUrl + domainHref; - - if (href) { - fullUrl += href; - } - - return html` - -
    -
    - ${keyword} - ${TYPE_LABEL_ENUM[type]} -
    -
    -
    - -
    -
    - `; - })} -
    - `, this.shadowRoot, { - host: this, - }); - } - - click(event) { - this.navigate(event.currentTarget); - } - - get results() { - return this.shadowRoot.querySelectorAll('a'); - } - - get selectedResult() { - return this.results[this._selected]; - } - - focusSelectedResult() { - if (this.selectedResult) { - this.selectedResult.classList.add('selected'); - this.selectedResult.scrollIntoView({block: 'center'}); - } - } - - focusDown() { - if (this._selected === undefined) { - this._selected = 0; - } else { - if (this.selectedResult) { - this.selectedResult.classList.remove('selected'); - } - this._selected = Math.min(this._selected + 1, this.matches.length - 1); - } - - this.focusSelectedResult(); - } - - focusUp() { - if (this._selected === undefined) { - return; - } - - if (this.selectedResult) { - this.selectedResult.classList.remove('selected'); - } - this._selected = Math.max(this._selected - 1, 0); - - this.focusSelectedResult(); - } - - select() { - if (this._selected === undefined || this.selectedResult === undefined) { - return; - } - - this.navigate(this.selectedResult); - } - - navigate(element) { - const oldURL = new URL(window.location.href); - const newURL = new URL(element.href); - window.location = newURL; - - if (oldURL.pathname === newURL.pathname) { - window.location.reload(true); - } - } -} -customElements.define('cr-search-results', CRSearchResults); - -customElements.define('cr-search-control', class extends HTMLElement { - constructor() { - super(); - this.attachShadow({mode: 'open'}); - - this.createMenu(); - } - - get baseUrl() { - return this.getAttribute('base-url'); - } - - get protocolSearchIndexUrl() { - return this.baseUrl + this.getAttribute('protocol-search-index'); - } - - get inputElement() { - return this.shadowRoot.querySelector('input'); - } - - createMenu() { - this.menuContainer = document.querySelector('main > section'); - this.menu = new CRSearchResults(this.baseUrl); - this.menu.addEventListener('navigation', () => { - this.menu.remove(); - this.inputElement.value = ''; - this.menuContainer.classList.remove('hidden'); - }); - - fetch(this.protocolSearchIndexUrl).then(response => { - return response.json(); - }).then(value => { - this.menu.keywordsModel = new KeywordsModel(value); - }); - } - - connectedCallback() { - render(html` - - - `, this.shadowRoot, { - host: this, - }); - } - - handleArrows(event) { - switch (event.code) { - case 'ArrowDown': - this.menu.focusDown(); - return; - case 'ArrowUp': - this.menu.focusUp(); - return; - case 'Enter': - event.preventDefault(); - this.menu.select(); - return; - } - - if (event.code === 'Escape') { - this.inputElement.value = ''; - this.inputElement.blur(); - } - - const textValue = this.inputElement.value; - - if (textValue === '') { - this.menu.replaceWith(this.menuContainer); - return; - } - - if (!this.menu.connected) { - this.menuContainer.replaceWith(this.menu); - } - - this.menu.searchString = textValue; - } -}); - -const menuNavigationButton = document.querySelector('.menu-link'); -const aside = document.querySelector('aside'); -const mainSection = document.querySelector('main'); -const asideCloseButton = document.querySelector('.aside-close-button'); - -document.addEventListener('keydown', (event) => { - // Make sure that copy-pasting works - if (event.metaKey || event.ctrlKey || event.altKey) { - return; - } - // One of `a-z` or `A-Z` - if (event.keyCode >= 65 && event.keyCode <= 90) { - document.querySelector('cr-search-control').inputElement.focus(); - } - // Escape key - if (event.key === 'Escape' && aside.classList.contains('shown')) { - aside.classList.remove('shown'); - } -}); - -menuNavigationButton.addEventListener('click', (event) => { - // Don't trigger the click event on the main section - event.stopPropagation(); - aside.addEventListener('transitionend', () => { - // Move focus into close button of drawer - asideCloseButton.focus(); - }, {once: true}); - aside.classList.add('shown'); -}); -function closeAside() { - if (!aside.classList.contains('shown')) { - return; - } - aside.classList.remove('shown'); - menuNavigationButton.focus(); -} -mainSection.addEventListener('click', closeAside); -asideCloseButton.addEventListener('click', closeAside); diff --git a/pages/service-worker.js b/pages/service-worker.js deleted file mode 100644 index 635404a34..000000000 --- a/pages/service-worker.js +++ /dev/null @@ -1,16 +0,0 @@ -// thx @nekrtemplar -// https://github.com/NekR/self-destroying-sw - -self.addEventListener('install', function(e) { - self.skipWaiting(); -}); - -self.addEventListener('activate', function(e) { - self.registration.unregister() - .then(function() { - return self.clients.matchAll(); - }) - .then(function(clients) { - clients.forEach(client => client.navigate(client.url)) - }); -}); diff --git a/pages/styles/protocol.css b/pages/styles/protocol.css deleted file mode 100644 index bf9263489..000000000 --- a/pages/styles/protocol.css +++ /dev/null @@ -1,416 +0,0 @@ -html, body { - padding: 0; - margin: 0; - height: 100%; - background-color: #fafafa; - - font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; - - --header-text-color: white; - /* Border used in the aside to mark different sections */ - --separation-border: 1px solid rgba(0, 0, 0, 0.14); - /* Material-like elevation shadow */ - --elevation-shadow: rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 1px 5px 0px, rgba(0, 0, 0, 0.2) 0px 3px 1px -2px; - - --home-icon: url('data:image/svg+xml;utf8,'); -} - -body { - display: flex; -} - -.monospace, code { - font-family: Consolas, Menlo, monospace; -} - -code { - color: #8E24AA; - font-size: 15px; - white-space: nowrap; -} - -a, .aside-close-button { - color: hsl(232, 50%, 45%); -} - -aside { - height: 100vh; - display: flex; - min-width: 200px; - flex-direction: column; - border-right: var(--separation-border); - -} - -nav { - background-color: white; -} - -.navs header { - font-size: 1.3em; - height: 2em; - display: flex; - justify-content: center; - align-items: center; - border-bottom: var(--separation-border); - color: #727272; - font-size: 16px; -} - -#domains { - display: flex; - flex-direction: column; - flex: 1; - /* - * Without this, the scroller won't properly compute its height and - * enforce that items in it are overflowing. - */ - min-height: 0; - - border-top: var(--separation-border); -} - -/* Scroller used to make sure that domains are overflowing with display flex */ -#domains .scroller { - flex: 1; - overflow-y: auto; -} - -#home a, .menu-link { - background-image: var(--home-icon); - background-repeat: no-repeat; - padding-left: 26px; - background-size: 14px 14px; - border-left: 0; - background-position: 6px; -} - -.navs a { - flex: 1; - font-weight: 400; - min-height: 32px; - padding: 0 16px; - border-left: 10px solid transparent; - display: flex; - align-items: center; - text-decoration: none; -} - -.navs a:hover { - background-color: hsl(0,0%, 96%); -} - -#domains nav a.experimental { - border-left: 10px solid #E5737399; -} - -#domains nav a.deprecated { - border-left: 10px solid #FFCC8099; -} - -#domains.version-tot nav a:not(.tot), -#domains.version-1-2 nav a:not([class~="1-2"]), -#domains.version-1-3 nav a:not([class~="1-3"]), -#domains.version-v8 nav a:not(.v8) { - display: none; -} - -main { - height: 100%; - display: flex; - flex-direction: column; - overflow-x: auto; - /* Make sure that if the content is too small, we still show the full header */ - flex-grow: 1; -} - -main, cr-search-results { - background-color: #fafafa; -} - -main > header { - background-color: #3f51b5; - color: var(--header-text-color); - justify-content: space-between; - align-items: center; -} - -main > header h1 { - --header-height: 26px; - font-size: var(--header-height); - line-height: var(--header-height); - padding-left: 10px; - display: inline-block; - font-weight: 400; -} - -main > header .menu-link { - color: var(--header-text-color); - background-color: inherit; - text-decoration: underline; - border: none; - display: none; - font-size: 1em; - margin-top: 20px; - /* - * Reset the background image, as we don't want it on this link, - * but we do want it on all others. - */ - background-image: none; -} - -/* When narrow, hide page title to avoid wrapping h1 + search */ -@media only screen and (max-width:825px) { - main > header h1 { - display:none; - } -} - -.aside-close-button { - display: none; -} - -/* hide sidenav on mobile */ -@media only screen and (max-width:640px) { - aside { - transform: translate(-200px, 0); - position: absolute; - width: 200px; - z-index: 1; - background: inherit; - visibility: hidden; - transition: cubic-bezier(0,0,0.32,1); - transition-duration: 200ms; - } - - aside.shown { - transform: none; - visibility: visible; - } - - .aside-close-button { - display: block; - font-size: 1em; - width: 48px; - } - - #home nav { - display: flex; - } - - main > header .menu-link { - display: inline-block; - } -} - -cr-search-control { - flex: 1; - margin: 14px 25px; - display: inline-block; -} - -main > section { - overflow-y: auto; - padding: 25px; - /* To make sure that the GitHub badge positions correctly */ - position: relative; - /* so scroll-anchors twoards the bottom of the page are positioned in the viewport correctly */ - padding-bottom: 80vh; -} -@media only screen and (max-width:640px) { - main > section { - padding: 25px 4vw; - } -} - -/* Make sure that code snippets don't overflow the full content container */ -pre { - overflow-x: auto; -} - -/* Make sure that images don't overflow the full content container */ -img { - max-width: 100%; - object-fit: scale-down; -} - -.gh-badge img { - float: right; - /* The image is a triangle facing the right */ - shape-outside: polygon(0 0, 150px 0, 150px 150px); - /* - * All content in the main section has a padding, but this - * image must be floated to the borders of the section. - */ - margin: -21px -15px; -} - -/* Limit content width and center it */ -.main-content-section { - max-width: 100ch; - margin: 0 auto; -} - -.main-content-section:not(.domain), -.domain-section > div { - padding: 5px 15px; - margin-bottom: 25px; - background-color: white; - box-shadow: var(--elevation-shadow); -} - -span.experimental, span.deprecated { - font-size: 70%; - text-transform: uppercase; - padding: 2px; - margin-right: 5px; - cursor: help; - vertical-align: baseline; - font-weight: normal; - font-family: inherit; -} - -span.experimental { - background-color: #ec8888; - color: #171616; - border: 1px solid transparent; -} - -span.deprecated { - background-color: #FFCC80; - color: black; - border: 1px solid #EF6C00; -} - -.domain-experimental span.experimental { - display: none; -} - -.domain-experimental .heading-domain span.experimental { - display: inline-block; -} - -.domain-experimental .heading-domain { - border: 1px solid #E57373; -} - -span.domain-dot { - color: #555555; -} - -.toc-link { - line-height: 1.1em; -} - -.details { - padding-bottom: 10px; - word-break: break-word; -} - -.details:not(:last-child) { - border-bottom: var(--separation-border); -} - -.details .permalink { - opacity: 0; -} - -.details:hover .permalink, .details .permalink:focus { - opacity: 1; -} - -.details .permalink.copied::after { - content: "Copied URL!"; - background-color: #E0E0E0; - color: initial; - display: inline-block; - text-decoration: none; - margin-left: 6px; - font-size: 70%; - padding: 1px 3px; - font-weight: normal; - animation: 1s fadeOut 1s forwards; -} -.details .permalink.copied__md::after{ - content: "Copied markdown!"; -} - -.details .properties-name { - color: #4c4b4b; - font-weight: 300; - text-transform: uppercase; - margin: 1rem 0 0; -} - -.properties-container { - display: grid; - grid-template-columns: repeat(1, 1fr 2fr); -} - -.properties-container dt { - flex: 1; - text-align: right; -} - -.properties-container dd { - flex: 2; - margin-left: 10px; -} - -.properties-container dt, .properties-container dd { - padding: 5px; -} - -.details-description { - display: inline; - font-size: 90%; -} - -.details-description p { - display: inline; -} - -.param-container:not(:last-child) { - margin-bottom: 10px; -} - -.optional::after { - content: "optional"; - opacity: .6; - font-size: 70%; - display: block; -} - -.param-type { - display: block; - font-weight: bold; -} - -.param-type__array { - font-weight: normal; -} - -.param-type .param-type__array .param-type { - display: inline; -} - -h4 { - /* use padding rather than margin for better positioning when viewing #method-navigate, etc. */ - margin-top: 0; - padding-top: 1.33em; -} - -h3 { - color: hsl(0, 0%, 47%); -} - -@media(max-width: 800px) { - .navs a { - min-height: 48px; - } -} - -@keyframes fadeOut { - 0% {opacity: 1} - 100% {opacity: 0} - } diff --git a/pages/tot.11ty.js b/pages/tot.11ty.js deleted file mode 100644 index 92a8ef03e..000000000 --- a/pages/tot.11ty.js +++ /dev/null @@ -1,7 +0,0 @@ -import {DomainGenerator} from './domainGenerator.js'; - -export default class extends DomainGenerator { - constructor() { - super('tot'); - } -} diff --git a/pages/tot.md b/pages/tot.md deleted file mode 100644 index 3e26f7125..000000000 --- a/pages/tot.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -layout: shell.hbs -title: Chrome DevTools Protocol - latest (tip-of-tree) -version: tot ---- -The **latest (tip-of-tree) protocol** changes -frequently and may break at any time. -However it captures the full capabilities of the Protocol, whereas the stable release is a subset. -There is no backwards compatibility support guaranteed for the capabilities it introduces. - -
    Latest browser_protocol.json version updated:
    -Date: 2026-09-04 04:40:14 +0000 - - -
    Latest js_protocol.json version updated:
    -Date: 2026-08-12 05:00:19 +0000 - diff --git a/pages/v8.11ty.js b/pages/v8.11ty.js deleted file mode 100644 index 0186ed4c9..000000000 --- a/pages/v8.11ty.js +++ /dev/null @@ -1,7 +0,0 @@ -import {DomainGenerator} from './domainGenerator.js'; - -export default class extends DomainGenerator { - constructor() { - super('v8'); - } -} diff --git a/pages/v8.md b/pages/v8.md deleted file mode 100644 index 513bb1c7f..000000000 --- a/pages/v8.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -layout: shell.hbs -title: Chrome DevTools Protocol - v8-inspector (node) -version: v8 ---- -This is the protocol available with --inspect in node 6.3+. -See Debugging Node.js with Chrome DevTools -to understand the basics of using the DevTools with Node. This protocol allows any tool to connect to node to debug it. - -The protocol domains available expose all underlying methods that deliver the JavaScript functionality found in Chrome DevTools. -More on the debugging protocol. diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index 121c80a1c..000000000 --- a/rollup.config.js +++ /dev/null @@ -1,11 +0,0 @@ -import resolve from '@rollup/plugin-node-resolve'; -import terser from "@rollup/plugin-terser"; - -export default { - input: 'pages/scripts/index.js', - output: { - file: 'devtools-protocol/scripts/index.js', - format: 'esm', - }, - plugins: [ resolve(), terser(), ], -} diff --git a/service-worker.js b/service-worker.js deleted file mode 100644 index 30b7678dd..000000000 --- a/service-worker.js +++ /dev/null @@ -1 +0,0 @@ -console.log('ServiceWorker disabled in development mode.'); diff --git a/pages/images/cdp-editor.png b/src/images/cdp-editor.png similarity index 100% rename from pages/images/cdp-editor.png rename to src/images/cdp-editor.png diff --git a/pages/images/logo.png b/src/images/logo.png similarity index 100% rename from pages/images/logo.png rename to src/images/logo.png diff --git a/pages/images/protocol-monitor.png b/src/images/protocol-monitor.png similarity index 100% rename from pages/images/protocol-monitor.png rename to src/images/protocol-monitor.png diff --git a/test/primitive_tests.sh b/test/primitive_tests.sh deleted file mode 100755 index f570c3292..000000000 --- a/test/primitive_tests.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash -set -euxo pipefail - -local_script_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -build_dp_path=$local_script_path/../devtools-protocol - -# v lame assertions -cat $build_dp_path/index.html | grep DOMSnapshot -cat $build_dp_path/tot/Page/index.html | grep --no-messages navigateToHistoryEntry - -stat $build_dp_path/search_index/v8.json -stat $build_dp_path/search_index/tot.json - -echo "assertions passed ✅" From 9c4d9ed0472ca80880feaae380866a1e1cbb69b5 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 8 Sep 2026 10:39:28 -0700 Subject: [PATCH 09/51] feat: sort standard before experimental before deprecated and remove experimental toggle --- create-search-index.cjs | 6 +++--- make-stable-protocol.cjs | 4 ++-- prep-tot-protocol-files.sh | 24 ++---------------------- scripts/generate-stubs.js | 2 +- src/index.html | 5 ----- src/main.js | 25 +++---------------------- src/protocol-model.js | 20 +++++++++----------- src/style.css | 36 ------------------------------------ test/protocol-model.test.js | 4 ++-- test/stubs.test.js | 2 +- 10 files changed, 23 insertions(+), 105 deletions(-) diff --git a/create-search-index.cjs b/create-search-index.cjs index caac650cd..362371cba 100644 --- a/create-search-index.cjs +++ b/create-search-index.cjs @@ -1,11 +1,11 @@ 'use strict'; /** - * Utility command that creates the search index for pages/_data/?.json. + * Utility command that creates the search index for data/?.json. */ const fs = require('fs'); -const VERSIONS_FILE = 'pages/_data/versions.json'; +const VERSIONS_FILE = 'data/versions.json'; const versionsText = fs.readFileSync(VERSIONS_FILE); const versions = JSON.parse(versionsText); @@ -13,7 +13,7 @@ const versions = JSON.parse(versionsText); versions.forEach(generateSearchIndex); function generateSearchIndex(version) { - const protocolText = fs.readFileSync(`pages/_data/${version.slug}.json`); + const protocolText = fs.readFileSync(`data/${version.slug}.json`); const protocol = JSON.parse(protocolText); // Set up Keyword bank diff --git a/make-stable-protocol.cjs b/make-stable-protocol.cjs index c650083e8..fa451e107 100644 --- a/make-stable-protocol.cjs +++ b/make-stable-protocol.cjs @@ -5,7 +5,7 @@ */ const fs = require('fs'); -const tot = JSON.parse(fs.readFileSync(__dirname + '/pages/_data/tot.json', 'utf8')); +const tot = JSON.parse(fs.readFileSync(__dirname + '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/data/tot.json', 'utf8')); const isNotExperimentalOrDeprecated = item => !item.experimental && !item.deprecated; @@ -26,5 +26,5 @@ stableProtocol.domains.forEach(domain => { }); // filter out command params, too? -fs.writeFileSync(__dirname + '/pages/_data/1-3.json', JSON.stringify(stableProtocol, null, 2)); +fs.writeFileSync(__dirname + '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/data/1-3.json', JSON.stringify(stableProtocol, null, 2)); diff --git a/prep-tot-protocol-files.sh b/prep-tot-protocol-files.sh index 7fe778b88..0f7f6d6f5 100755 --- a/prep-tot-protocol-files.sh +++ b/prep-tot-protocol-files.sh @@ -15,8 +15,8 @@ js_protocol_path="$protocol_repo_path/json/js_protocol.json" # => into viewer cd $local_script_path -local_tot_protocol_path="pages/_data/tot.json" -local_v8_protocol_path="pages/_data/v8.json" +local_tot_protocol_path="data/tot.json" +local_v8_protocol_path="data/v8.json" if ! [ -s $browser_protocol_path ]; then echo "error: couldn't find local protocol file" >&2; exit 1 @@ -30,24 +30,4 @@ node make-stable-protocol.cjs node create-search-index.cjs -# get the latest change -# => into chromium -cd $(dirname "$browser_protocol_path") -br_commit_line=$(git log --date=iso --no-color --max-count=1 -- browser_protocol.json | grep -E -o "^commit.*") -br_date_line=$(git log --date=iso --no-color --max-count=1 -- browser_protocol.json | grep -E -o "^Date.*") - -cd $(dirname "$js_protocol_path") -js_commit_line=$(git log --date=iso --no-color --max-count=1 -- js_protocol.json | grep -E -o "^commit.*") -js_date_line=$(git log --date=iso --no-color --max-count=1 -- js_protocol.json | grep -E -o "^Date.*") - -# copy it into the HTML file -# => into viewer -cd $local_script_path - -# we no longer printing the most recent protocol git hashes. -# we can restore this when the devtools-protocol repo starts includes that data - -cat pages/tot.md | sed -Ee "s/^()Date.*/\1$br_date_line/" > pages/tot.md.new -cat pages/tot.md.new | sed -Ee "s/^()Date.*/\1$js_date_line/" > pages/tot.md -rm -f pages/tot.md.new diff --git a/scripts/generate-stubs.js b/scripts/generate-stubs.js index 7ea78f1c8..d15680224 100644 --- a/scripts/generate-stubs.js +++ b/scripts/generate-stubs.js @@ -65,7 +65,7 @@ export function generateDomainStub(domain) { * @returns {{ domainCount: number, outputDir: string }} */ export function generateStubs({ - protocolPath = path.resolve('pages/_data/tot.json'), + protocolPath = path.resolve('data/tot.json'), outputDir = path.resolve('devtools-protocol'), srcDir = path.resolve('src'), } = {}) { diff --git a/src/index.html b/src/index.html index 9f4be7fe1..b30403a32 100644 --- a/src/index.html +++ b/src/index.html @@ -40,11 +40,6 @@ DevTools Protocol - - Enabled - Disabled - ⚡ Exp -
    diff --git a/src/main.js b/src/main.js index 2e768d6e7..6aa9483c8 100644 --- a/src/main.js +++ b/src/main.js @@ -146,7 +146,6 @@ class App { ]); this._prepareDatasets(browserProto, jsProto); - this._setupExperimentalToggle(); this._onRoute(); } catch (error) { this._contentElement.textContent = ''; @@ -182,7 +181,7 @@ class App { } // 2. Stable Protocol (1.3) - // Stable target is the stabilized Tip-of-Tree protocol + // Stable target is the stabilized Tip-of-Tree protocol (strictly no experimental domains/items) for (const d of stableTotDomains) { this._targetStore.stable.all.set(d.domain, d); this._targetStore.stable.stable.set(d.domain, d); @@ -207,23 +206,6 @@ class App { } } - _setupExperimentalToggle() { - const isExpEnabled = window.localStorage['experimental'] !== 'false'; - document.body.classList.toggle('experimental-enabled', isExpEnabled); - - const expToggle = document.getElementById('experimental'); - if (expToggle) { - expToggle.addEventListener('click', () => { - const currentlyEnabled = window.localStorage['experimental'] !== 'false'; - const nextState = !currentlyEnabled; - window.localStorage['experimental'] = nextState ? 'true' : 'false'; - document.body.classList.toggle('experimental-enabled', nextState); - this._updateActiveDomains(); - this._onRoute(); - }); - } - } - _setupDrawerEvents() { if (this._drawerToggle) { this._drawerToggle.addEventListener('click', () => this._toggleDrawer()); @@ -306,9 +288,8 @@ class App { } _updateActiveDomains() { - const isExp = window.localStorage['experimental'] !== 'false'; const store = this._targetStore[this._currentTarget] || this._targetStore.tot; - this._activeDomains = isExp ? store.all : store.stable; + this._activeDomains = store.all; this._search.setDomains(Array.from(this._activeDomains.values())); this._renderSidebar(this._activeDomains); @@ -388,7 +369,7 @@ class App { if (!this._activeDomains.has(domain)) { this._contentElement.appendChild( - renderError(`Unknown domain: ${domain}. Enable experimental domains above?`) + renderError(`Unknown domain: ${domain}.`) ); return; } diff --git a/src/protocol-model.js b/src/protocol-model.js index 2d41620d7..dae28d867 100644 --- a/src/protocol-model.js +++ b/src/protocol-model.js @@ -45,10 +45,9 @@ function nameProperty(collectionName) { /** * Deterministically sorts a collection in-place: - * 1. Non-experimental before experimental. - * 2. Non-deprecated before deprecated. - * 3. Required before optional. - * 4. Alphabetical by entity name. + * 1. Standard (0) before Experimental (1) before Deprecated (2). + * 2. Required before optional. + * 3. Alphabetical by entity name. * * @param {Array} items * @param {string|null} prop @@ -56,18 +55,17 @@ function nameProperty(collectionName) { function sortCollection(items, prop) { if (!prop) return; items.sort((a, b) => { - const aExp = a.experimental ? 1 : 0; - const bExp = b.experimental ? 1 : 0; - if (aExp !== bExp) return aExp - bExp; - - const aDep = a.deprecated ? 1 : 0; - const bDep = b.deprecated ? 1 : 0; - if (aDep !== bDep) return aDep - bDep; + // 1. Standard (0) before Experimental (1) before Deprecated (2) + const aRank = a.deprecated ? 2 : (a.experimental ? 1 : 0); + const bRank = b.deprecated ? 2 : (b.experimental ? 1 : 0); + if (aRank !== bRank) return aRank - bRank; + // 2. Required before optional const aOpt = a.optional ? 1 : 0; const bOpt = b.optional ? 1 : 0; if (aOpt !== bOpt) return aOpt - bOpt; + // 3. Alphabetical by entity name if (!a[prop] || !b[prop]) return 0; return a[prop].localeCompare(b[prop]); }); diff --git a/src/style.css b/src/style.css index 6744ccc3c..89d78f33a 100644 --- a/src/style.css +++ b/src/style.css @@ -136,42 +136,6 @@ code { white-space: nowrap; } -.experimental-pill { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 8px; - border-radius: 12px; - border: 1px solid var(--color-border); - background-color: #fff; - cursor: pointer; - font-size: 11px; - font-weight: 500; - color: var(--color-text-secondary); - user-select: none; - transition: all 0.15s ease; -} - -.experimental-pill:hover { - background-color: #f8f9fa; - border-color: #c6c6c6; -} - -.experimental-enabled .experimental-pill { - background-color: #fef7e0; - border-color: #f9ab00; - color: #b06000; -} - -#experimental-on, -.experimental-enabled #experimental-off { - display: none; -} - -.experimental-enabled #experimental-on { - display: block; -} - .header-center { flex: 1; max-width: 520px; diff --git a/test/protocol-model.test.js b/test/protocol-model.test.js index ab37aa5ed..93aeb110d 100644 --- a/test/protocol-model.test.js +++ b/test/protocol-model.test.js @@ -59,8 +59,8 @@ test('normalizeProtocol: deterministic sorting (experimental, deprecated, option const normalized = normalizeProtocol(raw); const commandNames = normalized.domains[0].commands.map(c => c.name); - // Non-experimental & non-deprecated alphabetical first, then deprecated, then experimental - assert.deepEqual(commandNames, ['alpha', 'beta', 'alphaDep', 'zetaExp']); + // Standard alphabetical first, then experimental, then deprecated + assert.deepEqual(commandNames, ['alpha', 'beta', 'zetaExp', 'alphaDep']); }); test('normalizeProtocol: does NOT mutate input objects (delete object.experimental)', () => { diff --git a/test/stubs.test.js b/test/stubs.test.js index 064c452bf..961e9af34 100644 --- a/test/stubs.test.js +++ b/test/stubs.test.js @@ -90,7 +90,7 @@ test('generateStubs: end-to-end stub generation in temporary directory', async t const scriptPath = path.resolve('scripts/generate-stubs.js'); const output = execFileSync( process.execPath, - [scriptPath, path.resolve('pages/_data/tot.json'), cliTmpDir], + [scriptPath, path.resolve('data/tot.json'), cliTmpDir], { encoding: 'utf8' } ); From 475b02108d1e028404eaacedcc34379c7c05e347 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 8 Sep 2026 10:42:05 -0700 Subject: [PATCH 10/51] style: update typography, card elevation, domain badging, and type colors --- src/protocol_renderer.js | 34 +++++++++++------- src/style.css | 78 ++++++++++++++++++++++++++++++++-------- 2 files changed, 85 insertions(+), 27 deletions(-) diff --git a/src/protocol_renderer.js b/src/protocol_renderer.js index 6f0a55477..15c1725e4 100644 --- a/src/protocol_renderer.js +++ b/src/protocol_renderer.js @@ -6,6 +6,12 @@ class ProtocolRenderer { static renderDomain(domain) { let result = E.div(); let main = result.div('domain'); + if (domain.experimental) { + main.classList.add('domain-experimental'); + } + if (domain.deprecated) { + main.classList.add('domain-deprecated'); + } ProtocolRenderer.applyBackground(domain, main); let padding = result.div('domain-padding', '\u2606'); { @@ -18,7 +24,7 @@ class ProtocolRenderer { let description = header.el('p'); description.textContent = domain.description || ''; - ProtocolRenderer.applyMarks(domain, title); + ProtocolRenderer.applyMarks(domain, title, false); if (domain.commands.length || domain.events.length || domain.types.length) { let jumpBar = header.div('section-jump-bar'); @@ -99,7 +105,7 @@ class ProtocolRenderer { let main = E.div('type'); ProtocolRenderer.applyBackground(type, main); ProtocolRenderer.applyBackground(domain, main); - main.appendChild(ProtocolRenderer.renderTitle(domain.domain, type.id, type, 'type')); + main.appendChild(ProtocolRenderer.renderTitle(domain.domain, type.id, type, 'type', Boolean(domain.experimental))); if (type.type) { main.el('p', '', 'Type: ') .text(type.type, 'parameter-type'); @@ -143,7 +149,7 @@ class ProtocolRenderer { return main; } - static renderTitle(domainName, title, item, titleType) { + static renderTitle(domainName, title, item, titleType, isParentDomainExperimental = false) { // Render heading. let heading = E.el('h4', 'monospace text-overflow'); @@ -158,33 +164,34 @@ class ProtocolRenderer { heading.setAttribute('id', ProtocolRenderer.titleId(domainName, title)); heading.text(domainName + '.', 'method-domain'); heading.text(title, 'method-name'); - ProtocolRenderer.applyMarks(item, heading); + ProtocolRenderer.applyMarks(item, heading, isParentDomainExperimental); let href = (window.app && window.app.formatRef) ? window.app.formatRef(id) : '#/' + id; heading.a(href, '#').classList.add('title-link'); return heading; } static renderTableOfContents(domain, container) { + const isDomainExp = Boolean(domain.experimental); let renderEventOrMethodEntry = (method, container) => ProtocolRenderer.renderTableOfContentsEntry(domain.domain, method.name, container); let renderTypeEntry = (type, container) => ProtocolRenderer.renderTableOfContentsEntry(domain.domain, type.id, container); if (domain.commands.length) - ProtocolRenderer.renderTableOfContentsSection("Methods", domain.commands, renderEventOrMethodEntry, container); + ProtocolRenderer.renderTableOfContentsSection("Methods", domain.commands, renderEventOrMethodEntry, container, isDomainExp); if (domain.events.length) - ProtocolRenderer.renderTableOfContentsSection("Events", domain.events, renderEventOrMethodEntry, container); + ProtocolRenderer.renderTableOfContentsSection("Events", domain.events, renderEventOrMethodEntry, container, isDomainExp); if (domain.types.length) - ProtocolRenderer.renderTableOfContentsSection("Types", domain.types, renderTypeEntry, container); + ProtocolRenderer.renderTableOfContentsSection("Types", domain.types, renderTypeEntry, container, isDomainExp); } - static renderTableOfContentsSection(sectionName, entries, renderer, container) { + static renderTableOfContentsSection(sectionName, entries, renderer, container, isDomainExp = false) { let title = container.el('h4'); title.textContent = sectionName; let section = container.div('div'); for (let entry of entries) { let row = renderer(entry, section); - ProtocolRenderer.applyMarks(entry, row); + ProtocolRenderer.applyMarks(entry, row, isDomainExp); } return section; } @@ -202,7 +209,7 @@ class ProtocolRenderer { let main = E.div('method'); ProtocolRenderer.applyBackground(method, main); ProtocolRenderer.applyBackground(domain, main); - main.appendChild(ProtocolRenderer.renderTitle(domain.domain, method.name, method, isEvent ? 'event' : 'method')); + main.appendChild(ProtocolRenderer.renderTitle(domain.domain, method.name, method, isEvent ? 'event' : 'method', Boolean(domain.experimental))); { // Render description. let p = main.el('p'); @@ -259,7 +266,7 @@ class ProtocolRenderer { description.append('.'); }); } - ProtocolRenderer.applyMarks(parameter, description); + ProtocolRenderer.applyMarks(parameter, description, Boolean(domain.experimental)); } return main; } @@ -306,8 +313,11 @@ class ProtocolRenderer { element.classList.add('deprecated-bg'); } - static applyMarks(item, element) { + static applyMarks(item, element, isParentDomainExperimental = false) { if (item.experimental) { + if (isParentDomainExperimental) { + return; + } let e = element.span('experimental', 'experimental'); e.title = 'This may be changed, moved or removed'; element.appendChild(e); diff --git a/src/style.css b/src/style.css index 89d78f33a..a33e8a196 100644 --- a/src/style.css +++ b/src/style.css @@ -22,8 +22,8 @@ body { color: var(--color-text); background-color: var(--color-bg); font-family: var(--font-sans); - font-size: 13px; - line-height: 1.5; + font-size: 14.5px; + line-height: 1.55; padding: 0; margin: 0; overflow: hidden; @@ -38,17 +38,37 @@ h1, h2, h3, h4, h5 { margin-top: 0; } +h1 { + font-size: 28px; + font-weight: 700; + letter-spacing: -0.3px; +} + +h2 { + font-size: 23px; + font-weight: 700; + letter-spacing: -0.2px; +} + +h3 { + font-size: 17px; + font-weight: 600; + margin: 24px 0 10px 0; +} + h4 { - padding-top: 1.2em; + font-size: 14.5px; + font-weight: 600; + padding-top: 1em; } h5 { color: var(--color-text-secondary); - font-weight: 500; + font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 11px; - margin: 1rem 0 0.5rem; + margin: 1.2rem 0 0.5rem; } a { @@ -340,19 +360,20 @@ code { .box { width: 96%; - max-width: 1100px; - border-radius: 4px; + max-width: 1000px; + border-radius: 6px; border: 1px solid var(--color-border); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.12); margin: 16px auto 24px auto; background-color: var(--color-surface); } .box-content, .method, .type { - padding: 16px 24px; + padding: 18px 26px; } .domain { - padding: 16px 24px; + padding: 20px 24px; } .domain-padding { @@ -499,20 +520,44 @@ h4:hover .title-link { span.experimental, span.deprecated { font-size: 10px; text-transform: uppercase; - background-color: #ea8600; - padding: 1px 4px; + background-color: #fef7e0; + color: #b06000; + border: 1px solid #fdd663; + padding: 1px 5px; border-radius: 3px; cursor: help; - color: white; vertical-align: middle; font-weight: 600; font-family: var(--font-sans); margin-left: 1ex; user-select: none; + letter-spacing: 0.3px; } span.deprecated { - background-color: #d93025; + background-color: #fce8e6; + color: #c5221f; + border: 1px solid #fad2cf; +} + +/* Domain-level badging: if the whole domain is experimental or deprecated, + badge the top card heading and suppress redundant per-item badges inside */ +.domain-experimental span.experimental { + display: none; +} + +.domain-experimental h2 span.experimental, +.domain-experimental .domain-title-heading span.experimental { + display: inline-block; +} + +.domain-deprecated span.deprecated { + display: none; +} + +.domain-deprecated h2 span.deprecated, +.domain-deprecated .domain-title-heading span.deprecated { + display: inline-block; } .entity-icon { @@ -530,16 +575,19 @@ span.deprecated { .entity-icon-method { background-color: #e8f0fe; color: #1a73e8; + border: 1px solid #d2e3fc; } .entity-icon-event { background-color: #e6f4ea; color: #137333; + border: 1px solid #ceead6; } .entity-icon-type { - background-color: #fef7e0; - color: #b06000; + background-color: #f3e8fd; + color: #7b1fa2; + border: 1px solid #e9d2fd; } .method:not(:last-child), .type:not(:last-child) { From ce4a7a07208fd7330048606cb4b556aeff47245a Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 8 Sep 2026 10:45:13 -0700 Subject: [PATCH 11/51] homepage: restore master documentation content and styling --- src/index.html | 186 +++++++++++++++++++++++++++++++++++++++------ src/style.css | 46 ++++++++++- test/e2e.test.js | 23 ++++++ test/stubs.test.js | 1 + 4 files changed, 229 insertions(+), 27 deletions(-) diff --git a/src/index.html b/src/index.html index b30403a32..d429651bf 100644 --- a/src/index.html +++ b/src/index.html @@ -81,29 +81,169 @@ diff --git a/src/style.css b/src/style.css index a33e8a196..b537873b8 100644 --- a/src/style.css +++ b/src/style.css @@ -81,15 +81,53 @@ a:hover { text-decoration: underline; } +pre { + background-color: #f1f3f4; + border-radius: 4px; + padding: 12px 16px; + font-family: var(--font-mono); + font-size: 13px; + line-height: 1.45; + overflow-x: auto; + border: 1px solid var(--color-border); +} + code { font-family: var(--font-mono); - color: #c2185b; - font-size: 12px; - background-color: rgba(0, 0, 0, 0.04); - padding: 1px 4px; + font-size: 0.9em; + background-color: #f1f3f4; + color: var(--color-text); + padding: 2px 5px; border-radius: 3px; } +pre code { + background-color: transparent; + padding: 0; + border-radius: 0; + color: inherit; +} + +figure.screenshot { + margin: 16px 0 24px 0; + text-align: center; +} + +figure.screenshot a { + display: block; + text-align: center; +} + +figure.screenshot img { + max-width: 100%; + max-height: 380px; + object-fit: contain; + height: auto; + border: 1px solid var(--color-border); + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + /* ========================================================================== Toolbar Header (44px) ========================================================================== */ diff --git a/test/e2e.test.js b/test/e2e.test.js index 8547d5d02..27ebe156b 100644 --- a/test/e2e.test.js +++ b/test/e2e.test.js @@ -430,6 +430,29 @@ test('Chrome DevTools Protocol Viewer E2E Tests', async (t) => { ); assert.strictEqual(hash, '#/stable/Page.navigate', `Expected 404 handler to redirect to "#/stable/Page.navigate", got "${hash}"`); }); + + await t.test('9. Root landing page rendering and rich content (#/)', async () => { + await cdp.send('Page.navigate', { url: `${baseUrl}/#/` }, sessionId); + + const landingHeading = await cdp.pollEvaluate( + 'document.querySelector("#content .box h1")?.textContent?.trim()', + (val) => val === 'Chrome DevTools Protocol', + sessionId + ); + assert.strictEqual(landingHeading, 'Chrome DevTools Protocol', 'Expected landing page title'); + + const monitorImageSrc = await cdp.evaluate( + 'document.querySelector("figure.screenshot img")?.getAttribute("src")', + sessionId + ); + assert.strictEqual(monitorImageSrc, 'images/protocol-monitor.png', 'Expected Protocol Monitor image'); + + const endpointsHeading = await cdp.evaluate( + 'document.getElementById("endpoints")?.textContent?.trim()', + sessionId + ); + assert.strictEqual(endpointsHeading, 'HTTP Endpoints', 'Expected HTTP Endpoints heading'); + }); } finally { // Teardown resources if (browserWs) { diff --git a/test/stubs.test.js b/test/stubs.test.js index 961e9af34..79a4823f0 100644 --- a/test/stubs.test.js +++ b/test/stubs.test.js @@ -75,6 +75,7 @@ test('generateStubs: end-to-end stub generation in temporary directory', async t 'utilities.js', 'style.css', 'favicons', + 'images', 'home.svg', ]; From 41c40e96b883044b3988c2c41701260a24a1615b Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 8 Sep 2026 11:58:31 -0700 Subject: [PATCH 12/51] style: align visual design with classic indigo material theme --- src/style.css | 360 +++++++++++++++++++++++++------------------------- 1 file changed, 182 insertions(+), 178 deletions(-) diff --git a/src/style.css b/src/style.css index b537873b8..3b998d57b 100644 --- a/src/style.css +++ b/src/style.css @@ -1,17 +1,26 @@ +/* ========================================================================== + Chrome DevTools Protocol Viewer - Material Design Theme + Authentic styling matching origin/master and vanilla-protocol-viewer + ========================================================================== */ + :root { - --header-height: 44px; - --sidebar-width: 240px; + --header-height: 50px; + --sidebar-width: 200px; --color-bg: #fafafa; --color-surface: #ffffff; - --color-text: #202124; - --color-text-secondary: #5f6368; - --color-border: #dadce0; - --color-hover: #e8f0fe; - --color-active: #d2e3fc; - --color-primary: #1a73e8; - --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, Monaco, "Courier New", monospace; - --parameters-grid-template-columns: minmax(200px, max-content) 1fr; + --color-text: #333333; + --color-text-secondary: #727272; + --color-border: rgba(0, 0, 0, 0.14); + --color-header: #3f51b5; + --color-header-text: #ffffff; + --color-hover: rgba(225, 245, 254, 0.49); + --color-active: rgb(225, 245, 254); + --color-primary: #3f51b5; + --color-link: hsl(232, 50%, 45%); + --elevation-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); + --font-sans: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; + --font-mono: Consolas, Menlo, Monaco, "Courier New", monospace; + --parameters-grid-template-columns: minmax(220px, max-content) 1fr; } * { @@ -22,8 +31,8 @@ body { color: var(--color-text); background-color: var(--color-bg); font-family: var(--font-sans); - font-size: 14.5px; - line-height: 1.55; + font-size: 14px; + line-height: 1.5; padding: 0; margin: 0; overflow: hidden; @@ -39,41 +48,42 @@ h1, h2, h3, h4, h5 { } h1 { - font-size: 28px; - font-weight: 700; - letter-spacing: -0.3px; + font-size: 26px; + font-weight: 400; + letter-spacing: -0.2px; } h2 { - font-size: 23px; - font-weight: 700; - letter-spacing: -0.2px; + font-size: 22px; + font-weight: 400; + letter-spacing: -0.1px; } h3 { font-size: 17px; - font-weight: 600; + font-weight: 500; margin: 24px 0 10px 0; + color: #3f51b5; } h4 { - font-size: 14.5px; + margin-top: 0; + padding-top: 1.33em; + font-size: 14px; font-weight: 600; - padding-top: 1em; } h5 { - color: var(--color-text-secondary); - font-weight: 600; + color: gray; + font-weight: 400; text-transform: uppercase; - letter-spacing: 0.5px; + letter-spacing: 1px; font-size: 11px; margin: 1.2rem 0 0.5rem; } a { - font-weight: normal; - color: var(--color-primary); + color: var(--color-link); text-decoration: none; } @@ -81,9 +91,15 @@ a:hover { text-decoration: underline; } +code { + font-family: var(--font-mono); + color: #8E24AA; + font-size: 14px; +} + pre { - background-color: #f1f3f4; - border-radius: 4px; + background-color: #f5f5f5; + border-radius: 2px; padding: 12px 16px; font-family: var(--font-mono); font-size: 13px; @@ -92,15 +108,6 @@ pre { border: 1px solid var(--color-border); } -code { - font-family: var(--font-mono); - font-size: 0.9em; - background-color: #f1f3f4; - color: var(--color-text); - padding: 2px 5px; - border-radius: 3px; -} - pre code { background-color: transparent; padding: 0; @@ -109,7 +116,7 @@ pre code { } figure.screenshot { - margin: 16px 0 24px 0; + margin: 20px 0 28px 0; text-align: center; } @@ -124,18 +131,18 @@ figure.screenshot img { object-fit: contain; height: auto; border: 1px solid var(--color-border); - border-radius: 4px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + border-radius: 2px; + box-shadow: var(--elevation-shadow); } /* ========================================================================== - Toolbar Header (44px) + Toolbar Header (50px) - Indigo Theme ========================================================================== */ .toolbar-header { height: var(--header-height); - background-color: var(--color-surface); - border-bottom: 1px solid var(--color-border); + background-color: var(--color-header); + border-bottom: 1px solid rgba(0, 0, 0, 0.14); position: fixed; top: 0; left: 0; @@ -144,22 +151,23 @@ figure.screenshot img { display: flex; align-items: center; justify-content: space-between; - padding: 0 12px; + padding: 0 16px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } .header-left { display: flex; align-items: center; - gap: 10px; - min-width: 220px; + gap: 12px; + min-width: 200px; } .drawer-toggle { display: none; background: none; border: none; - font-size: 18px; - color: var(--color-text); + font-size: 20px; + color: #ffffff; cursor: pointer; padding: 4px 8px; border-radius: 4px; @@ -168,26 +176,29 @@ figure.screenshot img { } .drawer-toggle:hover { - background-color: rgba(0, 0, 0, 0.06); + background-color: rgba(255, 255, 255, 0.15); } .brand-link { display: flex; align-items: center; - gap: 8px; - color: var(--color-text); - font-weight: 600; - font-size: 14px; + gap: 10px; + color: #ffffff; + font-weight: 400; + font-size: 18px; text-decoration: none; user-select: none; } .brand-link:hover { text-decoration: none; + color: #ffffff; } .brand-logo { display: block; + width: 24px; + height: 24px; } .brand-title { @@ -197,23 +208,20 @@ figure.screenshot img { .header-center { flex: 1; max-width: 520px; - margin: 0 16px; + margin: 0 20px; } .search-box { display: flex; align-items: center; - background-color: #f1f3f4; - border: 1px solid transparent; - border-radius: 6px; - padding: 4px 10px; - transition: all 0.15s ease; + background-color: transparent; + border-bottom: 1px solid rgba(255, 255, 255, 0.52); + padding: 0 0 3px 0; + transition: border-bottom-color 0.2s ease; } .search-box:focus-within { - background-color: #ffffff; - border-color: var(--color-primary); - box-shadow: 0 1px 3px rgba(26, 115, 232, 0.2); + border-bottom-color: #ffffff; } .search-box input[type="search"] { @@ -222,14 +230,19 @@ figure.screenshot img { background: transparent; outline: none; font-family: inherit; - font-size: 13px; - color: var(--color-text); - padding: 0; + font-size: 18px; + color: #ffffff; + text-align: center; width: 100%; } +.search-box input[type="search"]::placeholder { + color: rgba(255, 255, 255, 0.5); +} + .search-box input[type="search"]::-webkit-search-cancel-button { cursor: pointer; + filter: invert(1); } .search-kbd, @@ -237,9 +250,9 @@ figure.screenshot img { display: inline-block; font-family: var(--font-mono); font-size: 11px; - color: var(--color-text-secondary); - background-color: var(--color-surface); - border: 1px solid var(--color-border); + color: rgba(255, 255, 255, 0.7); + background-color: rgba(255, 255, 255, 0.15); + border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 3px; padding: 0 5px; line-height: 18px; @@ -256,19 +269,20 @@ figure.screenshot img { display: flex; align-items: center; justify-content: center; - color: var(--color-text-secondary); - padding: 4px; + color: rgba(255, 255, 255, 0.85); + padding: 6px; border-radius: 4px; } .toolbar-link:hover { - color: var(--color-text); - background-color: rgba(0, 0, 0, 0.05); + color: #ffffff; + background-color: rgba(255, 255, 255, 0.15); text-decoration: none; } .toolbar-icon.github { - opacity: 0.75; + filter: brightness(0) invert(1); + opacity: 0.85; transition: opacity 0.15s; } @@ -277,7 +291,7 @@ figure.screenshot img { } /* ========================================================================== - Sidebar Navigation + Sidebar Navigation (200px) ========================================================================== */ #sidebar { @@ -291,41 +305,41 @@ figure.screenshot img { display: flex; flex-direction: column; z-index: 50; + overflow-x: hidden; } .sidebar-header { - padding: 8px; + padding: 8px 10px; border-bottom: 1px solid var(--color-border); background-color: var(--color-surface); } .domain-filter { width: 100%; - border: 1px solid var(--color-border); - border-radius: 4px; - padding: 4px 8px; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 2px; + padding: 4px 6px; font-family: inherit; font-size: 12px; outline: none; - background-color: #f8f9fa; + background-color: #ffffff; } .domain-filter:focus { - background-color: #ffffff; - border-color: var(--color-primary); + border-color: #3f51b5; } .domain-list { flex: 1; overflow-y: auto; overflow-x: hidden; - padding: 4px 0; + padding: 0; } .sidebar-footer { - padding: 8px; + padding: 8px 10px; border-top: 1px solid var(--color-border); - background-color: #f8f9fa; + background-color: #fafafa; } .target-selector { @@ -335,14 +349,14 @@ figure.screenshot img { font-size: 12px; color: var(--color-text); background-color: #ffffff; - border: 1px solid var(--color-border); - border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 2px; outline: none; cursor: pointer; } .target-selector:focus { - border-color: var(--color-primary); + border-color: #3f51b5; } #sidebar .domain-link { @@ -351,12 +365,12 @@ figure.screenshot img { flex-direction: row; align-items: center; justify-content: space-between; - font-size: 13px; + font-size: 14px; color: var(--color-text); - line-height: 28px; - min-height: 28px; - padding: 0 12px; - border-left: 3px solid transparent; + line-height: 24px; + min-height: 38px; + padding: 0 16px; + border-left: 10px solid transparent; cursor: pointer; } @@ -366,22 +380,21 @@ figure.screenshot img { } #sidebar .active-link { - font-weight: 600; - background-color: var(--color-active); - border-left-color: var(--color-primary); - color: var(--color-primary); + font-weight: bold; + background-color: rgb(225, 245, 254); + color: #1a237e; } #sidebar .domain-link.experimental-bg { - background-color: rgba(254, 247, 224, 0.35); + border-left-color: #e5737399; } #sidebar .domain-link.deprecated-bg { - background-color: rgba(252, 232, 230, 0.35); + border-left-color: #ffcc8099; } /* ========================================================================== - Main Content Area + Main Content Area & Material Elevation Cards ========================================================================== */ #content { @@ -397,29 +410,29 @@ figure.screenshot img { } .box { - width: 96%; + width: 98%; max-width: 1000px; - border-radius: 6px; - border: 1px solid var(--color-border); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.12); - margin: 16px auto 24px auto; + border-radius: 2px; + box-shadow: var(--elevation-shadow); + margin: 16px auto 32px auto; background-color: var(--color-surface); } .box-content, .method, .type { - padding: 18px 26px; + padding: 16px 24px; } .domain { - padding: 20px 24px; + padding: 20px 48px 20px 48px; } .domain-padding { - height: 50vh; + height: 60vh; display: flex; - font-size: 48px; + font-size: 80px; align-items: center; - color: var(--color-border); + color: #3f51b5; + opacity: 0.15; justify-content: center; } @@ -436,12 +449,12 @@ figure.screenshot img { .section-jump-pill { display: inline-flex; align-items: center; - padding: 4px 10px; + padding: 3px 10px; font-size: 12px; font-weight: 500; - color: var(--color-text); - background-color: var(--color-surface); - border: 1px solid var(--color-border); + color: #3f51b5; + background-color: #f5f5f5; + border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 12px; text-decoration: none; transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease; @@ -449,9 +462,8 @@ figure.screenshot img { } .section-jump-pill:hover { - background-color: var(--color-hover); - border-color: var(--color-primary); - color: var(--color-primary); + background-color: rgba(63, 81, 181, 0.08); + border-color: #3f51b5; text-decoration: none; } @@ -460,7 +472,7 @@ figure.screenshot img { } .method-domain { - color: var(--color-text-secondary); + color: #ababab; } /* ========================================================================== @@ -488,11 +500,11 @@ figure.screenshot img { } .experimental-bg { - background-color: rgba(254, 247, 224, 0.35); + background-color: rgba(255, 212, 212, 0.2); } .deprecated-bg { - background-color: rgba(252, 232, 230, 0.35); + background-color: rgba(255, 249, 187, 0.2); } /* ========================================================================== @@ -500,11 +512,10 @@ figure.screenshot img { ========================================================================== */ .parameter-list { - margin: 8px 0 16px 0; + padding: 0.5rem 5px; display: grid; grid-template-columns: var(--parameters-grid-template-columns); - gap: 8px 16px; - padding: 0; + gap: 6px 12px; } .parameter { @@ -514,33 +525,34 @@ figure.screenshot img { .parameter-name { text-align: right; - color: var(--color-text); + color: black; word-wrap: break-word; - font-weight: 500; + padding: 5px 10px; + font-weight: normal; } .parameter-value { - gap: 4px; + overflow: hidden; + padding: 5px 10px; } .parameter-type { - font-weight: 600; + font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - color: #1a73e8; + color: #3f51b5; } .parameter-description { + font-size: 90%; color: var(--color-text); - font-size: 12px; } .optional::after { content: "optional"; - opacity: 0.65; - font-size: 10px; - font-weight: normal; + opacity: 0.6; + font-size: 70%; display: block; } @@ -556,26 +568,22 @@ h4:hover .title-link { } span.experimental, span.deprecated { - font-size: 10px; + font-size: 70%; text-transform: uppercase; - background-color: #fef7e0; - color: #b06000; - border: 1px solid #fdd663; - padding: 1px 5px; - border-radius: 3px; + background-color: #e57373; + padding: 2px 4px; cursor: help; - vertical-align: middle; - font-weight: 600; - font-family: var(--font-sans); + color: white; + vertical-align: baseline; + font-weight: normal; + font-family: inherit; margin-left: 1ex; user-select: none; - letter-spacing: 0.3px; + border-radius: 2px; } span.deprecated { - background-color: #fce8e6; - color: #c5221f; - border: 1px solid #fad2cf; + background-color: #ffa03b; } /* Domain-level badging: if the whole domain is experimental or deprecated, @@ -599,37 +607,32 @@ span.deprecated { } .entity-icon { + vertical-align: top; display: inline-block; - font-size: 10px; - text-transform: uppercase; - padding: 1px 5px; + padding: 2px 5px; + font-size: 11px; + color: white; border-radius: 3px; - font-weight: 700; user-select: none; + font-weight: 500; line-height: 14px; margin-right: 6px; } .entity-icon-method { - background-color: #e8f0fe; - color: #1a73e8; - border: 1px solid #d2e3fc; + background-color: #ffa000b5; } .entity-icon-event { - background-color: #e6f4ea; - color: #137333; - border: 1px solid #ceead6; + background-color: #2e7d32a8; } .entity-icon-type { - background-color: #f3e8fd; - color: #7b1fa2; - border: 1px solid #e9d2fd; + background-color: #7b1fa2; } .method:not(:last-child), .type:not(:last-child) { - border-bottom: 1px solid var(--color-border); + border-bottom: double #b6b6b6a6 3px; } /* ========================================================================== @@ -649,7 +652,7 @@ span.deprecated { } .reference-icon { - width: 60px; + width: 55px; display: inline-block; vertical-align: middle; text-align: right; @@ -662,27 +665,27 @@ span.deprecated { #sresults { display: none; - background-color: var(--color-surface); + background-color: #ffffff; position: absolute; left: var(--sidebar-width); top: var(--header-height); bottom: 0; right: 0; overflow-y: auto; - border-top: 1px solid var(--color-border); + border-top: 2px solid rgba(51, 51, 51, 0.52); -webkit-overflow-scrolling: touch; z-index: 90; } #sresults .search-item { - padding: 8px 16px; + padding: 6px 12px; align-items: center; cursor: pointer; - border-bottom: 1px solid #f1f3f4; + border-bottom: 1px solid rgba(51, 51, 51, 0.12); } #sresults .search-item.selected { - background-color: var(--color-hover); + background-color: #e3f2fd; } #sresults .search-item:hover { @@ -696,24 +699,25 @@ span.deprecated { } #sresults .search-item-title-domain { - color: var(--color-text-secondary); + color: hsla(0, 0%, 57%, 1); } #sresults .search-item-icon { - margin-right: 12px; - width: 55px; - text-align: right; + margin: 8px 14px 8px 6px; + width: 50px; + text-align: center; flex-shrink: 0; + flex-grow: 0; } #sresults .search-item-main { flex: 1 1 auto; - max-width: 900px; + max-width: 1000px; overflow: hidden; } #sresults .search-item-description { - font-size: 12px; + font-size: 90%; color: var(--color-text-secondary); } @@ -723,8 +727,8 @@ span.deprecated { } .search-results-message { - margin: 40px auto; - max-width: 400px; + margin: 30px auto; + max-width: 500px; text-align: center; color: var(--color-text-secondary); } @@ -732,14 +736,14 @@ span.deprecated { .custom-search-result { justify-content: center; height: 48px; - color: var(--color-primary); + color: #3f51b5; font-weight: 500; } .landing-footer-note { margin-top: 2rem; color: var(--color-text-secondary); - font-size: 11px; + font-size: 12px; } /* ========================================================================== @@ -756,11 +760,11 @@ span.deprecated { top: var(--header-height); bottom: 0; left: 0; - width: 280px; + width: 260px; transform: translateX(-100%); transition: transform 0.25s cubic-bezier(0, 0, 0.2, 1); z-index: 150; - box-shadow: 2px 0 12px rgba(0, 0, 0, 0.2); + box-shadow: 2px 0 12px rgba(0, 0, 0, 0.25); } body.drawer-open #sidebar { @@ -782,7 +786,7 @@ span.deprecated { #content { left: 0; - padding: 16px; + padding: 8px; } #sresults { From 868aaa8f177e27af462e187531619c958b6d1fe3 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 8 Sep 2026 12:28:30 -0700 Subject: [PATCH 13/51] renderer: convert backticks to code, style toc headings with entity badges, and remove jump bar --- src/protocol_renderer.js | 112 +++++++++++++++++++-------------------- src/style.css | 44 +++++++-------- test/e2e.test.js | 31 +++++++---- 3 files changed, 99 insertions(+), 88 deletions(-) diff --git a/src/protocol_renderer.js b/src/protocol_renderer.js index 15c1725e4..cf3fa87d4 100644 --- a/src/protocol_renderer.js +++ b/src/protocol_renderer.js @@ -21,48 +21,10 @@ class ProtocolRenderer { let title = header.el('h2'); title.textContent = domain.domain; - - let description = header.el('p'); - description.textContent = domain.description || ''; ProtocolRenderer.applyMarks(domain, title, false); - if (domain.commands.length || domain.events.length || domain.types.length) { - let jumpBar = header.div('section-jump-bar'); - const scrollToSection = (sectionId) => { - let target = result.querySelector('#' + sectionId) || document.getElementById(sectionId); - if (target) { - const prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; - target.scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth' }); - } - }; - - if (domain.commands.length) { - let pill = jumpBar.a('#methods', `Methods (${domain.commands.length})`); - pill.className = 'section-jump-pill'; - pill.addEventListener('click', event => { - event.preventDefault(); - event.stopPropagation(); - scrollToSection('methods'); - }); - } - if (domain.events.length) { - let pill = jumpBar.a('#events', `Events (${domain.events.length})`); - pill.className = 'section-jump-pill'; - pill.addEventListener('click', event => { - event.preventDefault(); - event.stopPropagation(); - scrollToSection('events'); - }); - } - if (domain.types.length) { - let pill = jumpBar.a('#types', `Types (${domain.types.length})`); - pill.className = 'section-jump-pill'; - pill.addEventListener('click', event => { - event.preventDefault(); - event.stopPropagation(); - scrollToSection('types'); - }); - } + if (domain.description) { + ProtocolRenderer.renderDescription(domain.description, header); } ProtocolRenderer.renderTableOfContents(domain, header); @@ -110,10 +72,8 @@ class ProtocolRenderer { main.el('p', '', 'Type: ') .text(type.type, 'parameter-type'); } - { - // Render description. - let p = main.el('p'); - p.textContent = type.description || ''; + if (type.description) { + ProtocolRenderer.renderDescription(type.description, main); } if (type.properties && type.properties.length) { // Render parameters. @@ -178,17 +138,29 @@ class ProtocolRenderer { ProtocolRenderer.renderTableOfContentsEntry(domain.domain, type.id, container); if (domain.commands.length) - ProtocolRenderer.renderTableOfContentsSection("Methods", domain.commands, renderEventOrMethodEntry, container, isDomainExp); + ProtocolRenderer.renderTableOfContentsSection("Methods", "method", domain.commands, renderEventOrMethodEntry, container, isDomainExp); if (domain.events.length) - ProtocolRenderer.renderTableOfContentsSection("Events", domain.events, renderEventOrMethodEntry, container, isDomainExp); + ProtocolRenderer.renderTableOfContentsSection("Events", "event", domain.events, renderEventOrMethodEntry, container, isDomainExp); if (domain.types.length) - ProtocolRenderer.renderTableOfContentsSection("Types", domain.types, renderTypeEntry, container, isDomainExp); + ProtocolRenderer.renderTableOfContentsSection("Types", "type", domain.types, renderTypeEntry, container, isDomainExp); } - static renderTableOfContentsSection(sectionName, entries, renderer, container, isDomainExp = false) { - let title = container.el('h4'); - title.textContent = sectionName; - let section = container.div('div'); + static renderTableOfContentsSection(sectionName, sectionType, entries, renderer, container, isDomainExp = false) { + let title = container.el('h4', 'toc-section-heading'); + let badge = title.span(`entity-icon entity-icon-${sectionType}`); + badge.textContent = sectionType; + let link = title.a(`#${sectionName.toLowerCase()}`, `${sectionName} (${entries.length})`); + link.className = 'toc-section-link'; + link.addEventListener('click', (event) => { + event.preventDefault(); + const target = document.getElementById(sectionName.toLowerCase()); + if (target) { + const prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + target.scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth' }); + } + }); + + let section = container.div('toc-entries'); for (let entry of entries) { let row = renderer(entry, section); ProtocolRenderer.applyMarks(entry, row, isDomainExp); @@ -197,7 +169,7 @@ class ProtocolRenderer { } static renderTableOfContentsEntry(domainName, name, container) { - let row = container.div('div'); + let row = container.div('toc-link'); let id = `${domainName}.${name}`; let link = ProtocolRenderer.renderRef(id); link.classList.add('monospace'); @@ -210,10 +182,8 @@ class ProtocolRenderer { ProtocolRenderer.applyBackground(method, main); ProtocolRenderer.applyBackground(domain, main); main.appendChild(ProtocolRenderer.renderTitle(domain.domain, method.name, method, isEvent ? 'event' : 'method', Boolean(domain.experimental))); - { - // Render description. - let p = main.el('p'); - p.textContent = method.description || ''; + if (method.description) { + ProtocolRenderer.renderDescription(method.description, main); } if (method.parameters.length) { // Render parameters. @@ -253,7 +223,7 @@ class ProtocolRenderer { let descriptions = []; if (parameter.description) descriptions.push(parameter.description); - description.textContent = descriptions.join(' '); + ProtocolRenderer.renderTextWithCode(descriptions.join(' '), description); if (parameter.enum) { description.append(' Allowed values: '); parameter.enum.forEach((value, index) => { @@ -351,5 +321,33 @@ class ProtocolRenderer { icon.classList.add('entity-icon-event'); return icon; } + + static renderTextWithCode(text, container) { + if (!text) return; + const clean = text.replace(/^LINT\..*$\n?/gm, ''); + const parts = clean.split(/`([^`]+)`/g); + for (let i = 0; i < parts.length; i++) { + if (!parts[i]) continue; + if (i % 2 === 1) { + const code = document.createElement('code'); + code.textContent = parts[i]; + container.appendChild(code); + } else { + container.appendChild(document.createTextNode(parts[i])); + } + } + } + + static renderDescription(text, parentElement) { + if (!text) return; + const clean = text.replace(/^LINT\..*$\n?/gm, '').trim(); + if (!clean) return; + const paragraphs = clean.split(/\n\s*\n/); + for (const para of paragraphs) { + const p = document.createElement('p'); + ProtocolRenderer.renderTextWithCode(para, p); + parentElement.appendChild(p); + } + } }; diff --git a/src/style.css b/src/style.css index 3b998d57b..c82221dab 100644 --- a/src/style.css +++ b/src/style.css @@ -436,37 +436,37 @@ figure.screenshot img { justify-content: center; } -/* In-Domain Section Quick Jump Bar */ -.section-jump-bar { +/* Table of Contents */ +.toc-section-heading { + margin-top: 20px; + margin-bottom: 8px; display: flex; - flex-direction: row; - flex-wrap: wrap; align-items: center; - gap: 8px; - margin: 12px 0 16px 0; + font-size: 14px; } -.section-jump-pill { - display: inline-flex; - align-items: center; - padding: 3px 10px; - font-size: 12px; - font-weight: 500; - color: #3f51b5; - background-color: #f5f5f5; - border: 1px solid rgba(0, 0, 0, 0.12); - border-radius: 12px; - text-decoration: none; - transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease; - cursor: pointer; +.toc-section-heading .entity-icon { + margin-right: 8px; } -.section-jump-pill:hover { - background-color: rgba(63, 81, 181, 0.08); - border-color: #3f51b5; +.toc-section-link { + color: var(--color-text); text-decoration: none; } +.toc-section-link:hover { + text-decoration: underline; + color: var(--color-primary); +} + +.toc-entries { + margin-bottom: 12px; +} + +.toc-link { + line-height: 1.6; +} + #methods, #events, #types { scroll-margin-top: calc(var(--header-height) + 16px); } diff --git a/test/e2e.test.js b/test/e2e.test.js index 27ebe156b..cbe36ba60 100644 --- a/test/e2e.test.js +++ b/test/e2e.test.js @@ -362,27 +362,40 @@ test('Chrome DevTools Protocol Viewer E2E Tests', async (t) => { assert.strictEqual(isFocused, true, 'Expected document.activeElement to be document.getElementById("search")'); }); - await t.test('5. In-domain quick jump pills (#/Page)', async () => { + await t.test('5. Table of contents badges and backtick code rendering (#/Page)', async () => { await cdp.send('Page.navigate', { url: `${baseUrl}/#/Page` }, sessionId); - const pillTexts = await cdp.pollEvaluate( - 'Array.from(document.querySelectorAll(".section-jump-pill")).map(el => el.textContent.trim())', + const tocHeadings = await cdp.pollEvaluate( + 'Array.from(document.querySelectorAll(".toc-section-heading")).map(el => el.textContent.trim())', (arr) => Array.isArray(arr) && arr.length >= 3, sessionId ); assert.ok( - pillTexts.some((text) => text.includes('Methods')), - `Expected pills to include "Methods", got: ${JSON.stringify(pillTexts)}` + tocHeadings.some((text) => text.includes('Methods')), + `Expected TOC headings to include "Methods", got: ${JSON.stringify(tocHeadings)}` ); assert.ok( - pillTexts.some((text) => text.includes('Events')), - `Expected pills to include "Events", got: ${JSON.stringify(pillTexts)}` + tocHeadings.some((text) => text.includes('Events')), + `Expected TOC headings to include "Events", got: ${JSON.stringify(tocHeadings)}` ); assert.ok( - pillTexts.some((text) => text.includes('Types')), - `Expected pills to include "Types", got: ${JSON.stringify(pillTexts)}` + tocHeadings.some((text) => text.includes('Types')), + `Expected TOC headings to include "Types", got: ${JSON.stringify(tocHeadings)}` ); + + const methodBadgeText = await cdp.evaluate( + 'document.querySelector(".toc-section-heading .entity-icon-method")?.textContent?.trim()', + sessionId + ); + assert.strictEqual(methodBadgeText, 'method', 'Expected method badge in TOC heading'); + + const hasCodeInDescription = await cdp.pollEvaluate( + 'document.querySelectorAll("#content .box-content p code, #content .parameter-description code").length > 0', + (val) => val === true, + sessionId + ); + assert.strictEqual(hasCodeInDescription, true, 'Expected backticks to be rendered as elements'); }); await t.test('6. Type cross-references (#/DOM.NodeId)', async () => { From 1b45fab15d86a0171aac7b9d81b73ef285c9a760 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 8 Sep 2026 12:30:34 -0700 Subject: [PATCH 14/51] typography: add roboto webfont and increase font sizes and readability --- src/index.html | 3 +++ src/style.css | 37 ++++++++++++++++++++++--------------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/index.html b/src/index.html index d429651bf..689524cfb 100644 --- a/src/index.html +++ b/src/index.html @@ -15,6 +15,9 @@ + + + diff --git a/src/style.css b/src/style.css index c82221dab..597c45a3e 100644 --- a/src/style.css +++ b/src/style.css @@ -18,7 +18,7 @@ --color-primary: #3f51b5; --color-link: hsl(232, 50%, 45%); --elevation-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); - --font-sans: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; + --font-sans: 'Roboto', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; --font-mono: Consolas, Menlo, Monaco, "Courier New", monospace; --parameters-grid-template-columns: minmax(220px, max-content) 1fr; } @@ -31,15 +31,18 @@ body { color: var(--color-text); background-color: var(--color-bg); font-family: var(--font-sans); - font-size: 14px; - line-height: 1.5; + font-size: 15px; + line-height: 1.6; padding: 0; margin: 0; overflow: hidden; + -webkit-font-smoothing: antialiased; } p { margin: 0.5rem 0 1rem; + font-size: 15px; + line-height: 1.6; } h1, h2, h3, h4, h5 { @@ -48,38 +51,38 @@ h1, h2, h3, h4, h5 { } h1 { - font-size: 26px; + font-size: 28px; font-weight: 400; letter-spacing: -0.2px; } h2 { - font-size: 22px; + font-size: 24px; font-weight: 400; letter-spacing: -0.1px; } h3 { - font-size: 17px; + font-size: 19px; font-weight: 500; - margin: 24px 0 10px 0; + margin: 28px 0 12px 0; color: #3f51b5; } h4 { margin-top: 0; padding-top: 1.33em; - font-size: 14px; + font-size: 15.5px; font-weight: 600; } h5 { color: gray; - font-weight: 400; + font-weight: 500; text-transform: uppercase; letter-spacing: 1px; - font-size: 11px; - margin: 1.2rem 0 0.5rem; + font-size: 11.5px; + margin: 1.4rem 0 0.5rem; } a { @@ -94,7 +97,7 @@ a:hover { code { font-family: var(--font-mono); color: #8E24AA; - font-size: 14px; + font-size: 15px; } pre { @@ -464,7 +467,8 @@ figure.screenshot img { } .toc-link { - line-height: 1.6; + font-size: 14.5px; + line-height: 1.7; } #methods, #events, #types { @@ -528,7 +532,8 @@ figure.screenshot img { color: black; word-wrap: break-word; padding: 5px 10px; - font-weight: normal; + font-weight: 500; + font-size: 14.5px; } .parameter-value { @@ -542,10 +547,12 @@ figure.screenshot img { overflow: hidden; text-overflow: ellipsis; color: #3f51b5; + font-size: 14.5px; } .parameter-description { - font-size: 90%; + font-size: 14.5px; + line-height: 1.5; color: var(--color-text); } From 88cef80285096dd784361fb16237e0c65a3f6db7 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 8 Sep 2026 12:37:27 -0700 Subject: [PATCH 15/51] target-selector: fix dropdown switching and update home h3 heading color to grey --- src/main.js | 6 +++++- src/protocol-model.js | 5 ++++- src/style.css | 2 +- test/e2e.test.js | 33 +++++++++++++++++++++++++++++++++ test/protocol-model.test.js | 24 ++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/main.js b/src/main.js index 6aa9483c8..f926b5d1e 100644 --- a/src/main.js +++ b/src/main.js @@ -243,9 +243,13 @@ class App { if (nextTarget === this._currentTarget) return; this._currentTarget = nextTarget; + const targetStore = this._targetStore[this._currentTarget] || this._targetStore.tot; + const domainExistsInTarget = this._currentDomain && targetStore.all.has(this._currentDomain); + const domain = domainExistsInTarget ? this._currentDomain : null; + const newRoute = formatRoute({ target: this._currentTarget, - domain: this._currentDomain, + domain, member: null, }); this.navigate(newRoute); diff --git a/src/protocol-model.js b/src/protocol-model.js index dae28d867..ff0e18c6e 100644 --- a/src/protocol-model.js +++ b/src/protocol-model.js @@ -366,6 +366,9 @@ export function parseRoute(routeString) { target = normalizeTarget(potentialTarget); targetAndRest = cleanHash.slice(slashIndex + 1); } + } else if (TARGET_MAP.has(cleanHash.toLowerCase())) { + target = normalizeTarget(cleanHash); + targetAndRest = ''; } // Strip trailing slashes from domain/member (e.g. #/Page/ -> Page) @@ -395,7 +398,7 @@ export function formatRoute({ target = 'tot', domain = null, member = null } = { const targetPrefix = normTarget === 'tot' ? '' : `${normTarget}/`; if (!domain) { - return '#/'; + return normTarget === 'tot' ? '#/' : `#/${targetPrefix}`; } if (member) { return `#/${targetPrefix}${domain}.${member}`; diff --git a/src/style.css b/src/style.css index 597c45a3e..29d258134 100644 --- a/src/style.css +++ b/src/style.css @@ -66,7 +66,7 @@ h3 { font-size: 19px; font-weight: 500; margin: 28px 0 12px 0; - color: #3f51b5; + color: #555555; } h4 { diff --git a/test/e2e.test.js b/test/e2e.test.js index cbe36ba60..de4aafcff 100644 --- a/test/e2e.test.js +++ b/test/e2e.test.js @@ -330,6 +330,39 @@ test('Chrome DevTools Protocol Viewer E2E Tests', async (t) => { assert.strictEqual(hasRuntimeEvaluate, true, 'Expected #Runtime_evaluate element to exist in DOM'); }); + await t.test('3b. Target selector user interaction: switching dropdown from tot to v8', async () => { + await cdp.send('Page.navigate', { url: `${baseUrl}/#/` }, sessionId); + await cdp.pollEvaluate('Boolean(window.app)', (v) => v === true, sessionId); + + // Select 'v8' in target-selector and dispatch change event + await cdp.evaluate(` + const selector = document.getElementById("target-selector"); + selector.value = "v8"; + selector.dispatchEvent(new Event("change")); + `, sessionId); + + // Assert that target-selector value is 'v8' + const finalTarget = await cdp.pollEvaluate( + 'document.getElementById("target-selector")?.value', + (val) => val === 'v8', + sessionId + ); + assert.strictEqual(finalTarget, 'v8', 'Target selector should stay "v8"'); + + // Assert that sidebar only contains v8 domains (Runtime exists, Page does not) + const hasPageDomain = await cdp.evaluate( + 'Boolean(document.querySelector("#domain-list [data-domain=\\"Page\\"]"))', + sessionId + ); + assert.strictEqual(hasPageDomain, false, 'Expected Page domain to NOT exist in V8 sidebar'); + + const hasRuntimeDomain = await cdp.evaluate( + 'Boolean(document.querySelector("#domain-list [data-domain=\\"Runtime\\"]"))', + sessionId + ); + assert.strictEqual(hasRuntimeDomain, true, 'Expected Runtime domain to exist in V8 sidebar'); + }); + await t.test('4. Search keyboard shortcut (/)', async () => { await cdp.send('Page.navigate', { url: `${baseUrl}/#/Page` }, sessionId); diff --git a/test/protocol-model.test.js b/test/protocol-model.test.js index 93aeb110d..935f95ec4 100644 --- a/test/protocol-model.test.js +++ b/test/protocol-model.test.js @@ -311,6 +311,22 @@ test('parseRoute: dynamic native subtests for all route formats', async (t) => { input: '#/v8/Runtime/', expected: { target: 'v8', domain: 'Runtime', member: null }, }, + { + input: '#/v8', + expected: { target: 'v8', domain: null, member: null }, + }, + { + input: '#/v8/', + expected: { target: 'v8', domain: null, member: null }, + }, + { + input: '#/stable', + expected: { target: 'stable', domain: null, member: null }, + }, + { + input: '#/stable/', + expected: { target: 'stable', domain: null, member: null }, + }, ]; for (const { input, expected } of cases) { @@ -338,6 +354,14 @@ test('formatRoute: canonical route formatting', () => { formatRoute({ target: 'stable', domain: 'Network', member: 'getCookies' }), '#/stable/Network.getCookies' ); + assert.equal( + formatRoute({ target: 'v8', domain: null, member: null }), + '#/v8/' + ); + assert.equal( + formatRoute({ target: 'stable', domain: null, member: null }), + '#/stable/' + ); assert.equal( formatRoute({ target: 'tot', domain: null, member: null }), '#/' From b783512d5b14a46502526150ecde51fc94251a39 Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 8 Sep 2026 12:44:43 -0700 Subject: [PATCH 16/51] style: refine vertical rhythm, compact toc pills, badge sizing, and chip contrast --- src/protocol_renderer.js | 20 ++- src/style.css | 363 ++++++++++++++++++++++++++++----------- 2 files changed, 271 insertions(+), 112 deletions(-) diff --git a/src/protocol_renderer.js b/src/protocol_renderer.js index cf3fa87d4..c550440d9 100644 --- a/src/protocol_renderer.js +++ b/src/protocol_renderer.js @@ -137,16 +137,20 @@ class ProtocolRenderer { let renderTypeEntry = (type, container) => ProtocolRenderer.renderTableOfContentsEntry(domain.domain, type.id, container); - if (domain.commands.length) - ProtocolRenderer.renderTableOfContentsSection("Methods", "method", domain.commands, renderEventOrMethodEntry, container, isDomainExp); - if (domain.events.length) - ProtocolRenderer.renderTableOfContentsSection("Events", "event", domain.events, renderEventOrMethodEntry, container, isDomainExp); - if (domain.types.length) - ProtocolRenderer.renderTableOfContentsSection("Types", "type", domain.types, renderTypeEntry, container, isDomainExp); + if (domain.commands.length || domain.events.length || domain.types.length) { + let toc = container.div('domain-toc'); + if (domain.commands.length) + ProtocolRenderer.renderTableOfContentsSection("Methods", "method", domain.commands, renderEventOrMethodEntry, toc, isDomainExp); + if (domain.events.length) + ProtocolRenderer.renderTableOfContentsSection("Events", "event", domain.events, renderEventOrMethodEntry, toc, isDomainExp); + if (domain.types.length) + ProtocolRenderer.renderTableOfContentsSection("Types", "type", domain.types, renderTypeEntry, toc, isDomainExp); + } } static renderTableOfContentsSection(sectionName, sectionType, entries, renderer, container, isDomainExp = false) { - let title = container.el('h4', 'toc-section-heading'); + let sectionWrapper = container.div('toc-section'); + let title = sectionWrapper.el('h4', 'toc-section-heading'); let badge = title.span(`entity-icon entity-icon-${sectionType}`); badge.textContent = sectionType; let link = title.a(`#${sectionName.toLowerCase()}`, `${sectionName} (${entries.length})`); @@ -160,7 +164,7 @@ class ProtocolRenderer { } }); - let section = container.div('toc-entries'); + let section = sectionWrapper.div('toc-entries'); for (let entry of entries) { let row = renderer(entry, section); ProtocolRenderer.applyMarks(entry, row, isDomainExp); diff --git a/src/style.css b/src/style.css index 29d258134..d037c38aa 100644 --- a/src/style.css +++ b/src/style.css @@ -8,19 +8,19 @@ --sidebar-width: 200px; --color-bg: #fafafa; --color-surface: #ffffff; - --color-text: #333333; - --color-text-secondary: #727272; - --color-border: rgba(0, 0, 0, 0.14); + --color-text: #202124; + --color-text-secondary: #5f6368; + --color-border: rgba(0, 0, 0, 0.12); --color-header: #3f51b5; --color-header-text: #ffffff; - --color-hover: rgba(225, 245, 254, 0.49); + --color-hover: rgba(225, 245, 254, 0.5); --color-active: rgb(225, 245, 254); --color-primary: #3f51b5; --color-link: hsl(232, 50%, 45%); - --elevation-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); + --elevation-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); --font-sans: 'Roboto', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; - --font-mono: Consolas, Menlo, Monaco, "Courier New", monospace; - --parameters-grid-template-columns: minmax(220px, max-content) 1fr; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --parameters-grid-template-columns: minmax(190px, max-content) 1fr; } * { @@ -37,52 +37,63 @@ body { margin: 0; overflow: hidden; -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } p { - margin: 0.5rem 0 1rem; + margin: 0 0 14px 0; font-size: 15px; - line-height: 1.6; + line-height: 1.65; + text-wrap: pretty; } h1, h2, h3, h4, h5 { color: var(--color-text); margin-top: 0; + line-height: 1.3; } h1 { font-size: 28px; font-weight: 400; - letter-spacing: -0.2px; + letter-spacing: -0.3px; + margin-bottom: 16px; + text-wrap: balance; } h2 { font-size: 24px; - font-weight: 400; - letter-spacing: -0.1px; + font-weight: 500; + letter-spacing: -0.2px; + margin-bottom: 12px; + display: flex; + align-items: center; + flex-wrap: wrap; } h3 { font-size: 19px; font-weight: 500; - margin: 28px 0 12px 0; - color: #555555; + margin: 32px 0 14px 0; + color: #3c4043; + letter-spacing: -0.1px; } h4 { - margin-top: 0; - padding-top: 1.33em; + margin: 0 0 8px 0; font-size: 15.5px; font-weight: 600; + line-height: 1.4; } h5 { - color: gray; - font-weight: 500; + color: var(--color-text-secondary); + font-weight: 600; text-transform: uppercase; - letter-spacing: 1px; - font-size: 11.5px; - margin: 1.4rem 0 0.5rem; + letter-spacing: 0.8px; + font-size: 11px; + margin: 18px 0 8px 0; + line-height: 1.3; } a { @@ -96,19 +107,23 @@ a:hover { code { font-family: var(--font-mono); - color: #8E24AA; - font-size: 15px; + color: #7b1fa2; + font-size: 0.92em; + background-color: rgba(0, 0, 0, 0.04); + padding: 1.5px 5px; + border-radius: 3px; } pre { - background-color: #f5f5f5; - border-radius: 2px; - padding: 12px 16px; + background-color: #f8f9fa; + border-radius: 4px; + padding: 14px 18px; font-family: var(--font-mono); font-size: 13px; - line-height: 1.45; + line-height: 1.55; overflow-x: auto; border: 1px solid var(--color-border); + margin: 16px 0; } pre code { @@ -116,6 +131,7 @@ pre code { padding: 0; border-radius: 0; color: inherit; + font-size: inherit; } figure.screenshot { @@ -311,27 +327,6 @@ figure.screenshot img { overflow-x: hidden; } -.sidebar-header { - padding: 8px 10px; - border-bottom: 1px solid var(--color-border); - background-color: var(--color-surface); -} - -.domain-filter { - width: 100%; - border: 1px solid rgba(0, 0, 0, 0.2); - border-radius: 2px; - padding: 4px 6px; - font-family: inherit; - font-size: 12px; - outline: none; - background-color: #ffffff; -} - -.domain-filter:focus { - border-color: #3f51b5; -} - .domain-list { flex: 1; overflow-y: auto; @@ -412,49 +407,99 @@ figure.screenshot img { outline: none; } +.domain { + padding: 24px 36px 40px 36px; + max-width: 1040px; + margin: 0 auto; +} + .box { - width: 98%; + width: 100%; max-width: 1000px; - border-radius: 2px; - box-shadow: var(--elevation-shadow); - margin: 16px auto 32px auto; + border-radius: 4px; + border: 1px solid rgba(0, 0, 0, 0.08); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04); + margin: 0 auto 28px auto; background-color: var(--color-surface); + overflow: hidden; } -.box-content, .method, .type { - padding: 16px 24px; +.box-content { + padding: 20px 28px; } -.domain { - padding: 20px 48px 20px 48px; +.method, .type { + padding: 20px 28px; +} + +.method:not(:last-child), .type:not(:last-child) { + border-bottom: 1px solid rgba(0, 0, 0, 0.08); +} + +.method h4, .type h4 { + font-size: 15.5px; + font-weight: 500; + margin: 0 0 10px 0; + padding: 0; + line-height: 1.4; + display: flex; + align-items: center; + flex-wrap: wrap; } .domain-padding { - height: 60vh; + height: 80px; display: flex; - font-size: 80px; + font-size: 20px; align-items: center; color: #3f51b5; - opacity: 0.15; + opacity: 0.12; justify-content: center; } /* Table of Contents */ +.domain-toc { + margin-top: 24px; + padding-top: 18px; + border-top: 1px solid var(--color-border); + display: flex; + flex-direction: column; + gap: 18px; +} + +.toc-section { + display: flex; + flex-direction: column; + gap: 8px; +} + .toc-section-heading { - margin-top: 20px; - margin-bottom: 8px; + margin: 0; + padding: 0; display: flex; align-items: center; - font-size: 14px; + gap: 8px; + font-size: 13px; + font-weight: 600; + line-height: 1.2; } .toc-section-heading .entity-icon { - margin-right: 8px; + margin-right: 0; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.5px; + text-transform: uppercase; + padding: 2px 6px; + border-radius: 3px; } .toc-section-link { color: var(--color-text); text-decoration: none; + font-size: 13.5px; + font-weight: 600; + letter-spacing: -0.1px; } .toc-section-link:hover { @@ -463,20 +508,65 @@ figure.screenshot img { } .toc-entries { - margin-bottom: 12px; + display: flex; + flex-wrap: wrap; + gap: 6px 8px; + margin: 0; + padding: 0; } .toc-link { - font-size: 14.5px; - line-height: 1.7; + display: inline-flex; + align-items: center; + padding: 3px 8px; + background-color: #f1f3f5; + border: 1px solid rgba(0, 0, 0, 0.06); + border-radius: 4px; + font-size: 12.5px; + line-height: 1.4; + transition: background-color 0.15s ease, border-color 0.15s ease; +} + +.toc-link a { + color: #374151; + text-decoration: none; + font-family: var(--font-mono); + font-size: 12.5px; +} + +.toc-link:hover { + background-color: #e8eaf6; + border-color: #c5cae9; } -#methods, #events, #types { +.toc-link:hover a { + color: #1a237e; + text-decoration: none; +} + +.toc-link span.experimental, +.toc-link span.deprecated { + font-size: 9px; + line-height: 1; + font-weight: 700; + padding: 1px 4px; + margin-left: 5px; + border-radius: 2px; + letter-spacing: 0.3px; +} + +.method, .type, #methods, #events, #types { scroll-margin-top: calc(var(--header-height) + 16px); } .method-domain { - color: #ababab; + color: #757575; + font-weight: 400; +} + +.method-name { + color: #202124; + font-weight: 600; } /* ========================================================================== @@ -516,10 +606,11 @@ figure.screenshot img { ========================================================================== */ .parameter-list { - padding: 0.5rem 5px; + padding: 4px 0 6px 0; display: grid; grid-template-columns: var(--parameters-grid-template-columns); - gap: 6px 12px; + gap: 8px 16px; + margin: 0; } .parameter { @@ -529,44 +620,54 @@ figure.screenshot img { .parameter-name { text-align: right; - color: black; + color: #202124; word-wrap: break-word; - padding: 5px 10px; + padding: 4px 10px 4px 0; font-weight: 500; - font-size: 14.5px; + font-size: 13.5px; + line-height: 1.5; } .parameter-value { overflow: hidden; - padding: 5px 10px; + padding: 4px 0; + display: flex; + flex-direction: column; + gap: 4px; } .parameter-type { - font-weight: bold; + font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #3f51b5; - font-size: 14.5px; + font-size: 13.5px; + line-height: 1.4; } .parameter-description { - font-size: 14.5px; - line-height: 1.5; + font-size: 14px; + line-height: 1.55; color: var(--color-text); } .optional::after { content: "optional"; - opacity: 0.6; - font-size: 70%; display: block; + font-size: 11px; + font-weight: 400; + color: var(--color-text-secondary); + font-family: var(--font-sans); + font-style: italic; + margin-top: 2px; } .title-link { - margin-left: 1ex; + margin-left: 8px; display: none; color: var(--color-text-secondary); + font-size: 13px; } h4:hover .title-link { @@ -574,23 +675,27 @@ h4:hover .title-link { user-select: none; } +/* Badges & Entity Icons */ span.experimental, span.deprecated { - font-size: 70%; + font-size: 10px; + line-height: 12px; text-transform: uppercase; - background-color: #e57373; - padding: 2px 4px; + background-color: #d32f2f; + padding: 2px 5px; cursor: help; color: white; - vertical-align: baseline; - font-weight: normal; - font-family: inherit; - margin-left: 1ex; + vertical-align: middle; + font-weight: 600; + letter-spacing: 0.4px; + font-family: var(--font-sans); + margin-left: 8px; user-select: none; - border-radius: 2px; + border-radius: 3px; + display: inline-block; } span.deprecated { - background-color: #ffa03b; + background-color: #e65100; } /* Domain-level badging: if the whole domain is experimental or deprecated, @@ -613,35 +718,53 @@ span.deprecated { display: inline-block; } +/* Badge inside h2: strictly controlled proportions (no more giant 17px badge) */ +h2 span.experimental, +h2 span.deprecated, +.domain-title-heading span.experimental, +.domain-title-heading span.deprecated { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.5px; + vertical-align: middle; + line-height: normal; + padding: 2px 6px; + border-radius: 3px; + margin-left: 10px; +} + +/* Entity Icons - Solid high-contrast accessible chips (WCAG AA compliant) */ .entity-icon { - vertical-align: top; - display: inline-block; - padding: 2px 5px; + vertical-align: middle; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px 6px; font-size: 11px; - color: white; + color: #ffffff; border-radius: 3px; user-select: none; - font-weight: 500; + font-weight: 600; + letter-spacing: 0.3px; line-height: 14px; - margin-right: 6px; + margin-right: 8px; } +/* Solid darker amber with white text: passes WCAG AA (> 6.2:1 contrast) */ .entity-icon-method { - background-color: #ffa000b5; + background-color: #8c5000; } +/* Solid green with white text: passes WCAG AA (5.14:1 contrast) */ .entity-icon-event { - background-color: #2e7d32a8; + background-color: #2e7d32; } +/* Solid purple with white text: passes WCAG AA (6.7:1 contrast) */ .entity-icon-type { background-color: #7b1fa2; } -.method:not(:last-child), .type:not(:last-child) { - border-bottom: double #b6b6b6a6 3px; -} - /* ========================================================================== References ("Used by") ========================================================================== */ @@ -650,20 +773,26 @@ span.deprecated { list-style-type: none; padding-left: 0; margin: 6px 0; + display: flex; + flex-direction: column; + gap: 6px; } .references-list li { - margin-bottom: 4px; display: flex; align-items: center; + font-size: 13.5px; + line-height: 1.5; } .reference-icon { width: 55px; - display: inline-block; + display: inline-flex; + justify-content: flex-end; vertical-align: middle; text-align: right; margin-right: 8px; + flex-shrink: 0; } /* ========================================================================== @@ -796,6 +925,32 @@ span.deprecated { padding: 8px; } + .domain { + padding: 16px 12px 32px 12px; + } + + .box-content, .method, .type { + padding: 16px; + } + + .parameter-list { + grid-template-columns: 1fr; + gap: 4px; + } + + .parameter-name { + text-align: left; + padding: 4px 0 0 0; + } + + .parameter-value { + padding: 0 0 8px 0; + } + + .toc-entries { + gap: 6px; + } + #sresults { left: 0; } From f2107d0551026f1442c85520e1a9025154922e7d Mon Sep 17 00:00:00 2001 From: Paul Irish Date: Tue, 8 Sep 2026 12:45:13 -0700 Subject: [PATCH 17/51] nav: remove redundant docs toolbar link and sidebar domain filter --- src/index.html | 6 ------ src/main.js | 23 ----------------------- 2 files changed, 29 deletions(-) diff --git a/src/index.html b/src/index.html index 689524cfb..c9dcdc2ba 100644 --- a/src/index.html +++ b/src/index.html @@ -53,9 +53,6 @@
    - - - GitHub @@ -65,9 +62,6 @@

    t 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 literal 0 HcmV?d00001 diff --git a/src/favicons/apple-touch-icon.png b/src/favicons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c6b90ad0458e1279f5aa4e0122e4729395c37ef8 GIT binary patch literal 34571 zcmV(_K-9m9P)004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00006VoOIv02}}l0O6HB4m$t< z010qNS#tmYE+YT{E+YYWr9XB6000McNliru;{yx<9tHH9r-J|hER%XvSaechcOYZJ>NO-e!c(o`1;BH ze%*Y2;rSl;9-d$1{d{+w*VE4%N_@ROe!p(Ye17otb)wfVIPv{2=*s%}bzFU&DEQ}f z_xiVK_H)ZWkMsKL=jSn=bN})0SQw0zI3Hu=cW}Y-wdyK?I|wawz6ZX#;JjWb{Os@R zKIFfTo%#Iu$KZW_`eW>Df4v@l4~4P$bwK`Oj9#z1^vCP(6KaIM@5udJ`PVOc^TmGu z_ceCsta{FVp03BtNM-YSsQ07XpEz(Zl;OTD^QZ7P@pHL<8h7eL zo33)@_r zzT)O5euzS5Fx_FUJYcuuImJlsC$_?!=fH8vMq{7UaAmztzz}im#LW=b(b?=ma@pD9 zeQ}O)tjtg6&V7h}Fas_nznnSDO$eFEsb1ISJ;mK)ee%!CKnoL*LMBzJ5{1c%F%dr{ zR${29kfKQ`_4ll~KW6Sf&zgJ9nr1BCnMY`~R3Zqp5q%+&|~-FSEA9d-KlNlZ6^n8xFw631-%bw-2dVE<9^aBdwTM z?UUh~5gd$pk5RecXg+2DZu3Lvb^FoV@}|VT7zjH`u(^#4<8a#z@M7=X6)}bJ}B1z_q62 zV(-k=1`Jb%X-`UhuEBJN!yGXRJD;sLZ6@iAH1#uQTsB3AR>;TCLJXaf<2rdX{oW9= z-S)6o3Fq8v5`v&eEIQhz#e;F`@;P<;^VsEG56xtrgwbwi>n-nJ$Fhe1c{7*t?Yz&N zb7XuQ5l7ZRE!=4c=6OFV;~d^U+c>%&kwMV<%5)Bfh=5r;@^JSd$)C@4bQSodMRS_& zDG-soUBBhf#^7=jDUQ+RAe_&1K5)i<;;#2(B+eO==X|b}mT7(@!qC;axZuy#&g*P7 zjjd2z9vpP_WIOchUsIsBnD3ErO2+3FzlQDo*t_<_%YC<4K{d~eAWu8fWu174~-{ffsf?axf&Fj}4z**l@r4h_Kd>>5c>FDR| z<`pyXuZQbieurZYh?LNmn~jHa+)(I@=IEy60V7S<8)yN}lsUNRP;>O9PEFB{yj8i` z?26tGfS`<=d&_0)9a0?w3^PuLNAkHfM{08lPT)wW&M&y_*`ypaj9?SvzZbAKMlWvV zF-a=$wG)7b+nZAW&;NVOP5LtEfM_mD|DJinuibA2z&_v}56(k`UhGK=6hva~-OTWj zNhK2E&dt~E2k&OgZl;g>mBBm_m9dAyJ@K!V`CH->=gQU-&Bzvb(ivJ6cW}wrCA;-n zwgDv2$@=|DNP;z?9A$A|abzZCfjJHqX+S#wIrrKXT%O$9VI+4Zju(*`7oqHdaFCF3 zVx&Ry$&|)18*vWp0DHp#jAxF@-RdeRJR&s?q{PXb04MeTI{$v`OZin`iw~6!w;JbCYiUbdlv$6=a@Vf85`)Ipr?Ue7`N(^ zkhJ#R3X~F7Z;`1zVi)$IM^K}N;{tYx(m>j)^FEk)ZAb;vFdv?|!mPP~#!BPy~d9|!sqF-Ov) z37tF|;LsBDw$DUy4Kl)^sd&m&d_U+HVvboTtU@MTB0}~A0DQc%lT(y;re?B=&|`$Sg9F4dJ(>F? z4acem8B2nddNq@8;+q8K4rrHmjvh#XB!fC?EqkuM7(>zwoz4(-*y2D;_L$vL;5W<- zaB9)fF!0#$QbsuBr7%}vfvIO^QCWz85(S>JYxdY2xfc449q@0+_rkoQteme22*ZSs zW&zTI=_+c}6CxS#&|ruUiE-4a;*bEBUM`zE3X@b{M7CW5y$!w|R0>`q5q8^98B5{`-vLg*T2_z+%zM!6kNyrSQO zKzWipwTsFd;E&z~DhA_Lw-6#|tokY)_gU+OhLSq;3FUq>F8<`}6(dl1WG^9-hPVxj z>V+y7cgWkRtV8x7=eaBXf!|QMDZjIvNJrL4<=~ey(w-+sm>x_3;ujMZ8gdeuz!0G~ zIIw_b;C6}&jUiFUEEAo`u4O8+O`;D=he&3?m>3`mC`Ye!;{6a?9{YzJSBd}w0DhVi zdPNsb@EU+E!4!X5t0*NPiLZne^AGux5N&$9NvRAXp60KG+Jhr_|8=GHGE9x!!9ufRU~&hsPQFq`z)~ z{y0hBpxwYoWG>2-SCDX8$%=Qb7KLY27I67&np8f4RXl1D20BQh(qQ_F#smS*)!hVV zd{)R^W)s@v1(2_d{+e^sbjl!q0+yvMlnkSSeCXi}3*nwnXW)8ogsBHR1c`c_H(zm! zfT{sN-mk#ZTs&nG7!&9RlHj0o9- zXl={plkx`72K?f2{QxLv5EZUVnRuMRe>xrp=#ffvp22AaF(IOrwM0%IF~ep0-Op3O0TaZyp*gu)a8Da zZSfS08cLyqOGFza32`}@@8QXibwIB$WRh*c(xKit7!Gb^@=D#}jtEz>bB=)>BptgV{-GdfwZk40$3bp3X9#16m*6Hn+Csn}N{S-SRaH?C5h9i+weS<+iuEhx ze+)cS5)FtYO^-X%EW#zy^c^?uA=_61#$|CTqaiaOw7|;3j&E7tbfSf=M zqp7M&i~;9x%WhsSBqfoG&2Eq^#38b5Vn)+~X~BV_SmD89k@)it{t*k~v${x_97H!% z;+sg4@s7-t!=-Xv$*C;2Ym&5HiNxaGj1RK&G*e?8{U9N+c?V*EWilI zp;8V8L?A=11T@u2^5+XOLpheH!?VX^Si;7Raz^ktZlZGJ5Ikd{mk@Ikp#(u!^Apbij}fmS^x%8 z{FH>p!tpcnNH+q@_Yu&BAf$%P2258}VX!;GgA_R2ejW)%>gZZdS2G%U1Oy4@dY^`Z zSKZ@^-_oYREZm_aA@PwyBw_~mLYo!l6~Ta&m0^$}BrD{;54bIrAi0A9sraPCiI^5< zQYL{(1BO>?-1^T9fx9?o1VXE_4JFF#518Hw=ca_h@x_oEOv9XMA^Oqgs5(H<@gCxi ztmm33lZi-RPH+KDWbif3F^XdhQG`YWNC-=hh(lNR;V0G~z7FvgeFzDa6)rBKn(~$~ z0r^Q`V2mt5t{U7F8Nt{fj`JMo5~L;|OG1F;z$XLRt}8$<@;p1D43EHHG|K(tT|aeiGc}Ot_49C(HAxggd(66%QVbj^QBQ6R#q&V9vOfj5!064i?XO zdy!@@L?xKcg?JDGz91Y+-P5VY<_px!cxK@d@vf!NFrYB z3Y;TcI$B_VX-0K76c2*R)`8YLQYE{%B1sqE@W7oy(TL`u$~iHerJY6sp!k=UIt+yr zA%t=Ys`J~ZpS2nVW$IRty>VXeGUZZQP=zc^iS9nq026x*XcPx*o2hh6T9mCS02U<2 zdbH|BFEQb~XS?fhQ!V32A^4=js=Oj?lqe z^2D6A@Ggu~#f0J4rAlC>5t}rpA;JKo0j&mn6!b?nbbbQa2`I83CfrLgHOm|}9qSry z=1B12ii2c=^t%Dz8${_;K@!{`)k$`s17Q*mG$Lu@;LZsszy(FTgu#K??f_J1hgq0r z@uZkn&Q^gwObiyBVFP6}7(K_+As~mW!Et4HsNs(x(uK|tvcWkRBF2bo;o)1do$w(O zGymEZ3B@ts3J*zYH!d*~;yie)1``l1PXTu(AV`AX7FjMXUGnXgG&!ft{@k>P-ru5D zL)$P*_Rc6Y++6COiBUN!iQmB^Mgn}7MYwNgD7Z37v*V57rJ0cD;FkjHbxZZ_&r1d) zt%C;}pOZT%r#Ir1Sy52{Y(%Hxyk$PaJxzA1SfT;Yx?oE#;DgE6EyP(fsyCoe3zoYe zBrpqkR9P&U2*fg$yFuL5AyQ!4LiEGZpeY9uJFbh9;5VPzG=K5f_{13|r-Jkr$hK9ofI@LRj={#z@^t)q zRwP8(j`vqsmg9#O-oTeuiAaGSaj${HTBGf0Agqw4ETwR@n*P*H=yvT zUu(G#r|r22c09m7t@3>8U4VvfNeQ~ZSuF$h?b$j$Ijih;xfBlqoy6%H*#;1ivVMYU zU>tD*fOvUqZvDzvD1vx1&Xb6fSYY_TV-MR731(6>K#VwY*Bt~4Cv27Z3lj$D1g9OwyFq6>T_9YAsL1kH={fwHxJk(Nka$`uhp7z7!7qOp-i1P4Tb z_ac-k*1a+z#OrC|O9fi$m$HGpt$IlqAY1@x132#q zSV{PNSF;WwOaM<6#TsZ`v_ibRQNbyqbvrYpLNzL+YGL@Mhy*X5k(7#Aynj%j8NK&7 zAkE&CU+GcN$sWuF`tChde|!?9_qyq zgO`NP1uhZql-C9XD=7fyOy*XlF~%om^^OdZJE&nmf)bj-OZ7bmj;ys5?@EHoK>Mg@ zS0MHczBuU5x?!Y27)sr{mJ50T&M=RFUfsbLr=obhAvigQJwzt66v_TA`+)rqTQ^|9 z8PzN&>|5G?cuQ-7>lOeS2RG}Yf8; zki>QvQt~_9Vgrau8sWxGa9Bks%u027h0)bw_G7+hb0e@hU+T~7=9UaRe_2uC4L9$`;kFRHkQh08C1*% z=m$s^7qJM&F8pu>rI;CH;0<|B7)mb>m0wlIf{z#Dxv@&Shpz=hSKu;LH$YC4Syf&iU#F!TeIi0H=k| zEl@Br15*P4C{IGu@@zN!Zro)huvD>&DiO=bV2Ekav`QM!o2Ku5)n-uy!_LBpkd35X zvQV`%{Vuv{mS&D(?|!}1ISj8wJ8T@^R~$hqTF0JQQ#S~McGto}F~3V<`L+53f|lnk zOK(AOs5*q*PjaQ|8d&A=0*td&2ac(eZO{zE4^Adj50?Y96aF1#5K~jkzV?%(bx{KH(I{N zWhX5t46?JRIVAdVi4jjzFeRV|kU8YIiR8faVuyp%XfqjF&Q2O5g4rS89kQm7m8zNK zsb#@5wP=U!1o)DwST}UE;&zf(UFdpd=u*p12HRIp127pS@8aj-2=|CG(;8JvDkU8JE+jE9o9+kG3MI;MuS+D7(gx*qFLhb+%MdZq*J`cRX zu__dM`f6fF2n~}fh~v?ZS?qpVt*Z3G>^tRlT@6NkF}#DjmA(P*oMd7IQR=ztOS#Uw z1YIZ5vV)dUBhiB|2dZPzl8jo~9Op3K7YzD8_~pmqwVp+)3CAX;H{nG|5C9A08P*q( z*+gblwOcJM(_xWeJFU@>pb|ig5kw3RS+Pb8w`%V!_6gMp7C~8CC}qKcayyBNj5-Ie zn}##Ucr`daGuW=81~B_@ zCrgEO*vs2g^T;PSUw{&=r&swe@ey!Yh{LKD34D@TD1gw6;=)eXVweeg8@U>j3-1tm zOGTnlk#-35s^}GXavVt=)iD%r*9Iww2Zf6?DH&4(9V(SoKeI8Yxj7VfO9sE6YfH^c zZe^0ynkw0ADF%|iA+(<9D-nvu$3y{Gz}Gsz7+wE0r#}Nnjt9nauM9+Y1^J5VJq{BW zi#c`)5ru>J0RZ*a<|6n)ZgEEZ_q+%&OXV=Ht_=q+ZRjW+d^EZSKJ%a%DUVSAnEnAc z?|Xq_K?Z_L#UHFKp4=g}3_7c4SqHogF@dh*cn|3&goA^VJ0V}-)0^;;UgK0L^Q6A2 zqTcUW2n(|#&RM)Wr}tVgm`wykHYkZmzz)ye_L|ZNKORK79(*d9OQcNp!E&*hh$Do{ zkb7ROVgrx@sHNaKx*vlv0y$6SkP3sujp|dwLpyx_J|mC*9&Bg8_!1oskH! z6pL;1M_BE@5c_i`(gVGs|#vCJ1w! z9EpXcdREh8Qo&daaX?PE*a{GmCS$Y7Q7vPuW#qQ1I04Uf zbyxfXxLVj2)W~7)rzf$BEQvmZfMgjjmWo|R5;*%YwrIjL_Vu+|8U1UQ)*vNO7ZqPF z07f(eA0{KdBc2b{a?4X)YhBn{xA0mEl8@^0a14>=sj91zM95l0QB$RsNlRX78KxT1 zbVfi6V9J5>8pw^^Z(X2^wzxr*WRy@|>K^w=KFO&f0DmDTlq=@8kTKO9qtDsfN>nI%BosP13n_*)%rNe=FC>Fq*|o?geseQNFcSU5;4pi2Gi?s$oFm575KjrY13a% z^;lfQknaX8Dq71T2D@sGs^rC(5aUHIvt2Z6ssiUV5l**B6-|!|Az`Dxi~opvW7kuC zg+yv)ZtxF2!K|+Ug$ShliDFciRj>bQ7)~`23FMNh^y=`aqHGnY^x(~*o%p!||AZt) z*Qw`~r@16OBneF~{5_L-_k{OB{xAm+Yr`mtPIYWel7;qKQ6|7|(hMJtObyfAasiy%cJi$b8 zCm{%30h_b{$Bm5Pyj8P`BbHTWC~hI}{7k?(?lVuQA_3Z_xn>#u2B{ZR(6YjQ;)67j zFvX#xX_ks5otwyy{gM`HWERhSdrP@hgDA`lq`f7gE@zM5>qX)4K1_{nL(Ygtfu+5~ z7-`~)Q+=hX>18y7y6plyY>`$6khMO*0B#gD@~#EfA32lQVScSPAuw)ro8Ww|!(7%0 z_yg6e64B@rlp4=O>fI$8)Fqd-aaeFgqI9b_LE@2dSEV0pA6>KB{6a(WbtgB~>j{ci zwB*67dq@`%pIUY$V9%;rACUM+7u?pk%Rw#y*jpvZFfJYCjv!TYM@1a>Io4*1q0fGkk zxmzrwF9dbbiky07Q%XZ!&PR)gZf|0ckz$BW03kht$4g4-CgGg58A7cZgg`)C3GU8~ zwOWa}!yN`KJWdrPFzJYTNY0SMy$jUst*c2}QPw)< zkY0=$-swO%oFYd2Tg69VUyfVa6~t7dDAZn&-Lrk*_RYY*;es147PBr?I?z36hZgcy zt2=5rE$X^rei)>Jl!{suBSh~`CvLSc4`n^g31~b{gc8@*kdmdIc|&2ac`U6L0RYgg zll))TG$8(z5>ioG!s1X3~T6?}uQK>YsR{Q}^N;#}Eg?Sk6aCmf49E2StcvZxV$ zW~v(o^@u1_`V)8qwT>Z0d?DJka-rR>@7G_Q$0+JTme1c)~4b)+A!5QaU5CIhB*AMNDO&C6bW zKeP&0VzTc?9$*Ak**P9(Xpm@Hdam04YabC9)=<;X3EJ|*#k6?f)oUr1M}Mgw zHKP#lT^4ntuBoM7+`Y9btq`_P@5r`HQb0zaMQ@umRy5sGtwpnRZJJfF-1h9(&y|6%GE}z!3$0sUIId| zs0%lC+q7!{A9|NsLcsWy+bl%p+O5=5iw&&$41j6LxuqUDG}uD9aOS# z*g3Ts`@6G>A+&lTpGn~yR8O5)I7UkqzrL}8+?OG(4|*O~5@Ap3U8aT!UzjB1ue`aBWCG?Dr_unyTeT?$fH|MidUT&%PaOs004zB1D7Q0GL#H zFm3P(Z@<0n&K7MeHZPP|a{*_yfyow;$la?yUxh1eZYDIOuc~sI@gXTDAa_g0$#*r8 z6r&9R14hCDs2DWVv!92yII7yp@c0VSx_GBb7~{F=?=o%LF$1QJd0Ktpne(#59qnjwr@Xy=8UZ#vyyVOwj%K%!x5j zC;rr`IwpY2TWvJls#*eVmrN~#ip*`X*}=H2%8a&dNyP7@$i!u7vn0HIA*{;aM=wk_h`?R@7^ewfQ8vKuTzVAs_P;j_*E1trapQ*VY7SNuZ0OoE5cmP0K0#xpqwmo}JA?_$rD2Yy&iZ?gL!! zU4YMqR;M;Agjg>6w1B&;o^uG8A3L?{MU65-zoUN0u3eIhFX*4nB6w5`E9yRh6j1;U zmUe0@hGkWoCHx3WVpL{^NFvEvA^X)nt9NSQ{~j1n2nZ^^Z*8u`h~9<)?MlF@it1D@ z-Qzgb@w$^)-UgxGGk}8;i&}vQcG=uf2y-Vh!>Ljo6`yI+YJ(euxc#gxgEfzX>ITjc;XI zqbz7QfOAh3G}Trspe|#W)ZDK-nHkTFK)oBkaQ5Ru0J=+^oVQl)QEc*Zyh|H>2Z5?> zyZ7S_3c^hC9Ep4!=-|P`sqI{MpG=?9a%UbqmFm3NOx3)%Rrdh&u5RMQd2>-aN>IfG zVKI-_C*;fmz-q8pQ8iP&@%jj!oljU`_Ii3~Gm@rgudCWVcJ^x1(`L6wk|iZjJgRLV zktGhT^awb}dAHX727?{;?E>^wYhex2SL8K;T!%^ol01{s;cbyjToDrw{t$P}z0&%}p830@ zn4iY)MYA-J$eLiEK&Q`!V!l?bi`W1$fT!9#xHDn=0b*8iNGm)`K)9EwR>WDRqV7Ww()ivYbh z1s86E(C=+nBp-pXkoT&5I+gr;oK01c;LqGP}}6oT5slL=~lDccfaK%H)Vgj+ekS{g1aw>eOs4iq6NljzI3ZeP&QW;ii2s$B6x=W@9yPs2LcQ2bb{1B*(xxG=xdD zCUt`PoGPQVP(#RhQUYpKDjbyVcRSOD5H(p(b83V$DXvYlFVW z&y%!_H;k$NBLr>GCxab`Ad?Ts!Ud|T>9bl`M5aM2+v9kvir4`|apSx%?SzRZm|8a} zk{)Vx1e&>1R4JF%PBYNh-=#l&oKaAt7vQLkU?lk&5wadQ{sKg4&j{16kF(JLoy@%~ zwJIR^v_PwD=85eHUVGpD&%52%v8~ypI<#@xFN1g2XG7?tQ$7dlLMjMm{{xBUBJog~jkYpPl={F%V;lYT$72qeB|`jgdOnJ7jf z$Ym(Bt#j#Pe1?|DyedsOAOuAF>Iu&+#=fS!yB0|k(RM;KaD}!|Z4^EBN{VA;ZUh4; zY&(%Bu<8{+#xN1h&#I4*)OH!f%ETrwEH;5;AP6k(s@kqqm&-7`R*IFe+BA+tMgiXs zrvf*UKA8wq!LytH0hI;3B$aAS1^@t9QAtEWRCwC$y?2}(*LCOny;ap|5-U9s_<&{TqYWAVBmX;lRB$woR@ho~be;iB zgt50fMEPS-MiKW^+vBzF-Q2_X^87Jau986o$DHNo({S7dP@t8`+J=WwY{fEP{pJOy z_i`d(W}rCz@j(Et|89X9ryTN0{UKYcJJfC8D9r)Z11|$7ANsi%zxs7U$t9*0_t=V0 z8O`N|5;KY}6usz+ZcOj+9ChJq4~q>L+&mapwLY-Hb$S74fOx;bD}kF=C^h-((c!up ze+=3KFJC=?v+<0gj2FPMAnrgiju&^#P7^cFSxS99W~pafj6N0f7#y|940*RGt*%#w zFFL?61k9HMFHQg9*vaBi7F&wnH69-yD=;*XgF*>(X!29B@V&fNusaOKA)i;23Y5VV z=^^%L?8ZR918x!WS*o~WBZ{(*7!&bj3+Ag~zya5VLZ;>>ros{1u4j}r>mjIvAvrd7 zDy~*d?^mn(liZzm%paGO`Ep<0I++>rZdF>{U}^ruD2^+D+rCl4z%0wK;XK!#<{0%~ zCi1Kr$zbJ6jTbuVxO+CiE;;nTbVEL?a0_(2)%Ju#8|a&6aEnmLs%VJq(R!HBvHjBw zo(uU*^)Auvx^_FJZs%qUSQZQ%$Fqz4$n7C_#C z0i%{!p8Bt2qiV2dgB|CrD|GMCdsIgX?37`wfEpXIRvFEI%-U}MhyU6I!1>fy3@L|L zrl|hY=`VQiCGJ)Na-9(R*|@sc*>OIFKGyqz%eed~DFN9{Z&it*^UZaj|S; z$hdqoo#Wo|9H5|7fZT*4HE3`W=cZ}rUo-t{eIBVHgA)@l8x5X;6XUACkXOI+t@r6A z!=8l|PpV|G$j^^unWp`PQVQGHySd+QQLmI#_I12 z;@)h58I4Sl0jJI)oH~ctbKjGx-=i}{&A=SyNv?)$p7kSHE_U(-Gri=g<`@mc4z8yxy&hPKG62z`QnE`#19j}`f)Y@@h{;azDGi-!RTOC zfTxLLxY{X|R9`#Kr}57!`Qk^PFMbq5?&SCAJ?a(p4H@i+U=GOBaB589`64*oFxW7s&Qc%v^B%oN-B4=KV8^OW zbDfX^iRI|{L{$%Jw|I6J<7~c1;kfVM_i$9Ar{&}7%8Rf3|CJd^@8XuGW`0vl0hlvg zf;phB01XX5J)tP&>G@MRmpFb9wyCLvpgK0hZHU?Odw&9wG3bw10&0@|pk2{9K){_T zK&}MYf&jhbLCLLv-cs^Yox?!y6_vp}swSP_&^4f;TBc477%=}bOnSjfWQ@dsg8(cQ zhoKbAnFjNx!;Ar#JqUBB!Em2^pNt8(la=Hjlu}h(TN}uipy$OtmRuv?!nIUz1?0!Z;oVcP=LGCL3j0pN@o~r& z0Y5(2`UzcU@J(|;Y(|fE;zkijGuPs1vfx(N{P)f-(e7wtAf6E8_+&uctJI(%1*;TN$N;AI z!xaA*{^ABG)Age#6@AYBk{7}}UxI+g-}_YFzp?hpS`;$R zm!}o}Ppk2oG+Lrg6&$7f6r#{tX#=OzqAYGdBx2Ewepo&i)-4jzXz@&WIakkzk8Ofi z_Q50D;pM%uyPPYMNINk1f*hCvSv6$ROGS+R2vg6-g5;6OMB7zq7$rL z2zOrw5ATG>w<`{%$N@PRdO=cS6d)8yYz zOu+NI;fc3JyvUaX9D(`m9P;5rp0B0A@+~m$o`(IW;K$ox{bJdX)-8fd7sAE!6t}E^ ze|TBIKbiyk41l_msJY3kLqJmtXlMxPQWKfF6Ygvp{OxI&o)RE`WWCspEhXl)C*Ow0 z-h#a+#42G~jqS))L!YFPKfDGl=Mv!_QFefUbB|@=k!|qeZdkV%?p#~p))&o%m-oTn zy#O!m0oQB44&72;5SwuZICDT#4`>ij7r{(42lZrtqm9^hH(dxnbA?zf;*OZHUfKsg zd>!67Du7pN>Ky;dlt!f~%Lo&tDCfE)X$d_Xz>=v+sy~c4)dB=lSPZ zsxhd~H83`tcQ?l=-#a;;hwr@tFYSd7uZKG?6|d;;-X$W**PrvbwblK4l!Nno>;a5Q zXB5ytUFM7FBB06BFqnjoZiJt@TY*Dd z3h<#3_>0HkmHqH@?-SYjo{eJu`-{hg8R!W9iCUU$JaaF|UUc*4KYylgsuXyxgjA*m zCC)*+M5=)4X>kev_HDvt1`=XF{+nln`;27ep#?iKv<=Zb6FwY*8z@YoKz=N-gxDgJ> zQC%)-D^cWZiW}kB5fkCKPRLT?O?~2R*n1K_eFI##67IT8;#>asA#px619jg}Z^VpV zt?wBXIT{XHfp)?31k_%Q_@Rvi%YX7J_~q*)-16O1@Rv`(kGDytM{RUj_wSoXSWBHt!P=WW`*u7Q%8^w~*pA>HKzaEC?cKN4H+JSTK~pYrMBPeMjrfr^iXlh9eCJxRr#|}*{Pk0Cbgc0f*bzq0vW@Os z%9RY0rG$}u?-g0+&)zE0e4qP(?3DMv28n^QZ_Q{%QwHd+9++>ZP8-F-^9;Xyh2n;l zl9c+;Hu&cAqQo>+QaX@HI~m^D1mD@Xbr8n-+%9p-eC8Iocpm)rtuQqHFfl^W7ZrqMN^0|55|$RJ=&#NZdNmBlhf zIP=$@12?9LCwjvYrb^T(uS0lEf`A|YV}-(0tJdyZWOX4L7&7cW34in;ymdq@O}}*u z+;s`%z1`0|tW){Ih7L0ase`E)g>p0aAEKBu&7aN^Qy#y`zCZe)O4-&IkEQS1Jy2K^;IE&C zBct_iyCUG!B7Qi&Ox;zt*B0l5Wm+d`=$~fjpRRFYP|QPm)FLyg$Y%u9;RZJ4T67Tu zkgAR~0`_8Fk)2SaM-`cIh37${&oDS_%DH(}W9?+YBOaC-@I8QIjbk$`1yAe{kuY0? z_gyGn+*JJRi?3%C&|&>Y8yHIZfkyKrHsfp0!rO;iZ(VMo0%|*MT0X0WxM~sL9|Z5m z`wRoanq(iiCCH3eWX2W6oboyBBo?3zRRP?Ed|Htn5pVU+omL zzyeSzDst)OpzZ`fPF%!_SORisMP^iqjNW1#&F0(5zju7B5)mh#QB0h+C>5maTK|xd z%nZ%x)qqT5qbINua-=c zeJA1DFG`(czv?VHbd6@VZiOgIxwPLLG;S)HCC%<1`bT5L+}1Z_#3YyzxwQ=Bvk<)0 zQ5KC4?D5kUS|cJ-fBhArfF>1~=EgdAXWNz(>-pNV;wIm?6h40WnGgO7p82E!Z5n)9 z9qU+d?;bBd}Pdd`=yR9|;rLB^yr}`iC`I!^DV1F+YVs?y*BCT&CCP zU{|ql5~4iXPINeKDxWL&8}bukQ81dwbwYaTpy?rbE~G~+N<}G98SgV~?;U`- zodVd76vCaq`Mea0oHYQycn!=PXpv>#WA}eRR&Y|(Pq$=@6#4#{E2PxGXx8w^n;wOl zmdxE`n*^LX8&VW&#S3G^o;)yJo?P0OL0gTt;o^26KX+3&XEMchK&&E}F^goMp>NPM z-TDCCKq9}(wdNuyfKldSTe8!B{aM+0H!P8?morUg)XH(&YDt$bd0;8UrSmn)lYR@z zbe}pEyljD@m<)kh`(Fl%U#XzTj!(Xq$JwGNO;X(=Oq{mLFK{@91S)psPQV<6EG6|l zp4uS*zju>lzD%{~QKhI(?X847EpE~eua~0UoA<%@UxS-2aacTD!Y!7%pA6NUs!IIu z6~%Z`#EkreqL5Jy&ug|o;rn#5MO6iFg|69X*DEW*rp*r7?)%{52kB3YDqK%7Fs#e< z?^2=IjKHQIhw^v|EGHKEvO&VAY4C+xRAeVA56>7wdPLzCRogJihIOC)IF4qE z96VHQjDEkdkPWu2$}t^{YRCng+||CU-@<*~6{j!>LV+FM>slr{uU?Mr29PgHq9Iy-o(2`5lc>WtrTv z3^V#+#XMNEKylGr7)n5~WEjgDj*l4*obZ^)L#}8@#FTW$(uTeSOz*RpHK;g$*ka~@ zVtT*Av82P*Yx_O+9`nc)G?r2|kEY@mHyzj)6Dfsli)?MjppcjKP=E;|Ox50})l#Vn zIBSE;&!}>@I_25!tjFg{rQj)M>{pMSW#Dzlo=%a{hJ8%wEGkg>)dy zDw##Hzp_KPC55siK07_8M2-l*F`?fL!8)i0>n_(xNDjzr|$c?rDy zjv}9L|GJo(pHU9>r=4~x3)DjjjNF#U&hw^AXyKDP1Q>xVWm{smd|@}-wK2gfdp%B! zy4ZyrDuJ+*i1!M}c6T#2e58HL#;@KWcIPK{h)JhbT-Qdn+G|%T5)Qn%UpiJA?SW-ulofpg zQ4)<-$}5U_*%F-uxUPhHYOUxSFlb#}%%+jD75v-sIq<1#;jVQsp7Hqm=Ux8s1(z** zJx-;;U`b&}%kLPgYOe&2A6BW|iZvj>W#wEblne(?Yc}t8*}Ti8;KEfaW8AzZMqk3> z;Aum)=-&r~hlT;IE8SQVg9h7%+=RHsV}7j@T~c@@h$Rimk>WSCI+UpacldXiqlF3v zt!#gms%F;m^Hf@~VTmxck|%LHf%4oGTis$%@rFJB#?ODgmtC*|(gr?w8QgM-6fgP4 z3j*9)Kr1C>UTq6*S#7cXpvTE^jfKH@F2=HD@gh)A%`~4@W!Wy46hLba$p-O3?P-PQ zHeO6m!(a+NvO#hjjpy;VPrH2Q1(!X?3`H+c+yY?LbFnZvaphM|DoX+lA6mJ`H!zkn zy!o!jb30wMf!o%`c;Ctx<2l3O5iO!c-Gh^plLN9-<;Jb@vkNLGd9H{Ho-0WNlKR!) zI%N{zD$B;Q1jwo?n7cW6l!A#IOz#&FZMa`b`8L8qPbdI%udiziOzVeVx>nxI7x%!A z-fX^&D5THz+J%bwGZb6)xc-AM;?uP(l-0ueVI(#qmNaC?tm;00AwZX_o@((Z)>b_= z$j)-n`S2Sz!P*6e2VQac%9Ad;j_PuaJ*agn%LTMAYX@A?^5x8ALgZ1`1C7Gqr))J5 zbAo?UN|7xXw!W)*b)UzAnKmEZ6lcbOV)t=f?nNldnSiSd<_W86jIoqhSv*&YKO3XU z>);j%S$|~A%Cf;{Cd#raAg+4O3e$$$X#?YV#g!Mr`O^i=TlP? zm&QEL?UHuF&15H~go{7;2K?eR4(l$6@#2o6fX8S|AqNRiO=N22K%(D}oltGPQsoLw zMjoa?NxKXmgg2~$pT7!@j%fbi{t`Qn$OXfVg~F!1z|)qD8O|G4%%5Q~ci3XVOq=08 zQ6yuwSUF0bA(Mv_6NZB)Jr0j(4xaKD$rwtmsp{Pw05nLn-FsZ~#}DRt|7wSyzb?+= z**0H(tiaymp5OdONkyXo%;9yYi!G3Ua0^h1E0TkX+=Pu$3KP!q?E%>q95^N2Za=h6 z3PIktrFptm*W!jxQv&Fi4cD!Zrj~CXk?_KH9hWQ%UVj(1A5nbd@&s=k^q9zcGHIhR z#T>*H%B~)=0aE>jQbDzh&8YpxEkF*51I|oHa_ZgdB!9uPhr?I=4nXA z6^m!WhNU)Zm)M*)O<`M#k+k91xZ&i4CQ~rPVhYzaSinG1v0#S9rVDIRaYepl*mpwn z${v?j_qiM$)p%M}y+FzbhU>u(Un{Zigyz%lPx3o=C;8ID1zz9pNd`|NFqa1&S_xkc zMp+aJih&sr(~3eaI*^;$^Bdu;=oy~ep}6^CafPp23_pCM(!Hyf)N?>|Rg4F>RPvpS7Er5r3d?rN3+KcQmSaedw3f5$ z?4^D{Q)U7NhYWXLthi+reCI`v2VU_gx<05vtROJo^(QSZTWoRjYKIHwC?;}-Z3jJm z^qR-s4z%hHS8Hg=33G zSS*RH`oy@$wgVmyY$-8!*y4(14mV!naNVjHFTUgQPcIkQd)$N?ZHg@J*l=XT@Fzbk z@tYrr@#&iqlpZSZ><%q2q4F&k%0ejtuoVNX6cnl3uG|86Nl5k?a_OjCIyxE^iqWj- zQfn7V@5Fi2E3HF1y<*F#zXcJj9Gy_imVlI%bEV48gKzkX5|sd#R-}zoy}WvbqCWwz z?Q_cp{=h_FvEr=#}GUY z&%pV^7Qb+PjE`RKaPTzz%@Z!)-|VvQxNu@IzxfJP$N~alw$nyo+`wEX$61z$D8?uz zvhda+&GS24PE8oDSry}!OJgVn`%Y*|9wDE&!cq!j!?q)u^QT$dzSd#aaX686u;Vd# z;Vs9v5GkC5uM{?EGvT_TP?ieOs_=KlU42iffooTYJAC_L*m1PS)(*uRJq5IVSkpg3p^>A+uEzRk%O1( z`irD#&Of~5@xW#Qb80?sS$YNUCru+ zHrHPqlTTBVmw!ElkSQ1p?X>H z+#G%xH~z?Qeqi7GN$;@rUdbLB`hp3`p4rMH(#KX_o_LsX?)L06okBZ zE9b%|-v>{;rTOPqMOOEUP{zYdo!ve|T@BavvqJFg&?P!zMEiv^75wJjPM zj^7(HDmQjWm7_541KG!v9o_ zrwZ(4d!0wWp2zrQoi70Ycxv$!JVsO z+`Yl(ThF_E^I4aItD4sgsdG4p10@K0Xl+cacNy2N!@-l9KYXCX?h~5dyd}oU*$!^a zbR8Qu@AkOwr4k=p3zy6foILHhbNC2}^)PjLp5eLGk3ZT&wv_Z*KRgPln6ziMI*5&n z!p(aoa%cl{ripx1bYbVw$yd=Sd6Eop-ZYyVE{T<|=On?+`!iJX(|9FJ15-zM5j+jc z&Xa(tAH1g7anxWtwOmDU`&yg3F0=T%=R6+Y?cpRUvTjofIJ2(+$-hy>wVQt$MqpOr zJ!~i_&0jxVWXBQs%xy82&9!lLWy}&Rpt%1Pk9SXa+_MouCzsvh!p+AJ0pRu*vG0wgr1mNMO@l8{#aSX=4;v zF(t(tLs4Q1jq!?@P?B6F{YgZCge~zI$Hoi~Z`I|PvLHm#({ROwil4gD;lWp39@y-X z%PLZX_}S#DY?5n4vWqo{bP7|w=v=^Er>@=NYbE-bK!!k@|XxWP=Ez9)x8UU9oTgESd@D4fWO@-7`R2N@Sv#4Tncy zG*f1tlNzuA9@z%2R@|{JMl4aO_*uW;S~#`Vb+&F#!=;Nv8TqGI45K-)RYfM(TCre; z;@@27@XCI}KfWwC@cdG~i2;Kh0<}_B-2jYfP&yz22244JspjJc8pRw#W?Z2RoK73Q z_+yvhev6;I#*tX((6wv}UfZvE?j6ltm%(tq>TIV}P%Plryq13m{%fdgs}99VU+fsq zOVY=J;oeqR>ItBYk;;>cX3F0OMnt0xPqf>L|CWN2X~|2wc9p}L#SX<>Xm$Lbl2M{S z8;n~cei+%>Ze0z#j~iauCvg*|e6GBR_g*1Rh;KgUkt>E2#Ud2ae&A22?nhux4z3Yo zaWvhL8=1@ScL-JgNe+PHDAMCrnbTSdb{^II?K35ASY>nda;uIDyB_@Ws~$7@;PMMP zuZ>|mdBI~Lv7@eXRS3w$|F~L#vQR6vE*?iLTp!GWxkuEh`f_>5;c=j>U$hwwQa$HX&-Bj`9^nar-ID zaP@KtUEF#=SE}pyG>}x>y}@GJA;SyrNF57Po2jZ)sFQ$z+8Bd2CZq(0%yu;eplfCC z@Jmp$BBc7ol9L&?(Aq!7@=N~vyFj4?|L}s#v_8d6m)QQbDuG9?Ry?vzGjBRvG_PKk zt}3`5`Y)J#;q8Pw2b3#G1RlWrVF_aEl^oqO4n1$WWI1OFa5~$Y!Bq`Z z!H$!Ltw$91TpnY_fNGz?Z47SFWoDmd!(!p`qaiPv^4;2N7bzCbP(1LeAz$Z7tNfu< zxad0A@rah3a^!vp@J%+_)Ib1cU-k`ze5!$ES&Ci9;qmPrx2&<5HK;0}_Sb9sLCwL_ zhO3t~_XnsZ;|Th_SW^&>skO9Q29cwu^-aEHSU|l2G%&MYwvLk%qG|U!BS9&#J^%A- zFs>>)X4G$&lT zr;98-08!{3`7e|%iT(^O2jbYq)$w4sKnibK}|HR8O9 z+y0<{v8*Ury~J~nO=A4?rbpj`TuJf44UP}ciY-~J8>W0-UkWaoXR-Z|WX)8OQfyqR z7|ThsB5^lcHL3g6W{W_sSAM2TZbHBqOIEH~Yfv`9PXhOSNNH5iH|TTbF-eQ6$lIYf z4^#6AvQ9jBW~XN1OvU2a7D1q(QX+C698sJ~!hYp`Rd6C06c@6i;pQqBj6Y#4nA(kj#LDl#NUx)WCx4CG(jjL7YB@BTNn3b{^ zN+{krPL<4Uki}fHK(YOhc(5wbc#UmuMh9^gRyk+Tt?DGuU=E>c8muxHC4a>tIiRo| zB?_RCERu2>RN3=bEpyktGjAV}*q2Ke`AVa|4wjPSgWbmsOQtCjR>&F>!DFrcu3PO& z4R^|&6{6IR<>cA{cA6dE3H7AlxnqkR8Uz@Vb$QpB^o$t!O`xb)qTua6rMUMBo4!Ql z_^^9g8^yct%Zsw>`M^Kt< z3R0!=CE3jZ`jg#n0<}=JST5RweM?XTB*P z%$ae^=lnGjE=S!_KDYj1is<5M#f~F}B{QKv5h$;fm+tV0A!bWyhK7%(*7O#(=Np5v znk$A;9(9$^0Lo^^@x1&!Jq59D+SN6no`@Wh*(`w06{C9uJMkXYRt>c+*melE>{onb zQ{Y^ve(IK`m_5zn*cep`pIlEdZ@QQmN3uZyl?JHs0TniPH`X;2rJ_YHZI>0rYK~q3 zX>vgN9%d$dJ98JFin0iz1k?vkOL?)GgCe4oUpXb6UW=|Y^D*@os(U8k?~GA(k#}8z zyXH$z3zr75LpxTqm!lo^MC9m?3x_VbzT(xy<=yD-$W#Mpg;eW&@O9`*oeSHZ8v(n_CEEk9R8(Zsmfsu{5u$^aL0KgG4UVJ8j@uY* zp{#bsvtYJ0N@5_BBiA@K2CjkghN@#X6flyJOqdz{oj==V?K*<*yCs9{vLGFVV-dS} zwYz^$M2=v>nvktC^wIvuGnL;vvZnaYvDRayl*q=9Zim~}SS*U{qqL6U9w7!*9R9VQBjyEocm#tQ-8wqpR6QhB zKTTCpv}(r`%-kqdHDBrZVv!sKC#gvGOG(?zm|r=+*_xJDvc_Ck{xlYgN?LZuM69wf zfvBU6qUcE`QI&7H0f6hCnW+v!3eUf4w{3@!@W(X9MJHHp^g((~#`< zw`3rZkaYOA9br2mBTd~C$@+;Xt;lM_BRdp7v(aM1GMm?rxL6h>ZHY&SB@BJjAQmd# zVj&`l6O(GCb_*GWJ#e3>->n^-Y|6P2atA2euJksebGdkHpsNTFG_jFB(~ z`eO!5DUwO(pRS16GA?5bj$d>=8JD@%)|3x3rRw=gQAf+FOsg4(ZZsRyB;*uCT(tb; zivA=FO*80xbPsc*%Q}|1*l|mfWAmN{?WzhF)nbrWy#zF(Wou0(SSOg35gh<6xR)# z@xo`ciOe$f-m=AO2vz&|gFjk&T!t!t%t zx-y26E1d=0Vl+TE8;6Q1oTfD`B9f~mAUxPkN+rK`To*!qGg3-n$7LIDmO5A{!1uPm zZ{KEd`68Q#Ue{PjzbK*yg^a>DmBqJ|uPACLh4|MdR3#7#B=EQ-KG*jh0nZ2YRF(F z8ub!y{q_gN)T_^^*b);lk&&z7Xnd5~ws=MJrRwjLKaU1Ro$?$do2T&u2)_i)9$Hhw zP>m&~9NW?cGfla(|G%CH2A*FU)>3}3c8@qSgcJ|mezMv~rKbLb;*PaqzM9BO(VN=Z zdfG?@%$WnC9GaT@wHs68#T6wi8EQAIbtS>J`7O* zCbht-+HRXnMoBG@hquA4t1TAIR2&;K%p9on$}5|rjD#@GndY0bA}(r`rr&@9&fKi2 zrCU(sCM-Ykw)H)uluiqdn>!>g{IT(R&<^*>OM7ynn)|k+z_Zz@BPGh1m69=mBySWO zRKM!xx|8S7N*ohGeYb&Rcd^jzyoTjq_ja>VVwWDxDehWtb1Ew_8;Mx`b%Xj4vj-!j zj6qX#Q$Wc;%ouBtZLLJd$d);Eot}z5u*hCR-x{H;_9jM4$%DQBRoJjpF>hFr zj7iz4N`Sje{NRWvB+KWt26V%UsWp@wFcO!ixEScp4C8Vs3WTfuL>ID^fu67aIfy>Z>Gn>Z8 z2lo$yIuJiPl_j-?IYUw;bL&CM#b*MF*;3Hu=!7BDpp(C0!kyEvdgc}d{i$UR3rn5OWzi^L4j-o3~e?T~N+^(Now>9(1=LdBu ztZ3J>Eh$I##11)r^?VpiR^Q7~Qtnq9*toPJRz%(M18YWRJS1k+$dGO4*o0F@A#Fql zxNfC@`RG^;pfy}LTdW-04>z|;)~$80hOug=8EwvqN(QNeIFJxy!qKtb1VHIt02<^i z3v=kEakmi`vDrrlo+YcZ2ma_ghW)2u@hrITyy_q&OUYtx*#|eSk}WWbi7F5?%A7e2 z<|fCtqX5+XgNAq_!bwKt&eaPt$dglWcnltRLw1cTFRX7~Kx^ zIeg)m?xyn#Tua?_EhKTKnkAPA$5tbS|+lx7qTT=N&xZF9_fNLd#FW?2&(PO&A=Sduh6g#Ua3IH z`O;jumco*7=>jRhldG|A7z3Lw5F7goyP_>sRoxHYZr`>_afe_AQB>zmm*0<$RdNfv zr7AJ9`^!{F`5vm{+fXiw3MX!{L?G4b6mA7m~5UL5Ud; zYWbNKV+#KsOBlRTrFVGale$x4lQCE{L;l`-vOb$A>N+-}o&kCy5BvS{PV2{D+zIHy9@^o4 zDSP2*v7MVPm_qs3nuB`-Xk=0M`ZevA%n}7_YUF5z6EhvTm4flz|Eh55TP~?T#!}Mc z=(Ts@m#%|_Gb^AEz}(Df(#?a7GUzH$$NkV;4L4sb-1l#vgLjTqnxqbR~#546`e>hw|z;t8Y%cMoi7wewHLVU6zy+Is>s_* z8Q`k({T#L_Ah#qo(x%4vMjXWF(x)ci?_Ux*`R0r3k8>=si2TLV(qiKmE>{evD%?CW zkh^j5H@Sue*35<9yhd?w48HWF#6N@^?id4oal@ysR#=wBKfe~i1lhp(%Fe zL40|yaIjBa1sBb$cS!UP8nzxY{P9ELjQQQWq|$t;gAh~j?>UL>GyvpntKmO?2=*L@ zKY18(mLZ<37U{GRv*S&x6c4;6HM*@fX=QcXSwW&tdXhKGV(p|7Y4~LWc`=vI5m1k0 z;jP2f$9J+e&B%%h)~K_1?SNQ3=1db=Nl*5gO{i2<*uL1&a{LX>^)&qBW~oN{Yd4B` zQL6yvCKOxt!he52UV{I6wOJgui+U{^D`J?zSb0 zVqpDHF~geqn$Nsn(U*YxwutwwRcu5v`*)(>P{=ii?DS{YEJms*q*eVyZi$9-4DU%ZW9Z@?|v5^cvWQ0 zKxS6H^<#VtE?cbmk9SC&>!)|Xi@RGe5t;zPNVx)~q@`QKG|3L;6MkL+X(M5*i)KPP zFM#fK7g+#%-VMELUJpLkw1u+__rcpo#rn}p(@gn0|6Z0OmXAi{#3f>^k;zT@!AZxm z@WC}Oe^9aYK)o9|u#oIKDS*klaP11XZB0e2$`mDX%=JRqH9p0XBKHSl)FJsea06er z5$rm^jEQ*4u;l=J;Zen}UJw7_cKFg`V!jJ=I)(gwDh=OxQ9N*0UMRr+ zty@HI*mFWelmn+Fw{I*f3TIyoTp*^!R5+twVoH|Gk~r=8)8!u94#Rg|ru~6w1S~3L#$$o>;ieg@MbZ)CPuI0}2h9yu6;HAA11Kmk6 zqNwBOIiLgi^x1a=(3dR~4s@`M0lix;V6=GDVktu*U0ds-H3e0HiaEs#yG2I*^&7>a z@a3muTMSa`!}e_3k6pR{H7P^3WR^7aym+4U5sy259Y4Q3m*p24x0FQv1wDkua-wrQ zx?Q|_N5|v^uvO(@hv(pFG0lBsJ>0uVv1N}-GNveLhwr~CGB8LBy6_sblI!i5ywsV| z+7hF^0dw&8#wF7B?{o&9-r3*JQ)%JgqZu)Ex_*%uOUXED#~lPSEuCffH&?@k#qgaM zitIa~`S*7v`Swe&{}?2a;!(_{rO%*I?ZJ>5)FNtxYj#XRZf=By{)C8BjwJ~dJC5|e zsZ>By0%*&Upp#ejiw*VK72>KL%ht*E29J zX}IMQ!@Zjn>AdC-zF%PbL684*Pl`jMipRGLK%_gD4{8*}JWV-kQ1?VXE$Q5Pq*R2#Q!exs@?sTRi3J(EsF|SDU z8S?4IMFPv5+4f6f)oiN)=gk)2A6^m{^G7$r?|e}7fCpZa7d7Ch4eO(;`Xqvc4J8}A z2y-`7Z1)2R!^S0sdp24u7=}l;mbia&kwd3FZeN$+;sp-><44kYEO;s0B4pE2>_(ev z@uPeX->MN$8YV`nw}i=hHtW{ghFdO{aEP~$zze&hD}vpOrGxyB&M5H6Z963L)B7)$ zAo^MhLNBRQluAGGK0~3VW}h)4D=JIsw3!yJ>|lIv9E4rR;hN=Qefa%*B>wo3?ILCb znHJ5!9o$#@pH{_k0?UV`VD_Nlilv5IR*O0A^?fD2_PEQN2VAt)%pJD5_sSTX_eof6 ztvFQ5gE68k=O%nk9ey@3;m(BgO1Gtas?X}EZSWCDQ%Qym%YFcqvrnxbe6 zdBCGt_~9Fpm3*nc?L5D$o`d!5p7`=~pCO(SCqaOtSh51eA}{xygh#e@77No2Ih_V-cS=w0kQjqY7sBmp;PrQ>mP;E0`HaZZPF!XdOGx#f zd`3lIsL;9umYW~F0nhA^_>ZfXOI-Cw){En1&k1Ts$9cxJdr~(N|6#m&9JZejE5=gaWVd8k zv38+!b1J&>(grAL?_VLJ#~o`#Hcb~)OVKU-T{o|s_Q0|giIky` z_0#d2tx|`V55{(C98QeEOS>WAbKYqw*}~ z@oF`^Lg!dc77kjDfxc z+`R^7^ht`?eVe7Ad0X4_WOobLZj~2!3O}|b-l3QG3dg$ZGI50leW^|8Lk~WQl)<*8 zGQaVlkOey~y-)4Dn zf_C7|)*K|!b#R8p^Iz;zu~H%-sL&Mk8B%k8k%rqgd{!p`xbA z^>j_qzedMt4d)MA+;c^YXWtR4O1l{1z|K4{T~o{}vJ(|$xD@0lMkr>}XpXHyb+r-C z@K0SXI>P3C(nqpw-RB;HV9Eep8^3HRiB$X1R{8z|tHnOu%Mijam}~1yXy3}=d6i-@ zoy?=^-{_Gd#eDr)(G#Z1&E0Ulf(7pNrF){>+!YE}BUgv@$@nBj&%-HcPWb8Cmm({TAx2^mnp54OT{yP}m)=p~&Iks~16 zf_*38YtKmh@Y;oN&-%{72pdrynA?zBl7}fx*2cDbjh6^0NyIHEcR&Cw>pkhhGV8E3}tZJ8k;Gt|`8*LG1Vu#nUyElI-h6Gdcvdk%B0e70mKCnBkbQ-#zG2!29%sGL$ z5eT&5IU=(2``0)um}&EmFZ+E{kVE4s!@#g6HDD;_6cZyBrP@rB7P?F$Y6R*i=et@$ z8h+(EDRlYrKKPrbq2RU8I-e>!D}V->@Y6dbOw;w?nicS|4f1AJ0n{&a9tLSVL4`B- z$Vw8J_X;^B;ipSKnuwaIC6#LR+&2t(Xt&lHCJQE+( z+KBT7C84LaFS9n%-s3lJhD}REZvN6^;y7y+?>^OFhO-FhfNPCp;mc3J`O_q9@{?Cd zo#_{L*T3nBWbeS-_H-X?Zc)N4Elauw*fELT3t}X+3!bOSI|+pF8g?z43Nx+>#tyt( zeh1z)i!5$lZS%lZI5-Mk5pt!_PSga*;rmW@9ZChrsY>W+sZas6HlhT6@+v8%=6d2V zd+_x$Z_%i|zTpf2y4^f1B}M!G@n3#Q3gk2sHO>T>1L}zkZCZSy zkY!1akU#ygWIivPA?2@DoiAd^WWZeOs_q48Ew=1pUOaL6wEQgQ6s4l72K@OG^471M3%_+UES;v> zw$2S-qnGO#IAJ<9gM}m1%8_dLW}BJ_GXs2Jt;MWC#dlto3QSEIvww*x&B~}*PqzfO zUa0u=^#bO%4#R(cP+aln5}4r}19V{d_@`Il>o16VeB~VY?OS2x`HdYf-H14?7AXAb zrUP!Pv6u#Lg{NWZY>PWDb9nSEiKT8D3ec@du4a7Uxwazho5}CG4B^a4*mDy8=ph01 zxdvu9#{dnofU%U}zBdei@wkMWuAB@1@pdUv?&;nEI9Qm5fk-`4w5qzuJM-9p3nE`I6bj|@fH7Gr=M@JRkdO;M$ zQ)wxb{kacFosTHqzbb0xMQi}#PDG1#yF>MNrX0UvjM%X~57sWWxN3#XeOsDzdkvgR zlaQB9`1f{RQ^aX9kbqylP8ui;r6jibKYtHi+y%LeqJKz=uJ?iwbUddP_c&{I=th(p z^c(c}C2d{AER-$6XKsN7!|?AvAjQXLeQqT|2P8LBDeTNPTQZ5K;N;E~$0#f7~V|dOx~BIQCyXRjE(WE+D2;F4Cb? z(|n#9ROrUqz^p;VM>b0Jqb>VFonWT`%v9qWGg&vSb4t19hces=uN*9Y!3$1DVVj9UE!+b@Tsds)Bp)E z`qE=klw8q1axMrx%}fZWGgAcWsC3pmg?+vPm>%sc1k~~q1itZ{aO_{dNs?{vx=aeJ z|J@7l#M`1s+BMFLUQs7ppPFN1j^d-6EYbz3J+1ZRl1Mw|%Enjxrc1x@yVgqDR5~yA z|G$1p!VlV2iLJU%m{U(PWX5_YessEP*>kV|{O2cs+s-I8C?T1hf;pg|;^(0eiOMUu zQX_8uuoPEcFhlCioytgqPVMz3ZaVCg=!6_cgVq#FiYu2{{PdL$-+Ufk-9Nc}%1*!> zPf7@CTq$n8M658CJ>Nhj$#v0=PuWJtP3#wYfP)hOf zD{S6A3Xi|teVS6IT-nv2jI;;2Yqca(q++7%{ooC0v%K$=UlekZ!TGhEx?Az1TYch; z0=k!A4k&m4&+U?iJ$GFu-If*%!@s#2Zn#MC;2V;Zdt##coMtp>5LaDG!bG)!n^#*b zovrx&`(=xFdf?V-8Z3o~GqxpFKtHlUaoq~3x#xQD;vUKCd~TQ6^V^N3({!%Pm~iUM zxU|RU1k{sCQn>8DoEZmZ8O$TS$uwz1K|bP&xl+C7rd9F)JqVlkK5gBHL45u0s?6o2xtA1Be}wOcBnrecP>=zO?kHQacSv?JC=+ADu| zi&!vD_%{4@E@Zt3qCnn&bH=~c=H2A|Hfl3wPY)45Apmi*s@n5_pZN4+RZJWQ|STo@l68m zSN2Q!fW0RqiMQyRp91dP5EBjN=xMlSrN#Os7GHQ2PEAC!w3MUtOvP?&DM-de7G6FF z)-4ui$f6lydNWAc*6Z)Wqi=~h^YEx#&r-4m5$Zh3{d@hqYGOaapzd{;?vwz!95vTN zYerWTa5n#^V~N7F{jk_x|8cXl>%Drp{G2ykw(X5eBsKr=s8~+69TrjHpx+!QR}3k2 zA%KU2++49mYoh_h_`HqitL7G{< zxJT-oynRH*9@OOy&p*_^q6p^=%Xy1th-ft2FUJIP9Lve>5wvyNenj*;7ED4KPpUL0AebTdwi$X>l$*v{g#{E}sLd7l?Ia(M$o>;+axr z0fa*rTmkZUUX;r5ocx|ELeW)R@AnwFVlix3pvomk#zRlV z({kNi#|6Y&56bSc=eXP}5JxPl`)rLfDH2v$KL!n1qJuihFnb2*sc@P!oYWL8exLxl zT4EUvjfe##5tF{%^M~QW*|2Pmlv$WNO~9Cn3m_NIY<}%eY-<0sTrJ#vEGOLd$e5HK z-gR8+03IHx$ny%{hTbJNJ`?NN?taBQ82o^y2%wHS%$@)`)n%YjCkt?uQ&(4_!!+p@7gy|{yejr)7=0H*`4?#A!tECfGrXbvRA}@&w z$HqmhnaB&U3#H2Z1BEn@ohR)8Itz+=_?}8h$J-1cPF-lMaCb7)Ndvlr`F=}bXDXOM`$kXTI2sWc1g%d2lA~Wu?|Z1 zPI}n_Gs+o`Rc`gX%dsEh)#69DW9%Bxb)Ee;6M7uSha}<>DzhF|E*UnPE!xh#DGia4 zc~;89wp!OP%{G$fQ@QO(rqYNap+c;(*~rs8Y)Yusx_X*24`Y|a@ep^N&gq}oZ zf6}Zs2dS<>DfTtA7z=%8M19LVCGfJJM~41n^b{&EbaP9A{lq|rQ{J?GPE{4W;Rb3Yiv9_9TnbUN$T`Y6fMUBC5ZJ#NUEiV>vDe(2d~gLvypUoKiBCyaV&MB4CXWOtBOkq>zl_;UkT z4lPMOJFERIm9g?>%us-)Y+Jgr7jR$QCAw39GZFAqfwp5uPbH2NN5&{Y<+J_wVXX9Q zPBwkYmw!5dEgD<+cCrqXAbFY8aiuRX>m7lvxEtrE@3|SwXk+q+Riv)L)tXfL`&7Tz z*Shd^Aq{5_%sSg%=2~0LLOt(+o+t1BDx#+?8Vqkrsm@2&aZ%J4O#Yym)1`hG8y23t z*1B7A&#n{5J1)o!-QCB6pYOr*fl{EIWH-cNEfF{%mw?D0QHy@EURM{}NN7DK=6_`Yc<<$jm{ia%W%RJ*1{ z(aTN4wbKaLA+KC`F@NqANKa~$0OJL# zzrAwRUCXLhijF}}hbq2|ai>?vl!w)%`;J%7U)z3W5Pz3EkUw=R&phj%y2Un!Qx_`R zhH;*|gBPDlxcEWHwP~<5J%@5jH6*GUhI<%`I+3AgIMNll_)VFQIn%Q0C2rLjR~+|J z9~$oh001m$uE8`y1dWJ^iYKZsfCC(XhQg6hgo8gEfkDDC@ZGlR2OKV|&uIB2K#3)g zNs0d-s8nH+)dBPG6NpSQ3FAVhojy#A_luy!MH2yg?7-iT@#NlJYHP<&3#;D3pG^6& z2?mbDAP``L6UG6Jac~S+7CWnD)1R^jCE^6pix_i)b`pScfO`mSt^_!IlZl4y*Gcp|{v19uqg07JlGdpuZJoLUM>@(-rrBN*Vg_*fD-nh2&P#u35f zI9e0{z$_Y`(O*Xmw6+%eNP0a!;{jS0>tRvrO)N|fu}!s1z}PfYJ3-5W>kpQ^e$n00 j+p(oXN0N5%rY8(AzIg*}#%1>v^$386t2egBB|PIl7&(+6 literal 0 HcmV?d00001 diff --git a/src/favicons/browserconfig.xml b/src/favicons/browserconfig.xml new file mode 100644 index 000000000..e762b1cf1 --- /dev/null +++ b/src/favicons/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/src/favicons/favicon-16x16.png b/src/favicons/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..a51b6c76211bde0206ca2e4fa24ba788050cab98 GIT binary patch literal 12768 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 wpfwzAiwr3ptw5}hu*&Vk}tW* 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 literal 0 HcmV?d00001 diff --git a/src/favicons/site.webmanifest b/src/favicons/site.webmanifest new file mode 100644 index 000000000..a476f60e4 --- /dev/null +++ b/src/favicons/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "android-chrome-384x384.png", + "sizes": "384x384", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/src/fuzzy_search.js b/src/fuzzy_search.js new file mode 100644 index 000000000..836d80cc3 --- /dev/null +++ b/src/fuzzy_search.js @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2013 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +const REGEX_SPECIAL_CHARACTERS = '^[]{}()\\.^$*+?|-,'; + +/** + * @unrestricted + */ +class FuzzySearch { + /** + * @param {string} query + */ + constructor(query) { + this._filterRegex = FuzzySearch._createFilterRegex(query); + this._query = query; + this._queryUpperCase = query.toUpperCase(); + this._score = new Int32Array(20 * 100); + this._sequence = new Int32Array(20 * 100); + this._dataUpperCase = ''; + } + + /** + * @return {string} + */ + query() { + return this._query; + } + + /** + * @param {string} data + * @param {?Array} matchIndexes + * @return {number} + */ + score(data, matchIndexes) { + if (!data || !this._query || !this._filterRegex.test(data)) + return 0; + var n = this._query.length; + var m = data.length; + if (!this._score || this._score.length < n * m) { + this._score = new Int32Array(n * m * 2); + this._sequence = new Int32Array(n * m * 2); + } + var score = this._score; + var sequence = /** @type {!Int32Array} */ (this._sequence); + this._dataUpperCase = data.toUpperCase(); + for (var i = 0; i < n; ++i) { + for (var j = 0; j < m; ++j) { + var skipCharScore = j === 0 ? 0 : score[i * m + j - 1]; + var prevCharScore = i === 0 || j === 0 ? 0 : score[(i - 1) * m + j - 1]; + var consecutiveMatch = i === 0 || j === 0 ? 0 : sequence[(i - 1) * m + j - 1]; + var pickCharScore = this._match(this._query, data, i, j, consecutiveMatch); + if (pickCharScore && prevCharScore + pickCharScore >= skipCharScore) { + sequence[i * m + j] = consecutiveMatch + 1; + score[i * m + j] = (prevCharScore + pickCharScore); + } else { + sequence[i * m + j] = 0; + score[i * m + j] = skipCharScore; + } + } + } + if (matchIndexes) + this._restoreMatchIndexes(sequence, n, m, matchIndexes); + return score[n * m - 1]; + } + + /** + * @param {!Int32Array} sequence + * @param {number} n + * @param {number} m + * @param {!Array} out + */ + _restoreMatchIndexes(sequence, n, m, out) { + var i = n - 1, j = m - 1; + while (i >= 0 && j >= 0) { + switch (sequence[i * m + j]) { + case 0: + --j; + break; + default: + out.push(j); + --i; + --j; + break; + } + } + out.reverse(); + } + + /** + * @param {string} query + * @param {string} data + * @param {number} i + * @param {number} j + * @param {number} consecutiveMatch + * @return {number} + */ + _match(query, data, i, j, consecutiveMatch) { + if (this._queryUpperCase[i] !== this._dataUpperCase[j]) + return 0; + + var isCapsMatch = query[i] === data[j] && query[i] === this._queryUpperCase[i]; + var score = 10; + if (isCapsMatch) + score += 6; + score += consecutiveMatch * 4; + return score; + } + + /** + * @param {string} query + * @return {!RegExp} + */ + static _createFilterRegex(query) { + const toEscape = REGEX_SPECIAL_CHARACTERS; + let regexString = ''; + for (let i = 0; i < query.length; ++i) { + let c = query.charAt(i); + if (toEscape.indexOf(c) !== -1) + c = '\\' + c; + if (i) + regexString += '[^\\0' + c + ']*'; + regexString += c; + } + return new RegExp(regexString, 'i'); + } +}; + diff --git a/src/github.png b/src/github.png new file mode 100644 index 0000000000000000000000000000000000000000..628da97c70890c73e59204f5b140c4e67671e92d GIT binary patch literal 1571 zcmaJ>c~BE~6izDPQq)#Nu*KOf(n^(VHY9;fiINM65``pc+9*v(mL$bwfCjbc%v9V{8r9iX|O%>Nr%pLD2qT{mty}c=LVleeamv znz3SOSm@kP8jThvOOq(56Yzh*fz(booe!uZij=BJC6+_lbvQ~B8nA2>kXdv_RDtRY z`5QXWWEySCe6vbTs^#f?J!WC*{1~RgVx!nJTJjQyO{dRANgx|FnymtGbD9%JmCh9^y)##j7{Dcqfn*1ta$rG89pJF6w-S7Z037$rr|y0;1Onp_ zGFJdT6Q!1C0AdVB0WOmpuV=AgAQ550Tn+-mivTtYPJmz*#75#_n9oV%!#rSOfmAfy zki%C~=fTp1{O#BLpJ|0jj#m6#|LRWit-vq3PE1z9ZqyvET4sX$-Icqy7t z<=aq5ff86AuBZBu6EjJsYWM0uejufWFTwPA7Su}0Bm$7KFb!q{Um_8~A{LUG#1l(l zSehUda@kU8LIRg9fkk2tZ;~ss5~R+mM<==F7hLHpxqLB>>PQS%Vc7b~?q!%T5+h8Q z4G=4Nzyi5WZ?^gkasJ{?Xhm`JC#WG6$1K2jb@=9&D3EgD#3UhGh#*21rJjulVXjCF zvp76q62jt0zzMG5C7DlfMgPl%C^3+~wf|}Lq=}jz|MmIcQjh1Ok6NjD$Em^Iv26D> z8tt_TnM9~^Tt8mflRGPOrrX|HtT3gG4LEuuk{g2Rn}QgJIa?gZo))!!=o_l9bvD%A zZ`aHajl8#~u?!4f7F#*b*->A=R2L)6!>saz?h>#wTXT-I(XmQ zx{84skS>k=i~i`(6k4C7;Zpfx%dCPVjPayMf8pugtGM=~s=Id1l#8MZJ1-73wV#Q3 zR3>v3%}jbQs1f_Z0xo;%=LILlA+nTpKI4ha%xWW}uqHrNao~&T4AY6m`P$_n-6h*g zhoX+e4n%~gl_lhe#s+AMb7d{5WzvYTa%6Q~si@@4{;s(0zU|H&P3fE+t{7X`S#Cj@ zC#vd}^4pcBD*77Ny5=j$h8EL2_t$O38$SQiJ6fPjJMimypr~MB2(&P0aI|h}$64<0 z>_~duqNjaT=DM^6+N{&B_lED;F2wrl?!4Lk*2((x!fmrcsw+=cI^qttuZ9C}-m~5E z-ryYVpL%^xR#&(0YI5hz<(}F7-p)?FPcyJO-zVO>%9ZDXJH8pnY;GJYFDQ>vd#j_* zRrd}L(r=!g+1#nQwsO?kpS`Qq8`NxE+Zy{gf7*_7J*U2V_|NpLo{iasj7VCg_V9&| ShohtYzipXxh2)4xTk + + + \ No newline at end of file diff --git a/src/ic_check_box_black_24px.svg b/src/ic_check_box_black_24px.svg new file mode 100644 index 000000000..f4a93dd11 --- /dev/null +++ b/src/ic_check_box_black_24px.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/ic_check_box_outline_blank_black_24px.svg b/src/ic_check_box_outline_blank_black_24px.svg new file mode 100644 index 000000000..431090cdf --- /dev/null +++ b/src/ic_check_box_outline_blank_black_24px.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/index.html b/src/index.html new file mode 100644 index 000000000..2f7a55304 --- /dev/null +++ b/src/index.html @@ -0,0 +1,72 @@ +DevTools Protocol Viewer + + + + + + + + + + + + + + + + + + + + + + +