diff --git a/.gitattributes b/.gitattributes index c78136b5f4..35a5334f7c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16,10 +16,20 @@ *.ixx text eol=crlf *.tlh text eol=crlf *.tli text eol=crlf +*.css text eol=crlf +*.js text eol=crlf +*.json text eol=crlf +*.md text eol=crlf +*.po text eol=crlf +*.vue text eol=crlf *.sh text eol=lf *.gif -text *.jpg -text +*.jpeg -text *.png -text +*.webm -text +*.mp4 -text *.exe -text +*.ico -text diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c3b8f39bd5..275b398dce 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -81,7 +81,7 @@ src/ All Phobos source code └── Blowfish/ Blowfish encryption replacement YRpp/ Game binary header definitions (git submodule) lib/ Third-party headers (e.g. nameof) -docs/ Sphinx documentation (Markdown/MyST) +docs/ VitePress documentation site and source Markdown scripts/ Build and setup scripts .editorconfig Code style enforcement (tabs, Allman braces, CRLF) .vsconfig Required VS components @@ -285,44 +285,66 @@ When the type or function you need is missing or incorrect in YRpp, add or fix i ## Documentation -The docs live in `docs/` and are built with [Sphinx](https://sphinx-doc.org/) + [MyST parser](https://myst-parser.readthedocs.io/) (Markdown with Sphinx directives). They are hosted on [Read the Docs](https://readthedocs.io/). +The docs live in `docs/` and are built with [VitePress](https://vitepress.dev/). Source pages are Markdown files in `docs/`; the VitePress config, theme, and build integration live under `docs/.vitepress/` and `docs/vitepress/build-scripts/`. + +Important documentation paths: + +| Path | Purpose | +|---|---| +| `docs/.vitepress/config.ts` | Main VitePress config, nav/sidebar, locales, rewrites, search, last-updated settings | +| `docs/.vitepress/theme/` | Custom theme wrapper, CSS overrides, Vue components used by docs | +| `docs/vitepress/build-scripts/` | Vite/VitePress plugins and offline export scripts | +| `docs/_static/` | VitePress public directory (`publicDir` is `_static`) | +| `docs/locale//LC_MESSAGES/*.po` | Source translations for localized docs | +| `docs/vitepress/generated/` | Generated root and locale pages; do not edit by hand | +| `docs/.vitepress/.temp/`, `docs/.artifacts/` | Build artifacts; do not commit | + +`README.md`, `CREDITS.md`, `LICENSE.md`, `logo.png`, and `logo-mono.png` are sourced from the repository root by the VitePress root pages plugin. Do not create or edit duplicate copies under `docs/`; edit the root files instead. The home page is generated from the root `README.md`. ### Syntax and formatting -Doc pages use **MyST-flavored Markdown** (`.md`), not reStructuredText. Key MyST features used in this project: -- **Directives**: `` ```{directive} `` blocks (e.g., `{hint}`, `{note}`, `{warning}`, `{toctree}`, `{include}`). -- **Colon fences**: `:::{directive}` / `:::` as an alternative to backtick fences. -- **Dollar math**: `$inline$` and `$$block$$` via `dollarmath` and `amsmath` extensions. -- **Heading anchors**: auto-generated up to depth 3 (`myst_heading_anchors = 3`). -- **Cross-references**: `(target-label)=` above a heading, then `` {ref}`target-label` `` to link. -- **sphinx-design components**: tabs (`{tab-set}`/`{tab-item}`), cards (`{card}`), grids (`{grid}`), badges (`` {bdg-primary}`text` ``), and icons (`` {octicon}`icon-name` ``). +Doc pages use VitePress Markdown. Prefer VitePress-native syntax: +- **Containers**: `::: tip`, `::: info`, `::: warning`, `::: danger`, and `::: details Click to show`. +- **Code fences**: use language-tagged fenced blocks such as ` ```ini ` and ` ```cpp `. +- **Links**: use normal Markdown links to source pages, e.g. `[what's new page](Whats-New.md)` or `[Credits](/CREDITS)`. +- **Heading anchors**: VitePress generates anchors from headings. Link to headings with normal hash links such as `Fixed-or-Improved-Logics.md#target-scan-guard-range-customizations`. +- **Vue components**: custom components registered in `docs/.vitepress/theme/index.ts` can be used directly in Markdown. For example, `CustomGameSpeedGenerator` is used in `docs/Miscellanous.md`. INI code snippets should use ` ```ini ` fenced blocks. See existing doc pages for the standard format of documenting INI keys (key name, default value, accepted types, explanatory comments). +Do not introduce legacy MyST-style syntax such as `` ```{note} ``, `` ```{dropdown} ``, `{ref}` links, `toctree`, grid/card/tab directives, or `(target-label)=` labels. Convert old blocks to VitePress containers when editing nearby content. + ### Building docs locally ``` -pip install -r docs/requirements.txt scripts\build_docs.bat ``` -Output goes to `docs/_build/html/`. Pull requests are automatically built and served by Read the Docs - check the PR status checks for a preview link. +The batch script installs `docs/node_modules` if needed and runs `npm run build` from `docs/`. You can also run the npm scripts directly: -### Translations +``` +cd docs +npm ci +npm run dev # local dev server +npm run build # production VitePress build +npm run build:offline # production build plus single-entry offline documentation export +npm run lint # ESLint for docs scripts/config/theme +npm run format:check # Prettier check for docs files +``` -The project uses Sphinx internationalization with `.po` files. Currently only **zh_CN** (Chinese) is maintained. The translation workflow: +Regular VitePress output goes to `docs/.artifacts/dist/`. Offline documentation output goes to `docs/.artifacts/offline-doc/`, with one top-level `offline-doc.htm` and supporting files under `offline-doc/`. Read the Docs publishes that offline output as an `htmlzip` download named `offline-doc.zip`. -1. **Regenerate `.pot` templates and update `.po` files**: - ``` - scripts\build_docs_locale.bat - ``` - This runs `sphinx-build -b gettext` then `sphinx-intl update -p ./locale -l zh_CN`. +### Translations -2. **Edit translations** in `docs/locale/zh_CN/LC_MESSAGES/*.po` - each `.po` file corresponds to a doc page. Key translation conventions: translate "AI agent" as **智能体** (not 代理, which means "proxy"). +The project uses `.po` files as translation sources. VitePress localized pages are generated by `docs/vitepress/build-scripts/vitepress-po-locale-plugin.ts`. Currently only **zh_CN** (Chinese) is maintained, but the plugin is written to support additional locales in the future. -3. **Build localized docs** with `sphinx-build -D language=zh_CN`. +- English source pages live in `docs/*.md` and selected root files (`README.md`, `CREDITS.md`, `LICENSE.md`). +- Chinese translations live in `docs/locale/zh_CN/LC_MESSAGES/*.po`. +- `docs/locale/zh_CN/LC_MESSAGES/index.po` also provides localized labels for VitePress theme text such as nav/sidebar labels, "On this page", "Last updated", and language names. +- Generated localized Markdown is written to `docs/vitepress/generated/locales/zh_CN/` during VitePress startup/build. Do not edit generated files. +- Use `scripts\build_docs.bat` for the regular online documentation build and `scripts\build_docs_offline.bat` for the offline HTML documentation export. Locale pages are generated automatically by both builds. -When editing English source docs, be aware that changes will invalidate corresponding `.po` entries (marked fuzzy), so translators will need to update them. +When editing English source docs, update the matching `.po` entries when appropriate. Key translation convention: translate "AI agent" as **智能体** (not 代理, which means "proxy"). ### English quality and style @@ -343,6 +365,8 @@ For non-trivial changes (unless labeled `No Documentation Needed`): Use `[Minor]` in the PR title for small changes that don't need documentation updates. +Use `[Docs]` at the start of the PR title for documentation-only changes that should skip the PR DLL build. + ## Trust These Instructions Trust the information here and proceed directly with implementation. Only search the codebase if these instructions are incomplete or produce errors. The build scripts, project structure, and patterns described above have been validated against the current `develop` branch at commit `a67278ed95c9cdc611e659d677bd4c918a887d16`. diff --git a/.github/workflows/pr-nightly.yml b/.github/workflows/pr-nightly.yml index ad7d13bd27..6413cd1dac 100644 --- a/.github/workflows/pr-nightly.yml +++ b/.github/workflows/pr-nightly.yml @@ -16,6 +16,7 @@ env: jobs: build: + if: ${{ !startsWith(toLower(github.event.pull_request.title), '[docs]') }} runs-on: windows-latest steps: diff --git a/.gitignore b/.gitignore index 8591b8d397..2718c9d4f5 100644 --- a/.gitignore +++ b/.gitignore @@ -14,11 +14,6 @@ Nightly/ DevBuild/ -docs/_build/ -docs/locale/.doctrees/ -*.pot -*.mo - /.vscode build diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 477fa7b27f..8b76ebb728 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -2,24 +2,26 @@ # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details -# Required version: 2 -# Set the version of Python and other tools you might need -build: - os: ubuntu-24.04 - tools: - python: "3.11" - -# Build documentation in the docs/ directory with Sphinx -sphinx: - configuration: docs/conf.py - -# Optionally build your docs in additional formats such as PDF formats: - - pdf + - htmlzip -# We recommend specifying your dependencies to enable reproducible builds: -python: - install: - - requirements: docs/requirements.txt +build: + os: ubuntu-24.04 + apt_packages: + - zip + tools: + nodejs: "22" + jobs: + install: + - cd docs && npm ci + build: + html: + - cd docs && npm run build + - mkdir -p $READTHEDOCS_OUTPUT/html + - cp -r docs/.artifacts/dist/* $READTHEDOCS_OUTPUT/html/ + htmlzip: + - cd docs && npm run build:offline + - mkdir -p $READTHEDOCS_OUTPUT/htmlzip + - cd docs/.artifacts && zip -r $READTHEDOCS_OUTPUT/htmlzip/offline-doc.zip offline-doc diff --git a/CREDITS.md b/CREDITS.md index 809c481da3..986a2d2c00 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -21,7 +21,7 @@ This page lists all the individual contributions to the project by their author. - Non-ASCII input fix - Building Placement Preview Adjustment - Check for Changelog/Documentation/Credits in Pull Requests - - Docs dark theme switcher + - VitePress documentation migration - Fix position and layer of info tip and reveal production cameo on selected building - Fix a glitch related to incorrect target setting for missiles - Ability to disable shadow for debris & meteor animations diff --git a/README.md b/README.md index 52c77fe94d..f0d908accd 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![Github All Releases](https://img.shields.io/github/downloads/Phobos-developers/Phobos/total.svg)](https://github.com/Phobos-developers/Phobos/releases) [![Docs status](https://readthedocs.org/projects/phobos/badge/?version=latest)](https://phobos.readthedocs.io/en/latest/?badge=latest) [![Workflow](https://img.shields.io/github/actions/workflow/status/Phobos-developers/Phobos/nightly.yml?branch=develop)](https://github.com/Phobos-developers/Phobos/actions) -[![EditorConfig](https://github.com/Phobos-developers/Phobos/workflows/EditorConfig/badge.svg)](https://github.com/Phobos-developers/Phobos/actions?query=workflow%3AEditorConfig) [![license](https://img.shields.io/github/license/Phobos-developers/Phobos.svg)](https://www.gnu.org/licenses/gpl-3.0.html) > **Warning** @@ -59,7 +58,7 @@ Documentation - [Community Chinese docs](https://docs.qq.com/doc/p/dc3da1ce39a6e787b6e133f7d33d6aebef581cb4) - Because the Chinese translation of the official docs is currently underdeveloped, at the time it is recommended to use the community docs for Chinese users. -You can switch between versions (displays latest develop nightly version by default) in the bottom right corner, as well as download a PDF version. +You can switch between versions (displays latest develop nightly version by default) in the bottom right corner, as well as download a zipped offline HTML version. The documentation is split by a few major categories, each represented with a page on the sidebar. Each page has its contents grouped into multiple subcategories, be it buildings, technotypes, infantry, superweapons or something else. diff --git a/docs/.editorconfig b/docs/.editorconfig new file mode 100755 index 0000000000..4f6c2605f8 --- /dev/null +++ b/docs/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.{css,js,ts,json,md,po,vue}] +charset = utf-8 +end_of_line = crlf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000000..e617c07fcf --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,8 @@ +node_modules/ + +.vitepress/cache/ +.vitepress/.temp/ +.vitepress/config.ts.*.mjs +/vitepress/generated/ + +/.artifacts/ diff --git a/docs/.prettierrc b/docs/.prettierrc new file mode 100755 index 0000000000..352402ccec --- /dev/null +++ b/docs/.prettierrc @@ -0,0 +1,13 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "bracketSpacing": true, + "quoteProps": "consistent", + "arrowParens": "avoid", + "endOfLine": "crlf" +} diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts new file mode 100644 index 0000000000..cb678b5c8b --- /dev/null +++ b/docs/.vitepress/config.ts @@ -0,0 +1,109 @@ +import { defineConfig } from 'vitepress' +import type { PluginOption } from 'vite' +import { + generatePoLocalePages, + getPoLocaleRewrites, + poLocalePlugin, +} from '../vitepress/build-scripts/vitepress-po-locale-plugin.ts' +import { + generateRootPages, + getRootPageRewrites, + rootPagesPlugin, +} from '../vitepress/build-scripts/vitepress-root-pages-plugin.ts' +import { createLastUpdatedTransform } from '../vitepress/build-scripts/vitepress-last-updated.ts' +import { offlineVitePressPlugin } from '../vitepress/build-scripts/vitepress-offline-plugin.ts' +import { mediaDimensionsPlugin } from '../vitepress/build-scripts/media-dimensions-plugin.ts' +import { artifactsDistDir, outputDir as offlineOutputDir } from '../vitepress/build-scripts/shared/offline.ts' +import { getEditLink } from './edit-link.ts' +import { headingAttributeAnchorLabelPlugin } from './markdown/heading-attribute-anchor-label-plugin.ts' +import { sphinxDirectiveFencePlugin } from './markdown/sphinx-directive-fence-plugin.ts' +import { staticPublicDirMarkdownPlugin } from './markdown/static-public-dir-markdown-plugin.ts' +import { renderSearchContent } from './search/local-search-renderer.ts' +import { createLocaleConfig, englishNav, englishSidebar } from './theme-config.ts' + +const isOfflineBuild = process.env.DOCS_VITEPRESS_OFFLINE === '1' +const vitePressBase = process.env.READTHEDOCS_CANONICAL_URL + ? new URL(process.env.READTHEDOCS_CANONICAL_URL).pathname.replace(/\/$/u, '') + : '/' +const rootPages = await generateRootPages() +const poLocalePages = await generatePoLocalePages({ sourcePages: rootPages }) +const rootPageRewrites = getRootPageRewrites(rootPages) +const poLocaleRewrites = await getPoLocaleRewrites(poLocalePages) +const transformPageData = createLastUpdatedTransform({ rootPages, localePages: poLocalePages }) +const vitePlugins: PluginOption[] = [ + staticPublicDirMarkdownPlugin, + mediaDimensionsPlugin(), + rootPagesPlugin(), + poLocalePlugin({ + sourcePages: rootPages, + prepareSources: generateRootPages, + }), +] + +if (isOfflineBuild) { + vitePlugins.push(offlineVitePressPlugin()) +} + +export default defineConfig({ + title: 'Phobos Documentation', + description: 'Community documentation for Phobos YR engine extension', + base: vitePressBase, + outDir: isOfflineBuild ? offlineOutputDir : artifactsDistDir, + cleanUrls: false, + lastUpdated: true, + ignoreDeadLinks: true, + transformPageData, + markdown: { + attrs: { + allowedAttributes: ['id'], + }, + config(md) { + md.use(headingAttributeAnchorLabelPlugin) + md.use(sphinxDirectiveFencePlugin) + }, + }, + rewrites: { + ...rootPageRewrites, + ...poLocaleRewrites, + }, + vite: { + publicDir: '_static', + plugins: vitePlugins, + }, + head: [ + ['link', { rel: 'icon', href: '/favicon.ico', type: 'image/x-icon' }], + ['link', { rel: 'shortcut icon', href: '/favicon.ico' }], + ['link', { rel: 'icon', href: '/favicon.png', type: 'image/png' }], + ['link', { rel: 'apple-touch-icon', href: '/favicon.png' }], + ], + themeConfig: { + logo: '/favicon.png', + siteTitle: 'Phobos', + outline: { + level: [2, 3], + label: 'On this page', + }, + nav: englishNav, + sidebar: englishSidebar, + search: { + provider: 'local', + options: { + _render: renderSearchContent, + }, + }, + editLink: { + pattern: getEditLink, + text: 'Edit this page', + }, + socialLinks: [{ icon: 'github', link: 'https://github.com/Phobos-developers/Phobos' }], + }, + locales: { + root: { + label: 'English', + lang: 'en-US', + }, + zh_CN: { + ...(await createLocaleConfig('zh_CN', 'Simplified Chinese', 'zh-CN')), + }, + }, +}) diff --git a/docs/.vitepress/edit-link.ts b/docs/.vitepress/edit-link.ts new file mode 100644 index 0000000000..97c059d289 --- /dev/null +++ b/docs/.vitepress/edit-link.ts @@ -0,0 +1,27 @@ +export function getEditLink({ filePath }: { filePath: string }) { + const repositoryEditRootUrl = 'https://github.com/Phobos-developers/Phobos/edit/develop/' + const getPoPathForGeneratedLocalePage = (locale: string, pagePath: string) => { + if (pagePath === 'README.md') { + return `docs/locale/${locale}/LC_MESSAGES/index.po` + } + + if (pagePath.endsWith('/README.md')) { + return `docs/locale/${locale}/LC_MESSAGES/${pagePath.replace(/\/README\.md$/u, '/index.po')}` + } + + return `docs/locale/${locale}/LC_MESSAGES/${pagePath.replace(/\.md$/u, '.po')}` + } + + const rootPage = filePath.match(/^vitepress\/generated\/root\/(.+)$/u) + if (rootPage) { + const sourcePath = rootPage[1] === 'License.md' ? 'LICENSE.md' : rootPage[1] + return `${repositoryEditRootUrl}${sourcePath}` + } + + const localePage = filePath.match(/^vitepress\/generated\/locales\/([^/]+)\/(.+)$/u) + if (localePage) { + return `${repositoryEditRootUrl}${getPoPathForGeneratedLocalePage(localePage[1], localePage[2])}` + } + + return `${repositoryEditRootUrl}docs/${filePath}` +} diff --git a/docs/.vitepress/markdown/heading-attribute-anchor-label-plugin.ts b/docs/.vitepress/markdown/heading-attribute-anchor-label-plugin.ts new file mode 100644 index 0000000000..b001a07a5a --- /dev/null +++ b/docs/.vitepress/markdown/heading-attribute-anchor-label-plugin.ts @@ -0,0 +1,22 @@ +import type MarkdownIt from 'markdown-it' + +const markdownHeadingAttributeInTitleRegExp = /\s+\{#[A-Za-z0-9_.:-]+\}(?="?$)/u + +export function headingAttributeAnchorLabelPlugin(md: MarkdownIt): void { + md.core.ruler.after('anchor', 'docs_heading_attribute_anchor_label', state => { + for (const token of state.tokens) { + if (token.type !== 'inline' || !token.children) { + continue + } + + for (const child of token.children) { + // markdown-it-anchor reads the heading title before markdown-it-attrs + // strips `{#id}`, so clean the generated permalink label afterwards. + const ariaLabel = child.attrGet('aria-label') + if (ariaLabel) { + child.attrSet('aria-label', ariaLabel.replace(markdownHeadingAttributeInTitleRegExp, '')) + } + } + } + }) +} diff --git a/docs/.vitepress/markdown/sphinx-directive-fence-plugin.ts b/docs/.vitepress/markdown/sphinx-directive-fence-plugin.ts new file mode 100644 index 0000000000..f69958833e --- /dev/null +++ b/docs/.vitepress/markdown/sphinx-directive-fence-plugin.ts @@ -0,0 +1,114 @@ +import type MarkdownIt from 'markdown-it' +import type StateBlock from 'markdown-it/lib/rules_block/state_block.mjs' + +type SphinxDirectiveContainer = { + title: string + type: 'danger' | 'details' | 'info' | 'tip' | 'warning' +} + +const sphinxDirectiveContainers: Record = { + admonition: { type: 'info', title: 'Note' }, + attention: { type: 'warning', title: 'Attention' }, + caution: { type: 'warning', title: 'Caution' }, + danger: { type: 'danger', title: 'Danger' }, + dropdown: { type: 'details', title: 'Details' }, + error: { type: 'danger', title: 'Error' }, + hint: { type: 'tip', title: 'Hint' }, + important: { type: 'warning', title: 'Important' }, + note: { type: 'info', title: 'Note' }, + seealso: { type: 'info', title: 'See also' }, + tip: { type: 'tip', title: 'Tip' }, + warning: { type: 'warning', title: 'Warning' }, +} + +function normalizeDirectiveTitle(title: string | undefined, fallback: string) { + return title?.trim() || fallback +} + +function findClosingFence(source: string, marker: string, startLine: number, endLine: number, state: StateBlock) { + for (let line = startLine; line < endLine; line += 1) { + const pos = state.bMarks[line] + state.tShift[line] + const max = state.eMarks[line] + + if (pos < max && state.sCount[line] < state.blkIndent) { + break + } + + if (source.slice(pos, max).trim() === marker) { + return line + } + } + + return -1 +} + +function extractDropdownOptions(content: string) { + const lines = content.split('\n') + const firstContentLine = lines.findIndex((line) => line.trim() !== '') + + if (firstContentLine === -1 || lines[firstContentLine].trim() !== ':open:') { + return { content, open: false } + } + + lines.splice(firstContentLine, 1) + return { content: lines.join('\n'), open: true } +} + +export function sphinxDirectiveFencePlugin(md: MarkdownIt) { + md.block.ruler.before('fence', 'sphinx_directive_fence', (state, startLine, endLine, silent) => { + const start = state.bMarks[startLine] + state.tShift[startLine] + const max = state.eMarks[startLine] + const lineText = state.src.slice(start, max) + const match = lineText.match(/^(`{3,}|~{3,})\{([A-Za-z][\w-]*)(?:\}\s*(.*)|\s+(.*)\}\s*)$/u) + + if (!match) { + return false + } + + const marker = match[1] + const directiveName = match[2].toLowerCase() + const directiveContainer = sphinxDirectiveContainers[directiveName] + + if (!directiveContainer) { + return false + } + + const closingLine = findClosingFence(state.src, marker, startLine + 1, endLine, state) + if (closingLine === -1) { + return false + } + + if (silent) { + return true + } + + const title = normalizeDirectiveTitle(match[3] || match[4], directiveContainer.title) + const token = state.push('sphinx_directive_container', '', 0) + token.block = true + token.info = directiveContainer.type + const content = state.getLines(startLine + 1, closingLine, state.blkIndent, true) + const dropdown = directiveName === 'dropdown' ? extractDropdownOptions(content) : undefined + + token.content = dropdown?.content ?? content + token.markup = title + token.meta = { open: dropdown?.open ?? false } + token.map = [startLine, closingLine + 1] + + state.line = closingLine + 1 + return true + }) + + md.renderer.rules.sphinx_directive_container = (tokens, index, options, env) => { + const token = tokens[index] + const title = md.utils.escapeHtml(token.markup) + const body = md.render(token.content, env) + + if (token.info === 'details') { + const open = token.meta?.open ? ' open' : '' + + return `${title}\n${body}\n` + } + + return `

${title}

\n${body}
\n` + } +} diff --git a/docs/.vitepress/markdown/static-public-dir-markdown-plugin.ts b/docs/.vitepress/markdown/static-public-dir-markdown-plugin.ts new file mode 100644 index 0000000000..881638c0bf --- /dev/null +++ b/docs/.vitepress/markdown/static-public-dir-markdown-plugin.ts @@ -0,0 +1,17 @@ +import type { PluginOption } from 'vite' + +export const staticPublicDirMarkdownPlugin: PluginOption = { + name: 'docs-static-public-dir-markdown', + enforce: 'pre', + transform(source, id) { + const modulePath = id.split('?')[0] + + if (!modulePath.endsWith('.md')) { + return null + } + + const rewritten = source.replace(/\bsrc=(["'])_static\//gu, 'src=$1/').replace(/(\]\()_static\//gu, '$1/') + + return rewritten === source ? null : rewritten + }, +} diff --git a/docs/.vitepress/search/local-search-renderer.ts b/docs/.vitepress/search/local-search-renderer.ts new file mode 100644 index 0000000000..577be98286 --- /dev/null +++ b/docs/.vitepress/search/local-search-renderer.ts @@ -0,0 +1,47 @@ +type SearchRenderEnv = { + frontmatter?: { + search?: boolean + } +} + +type SearchMarkdownRenderer = { + render: (source: string, env: SearchRenderEnv) => string +} + +// VitePress builds the local search index from rendered HTML, not from final +// page DOM. Strip media-only fragments here so filenames and captions do not +// become searchable while the actual documentation pages stay unchanged. +const searchMediaTagPattern = String.raw`(?:]*>|]*>[\s\S]*?<\/video>)` +const searchMediaWithNextCaptionRegExp = new RegExp( + String.raw`${searchMediaTagPattern}\s*

\s*[\s\S]*?<\/em>\s*<\/p>`, + 'giu', +) +const searchMediaParagraphWithInlineCaptionRegExp = new RegExp( + String.raw`

\s*${searchMediaTagPattern}\s*[\s\S]*?<\/em>\s*<\/p>`, + 'giu', +) +const searchMediaParagraphWithNextCaptionRegExp = new RegExp( + String.raw`

\s*${searchMediaTagPattern}\s*<\/p>\s*

\s*[\s\S]*?<\/em>\s*<\/p>`, + 'giu', +) +const searchMediaParagraphRegExp = new RegExp(String.raw`

\s*${searchMediaTagPattern}\s*<\/p>`, 'giu') +const searchStandaloneMediaRegExp = new RegExp(searchMediaTagPattern, 'giu') + +function stripSearchMedia(html: string) { + return html + .replace(searchMediaWithNextCaptionRegExp, '') + .replace(searchMediaParagraphWithNextCaptionRegExp, '') + .replace(searchMediaParagraphWithInlineCaptionRegExp, '') + .replace(searchMediaParagraphRegExp, '') + .replace(searchStandaloneMediaRegExp, '') +} + +export function renderSearchContent(source: string, env: SearchRenderEnv, md: SearchMarkdownRenderer) { + if (env.frontmatter?.search === false) { + return '' + } + + // Keep VitePress default behavior for search rendering, then remove only the + // generated media fragments from the copy that is handed to MiniSearch. + return stripSearchMedia(md.render(source, env)) +} diff --git a/docs/.vitepress/theme-config.ts b/docs/.vitepress/theme-config.ts new file mode 100644 index 0000000000..0ca01ee93d --- /dev/null +++ b/docs/.vitepress/theme-config.ts @@ -0,0 +1,106 @@ +import { getEditLink } from './edit-link.ts' +import { readLocaleIndexTranslationMap } from '../vitepress/build-scripts/vitepress-po-locale-plugin.ts' + +export const englishNav = [ + { text: 'Home', link: '/' }, + { text: "What's New", link: '/Whats-New' }, + { text: 'Contributing', link: '/Contributing' }, +] + +export const englishSidebar = [ + { + text: 'Project Info', + items: [ + { text: 'General Info', link: '/General-Info' }, + { text: "What's New", link: '/Whats-New' }, + { text: 'Contributing', link: '/Contributing' }, + { text: 'Credits', link: '/CREDITS' }, + { text: 'License', link: '/License' }, + ], + }, + { + text: 'Extension Documentation', + items: [ + { text: 'New or Enhanced Logics', link: '/New-or-Enhanced-Logics' }, + { text: 'Fixed or Improved Logics', link: '/Fixed-or-Improved-Logics' }, + { text: 'AI Scripting and Mapping', link: '/AI-Scripting-and-Mapping' }, + { text: 'User Interface', link: '/User-Interface' }, + { text: 'Miscellanous', link: '/Miscellanous' }, + ], + }, +] + +async function translateLocaleThemeText(locale: string, msgid: string) { + const translations = await readLocaleIndexTranslationMap(locale) + return translations.get(msgid) || msgid +} + +export async function createLocaleConfig(locale: string, labelMsgid: string, lang: string) { + const localeRoot = `/${locale}` + + return { + label: await translateLocaleThemeText(locale, labelMsgid), + lang, + title: await translateLocaleThemeText(locale, 'Phobos Documentation'), + description: await translateLocaleThemeText(locale, 'Community documentation for Phobos YR engine extension'), + themeConfig: { + nav: [ + { text: await translateLocaleThemeText(locale, 'Home'), link: `${localeRoot}/` }, + { text: await translateLocaleThemeText(locale, "What's New"), link: `${localeRoot}/Whats-New` }, + { text: await translateLocaleThemeText(locale, 'Contributing'), link: `${localeRoot}/Contributing` }, + ], + sidebar: [ + { + text: await translateLocaleThemeText(locale, 'Project Info'), + items: [ + { text: await translateLocaleThemeText(locale, 'General Info'), link: `${localeRoot}/General-Info` }, + { text: await translateLocaleThemeText(locale, "What's New"), link: `${localeRoot}/Whats-New` }, + { text: await translateLocaleThemeText(locale, 'Contributing'), link: `${localeRoot}/Contributing` }, + { text: await translateLocaleThemeText(locale, 'Credits'), link: `${localeRoot}/CREDITS` }, + { text: await translateLocaleThemeText(locale, 'License'), link: `${localeRoot}/License` }, + ], + }, + { + text: await translateLocaleThemeText(locale, 'Extension Documentation'), + items: [ + { + text: await translateLocaleThemeText(locale, 'New or Enhanced Logics'), + link: `${localeRoot}/New-or-Enhanced-Logics`, + }, + { + text: await translateLocaleThemeText(locale, 'Fixed or Improved Logics'), + link: `${localeRoot}/Fixed-or-Improved-Logics`, + }, + { + text: await translateLocaleThemeText(locale, 'AI Scripting and Mapping'), + link: `${localeRoot}/AI-Scripting-and-Mapping`, + }, + { text: await translateLocaleThemeText(locale, 'User Interface'), link: `${localeRoot}/User-Interface` }, + { text: await translateLocaleThemeText(locale, 'Miscellanous'), link: `${localeRoot}/Miscellanous` }, + ], + }, + ], + outline: { + level: [2, 3] as [number, number], + label: await translateLocaleThemeText(locale, 'On this page'), + }, + editLink: { + pattern: getEditLink, + text: await translateLocaleThemeText(locale, 'Edit this page'), + }, + lastUpdated: { + text: await translateLocaleThemeText(locale, 'Last updated'), + }, + docFooter: { + prev: await translateLocaleThemeText(locale, 'Previous page'), + next: await translateLocaleThemeText(locale, 'Next page'), + }, + darkModeSwitchLabel: await translateLocaleThemeText(locale, 'Appearance'), + lightModeSwitchTitle: await translateLocaleThemeText(locale, 'Switch to light theme'), + darkModeSwitchTitle: await translateLocaleThemeText(locale, 'Switch to dark theme'), + sidebarMenuLabel: await translateLocaleThemeText(locale, 'Menu'), + returnToTopLabel: await translateLocaleThemeText(locale, 'Return to top'), + langMenuLabel: await translateLocaleThemeText(locale, 'Select language'), + }, + } +} diff --git a/docs/.vitepress/theme/components/CustomGameSpeedGenerator.vue b/docs/.vitepress/theme/components/CustomGameSpeedGenerator.vue new file mode 100644 index 0000000000..bf83918f46 --- /dev/null +++ b/docs/.vitepress/theme/components/CustomGameSpeedGenerator.vue @@ -0,0 +1,258 @@ + + + + + diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css new file mode 100644 index 0000000000..dda18594cd --- /dev/null +++ b/docs/.vitepress/theme/custom.css @@ -0,0 +1,262 @@ +/* Increase default docs content width. */ +:root { + --vp-layout-max-width: min(1700px, 100vw); + --vp-sidebar-width: 288px; +} + +.VPDoc.has-aside .content-container { + max-width: none !important; + width: 100%; +} + +@media (min-width: 1280px) { + .VPDoc.has-aside .container { + justify-content: flex-start; + } + + .VPDoc.has-aside .content { + flex: 1 1 auto; + margin: 0; + } + + .VPDoc.has-aside .content-container { + margin: 0; + } +} + +html { + scrollbar-gutter: stable; +} + +.VPNav:has(.VPFlyout:hover), +.VPNav:has(.VPFlyout:focus-within) { + z-index: 2101; +} + +/* Markdown image caption pattern: + * ![alt](image) + * *caption* + */ +.vp-doc p > img + em, +.vp-doc p > img ~ em, +.vp-doc p > .docs-video + em, +.vp-doc p > .docs-video ~ em, +.vp-doc p:has(> a > img) > em { + display: block !important; + margin-top: 0.15rem !important; +} + +.vp-doc p > img:first-child, +.vp-doc p > a:first-child > img { + display: block; +} + +.vp-doc img { + border-radius: 8px; +} + +.vp-doc p:has(> img:only-child), +.vp-doc p:has(> a:only-child > img:only-child), +.vp-doc p:has(> .docs-video:only-child), +.vp-doc p:has(> img):has(+ p > em:only-child), +.vp-doc p:has(> a > img):has(+ p > em:only-child) { + margin-bottom: 0 !important; +} + +.vp-doc p:has(> img:only-child) + p:has(> em:only-child), +.vp-doc p:has(> a:only-child > img:only-child) + p:has(> em:only-child), +.vp-doc p:has(> .docs-video:only-child) + p:has(> em:only-child), +.vp-doc p:has(> img) + p:has(> em:only-child), +.vp-doc p:has(> a > img) + p:has(> em:only-child), +.vp-doc img + p:has(> em:only-child), +.vp-doc a:has(> img) + p:has(> em:only-child) { + margin-top: 0.15rem !important; +} + +.vp-doc p:has(> img:only-child) + p > em:only-child, +.vp-doc p:has(> a:only-child > img:only-child) + p > em:only-child, +.vp-doc p:has(> .docs-video:only-child) + p > em:only-child, +.vp-doc p:has(> img) + p > em:only-child, +.vp-doc p:has(> a > img) + p > em:only-child, +.vp-doc img + p > em:only-child, +.vp-doc a:has(> img) + p > em:only-child { + display: block !important; +} + +.vp-doc .docs-video { + display: block; + max-width: 100%; + height: auto; + border-radius: 8px; +} + +.vp-doc .docs-video + p { + margin-top: 0.15rem !important; +} + +.vp-doc .docs-video + p > em { + display: block !important; +} + +.VPLocalSearchBox .excerpt .vp-doc :is(img, video), +.VPLocalSearchBox .excerpt .vp-doc :is(img, video) + em { + display: none; +} + +:root { + --docs-hint-title-bg: #27b795; + --docs-hint-body-bg: #d8f5f0; + --docs-hint-text: var(--vp-c-text-1); + --docs-note-title-bg: #68addc; + --docs-note-body-bg: #eaf5fb; + --docs-note-text: var(--vp-c-text-1); + --docs-warning-title-bg: #f2b174; + --docs-warning-body-bg: #fff0d4; + --docs-warning-text: var(--vp-c-text-1); + --docs-container-title-text: #ffffff; + --docs-container-code-bg: rgb(255 255 255 / 58%); + --docs-container-code-text: var(--vp-code-color); +} + +.dark { + --docs-hint-title-bg: #2f6b3d; + --docs-hint-body-bg: #3f784d; + --docs-hint-text: #f2f7f1; + --docs-note-title-bg: #3e3f44; + --docs-note-body-bg: #57585d; + --docs-note-text: #f2f2f3; + --docs-warning-title-bg: #713333; + --docs-warning-body-bg: #82494c; + --docs-warning-text: #fff2f2; + --docs-container-title-text: #ffffff; + --docs-container-code-bg: rgb(0 0 0 / 20%); + --docs-container-code-text: #f1f3f5; +} + +.vp-doc .custom-block.info, +.vp-doc .custom-block.tip, +.vp-doc .custom-block.danger, +.vp-doc .custom-block.warning { + padding: 0 0 14px; + border: 0; + border-radius: 6px; + overflow: hidden; +} + +.vp-doc .custom-block.info { + color: var(--docs-note-text); + background-color: var(--docs-note-body-bg); +} + +.vp-doc .custom-block.tip { + color: var(--docs-hint-text); + background-color: var(--docs-hint-body-bg); +} + +.vp-doc .custom-block.warning { + color: var(--docs-warning-text); + background-color: var(--docs-warning-body-bg); +} + +.vp-doc .custom-block.danger { + color: var(--docs-warning-text); + background-color: var(--docs-warning-body-bg); +} + +.vp-doc .custom-block.info .custom-block-title, +.vp-doc .custom-block.tip .custom-block-title, +.vp-doc .custom-block.danger .custom-block-title, +.vp-doc .custom-block.warning .custom-block-title { + display: flex; + align-items: center; + gap: 0.42rem; + margin: 0 0 12px; + padding: 5px 12px; + color: var(--docs-container-title-text); + font-size: 0.82rem; + font-weight: 700; + line-height: 1.35; + white-space: nowrap; + text-transform: none; +} + +.vp-doc .custom-block.info .custom-block-title { + background-color: var(--docs-note-title-bg); +} + +.vp-doc .custom-block.tip .custom-block-title { + background-color: var(--docs-hint-title-bg); +} + +.vp-doc .custom-block.warning .custom-block-title { + background-color: var(--docs-warning-title-bg); +} + +.vp-doc .custom-block.danger .custom-block-title { + background-color: var(--docs-warning-title-bg); +} + +.vp-doc .custom-block.info .custom-block-title::before, +.vp-doc .custom-block.tip .custom-block-title::before, +.vp-doc .custom-block.danger .custom-block-title::before, +.vp-doc .custom-block.warning .custom-block-title::before { + display: inline-block; + width: 16px; + height: 16px; + content: ''; + background-position: center; + background-repeat: no-repeat; + background-size: contain; + transform: translateY(-0.02rem); +} + +.vp-doc .custom-block.info .custom-block-title::before { + background-image: url('./icons/custom-block-info.svg'); +} + +.vp-doc .custom-block.tip .custom-block-title::before { + background-image: url('./icons/custom-block-info.svg'); +} + +.vp-doc .custom-block.danger .custom-block-title::before, +.vp-doc .custom-block.warning .custom-block-title::before { + width: 16px; + height: 14px; + background-image: url('./icons/custom-block-warning.svg'); + transform: translateY(-0.01rem); +} + +.vp-doc .custom-block.info > :not(.custom-block-title), +.vp-doc .custom-block.tip > :not(.custom-block-title), +.vp-doc .custom-block.danger > :not(.custom-block-title), +.vp-doc .custom-block.warning > :not(.custom-block-title) { + margin-right: 12px; + margin-left: 12px; +} + +.vp-doc .custom-block.info a, +.vp-doc .custom-block.tip a, +.vp-doc .custom-block.danger a, +.vp-doc .custom-block.warning a { + color: inherit; + text-decoration: underline !important; + text-underline-offset: 2px; +} + +.vp-doc .custom-block.info code, +.vp-doc .custom-block.tip code, +.vp-doc .custom-block.danger code, +.vp-doc .custom-block.warning code { + color: var(--docs-container-code-text); + background-color: var(--docs-container-code-bg); +} + +::view-transition-new(root) { + animation: none; + mix-blend-mode: normal; +} + +::view-transition-old(root) { + animation: none; + mix-blend-mode: normal; +} diff --git a/docs/.vitepress/theme/icons/custom-block-info.svg b/docs/.vitepress/theme/icons/custom-block-info.svg new file mode 100644 index 0000000000..1c746c4cea --- /dev/null +++ b/docs/.vitepress/theme/icons/custom-block-info.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/.vitepress/theme/icons/custom-block-warning.svg b/docs/.vitepress/theme/icons/custom-block-warning.svg new file mode 100644 index 0000000000..f4ee590ace --- /dev/null +++ b/docs/.vitepress/theme/icons/custom-block-warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 0000000000..625cdde429 --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,177 @@ +import { h, nextTick, provide, type App, type Ref } from 'vue' +import { useData } from 'vitepress' +import DefaultTheme from 'vitepress/theme' +import CustomGameSpeedGenerator from './components/CustomGameSpeedGenerator.vue' +import './custom.css' + +type ViewTransitionDocument = Document & { + startViewTransition?: (callback: () => void | Promise) => { + ready: Promise + } +} + +type DocsWindow = Window & { + __DOCS_SEARCH_HOTKEY_INSTALLED__?: boolean + __DOCS_RTD_FILETREEDIFF_POSITION_PATCH_INSTALLED__?: boolean +} + +function isSearchKeyboardShortcut(event: KeyboardEvent) { + return (event.code === 'KeyK' || event.key.toLowerCase() === 'k') && (event.metaKey || event.ctrlKey) +} + +function installPhysicalSearchHotKey() { + const docsWindow = window as DocsWindow + + if (docsWindow.__DOCS_SEARCH_HOTKEY_INSTALLED__) { + return + } + + docsWindow.__DOCS_SEARCH_HOTKEY_INSTALLED__ = true + + window.addEventListener('keydown', event => { + if (!isSearchKeyboardShortcut(event)) { + return + } + + const searchButton = document.querySelector('#local-search .DocSearch-Button') + if (!searchButton) { + return + } + + event.preventDefault() + searchButton.click() + }) +} + +function shouldPatchReadTheDocsFileTreeDiffPosition() { + return window.location.hostname.endsWith('.readthedocs.build') +} + +function patchReadTheDocsFileTreeDiffPosition(host: Element) { + const shadowRoot = host.shadowRoot + + if (!shadowRoot || shadowRoot.querySelector('[data-docs-rtd-filetreediff-position]')) { + return + } + + const style = document.createElement('style') + style.dataset.docsRtdFiletreediffPosition = 'true' + style.textContent = ` + :host > div { + top: 72px !important; + right: 16px !important; + border-radius: 6px !important; + } + + @media (max-width: 768px) { + :host > div { + top: 64px !important; + } + } + ` + + shadowRoot.append(style) +} + +function installReadTheDocsFileTreeDiffPositionPatch() { + const docsWindow = window as DocsWindow + + if ( + docsWindow.__DOCS_RTD_FILETREEDIFF_POSITION_PATCH_INSTALLED__ || + !shouldPatchReadTheDocsFileTreeDiffPosition() + ) { + return + } + + docsWindow.__DOCS_RTD_FILETREEDIFF_POSITION_PATCH_INSTALLED__ = true + + const patchAll = () => { + document.querySelectorAll('readthedocs-filetreediff').forEach(patchReadTheDocsFileTreeDiffPosition) + } + + patchAll() + + new MutationObserver(patchAll).observe(document.documentElement, { + childList: true, + subtree: true, + }) +} + +function shouldUseViewTransition() { + return ( + typeof window !== 'undefined' && + typeof document !== 'undefined' && + typeof (document as ViewTransitionDocument).startViewTransition === 'function' && + !window.matchMedia('(prefers-reduced-motion: reduce)').matches + ) +} + +function getViewTransitionOrigin(event?: MouseEvent) { + if (event) { + return { + x: event.clientX, + y: event.clientY, + } + } + + return { + x: window.innerWidth - 48, + y: 32, + } +} + +function getViewTransitionEndRadius(x: number, y: number) { + return Math.hypot(Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y)) +} + +function toggleAppearanceWithTransition(isDark: Ref, event?: MouseEvent) { + if (!shouldUseViewTransition()) { + isDark.value = !isDark.value + return + } + + const { x, y } = getViewTransitionOrigin(event) + const endRadius = getViewTransitionEndRadius(x, y) + + const transition = (document as ViewTransitionDocument).startViewTransition?.(async () => { + isDark.value = !isDark.value + await nextTick() + }) + + transition?.ready.then(() => { + document.documentElement.animate( + [{ clipPath: `circle(0px at ${x}px ${y}px)` }, { clipPath: `circle(${endRadius}px at ${x}px ${y}px)` }], + { + duration: 300, + easing: 'cubic-bezier(.22, 1, .36, 1)', + pseudoElement: '::view-transition-new(root)', + }, + ) + }) +} + +const Layout = { + name: 'DocsThemeLayout', + setup() { + const { isDark } = useData() + + provide('toggle-appearance', (event?: MouseEvent) => { + toggleAppearanceWithTransition(isDark, event) + }) + + return () => h(DefaultTheme.Layout) + }, +} + +export default { + extends: DefaultTheme, + Layout, + enhanceApp({ app }: { app: App }) { + app.component('CustomGameSpeedGenerator', CustomGameSpeedGenerator) + + if (typeof window !== 'undefined') { + installPhysicalSearchHotKey() + installReadTheDocsFileTreeDiffPositionPatch() + } + }, +} diff --git a/docs/AI-Scripting-and-Mapping.md b/docs/AI-Scripting-and-Mapping.md index ef4402c666..82390cba87 100644 --- a/docs/AI-Scripting-and-Mapping.md +++ b/docs/AI-Scripting-and-Mapping.md @@ -137,7 +137,7 @@ ShowBriefing=true ; boolean ### `10000-10999` Ingame Actions -#### `10000-10049` Attack Actions +#### `10000-10049` Attack Actions {#attack-actions} - These actions instruct the TeamType to use the TaskForce to approach and attack the target specified by the second parameter which is an index of a generic pre-defined group. Look at the tables below for the possible actions (first parameter value) and arguments (the second parameter value). - For threat-based attack actions `TargetSpecialThreatCoefficientDefault` and `EnemyHouseThreatBonus` tags from `rulesmd.ini` are accounted. @@ -384,7 +384,7 @@ In `aimd.ini`: x=14003,0 ``` -### `14004` Force Global `OnlyTargetHouseEnemy` value in Teams for new attack / move actions introduced by Phobos +### `14004` Force Global `OnlyTargetHouseEnemy` value in Teams for new attack / move actions introduced by Phobos {#force-global-onlytargethouseenemy-value-in-teams-for-new-attack-move-actions-introduced-by-phobos} - Globally forcibly set a value for the `OnlyTargetHouseEnemy` tag on TeamType. Only affects the new attack / move actions introduced by Phobos. @@ -654,7 +654,7 @@ ID=ActionCount,[Action1],510,0,0,[MCVRedeploy],0,0,0,A,[ActionX] ... ``` -### `511` Undeploy Building to Waypoint +### `511` Undeploy Building to Waypoint {#undeploy-building-to-waypoint} - Undeploy specific BuildingTypes into VehicleTypes and move them to a specific Waypoint. - If `` is entered for the Building Type here, then undeploy all BuildingTypes. @@ -722,7 +722,7 @@ ID=ActionCount,[Action1],608,0,0,[HouseIndex],0,0,0,A,[ActionX] ... ``` -### `609` Set Radar Mode +### `609` Set Radar Mode {#set-radar-mode} - Change the current radar mode of the trigger house. @@ -743,7 +743,7 @@ ID=ActionCount,[Action1],609,0,0,[RadarMode],0,0,0,A,[ActionX] | 2 | Force enable radar | | 3 | Force disable radar | -### `610` Set house's `TeamDelays` value +### `610` Set house's `TeamDelays` value {#set-house-s-teamdelays-value} - Set the `TeamDelays` value of the trigger's house. - If this value is less than 0, then use the value of `[General] -> TeamDelays`. @@ -756,7 +756,7 @@ ID=ActionCount,[Action1],610,0,0,[Number],0,0,0,A,[ActionX] ... ``` -### `800-802` Display Banner +### `800-802` Display Banner {#display-banner} - Display a 'banner' at a fixed location that is relative to the screen. - Action `800` will create a new banner or replace the banner with the same Banner ID if it exists. Using a local variable's value when displaying a text banner. @@ -935,7 +935,7 @@ In `mycampaign.map`: | -1 | This value is ignored (any house is valid) | | -2 | Pick the owner of the map trigger | -### `606` AttachEffect is attaching to a Techno +### `606` AttachEffect is attaching to a Techno {#attacheffect-is-attaching-to-a-techno} - Checks if an `AttachEffectType` is attaching to a techno. Doesn't work for [attached effects](New-or-Enhanced-Logics.md#attached-effects) that were attached prior to the trigger's enabling. - To be elaborate, the event will be triggered during these occasions: diff --git a/docs/CREDITS.md b/docs/CREDITS.md deleted file mode 100644 index 4b45f5c619..0000000000 --- a/docs/CREDITS.md +++ /dev/null @@ -1,4 +0,0 @@ -```{include} ../CREDITS.md -:relative-docs: docs/ -:relative-images: -``` diff --git a/docs/Contributing.md b/docs/Contributing.md index 9fc7527616..b301e7df8b 100644 --- a/docs/Contributing.md +++ b/docs/Contributing.md @@ -1,9 +1,8 @@ - # Contributing Engine modding is a complicated process which is pretty hard to pull off, but there are also easier parts which don't require mastering the art of reverse-engineering or becoming a dank magician in C++. -## Research and reverse-engineering +### Research and reverse-engineering You can observe how the stuff works by using the engine and note which other stuff influences the behavior, but sooner or later you would want to see the innards of that. This is usually done using such tools as disassemblers/decompilers ([IDA](https://www.hex-rays.com/products/ida/), [Ghidra](https://ghidra-sre.org/)) to decipher what is written in the binary (`gamemd.exe` in case of the binary) and debuggers ([Cheat Engine](https://www.cheatengine.org)'s debugger is pretty good for that) to trace how the binary works. @@ -15,11 +14,11 @@ Reverse-engineering is a complex task, but don't be discouraged, if you want to [Assembly language](https://www.cs.virginia.edu/~evans/cs216/guides/x86.html) and C++ knowledge, understanding of computer architecture, memory structure, OOP and compiler theory would certainly help. ``` -## Development +### Development When you found out how the engine works and where you need to extend the logic you'd need to develop the code to achieve what you want. This is done by declaring a *hook* - some code which would be executed after the program execution reaches the certain address in binary. All the development is done in C++ using [YRpp](https://github.com/Phobos-developers/YRpp) (which provides a way to interact with YR code and inject code using Syringe) and usually [Visual Studio 2017/2019](https://visualstudio.microsoft.com) or newer. -### Quickstart guide for AI-assisted development +#### Quickstart guide for AI-assisted development The repository includes a [Copilot instructions file](https://github.com/Phobos-developers/Phobos/blob/develop/.github/copilot-instructions.md) that serves as a quickstart guide for the project - it covers building, project structure, hook patterns, patching macros, YRpp usage and more. It is automatically picked up by GitHub Copilot and similar AI coding agents, but is also a useful read for any new contributor looking to understand the codebase quickly. @@ -29,7 +28,6 @@ We encourage contributors to try AI coding agents (such as GitHub Copilot in age AI agents are a tool to assist development, but they are not perfect and can make mistakes. Always review and test any code generated or modified by an AI agent to ensure it meets the project's standards and works correctly. **You are responsible for the final code**, not the tool you use to write it. ``` -(contributing-changes-to-the-project)= ## Contributing changes to the project To ensure harmonious coexistence, developers and maintainers should first read our [Project guidelines and policies](Project-guidelines-and-policies.md). @@ -39,10 +37,12 @@ To contribute a feature or some sort of a change you you would need a Git client If you contribute something, please make sure: - you write documentation for the change; - you mention the change in the changelog and migration sections in the [what's new page](Whats-New.md); -- you mention your contribution in the [credits page](CREDITS.md). +- you mention your contribution in the [credits page](/CREDITS). If your change does not fit in standard criteria or too small that it doesn't need the above - add `[Minor]` to your pull request's title, so the CI won't yell at you for no reason. +If the pull request only changes documentation, start its title with `[Docs]` to skip the DLL build. + ```{hint} Every pull request push trigger a nightly build for the latest pushed commit, so you can check the build status at the bottom of PR page, press `Show all checks`, go to details of a build run and get the zip containing built DLL and PDB (for your testers, f. ex.), or download a build from an automatically posted comment. ``` @@ -51,7 +51,7 @@ Every pull request push trigger a nightly build for the latest pushed commit, so You'd benefit from C++ experience, knowledge of programming patterns, common techniques etc. [Basic assembly knowledge](https://www.cs.virginia.edu/~evans/cs216/guides/x86.html) would help to correctly write the interaction with the memory where you hook at. Basic understanding of Git and GitHub is also needed. ``` -## Testing +### Testing This is a job that any modder (and even sometimes player) can do. Look at a new feature or a change, try to think of all possible cases when it can work differently, try to think of any possible logic flaws, edge cases, unforeseen interactions or conditions etc., then test it according to your thoughts. Any bugs should be reported to issues section of this repo, if possible. @@ -59,7 +59,7 @@ This is a job that any modder (and even sometimes player) can do. Look at a new **General stability** can only be achieved by extensive play-testing of new changes, both offline and online. Most modders have beta testing teams, so please, if you want the extension to be stable - contribute to that by having your testers play with the new features! Also the check-list below can help you identify issues quicker. ``` -## Testing check-list +#### Testing check-list - **All possible valid use cases covered**. Try to check all of the valid feature use cases you can think of and verify that they work as intended with the feature. - **Correct saving and loading**. Most of the additions like new INI tags require storing them in saved object info. Sometimes this is not done correctly, especially on complex stuff (like radiation types). Please, ensure all the improvements work __identically__ before and after being saved and loaded (on the same version of Phobos, of course). @@ -72,25 +72,41 @@ This is a job that any modder (and even sometimes player) can do. Look at a new Knowledge on how to mod YR and having an inquisitive mind, being attentive to details would help. ``` -## Writing docs +### Writing docs No explanation needed. If you fully understand how some stuff in Phobos works you can help by writing a detailed description in these docs, or you can just improve the pieces of docs you think are not detailed enough. AI coding agents can also help with writing and improving documentation - see [Quickstart guide for AI-assisted development](#quickstart-guide-for-ai-assisted-development) above. -The docs are written in Markdown (which is dead simple, [learn MD in 60 seconds](https://commonmark.org/help/); if you need help on extended syntax have a look at [MyST parser reference](https://myst-parser.readthedocs.io/)). We use [Sphinx](https://sphinx-doc.org/) to build docs, [Read the Docs](https://readthedocs.io/) to host. +The docs are written in Markdown (which is dead simple, [learn MD in 60 seconds](https://commonmark.org/help/)). We use [VitePress](https://vitepress.dev/) to build the documentation site and [Read the Docs](https://readthedocs.org/) to host it. ```{hint} -You don't need to install Python, Sphinx and modules to see changes - every pull request you make is being built and served by Read the Docs automatically. Just like the nightly builds, scroll to the bottom, press `Show all checks` and see the built documentation in the details of a build run. +You can preview docs locally: + +1. `cd docs` +2. `npm ci` +3. `npm run dev` + +For a production build, run `npm run build`. + +Chinese docs are built from `docs/locale/zh_CN/LC_MESSAGES/*.po` automatically by the VitePress PO locale plugin during `dev` and `build`. +``` + +```{hint} +You don't need to install Node.js, VitePress and npm packages locally just to see documentation changes - every pull request you make is built and served by Read the Docs automatically. Just like the nightly builds, scroll to the bottom of the PR page, press `Show all checks` and open the details of a Read the Docs build run to see the built documentation. ``` There are two ways to edit the docs. -- **Edit from your PC**. Pretty much the same like what's described in [contributing changes section](contributing-changes-to-the-project); the docs are located in the `docs` folder. +- **Edit from your PC**. Pretty much the same like what's described in [contributing changes section](#contributing-changes-to-the-project); the docs are located in the `docs` folder. - **Edit via online editor**. Navigate to the doc piece that you want to edit, press the button on the top right - and it will take you to the file at GitHub which you would need to edit (look for the pencil icon to the top right). Press it - the fork will be created and you'll edit the docs in your version of the repo (fork). You can commit those changes (preferably to a new branch) and make them into a pull request to main repo. +```{note} +`README.md`, `CREDITS.md` and `LICENSE.md` are sourced from the repository root by the VitePress root pages plugin. Edit the files in the repository root instead. +``` + ```{note} OK English grammar and understanding of docs structure would be enough. You would also need a GitHub account. ``` -## Providing media to showcase features +### Providing media to showcase features Those would be used in docs and with a link to the respective mod as a bonus for the mod author. To record GIFs you can use such apps as, for example, [GifCam](http://blog.bahraniapps.com/gifcam/). @@ -98,6 +114,6 @@ Those would be used in docs and with a link to the respective mod as a bonus for Please, provide screenshots, GIFs and videos in their natural size and without excess stuff or length. ``` -## Promoting the work +### Promoting the work You can always help us by spreading the word about the project among people, whether you're an influential youtuber, a C&C related community leader or just an average player. diff --git a/docs/Fixed-or-Improved-Logics.md b/docs/Fixed-or-Improved-Logics.md index 7d743f2a52..8dffd9a0d9 100644 --- a/docs/Fixed-or-Improved-Logics.md +++ b/docs/Fixed-or-Improved-Logics.md @@ -701,7 +701,7 @@ UseWeeds.ReadinessAnimationPercentage=0.9 ; double - when this many weeds ![image](_static/images/VoxelLightSourceComparison1.png) ![image](_static/images/VoxelLightSourceComparison2.png) -*Voxel by C&CrispS* +*Voxel by [C&CrispS](https://bbs.ra2diy.com/home.php?mod=space&uid=20016&do=index)* - It is now possible to change the position of the light relative to the voxels. This allows for better lighting to be set up. - Only the direction of the light is accounted, the distance to the voxel is not accounted. @@ -2052,7 +2052,7 @@ SpawnsTiberium.Particle= ; Particle - In addition, TerrainTypes can now show 'crumbling' animation after their health has reached zero and before they are deleted from the map by setting `HasCrumblingFrames` to true. - Crumbling frames start from first frame after both regular & damaged frames and ends at halfway point of the frames in TerrainType's image. - Sound event from `CrumblingSound` (if set) is played when crumbling animation starts playing. - - [Destroy animation & sound](New-or-Enhanced-Logics.md#destroy-animation--sound) only play after crumbling animation has finished. + - [Destroy animation & sound](New-or-Enhanced-Logics.md#destroy-animation-sound) only play after crumbling animation has finished. In `rulesmd.ini`: ```ini diff --git a/docs/General-Info.md b/docs/General-Info.md index 5b1dc16cde..2351778f9c 100644 --- a/docs/General-Info.md +++ b/docs/General-Info.md @@ -10,7 +10,7 @@ There are three main types of Phobos builds: - *nightly builds* - bleeding edge versions which can include prototypes, proofs of concepts, scrapped features etc., in other words - we can't guarantee anything in those builds and they absolutely should NOT be used in mod releases and should only be used to help with development and testing. ```{hint} -You can find the downloads for these versions on the document's [main page](index.md#downloads). +You can find the downloads for these versions on the document's [main page](/#downloads). ``` ### Disabling development build warning diff --git a/docs/License.md b/docs/License.md deleted file mode 100644 index 2bd0a9f9ce..0000000000 --- a/docs/License.md +++ /dev/null @@ -1,6 +0,0 @@ -# License - -```{include} ../LICENSE.md -:relative-docs: docs/ -:relative-images: -``` diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d4bb2cbb9e..0000000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = . -BUILDDIR = _build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/Miscellanous.md b/docs/Miscellanous.md index cfd87586fc..5d622727c4 100644 --- a/docs/Miscellanous.md +++ b/docs/Miscellanous.md @@ -126,64 +126,9 @@ CampaignDefaultGameSpeed=4 ; integer Currently there is no way to set desired FPS directly. Use the generator below to get required values. The generator supports values from 10 to 60. ``` -```{dropdown} Click to show the generator -Enter desired FPS -

- -
- -Results (remember to replace N with your game speed number!): - -
-

-
-
-``` - - +::: details Click to show the generator + +::: ## INI diff --git a/docs/User-Interface.md b/docs/User-Interface.md index 56b0621e4b..b6c9cdf853 100644 --- a/docs/User-Interface.md +++ b/docs/User-Interface.md @@ -838,7 +838,7 @@ While the feature is usable without any extra graphics, you can find example ass ### Weeds counter -- Counter for amount of [weeds in storage](Fixed-or-Improved-Logics.md#weeds--weed-eaters) can be added near the credits indicator. +- Counter for amount of [weeds in storage](Fixed-or-Improved-Logics.md#weeds-weed-eaters) can be added near the credits indicator. - You can adjust counter position by `Sidebar.WeedsCounter.Offset` (per-side setting), negative means left/up, positive means right/down. - Counter is by default displayed in side's tooltip color, which can be overridden per side by setting `Sidebar.WeedsCounter.Color`. - The feature can be toggled on/off by user if enabled in mod via `ShowWeedsCounter` setting in `RA2MD.INI`. diff --git a/docs/Whats-New.md b/docs/Whats-New.md index e820a418eb..14572f6cab 100644 --- a/docs/Whats-New.md +++ b/docs/Whats-New.md @@ -423,9 +423,9 @@ New: - [Delay automatic attack on the controlled unit](Fixed-or-Improved-Logics.md#delay-automatic-attack-on-the-controlled-unit) (by CrimRecya) - [Parabombs](New-or-Enhanced-Logics.md#parabombs) (by Starkku & TaranDahl) - [Sinkablity and sinking speed customization](Fixed-or-Improved-Logics.md#sinking-behavior-dehardcode) (by TaranDahl) -- [Fast access vehicle](New-or-Enhanced-Logics.md#fast-access-vehicle) (by CrimRecya) +- [Fast access vehicle](New-or-Enhanced-Logics.md#fast-access-vehicle-structure) (by CrimRecya) - Laser, electric bolt and rad beam scatter (by CrimRecya) -- [Airburst weapon firing/source coordinate & firing effects customizations](Fixed-or-Improved-Logics.md#airburst--splits) (by Starkku) +- [Airburst weapon firing/source coordinate & firing effects customizations](Fixed-or-Improved-Logics.md#airburst-splits) (by Starkku) - [AlternateFLH on-turret toggle](Fixed-or-Improved-Logics.md#alternate-flh-customizations) (by Starkku) - [Fire weapon when Warhead kills something](New-or-Enhanced-Logics.md#fire-weapon-when-warhead-kills-something) (by Ollerus) - [Prone speed customization](Fixed-or-Improved-Logics.md#prone-speed-customization) (by TaranDahl) @@ -438,7 +438,7 @@ New: - [Spawned aircraft facing to match turret toggle](New-or-Enhanced-Logics.md#aircraft-spawner-customizations) (by Starkku) - [Removed dependency on `blowfish.dll`](Miscellanous.md#blowfish-dependency) (by ZivDero) - [Warhead that can not kill](New-or-Enhanced-Logics.md#warhead-that-can-not-kill) (by FS-21) -- [Customize parasite culling targets](Fixed-or-Improved-Logics.md#customizing-parasite-culling-targets) (by NetsuNegi) +- [Customize parasite culling targets](Fixed-or-Improved-Logics.md#customizing-parasite) (by NetsuNegi) - [Overload characteristic dehardcoded](New-or-Enhanced-Logics.md#overload-characteristic-dehardcoded) (by Otamaa) - [RadarInvisible for non-enemy house](Fixed-or-Improved-Logics.md#radarinvisible-for-non-enemy-house) (By TaranDahl) - New `Pips.HideIfNoStrength` and `SelfHealing.EnabledBy` additions for shields (by FS-21) @@ -454,7 +454,7 @@ New: - [Tiberium eater logic](New-or-Enhanced-Logics.md#tiberium-eater) (by NetsuNegi) - [Customize the damage taken when falling from a bridge](Fixed-or-Improved-Logics.md#customize-bridge-falling-down-damage) (by FlyStar) - Dehardcoded 255 limit of `OverlayType` (by secsome & ZivDero) -- [Customizable airstrike flare colors](Fixed-or-Improved-Logics.md#airstrike-flare-customizations) (by Starkku) +- [Customizable airstrike flare colors](Fixed-or-Improved-Logics.md#airstrike-flare-visual-customizations) (by Starkku) - Allowed player's self-healing effects to be benefited by allied or `PlayerControl=true` houses (by Ollerus) - [Exclusive SuperWeapon Sidebar](User-Interface.md#superweapon-sidebar) (by NetsuNegi & CrimRecya) - [Customize the scatter caused by aircraft attack mission](Fixed-or-Improved-Logics.md#customize-the-scatter-caused-by-aircraft-attack-mission) (by TaranDahl) @@ -569,7 +569,7 @@ New: - [Technos with Walk locomotor spawn wake like ship](Fixed-or-Improved-Logics.md#customizable-wake-anim) (by TaranDahl) - [Updateable firing anim](Fixed-or-Improved-Logics.md#updateable-firing-anim) (by TaranDahl) - [Hotkey for deselect object from current selection](User-Interface.md#deselect-object-s) (by FrozenFog) -- [Additional customizations for `Splits` concerning target selection](Fixed-or-Improved-Logics.md#airburst--splits) (by Starkku) +- [Additional customizations for `Splits` concerning target selection](Fixed-or-Improved-Logics.md#airburst-splits) (by Starkku) - [Allow replacing vanilla repairing with togglable auto repairing](User-Interface.md#allow-replacing-vanilla-repairing-with-togglable-auto-repairing) (by TaranDahl) - Use `OpenTopped.AllowFiringIfAttackedByLocomotor` to control whether the passengers of a non-building transport unit can fire when the unit is being attacked by a weapon whose warhead has `IsLocomotor=true` (by Noble_Fish) - Framework for dynamic sight (by TaranDahl) diff --git a/docs/_static/css/dark.css b/docs/_static/css/dark.css deleted file mode 100644 index cab4d881ee..0000000000 --- a/docs/_static/css/dark.css +++ /dev/null @@ -1,208 +0,0 @@ -.switcher__status { - background-color: #2b2c36; -} - -.switcher__radio:focus-visible ~ .switcher__status { - outline: #fff solid 2px; -} - -.switcher__radio--light, -.switcher__radio--auto, -.switcher__radio--dark { - filter: invert(1); -} - -.switcher__radio--light:checked, -.switcher__radio--auto:checked, -.switcher__radio--dark:checked { - filter: invert(0); -} - -:root { - color-scheme: dark; -} - -.rst-content table.docutils thead, -.wy-side-nav-search input[type="text"], -.wy-side-nav-search input[type="text"]::placeholder, -.wy-nav-content { - color: #d9d9d9 !important; - background: #2b2c36 !important; -} - -.wy-body-for-nav { - background: #161b22 !important; -} - -.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td, -.rst-versions, -.wy-nav-side { - background-color: #23242b !important; -} - -.rst-content .highlighted { - background: #4020A8; - box-shadow: 0 0 0 2px #4020A8; -} - -.wy-menu-vertical li.current > a button.toctree-expand { - color: #d9d9d9; -} - -.wy-menu-vertical li.current > a, -.wy-menu-vertical a { - color: #d9d9d9; - background-color: #23242b; -} - -.wy-menu-vertical li.current > a:hover { - background-color: #3e3e4a; -} - -.wy-menu-vertical li.toctree-l2 a { - color: #d9d9d9; - background-color: #2f3039; -} - -.wy-menu-vertical li.toctree-l3 a { - color: #d9d9d9; - background-color: #3d3e4a; -} - -.wy-menu-vertical li.toctree-l3.current > a, -.wy-menu-vertical li.toctree-l3.current li.toctree-l4 > a { - background: #565766; -} - -.wy-menu-vertical a:hover, -.wy-menu-vertical li.current a:hover, -.wy-menu-vertical li.toctree-l2.current > a:hover, -.wy-menu-vertical li.toctree-l3.current > a:hover, -.wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a:hover, -.wy-menu-vertical li.toctree-l3.current li.toctree-l4 > a:hover { - background: #5f5f6f !important; -} - -.wy-menu-vertical li.current > a, -.wy-menu-vertical li.on a { - background-color: #3d3e4a; -} - -.wy-menu-vertical li.current > a:hover button.toctree-expand, -.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand { - color: #d9d9d9; -} - -.wy-menu-vertical li.toctree-l2.current > a, -.wy-menu-vertical li.toctree-l2.current > a > button:hover, -.wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a { - background: #3d3e4a; - color: #d9d9d9; -} - -.wy-menu-vertical li.current a { - border: unset; -} - -.admonition, -.admonition-title, -.rst-content img { - border-radius: 6px !important; -} - -.highlight { - border-radius: 6px !important; - color: unset !important; - background-color: #161b22 !important; -} - -.highlight-cpp, -.highlight-ini { - border-style: none !important; -} - -.highlight-ini .highlight pre { - line-height: 1.5 !important; -} - -.highlight-ini .c1, -.s { - color: unset !important; - font-style: inherit !important; -} - -.highlight-ini .na, -.k { - color: rgb(39, 174, 96) !important; - font-weight: unset !important; -} - -.highlight-ini .o { - color: #7b8d80 !important; -} - -.literal { - padding: 0.2em 0.4em !important; - margin: 1px !important; - color: unset !important; - background-color: rgb(240, 240, 250, 0.15) !important; - border-radius: 6px !important; - border-style: none !important; -} - -.admonition .literal { - background-color: rgba(0, 0, 0, 0.25) !important; -} - -.admonition-title { - border-radius: 6px 6px 0 0 !important; - background: rgb(0, 0, 0, 0.25) !important; -} - -.hint { - background: rgb(90, 180, 90, 0.5) !important; -} - -.warning { - background: rgba(190, 90, 90, 0.5) !important; -} - -.note { - background: rgba(140, 140, 140, 0.5) !important; -} - -.btn { - border-radius: 6px !important; - box-shadow: inset 0 -2px 0 0 rgba(0, 0, 0, 0.15) !important; - background-color: rgb(255, 255, 255, 0.15) !important; -} - -a.btn.btn-neutral { - color: #d9d9d9 !important; -} - -.btn:hover { - background-color: #2980b9 !important; -} - -details { - color: #d9d9d9; - background-color: #24252d; - box-shadow: unset; -} - -.sd-summary-title { - background-color: rgb(36, 37, 45); -} - -details.sd-dropdown[open].sd-card { - background-color: rgb(36, 37, 45); -} - -details.sd-dropdown:not([open]) > .sd-card-header { - box-shadow: none; -} - -details.sd-dropdown[open] > .sd-card-header { - box-shadow: none; -} diff --git a/docs/_static/css/index.css b/docs/_static/css/index.css deleted file mode 100644 index 07c2bd8cfc..0000000000 --- a/docs/_static/css/index.css +++ /dev/null @@ -1,265 +0,0 @@ -:focus-visible { - outline: solid 2px; -} - -.wy-nav-top i { - line-height: 54px; - padding-left: 10px; - padding-right: 10px; -} - -.wy-nav-content { - max-width: none; -} - -.rst-content code.literal { - white-space: nowrap; -} - -.rst-content a { - color: #58a6ff; -} - -.rst-content p img ~ em { - display: block; -} - -.rst-content .btn:focus { - outline: unset; -} - -.rst-content .btn:focus-visible { - outline: 2px solid; -} - -.wy-body-for-nav { - background: #1a1a1a; - max-width: 1400px; - margin: auto; -} - -.wy-nav-side { - left: auto; - max-width: 300px; -} - -.wy-side-nav-search input[type="text"] { - border-radius: 6px; - box-shadow: unset; -} - -.wy-side-nav-search input[type="text"]:focus-visible { - outline: solid 2px; -} - -.wy-menu-vertical a:focus-visible { - outline-offset: -3px; -} - -.wy-menu-vertical li.current > a, -.wy-menu-vertical li.on a { - border-right: 1px solid #c9c9c9; -} - -.wy-menu-vertical li.toctree-l2.current > a:hover, -.wy-menu-vertical li.toctree-l2.current li.toctree-l3 > a:hover { - background: #d6d6d6; -} - -.wy-menu-vertical li.toctree-l1.current > a { - border-bottom: unset; - border-top: unset; -} - -.rst-versions { - left: auto; - width: 300px; - max-width: 85%; -} - -.wy-grid-for-nav { - position: unset; -} - -@media screen and (max-width: 768px) { - .wy-nav-side { - left: -300px; - } - - .wy-nav-top { - padding: unset; - width: 100%; - position: fixed; - height: 50px; - } - - .wy-nav-content-wrap.shift { - left: min(300px, 85%); - } - - .rst-content { - padding-top: 50px; - } -} - -.toctree-expand { - tab-in -} - -details { - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 4px; - background-color: #f3f6f6; - color: #404040; - box-shadow: inset 0 1px 2px -1px hsl(0deg 0% 100% / 50%), - inset 0 -2px 0 0 rgb(0 0 0 / 10%); - padding: 12px 12px 0; - margin-bottom: 24px; -} - -summary { - font-style: italic; - cursor: pointer; - margin: -12px -12px 0; - padding: 12px; - - -webkit-user-drag: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - -details[open] { - padding: 12px; -} - -details[open] summary { - border-bottom: 1px solid rgba(0, 0, 0, 0.1); - margin-bottom: 12px; -} - -details > *:last-child { - margin-bottom: 0 !important; -} - -.sd-summary-title { - background-color: rgb(243, 246, 246); -} - -details.sd-dropdown[open].sd-card { - background-color: rgb(243, 246, 246); -} - -details.sd-dropdown:not([open]) > .sd-card-header { - box-shadow: inset 0 1px 2px -1px hsl(0deg 0% 100% / 50%), - inset 0 -2px 0 0 rgb(0 0 0 / 10%); -} - -details.sd-dropdown[open] > .sd-card-header { - box-shadow: none; -} - -details.dropdown-toggle summary, -details > summary { - font-weight: normal !important; -} - -/* =================================== */ -/* Switcher */ - -.switcher { - position: absolute; - margin: 0; - display: grid; - grid-template-columns: 1fr 1fr 1fr; - border: none; - z-index: 200; - outline: unset !important; - - /* margin-left: -50px; */ - padding: 2px; - top: 12px; - left: 12px; -} - -/* Switcher Legend */ - -.switcher__legend { - position: absolute; - opacity: 0; - pointer-events: none; -} - -/* Switcher Radio */ - -.switcher__radio { - -webkit-appearance: none; - appearance: none; - margin: 0; - width: 22px; - height: 22px; - background-position: center; - background-repeat: no-repeat; - background-size: 16px; - transition: filter 0.1s ease-in; - - margin-right: unset !important; -} - -.switcher__radio:focus { - outline: unset !important; -} - -.switcher__radio--light { - background-image: url("../icons/light.svg"); -} - -.switcher__radio--auto { - background-image: url("../icons/auto.svg"); - background-size: 32px; -} - -.switcher__radio--dark { - background-image: url("../icons/dark.svg"); -} - -/* Switcher Status */ - -.switcher__status { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: -1; - border-radius: 18px; - background-color: #fcfcfc; - background-repeat: no-repeat; - background-image: url("../icons/status.svg"); - background-size: 22px; - background-position: center; - transition: background-position 0.1s ease-in; -} - -.switcher__radio:focus-visible ~ .switcher__status { - outline: #343131 solid 2px; -} - -.switcher__radio--light:checked ~ .switcher__status { - background-position: left 2px center; -} - -.switcher__radio--auto:checked ~ .switcher__status { - background-position: center center; -} - -.switcher__radio--dark:checked ~ .switcher__status { - background-position: right 2px center; -} - -.switcher__radio--light:checked, -.switcher__radio--auto:checked, -.switcher__radio--dark:checked { - filter: invert(1); -} diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico new file mode 100644 index 0000000000..3e56c4c165 Binary files /dev/null and b/docs/_static/favicon.ico differ diff --git a/docs/_static/favicon.png b/docs/_static/favicon.png new file mode 100644 index 0000000000..53e874f6c7 Binary files /dev/null and b/docs/_static/favicon.png differ diff --git a/docs/_static/icons/auto.svg b/docs/_static/icons/auto.svg deleted file mode 100644 index c9f84540aa..0000000000 --- a/docs/_static/icons/auto.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docs/_static/icons/dark.svg b/docs/_static/icons/dark.svg deleted file mode 100644 index e544ee5a6d..0000000000 --- a/docs/_static/icons/dark.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/_static/icons/light.svg b/docs/_static/icons/light.svg deleted file mode 100644 index 08b5fe99a7..0000000000 --- a/docs/_static/icons/light.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/docs/_static/icons/status.svg b/docs/_static/icons/status.svg deleted file mode 100644 index 22bcd91fd1..0000000000 --- a/docs/_static/icons/status.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/docs/_static/images/VoxelLightSourceComparison1.png b/docs/_static/images/VoxelLightSourceComparison1.png index e5da80128e..e7e2387170 100644 Binary files a/docs/_static/images/VoxelLightSourceComparison1.png and b/docs/_static/images/VoxelLightSourceComparison1.png differ diff --git a/docs/_static/images/VoxelLightSourceComparison2.png b/docs/_static/images/VoxelLightSourceComparison2.png index db8e817263..ca2ca51ab5 100644 Binary files a/docs/_static/images/VoxelLightSourceComparison2.png and b/docs/_static/images/VoxelLightSourceComparison2.png differ diff --git a/docs/_static/images/digital_display_shapes.png b/docs/_static/images/digital_display_shapes.png index d7f710f00a..46caa56218 100644 Binary files a/docs/_static/images/digital_display_shapes.png and b/docs/_static/images/digital_display_shapes.png differ diff --git a/docs/_static/images/initialstrength.cloning-01.png b/docs/_static/images/initialstrength.cloning-01.png index c70465bc8f..1ac3cdbe0b 100644 Binary files a/docs/_static/images/initialstrength.cloning-01.png and b/docs/_static/images/initialstrength.cloning-01.png differ diff --git a/docs/_static/images/placepreview.png b/docs/_static/images/placepreview.png index b49f856a20..4846394432 100644 Binary files a/docs/_static/images/placepreview.png and b/docs/_static/images/placepreview.png differ diff --git a/docs/_static/images/selectbox.png b/docs/_static/images/selectbox.png index 3e6bb6c114..046bd6f52f 100644 Binary files a/docs/_static/images/selectbox.png and b/docs/_static/images/selectbox.png differ diff --git a/docs/_static/images/sw_sidebar.png b/docs/_static/images/sw_sidebar.png index 44aac3ddbd..5066f85f52 100644 Binary files a/docs/_static/images/sw_sidebar.png and b/docs/_static/images/sw_sidebar.png differ diff --git a/docs/_static/images/translucency-fix.png b/docs/_static/images/translucency-fix.png index a697820bea..3d6aceb756 100644 Binary files a/docs/_static/images/translucency-fix.png and b/docs/_static/images/translucency-fix.png differ diff --git a/docs/_static/js/ini-block-maker.js b/docs/_static/js/ini-block-maker.js deleted file mode 100644 index f0cf9b77e5..0000000000 --- a/docs/_static/js/ini-block-maker.js +++ /dev/null @@ -1,53 +0,0 @@ -// Create an INI code block with given `newId` as a child of `parent` node, with an optional `height` -function makeINICodeBlock(parent, newId, height) { - let div1 = document.createElement("div"); - div1.className = "highlight-ini notranslate"; - if (height != null) { - div1.style.height = `${height}px`; - div1.style.overflow = "auto"; - } - let div2 = document.createElement("div"); - div2.className = "highlight"; - let pre = document.createElement("pre"); - pre.id = newId; - div2.appendChild(pre); - div1.appendChild(div2); - parent.appendChild(div1); -} - -// Add an INI line to a code block node (a direct parent
 node)
-// An INI line consists of a `key`, `value` and `comment`, all can be null
-function addINILine(codeBlockNode, line) {
-	if (line.key != null) {
-		let na = document.createElement("span");
-		na.className = "na";
-		na.textContent = line.key;
-		codeBlockNode.appendChild(na);
-		let o = document.createElement("span");
-		o.className = "o";
-		o.textContent = "=";
-		codeBlockNode.appendChild(o);
-	}
-	if (line.value != null) {
-		let s = document.createElement("span");
-		s.className = "s";
-		s.textContent = line.value;
-		codeBlockNode.appendChild(s);
-	}
-	if (line.comment != null) {
-		if (line.key != null || line.value != null) {
-			let w = document.createElement("span");
-			w.className = "w";
-			w.textContent = ' ';
-			codeBlockNode.appendChild(w);
-		}
-		let c1 = document.createElement("span");
-		c1.className = "c1";
-		c1.textContent = line.comment;
-		codeBlockNode.appendChild(c1);
-	}
-	let w = document.createElement("span");
-	w.className = "w";
-	w.textContent = '\n';
-	codeBlockNode.appendChild(w);
-}
diff --git a/docs/_static/js/scheme-switcher.js b/docs/_static/js/scheme-switcher.js
deleted file mode 100644
index 5f77d7a50d..0000000000
--- a/docs/_static/js/scheme-switcher.js
+++ /dev/null
@@ -1,96 +0,0 @@
-const lightStyles = document.querySelectorAll('link[rel=stylesheet][media*=prefers-color-scheme][media*=light]');
-const darkStyles = document.querySelectorAll('link[rel=stylesheet][media*=prefers-color-scheme][media*=dark]');
-const darkSchemeMedia = matchMedia('(prefers-color-scheme: dark)');
-
-setupScheme();
-
-function createSchemeSwitcher()
-{
-    const div = document.createElement('fieldset');
-    div.className = "switcher";
-    div.innerHTML = `
-    
-    
-    
-    
- `; - const switcher = document.querySelectorAll('.wy-side-nav-search').item(0); - switcher.before(div); - - setupSwitcher(); -} - -function setupSwitcher() { - const switcherRadios = document.querySelectorAll('.switcher__radio'); - const savedScheme = getSavedScheme(); - - if (savedScheme !== null) { - const currentRadio = document.querySelector(`.switcher__radio[value=${savedScheme}]`); - currentRadio.checked = true; - } - - [...switcherRadios].forEach((radio) => { - radio.addEventListener('change', (event) => { - setScheme(event.target.value); - }); - }); -} - -function setupScheme() { - const savedScheme = getSavedScheme(); - const systemScheme = getSystemScheme(); - - if (savedScheme !== systemScheme) { - setScheme(savedScheme); - } -} - -function setScheme(scheme) { - switchMedia(scheme); - - if (scheme === 'auto') { - clearScheme(); - } else { - saveScheme(scheme); - } -} - -function switchMedia(scheme) { - let lightMedia; - let darkMedia; - - if (scheme === 'auto') { - lightMedia = '(prefers-color-scheme: light)'; - darkMedia = '(prefers-color-scheme: dark)'; - } else { - lightMedia = (scheme === 'light') ? 'all' : 'not all'; - darkMedia = (scheme === 'dark') ? 'all' : 'not all'; - } - - [...lightStyles].forEach((link) => { - link.media = lightMedia; - }); - - [...darkStyles].forEach((link) => { - link.media = darkMedia; - }); -} - -function getSystemScheme() { - const darkScheme = darkSchemeMedia.matches; - - return darkScheme ? 'dark' : 'light'; -} - -function getSavedScheme() { - const result = localStorage.getItem('color-scheme'); - return result ? result : 'auto'; -} - -function saveScheme(scheme) { - localStorage.setItem('color-scheme', scheme); -} - -function clearScheme() { - localStorage.removeItem('color-scheme'); -} diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html deleted file mode 100644 index 1904e4beac..0000000000 --- a/docs/_templates/layout.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "!layout.html" %} -{% block extrahead %} - {{ super() }} - - - - - -{% endblock %} - -{% block footer %} - -{% endblock %} diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 253443e356..0000000000 --- a/docs/conf.py +++ /dev/null @@ -1,65 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# This file only contains a selection of the most common options. For a full -# list see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- Project information ----------------------------------------------------- - -project = 'Phobos' -copyright = '2026, The Phobos Contributors' -author = 'The Phobos Contributors' - -# -- General configuration --------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = ['sphinx_rtd_theme', 'myst_parser', 'sphinx.ext.mathjax', 'sphinx_design'] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -locale_dirs = ['locale/'] -gettext_compact = False -gettext_location = False - -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -myst_heading_anchors = 3 - -myst_enable_extensions = [ - "amsmath", - "dollarmath", - "colon_fence", -] - -html_theme_options = { - 'navigation_depth': 4, -} diff --git a/docs/cspell.json b/docs/cspell.json new file mode 100644 index 0000000000..f798cb29eb --- /dev/null +++ b/docs/cspell.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", + "language": "en", + "allowCompoundWords": true, + "useGitignore": true, + "ignorePaths": [ + // + "./package.json" + ], + "words": [ + "Shiki", // + "vitepress" + ] +} diff --git a/docs/eslint.config.ts b/docs/eslint.config.ts new file mode 100644 index 0000000000..ddd83dfcfa --- /dev/null +++ b/docs/eslint.config.ts @@ -0,0 +1,34 @@ +import js from '@eslint/js' +import globals from 'globals' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { + name: 'docs/ignores', + ignores: [ + 'node_modules/**', + '.vitepress/cache/**', + '.vitepress/.temp/**', + '.artifacts/**', + '.cache/**', + '.temp/**', + 'vitepress/generated/**', + 'zh_CN/**', + '**/*.d.ts', + ], + }, + { + name: 'docs/files', + files: ['**/*.{js,mjs,cjs,ts,mts,cts,vue}'], + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.browser, + ...globals.node, + }, + }, + }, + js.configs.recommended, + ...tseslint.configs.recommended, +) diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 6fd2d8e666..0000000000 --- a/docs/index.md +++ /dev/null @@ -1,25 +0,0 @@ -```{toctree} -:hidden: -:caption: Project Info -General Info -What's New -Contributing -Project guidelines and policies -Credits -License -``` - -```{toctree} -:hidden: -:caption: Extension Documentation -New / Enhanced Logics -Fixed / Improved Logics -AI Scripting and Mapping -User Interface -Miscellanous -``` - -```{include} ../README.md -:relative-docs: docs/ -:relative-images: -``` diff --git a/docs/locale/zh_CN/LC_MESSAGES/AI-Scripting-and-Mapping.po b/docs/locale/zh_CN/LC_MESSAGES/AI-Scripting-and-Mapping.po index c08ebea4ae..9dcb5eeef3 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/AI-Scripting-and-Mapping.po +++ b/docs/locale/zh_CN/LC_MESSAGES/AI-Scripting-and-Mapping.po @@ -901,7 +901,7 @@ msgstr "`12000` 无目标情况下等待" msgid "" "When executed before a new Attack ScriptType actions like `Generic Target" -" Type Attack` actions and `AITargetTypes Attack` actions the TeamType " +" Type Attack` actions and `AITargetTypes Attack` actions the TeamType " "will remember that must wait 1 second if no target was selected. The " "second parameter is a positive value that specifies how much retries the " "Attack will do when no target was found before new Attack ScriptType " @@ -1191,8 +1191,8 @@ msgstr "当前值 = 当前值 + 数字" msgid "CurrentValue = CurrentValue - Number" msgstr "当前值 = 当前值 - 数字" -msgid "CurrentValue = CurrentValue * Number" -msgstr "当前值 = 当前值 * 数字" +msgid "CurrentValue = CurrentValue \\* Number" +msgstr "当前值 = 当前值 \\* 数字" msgid "CurrentValue = CurrentValue / Number" msgstr "当前值 = 当前值/数字" @@ -1726,4 +1726,3 @@ msgid "" "Effects from other sources: granted, refreshing when trying to apply the " "same type of attached effect to the techno." msgstr "外源 AE:赋予时、对同一单位赋予同一 AE 使其刷新时。" - diff --git a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po index e883be971f..10ec4856c6 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po +++ b/docs/locale/zh_CN/LC_MESSAGES/CREDITS.po @@ -55,9 +55,6 @@ msgstr "建筑加载物逻辑增强" msgid "Option to hide health bar" msgstr "隐藏血条" -msgid "`Sidebar.GDIPosition`" -msgstr "`Sidebar.GDIPosition`" - msgid "Help with `CellSpread`" msgstr "`CellSpread` 相关的协助" @@ -82,8 +79,8 @@ msgstr "建筑放置预览调整" msgid "Check for Changelog/Documentation/Credits in Pull Requests" msgstr "在拉取请求中检查更新日志/文档/鸣谢" -msgid "Docs dark theme switcher" -msgstr "文档暗色主题切换" +msgid "VitePress documentation migration" +msgstr "VitePress 文档迁移" msgid "" "Fix position and layer of info tip and reveal production cameo on " @@ -150,9 +147,6 @@ msgstr "自定义 EBolt 电弧" msgid "Voxel light source position customization" msgstr "自定义 Voxel 光源" -msgid "`UseFixedVoxelLighting`" -msgstr "`UseFixedVoxelLighting`" - msgid "Warhead activation target health thresholds" msgstr "目标血量作为弹头生效条件" @@ -201,9 +195,6 @@ msgstr "生产进度指示器" msgid "Custom ore gathering anim" msgstr "自定义矿石采集动画" -msgid "`NoManualMove`" -msgstr "`NoManualMove`" - msgid "Weapon target house filtering" msgstr "武器瞄准所属方筛选" @@ -252,9 +243,6 @@ msgstr "解除心控弹头" msgid "Shields logic help" msgstr "护盾逻辑帮助" -msgid "`AnimList.PickRandom`" -msgstr "`AnimList.PickRandom`" - msgid "`MoveToCell` fix" msgstr "`3 - 移动到坐标` 修复" @@ -393,12 +381,6 @@ msgstr "**FS-21**:" msgid "Dump Object Info enhancements" msgstr "输出对象信息增强" -msgid "`Powered.KillSpawns`" -msgstr "`Powered.KillSpawns`" - -msgid "`Spawner.LimitRange`" -msgstr "`Spawner.LimitRange`" - msgid "Majority of ScriptType actions" msgstr "大多数动作脚本行为" @@ -518,9 +500,6 @@ msgstr "护盾修改弹头" msgid "Warhead decloaking toggle" msgstr "弹头解除隐形逻辑" -msgid "`Warp(In/Out)Weapon`" -msgstr "`Warp(In/Out)Weapon`" - msgid "Grinder improvements / additions" msgstr "回收站改进/补充" @@ -560,9 +539,6 @@ msgstr "核弹载体与载荷武器 `Bright` 修复" msgid "Display damage numbers hotkey command" msgstr "显示伤害数字快捷键命令" -msgid "`TransactMoney.Display`" -msgstr "`TransactMoney.Display`" - msgid "Building-provided self-heal customization" msgstr "自定义建筑提供的自愈" @@ -976,9 +952,6 @@ msgstr "无视 `TiltCrashJumpjet` 允许倾斜" msgid "Forbid firing when crashing" msgstr "禁止在坠毁中继续开火" -msgid "`OmniFire.TurnToTarget`" -msgstr "`OmniFire.TurnToTarget`" - msgid "Object Self-destruction logic" msgstr "物体自毁逻辑" @@ -2455,9 +2428,6 @@ msgid "" "an enemy instead of the nearest one when there were no enemies" msgstr "修复了没有敌人的 AI 寻找敌人时会使用数组中第一个所属方而非空间上最近者的问题" -msgid "`AllowBerzerkOnAllies`" -msgstr "`AllowBerzerkOnAllies`" - msgid "" "Fix an issue that retaliation will make the unit keep switching among " "multiple targets with the same amount of threat" @@ -2754,4 +2724,3 @@ msgid "" "Fix for units with Fly, Jumpjet or Rocket locomotors crashing off-map not" " being cleaned up" msgstr "修复了 `Locomotor` 为 `Fly`、`Jumpjet` 或 `Rocket` 的单位在地图外坠毁时未能清除的问题" - diff --git a/docs/locale/zh_CN/LC_MESSAGES/Contributing.po b/docs/locale/zh_CN/LC_MESSAGES/Contributing.po index ff74924b06..dd7f4a0144 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Contributing.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Contributing.po @@ -148,8 +148,8 @@ msgid "" "[what's new page](Whats-New.md);" msgstr "你在 [版本更新说明](Whats-New.md) 页的更新日志和迁移指南中提及了这些改动;" -msgid "you mention your contribution in the [credits page](CREDITS.md)." -msgstr "你在 [鸣谢](CREDITS.md) 页面列出了你的贡献。" +msgid "you mention your contribution in the [credits page](/CREDITS)." +msgstr "你在 [鸣谢](/zh_CN/CREDITS) 页面列出了你的贡献。" msgid "" "If your change does not fit in standard criteria or too small that it " @@ -157,6 +157,11 @@ msgid "" "the CI won't yell at you for no reason." msgstr "若变更不符合标准流程或规模过小无需上述步骤,请在拉取请求标题添加 `[Minor]`,以避免 CI 无故报错。" +msgid "" +"If the pull request only changes documentation, start its title with " +"`[Docs]` to skip the DLL build." +msgstr "如果拉取请求仅更改文档,请以 `[Docs]` 开头命名标题以跳过 DLL 构建。" + msgid "" "Every pull request push trigger a nightly build for the latest pushed " "commit, so you can check the build status at the bottom of PR page, press" @@ -280,36 +285,24 @@ msgstr "" msgid "" "The docs are written in Markdown (which is dead simple, [learn MD in 60 " -"seconds](https://commonmark.org/help/); if you need help on extended " -"syntax have a look at [MyST parser reference](https://myst-" -"parser.readthedocs.io/)). We use [Sphinx](https://sphinx-doc.org/) to " -"build docs, [Read the Docs](https://readthedocs.io/) to host." -msgstr "" -"这些文档使用 Markdown 编写(语法非常简单,[60秒即可掌握 " -"MD](https://commonmark.org/help/);如果你扩展语法相关的帮助请参考 [MyST 解析器](https" -"://myst-parser.readthedocs.io/))。我们使用 [Sphinx](https://sphinx-doc.org/) " -"构建文档,[Read the Docs](https://readthedocs.io/) 进行托管。" - -msgid "" -"You don't need to install Python, Sphinx and modules to see changes - " -"every pull request you make is being built and served by Read the Docs " -"automatically. Just like the nightly builds, scroll to the bottom, press " -"`Show all checks` and see the built documentation in the details of a " -"build run." +"seconds](https://commonmark.org/help/)). We use " +"[VitePress](https://vitepress.dev/) to build the documentation site and " +"[Read the Docs](https://readthedocs.org/) to host it." msgstr "" -"你无需安装 Python 和 Sphinx 模块即可查看更改——你创建的每个拉取请求均会触发 Read The Docs 的自动生成,类似 " -"Phobos 自动构建版本,滚动至页面底部点击 `Show all checks` 即可在构建详情中查看渲染后的文档。" +"这些文档使用 Markdown 编写(语法非常简单,[60秒即可掌握 MD](https://commonmark.org/help/))。我们使用 " +"[VitePress](https://vitepress.dev/) 构建文档站点,并使用 [Read the " +"Docs](https://readthedocs.org/) 托管。" msgid "There are two ways to edit the docs." msgstr "有两种方式可以编辑文档:" msgid "" "**Edit from your PC**. Pretty much the same like what's described in " -"[contributing changes section](contributing-changes-to-the-project); the " +"[contributing changes section](#contributing-changes-to-the-project); the " "docs are located in the `docs` folder." msgstr "" -"**本地编辑**:如 [向项目提交更改](contributing-changes-to-the-project) 中所述,文档位于 `docs`" -" 文件夹。" +"**本地编辑**:如 [向项目提交更改](#contributing-changes-to-the-project) 中所述,文档位于 `docs` " +"文件夹。" msgid "" "**Edit via online editor**. Navigate to the doc piece that you want to " @@ -323,6 +316,14 @@ msgstr "" "**在线编辑**:找到要编辑的文档,点击右上角按钮——跳转至 GitHub 文件编辑页(在右上角寻找铅笔图标)。编辑后将自动创建 fork " "仓库,你可以提交更改(建议新建分支)并向主仓库发起拉取请求。" +msgid "" +"`README.md`, `CREDITS.md` and `LICENSE.md` are sourced from the repository " +"root by the VitePress root pages plugin. Edit the files in the repository " +"root instead." +msgstr "" +"`README.md`、`CREDITS.md` 和 `LICENSE.md` 会由 VitePress root pages " +"插件从仓库根目录读取。请改为编辑仓库根目录中的文件。" + msgid "" "OK English grammar and understanding of docs structure would be enough. " "You would also need a GitHub account." @@ -337,12 +338,12 @@ msgid "" "example, [GifCam](http://blog.bahraniapps.com/gifcam/)." msgstr "" "这些将在文档中被使用,并附有对应 mod 的链接作为对 mod 作者的鼓励。录制 GIF 推荐使用 " -"[GifCam](http://blog.bahraniapps.com/gifcam/) 等工具" +"[GifCam](http://blog.bahraniapps.com/gifcam/) 等工具。" msgid "" -"Please, provide screenshots, GIFs and videos in their natural size and " -"without excess stuff or length." -msgstr "请提供适当尺寸的截图/GIF/视频,避免冗余内容与过长的时长。" +"Please, provide screenshots, GIFs and videos in their natural size and without " +"excess stuff or length." +msgstr "请提供适当尺寸的截图、GIF 动图和视频,避免冗余内容与过长的时长。" msgid "Promoting the work" msgstr "促进工作" @@ -353,3 +354,26 @@ msgid "" "leader or just an average player." msgstr "无论你是知名 Youtuber、C&C 社区领袖还是普通玩家都可以通过向其他人宣传本项目来帮助我们。" +msgid "You can preview docs locally:" +msgstr "你可以在本地预览文档:" + +msgid "For a production build, run `npm run build`." +msgstr "生产构建请运行 `npm run build`。" + +msgid "" +"Chinese docs are built from `docs/locale/zh_CN/LC_MESSAGES/*.po` " +"automatically by the VitePress PO locale plugin during `dev` and `build`." +msgstr "" +"中文文档会在 `dev` 和 `build` 期间由 VitePress PO locale 插件自动从 " +"`docs/locale/zh_CN/LC_MESSAGES/*.po` 构建。" + +msgid "" +"You don't need to install Node.js, VitePress and npm packages locally just " +"to see documentation changes - every pull request you make is built and " +"served by Read the Docs automatically. Just like the nightly builds, scroll " +"to the bottom of the PR page, press `Show all checks` and open the details " +"of a Read the Docs build run to see the built documentation." +msgstr "" +"如果只是想查看文档更改效果,你不需要在本地安装 Node.js、VitePress 和 npm " +"包——你提交的每个拉取请求都会由 Read the Docs 自动构建并托管。和 nightly " +"builds 一样,滚动到拉取请求页面底部,点击 `Show all checks`,然后打开 Read the Docs 构建运行详情即可查看构建后的文档。" diff --git a/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po b/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po index f721dffa13..0aa12a0eb7 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Fixed-or-Improved-Logics.po @@ -142,8 +142,8 @@ msgid "" "![image](_static/images/jumpjet-turning.gif) *Jumpjet turning to target " "applied in Robot Storm X*" msgstr "" -"![image](_static/images/jumpjet-turning.gif) *Robot Storm X 中的 Jumpjet " -"转向目标应用实例*" +"![image](_static/images/jumpjet-turning.gif) " +"*Robot Storm X 中的 Jumpjet 转向目标应用实例*" msgid "image" msgstr "图像" @@ -227,8 +227,9 @@ msgid "" " keeping target on attack order in [C&C: " "Reloaded](https://www.moddb.com/mods/cncreloaded/)*" msgstr "" -"![image](_static/images/remember-target-after-deploying-01.gif) *在 [C&C: " -"Reloaded](https://www.moddb.com/mods/cncreloaded/) 中 Nod 吊炮保持了攻击指令的目标*" +"![image](_static/images/remember-target-after-deploying-01.gif) " +"*在 [C&C: Reloaded](https://www.moddb.com/mods/cncreloaded/) 中 Nod " +"吊炮保持了攻击指令的目标*" msgid "" "Vehicle to building deployers now keep their target when deploying with " @@ -2192,7 +2193,7 @@ msgstr "范围指示环可见性" msgid "" "In vanilla game, a structure's radial indicator can be drawn only when it" -" belongs to the player. Now it can also be visible to observer. On top of" +" belongs to the player. Now it can also be visible to observer.\nOn top of" " that, you can specify its visibility from other houses." msgstr "在原本的游戏中,建筑的范围指示环只有在玩家拥有时才绘制。现在它也对观察者可见。此外,你还可以指定其他所属方的可见性。" @@ -2324,10 +2325,10 @@ msgstr "" "决定了泰伯利亚藤蔓的生命值。" msgid "" -"Everything listed below functions identically to Tiberian Sun. Many of " +"Everything listed below functions identically to Tiberian Sun.\nMany of " "the tags from Tiberian Sun have been re-enabled. The values provided " "below are identical to those found in TS and YR rules. You can read more " -"about them on ModENC: " +"about them on ModENC:\n" "[VeinholeGrowthRate](https://modenc.renegadeprojects.com/VeinholeGrowthRate)," " " "[VeinholeShrinkRate](https://modenc.renegadeprojects.com/VeinholeShrinkRate)," @@ -2335,7 +2336,7 @@ msgid "" "[MaxVeinholeGrowth](https://modenc.renegadeprojects.com/MaxVeinholeGrowth)," " [VeinDamage](https://modenc.renegadeprojects.com/VeinDamage), " "[VeinholeTypeClass](https://modenc.renegadeprojects.com/VeinholeTypeClass)," -" [VeinholeWarhead](https://modenc.renegadeprojects.com/VeinholeWarhead), " +"\n[VeinholeWarhead](https://modenc.renegadeprojects.com/VeinholeWarhead), " "[Veinhole](https://modenc.renegadeprojects.com/Veinhole), " "[VeinAttack](https://modenc.renegadeprojects.com/VeinAttack), " "[ImmuneToVeins](https://modenc.renegadeprojects.com/ImmuneToVeins), " @@ -2399,16 +2400,12 @@ msgstr "自定义 Voxel 光源" msgid "" "![image](_static/images/VoxelLightSourceComparison1.png) " -"![image](_static/images/VoxelLightSourceComparison2.png) *Voxel by C&CrispS*" +"![image](_static/images/VoxelLightSourceComparison2.png) *Voxel by " +"[C&CrispS](https://bbs.ra2diy.com/home.php?mod=space&uid=20016&do=index)*" msgstr "" "![image](_static/images/VoxelLightSourceComparison1.png) " -"![image](_static/images/VoxelLightSourceComparison2.png) *Voxel by C&CrispS(另存为并解压第二张图片即可获得包括 pal & vpl 的示例文件)*" +"![image](_static/images/VoxelLightSourceComparison2.png) " +"*Voxel by [C&CrispS](https://bbs.ra2diy.com/home.php?mod=space&uid=20016&do=index)(另存为并解压第二张图片即可获得包括 pal & vpl 的示例文件)*" msgid "" "It is now possible to change the position of the light relative to the " @@ -3066,8 +3063,8 @@ msgid "" " vehicles only and refund display ([Project " "Phantom](https://www.moddb.com/mods/project-phantom))*" msgstr "" -"![image](_static/images/grinding.gif) *[幽灵计划](https://www.moddb.com/mods" -"/project-phantom) 中使用友军的部队回收站、仅限载具以及显示资金*" +"![image](_static/images/grinding.gif) " +"*[幽灵计划](https://www.moddb.com/mods/project-phantom) 中使用友军的部队回收站、仅限载具以及显示资金*" msgid "" "You can now customize which types of objects a building with `Grinding` " @@ -3539,8 +3536,8 @@ msgid "" "& buildings in [Project Phantom](https://www.moddb.com/mods/project-" "phantom)*" msgstr "" -"![image](_static/images/shrapnel.gif) *[幽灵计划](https://www.moddb.com/mods" -"/project-phantom) 中击中地面和建筑的溅射*" +"![image](_static/images/shrapnel.gif) " +"*[幽灵计划](https://www.moddb.com/mods/project-phantom) 中击中地面和建筑的溅射*" msgid "" "`ShrapnelWeapon` can now be triggered against ground & buildings via " @@ -3772,8 +3769,8 @@ msgid "" "![image](_static/images/oregath.gif) *Custom ore gathering anims in " "[Project Phantom](https://www.moddb.com/mods/project-phantom)*" msgstr "" -"![image](_static/images/oregath.gif) *在 [幽灵计划](https://www.moddb.com/mods" -"/project-phantom) 中的自定义矿石采集动画*" +"![image](_static/images/oregath.gif) " +"*在 [幽灵计划](https://www.moddb.com/mods/project-phantom) 中的自定义矿石采集动画*" msgid "" "You can now specify which anim should be drawn when a harvester of " @@ -3806,7 +3803,7 @@ msgid "" "if they are within weapon range (`TargetZoneScanType=inrange`)." msgstr "" "默认情况下,任何非战机类类型单位通过动作脚本 `0 攻击目标类型` 或任何 [Phobos 引入的小队攻击任务](AI-Scripting-" -"and-Mapping.md#10000-10049-attack-actions) " +"and-Mapping.md#attack-actions) " "寻找目标时都会检查潜在目标与攻击单位是否处于同一区域,以便将其选为攻击目标。现在这可以被自定义为允许选择来自任何地图区域的对象且不受限制 " "(`TargetZoneScanType=any`) 或者仅当目标在武器射程内时才允许选择 " "(`TargetZoneScanType=inrange`)。" @@ -3819,9 +3816,9 @@ msgid "" "using different teleportation settings in [YR: New " "War](https://www.moddb.com/mods/yuris-revenge-new-war)*" msgstr "" -"![image](_static/images/cust-Chrono.gif) *[YR: New " -"War](https://www.moddb.com/mods/yuris-revenge-new-war) 中的超时空军团兵与 Ronco " -"使用不同的超时空设置*" +"![image](_static/images/cust-Chrono.gif) " +"*[YR: New War](https://www.moddb.com/mods/yuris-revenge-new-war) 中的超时空军团兵与 " +"Ronco 使用不同的超时空设置*" msgid "" "You can now specify Teleport/Chrono Locomotor settings per TechnoType to " @@ -4522,11 +4519,11 @@ msgid "Animated TerrainTypes" msgstr "动画化地形对象" msgid "" -"![Waving trees](_static/images/tree-shake.gif) *Animated trees used in " -"[Ion Shock](https://www.moddb.com/mods/tiberian-war-ionshock)*" +"![Waving trees](_static/images/tree-shake.gif) *Animated trees used in [Ion " +"Shock](https://www.moddb.com/mods/tiberian-war-ionshock)*" msgstr "" -"![Waving trees](_static/images/tree-shake.gif) *[Ion " -"Shock](https://www.moddb.com/mods/tiberian-war-ionshock) 中会动的树*" +"![Waving trees](_static/images/tree-shake.gif) " +"*[Ion Shock](https://www.moddb.com/mods/tiberian-war-ionshock) 中会动的树*" msgid "Waving trees" msgstr "会动的树" @@ -4627,9 +4624,10 @@ msgid "" msgstr "当倒坍动画开始播放时将会播放 `CrumblingSound`(如果设置)指定的音效。" msgid "" -"[Destroy animation & sound](New-or-Enhanced-Logics.md#destroy-animation--" -"sound) only play after crumbling animation has finished." -msgstr "[摧毁动画和音效](New-or-Enhanced-Logics.md#destroy-animation--sound) 仅在倒坍动画结束后播放。" +"[Destroy animation & " +"sound](New-or-Enhanced-Logics.md#destroy-animation-sound) only play after " +"crumbling animation has finished." +msgstr "[摧毁动画和音效](New-or-Enhanced-Logics.md#destroy-animation-sound) 仅在倒坍动画结束后播放。" msgid "" "The number of regular & damage frames considered for this depends on " @@ -5004,7 +5002,9 @@ msgid "Preserve Iron Curtain / Force Shield status on type conversion" msgstr "在单位转换时保留铁幕/力场护盾状态" msgid "![image](_static/images/preserve-ic.gif) *Bugfix in action*" -msgstr "![image](_static/images/preserve-ic.gif) *Bug 修复后的行为*" +msgstr "" +"![image](_static/images/preserve-ic.gif) " +"*Bug 修复后的行为*" msgid "" "Iron Curtain status is now preserved by default when converting between " @@ -5563,8 +5563,8 @@ msgid "" "different Tesla bolt weapon usage ([RA2: " "Reboot](https://www.moddb.com/mods/reboot))*" msgstr "" -"![image](_static/images/ebolt.gif) *[RA2: " -"Reboot](https://www.moddb.com/mods/reboot) 中自定义用于不同磁爆电流武器的 EBolt 效果*" +"![image](_static/images/ebolt.gif) " +"*[RA2: Reboot](https://www.moddb.com/mods/reboot) 中自定义用于不同磁爆电流武器的 EBolt 效果*" msgid "" "You can now specify individual bolts you want to disable for " @@ -5651,9 +5651,9 @@ msgid "" "`IsSingleColor=yes` lasers with higher thickness to regular ones ([RA2: " "Reboot](https://www.moddb.com/mods/reboot))*" msgstr "" -"![image](_static/images/issinglecolor.gif) *[RA2: " -"Reboot](https://www.moddb.com/mods/reboot) 中宽度更大的 `IsSingleColor=yes` " -"激光与常规激光的比较*" +"![image](_static/images/issinglecolor.gif) " +"*[RA2: Reboot](https://www.moddb.com/mods/reboot) 中宽度更大的 " +"`IsSingleColor=yes` 激光与常规激光的比较*" msgid "" "You can now set laser to draw using only `LaserInnerColor` by setting " @@ -5678,4 +5678,3 @@ msgid "" "Next`. `Next` modifies the Anim type over time, while this function " "changes it back, resulting in the Anim being unable to end." msgstr "该效果与 `[Animation] -> Next` 共用会导致问题。`Next` 会随时间更改动画类型,而本功能会把它改回去,导致动画无法结束。" - diff --git a/docs/locale/zh_CN/LC_MESSAGES/General-Info.po b/docs/locale/zh_CN/LC_MESSAGES/General-Info.po index 422c786fc3..4e8190166d 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/General-Info.po +++ b/docs/locale/zh_CN/LC_MESSAGES/General-Info.po @@ -57,8 +57,8 @@ msgstr "" msgid "" "You can find the downloads for these versions on the document's [main " -"page](index.md#downloads)." -msgstr "你可以在文档 [主页面](index.md#downloads) 找到这些版本的下载。" +"page](/#downloads)." +msgstr "你可以在文档 [主页面](/zh_CN/#downloads) 找到这些版本的下载。" msgid "Disabling development build warning" msgstr "关闭开发版本警告" @@ -120,4 +120,3 @@ msgid "" "developers from international community. We welcome any help on the " "matter though!" msgstr "尽管我们也希望支持 HAres,但由于其用户群体及开发者与国际社区分离,所以我们无法保证兼容性。但我们欢迎就此问题提供任何帮助!" - diff --git a/docs/locale/zh_CN/LC_MESSAGES/Miscellanous.po b/docs/locale/zh_CN/LC_MESSAGES/Miscellanous.po index 500c687722..43a8904eb2 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Miscellanous.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Miscellanous.po @@ -300,12 +300,6 @@ msgstr "目前无法直接设置所需的 FPS。需要使用下面的生成器 msgid "Click to show the generator" msgstr "点击显示生成器" -msgid "Enter desired FPS" -msgstr "输入所需的 FPS" - -msgid "Results (remember to replace N with your game speed number!):" -msgstr "结果(别忘了把 N 替换成你的游戏速度编号):" - msgid "INI" msgstr "INI" @@ -470,4 +464,3 @@ msgstr "" "例如:由于技术不兼容,启用此功能会禁用 [Ares 的 “可定制的玩家颜色” 功能](https://ares-" "developers.github.io/Ares-docs/ui-" "features/customizabledropdowncolors.html)。" - diff --git a/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po b/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po index 5880ce9677..3eb9ddb935 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po +++ b/docs/locale/zh_CN/LC_MESSAGES/New-or-Enhanced-Logics.po @@ -703,8 +703,8 @@ msgid "Laser Trails" msgstr "激光尾焰" msgid "" -"![Laser Trails](_static/images/lasertrails.gif) *Laser trails used in " -"[Rise of the East](https://www.moddb.com/mods/riseoftheeast)*" +"![Laser Trails](_static/images/lasertrails.gif) *Laser trails used in [Rise of the " +"East](https://www.moddb.com/mods/riseoftheeast)*" msgstr "" "![Laser Trails](_static/images/lasertrails.gif) " "*[东方崛起](https://www.moddb.com/mods/riseoftheeast) 中的激光尾焰应用实例*" @@ -2022,11 +2022,11 @@ msgid "Straight trajectory" msgstr "直线弹道" msgid "" -"![Straigh trajectory blasters](_static/images/straight.gif) *Straight " -"trajectory used to make blasters in a private mod by @brsajo#9745*" +"![Straigh trajectory blasters](_static/images/straight.gif) *Straight trajectory used to make " +"blasters in a private mod by @brsajo#9745*" msgstr "" -"![Straigh trajectory blasters](_static/images/straight.gif) *@brsajo#9745" -" 在其个人 mod 中使用直线弹道制作的爆能枪*" +"![Straigh trajectory blasters](_static/images/straight.gif) " +"*@brsajo#9745 在其个人 mod 中使用直线弹道制作的爆能枪*" msgid "Straigh trajectory blasters" msgstr "直线弹道爆能枪" @@ -3144,8 +3144,8 @@ msgid "" "system in [Cylearun](https://www.moddb.com/mods/Cylearun)*" msgstr "" "![image](_static/images/swnext.gif) " -"*[虚幻](https://www.moddb.com/mods/Cylearun) 中通过 `SW.Next` 将多个 ChronoSphere" -" 与 ChronoWarp 类型的超武组成链式超武系统*" +"*[虚幻](https://www.moddb.com/mods/Cylearun) 中通过 `SW.Next` 将多个 ChronoSphere 与 " +"ChronoWarp 类型的超武组成链式超武系统*" msgid "" "Superweapons can now launch other superweapons at the same target. " @@ -3664,15 +3664,15 @@ msgid "" msgstr "`BuildLimitGroup.ExtraLimit.Types` 决定了将用于额外值计算的科技类型。" msgid "" -"`BuildLimitGroup.ExtraLimit.Nums` determines the actual value of " -"increment. Value matching the position in " -"`BuildLimitGroup.ExtraLimit.Types` is used for that type. For each of " -"these technos, it'll increase the extra value by its amount * " -"corresponding value from the list." +"`BuildLimitGroup.ExtraLimit.Nums` determines the actual value of" +" increment. Value matching the position in" +" `BuildLimitGroup.ExtraLimit.Types` is used for that type. For each of" +" these technos, it'll increase the extra value by its amount" +" \\* corresponding value from the list." msgstr "" -"`BuildLimitGroup.ExtraLimit.Nums` 决定了其额外值的增量。与 " -"`BuildLimitGroup.ExtraLimit.Types` " -"中对应位置的科技类型一一对应。每个列表中的科技类型会使建造限制增加其场上存在的数量 * 该列表中对应值。" +"`BuildLimitGroup.ExtraLimit.Nums` 决定了其额外值的增量。与" +" `BuildLimitGroup.ExtraLimit.Types`" +" 中对应位置的科技类型一一对应。每个列表中的科技类型会使建造限制增加其场上存在的数量 \\* 该列表中对应值。" msgid "" "`BuildLimitGroup.ExtraLimit.MaxCount` determines the maximum amount of " @@ -3953,7 +3953,7 @@ msgstr "`OpenTransportWeapon=1` 的单位在一个 `OpenTopped=true` 的运输 msgid "" "`NoAmmoWeapon=1` on an unit with `Ammo` value higher than 0 and current " -"ammo count lower or equal to `NoAmmoAmount`." +"ammo count lower or equal to `NoAmmoAmount`." msgstr "`NoAmmoWeapon=1` 的单位在 `Ammo` 大于 0 且当前弹药数量小于等于 `NoAmmoAmount`。" msgid "" @@ -4186,8 +4186,8 @@ msgid "" "target behavior with `ForceWeapon.Naval.Decloaked` in [C&C: " "Reloaded](https://www.moddb.com/mods/cncreloaded)*" msgstr "" -"![image](_static/images/underwater-new-attack-tag.gif) *[C&C: " -"Reloaded](https://www.moddb.com/mods/cncreloaded) 中使用 " +"![image](_static/images/underwater-new-attack-tag.gif) " +"*[C&C: Reloaded](https://www.moddb.com/mods/cncreloaded) 中使用 " "`ForceWeapon.Naval.Decloaked` 的海军对水下目标行为*" msgid "" @@ -4195,8 +4195,8 @@ msgid "" "targets with `ForceWeapon.UnderEMP` in [C&C: " "Reloaded](https://www.moddb.com/mods/cncreloaded)*" msgstr "" -"![image](_static/images/forceweapon_emp.gif) *[C&C: " -"Reloaded](https://www.moddb.com/mods/cncreloaded) 中敌人使用 " +"![image](_static/images/forceweapon_emp.gif) " +"*[C&C: Reloaded](https://www.moddb.com/mods/cncreloaded) 中敌人使用 " "`ForceWeapon.UnderEMP` 攻击 EMP 状态目标的行为*" msgid "" @@ -4473,15 +4473,14 @@ msgid "Mind Control enhancement" msgstr "心灵控制增强" msgid "" -"![image](_static/images/mindcontrol-max-range-01.gif) *Mind Control Range" -" Limit used in [Fantasy ADVENTURE](https://www.moddb.com/mods/fantasy-" -"adventure)* ![image](_static/images/mindcontrol-multiple-01.gif) " -"*Multiple Mind Control unit auto-releases the first victim in [Fantasy " +"![image](_static/images/mindcontrol-max-range-01.gif) *Mind Control " +"Range Limit used in [Fantasy " +"ADVENTURE](https://www.moddb.com/mods/fantasy-adventure)* ![image](_static/images/mindcontrol-multiple-01.gif) *Multiple Mind Control " +"unit auto-releases the first victim in [Fantasy " "ADVENTURE](https://www.moddb.com/mods/fantasy-adventure)*" msgstr "" "![image](_static/images/mindcontrol-max-range-01.gif) " -"*[幻想奇遇](https://www.moddb.com/mods/fantasy-adventure) 中的心灵控制范围限制* " -"![image](_static/images/mindcontrol-multiple-01.gif) " +"*[幻想奇遇](https://www.moddb.com/mods/fantasy-adventure) 中的心灵控制范围限制* ![image](_static/images/mindcontrol-multiple-01.gif) " "*[幻想奇遇](https://www.moddb.com/mods/fantasy-adventure) 中的多重心控单位自动释放第一个受害者*" msgid "" @@ -4823,11 +4822,11 @@ msgid "Revenge weapon" msgstr "复仇武器" msgid "" -"![Revenge Weapon](_static/images/revengeweapon.gif) *Revenge Weapon usage" -" in [RA2: Reboot](https://www.moddb.com/mods/reboot)*" +"![Revenge Weapon](_static/images/revengeweapon.gif) *Revenge Weapon usage in [RA2: " +"Reboot](https://www.moddb.com/mods/reboot)*" msgstr "" -"![image](_static/images/revengeweapon.gif) *[RA2: " -"Reboot](https://www.moddb.com/mods/reboot) 中的复仇武器*" +"![复仇武器](_static/images/revengeweapon.gif) " +"*[RA2: Reboot](https://www.moddb.com/mods/reboot) 中的复仇武器*" msgid "Revenge Weapon" msgstr "复仇武器" @@ -4868,11 +4867,11 @@ msgid "Shared Ammo" msgstr "共享弹药" msgid "" -"Transports with `OpenTopped=yes` and `Ammo.Shared=yes` will transfer ammo" -" to passengers that have `Ammo.Shared=yes`. In addition, a transport can " -"filter who will receive ammo if passengers have the same value in " -"`Ammo.Shared.Group=` of the transport, ignoring other passengers" -" with different groups values." +"Transports with `OpenTopped=yes` and `Ammo.Shared=yes` will transfer ammo " +"to passengers that have `Ammo.Shared=yes`.\n" +" In addition, a transport can filter who will receive ammo if passengers " +"have the same value in `Ammo.Shared.Group=` of the transport, " +"ignoring other passengers with different groups values." msgstr "" "拥有 `OpenTopped=yes` 和 `Ammo.Shared=yes` 的运输工具将像拥有 `Ammo.Shared=yes` " "的载员共享弹药。此外如果载员拥有与运输工具相同的 `Ammo.Shared.Group=` " @@ -5130,8 +5129,7 @@ msgid "" " - Conquer](https://www.moddb.com/mods/project-rush-conquer)*" msgstr "" "![image](_static/images/jumpjet-tilt.gif) " -"*[冲击计划:征服](https://www.moddb.com/mods/project-rush-conquer) 中的 Jumpjet " -"前倾*" +"*[冲击计划:征服](https://www.moddb.com/mods/project-rush-conquer) 中的 Jumpjet 前倾*" msgid "" "Now you can make jumpjets tilt forward when moving forward and sideways " @@ -5236,8 +5234,8 @@ msgid "" "![image](_static/images/remove-mc.gif) *Mind control break warhead being " "utilized in [RA2: Reboot](https://www.moddb.com/mods/reboot)*" msgstr "" -"![image](_static/images/remove-mc.gif) *[RA2: " -"Reboot](https://www.moddb.com/mods/reboot) 中的心控解除弹头*" +"![image](_static/images/remove-mc.gif) " +"*[RA2: Reboot](https://www.moddb.com/mods/reboot) 中的心控解除弹头*" msgid "" "Warheads can now break mind control (doesn't apply to perma-MC-ed " @@ -5413,8 +5411,8 @@ msgid "" "[NanoStorm](https://www.bilibili.com/opus/896077937747427433)*" msgstr "" "![image](_static/images/convertwh.gif) " -"*[NanodaSupercalifragilisticexpialidocious](https://www.bilibili.com/opus/896077937747427433)" -" 中的载具版基因突变*" +"*[NanodaSupercalifragilisticexpialidocious](https://www.bilibili.com/opus/89" +"6077937747427433) 中的载具版基因突变*" msgid "" "In example, this warhead would convert all affected owned and friendly " @@ -6109,8 +6107,8 @@ msgid "Unlimbo detonate warhead" msgstr "去虚拟化弹头" msgid "" -"![Unlimbo Detonate](_static/images/unlimbodetonate.gif) *Unlimbo Detonate" -" used in **The Call of the Panic Spear** by @[Octagonal " +"![Unlimbo Detonate](_static/images/unlimbodetonate.gif) *Unlimbo Detonate used in **The " +"Call of the Panic Spear** by @[Octagonal " "prism](https://space.bilibili.com/360577336)*" msgstr "" "![去虚拟化弹头](_static/images/unlimbodetonate.gif) " @@ -6484,8 +6482,7 @@ msgid "" "Phantom](https://www.moddb.com/mods/project-phantom)*" msgstr "" "![image](_static/images/feedbackweapon.gif) " -"*[幽灵计划](https://www.moddb.com/mods/project-phantom) " -"中使用反馈武器在发射武器时对目标施加治疗光环*" +"*[幽灵计划](https://www.moddb.com/mods/project-phantom) 中使用反馈武器在发射武器时对目标施加治疗光环*" msgid "" "You can now specify an auxiliary weapon to be fired on the firer itself " @@ -6643,18 +6640,18 @@ msgstr "" "设定的次数一致。也就是扫射不再会由于目标被摧毁而中断。" msgid "" -"`Strafing.EndDelay` can be used to override the delay after firing last " -"shot in strafing run before aircraft resumes another strafing run or " -"returns to base. Defaults to (Weapon `Range` * 256 + 1024) / Aircraft " -"`Speed`. Note that using a short delay with aircraft that can do multiple" -" strafing runs with their ammo can cause undesired behaviour like dancing" -" around or facing weird way depending on other factors like ROF and/or " -"movement speed." +"`Strafing.EndDelay` can be used to override the delay after firing last" +" shot in strafing run before aircraft resumes another strafing run or" +" returns to base. Defaults to (Weapon `Range` \\* 256 + 1024) /" +" Aircraft `Speed`. Note that using a short delay with aircraft that can" +" do multiple strafing runs with their ammo can cause undesired behaviour" +" like dancing around or facing weird way depending on other factors" +" like ROF and/or movement speed." msgstr "" -"`Strafing.EndDelay` 可用于覆盖战机在扫射过程中射出最后一发后进行另一轮扫射或返航前的时间间隔。默认为 " -"(`[WeaponType] -> Range` * 256 + 1024) / `[AircraftType] -> " -"Speed`。注意对于弹药量足够进行多次扫射的战机而言过短的间隔可能导致并不理想的行为例如四处游走或朝向怪异的方向,具体取决于 " -"`[WeaponType] -> ROF` 和 `[AircraftType] -> Speed` 等其他因素。" +"`Strafing.EndDelay` 可用于覆盖战机在扫射过程中射出最后一发后进行另一轮扫射或返航前的时间间隔。默认为" +" (`[WeaponType] -> Range` \\* 256 + 1024) / `[AircraftType]" +" -> Speed`。注意对于弹药量足够进行多次扫射的战机而言过短的间隔可能导致并不理想的行为例如四处游走或朝向怪异的方向,具体取决于" +" `[WeaponType] -> ROF` 和 `[AircraftType] -> Speed` 等其他因素。" msgid "" "There is a special case for aircraft spawned by `Type=SpyPlane` " @@ -6718,4 +6715,3 @@ msgid "" "`CanTarget` explicitly requires either `all` or `empty` to be listed for " "the weapon to be able to fire at cells containing no TechnoTypes." msgstr "`CanTarget` 要求明确列出 `all` 或 `empty` 才能对不包含任何科技类型的单元格开火。" - diff --git a/docs/locale/zh_CN/LC_MESSAGES/Project-guidelines-and-policies.po b/docs/locale/zh_CN/LC_MESSAGES/Project-guidelines-and-policies.po index 414a2db174..b6d0204c74 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Project-guidelines-and-policies.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Project-guidelines-and-policies.po @@ -407,7 +407,7 @@ msgstr "" msgid "" "Braceless code block bodies should be made only when both code block head" -" and body are single line, statements split into multiple lines and " +" and body are single line, statements split into multiple lines and " "nested braceless blocks are not allowed within braceless blocks:" msgstr "只有代码块头部和代码块主体均为单行时才应当使用无大括号的代码块,且无大括号的块中不允许存在跨多行的语句拆分以及嵌套的无大括号块:" @@ -700,4 +700,3 @@ msgid "" " it compiles like normal, and then commit and push it to your Phobos " "branch that you made for your pull request." msgstr "当 YRpp 拉取请求接受后,你需要再子模块中切换到已合并的最新提交,验证正常编译后,将其提交并推送到 Phobos 拉取请求创建的分支。" - diff --git a/docs/locale/zh_CN/LC_MESSAGES/User-Interface.po b/docs/locale/zh_CN/LC_MESSAGES/User-Interface.po index eb3fb18771..bad239adc3 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/User-Interface.po +++ b/docs/locale/zh_CN/LC_MESSAGES/User-Interface.po @@ -175,8 +175,8 @@ msgstr "0 - 存活的子机," msgid "1 - docked spawns," msgstr "1 - 返航的子机," -msgid "2 - launching spawns.

" -msgstr "2 - 发射中的子机。

" +msgid "2 - launching spawns." +msgstr "2 - 发射中的子机。" msgid "In `InfoType=Tiberium`," msgstr "在 `InfoType=Tiberium` 中," @@ -187,8 +187,8 @@ msgstr "0 - 所有类型," msgid "1 - the first tiberium," msgstr "1 - 第一类矿石," -msgid "2 - the second tiberium,
..." -msgstr "2 - 第二类矿石,
..." +msgid "2 - the second tiberium," +msgstr "2 - 第二类矿石," msgid "In `InfoType=SpawnTimer`," msgstr "在 `InfoType=SpawnTimer` 中," @@ -199,8 +199,8 @@ msgstr "0 - 最快的子机," msgid "1 - the first spawnee," msgstr "1 - 第一个子机," -msgid "2 - the second spawnee,
..." -msgstr "2 - 第二个子机,
..." +msgid "2 - the second spawnee," +msgstr "2 - 第二个子机," msgid "In `InfoType=SuperWeapon`," msgstr "在 `InfoType=SuperWeapon` 中," @@ -214,8 +214,8 @@ msgstr "1 - `[BuildingType] -> SuperWeapon`," msgid "2 - `[BuildingType] -> SuperWeapon2`," msgstr "2 - `[BuildingType] -> SuperWeapon2`," -msgid "3 - the first SW in `[BuildingType] -> SuperWeapons`,
..." -msgstr "3 - `[BuildingType] -> SuperWeapons` 中的第一个,
..." +msgid "3 - the first SW in `[BuildingType] -> SuperWeapons`," +msgstr "3 - `[BuildingType] -> SuperWeapons` 中的第一个," msgid "In `InfoType=FactoryProcess`," msgstr "在 `InfoType=FactoryProcess` 中," @@ -226,8 +226,8 @@ msgstr "0 - 正在生产的首个产线," msgid "1 - primary factory," msgstr "1 - 主/科技建筑生产线," -msgid "2 - secondary factory.

" -msgstr "2 - 副/防御建筑生产线。

" +msgid "2 - secondary factory." +msgstr "2 - 副/防御建筑生产线。" msgid "" "`Anchor.Horizontal` and `Anchor.Vertical` set the anchor point from which" @@ -360,7 +360,8 @@ msgstr "例如:你可以为单位制作一个圆环形状的血条,这个圆 msgid "" "![image](_static/images/ring-health-bar.gif) *Example of a ring-shaped " "health bar*" -msgstr "![image](_static/images/ring-health-bar.gif) *圆环形状的血条样例*" +msgstr "" +"![image](_static/images/ring-health-bar.gif) *圆环形状的血条样例*" msgid "" "The arrangement of static images on the plane is entirely up to you to " @@ -396,8 +397,8 @@ msgid "Low priority for box selection" msgstr "低优先级框选" msgid "" -"![smartvesters](_static/images/lowpriority-01.gif) *Harvesters not " -"selected together with battle units in [Rise of the " +"![smartvesters](_static/images/lowpriority-01.gif) *Harvesters not selected " +"together with battle units in [Rise of the " "East](https://www.moddb.com/mods/riseoftheeast)*" msgstr "" "![智能矿车](_static/images/lowpriority-01.gif) " @@ -631,12 +632,12 @@ msgid "Task subtitles display in the middle of the screen" msgstr "字幕居中" msgid "" -"![Message Display In Center](_static/images/messagedisplayincenter.gif) " -"*Taking a campaign in [Mental Omega](https://www.mentalomega.com) as an " -"example to display messages in center*" +"![Message Display In Center](_static/images/messagedisplayincenter.gif) *Taking a campaign in " +"[Mental Omega](https://www.mentalomega.com) as an example to display " +"messages in center*" msgstr "" -"![字幕居中](_static/images/messagedisplayincenter.gif) *以 " -"[心灵终结](https://www.mentalomega.com) 中的一个战役为例展示字幕居中*" +"![字幕居中](_static/images/messagedisplayincenter.gif) " +"*以 [心灵终结](https://www.mentalomega.com) 中的一个战役为例展示字幕居中*" msgid "Message Display In Center" msgstr "字幕居中" @@ -1211,8 +1212,8 @@ msgid "" "![image](_static/images/powerdelta-01.gif) *Power delta Counter in " "[Assault Amerika](https://www.moddb.com/mods/assault-amerika)*" msgstr "" -"![image](_static/images/powerdelta-01.gif) *[Assault " -"Amerika](https://www.moddb.com/mods/assault-amerika) 中的电力差值计数器*" +"![image](_static/images/powerdelta-01.gif) " +"*[Assault Amerika](https://www.moddb.com/mods/assault-amerika) 中的电力差值计数器*" msgid "" "An additional counter for your power delta (surplus) can be added near " @@ -1438,11 +1439,12 @@ msgid "Weeds counter" msgstr "废矿计数器" msgid "" -"Counter for amount of [weeds in storage](Fixed-or-Improved-" -"Logics.md#weeds--weed-eaters) can be added near the credits indicator." +"Counter for amount of [weeds in " +"storage](Fixed-or-Improved-Logics.md#weeds-weed-eaters) can be added near " +"the credits indicator." msgstr "" "你可以在资金示数器附近添加一个额外的计数器用于显示[存储的泰伯利亚废矿](Fixed-or-Improved-Logics.md#weeds" -"--weed-eaters)的数量。" +"-weed-eaters)的数量。" msgid "" "You can adjust counter position by `Sidebar.WeedsCounter.Offset` (per-" @@ -1489,7 +1491,7 @@ msgid "" msgstr "科技类型的拓展工具条将显示其名称、成本、电力、建造时间和描述(如果适用)。" msgid "" -"SWType's tooltip would display it's name, cost, and recharge time (when " +"SWType's tooltip would display it's name, cost, and recharge time (when " "applicable)." msgstr "超级武器类型的拓展工具条将显示其名称、成本和充能时间(如果适用)。" @@ -1555,4 +1557,3 @@ msgid "" "The blur effect is resource intensive. Please make sure you really want " "to enable this effect, otherwise leave it to 0.0 so it stays disabled." msgstr "模糊效果非常占用资源,请确保你的确想要启用此效果,否则请保持 0.0 以禁用。" - diff --git a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po index c28315fa0e..f8d9f70abd 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po +++ b/docs/locale/zh_CN/LC_MESSAGES/Whats-New.po @@ -947,18 +947,18 @@ msgstr "" "dehardcode)(by TaranDahl)" msgid "" -"[Fast access vehicle](New-or-Enhanced-Logics.md#fast-access-vehicle) (by " +"[Fast access vehicle](New-or-Enhanced-Logics.md#fast-access-vehicle-structure) (by " "CrimRecya)" -msgstr "[快速上车](New-or-Enhanced-Logics.md#fast-access-vehicle)(by CrimRecya)" +msgstr "[快速上车](New-or-Enhanced-Logics.md#fast-access-vehicle-structure)(by CrimRecya)" msgid "Laser, electric bolt and rad beam scatter (by CrimRecya)" msgstr "激光、EBolt 和辐射波的散布(by CrimRecya)" msgid "" "[Airburst weapon firing/source coordinate & firing effects customizations" -"](Fixed-or-Improved-Logics.md#airburst--splits) (by Starkku)" +"](Fixed-or-Improved-Logics.md#airburst-splits) (by Starkku)" msgstr "" -"[自定义空爆武器中心坐标和开火效果](Fixed-or-Improved-Logics.md#airburst--splits)(by " +"[自定义空爆武器中心坐标和开火效果](Fixed-or-Improved-Logics.md#airburst-splits)(by " "Starkku)" msgid "" @@ -1043,10 +1043,10 @@ msgstr "[弹头击杀限制](New-or-Enhanced-Logics.md#warhead-that-can-not-kill msgid "" "[Customize parasite culling targets](Fixed-or-Improved-Logics.md" -"#customizing-parasite-culling-targets) (by NetsuNegi)" +"#customizing-parasite) (by NetsuNegi)" msgstr "" -"[自定义寄生秒杀目标](Fixed-or-Improved-Logics.md#customizing-parasite-culling-" -"targets)(by NetsuNegi)" +"[自定义寄生秒杀目标](Fixed-or-Improved-Logics.md#customizing-parasite)(by " +"NetsuNegi)" msgid "" "[Overload characteristic dehardcoded](New-or-Enhanced-Logics.md#overload-" @@ -1141,7 +1141,7 @@ msgstr "解除了覆盖物最多 255 个的数量限制(by secsome 与 ZivDero msgid "" "[Customizable airstrike flare colors](Fixed-or-Improved-Logics.md" -"#airstrike-flare-customizations) (by Starkku)" +"#airstrike-flare-visual-customizations) (by Starkku)" msgstr "" "[自定义空袭引导效果](Fixed-or-Improved-Logics.md#airstrike-flare-visual-" "customizations)(by Starkku)" @@ -1627,7 +1627,7 @@ msgstr "[低优先级部署](New-or-Enhanced-Logics.md#low-priority-for-deploy) msgid "" "[Weapon target filtering by target veterancy](New-or-Enhanced-Logics.md" "#weapon-targeting-filter) (by Flactine)" -msgstr "[根据目标经验等级的武器目标筛选](New-or-Enhanced-Logics.md#attached-effects)(by Flactine)" +msgstr "[根据目标经验等级的武器目标筛选](New-or-Enhanced-Logics.md#weapon-targeting-filter)(by Flactine)" msgid "" "[Warhead effect filtering by target veterancy](Fixed-or-Improved-" @@ -1845,13 +1845,13 @@ msgstr "" msgid "" "[Hotkey for deselect object from current selection](User-Interface.md" "#deselect-object-s) (by FrozenFog)" -msgstr "[从当前已选中中取消选择对象的快捷键](User-Interface.md#deselect-object)(by FrozenFog)" +msgstr "[从当前已选中中取消选择对象的快捷键](User-Interface.md#deselect-object-s)(by FrozenFog)" msgid "" "[Additional customizations for `Splits` concerning target selection" -"](Fixed-or-Improved-Logics.md#airburst--splits) (by Starkku)" +"](Fixed-or-Improved-Logics.md#airburst-splits) (by Starkku)" msgstr "" -"[关于 `Splits` 目标选择的额外自定义](Fixed-or-Improved-Logics.md#airburst--splits)(by" +"[关于 `Splits` 目标选择的额外自定义](Fixed-or-Improved-Logics.md#airburst-splits)(by" " Starkku)" msgid "" @@ -5784,4 +5784,3 @@ msgstr "现在侧边栏拓展工具条可以超出侧边栏边界(by Belonit msgid "Lifted stupidly small limit for tooltip character amount (by Belonit)" msgstr "解除了拓展工具条字符数量小得愚蠢的上限(by Belonit)" - diff --git a/docs/locale/zh_CN/LC_MESSAGES/index.po b/docs/locale/zh_CN/LC_MESSAGES/index.po index a158b9f6f9..117cf29107 100644 --- a/docs/locale/zh_CN/LC_MESSAGES/index.po +++ b/docs/locale/zh_CN/LC_MESSAGES/index.po @@ -58,6 +58,57 @@ msgstr "杂项内容" msgid "Extension Documentation" msgstr "扩展文档" +msgid "Phobos Documentation" +msgstr "Phobos 文档" + +msgid "Community documentation for Phobos YR engine extension" +msgstr "Phobos 引擎扩展社区文档" + +msgid "Home" +msgstr "主页" + +msgid "New or Enhanced Logics" +msgstr "新增或增强逻辑" + +msgid "Fixed or Improved Logics" +msgstr "修复或改进逻辑" + +msgid "Simplified Chinese" +msgstr "简体中文" + +msgid "On this page" +msgstr "本页目录" + +msgid "Edit this page" +msgstr "编辑此页" + +msgid "Last updated" +msgstr "最后更新" + +msgid "Previous page" +msgstr "上一页" + +msgid "Next page" +msgstr "下一页" + +msgid "Appearance" +msgstr "外观" + +msgid "Switch to light theme" +msgstr "切换到浅色模式" + +msgid "Switch to dark theme" +msgstr "切换到深色模式" + +msgid "Menu" +msgstr "菜单" + +msgid "Return to top" +msgstr "返回顶部" + +msgid "Select language" +msgstr "选择语言" + msgid "![Phobos YR Engine Extension](logo.png)" msgstr "![Phobos YR Engine Extension](logo.png)" @@ -314,10 +365,10 @@ msgid "" msgstr "由于官方文档的中文翻译尚不完善,目前建议中国用户使用社区文档" msgid "" -"You can switch between versions (displays latest develop nightly version " -"by default) in the bottom right corner, as well as download a PDF " -"version." -msgstr "你可以在右下角切换版本(默认显示开发分支最新一次提交对应的版本),也可以在官方英文文档下载 PDF 版本。" +"You can switch between versions (displays latest develop nightly version by " +"default) in the bottom right corner, as well as download a zipped offline " +"HTML version." +msgstr "你可以在右下角切换文档版本(默认显示最新的 develop 每夜构建版),也可以下载离线 HTML 压缩包版本。" msgid "" "The documentation is split by a few major categories, each represented " @@ -539,16 +590,6 @@ msgstr "" msgid "Legal and License" msgstr "法律与许可证" -msgid "" -"[![GPL " -"v3](https://www.gnu.org/graphics/gplv3-127x51.png)](https://opensource.org/license/GPL-3.0)" -msgstr "" -"[![GPL " -"v3](https://www.gnu.org/graphics/gplv3-127x51.png)](https://opensource.org/license/GPL-3.0)" - -msgid "GPL v3" -msgstr "GPL v3" - msgid "" "The Phobos project is an unofficial open-source community collaboration " "project to extend the Red Alert 2 Yuri's Revenge engine for modding and " @@ -575,4 +616,3 @@ msgstr "" "本项目与艺电公司(Electronic Arts Inc.)没有任何直接关联。「Command & Conquer」、「Command & " "Conquer Red Alert 2」 和 「Command & Conquer Yuri's " "Revenge」均为艺电公司的注册商标。版权所有。" - diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 922152e96a..0000000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=. -set BUILDDIR=_build - -if "%1" == "" goto help - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 0000000000..91994cd76d --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,3980 @@ +{ + "name": "docs", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "docs", + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^25.9.1", + "eslint": "^10.4.0", + "gettext-parser": "^9.0.2", + "globals": "^17.6.0", + "image-size": "^2.0.2", + "jiti": "^2.7.0", + "prettier": "^3.8.3", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.4", + "vite": "^5.4.21", + "vitepress": "^1.6.4", + "vue-tsc": "^3.3.1" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.18.1.tgz", + "integrity": "sha512-aehCadlWOGvrT91KUIZpC0MbB8KBW9yUuvTJFd2xesR7le/IsT4nJUnjCCZ4ZqZCeTcPHPV5mo//fZ5oxcSVYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.52.1.tgz", + "integrity": "sha512-HmXOGBOAOJPounpBzBpuY0zDYeiCpxgHnQmuA7JO6ScukcBdGp3/XM9zJk5pJx/xNGD68mbPGXWpDxGtl6BwDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.52.1.tgz", + "integrity": "sha512-5oo4+I8iixie9vXhCyNFCzeIr8pqA3FQ//VsLHTDvZAV4ttYOPGvYHGQq5NSalrLx5Jc3dRro/5uDOlnUMcBJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.52.1.tgz", + "integrity": "sha512-qCDoZfx5MpX7XQzvQ3bC4tSEMkQWQMaF/ABtLuoze03Y/flR563CCSws02qIJ23oX7lxl92LsilZjINVyTdtLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.52.1.tgz", + "integrity": "sha512-hnGs0/lsFJ2PWDxNBz7pxreXo/Xz7gxYRcfePBUjsH26ad0kU/sgnVZd9LwWBpsQv65z2jlb5dkyaB9WE9M9FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.52.1.tgz", + "integrity": "sha512-2VxxNc/uBysyKvGeBdSM5n9eIDKH8kWD7wd9/yqbJAiVwU4Yv6tU1LSJusHKrXV/aCu1KW7t9Gug9QyeEmtn/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.52.1.tgz", + "integrity": "sha512-O6mPtsw3xEfNOe6gWFpYLeAZAIljNa4Hgna3bq15PwyN7nbjTY0wXJFRbzs/0YVf75Br+SbOQUmjKxXYjDiSiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.52.1.tgz", + "integrity": "sha512-gA8oJOV1LnQQkDf91iebNnFInHuW0gRPEgLSOQ7EfipCEjYTHm5swm1DlH9H5RaRw4RrHuzHBegnlzc0MAstcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.52.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.52.1.tgz", + "integrity": "sha512-U9zZfc5xIu9wRxZkt+HceJUAD4VKHKbAyLSloJdEyMRmphXeibfrY9cxqIXBcmPeZzGhn3Imb35Dq8l19PkJhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.52.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.52.1.tgz", + "integrity": "sha512-a3SGNceHmkQfq77iG8Ka+w1pvwfZa/0lzEIgse30fL0kD+yKnd/dg0dQvSfFPAEt2f21DMcGkDSSeJlO3KdQjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.52.1.tgz", + "integrity": "sha512-z98QEguCFDpxb4S/PyrUK1igqF8tPsdbqOUUO6ON91vJ58w+Gwa6ncrI0oNXSFcrkxA5EqPKPQ2A1PBCn08TYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.52.1.tgz", + "integrity": "sha512-CI7+/0I11QeZM59Uc8whd2or0kqzFVjpaPn9Qpwll/krHcBAxk24WkAQ6WX+IwDVMfpont4YGbKwAmCre3vE8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.52.1.tgz", + "integrity": "sha512-S6bDuw9byfOvm3T71cgdoZgrgnZq6hpdMLkx52Louh57nUAmvGQESz2aojOynQHjbTiV55smvAFbgn0qT4tJrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.52.1.tgz", + "integrity": "sha512-tqZXM+54rWo4mk5jL5Z/flE11nPmNEdXwFBM5py9DkOmbjeCNemfVd45FyM97XdzfZ0dl9uOJC6PYn1FpkeyQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.83", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.83.tgz", + "integrity": "sha512-6Pp9V++XisT9RKH7FB4RLPqUDzcmLtSma0ovOEIoEWGrXtHwBFsH7oN1z8vvCVCb95fb87QgR46/zRLyN9Y3kg==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", + "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", + "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", + "integrity": "sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/type-utils": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.59.4", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.4.tgz", + "integrity": "sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.4.tgz", + "integrity": "sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.59.4", + "@typescript-eslint/types": "^8.59.4", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.4.tgz", + "integrity": "sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.4.tgz", + "integrity": "sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.4.tgz", + "integrity": "sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.4.tgz", + "integrity": "sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.4.tgz", + "integrity": "sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.59.4", + "@typescript-eslint/tsconfig-utils": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/visitor-keys": "8.59.4", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.4.tgz", + "integrity": "sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.59.4", + "@typescript-eslint/types": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.4.tgz", + "integrity": "sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.59.4", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", + "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/shared": "3.5.29", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", + "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", + "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@vue/compiler-core": "3.5.29", + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", + "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/language-core": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.1.tgz", + "integrity": "sha512-NP8g6V7x81NVOXbLupUvYY6i6LqUkjkVowe2epRedmpgaFCOdjgWHE/rQBvEJ4r7koAYODIjGeBWEdt6n7jYXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz", + "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz", + "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.29", + "@vue/shared": "3.5.29" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz", + "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.29", + "@vue/runtime-core": "3.5.29", + "@vue/shared": "3.5.29", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz", + "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29" + }, + "peerDependencies": { + "vue": "3.5.29" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", + "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/integrations": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/algoliasearch": { + "version": "5.52.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.52.1.tgz", + "integrity": "sha512-fHA8+kXTbjagw3jkLiaS7KKrH8qe2DyOsiUhGlN4cdT77PEsfqXZl7ewDk1hsg+pJnPlnE50XtLxjR91iJOpmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.18.1", + "@algolia/client-abtesting": "5.52.1", + "@algolia/client-analytics": "5.52.1", + "@algolia/client-common": "5.52.1", + "@algolia/client-insights": "5.52.1", + "@algolia/client-personalization": "5.52.1", + "@algolia/client-query-suggestions": "5.52.1", + "@algolia/client-search": "5.52.1", + "@algolia/ingestion": "1.52.1", + "@algolia/monitoring": "1.52.1", + "@algolia/recommend": "5.52.1", + "@algolia/requester-browser-xhr": "5.52.1", + "@algolia/requester-fetch": "5.52.1", + "@algolia/requester-node-http": "5.52.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/alien-signals": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gettext-parser": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-9.0.2.tgz", + "integrity": "sha512-dGvq3S1gpS6e9KzNkwgPED5xxfWk7mNYzzdi/fPdJF5qS7B+yo8El2ZQyyhJ79PyzTtHbwiqYOFsqBdzbQ0GPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "encoding": "^0.1.13" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", + "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", + "dev": true, + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", + "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.2.tgz", + "integrity": "sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shiki": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", + "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/langs": "2.5.0", + "@shikijs/themes": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.59.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.4.tgz", + "integrity": "sha512-Rw6+44QNFaXtgHSjPy+Kw8hrJniMYzR85E9yLmOLcfZ91/rz+JXQbDTCmc6ccxMPY6K6PgAq26f0JCBfR7LIPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.59.4", + "@typescript-eslint/parser": "8.59.4", + "@typescript-eslint/typescript-estree": "8.59.4", + "@typescript-eslint/utils": "8.59.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vitepress/node_modules/@algolia/autocomplete-core": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" + } + }, + "node_modules/vitepress/node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/vitepress/node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/vitepress/node_modules/@algolia/autocomplete-shared": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/vitepress/node_modules/@docsearch/css": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitepress/node_modules/@docsearch/js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/react": "3.8.2", + "preact": "^10.0.0" + } + }, + "node_modules/vitepress/node_modules/@docsearch/react": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/vitepress/node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/vitepress/node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/vitepress/node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/vitepress/node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/vitepress/node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/vitepress/node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/vitepress/node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/vitepress/node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", + "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-sfc": "3.5.29", + "@vue/runtime-dom": "3.5.29", + "@vue/server-renderer": "3.5.29", + "@vue/shared": "3.5.29" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-tsc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.1.tgz", + "integrity": "sha512-webBP3jhlxzhELZ2g+11KJ6pg5OVY1xWhWrj7N/yQMi1CrtxJnW+tUACyRVeDK0cQNLP2Va5HNYK8pe+7c+msw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.3.1" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000000..b48df94d95 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,30 @@ +{ + "name": "docs", + "private": true, + "type": "module", + "scripts": { + "dev": "vitepress dev .", + "build": "vitepress build .", + "build:offline": "node ./vitepress/build-scripts/build-offline-docs.ts", + "typecheck": "vue-tsc --noEmit -p tsconfig.json", + "lint": "eslint \"./**/*.{js,cjs,ts,mts,cts}\"", + "lint:fix": "npm run lint -- --fix", + "format": "prettier --write \"**/*.{md,vue,ts,tsx,mts,cts,js,jsx,cjs,json,yml,yaml,scss,css}\"", + "format:check": "prettier --check \"**/*.{md,vue,ts,tsx,mts,cts,js,jsx,cjs,json,yml,yaml,scss,css}\"" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^25.9.1", + "eslint": "^10.4.0", + "gettext-parser": "^9.0.2", + "globals": "^17.6.0", + "image-size": "^2.0.2", + "jiti": "^2.7.0", + "prettier": "^3.8.3", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.4", + "vite": "^5.4.21", + "vitepress": "^1.6.4", + "vue-tsc": "^3.3.1" + } +} diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 79e20149f1..0000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -myst-parser==4.0.0 -Sphinx==7.4.7 -sphinx-rtd-theme==2.0.0 -sphinx-design==0.6.1 diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 0000000000..cbbfb5feef --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "esModuleInterop": true, + "lib": ["ES2024", "DOM", "DOM.Iterable"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "types": ["node", "vitepress/client"] + }, + "include": [".vitepress/**/*.ts", ".vitepress/**/*.vue", "eslint.config.ts", "vitepress/**/*.ts"], + "exclude": ["node_modules", ".vitepress/cache", ".vitepress/.temp", ".artifacts", "vitepress/generated"] +} diff --git a/docs/vitepress/build-scripts/build-export-offline-html.ts b/docs/vitepress/build-scripts/build-export-offline-html.ts new file mode 100644 index 0000000000..1050d03a5c --- /dev/null +++ b/docs/vitepress/build-scripts/build-export-offline-html.ts @@ -0,0 +1,64 @@ +import { readFile, stat, writeFile } from 'node:fs/promises' +import { dirname, relative } from 'node:path' +import { rewriteCssForOffline } from './offline-css.ts' +import { rewriteHtmlForOffline } from './offline-html.ts' +import { mapRelPathToOfflineRelPath, rewriteHtmlForFilesLayout } from './offline-layout.ts' +import { + moveOfflineFilesUnderPayloadDir, + removeBundledVitePressJsFiles, + removeEmptyDirs, + removeInlinedFontFiles, + removeStaticHtmlPageCopies, + renameOfflineEntryFile, +} from './offline-payload.ts' +import { buildOfflineVitePressRuntime } from './offline-runtime.ts' +import { collectFiles } from './shared/fs.ts' +import { normalizeUrlPath } from './shared/paths.ts' +import { offlineFilesDir, outputDir } from './shared/offline.ts' + +const outputStats = await stat(outputDir).catch(() => null) +if (!outputStats || !outputStats.isDirectory()) { + throw new Error('Offline VitePress output not found. Run "npm run build:offline" first.') +} + +const bundledPageModules = await buildOfflineVitePressRuntime() + +const htmlFiles = await collectFiles(outputDir, entry => entry.name.endsWith('.html')) +const cssFiles = await collectFiles(outputDir, entry => entry.name.endsWith('.css')) +const htmlPageRelPaths = new Set(htmlFiles.map(htmlPath => normalizeUrlPath(relative(outputDir, htmlPath)))) + +for (const htmlPath of htmlFiles) { + const relPath = relative(outputDir, htmlPath) + const normalizedRelPath = normalizeUrlPath(relPath) + const finalRelPath = mapRelPathToOfflineRelPath(normalizedRelPath) + const rootDepth = dirname(relPath) === '.' ? 0 : dirname(relPath).split('/').length + const source = await readFile(htmlPath, 'utf8') + const offlineHtml = rewriteHtmlForOffline(source, rootDepth) + const rewritten = rewriteHtmlForFilesLayout(offlineHtml, normalizedRelPath, finalRelPath, htmlPageRelPaths) + await writeFile(htmlPath, rewritten) +} + +for (const cssPath of cssFiles) { + const relPath = relative(outputDir, cssPath) + const normalizedRelPath = normalizeUrlPath(relPath) + const source = await readFile(cssPath, 'utf8') + const rewritten = await rewriteCssForOffline(source, normalizedRelPath, htmlPageRelPaths) + await writeFile(cssPath, rewritten) +} + +const removedFontFiles = await removeInlinedFontFiles() +const removedBundledJsFiles = await removeBundledVitePressJsFiles() +const removedHtmlPageCopies = await removeStaticHtmlPageCopies() +const removedEmptyDirs = await removeEmptyDirs() +await renameOfflineEntryFile() +const movedRootEntries = await moveOfflineFilesUnderPayloadDir() + +console.log(`Offline HTML export ready: ${outputDir}`) +console.log(`Processed ${htmlFiles.length} HTML files.`) +console.log(`Processed ${cssFiles.length} CSS files.`) +console.log(`Bundled ${bundledPageModules} VitePress page modules into the offline runtime.`) +console.log(`Removed ${removedFontFiles} inlined font files from the offline payload.`) +console.log(`Removed ${removedBundledJsFiles} bundled VitePress JS source files from the offline payload.`) +console.log(`Removed ${removedHtmlPageCopies} static HTML page copies from the offline payload.`) +console.log(`Removed ${removedEmptyDirs} empty directories from the offline payload.`) +console.log(`Moved ${movedRootEntries} root entries to ${offlineFilesDir}.`) diff --git a/docs/vitepress/build-scripts/build-offline-docs.ts b/docs/vitepress/build-scripts/build-offline-docs.ts new file mode 100644 index 0000000000..93cfe4ba0a --- /dev/null +++ b/docs/vitepress/build-scripts/build-offline-docs.ts @@ -0,0 +1,26 @@ +import { spawn } from 'node:child_process' + +const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm' + +function runNpm(args: string[], env: NodeJS.ProcessEnv = {}): Promise { + return new Promise((resolve, reject) => { + const child = spawn(npmCommand, args, { + env: { ...process.env, ...env }, + shell: false, + stdio: 'inherit', + }) + + child.on('error', reject) + child.on('exit', code => { + if (code === 0) { + resolve() + return + } + + reject(new Error(`${npmCommand} ${args.join(' ')} exited with code ${code}`)) + }) + }) +} + +await runNpm(['run', 'build'], { DOCS_VITEPRESS_OFFLINE: '1' }) +await import('./build-export-offline-html.ts') diff --git a/docs/vitepress/build-scripts/gettext-parser.d.ts b/docs/vitepress/build-scripts/gettext-parser.d.ts new file mode 100644 index 0000000000..a41d6189fb --- /dev/null +++ b/docs/vitepress/build-scripts/gettext-parser.d.ts @@ -0,0 +1,23 @@ +declare module 'gettext-parser' { + export type PoTranslationEntry = { + comments?: { + flag?: string + } + msgid?: string + msgstr?: string[] + } + + export type PoParseResult = { + headers: Record + translations: Record> + } + + const gettextParser: { + po: { + compile(parsed: PoParseResult): Buffer + parse(buffer: Buffer): PoParseResult + } + } + + export default gettextParser +} diff --git a/docs/vitepress/build-scripts/media-dimensions-plugin.ts b/docs/vitepress/build-scripts/media-dimensions-plugin.ts new file mode 100644 index 0000000000..609b66dd0e --- /dev/null +++ b/docs/vitepress/build-scripts/media-dimensions-plugin.ts @@ -0,0 +1,564 @@ +import { readFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import type { Plugin } from 'vite' +import { imageSize } from 'image-size' +import { docsDir, normalizePath } from './shared/paths.ts' + +type MediaDimensions = { + width: number + height: number +} + +type Vint = { + length: number + value: number +} + +const htmlVideoTagRegExp = /]*)>/giu +const htmlImageTagRegExp = /]*)>/giu +const markdownImageRegExp = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/gu +const markdownVideoLinkRegExp = /(?`]+)))?/gu +const videoDimensionsCache = new Map>() +const imageDimensionsCache = new Map>() +const markdownCaptionAfterHtmlMediaRegExp = /((?:]*>|]*><\/video>))(\r?\n)(?=[_*])/giu + +function readVint(buffer: Buffer, offset: number, keepMarker: boolean): Vint | null { + if (offset >= buffer.length) { + return null + } + + const firstByte = buffer[offset] + let marker = 0x80 + let length = 1 + + while (length <= 8 && (firstByte & marker) === 0) { + marker >>= 1 + length += 1 + } + + if (length > 8 || offset + length > buffer.length) { + return null + } + + let value = keepMarker ? firstByte : firstByte & (marker - 1) + + for (let index = 1; index < length; index += 1) { + value = value * 256 + buffer[offset + index] + } + + return { length, value } +} + +function readUnsignedInteger(buffer: Buffer, start: number, end: number): number { + let value = 0 + + for (let index = start; index < end; index += 1) { + value = value * 256 + buffer[index] + } + + return value +} + +function isTopLevelContainer(id: number): boolean { + return id === 0x1a45dfa3 || id === 0x18538067 || id === 0x1654ae6b +} + +function findWebmVideoDimensions(buffer: Buffer): MediaDimensions | null { + const parseElements = (start: number, end: number): MediaDimensions | null => { + let offset = start + + while (offset < end) { + const id = readVint(buffer, offset, true) + + if (!id) { + return null + } + + const size = readVint(buffer, offset + id.length, false) + + if (!size) { + return null + } + + const dataStart = offset + id.length + size.length + const dataEnd = Math.min(dataStart + size.value, end) + + if (id.value === 0xae) { + const dimensions = parseTrackEntry(dataStart, dataEnd) + + if (dimensions) { + return dimensions + } + } else if (isTopLevelContainer(id.value)) { + const dimensions = parseElements(dataStart, dataEnd) + + if (dimensions) { + return dimensions + } + } + + offset = dataEnd + } + + return null + } + + const parseTrackEntry = (start: number, end: number): MediaDimensions | null => { + let offset = start + let isVideoTrack = false + let videoDimensions: MediaDimensions | null = null + + while (offset < end) { + const id = readVint(buffer, offset, true) + + if (!id) { + return null + } + + const size = readVint(buffer, offset + id.length, false) + + if (!size) { + return null + } + + const dataStart = offset + id.length + size.length + const dataEnd = Math.min(dataStart + size.value, end) + + if (id.value === 0x83) { + isVideoTrack = readUnsignedInteger(buffer, dataStart, dataEnd) === 1 + } else if (id.value === 0xe0) { + videoDimensions = parseVideoElement(dataStart, dataEnd) + } + + offset = dataEnd + } + + return isVideoTrack ? videoDimensions : null + } + + const parseVideoElement = (start: number, end: number): MediaDimensions | null => { + let offset = start + let width: number | null = null + let height: number | null = null + + while (offset < end) { + const id = readVint(buffer, offset, true) + + if (!id) { + return null + } + + const size = readVint(buffer, offset + id.length, false) + + if (!size) { + return null + } + + const dataStart = offset + id.length + size.length + const dataEnd = Math.min(dataStart + size.value, end) + + if (id.value === 0xb0) { + width = readUnsignedInteger(buffer, dataStart, dataEnd) + } else if (id.value === 0xba) { + height = readUnsignedInteger(buffer, dataStart, dataEnd) + } + + offset = dataEnd + } + + return width && height ? { width, height } : null + } + + return parseElements(0, buffer.length) +} + +function separateMarkdownCaptionsFromHtmlMedia(source: string): string { + return source.replace(markdownCaptionAfterHtmlMediaRegExp, '$1$2$2') +} + +function readMp4BoxSize(buffer: Buffer, offset: number, end: number): { size: number; headerSize: number } | null { + if (offset + 8 > end) { + return null + } + + const smallSize = buffer.readUInt32BE(offset) + + if (smallSize === 1) { + if (offset + 16 > end) { + return null + } + + const largeSize = Number(buffer.readBigUInt64BE(offset + 8)) + + return { size: largeSize, headerSize: 16 } + } + + if (smallSize === 0) { + return { size: end - offset, headerSize: 8 } + } + + return { size: smallSize, headerSize: 8 } +} + +function findMp4VideoDimensions(buffer: Buffer): MediaDimensions | null { + type Mp4Box = { + dataStart: number + end: number + type: string + } + + const findChildBoxes = (start: number, end: number, type: string): Mp4Box[] => { + const boxes: Mp4Box[] = [] + let offset = start + + while (offset + 8 <= end) { + const size = readMp4BoxSize(buffer, offset, end) + + if (!size || size.size < size.headerSize) { + break + } + + const boxEnd = Math.min(offset + size.size, end) + const boxType = buffer.subarray(offset + 4, offset + 8).toString('ascii') + + if (boxType === type) { + boxes.push({ dataStart: offset + size.headerSize, end: boxEnd, type: boxType }) + } + + offset = boxEnd + } + + return boxes + } + + const readHandlerType = (mdia: Mp4Box): string | null => { + const hdlr = findChildBoxes(mdia.dataStart, mdia.end, 'hdlr')[0] + + if (!hdlr || hdlr.dataStart + 12 > hdlr.end) { + return null + } + + return buffer.subarray(hdlr.dataStart + 8, hdlr.dataStart + 12).toString('ascii') + } + + const readTrackHeaderDimensions = (trak: Mp4Box): MediaDimensions | null => { + const tkhd = findChildBoxes(trak.dataStart, trak.end, 'tkhd')[0] + + if (!tkhd || tkhd.dataStart >= tkhd.end) { + return null + } + + const version = buffer[tkhd.dataStart] + const dimensionsOffset = version === 1 ? tkhd.dataStart + 88 : tkhd.dataStart + 76 + + if (dimensionsOffset + 8 > tkhd.end) { + return null + } + + const width = buffer.readUInt32BE(dimensionsOffset) / 0x10000 + const height = buffer.readUInt32BE(dimensionsOffset + 4) / 0x10000 + + return width && height ? { width: Math.round(width), height: Math.round(height) } : null + } + + for (const moov of findChildBoxes(0, buffer.length, 'moov')) { + for (const trak of findChildBoxes(moov.dataStart, moov.end, 'trak')) { + const mdia = findChildBoxes(trak.dataStart, trak.end, 'mdia')[0] + + if (!mdia || readHandlerType(mdia) !== 'vide') { + continue + } + + const dimensions = readTrackHeaderDimensions(trak) + + if (dimensions) { + return dimensions + } + } + } + + return null +} + +function findVideoDimensions(buffer: Buffer): MediaDimensions | null { + return findWebmVideoDimensions(buffer) || findMp4VideoDimensions(buffer) +} + +function findImageDimensions(buffer: Buffer): MediaDimensions | null { + const dimensions = imageSize(buffer) + + return dimensions.width && dimensions.height ? { width: dimensions.width, height: dimensions.height } : null +} + +function getAttributes(tagAttributes: string): Map { + const attributes = new Map() + + for (const match of tagAttributes.matchAll(htmlAttributeRegExp)) { + attributes.set(match[1].toLowerCase(), match[2] ?? match[3] ?? match[4] ?? '') + } + + return attributes +} + +function resolveVideoPath(src: string, markdownPath: string): string | null { + if (/^(?:[a-z]+:)?\/\//iu.test(src) || src.startsWith('data:')) { + return null + } + + if (src.startsWith('/')) { + return resolve(docsDir, '_static', src.slice(1)) + } + + if (src.startsWith('_static/')) { + return resolve(docsDir, src) + } + + return resolve(dirname(markdownPath), src) +} + +function escapeHtml(value: string): string { + return value // + .replace(/&/gu, '&') + .replace(/"/gu, '"') + .replace(//gu, '>') +} + +function normalizeHtmlMediaSrc(src: string): string { + if (src.startsWith('/') || src.startsWith('./') || src.startsWith('../') || src.startsWith('_static/')) { + return src + } + + return `./${src}` +} + +function readVideoDimensions(videoPath: string): Promise { + const normalizedVideoPath = normalizePath(videoPath) + const cachedDimensions = videoDimensionsCache.get(normalizedVideoPath) + + if (cachedDimensions) { + return cachedDimensions + } + + const dimensions = readFile(videoPath) + .then(buffer => findVideoDimensions(buffer)) + .catch(() => null) + + videoDimensionsCache.set(normalizedVideoPath, dimensions) + return dimensions +} + +function readImageDimensions(imagePath: string): Promise { + const normalizedImagePath = normalizePath(imagePath) + const cachedDimensions = imageDimensionsCache.get(normalizedImagePath) + + if (cachedDimensions) { + return cachedDimensions + } + + const dimensions = readFile(imagePath) + .then(buffer => findImageDimensions(buffer)) + .catch(() => null) + + imageDimensionsCache.set(normalizedImagePath, dimensions) + return dimensions +} + +async function addDimensionsToHtmlMediaTags( + source: string, + modulePath: string, + tagRegExp: RegExp, + dimensionsReader: (path: string) => Promise, +): Promise<{ source: string; didChange: boolean }> { + let didChange = false + const replacements = await Promise.all( + [...source.matchAll(tagRegExp)].map(async match => { + const fullTag = match[0] + const attributes = getAttributes(match[1]) + const src = attributes.get('src') + + if (!src || (attributes.has('width') && attributes.has('height'))) { + return fullTag + } + + const mediaPath = resolveVideoPath(src, modulePath) + + if (!mediaPath) { + return fullTag + } + + const dimensions = await dimensionsReader(mediaPath) + + if (!dimensions) { + return fullTag + } + + didChange = true + const width = attributes.has('width') ? '' : ` width="${dimensions.width}"` + const height = attributes.has('height') ? '' : ` height="${dimensions.height}"` + + return fullTag.replace(/>$/u, `${width}${height}>`) + }), + ) + + let replacementIndex = 0 + + return { + source: didChange ? source.replace(tagRegExp, () => replacements[replacementIndex++]) : source, + didChange, + } +} + +async function addDimensionsToMarkdownImages( + source: string, + modulePath: string, +): Promise<{ source: string; didChange: boolean }> { + let didChange = false + const replacements = await Promise.all( + [...source.matchAll(markdownImageRegExp)].map(async match => { + const fullImage = match[0] + const alt = match[1] + const src = match[2] + const imagePath = resolveVideoPath(src, modulePath) + + if (!imagePath) { + return fullImage + } + + const dimensions = await readImageDimensions(imagePath) + + if (!dimensions) { + return fullImage + } + + didChange = true + + return `${escapeHtml(alt)}` + }), + ) + + let replacementIndex = 0 + + return { + source: didChange ? source.replace(markdownImageRegExp, () => replacements[replacementIndex++]) : source, + didChange, + } +} + +async function rewriteMarkdownVideoLinks( + source: string, + modulePath: string, +): Promise<{ source: string; didChange: boolean }> { + let didChange = false + const replacements = await Promise.all( + [...source.matchAll(markdownVideoLinkRegExp)].map(async match => { + const fullLink = match[0] + const src = match[2] + const videoPath = resolveVideoPath(src, modulePath) + + if (!videoPath) { + return fullLink + } + + const dimensions = await readVideoDimensions(videoPath) + + if (!dimensions) { + return fullLink + } + + didChange = true + + return [ + ``, + ].join('') + }), + ) + + let replacementIndex = 0 + + return { + source: didChange ? source.replace(markdownVideoLinkRegExp, () => replacements[replacementIndex++]) : source, + didChange, + } +} + +export function mediaDimensionsPlugin(): Plugin { + return { + name: 'media-dimensions', + enforce: 'pre', + async transformIndexHtml(html) { + let rewritten = html + const modulePath = resolve(docsDir, 'index.html') + const videoResult = await addDimensionsToHtmlMediaTags( + rewritten, + modulePath, + htmlVideoTagRegExp, + readVideoDimensions, + ) + rewritten = videoResult.source + + const imageTagResult = await addDimensionsToHtmlMediaTags( + rewritten, + modulePath, + htmlImageTagRegExp, + readImageDimensions, + ) + rewritten = imageTagResult.source + + return rewritten === html ? undefined : rewritten + }, + async transform(source, id) { + const modulePath = id.split('?')[0] + + if ( + !modulePath.endsWith('.md') || + (!source.includes('() + +function getFontMimeType(fontPath: string): string { + if (/\.woff2(?:[?#].*)?$/iu.test(fontPath)) { + return 'font/woff2' + } + + if (/\.woff(?:[?#].*)?$/iu.test(fontPath)) { + return 'font/woff' + } + + if (/\.ttf(?:[?#].*)?$/iu.test(fontPath)) { + return 'font/ttf' + } + + if (/\.otf(?:[?#].*)?$/iu.test(fontPath)) { + return 'font/otf' + } + + return 'application/octet-stream' +} + +function resolveCssAssetPath(url: string, cssRelPath: string): string | null { + if (!url || /^(?:data:|[a-z][a-z0-9+.-]*:|\/\/)/iu.test(url)) { + return null + } + + const [pathOnly] = url.split(/[?#]/u) + const decodedPath = decodeURIComponent(pathOnly) + + if (decodedPath.startsWith('/')) { + return resolve(outputDir, decodedPath.replace(/^\/+/u, '')) + } + + const cssDir = posix.dirname(normalizeUrlPath(cssRelPath)) + const normalizedCssDir = cssDir === '.' ? '' : cssDir + + return resolve(outputDir, normalizeUrlPath(posix.join(normalizedCssDir, decodedPath))) +} + +async function getCssFontDataUrl(url: string, cssRelPath: string): Promise { + const fontPath = resolveCssAssetPath(url, cssRelPath) + if (!fontPath) { + return null + } + + const cached = cssFontDataUrlCache.get(fontPath) + if (cached) { + return cached + } + + const font = await readFile(fontPath) + const dataUrl = `data:${getFontMimeType(fontPath)};base64,${font.toString('base64')}` + cssFontDataUrlCache.set(fontPath, dataUrl) + + return dataUrl +} + +async function inlineCssFontUrls(css: string, cssRelPath: string): Promise { + let output = '' + let lastIndex = 0 + const urlPattern = /url\((["']?)([^"')]+)\1\)/giu + + for (const match of css.matchAll(urlPattern)) { + const [raw, , url] = match + + if (!/\.(?:woff2?|ttf|otf)(?:[?#]|$)/iu.test(url)) { + continue + } + + const dataUrl = await getCssFontDataUrl(url, cssRelPath) + if (!dataUrl) { + continue + } + + output += css.slice(lastIndex, match.index) + output += `url("${dataUrl}")` + lastIndex = match.index + raw.length + } + + return lastIndex === 0 ? css : output + css.slice(lastIndex) +} + +export async function rewriteCssForOffline( + css: string, + cssRelPath: string, + htmlPageRelPaths: Set, +): Promise { + const cssWithInlineFonts = await inlineCssFontUrls(css, cssRelPath) + const rewrittenUrls = cssWithInlineFonts.replace( + /url\((["']?)\/(?!\/)([^"')]+)\1\)/gu, + (_match, quote: string, path: string) => { + const finalCssPath = mapRelPathToOfflineRelPath(cssRelPath) + const localUrl = rewriteLocalUrlForFilesLayout(`/${path}`, cssRelPath, finalCssPath, 'src', htmlPageRelPaths) + return `url(${quote}${localUrl}${quote})` + }, + ) + + // In offline docs we prefer visual stability over early paint, so avoid + // font swap flashes on each full-page navigation. + return rewrittenUrls.replace(/font-display:\s*swap\b/giu, 'font-display:block') +} diff --git a/docs/vitepress/build-scripts/offline-html.ts b/docs/vitepress/build-scripts/offline-html.ts new file mode 100644 index 0000000000..e071624a7f --- /dev/null +++ b/docs/vitepress/build-scripts/offline-html.ts @@ -0,0 +1,96 @@ +import { offlineRuntimeRelPath } from './shared/offline.ts' + +function normalizeLocalHref(targetPath: string): string { + if (!targetPath || targetPath === '/') { + return '/index.html' + } + + if (targetPath.endsWith('/')) { + return `${targetPath}index.html` + } + + if (!/\.[^/]+$/u.test(targetPath)) { + return `${targetPath}.html` + } + + return targetPath +} + +function getRelativePrefix(rootDepth: number): string { + return rootDepth === 0 ? './' : '../'.repeat(rootDepth) +} + +function resolveLocalUrl(absoluteUrl: string, rootDepth: number): string { + const [pathAndQuery, hash = ''] = absoluteUrl.split('#') + const [pathname, query = ''] = pathAndQuery.split('?') + const normalizedPathname = normalizeLocalHref(pathname) + const relPrefix = getRelativePrefix(rootDepth) + const queryPart = query ? `?${query}` : '' + const hashPart = hash ? `#${hash}` : '' + + return `${relPrefix}${normalizedPathname.replace(/^\//u, '')}${queryPart}${hashPart}` +} + +function injectOfflineRuntimeScript(html: string, rootDepth: number): string { + const relativePrefix = getRelativePrefix(rootDepth) + const scriptTag = `` + + if (html.includes('')) { + return html.replace('', `${scriptTag}`) + } + + return `${html}${scriptTag}` +} + +function normalizeOfflineNavBarClasses(html: string): string { + if (!html.includes('class="VPSidebar"') && !html.includes("class='VPSidebar'")) { + return html + } + + return html.replace(/class=(["'])([^"']*\bVPNavBar\b[^"']*)\1/u, (_match, quote: string, classValue: string) => { + const classes = classValue.split(/\s+/u).filter(Boolean) + + for (const className of ['has-sidebar', 'top']) { + if (!classes.includes(className)) { + classes.push(className) + } + } + + return `class=${quote}${classes.join(' ')}${quote}` + }) +} + +export function rewriteHtmlForOffline(html: string, rootDepth: number): string { + let output = html + + // Clean up any legacy offline layout overrides from previous exporter versions. + output = output.replace(/[\s\S]*?<\/style>\s*/giu, '') + output = normalizeOfflineNavBarClasses(output) + + // The offline runtime is bundled into a classic script, so browsers do not + // need file:// ES module loading support. + output = output.replace(/]*\btype=(["'])module\1[^>]*>[\s\S]*?<\/script>\s*/giu, '') + output = output.replace(/]*>\s*/giu, '') + output = output.replace(/]*>\s*/giu, '') + output = output.replace(/]*\bas=(["'])font\1[^>]*>\s*/giu, '') + + // Convert root-absolute links/assets to file-relative links. + output = output.replace( + /\b(href|src)=(")\/(?!\/)([^"]*)\2/gu, + (_match, attr: string, quote: string, path: string) => { + const localUrl = resolveLocalUrl(`/${path}`, rootDepth) + return `${attr}=${quote}${localUrl}${quote}` + }, + ) + + output = output.replace( + /\b(href|src)=(')\/(?!\/)([^']*)\2/gu, + (_match, attr: string, quote: string, path: string) => { + const localUrl = resolveLocalUrl(`/${path}`, rootDepth) + return `${attr}=${quote}${localUrl}${quote}` + }, + ) + + output = injectOfflineRuntimeScript(output, rootDepth) + return output +} diff --git a/docs/vitepress/build-scripts/offline-layout.ts b/docs/vitepress/build-scripts/offline-layout.ts new file mode 100644 index 0000000000..6869a3c307 --- /dev/null +++ b/docs/vitepress/build-scripts/offline-layout.ts @@ -0,0 +1,107 @@ +import { posix } from 'node:path' +import { normalizeUrlPath } from './shared/paths.ts' +import { flattenedPayloadDirs, offlineEntryFileName, offlineFilesDirName } from './shared/offline.ts' + +export function mapRelPathToOfflineRelPath(relPath: string): string { + const normalizedRelPath = normalizeUrlPath(relPath) + const [firstSegment, ...rest] = normalizedRelPath.split('/') + + if (flattenedPayloadDirs.has(firstSegment) && rest.length > 0) { + return normalizeUrlPath(posix.join(offlineFilesDirName, posix.basename(normalizedRelPath))) + } + + return normalizedRelPath === 'index.html' + ? offlineEntryFileName + : normalizeUrlPath(posix.join(offlineFilesDirName, normalizedRelPath)) +} + +function resolveOriginalRelPath(sourceRelPath: string, targetPath: string): string { + const sourceDir = posix.dirname(normalizeUrlPath(sourceRelPath)) + const normalizedSourceDir = sourceDir === '.' ? '' : sourceDir + + if (targetPath.startsWith('/')) { + return normalizeUrlPath(targetPath.replace(/^\/+/u, '')) + } + + return normalizeUrlPath(posix.join(normalizedSourceDir, targetPath)) +} + +function toRelativeUrl(fromRelPath: string, targetRelPath: string): string { + const fromDir = posix.dirname(normalizeUrlPath(fromRelPath)) + const normalizedFromDir = fromDir === '.' ? '' : fromDir + const relativePath = + posix.relative(normalizedFromDir, normalizeUrlPath(targetRelPath)) || posix.basename(targetRelPath) + + return relativePath.startsWith('.') ? relativePath : `./${relativePath}` +} + +function resolveHtmlRouteTargetPath(originalTargetPath: string, htmlPageRelPaths: Set): string | null { + if (htmlPageRelPaths.has(originalTargetPath)) { + return originalTargetPath + } + + if (originalTargetPath.endsWith('/')) { + const indexTargetPath = normalizeUrlPath(posix.join(originalTargetPath, 'index.html')) + return htmlPageRelPaths.has(indexTargetPath) ? indexTargetPath : null + } + + if (!/\.[^/]+$/u.test(originalTargetPath)) { + const htmlTargetPath = `${originalTargetPath}.html` + return htmlPageRelPaths.has(htmlTargetPath) ? htmlTargetPath : null + } + + return null +} + +export function rewriteLocalUrlForFilesLayout( + url: string, + originalRelPath: string, + finalRelPath: string, + attr: string, + htmlPageRelPaths: Set, +): string { + if (!url || /^(?:#|[a-z][a-z0-9+.-]*:|\/\/)/iu.test(url)) { + return url + } + + const [pathAndQuery, hash = ''] = url.split('#') + const [pathname, query = ''] = pathAndQuery.split('?') + if (!pathname) { + return url + } + + const originalTargetPath = resolveOriginalRelPath(originalRelPath, pathname) + const htmlTargetPath = attr === 'href' ? resolveHtmlRouteTargetPath(originalTargetPath, htmlPageRelPaths) : null + if (htmlTargetPath) { + const routePath = `/${htmlTargetPath}`.replace(/\/index\.html$/u, '/').replace(/\.html$/u, '') + const rootIndexUrl = toRelativeUrl(finalRelPath, offlineEntryFileName) + const queryPart = query ? `?${query}` : '' + const hashPart = hash ? `#${hash}` : '' + + return `${rootIndexUrl}#${routePath}${queryPart}${hashPart}` + } + + const finalTargetPath = mapRelPathToOfflineRelPath(originalTargetPath) + const relativeUrl = toRelativeUrl(finalRelPath, finalTargetPath) + const queryPart = query ? `?${query}` : '' + const hashPart = hash ? `#${hash}` : '' + + return `${relativeUrl}${queryPart}${hashPart}` +} + +export function rewriteHtmlForFilesLayout( + html: string, + originalRelPath: string, + finalRelPath: string, + htmlPageRelPaths: Set, +): string { + return html + .replace(/\b(href|src)=(")([^"]*)\2/gu, (_match, attr: string, quote: string, url: string) => { + const rewritten = rewriteLocalUrlForFilesLayout(url, originalRelPath, finalRelPath, attr, htmlPageRelPaths) + return `${attr}=${quote}${rewritten}${quote}` + }) + .replace(/\b(href|src)=(')([^']*)\2/gu, (_match, attr: string, quote: string, url: string) => { + const rewritten = rewriteLocalUrlForFilesLayout(url, originalRelPath, finalRelPath, attr, htmlPageRelPaths) + return `${attr}=${quote}${rewritten}${quote}` + }) +} diff --git a/docs/vitepress/build-scripts/offline-payload.ts b/docs/vitepress/build-scripts/offline-payload.ts new file mode 100644 index 0000000000..228e8a2c64 --- /dev/null +++ b/docs/vitepress/build-scripts/offline-payload.ts @@ -0,0 +1,123 @@ +import { mkdir, readdir, rename, rm } from 'node:fs/promises' +import { posix, relative, resolve } from 'node:path' +import { collectFiles } from './shared/fs.ts' +import { normalizeUrlPath } from './shared/paths.ts' +import { + flattenedPayloadDirs, + offlineEntryFileName, + offlineFilesDir, + offlineFilesDirName, + outputDir, +} from './shared/offline.ts' + +export async function moveOfflineFilesUnderPayloadDir(): Promise { + const entries = await readdir(outputDir, { withFileTypes: true }) + const moves: Array<{ from: string; to: string }> = [] + const targetPaths = new Set() + + await mkdir(offlineFilesDir, { recursive: true }) + + for (const entry of entries) { + if (entry.name === 'index.html' || entry.name === offlineEntryFileName || entry.name === offlineFilesDirName) { + continue + } + + const entryPath = resolve(outputDir, entry.name) + + if (entry.isDirectory() && flattenedPayloadDirs.has(entry.name)) { + const nestedFiles = await collectFiles(entryPath, () => true) + for (const nestedFile of nestedFiles) { + moves.push({ + from: nestedFile, + to: resolve(offlineFilesDir, posix.basename(normalizeUrlPath(nestedFile))), + }) + } + continue + } + + moves.push({ + from: entryPath, + to: resolve(offlineFilesDir, entry.name), + }) + } + + for (const { to } of moves) { + const normalizedTo = normalizeUrlPath(to) + if (targetPaths.has(normalizedTo)) { + throw new Error(`Offline payload flattening target conflict: ${normalizedTo}`) + } + targetPaths.add(normalizedTo) + } + + for (const { from, to } of moves) { + await rename(from, to) + } + + for (const dirName of flattenedPayloadDirs) { + await rm(resolve(outputDir, dirName), { recursive: true, force: true }) + } + + return moves.length +} + +export async function removeInlinedFontFiles(): Promise { + const fontFiles = await collectFiles(outputDir, entry => /\.(?:woff2?|ttf|otf)$/iu.test(entry.name)) + + for (const fontFile of fontFiles) { + await rm(fontFile, { force: true }) + } + + return fontFiles.length +} + +export async function removeBundledVitePressJsFiles(): Promise { + const assetsDir = resolve(outputDir, 'assets') + const jsFiles = await collectFiles(assetsDir, entry => entry.name.endsWith('.js')).catch(() => []) + + for (const jsFile of jsFiles) { + await rm(jsFile, { force: true }) + } + + return jsFiles.length +} + +export async function removeStaticHtmlPageCopies(): Promise { + const htmlFiles = await collectFiles(outputDir, entry => entry.name.endsWith('.html')) + const pageCopies = htmlFiles.filter(htmlFile => normalizeUrlPath(relative(outputDir, htmlFile)) !== 'index.html') + + for (const pageCopy of pageCopies) { + await rm(pageCopy, { force: true }) + } + + await rm(resolve(outputDir, 'zh_CN'), { recursive: true, force: true }) + + return pageCopies.length +} + +export async function renameOfflineEntryFile(): Promise { + const sourcePath = resolve(outputDir, 'index.html') + const targetPath = resolve(outputDir, offlineEntryFileName) + + await rename(sourcePath, targetPath) +} + +export async function removeEmptyDirs(dir = outputDir): Promise { + const entries = await readdir(dir, { withFileTypes: true }).catch(() => []) + let removed = 0 + + for (const entry of entries) { + if (!entry.isDirectory()) { + continue + } + + removed += await removeEmptyDirs(resolve(dir, entry.name)) + } + + const remainingEntries = await readdir(dir).catch(() => []) + if (remainingEntries.length === 0 && dir !== outputDir) { + await rm(dir, { recursive: true, force: true }) + removed += 1 + } + + return removed +} diff --git a/docs/vitepress/build-scripts/offline-runtime.ts b/docs/vitepress/build-scripts/offline-runtime.ts new file mode 100644 index 0000000000..b22531929b --- /dev/null +++ b/docs/vitepress/build-scripts/offline-runtime.ts @@ -0,0 +1,148 @@ +import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises' +import { posix, relative, resolve } from 'node:path' +import type { build as esbuildBuild } from 'esbuild' +import { collectFiles } from './shared/fs.ts' +import { normalizeUrlPath } from './shared/paths.ts' +import { offlineFilesDirName, offlineRuntimeFileName, outputDir } from './shared/offline.ts' + +function toJsString(value: string): string { + return JSON.stringify(value) +} + +function toRelativeImportPath(fromDir: string, targetPath: string): string { + const relativePath = posix.relative(normalizeUrlPath(fromDir), normalizeUrlPath(targetPath)) + return relativePath.startsWith('.') ? relativePath : `./${relativePath}` +} + +function getOfflinePageLoaderSource(pageModuleRelPaths: string[], fromRelDir: string): string { + const imports: string[] = [] + const entries: string[] = [] + + pageModuleRelPaths.forEach((relPath, index) => { + const binding = `__vp_offline_page_${index}` + const importPath = toRelativeImportPath(fromRelDir, relPath) + imports.push(`import * as ${binding} from ${toJsString(importPath)};`) + entries.push(`[${toJsString(`/${relPath}`)}, () => Promise.resolve(${binding})]`) + }) + + return `${imports.join('\n')} +const __vpOfflinePageModules = new Map([${entries.join(',')}]); +globalThis.__DOCS_OFFLINE_LOAD_PAGE__ = path => { + const key = String(path || '').replace(/^.*\\/assets\\//u, '/assets/'); + const load = __vpOfflinePageModules.get(key); + if (!load) { + console.error('Offline VitePress page module was not found:', path); + return Promise.resolve(null); + } + return load(); +}; +` +} + +async function collectPageModuleRelPaths(): Promise { + const assetsDir = resolve(outputDir, 'assets') + const entries = await readdir(assetsDir, { withFileTypes: true }) + return entries + .filter(entry => entry.isFile() && /\.md\.[^.]+(?:\.lean)?\.js$/u.test(entry.name)) + .map(entry => normalizeUrlPath(posix.join('assets', entry.name))) +} + +async function getLocalSearchChunkPath(): Promise { + const chunksDir = resolve(outputDir, 'assets', 'chunks') + const entries = await readdir(chunksDir, { withFileTypes: true }) + const searchChunk = entries.find(entry => entry.isFile() && /^VPLocalSearchBox\..*\.js$/u.test(entry.name)) + return searchChunk ? resolve(chunksDir, searchChunk.name) : null +} + +async function collectJsFiles(dir: string): Promise { + return collectFiles(dir, entry => entry.name.endsWith('.js')) +} + +async function rewriteJsAssetUrlsForOfflineRuntime(): Promise { + const jsFiles = await collectJsFiles(resolve(outputDir, 'assets')) + + for (const modulePath of jsFiles) { + const source = await readFile(modulePath, 'utf8') + const rewritten = source + .replace(/(["'])\/(?:images|assets)\//gu, `$1./${offlineFilesDirName}/`) + .replace(/\/images\//gu, `./${offlineFilesDirName}/`) + + if (rewritten !== source) { + await writeFile(modulePath, rewritten) + } + } +} + +async function assertFileIncludes(filePath: string, snippets: string[], description: string): Promise { + const source = await readFile(filePath, 'utf8') + const missingSnippet = snippets.find(snippet => !source.includes(snippet)) + + if (missingSnippet) { + throw new Error( + `${description} is not offline-ready. Run docs build with DOCS_VITEPRESS_OFFLINE=1 before exporting.`, + ) + } +} + +async function assertOfflineVitePressRuntime(appEntryPath: string, frameworkPath: string): Promise { + await assertFileIncludes( + appEntryPath, + ['__DOCS_OFFLINE_LOAD_PAGE__', '__DOCS_VITEPRESS_DATA__', '__DOCS_VITEPRESS_ROUTER__'], + 'VitePress app bundle', + ) + await assertFileIncludes(frameworkPath, [`./${offlineFilesDirName}/`, '#/'], 'VitePress framework bundle') + + const localSearchChunkPath = await getLocalSearchChunkPath() + if (localSearchChunkPath) { + await assertFileIncludes(localSearchChunkPath, ['__DOCS_OFFLINE_LOAD_PAGE__'], 'VitePress local search bundle') + } +} + +async function writeOfflineRuntimeEntry(appEntryPath: string, pageModuleRelPaths: string[]): Promise { + const appEntryRelPath = normalizeUrlPath(relative(outputDir, appEntryPath)) + const runtimeEntryPath = resolve(outputDir, 'offline-runtime-entry.js') + const runtimeEntrySource = `${getOfflinePageLoaderSource(pageModuleRelPaths, '')} +import(${toJsString(toRelativeImportPath('', appEntryRelPath))}); +` + + await writeFile(runtimeEntryPath, runtimeEntrySource) + return runtimeEntryPath +} + +export async function buildOfflineVitePressRuntime(): Promise { + const assetsDir = resolve(outputDir, 'assets') + const chunksDir = resolve(assetsDir, 'chunks') + const assetEntries = await readdir(assetsDir, { withFileTypes: true }) + const chunkEntries = await readdir(chunksDir, { withFileTypes: true }) + const appEntry = assetEntries.find(entry => entry.isFile() && /^app\..*\.js$/u.test(entry.name)) + const frameworkChunk = chunkEntries.find(entry => entry.isFile() && /^framework\..*\.js$/u.test(entry.name)) + + if (!appEntry || !frameworkChunk) { + throw new Error('Could not find VitePress runtime entry/chunks in offline output.') + } + + const pageModuleRelPaths = await collectPageModuleRelPaths() + const appEntryPath = resolve(assetsDir, appEntry.name) + const frameworkPath = resolve(chunksDir, frameworkChunk.name) + const runtimeEntryPath = await writeOfflineRuntimeEntry(appEntryPath, pageModuleRelPaths) + + await assertOfflineVitePressRuntime(appEntryPath, frameworkPath) + await rewriteJsAssetUrlsForOfflineRuntime() + + await mkdir(outputDir, { recursive: true }) + const { build } = (await import('esbuild')) as { build: typeof esbuildBuild } + await build({ + entryPoints: [runtimeEntryPath], + bundle: true, + format: 'iife', + target: ['es2020'], + minify: true, + legalComments: 'none', + outfile: resolve(outputDir, offlineRuntimeFileName), + logLevel: 'silent', + write: true, + }) + await rm(runtimeEntryPath, { force: true }) + + return pageModuleRelPaths.length +} diff --git a/docs/vitepress/build-scripts/shared/fs.ts b/docs/vitepress/build-scripts/shared/fs.ts new file mode 100644 index 0000000000..8e0ee35493 --- /dev/null +++ b/docs/vitepress/build-scripts/shared/fs.ts @@ -0,0 +1,43 @@ +import type { Dirent } from 'node:fs' +import { readdir } from 'node:fs/promises' +import { join } from 'node:path' + +export type ReadDirSafeOptions = { + withFileTypes: true +} + +export function isErrnoException(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && 'code' in error +} + +export async function readDirSafe(dir: string, options: ReadDirSafeOptions): Promise { + try { + return await readdir(dir, options) + } catch (error) { + if (isErrnoException(error) && error.code === 'ENOENT') { + return [] + } + + throw error + } +} + +export async function collectFiles(dir: string, predicate: (entry: Dirent) => boolean): Promise { + const result: string[] = [] + const entries = await readdir(dir, { withFileTypes: true }) + + for (const entry of entries) { + const entryPath = join(dir, entry.name) + + if (entry.isDirectory()) { + result.push(...(await collectFiles(entryPath, predicate))) + continue + } + + if (entry.isFile() && predicate(entry)) { + result.push(entryPath) + } + } + + return result +} diff --git a/docs/vitepress/build-scripts/shared/offline.ts b/docs/vitepress/build-scripts/shared/offline.ts new file mode 100644 index 0000000000..157cf40b79 --- /dev/null +++ b/docs/vitepress/build-scripts/shared/offline.ts @@ -0,0 +1,13 @@ +import { resolve } from 'node:path' +import { docsDir } from './paths.ts' + +export const offlineOutputDirName = 'offline-doc' +export const offlineEntryFileName = 'offline-doc.htm' +export const offlineFilesDirName = 'offline-doc' +export const offlineRuntimeFileName = 'vitepress-runtime.js' +export const offlineRuntimeRelPath = offlineRuntimeFileName +export const flattenedPayloadDirs = new Set(['assets', 'images', 'offline-assets']) + +export const artifactsDistDir = resolve(docsDir, '.artifacts', 'dist') +export const outputDir = resolve(docsDir, '.artifacts', offlineOutputDirName) +export const offlineFilesDir = resolve(outputDir, offlineFilesDirName) diff --git a/docs/vitepress/build-scripts/shared/pages.ts b/docs/vitepress/build-scripts/shared/pages.ts new file mode 100644 index 0000000000..72ed172c23 --- /dev/null +++ b/docs/vitepress/build-scripts/shared/pages.ts @@ -0,0 +1,21 @@ +export type GeneratedPage = { + page: string + sourcePath: string + originalSourcePath?: string + generatedRelPath: string + outputPath: string +} + +export type SourcePage = { + page: string + sourcePath: string + originalSourcePath?: string +} + +export type LocalePage = SourcePage & { + locale: string + poPath: string + targetPath: string + generatedRelPath: string + outputPath: string +} diff --git a/docs/vitepress/build-scripts/shared/paths.ts b/docs/vitepress/build-scripts/shared/paths.ts new file mode 100644 index 0000000000..1735a92d3c --- /dev/null +++ b/docs/vitepress/build-scripts/shared/paths.ts @@ -0,0 +1,15 @@ +import { dirname, posix, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +export const buildScriptsDir = dirname(fileURLToPath(import.meta.url)) +export const docsDir = resolve(buildScriptsDir, '..', '..', '..') +export const rootDir = resolve(docsDir, '..') + +export function normalizePath(value: string): string { + return value.replace(/\\/gu, '/') +} + +export function normalizeUrlPath(pathname: string): string { + const normalized = posix.normalize(pathname.replace(/\\/gu, '/')) + return normalized === '.' ? '' : normalized.replace(/^\.\//u, '') +} diff --git a/docs/vitepress/build-scripts/vitepress-last-updated.ts b/docs/vitepress/build-scripts/vitepress-last-updated.ts new file mode 100644 index 0000000000..c2b09fb90a --- /dev/null +++ b/docs/vitepress/build-scripts/vitepress-last-updated.ts @@ -0,0 +1,84 @@ +import { execFile } from 'node:child_process' +import { relative } from 'node:path' +import { promisify } from 'node:util' +import { isErrnoException } from './shared/fs.ts' +import { normalizePath, rootDir } from './shared/paths.ts' +import type { GeneratedPage, LocalePage } from './shared/pages.ts' + +const execFileAsync = promisify(execFile) +const gitTimestampCache = new Map() + +type LastUpdatedTransformOptions = { + rootPages?: GeneratedPage[] + localePages?: LocalePage[] +} + +function getRepositoryRelativePath(filePath: string): string { + return normalizePath(relative(rootDir, filePath)) +} + +async function getGitTimestampMs(filePath: string): Promise { + const repositoryRelativePath = getRepositoryRelativePath(filePath) + + if (gitTimestampCache.has(repositoryRelativePath)) { + return gitTimestampCache.get(repositoryRelativePath) ?? null + } + + let timestamp = null + + try { + const { stdout } = await execFileAsync('git', ['log', '-1', '--format=%ct', '--', repositoryRelativePath], { + cwd: rootDir, + }) + const seconds = Number(stdout.trim()) + + if (Number.isFinite(seconds) && seconds > 0) { + timestamp = seconds * 1000 + } + } catch (error) { + if (!isErrnoException(error) || error.code !== 'ENOENT') { + throw error + } + } + + gitTimestampCache.set(repositoryRelativePath, timestamp) + return timestamp +} + +async function getMaxGitTimestampMs(filePaths: string[]): Promise { + const timestamps = await Promise.all([...new Set(filePaths)].map(getGitTimestampMs)) + const validTimestamps = timestamps.filter((timestamp): timestamp is number => Number.isFinite(timestamp)) + + return validTimestamps.length ? Math.max(...validTimestamps) : null +} + +export function createLastUpdatedTransform(options: LastUpdatedTransformOptions = {}) { + const sourcePathsByGeneratedPage = new Map() + + for (const page of options.rootPages || []) { + sourcePathsByGeneratedPage.set(normalizePath(page.generatedRelPath), [page.originalSourcePath || page.sourcePath]) + } + + for (const page of options.localePages || []) { + sourcePathsByGeneratedPage.set(normalizePath(page.generatedRelPath), [ + page.originalSourcePath || page.sourcePath, + page.poPath, + ]) + } + + return async (pageData: { filePath: string }) => { + const sourcePaths = sourcePathsByGeneratedPage.get(normalizePath(pageData.filePath)) + + if (!sourcePaths) { + return undefined + } + + const lastUpdated = await getMaxGitTimestampMs(sourcePaths) + + if (!lastUpdated) { + return undefined + } + + return { lastUpdated } + } +} diff --git a/docs/vitepress/build-scripts/vitepress-offline-plugin.ts b/docs/vitepress/build-scripts/vitepress-offline-plugin.ts new file mode 100644 index 0000000000..eee9f98224 --- /dev/null +++ b/docs/vitepress/build-scripts/vitepress-offline-plugin.ts @@ -0,0 +1,604 @@ +import type { Plugin, ResolvedConfig } from 'vite' +import { offlineFilesDirName } from './shared/offline.ts' + +const vitePressClientMarker = '/vitepress/dist/client/' +const offlineRetryDelays = '[100, 300, 700, 1200]' + +type VitePressPatch = { + search: string | RegExp + replacement: string + name: string +} + +type VitePressModulePatcher = { + match: (moduleId: string, id: string) => boolean + patch: (source: string) => string +} + +// Offline docs store the page route in location.hash, for example +// #/Miscellanous#player-colors. VitePress internals still expect just +// #player-colors when matching active anchors. +const normalizeOfflineHashHelper = `function normalizeOfflineHash(hash) { + if (!hash.startsWith('#/')) { + return hash; + } + const routeHash = hash.slice(1); + const anchorIndex = routeHash.indexOf('#'); + return anchorIndex >= 0 ? routeHash.slice(anchorIndex) : ''; +}` + +// This replaces VitePress' URL normalizer. Offline docs have one browser +// document, so route changes must become hash changes instead of path changes. +const offlineRouterHelpers = `function normalizeOfflinePathname(pathname) { + let normalizedPathname = pathname.replace(/(^|\\/)index(\\.html)?$/, '$1'); + if (siteDataRef.value.cleanUrls) + normalizedPathname = normalizedPathname.replace(/\\.html$/, ''); + else if (!normalizedPathname.endsWith('/') && !normalizedPathname.endsWith('.html')) + normalizedPathname += '.html'; + return normalizedPathname || '/'; +} +function normalizeHref(href) { + const url = new URL(href, fakeHost); + // In the offline build the browser URL points to a single HTML file, while + // the real VitePress route lives after the first hash: file.htm#/Page#anchor. + if (url.hash.startsWith('#/') && (String(href).startsWith('#/') || !inBrowser || url.pathname === location.pathname)) { + const hashValue = url.hash.slice(1); + const anchorIndex = hashValue.indexOf('#'); + const pathAndSearch = anchorIndex >= 0 ? hashValue.slice(0, anchorIndex) : hashValue; + const anchor = anchorIndex >= 0 ? hashValue.slice(anchorIndex) : ''; + const routeUrl = new URL(pathAndSearch || '/', fakeHost); + return normalizeOfflinePathname(routeUrl.pathname) + routeUrl.search + anchor; + } + if (inBrowser && url.hash.startsWith('#/')) { + const hashValue = url.hash.slice(1); + const anchorIndex = hashValue.indexOf('#'); + const anchor = anchorIndex >= 0 ? hashValue.slice(anchorIndex) : ''; + return normalizeOfflinePathname(url.pathname) + url.search + anchor; + } + if (inBrowser && url.pathname === location.pathname) { + return '/' + url.search + url.hash; + } + return normalizeOfflinePathname(url.pathname) + url.search + url.hash; +} +function toOfflineDocumentLinkHref(linkHref, routePath) { + if (/^(?:[a-z][a-z0-9+.-]*:|\\/\\/)/i.test(linkHref)) { + return null; + } + if (linkHref.startsWith('#/')) { + return location.pathname + location.search + linkHref; + } + if (linkHref.startsWith('#')) { + return null; + } + const currentUrl = new URL(location.href); + const normalizedRoutePath = routePath || '/'; + const routeDir = normalizedRoutePath.endsWith('/') + ? normalizedRoutePath + : normalizedRoutePath.replace(/[^/]*$/, ''); + const targetUrl = linkHref.startsWith('/') + ? new URL(linkHref, currentUrl) + : new URL(linkHref, currentUrl.origin + routeDir); + if (targetUrl.origin === currentUrl.origin && treatAsHtml(targetUrl.pathname)) { + return toOfflineBrowserHref(targetUrl.href); + } + return null; +} +function rewriteOfflineDocumentLinks(routePath) { + const rewriteLinks = () => { + document.querySelectorAll('.vp-doc a[href]').forEach(link => { + if (link.closest('.vp-raw') || link.hasAttribute('download') || link.hasAttribute('target')) { + return; + } + const linkHref = link.getAttribute('href') + || (link instanceof SVGAElement ? link.getAttribute('xlink:href') : null); + if (linkHref == null) { + return; + } + const offlineHref = toOfflineDocumentLinkHref(linkHref, routePath); + if (offlineHref) { + link.setAttribute('href', offlineHref); + } + }); + }; + rewriteLinks(); + // Markdown content can be swapped after route changes; retry a few times so + // links rendered by async Vue updates are also normalized. + for (const delay of ${offlineRetryDelays}) { + setTimeout(rewriteLinks, delay); + } +} +function toOfflineBrowserHref(href) { + return location.pathname + location.search + '#' + normalizeHref(href); +} +function toOfflineAnchorBrowserHref(routePath, hash) { + return location.pathname + location.search + '#' + (routePath || '/') + (hash || ''); +} +function scrollToOfflineHashTarget(target, hash) { + scrollTo(target, hash); + // Deep-link targets may appear after the first tick while the offline + // runtime mounts the page module. + for (const delay of ${offlineRetryDelays}) { + setTimeout(() => { + let retryTarget = null; + try { + retryTarget = document.getElementById(decodeURIComponent(hash).slice(1)); + } + catch (e) { + console.warn(e); + } + if (retryTarget) { + scrollTo(retryTarget, hash); + } + }, delay); + } +}` + +// VitePress route navigation normally writes /Some-Page to history. Offline +// docs must keep the single HTML file and put the route after '#/' instead. +const routePushStatePatch = `const oldURL = location.href; + const offlineHref = toOfflineBrowserHref(href); + history.pushState({}, '', offlineHref); + if (new URL(oldURL).hash !== new URL(offlineHref, location.href).hash) { + window.dispatchEvent(new HashChangeEvent('hashchange', { + oldURL, + newURL: location.href + })); + }` + +const anchorPushStatePatch = `const offlineHref = toOfflineAnchorBrowserHref(route.path, hash); + history.pushState({}, '', offlineHref); + // still emit the event so we can listen to it in themes + window.dispatchEvent(new HashChangeEvent('hashchange', { + oldURL: currentUrl.href, + newURL: offlineHref + }));` + +// Links like file.htm#/Page#anchor are valid for Ctrl-click/open-in-new-tab, but +// normal clicks on the current page should stay instant and skip router.go(). +const hashRouteClickPatch = `if (origin === currentUrl.origin && treatAsHtml(pathname)) { + e.preventDefault(); + if (hash.startsWith('#/') && pathname === currentUrl.pathname && search === currentUrl.search) { + const targetHref = normalizeHref(hash); + const targetLoc = new URL(targetHref, fakeHost); + // Same-page outline/header anchors should scroll immediately. + // Calling go() here reloads the current page module and feels laggy. + if (targetLoc.pathname === route.path) { + const offlineHref = location.pathname + location.search + hash; + if (hash !== currentUrl.hash) { + history.pushState({}, '', offlineHref); + window.dispatchEvent(new HashChangeEvent('hashchange', { + oldURL: currentUrl.href, + newURL: location.href + })); + } + if (targetLoc.hash) { + scrollTo(link, targetLoc.hash, link.classList.contains('header-anchor')); + } + else { + window.scrollTo(0, 0); + } + return; + } + go(targetHref); + return; + }` + +// VitePress builds the right aside outline from raw heading anchors. These +// helpers keep those links route-aware in the offline URL scheme. +const outlineHelpers = `${normalizeOfflineHashHelper} +function getOfflineRouteHash() { + if (!location.hash.startsWith('#/')) { + return ''; + } + const routeHash = location.hash.slice(1); + const anchorIndex = routeHash.indexOf('#'); + return anchorIndex >= 0 ? routeHash.slice(0, anchorIndex) : routeHash; +} +function toOfflineOutlineLink(hash) { + // Right aside links are generated from raw heading IDs. Make them usable from + // file:// and Ctrl-click by preserving the current offline route before the anchor. + if (!hash || !hash.startsWith('#')) { + return hash; + } + if (hash.startsWith('#/')) { + return location.pathname + location.search + hash; + } + const routeHash = getOfflineRouteHash(); + return routeHash ? location.pathname + location.search + '#' + routeHash + hash : hash; +}` + +// Components should still see useData().hash as a pure anchor. The #/Page part +// is only a transport detail for the offline router. +const appDataPatch = `const data = initData(router.route); + const rawHash = data.hash; + ${normalizeOfflineHashHelper} + // Expose only the anchor part to components that consume useData().hash. + // The offline route prefix is an implementation detail of the single-file build. + data.hash = { + get value() { + return normalizeOfflineHash(rawHash.value); + } + }; + globalThis.__DOCS_VITEPRESS_DATA__ = data;` + +// VitePress' lean initial module is an optimization for regular multi-file +// builds. Offline docs bundle complete page modules into the runtime instead. +const disableLeanInitialLoadPatch = `// The offline runtime bundles full page modules into one file. Lean modules + // are separate dynamic chunks in regular VitePress builds. + if (false) { + pageFilePath = pageFilePath.replace(/\\.js$/, '.lean.js'); + }` + +// Initial hash scrolling needs a retry loop because the page component is +// loaded from the offline runtime and the target heading can appear after mount. +const initialHashScrollPatch = `// scroll to hash after the offline app is mounted. The initial router.go() + // runs before mount, so deep-link anchors inside #/page#anchor are + // not in the DOM yet at that point. + if (location.hash) { + const hashValue = location.hash.startsWith('#/') ? location.hash.slice(1) : location.hash; + const anchorIndex = hashValue.indexOf('#'); + const anchorHash = location.hash.startsWith('#/') && anchorIndex >= 0 + ? hashValue.slice(anchorIndex) + : location.hash; + const scrollToAnchor = () => { + if (!anchorHash || anchorHash === '#/') { + return; + } + let target = null; + try { + target = document.getElementById(decodeURIComponent(anchorHash).slice(1)); + } + catch (e) { + console.warn(e); + } + if (target) { + scrollTo(target, anchorHash); + } + }; + scrollToAnchor(); + for (const delay of ${offlineRetryDelays}) { + setTimeout(scrollToAnchor, delay); + } + }` + +function normalizeModuleId(id: string): string { + return id.split('?')[0].replace(/\\/gu, '/') +} + +function replaceRequired(source: string, search: string | RegExp, replacement: string, patchName: string): string { + const rewritten = source.replace(search, replacement) + + if (rewritten === source) { + // VitePress is patched by matching its compiled client code. Failing loudly + // makes upstream changes visible instead of silently producing broken offline docs. + throw new Error(`VitePress offline patch failed: ${patchName}`) + } + + return rewritten +} + +function applyRequiredPatches(source: string, patches: VitePressPatch[]): string { + return patches.reduce((output, patch) => replaceRequired(output, patch.search, patch.replacement, patch.name), source) +} + +function vitePressPatch(name: string, search: string | RegExp, replacement: string): VitePressPatch { + return { name, replacement, search } +} + +// Static assets still live next to the offline HTML payload, while internal +// documentation links must route through the hash-based single-page shell. +const vitePressUtilsPatches = [ + vitePressPatch( + 'client/app/utils.js withBase', + `export function withBase(path) { + return EXTERNAL_URL_RE.test(path) || !path.startsWith('/') + ? path + : joinPath(siteDataRef.value.base, path); +}`, + `export function withBase(path) { + if (EXTERNAL_URL_RE.test(path) || !path.startsWith('/')) + return path; + if (path.startsWith('/favicon.') || path.startsWith('/images/') || path.startsWith('/assets/')) + return './${offlineFilesDirName}/' + path.split('/').pop(); + return '#' + joinPath(siteDataRef.value.base, path); +}`, + ), +] + +function patchVitePressUtils(source: string): string { + return applyRequiredPatches(source, vitePressUtilsPatches) +} + +// Active nav/link checks receive offline hash URLs in several places. Normalize +// those back to VitePress' expected route/anchor shape before matching. +const vitePressSharedPatches = [ + vitePressPatch( + 'client/shared.js isActive offline match path', + `export function isActive(currentPath, matchPath, asRegex = false) { + if (matchPath === undefined) { + return false; + }`, + `${normalizeOfflineHashHelper} +function normalizeOfflineMatchPath(matchPath) { + if (typeof matchPath !== 'string' || !matchPath.startsWith('#/')) { + return matchPath; + } + const routeHash = matchPath.slice(1); + const anchorIndex = routeHash.indexOf('#'); + return anchorIndex >= 0 ? routeHash.slice(0, anchorIndex) : routeHash; +} +export function isActive(currentPath, matchPath, asRegex = false) { + if (matchPath === undefined) { + return false; + } + matchPath = normalizeOfflineMatchPath(matchPath);`, + ), + vitePressPatch( + 'client/shared.js isActive offline hash', + `return (inBrowser ? location.hash : '') === hashMatch[0];`, + `return (inBrowser ? normalizeOfflineHash(location.hash) : '') === hashMatch[0];`, + ), +] + +function patchVitePressShared(source: string): string { + return applyRequiredPatches(source, vitePressSharedPatches) +} + +// Router patches are the core of offline navigation: they translate VitePress' +// pathname-based routing into single-file hash routing and rewrite rendered +// markdown links after every page load. +const vitePressRouterPatches = [ + vitePressPatch( + 'client/app/router.js normalizeHref', + `function normalizeHref(href) { + const url = new URL(href, fakeHost); + url.pathname = url.pathname.replace(/(^|\\/)index(\\.html)?$/, '$1'); + // ensure correct deep link so page refresh lands on correct files. + if (siteDataRef.value.cleanUrls) + url.pathname = url.pathname.replace(/\\.html$/, ''); + else if (!url.pathname.endsWith('/') && !url.pathname.endsWith('.html')) + url.pathname += '.html'; + return url.pathname + url.search + url.hash; +}`, + offlineRouterHelpers, + ), + vitePressPatch( + 'client/app/router.js document links', + `if (targetLoc.hash && !scrollPosition) {`, + `rewriteOfflineDocumentLinks(route.path); + if (targetLoc.hash && !scrollPosition) {`, + ), + vitePressPatch('client/app/router.js route pushState', `history.pushState({}, '', href);`, routePushStatePatch), + vitePressPatch( + 'client/app/router.js replaceState href', + `history.replaceState({}, '', href);`, + `history.replaceState({}, '', toOfflineBrowserHref(href));`, + ), + vitePressPatch( + 'client/app/router.js anchor pushState', + `history.pushState({}, '', href); + // still emit the event so we can listen to it in themes + window.dispatchEvent(new HashChangeEvent('hashchange', { + oldURL: currentUrl.href, + newURL: href + }));`, + anchorPushStatePatch, + ), + vitePressPatch( + 'client/app/router.js delayed hash scroll', + `scrollTo(target, targetLoc.hash); + return;`, + `scrollToOfflineHashTarget(target, targetLoc.hash); + return;`, + ), + vitePressPatch( + 'client/app/router.js hash route click', + `if (origin === currentUrl.origin && treatAsHtml(pathname)) { + e.preventDefault();`, + hashRouteClickPatch, + ), +] + +function patchVitePressRouter(source: string): string { + return applyRequiredPatches(source, vitePressRouterPatches) +} + +// The right aside outline is generated outside markdown content, so it needs +// its own route-aware links and hashchange handling. +const outlinePatches = [ + vitePressPatch( + 'theme-default/composables/outline.js offline hash normalizer', + `const resolvedHeaders = [];`, + `const resolvedHeaders = []; +${outlineHelpers}`, + ), + vitePressPatch( + 'theme-default/composables/outline.js route-aware outline link', + `link: '#' + el.id,`, + `link: toOfflineOutlineLink('#' + el.id),`, + ), + vitePressPatch( + 'theme-default/composables/outline.js active hash', + `activateLink(location.hash);`, + `activateLink(normalizeOfflineHash(location.hash));`, + ), + vitePressPatch( + 'theme-default/composables/outline.js active outline link selector', + `prevActiveLink = container.value.querySelector(\`a[href="\${decodeURIComponent(hash)}"]\`);`, + `prevActiveLink = container.value.querySelector(\`a[href="\${decodeURIComponent(toOfflineOutlineLink(hash))}"]\`);`, + ), + vitePressPatch( + 'theme-default/composables/outline.js hashchange handler', + `let prevActiveLink = null;`, + `let prevActiveLink = null; + const onHashChange = () => activateLink(normalizeOfflineHash(location.hash));`, + ), + vitePressPatch( + 'theme-default/composables/outline.js hashchange listener', + `window.addEventListener('scroll', onScroll);`, + `window.addEventListener('scroll', onScroll); + window.addEventListener('hashchange', onHashChange);`, + ), + vitePressPatch( + 'theme-default/composables/outline.js remove hashchange listener', + `window.removeEventListener('scroll', onScroll);`, + `window.removeEventListener('scroll', onScroll); + window.removeEventListener('hashchange', onHashChange);`, + ), +] + +function patchOutline(source: string): string { + return applyRequiredPatches(source, outlinePatches) +} + +// App bootstrap patches keep the offline runtime self-contained: no SSR app, +// no prefetching/dynamic page chunks, and anchor scrolling after mount. +const vitePressAppPatches = [ + vitePressPatch( + 'client/app/index.js prefetch', + `if (import.meta.env.PROD && site.value.router.prefetchLinks) {`, + `if (false) {`, + ), + vitePressPatch( + 'client/app/index.js expose router', + `const router = newRouter();`, + `const router = newRouter(); + globalThis.__DOCS_VITEPRESS_ROUTER__ = router;`, + ), + vitePressPatch('client/app/index.js expose data', `const data = initData(router.route);`, appDataPatch), + vitePressPatch( + 'client/app/index.js client app', + `? createSSRApp(VitePressApp) + : createClientApp(VitePressApp);`, + `? createClientApp(VitePressApp) + : createClientApp(VitePressApp);`, + ), + vitePressPatch( + 'client/app/index.js lean initial load', + `if (isInitialPageLoad) { + pageFilePath = pageFilePath.replace(/\\.js$/, '.lean.js'); + }`, + disableLeanInitialLoadPatch, + ), + vitePressPatch( + 'client/app/index.js offline initial hash scroll', + `// scroll to hash on new tab during dev + if (import.meta.env.DEV && location.hash) { + const target = document.getElementById(decodeURIComponent(location.hash).slice(1)); + if (target) { + scrollTo(target, location.hash); + } + }`, + initialHashScrollPatch, + ), + vitePressPatch( + 'client/app/index.js page loader', + `pageModule = import(/*@vite-ignore*/ pageFilePath);`, + `pageModule = globalThis.__DOCS_OFFLINE_LOAD_PAGE__(pageFilePath);`, + ), +] + +function patchVitePressApp(source: string): string { + return applyRequiredPatches(source, vitePressAppPatches) +} + +// Search excerpts lazy-load page modules in regular VitePress. Offline docs +// load them from the bundled runtime map instead. +const localSearchBoxPatches = [ + vitePressPatch( + 'theme-default/VPLocalSearchBox.vue excerpt loader', + /return\s+\{\s*id,\s*mod:\s*await\s+import\(\s*\/\*@vite-ignore\*\/\s*file\s*\)\s*\}/u, + `return { id, mod: await globalThis.__DOCS_OFFLINE_LOAD_PAGE__(file) }`, + ), + vitePressPatch('theme-default/VPLocalSearchBox.vue result href', `:href="p.id"`, `:href="'#' + p.id"`), +] + +function patchLocalSearchBox(source: string): string { + return applyRequiredPatches(source, localSearchBoxPatches) +} + +// The search box component itself must be statically included; otherwise Vite +// would emit an async chunk that is removed from the offline payload. +const navBarSearchPatches = [ + vitePressPatch( + 'theme-default/VPNavBarSearch.vue static search import', + `import VPNavBarSearchButton from './VPNavBarSearchButton.vue'`, + `import VPNavBarSearchButton from './VPNavBarSearchButton.vue' +// Avoid an async component chunk: offline docs keep the client runtime in one JS file. +import VPLocalSearchBoxOffline from './VPLocalSearchBox.vue'`, + ), + vitePressPatch( + 'theme-default/VPNavBarSearch.vue local search component', + `const VPLocalSearchBox = __VP_LOCAL_SEARCH__ + ? defineAsyncComponent(() => import('./VPLocalSearchBox.vue')) + : () => null`, + `const VPLocalSearchBox = __VP_LOCAL_SEARCH__ + ? VPLocalSearchBoxOffline + : () => null`, + ), +] + +function patchNavBarSearch(source: string): string { + return applyRequiredPatches(source, navBarSearchPatches) +} + +// Vite's transform hook is called for many modules. Keep the dispatch table +// narrow so patches only run against known compiled VitePress client files. +const vitePressModulePatchers: VitePressModulePatcher[] = [ + { + match: moduleId => moduleId.endsWith('/shared.js'), + patch: patchVitePressShared, + }, + { + match: moduleId => moduleId.endsWith('/app/utils.js'), + patch: patchVitePressUtils, + }, + { + match: moduleId => moduleId.endsWith('/app/router.js'), + patch: patchVitePressRouter, + }, + { + match: moduleId => moduleId.endsWith('/app/index.js'), + patch: patchVitePressApp, + }, + { + match: (moduleId, id) => moduleId.endsWith('/theme-default/components/VPLocalSearchBox.vue') && !id.includes('?'), + patch: patchLocalSearchBox, + }, + { + match: (moduleId, id) => moduleId.endsWith('/theme-default/components/VPNavBarSearch.vue') && !id.includes('?'), + patch: patchNavBarSearch, + }, + { + match: moduleId => moduleId.endsWith('/theme-default/composables/outline.js'), + patch: patchOutline, + }, +] + +export function offlineVitePressPlugin(): Plugin { + let isSsrBuild = false + + return { + name: 'docs-offline-vitepress', + apply: 'build', + enforce: 'pre', + configResolved(config: ResolvedConfig) { + isSsrBuild = Boolean(config.build.ssr) + }, + transform(source: string, id: string) { + if (isSsrBuild) { + return null + } + + const moduleId = normalizeModuleId(id) + + if (!moduleId.includes(vitePressClientMarker)) { + return null + } + + const modulePatcher = vitePressModulePatchers.find(patcher => patcher.match(moduleId, id)) + + return modulePatcher ? modulePatcher.patch(source) : null + }, + } +} diff --git a/docs/vitepress/build-scripts/vitepress-po-locale-plugin.ts b/docs/vitepress/build-scripts/vitepress-po-locale-plugin.ts new file mode 100644 index 0000000000..2e77c6a4f4 --- /dev/null +++ b/docs/vitepress/build-scripts/vitepress-po-locale-plugin.ts @@ -0,0 +1,616 @@ +import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises' +import { basename, dirname, extname, join, posix, relative, resolve } from 'node:path' +import type { Plugin, ViteDevServer } from 'vite' +import gettextParser from 'gettext-parser' +import { isErrnoException, readDirSafe } from './shared/fs.ts' +import { docsDir, normalizePath } from './shared/paths.ts' +import type { LocalePage, SourcePage } from './shared/pages.ts' +import { getRootAssetRelPath } from './vitepress-root-pages-plugin.ts' + +const localeRootDir = resolve(docsDir, 'locale') +const localeMessagesDirName = 'LC_MESSAGES' +const generatedRootRelPath = 'vitepress/generated/locales' +const generatedRootDir = resolve(docsDir, generatedRootRelPath) + +// Fuzzy translations are useful while the migration is still being reviewed, +// but CI/release builds can opt out through DOCS_PO_INCLUDE_FUZZY=0. +const includeFuzzy = process.env.DOCS_PO_INCLUDE_FUZZY !== '0' + +// Only real documentation sources should be discovered. Generated output, +// VitePress internals, static assets, and locale sources would otherwise feed +// back into the PO generation pass. +const ignoredDocsMarkdownDirs = new Set(['.artifacts', '.vitepress', '_static', 'locale', 'node_modules', 'vitepress']) + +type PoLocaleOptions = { + prepareSources?: () => Promise | SourcePage[] + sourcePages?: SourcePage[] +} + +async function fileExists(path: string): Promise { + try { + const fileStat = await stat(path) + return fileStat.isFile() + } catch (error) { + if (isErrnoException(error) && error.code === 'ENOENT') { + return false + } + + throw error + } +} + +async function collectPoFiles(dir: string, baseDir = dir): Promise { + const entries = await readDirSafe(dir, { withFileTypes: true }) + const poFiles: string[] = [] + + for (const entry of entries) { + const entryPath = resolve(dir, entry.name) + + if (entry.isDirectory()) { + poFiles.push(...(await collectPoFiles(entryPath, baseDir))) + continue + } + + if (entry.isFile() && extname(entry.name) === '.po') { + poFiles.push(normalizePath(relative(baseDir, entryPath))) + } + } + + return poFiles +} + +async function collectDocsMarkdownFiles(dir = docsDir, baseDir = docsDir): Promise { + const entries = await readDirSafe(dir, { withFileTypes: true }) + const pages: SourcePage[] = [] + + for (const entry of entries) { + const entryPath = resolve(dir, entry.name) + + if (entry.isDirectory()) { + if (dir === docsDir && ignoredDocsMarkdownDirs.has(entry.name)) { + continue + } + + pages.push(...(await collectDocsMarkdownFiles(entryPath, baseDir))) + continue + } + + if (entry.isFile() && extname(entry.name) === '.md') { + pages.push({ + page: normalizePath(relative(baseDir, entryPath)), + sourcePath: entryPath, + }) + } + } + + return pages +} + +function getSourcePageCandidatesForPoRelPath(poRelPath: string): string[] { + const dir = dirname(poRelPath) + const name = basename(poRelPath, '.po') + + if (name !== 'index') { + return [normalizePath(join(dir === '.' ? '' : dir, `${name}.md`))] + } + + const parentDir = dir === '.' ? '' : dir + // gettext convention names the translated landing page index.po, while the + // source can be either README.md or index.md. + return [normalizePath(join(parentDir, 'README.md')), normalizePath(join(parentDir, 'index.md'))] +} + +function getGeneratedRelPath(locale: string, page: string): string { + return `${generatedRootRelPath}/${locale}/${page}` +} + +function getLocalizedOutputPath(locale: string, page: string): string { + if (basename(page) === 'README.md') { + // VitePress treats README.md as the directory index page. + const pageDir = dirname(page) + const localeDir = pageDir === '.' ? locale : `${locale}/${normalizePath(pageDir)}` + return `${localeDir}/index.md` + } + + return `${locale}/${page}` +} + +function getSourcePageMap(sourcePages: SourcePage[]): Map { + return new Map(sourcePages.map(page => [page.page, page])) +} + +async function collectMarkdownSourcePages(sourcePages: SourcePage[]): Promise { + const pages = new Map(sourcePages.map(page => [page.page, page])) + + // sourcePages already includes generated root pages such as README.md and + // CREDITS.md. Fill the rest from docs/ so translated links can resolve both + // root-generated pages and normal documentation pages. + for (const page of await collectDocsMarkdownFiles()) { + if (pages.has(page.page)) { + continue + } + + pages.set(page.page, page) + } + + return [...pages.values()] +} + +async function resolveSourcePageForPo( + locale: string, + poRelPath: string, + sourcePageMap: Map, +): Promise { + for (const page of getSourcePageCandidatesForPoRelPath(poRelPath)) { + const generatedSource = sourcePageMap.get(page) + if (generatedSource) { + // Root pages are normalized into docs/vitepress/generated/root before PO + // translation, so use that generated markdown as the source but keep the + // original path for dev-server watch invalidation. + return { + page, + sourcePath: generatedSource.sourcePath, + originalSourcePath: generatedSource.originalSourcePath || generatedSource.sourcePath, + } + } + + const sourcePath = resolve(docsDir, page) + + if (await fileExists(sourcePath)) { + return { page, sourcePath, originalSourcePath: sourcePath } + } + } + + const candidates = getSourcePageCandidatesForPoRelPath(poRelPath).join(' or ') + console.warn(`Skipping ${locale}/${poRelPath}: source page ${candidates} was not found.`) + return null +} + +export async function discoverPoLocalePages(options: PoLocaleOptions = {}): Promise { + const sourcePageMap = getSourcePageMap(options.sourcePages || []) + const localeDirs = await readDirSafe(localeRootDir, { withFileTypes: true }) + const discoveredPages = [] + + for (const localeDir of localeDirs) { + if (!localeDir.isDirectory()) { + continue + } + + const locale = localeDir.name + const localeMessagesDirPath = resolve(localeRootDir, locale, localeMessagesDirName) + const poFiles = await collectPoFiles(localeMessagesDirPath) + + for (const poRelPath of poFiles) { + const source = await resolveSourcePageForPo(locale, poRelPath, sourcePageMap) + if (!source) { + continue + } + + discoveredPages.push({ + locale, + page: source.page, + sourcePath: source.sourcePath, + originalSourcePath: source.originalSourcePath, + poPath: resolve(localeMessagesDirPath, poRelPath), + targetPath: resolve(docsDir, getGeneratedRelPath(locale, source.page)), + generatedRelPath: getGeneratedRelPath(locale, source.page), + outputPath: getLocalizedOutputPath(locale, source.page), + }) + } + } + + return discoveredPages.sort((a, b) => { + const localeOrder = a.locale.localeCompare(b.locale) + return localeOrder || a.page.localeCompare(b.page) + }) +} + +function parsePo(buffer: Buffer): Map { + const result = new Map() + const parsed = gettextParser.po.parse(buffer) + const table = parsed.translations[''] || {} + + for (const entry of Object.values(table)) { + const msgid = entry.msgid || '' + // gettext-parser exposes plural forms as msgstr array entries. The docs + // extractor only emits singular markdown strings, so the first entry is the + // one used for this pipeline. + const msgstr = Array.isArray(entry.msgstr) ? entry.msgstr[0] || '' : '' + const fuzzy = Boolean(entry.comments?.flag?.includes('fuzzy')) + + if (!msgid || !msgstr) { + continue + } + if (!includeFuzzy && fuzzy) { + continue + } + if (!result.has(msgid)) { + result.set(msgid, msgstr) + } + } + + return result +} + +async function readLocaleIndexTranslations(locale: string): Promise> { + const indexPoPath = resolve(localeRootDir, locale, localeMessagesDirName, 'index.po') + + if (!(await fileExists(indexPoPath))) { + return new Map() + } + + return parsePo(await readFile(indexPoPath)) +} + +const localeIndexTranslationsCache = new Map>>() + +export async function readLocaleIndexTranslationMap(locale: string): Promise> { + // The VitePress config asks for several theme labels from index.po. Cache the + // parse result so each locale only reads and parses the file once per config + // evaluation. + if (!localeIndexTranslationsCache.has(locale)) { + localeIndexTranslationsCache.set(locale, readLocaleIndexTranslations(locale)) + } + + return localeIndexTranslationsCache.get(locale) ?? new Map() +} + +function getMarkdownTitle(content: string): string | null { + const title = content.match(/^#\s+(.+?)\s*#*\s*$/m) + return title?.[1]?.trim() || null +} + +async function getLocalizedDocLinkAliases(locale: string, sourcePages: SourcePage[]): Promise> { + const translations = await readLocaleIndexTranslations(locale) + const aliases = new Map() + + if (!translations.size) { + return aliases + } + + for (const sourcePage of await collectMarkdownSourcePages(sourcePages)) { + // Users may write links to translated page titles in PO strings. Map those + // human-facing translated filenames back to the stable source filenames that + // VitePress actually generates. + const title = getMarkdownTitle(await readFile(sourcePage.sourcePath, 'utf8')) + const translatedTitle = title ? translations.get(title) : null + + if (!translatedTitle || translatedTitle === title) { + continue + } + + const sourcePagePath = normalizePath(sourcePage.page) + const sourcePageDir = posix.dirname(sourcePagePath) + const translatedFileName = `${translatedTitle}.md` + + aliases.set(translatedFileName, posix.basename(sourcePagePath)) + + if (sourcePageDir !== '.') { + aliases.set(`${sourcePageDir}/${translatedFileName}`, sourcePagePath) + } + } + + return aliases +} + +async function getLocalizedDocLinkAliasesByLocale( + localePages: LocalePage[], + sourcePages: SourcePage[], +): Promise>> { + const aliasesByLocale = new Map>() + + for (const locale of new Set(localePages.map(page => page.locale))) { + aliasesByLocale.set(locale, await getLocalizedDocLinkAliases(locale, sourcePages)) + } + + return aliasesByLocale +} + +function isUnsafeStandaloneToken(msgid: string): boolean { + // Plain identifiers are often INI values or code-like tokens. Translating + // them globally can corrupt examples, so only structured markdown contexts + // such as headings get a chance to use these translations. + return /^[A-Za-z][A-Za-z0-9_-]*$/.test(msgid) +} + +function translateExactText(text: string, translations: Map): string | null { + const translatedText = translations.get(text) + return translatedText && translatedText !== text ? translatedText : null +} + +function translateAtxMarkdownHeadingLine(line: string, translations: Map): string { + const atxHeading = line.match(/^([ \t]{0,3}#{1,6}[ \t]+)(.*?)([ \t]+#+[ \t]*)?$/u) + if (!atxHeading) { + return line + } + + const headingText = atxHeading[2].trim() + const translatedHeadingText = translateExactText(headingText, translations) + if (!translatedHeadingText) { + return line + } + + return `${atxHeading[1]}${translatedHeadingText}${atxHeading[3] || ''}` +} + +function translateSetextMarkdownHeadingLine(line: string, translations: Map): string { + const setextHeading = line.match(/^([ \t]{0,3})(\S.*?)\s*$/u) + const headingText = setextHeading?.[2].trim() || '' + const translatedHeadingText = translateExactText(headingText, translations) + if (!setextHeading || !translatedHeadingText) { + return line + } + + return `${setextHeading[1]}${translatedHeadingText}` +} + +function getMarkdownFenceMarker(line: string): string | null { + return line.match(/^[ \t]*(`{3,}|~{3,})/u)?.[1] || null +} + +function isClosingMarkdownFence(marker: string, openingMarker: string): boolean { + return marker[0] === openingMarker[0] && marker.length >= openingMarker.length +} + +function isSetextHeadingUnderline(line: string): boolean { + return Boolean(line.match(/^[ \t]{0,3}(?:=+|-+)[ \t]*$/u)) +} + +function translateMarkdownHeadings(content: string, translations: Map): string { + let openingFenceMarker: string | null = null + const lines = content.split('\n') + + // Headings need a separate pass because some valid headings are single words + // like "Migrating" or "Downloads", which are intentionally skipped by the + // later global replacement pass. + return lines + .map((line, index) => { + const fenceMarker = getMarkdownFenceMarker(line) + + // Keep code fences opaque. A line that looks like a markdown heading + // inside an INI or shell snippet must remain untranslated. + if (fenceMarker && (!openingFenceMarker || isClosingMarkdownFence(fenceMarker, openingFenceMarker))) { + openingFenceMarker = openingFenceMarker ? null : fenceMarker + return line + } + + if (openingFenceMarker) { + return line + } + + // Setext headings are two-line constructs, so the title line is only + // safe to translate when the following line is the underline marker. + const nextLine = lines[index + 1] || '' + if (isSetextHeadingUnderline(nextLine)) { + return translateSetextMarkdownHeadingLine(line, translations) + } + + return translateAtxMarkdownHeadingLine(line, translations) + }) + .join('\n') +} + +function getLineEnding(content: string): '\n' | '\r\n' { + return content.includes('\r\n') ? '\r\n' : '\n' +} + +function normalizeLineEndings(content: string): string { + return content.replace(/\r\n?/g, '\n') +} + +function compactMarkdownMediaCaptions(content: string): string { + const media = String.raw`!\[[^\]]*\]\([^)]+\)` + const caption = String.raw`\*[^\n]+\*` + const mediaCaption = new RegExp(`((?:${media}(?:\\s|\\n)+)+)(${caption})`, 'g') + + // Older PO files often keep image and caption in one logical Markdown line, + // while migrated sources may put them on adjacent lines for readability. + return content.replace(mediaCaption, match => match.replace(/\s*\n\s*/g, ' ')) +} + +function splitMarkdownMediaCaptions(content: string): string { + const media = String.raw`!\[[^\]]*\]\([^)]+\)` + const caption = String.raw`\*[^\n]+\*` + const mediaCaption = new RegExp(`((?:${media}\\s+)+)(${caption})`, 'g') + const mediaWithTrailingSpace = new RegExp(`(${media})\\s+`, 'g') + + return content.replace(mediaCaption, match => match.replace(mediaWithTrailingSpace, '$1\n')) +} + +function addNormalizedTranslation(translations: Map, msgid: string, msgstr: string): void { + if (!translations.has(msgid)) { + translations.set(msgid, msgstr) + } +} + +function restoreLineEndings(content: string, lineEnding: '\n' | '\r\n'): string { + return lineEnding === '\n' ? content : content.replace(/\n/g, lineEnding) +} + +function getRelativeGeneratedLink(fromGeneratedRelPath: string, targetGeneratedRelPath: string): string { + const relativePath = posix.relative(posix.dirname(fromGeneratedRelPath), targetGeneratedRelPath) + return relativePath.startsWith('.') ? relativePath : `./${relativePath}` +} + +function normalizeLocalizedDocLinks( + content: string, + page: LocalePage, + aliases: Map = new Map(), +): string { + const { generatedRelPath } = page + const rootAssets = new Set(['logo.png', 'logo-mono.png']) + + return content.replace(/\]\(([^)]+)\)/g, (full: string, target: string) => { + // Translated pages are generated into docs/vitepress/generated/locales. + // Relative links from the original markdown may therefore need to point + // back to generated root assets or untranslated source page names. + const assetMatch = target.match(/^([^#\s]+)(#[^)]+)?$/) + if (!assetMatch) { + return full + } + + const rawPath = assetMatch[1] + const hash = assetMatch[2] || '' + + if (/^(?:https?:|\/|#)/i.test(rawPath)) { + return full + } + + const rootAssetName = basename(rawPath) + if (rootAssets.has(rootAssetName)) { + const assetLink = getRelativeGeneratedLink(generatedRelPath, getRootAssetRelPath(rootAssetName)) + return `](${assetLink}${hash})` + } + + if (rawPath.startsWith('_static/')) { + return `](/${rawPath.slice('_static/'.length)}${hash})` + } + + if (!rawPath.endsWith('.md')) { + return full + } + + const mapped = aliases.get(rawPath) + if (!mapped) { + return full + } + + return `](${mapped}${hash})` + }) +} + +function translateMarkdown(content: string, translations: Map): string { + const lineEnding = getLineEnding(content) + let output = normalizeLineEndings(content) + const normalizedTranslations = new Map() + + // Normalize PO entries before matching so translators do not have to match + // the platform-specific line endings of the source markdown exactly. + for (const [msgid, msgstr] of translations.entries()) { + const normalizedMsgid = normalizeLineEndings(msgid) + const normalizedMsgstr = normalizeLineEndings(msgstr) + + addNormalizedTranslation(normalizedTranslations, normalizedMsgid, normalizedMsgstr) + addNormalizedTranslation( + normalizedTranslations, + compactMarkdownMediaCaptions(normalizedMsgid), + normalizedMsgstr, + ) + addNormalizedTranslation( + normalizedTranslations, + splitMarkdownMediaCaptions(normalizedMsgid), + normalizedMsgstr, + ) + } + + output = translateMarkdownHeadings(output, normalizedTranslations) + + // Longest-first replacement prevents a short msgid from partially replacing + // text that also has a more specific multi-word translation. + const entries = [...normalizedTranslations.entries()] + .filter(([msgid, msgstr]) => msgid && msgstr && msgid !== msgstr && !isUnsafeStandaloneToken(msgid)) + .sort((a, b) => b[0].length - a[0].length) + + for (const [msgid, msgstr] of entries) { + if (output.includes(msgid)) { + output = output.split(msgid).join(msgstr) + } + } + + return restoreLineEndings(output, lineEnding) +} + +export async function getPoLocaleRewrites(localePages: LocalePage[] | null = null, options: PoLocaleOptions = {}) { + const pages = localePages || (await discoverPoLocalePages(options)) + return Object.fromEntries(pages.map(page => [page.generatedRelPath, page.outputPath])) +} + +async function prepareSourcePages(options: PoLocaleOptions): Promise { + const preparedSourcePages = options.prepareSources ? await options.prepareSources() : null + + return options.sourcePages || preparedSourcePages || [] +} + +export async function generatePoLocalePages(options: PoLocaleOptions = {}): Promise { + const sourcePages = await prepareSourcePages(options) + const localePages = await discoverPoLocalePages({ ...options, sourcePages }) + const linkAliasesByLocale = await getLocalizedDocLinkAliasesByLocale(localePages, sourcePages) + + // Generated locale pages are disposable build artifacts. Removing the whole + // directory prevents stale pages from surviving after a source page or PO file + // has been deleted. + await rm(generatedRootDir, { recursive: true, force: true }) + + for (const page of localePages) { + await mkdir(dirname(page.targetPath), { recursive: true }) + + const [sourceText, poBuffer] = await Promise.all([readFile(page.sourcePath, 'utf8'), readFile(page.poPath)]) + const translatedText = normalizeLocalizedDocLinks( + translateMarkdown(sourceText, parsePo(poBuffer)), + page, + linkAliasesByLocale.get(page.locale), + ) + await writeFile(page.targetPath, translatedText) + } + + return localePages +} + +function shouldRegenerateForPath(changedPath: string, sourcePages: SourcePage[]): boolean { + const normalizedPath = normalizePath(resolve(changedPath)) + const normalizedLocaleRootDir = `${normalizePath(localeRootDir)}/` + const normalizedDocsDir = normalizePath(docsDir) + + if (normalizedPath.startsWith(normalizedLocaleRootDir) && normalizedPath.endsWith('.po')) { + return true + } + + for (const sourcePage of sourcePages) { + // Generated root pages are derived from files outside docs/. Watch their + // original paths, not only their generated markdown output. + const watchedSourcePath = sourcePage.originalSourcePath || sourcePage.sourcePath + if (normalizedPath === normalizePath(watchedSourcePath)) { + return true + } + } + + return dirname(normalizedPath) === normalizedDocsDir && normalizedPath.endsWith('.md') +} + +export function poLocalePlugin(options: PoLocaleOptions = {}): Plugin { + let pendingGeneration: Promise | null = null + const regenerate = async () => { + // Several watcher events can fire for one save. Share the same generation + // promise so VitePress does not run overlapping writes into generated/. + pendingGeneration ??= generatePoLocalePages(options).finally(() => { + pendingGeneration = null + }) + + await pendingGeneration + } + + return { + name: 'docs-po-locale-pages', + enforce: 'pre', + async buildStart() { + await regenerate() + }, + configureServer(server: ViteDevServer) { + const sourcePaths = (options.sourcePages || []).map(page => page.originalSourcePath || page.sourcePath) + server.watcher.add([localeRootDir, docsDir, ...sourcePaths]) + + const handleFilesystemChange = async (changedPath: string) => { + if (!shouldRegenerateForPath(changedPath, options.sourcePages || [])) { + return + } + + await regenerate() + server.ws.send({ type: 'full-reload' }) + } + + server.watcher.on('add', handleFilesystemChange) + server.watcher.on('change', handleFilesystemChange) + server.watcher.on('unlink', handleFilesystemChange) + }, + } +} diff --git a/docs/vitepress/build-scripts/vitepress-root-pages-plugin.ts b/docs/vitepress/build-scripts/vitepress-root-pages-plugin.ts new file mode 100644 index 0000000000..fa83b3607f --- /dev/null +++ b/docs/vitepress/build-scripts/vitepress-root-pages-plugin.ts @@ -0,0 +1,172 @@ +import { copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' +import type { Plugin, ViteDevServer } from 'vite' +import { docsDir, normalizePath, rootDir } from './shared/paths.ts' +import type { GeneratedPage } from './shared/pages.ts' +export type { GeneratedPage } from './shared/pages.ts' + +type RootSourcePage = { + page: string + outputPath: string + sourcePath: string + transform: (content: string) => string +} + +type RootGeneratedSourcePage = RootSourcePage & { + generatedRelPath: string + targetPath: string +} + +type RootAsset = { + fileName: string + sourcePath: string +} + +const generatedRootRelPath = 'vitepress/generated/root' +const generatedRootDir = resolve(docsDir, generatedRootRelPath) +const repositoryDocsUrl = 'https://github.com/Phobos-developers/Phobos/tree/develop/docs/' +let pendingRootPagesGeneration: Promise | null = null + +function normalizeReadmeForVitePress(content: string): string { + let output = content + + // Remove all markdown badges from docs README. + output = output.replace(/^\[!\[[^\]]*]\([^)]*\)]\([^)]*\)\s*$/gm, '') + output = output.replace(/\s*\[!\[[^\]]*]\([^)]*\)]\([^)]*\)\s*/g, ' ') + + // Render GitHub-style warning quotes as VitePress warning containers. + output = output.replace(/^> (?:\*\*Warning\*\*|\[!WARNING])\r?\n((?:> .+\r?\n)+)/m, (_match, body: string) => { + const warningText = body + .split(/\r?\n/) + .filter(Boolean) + .map((line: string) => line.replace(/^> ?/, '')) + .join('\n') + + return `::: warning\n${warningText}\n:::\n` + }) + + output = output.replace(/\[Official docs source\]\(docs\/?\)/u, `[Official docs source](${repositoryDocsUrl})`) + output = output.replace(/\]\(docs\/?\)/gu, `](${repositoryDocsUrl})`) + output = output.replace(/\]\((logo(?:-mono)?\.png)(#[^)]+)?\)/gu, (_match, path: string, hash = '') => { + return `](${path}${hash})` + }) + + // Clean up empty lines left after badge removal. + output = output.replace(/[ \t]+$/gm, '') + output = output.replace(/(?:\r?\n){3,}/g, '\n\n') + + return `${output.trim()}\n` +} + +function normalizeLicenseForVitePress(content: string): string { + return `# License\n\n${content.trim()}\n` +} + +const rootPages: RootSourcePage[] = [ + { + page: 'README.md', + outputPath: 'index.md', + sourcePath: resolve(rootDir, 'README.md'), + transform: normalizeReadmeForVitePress, + }, + { + page: 'CREDITS.md', + outputPath: 'CREDITS.md', + sourcePath: resolve(rootDir, 'CREDITS.md'), + transform: (content: string) => `${content.trim()}\n`, + }, + { + page: 'License.md', + outputPath: 'License.md', + sourcePath: resolve(rootDir, 'LICENSE.md'), + transform: normalizeLicenseForVitePress, + }, +] + +function getGeneratedRelPath(page: string): string { + return `${generatedRootRelPath}/${page}` +} + +const rootAssets: RootAsset[] = [ + { fileName: 'logo.png', sourcePath: resolve(rootDir, 'logo.png') }, + { fileName: 'logo-mono.png', sourcePath: resolve(rootDir, 'logo-mono.png') }, +] + +export function getRootAssetRelPath(fileName: string): string { + return getGeneratedRelPath(fileName) +} + +function getRootPagesWithGeneratedPaths(): RootGeneratedSourcePage[] { + return rootPages.map(page => ({ + ...page, + generatedRelPath: getGeneratedRelPath(page.page), + targetPath: resolve(docsDir, getGeneratedRelPath(page.page)), + })) +} + +export function getRootPageRewrites( + pages: GeneratedPage[] | RootGeneratedSourcePage[] = getRootPagesWithGeneratedPaths(), +): Record { + return Object.fromEntries(pages.map(page => [page.generatedRelPath, page.outputPath])) +} + +async function generateRootPagesOnce(): Promise { + const pages = getRootPagesWithGeneratedPaths() + + await rm(generatedRootDir, { recursive: true, force: true }) + + for (const page of pages) { + await mkdir(dirname(page.targetPath), { recursive: true }) + const sourceText = await readFile(page.sourcePath, 'utf8') + await writeFile(page.targetPath, page.transform(sourceText)) + } + + for (const asset of rootAssets) { + const targetPath = resolve(generatedRootDir, asset.fileName) + await mkdir(dirname(targetPath), { recursive: true }) + await copyFile(asset.sourcePath, targetPath) + } + + return pages.map(page => ({ + page: page.page, + sourcePath: page.targetPath, + originalSourcePath: page.sourcePath, + generatedRelPath: page.generatedRelPath, + outputPath: page.outputPath, + })) +} + +export async function generateRootPages(): Promise { + pendingRootPagesGeneration ??= generateRootPagesOnce().finally(() => { + pendingRootPagesGeneration = null + }) + + return pendingRootPagesGeneration +} + +export function rootPagesPlugin(): Plugin { + const sourcePaths = [...rootPages.map(page => page.sourcePath), ...rootAssets.map(asset => asset.sourcePath)] + const normalizedSourcePaths = new Set(sourcePaths.map(normalizePath)) + const regenerate = async () => { + await generateRootPages() + } + + return { + name: 'docs-root-pages', + enforce: 'pre', + async buildStart() { + await regenerate() + }, + configureServer(server: ViteDevServer) { + server.watcher.add(sourcePaths) + server.watcher.on('change', async (changedPath: string) => { + if (!normalizedSourcePaths.has(normalizePath(resolve(changedPath)))) { + return + } + + await regenerate() + server.ws.send({ type: 'full-reload' }) + }) + }, + } +} diff --git a/scripts/build_docs.bat b/scripts/build_docs.bat index 889a8e5edd..c744758b8c 100644 --- a/scripts/build_docs.bat +++ b/scripts/build_docs.bat @@ -1,9 +1,14 @@ @if not defined _echo echo off -rem Builds Phobos docs with Sphinx. +rem Builds docs with VitePress. rem Ensure we're in correct directory. cd /D "%~dp0" cd ..\docs -call make.bat html +if not exist node_modules ( + call npm ci + if errorlevel 1 exit /b %errorlevel% +) + +call npm run build diff --git a/scripts/build_docs_locale.bat b/scripts/build_docs_locale.bat deleted file mode 100644 index d8ca69b16e..0000000000 --- a/scripts/build_docs_locale.bat +++ /dev/null @@ -1,10 +0,0 @@ -@if not defined _echo echo off - -rem Builds Phobos docs with Sphinx. - -rem Ensure we're in correct directory. -cd /D "%~dp0" -cd ..\docs - -sphinx-build -b gettext ./ ./locale -sphinx-intl update -p ./locale -l zh_CN diff --git a/scripts/build_docs_offline.bat b/scripts/build_docs_offline.bat new file mode 100644 index 0000000000..e918c01ef9 --- /dev/null +++ b/scripts/build_docs_offline.bat @@ -0,0 +1,14 @@ +@if not defined _echo echo off + +rem Builds offline docs bundle with VitePress. + +rem Ensure we're in correct directory. +cd /D "%~dp0" +cd ..\docs + +if not exist node_modules ( + call npm ci + if errorlevel 1 exit /b %errorlevel% +) + +call npm run build:offline