From d6609beac6bde16cfd6be80f12523e5c4e8d45e6 Mon Sep 17 00:00:00 2001
From: Christian Findlay <16697547+MelbourneDeveloper@users.noreply.github.com>
Date: Sat, 3 Oct 2026 22:33:07 +1000
Subject: [PATCH 1/3] Rebuild documentation site with native motion and source
API references
---
.agents/skills/website-audit/SKILL.md | 6 +-
.github/workflows/deploy-website.yml | 48 +-
.gitignore | 3 +-
AGENTS.md | 6 +
Website/README.md | 61 +
Website/eleventy.config.js | 67 +-
Website/examples/Directory.Build.props | 12 +
Website/examples/Examples.csproj | 8 +
Website/examples/Program.cs | 86 ++
Website/examples/Tests/Examples.Tests.csproj | 9 +
Website/examples/Tests/Program.cs | 183 +++
Website/package-lock.json | 887 +++--------
Website/package.json | 20 +-
Website/playwright.config.js | 13 +-
Website/scripts/check-site.js | 185 +++
Website/scripts/generate-api-docs.js | 1285 +--------------
Website/scripts/generate-api-docs.sh | 19 +-
Website/src/_data/examples.js | 4 +
Website/src/_data/site.js | 8 +
Website/src/_data/site.json | 12 -
Website/src/_includes/home.njk | 41 +
Website/src/_includes/layouts/api.njk | 4 +
Website/src/_includes/layouts/base.njk | 29 +
Website/src/_includes/layouts/blog.njk | 4 +
Website/src/_includes/layouts/docs.njk | 5 +
Website/src/api/deleteasync.md | 58 +
Website/src/api/getasync.md | 73 +
Website/src/api/httpclient-extensions.md | 33 +
Website/src/api/index.njk | 7 +
Website/src/api/mcp-generator.md | 107 ++
Website/src/api/openapi-generator.md | 103 ++
Website/src/api/patchasync.md | 57 +
Website/src/api/postasync.md | 73 +
Website/src/api/putasync.md | 57 +
Website/src/api/result-types.md | 168 ++
Website/src/api/serialization.md | 163 ++
Website/src/assets/css/styles.css | 1462 +-----------------
Website/src/assets/images/mark.svg | 1 +
Website/src/assets/images/social-card.png | Bin 0 -> 36202 bytes
Website/src/assets/images/social-card.svg | 1 +
Website/src/assets/js/site.js | 124 ++
Website/src/blog/index.njk | 7 +
Website/src/docs/api/mcp-generator.md | 4 +-
Website/src/docs/api/openapi-generator.md | 4 +-
Website/src/docs/exhaustion.md | 5 +
Website/src/examples/index.njk | 298 +---
Website/src/feed.njk | 22 +
Website/src/index.njk | 114 +-
Website/src/journal-archives.njk | 13 +
Website/src/llms.txt.njk | 22 +
Website/src/robots.txt.njk | 9 +
Website/src/sitemap.njk | 9 +
Website/src/zh/api/index.njk | 77 +-
Website/src/zh/blog/index.njk | 7 +
Website/src/zh/docs/api/index.md | 16 +-
Website/src/zh/docs/exhaustion.md | 5 +
Website/src/zh/examples/index.njk | 295 +---
Website/src/zh/index.njk | 109 +-
Website/tests-node/api-export.test.js | 179 +++
Website/tests-node/examples.test.js | 28 +
Website/tests-node/site-guards.test.js | 160 ++
Website/tests/api.test.js | 6 +-
Website/tests/blog.test.js | 7 +-
Website/tests/chinese-i18n.test.js | 6 +-
Website/tests/docs.test.js | 2 +-
Website/tests/fixtures.js | 25 +
Website/tests/homepage.test.js | 2 +-
Website/tests/motion-prose.test.js | 195 +++
Website/tests/seo.test.js | 2 +-
Website/tests/syntax-highlighting.test.js | 2 +-
Website/tests/visual-qa.test.js | 365 ++---
Website/tools/ApiDocs/ApiDocs.csproj | 12 +
Website/tools/ApiDocs/Directory.Build.props | 11 +
Website/tools/ApiDocs/Program.cs | 584 +++++++
Website/tools/ApiDocs/README.md | 43 +
Website/tools/ApiDocs/schema.json | 51 +
76 files changed, 3684 insertions(+), 4504 deletions(-)
create mode 100644 Website/README.md
create mode 100644 Website/examples/Directory.Build.props
create mode 100644 Website/examples/Examples.csproj
create mode 100644 Website/examples/Program.cs
create mode 100644 Website/examples/Tests/Examples.Tests.csproj
create mode 100644 Website/examples/Tests/Program.cs
create mode 100644 Website/scripts/check-site.js
create mode 100644 Website/src/_data/examples.js
create mode 100644 Website/src/_data/site.js
delete mode 100644 Website/src/_data/site.json
create mode 100644 Website/src/_includes/home.njk
create mode 100644 Website/src/_includes/layouts/api.njk
create mode 100644 Website/src/_includes/layouts/base.njk
create mode 100644 Website/src/_includes/layouts/blog.njk
create mode 100644 Website/src/_includes/layouts/docs.njk
create mode 100644 Website/src/api/deleteasync.md
create mode 100644 Website/src/api/getasync.md
create mode 100644 Website/src/api/httpclient-extensions.md
create mode 100644 Website/src/api/index.njk
create mode 100644 Website/src/api/mcp-generator.md
create mode 100644 Website/src/api/openapi-generator.md
create mode 100644 Website/src/api/patchasync.md
create mode 100644 Website/src/api/postasync.md
create mode 100644 Website/src/api/putasync.md
create mode 100644 Website/src/api/result-types.md
create mode 100644 Website/src/api/serialization.md
create mode 100644 Website/src/assets/images/mark.svg
create mode 100644 Website/src/assets/images/social-card.png
create mode 100644 Website/src/assets/images/social-card.svg
create mode 100644 Website/src/assets/js/site.js
create mode 100644 Website/src/blog/index.njk
create mode 100644 Website/src/feed.njk
create mode 100644 Website/src/journal-archives.njk
create mode 100644 Website/src/llms.txt.njk
create mode 100644 Website/src/robots.txt.njk
create mode 100644 Website/src/sitemap.njk
create mode 100644 Website/src/zh/blog/index.njk
create mode 100644 Website/tests-node/api-export.test.js
create mode 100644 Website/tests-node/examples.test.js
create mode 100644 Website/tests-node/site-guards.test.js
create mode 100644 Website/tests/fixtures.js
create mode 100644 Website/tests/motion-prose.test.js
create mode 100644 Website/tools/ApiDocs/ApiDocs.csproj
create mode 100644 Website/tools/ApiDocs/Directory.Build.props
create mode 100644 Website/tools/ApiDocs/Program.cs
create mode 100644 Website/tools/ApiDocs/README.md
create mode 100644 Website/tools/ApiDocs/schema.json
diff --git a/.agents/skills/website-audit/SKILL.md b/.agents/skills/website-audit/SKILL.md
index 990cd96f..b1270fac 100644
--- a/.agents/skills/website-audit/SKILL.md
+++ b/.agents/skills/website-audit/SKILL.md
@@ -6,7 +6,9 @@ description: Audits a website for SEO, AI search performance, structured data, m
## RestClient.Net context
-The website is in `Website/`, uses Eleventy and `eleventy-plugin-techdoc`, and builds with `npm ci` then `npm run build` from that directory. Edit sources, not `Website/_site/`. Use `npx @11ty/eleventy --serve --port=8081` for an isolated preview: the existing npm dev command terminates processes on port 8080. The deployment workflow is `.github/workflows/deploy-website.yml`.
+The website is in `Website/`, uses Eleventy with local layouts, and builds with `npm ci` then `npm run build` from that directory. It needs Node.js 22 and the .NET 9 SDK for source API export. Edit sources, not `Website/_site/` or generated `Website/src/api/reference/`. `npm run dev` previews on port 4173 without terminating other processes. The deployment workflow is `.github/workflows/deploy-website.yml`.
+
+The user's total CSS budget is 2,500 raw UTF-8 bytes. Keep the local theme and shared `.prose` styling; do not reintroduce `eleventy-plugin-techdoc`, whose mandatory stylesheet alone exceeds that budget. `npm run build` audits the generated output. Run `npm run test:unit` and `npm test` for export, budget, accessibility, motion, and route regressions. Production URLs come from `SITE_URL` and `SITE_PATH_PREFIX`, populated by GitHub Pages metadata.
# Website Audit
@@ -36,7 +38,7 @@ Audit Progress:
- [ ] Step 12: Report findings
```
-- **Theme:** dev-tool/docs sites MUST use [`eleventy-plugin-techdoc`](https://github.com/Nimblesite/eleventy-plugin-techdoc) on Eleventy 3.x. Verify it is the theme in use, and **upgrade it (and `@11ty/eleventy`) to the latest version** before auditing, then rebuild and audit the upgraded output.
+- **Theme:** retain the repository's local Eleventy layouts and shared prose stylesheet within the user's CSS budget. Dependency upgrades are a separate authorized task; do not replace the theme as part of an audit.
- Check the outputted HTML/CSS/JavaScript AFTER the website is generated by the static content generator. - Don't just check the static content before the website is generated.
- Fix issues at the core where the static content templates are stored - not in the outputted HTML (e.g. _site)
- Never manually edit the generated website content directly
diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml
index 9851adb2..5adfede4 100644
--- a/.github/workflows/deploy-website.yml
+++ b/.github/workflows/deploy-website.yml
@@ -1,22 +1,27 @@
-name: Deploy Website to GitHub Pages
+name: Website
on:
+ pull_request:
+ branches:
+ - main
push:
branches:
- main
permissions:
contents: read
- pages: write
- id-token: write
concurrency:
- group: "pages"
+ group: "pages-${{ github.ref }}"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
+ timeout-minutes: 15
+ permissions:
+ contents: read
+ pages: read
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -24,27 +29,58 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version: "20"
+ node-version: "22"
cache: "npm"
cache-dependency-path: Website/package-lock.json
+ - name: Setup .NET for source API export
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: "9.0.x"
+
- name: Install dependencies
working-directory: Website
run: npm ci
- - name: Build website
+ - name: Test API export and website guards
+ working-directory: Website
+ run: npm run test:unit
+
+ - name: Build and audit local-root website
working-directory: Website
run: npm run build
+ - name: Install browser
+ working-directory: Website
+ run: npx playwright install --with-deps chromium
+
+ - name: Test website interactions and content
+ working-directory: Website
+ run: npm test
+
- name: Setup Pages
+ id: pages
uses: actions/configure-pages@v5
+ - name: Build and audit production URLs
+ working-directory: Website
+ env:
+ SITE_URL: ${{ steps.pages.outputs.origin }}
+ SITE_PATH_PREFIX: ${{ steps.pages.outputs.base_path }}
+ run: npm run build
+
- name: Upload artifact
+ if: github.event_name == 'push'
uses: actions/upload-pages-artifact@v3
with:
path: Website/_site
deploy:
+ if: github.event_name == 'push'
+ permissions:
+ contents: read
+ pages: write
+ id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
diff --git a/.gitignore b/.gitignore
index e7e9c6f6..3d371c48 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@ __pycache__/
# Website
Website/node_modules/
Website/_site/
+Website/_site-prefix/
Website/playwright-report/
Website/test-results/
-Website/src/api/
+Website/src/api/reference/
diff --git a/AGENTS.md b/AGENTS.md
index 23191bf5..efd7c2f4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -25,6 +25,12 @@ The existing dotnet commands are the local equivalents of the Makefile commands
Use .NET 8 and 9 runtimes for the repository's targets. Run focused tests while iterating, then the relevant CI checks. Preserve the existing assertions and regression coverage. Generated sample code is regenerated by the existing build targets.
+## Website
+
+The Eleventy site uses local layouts and a shared `.prose` class for docs, API references, and blog posts. Total shipped CSS must remain at or below **2,500 raw UTF-8 bytes**, including every stylesheet. Do not bypass the budget with inline styles, external CSS, or JavaScript that injects CSS. `npm run build` enforces the budget and checks generated links and metadata.
+
+API reference pages are exported from C# declarations and XML comments by `Website/tools/ApiDocs/`; edit the source rather than generated `Website/src/api/reference/` files. Run `npm run test:unit`, `npm run build`, and `npm test` in `Website/`. The deployment workflow also verifies the GitHub Pages path prefix. See `Website/README.md`.
+
## Git and CI
Use a feature branch and a PR to `main`; derive the PR title and description from the diff with `origin/main`. Follow the user's authorization for committing, pushing, and merging. Monitor the latest PR commit's checks and resolve failures before merging. Do not add AI co-author trailers.
diff --git a/Website/README.md b/Website/README.md
new file mode 100644
index 00000000..9cb0282e
--- /dev/null
+++ b/Website/README.md
@@ -0,0 +1,61 @@
+# RestClient.Net website
+
+The site uses Eleventy, local layouts, and one shared `.prose` class for guides,
+API reference pages, and articles. Its interactive HTTP/Result diagram is drawn
+in canvas; the code examples and outcome controls remain ordinary accessible HTML.
+
+## Develop and verify
+
+Install Node.js 22 and the .NET 9 SDK, then run these commands from `Website/`:
+
+```sh
+npm ci
+npm run dev
+```
+
+The preview uses port 4173. It does not terminate other processes.
+
+```sh
+npm run test:unit
+npm run build
+npm test
+```
+
+The build exports API documentation, renders the site, and audits the result.
+The browser tests require Chromium: `npx playwright install chromium` installs it.
+
+English and Chinese example pages render `examples/Program.cs`. The unit checks
+compile that source against RestClient.Net 7.3.1 and exercise requests and errors
+through a local message handler, without making network requests.
+
+The CSS budget is **2,500 raw UTF-8 bytes across all shipped CSS files**. The build
+guard also rejects inline styles, external stylesheets, and JavaScript that injects
+CSS. Canvas drawing is used for the actual motion graphic, not to replace content
+layout or typography. Reduced-motion preferences and the animation pause control
+are respected.
+
+## API reference
+
+`npm run generate-api` runs `scripts/generate-api-docs.js`, which invokes the
+Roslyn exporter under `tools/ApiDocs/`. It reads public C# declarations and XML
+comments from the library source and writes deterministic Markdown and JSON to
+`src/api/reference/`. This generated directory is ignored by Git and recreated
+on every build. Edit C# declarations and comments to update the reference; curated
+API guides remain tracked under `src/api/`.
+
+Exporter tests change fixture declarations and comments, exercise overloads, and
+check that private implementation details stay out of the reference.
+
+## Deployment URLs
+
+Local previews use root-relative routes. Set `SITE_URL` to the production origin
+and `SITE_PATH_PREFIX` to the mount path when deploying, for example:
+
+```sh
+SITE_URL=https://melbournedeveloper.github.io \
+SITE_PATH_PREFIX=/RestClient.Net/ npm run build
+```
+
+The GitHub Pages workflow reads these values from `actions/configure-pages`.
+It runs unit and browser tests before building and auditing the production URLs.
+Pull requests run these checks; only pushes to `main` deploy the resulting site.
diff --git a/Website/eleventy.config.js b/Website/eleventy.config.js
index a8e3ca95..496c5f02 100644
--- a/Website/eleventy.config.js
+++ b/Website/eleventy.config.js
@@ -1,29 +1,44 @@
-import techdoc from "eleventy-plugin-techdoc";
+import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
+import markdownIt from "markdown-it";
+import anchor from "markdown-it-anchor";
-export default function(eleventyConfig) {
- eleventyConfig.addPlugin(techdoc, {
- site: {
- name: "RestClient.Net",
- url: "https://restclient.net",
- description: "The safest way to make REST calls in C#. Built with functional programming, type safety, and modern .NET patterns.",
- },
- features: {
- blog: true,
- docs: true,
- darkMode: true,
- i18n: true,
- },
- i18n: {
- defaultLanguage: 'en',
- languages: ['en', 'zh'],
- },
+const origin = process.env.SITE_URL || "https://melbournedeveloper.github.io";
+const prefix = "/" + (process.env.SITE_PATH_PREFIX || "").replace(/^\/+|\/+$/g, "") + "/";
+const basePath = prefix === "//" ? "/" : prefix;
+const absoluteUrl = value => new URL(String(value || "/").replace(/^\//,""), new URL(basePath,origin)).href;
+export default function (config) {
+ config.addPlugin(syntaxHighlight);
+ config.setLibrary("md", markdownIt({html:true,linkify:true}).use(anchor, {slugify:s=>s.toLowerCase().replace(/\s+/g,"-").replace(/[^\p{L}\p{N}_-]/gu,"")}));
+ config.addPassthroughCopy("src/assets");
+ config.addPassthroughCopy({"src/api/reference/api.json":"api/reference/api.json", "src/api/reference/schema.json":"api/reference/schema.json"});
+ config.addCollection("posts", api=>api.getFilteredByGlob("src/blog/*.md").sort((a,b)=>b.date-a.date));
+ config.addCollection("zhposts", api=>api.getFilteredByGlob("src/zh/blog/*.md").sort((a,b)=>b.date-a.date));
+ config.addCollection("journalArchives", api => {
+ const archives = [];
+ for (const lang of ["en", "zh"]) {
+ const prefix = lang === "zh" ? "/zh" : "";
+ const posts = api.getFilteredByGlob(`src${prefix}/blog/*.md`).sort((a,b)=>b.date-a.date);
+ for (const kind of ["tags", "categories"]) {
+ archives.push({url:`${prefix}/blog/${kind}/`,lang,title:lang === "zh" ? (kind === "tags" ? "博客主题" : "博客分类") : (kind === "tags" ? "Journal topics" : "Journal categories"),posts});
+ const values = [...new Set(posts.flatMap(p=>kind === "tags" ? (p.data.tags || []).filter(t=>!["post","posts"].includes(t)) : p.data.category ? [p.data.category] : []))];
+ for (const value of values) archives.push({url:`${prefix}/blog/${kind}/${value.toLowerCase().replace(/[^a-z0-9]+/g,"-")}/`,lang,title:(lang === "zh" ? "博客 / " : "Journal / ")+value,posts:posts.filter(p=>kind === "tags" ? (p.data.tags || []).includes(value) : p.data.category === value)});
+ }
+ }
+ return archives;
});
-
- eleventyConfig.addPassthroughCopy("src/assets");
-
- return {
- dir: { input: "src", output: "_site" },
- markdownTemplateEngine: "njk",
- pathPrefix: "/RestClient.Net/",
- };
+ config.addCollection("publicPages", api=>api.getAll().filter(p=>p.url && !p.data.eleventyExcludeFromCollections));
+ config.addFilter("isoDate", value=>new Date(value).toISOString());
+ config.addFilter("dateFormat", value=>new Date(value).toLocaleDateString("en",{year:"numeric",month:"long",day:"numeric",timeZone:"UTC"}));
+ config.addFilter("xmlEscape", value=>String(value??"").replace(/[<>&"']/g,c=>({"<":"<",">":">","&":"&",'"':""","'":"'"}[c])));
+ config.addFilter("absoluteUrl", absoluteUrl);
+ config.addFilter("translationUrl", (url,lang,pages=[])=>{const route=(lang==="zh"?"/zh":"")+url.replace(/^\/zh(?=\/)/,"");return pages.some(p=>p.url===route)?route:(lang==="zh"?"/zh/":"/");});
+ config.addFilter("hasTranslation",(url,lang,pages=[])=>pages.some(p=>p.url===(lang==="zh"?"/zh":"")+url.replace(/^\/zh(?=\/)/,"")));
+ config.addFilter("section", url=>url.replace(/^\/zh(?=\/)/,"").split("/")[1]||"home");
+ config.addFilter("withoutHeading", html=>html.replace(/
]*>[\s\S]*?<\/h1>/,""));
+ config.addFilter("json", value=>JSON.stringify(value).replace(/`${attr}=${quote}${path.startsWith(basePath.slice(1))?"/":""}${path.startsWith(basePath.slice(1))?path:basePath+path}${quote}`);
+ });
+ return {dir:{input:"src",output:"_site"},markdownTemplateEngine:"njk",pathPrefix:basePath};
}
diff --git a/Website/examples/Directory.Build.props b/Website/examples/Directory.Build.props
new file mode 100644
index 00000000..68594e4c
--- /dev/null
+++ b/Website/examples/Directory.Build.props
@@ -0,0 +1,12 @@
+
+
+ net9.0
+ enable
+ enable
+ false
+ true
+
+
+
+
+
diff --git a/Website/examples/Examples.csproj b/Website/examples/Examples.csproj
new file mode 100644
index 00000000..c1581b5d
--- /dev/null
+++ b/Website/examples/Examples.csproj
@@ -0,0 +1,8 @@
+
+
+ Exe
+
+
+
+
+
diff --git a/Website/examples/Program.cs b/Website/examples/Program.cs
new file mode 100644
index 00000000..99e76347
--- /dev/null
+++ b/Website/examples/Program.cs
@@ -0,0 +1,86 @@
+using System.Net.Http.Json;
+using Outcome;
+using RestClient.Net;
+using Urls;
+
+namespace WebsiteExamples;
+
+public static class RestClientExamples
+{
+ // This runnable entry point calls JSONPlaceholder. Tests use a local handler instead.
+ public static async Task Main()
+ {
+ using var client = new HttpClient();
+ using var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(15));
+ var result = await GetPostAsync(client, 1, cancellation.Token);
+ Console.WriteLine(Describe(result));
+ }
+
+ public static Task>> GetPostAsync(
+ HttpClient client,
+ int id,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.GetAsync(
+ url: $"https://jsonplaceholder.typicode.com/posts/{id}".ToAbsoluteUrl(),
+ deserializeSuccess: ReadPostAsync,
+ deserializeError: ReadErrorAsync,
+ headers: new Dictionary { ["Accept"] = "application/json" },
+ cancellationToken: cancellationToken
+ );
+
+ public static Task>> CreatePostAsync(
+ HttpClient client,
+ CreatePost request,
+ CancellationToken cancellationToken = default
+ ) =>
+ client.PostAsync(
+ url: "https://jsonplaceholder.typicode.com/posts".ToAbsoluteUrl(),
+ requestBody: JsonContent.Create(request),
+ deserializeSuccess: ReadPostAsync,
+ deserializeError: ReadErrorAsync,
+ cancellationToken: cancellationToken
+ );
+
+ // Register a named client with services.AddHttpClient("posts") in your application.
+ public static Task>> GetUsingFactoryAsync(
+ IHttpClientFactory factory,
+ int id,
+ CancellationToken cancellationToken = default
+ ) =>
+ factory.GetAsync(
+ clientName: "posts",
+ url: $"https://jsonplaceholder.typicode.com/posts/{id}".ToAbsoluteUrl(),
+ deserializeSuccess: ReadPostAsync,
+ deserializeError: ReadErrorAsync,
+ cancellationToken: cancellationToken
+ );
+
+ // Both success and failure are explicit. HTTP failures retain the response body.
+ public static string Describe(Result> result) =>
+ result.Match(
+ onSuccess: post => $"Post {post.Id}: {post.Title}",
+ onError: error =>
+ error.Match(
+ onException: exception => $"Connection failed: {exception.Message}",
+ onErrorResponse: (body, status, headers) => $"HTTP {(int)status}: {body}"
+ )
+ );
+
+ // Deserialize receives HttpResponseMessage: read its Content property.
+ private static async Task ReadPostAsync(
+ HttpResponseMessage response,
+ CancellationToken cancellationToken
+ ) =>
+ await response.Content.ReadFromJsonAsync(cancellationToken)
+ ?? throw new InvalidDataException("The response contained no post.");
+
+ private static Task ReadErrorAsync(
+ HttpResponseMessage response,
+ CancellationToken cancellationToken
+ ) => response.Content.ReadAsStringAsync(cancellationToken);
+}
+
+public sealed record Post(int UserId, int Id, string Title, string Body);
+
+public sealed record CreatePost(int UserId, string Title, string Body);
diff --git a/Website/examples/Tests/Examples.Tests.csproj b/Website/examples/Tests/Examples.Tests.csproj
new file mode 100644
index 00000000..86aa4422
--- /dev/null
+++ b/Website/examples/Tests/Examples.Tests.csproj
@@ -0,0 +1,9 @@
+
+
+ Exe
+ WebsiteExamples.ExampleTests
+
+
+
+
+
diff --git a/Website/examples/Tests/Program.cs b/Website/examples/Tests/Program.cs
new file mode 100644
index 00000000..4a094feb
--- /dev/null
+++ b/Website/examples/Tests/Program.cs
@@ -0,0 +1,183 @@
+using System.Net;
+using System.Text;
+using System.Text.Json;
+using Outcome;
+
+namespace WebsiteExamples;
+
+internal static class ExampleTests
+{
+ private static int assertions;
+
+ public static async Task Main()
+ {
+ using var handler = new LocalHandler();
+ using var client = new HttpClient(handler);
+ var success = await RestClientExamples.GetPostAsync(client, 7);
+ Check(success.IsOk && !success.IsError, "GET returns a success");
+ Check(RestClientExamples.Describe(success) == "Post 7: Local post", "success display");
+ Check(handler.LastMethod == HttpMethod.Get, "GET method");
+ Check(
+ handler.LastUri == "https://jsonplaceholder.typicode.com/posts/7",
+ "absolute GET URL"
+ );
+ Check(handler.Accept == "application/json", "request headers");
+ success.Tap(post =>
+ Check(post.UserId == 3 && post.Body == "Local body", "typed response fields")
+ );
+
+ handler.Status = HttpStatusCode.NotFound;
+ var missing = await RestClientExamples.GetPostAsync(client, 404);
+ Check(missing.IsError && !missing.IsOk, "HTTP error is a failure result");
+ Check(
+ RestClientExamples.Describe(missing) == "HTTP 404: Missing post",
+ "HTTP error retains status and body"
+ );
+ missing.Tap(onError: error =>
+ {
+ Check(error.IsErrorResponse && !error.IsExceptionError, "HTTP error classification");
+ _ = error.Match(
+ onException: _ => throw new InvalidOperationException("Unexpected network error"),
+ onErrorResponse: (body, status, headers) =>
+ {
+ Check(
+ body == "Missing post" && status == HttpStatusCode.NotFound,
+ "structured HTTP error"
+ );
+ Check(
+ headers.GetValues("X-Test").Single() == "local",
+ "response headers preserved"
+ );
+ return body;
+ }
+ );
+ });
+
+ handler.Failure = new HttpRequestException("Offline fixture");
+ var offline = await RestClientExamples.GetPostAsync(client, 1);
+ Check(offline.IsError, "network exceptions become failure results");
+ Check(
+ RestClientExamples.Describe(offline) == "Connection failed: Offline fixture",
+ "network error display"
+ );
+ offline.Tap(onError: error =>
+ Check(error.IsExceptionError && !error.IsErrorResponse, "network error classification")
+ );
+
+ handler.Failure = null;
+ handler.Status = HttpStatusCode.Created;
+ var created = await RestClientExamples.CreatePostAsync(
+ client,
+ new CreatePost(3, "A new post", "New body")
+ );
+ Check(created.IsOk, "POST returns success");
+ Check(handler.LastMethod == HttpMethod.Post, "POST method");
+ Check(handler.LastUri == "https://jsonplaceholder.typicode.com/posts", "absolute POST URL");
+ Check(
+ handler.ContentType == "application/json; charset=utf-8",
+ "JSON request content type"
+ );
+ using var body = JsonDocument.Parse(handler.Body!);
+ Check(
+ body.RootElement.GetProperty("title").GetString() == "A new post",
+ "POST title serialized"
+ );
+ Check(
+ body.RootElement.GetProperty("body").GetString() == "New body",
+ "POST body serialized"
+ );
+ Check(body.RootElement.GetProperty("userId").GetInt32() == 3, "POST user ID serialized");
+
+ var factory = new LocalFactory(client);
+ var factoryResult = await RestClientExamples.GetUsingFactoryAsync(factory, 19);
+ Check(factoryResult.IsOk, "factory request succeeds");
+ Check(factory.Name == "posts", "named factory client selected");
+ Check(
+ handler.LastUri!.EndsWith("/posts/19", StringComparison.Ordinal),
+ "factory receives absolute request URL"
+ );
+
+ using var cancelled = new CancellationTokenSource();
+ cancelled.Cancel();
+ var cancellation = await RestClientExamples.GetPostAsync(client, 1, cancelled.Token);
+ Check(cancellation.IsError, "cancellation is explicit");
+ cancellation.Tap(onError: error =>
+ {
+ Check(error.IsExceptionError, "cancellation classification");
+ _ = error.Match(
+ onException: exception =>
+ {
+ Check(
+ exception is OperationCanceledException,
+ "original cancellation exception retained"
+ );
+ return "cancelled";
+ },
+ onErrorResponse: (_, _, _) =>
+ throw new InvalidOperationException("Unexpected HTTP response")
+ );
+ });
+ Check(handler.Calls == 6, "all live calls were intercepted by the local handler");
+ Console.WriteLine($"PASS: {assertions} example assertions; no network requests.");
+ }
+
+ private static void Check(bool condition, string description)
+ {
+ if (!condition)
+ throw new InvalidOperationException(description);
+ assertions++;
+ }
+
+ private sealed class LocalFactory(HttpClient client) : IHttpClientFactory
+ {
+ public string? Name { get; private set; }
+
+ public HttpClient CreateClient(string name)
+ {
+ Name = name;
+ return client;
+ }
+ }
+
+ private sealed class LocalHandler : HttpMessageHandler
+ {
+ public HttpStatusCode Status { get; set; } = HttpStatusCode.OK;
+ public Exception? Failure { get; set; }
+ public int Calls { get; private set; }
+ public HttpMethod? LastMethod { get; private set; }
+ public string? LastUri { get; private set; }
+ public string? Accept { get; private set; }
+ public string? Body { get; private set; }
+ public string? ContentType { get; private set; }
+
+ protected override async Task SendAsync(
+ HttpRequestMessage request,
+ CancellationToken cancellationToken
+ )
+ {
+ Calls++;
+ LastMethod = request.Method;
+ LastUri = request.RequestUri!.AbsoluteUri;
+ Accept = string.Join(",", request.Headers.Accept);
+ cancellationToken.ThrowIfCancellationRequested();
+ if (Failure is not null)
+ throw Failure;
+ Body = request.Content is null
+ ? null
+ : await request.Content.ReadAsStringAsync(cancellationToken);
+ ContentType = request.Content?.Headers.ContentType?.ToString();
+ var response = new HttpResponseMessage(Status)
+ {
+ Content = new StringContent(
+ Status == HttpStatusCode.NotFound
+ ? "Missing post"
+ : "{\"userId\":3,\"id\":7,\"title\":\"Local post\",\"body\":\"Local body\"}",
+ Encoding.UTF8,
+ "application/json"
+ ),
+ };
+ response.Headers.Add("X-Test", "local");
+ return response;
+ }
+ }
+}
diff --git a/Website/package-lock.json b/Website/package-lock.json
index 1ce1af13..92567d72 100644
--- a/Website/package-lock.json
+++ b/Website/package-lock.json
@@ -8,17 +8,22 @@
"name": "restclient-net-website",
"version": "1.0.0",
"dependencies": {
- "eleventy-plugin-techdoc": "^0.1.0"
+ "@11ty/eleventy-plugin-syntaxhighlight": "5.0.2",
+ "markdown-it": "14.3.2",
+ "markdown-it-anchor": "9.2.0"
},
"devDependencies": {
"@11ty/eleventy": "^3.1.2",
- "@playwright/test": "^1.40.0"
+ "@playwright/test": "^1.40.0",
+ "acorn": "8.18.0",
+ "parse5": "8.0.1"
}
},
"node_modules/@11ty/dependency-tree": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@11ty/dependency-tree/-/dependency-tree-4.0.2.tgz",
"integrity": "sha512-RTF6VTZHatYf7fSZBUN3RKwiUeJh5dhWV61gDPrHhQF2/gzruAkYz8yXuvGLx3w3ZBKreGrR+MfYpSVkdbdbLA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@11ty/eleventy-utils": "^2.0.1"
@@ -28,6 +33,7 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@11ty/dependency-tree-esm/-/dependency-tree-esm-2.0.4.tgz",
"integrity": "sha512-MYKC0Ac77ILr1HnRJalzKDlb9Z8To3kXQCltx299pUXXUFtJ1RIONtULlknknqW8cLe19DLVgmxVCtjEFm7h0A==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@11ty/eleventy-utils": "^2.0.7",
@@ -37,44 +43,45 @@
}
},
"node_modules/@11ty/eleventy": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@11ty/eleventy/-/eleventy-3.1.2.tgz",
- "integrity": "sha512-IcsDlbXnBf8cHzbM1YBv3JcTyLB35EK88QexmVyFdVJVgUU6bh9g687rpxryJirHzo06PuwnYaEEdVZQfIgRGg==",
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/@11ty/eleventy/-/eleventy-3.1.6.tgz",
+ "integrity": "sha512-ZlSiR1PLdS2lv7TelBgWAhcvMiLNZkPBlLEb+lh7kGYZ+Mk0bo9qcYgVsewvw9W7Em0RH3wd01h5fAstNDh0zA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@11ty/dependency-tree": "^4.0.0",
- "@11ty/dependency-tree-esm": "^2.0.0",
+ "@11ty/dependency-tree": "^4.0.2",
+ "@11ty/dependency-tree-esm": "^2.0.4",
"@11ty/eleventy-dev-server": "^2.0.8",
- "@11ty/eleventy-plugin-bundle": "^3.0.6",
+ "@11ty/eleventy-plugin-bundle": "^3.0.7",
"@11ty/eleventy-utils": "^2.0.7",
"@11ty/lodash-custom": "^4.17.21",
- "@11ty/posthtml-urls": "^1.0.1",
- "@11ty/recursive-copy": "^4.0.2",
+ "@11ty/posthtml-urls": "^1.0.3",
+ "@11ty/recursive-copy": "^4.0.4",
"@sindresorhus/slugify": "^2.2.1",
"bcp-47-normalize": "^2.3.0",
"chokidar": "^3.6.0",
- "debug": "^4.4.1",
+ "debug": "^4.4.3",
"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",
+ "js-yaml": "^4.1.1",
"kleur": "^4.1.5",
- "liquidjs": "^10.21.1",
- "luxon": "^3.6.1",
- "markdown-it": "^14.1.0",
+ "liquidjs": "^10.27.0",
+ "luxon": "^3.7.2",
+ "markdown-it": "^14.2.0",
"minimist": "^1.2.8",
- "moo": "^0.5.2",
+ "moo": "0.5.2",
"node-retrieve-globals": "^6.0.1",
"nunjucks": "^3.2.4",
- "picomatch": "^4.0.2",
+ "picomatch": "^4.0.4",
"please-upgrade-node": "^3.2.0",
- "posthtml": "^0.16.6",
+ "posthtml": "^0.16.7",
"posthtml-match-helper": "^2.0.3",
- "semver": "^7.7.2",
- "slugify": "^1.6.6",
- "tinyglobby": "^0.2.14"
+ "semver": "^7.8.1",
+ "slugify": "^1.6.9",
+ "tinyglobby": "^0.2.16"
},
"bin": {
"eleventy": "cmd.cjs"
@@ -91,6 +98,7 @@
"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==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@11ty/eleventy-utils": "^2.0.1",
@@ -117,32 +125,11 @@
"url": "https://opencollective.com/11ty"
}
},
- "node_modules/@11ty/eleventy-navigation": {
- "version": "0.3.5",
- "resolved": "https://registry.npmjs.org/@11ty/eleventy-navigation/-/eleventy-navigation-0.3.5.tgz",
- "integrity": "sha512-4aKW5aIQDFed8xs1G1pWcEiFPcDSwZtA4IH1eERtoJ+Xy+/fsoe0pzbDmw84bHZ9ACny5jblENhfZhcCxklqQw==",
- "license": "MIT",
- "dependencies": {
- "dependency-graph": "^0.11.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/11ty"
- }
- },
- "node_modules/@11ty/eleventy-navigation/node_modules/dependency-graph": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz",
- "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6.0"
- }
- },
"node_modules/@11ty/eleventy-plugin-bundle": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-bundle/-/eleventy-plugin-bundle-3.0.7.tgz",
"integrity": "sha512-QK1tRFBhQdZASnYU8GMzpTdsMMFLVAkuU0gVVILqNyp09xJJZb81kAS3AFrNrwBCsgLxTdWHJ8N64+OTTsoKkA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@11ty/eleventy-utils": "^2.0.2",
@@ -157,22 +144,6 @@
"url": "https://opencollective.com/11ty"
}
},
- "node_modules/@11ty/eleventy-plugin-rss": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-rss/-/eleventy-plugin-rss-2.0.4.tgz",
- "integrity": "sha512-LF60sGVlxGTryQe3hTifuzrwF8R7XbrNsM2xfcDcNMSliLN4kmB+7zvoLRySRx0AQDjqhPTAeeeT0ra6/9zHUQ==",
- "license": "MIT",
- "dependencies": {
- "@11ty/eleventy-utils": "^2.0.0",
- "@11ty/posthtml-urls": "^1.0.1",
- "debug": "^4.4.0",
- "posthtml": "^0.16.6"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/11ty"
- }
- },
"node_modules/@11ty/eleventy-plugin-syntaxhighlight": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@11ty/eleventy-plugin-syntaxhighlight/-/eleventy-plugin-syntaxhighlight-5.0.2.tgz",
@@ -190,6 +161,7 @@
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@11ty/eleventy-utils/-/eleventy-utils-2.0.7.tgz",
"integrity": "sha512-6QE+duqSQ0GY9rENXYb4iPR4AYGdrFpqnmi59tFp9VrleOl0QSh8VlBr2yd6dlhkdtj7904poZW5PvGr9cMiJQ==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -203,6 +175,7 @@
"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==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
@@ -213,9 +186,10 @@
}
},
"node_modules/@11ty/posthtml-urls": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@11ty/posthtml-urls/-/posthtml-urls-1.0.2.tgz",
- "integrity": "sha512-0vaV3Wt0surZ+oS1VdKKe0axeeupuM+l7W/Z866WFQwF+dGg2Tc/nmhk/5l74/Y55P8KyImnLN9CdygNw2huHg==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@11ty/posthtml-urls/-/posthtml-urls-1.0.3.tgz",
+ "integrity": "sha512-1YvhnkaNlFnnJic1rBMWmTC2adbuy+JQiBfl1Hecr1Wjjik1pQZmGyk/eC9zKX/FQv52s2Nht1Gi/UwhYqrBeg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"evaluate-value": "^2.0.0",
@@ -228,354 +202,21 @@
}
},
"node_modules/@11ty/recursive-copy": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/@11ty/recursive-copy/-/recursive-copy-4.0.3.tgz",
- "integrity": "sha512-SX48BTLEGX8T/OsKWORsHAAeiDsbFl79Oa/0Wg/mv/d27b7trCVZs7fMHvpSgDvZz/fZqx5rDk8+nx5oyT7xBw==",
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/@11ty/recursive-copy/-/recursive-copy-4.0.4.tgz",
+ "integrity": "sha512-oI7m8pa7/IAU/3lqRU9vjBbs20iKFo7x+1K9kT3aVira6scc1X9MjBdgLCHzLJeJ7iB6wydioA+kr9/qPnvmlQ==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"errno": "^1.0.0",
"junk": "^3.1.0",
- "maximatch": "^0.1.0",
+ "minimatch": "^3.1.5",
"slash": "^3.0.0"
},
"engines": {
"node": ">=18"
}
},
- "node_modules/@inquirer/ansi": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
- "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@inquirer/checkbox": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz",
- "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==",
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^1.0.2",
- "@inquirer/core": "^10.3.2",
- "@inquirer/figures": "^1.0.15",
- "@inquirer/type": "^3.0.10",
- "yoctocolors-cjs": "^2.1.3"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/confirm": {
- "version": "5.1.21",
- "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz",
- "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==",
- "license": "MIT",
- "dependencies": {
- "@inquirer/core": "^10.3.2",
- "@inquirer/type": "^3.0.10"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/core": {
- "version": "10.3.2",
- "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz",
- "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==",
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^1.0.2",
- "@inquirer/figures": "^1.0.15",
- "@inquirer/type": "^3.0.10",
- "cli-width": "^4.1.0",
- "mute-stream": "^2.0.0",
- "signal-exit": "^4.1.0",
- "wrap-ansi": "^6.2.0",
- "yoctocolors-cjs": "^2.1.3"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/editor": {
- "version": "4.2.23",
- "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz",
- "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==",
- "license": "MIT",
- "dependencies": {
- "@inquirer/core": "^10.3.2",
- "@inquirer/external-editor": "^1.0.3",
- "@inquirer/type": "^3.0.10"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/expand": {
- "version": "4.0.23",
- "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz",
- "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==",
- "license": "MIT",
- "dependencies": {
- "@inquirer/core": "^10.3.2",
- "@inquirer/type": "^3.0.10",
- "yoctocolors-cjs": "^2.1.3"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/external-editor": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
- "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==",
- "license": "MIT",
- "dependencies": {
- "chardet": "^2.1.1",
- "iconv-lite": "^0.7.0"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/figures": {
- "version": "1.0.15",
- "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
- "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@inquirer/input": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz",
- "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==",
- "license": "MIT",
- "dependencies": {
- "@inquirer/core": "^10.3.2",
- "@inquirer/type": "^3.0.10"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/number": {
- "version": "3.0.23",
- "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz",
- "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==",
- "license": "MIT",
- "dependencies": {
- "@inquirer/core": "^10.3.2",
- "@inquirer/type": "^3.0.10"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/password": {
- "version": "4.0.23",
- "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz",
- "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==",
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^1.0.2",
- "@inquirer/core": "^10.3.2",
- "@inquirer/type": "^3.0.10"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/prompts": {
- "version": "7.10.1",
- "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz",
- "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==",
- "license": "MIT",
- "dependencies": {
- "@inquirer/checkbox": "^4.3.2",
- "@inquirer/confirm": "^5.1.21",
- "@inquirer/editor": "^4.2.23",
- "@inquirer/expand": "^4.0.23",
- "@inquirer/input": "^4.3.1",
- "@inquirer/number": "^3.0.23",
- "@inquirer/password": "^4.0.23",
- "@inquirer/rawlist": "^4.1.11",
- "@inquirer/search": "^3.2.2",
- "@inquirer/select": "^4.4.2"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/rawlist": {
- "version": "4.1.11",
- "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz",
- "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==",
- "license": "MIT",
- "dependencies": {
- "@inquirer/core": "^10.3.2",
- "@inquirer/type": "^3.0.10",
- "yoctocolors-cjs": "^2.1.3"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/search": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz",
- "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==",
- "license": "MIT",
- "dependencies": {
- "@inquirer/core": "^10.3.2",
- "@inquirer/figures": "^1.0.15",
- "@inquirer/type": "^3.0.10",
- "yoctocolors-cjs": "^2.1.3"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/select": {
- "version": "4.4.2",
- "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz",
- "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==",
- "license": "MIT",
- "dependencies": {
- "@inquirer/ansi": "^1.0.2",
- "@inquirer/core": "^10.3.2",
- "@inquirer/figures": "^1.0.15",
- "@inquirer/type": "^3.0.10",
- "yoctocolors-cjs": "^2.1.3"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/@inquirer/type": {
- "version": "3.0.10",
- "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz",
- "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@types/node": ">=18"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
"node_modules/@playwright/test": {
"version": "1.58.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.0.tgz",
@@ -596,6 +237,7 @@
"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",
@@ -612,6 +254,7 @@
"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"
@@ -631,9 +274,9 @@
"peer": true
},
"node_modules/@types/markdown-it": {
- "version": "14.1.2",
- "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
- "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz",
+ "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -652,12 +295,14 @@
"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/acorn": {
- "version": "8.15.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
- "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
+ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
+ "dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -670,6 +315,7 @@
"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"
@@ -678,34 +324,11 @@
"node": ">=0.4.0"
}
},
- "node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/ansi-styles": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
- "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1"
- },
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/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",
@@ -716,9 +339,10 @@
}
},
"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==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -733,61 +357,25 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"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==",
- "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==",
- "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==",
- "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==",
- "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",
@@ -803,6 +391,7 @@
"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",
@@ -813,6 +402,7 @@
"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",
@@ -827,6 +417,7 @@
"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"
@@ -839,6 +430,7 @@
"version": "1.1.21",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
"integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -849,6 +441,7 @@
"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"
@@ -857,16 +450,11 @@
"node": ">=8"
}
},
- "node_modules/chardet": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
- "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
- "license": "MIT"
- },
"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",
@@ -887,37 +475,11 @@
"fsevents": "~2.3.2"
}
},
- "node_modules/cli-width": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
- "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
- "license": "ISC",
- "engines": {
- "node": ">= 12"
- }
- },
- "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==",
- "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==",
- "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"
@@ -927,12 +489,14 @@
"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/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -950,6 +514,7 @@
"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"
@@ -959,6 +524,7 @@
"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"
@@ -968,6 +534,7 @@
"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",
@@ -982,6 +549,7 @@
"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": "https://github.com/fb55/entities?sponsor=1"
@@ -991,6 +559,7 @@
"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",
@@ -1003,6 +572,7 @@
"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"
@@ -1018,6 +588,7 @@
"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",
@@ -1032,41 +603,14 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
- "license": "MIT"
- },
- "node_modules/eleventy-plugin-techdoc": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/eleventy-plugin-techdoc/-/eleventy-plugin-techdoc-0.1.0.tgz",
- "integrity": "sha512-XAMXG5j+jpOpmojAet5ZwSyB7hV5JhFkAW6viZn9nxu0BjLUwg7hMQ7MKmpyKdWZiBjOQpcHlDmSgK75pD2qTw==",
- "license": "MIT",
- "dependencies": {
- "@11ty/eleventy-navigation": "^0.3.5",
- "@11ty/eleventy-plugin-rss": "^2.0.2",
- "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0",
- "@inquirer/prompts": "^7.0.0",
- "markdown-it": "^14.1.0",
- "markdown-it-anchor": "^9.2.0"
- },
- "bin": {
- "eleventy-plugin-techdoc": "bin/init.js"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@11ty/eleventy": "^3.1.2"
- }
- },
- "node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "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"
@@ -1076,6 +620,7 @@
"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"
@@ -1088,6 +633,7 @@
"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"
@@ -1100,12 +646,14 @@
"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"
@@ -1118,6 +666,7 @@
"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"
@@ -1127,6 +676,7 @@
"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",
@@ -1140,6 +690,7 @@
"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"
@@ -1149,6 +700,7 @@
"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"
@@ -1158,6 +710,7 @@
"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"
@@ -1170,6 +723,7 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -1187,6 +741,7 @@
"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"
@@ -1196,6 +751,7 @@
"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"
@@ -1208,6 +764,7 @@
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"debug": "2.6.9",
@@ -1226,6 +783,7 @@
"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"
@@ -1235,12 +793,14 @@
"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/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"
@@ -1250,6 +810,7 @@
"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,
@@ -1264,6 +825,7 @@
"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"
@@ -1276,6 +838,7 @@
"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",
@@ -1291,6 +854,7 @@
"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"
@@ -1300,6 +864,7 @@
"version": "3.15.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz",
"integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^1.0.7",
@@ -1313,6 +878,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz",
"integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==",
+ "dev": true,
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
{
@@ -1332,6 +898,7 @@
"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"
@@ -1344,6 +911,7 @@
"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"
@@ -1353,6 +921,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
@@ -1369,32 +938,18 @@
"url": "https://opencollective.com/express"
}
},
- "node_modules/iconv-lite": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
- "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"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",
@@ -1405,6 +960,7 @@
"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",
@@ -1419,6 +975,7 @@
"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"
@@ -1431,6 +988,7 @@
"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",
@@ -1441,6 +999,7 @@
"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"
@@ -1450,24 +1009,17 @@
"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==",
- "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"
@@ -1480,12 +1032,14 @@
"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-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"
@@ -1495,6 +1049,7 @@
"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"
@@ -1504,6 +1059,7 @@
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
+ "dev": true,
"funding": [
{
"type": "github",
@@ -1526,6 +1082,7 @@
"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"
@@ -1535,6 +1092,7 @@
"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"
@@ -1544,6 +1102,7 @@
"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"
@@ -1572,6 +1131,7 @@
"version": "10.30.0",
"resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-10.30.0.tgz",
"integrity": "sha512-Fw4wA+8CZsJkaeLEIlPE3AoponfjEZhj7FQvYNcAEcg7jNRik7mEMoaco382FBdrYlykVf6w4vClPNctz2OYjg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"commander": "^10.0.0"
@@ -1592,12 +1152,14 @@
"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/luxon": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -1652,21 +1214,6 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
- "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==",
- "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",
@@ -1677,6 +1224,7 @@
"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"
@@ -1689,6 +1237,7 @@
"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"
@@ -1698,6 +1247,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
@@ -1711,9 +1261,10 @@
}
},
"node_modules/minimatch": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
- "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@@ -1726,6 +1277,7 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -1735,6 +1287,7 @@
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -1744,33 +1297,28 @@
"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.8",
"resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.7.8.tgz",
"integrity": "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg==",
+ "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==",
+ "dev": true,
"license": "MIT"
},
- "node_modules/mute-stream": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
- "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
- "license": "ISC",
- "engines": {
- "node": "^18.17.0 || >=20.5.0"
- }
- },
"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",
@@ -1782,6 +1330,7 @@
"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"
@@ -1791,6 +1340,7 @@
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz",
"integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==",
+ "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"a-sync-waterfall": "^1.0.0",
@@ -1816,6 +1366,7 @@
"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"
@@ -1825,6 +1376,7 @@
"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": {
"ee-first": "1.1.1"
@@ -1837,21 +1389,50 @@
"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/parse5": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
+ "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^8.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
+ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"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",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -1911,6 +1492,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
"integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"semver-compare": "^1.0.0"
@@ -1920,6 +1502,7 @@
"version": "0.16.7",
"resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz",
"integrity": "sha512-7Hc+IvlQ7hlaIfQFZnxlRl0jnpWq2qwibORBhQYIb0QbNtuicc5ZxvKkVT71HJ4Py1wSZ/3VR1r8LfkCtoCzhw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"posthtml-parser": "^0.11.0",
@@ -1933,6 +1516,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/posthtml-match-helper/-/posthtml-match-helper-2.0.3.tgz",
"integrity": "sha512-p9oJgTdMF2dyd7WE54QI1LvpBIkNkbSiiECKezNnDVYhGhD1AaOnAkw0Uh0y5TW+OHO8iBdSqnd8Wkpb6iUqmw==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -1945,6 +1529,7 @@
"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==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"htmlparser2": "^7.1.1"
@@ -1957,6 +1542,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-3.0.0.tgz",
"integrity": "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"is-json": "^2.0.1"
@@ -1978,6 +1564,7 @@
"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": {
@@ -1993,6 +1580,7 @@
"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": {
"node": ">= 0.6"
@@ -2002,6 +1590,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
@@ -2011,9 +1600,10 @@
}
},
"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==",
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -2022,16 +1612,11 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "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",
@@ -2042,9 +1627,10 @@
}
},
"node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -2057,12 +1643,14 @@
"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.1",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.4.3",
@@ -2089,33 +1677,24 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "dev": true,
"license": "ISC"
},
- "node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/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==",
+ "version": "1.6.9",
+ "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.9.tgz",
+ "integrity": "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8.0.0"
@@ -2125,12 +1704,14 @@
"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"
@@ -2143,54 +1724,31 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
- "node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "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/tinyglobby": {
- "version": "0.2.15",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
- "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
- "picomatch": "^4.0.3"
+ "picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
@@ -2203,6 +1761,7 @@
"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"
@@ -2215,6 +1774,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.6"
@@ -2230,6 +1790,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
@@ -2239,26 +1800,14 @@
"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/wrap-ansi": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
- "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/ws": {
- "version": "8.19.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
- "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+ "version": "8.22.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
+ "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -2275,18 +1824,6 @@
"optional": true
}
}
- },
- "node_modules/yoctocolors-cjs": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
- "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
}
}
}
diff --git a/Website/package.json b/Website/package.json
index dcb947d6..3c7746d9 100644
--- a/Website/package.json
+++ b/Website/package.json
@@ -5,15 +5,21 @@
"type": "module",
"scripts": {
"generate-api": "node scripts/generate-api-docs.js",
- "dev": "lsof -ti:8080 | xargs kill -9 2>/dev/null; npx @11ty/eleventy --serve --port=8080",
- "build": "npm run generate-api && npx @11ty/eleventy",
- "test": "playwright test"
- },
- "dependencies": {
- "eleventy-plugin-techdoc": "^0.1.0"
+ "dev": "npm run generate-api && eleventy --serve --port=4173",
+ "build": "npm run generate-api && npm run clean && eleventy && node scripts/check-site.js",
+ "test:unit": "node --test tests-node/*.test.js",
+ "test": "playwright test",
+ "clean": "node -e \"require('node:fs').rmSync('_site', {recursive: true, force: true})\""
},
"devDependencies": {
"@11ty/eleventy": "^3.1.2",
- "@playwright/test": "^1.40.0"
+ "@playwright/test": "^1.40.0",
+ "acorn": "8.18.0",
+ "parse5": "8.0.1"
+ },
+ "dependencies": {
+ "@11ty/eleventy-plugin-syntaxhighlight": "5.0.2",
+ "markdown-it": "14.3.2",
+ "markdown-it-anchor": "9.2.0"
}
}
diff --git a/Website/playwright.config.js b/Website/playwright.config.js
index 94f93b70..b585e19e 100644
--- a/Website/playwright.config.js
+++ b/Website/playwright.config.js
@@ -1,20 +1,23 @@
import { defineConfig } from '@playwright/test';
+const baseURL = process.env.WEBSITE_TEST_BASE_URL || 'http://127.0.0.1:4173';
+const port = new URL(baseURL).port || '4173';
+
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
- workers: process.env.CI ? 1 : undefined,
+ workers: process.env.CI ? 2 : 4,
reporter: 'list',
use: {
- baseURL: 'http://localhost:8080',
+ baseURL,
trace: 'on-first-retry',
},
webServer: {
- command: 'npx @11ty/eleventy --serve --port=8080',
- url: 'http://localhost:8080',
- reuseExistingServer: !process.env.CI,
+ command: `python3 -m http.server ${port} --bind 127.0.0.1 --directory _site`,
+ url: baseURL,
+ reuseExistingServer: false,
timeout: 120000,
},
});
diff --git a/Website/scripts/check-site.js b/Website/scripts/check-site.js
new file mode 100644
index 00000000..d0cf1224
--- /dev/null
+++ b/Website/scripts/check-site.js
@@ -0,0 +1,185 @@
+#!/usr/bin/env node
+import { readFile, readdir, stat } from 'node:fs/promises';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { parse as parseHtml } from 'parse5';
+import { parse as parseJavaScript } from 'acorn';
+
+export const CSS_BUDGET = 2500;
+
+async function filesUnder(directory) {
+ const entries = await readdir(directory, { withFileTypes: true });
+ return (await Promise.all(entries.map(entry => entry.isDirectory()
+ ? filesUnder(path.join(directory, entry.name)) : path.join(directory, entry.name)))).flat();
+}
+
+function elements(document) {
+ const result = [];
+ const visit = node => {
+ if (node.tagName) result.push(node);
+ for (const child of node.childNodes ?? []) visit(child);
+ if (node.content) visit(node.content);
+ };
+ visit(document);
+ return result;
+}
+
+const attribute = (node, name) => node.attrs?.find(item => item.name === name)?.value;
+const content = node => node.nodeName === '#text' ? node.value : (node.childNodes ?? []).map(content).join('');
+const routeFor = file => `/${file.replaceAll(path.sep, '/').replace(/index\.html$/, '')}`;
+const decodeXml = value => value.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>');
+
+export function cssInjectionErrors(source, filename = 'script.js') {
+ const errors = [];
+ let tree;
+ try { tree = parseJavaScript(source, { ecmaVersion: 'latest', sourceType: 'module' }); }
+ catch (error) { return [`${filename}: invalid JavaScript: ${error.message}`]; }
+ const literal = node => node?.type === 'Literal' ? node.value : node?.type === 'BinaryExpression' && node.operator === '+' && typeof literal(node.left) === 'string' && typeof literal(node.right) === 'string' ? literal(node.left) + literal(node.right) : undefined;
+ const property = node => node?.computed ? literal(node.property) : node?.property?.name;
+ const forbidden = new Set(['style', 'cssText', 'styleSheet', 'adoptedStyleSheets', 'CSSStyleSheet', 'insertRule', 'addRule', 'replaceSync', 'setProperty', 'animate']);
+ const visit = node => {
+ if (!node || typeof node !== 'object') return;
+ if (node.type === 'MemberExpression' && forbidden.has(property(node))) errors.push(`${filename}: CSS injection through ${property(node)} is outside the stylesheet budget`);
+ if (node.type === 'Identifier' && node.name === 'CSSStyleSheet') errors.push(`${filename}: constructed stylesheets bypass the CSS budget`);
+ if (node.type === 'CallExpression' && property(node.callee) === 'createElement' && node.arguments[0]?.value?.toLowerCase() === 'style') errors.push(`${filename}: dynamically created style element`);
+ if (node.type === 'CallExpression' && property(node.callee) === 'setAttribute' && node.arguments[0]?.value?.toLowerCase() === 'style') errors.push(`${filename}: dynamically created style attribute`);
+ if (node.type === 'AssignmentExpression' && property(node.left) === 'rel' && literal(node.right) === 'stylesheet') errors.push(`${filename}: dynamically created stylesheet link`);
+ if (node.type === 'CallExpression' && property(node.callee) === 'setAttribute' && literal(node.arguments[0]) === 'rel' && literal(node.arguments[1]) === 'stylesheet') errors.push(`${filename}: dynamically created stylesheet link`);
+ const value = node.type === 'Literal' ? node.value : node.type === 'TemplateElement' ? node.value.raw : null;
+ if (typeof value === 'string' && /', 'Docs
']) {
+ await site.write('index.html', site.html(body));
+ assert.match((await site.run()).errors.join('\n'), /inline CSS bypasses/);
+ }
+});
+
+test('external styles, data CSS, imports, and stylesheet preloads cannot bypass budget', async t => {
+ const site = await fixture(t);
+ for (const link of ['', '', '']) {
+ await site.write('index.html', site.html('Docs
', link));
+ assert.match((await site.run()).errors.join('\n'), /external or embedded stylesheet/);
+ }
+ await site.write('index.html', site.html());
+ await site.write('assets/site.css', '@import "https://cdn.test/theme.css";');
+ assert.match((await site.run()).errors.join('\n'), /unbudgeted CSS/);
+});
+
+test('JavaScript CSS injection is rejected across DOM, CSSOM, and generated markup', () => {
+ for (const source of [
+ 'document.body.style.color="red";',
+ 'document.body["style"].cssText="color:red";',
+ 'document.body["st"+"yle"].color="red";',
+ 'document.createElement("style");',
+ 'document["createElement"]("style");',
+ 'document.body.setAttribute("style","color:red");',
+ 'new CSSStyleSheet().replaceSync("body{}");',
+ 'document.adoptedStyleSheets=[];',
+ 'sheet.insertRule("body{}");',
+ 'document.body.animate([{opacity:0},{opacity:1}],100);',
+ 'document.body.innerHTML=`hi`;',
+ 'link.rel="stylesheet";',
+ 'link.setAttribute("rel","stylesheet");',
+ ]) assert.ok(cssInjectionErrors(source).length > 0, source);
+});
+
+test('native canvas rendering and comment text do not count as CSS injection', () => {
+ const source = '// document.body.style.color="red";\nconst canvas=document.querySelector("canvas");const ctx=canvas.getContext("2d");ctx.fillStyle="#123";ctx.fillRect(0,0,10,10);requestAnimationFrame(()=>{});';
+ assert.deepEqual(cssInjectionErrors(source), []);
+ assert.match(cssInjectionErrors('const = ;').join('\n'), /invalid JavaScript/);
+});
+
+test('external scripts cannot outsource hidden CSS', async t => {
+ const site = await fixture(t);
+ await site.write('index.html', site.html('Docs
', ''));
+ assert.match((await site.run()).errors.join('\n'), /external JavaScript/);
+});
+
+test('SVG assets cannot conceal extra CSS outside the shared budget', async t => {
+ const site = await fixture(t);
+ for (const svg of ['', '']) {
+ await site.write('assets/graphic.svg', svg);
+ assert.match((await site.run()).errors.join('\n'), /SVG styles bypass/);
+ }
+ await site.write('assets/graphic.svg', '');
+ assert.deepEqual((await site.run()).errors, []);
+});
+
+test('local script injection is checked even when the initial HTML is clean', async t => {
+ const site = await fixture(t);
+ await site.write('assets/motion.js', 'document.body.style.background="red";');
+ assert.match((await site.run()).errors.join('\n'), /CSS injection/);
+});
+
+test('root and project-prefix deployments use real canonical and internal routes', async t => {
+ for (const prefix of ['/', '/RestClient.Net/']) {
+ const site = await fixture(t, { prefix });
+ await site.write('index.html', site.html(`Docs
Top`));
+ assert.deepEqual((await site.run()).errors, []);
+ if (prefix !== '/') {
+ await site.write('index.html', site.html('Docs
Wrong deployment root'));
+ assert.match((await site.run()).errors.join('\n'), /escapes deployment prefix/);
+ }
+ }
+});
+
+test('missing local routes and anchor fragments fail the build', async t => {
+ const site = await fixture(t);
+ await site.write('index.html', site.html('Docs
MissingMissing heading'));
+ const errors = (await site.run()).errors.join('\n');
+ assert.match(errors, /broken internal link/);
+ assert.match(errors, /missing anchor/);
+});
+
+test('hreflang cannot advertise a missing translation', async t => {
+ const site = await fixture(t);
+ await site.write('index.html', site.html('Docs
', ''));
+ const errors = (await site.run()).errors.join('\n');
+ assert.match(errors, /hreflang does not point to a real local page/);
+ assert.match(errors, /broken internal link/);
+});
+
+test('canonical, structured data, and crawl output regressions fail visibly', async t => {
+ const site = await fixture(t);
+ await site.write('index.html', site.html().replace('rel="canonical"', 'rel="unrelated"').replace('"@context":', 'broken:'));
+ await site.write('sitemap.xml', 'https://example.test/missing/');
+ await site.write('robots.txt', 'User-agent: *');
+ const errors = (await site.run()).errors.join('\n');
+ assert.match(errors, /canonical must be/);
+ assert.match(errors, /invalid JSON-LD/);
+ assert.match(errors, /Sitemap has a nonexistent page/);
+ assert.match(errors, /Sitemap omits/);
+ assert.match(errors, /wrong sitemap/);
+});
diff --git a/Website/tests/api.test.js b/Website/tests/api.test.js
index 6c05b379..75342e62 100644
--- a/Website/tests/api.test.js
+++ b/Website/tests/api.test.js
@@ -1,7 +1,7 @@
/**
* API Reference Tests
*/
-import { test, expect } from '@playwright/test';
+import { test, expect } from './fixtures.js';
test.describe('API Reference', () => {
test('API index loads', async ({ page }) => {
@@ -12,7 +12,9 @@ test.describe('API Reference', () => {
test('API page has title', async ({ page }) => {
await page.goto('/api/');
const h1 = page.locator('h1');
- await expect(h1).toContainText('API');
+ await expect(h1).toHaveText('Know every signature.');
+ await expect(page).toHaveTitle(/API reference/);
+ await expect(page.locator('main')).toContainText('Source API reference');
});
test('API page has package links', async ({ page }) => {
diff --git a/Website/tests/blog.test.js b/Website/tests/blog.test.js
index 8d843cc3..490ad704 100644
--- a/Website/tests/blog.test.js
+++ b/Website/tests/blog.test.js
@@ -1,7 +1,7 @@
/**
* Blog Tests
*/
-import { test, expect } from '@playwright/test';
+import { test, expect } from './fixtures.js';
test.describe('Blog', () => {
test('blog index loads', async ({ page }) => {
@@ -12,12 +12,13 @@ test.describe('Blog', () => {
test('blog has title', async ({ page }) => {
await page.goto('/blog/');
const h1 = page.locator('h1');
- await expect(h1).toContainText('Blog');
+ await expect(h1).toHaveText('Ideas worth building on.');
+ await expect(page).toHaveTitle(/Journal/);
});
test('blog has post list', async ({ page }) => {
await page.goto('/blog/');
- const posts = page.locator('.post-list li, ul li a');
+ const posts = page.locator('article.feature-card');
const count = await posts.count();
expect(count).toBeGreaterThan(0);
});
diff --git a/Website/tests/chinese-i18n.test.js b/Website/tests/chinese-i18n.test.js
index 2583e7a5..47e66903 100644
--- a/Website/tests/chinese-i18n.test.js
+++ b/Website/tests/chinese-i18n.test.js
@@ -1,7 +1,7 @@
/**
* Chinese i18n Tests
*/
-import { test, expect } from '@playwright/test';
+import { test, expect } from './fixtures.js';
test.describe('Chinese Homepage', () => {
test('/zh/ loads', async ({ page }) => {
@@ -67,13 +67,13 @@ test.describe('Chinese API', () => {
test.describe('Language Selector', () => {
test('language selector exists', async ({ page }) => {
await page.goto('/');
- const langBtn = page.locator('.language-btn, .language-switcher button, [aria-label*="language"]');
+ const langBtn = page.locator('.language-switcher summary');
await expect(langBtn.first()).toBeVisible();
});
test('language dropdown has Chinese option', async ({ page }) => {
await page.goto('/');
- const langBtn = page.locator('.language-btn, .language-switcher button').first();
+ const langBtn = page.locator('.language-switcher summary').first();
await langBtn.click();
const zhOption = page.locator('.language-dropdown a[lang="zh"], .language-dropdown a:has-text("中文")');
await expect(zhOption.first()).toBeVisible();
diff --git a/Website/tests/docs.test.js b/Website/tests/docs.test.js
index 54fc67e2..32a64ca2 100644
--- a/Website/tests/docs.test.js
+++ b/Website/tests/docs.test.js
@@ -1,7 +1,7 @@
/**
* Documentation Tests
*/
-import { test, expect } from '@playwright/test';
+import { test, expect } from './fixtures.js';
test.describe('Documentation', () => {
test('docs index loads', async ({ page }) => {
diff --git a/Website/tests/fixtures.js b/Website/tests/fixtures.js
new file mode 100644
index 00000000..0fed2da8
--- /dev/null
+++ b/Website/tests/fixtures.js
@@ -0,0 +1,25 @@
+import { test as base, expect } from '@playwright/test';
+
+export const test = base.extend({
+ browserHealth: [async ({ page }, use) => {
+ const errors = [];
+ page.on('pageerror', error => errors.push(error.message));
+ page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); });
+ await use();
+ expect(errors, 'Every interaction must finish without browser errors').toEqual([]);
+ if (await page.locator('meta[name="viewport"]').count()) {
+ await expect(page.locator('main')).toHaveCount(1);
+ await expect(page.locator('h1')).toHaveCount(1);
+ await expect(page.locator('html')).toHaveAttribute('lang', /^(en|zh)$/);
+ await expect(page).toHaveTitle(/RestClient\.Net/);
+ await expect(page.locator('meta[name="description"]')).toHaveAttribute('content', /\S/);
+ await expect(page.locator('link[rel="canonical"]')).toHaveCount(1);
+ await expect(page.locator('link[rel="stylesheet"]')).toHaveCount(1);
+ await expect(page.locator('style,[style]'), 'Styles must stay inside the 2500-byte stylesheet budget').toHaveCount(0);
+ const remoteStyles = await page.evaluate(() => [...document.styleSheets].filter(sheet => !sheet.href || new URL(sheet.href).origin !== location.origin).length);
+ expect(remoteStyles).toBe(0);
+ }
+ }, { auto: true }],
+});
+
+export { expect };
diff --git a/Website/tests/homepage.test.js b/Website/tests/homepage.test.js
index 3c0f1783..d48e5e14 100644
--- a/Website/tests/homepage.test.js
+++ b/Website/tests/homepage.test.js
@@ -1,7 +1,7 @@
/**
* Homepage Tests
*/
-import { test, expect } from '@playwright/test';
+import { test, expect } from './fixtures.js';
test.describe('Homepage', () => {
test('homepage loads', async ({ page }) => {
diff --git a/Website/tests/motion-prose.test.js b/Website/tests/motion-prose.test.js
new file mode 100644
index 00000000..5b567219
--- /dev/null
+++ b/Website/tests/motion-prose.test.js
@@ -0,0 +1,195 @@
+import { test, expect } from './fixtures.js';
+import { mkdir } from 'node:fs/promises';
+import path from 'node:path';
+
+const pixels = canvas => canvas.evaluate(element => {
+ const data = element.toDataURL();
+ let hash = 2166136261;
+ for (let index = 0; index < data.length; index++) hash = Math.imul(hash ^ data.charCodeAt(index), 16777619);
+ return hash >>> 0;
+});
+
+for (const viewport of [{ width: 1280, height: 800 }, { width: 375, height: 667 }]) {
+ test(`all outcome edit cycles change real canvas pixels at ${viewport.width}px`, async ({ page }) => {
+ await page.setViewportSize(viewport);
+ await page.emulateMedia({ reducedMotion: 'reduce' });
+ const outgoing = [];
+ page.on('request', request => { if (['fetch', 'xhr'].includes(request.resourceType())) outgoing.push(request.url()); });
+ await page.goto('/');
+ const canvas = page.locator('#flow-canvas');
+ await canvas.scrollIntoViewIfNeeded();
+ await expect(canvas).toHaveAttribute('data-motion', 'paused');
+ await expect(page.locator('#motion-toggle')).toHaveAttribute('aria-pressed', 'true');
+ let previous = await pixels(canvas);
+ const signatures = new Map();
+ for (const [outcome, status, branch] of [
+ ['response', '404 Not Found', 'ResponseErrorPost'],
+ ['exception', 'Connection failed', 'ExceptionErrorPost'],
+ ['success', '200 OK', 'OkPost'],
+ ['response', '404 Not Found', 'ResponseErrorPost'],
+ ['success', '200 OK', 'OkPost'],
+ ]) {
+ const button = page.locator(`button[data-outcome="${outcome}"]`);
+ await expect(button).toBeEnabled();
+ await button.click();
+ await expect(button).toHaveAttribute('aria-pressed', 'true');
+ await expect(page.locator('button[data-outcome][aria-pressed="true"]')).toHaveCount(1);
+ await expect(page.locator('button[data-outcome][aria-pressed="false"]')).toHaveCount(2);
+ await expect(page.locator('#outcome-status')).toContainText(status);
+ await expect(page.locator('#outcome-status')).toHaveAttribute('aria-atomic', 'true');
+ await expect(page.locator('#outcome-code strong')).toHaveCount(1);
+ await expect(page.locator('#outcome-code strong')).toContainText(branch);
+ await expect(canvas).toHaveAttribute('data-outcome', outcome);
+ await expect(canvas).toHaveAttribute('data-motion', 'paused');
+ const current = await pixels(canvas);
+ expect(current, 'Changing outcome must redraw real graphics').not.toBe(previous);
+ if (signatures.has(outcome)) expect(current, 'Returning to a paused outcome must restore its exact rendering').toBe(signatures.get(outcome));
+ signatures.set(outcome, current);
+ previous = current;
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
+ await expect(page.locator('style,[style]')).toHaveCount(0);
+ }
+ expect(new Set(signatures.values()).size).toBe(3);
+ expect(outgoing).toEqual([]);
+ });
+}
+
+test('animation changes pixels while playing and freezes across pause/resume cycles', async ({ page }) => {
+ await page.goto('/');
+ const canvas = page.locator('#flow-canvas');
+ await canvas.scrollIntoViewIfNeeded();
+ const toggle = page.locator('#motion-toggle');
+ await expect(canvas).toHaveAttribute('data-motion', 'running');
+ let initial = await pixels(canvas);
+ await expect.poll(() => pixels(canvas)).not.toBe(initial);
+ for (let cycle = 0; cycle < 2; cycle++) {
+ await toggle.click();
+ await expect(toggle).toHaveText('Play motion');
+ await expect(toggle).toHaveAttribute('aria-pressed', 'true');
+ await expect(canvas).toHaveAttribute('data-motion', 'paused');
+ const paused = await pixels(canvas);
+ await page.waitForTimeout(180);
+ expect(await pixels(canvas), 'Paused motion must not keep rendering changing pixels').toBe(paused);
+ await toggle.click();
+ await expect(toggle).toHaveText('Pause motion');
+ await expect(toggle).toHaveAttribute('aria-pressed', 'false');
+ await expect(canvas).toHaveAttribute('data-motion', 'running');
+ initial = await pixels(canvas);
+ await expect.poll(() => pixels(canvas)).not.toBe(initial);
+ }
+});
+
+test('reduced-motion preference freezes graphics and changing the preference updates behavior', async ({ page }) => {
+ await page.emulateMedia({ reducedMotion: 'reduce' });
+ await page.goto('/');
+ const canvas = page.locator('#flow-canvas');
+ await canvas.scrollIntoViewIfNeeded();
+ await expect(canvas).toHaveAttribute('data-motion', 'paused');
+ const initial = await pixels(canvas);
+ await page.waitForTimeout(180);
+ expect(await pixels(canvas)).toBe(initial);
+ expect(await page.locator('html').evaluate(element => getComputedStyle(element).scrollBehavior)).toBe('auto');
+ await page.emulateMedia({ reducedMotion: 'no-preference' });
+ await expect(canvas).toHaveAttribute('data-motion', 'running');
+ await expect.poll(() => pixels(canvas)).not.toBe(initial);
+ await page.emulateMedia({ reducedMotion: 'reduce' });
+ await expect(canvas).toHaveAttribute('data-motion', 'paused');
+ const stopped = await pixels(canvas);
+ await page.waitForTimeout(180);
+ expect(await pixels(canvas)).toBe(stopped);
+});
+
+test('keyboard outcome selection keeps code, live status, and selected graphics synchronized', async ({ page }) => {
+ await page.goto('/');
+ for (const [outcome, text] of [['exception', 'ExceptionError'], ['response', 'ResponseError'], ['success', '200 OK']]) {
+ const button = page.locator(`button[data-outcome="${outcome}"]`);
+ await button.focus();
+ await page.keyboard.press('Space');
+ await expect(button).toBeFocused();
+ await expect(button).toHaveAttribute('aria-pressed', 'true');
+ await expect(page.locator('#outcome-status')).toContainText(text);
+ await expect(page.locator('#flow-canvas')).toHaveAttribute('data-outcome', outcome);
+ await expect(page.locator('button[data-outcome][aria-pressed="true"]')).toHaveCount(1);
+ }
+});
+
+test('docs, blog, and source API reference share the exact same prose CSS', async ({ page }) => {
+ const samples = [];
+ const screenshots = path.resolve('../.artifacts/website-review');
+ await mkdir(screenshots, { recursive: true });
+ for (const [route, name] of [['/docs/', 'docs'], ['/blog/introducing-restclient/', 'blog'], ['/api/reference/restclient-net-httpclientextensions/', 'api']]) {
+ await page.goto(route);
+ await expect(page.locator('.prose')).toHaveCount(1);
+ await expect(page.locator('.prose h1')).toHaveCount(1);
+ const styles = {};
+ for (const selector of ['.prose > p:not(.eyebrow)', '.prose h2', '.prose pre']) {
+ const element = page.locator(selector).first();
+ await expect(element).toBeVisible();
+ styles[selector] = await element.evaluate(node => {
+ const style = getComputedStyle(node);
+ return Object.fromEntries(['fontFamily', 'fontSize', 'fontWeight', 'lineHeight', 'color', 'letterSpacing'].map(key => [key, style[key]]));
+ });
+ }
+ samples.push(styles);
+ await page.screenshot({ path: path.join(screenshots, `${name}-desktop.png`), fullPage: true });
+ await page.setViewportSize({ width: 375, height: 667 });
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
+ await page.screenshot({ path: path.join(screenshots, `${name}-mobile.png`), fullPage: true });
+ await page.setViewportSize({ width: 1280, height: 720 });
+ }
+ expect(samples[1]).toEqual(samples[0]);
+ expect(samples[2]).toEqual(samples[0]);
+});
+
+test('core content and navigation remain usable with JavaScript disabled', async ({ browser, baseURL }) => {
+ const context = await browser.newContext({ javaScriptEnabled: false });
+ const page = await context.newPage();
+ await page.goto(baseURL);
+ await expect(page.locator('.hero h1')).toBeVisible();
+ const fallback = page.locator('noscript p');
+ await expect(fallback).toBeVisible();
+ expect(await fallback.textContent()).toContain('A request produces a success');
+ await expect(page.locator('#outcome-code')).toContainText('OkPost');
+ await expect(page.locator('button[data-outcome]')).toHaveCount(3);
+ for (const button of await page.locator('button[data-outcome]').all()) await expect(button).toBeDisabled();
+ await page.locator('.hero-actions a').first().click();
+ await expect(page.locator('.prose')).toBeVisible();
+ await expect(page.locator('.prose pre code').first()).toBeVisible();
+ await context.close();
+});
+
+test('every generated API type and its long qualified heading fit mobile width', async ({ page, request }) => {
+ test.setTimeout(60000);
+ await page.setViewportSize({ width: 375, height: 667 });
+ const response = await request.get('/api/reference/api.json');
+ expect(response.status()).toBe(200);
+ const api = await response.json();
+ expect(api.types.length).toBeGreaterThan(20);
+ expect(api.types.some(type => type.displayName.includes('OpenApiCodeGenerator'))).toBe(true);
+ for (const type of api.types) {
+ const loaded = await page.goto(type.url);
+ expect(loaded.status(), type.url).toBe(200);
+ await expect(page.locator('.prose h1')).toHaveText(type.displayName);
+ const bounds = await page.locator('.prose h1').boundingBox();
+ expect(bounds.width, type.url).toBeLessThanOrEqual(375);
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), type.url).toBe(true);
+ await expect(page.locator('.prose pre code').first()).toBeVisible();
+ await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', new RegExp(`${type.url}$`));
+ }
+});
+
+test('page lifecycle suspension freezes graphics and restoration resumes real motion', async ({ page }) => {
+ await page.goto('/');
+ const canvas = page.locator('#flow-canvas');
+ await canvas.scrollIntoViewIfNeeded();
+ const running = await pixels(canvas);
+ await expect.poll(() => pixels(canvas)).not.toBe(running);
+ await page.evaluate(() => dispatchEvent(new PageTransitionEvent('pagehide', { persisted: true })));
+ const suspended = await pixels(canvas);
+ await page.waitForTimeout(180);
+ expect(await pixels(canvas)).toBe(suspended);
+ await page.evaluate(() => dispatchEvent(new PageTransitionEvent('pageshow', { persisted: true })));
+ await expect.poll(() => pixels(canvas)).not.toBe(suspended);
+ await expect(page.locator('#motion-toggle')).toHaveAttribute('aria-pressed', 'false');
+ await expect(canvas).toHaveAttribute('data-motion', 'running');
+});
diff --git a/Website/tests/seo.test.js b/Website/tests/seo.test.js
index 9ada13cb..b7cda881 100644
--- a/Website/tests/seo.test.js
+++ b/Website/tests/seo.test.js
@@ -1,7 +1,7 @@
/**
* SEO Tests
*/
-import { test, expect } from '@playwright/test';
+import { test, expect } from './fixtures.js';
test.describe('SEO Files', () => {
test('robots.txt exists', async ({ page }) => {
diff --git a/Website/tests/syntax-highlighting.test.js b/Website/tests/syntax-highlighting.test.js
index ce963c60..792a0973 100644
--- a/Website/tests/syntax-highlighting.test.js
+++ b/Website/tests/syntax-highlighting.test.js
@@ -3,7 +3,7 @@
* Verifies that ALL pages with code blocks have proper Prism syntax highlighting
* NO SKIPPING - FAIL HARD if anything is wrong!
*/
-import { test, expect } from '@playwright/test';
+import { test, expect } from './fixtures.js';
// All pages that MUST contain code blocks with syntax highlighting
// If any page doesn't have code or highlighting, TEST FAILS!
diff --git a/Website/tests/visual-qa.test.js b/Website/tests/visual-qa.test.js
index 545a6ede..ee8a902c 100644
--- a/Website/tests/visual-qa.test.js
+++ b/Website/tests/visual-qa.test.js
@@ -1,179 +1,194 @@
-import { test, expect } from '@playwright/test';
-
-const baseUrl = 'http://localhost:8080';
-
-test.describe('Visual QA Checks', () => {
- test('homepage hero section renders correctly', async ({ page }) => {
- await page.goto(baseUrl);
- const hero = page.locator('.hero');
- await expect(hero).toBeVisible();
- await expect(page.locator('.hero-logo')).toBeVisible();
- await expect(page.locator('.hero-tagline')).toBeVisible();
- await expect(page.locator('.hero-actions')).toBeVisible();
- });
-
- test('homepage feature cards have proper styling', async ({ page }) => {
- await page.goto(baseUrl);
- const cards = page.locator('.feature-card');
- await expect(cards.first()).toBeVisible();
- const count = await cards.count();
- expect(count).toBeGreaterThanOrEqual(6);
- });
-
- test('Why Discriminated Unions section renders', async ({ page }) => {
- await page.goto(baseUrl);
- const unionSection = page.locator('text=Why Discriminated Unions?');
- await expect(unionSection).toBeVisible();
-
- // Check both code examples exist
- const withoutExhaustion = page.locator('text=Without Exhaustion');
- const withExhaustion = page.locator('text=With Exhaustion');
- await expect(withoutExhaustion).toBeVisible();
- await expect(withExhaustion).toBeVisible();
-
- // Check code blocks in this section exist
- const codeBlocks = page.locator('.feature-card pre code');
- const codeCount = await codeBlocks.count();
- expect(codeCount).toBeGreaterThanOrEqual(2);
- });
-
- test('navigation links work', async ({ page }) => {
- await page.goto(baseUrl);
-
- // Check docs link
- await page.click('a[href="/docs/"]');
- await expect(page).toHaveURL(/\/docs\//);
-
- // Check API link
- await page.click('a[href="/api/"]');
- await expect(page).toHaveURL(/\/api\//);
-
- // Check blog link
- await page.click('a[href="/blog/"]');
- await expect(page).toHaveURL(/\/blog\//);
- });
-
- test('navigation active state is exclusive - only ONE nav item should be active per page', async ({ page }) => {
- // Test home page - only Home should be active
- await page.goto(baseUrl);
- let activeLinks = page.locator('.nav-link.active');
- await expect(activeLinks).toHaveCount(1);
- await expect(activeLinks.first()).toHaveText('Home');
-
- // Test docs page - only Docs should be active
- await page.goto(`${baseUrl}/docs/`);
- activeLinks = page.locator('.nav-link.active');
- await expect(activeLinks).toHaveCount(1);
- await expect(activeLinks.first()).toHaveText('Docs');
-
- // Test API page - only API should be active
- await page.goto(`${baseUrl}/api/`);
- activeLinks = page.locator('.nav-link.active');
- await expect(activeLinks).toHaveCount(1);
- await expect(activeLinks.first()).toHaveText('API');
-
- // Test blog page - only Blog should be active
- await page.goto(`${baseUrl}/blog/`);
- activeLinks = page.locator('.nav-link.active');
- await expect(activeLinks).toHaveCount(1);
- await expect(activeLinks.first()).toHaveText('Blog');
-
- // Test examples page - only Examples should be active
- await page.goto(`${baseUrl}/examples/`);
- activeLinks = page.locator('.nav-link.active');
- await expect(activeLinks).toHaveCount(1);
- await expect(activeLinks.first()).toHaveText('Examples');
- });
-
- test('docs sidebar navigation works', async ({ page }) => {
- await page.goto(`${baseUrl}/docs/`);
- const sidebar = page.locator('.sidebar, .docs-sidebar');
- await expect(sidebar.first()).toBeVisible();
- });
-
- test('code blocks have proper syntax highlighting colors', async ({ page }) => {
- await page.goto(baseUrl);
- const codeBlock = page.locator('pre code').first();
- await expect(codeBlock).toBeVisible();
-
- // Check that syntax highlighting classes are present (Prism uses .token, hljs uses .hljs)
- const hasHighlight = await page.evaluate(() => {
- const code = document.querySelector('pre code');
- return code && (
- code.classList.contains('hljs') ||
- code.querySelector('.hljs-keyword') ||
- code.querySelector('.token') ||
- code.classList.contains('language-csharp') ||
- code.classList.contains('language-bash')
- );
- });
- expect(hasHighlight).toBeTruthy();
- });
-
- test('footer renders with all sections', async ({ page }) => {
- await page.goto(baseUrl);
- const footer = page.locator('footer');
- await expect(footer).toBeVisible();
-
- // Check footer has links
- const footerLinks = footer.locator('a');
- const linkCount = await footerLinks.count();
- expect(linkCount).toBeGreaterThan(5);
- });
-
- test('theme toggle button exists', async ({ page }) => {
- await page.goto(baseUrl);
- const themeToggle = page.locator('#theme-toggle');
- await expect(themeToggle).toBeVisible();
- });
-
- test('language switcher exists and works', async ({ page }) => {
- await page.goto(baseUrl);
- const langSwitcher = page.locator('.language-switcher');
- if (await langSwitcher.count() > 0) {
- await expect(langSwitcher).toBeVisible();
- }
- });
-
- test('Chinese pages load correctly', async ({ page }) => {
- await page.goto(`${baseUrl}/zh/`);
- await expect(page).toHaveURL(/\/zh\//);
-
- // Check lang attribute
- const html = page.locator('html');
- await expect(html).toHaveAttribute('lang', 'zh');
- });
-
- test('mobile menu toggle exists on mobile viewport', async ({ page }) => {
- await page.setViewportSize({ width: 375, height: 667 });
- await page.goto(baseUrl);
-
- // Mobile menu toggle should be visible on mobile
- const mobileToggle = page.locator('#mobile-menu-toggle, .mobile-menu-toggle, [aria-label*="menu"]');
- // This may or may not be visible depending on CSS - just check page loads
- await expect(page.locator('.hero')).toBeVisible();
- });
-
- test('blog posts have proper structure', async ({ page }) => {
- await page.goto(`${baseUrl}/blog/`);
- const posts = page.locator('article, .post, .blog-post');
-
- // Should have blog posts
- const postCount = await posts.count();
- expect(postCount).toBeGreaterThan(0);
- });
-
- test('API reference pages load', async ({ page }) => {
- await page.goto(`${baseUrl}/api/`);
- await expect(page.locator('h1')).toBeVisible();
+import { test, expect } from './fixtures.js';
+
+const routes = [['/', 'Home'], ['/docs/', 'Docs'], ['/api/', 'API'], ['/blog/', 'Blog'], ['/examples/', 'Examples']];
+
+test('homepage hero has clear copy and working actions', async ({ page }) => {
+ await page.goto('/');
+ await expect(page.locator('.hero')).toBeVisible();
+ await expect(page.locator('.hero h1')).toHaveText(/Every outcome.*In view/s);
+ await expect(page.locator('.hero-tagline')).toContainText('HTTP');
+ await expect(page.locator('.hero-actions a')).toHaveCount(2);
+ await page.locator('.hero-actions a').first().click();
+ await expect(page).toHaveURL(/\/docs\/$/);
+ await expect(page.locator('.prose')).toBeVisible();
+ await expect(page.locator('h1')).toHaveCount(1);
+});
- // Navigate to a specific API page
- await page.goto(`${baseUrl}/api/httpclient-extensions/`);
- await expect(page.locator('h1')).toBeVisible();
- });
+test('feature cards expose explanations and destination links', async ({ page }) => {
+ await page.goto('/');
+ const cards = page.locator('.feature-card');
+ expect(await cards.count()).toBeGreaterThanOrEqual(6);
+ for (const card of await cards.all()) {
+ await expect(card).toBeVisible();
+ await expect(card.locator('h3')).not.toBeEmpty();
+ await expect(card).toHaveAttribute('href', /\//);
+ }
+});
- test('examples page loads', async ({ page }) => {
- await page.goto(`${baseUrl}/examples/`);
+test('request explorer explains every outcome with native graphics', async ({ page }) => {
+ await page.goto('/');
+ await expect(page.locator('#flow-canvas')).toBeVisible();
+ await expect(page.locator('button[data-outcome]')).toHaveCount(3);
+ await expect(page.locator('#outcome-status')).toHaveAttribute('aria-live', 'polite');
+ await expect(page.locator('#outcome-code')).toContainText('switch');
+ await expect(page.locator('video,iframe')).toHaveCount(0);
+ await expect(page.locator('#motion-toggle')).toBeVisible();
+});
+
+test('navigation supports a multi-page reading journey', async ({ page }) => {
+ await page.goto('/');
+ for (const [route, label] of routes) {
+ await page.locator('#mobile-nav nav').locator(`a[href="${route}"]`).click();
+ expect(new URL(page.url()).pathname).toBe(route);
await expect(page.locator('h1')).toBeVisible();
- });
+ const active = page.locator('#mobile-nav [aria-current="page"]');
+ await expect(active).toHaveCount(1);
+ await expect(active).toHaveText(label);
+ await expect(active).toHaveAttribute('href', route);
+ }
+});
+
+test('active navigation stays exclusive on child pages too', async ({ page }) => {
+ for (const [route, label] of [...routes, ['/docs/basic-usage/', 'Docs']]) {
+ await page.goto(route);
+ const active = page.locator('#mobile-nav .nav-link.active');
+ await expect(active).toHaveCount(1);
+ await expect(active).toHaveText(label);
+ await expect(active).toHaveAttribute('aria-current', 'page');
+ }
+});
+
+test('docs sidebar opens a guide and returns to the overview', async ({ page }) => {
+ await page.goto('/docs/');
+ const sidebar = page.locator('.docs-sidebar');
+ await expect(sidebar).toBeVisible();
+ await sidebar.locator('a[href="/docs/basic-usage/"]').click();
+ await expect(page.locator('.prose h1')).toContainText('Basic');
+ await expect(sidebar.locator('[aria-current="page"]')).toHaveAttribute('href', '/docs/basic-usage/');
+ await sidebar.locator('a[href="/docs/"]').click();
+ await expect(page).toHaveURL(/\/docs\/$/);
+ await expect(page.locator('.prose pre code').first()).toBeVisible();
+});
+
+test('code stays selectable highlighted text with distinct token colors', async ({ page }) => {
+ await page.goto('/');
+ const code = page.locator('pre code').first();
+ await expect(code).toBeVisible();
+ await expect(code).toHaveClass(/language-csharp/);
+ expect(await code.locator('.token').count()).toBeGreaterThan(0);
+ const colors = await code.evaluate(element => [getComputedStyle(element.querySelector('.keyword')).color, getComputedStyle(element.querySelector('.string')).color]);
+ expect(colors[0]).not.toBe(colors[1]);
+ expect(await code.textContent()).toContain('result');
+});
+
+test('footer links to guides, source, packages, and generated reference', async ({ page }) => {
+ await page.goto('/');
+ const footer = page.locator('footer');
+ await expect(footer).toBeVisible();
+ expect(await footer.locator('a').count()).toBeGreaterThan(5);
+ await expect(footer.locator('a[href*="nuget.org"]')).toBeVisible();
+ await expect(footer.locator('a[href*="github.com"]')).toBeVisible();
+ await footer.locator('a[href="/api/reference/"]').click();
+ await expect(page).toHaveURL(/\/api\/reference\/$/);
+ await expect(page.locator('.prose')).toBeVisible();
+});
+
+test('motion control works with keyboard and creates no inline CSS', async ({ page }) => {
+ await page.goto('/');
+ const toggle = page.locator('#motion-toggle');
+ await expect(toggle).toBeVisible();
+ await expect(toggle).toHaveAccessibleName(/motion/i);
+ await toggle.focus();
+ await page.keyboard.press('Enter');
+ await expect(toggle).toHaveAttribute('aria-pressed', 'true');
+ await expect(page.locator('#flow-canvas')).toHaveAttribute('data-motion', 'paused');
+ await page.keyboard.press('Enter');
+ await expect(toggle).toHaveAttribute('aria-pressed', 'false');
+ await expect(page.locator('#flow-canvas')).toHaveAttribute('data-motion', 'running');
+ await expect(page.locator('style,[style]')).toHaveCount(0);
+});
+
+test('language selector visits translated document and its original', async ({ page }) => {
+ await page.goto('/docs/basic-usage/');
+ await page.locator('.language-switcher summary').click();
+ await expect(page.locator('.language-switcher')).toHaveAttribute('open', '');
+ await page.locator('.language-switcher a[lang="zh"]').click();
+ await expect(page).toHaveURL(/\/zh\/docs\/basic-usage\/$/);
+ await expect(page.locator('html')).toHaveAttribute('lang', 'zh');
+ await page.locator('.language-switcher summary').click();
+ await page.locator('.language-switcher a[lang="en"]').click();
+ await expect(page).toHaveURL(/\/docs\/basic-usage\/$/);
+ await expect(page.locator('html')).toHaveAttribute('lang', 'en');
+});
+
+test('Chinese homepage keeps localized navigation and outcome controls', async ({ page }) => {
+ await page.goto('/zh/');
+ await expect(page.locator('html')).toHaveAttribute('lang', 'zh');
+ await expect(page.locator('h1')).toContainText('每种结果');
+ await expect(page.locator('#mobile-nav [aria-current="page"]')).toHaveText('首页');
+ await expect(page.locator('button[data-outcome="success"]')).toContainText('成功');
+ await expect(page.locator('.hero-actions a').first()).toHaveAttribute('href', '/zh/docs/');
+});
+
+test('mobile navigation opens and closes using keyboard and pointer', async ({ page }) => {
+ await page.setViewportSize({ width: 375, height: 667 });
+ await page.goto('/');
+ const menu = page.locator('#mobile-nav');
+ const summary = menu.locator('summary');
+ await expect(summary).toBeVisible();
+ for (const control of [summary, page.locator('.language-switcher summary')]) {
+ const bounds = await control.boundingBox();
+ expect(bounds.height, 'Mobile controls must provide a usable touch target').toBeGreaterThanOrEqual(44);
+ expect(bounds.width).toBeGreaterThanOrEqual(44);
+ }
+ if (await menu.getAttribute('open') !== null) await summary.click();
+ await expect(menu).not.toHaveAttribute('open');
+ await expect(menu.locator('nav')).toBeHidden();
+ await summary.focus();
+ await page.keyboard.press('Enter');
+ await expect(menu).toHaveAttribute('open', '');
+ await expect(menu.locator('nav')).toBeVisible();
+ await menu.locator('a[href="/docs/"]').click();
+ await expect(page).toHaveURL(/\/docs\/$/);
+ await expect(page.locator('.prose')).toBeVisible();
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
+});
+
+test('blog articles contain dates, headings, examples, and a return link', async ({ page }) => {
+ await page.goto('/blog/');
+ const article = page.locator('article h2 a[href="/blog/introducing-restclient/"]');
+ await expect(article).toBeVisible();
+ await article.click();
+ await expect(page.locator('article.prose h1')).toContainText('RestClient.Net');
+ await expect(page.locator('article.prose time')).toHaveAttribute('datetime', /^\d{4}-\d{2}-\d{2}/);
+ await expect(page.locator('article.prose pre code').first()).toBeVisible();
+ await expect(page.locator('article.prose a[href="/blog/"]')).toBeVisible();
+});
+
+test('API guide and generated reference contain useful source documentation', async ({ page }) => {
+ await page.goto('/api/httpclient-extensions/');
+ await expect(page.locator('h1')).toBeVisible();
+ await expect(page.locator('main')).toContainText('GetAsync');
+ await page.goto('/api/reference/');
+ await expect(page.locator('h1')).toContainText('API');
+ expect(await page.locator('.prose a').count()).toBeGreaterThan(5);
+});
+
+test('examples remain readable without horizontal page overflow on mobile', async ({ page }) => {
+ await page.setViewportSize({ width: 375, height: 667 });
+ await page.goto('/examples/');
+ await expect(page.locator('h1')).toBeVisible();
+ expect(await page.locator('pre code').count()).toBeGreaterThan(0);
+ await expect(page.locator('pre code').first()).toBeVisible();
+ const listing = page.locator('pre code.language-csharp');
+ await expect(listing).toHaveCount(1);
+ for (const symbol of ['RestClientExamples', 'GetPostAsync', 'CreatePostAsync', 'GetUsingFactoryAsync', '.Match(', 'CancellationToken']) {
+ await expect(listing).toContainText(symbol);
+ }
+ await expect(page.locator('pre code.language-bash')).toHaveText('dotnet run --project Website/examples/Examples.csproj');
+ await expect(page.locator('.prose')).not.toContainText('{{ examples.source');
+ for (const anchor of ['basic-get', 'post-request', 'ihttpclientfactory', 'status-code-handling']) {
+ await expect(page.locator(`#${anchor}`)).toHaveCount(1);
+ }
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
});
diff --git a/Website/tools/ApiDocs/ApiDocs.csproj b/Website/tools/ApiDocs/ApiDocs.csproj
new file mode 100644
index 00000000..a575cc15
--- /dev/null
+++ b/Website/tools/ApiDocs/ApiDocs.csproj
@@ -0,0 +1,12 @@
+
+
+ Exe
+
+
+
+
+
+
+
+
+
diff --git a/Website/tools/ApiDocs/Directory.Build.props b/Website/tools/ApiDocs/Directory.Build.props
new file mode 100644
index 00000000..42833fbf
--- /dev/null
+++ b/Website/tools/ApiDocs/Directory.Build.props
@@ -0,0 +1,11 @@
+
+
+
+ net9.0
+ enable
+ enable
+ 12
+ false
+ true
+
+
diff --git a/Website/tools/ApiDocs/Program.cs b/Website/tools/ApiDocs/Program.cs
new file mode 100644
index 00000000..7fa762ad
--- /dev/null
+++ b/Website/tools/ApiDocs/Program.cs
@@ -0,0 +1,584 @@
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Xml.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+var options = Arguments.Parse(args);
+var projects =
+ options.Projects.Length > 0
+ ? options.Projects
+ :
+ [
+ "RestClient.Net",
+ "Outcome",
+ "RestClient.Net.OpenApiGenerator",
+ "RestClient.Net.McpGenerator",
+ "Exhaustion",
+ ];
+var files = projects
+ .SelectMany(project =>
+ Directory
+ .EnumerateFiles(
+ Path.Combine(options.Root, project),
+ "*.cs",
+ SearchOption.AllDirectories
+ )
+ .Where(file =>
+ !Path.GetRelativePath(options.Root, file)
+ .Split(Path.DirectorySeparatorChar)
+ .Any(part => part is "bin" or "obj")
+ )
+ .Select(file => (Project: project, Path: file))
+ )
+ .OrderBy(item => item.Path, StringComparer.Ordinal)
+ .ToArray();
+var trees = files
+ .Select(file =>
+ CSharpSyntaxTree.ParseText(
+ File.ReadAllText(file.Path),
+ new CSharpParseOptions(LanguageVersion.CSharp12, DocumentationMode.Diagnose),
+ file.Path
+ )
+ )
+ .ToArray();
+foreach (
+ var error in trees
+ .SelectMany(tree => tree.GetDiagnostics())
+ .Where(item => item.Severity == DiagnosticSeverity.Error)
+)
+ throw new InvalidOperationException($"Cannot export invalid C# source: {error}");
+var references = ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!)
+ .Split(Path.PathSeparator)
+ .Select(file => MetadataReference.CreateFromFile(file));
+
+// Match the SDK implicit imports enabled by the repository Directory.Build.props.
+// Semantic symbols identify public declarations and overloads; signatures retain their exact
+// source types/aliases, constraints and default expressions without building product packages.
+var compilation = CSharpCompilation.Create(
+ "ApiDocumentation",
+ trees.Append(
+ CSharpSyntaxTree.ParseText(
+ """
+ global using global::System;
+ global using global::System.Collections.Generic;
+ global using global::System.IO;
+ global using global::System.Linq;
+ global using global::System.Net.Http;
+ global using global::System.Threading;
+ global using global::System.Threading.Tasks;
+ """,
+ new CSharpParseOptions(LanguageVersion.CSharp12)
+ )
+ ),
+ references,
+ new CSharpCompilationOptions(
+ OutputKind.DynamicallyLinkedLibrary,
+ nullableContextOptions: NullableContextOptions.Enable
+ )
+);
+var types = new Dictionary(StringComparer.Ordinal);
+for (var index = 0; index < trees.Length; index++)
+{
+ var tree = trees[index];
+ var model = compilation.GetSemanticModel(tree);
+ foreach (
+ var declaration in tree.GetRoot()
+ .DescendantNodes()
+ .Where(node => node is BaseTypeDeclarationSyntax or DelegateDeclarationSyntax)
+ )
+ {
+ if (
+ model.GetDeclaredSymbol(declaration) is not INamedTypeSymbol symbol
+ || !Exporter.IsPublic(symbol)
+ )
+ continue;
+ var id = symbol.GetDocumentationCommentId()!;
+ if (types.ContainsKey(id))
+ continue;
+ var declarations = symbol
+ .DeclaringSyntaxReferences.Select(reference => reference.GetSyntax())
+ .OrderBy(node => node.SyntaxTree.FilePath, StringComparer.Ordinal)
+ .ThenBy(node => node.SpanStart)
+ .ToArray();
+ var primary =
+ declarations.FirstOrDefault(node => Exporter.Documentation(node).Summary.Length > 0)
+ ?? declarations[0];
+ var members = new List();
+ foreach (
+ var member in symbol
+ .GetMembers()
+ .Where(member =>
+ !member.IsImplicitlyDeclared
+ && member is not INamedTypeSymbol
+ && Exporter.IsPublic(member)
+ )
+ )
+ {
+ var node = member.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax();
+ if (
+ node is null
+ || node
+ is not (MemberDeclarationSyntax or VariableDeclaratorSyntax or ParameterSyntax)
+ )
+ continue;
+ members.Add(Exporter.Member(member, node, options));
+ }
+ types[id] = new ApiType(
+ id,
+ symbol.Name,
+ symbol.ToDisplayString(Exporter.TypeNames),
+ symbol.ContainingNamespace.ToDisplayString(),
+ files[index].Project,
+ symbol.TypeKind.ToString().ToLowerInvariant(),
+ Exporter.Signature(primary),
+ Exporter.Documentation(primary),
+ Exporter.Parameters(primary),
+ Exporter.Source(primary, options),
+ "/api/reference/" + Exporter.Slug(id[2..]) + "/",
+ members.OrderBy(member => member.Id, StringComparer.Ordinal).ToArray()
+ );
+ }
+}
+var api = new ApiIndex(
+ 1,
+ options.SourceRef,
+ projects.Order(StringComparer.Ordinal).ToArray(),
+ types.Values.OrderBy(type => type.Id, StringComparer.Ordinal).ToArray()
+);
+Directory.CreateDirectory(options.Output);
+foreach (var old in Directory.EnumerateFiles(options.Output, "*.md"))
+ File.Delete(old);
+File.WriteAllText(
+ Path.Combine(options.Output, "api.json"),
+ JsonSerializer.Serialize(api, Exporter.JsonOptions) + "\n"
+);
+foreach (var type in api.Types)
+ File.WriteAllText(
+ Path.Combine(options.Output, Exporter.Slug(type.Id[2..]) + ".md"),
+ Exporter.Markdown(type)
+ );
+File.WriteAllText(Path.Combine(options.Output, "index.md"), Exporter.IndexMarkdown(api));
+Console.WriteLine(
+ $"Exported {api.Types.Length} public types and {api.Types.Sum(type => type.Members.Length)} members from {files.Length} C# source files."
+);
+
+internal sealed record Arguments(string Root, string Output, string SourceRef, string[] Projects)
+{
+ public static Arguments Parse(string[] args)
+ {
+ var values = new Dictionary();
+ for (var index = 0; index < args.Length; index += 2)
+ {
+ if (
+ index + 1 >= args.Length
+ || !new[] { "--root", "--output", "--source-ref", "--projects" }.Contains(
+ args[index]
+ )
+ )
+ throw new ArgumentException(
+ "Use --root PATH --output PATH [--source-ref REF] [--projects dir,dir]"
+ );
+ values.Add(args[index], args[index + 1]);
+ }
+ return new Arguments(
+ Path.GetFullPath(values["--root"]),
+ Path.GetFullPath(values["--output"]),
+ values.GetValueOrDefault("--source-ref", "main"),
+ values
+ .GetValueOrDefault("--projects", "")
+ .Split(',', StringSplitOptions.RemoveEmptyEntries)
+ );
+ }
+}
+
+internal sealed record SourceLocation(string File, int Line, string Url);
+
+internal sealed record Parameter(string Name, string Type, string Modifiers, string? Default);
+
+internal sealed record NamedDocumentation(string Name, string Description);
+
+internal sealed record Documentation(
+ string Summary,
+ string Remarks,
+ string Returns,
+ NamedDocumentation[] Parameters,
+ NamedDocumentation[] TypeParameters
+);
+
+internal sealed record ApiMember(
+ string Id,
+ string Anchor,
+ string Name,
+ string Kind,
+ string Signature,
+ Documentation Docs,
+ Parameter[] Parameters,
+ SourceLocation Source
+);
+
+internal sealed record ApiType(
+ string Id,
+ string Name,
+ string DisplayName,
+ string Namespace,
+ string Project,
+ string Kind,
+ string Signature,
+ Documentation Docs,
+ Parameter[] Parameters,
+ SourceLocation Source,
+ string Url,
+ ApiMember[] Members
+);
+
+internal sealed record ApiIndex(
+ int SchemaVersion,
+ string SourceRef,
+ string[] Projects,
+ ApiType[] Types
+);
+
+internal static partial class Exporter
+{
+ [GeneratedRegex("[^a-z0-9]+")]
+ private static partial Regex SlugPattern();
+
+ [GeneratedRegex(@"\s+")]
+ private static partial Regex Whitespace();
+
+ [GeneratedRegex(@"(?m)^\s*/// ?")]
+ private static partial Regex DocumentationPrefix();
+
+ [GeneratedRegex(@"^\s*/\*\*|\*/\s*$")]
+ private static partial Regex BlockCommentBoundary();
+
+ [GeneratedRegex(@"(?m)^\s*\* ?")]
+ private static partial Regex BlockCommentLine();
+
+ public static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ WriteIndented = true,
+ };
+ public static readonly SymbolDisplayFormat TypeNames = new(
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
+ genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
+ miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes
+ | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier
+ );
+
+ public static bool IsPublic(ISymbol symbol) =>
+ symbol.DeclaredAccessibility == Accessibility.Public
+ && (symbol.ContainingType is null || IsPublic(symbol.ContainingType));
+
+ public static string Slug(string value) =>
+ SlugPattern().Replace(value.ToLowerInvariant(), "-").Trim('-');
+
+ private static string Anchor(ISymbol symbol) =>
+ Slug(symbol.Name)
+ + "-"
+ + Convert
+ .ToHexString(
+ SHA256.HashData(Encoding.UTF8.GetBytes(symbol.GetDocumentationCommentId()!))
+ )[..12]
+ .ToLowerInvariant();
+
+ public static SourceLocation Source(SyntaxNode node, Arguments options)
+ {
+ var file = Path.GetRelativePath(options.Root, node.SyntaxTree.FilePath).Replace('\\', '/');
+ var line = node.GetLocation().GetLineSpan().StartLinePosition.Line + 1;
+ return new SourceLocation(
+ file,
+ line,
+ "https://github.com/MelbourneDeveloper/RestClient.Net/blob/"
+ + Uri.EscapeDataString(options.SourceRef)
+ + "/"
+ + string.Join('/', file.Split('/').Select(Uri.EscapeDataString))
+ + "#L"
+ + line
+ );
+ }
+
+ public static ApiMember Member(ISymbol symbol, SyntaxNode node, Arguments options)
+ {
+ var documentationNode = node is VariableDeclaratorSyntax ? node.Parent!.Parent! : node;
+ return new ApiMember(
+ symbol.GetDocumentationCommentId()!,
+ Anchor(symbol),
+ symbol.Name,
+ symbol.Kind.ToString().ToLowerInvariant(),
+ symbol is IMethodSymbol { MethodKind: MethodKind.Constructor }
+ && node is TypeDeclarationSyntax
+ ? $"public {symbol.ContainingType.Name}({string.Join(", ", Parameters(node).Select(parameter => parameter.Type + " " + parameter.Name + (parameter.Default is null ? "" : " = " + parameter.Default)))});"
+ : Signature(node),
+ Documentation(documentationNode),
+ Parameters(node),
+ Source(node, options)
+ );
+ }
+
+ public static Parameter[] Parameters(SyntaxNode node)
+ {
+ var parameters = node switch
+ {
+ BaseMethodDeclarationSyntax method => method.ParameterList.Parameters,
+ DelegateDeclarationSyntax declaration => declaration.ParameterList.Parameters,
+ RecordDeclarationSyntax record => record.ParameterList?.Parameters ?? default,
+ ClassDeclarationSyntax type => type.ParameterList?.Parameters ?? default,
+ StructDeclarationSyntax type => type.ParameterList?.Parameters ?? default,
+ IndexerDeclarationSyntax indexer => indexer.ParameterList.Parameters,
+ _ => default,
+ };
+ return parameters
+ .Select(parameter => new Parameter(
+ parameter.Identifier.ValueText,
+ parameter.Type?.ToString() ?? "",
+ string.Join(' ', parameter.Modifiers.Select(token => token.Text)),
+ parameter.Default?.Value.ToString()
+ ))
+ .ToArray();
+ }
+
+ public static string Signature(SyntaxNode node)
+ {
+ SyntaxNode signature = node switch
+ {
+ MethodDeclarationSyntax value => value
+ .WithBody(null)
+ .WithExpressionBody(null)
+ .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
+ ConstructorDeclarationSyntax value => value
+ .WithBody(null)
+ .WithExpressionBody(null)
+ .WithInitializer(null)
+ .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
+ OperatorDeclarationSyntax value => value
+ .WithBody(null)
+ .WithExpressionBody(null)
+ .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
+ ConversionOperatorDeclarationSyntax value => value
+ .WithBody(null)
+ .WithExpressionBody(null)
+ .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
+ PropertyDeclarationSyntax value => value
+ .WithExpressionBody(null)
+ .WithInitializer(null)
+ .WithAccessorList(Accessors(value.AccessorList))
+ .WithSemicolonToken(default),
+ IndexerDeclarationSyntax value => value
+ .WithExpressionBody(null)
+ .WithAccessorList(Accessors(value.AccessorList))
+ .WithSemicolonToken(default),
+ EventDeclarationSyntax value => value.WithAccessorList(Accessors(value.AccessorList)),
+ ParameterSyntax value => SyntaxFactory.ParseMemberDeclaration(
+ $"public {value.Type} {value.Identifier} {{ get; init; }}"
+ )!,
+ VariableDeclaratorSyntax value
+ when value.Parent?.Parent is FieldDeclarationSyntax field => field.WithDeclaration(
+ field.Declaration.WithVariables(
+ SyntaxFactory.SingletonSeparatedList(
+ field.Modifiers.Any(SyntaxKind.ConstKeyword)
+ ? value
+ : value.WithInitializer(null)
+ )
+ )
+ ),
+ VariableDeclaratorSyntax value
+ when value.Parent?.Parent is EventFieldDeclarationSyntax field =>
+ field.WithDeclaration(
+ field.Declaration.WithVariables(
+ SyntaxFactory.SingletonSeparatedList(value.WithInitializer(null))
+ )
+ ),
+ _ => node,
+ };
+ signature = signature switch
+ {
+ TypeDeclarationSyntax type => type.WithMembers(default)
+ .WithOpenBraceToken(default)
+ .WithCloseBraceToken(default)
+ .WithSemicolonToken(default),
+ EnumDeclarationSyntax type => type.WithMembers(default)
+ .WithOpenBraceToken(default)
+ .WithCloseBraceToken(default)
+ .WithSemicolonToken(default),
+ _ => signature,
+ };
+ return Clean(signature);
+ }
+
+ private static string Clean(SyntaxNode node) =>
+ node.ReplaceTokens(node.DescendantTokens(), (_, token) => token.WithoutTrivia())
+ .WithoutTrivia()
+ .NormalizeWhitespace()
+ .ToFullString();
+
+ private static AccessorListSyntax Accessors(AccessorListSyntax? list) =>
+ list is null
+ ? SyntaxFactory.AccessorList(
+ SyntaxFactory.SingletonList(
+ SyntaxFactory
+ .AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
+ .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
+ )
+ )
+ : list.WithAccessors(
+ SyntaxFactory.List(
+ list.Accessors.Select(accessor =>
+ accessor
+ .WithBody(null)
+ .WithExpressionBody(null)
+ .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
+ )
+ )
+ );
+
+ public static Documentation Documentation(SyntaxNode node)
+ {
+ var comments = node.GetLeadingTrivia()
+ .Where(trivia =>
+ trivia.HasStructure && trivia.GetStructure() is DocumentationCommentTriviaSyntax
+ )
+ .Select(trivia => trivia.ToFullString());
+ var xml = string.Join("\n", comments);
+ xml = DocumentationPrefix().Replace(xml, "");
+ xml = BlockCommentBoundary().Replace(xml, "");
+ xml = BlockCommentLine().Replace(xml, "");
+ var root = XElement.Parse("" + xml + "");
+ string Read(string name) => string.Join("\n\n", root.Elements(name).Select(Render)).Trim();
+ NamedDocumentation[] Named(string name) =>
+ root.Elements(name)
+ .Select(element => new NamedDocumentation(
+ element.Attribute("name")?.Value ?? "",
+ Render(element).Trim()
+ ))
+ .ToArray();
+ return new Documentation(
+ Read("summary"),
+ Read("remarks"),
+ Read("returns"),
+ Named("param"),
+ Named("typeparam")
+ );
+ }
+
+ private static string Render(XNode node) =>
+ node switch
+ {
+ XText text => Whitespace()
+ .Replace(text.Value, " ")
+ .Replace("<", "<")
+ .Replace(">", ">"),
+ XElement element when element.Name.LocalName is "paramref" or "typeparamref" => "`"
+ + element.Attribute("name")?.Value
+ + "`",
+ XElement element when element.Name.LocalName == "see" => "`"
+ + (
+ element.Attribute("cref")?.Value
+ ?? element.Attribute("langword")?.Value
+ ?? element.Value
+ )
+ + "`",
+ XElement element when element.Name.LocalName == "c" => "`" + element.Value + "`",
+ XElement element when element.Name.LocalName == "code" => "\n\n```csharp\n"
+ + element.Value.Trim()
+ + "\n```\n\n",
+ XElement element when element.Name.LocalName == "para" => "\n\n"
+ + string.Concat(element.Nodes().Select(Render)).Trim()
+ + "\n\n",
+ XElement element when element.Name.LocalName == "item" => "\n- "
+ + string.Concat(element.Nodes().Select(Render)).Trim(),
+ XElement element => string.Concat(element.Nodes().Select(Render)),
+ _ => "",
+ };
+
+ private static string Frontmatter(string title, string permalink) =>
+ "---\nlayout: layouts/api.njk\ntitle: "
+ + JsonSerializer.Serialize(title)
+ + "\nlang: en\npermalink: "
+ + permalink
+ + "\ntemplateEngineOverride: md\n---\n\n";
+
+ private static string Cell(string value) => value.Replace("|", "\\|").Replace("\n", " ");
+
+ private static void AppendDocumentation(
+ StringBuilder builder,
+ Documentation docs,
+ Parameter[] parameters
+ )
+ {
+ if (docs.Summary.Length > 0)
+ builder.AppendLine(docs.Summary).AppendLine();
+ if (parameters.Length > 0)
+ {
+ builder
+ .AppendLine("| Parameter | Type | Default | Description |")
+ .AppendLine("| --- | --- | --- | --- |");
+ foreach (var parameter in parameters)
+ builder.AppendLine(
+ $"| `{parameter.Name}` | `{Cell(parameter.Type)}` | {(parameter.Default is null ? "Required" : "`" + Cell(parameter.Default) + "`")} | {Cell(docs.Parameters.FirstOrDefault(item => item.Name == parameter.Name)?.Description ?? "")} |"
+ );
+ builder.AppendLine();
+ }
+ foreach (var parameter in docs.TypeParameters)
+ builder.AppendLine($"**Type parameter `{parameter.Name}`:** {parameter.Description}\n");
+ if (docs.Returns.Length > 0)
+ builder.AppendLine("**Returns:** " + docs.Returns).AppendLine();
+ if (docs.Remarks.Length > 0)
+ builder.AppendLine("**Remarks:** " + docs.Remarks).AppendLine();
+ }
+
+ public static string Markdown(ApiType type)
+ {
+ var builder = new StringBuilder(Frontmatter(type.DisplayName, type.Url));
+ builder.AppendLine(
+ $"[All API types](/api/reference/) · `{type.Project}` · [View source]({type.Source.Url})\n"
+ );
+ builder.AppendLine("```csharp").AppendLine(type.Signature).AppendLine("```\n");
+ AppendDocumentation(builder, type.Docs, type.Parameters);
+ if (type.Members.Length > 0)
+ {
+ builder.AppendLine("## Members\n");
+ foreach (var member in type.Members)
+ builder.AppendLine($"- [{member.Name}](#{member.Anchor})");
+ builder.AppendLine();
+ }
+ foreach (var member in type.Members)
+ {
+ builder.AppendLine(
+ $"{System.Net.WebUtility.HtmlEncode(member.Name)}
\n"
+ );
+ builder.AppendLine("```csharp").AppendLine(member.Signature).AppendLine("```\n");
+ AppendDocumentation(builder, member.Docs, member.Parameters);
+ builder.AppendLine(
+ $"[View source: {member.Source.File}:{member.Source.Line}]({member.Source.Url})\n"
+ );
+ }
+ return builder.ToString();
+ }
+
+ public static string IndexMarkdown(ApiIndex api)
+ {
+ var builder = new StringBuilder(Frontmatter("Source API reference", "/api/reference/"));
+ builder.AppendLine(
+ $"Explore {api.Types.Length} public types and {api.Types.Sum(type => type.Members.Length)} members, exported directly from the C# source and XML documentation.\n"
+ );
+ builder.AppendLine(
+ "[Download the API index](/api/reference/api.json) · [JSON schema](/api/reference/schema.json)\n"
+ );
+ foreach (var project in api.Projects)
+ {
+ builder.AppendLine("## " + project + "\n");
+ foreach (var type in api.Types.Where(type => type.Project == project))
+ builder.AppendLine(
+ $"- [`{type.DisplayName}`]({type.Url}){(type.Docs.Summary.Length == 0 ? "" : " — " + type.Docs.Summary.Split('\n')[0])}"
+ );
+ builder.AppendLine();
+ }
+ return builder.ToString();
+ }
+}
diff --git a/Website/tools/ApiDocs/README.md b/Website/tools/ApiDocs/README.md
new file mode 100644
index 00000000..adda4b5a
--- /dev/null
+++ b/Website/tools/ApiDocs/README.md
@@ -0,0 +1,43 @@
+# Source API exporter
+
+`npm run generate-api` runs this .NET 9 command-line tool using Roslyn 4.8.0,
+matching the analyzer project's Roslyn version. Only this small project needs
+restoring; the application, sample projects, and their analyzers are not built.
+
+The exporter reads the actual C# files in RestClient.Net, Outcome, both generator
+projects, and Exhaustion. Roslyn syntax and semantic symbols supply public types,
+nested types, partial declarations, members, delegate signatures, positional record
+properties, parameters, constraints, defaults, and overload identities. Private and
+internal declarations (including public children of internal types) stay hidden.
+Signatures preserve source aliases and expressions; compiler-generated record
+helpers and inherited members are not duplicated as declared API.
+
+XML summaries, remarks, parameter/type parameter descriptions, and return prose
+become both JSON data and Markdown. Every declaration carries its repository file,
+line, and source URL. The output has no timestamp, absolute local paths, or machine
+metadata; repeating an export with identical inputs is byte-for-byte deterministic.
+
+Generated files live under `src/api/reference/`. `api.json` follows the versioned
+`schema.json` contract. Type pages use `layouts/api.njk`, the site's shared prose
+styles, and `templateEngineOverride: md` so code and XML comments containing
+`{{ expressions }}` cannot be evaluated as Nunjucks templates. Member anchors derive
+from Roslyn documentation IDs, keeping overloads distinct and documentation-only
+edits stable. Curated `/api/` guides are ordinary Markdown and are not overwritten.
+
+```sh
+node scripts/generate-api-docs.js
+node scripts/generate-api-docs.js --source-ref COMMIT_SHA
+node --test tests-node/api-export.test.js
+```
+
+For isolated fixtures, pass `--root PATH --projects ProjectOne,ProjectTwo --output
+PATH`. The output directory is exporter-owned: stale generated Markdown is removed
+on each run. Source syntax errors fail generation. The small tool restores matching
+metadata packages for Urls, Microsoft.Extensions.Http, and Microsoft.OpenApi so
+public signature symbols resolve correctly, and supplies the standard SDK implicit
+global imports enabled by the repository. No product build or execution is needed.
+Displayed signatures retain the exact source-level type names and aliases.
+
+Exports use C# 12 parsing with the supplied source files; project-specific conditional
+compilation and source generators are not evaluated. When a new external type enters
+a public signature, add its matching metadata package to this tool as well.
diff --git a/Website/tools/ApiDocs/schema.json b/Website/tools/ApiDocs/schema.json
new file mode 100644
index 00000000..c59271b8
--- /dev/null
+++ b/Website/tools/ApiDocs/schema.json
@@ -0,0 +1,51 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://restclient.net/api/reference/schema.json",
+ "title": "RestClient.Net source API reference",
+ "type": "object",
+ "required": ["schemaVersion", "sourceRef", "projects", "types"],
+ "properties": {
+ "schemaVersion": { "const": 1 },
+ "sourceRef": { "type": "string" },
+ "projects": { "type": "array", "items": { "type": "string" } },
+ "types": { "type": "array", "items": { "$ref": "#/$defs/type" } }
+ },
+ "$defs": {
+ "source": {
+ "type": "object",
+ "required": ["file", "line", "url"],
+ "properties": { "file": { "type": "string" }, "line": { "type": "integer", "minimum": 1 }, "url": { "type": "string", "format": "uri" } }
+ },
+ "parameter": {
+ "type": "object",
+ "required": ["name", "type", "modifiers", "default"],
+ "properties": { "name": { "type": "string" }, "type": { "type": "string" }, "modifiers": { "type": "string" }, "default": { "type": ["string", "null"] } }
+ },
+ "namedDocumentation": {
+ "type": "object", "required": ["name", "description"],
+ "properties": { "name": { "type": "string" }, "description": { "type": "string" } }
+ },
+ "documentation": {
+ "type": "object", "required": ["summary", "remarks", "returns", "parameters", "typeParameters"],
+ "properties": {
+ "summary": { "type": "string" }, "remarks": { "type": "string" }, "returns": { "type": "string" },
+ "parameters": { "type": "array", "items": { "$ref": "#/$defs/namedDocumentation" } },
+ "typeParameters": { "type": "array", "items": { "$ref": "#/$defs/namedDocumentation" } }
+ }
+ },
+ "member": {
+ "type": "object", "required": ["id", "anchor", "name", "kind", "signature", "docs", "parameters", "source"],
+ "properties": {
+ "id": { "type": "string" }, "anchor": { "type": "string" }, "name": { "type": "string" }, "kind": { "type": "string" }, "signature": { "type": "string" },
+ "docs": { "$ref": "#/$defs/documentation" }, "parameters": { "type": "array", "items": { "$ref": "#/$defs/parameter" } }, "source": { "$ref": "#/$defs/source" }
+ }
+ },
+ "type": {
+ "type": "object", "required": ["id", "name", "displayName", "namespace", "project", "kind", "signature", "docs", "parameters", "source", "url", "members"],
+ "properties": {
+ "id": { "type": "string" }, "name": { "type": "string" }, "displayName": { "type": "string" }, "namespace": { "type": "string" }, "project": { "type": "string" }, "kind": { "type": "string" }, "signature": { "type": "string" },
+ "docs": { "$ref": "#/$defs/documentation" }, "parameters": { "type": "array", "items": { "$ref": "#/$defs/parameter" } }, "source": { "$ref": "#/$defs/source" }, "url": { "type": "string" }, "members": { "type": "array", "items": { "$ref": "#/$defs/member" } }
+ }
+ }
+ }
+}
From 6f161e0102f8692ec34c729c2b6da280d0038fd0 Mon Sep 17 00:00:00 2001
From: Christian Findlay <16697547+MelbourneDeveloper@users.noreply.github.com>
Date: Sat, 3 Oct 2026 22:35:48 +1000
Subject: [PATCH 2/3] Serialize dotnet-backed website tests during SDK
initialization
---
Website/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Website/package.json b/Website/package.json
index 3c7746d9..cfd59ba1 100644
--- a/Website/package.json
+++ b/Website/package.json
@@ -7,7 +7,7 @@
"generate-api": "node scripts/generate-api-docs.js",
"dev": "npm run generate-api && eleventy --serve --port=4173",
"build": "npm run generate-api && npm run clean && eleventy && node scripts/check-site.js",
- "test:unit": "node --test tests-node/*.test.js",
+ "test:unit": "node --test --test-concurrency=1 tests-node/*.test.js",
"test": "playwright test",
"clean": "node -e \"require('node:fs').rmSync('_site', {recursive: true, force: true})\""
},
From eca02c85f0009a822fbe421fe1a3d89be41b2f0c Mon Sep 17 00:00:00 2001
From: Christian Findlay <16697547+MelbourneDeveloper@users.noreply.github.com>
Date: Sat, 3 Oct 2026 22:43:56 +1000
Subject: [PATCH 3/3] Decode sitemap XML once and anchor page title assertions
---
Website/scripts/check-site.js | 2 +-
Website/tests-node/site-guards.test.js | 8 ++++++++
Website/tests/fixtures.js | 2 +-
3 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/Website/scripts/check-site.js b/Website/scripts/check-site.js
index d0cf1224..1eb54958 100644
--- a/Website/scripts/check-site.js
+++ b/Website/scripts/check-site.js
@@ -27,7 +27,7 @@ function elements(document) {
const attribute = (node, name) => node.attrs?.find(item => item.name === name)?.value;
const content = node => node.nodeName === '#text' ? node.value : (node.childNodes ?? []).map(content).join('');
const routeFor = file => `/${file.replaceAll(path.sep, '/').replace(/index\.html$/, '')}`;
-const decodeXml = value => value.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>');
+const decodeXml = value => value.replace(/&(amp|quot|apos|lt|gt);/g, (_, entity) => ({ amp: '&', quot: '"', apos: "'", lt: '<', gt: '>' })[entity]);
export function cssInjectionErrors(source, filename = 'script.js') {
const errors = [];
diff --git a/Website/tests-node/site-guards.test.js b/Website/tests-node/site-guards.test.js
index 2db4c007..0a0ea540 100644
--- a/Website/tests-node/site-guards.test.js
+++ b/Website/tests-node/site-guards.test.js
@@ -158,3 +158,11 @@ test('canonical, structured data, and crawl output regressions fail visibly', as
assert.match(errors, /Sitemap omits/);
assert.match(errors, /wrong sitemap/);
});
+
+test('sitemap URL validation decodes exactly one XML entity layer', async t => {
+ const site = await fixture(t);
+ await site.write('sitemap.xml', 'https://example.test/<missing>');
+ const report = await site.run();
+ assert.ok(report.errors.includes('Sitemap has a nonexistent page: https://example.test/<missing>'));
+ assert.ok(!report.errors.some(error => error.includes('https://example.test/')));
+});
diff --git a/Website/tests/fixtures.js b/Website/tests/fixtures.js
index 0fed2da8..45e7d36c 100644
--- a/Website/tests/fixtures.js
+++ b/Website/tests/fixtures.js
@@ -11,7 +11,7 @@ export const test = base.extend({
await expect(page.locator('main')).toHaveCount(1);
await expect(page.locator('h1')).toHaveCount(1);
await expect(page.locator('html')).toHaveAttribute('lang', /^(en|zh)$/);
- await expect(page).toHaveTitle(/RestClient\.Net/);
+ await expect(page).toHaveTitle(/^(?:.+ · )?RestClient\.Net$/);
await expect(page.locator('meta[name="description"]')).toHaveAttribute('content', /\S/);
await expect(page.locator('link[rel="canonical"]')).toHaveCount(1);
await expect(page.locator('link[rel="stylesheet"]')).toHaveCount(1);