diff --git a/.circleci/config.yml b/.circleci/config.yml index 6636a03704..1512b0f66d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,6 +42,9 @@ jobs: - run: name: Hugo Build command: yarn hugo --environment production --logLevel info --gc --destination workspace/public + - run: + name: Generate LLM-friendly Markdown + command: yarn build:md - persist_to_workspace: root: workspace paths: diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..1a0665d397 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,16 @@ +{ + "permissions": { + "allow": [ + ], + "deny": [ + "Read(./.env)", + "Read(./.env.*)", + "Read(./secrets/**)", + "Read(./config/credentials.json)", + "Read(./build)" + ], + "ask": [ + "Bash(git push:*)" + ] + } +} diff --git a/.gitignore b/.gitignore index edb903c392..5b5487a3d5 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ tmp # TypeScript build output **/dist/ +**/dist-lambda/ # User context files for AI assistant tools .context/* @@ -45,3 +46,17 @@ tmp # External repos .ext/* + +# Lambda deployment artifacts +deploy/llm-markdown/lambda-edge/markdown-generator/*.zip +deploy/llm-markdown/lambda-edge/markdown-generator/package-lock.json +deploy/llm-markdown/lambda-edge/markdown-generator/.package-tmp/ +deploy/llm-markdown/lambda-edge/markdown-generator/yarn.lock +deploy/llm-markdown/lambda-edge/markdown-generator/config.json + +# JavaScript/TypeScript build artifacts +*.tsbuildinfo +*.d.ts +*.d.ts.map +*.js.map +.eslintcache diff --git a/.s3deploy.yml b/.s3deploy.yml index c4170379e7..0182b85c36 100644 --- a/.s3deploy.yml +++ b/.s3deploy.yml @@ -4,5 +4,5 @@ routes: headers: Cache-Control: "max-age=630720000, no-transform, public" gzip: true - - route: "^.+\\.(html|xml|json|js)$" + - route: "^.+\\.(html|xml|json|js|md)$" gzip: true diff --git a/DOCS-DEPLOYING.md b/DOCS-DEPLOYING.md new file mode 100644 index 0000000000..385e8fbe0b --- /dev/null +++ b/DOCS-DEPLOYING.md @@ -0,0 +1,330 @@ +# Deploying InfluxData Documentation + +This guide covers deploying the docs-v2 site to staging and production environments, as well as LLM markdown generation. + +## Table of Contents + +- [Staging Deployment](#staging-deployment) +- [Production Deployment](#production-deployment) +- [LLM Markdown Generation](#llm-markdown-generation) +- [Testing and Validation](#testing-and-validation) +- [Troubleshooting](#troubleshooting) + +## Staging Deployment + +Staging deployments are manual and run locally with your AWS credentials. + +### Prerequisites + +1. **AWS Credentials** - Configure AWS CLI with appropriate permissions: + ```bash + aws configure + ``` + +2. **s3deploy** - Install the s3deploy binary: + ```bash + ./deploy/ci-install-s3deploy.sh + ``` + +3. **Environment Variables** - Set required variables: + ```bash + export STAGING_BUCKET="test2.docs.influxdata.com" + export AWS_REGION="us-east-1" + export STAGING_CF_DISTRIBUTION_ID="E1XXXXXXXXXX" # Optional + ``` + +### Deploy to Staging + +Use the staging deployment script: + +```bash +yarn deploy:staging +``` + +Or run the script directly: + +```bash +./scripts/deploy-staging.sh +``` + +### What the Script Does + +1. **Builds Hugo site** with staging configuration (`config/staging/hugo.yml`) +2. **Generates LLM-friendly Markdown** (`yarn build:md`) +3. **Uploads to S3** using s3deploy +4. **Invalidates CloudFront cache** (if `STAGING_CF_DISTRIBUTION_ID` is set) + +### Optional Environment Variables + +Skip specific steps for faster iteration: + +```bash +# Skip Hugo build (use existing public/) +export SKIP_BUILD=true + +# Skip markdown generation +export SKIP_MARKDOWN=true + +# Build only (no S3 upload) +export SKIP_DEPLOY=true +``` + +### Example: Test Markdown Generation Only + +```bash +SKIP_DEPLOY=true ./scripts/deploy-staging.sh +``` + +## Production Deployment + +Production deployments are **automatic** via CircleCI when merging to `master`. + +### Workflow + +1. **Build Job** (`.circleci/config.yml`): + - Installs dependencies + - Builds Hugo site with production config + - Generates LLM-friendly Markdown (`yarn build:md`) + - Persists workspace for deploy job + +2. **Deploy Job**: + - Attaches workspace + - Uploads to S3 using s3deploy + - Invalidates CloudFront cache + - Posts success notification to Slack + +### Environment Variables (CircleCI) + +Production deployment requires the following environment variables set in CircleCI: + +- `BUCKET` - Production S3 bucket name +- `REGION` - AWS region +- `CF_DISTRIBUTION_ID` - CloudFront distribution ID +- `SLACK_WEBHOOK_URL` - Slack notification webhook + +### Trigger Production Deploy + +```bash +git push origin master +``` + +CircleCI will automatically build and deploy. + +## LLM Markdown Generation + +Both staging and production deployments generate LLM-friendly Markdown files at build time. + +### Output Files + +The build generates two types of markdown files in `public/`: + +1. **Single-page markdown** (`index.md`) + - Individual page content with frontmatter + - Contains: title, description, URL, product, version, token estimate + +2. **Section bundles** (`index.section.md`) + - Aggregated section with all child pages + - Includes child page list in frontmatter + - Optimized for LLM context windows + +### Generation Script + +```bash +# Generate all markdown +yarn build:md + +# Generate for specific path +node scripts/build-llm-markdown.js --path influxdb3/core/get-started + +# Limit number of files (for testing) +node scripts/build-llm-markdown.js --limit 100 +``` + +### Configuration + +Edit `scripts/build-llm-markdown.js` to adjust: + +```javascript +// Skip files smaller than this (Hugo alias redirects) +const MIN_HTML_SIZE_BYTES = 1024; + +// Token estimation ratio +const CHARS_PER_TOKEN = 4; + +// Concurrency (workers) +const CONCURRENCY = process.env.CI ? 10 : 20; +``` + +### Performance + +- **Speed**: \~105 seconds for 5,000 pages + 500 sections +- **Memory**: \~300MB peak (safe for 2GB CircleCI) +- **Rate**: \~23 files/second with memory-bounded parallelism + +## Testing and Validation + +### Local Testing + +Test markdown generation locally before deploying: + +```bash +# Prerequisites +yarn install +yarn build:ts +npx hugo --quiet + +# Generate markdown for testing +yarn build:md + +# Generate markdown for specific path +node scripts/build-llm-markdown.js --path influxdb3/core/get-started --limit 10 + +# Run validation tests +node cypress/support/run-e2e-specs.js \ + --spec "cypress/e2e/content/markdown-content-validation.cy.js" +``` + +### Validation Checks + +The Cypress tests validate: + +- ✅ No raw Hugo shortcodes (`{{< >}}` or `{{% %}}`) +- ✅ No HTML comments +- ✅ Proper YAML frontmatter with required fields +- ✅ UI elements removed (feedback forms, navigation) +- ✅ GitHub-style callouts (Note, Warning, etc.) +- ✅ Properly formatted tables, lists, and code blocks +- ✅ Product context metadata +- ✅ Clean link formatting + +See [DOCS-TESTING.md](DOCS-TESTING.md) for comprehensive testing documentation. + +## Troubleshooting + +### s3deploy Not Found + +Install the s3deploy binary: + +```bash +./deploy/ci-install-s3deploy.sh +``` + +Verify installation: + +```bash +s3deploy -version +``` + +### Missing Environment Variables + +Check required variables are set: + +```bash +echo $STAGING_BUCKET +echo $AWS_REGION +``` + +Set them if missing: + +```bash +export STAGING_BUCKET="test2.docs.influxdata.com" +export AWS_REGION="us-east-1" +``` + +### AWS Permission Errors + +Ensure your AWS credentials have the required permissions: + +- `s3:PutObject` - Upload files to S3 +- `s3:DeleteObject` - Delete old files from S3 +- `cloudfront:CreateInvalidation` - Invalidate cache + +Check your AWS profile: + +```bash +aws sts get-caller-identity +``` + +### Hugo Build Fails + +Check for: + +- Missing dependencies (`yarn install`) +- TypeScript compilation errors (`yarn build:ts`) +- Invalid Hugo configuration + +Build Hugo separately to isolate the issue: + +```bash +yarn hugo --environment staging +``` + +### Markdown Generation Fails + +Check for: + +- Hugo build completed successfully +- TypeScript compiled (`yarn build:ts`) +- Sufficient memory available + +Test markdown generation separately: + +```bash +yarn build:md --limit 10 +``` + +### CloudFront Cache Not Invalidating + +If you see stale content after deployment: + +1. Check `STAGING_CF_DISTRIBUTION_ID` is set correctly +2. Verify AWS credentials have `cloudfront:CreateInvalidation` permission +3. Manual invalidation: + ```bash + aws cloudfront create-invalidation \ + --distribution-id E1XXXXXXXXXX \ + --paths "/*" + ``` + +### Deployment Timing Out + +For large deployments: + +1. **Skip markdown generation** if unchanged: + ```bash + SKIP_MARKDOWN=true ./scripts/deploy-staging.sh + ``` + +2. **Use s3deploy's incremental upload**: + - s3deploy only uploads changed files + - First deploy is slower, subsequent deploys are faster + +3. **Check network speed**: + - Large uploads require good bandwidth + - Consider deploying from an AWS region closer to the S3 bucket + +## Deployment Checklist + +### Before Deploying to Staging + +- [ ] Run tests locally (`yarn lint`) +- [ ] Build Hugo successfully (`yarn hugo --environment staging`) +- [ ] Generate markdown successfully (`yarn build:md`) +- [ ] Set staging environment variables +- [ ] Have AWS credentials configured + +### Before Merging to Master (Production) + +- [ ] Test on staging first +- [ ] Verify LLM markdown quality +- [ ] Check for broken links (`yarn test:links`) +- [ ] Run code block tests (`yarn test:codeblocks:all`) +- [ ] Review CircleCI configuration changes +- [ ] Ensure all tests pass + +## Related Documentation + +- [Contributing Guide](DOCS-CONTRIBUTING.md) +- [Testing Guide](DOCS-TESTING.md) +- [CircleCI Configuration](.circleci/config.yml) +- [S3 Deploy Configuration](.s3deploy.yml) diff --git a/DOCS-TESTING.md b/DOCS-TESTING.md index b012eb664d..341fff3399 100644 --- a/DOCS-TESTING.md +++ b/DOCS-TESTING.md @@ -11,12 +11,13 @@ This guide covers all testing procedures for the InfluxData documentation, inclu ## Test Types Overview -| Test Type | Purpose | Command | -|-----------|---------|---------| -| **Code blocks** | Validate shell/Python code examples | `yarn test:codeblocks:all` | -| **Link validation** | Check internal/external links | `yarn test:links` | -| **Style linting** | Enforce writing standards | `docker compose run -T vale` | -| **E2E tests** | UI and functionality testing | `yarn test:e2e` | +| Test Type | Purpose | Command | +| ----------------------- | ----------------------------------- | ---------------------------- | +| **Code blocks** | Validate shell/Python code examples | `yarn test:codeblocks:all` | +| **Link validation** | Check internal/external links | `yarn test:links` | +| **Style linting** | Enforce writing standards | `docker compose run -T vale` | +| **Markdown generation** | Generate LLM-friendly Markdown | `yarn build:md` | +| **E2E tests** | UI and functionality testing | `yarn test:e2e` | ## Code Block Testing @@ -70,7 +71,8 @@ See `./test/src/prepare-content.sh` for the full list of variables you may need. For influxctl commands to run in tests, move or copy your `config.toml` file to the `./test` directory. -> [!Warning] +> \[!Warning] +> > - The database you configure in `.env.test` and any written data may be deleted during test runs > - Don't add your `.env.test` files to Git. Git is configured to ignore `.env*` files to prevent accidentally committing credentials @@ -111,6 +113,7 @@ pytest-codeblocks has features for skipping tests and marking blocks as failed. #### "Pytest collected 0 items" Potential causes: + - Check test discovery options in `pytest.ini` - Use `python` (not `py`) for Python code block language identifiers: ```python @@ -121,6 +124,215 @@ Potential causes: # This is ignored ``` +## LLM-Friendly Markdown Generation + +The documentation includes tooling to generate LLM-friendly Markdown versions of documentation pages, both locally via CLI and on-demand via Lambda\@Edge in production. + +### Quick Start + +```bash +# Prerequisites (run once) +yarn install +yarn build:ts +npx hugo --quiet + +# Generate Markdown +node scripts/html-to-markdown.js --path influxdb3/core/get-started --limit 10 + +# Validate generated Markdown +node cypress/support/run-e2e-specs.js \ + --spec "cypress/e2e/content/markdown-content-validation.cy.js" +``` + +### Comprehensive Documentation + +For complete documentation including prerequisites, usage examples, output formats, frontmatter structure, troubleshooting, and architecture details, see the inline documentation: + +```bash +# Or view the first 150 lines in terminal +head -150 scripts/html-to-markdown.js +``` + +The script documentation includes: + +- Prerequisites and setup steps +- Command-line options and examples +- Output file types (single page vs section aggregation) +- Frontmatter structure for both output types +- Testing procedures +- Common issues and solutions +- Architecture overview +- Related files + +### Related Files + +- **CLI tool**: `scripts/html-to-markdown.js` - Comprehensive inline documentation +- **Core logic**: `scripts/lib/markdown-converter.js` - Shared conversion library +- **Lambda handler**: `deploy/llm-markdown/lambda-edge/markdown-generator/index.js` - Production deployment +- **Lambda docs**: `deploy/llm-markdown/README.md` - Deployment guide +- **Cypress tests**: `cypress/e2e/content/markdown-content-validation.cy.js` - Validation tests + +### Frontmatter Structure + +All generated markdown files include structured YAML frontmatter: + +```yaml +--- +title: Page Title +description: Page description for SEO +url: /influxdb3/core/get-started/ +product: InfluxDB 3 Core +version: core +date: 2024-01-15T00:00:00Z +lastmod: 2024-11-20T00:00:00Z +type: page +estimated_tokens: 2500 +--- +``` + +Section pages include additional fields: + +```yaml +--- +type: section +pages: 4 +child_pages: + - title: Set up InfluxDB 3 Core + url: /influxdb3/core/get-started/setup/ + - title: Write data + url: /influxdb3/core/get-started/write/ +--- +``` + +### Testing Generated Markdown + +#### Manual Testing + +```bash +# Generate markdown with verbose output +node scripts/html-to-markdown.js --path influxdb3/core/get-started --limit 2 --verbose + +# Check files were created +ls -la public/influxdb3/core/get-started/*.md + +# View generated content +cat public/influxdb3/core/get-started/index.md + +# Check frontmatter +head -20 public/influxdb3/core/get-started/index.md +``` + +#### Automated Testing with Cypress + +The repository includes comprehensive Cypress tests for markdown validation: + +```bash +# Run all markdown validation tests +node cypress/support/run-e2e-specs.js --spec "cypress/e2e/content/markdown-content-validation.cy.js" + +# Test specific content file +node cypress/support/run-e2e-specs.js \ + --spec "cypress/e2e/content/markdown-content-validation.cy.js" \ + content/influxdb3/core/query-data/execute-queries/_index.md +``` + +The Cypress tests validate: + +- ✅ No raw Hugo shortcodes (`{{< >}}` or `{{% %}}`) +- ✅ No HTML comments +- ✅ Proper YAML frontmatter with required fields +- ✅ UI elements removed (feedback forms, navigation) +- ✅ GitHub-style callouts (Note, Warning, etc.) +- ✅ Properly formatted tables, lists, and code blocks +- ✅ Product context metadata +- ✅ Clean link formatting + +### Common Issues and Solutions + +#### Issue: "No article content found" warnings + +**Cause**: Page doesn't have `
` element (common for index/list pages) + +**Solution**: This is normal behavior. The converter skips pages without article content. To verify: + +```bash +# Check HTML structure +grep -l 'article--content' public/path/to/page/index.html +``` + +#### Issue: "Cannot find module" errors + +**Cause**: TypeScript not compiled (product-mappings.js missing) + +**Solution**: Build TypeScript first: + +```bash +yarn build:ts +ls -la dist/utils/product-mappings.js +``` + +#### Issue: Memory issues when processing all files + +**Cause**: Attempting to process thousands of pages at once + +**Solution**: Use `--limit` flag to process in batches: + +```bash +# Process 1000 files at a time +node scripts/html-to-markdown.js --limit 1000 +``` + +#### Issue: Missing or incorrect product detection + +**Cause**: Product mappings not up to date or path doesn't match known patterns + +**Solution**: + +1. Rebuild TypeScript: `yarn build:ts` +2. Check product mappings in `assets/js/utils/product-mappings.ts` +3. Add new product paths if needed + +### Validation Checklist + +Before committing markdown generation changes: + +- [ ] Run TypeScript build: `yarn build:ts` +- [ ] Build Hugo site: `npx hugo --quiet` +- [ ] Generate markdown for affected paths +- [ ] Run Cypress validation tests +- [ ] Manually check sample output files: + - [ ] Frontmatter is valid YAML + - [ ] No shortcode remnants (`{{<`, `{{%`) + - [ ] No HTML comments (``) + - [ ] Product context is correct + - [ ] Links are properly formatted + - [ ] Code blocks have language identifiers + - [ ] Tables render correctly + +### Architecture + +The markdown generation uses a shared library architecture: + +``` +docs-v2/ +├── scripts/ +│ ├── html-to-markdown.js # CLI wrapper (filesystem operations) +│ └── lib/ +│ └── markdown-converter.js # Core conversion logic (shared library) +├── dist/ +│ └── utils/ +│ └── product-mappings.js # Product detection (compiled from TS) +└── public/ # Generated HTML + Markdown files +``` + +The shared library (`scripts/lib/markdown-converter.js`) is: + +- Used by local markdown generation scripts +- Imported by docs-tooling Lambda\@Edge for on-demand generation +- Tested independently with isolated conversion logic + +For deployment details, see [deploy/lambda-edge/markdown-generator/README.md](deploy/lambda-edge/markdown-generator/README.md). + ## Link Validation with Link-Checker Link validation uses the `link-checker` tool to validate internal and external links in documentation files. @@ -158,8 +370,8 @@ chmod +x link-checker ./link-checker --version ``` -> [!Note] -> Pre-built binaries are currently Linux x86_64 only. For macOS development, use Option 1 to build from source. +> \[!Note] +> Pre-built binaries are currently Linux x86\_64 only. For macOS development, use Option 1 to build from source. ```bash # Clone and build link-checker @@ -188,11 +400,11 @@ cp target/release/link-checker /usr/local/bin/ curl -L -H "Authorization: Bearer $(gh auth token)" \ -o link-checker-linux-x86_64 \ "https://github.com/influxdata/docs-tooling/releases/download/link-checker-v1.2.x/link-checker-linux-x86_64" - + curl -L -H "Authorization: Bearer $(gh auth token)" \ -o checksums.txt \ "https://github.com/influxdata/docs-tooling/releases/download/link-checker-v1.2.x/checksums.txt" - + # Create docs-v2 release gh release create \ --repo influxdata/docs-v2 \ @@ -209,7 +421,7 @@ cp target/release/link-checker /usr/local/bin/ sed -i 's/link-checker-v[0-9.]*/link-checker-v1.2.x/' .github/workflows/pr-link-check.yml ``` -> [!Note] +> \[!Note] > The manual distribution is required because docs-tooling is a private repository and the default GitHub token doesn't have cross-repository access for private repos. #### Core Commands @@ -230,6 +442,7 @@ link-checker config The link-checker automatically handles relative link resolution based on the input type: **Local Files → Local Resolution** + ```bash # When checking local files, relative links resolve to the local filesystem link-checker check public/influxdb3/core/admin/scale-cluster/index.html @@ -238,6 +451,7 @@ link-checker check public/influxdb3/core/admin/scale-cluster/index.html ``` **URLs → Production Resolution** + ```bash # When checking URLs, relative links resolve to the production site link-checker check https://docs.influxdata.com/influxdb3/core/admin/scale-cluster/ @@ -246,6 +460,7 @@ link-checker check https://docs.influxdata.com/influxdb3/core/admin/scale-cluste ``` **Why This Matters** + - **Testing new content**: Tag pages generated locally will be found when testing local files - **Production validation**: Production URLs validate against the live site - **No false positives**: New content won't appear broken when testing locally before deployment @@ -321,6 +536,7 @@ The docs-v2 repository includes automated link checking for pull requests: - **Results reporting**: Broken links reported as GitHub annotations with detailed summaries The workflow automatically: + 1. Detects content changes in PRs using GitHub Files API 2. Downloads latest link-checker binary from docs-v2 releases 3. Builds Hugo site and maps changed content to public HTML files @@ -405,6 +621,7 @@ docs-v2 uses [Lefthook](https://github.com/evilmartians/lefthook) to manage Git ### What Runs Automatically When you run `git commit`, Git runs: + - **Vale**: Style linting (if configured) - **Prettier**: Code formatting - **Cypress**: Link validation tests @@ -459,6 +676,7 @@ For JavaScript code in the documentation UI (`assets/js`): ``` 3. Start Hugo: `yarn hugo server` + 4. In VS Code, select "Debug JS (debug-helpers)" configuration Remember to remove debug statements before committing. @@ -490,6 +708,18 @@ yarn test:codeblocks:stop-monitors - Format code to fit within 80 characters - Use long options in command-line examples (`--option` vs `-o`) +### Markdown Generation + +- Build Hugo site before generating markdown: `npx hugo --quiet` +- Compile TypeScript before generation: `yarn build:ts` +- Test on small subsets first using `--limit` flag +- Use `--verbose` flag to debug conversion issues +- Always run Cypress validation tests after generation +- Check sample output manually for quality +- Verify shortcodes are evaluated (no `{{<` or `{{%` in output) +- Ensure UI elements are removed (no "Copy page", "Was this helpful?") +- Test both single pages (`index.md`) and section pages (`index.section.md`) + ### Link Validation - Test links regularly, especially after content restructuring @@ -511,9 +741,14 @@ yarn test:codeblocks:stop-monitors - **Scripts**: `.github/scripts/` directory - **Test data**: `./test/` directory - **Vale config**: `.ci/vale/styles/` +- **Markdown generation**: + - `scripts/html-to-markdown.js` - CLI wrapper + - `scripts/lib/markdown-converter.js` - Core conversion library + - `deploy/lambda-edge/markdown-generator/` - Lambda deployment + - `cypress/e2e/content/markdown-content-validation.cy.js` - Validation tests ## Getting Help - **GitHub Issues**: [docs-v2 issues](https://github.com/influxdata/docs-v2/issues) - **Good first issues**: [good-first-issue label](https://github.com/influxdata/docs-v2/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) -- **InfluxData CLA**: [Sign here](https://www.influxdata.com/legal/cla/) for substantial contributions \ No newline at end of file +- **InfluxData CLA**: [Sign here](https://www.influxdata.com/legal/cla/) for substantial contributions diff --git a/assets/js/ask-ai.ts b/assets/js/ask-ai.ts index a58dbcb89d..439728a6d1 100644 --- a/assets/js/ask-ai.ts +++ b/assets/js/ask-ai.ts @@ -160,14 +160,21 @@ function getVersionSpecificConfig(configKey: string): unknown { // Try version-specific config first (e.g., ai_sample_questions__v1) if (version && version !== 'n/a') { const versionKey = `${configKey}__v${version}`; - const versionConfig = productData?.product?.[versionKey]; - if (versionConfig) { - return versionConfig; + const product = productData?.product; + if (product && typeof product === 'object' && !Array.isArray(product)) { + const versionConfig = product[versionKey]; + if (versionConfig) { + return versionConfig; + } } } // Fall back to default config - return productData?.product?.[configKey]; + const product = productData?.product; + if (product && typeof product === 'object' && !Array.isArray(product)) { + return product[configKey]; + } + return undefined; } function getProductExampleQuestions(): string { diff --git a/assets/js/components/format-selector.ts b/assets/js/components/format-selector.ts new file mode 100644 index 0000000000..590c85876d --- /dev/null +++ b/assets/js/components/format-selector.ts @@ -0,0 +1,666 @@ +/** + * Format Selector Component + * + * Provides a dropdown menu for users and AI agents to access documentation + * in different formats (Markdown for LLMs, ChatGPT/Claude integration, MCP servers). + * + * FEATURES: + * - Copy page/section as Markdown to clipboard + * - Open page in ChatGPT or Claude with context + * - Connect to MCP servers (Cursor, VS Code) - future enhancement + * - Adaptive UI for leaf nodes (single pages) vs branch nodes (sections) + * - Smart section download for large sections (>10 pages) + * + * UI PATTERN: + * Matches Mintlify's format selector with dark dropdown, icons, and sublabels. + * See `.context/Screenshot 2025-11-13 at 11.39.13 AM.png` for reference. + */ + +interface FormatSelectorConfig { + pageType: 'leaf' | 'branch'; // Leaf = single page, Branch = section with children + markdownUrl: string; + sectionMarkdownUrl?: string; // For branch nodes - aggregated content + markdownContent?: string; // For clipboard copy (lazy-loaded) + pageTitle: string; + pageUrl: string; + + // For branch nodes (sections) + childPageCount?: number; + estimatedTokens?: number; + sectionDownloadUrl?: string; + + // AI integration URLs + chatGptUrl: string; + claudeUrl: string; + + // Future MCP server links + mcpCursorUrl?: string; + mcpVSCodeUrl?: string; +} + +interface FormatSelectorOption { + label: string; + sublabel: string; + icon: string; // SVG icon name or class + action: () => void; + href?: string; // For external links + target?: string; // '_blank' for external links + external: boolean; // Shows ↗ arrow + visible: boolean; // Conditional display based on pageType/size + dataAttribute: string; // For testing (e.g., 'copy-page', 'open-chatgpt') +} + +interface ComponentOptions { + component: HTMLElement; +} + +/** + * Initialize format selector component + * @param {ComponentOptions} options - Component configuration + */ +export default function FormatSelector(options: ComponentOptions) { + const { component } = options; + + // State + let isOpen = false; + let config: FormatSelectorConfig = { + pageType: 'leaf', + markdownUrl: '', + pageTitle: '', + pageUrl: '', + chatGptUrl: '', + claudeUrl: '', + }; + + // DOM elements + const button = component.querySelector('button') as HTMLButtonElement; + const dropdownMenu = component.querySelector( + '[data-dropdown-menu]' + ) as HTMLElement; + + if (!button || !dropdownMenu) { + console.error('Format selector: Missing required elements'); + return; + } + + /** + * Initialize component config from page context and data attributes + */ + function initConfig(): void { + // page-context exports individual properties, not a detect() function + const currentUrl = window.location.href; + const currentPath = window.location.pathname; + + // Determine page type (leaf vs branch) + const childCount = parseInt(component.dataset.childCount || '0', 10); + const pageType: 'leaf' | 'branch' = childCount > 0 ? 'branch' : 'leaf'; + + // Construct markdown URL + // Hugo generates markdown files as index.md in directories matching the URL path + let markdownUrl = currentPath; + if (!markdownUrl.endsWith('.md')) { + // Ensure path ends with / + if (!markdownUrl.endsWith('/')) { + markdownUrl += '/'; + } + // Append index.md + markdownUrl += 'index.md'; + } + + // Construct section markdown URL (for branch pages only) + let sectionMarkdownUrl: string | undefined; + if (pageType === 'branch') { + sectionMarkdownUrl = markdownUrl.replace('index.md', 'index.section.md'); + } + + // Get page title from meta or h1 + const pageTitle = + document + .querySelector('meta[property="og:title"]') + ?.getAttribute('content') || + document.querySelector('h1')?.textContent || + document.title; + + config = { + pageType, + markdownUrl, + sectionMarkdownUrl, + pageTitle, + pageUrl: currentUrl, + childPageCount: childCount, + estimatedTokens: parseInt(component.dataset.estimatedTokens || '0', 10), + sectionDownloadUrl: component.dataset.sectionDownloadUrl, + + // AI integration URLs + chatGptUrl: generateChatGPTUrl(pageTitle, currentUrl, markdownUrl), + claudeUrl: generateClaudeUrl(pageTitle, currentUrl, markdownUrl), + + // Future MCP server links + mcpCursorUrl: component.dataset.mcpCursorUrl, + mcpVSCodeUrl: component.dataset.mcpVSCodeUrl, + }; + + // Update button label based on page type + updateButtonLabel(); + } + + /** + * Update button label: "Copy page for AI" vs "Copy section for AI" + */ + function updateButtonLabel(): void { + const label = + config.pageType === 'leaf' ? 'Copy page for AI' : 'Copy section for AI'; + const buttonText = button.querySelector('[data-button-text]'); + if (buttonText) { + buttonText.textContent = label; + } + } + + /** + * Generate ChatGPT share URL with page context + */ + function generateChatGPTUrl( + title: string, + pageUrl: string, + markdownUrl: string + ): string { + // ChatGPT share URL pattern (as of 2025) + // This may need updating based on ChatGPT's URL scheme + const baseUrl = 'https://chatgpt.com'; + const markdownFullUrl = `${window.location.origin}${markdownUrl}`; + const prompt = `Read from ${markdownFullUrl} so I can ask questions about it.`; + return `${baseUrl}/?q=${encodeURIComponent(prompt)}`; + } + + /** + * Generate Claude share URL with page context + */ + function generateClaudeUrl( + title: string, + pageUrl: string, + markdownUrl: string + ): string { + // Claude.ai share URL pattern (as of 2025) + const baseUrl = 'https://claude.ai/new'; + const markdownFullUrl = `${window.location.origin}${markdownUrl}`; + const prompt = `Read from ${markdownFullUrl} so I can ask questions about it.`; + return `${baseUrl}?q=${encodeURIComponent(prompt)}`; + } + + /** + * Fetch markdown content for clipboard copy + */ + async function fetchMarkdownContent(): Promise { + try { + const response = await fetch(config.markdownUrl); + if (!response.ok) { + throw new Error(`Failed to fetch Markdown: ${response.statusText}`); + } + return await response.text(); + } catch (error) { + console.error('Error fetching Markdown content:', error); + throw error; + } + } + + /** + * Copy content to clipboard + */ + async function copyToClipboard(text: string): Promise { + try { + await navigator.clipboard.writeText(text); + showNotification('Copied to clipboard!', 'success'); + } catch (error) { + console.error('Failed to copy to clipboard:', error); + showNotification('Failed to copy to clipboard', 'error'); + } + } + + /** + * Show notification (integrates with existing notifications module) + */ + function showNotification(message: string, type: 'success' | 'error'): void { + // TODO: Integrate with existing notifications module + // For now, use a simple console log + console.log(`[${type.toUpperCase()}] ${message}`); + + // Optionally add a simple visual notification + const notification = document.createElement('div'); + notification.textContent = message; + notification.style.cssText = ` + position: fixed; + bottom: 20px; + right: 20px; + padding: 12px 20px; + background: ${type === 'success' ? '#10b981' : '#ef4444'}; + color: white; + border-radius: 6px; + z-index: 10000; + font-size: 14px; + `; + document.body.appendChild(notification); + setTimeout(() => notification.remove(), 3000); + } + + /** + * Handle copy page action + */ + async function handleCopyPage(): Promise { + try { + const markdown = await fetchMarkdownContent(); + await copyToClipboard(markdown); + closeDropdown(); + } catch (error) { + console.error('Failed to copy page:', error); + } + } + + /** + * Handle copy section action (aggregates child pages) + */ + async function handleCopySection(): Promise { + try { + // Fetch aggregated section markdown (includes all child pages) + const url = config.sectionMarkdownUrl || config.markdownUrl; + const response = await fetch(url); + + if (!response.ok) { + throw new Error( + `Failed to fetch section markdown: ${response.statusText}` + ); + } + + const markdown = await response.text(); + await copyToClipboard(markdown); + showNotification('Section copied to clipboard', 'success'); + closeDropdown(); + } catch (error) { + console.error('Failed to copy section:', error); + showNotification('Failed to copy section', 'error'); + } + } + + /** + * Handle download page action (for single pages) + * Commented out - not needed right now + */ + /* + function handleDownloadPage(): void { + // Trigger download of current page as markdown + window.open(config.markdownUrl, '_self'); + closeDropdown(); + } + */ + + /** + * Handle download section action + * Commented out - not yet implemented + */ + /* + function handleDownloadSection(): void { + if (config.sectionDownloadUrl) { + window.open(config.sectionDownloadUrl, '_self'); + closeDropdown(); + } + } + */ + + /** + * Handle external link action + */ + function handleExternalLink(url: string): void { + window.open(url, '_blank', 'noopener,noreferrer'); + closeDropdown(); + } + + /** + * Build dropdown options based on config + */ + function buildOptions(): FormatSelectorOption[] { + const options: FormatSelectorOption[] = []; + + // Option 1: Copy page/section + if (config.pageType === 'leaf') { + options.push({ + label: 'Copy page for AI', + sublabel: 'Clean Markdown optimized for AI assistants', + icon: 'document', + action: handleCopyPage, + external: false, + visible: true, + dataAttribute: 'copy-page', + }); + } else { + options.push({ + label: 'Copy section for AI', + sublabel: `${config.childPageCount} pages combined as clean Markdown for AI assistants`, + icon: 'document', + action: handleCopySection, + external: false, + visible: true, + dataAttribute: 'copy-section', + }); + } + + // Option 1b: Download page (for leaf nodes) + // Removed - not needed right now + /* + if (config.pageType === 'leaf' && config.markdownUrl) { + options.push({ + label: 'Download page', + sublabel: 'Download page as Markdown file', + icon: 'download', + action: handleDownloadPage, + external: false, + visible: true, + dataAttribute: 'download-page', + }); + } + */ + + // Option 2: Open in ChatGPT + options.push({ + label: 'Open in ChatGPT', + sublabel: 'Ask questions about this page', + icon: 'chatgpt', + action: () => handleExternalLink(config.chatGptUrl), + href: config.chatGptUrl, + target: '_blank', + external: true, + visible: true, + dataAttribute: 'open-chatgpt', + }); + + // Option 3: Open in Claude + options.push({ + label: 'Open in Claude', + sublabel: 'Ask questions about this page', + icon: 'claude', + action: () => handleExternalLink(config.claudeUrl), + href: config.claudeUrl, + target: '_blank', + external: true, + visible: true, + dataAttribute: 'open-claude', + }); + + // Future: Download section option + // Commented out - not yet implemented + /* + if (config.pageType === 'branch') { + const shouldShowDownload = + (config.childPageCount && config.childPageCount > 10) || + (config.estimatedTokens && config.estimatedTokens >= 50000); + + if (shouldShowDownload && config.sectionDownloadUrl) { + options.push({ + label: 'Download section', + sublabel: `Download all ${config.childPageCount} pages (.zip with /md and /txt folders)`, + icon: 'download', + action: handleDownloadSection, + external: false, + visible: true, + dataAttribute: 'download-section', + }); + } + } + */ + + // Future: MCP server options + // Commented out for now - will be implemented as future enhancement + /* + if (config.mcpCursorUrl) { + options.push({ + label: 'Connect to Cursor', + sublabel: 'Install MCP Server on Cursor', + icon: 'cursor', + action: () => handleExternalLink(config.mcpCursorUrl!), + href: config.mcpCursorUrl, + target: '_blank', + external: true, + visible: true, + dataAttribute: 'connect-cursor', + }); + } + + if (config.mcpVSCodeUrl) { + options.push({ + label: 'Connect to VS Code', + sublabel: 'Install MCP Server on VS Code', + icon: 'vscode', + action: () => handleExternalLink(config.mcpVSCodeUrl!), + href: config.mcpVSCodeUrl, + target: '_blank', + external: true, + visible: true, + dataAttribute: 'connect-vscode', + }); + } + */ + + return options.filter((opt) => opt.visible); + } + + /** + * Get SVG icon for option + */ + function getIconSVG(iconName: string): string { + const icons: Record = { + document: ` + + + `, + chatgpt: ` + + + + + + + + + `, + claude: ` + + `, + download: ` + + + `, + cursor: ` + + `, + vscode: ` + + + + `, + }; + return icons[iconName] || icons.document; + } + + /** + * Render dropdown options + */ + function renderOptions(): void { + const options = buildOptions(); + dropdownMenu.innerHTML = ''; + + options.forEach((option) => { + const optionEl = document.createElement(option.href ? 'a' : 'button'); + optionEl.classList.add('format-selector__option'); + optionEl.setAttribute('data-option', option.dataAttribute); + + if (option.href) { + (optionEl as HTMLAnchorElement).href = option.href; + if (option.target) { + (optionEl as HTMLAnchorElement).target = option.target; + (optionEl as HTMLAnchorElement).rel = 'noopener noreferrer'; + } + } + + optionEl.innerHTML = ` + + ${getIconSVG(option.icon)} + + + + ${option.label} + ${option.external ? '' : ''} + + ${option.sublabel} + + `; + + optionEl.addEventListener('click', (e) => { + if (!option.href) { + e.preventDefault(); + option.action(); + } + }); + + dropdownMenu.appendChild(optionEl); + }); + } + + /** + * Position dropdown relative to button using fixed positioning + * Ensures dropdown stays within viewport bounds + */ + function positionDropdown(): void { + const buttonRect = button.getBoundingClientRect(); + const dropdownWidth = dropdownMenu.offsetWidth; + const viewportWidth = window.innerWidth; + const padding = 8; // Minimum padding from viewport edge + + // Always position dropdown below button with 8px gap + dropdownMenu.style.top = `${buttonRect.bottom + 8}px`; + + // Calculate ideal left position (right-aligned with button) + let leftPos = buttonRect.right - dropdownWidth; + + // Ensure dropdown doesn't go off the left edge + if (leftPos < padding) { + leftPos = padding; + } + + // Ensure dropdown doesn't go off the right edge + if (leftPos + dropdownWidth > viewportWidth - padding) { + leftPos = viewportWidth - dropdownWidth - padding; + } + + dropdownMenu.style.left = `${leftPos}px`; + } + + /** + * Handle resize events to reposition dropdown + */ + function handleResize(): void { + if (isOpen) { + positionDropdown(); + } + } + + /** + * Open dropdown + */ + function openDropdown(): void { + isOpen = true; + dropdownMenu.classList.add('is-open'); + button.setAttribute('aria-expanded', 'true'); + + // Position dropdown relative to button + positionDropdown(); + + // Add listeners for repositioning and closing + setTimeout(() => { + document.addEventListener('click', handleClickOutside); + }, 0); + window.addEventListener('resize', handleResize); + window.addEventListener('scroll', handleResize, true); // Capture scroll on any element + } + + /** + * Close dropdown + */ + function closeDropdown(): void { + isOpen = false; + dropdownMenu.classList.remove('is-open'); + button.setAttribute('aria-expanded', 'false'); + document.removeEventListener('click', handleClickOutside); + window.removeEventListener('resize', handleResize); + window.removeEventListener('scroll', handleResize, true); + } + + /** + * Toggle dropdown + */ + function toggleDropdown(): void { + if (isOpen) { + closeDropdown(); + } else { + openDropdown(); + } + } + + /** + * Handle click outside dropdown + */ + function handleClickOutside(event: Event): void { + if (!component.contains(event.target as Node)) { + closeDropdown(); + } + } + + /** + * Handle button click + */ + function handleButtonClick(event: Event): void { + event.preventDefault(); + event.stopPropagation(); + toggleDropdown(); + } + + /** + * Handle escape key + */ + function handleKeyDown(event: KeyboardEvent): void { + if (event.key === 'Escape' && isOpen) { + closeDropdown(); + button.focus(); + } + } + + /** + * Initialize component + */ + function init(): void { + // Initialize config + initConfig(); + + // Render options + renderOptions(); + + // Add event listeners + button.addEventListener('click', handleButtonClick); + document.addEventListener('keydown', handleKeyDown); + + // Set initial ARIA attributes + button.setAttribute('aria-expanded', 'false'); + button.setAttribute('aria-haspopup', 'true'); + dropdownMenu.setAttribute('role', 'menu'); + } + + // Initialize on load + init(); + + // Expose for debugging + return { + get config() { + return config; + }, + openDropdown, + closeDropdown, + renderOptions, + }; +} diff --git a/assets/js/main.js b/assets/js/main.js index c1b8a70886..826ad9a116 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -35,6 +35,7 @@ import DocSearch from './components/doc-search.js'; import FeatureCallout from './feature-callouts.js'; import FluxGroupKeysDemo from './flux-group-keys.js'; import FluxInfluxDBVersionsTrigger from './flux-influxdb-versions.js'; +import FormatSelector from './components/format-selector.ts'; import InfluxDBVersionDetector from './influxdb-version-detector.ts'; import KeyBinding from './keybindings.js'; import ListFilters from './list-filters.js'; @@ -65,6 +66,7 @@ const componentRegistry = { 'feature-callout': FeatureCallout, 'flux-group-keys-demo': FluxGroupKeysDemo, 'flux-influxdb-versions-trigger': FluxInfluxDBVersionsTrigger, + 'format-selector': FormatSelector, 'influxdb-version-detector': InfluxDBVersionDetector, keybinding: KeyBinding, 'list-filters': ListFilters, diff --git a/assets/js/page-context.js b/assets/js/page-context.ts similarity index 54% rename from assets/js/page-context.js rename to assets/js/page-context.ts index f8e816c932..40560abbff 100644 --- a/assets/js/page-context.js +++ b/assets/js/page-context.ts @@ -1,12 +1,32 @@ -/** This module retrieves browser context information and site data for the +/** + * This module retrieves browser context information and site data for the * current page, version, and product. */ import { products } from './services/influxdata-products.js'; import { influxdbUrls } from './services/influxdb-urls.js'; +import { getProductKeyFromPath } from './utils/product-mappings.js'; -function getCurrentProductData() { +/** + * Product data return type + */ +interface ProductDataResult { + product: string | Record; + urls: Record; +} + +/** + * Get current product data based on URL path + */ +function getCurrentProductData(): ProductDataResult { const path = window.location.pathname; - const mappings = [ + + interface ProductMapping { + pattern: RegExp; + product: Record | string; + urls: Record; + } + + const mappings: ProductMapping[] = [ { pattern: /\/influxdb\/cloud\//, product: products.influxdb_cloud, @@ -87,57 +107,58 @@ function getCurrentProductData() { return { product: 'other', urls: {} }; } -// Return the page context -// (cloud, serverless, oss/enterprise, dedicated, clustered, explorer, other) -function getContext() { - if (/\/influxdb\/cloud\//.test(window.location.pathname)) { - return 'cloud'; - } else if (/\/influxdb3\/core/.test(window.location.pathname)) { - return 'core'; - } else if (/\/influxdb3\/enterprise/.test(window.location.pathname)) { - return 'enterprise'; - } else if (/\/influxdb3\/cloud-serverless/.test(window.location.pathname)) { - return 'serverless'; - } else if (/\/influxdb3\/cloud-dedicated/.test(window.location.pathname)) { - return 'dedicated'; - } else if (/\/influxdb3\/clustered/.test(window.location.pathname)) { - return 'clustered'; - } else if (/\/influxdb3\/explorer/.test(window.location.pathname)) { - return 'explorer'; - } else if ( - /\/(enterprise_|influxdb).*\/v[1-2]\//.test(window.location.pathname) - ) { - return 'oss/enterprise'; - } else { - return 'other'; - } +/** + * Return the page context + * (cloud, serverless, oss/enterprise, dedicated, clustered, core, enterprise, other) + * Uses shared product key detection for consistency + */ +function getContext(): string { + const productKey = getProductKeyFromPath(window.location.pathname); + + // Map product keys to context strings + const contextMap: Record = { + influxdb_cloud: 'cloud', + influxdb3_core: 'core', + influxdb3_enterprise: 'enterprise', + influxdb3_cloud_serverless: 'serverless', + influxdb3_cloud_dedicated: 'dedicated', + influxdb3_clustered: 'clustered', + enterprise_influxdb: 'oss/enterprise', + influxdb: 'oss/enterprise', + }; + + return contextMap[productKey || ''] || 'other'; } // Store the host value for the current page -const currentPageHost = window.location.href.match(/^(?:[^/]*\/){2}[^/]+/g)[0]; +const currentPageHost = + window.location.href.match(/^(?:[^/]*\/){2}[^/]+/g)?.[0] || ''; -function getReferrerHost() { +/** + * Get referrer host from document.referrer + */ +function getReferrerHost(): string { // Extract the protocol and hostname of referrer const referrerMatch = document.referrer.match(/^(?:[^/]*\/){2}[^/]+/g); return referrerMatch ? referrerMatch[0] : ''; } -const context = getContext(), - host = currentPageHost, - hostname = location.hostname, - path = location.pathname, - pathArr = location.pathname.split('/').slice(1, -1), - product = pathArr[0], - productData = getCurrentProductData(), - protocol = location.protocol, - referrer = document.referrer === '' ? 'direct' : document.referrer, - referrerHost = getReferrerHost(), - // TODO: Verify this works since the addition of InfluxDB 3 naming - // and the Core and Enterprise versions. - version = - /^v\d/.test(pathArr[1]) || pathArr[1]?.includes('cloud') - ? pathArr[1].replace(/^v/, '') - : 'n/a'; +const context = getContext(); +const host = currentPageHost; +const hostname = location.hostname; +const path = location.pathname; +const pathArr = location.pathname.split('/').slice(1, -1); +const product = pathArr[0]; +const productData = getCurrentProductData(); +const protocol = location.protocol; +const referrer = document.referrer === '' ? 'direct' : document.referrer; +const referrerHost = getReferrerHost(); +// TODO: Verify this works since the addition of InfluxDB 3 naming +// and the Core and Enterprise versions. +const version = + /^v\d/.test(pathArr[1]) || pathArr[1]?.includes('cloud') + ? pathArr[1].replace(/^v/, '') + : 'n/a'; export { context, diff --git a/assets/js/utils/node-shim.ts b/assets/js/utils/node-shim.ts new file mode 100644 index 0000000000..884ee434c0 --- /dev/null +++ b/assets/js/utils/node-shim.ts @@ -0,0 +1,126 @@ +/** + * Node.js module shim for TypeScript code that runs in both browser and Node.js + * + * This utility provides conditional imports for Node.js-only modules, allowing + * TypeScript files to be bundled for the browser (via Hugo/esbuild) while still + * working in Node.js environments. + * + * @module utils/node-shim + */ + +/** + * Detect if running in Node.js vs browser environment + */ +export const isNode = + typeof process !== 'undefined' && + process.versions != null && + process.versions.node != null; + +/** + * Node.js module references (lazily loaded in Node.js environment) + */ +export interface NodeModules { + fileURLToPath: (url: string) => string; + dirname: (path: string) => string; + join: (...paths: string[]) => string; + readFileSync: (path: string, encoding: BufferEncoding) => string; + existsSync: (path: string) => boolean; + yaml: { load: (content: string) => unknown }; +} + +let nodeModulesCache: NodeModules | undefined; + +/** + * Lazy load Node.js modules (only when running in Node.js) + * + * This function dynamically imports Node.js built-in modules (`url`, `path`, `fs`) + * and third-party modules (`js-yaml`) only when called in a Node.js environment. + * In browser environments, this returns undefined and the imports are tree-shaken out. + * + * @returns Promise resolving to NodeModules or undefined + * + * @example + * ```typescript + * import { loadNodeModules, isNode } from './utils/node-shim.js'; + * + * async function readConfig() { + * if (!isNode) return null; + * + * const nodeModules = await loadNodeModules(); + * if (!nodeModules) return null; + * + * const configPath = nodeModules.join(__dirname, 'config.yml'); + * if (nodeModules.existsSync(configPath)) { + * const content = nodeModules.readFileSync(configPath, 'utf8'); + * return nodeModules.yaml.load(content); + * } + * } + * ``` + */ +export async function loadNodeModules(): Promise { + // Early return for browser - this branch will be eliminated by tree-shaking + if (!isNode) { + return undefined; + } + + // Return cached modules if already loaded + if (nodeModulesCache) { + return nodeModulesCache; + } + + // This code path is never reached in browser builds due to isNode check above + // The dynamic imports will be tree-shaken out by esbuild + try { + // Use Function constructor to hide imports from static analysis + // This prevents esbuild from trying to resolve them during browser builds + const loadModule = new Function('moduleName', 'return import(moduleName)'); + + const [urlModule, pathModule, fsModule, yamlModule] = await Promise.all([ + loadModule('url'), + loadModule('path'), + loadModule('fs'), + loadModule('js-yaml'), + ]); + + nodeModulesCache = { + fileURLToPath: urlModule.fileURLToPath, + dirname: pathModule.dirname, + join: pathModule.join, + readFileSync: fsModule.readFileSync, + existsSync: fsModule.existsSync, + yaml: yamlModule.default as { load: (content: string) => unknown }, + }; + + return nodeModulesCache; + } catch (err) { + if (err instanceof Error) { + console.warn('Failed to load Node.js modules:', err.message); + } + return undefined; + } +} + +/** + * Get the directory path of the current module (Node.js only) + * + * @param importMetaUrl - import.meta.url from the calling module + * @returns Directory path or undefined if not in Node.js + * + * @example + * ```typescript + * import { getModuleDir } from './utils/node-shim.js'; + * + * const moduleDir = await getModuleDir(import.meta.url); + * ``` + */ +export async function getModuleDir( + importMetaUrl: string +): Promise { + const nodeModules = await loadNodeModules(); + if (!nodeModules) { + return undefined; + } + + const filename = nodeModules.fileURLToPath(importMetaUrl); + return nodeModules.dirname(filename); +} diff --git a/assets/js/utils/product-mappings.ts b/assets/js/utils/product-mappings.ts new file mode 100644 index 0000000000..b87a4006f3 --- /dev/null +++ b/assets/js/utils/product-mappings.ts @@ -0,0 +1,234 @@ +/** + * Shared product mapping and detection utilities + * + * This module provides URL-to-product mapping for both browser and Node.js environments. + * In Node.js, it reads from data/products.yml. In browser, it uses fallback mappings. + * + * @module utils/product-mappings + */ + +import { isNode, loadNodeModules } from './node-shim.js'; + +/** + * Product information interface + */ +export interface ProductInfo { + /** Full product display name */ + name: string; + /** Product version or context identifier */ + version: string; +} + +/** + * Full product data from products.yml + */ +export interface ProductData { + name: string; + altname?: string; + namespace: string; + menu_category?: string; + versions?: string[]; + list_order?: number; + latest?: string; + latest_patch?: string; + latest_patches?: Record; + latest_cli?: string | Record; + placeholder_host?: string; + link?: string; + succeeded_by?: string; + detector_config?: { + query_languages?: Record; + characteristics?: string[]; + detection?: { + ping_headers?: Record; + url_contains?: string[]; + }; + }; + ai_sample_questions?: string[]; +} + +/** + * Products YAML data structure + */ +type ProductsData = Record; + +let productsData: ProductsData | null = null; + +/** + * Load products data from data/products.yml (Node.js only) + */ +async function loadProductsData(): Promise { + if (!isNode) { + return null; + } + + if (productsData) { + return productsData; + } + + try { + // Lazy load Node.js modules using shared shim + const nodeModules = await loadNodeModules(); + if (!nodeModules) { + return null; + } + + const __filename = nodeModules.fileURLToPath(import.meta.url); + const __dirname = nodeModules.dirname(__filename); + const productsPath = nodeModules.join( + __dirname, + '../../../data/products.yml' + ); + + if (nodeModules.existsSync(productsPath)) { + const fileContents = nodeModules.readFileSync(productsPath, 'utf8'); + productsData = nodeModules.yaml.load(fileContents) as ProductsData; + return productsData; + } + } catch (err) { + if (err instanceof Error) { + console.warn('Could not load products.yml:', err.message); + } + } + + return null; +} + +/** + * URL pattern to product key mapping + * Used for quick lookups based on URL path + */ +const URL_PATTERN_MAP: Record = { + '/influxdb3/core/': 'influxdb3_core', + '/influxdb3/enterprise/': 'influxdb3_enterprise', + '/influxdb3/cloud-dedicated/': 'influxdb3_cloud_dedicated', + '/influxdb3/cloud-serverless/': 'influxdb3_cloud_serverless', + '/influxdb3/clustered/': 'influxdb3_clustered', + '/influxdb3/explorer/': 'influxdb3_explorer', + '/influxdb/cloud/': 'influxdb_cloud', + '/influxdb/v2': 'influxdb', + '/influxdb/v1': 'influxdb', + '/enterprise_influxdb/': 'enterprise_influxdb', + '/telegraf/': 'telegraf', + '/chronograf/': 'chronograf', + '/kapacitor/': 'kapacitor', + '/flux/': 'flux', +}; + +/** + * Get the product key from a URL path + * + * @param path - URL path (e.g., '/influxdb3/core/get-started/') + * @returns Product key (e.g., 'influxdb3_core') or null + */ +export function getProductKeyFromPath(path: string): string | null { + for (const [pattern, key] of Object.entries(URL_PATTERN_MAP)) { + if (path.includes(pattern)) { + return key; + } + } + return null; +} + +// Fallback product mappings (used in browser and as fallback in Node.js) +const PRODUCT_FALLBACK_MAP: Record = { + influxdb3_core: { name: 'InfluxDB 3 Core', version: 'core' }, + influxdb3_enterprise: { + name: 'InfluxDB 3 Enterprise', + version: 'enterprise', + }, + influxdb3_cloud_dedicated: { + name: 'InfluxDB Cloud Dedicated', + version: 'cloud-dedicated', + }, + influxdb3_cloud_serverless: { + name: 'InfluxDB Cloud Serverless', + version: 'cloud-serverless', + }, + influxdb3_clustered: { name: 'InfluxDB Clustered', version: 'clustered' }, + influxdb3_explorer: { name: 'InfluxDB 3 Explorer', version: 'explorer' }, + influxdb_cloud: { name: 'InfluxDB Cloud (TSM)', version: 'cloud' }, + influxdb: { name: 'InfluxDB', version: 'v1' }, // Will be refined below + enterprise_influxdb: { name: 'InfluxDB Enterprise v1', version: 'v1' }, + telegraf: { name: 'Telegraf', version: 'v1' }, + chronograf: { name: 'Chronograf', version: 'v1' }, + kapacitor: { name: 'Kapacitor', version: 'v1' }, + flux: { name: 'Flux', version: 'v0' }, +}; + +/** + * Get product information from a URL path (synchronous) + * Returns simplified product info with name and version + * + * @param path - URL path to check (e.g., '/influxdb3/core/get-started/') + * @returns Product info or null if no match + * + * @example + * ```typescript + * const product = getProductFromPath('/influxdb3/core/admin/'); + * // Returns: { name: 'InfluxDB 3 Core', version: 'core' } + * ``` + */ +export function getProductFromPath(path: string): ProductInfo | null { + const productKey = getProductKeyFromPath(path); + if (!productKey) { + return null; + } + + // If we have cached YAML data (Node.js), use it + if (productsData && productsData[productKey]) { + const product = productsData[productKey]; + return { + name: product.name, + version: product.latest || product.versions?.[0] || 'unknown', + }; + } + + // Use fallback map + const fallbackInfo = PRODUCT_FALLBACK_MAP[productKey]; + if (!fallbackInfo) { + return null; + } + + // Handle influxdb product which can be v1 or v2 + if (productKey === 'influxdb') { + return { + name: path.includes('/v2') ? 'InfluxDB OSS v2' : 'InfluxDB OSS v1', + version: path.includes('/v2') ? 'v2' : 'v1', + }; + } + + return fallbackInfo; +} + +/** + * Initialize product data from YAML (Node.js only, async) + * Call this in Node.js scripts to load product data before using getProductFromPath + */ +export async function initializeProductData(): Promise { + if (isNode && !productsData) { + await loadProductsData(); + } +} + +/** + * Get full product data from products.yml (Node.js only) + * Note: Call initializeProductData() first to load the YAML data + * + * @param productKey - Product key (e.g., 'influxdb3_core') + * @returns Full product data object or null + */ +export function getProductData(productKey: string): ProductData | null { + if (!isNode) { + console.warn('getProductData() is only available in Node.js environment'); + return null; + } + + // Use cached data (requires initializeProductData() to have been called) + return productsData?.[productKey] || null; +} + +/** + * Export URL pattern map for external use + */ +export { URL_PATTERN_MAP }; diff --git a/assets/styles/components/_format-selector.scss b/assets/styles/components/_format-selector.scss new file mode 100644 index 0000000000..f3be75744d --- /dev/null +++ b/assets/styles/components/_format-selector.scss @@ -0,0 +1,243 @@ +/** + * Format Selector Component Styles + * + * Dropdown menu for accessing documentation in LLM-friendly formats. + * Uses theme colors to match light/dark modes. + */ + +.format-selector { + position: relative; + display: inline-flex; + align-items: center; + margin-left: auto; // Right-align in title container + margin-top: 0.5rem; + + // Position near article title + .title & { + margin-left: auto; + } +} + +.format-selector__button { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + background: $sidebar-search-bg; + color: $article-text; + border: 1px solid $nav-border; + border-radius: $radius; + font-size: 14px; + font-weight: 500; + line-height: 1; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + box-shadow: 2px 2px 6px $sidebar-search-shadow; + + &:hover { + border-color: $sidebar-search-highlight; + box-shadow: 1px 1px 10px rgba($sidebar-search-highlight, .5); + } + + &:focus { + outline: 2px solid $sidebar-search-highlight; + outline-offset: 2px; + } + + &[aria-expanded='true'] { + border-color: $sidebar-search-highlight; + + .format-selector__button-arrow svg { + transform: rotate(180deg); + } + } +} + +.format-selector__button-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + + svg { + width: 100%; + height: 100%; + color: $nav-item; + } +} + +.format-selector__button-text { + font-size: 14px; + font-weight: 500; +} + +.format-selector__button-arrow { + display: inline-flex; + align-items: center; + justify-content: center; + width: 12px; + height: 12px; + margin-left: 0.25rem; + + svg { + width: 100%; + height: 100%; + transition: transform 0.2s ease; + } +} + +// Dropdown menu +.format-selector__dropdown { + position: fixed; // Use fixed to break out of parent stacking context + // Position will be calculated by JavaScript to align with button + min-width: 280px; + max-width: 320px; + background: $article-bg; + border: 1px solid $nav-border; + border-radius: 8px; + box-shadow: 2px 2px 6px $article-shadow; + padding: 0.5rem; + z-index: 10000; // Higher than sidebar and other elements + opacity: 0; + visibility: hidden; + transform: translateY(-8px); + transition: all 0.2s ease; + pointer-events: none; + + &.is-open { + opacity: 1; + visibility: visible; + transform: translateY(0); + pointer-events: auto; + } +} + +// Dropdown options (buttons and links) +.format-selector__option { + display: flex; + align-items: flex-start; + gap: 0.75rem; + width: 100%; + padding: 0.75rem; + background: transparent; + color: $article-text; + border: none; + border-radius: $radius; + text-align: left; + text-decoration: none; + cursor: pointer; + transition: background 0.15s ease; + + &:hover { + background: $sidebar-search-bg; + color: $nav-item-hover; + } + + &:focus { + outline: 2px solid $sidebar-search-highlight; + outline-offset: -2px; + } + + &:not(:last-child) { + margin-bottom: 0.25rem; + } +} + +.format-selector__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + flex-shrink: 0; + margin-top: 2px; // Align with first line of text + + svg { + width: 100%; + height: 100%; + + // Support both stroke and fill-based icons + stroke: $nav-item; + + // For fill-based icons (like OpenAI Blossom), use currentColor + [fill]:not([fill="none"]):not([fill="white"]) { + fill: $nav-item; + } + } +} + +.format-selector__label-group { + display: flex; + flex-direction: column; + gap: 0.25rem; + flex: 1; + min-width: 0; // Allow text truncation +} + +.format-selector__label { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 14px; + font-weight: 500; + line-height: 1.3; + color: $article-text; +} + +.format-selector__external { + display: inline-flex; + align-items: center; + font-size: 12px; + color: $nav-item; + margin-left: 0.25rem; + opacity: 0.7; +} + +.format-selector__sublabel { + font-size: 12px; + line-height: 1.4; + color: $nav-item; +} + +// Responsive adjustments +@media (max-width: 768px) { + .format-selector { + // Stack vertically on mobile + margin-left: 0; + margin-top: 1rem; + } + + .format-selector__dropdown { + right: auto; + left: 0; + min-width: 100%; + max-width: 100%; + } +} + +// Theme styles are now automatically handled by SCSS variables +// that switch based on the active theme (light/dark) + +// Ensure dropdown appears above other content +.format-selector__dropdown { + isolation: isolate; +} + +// Animation for notification (temporary toast) +@keyframes slideInUp { + from { + transform: translateY(100%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +// Add smooth transitions +* { + box-sizing: border-box; +} diff --git a/assets/styles/layouts/_landing.scss b/assets/styles/layouts/_landing.scss index 16aee750de..bfb3d988bb 100644 --- a/assets/styles/layouts/_landing.scss +++ b/assets/styles/layouts/_landing.scss @@ -2,7 +2,7 @@ display: flex; flex-direction: row; position: relative; - overflow: hidden; + overflow: visible; // Changed from hidden to allow format-selector dropdown border-radius: $radius 0 0 $radius; min-height: 700px; @include gradient($landing-artwork-gradient); diff --git a/assets/styles/styles-default.scss b/assets/styles/styles-default.scss index 6f873fb20f..8852a240c3 100644 --- a/assets/styles/styles-default.scss +++ b/assets/styles/styles-default.scss @@ -35,5 +35,6 @@ "layouts/v3-wayfinding"; // Import Components -@import "components/influxdb-version-detector"; +@import "components/influxdb-version-detector", + "components/format-selector"; diff --git a/config/_default/hugo.yml b/config/_default/hugo.yml index b98cf11f7c..c576413cd4 100644 --- a/config/_default/hugo.yml +++ b/config/_default/hugo.yml @@ -55,6 +55,24 @@ outputFormats: mediaType: application/json baseName: pages isPlainText: true + llmstxt: + mediaType: text/plain + baseName: llms + isPlainText: true + notAlternative: true + permalinkable: true + suffixes: + - txt + +outputs: + page: + - HTML + section: + - HTML + # llmstxt disabled for sections - using .md files via Lambda@Edge instead + home: + - HTML + - llmstxt # Root /llms.txt for AI agent discovery # Asset processing configuration for development build: diff --git a/cypress/e2e/content/influxdb-version-detector.cy.js b/cypress/e2e/content/influxdb-version-detector.cy.js index 937dea6775..ba20aa0ed0 100644 --- a/cypress/e2e/content/influxdb-version-detector.cy.js +++ b/cypress/e2e/content/influxdb-version-detector.cy.js @@ -139,13 +139,15 @@ describe('InfluxDB Version Detector Component', function () { // Each describe block will visit the page once describe('Component Data Attributes', function () { - beforeEach(() => { + it('should not throw JavaScript console errors', function () { cy.visit('/test-version-detector/'); - // The trigger is an anchor element with .btn class, not a button cy.contains(modalTriggerSelector, 'Detect my InfluxDB version').click(); - }); - it('should not throw JavaScript console errors', function () { + // Wait for modal to be visible + cy.get('[data-component="influxdb-version-detector"]', { + timeout: 5000, + }).should('be.visible'); + cy.window().then((win) => { const logs = []; const originalError = win.console.error; @@ -177,7 +179,10 @@ describe('InfluxDB Version Detector Component', function () { cy.get('[data-component="influxdb-version-detector"]') .eq(0) .within(() => { - cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.get('#q-url-known .option-button') + .contains('Yes, I know the URL') + .should('be.visible') + .click(); it('should suggest legacy editions for custom URL or hostname', function () { cy.get('#url-input', { timeout: 10000 }) .clear() @@ -358,8 +363,23 @@ describe('InfluxDB Version Detector Component', function () { }); it('should handle cloud context detection', function () { - // Click "Yes, I know the URL" first - cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.visit('/test-version-detector/'); + cy.contains(modalTriggerSelector, 'Detect my InfluxDB version').click(); + + // Wait for the button within the modal and question to be interactable + cy.get('[data-component="influxdb-version-detector"]', { timeout: 5000 }) + .should('be.visible') + .within(() => { + cy.get('#q-url-known', { timeout: 5000 }) + .should('be.visible') + .within(() => { + cy.contains('.option-button', 'Yes, I know the URL', { + timeout: 5000, + }) + .should('be.visible') + .click(); + }); + }); // Wait for URL input question to appear and then enter cloud context cy.get('#q-url-input', { timeout: 10000 }).should('be.visible'); @@ -367,7 +387,7 @@ describe('InfluxDB Version Detector Component', function () { .should('be.visible') .clear() .type('cloud 2'); - cy.get('.submit-button').click(); + cy.get('#q-url-input .submit-button').click(); // Should proceed to next step - either show result or start questionnaire // Don't be too specific about what happens next, just verify it progresses @@ -381,8 +401,23 @@ describe('InfluxDB Version Detector Component', function () { }); it('should handle v3 port detection', function () { - // Click "Yes, I know the URL" first - cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.visit('/test-version-detector/'); + cy.contains(modalTriggerSelector, 'Detect my InfluxDB version').click(); + + // Wait for the button within the modal and question to be interactable + cy.get('[data-component="influxdb-version-detector"]', { timeout: 5000 }) + .should('be.visible') + .within(() => { + cy.get('#q-url-known', { timeout: 5000 }) + .should('be.visible') + .within(() => { + cy.contains('.option-button', 'Yes, I know the URL', { + timeout: 5000, + }) + .should('be.visible') + .click(); + }); + }); // Wait for URL input question to appear and then test v3 port detection (8181) cy.get('#q-url-input', { timeout: 10000 }).should('be.visible'); @@ -390,7 +425,7 @@ describe('InfluxDB Version Detector Component', function () { .should('be.visible') .clear() .type('http://localhost:8181'); - cy.get('.submit-button').click(); + cy.get('#q-url-input .submit-button').click(); // Should progress to either result or questionnaire cy.get('body', { timeout: 15000 }).then(($body) => { @@ -408,10 +443,18 @@ describe('InfluxDB Version Detector Component', function () { cy.visit('/test-version-detector/'); // The trigger is an anchor element with .btn class, not a button cy.contains(modalTriggerSelector, 'Detect my InfluxDB version').click(); + + // Wait for modal to be visible + cy.get('[data-component="influxdb-version-detector"]', { + timeout: 5000, + }).should('be.visible'); }); it('should start questionnaire for unknown URL', function () { // Click "Yes, I know the URL" first - cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.get('#q-url-known .option-button') + .contains('Yes, I know the URL') + .should('be.visible') + .click(); cy.get('#url-input').clear().type('https://unknown-server.com:9999'); cy.get('.submit-button').click(); @@ -422,7 +465,10 @@ describe('InfluxDB Version Detector Component', function () { it('should complete basic questionnaire flow', function () { // Click "Yes, I know the URL" first - cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.get('#q-url-known .option-button') + .contains('Yes, I know the URL') + .should('be.visible') + .click(); // Start questionnaire cy.get('#url-input') @@ -469,7 +515,10 @@ describe('InfluxDB Version Detector Component', function () { it('should NOT recommend InfluxDB 3 for Flux users (regression test)', function () { // Click "Yes, I know the URL" first - cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.get('#q-url-known .option-button') + .contains('Yes, I know the URL') + .should('be.visible') + .click(); cy.get('#url-input').should('be.visible').clear().type('cloud 2'); cy.get('.submit-button').click(); @@ -639,7 +688,10 @@ describe('InfluxDB Version Detector Component', function () { questionnaireScenarios.forEach((scenario) => { it(`should handle questionnaire scenario: ${scenario.name}`, function () { // Click "Yes, I know the URL" first - cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.get('#q-url-known .option-button') + .contains('Yes, I know the URL') + .should('be.visible') + .click(); // Start questionnaire cy.get('#url-input').clear().type('https://unknown-server.com:9999'); @@ -669,7 +721,10 @@ describe('InfluxDB Version Detector Component', function () { it('should NOT recommend InfluxDB 3 for 5+ year installations (time-aware)', function () { // Click "Yes, I know the URL" first - cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.get('#q-url-known .option-button') + .contains('Yes, I know the URL') + .should('be.visible') + .click(); cy.get('#url-input').clear().type('https://unknown-server.com:9999'); cy.get('.submit-button').click(); @@ -693,7 +748,10 @@ describe('InfluxDB Version Detector Component', function () { it('should apply -100 Flux penalty to InfluxDB 3 products', function () { // Click "Yes, I know the URL" first - cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.get('#q-url-known .option-button') + .contains('Yes, I know the URL') + .should('be.visible') + .click(); cy.get('#url-input').clear().type('https://unknown-server.com:9999'); cy.get('.submit-button').click(); @@ -716,7 +774,10 @@ describe('InfluxDB Version Detector Component', function () { const cloudPatterns = ['cloud 2', 'cloud v2', 'influxdb cloud 2']; // Test first pattern in current session - cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.get('#q-url-known .option-button') + .contains('Yes, I know the URL') + .should('be.visible') + .click(); cy.get('#url-input').clear().type(cloudPatterns[0]); cy.get('.submit-button').click(); cy.get('.question.active').should('be.visible'); @@ -725,7 +786,10 @@ describe('InfluxDB Version Detector Component', function () { // Navigation and interaction tests it('should allow going back through questionnaire questions', function () { // Click "Yes, I know the URL" first - cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.get('#q-url-known .option-button') + .contains('Yes, I know the URL') + .should('be.visible') + .click(); // Start questionnaire cy.get('#url-input').clear().type('https://unknown-server.com:9999'); @@ -747,7 +811,10 @@ describe('InfluxDB Version Detector Component', function () { it('should allow restarting questionnaire from results', function () { // Click "Yes, I know the URL" first - cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.get('#q-url-known .option-button') + .contains('Yes, I know the URL') + .should('be.visible') + .click(); // Complete a questionnaire cy.get('#url-input').clear().type('https://unknown-server.com:9999'); @@ -779,11 +846,19 @@ describe('InfluxDB Version Detector Component', function () { cy.visit('/test-version-detector/'); // The trigger is an anchor element with .btn class, not a button cy.contains(modalTriggerSelector, 'Detect my InfluxDB version').click(); + + // Wait for modal to be visible + cy.get('[data-component="influxdb-version-detector"]', { + timeout: 5000, + }).should('be.visible'); }); it('should handle empty URL input gracefully', function () { // Click "Yes, I know the URL" first - cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.get('#q-url-known .option-button') + .contains('Yes, I know the URL') + .should('be.visible') + .click(); cy.get('#url-input').clear(); cy.get('.submit-button').click(); @@ -794,7 +869,10 @@ describe('InfluxDB Version Detector Component', function () { it('should handle invalid URL format gracefully', function () { // Click "Yes, I know the URL" first - cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.get('#q-url-known .option-button') + .contains('Yes, I know the URL') + .should('be.visible') + .click(); cy.get('#url-input').clear().type('not-a-valid-url'); cy.get('.submit-button').click(); @@ -809,11 +887,19 @@ describe('InfluxDB Version Detector Component', function () { cy.visit('/test-version-detector/'); // The trigger is an anchor element with .btn class, not a button cy.contains(modalTriggerSelector, 'Detect my InfluxDB version').click(); + + // Wait for modal to be visible + cy.get('[data-component="influxdb-version-detector"]', { + timeout: 5000, + }).should('be.visible'); }); it('should only show InfluxDB 3 products when SQL is selected', function () { // Click "Yes, I know the URL" first - cy.get('.option-button').contains('Yes, I know the URL').click(); + cy.get('#q-url-known .option-button') + .contains('Yes, I know the URL') + .should('be.visible') + .click(); // Start questionnaire with unknown URL cy.get('#url-input').clear().type('https://unknown-server.com:9999'); diff --git a/cypress/e2e/content/llm-format-selector.cy.js b/cypress/e2e/content/llm-format-selector.cy.js new file mode 100644 index 0000000000..74e6250f3f --- /dev/null +++ b/cypress/e2e/content/llm-format-selector.cy.js @@ -0,0 +1,289 @@ +/** + * E2E tests for LLM-friendly format selector component + * These tests validate the format selector dropdown for both leaf nodes (single pages) + * and branch nodes (sections with children). + */ + +describe('LLM Format Selector', () => { + // Test configuration + const LEAF_PAGE_URL = '/influxdb3/core/get-started/setup/'; + const SMALL_SECTION_URL = '/influxdb3/core/get-started/'; // Section with ≤10 pages + const LARGE_SECTION_URL = '/influxdb3/core/query-data/'; // Section with >10 pages (if exists) + + /** + * Setup: Generate markdown files for test paths + * This runs once before all tests in this suite + */ + before(() => { + cy.log('Generating markdown files for test paths...'); + + // Generate markdown for get-started section (small section + leaf page) + cy.exec( + 'node scripts/html-to-markdown.js --path influxdb3/core/get-started', + { + failOnNonZeroExit: false, + timeout: 60000, + } + ).then((result) => { + if (result.code !== 0) { + cy.log( + 'Warning: get-started markdown generation had issues:', + result.stderr + ); + } + }); + + // Generate markdown for query-data section (large section) + cy.exec( + 'node scripts/html-to-markdown.js --path influxdb3/core/query-data --limit 15', + { + failOnNonZeroExit: false, + timeout: 60000, + } + ).then((result) => { + if (result.code !== 0) { + cy.log( + 'Warning: query-data markdown generation had issues:', + result.stderr + ); + } + cy.log('Markdown files generated successfully'); + }); + }); + + describe('Format Selector - Leaf Nodes (Single Pages)', () => { + beforeEach(() => { + cy.visit(LEAF_PAGE_URL); + + // Wait for component initialization + cy.window().should((win) => { + expect(win.influxdatadocs).to.exist; + expect(win.influxdatadocs.instances).to.exist; + expect(win.influxdatadocs.instances['format-selector']).to.exist; + }); + }); + + it('should display format selector button with correct label', () => { + cy.get('[data-component="format-selector"]') + .should('exist') + .should('be.visible'); + + cy.get( + '[data-component="format-selector"] .format-selector__button' + ).should('contain', 'Copy page for AI'); + }); + + describe('Dropdown functionality', () => { + beforeEach(() => { + // Open dropdown once for all tests in this block + cy.get( + '[data-component="format-selector"] .format-selector__button' + ).trigger('click'); + + // Wait for dropdown animation (0.2s transition + small buffer) + cy.wait(300); + + // Verify dropdown is open + cy.get('[data-dropdown-menu].is-open') + .should('exist') + .should('be.visible'); + }); + + it('should display dropdown menu with all options', () => { + // Check that dropdown has options + cy.get('[data-dropdown-menu].is-open [data-option]').should( + 'have.length.at.least', + 3 + ); // copy-page, open-chatgpt, open-claude + }); + + it('should display "Copy page for AI" option', () => { + cy.get('[data-dropdown-menu].is-open [data-option="copy-page"]') + .should('be.visible') + .should('contain', 'Copy page for AI') + .should('contain', 'Clean Markdown optimized for AI assistants'); + }); + + it('should display "Open in ChatGPT" option with external link indicator', () => { + cy.get('[data-dropdown-menu].is-open [data-option="open-chatgpt"]') + .should('be.visible') + .should('contain', 'Open in ChatGPT') + .should('contain', 'Ask questions about this page') + .should('contain', '↗') + .should('have.attr', 'href') + .and('include', 'chatgpt.com'); + }); + + it('should display "Open in Claude" option with external link indicator', () => { + cy.get('[data-dropdown-menu].is-open [data-option="open-claude"]') + .should('be.visible') + .should('contain', 'Open in Claude') + .should('contain', 'Ask questions about this page') + .should('contain', '↗') + .should('have.attr', 'href') + .and('include', 'claude.ai'); + }); + + it('should display icons for each option', () => { + cy.get('[data-dropdown-menu].is-open [data-option]').each(($option) => { + cy.wrap($option).find('.format-selector__icon').should('exist'); + }); + }); + + it('should open AI integration links in new tab', () => { + cy.get( + '[data-dropdown-menu].is-open [data-option="open-chatgpt"]' + ).should('have.attr', 'target', '_blank'); + + cy.get( + '[data-dropdown-menu].is-open [data-option="open-claude"]' + ).should('have.attr', 'target', '_blank'); + }); + }); + }); + + describe('Format Selector - Branch Nodes (Small Sections)', () => { + beforeEach(() => { + cy.visit(SMALL_SECTION_URL); + + // Wait for component initialization + cy.window().should((win) => { + expect(win.influxdatadocs).to.exist; + expect(win.influxdatadocs.instances).to.exist; + expect(win.influxdatadocs.instances['format-selector']).to.exist; + }); + }); + + it('should show "Copy section for AI" label for branch nodes', () => { + cy.get( + '[data-component="format-selector"] .format-selector__button' + ).should('contain', 'Copy section for AI'); + }); + + describe('Dropdown functionality', () => { + beforeEach(() => { + // Open dropdown once for all tests in this block + cy.get( + '[data-component="format-selector"] .format-selector__button' + ).trigger('click'); + + // Wait for dropdown animation + cy.wait(300); + + // Verify dropdown is open + cy.get('[data-dropdown-menu].is-open') + .should('exist') + .should('be.visible'); + }); + + it('should display "Copy section for AI" option with page count', () => { + cy.get('[data-dropdown-menu].is-open [data-option="copy-section"]') + .should('be.visible') + .should('contain', 'Copy section for AI') + .should( + 'contain', + 'pages combined as clean Markdown for AI assistants' + ); + }); + + it('should NOT show "Download section" option for small sections', () => { + cy.get( + '[data-dropdown-menu].is-open [data-option="download-section"]' + ).should('not.exist'); + }); + + it('should display ChatGPT and Claude options', () => { + cy.get( + '[data-dropdown-menu].is-open [data-option="open-chatgpt"]' + ).should('be.visible'); + + cy.get( + '[data-dropdown-menu].is-open [data-option="open-claude"]' + ).should('be.visible'); + }); + }); + }); + + describe('Format Selector - Branch Nodes (Large Sections)', () => { + beforeEach(() => { + // Skip if large section doesn't exist + cy.visit(LARGE_SECTION_URL, { failOnStatusCode: false }); + + // Wait for component initialization if it exists + cy.window().then((win) => { + if (win.influxdatadocs && win.influxdatadocs.instances) { + expect(win.influxdatadocs.instances['format-selector']).to.exist; + } + }); + }); + + it('should show "Download section" option for large sections (>10 pages)', () => { + // First check if this is actually a large section + cy.get('[data-component="format-selector"]').then(($selector) => { + const childCount = $selector.data('child-count'); + + if (childCount && childCount > 10) { + cy.get('[data-component="format-selector"] button').trigger('click'); + + cy.wait(300); + + cy.get( + '[data-dropdown-menu].is-open [data-option="download-section"]' + ) + .should('be.visible') + .should('contain', 'Download section') + .should('contain', '.zip'); + } else { + cy.log('Skipping: This section has ≤10 pages'); + } + }); + }); + }); + + describe('Markdown Content Quality', () => { + it('should contain actual page content from HTML version', () => { + // First, get the HTML version and extract some text + cy.visit(LEAF_PAGE_URL); + + // Get the page title from h1 + cy.get('h1') + .first() + .invoke('text') + .then((pageTitle) => { + // Get some body content from the article + cy.get('article') + .first() + .invoke('text') + .then((articleText) => { + // Extract a meaningful snippet (first 50 chars of article text, trimmed) + const contentSnippet = articleText.trim().substring(0, 50).trim(); + + // Now fetch the markdown version + cy.request(LEAF_PAGE_URL + 'index.md').then((response) => { + expect(response.status).to.eq(200); + + const markdown = response.body; + + // Basic structure checks + expect(markdown).to.include('---'); // Frontmatter delimiter + expect(markdown).to.match(/^#+ /m); // Has headings + + // Content from HTML should appear in markdown + expect(markdown).to.include(pageTitle.trim()); + expect(markdown).to.include(contentSnippet); + + // Clean markdown (no raw HTML or Hugo syntax) + expect(markdown).to.not.include(''); + expect(markdown).to.not.include(' { + // Test URLs for different page types + const LEAF_PAGE_URL = '/influxdb3/core/get-started/setup/'; + const SECTION_PAGE_URL = '/influxdb3/core/get-started/'; + const ENTERPRISE_INDEX_URL = '/influxdb3/enterprise/'; + + /** + * Setup: Generate markdown files for test paths + * This runs once before all tests in this suite + */ + before(() => { + cy.log('Generating markdown files for test paths...'); + + // Generate markdown for get-started section + cy.exec( + 'node scripts/html-to-markdown.js --path influxdb3/core/get-started', + { + failOnNonZeroExit: false, + timeout: 60000, + } + ).then((result) => { + if (result.code !== 0) { + cy.log( + 'Warning: get-started markdown generation had issues:', + result.stderr + ); + } + }); + + // Generate markdown for enterprise index page + cy.exec( + 'node scripts/html-to-markdown.js --path influxdb3/enterprise --limit 1', + { + failOnNonZeroExit: false, + timeout: 60000, + } + ).then((result) => { + if (result.code !== 0) { + cy.log( + 'Warning: enterprise markdown generation had issues:', + result.stderr + ); + } + cy.log('Markdown files generated successfully'); + }); + }); + + describe('Markdown Format - Basic Validation', () => { + it('should return 200 status for markdown file requests', () => { + cy.request(`${LEAF_PAGE_URL}index.md`).then((response) => { + expect(response.status).to.eq(200); + }); + }); + + it('should have correct content-type for markdown files', () => { + cy.request(`${LEAF_PAGE_URL}index.md`).then((response) => { + // Note: Hugo may serve as text/plain or text/markdown depending on config + expect(response.headers['content-type']).to.match( + /text\/(plain|markdown)/ + ); + }); + }); + + it('should be accessible at URL/index.md', () => { + // Hugo generates markdown as index.md in directory matching URL path + // Note: llmstxt.org spec recommends /path/index.html.md, but we use + // /path/index.md for cleaner URLs and Hugo compatibility + cy.visit(`${LEAF_PAGE_URL}`); + cy.url().then((htmlUrl) => { + const markdownUrl = htmlUrl + 'index.md'; + cy.request(markdownUrl).then((response) => { + expect(response.status).to.eq(200); + }); + }); + }); + }); + + describe('Frontmatter Validation', () => { + it('should start with YAML frontmatter delimiters', () => { + cy.request(`${LEAF_PAGE_URL}index.md`).then((response) => { + expect(response.body).to.match(/^---\n/); + }); + }); + + it('should include required frontmatter fields', () => { + cy.request(`${LEAF_PAGE_URL}index.md`).then((response) => { + const frontmatterMatch = response.body.match(/^---\n([\s\S]*?)\n---/); + expect(frontmatterMatch).to.not.be.null; + + const frontmatter = frontmatterMatch[1]; + expect(frontmatter).to.include('title:'); + expect(frontmatter).to.include('description:'); + expect(frontmatter).to.include('url:'); + }); + }); + + it('should include product context in frontmatter', () => { + cy.request(`${LEAF_PAGE_URL}index.md`).then((response) => { + const frontmatterMatch = response.body.match(/^---\n([\s\S]*?)\n---/); + const frontmatter = frontmatterMatch[1]; + + expect(frontmatter).to.include('product:'); + expect(frontmatter).to.include('product_version:'); + }); + }); + + it('should include date and lastmod fields', () => { + cy.request(`${LEAF_PAGE_URL}index.md`).then((response) => { + const frontmatterMatch = response.body.match(/^---\n([\s\S]*?)\n---/); + const frontmatter = frontmatterMatch[1]; + + expect(frontmatter).to.include('date:'); + expect(frontmatter).to.include('lastmod:'); + }); + }); + }); + + describe('Shortcode Evaluation', () => { + it('should NOT contain raw Hugo shortcodes with {{< >}}', () => { + cy.request(`${LEAF_PAGE_URL}index.md`).then((response) => { + // Check for common shortcode patterns + expect(response.body).to.not.include('{{<'); + expect(response.body).to.not.include('>}}'); + }); + }); + + it('should NOT contain raw Hugo shortcodes with {{% %}}', () => { + cy.request(`${LEAF_PAGE_URL}index.md`).then((response) => { + expect(response.body).to.not.include('{{%'); + expect(response.body).to.not.include('%}}'); + }); + }); + + it('should have evaluated product-name shortcode', () => { + cy.request(`${ENTERPRISE_INDEX_URL}index.md`).then((response) => { + // Should contain "InfluxDB 3 Enterprise" not "{{< product-name >}}" + expect(response.body).to.include('InfluxDB 3 Enterprise'); + expect(response.body).to.not.include('{{< product-name >}}'); + }); + }); + + it('should have evaluated req shortcode for required markers', () => { + cy.request(`${LEAF_PAGE_URL}index.md`).then((response) => { + // Should NOT contain raw {{< req >}} shortcode + expect(response.body).to.not.include('{{< req >}}'); + expect(response.body).to.not.include('{{< req '); + }); + }); + + it('should have evaluated code-placeholder shortcode', () => { + cy.request(`${LEAF_PAGE_URL}index.md`).then((response) => { + // Should NOT contain code-placeholder shortcodes + expect(response.body).to.not.include('{{< code-placeholder'); + expect(response.body).to.not.include('{{% code-placeholder'); + }); + }); + }); + + describe('Comment Removal', () => { + it('should NOT contain HTML comments', () => { + cy.request(`${LEAF_PAGE_URL}index.md`).then((response) => { + // Check for HTML comment patterns + expect(response.body).to.not.include(''); + }); + }); + + it('should NOT contain source file comments', () => { + cy.request(`${ENTERPRISE_INDEX_URL}index.md`).then((response) => { + // Check for the "SOURCE - content/shared/..." comments + expect(response.body).to.not.include('SOURCE -'); + expect(response.body).to.not.include('//SOURCE'); + expect(response.body).to.not.include('content/shared/'); + }); + }); + + it('should NOT contain editorial comments', () => { + cy.request(`${LEAF_PAGE_URL}index.md`).then((response) => { + // Common editorial comment patterns + expect(response.body).to.not.match(/ +
+
+ {{ partial "article/format-selector.html" . }} +
+
diff --git a/layouts/_default/landing-influxdb.llmstxt.txt b/layouts/_default/landing-influxdb.llmstxt.txt new file mode 100644 index 0000000000..4e16954d54 --- /dev/null +++ b/layouts/_default/landing-influxdb.llmstxt.txt @@ -0,0 +1,55 @@ +{{- /* + Generate llms.txt file for landing pages following https://llmstxt.org specification + + Required elements: + 1. H1 with project/site name (required) + 2. Blockquote with brief summary (optional) + 3. Zero or more markdown sections (NO headings allowed) (optional) + 4. H2-delimited file list sections with URLs (optional) + 5. Optional H2 section for secondary information (optional) +*/ -}} +{{- $productKey := "" -}} +{{- $productName := "" -}} +{{- $productDescription := "" -}} + +{{- /* Detect product from URL path */ -}} +{{- if hasPrefix .RelPermalink "/influxdb/cloud/" -}} + {{- $productKey = "influxdb_cloud" -}} +{{- else if hasPrefix .RelPermalink "/influxdb/v2" -}} + {{- $productKey = "influxdb" -}} +{{- end -}} + +{{- /* Get product data from products.yml */ -}} +{{- if $productKey -}} + {{- $product := index .Site.Data.products $productKey -}} + {{- if $product -}} + {{- $productName = $product.name -}} + {{- $productDescription = $product.description -}} + {{- end -}} +{{- end -}} + +{{- /* Use product name or fallback to page title */ -}} +{{- $h1Title := .Title -}} +{{- if $productName -}} + {{- $h1Title = $productName -}} +{{- end -}} + +# {{ $h1Title }} +{{- with $productDescription }} + +> {{ . }} +{{- end }} +{{- with .Description }} + +> {{ . }} +{{- end }} + +This is the landing page for {{ $h1Title }} documentation. Select a section below to get started. +{{- /* List main documentation sections if available */ -}} +{{- if .Pages }} + +## Main documentation sections +{{ range .Pages }} +- [{{ .Title }}]({{ .RelPermalink }}){{ with .Description }}: {{ . }}{{ end }} +{{- end }} +{{- end -}} diff --git a/layouts/index.llmstxt.txt b/layouts/index.llmstxt.txt new file mode 100644 index 0000000000..d55cfbef95 --- /dev/null +++ b/layouts/index.llmstxt.txt @@ -0,0 +1,47 @@ +{{- /* + Root /llms.txt file following https://llmstxt.org specification + + This is the main discovery file for AI agents. + It points to aggregated .section.md files for each major product area. + + Per llmstxt.org spec: + - H1 with site/project name (required) + - Blockquote with brief summary (optional) + - Content sections with details (optional) + - H2-delimited file lists with curated links (optional) +*/ -}} +# InfluxData Documentation + +> Documentation for InfluxDB time series database and related tools including Telegraf, Chronograf, and Kapacitor. + +This documentation covers all InfluxDB versions and ecosystem tools. Each section provides comprehensive guides, API references, and tutorials. + +## InfluxDB 3 + +- [InfluxDB 3 Core](influxdb3/core/index.section.md): Open source time series database optimized for real-time data +- [InfluxDB 3 Enterprise](influxdb3/enterprise/index.section.md): Enterprise features including clustering and high availability +- [InfluxDB Cloud Dedicated](influxdb3/cloud-dedicated/index.section.md): Dedicated cloud deployment with predictable performance +- [InfluxDB Cloud Serverless](influxdb3/cloud-serverless/index.section.md): Serverless cloud deployment with usage-based pricing +- [InfluxDB Clustered](influxdb3/clustered/index.section.md): Self-managed clustered deployment +- [InfluxDB 3 Explorer](influxdb3/explorer/index.md): Web-based data exploration tool + +## InfluxDB 2 + +- [InfluxDB OSS v2](influxdb/v2/index.section.md): Open source version 2.x documentation +- [InfluxDB Cloud (TSM)](influxdb/cloud/index.section.md): Managed cloud service based on InfluxDB 2.x + +## InfluxDB 1 + +- [InfluxDB OSS v1](influxdb/v1/index.section.md): Open source version 1.x documentation +- [InfluxDB Enterprise v1](enterprise_influxdb/v1/index.section.md): Enterprise features for version 1.x + +## Tools and Integrations + +- [Telegraf](telegraf/v1/index.section.md): Plugin-driven server agent for collecting and sending metrics +- [Chronograf](chronograf/v1/index.section.md): User interface and administrative component +- [Kapacitor](kapacitor/v1/index.section.md): Real-time streaming data processing engine +- [Flux](flux/v0/index.section.md): Functional data scripting language + +## API References + +For API documentation, see the API reference section within each product's documentation. diff --git a/layouts/partials/article.html b/layouts/partials/article.html index f97e72b76c..cd93b69ca2 100644 --- a/layouts/partials/article.html +++ b/layouts/partials/article.html @@ -4,6 +4,7 @@

{{ .RenderString .Title }}

{{ partial "article/supported-versions.html" . }} {{ partial "article/page-meta.html" . }} + {{ partial "article/format-selector.html" . }} {{ partial "article/special-state.html" . }} {{ partial "article/stable-version.html" . }} diff --git a/layouts/partials/article/format-selector.html b/layouts/partials/article/format-selector.html new file mode 100644 index 0000000000..b9c36f0bc1 --- /dev/null +++ b/layouts/partials/article/format-selector.html @@ -0,0 +1,87 @@ +{{/* + Format Selector Component + + Provides a dropdown menu for accessing documentation in LLM-friendly formats. + Supports both leaf nodes (single pages) and branch nodes (sections with children). + + Features: + - Copy page/section as Markdown + - Open in ChatGPT or Claude + - Download section ZIP (for large sections) + - Future: MCP server integration + + UI Pattern: Matches Mintlify's format selector style +*/}} + +{{- $childCount := 0 -}} +{{- $isSection := false -}} + +{{/* Determine if this is a section (branch node) by checking for child pages */}} +{{- if .IsSection -}} + {{- $isSection = true -}} + {{- range .Pages -}} + {{- $childCount = add $childCount 1 -}} + {{- end -}} +{{- end -}} + +{{/* Calculate estimated tokens (rough estimate: ~500 tokens per page) */}} +{{- $estimatedTokens := mul $childCount 500 -}} + +{{/* Construct section download URL if applicable */}} +{{- $sectionDownloadUrl := "" -}} +{{- if $isSection -}} + {{- $sectionDownloadUrl = printf "%s-download.zip" .RelPermalink -}} +{{- end -}} + +{{/* Only show format selector on documentation pages, not on special pages */}} +{{- if not (in .RelPermalink "/search") -}} + +
+ {{/* Button triggers dropdown */}} + + + {{/* Dropdown menu - populated by TypeScript */}} + +
+ +{{- end -}} diff --git a/layouts/section/landing-influxdb.llms.txt b/layouts/section/landing-influxdb.llms.txt new file mode 100644 index 0000000000..e0d4a722ca --- /dev/null +++ b/layouts/section/landing-influxdb.llms.txt @@ -0,0 +1,70 @@ +{{- /* + Generate llms.txt file for a section following https://llmstxt.org specification + + Required: H1 with project/product name + Optional: Blockquote with brief summary + Optional: Content sections (NO HEADINGS allowed) + Optional: H2-delimited file list sections with curated links +*/ -}} +{{- $productKey := "" -}} +{{- $productName := "" -}} +{{- $productDescription := "" -}} +{{- $sectionName := .Title -}} + +{{- /* Detect product from URL path */ -}} +{{- if hasPrefix .RelPermalink "/influxdb3/core/" -}} + {{- $productKey = "influxdb3_core" -}} +{{- else if hasPrefix .RelPermalink "/influxdb3/enterprise/" -}} + {{- $productKey = "influxdb3_enterprise" -}} +{{- else if hasPrefix .RelPermalink "/influxdb3/cloud-dedicated/" -}} + {{- $productKey = "influxdb3_cloud_dedicated" -}} +{{- else if hasPrefix .RelPermalink "/influxdb3/cloud-serverless/" -}} + {{- $productKey = "influxdb3_cloud_serverless" -}} +{{- else if hasPrefix .RelPermalink "/influxdb3/clustered/" -}} + {{- $productKey = "influxdb3_clustered" -}} +{{- else if hasPrefix .RelPermalink "/influxdb/cloud/" -}} + {{- $productKey = "influxdb_cloud" -}} +{{- else if hasPrefix .RelPermalink "/influxdb/v2" -}} + {{- $productKey = "influxdb" -}} +{{- else if hasPrefix .RelPermalink "/telegraf/" -}} + {{- $productKey = "telegraf" -}} +{{- else if hasPrefix .RelPermalink "/chronograf/" -}} + {{- $productKey = "chronograf" -}} +{{- else if hasPrefix .RelPermalink "/kapacitor/" -}} + {{- $productKey = "kapacitor" -}} +{{- else if hasPrefix .RelPermalink "/flux/" -}} + {{- $productKey = "flux" -}} +{{- else if hasPrefix .RelPermalink "/influxdb3_explorer/" -}} + {{- $productKey = "influxdb3_explorer" -}} +{{- end -}} + +{{- /* Get product data from products.yml */ -}} +{{- if $productKey -}} + {{- $product := index .Site.Data.products $productKey -}} + {{- if $product -}} + {{- $productName = $product.name -}} + {{- end -}} +{{- end -}} + +{{- /* Use product name for root product sections, otherwise use section title */ -}} +{{- $h1Title := $sectionName -}} +{{- if and $productName (or (eq .RelPermalink (printf "/influxdb3/core/")) (eq .RelPermalink (printf "/influxdb3/enterprise/")) (eq .RelPermalink (printf "/influxdb3/cloud-dedicated/")) (eq .RelPermalink (printf "/influxdb3/cloud-serverless/")) (eq .RelPermalink (printf "/influxdb3/clustered/")) (eq .RelPermalink (printf "/influxdb/cloud/")) (eq .RelPermalink (printf "/influxdb/v2/")) (eq .RelPermalink (printf "/telegraf/v1/")) (eq .RelPermalink (printf "/chronograf/v1/")) (eq .RelPermalink (printf "/kapacitor/v1/")) (eq .RelPermalink (printf "/flux/v0/")) (eq .RelPermalink (printf "/influxdb3_explorer/"))) -}} + {{- $h1Title = $productName -}} +{{- end -}} + +# {{ $h1Title }} +{{- with .Description }} + +> {{ . }} +{{- end }} +{{- with .Content }} +{{ . | plainify | truncate 500 }} +{{- end }} +{{- /* Only show child pages if there are any */ -}} +{{- if .Pages }} + +## Pages in this section +{{ range .Pages }} +- [{{ .Title }}]({{ .RelPermalink }}){{ with .Description }}: {{ . }}{{ end }} +{{- end }} +{{- end -}} diff --git a/package.json b/package.json index 872e11fe6d..22dc9e6864 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,13 @@ { "private": true, - "name": "docs-v2", + "name": "@influxdata/docs-site", "version": "1.0.0", "description": "InfluxDB documentation", "license": "MIT", + "exports": { + "./markdown-converter": "./scripts/lib/markdown-converter.cjs", + "./product-mappings": "./dist/utils/product-mappings.js" + }, "bin": { "docs": "scripts/docs-cli.js" }, @@ -13,6 +17,7 @@ "devDependencies": { "@eslint/js": "^9.18.0", "@evilmartians/lefthook": "^1.7.1", + "@types/js-yaml": "^4.0.9", "@vvago/vale": "^3.12.0", "autoprefixer": ">=10.2.5", "cypress": "^14.0.1", @@ -27,19 +32,29 @@ "postcss-cli": ">=9.1.0", "prettier": "^3.2.5", "prettier-plugin-sql": "^0.18.0", + "remark": "^15.0.1", + "remark-frontmatter": "^5.0.0", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", "typescript": "^5.8.3", "typescript-eslint": "^8.32.1", + "unified": "^11.0.5", "winston": "^3.16.0" }, "dependencies": { + "@types/turndown": "^5.0.6", "axios": "^1.12.0", + "glob": "^10.3.10", "gray-matter": "^4.0.3", "jquery": "^3.7.1", "js-cookie": "^3.0.5", "js-yaml": "^4.1.1", + "jsdom": "^27.2.0", "lefthook": "^1.10.10", "markdown-link": "^0.1.1", "mermaid": "^11.10.0", + "p-limit": "^5.0.0", + "turndown": "^7.2.2", "vanillajs-datepicker": "^1.3.4" }, "scripts": { @@ -52,6 +67,10 @@ "build:agent:instructions": "node ./helper-scripts/build-agent-instructions.js", "build:ts": "tsc --project tsconfig.json --outDir dist", "build:ts:watch": "tsc --project tsconfig.json --outDir dist --watch", + "build:md": "node scripts/build-llm-markdown.js", + "build:md:legacy": "node scripts/html-to-markdown.js", + "build:md:verbose": "node scripts/html-to-markdown.js --verbose", + "deploy:staging": "sh scripts/deploy-staging.sh", "lint": "LEFTHOOK_EXCLUDE=test lefthook run pre-commit && lefthook run pre-push", "pre-commit": "lefthook run pre-commit", "test": "echo \"Run 'yarn test:e2e', 'yarn test:links', 'yarn test:codeblocks:all' or a specific test command. e2e and links test commands can take a glob of file paths to test. Some commands run automatically during the git pre-commit and pre-push hooks.\" && exit 0", diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000000..e3859cc8dd --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,171 @@ +# Documentation Build Scripts + +## html-to-markdown.js + +Converts Hugo-generated HTML files to fully-rendered Markdown with evaluated shortcodes, dereferenced shared content, and removed comments. + +### Purpose + +This script generates production-ready Markdown output for LLM consumption and user downloads. The generated Markdown: + +- Has all Hugo shortcodes evaluated to text (e.g., `{{% product-name %}}` → "InfluxDB 3 Core") +- Includes dereferenced shared content in the body +- Removes HTML/Markdown comments +- Adds product context to frontmatter +- Mirrors the HTML version but in clean Markdown format + +### Usage + +```bash +# Generate all markdown files (run after Hugo build) +yarn build:md + +# Generate with verbose logging +yarn build:md:verbose + +# Generate for specific path +node scripts/html-to-markdown.js --path influxdb3/core + +# Generate limited number for testing +node scripts/html-to-markdown.js --limit 10 + +# Combine options +node scripts/html-to-markdown.js --path telegraf/v1 --verbose +``` + +### Options + +- `--path `: Process specific path within `public/` (default: process all) +- `--limit `: Limit number of files to process (useful for testing) +- `--verbose`: Enable detailed logging of conversion progress + +### Build Process + +1. **Hugo generates HTML** (with all shortcodes evaluated): + ```bash + npx hugo --quiet + ``` + +2. **Script converts HTML to Markdown**: + ```bash + yarn build:md + ``` + +3. **Generated files**: + - Location: `public/**/index.md` (alongside `index.html`) + - Git status: Ignored (entire `public/` directory is gitignored) + - Deployment: Generated at build time, like API docs + +### Features + +#### Product Context Detection + +Automatically detects and adds product information to frontmatter: + +```yaml +--- +title: Set up InfluxDB 3 Core +description: Install, configure, and set up authorization... +url: /influxdb3/core/get-started/setup/ +product: InfluxDB 3 Core +product_version: core +date: 2025-11-13 +lastmod: 2025-11-13 +--- +``` + +Supported products: +- InfluxDB 3 Core, Enterprise, Cloud Dedicated, Cloud Serverless, Clustered +- InfluxDB v2, v1, Cloud (TSM), Enterprise v1 +- Telegraf, Chronograf, Kapacitor, Flux + +#### Turndown Configuration + +Custom Turndown rules for InfluxData documentation: + +- **Code blocks**: Preserves language identifiers +- **GitHub callouts**: Converts to `> [!Note]` format +- **Tables**: GitHub-flavored markdown tables +- **Lists**: Preserves nested lists and formatting +- **Links**: Keeps relative links intact +- **Images**: Preserves alt text and paths + +#### Content Extraction + +Extracts only article content (removes navigation, footer, etc.): +- Target selector: `article.article--content` +- Skips files without article content (with warning) + +### Integration + +**Local Development:** +```bash +# After making content changes +npx hugo --quiet && yarn build:md +``` + +**CircleCI Build Pipeline:** + +The script runs automatically in the CircleCI build pipeline after Hugo generates HTML: + +```yaml +# .circleci/config.yml +- run: + name: Hugo Build + command: yarn hugo --environment production --logLevel info --gc --destination workspace/public +- run: + name: Generate LLM-friendly Markdown + command: node scripts/html-to-markdown.js +``` + +**Build order:** +1. Hugo builds HTML → `workspace/public/**/*.html` +2. `html-to-markdown.js` converts HTML → `workspace/public/**/*.md` +3. All files deployed to S3 + +**Production Build (Manual):** +```bash +npx hugo --quiet +yarn build:md +``` + +**Watch Mode:** +For development with auto-regeneration, run Hugo server and regenerate markdown after content changes: +```bash +# Terminal 1: Hugo server +npx hugo server + +# Terminal 2: After making changes +yarn build:md +``` + +### Performance + +- **Processing speed**: ~10-20 files/second +- **Full site**: 5,581 HTML files in ~5 minutes +- **Memory usage**: Minimal (processes files sequentially) +- **Caching**: None (regenerates from HTML each time) + +### Troubleshooting + +**No article content found:** +``` +⚠️ No article content found in /path/to/file.html +``` +- File doesn't have `article.article--content` selector +- Usually navigation pages or redirects +- Safe to ignore + +**Shortcodes still present:** +- Run after Hugo has generated HTML, not before +- Hugo must complete its build first + +**Missing product context:** +- Check that URL path matches patterns in `PRODUCT_MAP` +- Add new products to the map if needed + +### See Also + +- [Plan document](../.context/PLAN-markdown-rendering.md) - Architecture decisions +- [API docs generation](../api-docs/README.md) - Similar pattern for API reference +- [Package.json scripts](../package.json) - Build commands diff --git a/scripts/build-llm-markdown.js b/scripts/build-llm-markdown.js new file mode 100644 index 0000000000..be939721b6 --- /dev/null +++ b/scripts/build-llm-markdown.js @@ -0,0 +1,453 @@ +#!/usr/bin/env node +/** + * Build LLM-friendly Markdown from Hugo-generated HTML + * + * This script generates static .md files at build time for optimal performance. + * Two-phase approach: + * 1. Convert HTML → individual page markdown (memory-bounded parallelism) + * 2. Combine pages → section bundles (fast string concatenation) + * + */ + +import { glob } from 'glob'; +import fs from 'fs/promises'; +import { readFileSync } from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import { createRequire } from 'module'; +import yaml from 'js-yaml'; +import pLimit from 'p-limit'; + +// Get __dirname equivalent in ESM +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Create require function for CommonJS modules +const require = createRequire(import.meta.url); +const { convertToMarkdown } = require('./lib/markdown-converter.cjs'); + +// ============================================================================ +// CONFIGURATION +// ============================================================================ + +/** + * Minimum file size threshold for processing HTML files. + * Files smaller than this are assumed to be Hugo alias redirects and skipped. + * + * Hugo alias redirects are typically 300-400 bytes (simple meta refresh pages). + * Content pages are typically 30KB-100KB+. + * + * Set to 0 to disable redirect detection (process all files). + * + * @default 1024 (1KB) - Safe threshold with large margin + */ +const MIN_HTML_SIZE_BYTES = 1024; + +/** + * Approximate character-to-token ratio for estimation. + * Used to estimate token count from markdown content length. + * + * @default 4 - Rough heuristic (4 characters ≈ 1 token) + */ +const CHARS_PER_TOKEN = 4; + +// ============================================================================ +// PHASE 1: HTML → MARKDOWN CONVERSION +// ============================================================================ + +/** + * Phase 1: Convert all HTML files to individual page markdown + * Uses memory-bounded parallelism to avoid OOM in CI + */ +async function buildPageMarkdown() { + console.log('📄 Converting HTML to Markdown (individual pages)...\n'); + const startTime = Date.now(); + + // Find all HTML files + const htmlFiles = await glob('public/**/index.html', { + ignore: ['**/node_modules/**', '**/api-docs/**'], + }); + + console.log(`Found ${htmlFiles.length} HTML files\n`); + + // Memory-bounded concurrency + // CircleCI medium (2GB RAM): 10 workers safe + // Local development (16GB RAM): 20 workers faster + const CONCURRENCY = process.env.CI ? 10 : 20; + const limit = pLimit(CONCURRENCY); + + let converted = 0; + let skipped = 0; + const errors = []; + + // Map all files to limited-concurrency tasks + const tasks = htmlFiles.map((htmlPath) => + limit(async () => { + try { + // Check file size before reading (skip Hugo alias redirects) + if (MIN_HTML_SIZE_BYTES > 0) { + const stats = await fs.stat(htmlPath); + if (stats.size < MIN_HTML_SIZE_BYTES) { + skipped++; + return; // Skip redirect page + } + } + + // Read HTML + const html = await fs.readFile(htmlPath, 'utf-8'); + + // Derive URL path for frontmatter + const urlPath = htmlPath + .replace(/^public/, '') + .replace(/\/index\.html$/, '/'); + + // Convert to markdown (JSDOM + Turndown processing) + const markdown = await convertToMarkdown(html, urlPath); + + if (!markdown) { + skipped++; + return; + } + + // Write .md file next to .html + const mdPath = htmlPath.replace(/index\.html$/, 'index.md'); + await fs.writeFile(mdPath, markdown, 'utf-8'); + + converted++; + + // Progress logging + if (converted % 100 === 0) { + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); + const rate = ((converted / (Date.now() - startTime)) * 1000).toFixed( + 0 + ); + const memUsed = ( + process.memoryUsage().heapUsed / + 1024 / + 1024 + ).toFixed(0); + console.log( + ` ✓ ${converted}/${htmlFiles.length} (${rate}/sec, ${elapsed}s elapsed, ${memUsed}MB memory)` + ); + } + } catch (error) { + errors.push({ file: htmlPath, error: error.message }); + console.error(` ✗ ${htmlPath}: ${error.message}`); + } + }) + ); + + // Execute all tasks (p-limit ensures only CONCURRENCY run simultaneously) + await Promise.all(tasks); + + const duration = ((Date.now() - startTime) / 1000).toFixed(1); + const rate = ((converted / (Date.now() - startTime)) * 1000).toFixed(0); + + console.log(`\n✅ Converted ${converted} files (${rate}/sec)`); + if (MIN_HTML_SIZE_BYTES > 0) { + console.log( + `⏭️ Skipped ${skipped} files (Hugo alias redirects < ${MIN_HTML_SIZE_BYTES} bytes)` + ); + } else { + console.log(`⏭️ Skipped ${skipped} files (no article content)`); + } + console.log(`⏱️ Phase 1 time: ${duration}s`); + + if (errors.length > 0) { + console.log(`⚠️ ${errors.length} errors occurred`); + } + + console.log(''); + + return { converted, skipped, errors }; +} + +/** + * Phase 2: Build section bundles by combining individual markdown files + * Fast string concatenation with minimal memory usage + */ +async function buildSectionBundles() { + console.log('📦 Building section bundles...\n'); + const startTime = Date.now(); + + // Find all sections (directories with index.md + child index.md files) + const sections = await findSections(); + + console.log(`Found ${sections.length} sections\n`); + + let built = 0; + const errors = []; + + // High concurrency OK - just string operations, minimal memory + const limit = pLimit(50); + + const tasks = sections.map((section) => + limit(async () => { + try { + // Read parent markdown + const parentMd = await fs.readFile(section.mdPath, 'utf-8'); + + // Read all child markdowns + const childMds = await Promise.all( + section.children.map(async (child) => ({ + markdown: await fs.readFile(child.mdPath, 'utf-8'), + url: child.url, + title: child.title, + })) + ); + + // Combine markdown files (string manipulation only) + const combined = combineMarkdown(parentMd, childMds, section.url); + + // Write section bundle + const sectionMdPath = section.mdPath.replace( + /index\.md$/, + 'index.section.md' + ); + await fs.writeFile(sectionMdPath, combined, 'utf-8'); + + built++; + + if (built % 50 === 0) { + console.log(` ✓ Built ${built}/${sections.length} sections`); + } + } catch (error) { + errors.push({ section: section.url, error: error.message }); + console.error(` ✗ ${section.url}: ${error.message}`); + } + }) + ); + + await Promise.all(tasks); + + const duration = ((Date.now() - startTime) / 1000).toFixed(1); + console.log(`\n✅ Built ${built} section bundles`); + console.log(`⏱️ Phase 2 time: ${duration}s`); + + if (errors.length > 0) { + console.log(`⚠️ ${errors.length} errors occurred`); + } + + console.log(''); + + return { built, errors }; +} + +/** + * Find all sections (parent pages with child pages) + */ +async function findSections() { + const allMdFiles = await glob('public/**/index.md'); + const sections = []; + + for (const mdPath of allMdFiles) { + const dir = path.dirname(mdPath); + + // Find child directories with index.md + const childMdFiles = await glob(path.join(dir, '*/index.md')); + + if (childMdFiles.length === 0) continue; // Not a section + + sections.push({ + mdPath: mdPath, + url: dir.replace(/^public/, '') + '/', + children: childMdFiles.map((childMdPath) => ({ + mdPath: childMdPath, + url: path.dirname(childMdPath).replace(/^public/, '') + '/', + title: extractTitleFromMd(childMdPath), + })), + }); + } + + return sections; +} + +/** + * Extract title from markdown file (quick regex, no full parsing) + */ +function extractTitleFromMd(mdPath) { + try { + const content = readFileSync(mdPath, 'utf-8'); + const match = content.match(/^---[\s\S]+?title:\s*(.+?)$/m); + return match ? match[1].trim() : 'Untitled'; + } catch { + return 'Untitled'; + } +} + +/** + * Combine parent and child markdown into section bundle + */ +function combineMarkdown(parentMd, childMds, sectionUrl) { + // Parse parent frontmatter + content + const parent = parseMarkdown(parentMd); + + // Parse child frontmatter + content + const children = childMds.map(({ markdown, url, title }) => { + const child = parseMarkdown(markdown); + + // Remove h1 heading (will be added as h2 to avoid duplicate) + const contentWithoutH1 = child.content.replace(/^#\s+.+?\n+/, ''); + + return { + title: child.frontmatter.title || title, + url: child.frontmatter.url || url, // Use full URL from frontmatter + content: `## ${child.frontmatter.title || title}\n\n${contentWithoutH1}`, + tokens: child.frontmatter.estimated_tokens || 0, + }; + }); + + // Calculate total tokens + const totalTokens = + (parent.frontmatter.estimated_tokens || 0) + + children.reduce((sum, c) => sum + c.tokens, 0); + + // Sanitize description (remove newlines, truncate to reasonable length) + let description = parent.frontmatter.description || ''; + description = description + .replace(/\s+/g, ' ') // Replace all whitespace (including newlines) with single space + .trim() + .substring(0, 500); // Truncate to 500 characters max + + // Build section frontmatter object (will be serialized to YAML) + const frontmatterObj = { + title: parent.frontmatter.title, + description: description, + url: parent.frontmatter.url || sectionUrl, // Use full URL from parent frontmatter + product: parent.frontmatter.product || '', + type: 'section', + pages: children.length + 1, + estimated_tokens: totalTokens, + child_pages: children.map((c) => ({ + url: c.url, + title: c.title, + })), + }; + + // Serialize to YAML (handles special characters properly) + const sectionFrontmatter = + '---\n' + + yaml + .dump(frontmatterObj, { + lineWidth: -1, // Disable line wrapping + noRefs: true, // Disable anchors/aliases + }) + .trim() + + '\n---'; + + // Combine all content + const allContent = [parent.content, ...children.map((c) => c.content)].join( + '\n\n---\n\n' + ); + + return `${sectionFrontmatter}\n\n${allContent}\n`; +} + +/** + * Parse markdown into frontmatter + content + */ +function parseMarkdown(markdown) { + const match = markdown.match(/^---\n([\s\S]+?)\n---\n\n([\s\S]+)$/); + + if (!match) { + return { frontmatter: {}, content: markdown }; + } + + try { + const frontmatter = yaml.load(match[1]); + const content = match[2]; + return { frontmatter, content }; + } catch (error) { + console.warn('Failed to parse frontmatter:', error.message); + return { frontmatter: {}, content: markdown }; + } +} + +// ============================================================================ +// COMMAND-LINE ARGUMENT PARSING +// ============================================================================ + +/** + * Parse command-line arguments + */ +function parseArgs() { + const args = process.argv.slice(2); + const options = { + environment: null, + }; + + for (let i = 0; i < args.length; i++) { + if ((args[i] === '-e' || args[i] === '--env') && args[i + 1]) { + options.environment = args[++i]; + } + } + + return options; +} + +// Parse arguments and set environment +const cliOptions = parseArgs(); +if (cliOptions.environment) { + process.env.HUGO_ENV = cliOptions.environment; +} + +/** + * Main execution + */ +async function main() { + console.log('🚀 Building LLM-friendly Markdown\n'); + + // Show environment if specified + if (cliOptions.environment) { + console.log(`🌍 Environment: ${cliOptions.environment}\n`); + } + + console.log('════════════════════════════════\n'); + + const overallStart = Date.now(); + + // Phase 1: Generate individual page markdown + const pageResults = await buildPageMarkdown(); + + // Phase 2: Build section bundles + const sectionResults = await buildSectionBundles(); + + // Summary + const totalDuration = ((Date.now() - overallStart) / 1000).toFixed(1); + const totalFiles = pageResults.converted + sectionResults.built; + + console.log('════════════════════════════════\n'); + console.log('📊 Summary:'); + console.log(` Pages: ${pageResults.converted}`); + console.log(` Sections: ${sectionResults.built}`); + console.log(` Total: ${totalFiles} markdown files`); + console.log(` Skipped: ${pageResults.skipped} (no article content)`); + + const totalErrors = pageResults.errors.length + sectionResults.errors.length; + if (totalErrors > 0) { + console.log(` Errors: ${totalErrors}`); + } + + console.log(` Time: ${totalDuration}s\n`); + + // Exit with error code if there were errors + if (totalErrors > 0) { + process.exit(1); + } +} + +// Run if called directly +main().catch((error) => { + console.error('Fatal error:', error); + process.exit(1); +}); + +// Export functions for testing +export { + buildPageMarkdown, + buildSectionBundles, + findSections, + combineMarkdown, + parseMarkdown, +}; diff --git a/scripts/deploy-staging.sh b/scripts/deploy-staging.sh new file mode 100755 index 0000000000..aedb35d973 --- /dev/null +++ b/scripts/deploy-staging.sh @@ -0,0 +1,182 @@ +#!/bin/bash +# +# Deploy docs-v2 to staging environment +# +# This script handles the complete staging deployment workflow: +# 1. Build Hugo site with staging config +# 2. Generate LLM-friendly Markdown +# 3. Deploy to S3 staging bucket +# 4. Invalidate CloudFront cache +# +# Usage: +# ./scripts/deploy-staging.sh +# +# Required environment variables: +# STAGING_BUCKET - S3 bucket name (e.g., new-docs-test-docsbucket-1ns6x5tp79507) +# AWS_REGION - AWS region (e.g., us-east-1) +# STAGING_CF_DISTRIBUTION_ID - CloudFront distribution ID (optional, for cache invalidation) +# +# Optional environment variables: +# STAGING_URL - Staging site URL (default: https://test2.docs.influxdata.com) +# SKIP_BUILD - Set to 'true' to skip Hugo build (use existing public/) +# SKIP_MARKDOWN - Set to 'true' to skip markdown generation +# SKIP_DEPLOY - Set to 'true' to build only (no S3 upload) +# + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Helper functions +info() { + echo -e "${BLUE}ℹ${NC} $1" +} + +success() { + echo -e "${GREEN}✓${NC} $1" +} + +warning() { + echo -e "${YELLOW}⚠${NC} $1" +} + +error() { + echo -e "${RED}✗${NC} $1" + exit 1 +} + +# Validate required environment variables +validate_env() { + local missing=() + + if [ -z "$STAGING_BUCKET" ]; then + missing+=("STAGING_BUCKET") + fi + + if [ -z "$AWS_REGION" ]; then + missing+=("AWS_REGION") + fi + + if [ ${#missing[@]} -gt 0 ]; then + error "Missing required environment variables: ${missing[*]}" + fi + + # Set default staging URL if not provided + if [ -z "$STAGING_URL" ]; then + STAGING_URL="https://test2.docs.influxdata.com" + fi + export STAGING_URL + + success "Environment variables validated" +} + +# Check if s3deploy is installed +check_s3deploy() { + if ! command -v s3deploy &> /dev/null; then + error "s3deploy not found. Install with: deploy/ci-install-s3deploy.sh" + fi + success "s3deploy found: $(s3deploy -V | head -1)" +} + +# Build Hugo site +build_hugo() { + if [ "$SKIP_BUILD" = "true" ]; then + warning "Skipping Hugo build (SKIP_BUILD=true)" + return + fi + + info "Building Hugo site with staging config..." + yarn hugo --environment staging --logLevel info --gc --destination public + success "Hugo build complete" +} + +# Generate LLM-friendly Markdown +build_markdown() { + if [ "$SKIP_MARKDOWN" = "true" ]; then + warning "Skipping markdown generation (SKIP_MARKDOWN=true)" + return + fi + + info "Generating LLM-friendly Markdown..." + yarn build:md -e staging + success "Markdown generation complete" +} + +# Deploy to S3 +deploy_to_s3() { + if [ "$SKIP_DEPLOY" = "true" ]; then + warning "Skipping S3 deployment (SKIP_DEPLOY=true)" + return + fi + + info "Deploying to S3 bucket: $STAGING_BUCKET" + s3deploy -source=public/ \ + -bucket="$STAGING_BUCKET" \ + -region="$AWS_REGION" \ + -distribution-id="${STAGING_CF_DISTRIBUTION_ID}" \ + -key=$AWS_ACCESS_KEY_ID \ + -secret=$AWS_SECRET_KEY \ + -force \ + -v + success "Deployment to S3 complete" +} + +# Invalidate CloudFront cache +invalidate_cloudfront() { + if [ "$SKIP_DEPLOY" = "true" ] || [ -z "$STAGING_CF_DISTRIBUTION_ID" ]; then + return + fi + + info "CloudFront cache invalidation initiated" + if [ -n "$STAGING_CF_DISTRIBUTION_ID" ]; then + info "Distribution ID: $STAGING_CF_DISTRIBUTION_ID" + success "Cache will be invalidated by s3deploy" + else + warning "No STAGING_CF_DISTRIBUTION_ID set, skipping cache invalidation" + fi +} + +# Print summary +print_summary() { + echo "" + echo "════════════════════════════════════════" + success "Staging deployment complete!" + echo "════════════════════════════════════════" + echo "" + info "Staging URL: $STAGING_URL" + if [ -n "$STAGING_CF_DISTRIBUTION_ID" ]; then + info "CloudFront: $STAGING_CF_DISTRIBUTION_ID" + warning "Cache invalidation may take 5-10 minutes" + fi + echo "" +} + +# Main execution +main() { + echo "" + echo "════════════════════════════════════════" + info "docs-v2 Staging Deployment" + echo "════════════════════════════════════════" + echo "" + + validate_env + check_s3deploy + + echo "" + build_hugo + build_markdown + + echo "" + deploy_to_s3 + invalidate_cloudfront + + print_summary +} + +# Run main function +main diff --git a/scripts/html-to-markdown.js b/scripts/html-to-markdown.js new file mode 100644 index 0000000000..8f20f19fc8 --- /dev/null +++ b/scripts/html-to-markdown.js @@ -0,0 +1,461 @@ +#!/usr/bin/env node + +/** + * HTML to Markdown Converter CLI for InfluxData Documentation + * + * Generates LLM-friendly Markdown from Hugo-generated HTML documentation. + * This script is the local CLI companion to the Lambda@Edge function that serves + * Markdown on-demand at docs.influxdata.com. + * + * ## Architecture + * + * The core conversion logic lives in ./lib/markdown-converter.js, which is shared + * between this CLI tool and the Lambda@Edge function in deploy/llm-markdown/. + * This ensures local builds and production Lambda use identical conversion logic. + * + * ## Prerequisites + * + * Before running this script, you must: + * + * 1. Install dependencies: + * ```bash + * yarn install + * ``` + * + * 2. Compile TypeScript (for product mappings): + * ```bash + * yarn build:ts + * ``` + * + * 3. Build the Hugo site: + * ```bash + * npx hugo --quiet + * ``` + * + * ## Usage + * + * Basic usage: + * ```bash + * node scripts/html-to-markdown.js [options] + * ``` + * + * ## Options + * + * --path Process specific content path relative to public/ directory + * Example: influxdb3/core/get-started + * + * --limit Limit number of files to process (useful for testing) + * Example: --limit 10 + * + * -e, --env Set environment (development, staging, production) + * Controls base URL in frontmatter (matches Hugo's -e flag) + * Example: -e staging + * + * --verbose Enable detailed logging showing each file processed + * + * ## Examples + * + * Generate Markdown for all documentation: + * ```bash + * node scripts/html-to-markdown.js + * ``` + * + * Generate Markdown for InfluxDB 3 Core documentation: + * ```bash + * node scripts/html-to-markdown.js --path influxdb3/core + * ``` + * + * Generate Markdown for a specific section (testing): + * ```bash + * node scripts/html-to-markdown.js --path influxdb3/core/get-started --limit 10 + * ``` + * + * Generate with verbose output: + * ```bash + * node scripts/html-to-markdown.js --path influxdb3/core --limit 5 --verbose + * ``` + * + * Generate Markdown with staging URLs: + * ```bash + * node scripts/html-to-markdown.js --path influxdb3/core -e staging + * ``` + * + * ## Output Files + * + * This script generates two types of Markdown files: + * + * 1. **Single page**: `index.md` + * - Mirrors the HTML page structure + * - Contains YAML frontmatter with title, description, URL, product info + * - Located alongside the source `index.html` + * + * 2. **Section aggregation**: `index.section.md` + * - Combines parent page + all child pages in one file + * - Optimized for LLM context windows + * - Only generated for pages that have child pages + * - Enhanced frontmatter includes child page list and token estimate + * + * ## Frontmatter Structure + * + * Single page frontmatter: + * ```yaml + * --- + * title: Page Title + * description: Page description from meta tags + * url: /influxdb3/core/path/to/page/ + * product: InfluxDB 3 Core + * version: core + * --- + * ``` + * + * Section aggregation frontmatter includes additional fields: + * ```yaml + * --- + * title: Section Title + * description: Section description + * url: /influxdb3/core/section/ + * type: section + * pages: 5 + * estimated_tokens: 12500 + * product: InfluxDB 3 Core + * version: core + * child_pages: + * - url: /influxdb3/core/section/page1/ + * title: Page 1 Title + * - url: /influxdb3/core/section/page2/ + * title: Page 2 Title + * --- + * ``` + * + * ## Testing Generated Markdown + * + * Use Cypress to validate generated Markdown: + * ```bash + * node cypress/support/run-e2e-specs.js \ + * --spec "cypress/e2e/content/markdown-content-validation.cy.js" + * ``` + * + * ## Common Issues + * + * **Error: Directory not found** + * - Solution: Run `npx hugo --quiet` first to generate HTML files + * + * **No article content found warnings** + * - This is normal for alias/redirect pages + * - The script skips these pages automatically + * + * **Memory issues with large builds** + * - Use `--path` to process specific sections + * - Use `--limit` for testing with small batches + * - Script includes periodic garbage collection hints + * + * ## Related Files + * + * - Core logic: `scripts/lib/markdown-converter.js` + * - Lambda handler: `deploy/llm-markdown/lambda-edge/markdown-generator/index.js` + * - Product detection: `dist/utils/product-mappings.js` (compiled from TypeScript) + * - Cypress tests: `cypress/e2e/content/markdown-content-validation.cy.js` + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { + convertToMarkdown, + convertSectionToMarkdown, +} from './lib/markdown-converter.cjs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Parse command line arguments +const args = process.argv.slice(2); +const options = { + publicDir: path.join(__dirname, '..', 'public'), + limit: null, + verbose: false, + specificPath: null, + environment: null, +}; + +// Parse command-line arguments +for (let i = 0; i < args.length; i++) { + if (args[i] === '--path' && args[i + 1]) { + options.specificPath = args[++i]; + } else if (args[i] === '--limit' && args[i + 1]) { + options.limit = parseInt(args[++i], 10); + } else if ((args[i] === '-e' || args[i] === '--env') && args[i + 1]) { + options.environment = args[++i]; + } else if (args[i] === '--verbose') { + options.verbose = true; + } +} + +// Set HUGO_ENV environment variable based on --env flag (matches Hugo's -e flag behavior) +if (options.environment) { + process.env.HUGO_ENV = options.environment; + console.log(`🌍 Environment set to: ${options.environment}`); +} + +/** + * Check if a directory is a section (has child directories with index.html) + */ +function isSection(dirPath) { + try { + const files = fs.readdirSync(dirPath); + return files.some((file) => { + const fullPath = path.join(dirPath, file); + const stat = fs.statSync(fullPath); + return ( + stat.isDirectory() && fs.existsSync(path.join(fullPath, 'index.html')) + ); + }); + } catch (error) { + return false; + } +} + +/** + * Find all child page HTML files in a section + */ +function findChildPages(sectionPath) { + try { + const files = fs.readdirSync(sectionPath); + const childPages = []; + + for (const file of files) { + const fullPath = path.join(sectionPath, file); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + const childIndexPath = path.join(fullPath, 'index.html'); + if (fs.existsSync(childIndexPath)) { + childPages.push(childIndexPath); + } + } + } + + return childPages; + } catch (error) { + console.error( + `Error finding child pages in ${sectionPath}:`, + error.message + ); + return []; + } +} + +/** + * Convert single HTML file to Markdown using the shared library + */ +async function convertHtmlFileToMarkdown(htmlFilePath) { + try { + const htmlContent = fs.readFileSync(htmlFilePath, 'utf-8'); + + // Derive URL path from file path + const relativePath = path.relative(options.publicDir, htmlFilePath); + const urlPath = + '/' + relativePath.replace(/\/index\.html$/, '/').replace(/\\/g, '/'); + + // Use shared conversion function + const markdown = await convertToMarkdown(htmlContent, urlPath); + if (!markdown) { + return null; + } + + // Write to index.md in same directory + const markdownFilePath = htmlFilePath.replace(/index\.html$/, 'index.md'); + fs.writeFileSync(markdownFilePath, markdown, 'utf-8'); + + if (options.verbose) { + console.log(` ✓ Converted: ${relativePath}`); + } + + return markdownFilePath; + } catch (error) { + console.error(` ✗ Error converting ${htmlFilePath}:`, error.message); + return null; + } +} + +/** + * Aggregate section and child page markdown using the shared library + */ +async function aggregateSectionMarkdown(sectionHtmlPath) { + try { + const sectionDir = path.dirname(sectionHtmlPath); + + // Read section HTML + const sectionHtml = fs.readFileSync(sectionHtmlPath, 'utf-8'); + + // Derive URL path + const sectionUrlPath = + '/' + + path + .relative(options.publicDir, sectionHtmlPath) + .replace(/\/index\.html$/, '/') + .replace(/\\/g, '/'); + + // Find and read child pages + const childPaths = findChildPages(sectionDir); + const childHtmls = []; + + for (const childPath of childPaths) { + try { + const childHtml = fs.readFileSync(childPath, 'utf-8'); + const childUrl = + '/' + + path + .relative(options.publicDir, childPath) + .replace(/\/index\.html$/, '/') + .replace(/\\/g, '/'); + + childHtmls.push({ html: childHtml, url: childUrl }); + } catch (error) { + if (options.verbose) { + console.warn(` ⚠️ Could not read child page: ${childPath}`); + } + } + } + + // Use shared conversion function + const markdown = await convertSectionToMarkdown( + sectionHtml, + sectionUrlPath, + childHtmls + ); + + return markdown; + } catch (error) { + console.error( + `Error aggregating section ${sectionHtmlPath}:`, + error.message + ); + return null; + } +} + +/** + * Find all HTML files recursively + */ +function findHtmlFiles(dir, fileList = []) { + const files = fs.readdirSync(dir); + + for (const file of files) { + const filePath = path.join(dir, file); + const stat = fs.statSync(filePath); + + if (stat.isDirectory()) { + findHtmlFiles(filePath, fileList); + } else if (file === 'index.html') { + fileList.push(filePath); + } + } + + return fileList; +} + +/** + * Main function + */ +async function main() { + console.log('🚀 Starting HTML to Markdown conversion...\n'); + + const startDir = options.specificPath + ? path.join(options.publicDir, options.specificPath) + : options.publicDir; + + if (!fs.existsSync(startDir)) { + console.error(`❌ Error: Directory not found: ${startDir}`); + console.error(' Run "npx hugo --quiet" first to generate HTML files.'); + process.exit(1); + } + + console.log(`📂 Scanning: ${path.relative(process.cwd(), startDir)}`); + + const htmlFiles = findHtmlFiles(startDir); + + // Sort files by depth (shallow first) so root index.html files are processed first + htmlFiles.sort((a, b) => { + const depthA = a.split(path.sep).length; + const depthB = b.split(path.sep).length; + return depthA - depthB; + }); + + const totalFiles = options.limit + ? Math.min(htmlFiles.length, options.limit) + : htmlFiles.length; + + console.log(`📄 Found ${htmlFiles.length} HTML files`); + if (options.limit) { + console.log( + `🎯 Processing first ${totalFiles} files (--limit ${options.limit})` + ); + } + console.log(''); + + let converted = 0; + let skipped = 0; + let sectionsGenerated = 0; + + const filesToProcess = htmlFiles.slice(0, totalFiles); + + for (let i = 0; i < filesToProcess.length; i++) { + const htmlFile = filesToProcess[i]; + + if (!options.verbose && i > 0 && i % 100 === 0) { + console.log(` Progress: ${i}/${totalFiles} files...`); + } + + // Generate regular index.md + const result = await convertHtmlFileToMarkdown(htmlFile); + if (result) { + converted++; + } else { + skipped++; + } + + // Check if this is a section and generate aggregated markdown + const htmlDir = path.dirname(htmlFile); + if (result && isSection(htmlDir)) { + try { + const sectionMarkdown = await aggregateSectionMarkdown(htmlFile); + if (sectionMarkdown) { + const sectionFilePath = htmlFile.replace( + /index\.html$/, + 'index.section.md' + ); + fs.writeFileSync(sectionFilePath, sectionMarkdown, 'utf-8'); + sectionsGenerated++; + + if (options.verbose) { + const relativePath = path.relative( + options.publicDir, + sectionFilePath + ); + console.log(` ✓ Generated section: ${relativePath}`); + } + } + } catch (error) { + console.error( + ` ✗ Error generating section for ${htmlFile}:`, + error.message + ); + } + } + + // Periodic garbage collection hint every 100 files + if (i > 0 && i % 100 === 0 && global.gc) { + global.gc(); + } + } + + console.log('\n✅ Conversion complete!'); + console.log(` Converted: ${converted} files`); + console.log(` Sections: ${sectionsGenerated} aggregated files`); + console.log(` Skipped: ${skipped} files`); + console.log(` Total: ${totalFiles} files processed`); +} + +// Run main function +main(); diff --git a/scripts/lib/markdown-converter.cjs b/scripts/lib/markdown-converter.cjs new file mode 100644 index 0000000000..dd172084aa --- /dev/null +++ b/scripts/lib/markdown-converter.cjs @@ -0,0 +1,635 @@ +/** + * Markdown Converter Library + * + * Core conversion logic for transforming HTML to Markdown. + * This library is used by both: + * - docs-v2 build scripts (html-to-markdown.js) + * - docs-tooling Lambda@Edge function + * + * Exports reusable functions for HTML→Markdown conversion + */ + +const TurndownService = require('turndown'); +const { JSDOM } = require('jsdom'); +const path = require('path'); +const fs = require('fs'); +const yaml = require('js-yaml'); + +// Try to load Rust converter (10x faster), fall back to JavaScript +let rustConverter = null; +let USE_RUST = false; +try { + rustConverter = require('../rust-markdown-converter'); + USE_RUST = true; + console.log('✓ Rust markdown converter loaded'); +} catch (err) { + console.log('ℹ Using JavaScript converter (Rust not available)'); + rustConverter = null; +} + +// Built-in product mappings (fallback since ESM module can't be required from CommonJS) +const URL_PATTERN_MAP = { + '/influxdb3/core/': 'influxdb3_core', + '/influxdb3/enterprise/': 'influxdb3_enterprise', + '/influxdb3/cloud-dedicated/': 'influxdb3_cloud_dedicated', + '/influxdb3/cloud-serverless/': 'influxdb3_cloud_serverless', + '/influxdb3/clustered/': 'influxdb3_clustered', + '/influxdb3/explorer/': 'influxdb3_explorer', + '/influxdb/cloud/': 'influxdb_cloud', + '/influxdb/v2': 'influxdb_v2', + '/influxdb/v1': 'influxdb_v1', + '/enterprise_influxdb/': 'enterprise_influxdb', + '/telegraf/': 'telegraf', + '/chronograf/': 'chronograf', + '/kapacitor/': 'kapacitor', + '/flux/': 'flux', +}; + +const PRODUCT_NAME_MAP = { + influxdb3_core: { name: 'InfluxDB 3 Core', version: 'core' }, + influxdb3_enterprise: { name: 'InfluxDB 3 Enterprise', version: 'enterprise' }, + influxdb3_cloud_dedicated: { name: 'InfluxDB Cloud Dedicated', version: 'cloud-dedicated' }, + influxdb3_cloud_serverless: { name: 'InfluxDB Cloud Serverless', version: 'cloud-serverless' }, + influxdb3_clustered: { name: 'InfluxDB Clustered', version: 'clustered' }, + influxdb3_explorer: { name: 'InfluxDB 3 Explorer', version: 'explorer' }, + influxdb_cloud: { name: 'InfluxDB Cloud (TSM)', version: 'cloud' }, + influxdb_v2: { name: 'InfluxDB OSS v2', version: 'v2' }, + influxdb_v1: { name: 'InfluxDB OSS v1', version: 'v1' }, + enterprise_influxdb: { name: 'InfluxDB Enterprise v1', version: 'v1' }, + telegraf: { name: 'Telegraf', version: 'v1' }, + chronograf: { name: 'Chronograf', version: 'v1' }, + kapacitor: { name: 'Kapacitor', version: 'v1' }, + flux: { name: 'Flux', version: 'v0' }, +}; + +// Note: ESM product-mappings module can't be required from CommonJS +// Using built-in mappings above instead +let productMappings = null; + +// Debug mode - set to true to enable verbose logging +const DEBUG = false; + +// Product data cache +let productsData = null; + +/** + * Detect base URL for the current environment + * @returns {string} Base URL (http://localhost:1313, staging URL, or production URL) + */ +function detectBaseUrl() { + // Check environment variables first + if (process.env.BASE_URL) { + return process.env.BASE_URL; + } + + // Check if Hugo dev server is running on localhost + if (process.env.HUGO_ENV === 'development' || process.env.NODE_ENV === 'development') { + return 'http://localhost:1313'; + } + + // Check for staging environment + if (process.env.HUGO_ENV === 'staging' || process.env.DEPLOY_ENV === 'staging') { + return process.env.STAGING_URL || 'https://test2.docs.influxdata.com'; + } + + // Default to production + return 'https://docs.influxdata.com'; +} + +/** + * Initialize product data + * Uses the product-mappings module (compiled from TypeScript) + */ +async function ensureProductDataInitialized() { + if (productsData) { + return; + } + + if (productMappings && productMappings.initializeProductData) { + try { + await productMappings.initializeProductData(); + productsData = true; // Mark as initialized + } catch (err) { + console.warn('Failed to initialize product-mappings:', err.message); + productsData = true; // Mark as initialized anyway to avoid retries + } + } else { + productsData = true; // Mark as initialized (fallback mode) + } +} + +/** + * Get product info from URL path + * Uses built-in URL pattern maps for detection + */ +function getProductFromPath(urlPath) { + // Find matching product key from URL patterns + for (const [pattern, productKey] of Object.entries(URL_PATTERN_MAP)) { + if (urlPath.includes(pattern)) { + const productInfo = PRODUCT_NAME_MAP[productKey]; + if (productInfo) { + return productInfo; + } + } + } + return null; +} + +/** + * Detect product context from URL path + */ +function detectProduct(urlPath) { + return getProductFromPath(urlPath); +} + +/** + * Configure Turndown for InfluxData documentation + */ +function createTurndownService() { + const turndownService = new TurndownService({ + headingStyle: 'atx', + codeBlockStyle: 'fenced', + fence: '```', + emDelimiter: '*', + strongDelimiter: '**', + // Note: linkStyle: 'inline' breaks link conversion in Turndown 7.2.2 + // Using default 'referenced' style which works correctly + bulletListMarker: '-', + }); + + // Preserve code block language identifiers + turndownService.addRule('fencedCodeBlock', { + filter: function (node, options) { + return ( + options.codeBlockStyle === 'fenced' && + node.nodeName === 'PRE' && + node.firstChild && + node.firstChild.nodeName === 'CODE' + ); + }, + replacement: function (content, node, options) { + const code = node.firstChild; + const language = code.className.replace(/^language-/, '') || ''; + const fence = options.fence; + return `\n\n${fence}${language}\n${code.textContent}\n${fence}\n\n`; + }, + }); + + // Improve list item handling - ensure proper spacing + turndownService.addRule('listItems', { + filter: 'li', + replacement: function (content, node, options) { + content = content + .replace(/^\n+/, '') // Remove leading newlines + .replace(/\n+$/, '\n') // Single trailing newline + .replace(/\n/gm, '\n '); // Indent nested content + + let prefix = options.bulletListMarker + ' '; // Dash + 3 spaces for unordered lists + const parent = node.parentNode; + + if (parent.nodeName === 'OL') { + const start = parent.getAttribute('start'); + const index = Array.prototype.indexOf.call(parent.children, node); + prefix = (start ? Number(start) + index : index + 1) + '. '; + } + + return ( + prefix + + content + + (node.nextSibling && !/\n$/.test(content) ? '\n' : '') + ); + }, + }); + + // Convert HTML tables to Markdown tables + turndownService.addRule('tables', { + filter: 'table', + replacement: function (content, node) { + // Get all rows from tbody and thead + const theadRows = Array.from(node.querySelectorAll('thead tr')); + const tbodyRows = Array.from(node.querySelectorAll('tbody tr')); + + // If no thead/tbody, fall back to all tr elements + const allRows = + theadRows.length || tbodyRows.length + ? [...theadRows, ...tbodyRows] + : Array.from(node.querySelectorAll('tr')); + + if (allRows.length === 0) return ''; + + // Extract headers from first row + const headerRow = allRows[0]; + const headers = Array.from(headerRow.querySelectorAll('th, td')).map( + (cell) => cell.textContent.trim() + ); + + // Build separator row + const separator = headers.map(() => '---').join(' | '); + + // Extract data rows (skip first row which is the header) + const dataRows = allRows + .slice(1) + .map((row) => { + const cells = Array.from(row.querySelectorAll('td, th')).map((cell) => + cell.textContent.trim().replace(/\n/g, ' ') + ); + return '| ' + cells.join(' | ') + ' |'; + }) + .join('\n'); + + return ( + '\n| ' + + headers.join(' | ') + + ' |\n| ' + + separator + + ' |\n' + + dataRows + + '\n\n' + ); + }, + }); + + // Handle GitHub-style callouts (notes, warnings, etc.) + turndownService.addRule('githubCallouts', { + filter: function (node) { + return ( + node.nodeName === 'BLOCKQUOTE' && + node.classList && + (node.classList.contains('note') || + node.classList.contains('warning') || + node.classList.contains('important') || + node.classList.contains('tip') || + node.classList.contains('caution')) + ); + }, + replacement: function (content, node) { + const type = Array.from(node.classList).find((c) => + ['note', 'warning', 'important', 'tip', 'caution'].includes(c) + ); + const emoji = + { + note: 'Note', + warning: 'Warning', + caution: 'Caution', + important: 'Important', + tip: 'Tip', + }[type] || 'Note'; + + return `\n> [!${emoji}]\n> ${content.trim().replace(/\n/g, '\n> ')}\n\n`; + }, + }); + + // Remove navigation, footer, and other non-content elements + turndownService.remove([ + 'nav', + 'header', + 'footer', + 'script', + 'style', + 'noscript', + 'iframe', + '.format-selector', // Remove format selector buttons (Copy page, etc.) + '.page-feedback', // Remove page feedback form + '#page-feedback', // Remove feedback modal + ]); + + return turndownService; +} + +/** + * Extract article content from HTML + * @param {string} htmlContent - Raw HTML content + * @param {string} contextInfo - Context info for error messages (file path or URL) + * @returns {Object|null} Object with title, description, content or null if not found + */ +function extractArticleContent(htmlContent, contextInfo = '') { + const dom = new JSDOM(htmlContent); + const document = dom.window.document; + + try { + // Find the main article content + const article = document.querySelector('article.article--content'); + + // Debug logging + if (DEBUG) { + console.log(`[DEBUG] Looking for article in ${contextInfo}`); + console.log(`[DEBUG] HTML length: ${htmlContent.length}`); + console.log(`[DEBUG] Article found: ${!!article}`); + } + + if (!article) { + // Try alternative selectors to debug + if (DEBUG) { + const anyArticle = document.querySelector('article'); + const articleContent = document.querySelector('.article--content'); + console.log(`[DEBUG] Any article element: ${!!anyArticle}`); + console.log(`[DEBUG] .article--content element: ${!!articleContent}`); + } + + console.warn( + ` ⚠️ No article content found in ${contextInfo}. This is typically not a problem and represents an aliased path.` + ); + return null; + } + + // Remove unwanted elements from article before conversion + const elementsToRemove = [ + '.format-selector', // Remove format selector buttons + '.page-feedback', // Remove page feedback form + '#page-feedback', // Remove feedback modal + '.feedback-widget', // Remove any feedback widgets + '.helpful', // Remove "Was this page helpful?" section + '.feedback.block', // Remove footer feedback/support section + 'hr', // Remove horizontal rules (often used as separators before footer) + ]; + + elementsToRemove.forEach((selector) => { + const elements = article.querySelectorAll(selector); + elements.forEach((el) => el.remove()); + }); + + // Extract metadata + const title = + document.querySelector('h1')?.textContent?.trim() || + document.querySelector('title')?.textContent?.trim() || + 'Untitled'; + + const description = + document + .querySelector('meta[name="description"]') + ?.getAttribute('content') || + document + .querySelector('meta[property="og:description"]') + ?.getAttribute('content') || + ''; + + // Get the content before closing the DOM + const content = article.innerHTML; + + return { + title, + description, + content, + }; + } finally { + // Clean up JSDOM to prevent memory leaks + dom.window.close(); + } +} + +/** + * Generate frontmatter for markdown file (single page) + * @param {Object} metadata - Object with title, description + * @param {string} urlPath - URL path for the page + * @param {string} baseUrl - Base URL for full URL construction + * @returns {string} YAML frontmatter as string + */ +function generateFrontmatter(metadata, urlPath, baseUrl = '') { + const product = detectProduct(urlPath); + + // Sanitize description (remove newlines, truncate to reasonable length) + let description = metadata.description || ''; + description = description + .replace(/\s+/g, ' ') // Replace all whitespace (including newlines) with single space + .trim() + .substring(0, 500); // Truncate to 500 characters max + + // Add token estimate (rough: 4 chars per token) + const contentLength = metadata.content?.length || 0; + const estimatedTokens = Math.ceil(contentLength / 4); + + // Build full URL (baseUrl + path) + const fullUrl = baseUrl ? `${baseUrl.replace(/\/$/, '')}${urlPath}` : urlPath; + + // Build frontmatter object (will be serialized to YAML) + const frontmatterObj = { + title: metadata.title, + description: description, + url: fullUrl, + estimated_tokens: estimatedTokens + }; + + if (product) { + frontmatterObj.product = product.name; + if (product.version) { + frontmatterObj.version = product.version; + } + } + + // Serialize to YAML (handles special characters properly) + return '---\n' + yaml.dump(frontmatterObj, { + lineWidth: -1, // Disable line wrapping + noRefs: true // Disable anchors/aliases + }).trim() + '\n---'; +} + +/** + * Generate enhanced frontmatter for section aggregation + * @param {Object} metadata - Object with title, description + * @param {string} urlPath - URL path for the section + * @param {Array} childPages - Array of child page objects with url and title + * @param {string} baseUrl - Base URL for full URL construction + * @returns {string} YAML frontmatter as string + */ +function generateSectionFrontmatter(metadata, urlPath, childPages, baseUrl = '') { + const product = detectProduct(urlPath); + + // Sanitize description (remove newlines, truncate to reasonable length) + let description = metadata.description || ''; + description = description + .replace(/\s+/g, ' ') // Replace all whitespace (including newlines) with single space + .trim() + .substring(0, 500); // Truncate to 500 characters max + + // Add token estimate (rough: 4 chars per token) + const contentLength = metadata.content?.length || 0; + const childContentLength = childPages.reduce( + (sum, child) => sum + (child.content?.length || 0), + 0 + ); + const totalLength = contentLength + childContentLength; + const estimatedTokens = Math.ceil(totalLength / 4); + + // Build full URL (baseUrl + path) + const fullUrl = baseUrl ? `${baseUrl.replace(/\/$/, '')}${urlPath}` : urlPath; + const normalizedBaseUrl = baseUrl ? baseUrl.replace(/\/$/, '') : ''; + + // Build frontmatter object (will be serialized to YAML) + const frontmatterObj = { + title: metadata.title, + description: description, + url: fullUrl, + type: 'section', + pages: childPages.length, + estimated_tokens: estimatedTokens + }; + + if (product) { + frontmatterObj.product = product.name; + if (product.version) { + frontmatterObj.version = product.version; + } + } + + // List child pages with full URLs + if (childPages.length > 0) { + frontmatterObj.child_pages = childPages.map(child => ({ + url: normalizedBaseUrl ? `${normalizedBaseUrl}${child.url}` : child.url, + title: child.title + })); + } + + // Serialize to YAML (handles special characters properly) + return '---\n' + yaml.dump(frontmatterObj, { + lineWidth: -1, // Disable line wrapping + noRefs: true // Disable anchors/aliases + }).trim() + '\n---'; +} + +/** + * Convert HTML content to Markdown (single page) + * @param {string} htmlContent - Raw HTML content + * @param {string} urlPath - URL path for the page (for frontmatter) + * @returns {Promise} Markdown content with frontmatter or null if conversion fails + */ +async function convertToMarkdown(htmlContent, urlPath) { + await ensureProductDataInitialized(); + + // Detect base URL for the environment + const baseUrl = detectBaseUrl(); + if (DEBUG) { + console.log(`[DEBUG] Base URL detected: ${baseUrl} (NODE_ENV=${process.env.NODE_ENV}, HUGO_ENV=${process.env.HUGO_ENV}, BASE_URL=${process.env.BASE_URL})`); + } + + // Use Rust converter if available (10× faster) + if (USE_RUST && rustConverter) { + try { + return rustConverter.convertToMarkdown(htmlContent, urlPath, baseUrl); + } catch (err) { + console.warn(`Rust conversion failed for ${urlPath}, falling back to JavaScript:`, err.message); + // Fall through to JavaScript implementation + } + } + + // JavaScript fallback implementation + const turndownService = createTurndownService(); + const metadata = extractArticleContent(htmlContent, urlPath); + + if (!metadata) { + return null; + } + + // Convert HTML to markdown + let markdown = turndownService.turndown(metadata.content); + + // Clean up excessive newlines and separator artifacts + markdown = markdown + .replace(/\n{3,}/g, '\n\n') + .replace(/\* \* \*\s*\n\s*\* \* \*/g, '') + .replace(/\* \* \*\s*$/g, '') + .trim(); + + // Generate frontmatter with full URL + const frontmatter = generateFrontmatter(metadata, urlPath, baseUrl); + + return `${frontmatter}\n\n${markdown}\n`; +} + +/** + * Convert section HTML with child pages to aggregated Markdown + * @param {string} sectionHtml - HTML content of the section index page + * @param {string} sectionUrlPath - URL path for the section + * @param {Array} childHtmls - Array of objects with {html, url} for each child page + * @returns {Promise} Aggregated markdown content or null if conversion fails + */ +async function convertSectionToMarkdown( + sectionHtml, + sectionUrlPath, + childHtmls +) { + await ensureProductDataInitialized(); + + // Detect base URL for the environment + const baseUrl = detectBaseUrl(); + + // Use Rust converter if available (10× faster) + if (USE_RUST && rustConverter) { + try { + return rustConverter.convertSectionToMarkdown(sectionHtml, sectionUrlPath, childHtmls, baseUrl); + } catch (err) { + console.warn(`Rust section conversion failed for ${sectionUrlPath}, falling back to JavaScript:`, err.message); + // Fall through to JavaScript implementation + } + } + + // JavaScript fallback implementation + const turndownService = createTurndownService(); + + // Extract section metadata and content + const sectionMetadata = extractArticleContent(sectionHtml, sectionUrlPath); + if (!sectionMetadata) { + return null; + } + + // Convert section content to markdown + let sectionMarkdown = turndownService.turndown(sectionMetadata.content); + sectionMarkdown = sectionMarkdown + .replace(/\n{3,}/g, '\n\n') + .replace(/\* \* \*\s*\n\s*\* \* \*/g, '') + .replace(/\* \* \*\s*$/g, '') + .trim(); + + // Process child pages + const childContents = []; + const childPageInfo = []; + + for (const { html, url } of childHtmls) { + const childMetadata = extractArticleContent(html, url); + if (childMetadata) { + let childMarkdown = turndownService.turndown(childMetadata.content); + childMarkdown = childMarkdown + .replace(/\n{3,}/g, '\n\n') + .replace(/\* \* \*\s*\n\s*\* \* \*/g, '') + .replace(/\* \* \*\s*$/g, '') + .trim(); + + // Remove the first h1 heading (page title) to avoid redundancy + // since we're adding it as an h2 heading + childMarkdown = childMarkdown.replace(/^#\s+.+?\n+/, ''); + + // Add child page title as heading + childContents.push(`## ${childMetadata.title}\n\n${childMarkdown}`); + + // Track child page info for frontmatter + childPageInfo.push({ + url: url, + title: childMetadata.title, + content: childMarkdown, + }); + } + } + + // Generate section frontmatter with child page info and full URLs + const frontmatter = generateSectionFrontmatter( + { ...sectionMetadata, content: sectionMarkdown }, + sectionUrlPath, + childPageInfo, + baseUrl + ); + + // Combine section content with child pages + const allContent = [sectionMarkdown, ...childContents].join('\n\n---\n\n'); + + return `${frontmatter}\n\n${allContent}\n`; +} + +// Export all functions for CommonJS +module.exports = { + detectProduct, + createTurndownService, + extractArticleContent, + generateFrontmatter, + generateSectionFrontmatter, + convertToMarkdown, + convertSectionToMarkdown, +}; diff --git a/scripts/rust-markdown-converter/.gitignore b/scripts/rust-markdown-converter/.gitignore new file mode 100644 index 0000000000..b0320187e3 --- /dev/null +++ b/scripts/rust-markdown-converter/.gitignore @@ -0,0 +1,7 @@ +target/ +node_modules/ +*.node +.cargo/ +Cargo.lock +*.d.ts +index.js diff --git a/scripts/rust-markdown-converter/Cargo.toml b/scripts/rust-markdown-converter/Cargo.toml new file mode 100644 index 0000000000..79948205aa --- /dev/null +++ b/scripts/rust-markdown-converter/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "rust-markdown-converter" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +# NAPI for Node.js bindings +napi = { version = "2.16", features = ["serde-json"] } +napi-derive = "2.16" + +# HTML parsing and conversion +html2md = "0.2" +scraper = "0.20" + +# YAML frontmatter +serde_yaml = "0.9" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +# Regex for text processing +regex = "1.11" +lazy_static = "1.5" + +# Date/time for timestamps +chrono = "0.4" + +[build-dependencies] +napi-build = "2.1" +serde_yaml = "0.9" + +[profile.release] +lto = true +codegen-units = 1 +opt-level = 3 +strip = true diff --git a/scripts/rust-markdown-converter/build.rs b/scripts/rust-markdown-converter/build.rs new file mode 100644 index 0000000000..4cd2e9f44e --- /dev/null +++ b/scripts/rust-markdown-converter/build.rs @@ -0,0 +1,119 @@ +extern crate napi_build; + +use std::env; +use std::fs; +use std::path::Path; + +fn main() { + napi_build::setup(); + + // Generate product mappings from products.yml + generate_product_mappings(); +} + +fn generate_product_mappings() { + // Tell Cargo to rerun this build script if products.yml changes + println!("cargo:rerun-if-changed=../../data/products.yml"); + + let out_dir = env::var("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("product_mappings.rs"); + + // Read products.yml + let products_path = "../../data/products.yml"; + let yaml_content = fs::read_to_string(products_path) + .expect("Failed to read products.yml"); + + // Parse YAML using serde_yaml + let products: serde_yaml::Value = serde_yaml::from_str(&yaml_content) + .expect("Failed to parse products.yml"); + + // Generate Rust code for the URL pattern map + let mut mappings = Vec::new(); + + if let serde_yaml::Value::Mapping(products_map) = products { + for (key, value) in products_map.iter() { + if let (serde_yaml::Value::String(_product_key), serde_yaml::Value::Mapping(product_data)) = (key, value) { + // Extract name + let name = product_data.get(&serde_yaml::Value::String("name".to_string())) + .and_then(|v| v.as_str()) + .unwrap_or(""); + + // Extract namespace + let namespace = product_data.get(&serde_yaml::Value::String("namespace".to_string())) + .and_then(|v| v.as_str()) + .unwrap_or(""); + + // Extract versions array (if exists) + let versions = product_data.get(&serde_yaml::Value::String("versions".to_string())) + .and_then(|v| v.as_sequence()) + .map(|seq| { + seq.iter() + .filter_map(|v| v.as_str()) + .collect::>() + }) + .unwrap_or_default(); + + // Build URL patterns from namespace and versions data + // Convert namespace like "influxdb3_explorer" to URL path "/influxdb3/explorer/" + let url_base = if namespace.contains('_') { + let parts: Vec<&str> = namespace.split('_').collect(); + format!("/{}/", parts.join("/")) + } else { + format!("/{}/", namespace) + }; + + if !versions.is_empty() { + // For products with versions, create a mapping for each version + for version in &versions { + // Build URL: base + version (without trailing slash for now, added later if needed) + let url_pattern = url_base.trim_end_matches('/').to_string() + "/" + version; + + // Try to get version-specific name (e.g., name__v2, name__cloud) + let version_key = format!("name__{}", version.replace("-", "")); + let version_name = product_data.get(&serde_yaml::Value::String(version_key)) + .and_then(|v| v.as_str()) + .unwrap_or(name); + + mappings.push(format!( + " m.insert(\"{}\", (\"{}\", \"{}\"));", + url_pattern, version_name, version + )); + } + } else if !namespace.is_empty() { + // For products without versions, use namespace directly + // Extract the version identifier from the latest field + let latest = product_data.get(&serde_yaml::Value::String("latest".to_string())) + .and_then(|v| v.as_str()) + .unwrap_or(""); + + // Use the base path without trailing slash for pattern matching + let url_pattern = url_base.trim_end_matches('/').to_string(); + + mappings.push(format!( + " m.insert(\"{}/\", (\"{}\", \"{}\"));", + url_pattern, name, latest + )); + } + } + } + } + + // Generate the Rust code + let generated_code = format!( + r#"// Auto-generated from data/products.yml - DO NOT EDIT MANUALLY +// Note: HashMap and lazy_static are already imported in lib.rs + +lazy_static! {{ + pub static ref URL_PATTERN_MAP: HashMap<&'static str, (&'static str, &'static str)> = {{ + let mut m = HashMap::new(); +{} + m + }}; +}} +"#, + mappings.join("\n") + ); + + fs::write(&dest_path, generated_code) + .expect("Failed to write product_mappings.rs"); +} diff --git a/scripts/rust-markdown-converter/package.json b/scripts/rust-markdown-converter/package.json new file mode 100644 index 0000000000..afb0698e1f --- /dev/null +++ b/scripts/rust-markdown-converter/package.json @@ -0,0 +1,31 @@ +{ + "name": "@influxdata/rust-markdown-converter", + "version": "0.1.0", + "description": "High-performance HTML to Markdown converter for InfluxData documentation (Rust + napi-rs)", + "main": "index.js", + "types": "index.d.ts", + "napi": { + "binaryName": "rust-markdown-converter", + "targets": [ + "aarch64-apple-darwin", + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu" + ] + }, + "scripts": { + "artifacts": "napi artifacts", + "build": "napi build --platform --release", + "build:debug": "napi build --platform", + "prepublishOnly": "napi prepublish -t npm", + "test": "cargo test", + "universal": "napi universal", + "version": "napi version" + }, + "devDependencies": { + "@napi-rs/cli": "^3.4.1" + }, + "engines": { + "node": ">= 10" + }, + "license": "MIT" +} diff --git a/scripts/rust-markdown-converter/src/lib.rs b/scripts/rust-markdown-converter/src/lib.rs new file mode 100644 index 0000000000..4899b6f539 --- /dev/null +++ b/scripts/rust-markdown-converter/src/lib.rs @@ -0,0 +1,696 @@ +/*! + * Rust Markdown Converter Library + * + * High-performance HTML to Markdown converter for InfluxData documentation. + * This library provides Node.js bindings via napi-rs for seamless integration. + * + * Features: + * - Custom Turndown-like conversion rules + * - Product detection from URL paths + * - GitHub-style callout support + * - UI element removal + * - YAML frontmatter generation + */ + +#[macro_use] +extern crate napi_derive; + +use napi::Result; +use scraper::{Html, Selector}; +use serde::{Deserialize, Serialize}; +use regex::Regex; +use lazy_static::lazy_static; +use std::collections::HashMap; + +// ============================================================================ +// Product Detection +// ============================================================================ + +#[napi(object)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProductInfo { + pub name: String, + pub version: String, +} + +// Include auto-generated product mappings from data/products.yml +// This is generated at compile time by build.rs +include!(concat!(env!("OUT_DIR"), "/product_mappings.rs")); + +fn detect_product(url_path: &str) -> Option { + for (pattern, (name, version)) in URL_PATTERN_MAP.iter() { + if url_path.contains(pattern) { + return Some(ProductInfo { + name: name.to_string(), + version: version.to_string(), + }); + } + } + None +} + +// ============================================================================ +// HTML Processing +// ============================================================================ + +/// Remove unwanted UI elements from HTML +fn clean_html(html: &str) -> String { + let document = Html::parse_document(html); + let mut cleaned = html.to_string(); + + // Configurable list of CSS selectors for elements to remove from article content + // Add new selectors here to remove unwanted UI elements, forms, navigation, etc. + let remove_selectors = vec![ + // Navigation and structure + "nav", + "header", + "footer", + + // Scripts and styles + "script", + "style", + "noscript", + "iframe", + + // UI widgets and controls + ".format-selector", + ".format-selector__button", + "button[aria-label*='Copy']", + "hr", + + // Feedback and support sections (inside article content) + ".helpful", // "Was this page helpful?" form + "div.feedback.block", // Block-level feedback sections (combined class selector) + ".feedback", // General feedback sections (must come after specific .feedback.block) + ".page-feedback", + "#page-feedback", + ".feedback-widget", + ".support", // Support section at bottom of pages + ]; + + for selector_str in remove_selectors { + if let Ok(selector) = Selector::parse(selector_str) { + for element in document.select(&selector) { + // Get the full HTML of the element to remove + let element_html = element.html(); + cleaned = cleaned.replace(&element_html, ""); + } + } + } + + cleaned +} + +/// Replace icon spans with text checkmarks +/// Converts to ✓ +fn replace_icon_spans(html: &str) -> String { + let document = Html::parse_document(html); + let mut result = html.to_string(); + + // Select icon spans (specifically checkmark icons used in tables) + // The selector matches any span that has both cf-icon and Checkmark_New classes + if let Ok(selector) = Selector::parse("span[class*='cf-icon'][class*='Checkmark_New']") { + for element in document.select(&selector) { + // Build the full element HTML to replace (empty span with classes) + let class_attr = element.value().attr("class").unwrap_or(""); + let full_html = format!("", class_attr); + + // Replace with checkmark character + result = result.replace(&full_html, "✓"); + } + } + + result +} + +/// Extract article content from HTML +fn extract_article_content(html: &str) -> Option<(String, String, String)> { + let document = Html::parse_document(html); + + // Find main article content + let article_selector = Selector::parse("article.article--content").ok()?; + let article = document.select(&article_selector).next()?; + + // Extract title + let title = if let Ok(h1_sel) = Selector::parse("h1") { + document + .select(&h1_sel) + .next() + .map(|el| el.text().collect::>().join(" ")) + .or_else(|| { + if let Ok(title_sel) = Selector::parse("title") { + document + .select(&title_sel) + .next() + .map(|el| el.text().collect::>().join(" ")) + } else { + None + } + }) + .unwrap_or_else(|| "Untitled".to_string()) + } else { + "Untitled".to_string() + }; + + // Extract description from meta tags + let description = if let Ok(meta_sel) = Selector::parse("meta[name='description']") { + document + .select(&meta_sel) + .next() + .and_then(|el| el.value().attr("content")) + .or_else(|| { + if let Ok(og_sel) = Selector::parse("meta[property='og:description']") { + document + .select(&og_sel) + .next() + .and_then(|el| el.value().attr("content")) + } else { + None + } + }) + .unwrap_or("") + .to_string() + } else { + String::new() + }; + + // Get cleaned article HTML + let content = clean_html(&article.html()); + + Some((title, description, content)) +} + +// ============================================================================ +// Markdown Conversion +// ============================================================================ + +lazy_static! { + // Regex patterns for post-processing + static ref EXCESSIVE_NEWLINES: Regex = Regex::new(r"\n{3,}").unwrap(); + static ref SEPARATOR_ARTIFACTS: Regex = Regex::new(r"\* \* \*\s*\n\s*\* \* \*").unwrap(); + static ref TRAILING_SEPARATOR: Regex = Regex::new(r"\* \* \*\s*$").unwrap(); + static ref CODE_FENCE: Regex = Regex::new(r"```(\w+)?\n").unwrap(); +} + +/// Convert HTML blockquote callouts to GitHub-style +fn convert_callouts(markdown: &str, html: &str) -> String { + let document = Html::parse_document(html); + let mut result = markdown.to_string(); + + // Process both
elements and
callouts + let selectors = vec![ + "blockquote.note", + "blockquote.warning", + "blockquote.important", + "blockquote.tip", + "blockquote.caution", + "div.block.note", + "div.block.warning", + "div.block.important", + "div.block.tip", + "div.block.caution", + ]; + + for selector_str in selectors { + if let Ok(callout_sel) = Selector::parse(selector_str) { + // Determine callout type from selector + let callout_type = if selector_str.ends_with("note") { + "note" + } else if selector_str.ends_with("warning") { + "warning" + } else if selector_str.ends_with("caution") { + "caution" + } else if selector_str.ends_with("important") { + "important" + } else if selector_str.ends_with("tip") { + "tip" + } else { + "note" + }; + + for element in document.select(&callout_sel) { + let label = match callout_type { + "note" => "Note", + "warning" => "Warning", + "caution" => "Caution", + "important" => "Important", + "tip" => "Tip", + _ => "Note", + }; + + // Convert the callout content to markdown preserving structure + let callout_html = element.html(); + let callout_markdown = html2md::parse_html(&callout_html); + + if !callout_markdown.trim().is_empty() && callout_markdown.len() > 10 { + // Build GitHub-style callout + let mut callout_lines = vec![format!("> [!{}]", label)]; + + // Process markdown line by line, preserving headings and structure + for line in callout_markdown.lines() { + let trimmed = line.trim(); + if !trimmed.is_empty() { + // Preserve markdown headings (#### becomes > ####) + callout_lines.push(format!("> {}", trimmed)); + } + } + + // Check for modal trigger links and add annotations + if let Ok(modal_sel) = Selector::parse("a.influxdb-detector-trigger, a[onclick*='toggleModal']") { + if element.select(&modal_sel).next().is_some() { + // Add annotation about interactive modal + callout_lines.push("> *(Interactive feature in HTML: Opens version detector modal)*".to_string()); + } + } + + let callout = callout_lines.join("\n") + "\n"; + + // Try to find and replace in markdown + // Extract the first line of content (likely a heading or distinctive text) + let first_content_line = callout_markdown.lines() + .map(|l| l.trim()) + .find(|l| !l.is_empty() && l.len() > 3) + .unwrap_or(""); + + if !first_content_line.is_empty() { + // Try to find this content in the markdown + if let Some(idx) = result.find(first_content_line) { + // Find the end of this section (next heading or double newline) + let after_start = &result[idx..]; + if let Some(section_end) = after_start.find("\n\n") { + let end_idx = idx + section_end; + result.replace_range(idx..end_idx, &callout); + } + } + } + } + } + } + } + + result +} + +/// Convert HTML tables to Markdown format +fn convert_tables(markdown: &str, html: &str) -> String { + let document = Html::parse_document(html); + let mut result = markdown.to_string(); + + if let Ok(table_sel) = Selector::parse("table") { + for table in document.select(&table_sel) { + // Get headers + let mut headers = Vec::new(); + if let Ok(th_sel) = Selector::parse("thead th, thead td") { + for th in table.select(&th_sel) { + headers.push(th.text().collect::>().join(" ").trim().to_string()); + } + } + + // If no thead, try first tr + if headers.is_empty() { + if let Ok(tr_sel) = Selector::parse("tr") { + if let Some(first_row) = table.select(&tr_sel).next() { + if let Ok(cell_sel) = Selector::parse("th, td") { + for cell in first_row.select(&cell_sel) { + headers.push(cell.text().collect::>().join(" ").trim().to_string()); + } + } + } + } + } + + if headers.is_empty() { + continue; + } + + // Build separator + let separator = headers.iter().map(|_| "---").collect::>().join(" | "); + + // Get data rows + let mut data_rows = Vec::new(); + if let Ok(tr_sel) = Selector::parse("tbody tr, tr") { + for (idx, row) in table.select(&tr_sel).enumerate() { + // Skip first row if it was used for headers + if idx == 0 && !table.select(&Selector::parse("thead").unwrap()).next().is_some() { + continue; + } + + let mut cells = Vec::new(); + if let Ok(cell_sel) = Selector::parse("td, th") { + for cell in row.select(&cell_sel) { + cells.push(cell.text().collect::>().join(" ").trim().replace('\n', " ")); + } + } + + if !cells.is_empty() { + data_rows.push(format!("| {} |", cells.join(" | "))); + } + } + } + + // Build markdown table + let md_table = format!( + "\n| {} |\n| {} |\n{}\n\n", + headers.join(" | "), + separator, + data_rows.join("\n") + ); + + // This is approximate replacement + result.push_str(&md_table); + } + } + + result +} + +/// Add headings to delimit tabbed content in markdown +/// Finds patterns like [Go](#)[Node.js](#)[Python](#) and replaces with heading +fn add_tab_delimiters_to_markdown(markdown: &str) -> String { + use regex::Regex; + + // Pattern to match 2+ consecutive tab links + let tabs_pattern = Regex::new(r"(\[[^\]]+\]\(#\)){2,}").unwrap(); + let first_tab_re = Regex::new(r"\[([^\]]+)\]\(#\)").unwrap(); + + tabs_pattern.replace_all(markdown, |caps: ®ex::Captures| { + let full_match = &caps[0]; + + // Extract first tab name + if let Some(first_cap) = first_tab_re.captures(full_match) { + format!("#### {} ####", &first_cap[1]) + } else { + full_match.to_string() + } + }).to_string() +} + +/// Post-process markdown to clean up formatting +fn postprocess_markdown(markdown: &str, html: &str, remove_h1: bool) -> String { + let mut result = markdown.to_string(); + + if remove_h1 { + // Remove the first h1 heading (title is already in frontmatter) + // Match both formats: + // 1. ATX style: # Title\n + // 2. Setext style: Title\n=====\n + let h1_atx_pattern = Regex::new(r"^#\s+.*?\n+").unwrap(); + let h1_setext_pattern = Regex::new(r"^.+?\n=+\s*\n+").unwrap(); + + // Try ATX style first + if h1_atx_pattern.is_match(&result) { + result = h1_atx_pattern.replace(&result, "").to_string(); + } else { + // Try Setext style + result = h1_setext_pattern.replace(&result, "").to_string(); + } + } + + // Convert callouts + result = convert_callouts(&result, html); + + // Convert tables (html2md might not handle them well) + result = convert_tables(&result, html); + + // Add tab delimiters for tabbed content + result = add_tab_delimiters_to_markdown(&result); + + // Remove UI element text that shouldn't be in markdown + result = result.replace("Copy section", ""); + result = result.replace("Copy page", ""); + result = result.replace(" Copy to clipboard", ""); + + // Remove HTML comments (, , etc.) + let comment_pattern = Regex::new(r"").unwrap(); + result = comment_pattern.replace_all(&result, "").to_string(); + + // Remove feedback and support sections at the bottom + // Match "Was this page helpful?" to end of document + let feedback_section = Regex::new(r"(?s)Was this page helpful\?.*$").unwrap(); + result = feedback_section.replace(&result, "").to_string(); + + // Also remove "Support and feedback" heading if it somehow remains + let support_section = Regex::new(r"(?s)#{2,6}\s+Support and feedback\s*\n.*$").unwrap(); + result = support_section.replace(&result, "").to_string(); + + // Clean up excessive newlines + result = EXCESSIVE_NEWLINES.replace_all(&result, "\n\n").to_string(); + + // Remove separator artifacts + result = SEPARATOR_ARTIFACTS.replace_all(&result, "").to_string(); + result = TRAILING_SEPARATOR.replace_all(&result, "").to_string(); + + result.trim().to_string() +} + +/// Fix code block language identifiers +/// html2md doesn't preserve language classes, so we need to extract them from HTML +/// and add them to the markdown code fences +fn fix_code_block_languages(markdown: &str, html: &str) -> String { + let document = Html::parse_document(html); + let mut result = markdown.to_string(); + + // Find all code blocks with language classes or data-lang attributes + if let Ok(code_selector) = Selector::parse("code[class*='language-'], code[data-lang]") { + for code_element in document.select(&code_selector) { + let mut lang: Option = None; + + // Try to extract language from class (e.g., "language-bash" -> "bash") + if let Some(class_attr) = code_element.value().attr("class") { + for class in class_attr.split_whitespace() { + if class.starts_with("language-") { + lang = Some(class[9..].to_string()); // Skip "language-" prefix + break; + } + } + } + + // Fallback to data-lang attribute if class didn't have language + if lang.is_none() { + if let Some(data_lang) = code_element.value().attr("data-lang") { + lang = Some(data_lang.to_string()); + } + } + + // If we found a language identifier, add it to the markdown fence + if let Some(lang_str) = lang { + // Get the code content + let code_text = code_element.text().collect::>().join(""); + let code_text = code_text.trim(); + + // Find the code block in markdown (without language identifier) + // Look for ```\n\n``` pattern + let fence_pattern = format!("```\n{}\n```", code_text); + let fence_with_lang = format!("```{}\n{}\n```", lang_str, code_text); + + // Replace first occurrence + if result.contains(&fence_pattern) { + result = result.replacen(&fence_pattern, &fence_with_lang, 1); + } + } + } + } + + result +} + +/// Convert HTML to Markdown +fn html_to_markdown(html: &str, remove_h1: bool) -> String { + // Pre-process HTML + let html = replace_icon_spans(html); + // Note: tab delimiters are added in post-processing on markdown, not HTML preprocessing + + // Use html2md for basic conversion + let markdown = html2md::parse_html(&html); + + // Apply post-processing + let markdown = postprocess_markdown(&markdown, &html, remove_h1); + + // Fix code block language identifiers + fix_code_block_languages(&markdown, &html) +} + +// ============================================================================ +// Frontmatter Generation +// ============================================================================ + +#[derive(Debug, Serialize)] +struct Frontmatter { + title: String, + description: String, + url: String, + #[serde(skip_serializing_if = "Option::is_none")] + product: Option, + #[serde(skip_serializing_if = "Option::is_none")] + product_version: Option, + date: String, + lastmod: String, + estimated_tokens: usize, +} + +fn generate_frontmatter( + title: &str, + description: &str, + url_path: &str, + content_length: usize, + base_url: &str, +) -> String { + let product = detect_product(url_path); + + // Sanitize description + let description = description + .chars() + .filter(|c| !c.is_control() || *c == '\n') + .collect::() + .split_whitespace() + .collect::>() + .join(" ") + .chars() + .take(500) + .collect::(); + + // Estimate tokens (4 chars per token) + let estimated_tokens = (content_length + 3) / 4; + + // Generate current timestamp in ISO 8601 format + let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true); + + // Convert relative URL to full URL using the provided base URL + let full_url = format!("{}{}", base_url, url_path); + + let frontmatter = Frontmatter { + title: title.to_string(), + description, + url: full_url, + product: product.as_ref().map(|p| p.name.clone()), + product_version: product.as_ref().map(|p| p.version.clone()), + date: now.clone(), + lastmod: now, + estimated_tokens, + }; + + match serde_yaml::to_string(&frontmatter) { + Ok(yaml) => format!("---\n{}---", yaml), + Err(_) => "---\n---".to_string(), + } +} + +// ============================================================================ +// Node.js API (napi-rs bindings) +// ============================================================================ + +/// Convert HTML to Markdown with frontmatter +/// +/// # Arguments +/// * `html_content` - Raw HTML content +/// * `url_path` - URL path for the page (for frontmatter generation) +/// * `base_url` - Base URL for the site (e.g., "http://localhost:1313" or "https://docs.influxdata.com") +/// +/// # Returns +/// Markdown string with YAML frontmatter, or null if conversion fails +#[napi] +pub fn convert_to_markdown(html_content: String, url_path: String, base_url: String) -> Result> { + match extract_article_content(&html_content) { + Some((title, description, content)) => { + // For single pages, remove h1 since title is in frontmatter + let markdown = html_to_markdown(&content, true); + let frontmatter = generate_frontmatter(&title, &description, &url_path, markdown.len(), &base_url); + + // Product info is already in frontmatter, no need to duplicate in content + Ok(Some(format!("{}\n\n{}\n", frontmatter, markdown))) + } + None => Ok(None), + } +} + +/// Convert section HTML with child pages to aggregated Markdown +/// +/// # Arguments +/// * `section_html` - HTML content of the section index page +/// * `section_url_path` - URL path for the section +/// * `child_htmls` - Array of child page objects with `{html, url}` structure +/// * `base_url` - Base URL for the site (e.g., "http://localhost:1313" or "https://docs.influxdata.com") +/// +/// # Returns +/// Aggregated markdown content or null if conversion fails +#[napi(object)] +pub struct ChildPageInput { + pub html: String, + pub url: String, +} + +#[napi] +pub fn convert_section_to_markdown( + section_html: String, + section_url_path: String, + child_htmls: Vec, + base_url: String, +) -> Result> { + // Extract section metadata + let (section_title, section_description, section_content) = match extract_article_content(§ion_html) { + Some(data) => data, + None => return Ok(None), + }; + + // For section pages, keep the h1 title in content + let section_markdown = html_to_markdown(§ion_content, false); + + // Process child pages + let mut child_contents = Vec::new(); + let mut total_length = section_markdown.len(); + + for child in child_htmls { + if let Some((title, _desc, content)) = extract_article_content(&child.html) { + // For child pages, remove h1 since we add them as h2 + let child_markdown = html_to_markdown(&content, true); + + // Add as h2 heading with URL + let full_child_url = format!("{}{}", base_url, child.url); + child_contents.push(format!("## {}\n\n**URL**: {}\n\n{}", title, full_child_url, child_markdown)); + total_length += child_markdown.len(); + } + } + + // Generate frontmatter + let frontmatter = generate_frontmatter( + §ion_title, + §ion_description, + §ion_url_path, + total_length, + &base_url, + ); + + // Combine all content + let mut all_content = vec![section_markdown]; + all_content.extend(child_contents); + let combined = all_content.join("\n\n---\n\n"); + + Ok(Some(format!("{}\n\n{}\n", frontmatter, combined))) +} + +/// Detect product from URL path +#[napi] +pub fn detect_product_from_path(url_path: String) -> Result> { + Ok(detect_product(&url_path)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_product_detection() { + let product = detect_product("/influxdb3/core/get-started/"); + assert!(product.is_some()); + let p = product.unwrap(); + assert_eq!(p.name, "InfluxDB 3 Core"); + assert_eq!(p.version, "core"); + } + + #[test] + fn test_html_to_markdown() { + let html = "

Hello world!

"; + let md = html_to_markdown(html, false); + assert!(md.contains("Hello **world**!")); + } +} diff --git a/scripts/rust-markdown-converter/yarn.lock b/scripts/rust-markdown-converter/yarn.lock new file mode 100644 index 0000000000..2b1bc4eca8 --- /dev/null +++ b/scripts/rust-markdown-converter/yarn.lock @@ -0,0 +1,779 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@emnapi/core@^1.5.0": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.7.1.tgz#3a79a02dbc84f45884a1806ebb98e5746bdfaac4" + integrity sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg== + dependencies: + "@emnapi/wasi-threads" "1.1.0" + tslib "^2.4.0" + +"@emnapi/runtime@^1.5.0": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.7.1.tgz#a73784e23f5d57287369c808197288b52276b791" + integrity sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA== + dependencies: + tslib "^2.4.0" + +"@emnapi/wasi-threads@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz#60b2102fddc9ccb78607e4a3cf8403ea69be41bf" + integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ== + dependencies: + tslib "^2.4.0" + +"@inquirer/ansi@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@inquirer/ansi/-/ansi-1.0.2.tgz#674a4c4d81ad460695cb2a1fc69d78cd187f337e" + integrity sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ== + +"@inquirer/checkbox@^4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@inquirer/checkbox/-/checkbox-4.3.2.tgz#e1483e6519d6ffef97281a54d2a5baa0d81b3f3b" + integrity sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA== + 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" + +"@inquirer/confirm@^5.1.21": + version "5.1.21" + resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.21.tgz#610c4acd7797d94890a6e2dde2c98eb1e891dd12" + integrity sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ== + dependencies: + "@inquirer/core" "^10.3.2" + "@inquirer/type" "^3.0.10" + +"@inquirer/core@^10.3.2": + version "10.3.2" + resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.3.2.tgz#535979ff3ff4fe1e7cc4f83e2320504c743b7e20" + integrity sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A== + 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" + +"@inquirer/editor@^4.2.23": + version "4.2.23" + resolved "https://registry.yarnpkg.com/@inquirer/editor/-/editor-4.2.23.tgz#fe046a3bfdae931262de98c1052437d794322e0b" + integrity sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ== + dependencies: + "@inquirer/core" "^10.3.2" + "@inquirer/external-editor" "^1.0.3" + "@inquirer/type" "^3.0.10" + +"@inquirer/expand@^4.0.23": + version "4.0.23" + resolved "https://registry.yarnpkg.com/@inquirer/expand/-/expand-4.0.23.tgz#a38b5f32226d75717c370bdfed792313b92bdc05" + integrity sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew== + dependencies: + "@inquirer/core" "^10.3.2" + "@inquirer/type" "^3.0.10" + yoctocolors-cjs "^2.1.3" + +"@inquirer/external-editor@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@inquirer/external-editor/-/external-editor-1.0.3.tgz#c23988291ee676290fdab3fd306e64010a6d13b8" + integrity sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA== + dependencies: + chardet "^2.1.1" + iconv-lite "^0.7.0" + +"@inquirer/figures@^1.0.15": + version "1.0.15" + resolved "https://registry.yarnpkg.com/@inquirer/figures/-/figures-1.0.15.tgz#dbb49ed80df11df74268023b496ac5d9acd22b3a" + integrity sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g== + +"@inquirer/input@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@inquirer/input/-/input-4.3.1.tgz#778683b4c4c4d95d05d4b05c4a854964b73565b4" + integrity sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g== + dependencies: + "@inquirer/core" "^10.3.2" + "@inquirer/type" "^3.0.10" + +"@inquirer/number@^3.0.23": + version "3.0.23" + resolved "https://registry.yarnpkg.com/@inquirer/number/-/number-3.0.23.tgz#3fdec2540d642093fd7526818fd8d4bdc7335094" + integrity sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg== + dependencies: + "@inquirer/core" "^10.3.2" + "@inquirer/type" "^3.0.10" + +"@inquirer/password@^4.0.23": + version "4.0.23" + resolved "https://registry.yarnpkg.com/@inquirer/password/-/password-4.0.23.tgz#b9f5187c8c92fd7aa9eceb9d8f2ead0d7e7b000d" + integrity sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA== + dependencies: + "@inquirer/ansi" "^1.0.2" + "@inquirer/core" "^10.3.2" + "@inquirer/type" "^3.0.10" + +"@inquirer/prompts@^7.8.4": + version "7.10.1" + resolved "https://registry.yarnpkg.com/@inquirer/prompts/-/prompts-7.10.1.tgz#e1436c0484cf04c22548c74e2cd239e989d5f847" + integrity sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg== + 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" + +"@inquirer/rawlist@^4.1.11": + version "4.1.11" + resolved "https://registry.yarnpkg.com/@inquirer/rawlist/-/rawlist-4.1.11.tgz#313c8c3ffccb7d41e990c606465726b4a898a033" + integrity sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw== + dependencies: + "@inquirer/core" "^10.3.2" + "@inquirer/type" "^3.0.10" + yoctocolors-cjs "^2.1.3" + +"@inquirer/search@^3.2.2": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@inquirer/search/-/search-3.2.2.tgz#4cc6fd574dcd434e4399badc37c742c3fd534ac8" + integrity sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA== + dependencies: + "@inquirer/core" "^10.3.2" + "@inquirer/figures" "^1.0.15" + "@inquirer/type" "^3.0.10" + yoctocolors-cjs "^2.1.3" + +"@inquirer/select@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@inquirer/select/-/select-4.4.2.tgz#2ac8fca960913f18f1d1b35323ed8fcd27d89323" + integrity sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w== + 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" + +"@inquirer/type@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@inquirer/type/-/type-3.0.10.tgz#11ed564ec78432a200ea2601a212d24af8150d50" + integrity sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA== + +"@napi-rs/cli@^3.4.1": + version "3.4.1" + resolved "https://registry.yarnpkg.com/@napi-rs/cli/-/cli-3.4.1.tgz#424d505b0c57e87f6d869d81d354833a51657fa6" + integrity sha512-ayhm+NfrP5Hmh7vy5pfyYm/ktYtLh2PrgdLuqHTAubO7RoO2JkUE4F991AtgYxNewwXI8+guZLxU8itV7QnDrQ== + dependencies: + "@inquirer/prompts" "^7.8.4" + "@napi-rs/cross-toolchain" "^1.0.3" + "@napi-rs/wasm-tools" "^1.0.1" + "@octokit/rest" "^22.0.0" + clipanion "^4.0.0-rc.4" + colorette "^2.0.20" + debug "^4.4.1" + emnapi "^1.5.0" + es-toolkit "^1.39.10" + js-yaml "^4.1.0" + semver "^7.7.2" + typanion "^3.14.0" + +"@napi-rs/cross-toolchain@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@napi-rs/cross-toolchain/-/cross-toolchain-1.0.3.tgz#8e345d0c9a8aeeaf9287e7af1d4ce83476681373" + integrity sha512-ENPfLe4937bsKVTDA6zdABx4pq9w0tHqRrJHyaGxgaPq03a2Bd1unD5XSKjXJjebsABJ+MjAv1A2OvCgK9yehg== + dependencies: + "@napi-rs/lzma" "^1.4.5" + "@napi-rs/tar" "^1.1.0" + debug "^4.4.1" + +"@napi-rs/lzma-android-arm-eabi@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-android-arm-eabi/-/lzma-android-arm-eabi-1.4.5.tgz#c6722a1d7201e269fdb6ba997d28cb41223e515c" + integrity sha512-Up4gpyw2SacmyKWWEib06GhiDdF+H+CCU0LAV8pnM4aJIDqKKd5LHSlBht83Jut6frkB0vwEPmAkv4NjQ5u//Q== + +"@napi-rs/lzma-android-arm64@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-android-arm64/-/lzma-android-arm64-1.4.5.tgz#05df61667e84419e0550200b48169057b734806f" + integrity sha512-uwa8sLlWEzkAM0MWyoZJg0JTD3BkPknvejAFG2acUA1raXM8jLrqujWCdOStisXhqQjZ2nDMp3FV6cs//zjfuQ== + +"@napi-rs/lzma-darwin-arm64@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-darwin-arm64/-/lzma-darwin-arm64-1.4.5.tgz#c37a01c53f25cb7f014870d2ea6c5576138bcaaa" + integrity sha512-0Y0TQLQ2xAjVabrMDem1NhIssOZzF/y/dqetc6OT8mD3xMTDtF8u5BqZoX3MyPc9FzpsZw4ksol+w7DsxHrpMA== + +"@napi-rs/lzma-darwin-x64@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-darwin-x64/-/lzma-darwin-x64-1.4.5.tgz#555b1dd65d7b104d28b2a12d925d7059226c7f4b" + integrity sha512-vR2IUyJY3En+V1wJkwmbGWcYiT8pHloTAWdW4pG24+51GIq+intst6Uf6D/r46citObGZrlX0QvMarOkQeHWpw== + +"@napi-rs/lzma-freebsd-x64@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-freebsd-x64/-/lzma-freebsd-x64-1.4.5.tgz#683beff15b37774ec91e1de7b4d337894bf43694" + integrity sha512-XpnYQC5SVovO35tF0xGkbHYjsS6kqyNCjuaLQ2dbEblFRr5cAZVvsJ/9h7zj/5FluJPJRDojVNxGyRhTp4z2lw== + +"@napi-rs/lzma-linux-arm-gnueabihf@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-arm-gnueabihf/-/lzma-linux-arm-gnueabihf-1.4.5.tgz#505f659a9131474b7270afa4a4e9caf709c4d213" + integrity sha512-ic1ZZMoRfRMwtSwxkyw4zIlbDZGC6davC9r+2oX6x9QiF247BRqqT94qGeL5ZP4Vtz0Hyy7TEViWhx5j6Bpzvw== + +"@napi-rs/lzma-linux-arm64-gnu@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-arm64-gnu/-/lzma-linux-arm64-gnu-1.4.5.tgz#ecbb944635fa004a9415d1f50f165bc0d26d3807" + integrity sha512-asEp7FPd7C1Yi6DQb45a3KPHKOFBSfGuJWXcAd4/bL2Fjetb2n/KK2z14yfW8YC/Fv6x3rBM0VAZKmJuz4tysg== + +"@napi-rs/lzma-linux-arm64-musl@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-arm64-musl/-/lzma-linux-arm64-musl-1.4.5.tgz#c0d17f40ce2db0b075469a28f233fd8ce31fbb95" + integrity sha512-yWjcPDgJ2nIL3KNvi4536dlT/CcCWO0DUyEOlBs/SacG7BeD6IjGh6yYzd3/X1Y3JItCbZoDoLUH8iB1lTXo3w== + +"@napi-rs/lzma-linux-ppc64-gnu@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-ppc64-gnu/-/lzma-linux-ppc64-gnu-1.4.5.tgz#2f17b9d1fc920c6c511d2086c7623752172c2f07" + integrity sha512-0XRhKuIU/9ZjT4WDIG/qnX7Xz7mSQHYZo9Gb3MP2gcvBgr6BA4zywQ9k3gmQaPn9ECE+CZg2V7DV7kT+x2pUMQ== + +"@napi-rs/lzma-linux-riscv64-gnu@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-riscv64-gnu/-/lzma-linux-riscv64-gnu-1.4.5.tgz#63c2a4e1157586252186e39604370d5b29c6db85" + integrity sha512-QrqDIPEUUB23GCpyQj/QFyMlr8SGxxyExeZz9OWFnHfb70kXdTLWrHS/hEI1Ru+lSbQ/6xRqeoGyQ4Aqdg+/RA== + +"@napi-rs/lzma-linux-s390x-gnu@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-s390x-gnu/-/lzma-linux-s390x-gnu-1.4.5.tgz#6f2ca44bf5c5bef1b31d7516bf15d63c35cdf59f" + integrity sha512-k8RVM5aMhW86E9H0QXdquwojew4H3SwPxbRVbl49/COJQWCUjGi79X6mYruMnMPEznZinUiT1jgKbFo2A00NdA== + +"@napi-rs/lzma-linux-x64-gnu@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.4.5.tgz#54879d88a9c370687b5463c7c1b6208b718c1ab2" + integrity sha512-6rMtBgnIq2Wcl1rQdZsnM+rtCcVCbws1nF8S2NzaUsVaZv8bjrPiAa0lwg4Eqnn1d9lgwqT+cZgm5m+//K08Kw== + +"@napi-rs/lzma-linux-x64-musl@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-linux-x64-musl/-/lzma-linux-x64-musl-1.4.5.tgz#412705f6925f10f45122bd0f3e2fb6e597bed4f8" + integrity sha512-eiadGBKi7Vd0bCArBUOO/qqRYPHt/VQVvGyYvDFt6C2ZSIjlD+HuOl+2oS1sjf4CFjK4eDIog6EdXnL0NE6iyQ== + +"@napi-rs/lzma-wasm32-wasi@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-wasm32-wasi/-/lzma-wasm32-wasi-1.4.5.tgz#4b74abfd144371123cb6f5b7bad5bae868206ecf" + integrity sha512-+VyHHlr68dvey6fXc2hehw9gHVFIW3TtGF1XkcbAu65qVXsA9D/T+uuoRVqhE+JCyFHFrO0ixRbZDRK1XJt1sA== + dependencies: + "@napi-rs/wasm-runtime" "^1.0.3" + +"@napi-rs/lzma-win32-arm64-msvc@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-win32-arm64-msvc/-/lzma-win32-arm64-msvc-1.4.5.tgz#7ed8c80d588fa244a7fd55249cb0d011d04bf984" + integrity sha512-eewnqvIyyhHi3KaZtBOJXohLvwwN27gfS2G/YDWdfHlbz1jrmfeHAmzMsP5qv8vGB+T80TMHNkro4kYjeh6Deg== + +"@napi-rs/lzma-win32-ia32-msvc@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-win32-ia32-msvc/-/lzma-win32-ia32-msvc-1.4.5.tgz#e6f70ca87bd88370102aa610ee9e44ec28911b46" + integrity sha512-OeacFVRCJOKNU/a0ephUfYZ2Yt+NvaHze/4TgOwJ0J0P4P7X1mHzN+ig9Iyd74aQDXYqc7kaCXA2dpAOcH87Cg== + +"@napi-rs/lzma-win32-x64-msvc@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma-win32-x64-msvc/-/lzma-win32-x64-msvc-1.4.5.tgz#ecfcfe364e805915608ce0ff41ed4c950fdb51b8" + integrity sha512-T4I1SamdSmtyZgDXGAGP+y5LEK5vxHUFwe8mz6D4R7Sa5/WCxTcCIgPJ9BD7RkpO17lzhlaM2vmVvMy96Lvk9Q== + +"@napi-rs/lzma@^1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@napi-rs/lzma/-/lzma-1.4.5.tgz#43e17cdfe332a3f33fa640422da348db3d8825e1" + integrity sha512-zS5LuN1OBPAyZpda2ZZgYOEDC+xecUdAGnrvbYzjnLXkrq/OBC3B9qcRvlxbDR3k5H/gVfvef1/jyUqPknqjbg== + optionalDependencies: + "@napi-rs/lzma-android-arm-eabi" "1.4.5" + "@napi-rs/lzma-android-arm64" "1.4.5" + "@napi-rs/lzma-darwin-arm64" "1.4.5" + "@napi-rs/lzma-darwin-x64" "1.4.5" + "@napi-rs/lzma-freebsd-x64" "1.4.5" + "@napi-rs/lzma-linux-arm-gnueabihf" "1.4.5" + "@napi-rs/lzma-linux-arm64-gnu" "1.4.5" + "@napi-rs/lzma-linux-arm64-musl" "1.4.5" + "@napi-rs/lzma-linux-ppc64-gnu" "1.4.5" + "@napi-rs/lzma-linux-riscv64-gnu" "1.4.5" + "@napi-rs/lzma-linux-s390x-gnu" "1.4.5" + "@napi-rs/lzma-linux-x64-gnu" "1.4.5" + "@napi-rs/lzma-linux-x64-musl" "1.4.5" + "@napi-rs/lzma-wasm32-wasi" "1.4.5" + "@napi-rs/lzma-win32-arm64-msvc" "1.4.5" + "@napi-rs/lzma-win32-ia32-msvc" "1.4.5" + "@napi-rs/lzma-win32-x64-msvc" "1.4.5" + +"@napi-rs/tar-android-arm-eabi@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-android-arm-eabi/-/tar-android-arm-eabi-1.1.0.tgz#08ae6ebbaf38d416954a28ca09bf77410d5b0c2b" + integrity sha512-h2Ryndraj/YiKgMV/r5by1cDusluYIRT0CaE0/PekQ4u+Wpy2iUVqvzVU98ZPnhXaNeYxEvVJHNGafpOfaD0TA== + +"@napi-rs/tar-android-arm64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-android-arm64/-/tar-android-arm64-1.1.0.tgz#825a76140116f89d7e930245bda9f70b196da565" + integrity sha512-DJFyQHr1ZxNZorm/gzc1qBNLF/FcKzcH0V0Vwan5P+o0aE2keQIGEjJ09FudkF9v6uOuJjHCVDdK6S6uHtShAw== + +"@napi-rs/tar-darwin-arm64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-darwin-arm64/-/tar-darwin-arm64-1.1.0.tgz#8821616c40ea52ec2c00a055be56bf28dee76013" + integrity sha512-Zz2sXRzjIX4e532zD6xm2SjXEym6MkvfCvL2RMpG2+UwNVDVscHNcz3d47Pf3sysP2e2af7fBB3TIoK2f6trPw== + +"@napi-rs/tar-darwin-x64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-darwin-x64/-/tar-darwin-x64-1.1.0.tgz#4a975e41932a145c58181cb43c8f483c3858e359" + integrity sha512-EI+CptIMNweT0ms9S3mkP/q+J6FNZ1Q6pvpJOEcWglRfyfQpLqjlC0O+dptruTPE8VamKYuqdjxfqD8hifZDOA== + +"@napi-rs/tar-freebsd-x64@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-freebsd-x64/-/tar-freebsd-x64-1.1.0.tgz#5ebc0633f257b258aacc59ac1420835513ed0967" + integrity sha512-J0PIqX+pl6lBIAckL/c87gpodLbjZB1OtIK+RDscKC9NLdpVv6VGOxzUV/fYev/hctcE8EfkLbgFOfpmVQPg2g== + +"@napi-rs/tar-linux-arm-gnueabihf@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-arm-gnueabihf/-/tar-linux-arm-gnueabihf-1.1.0.tgz#1d309bd4f46f0490353d9608e79d260cf6c7cd43" + integrity sha512-SLgIQo3f3EjkZ82ZwvrEgFvMdDAhsxCYjyoSuWfHCz0U16qx3SuGCp8+FYOPYCECHN3ZlGjXnoAIt9ERd0dEUg== + +"@napi-rs/tar-linux-arm64-gnu@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-arm64-gnu/-/tar-linux-arm64-gnu-1.1.0.tgz#88d974821f3f8e9ee6948b4d51c78c019dee88ad" + integrity sha512-d014cdle52EGaH6GpYTQOP9Py7glMO1zz/+ynJPjjzYFSxvdYx0byrjumZk2UQdIyGZiJO2MEFpCkEEKFSgPYA== + +"@napi-rs/tar-linux-arm64-musl@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-arm64-musl/-/tar-linux-arm64-musl-1.1.0.tgz#ab2baee7b288df5e68cef0b2d12fa79d2a551b58" + integrity sha512-L/y1/26q9L/uBqiW/JdOb/Dc94egFvNALUZV2WCGKQXc6UByPBMgdiEyW2dtoYxYYYYc+AKD+jr+wQPcvX2vrQ== + +"@napi-rs/tar-linux-ppc64-gnu@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-ppc64-gnu/-/tar-linux-ppc64-gnu-1.1.0.tgz#7500e60d27849ba36fa4802a346249974e7ecf74" + integrity sha512-EPE1K/80RQvPbLRJDJs1QmCIcH+7WRi0F73+oTe1582y9RtfGRuzAkzeBuAGRXAQEjRQw/RjtNqr6UTJ+8UuWQ== + +"@napi-rs/tar-linux-s390x-gnu@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-s390x-gnu/-/tar-linux-s390x-gnu-1.1.0.tgz#cfc0923bfad1dea8ef9da22148a8d4932aa52d08" + integrity sha512-B2jhWiB1ffw1nQBqLUP1h4+J1ovAxBOoe5N2IqDMOc63fsPZKNqF1PvO/dIem8z7LL4U4bsfmhy3gBfu547oNQ== + +"@napi-rs/tar-linux-x64-gnu@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-x64-gnu/-/tar-linux-x64-gnu-1.1.0.tgz#5fdf9e1bb12b10a951c6ab03268a9f8d9788c929" + integrity sha512-tbZDHnb9617lTnsDMGo/eAMZxnsQFnaRe+MszRqHguKfMwkisc9CCJnks/r1o84u5fECI+J/HOrKXgczq/3Oww== + +"@napi-rs/tar-linux-x64-musl@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-linux-x64-musl/-/tar-linux-x64-musl-1.1.0.tgz#f001fc0a0a2996dcf99e787a15eade8dce215e91" + integrity sha512-dV6cODlzbO8u6Anmv2N/ilQHq/AWz0xyltuXoLU3yUyXbZcnWYZuB2rL8OBGPmqNcD+x9NdScBNXh7vWN0naSQ== + +"@napi-rs/tar-wasm32-wasi@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-wasm32-wasi/-/tar-wasm32-wasi-1.1.0.tgz#c1c7df7738b23f1cdbcff261d5bea6968d0a3c9a" + integrity sha512-jIa9nb2HzOrfH0F8QQ9g3WE4aMH5vSI5/1NYVNm9ysCmNjCCtMXCAhlI3WKCdm/DwHf0zLqdrrtDFXODcNaqMw== + dependencies: + "@napi-rs/wasm-runtime" "^1.0.3" + +"@napi-rs/tar-win32-arm64-msvc@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-win32-arm64-msvc/-/tar-win32-arm64-msvc-1.1.0.tgz#4c8519eab28021e1eda0847433cab949d5389833" + integrity sha512-vfpG71OB0ijtjemp3WTdmBKJm9R70KM8vsSExMsIQtV0lVzP07oM1CW6JbNRPXNLhRoue9ofYLiUDk8bE0Hckg== + +"@napi-rs/tar-win32-ia32-msvc@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-win32-ia32-msvc/-/tar-win32-ia32-msvc-1.1.0.tgz#4f61af0da2c53b23f7d58c77970eaa4449e8eb79" + integrity sha512-hGPyPW60YSpOSgzfy68DLBHgi6HxkAM+L59ZZZPMQ0TOXjQg+p2EW87+TjZfJOkSpbYiEkULwa/f4a2hcVjsqQ== + +"@napi-rs/tar-win32-x64-msvc@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar-win32-x64-msvc/-/tar-win32-x64-msvc-1.1.0.tgz#eb63fb44ecde001cce6be238f175e66a06c15035" + integrity sha512-L6Ed1DxXK9YSCMyvpR8MiNAyKNkQLjsHsHK9E0qnHa8NzLFqzDKhvs5LfnWxM2kJ+F7m/e5n9zPm24kHb3LsVw== + +"@napi-rs/tar@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@napi-rs/tar/-/tar-1.1.0.tgz#acecd9e29f705a3f534d5fb3d8aa36b3266727d0" + integrity sha512-7cmzIu+Vbupriudo7UudoMRH2OA3cTw67vva8MxeoAe5S7vPFI7z0vp0pMXiA25S8IUJefImQ90FeJjl8fjEaQ== + optionalDependencies: + "@napi-rs/tar-android-arm-eabi" "1.1.0" + "@napi-rs/tar-android-arm64" "1.1.0" + "@napi-rs/tar-darwin-arm64" "1.1.0" + "@napi-rs/tar-darwin-x64" "1.1.0" + "@napi-rs/tar-freebsd-x64" "1.1.0" + "@napi-rs/tar-linux-arm-gnueabihf" "1.1.0" + "@napi-rs/tar-linux-arm64-gnu" "1.1.0" + "@napi-rs/tar-linux-arm64-musl" "1.1.0" + "@napi-rs/tar-linux-ppc64-gnu" "1.1.0" + "@napi-rs/tar-linux-s390x-gnu" "1.1.0" + "@napi-rs/tar-linux-x64-gnu" "1.1.0" + "@napi-rs/tar-linux-x64-musl" "1.1.0" + "@napi-rs/tar-wasm32-wasi" "1.1.0" + "@napi-rs/tar-win32-arm64-msvc" "1.1.0" + "@napi-rs/tar-win32-ia32-msvc" "1.1.0" + "@napi-rs/tar-win32-x64-msvc" "1.1.0" + +"@napi-rs/wasm-runtime@^1.0.3": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz#dcfea99a75f06209a235f3d941e3460a51e9b14c" + integrity sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw== + dependencies: + "@emnapi/core" "^1.5.0" + "@emnapi/runtime" "^1.5.0" + "@tybys/wasm-util" "^0.10.1" + +"@napi-rs/wasm-tools-android-arm-eabi@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-android-arm-eabi/-/wasm-tools-android-arm-eabi-1.0.1.tgz#a709f93ddd95508a4ef949b5ceff2b2e85b676f7" + integrity sha512-lr07E/l571Gft5v4aA1dI8koJEmF1F0UigBbsqg9OWNzg80H3lDPO+auv85y3T/NHE3GirDk7x/D3sLO57vayw== + +"@napi-rs/wasm-tools-android-arm64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-android-arm64/-/wasm-tools-android-arm64-1.0.1.tgz#304b5761b4fcc871b876ebd34975c72c9d11a7fc" + integrity sha512-WDR7S+aRLV6LtBJAg5fmjKkTZIdrEnnQxgdsb7Cf8pYiMWBHLU+LC49OUVppQ2YSPY0+GeYm9yuZWW3kLjJ7Bg== + +"@napi-rs/wasm-tools-darwin-arm64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-darwin-arm64/-/wasm-tools-darwin-arm64-1.0.1.tgz#dafb4330986a8b46e8de1603ea2f6932a19634c6" + integrity sha512-qWTI+EEkiN0oIn/N2gQo7+TVYil+AJ20jjuzD2vATS6uIjVz+Updeqmszi7zq7rdFTLp6Ea3/z4kDKIfZwmR9g== + +"@napi-rs/wasm-tools-darwin-x64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-darwin-x64/-/wasm-tools-darwin-x64-1.0.1.tgz#0919e63714ee0a52b1120f6452bbc3a4d793ce3c" + integrity sha512-bA6hubqtHROR5UI3tToAF/c6TDmaAgF0SWgo4rADHtQ4wdn0JeogvOk50gs2TYVhKPE2ZD2+qqt7oBKB+sxW3A== + +"@napi-rs/wasm-tools-freebsd-x64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-freebsd-x64/-/wasm-tools-freebsd-x64-1.0.1.tgz#1f50a2d5d5af041c55634f43f623ae49192bce9c" + integrity sha512-90+KLBkD9hZEjPQW1MDfwSt5J1L46EUKacpCZWyRuL6iIEO5CgWU0V/JnEgFsDOGyyYtiTvHc5bUdUTWd4I9Vg== + +"@napi-rs/wasm-tools-linux-arm64-gnu@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-linux-arm64-gnu/-/wasm-tools-linux-arm64-gnu-1.0.1.tgz#6106d5e65a25ec2ae417c2fcfebd5c8f14d80e84" + integrity sha512-rG0QlS65x9K/u3HrKafDf8cFKj5wV2JHGfl8abWgKew0GVPyp6vfsDweOwHbWAjcHtp2LHi6JHoW80/MTHm52Q== + +"@napi-rs/wasm-tools-linux-arm64-musl@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-linux-arm64-musl/-/wasm-tools-linux-arm64-musl-1.0.1.tgz#0eb3d4d1fbc1938b0edd907423840365ebc53859" + integrity sha512-jAasbIvjZXCgX0TCuEFQr+4D6Lla/3AAVx2LmDuMjgG4xoIXzjKWl7c4chuaD+TI+prWT0X6LJcdzFT+ROKGHQ== + +"@napi-rs/wasm-tools-linux-x64-gnu@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-linux-x64-gnu/-/wasm-tools-linux-x64-gnu-1.0.1.tgz#5de6a567083a83efed16d046f47b680cbe7c9b53" + integrity sha512-Plgk5rPqqK2nocBGajkMVbGm010Z7dnUgq0wtnYRZbzWWxwWcXfZMPa8EYxrK4eE8SzpI7VlZP1tdVsdjgGwMw== + +"@napi-rs/wasm-tools-linux-x64-musl@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-linux-x64-musl/-/wasm-tools-linux-x64-musl-1.0.1.tgz#04cc17ef12b4e5012f2d0e46b09cabe473566e5a" + integrity sha512-GW7AzGuWxtQkyHknHWYFdR0CHmW6is8rG2Rf4V6GNmMpmwtXt/ItWYWtBe4zqJWycMNazpfZKSw/BpT7/MVCXQ== + +"@napi-rs/wasm-tools-wasm32-wasi@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-wasm32-wasi/-/wasm-tools-wasm32-wasi-1.0.1.tgz#6ced3bd03428c854397f00509b1694c3af857a0f" + integrity sha512-/nQVSTrqSsn7YdAc2R7Ips/tnw5SPUcl3D7QrXCNGPqjbatIspnaexvaOYNyKMU6xPu+pc0BTnKVmqhlJJCPLA== + dependencies: + "@napi-rs/wasm-runtime" "^1.0.3" + +"@napi-rs/wasm-tools-win32-arm64-msvc@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-win32-arm64-msvc/-/wasm-tools-win32-arm64-msvc-1.0.1.tgz#e776f66eb637eee312b562e987c0a5871ddc6dac" + integrity sha512-PFi7oJIBu5w7Qzh3dwFea3sHRO3pojMsaEnUIy22QvsW+UJfNQwJCryVrpoUt8m4QyZXI+saEq/0r4GwdoHYFQ== + +"@napi-rs/wasm-tools-win32-ia32-msvc@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-win32-ia32-msvc/-/wasm-tools-win32-ia32-msvc-1.0.1.tgz#9167919a62d24cb3a46f01fada26fee38aeaf884" + integrity sha512-gXkuYzxQsgkj05Zaq+KQTkHIN83dFAwMcTKa2aQcpYPRImFm2AQzEyLtpXmyCWzJ0F9ZYAOmbSyrNew8/us6bw== + +"@napi-rs/wasm-tools-win32-x64-msvc@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools-win32-x64-msvc/-/wasm-tools-win32-x64-msvc-1.0.1.tgz#f896ab29a83605795bb12cf2cfc1a215bc830c65" + integrity sha512-rEAf05nol3e3eei2sRButmgXP+6ATgm0/38MKhz9Isne82T4rPIMYsCIFj0kOisaGeVwoi2fnm7O9oWp5YVnYQ== + +"@napi-rs/wasm-tools@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-tools/-/wasm-tools-1.0.1.tgz#f54caa0132322fd5275690b2aeb581d11539262f" + integrity sha512-enkZYyuCdo+9jneCPE/0fjIta4wWnvVN9hBo2HuiMpRF0q3lzv1J6b/cl7i0mxZUKhBrV3aCKDBQnCOhwKbPmQ== + optionalDependencies: + "@napi-rs/wasm-tools-android-arm-eabi" "1.0.1" + "@napi-rs/wasm-tools-android-arm64" "1.0.1" + "@napi-rs/wasm-tools-darwin-arm64" "1.0.1" + "@napi-rs/wasm-tools-darwin-x64" "1.0.1" + "@napi-rs/wasm-tools-freebsd-x64" "1.0.1" + "@napi-rs/wasm-tools-linux-arm64-gnu" "1.0.1" + "@napi-rs/wasm-tools-linux-arm64-musl" "1.0.1" + "@napi-rs/wasm-tools-linux-x64-gnu" "1.0.1" + "@napi-rs/wasm-tools-linux-x64-musl" "1.0.1" + "@napi-rs/wasm-tools-wasm32-wasi" "1.0.1" + "@napi-rs/wasm-tools-win32-arm64-msvc" "1.0.1" + "@napi-rs/wasm-tools-win32-ia32-msvc" "1.0.1" + "@napi-rs/wasm-tools-win32-x64-msvc" "1.0.1" + +"@octokit/auth-token@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-6.0.0.tgz#b02e9c08a2d8937df09a2a981f226ad219174c53" + integrity sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w== + +"@octokit/core@^7.0.6": + version "7.0.6" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-7.0.6.tgz#0d58704391c6b681dec1117240ea4d2a98ac3916" + integrity sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q== + dependencies: + "@octokit/auth-token" "^6.0.0" + "@octokit/graphql" "^9.0.3" + "@octokit/request" "^10.0.6" + "@octokit/request-error" "^7.0.2" + "@octokit/types" "^16.0.0" + before-after-hook "^4.0.0" + universal-user-agent "^7.0.0" + +"@octokit/endpoint@^11.0.2": + version "11.0.2" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-11.0.2.tgz#a8d955e053a244938b81d86cd73efd2dcb5ef5af" + integrity sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ== + dependencies: + "@octokit/types" "^16.0.0" + universal-user-agent "^7.0.2" + +"@octokit/graphql@^9.0.3": + version "9.0.3" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-9.0.3.tgz#5b8341c225909e924b466705c13477face869456" + integrity sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA== + dependencies: + "@octokit/request" "^10.0.6" + "@octokit/types" "^16.0.0" + universal-user-agent "^7.0.0" + +"@octokit/openapi-types@^27.0.0": + version "27.0.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-27.0.0.tgz#374ea53781965fd02a9d36cacb97e152cefff12d" + integrity sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA== + +"@octokit/plugin-paginate-rest@^14.0.0": + version "14.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz#44dc9fff2dacb148d4c5c788b573ddc044503026" + integrity sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw== + dependencies: + "@octokit/types" "^16.0.0" + +"@octokit/plugin-request-log@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz#de1c1e557df6c08adb631bf78264fa741e01b317" + integrity sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q== + +"@octokit/plugin-rest-endpoint-methods@^17.0.0": + version "17.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz#8c54397d3a4060356a1c8a974191ebf945924105" + integrity sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw== + dependencies: + "@octokit/types" "^16.0.0" + +"@octokit/request-error@^7.0.2": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-7.1.0.tgz#440fa3cae310466889778f5a222b47a580743638" + integrity sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw== + dependencies: + "@octokit/types" "^16.0.0" + +"@octokit/request@^10.0.6": + version "10.0.7" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-10.0.7.tgz#93f619914c523750a85e7888de983e1009eb03f6" + integrity sha512-v93h0i1yu4idj8qFPZwjehoJx4j3Ntn+JhXsdJrG9pYaX6j/XRz2RmasMUHtNgQD39nrv/VwTWSqK0RNXR8upA== + dependencies: + "@octokit/endpoint" "^11.0.2" + "@octokit/request-error" "^7.0.2" + "@octokit/types" "^16.0.0" + fast-content-type-parse "^3.0.0" + universal-user-agent "^7.0.2" + +"@octokit/rest@^22.0.0": + version "22.0.1" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-22.0.1.tgz#4d866c32b76b711d3f736f91992e2b534163b416" + integrity sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw== + dependencies: + "@octokit/core" "^7.0.6" + "@octokit/plugin-paginate-rest" "^14.0.0" + "@octokit/plugin-request-log" "^6.0.0" + "@octokit/plugin-rest-endpoint-methods" "^17.0.0" + +"@octokit/types@^16.0.0": + version "16.0.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-16.0.0.tgz#fbd7fa590c2ef22af881b1d79758bfaa234dbb7c" + integrity sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg== + dependencies: + "@octokit/openapi-types" "^27.0.0" + +"@tybys/wasm-util@^0.10.1": + version "0.10.1" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414" + integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg== + dependencies: + tslib "^2.4.0" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +before-after-hook@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-4.0.0.tgz#cf1447ab9160df6a40f3621da64d6ffc36050cb9" + integrity sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ== + +chardet@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-2.1.1.tgz#5c75593704a642f71ee53717df234031e65373c8" + integrity sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ== + +cli-width@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5" + integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ== + +clipanion@^4.0.0-rc.4: + version "4.0.0-rc.4" + resolved "https://registry.yarnpkg.com/clipanion/-/clipanion-4.0.0-rc.4.tgz#7191a940e47ef197e5f18c9cbbe419278b5f5903" + integrity sha512-CXkMQxU6s9GklO/1f714dkKBMu1lopS1WFF0B8o4AxPykR1hpozxSiUZ5ZUeBjfPgCWqbcNOtZVFhB8Lkfp1+Q== + dependencies: + typanion "^3.8.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.20: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +debug@^4.4.1: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +emnapi@^1.5.0: + version "1.7.1" + resolved "https://registry.yarnpkg.com/emnapi/-/emnapi-1.7.1.tgz#5cbb09ca201c648417077f2d8825289c106461de" + integrity sha512-wlLK2xFq+T+rCBlY6+lPlFVDEyE93b7hSn9dMrfWBIcPf4ArwUvymvvMnN9M5WWuiryYQe9M+UJrkqw4trdyRA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +es-toolkit@^1.39.10: + version "1.42.0" + resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.42.0.tgz#c9e87c7e2d4759ca26887814e6bc780cf4747fc5" + integrity sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA== + +fast-content-type-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz#5590b6c807cc598be125e6740a9fde589d2b7afb" + integrity sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg== + +iconv-lite@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.0.tgz#c50cd80e6746ca8115eb98743afa81aa0e147a3e" + integrity sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +js-yaml@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== + dependencies: + argparse "^2.0.1" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +mute-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-2.0.0.tgz#a5446fc0c512b71c83c44d908d5c7b7b4c493b2b" + integrity sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA== + +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^7.7.2: + version "7.7.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== + +signal-exit@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +tslib@^2.4.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +typanion@^3.14.0, typanion@^3.8.0: + version "3.14.0" + resolved "https://registry.yarnpkg.com/typanion/-/typanion-3.14.0.tgz#a766a91810ce8258033975733e836c43a2929b94" + integrity sha512-ZW/lVMRabETuYCd9O9ZvMhAh8GslSqaUjxmK/JLPCh6l73CvLBiuXswj/+7LdnWOgYsQ130FqLzFz5aGT4I3Ug== + +universal-user-agent@^7.0.0, universal-user-agent@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.3.tgz#c05870a58125a2dc00431f2df815a77fe69736be" + integrity sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A== + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +yoctocolors-cjs@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz#7e4964ea8ec422b7a40ac917d3a344cfd2304baa" + integrity sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw== diff --git a/yarn.lock b/yarn.lock index 7902f3bf9a..9761223ff4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@acemir/cssom@^0.9.23": + version "0.9.23" + resolved "https://registry.yarnpkg.com/@acemir/cssom/-/cssom-0.9.23.tgz#9930458ccace533c597e1cd90c200edc336ed80a" + integrity sha512-2kJ1HxBKzPLbmhZpxBiTZggjtgCwKg1ma5RHShxvd6zgqhDEdEkzpiwe7jLkI2p2BrZvFCXIihdoMkl1H39VnA== + "@antfu/install-pkg@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@antfu/install-pkg/-/install-pkg-1.1.0.tgz#78fa036be1a6081b5a77a5cf59f50c7752b6ba26" @@ -15,6 +20,33 @@ resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-9.3.0.tgz#e05e277f788ac3bec771f57a49fb64546bb32374" integrity sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA== +"@asamuzakjp/css-color@^4.0.3": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@asamuzakjp/css-color/-/css-color-4.1.0.tgz#4c8c6f48ed2e5c1ad9cc1aa23c80d665e56dd458" + integrity sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + lru-cache "^11.2.2" + +"@asamuzakjp/dom-selector@^6.7.4": + version "6.7.4" + resolved "https://registry.yarnpkg.com/@asamuzakjp/dom-selector/-/dom-selector-6.7.4.tgz#1b7cafe7793e399f9291de2689fdd2efc01838dd" + integrity sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA== + dependencies: + "@asamuzakjp/nwsapi" "^2.3.9" + bidi-js "^1.0.3" + css-tree "^3.1.0" + is-potential-custom-element-name "^1.0.1" + lru-cache "^11.2.2" + +"@asamuzakjp/nwsapi@^2.3.9": + version "2.3.9" + resolved "https://registry.yarnpkg.com/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz#ad5549322dfe9d153d4b4dd6f7ff2ae234b06e24" + integrity sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q== + "@babel/code-frame@^7.0.0": version "7.27.1" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" @@ -71,6 +103,39 @@ resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== +"@csstools/color-helpers@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz#106c54c808cabfd1ab4c602d8505ee584c2996ef" + integrity sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA== + +"@csstools/css-calc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.4.tgz#8473f63e2fcd6e459838dd412401d5948f224c65" + integrity sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ== + +"@csstools/css-color-parser@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz#4e386af3a99dd36c46fef013cfe4c1c341eed6f0" + integrity sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA== + dependencies: + "@csstools/color-helpers" "^5.1.0" + "@csstools/css-calc" "^2.1.4" + +"@csstools/css-parser-algorithms@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz#5755370a9a29abaec5515b43c8b3f2cf9c2e3076" + integrity sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ== + +"@csstools/css-syntax-patches-for-csstree@^1.0.14": + version "1.0.17" + resolved "https://registry.yarnpkg.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.17.tgz#307685b5204cdf9fc029a5850a94edd3a8dc100f" + integrity sha512-LCC++2h8pLUSPY+EsZmrrJ1EOUu+5iClpEiDhhdw3zRJpPbABML/N5lmRuBHjxtKm9VnRcsUzioyD0sekFMF0A== + +"@csstools/css-tokenizer@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz#333fedabc3fd1a8e5d0100013731cf19e6a8c5d3" + integrity sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw== + "@cypress/request@^3.0.9": version "3.0.9" resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.9.tgz#8ed6e08fea0c62998b5552301023af7268f11625" @@ -197,9 +262,9 @@ integrity sha512-79vplrUBWL4Fkt59YkEBdSpqBVhNrY8t5+jEp+wX5QbsmbQLcSULqwS7FmbNRyECa2LWMrUWpe6ENIfNwB4jiw== "@github/copilot@latest": - version "0.0.361" - resolved "https://registry.yarnpkg.com/@github/copilot/-/copilot-0.0.361.tgz#b228d314ce4cc5336f64883c62a482be61ad4c66" - integrity sha512-jjd/cNe7hAriRFW+3H+KuW9JAJuQ+dznDjdkda7YlojNmS0e7/9qsTOdwR95kSfYXZ/p5g3+MQMvqUhdybQCLw== + version "0.0.362" + resolved "https://registry.yarnpkg.com/@github/copilot/-/copilot-0.0.362.tgz#4b5bf1c6c09f9b5bf67b0ed407aa8f38500ee2da" + integrity sha512-oRsXkK0YltgXwdDCOlJTKyTnCvSaJ8hV99o57c8RbcDf5DrQj7BQJbONRqpmTaZQBJY/4UOCXpmTk2Sq4eusGQ== "@humanfs/core@^0.19.1": version "0.19.1" @@ -262,6 +327,11 @@ dependencies: langium "3.3.1" +"@mixmark-io/domino@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@mixmark-io/domino/-/domino-2.2.0.tgz#4e8ec69bf1afeb7a14f0628b7e2c0f35bdb336c3" + integrity sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -523,6 +593,13 @@ "@types/d3-transition" "*" "@types/d3-zoom" "*" +"@types/debug@^4.0.0": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + "@types/estree@^1.0.6": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" @@ -538,6 +615,11 @@ resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== +"@types/js-yaml@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" + integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== + "@types/json-schema@^7.0.15": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -548,6 +630,18 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/mdast@^4.0.0": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" + integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== + dependencies: + "@types/unist" "*" + +"@types/ms@*": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== + "@types/node@*": version "24.10.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.1.tgz#91e92182c93db8bd6224fca031e2370cef9a8f01" @@ -580,6 +674,16 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== +"@types/turndown@^5.0.6": + version "5.0.6" + resolved "https://registry.yarnpkg.com/@types/turndown/-/turndown-5.0.6.tgz#42a27397298a312d6088f29c0ff4819c518c1ecb" + integrity sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg== + +"@types/unist@*", "@types/unist@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + "@types/yauzl@^2.9.1": version "2.10.3" resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" @@ -705,6 +809,11 @@ acorn@^8.15.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== +agent-base@^7.1.0, agent-base@^7.1.2: + version "7.1.4" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" + integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -950,6 +1059,11 @@ axobject-query@^4.1.0: resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -961,9 +1075,9 @@ base64-js@^1.3.1: integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== baseline-browser-mapping@^2.8.25: - version "2.8.29" - resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.29.tgz#d8800b71399c783cb1bf2068c2bcc3b6cfd7892c" - integrity sha512-sXdt2elaVnhpDNRDz+1BDx1JQoJRuNk7oVlAlbGiFkLikHCAQiccexF/9e91zVi6RCgqspl04aP+6Cnl9zRLrA== + version "2.8.30" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz#5c7420acc2fd20f3db820a40c6521590a671d137" + integrity sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA== bcrypt-pbkdf@^1.0.0: version "1.0.2" @@ -972,6 +1086,13 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bidi-js@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/bidi-js/-/bidi-js-1.0.3.tgz#6f8bcf3c877c4d9220ddf49b9bb6930c88f877d2" + integrity sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw== + dependencies: + require-from-string "^2.0.2" + big-integer@^1.6.17, big-integer@^1.6.48: version "1.6.52" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" @@ -1142,9 +1263,9 @@ callsites@^3.0.0: integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== caniuse-lite@^1.0.30001754: - version "1.0.30001756" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz#fe80104631102f88e58cad8aa203a2c3e5ec9ebd" - integrity sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A== + version "1.0.30001757" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz" + integrity sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ== careful-downloader@^3.0.0: version "3.0.0" @@ -1163,6 +1284,11 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + chainsaw@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" @@ -1183,6 +1309,11 @@ chalk@^5.0.0: resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea" integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA== +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + check-more-types@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" @@ -1418,6 +1549,23 @@ crypto-random-string@^4.0.0: dependencies: type-fest "^1.0.1" +css-tree@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-3.1.0.tgz#7aabc035f4e66b5c86f54570d55e05b1346eb0fd" + integrity sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w== + dependencies: + mdn-data "2.12.2" + source-map-js "^1.0.1" + +cssstyle@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-5.3.3.tgz#977f3868f379c17d619e9672839f9b5bb3db9861" + integrity sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw== + dependencies: + "@asamuzakjp/css-color" "^4.0.3" + "@csstools/css-syntax-patches-for-csstree" "^1.0.14" + css-tree "^3.1.0" + cypress@^14.0.1: version "14.5.4" resolved "https://registry.yarnpkg.com/cypress/-/cypress-14.5.4.tgz#d821fbb6220c3328e7413acc7724b75319c9e64d" @@ -1778,6 +1926,14 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-urls@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-6.0.0.tgz#95a7943c8ac14c1d563b771f2621cc50e8ec7744" + integrity sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA== + dependencies: + whatwg-mimetype "^4.0.0" + whatwg-url "^15.0.0" + data-view-buffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" @@ -1810,6 +1966,13 @@ dayjs@^1.10.4, dayjs@^1.11.18: resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.19.tgz#15dc98e854bb43917f12021806af897c58ae2938" integrity sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw== +debug@4, debug@^4.0.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.1: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -1817,12 +1980,17 @@ debug@^3.1.0, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.1: - version "4.4.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" - integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== +decimal.js@^10.6.0: + version "10.6.0" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a" + integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== + +decode-named-character-reference@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz#25c32ae6dd5e21889549d40f676030e9514cc0ed" + integrity sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q== dependencies: - ms "^2.1.3" + character-entities "^2.0.0" decompress-response@^6.0.0: version "6.0.0" @@ -1929,6 +2097,18 @@ dependency-graph@^1.0.0: resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-1.0.0.tgz#bb5e85aec1310bc13b22dbd76e3196c4ee4c10d2" integrity sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg== +dequal@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +devlop@^1.0.0, devlop@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== + dependencies: + dequal "^2.0.0" + discontinuous-range@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a" @@ -1978,9 +2158,9 @@ ecc-jsbn@~0.1.1: safer-buffer "^2.1.0" electron-to-chromium@^1.5.249: - version "1.5.257" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.257.tgz#5ddd164fdc45fdfcb1ae88a424ec38734e4b8874" - integrity sha512-VNSOB6JZan5IQNMqaurYpZC4bDPXcvKlUwVD/ztMeVD7SwOpMYGOY7dgt+4lNiIHIpvv/FdULnZKqKEy2KcuHQ== + version "1.5.259" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz#d4393167ec14c5a046cebaec3ddf3377944ce965" + integrity sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ== emoji-regex@^8.0.0: version "8.0.0" @@ -2012,6 +2192,11 @@ enquirer@^2.3.6: ansi-colors "^4.1.1" strip-ansi "^6.0.1" +entities@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694" + integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== + error-ex@^1.3.1: version "1.3.4" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" @@ -2137,6 +2322,11 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + eslint-config-prettier@^10.1.5: version "10.1.8" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz#15734ce4af8c2778cc32f0b01b37b0b5cd1ecb97" @@ -2355,7 +2545,7 @@ extend-shallow@^2.0.1: dependencies: is-extendable "^0.1.0" -extend@~3.0.2: +extend@^3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -2414,6 +2604,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fault@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fault/-/fault-2.0.1.tgz#d47ca9f37ca26e4bd38374a7c500b5a384755b6c" + integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ== + dependencies: + format "^0.2.0" + fd-slicer@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" @@ -2542,6 +2739,11 @@ form-data@^4.0.4, form-data@~4.0.4: hasown "^2.0.2" mime-types "^2.1.12" +format@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== + fraction.js@^5.3.4: version "5.3.4" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-5.3.4.tgz#8c0fcc6a9908262df4ed197427bdeef563e0699a" @@ -2711,7 +2913,7 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^10.3.7: +glob@^10.3.10, glob@^10.3.7: version "10.5.0" resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== @@ -2865,11 +3067,26 @@ hosted-git-info@^4.0.1: dependencies: lru-cache "^6.0.0" +html-encoding-sniffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448" + integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ== + dependencies: + whatwg-encoding "^3.1.1" + http-cache-semantics@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz#205f4db64f8562b76a4ff9235aa5279839a09dd5" integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ== +http-proxy-agent@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + http-signature@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.4.0.tgz#dee5a9ba2bf49416abc544abd6d967f6a94c8c3f" @@ -2887,6 +3104,14 @@ http2-wrapper@^2.1.10: quick-lru "^5.1.1" resolve-alpn "^1.2.0" +https-proxy-agent@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + hugo-extended@>=0.101.0: version "0.152.2" resolved "https://registry.yarnpkg.com/hugo-extended/-/hugo-extended-0.152.2.tgz#c860dcbaf879832d5536e5cd9eda0710ca290a6b" @@ -2901,7 +3126,7 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== -iconv-lite@0.6: +iconv-lite@0.6, iconv-lite@0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== @@ -3140,6 +3365,16 @@ is-path-inside@^4.0.0: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-4.0.0.tgz#805aeb62c47c1b12fc3fd13bfb3ed1e7430071db" integrity sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA== +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + is-regex@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" @@ -3305,6 +3540,32 @@ jsdoc-type-pratt-parser@~4.1.0: resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz#ff6b4a3f339c34a6c188cbf50a16087858d22113" integrity sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg== +jsdom@^27.2.0: + version "27.2.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-27.2.0.tgz#499a41eef477c3632f44009e095cb8e418fdd714" + integrity sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA== + dependencies: + "@acemir/cssom" "^0.9.23" + "@asamuzakjp/dom-selector" "^6.7.4" + cssstyle "^5.3.3" + data-urls "^6.0.0" + decimal.js "^10.6.0" + html-encoding-sniffer "^4.0.0" + http-proxy-agent "^7.0.2" + https-proxy-agent "^7.0.6" + is-potential-custom-element-name "^1.0.1" + parse5 "^8.0.0" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^6.0.0" + w3c-xmlserializer "^5.0.0" + webidl-conversions "^8.0.0" + whatwg-encoding "^3.1.1" + whatwg-mimetype "^4.0.0" + whatwg-url "^15.1.0" + ws "^8.18.3" + xml-name-validator "^5.0.0" + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" @@ -3632,6 +3893,11 @@ logform@^2.7.0: safe-stable-stringify "^2.3.1" triple-beam "^1.3.0" +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== + lowercase-keys@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" @@ -3642,6 +3908,11 @@ lru-cache@^10.2.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== +lru-cache@^11.2.2: + version "11.2.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.2.2.tgz#40fd37edffcfae4b2940379c0722dc6eeaa75f24" + integrity sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -3661,6 +3932,11 @@ markdown-link@^0.1.1: resolved "https://registry.yarnpkg.com/markdown-link/-/markdown-link-0.1.1.tgz#32c5c65199a6457316322d1e4229d13407c8c7cf" integrity sha512-TurLymbyLyo+kAUUAV9ggR9EPcDjP/ctlv9QAFiqUH7c+t6FlsbivPo9OKTU8xdOx9oNd2drW/Fi5RRElQbUqA== +markdown-table@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.4.tgz#fe44d6d410ff9d6f2ea1797a3f60aa4d2b631c2a" + integrity sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw== + marked@^16.2.1: version "16.4.2" resolved "https://registry.yarnpkg.com/marked/-/marked-16.4.2.tgz#4959a64be6c486f0db7467ead7ce288de54290a3" @@ -3671,6 +3947,146 @@ math-intrinsics@^1.1.0: resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== +mdast-util-find-and-replace@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz#70a3174c894e14df722abf43bc250cbae44b11df" + integrity sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg== + dependencies: + "@types/mdast" "^4.0.0" + escape-string-regexp "^5.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + +mdast-util-from-markdown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz#4850390ca7cf17413a9b9a0fbefcd1bc0eb4160a" + integrity sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + mdast-util-to-string "^4.0.0" + micromark "^4.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-decode-string "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + unist-util-stringify-position "^4.0.0" + +mdast-util-frontmatter@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz#f5f929eb1eb36c8a7737475c7eb438261f964ee8" + integrity sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + escape-string-regexp "^5.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + micromark-extension-frontmatter "^2.0.0" + +mdast-util-gfm-autolink-literal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz#abd557630337bd30a6d5a4bd8252e1c2dc0875d5" + integrity sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ== + dependencies: + "@types/mdast" "^4.0.0" + ccount "^2.0.0" + devlop "^1.0.0" + mdast-util-find-and-replace "^3.0.0" + micromark-util-character "^2.0.0" + +mdast-util-gfm-footnote@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz#7778e9d9ca3df7238cc2bd3fa2b1bf6a65b19403" + integrity sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + +mdast-util-gfm-strikethrough@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz#d44ef9e8ed283ac8c1165ab0d0dfd058c2764c16" + integrity sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-gfm-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz#7a435fb6223a72b0862b33afbd712b6dae878d38" + integrity sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + markdown-table "^3.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-gfm-task-list-item@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz#e68095d2f8a4303ef24094ab642e1047b991a936" + integrity sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ== + dependencies: + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-gfm@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz#2cdf63b92c2a331406b0fb0db4c077c1b0331751" + integrity sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ== + dependencies: + mdast-util-from-markdown "^2.0.0" + mdast-util-gfm-autolink-literal "^2.0.0" + mdast-util-gfm-footnote "^2.0.0" + mdast-util-gfm-strikethrough "^2.0.0" + mdast-util-gfm-table "^2.0.0" + mdast-util-gfm-task-list-item "^2.0.0" + mdast-util-to-markdown "^2.0.0" + +mdast-util-phrasing@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3" + integrity sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w== + dependencies: + "@types/mdast" "^4.0.0" + unist-util-is "^6.0.0" + +mdast-util-to-markdown@^2.0.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz#f910ffe60897f04bb4b7e7ee434486f76288361b" + integrity sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA== + dependencies: + "@types/mdast" "^4.0.0" + "@types/unist" "^3.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^4.0.0" + mdast-util-to-string "^4.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-decode-string "^2.0.0" + unist-util-visit "^5.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" + integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg== + dependencies: + "@types/mdast" "^4.0.0" + +mdn-data@2.12.2: + version "2.12.2" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.12.2.tgz#9ae6c41a9e65adf61318b32bff7b64fbfb13f8cf" + integrity sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -3707,6 +4123,289 @@ mermaid@^11.10.0: ts-dedent "^2.2.0" uuid "^11.1.0" +micromark-core-commonmark@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz#c691630e485021a68cf28dbc2b2ca27ebf678cd4" + integrity sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg== + dependencies: + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-factory-destination "^2.0.0" + micromark-factory-label "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-factory-title "^2.0.0" + micromark-factory-whitespace "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-html-tag-name "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-frontmatter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz#651c52ffa5d7a8eeed687c513cd869885882d67a" + integrity sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg== + dependencies: + fault "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-autolink-literal@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz#6286aee9686c4462c1e3552a9d505feddceeb935" + integrity sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-footnote@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz#4dab56d4e398b9853f6fe4efac4fc9361f3e0750" + integrity sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw== + dependencies: + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-strikethrough@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz#86106df8b3a692b5f6a92280d3879be6be46d923" + integrity sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-table@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz#fac70bcbf51fe65f5f44033118d39be8a9b5940b" + integrity sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm-tagfilter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz#f26d8a7807b5985fba13cf61465b58ca5ff7dc57" + integrity sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg== + dependencies: + micromark-util-types "^2.0.0" + +micromark-extension-gfm-task-list-item@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz#bcc34d805639829990ec175c3eea12bb5b781f2c" + integrity sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw== + dependencies: + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz#3e13376ab95dd7a5cfd0e29560dfe999657b3c5b" + integrity sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w== + dependencies: + micromark-extension-gfm-autolink-literal "^2.0.0" + micromark-extension-gfm-footnote "^2.0.0" + micromark-extension-gfm-strikethrough "^2.0.0" + micromark-extension-gfm-table "^2.0.0" + micromark-extension-gfm-tagfilter "^2.0.0" + micromark-extension-gfm-task-list-item "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-destination@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz#8fef8e0f7081f0474fbdd92deb50c990a0264639" + integrity sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-label@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz#5267efa97f1e5254efc7f20b459a38cb21058ba1" + integrity sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg== + dependencies: + devlop "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-space@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz#36d0212e962b2b3121f8525fc7a3c7c029f334fc" + integrity sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-title@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz#237e4aa5d58a95863f01032d9ee9b090f1de6e94" + integrity sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-factory-whitespace@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz#06b26b2983c4d27bfcc657b33e25134d4868b0b1" + integrity sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ== + dependencies: + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-character@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6" + integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== + dependencies: + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-chunked@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz#47fbcd93471a3fccab86cff03847fc3552db1051" + integrity sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-classify-character@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz#d399faf9c45ca14c8b4be98b1ea481bced87b629" + integrity sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-combine-extensions@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz#2a0f490ab08bff5cc2fd5eec6dd0ca04f89b30a9" + integrity sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg== + dependencies: + micromark-util-chunked "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-decode-numeric-character-reference@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz#fcf15b660979388e6f118cdb6bf7d79d73d26fe5" + integrity sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-decode-string@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz#6cb99582e5d271e84efca8e61a807994d7161eb2" + integrity sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-encode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8" + integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== + +micromark-util-html-tag-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz#e40403096481986b41c106627f98f72d4d10b825" + integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA== + +micromark-util-normalize-identifier@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz#c30d77b2e832acf6526f8bf1aa47bc9c9438c16d" + integrity sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q== + dependencies: + micromark-util-symbol "^2.0.0" + +micromark-util-resolve-all@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz#e1a2d62cdd237230a2ae11839027b19381e31e8b" + integrity sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg== + dependencies: + micromark-util-types "^2.0.0" + +micromark-util-sanitize-uri@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7" + integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== + dependencies: + micromark-util-character "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-symbol "^2.0.0" + +micromark-util-subtokenize@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz#d8ade5ba0f3197a1cf6a2999fbbfe6357a1a19ee" + integrity sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA== + dependencies: + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-util-symbol@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8" + integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== + +micromark-util-types@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e" + integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== + +micromark@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.2.tgz#91395a3e1884a198e62116e33c9c568e39936fdb" + integrity sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-decode-numeric-character-reference "^2.0.0" + micromark-util-encode "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-subtokenize "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + micromatch@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" @@ -4004,6 +4703,13 @@ p-limit@^4.0.0: dependencies: yocto-queue "^1.0.0" +p-limit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-5.0.0.tgz#6946d5b7140b649b7a33a027d89b4c625b3a5985" + integrity sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ== + dependencies: + yocto-queue "^1.0.0" + p-locate@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" @@ -4064,6 +4770,13 @@ parse-statements@1.0.11: resolved "https://registry.yarnpkg.com/parse-statements/-/parse-statements-1.0.11.tgz#8787c5d383ae5746568571614be72b0689584344" integrity sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA== +parse5@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-8.0.0.tgz#aceb267f6b15f9b6e6ba9e35bfdd481fc2167b12" + integrity sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA== + dependencies: + entities "^6.0.0" + path-data-parser@0.1.0, path-data-parser@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/path-data-parser/-/path-data-parser-0.1.0.tgz#8f5ba5cc70fc7becb3dcefaea08e2659aba60b8c" @@ -4295,7 +5008,7 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^2.1.0: +punycode@^2.1.0, punycode@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== @@ -4423,6 +5136,57 @@ regexp.prototype.flags@^1.5.4: gopd "^1.2.0" set-function-name "^2.0.2" +remark-frontmatter@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz#b68d61552a421ec412c76f4f66c344627dc187a2" + integrity sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-frontmatter "^2.0.0" + micromark-extension-frontmatter "^2.0.0" + unified "^11.0.0" + +remark-gfm@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.1.tgz#33227b2a74397670d357bf05c098eaf8513f0d6b" + integrity sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-gfm "^3.0.0" + micromark-extension-gfm "^3.0.0" + remark-parse "^11.0.0" + remark-stringify "^11.0.0" + unified "^11.0.0" + +remark-parse@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-11.0.0.tgz#aa60743fcb37ebf6b069204eb4da304e40db45a1" + integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + micromark-util-types "^2.0.0" + unified "^11.0.0" + +remark-stringify@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-11.0.0.tgz#4c5b01dd711c269df1aaae11743eb7e2e7636fd3" + integrity sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-to-markdown "^2.0.0" + unified "^11.0.0" + +remark@^15.0.1: + version "15.0.1" + resolved "https://registry.yarnpkg.com/remark/-/remark-15.0.1.tgz#ac7e7563260513b66426bc47f850e7aa5862c37c" + integrity sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A== + dependencies: + "@types/mdast" "^4.0.0" + remark-parse "^11.0.0" + remark-stringify "^11.0.0" + unified "^11.0.0" + request-progress@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" @@ -4435,6 +5199,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + resolve-alpn@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" @@ -4580,6 +5349,13 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +saxes@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5" + integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== + dependencies: + xmlchars "^2.2.0" + section-matter@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" @@ -4733,7 +5509,7 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -source-map-js@^1.2.1: +source-map-js@^1.0.1, source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== @@ -4967,6 +5743,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + tar-stream@^1.5.2: version "1.6.2" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" @@ -5045,6 +5826,11 @@ tldts-core@^6.1.86: resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.86.tgz#a93e6ed9d505cb54c542ce43feb14c73913265d8" integrity sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA== +tldts-core@^7.0.18: + version "7.0.18" + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-7.0.18.tgz#78edfd38e8c35e20fb4d2cde63c759139e169d31" + integrity sha512-jqJC13oP4FFAahv4JT/0WTDrCF9Okv7lpKtOZUGPLiAnNbACcSg8Y8T+Z9xthOmRBqi/Sob4yi0TE0miRCvF7Q== + tldts@^6.1.32: version "6.1.86" resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.86.tgz#087e0555b31b9725ee48ca7e77edc56115cd82f7" @@ -5052,6 +5838,13 @@ tldts@^6.1.32: dependencies: tldts-core "^6.1.86" +tldts@^7.0.5: + version "7.0.18" + resolved "https://registry.yarnpkg.com/tldts/-/tldts-7.0.18.tgz#72cac7a2bdb6bba78f8a09fdf7ef84843b09aa94" + integrity sha512-lCcgTAgMxQ1JKOWrVGo6E69Ukbnx4Gc1wiYLRf6J5NN4HRYJtCby1rPF8rkQ4a6qqoFBK5dvjJ1zJ0F7VfDSvw== + dependencies: + tldts-core "^7.0.18" + tmp@~0.2.3: version "0.2.5" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" @@ -5080,6 +5873,20 @@ tough-cookie@^5.0.0: dependencies: tldts "^6.1.32" +tough-cookie@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-6.0.0.tgz#11e418b7864a2c0d874702bc8ce0f011261940e5" + integrity sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w== + dependencies: + tldts "^7.0.5" + +tr46@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-6.0.0.tgz#f5a1ae546a0adb32a277a2278d0d17fa2f9093e6" + integrity sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw== + dependencies: + punycode "^2.3.1" + "traverse@>=0.3.0 <0.4": version "0.3.9" resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" @@ -5095,6 +5902,11 @@ triple-beam@^1.3.0: resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984" integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg== +trough@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" + integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== + ts-api-utils@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" @@ -5127,6 +5939,13 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +turndown@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/turndown/-/turndown-7.2.2.tgz#9557642b54046c5912b3d433f34dd588de455a43" + integrity sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ== + dependencies: + "@mixmark-io/domino" "^2.2.0" + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -5247,6 +6066,19 @@ undici-types@~7.16.0: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== +unified@^11.0.0, unified@^11.0.5: + version "11.0.5" + resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.5.tgz#f66677610a5c0a9ee90cab2b8d4d66037026d9e1" + integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA== + dependencies: + "@types/unist" "^3.0.0" + bail "^2.0.0" + devlop "^1.0.0" + extend "^3.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^6.0.0" + unique-string@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-3.0.0.tgz#84a1c377aff5fd7a8bc6b55d8244b2bd90d75b9a" @@ -5254,6 +6086,37 @@ unique-string@^3.0.0: dependencies: crypto-random-string "^4.0.0" +unist-util-is@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.1.tgz#d0a3f86f2dd0db7acd7d8c2478080b5c67f9c6a9" + integrity sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + dependencies: + "@types/unist" "^3.0.0" + +unist-util-visit-parents@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz#777df7fb98652ce16b4b7cd999d0a1a40efa3a02" + integrity sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + +unist-util-visit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" + integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== + dependencies: + "@types/unist" "^3.0.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" + universalify@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" @@ -5332,6 +6195,22 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vfile-message@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4" + integrity sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + +vfile@^6.0.0: + version "6.0.3" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" + integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== + dependencies: + "@types/unist" "^3.0.0" + vfile-message "^4.0.0" + vscode-jsonrpc@8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz#f43dfa35fb51e763d17cd94dcca0c9458f35abf9" @@ -5367,6 +6246,38 @@ vscode-uri@~3.0.8: resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.8.tgz#1770938d3e72588659a172d0fd4642780083ff9f" integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw== +w3c-xmlserializer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" + integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA== + dependencies: + xml-name-validator "^5.0.0" + +webidl-conversions@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-8.0.0.tgz#821c92aa4f88d88a31264d887e244cb9655690c6" + integrity sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA== + +whatwg-encoding@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5" + integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== + dependencies: + iconv-lite "0.6.3" + +whatwg-mimetype@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" + integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== + +whatwg-url@^15.0.0, whatwg-url@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-15.1.0.tgz#5c433439b9a5789eeb3806bbd0da89a8bd40b8d7" + integrity sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g== + dependencies: + tr46 "^6.0.0" + webidl-conversions "^8.0.0" + which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" @@ -5499,6 +6410,21 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== +ws@^8.18.3: + version "8.18.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472" + integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg== + +xml-name-validator@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673" + integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -5554,3 +6480,8 @@ yocto-queue@^1.0.0: version "1.2.2" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.2.2.tgz#3e09c95d3f1aa89a58c114c99223edf639152c00" integrity sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ== + +zwitch@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==