[
  {
    "path": ".changeset/README.md",
    "content": "# Changesets\n\nRun `npm run changeset` to add release notes and version bumps.\n"
  },
  {
    "path": ".changeset/config.json",
    "content": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@3.0.0/schema.json\",\n  \"changelog\": \"@changesets/cli/changelog\",\n  \"commit\": false,\n  \"fixed\": [\n    [\n      \"@form2js/core\",\n      \"@form2js/dom\",\n      \"@form2js/form-data\",\n      \"@form2js/js2form\",\n      \"@form2js/jquery\",\n      \"@form2js/react\"\n    ]\n  ],\n  \"linked\": [],\n  \"access\": \"public\",\n  \"baseBranch\": \"master\",\n  \"updateInternalDependencies\": \"patch\",\n  \"ignore\": []\n}\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - master\n  pull_request:\n\npermissions:\n  contents: read\n\njobs:\n  checks:\n    name: checks (node ${{ matrix.node-version }})\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        node-version: [22.14.0]\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Node\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: npm\n\n      - name: Install\n        run: npm ci\n\n      - name: Lint\n        run: npm run lint\n\n      - name: Typecheck\n        run: npm run typecheck\n\n      - name: Test\n        run: npm run test:packages && npm run test:integration\n\n      - name: Build\n        run: npm run build\n\n      - name: Package Dry Run\n        run: npm run pack:dry-run\n\n  docs-e2e:\n    name: docs-e2e (node 22.14.0)\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22.14.0\n          cache: npm\n\n      - name: Install\n        run: npm ci\n\n      - name: Install Playwright Chromium\n        run: npm -w @form2js/docs exec playwright install --with-deps chromium\n\n      - name: Build workspace packages\n        run: npm run build:packages\n\n      - name: Test Docs E2E\n        run: npm run test:docs\n"
  },
  {
    "path": ".github/workflows/pages.yml",
    "content": "name: Deploy Docs Site\n\non:\n  push:\n    branches:\n      - master\n    paths:\n      - \"apps/docs/**\"\n      - \"docs/**\"\n      - \"packages/**\"\n      - \".github/workflows/pages.yml\"\n      - \"package.json\"\n      - \"package-lock.json\"\n      - \"turbo.json\"\n      - \"tsconfig.base.json\"\n  workflow_dispatch:\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\nconcurrency:\n  group: pages\n  cancel-in-progress: true\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22.14.0\n          cache: npm\n\n      - name: Install\n        run: npm ci\n\n      - name: Configure Pages\n        id: pages\n        uses: actions/configure-pages@v5\n\n      - name: Build workspace packages\n        run: npm run build:packages\n\n      - name: Build docs app\n        run: npm -w @form2js/docs run build\n        env:\n          DOCS_BASE_PATH: ${{ steps.pages.outputs.base_path }}\n\n      - name: Upload Pages artifact\n        uses: actions/upload-pages-artifact@v3\n        with:\n          path: apps/docs/dist\n\n  deploy:\n    runs-on: ubuntu-latest\n    needs: build\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    branches:\n      - master\n\npermissions:\n  contents: write\n  pull-requests: write\n  id-token: write\n\njobs:\n  release:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 22.14.0\n          registry-url: https://registry.npmjs.org\n          cache: npm\n\n      - name: Use npm 11\n        run: npm install --global npm@11.6.2\n\n      - name: Verify toolchain\n        run: node --version && npm --version\n\n      - name: Install\n        run: npm ci\n\n      - name: Build workspace packages\n        run: npm run build:packages\n\n      - name: Create release PR or publish\n        uses: changesets/action@v1\n        with:\n          version: npx changeset version\n          publish: npm run release\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          NPM_CONFIG_PROVENANCE: true\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n.npm\n.turbo\ndist\ncoverage\n*.tsbuildinfo\n.idea\n.hg\n.hgignore\n.astro\n**/.astro/\ndocs/triage-*\n.env\n.env.local\n.superpowers/brainstorm\n.worktrees/\n.worktrees\napps/docs/test-results/\napps/docs/playwright-report/\ndocs/superpowers/\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2010 Maxim Vasiliev\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# form2js\n\n🚀 **form2js is back — modernized and actively maintained.**\n\nOriginally created in 2010, now rewritten for modern JavaScript, TypeScript, ESM, React, and modular usage.\n\nLegacy version is available in the [legacy branch](https://github.com/maxatwork/form2js/tree/legacy).\n\nMigrating from legacy form2js? Start with the [migration guide](https://maxatwork.github.io/form2js/migrate/).\n\n## Description\n\nA small family of packages for turning form-shaped data into objects, and objects back into forms.\n\nIt is not a serializer, not an ORM, and not a new religion. It just does this one job, does it reliably, and leaves before anyone starts a committee about it.\n\n## Documentation\n\n- [Docs Site](https://maxatwork.github.io/form2js/) - overview, installation, unified playground, and published API reference.\n- [Migration Guide](https://maxatwork.github.io/form2js/migrate/) - map old `form2js` and `jquery.toObject` usage to the current package family.\n- [API Reference Source](docs/api-index.md) - markdown source for the published API docs page.\n\n## Migration from Legacy\n\nIf you are moving from the archived single-package version, start with the [migration guide](https://maxatwork.github.io/form2js/migrate/).\n\nQuick package map:\n\n- Legacy browser `form2js(...)` usage -> `@form2js/dom`\n- Legacy jQuery `$(\"#form\").toObject()` usage -> `@form2js/jquery`\n- Server or pipeline `FormData` parsing -> `@form2js/form-data`\n- React submit handling -> `@form2js/react`\n- Object back into fields -> `@form2js/js2form`\n\nThe current project keeps the naming rules and core parsing model, but splits the old browser-era API into environment-specific packages.\n\n## Packages\n\n| Package | npm | Purpose | Module | Standalone | Node.js |\n| ------- | --- | ------- | ------ | ---------- | ------- |\n| [`@form2js/react`](https://www.npmjs.com/package/@form2js/react) | [![npm version](https://img.shields.io/npm/v/%40form2js%2Freact?label=npm)](https://www.npmjs.com/package/@form2js/react) | React submit hook with parsing/validation state | Yes | No | Browser-focused |\n| [`@form2js/dom`](https://www.npmjs.com/package/@form2js/dom) | [![npm version](https://img.shields.io/npm/v/%40form2js%2Fdom?label=npm)](https://www.npmjs.com/package/@form2js/dom) | Extract DOM fields to object (`formToObject`, `form2js`) | Yes | Yes | With DOM shim (`jsdom`) |\n| [`@form2js/form-data`](https://www.npmjs.com/package/@form2js/form-data) | [![npm version](https://img.shields.io/npm/v/%40form2js%2Fform-data?label=npm)](https://www.npmjs.com/package/@form2js/form-data) | Convert `FormData`/entries to object | Yes | No | Yes |\n| [`@form2js/js2form`](https://www.npmjs.com/package/@form2js/js2form) | [![npm version](https://img.shields.io/npm/v/%40form2js%2Fjs2form?label=npm)](https://www.npmjs.com/package/@form2js/js2form) | Populate DOM fields from object (`objectToForm`, `js2form`) | Yes | No | With DOM shim (`jsdom`) |\n| [`@form2js/core`](https://www.npmjs.com/package/@form2js/core) | [![npm version](https://img.shields.io/npm/v/%40form2js%2Fcore?label=npm)](https://www.npmjs.com/package/@form2js/core) | Path parsing and object transformation engine | Yes | No | Yes |\n| [`@form2js/jquery`](https://www.npmjs.com/package/@form2js/jquery) | [![npm version](https://img.shields.io/npm/v/%40form2js%2Fjquery?label=npm)](https://www.npmjs.com/package/@form2js/jquery) | jQuery plugin adapter (`$.fn.toObject`) | Yes | Yes | Browser-focused |\n\n## Installation\n\nInstall only what you need:\n\n```bash\nnpm install @form2js/react react\nnpm install @form2js/dom\nnpm install @form2js/form-data\nnpm install @form2js/js2form\nnpm install @form2js/core\nnpm install @form2js/jquery jquery\n```\n\nFor browser standalone usage, use script builds where available:\n\n- `@form2js/dom`: `dist/standalone.global.js`\n- `@form2js/jquery`: `dist/standalone.global.js`\n\n## Usage\n\n### `@form2js/dom`\n\nHTML used in examples:\n\n```html\n<form id=\"profileForm\">\n  <input name=\"person.name.first\" value=\"Esme\" />\n  <input name=\"person.name.last\" value=\"Weatherwax\" />\n  <label\n    ><input type=\"checkbox\" name=\"person.tags[]\" value=\"witch\" checked />\n    witch</label\n  >\n</form>\n```\n\nModule:\n\n```ts\nimport { formToObject } from \"@form2js/dom\";\n\nconst result = formToObject(document.getElementById(\"profileForm\"));\n// => { person: { name: { first: \"Esme\", last: \"Weatherwax\" }, tags: [\"witch\"] } }\n```\n\nStandalone:\n\n```html\n<script src=\"https://unpkg.com/@form2js/dom/dist/standalone.global.js\"></script>\n<script>\n  const result = formToObject(document.getElementById(\"profileForm\"));\n  // or form2js(...)\n</script>\n```\n\n### `@form2js/form-data`\n\nModule (browser or Node 18+):\n\n```ts\nimport { formDataToObject } from \"@form2js/form-data\";\n\nconst fd = new FormData(formElement);\nconst result = formDataToObject(fd);\n```\n\nNode.js note:\n\n- Node 18+ has global `FormData`.\n- You can also pass iterable entries directly, which is handy in server pipelines:\n\n```ts\nimport { entriesToObject } from \"@form2js/form-data\";\n\nconst result = entriesToObject([\n  [\"person.name.first\", \"Sam\"],\n  [\"person.roles[]\", \"captain\"],\n]);\n// => { person: { name: { first: \"Sam\" }, roles: [\"captain\"] } }\n```\n\nWith schema validation (works with Zod or any `{ parse(unknown) }` schema):\n\n```ts\nimport { z } from \"zod\";\nimport { formDataToObject } from \"@form2js/form-data\";\n\nconst PersonSchema = z.object({\n  person: z.object({\n    age: z.coerce.number().int().min(0)\n  })\n});\n\nconst result = formDataToObject([[\"person.age\", \"42\"]], {\n  schema: PersonSchema\n});\n// => { person: { age: 42 } }\n```\n\nStandalone:\n\n- Not shipped for this package. Use module imports.\n\n### `@form2js/react`\n\nModule:\n\n```ts\nimport { z } from \"zod\";\nimport { useForm2js } from \"@form2js/react\";\n\nconst FormDataSchema = z.object({\n  person: z.object({\n    name: z.object({\n      first: z.string().min(1)\n    })\n  })\n});\n\nexport function ProfileForm(): React.JSX.Element {\n  const { onSubmit, isSubmitting, isError, error, isSuccess, reset } = useForm2js(\n    async (data) => {\n      // data is inferred from schema when schema is provided\n      await sendFormData(data);\n    },\n    {\n      schema: FormDataSchema\n    }\n  );\n\n  return (\n    <form\n      onSubmit={(event) => {\n        void onSubmit(event);\n      }}\n    >\n      <input name=\"person.name.first\" defaultValue=\"Sam\" />\n      <button type=\"submit\" disabled={isSubmitting}>\n        {isSubmitting ? \"Saving...\" : \"Save\"}\n      </button>\n      {isError ? <p>{String(error)}</p> : null}\n      {isSuccess ? <p>Saved</p> : null}\n      <button type=\"button\" onClick={reset}>\n        Reset state\n      </button>\n    </form>\n  );\n}\n```\n\n### `@form2js/jquery`\n\nHTML used in examples:\n\n```html\n<form id=\"profileForm\">\n  <input name=\"person.name.first\" value=\"Sam\" />\n  <input name=\"person.name.last\" value=\"Vimes\" />\n</form>\n```\n\nModule:\n\n```ts\nimport $ from \"jquery\";\nimport { installToObjectPlugin } from \"@form2js/jquery\";\n\ninstallToObjectPlugin($);\nconst data = $(\"#profileForm\").toObject({ mode: \"first\" });\n// => { person: { name: { first: \"Sam\", last: \"Vimes\" } } }\n```\n\nStandalone:\n\n```html\n<script src=\"https://code.jquery.com/jquery-3.7.1.min.js\"></script>\n<script src=\"https://unpkg.com/@form2js/jquery/dist/standalone.global.js\"></script>\n<script>\n  const data = $(\"#profileForm\").toObject({ mode: \"combine\" });\n</script>\n```\n\n### `@form2js/js2form`\n\nHTML used in examples (before calling `objectToForm`):\n\n```html\n<form id=\"profileForm\">\n  <input name=\"person.name.first\" />\n  <input name=\"person.name.last\" />\n</form>\n```\n\nModule:\n\n```ts\nimport { objectToForm } from \"@form2js/js2form\";\n\nobjectToForm(document.getElementById(\"profileForm\"), {\n  person: { name: { first: \"Tiffany\", last: \"Aching\" } },\n});\n// fields are now populated in the form\n```\n\nStandalone:\n\n- Not shipped as a dedicated global bundle. Use module imports.\n\n### `@form2js/core`\n\nModule:\n\n```ts\nimport { entriesToObject, objectToEntries } from \"@form2js/core\";\n\nconst data = entriesToObject([\n  { key: \"person.name.first\", value: \"Vimes\" },\n  { key: \"person.tags[]\", value: \"watch\" },\n]);\n\nconst pairs = objectToEntries(data);\n```\n\nNode.js:\n\n- Fully supported (no DOM dependency).\n\nStandalone:\n\n- Not shipped for this package. Use module imports.\n\n## Legacy behavior notes\n\nCompatibility with the old project is intentional.\n\n- Name paths define output shape (`person.name.first`).\n- Array and indexed syntax is preserved (`items[]`, `items[5].name`).\n- Rails-style names are supported (`rails[field][value]`).\n- DOM extraction follows native browser form submission semantics for checkbox and radio values.\n- Unsafe key path segments (`__proto__`, `prototype`, `constructor`) are rejected by default.\n- This library does data shaping, not JSON/XML serialization.\n\n## Design boundaries and non-goals\n\nThese boundaries are intentional and are used for issue triage.\n\n- Sparse indexes are compacted in first-seen order (`items[5]`, `items[8]` -> `items[0]`, `items[1]`).\n- Type inference is minimal by design; DOM extraction keeps native string values instead of coercing checkbox/radio fields.\n- Unchecked indexed controls are omitted and therefore do not reserve compacted array slots; include another submitted field when row identity matters.\n- `formToObject` reads successful form control values, not option labels. Disabled controls (including disabled fieldset descendants) and button-like inputs are excluded unless you explicitly opt in to disabled values.\n- `extractPairs`/`formToObject` support `nodeCallback`; return `SKIP_NODE` to exclude a node entirely, or `{ name|key, value }` to inject a custom entry.\n- Parser inputs reject unsafe path segments by default. Use `allowUnsafePathSegments: true` only with trusted inputs.\n- `objectToForm` supports `nodeCallback`; returning `false` skips the default assignment for that node.\n- `objectToForm` sets form control state and values; it does not dispatch synthetic `change` or `input` events.\n- Empty collections are not synthesized when no matching fields are present (for example, unchecked checkbox groups).\n- Dynamic key/value remapping (for example, converting `key`/`val` fields into arbitrary object keys) is application logic.\n- For file payloads and richer multipart semantics, use `FormData` and `@form2js/form-data`.\n\n## Contributing\n\n### Setup\n\n```bash\nnpm ci\n```\n\n### Local checks\n\n```bash\nnpm run lint\nnpm run typecheck\nnpm run test\nnpm run build\nnpm run pack:dry-run\n```\n\n### Local docs site\n\n```bash\nnpm run docs\nnpm run docs:build\n```\n\nThe homepage includes the unified playground for `@form2js/react`, `@form2js/dom`, `@form2js/jquery`, `@form2js/js2form`, `@form2js/core`, and `@form2js/form-data`.\n\n### GitHub Pages docs site\n\nThe published docs site is deployed by `.github/workflows/pages.yml`.\n\n- Trigger: pushes to `master` that touch `apps/docs/**`, `docs/**`, `packages/**`, `.github/workflows/pages.yml`, `package.json`, `package-lock.json`, `turbo.json`, or `tsconfig.base.json`, plus manual `workflow_dispatch`.\n- Output: `apps/docs/dist`.\n- URL: `https://maxatwork.github.io/form2js/`.\n\nIn repository settings, set Pages source to `GitHub Actions` once, and then the workflow handles updates.\n\n### Before opening a PR\n\n1. Keep changes focused to one problem area where possible.\n2. Add or update tests for behavior changes.\n3. Add a changeset (`npm run changeset`) for user-visible changes.\n4. Include migration notes in README if behavior or API changes.\n\n### Filing PRs and issues\n\nPlease include:\n\n- Clear expected vs actual behavior.\n- Minimal reproduction (HTML snippet or input entries).\n- Package name and version.\n- Environment (`node -v`, browser/version if relevant).\n\n## Release workflow\n\n- CI runs lint, typecheck, test, build, and package dry-run.\n- Releases are managed with Changesets and the published `@form2js/*` packages are versioned in lockstep.\n\n## Scope rewrite helper\n\nDefault scope is `@form2js/*`.\n\nIf you need to publish under another scope:\n\n```bash\nnpm run scope:rewrite -- --scope @your-scope --dry-run\nnpm run scope:rewrite -- --scope @your-scope\n```\n\nThis rewrites package names, internal dependencies, and import references.\n\n## License\n\nMIT, see `LICENSE`.\n"
  },
  {
    "path": "apps/docs/astro.config.mjs",
    "content": "import { defineConfig } from \"astro/config\";\nimport react from \"@astrojs/react\";\n\nconst base = process.env.DOCS_BASE_PATH ?? \"/\";\n\nexport default defineConfig({\n  base,\n  integrations: [react()],\n  vite: {\n    server: {\n      fs: {\n        allow: [\"../..\"]\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "apps/docs/package.json",
    "content": "{\n  \"name\": \"@form2js/docs\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"astro dev\",\n    \"build\": \"astro build\",\n    \"preview\": \"astro preview\",\n    \"test\": \"vitest run\",\n    \"test:e2e\": \"playwright test\",\n    \"lint\": \"eslint \\\"src/**/*.{ts,tsx}\\\" \\\"test/**/*.{ts,tsx}\\\" \\\"test-e2e/**/*.ts\\\"\",\n    \"typecheck\": \"astro sync && tsc --noEmit\",\n    \"clean\": \"rimraf dist .astro\"\n  },\n  \"dependencies\": {\n    \"@astrojs/react\": \"^5.0.2\",\n    \"@form2js/core\": \"3.4.0\",\n    \"@form2js/dom\": \"3.4.0\",\n    \"@form2js/form-data\": \"3.4.0\",\n    \"@form2js/jquery\": \"3.4.0\",\n    \"@form2js/js2form\": \"3.4.0\",\n    \"@form2js/react\": \"3.4.0\",\n    \"astro\": \"^6.1.1\",\n    \"jquery\": \"^3.7.1\",\n    \"react\": \"^19.1.1\",\n    \"react-dom\": \"^19.1.1\",\n    \"rehype-stringify\": \"^10.0.1\",\n    \"remark-gfm\": \"^4.0.1\",\n    \"remark-parse\": \"^11.0.0\",\n    \"remark-rehype\": \"^11.1.2\",\n    \"unified\": \"^11.0.5\",\n    \"zod\": \"^4.1.5\"\n  },\n  \"devDependencies\": {\n    \"@playwright/test\": \"^1.54.2\",\n    \"@types/jquery\": \"^3.5.33\",\n    \"@types/react\": \"^19.1.12\",\n    \"@types/react-dom\": \"^19.1.9\"\n  }\n}\n"
  },
  {
    "path": "apps/docs/playwright.config.ts",
    "content": "import { defineConfig, devices } from \"@playwright/test\";\n\nconst docsE2eHost = process.env.DOCS_E2E_HOST ?? \"127.0.0.1\";\nconst docsE2ePort = Number(process.env.DOCS_E2E_PORT ?? \"4321\");\nconst docsE2eUrl = `http://${docsE2eHost}:${docsE2ePort}/`;\n\nexport default defineConfig({\n  testDir: \"./test-e2e\",\n  use: {\n    baseURL: docsE2eUrl,\n    trace: \"retain-on-failure\"\n  },\n  projects: [\n    {\n      name: \"chromium\",\n      use: {\n        ...devices[\"Desktop Chrome\"]\n      }\n    }\n  ],\n  webServer: {\n    command:\n      `PUBLIC_DOCS_E2E_FAULTS=1 npm -w @form2js/docs run build && npm -w @form2js/docs run preview -- --host ${docsE2eHost} --port ${docsE2ePort}`,\n    url: docsE2eUrl,\n    reuseExistingServer: !process.env.CI\n  }\n});\n"
  },
  {
    "path": "apps/docs/src/components/api/ApiPackageNav.tsx",
    "content": "import React from \"react\";\n\nimport type { ApiPackageEntry } from \"../../lib/api-packages\";\nimport { apiPackageDocsPath } from \"../../lib/site-routes\";\n\ntype ApiPackageNavEntry = Pick<ApiPackageEntry, \"slug\" | \"packageName\">;\n\ninterface ApiPackageNavProps {\n  activeSlug?: ApiPackageNavEntry[\"slug\"];\n  basePath: string;\n  packages: ApiPackageNavEntry[];\n}\n\nexport function ApiPackageNav({\n  activeSlug,\n  basePath,\n  packages\n}: ApiPackageNavProps): React.JSX.Element {\n  return (\n    <nav aria-label=\"API packages\" className=\"api-package-nav\">\n      <p className=\"api-package-nav__eyebrow\">Packages</p>\n      <ul className=\"api-package-nav__list\">\n        {packages.map((entry) => (\n          <li key={entry.slug}>\n            <a\n              aria-current={entry.slug === activeSlug ? \"page\" : undefined}\n              className=\"api-package-nav__link\"\n              href={apiPackageDocsPath(basePath, entry.slug)}\n            >\n              {entry.packageName}\n            </a>\n          </li>\n        ))}\n      </ul>\n    </nav>\n  );\n}\n"
  },
  {
    "path": "apps/docs/src/components/api/ApiPackageSummaryList.tsx",
    "content": "import React from \"react\";\n\nimport type { ApiPackageEntry } from \"../../lib/api-packages\";\nimport { apiPackageDocsPath } from \"../../lib/site-routes\";\n\ntype ApiPackageSummaryEntry = Pick<\n  ApiPackageEntry,\n  \"slug\" | \"packageName\" | \"summary\"\n>;\n\ninterface ApiPackageSummaryListProps {\n  basePath: string;\n  packages: ApiPackageSummaryEntry[];\n}\n\nexport function ApiPackageSummaryList({\n  basePath,\n  packages\n}: ApiPackageSummaryListProps): React.JSX.Element {\n  return (\n    <section aria-labelledby=\"api-package-list\" className=\"api-package-summary-list\">\n      <h2 id=\"api-package-list\">Packages</h2>\n      <div className=\"api-package-summary-list__items\">\n        {packages.map((entry) => (\n          <article className=\"api-package-summary\" key={entry.slug}>\n            <h3>\n              <a href={apiPackageDocsPath(basePath, entry.slug)}>{entry.packageName}</a>\n            </h3>\n            <p>{entry.summary}</p>\n          </article>\n        ))}\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/docs/src/components/api/ApiToc.tsx",
    "content": "import React, { useEffect, useMemo, useState } from \"react\";\n\nimport type { ApiHeading } from \"../../lib/api-docs-source\";\n\ninterface ApiTocProps {\n  headings: ApiHeading[];\n  initialActiveSlug?: string;\n}\n\ninterface TocGroup {\n  heading: ApiHeading;\n  children: ApiHeading[];\n}\n\nfunction groupHeadings(headings: ApiHeading[]): TocGroup[] {\n  const groups: TocGroup[] = [];\n\n  for (const heading of headings) {\n    if (heading.depth === 2 || groups.length === 0) {\n      groups.push({\n        heading,\n        children: []\n      });\n      continue;\n    }\n\n    groups[groups.length - 1]?.children.push(heading);\n  }\n\n  return groups;\n}\n\nexport function ApiToc({ headings, initialActiveSlug }: ApiTocProps): React.JSX.Element {\n  const groups = useMemo(() => groupHeadings(headings), [headings]);\n  const [activeSlug, setActiveSlug] = useState(initialActiveSlug ?? headings[0]?.slug ?? \"\");\n\n  useEffect(() => {\n    if (typeof window === \"undefined\") {\n      return;\n    }\n\n    const hashSlug = window.location.hash.replace(/^#/, \"\");\n    if (hashSlug) {\n      setActiveSlug(hashSlug);\n    }\n\n    const observedHeadings = headings\n      .map((heading) => document.getElementById(heading.slug))\n      .filter((heading): heading is HTMLElement => Boolean(heading));\n\n    if (observedHeadings.length === 0 || typeof IntersectionObserver === \"undefined\") {\n      return;\n    }\n\n    const observer = new IntersectionObserver(\n      (entries) => {\n        const visibleEntry = entries\n          .filter((entry) => entry.isIntersecting)\n          .sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];\n\n        if (visibleEntry?.target.id) {\n          setActiveSlug(visibleEntry.target.id);\n        }\n      },\n      {\n        rootMargin: \"-20% 0px -60% 0px\",\n        threshold: [0.2, 0.6, 1]\n      }\n    );\n\n    for (const heading of observedHeadings) {\n      observer.observe(heading);\n    }\n\n    const handleHashChange = (): void => {\n      const nextHashSlug = window.location.hash.replace(/^#/, \"\");\n      if (nextHashSlug) {\n        setActiveSlug(nextHashSlug);\n      }\n    };\n\n    window.addEventListener(\"hashchange\", handleHashChange);\n\n    return () => {\n      observer.disconnect();\n      window.removeEventListener(\"hashchange\", handleHashChange);\n    };\n  }, [headings]);\n\n  return (\n    <nav aria-label=\"On this page\" className=\"api-toc\">\n      <p className=\"api-toc__eyebrow\">On this page</p>\n      <ul className=\"api-toc__list\">\n        {groups.map((group) => (\n          <li key={group.heading.slug}>\n            <a\n              aria-current={activeSlug === group.heading.slug ? \"true\" : undefined}\n              className=\"api-toc__link\"\n              href={`#${group.heading.slug}`}\n              onClick={() => {\n                setActiveSlug(group.heading.slug);\n              }}\n            >\n              {group.heading.text}\n            </a>\n            {group.children.length > 0 ? (\n              <ul className=\"api-toc__sublist\">\n                {group.children.map((child) => (\n                  <li key={child.slug}>\n                    <a\n                      aria-current={activeSlug === child.slug ? \"true\" : undefined}\n                      className=\"api-toc__sublink\"\n                      href={`#${child.slug}`}\n                      onClick={() => {\n                        setActiveSlug(child.slug);\n                      }}\n                    >\n                      {child.text}\n                    </a>\n                  </li>\n                ))}\n              </ul>\n            ) : null}\n          </li>\n        ))}\n      </ul>\n    </nav>\n  );\n}\n"
  },
  {
    "path": "apps/docs/src/components/landing/ApiDocsCta.astro",
    "content": "---\n// apps/docs/src/components/landing/ApiDocsCta.astro\nimport { apiDocsPath } from \"../../lib/site-routes\";\nconst basePath = import.meta.env.BASE_URL;\n---\n\n<section class=\"section content-wrap\">\n  <p class=\"section-eyebrow\">Reference</p>\n  <h2 class=\"section-title\">API Documentation</h2>\n  <p class=\"section-desc\">\n    Exact signatures, option defaults, TypeScript types, and compatibility\n    notes for every package — all generated from the same source of truth.\n  </p>\n  <a class=\"btn-primary\" href={apiDocsPath(basePath)}>Open API Docs →</a>\n</section>\n"
  },
  {
    "path": "apps/docs/src/components/landing/Hero.astro",
    "content": "---\n// apps/docs/src/components/landing/Hero.astro\nimport { apiDocsPath } from \"../../lib/site-routes\";\nconst basePath = import.meta.env.BASE_URL;\n---\n\n<section class=\"hero\">\n  <p class=\"hero__eyebrow\">form serialization library</p>\n  <h1 class=\"hero__title\">Turn forms<br />into objects.</h1>\n  <p class=\"hero__desc\">\n    Parse browser forms into structured JavaScript objects. Six adapters —\n    React hooks, vanilla DOM, jQuery, FormData, and more. One coherent API.\n  </p>\n  <div class=\"hero__actions\">\n    <span class=\"hero__install\">\n      <span class=\"hero__install-prefix\">$</span>\n      <span id=\"hero-install-pkg\">npm install @form2js/react react</span>\n    </span>\n    <a class=\"btn-primary\" href=\"#playground\">Try the playground ↓</a>\n    <a class=\"btn-ghost\" href={apiDocsPath(basePath)}>API Docs →</a>\n  </div>\n</section>\n\n<script>\n  window.addEventListener(\"form2js:variant-change\", (event) => {\n    if (!(event instanceof CustomEvent)) return;\n\n    const detail = event.detail;\n    const variantId = typeof detail?.variantId === \"string\" ? detail.variantId : null;\n    const firstPackage =\n      Array.isArray(detail?.packages) && typeof detail.packages[0] === \"string\"\n        ? detail.packages[0]\n        : null;\n    const el = document.getElementById(\"hero-install-pkg\");\n\n    if (!el) return;\n\n    if (variantId === \"react\") {\n      el.textContent = \"npm install @form2js/react react\";\n      return;\n    }\n\n    if (variantId === \"jquery\") {\n      el.textContent = \"npm install @form2js/jquery jquery\";\n      return;\n    }\n\n    if (firstPackage) {\n      el.textContent = `npm install ${firstPackage}`;\n    }\n  });\n</script>\n"
  },
  {
    "path": "apps/docs/src/components/landing/InstallSection.astro",
    "content": "---\n// apps/docs/src/components/landing/InstallSection.astro\n---\n\n<section class=\"section content-wrap\" id=\"install\">\n  <p class=\"section-eyebrow\">Use in your project</p>\n  <div class=\"install-grid\">\n    <div>\n      <h2 class=\"section-title\">Install</h2>\n      <p class=\"section-desc\">\n        Pick the adapter for your stack. All packages share the same\n        path-syntax so switching is painless.\n      </p>\n    </div>\n    <div>\n      <span id=\"install-cmd\" class=\"install-cmd\">npm install @form2js/react react</span>\n      <pre id=\"install-snippet\" class=\"code-block\">{`import { useForm2js } from '@form2js/react'\n\nconst { onSubmit, isSubmitting } = useForm2js(handler)\n\n// nested names just work:\n// <input name=\"user.email\" />\n// → { user: { email: '…' } }`}</pre>\n      <p id=\"install-alt-label\" class=\"section-desc\" hidden>Browser global via &lt;script&gt;</p>\n      <pre id=\"install-alt-snippet\" class=\"code-block\" hidden></pre>\n    </div>\n  </div>\n</section>\n\n<script>\n  window.addEventListener(\"form2js:variant-change\", (event) => {\n    if (!(event instanceof CustomEvent)) return;\n\n    const detail = event.detail;\n    const cmdEl = document.getElementById(\"install-cmd\");\n    const snippetEl = document.getElementById(\"install-snippet\");\n    const altLabelEl = document.getElementById(\"install-alt-label\");\n    const altSnippetEl = document.getElementById(\"install-alt-snippet\");\n    const variantId = typeof detail?.variantId === \"string\" ? detail.variantId : null;\n\n    let install = null;\n\n    switch (variantId) {\n      case \"react\":\n        install = {\n          command: \"npm install @form2js/react react\",\n          snippet: `import { useForm2js } from '@form2js/react'\n\nconst { onSubmit, isSubmitting } = useForm2js(handler)`,\n        };\n        break;\n      case \"form\":\n        install = {\n          command: \"npm install @form2js/dom\",\n          snippet: `import { formToObject } from '@form2js/dom'\n\nconst data = formToObject(formElement)`,\n          altLabel: \"Browser global via <script>\",\n          altSnippet: `<script src=\"https://unpkg.com/@form2js/dom/dist/standalone.global.js\"><\\/script>\n<script>\n  const data = formToObject(formElement)\n<\\/script>`,\n        };\n        break;\n      case \"jquery\":\n        install = {\n          command: \"npm install @form2js/jquery jquery\",\n          snippet: `import { installToObjectPlugin } from '@form2js/jquery'\n\ninstallToObjectPlugin($)\nconst data = $('form').toObject()`,\n          altLabel: \"Browser global via <script>\",\n          altSnippet: `<script src=\"https://code.jquery.com/jquery-3.7.1.min.js\"><\\/script>\n<script src=\"https://unpkg.com/@form2js/jquery/dist/standalone.global.js\"><\\/script>\n<script>\n  const data = $('form').toObject()\n<\\/script>`,\n        };\n        break;\n      case \"js2form\":\n        install = {\n          command: \"npm install @form2js/js2form\",\n          snippet: `import { objectToForm } from '@form2js/js2form'\n\nobjectToForm(formElement, { user: { name: 'Alice' } })`,\n        };\n        break;\n      case \"core\":\n        install = {\n          command: \"npm install @form2js/core\",\n          snippet: `import { entriesToObject } from '@form2js/core'\n\nconst data = entriesToObject([\n  { key: 'user.name', value: 'Alice' }\n])`,\n        };\n        break;\n      case \"form-data\":\n        install = {\n          command: \"npm install @form2js/form-data\",\n          snippet: `import { formDataToObject } from '@form2js/form-data'\n\nconst data = formDataToObject(new FormData(formElement))`,\n        };\n        break;\n      default:\n        install = null;\n    }\n\n    if (cmdEl && install?.command) cmdEl.textContent = install.command;\n    if (snippetEl && install?.snippet) snippetEl.textContent = install.snippet;\n\n    if (altLabelEl) altLabelEl.hidden = !install?.altLabel;\n    if (altSnippetEl) altSnippetEl.hidden = !install?.altSnippet;\n    if (altLabelEl && install?.altLabel) altLabelEl.textContent = install.altLabel;\n    if (altSnippetEl) altSnippetEl.textContent = install?.altSnippet ?? \"\";\n  });\n</script>\n"
  },
  {
    "path": "apps/docs/src/components/playground/PlaygroundShell.tsx",
    "content": "// apps/docs/src/components/playground/PlaygroundShell.tsx\nimport React, { useEffect, useState } from \"react\";\n\nimport { VARIANT_IDS, variantsById } from \"./variant-registry\";\nimport { ResultPanel } from \"./ResultPanel\";\nimport type { ErrorInfo, OutputState, VariantDefinition, VariantId } from \"./types\";\nimport { VariantHeader } from \"./VariantHeader\";\n\nfunction getRequestedRenderFault(): { variantId: VariantId; message: string } | null {\n  if (typeof window === \"undefined\") return null;\n  const requestedFault = new URLSearchParams(window.location.search).get(\"__fault\");\n  if (!requestedFault) return null;\n  const [requestedVariantId, source] = requestedFault.split(\":\");\n  if (source !== \"render\" || !VARIANT_IDS.includes(requestedVariantId as VariantId)) return null;\n  const variantId = requestedVariantId as VariantId;\n  return { variantId, message: `Injected render fault for ${variantsById[variantId].label}` };\n}\n\nfunction getActiveVariantId(): VariantId {\n  if (typeof window === \"undefined\") return \"react\";\n  const current = new URLSearchParams(window.location.search).get(\"variant\");\n  if (current && VARIANT_IDS.includes(current as VariantId)) return current as VariantId;\n  return \"react\";\n}\n\nfunction createInitialOutputStates(\n  variantId: VariantId\n): Partial<Record<VariantId, OutputState>> {\n  return { [variantId]: variantsById[variantId].createInitialOutputState() };\n}\n\nfunction dispatchVariantChange(variantId: VariantId): void {\n  if (typeof window === \"undefined\") return;\n  const variant = variantsById[variantId];\n  window.dispatchEvent(\n    new CustomEvent(\"form2js:variant-change\", {\n      detail: { variantId, packages: variant.packages }\n    })\n  );\n}\n\ninterface VariantErrorBoundaryProps {\n  children: React.ReactNode;\n  onError: (errorInfo: ErrorInfo) => void;\n}\ninterface VariantErrorBoundaryState { hasError: boolean; }\n\nclass VariantErrorBoundary extends React.Component<VariantErrorBoundaryProps, VariantErrorBoundaryState> {\n  state: VariantErrorBoundaryState = { hasError: false };\n  static getDerivedStateFromError(): VariantErrorBoundaryState { return { hasError: true }; }\n  componentDidCatch(error: Error): void {\n    this.props.onError({ message: error.message, source: \"render\" });\n  }\n  render(): React.ReactNode {\n    if (this.state.hasError) return null;\n    return this.props.children;\n  }\n}\n\nexport function PlaygroundShell(): React.JSX.Element {\n  const [activeVariantId, setActiveVariantId] = useState<VariantId>(() => getActiveVariantId());\n  const [mountedVariantIds, setMountedVariantIds] = useState<VariantId[]>(() => [getActiveVariantId()]);\n  const [outputStates, setOutputStates] = useState<Partial<Record<VariantId, OutputState>>>(() =>\n    createInitialOutputStates(getActiveVariantId())\n  );\n  const [failedVariants, setFailedVariants] = useState<Partial<Record<VariantId, ErrorInfo>>>({});\n\n  useEffect(() => {\n    dispatchVariantChange(activeVariantId);\n  }, []);\n\n  useEffect(() => {\n    if (!mountedVariantIds.includes(activeVariantId)) {\n      setMountedVariantIds((current) => [...current, activeVariantId]);\n    }\n    setOutputStates((current) => {\n      if (current[activeVariantId]) return current;\n      return { ...current, [activeVariantId]: variantsById[activeVariantId].createInitialOutputState() };\n    });\n  }, [activeVariantId, mountedVariantIds]);\n\n  useEffect(() => {\n    const requestedFault = getRequestedRenderFault();\n    if (requestedFault?.variantId !== activeVariantId || failedVariants[activeVariantId] !== undefined) {\n      return;\n    }\n\n    handleVariantFailure(activeVariantId, { message: requestedFault.message, source: \"render\" });\n  }, [activeVariantId, failedVariants]);\n\n  const activeVariant = variantsById[activeVariantId];\n  const activeOutputState = outputStates[activeVariantId] ?? activeVariant.createInitialOutputState();\n  const variants = VARIANT_IDS.map((variantId) => variantsById[variantId]);\n\n  function handleVariantFailure(variantId: VariantId, errorInfo: ErrorInfo): void {\n    setFailedVariants((current) => ({ ...current, [variantId]: errorInfo }));\n    setOutputStates((current) => {\n      return Object.fromEntries(\n        Object.entries(current).filter(([currentVariantId]) => currentVariantId !== variantId)\n      ) as Partial<Record<VariantId, OutputState>>;\n    });\n  }\n\n  function selectVariant(variantId: VariantId): void {\n    if (typeof window !== \"undefined\") {\n      const nextUrl = new URL(window.location.href);\n      nextUrl.searchParams.set(\"variant\", variantId);\n      window.history.replaceState({}, \"\", `${nextUrl.pathname}${nextUrl.search}`);\n      dispatchVariantChange(variantId);\n    }\n    setActiveVariantId(variantId);\n  }\n\n  return (\n    <section aria-label=\"Unified playground\" className=\"pg-shell\">\n      <VariantHeader activeId={activeVariantId} onSelect={selectVariant} variants={variants} />\n      <div className=\"pg-body\">\n        <div className=\"pg-form-col\">\n          <p className=\"pg-variant-summary\">{activeVariant.summary}</p>\n          {mountedVariantIds.map((variantId) => {\n            const variant: VariantDefinition = variantsById[variantId];\n            const isActive = variantId === activeVariantId;\n            if (failedVariants[variantId]) return null;\n            return (\n              <div hidden={!isActive} key={variantId}>\n                <VariantErrorBoundary\n                  onError={(errorInfo) => {\n                    handleVariantFailure(variantId, errorInfo);\n                  }}\n                >\n                  <variant.Component\n                    isActive={isActive}\n                    onOutputChange={(outputState) => {\n                      setOutputStates((current) => ({ ...current, [variantId]: outputState }));\n                    }}\n                    reportFatalError={(errorInfo) => {\n                      handleVariantFailure(variantId, errorInfo);\n                    }}\n                  />\n                </VariantErrorBoundary>\n              </div>\n            );\n          })}\n          {failedVariants[activeVariantId] && (\n            <section aria-label=\"Failed variant\">\n              <p>{activeVariant.label} failed to load.</p>\n              <p>{failedVariants[activeVariantId]?.message}</p>\n            </section>\n          )}\n        </div>\n        <div className=\"pg-output-col\">\n          <ResultPanel outputState={activeOutputState} />\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/docs/src/components/playground/ReactInspectorPanel.tsx",
    "content": "import React from \"react\";\n\nimport type { ReactOutputState } from \"./types\";\n\ninterface ReactInspectorPanelProps {\n  outputState: ReactOutputState;\n}\n\nexport function ReactInspectorPanel({ outputState }: ReactInspectorPanelProps): React.JSX.Element {\n  const metaEntries = outputState.meta ? Object.entries(outputState.meta) : [];\n  const hasParsedPayload = outputState.parsedPayload !== null;\n\n  return (\n    <section aria-label=\"React output\">\n      <h2>Submit state</h2>\n      <p>{outputState.statusMessage}</p>\n      <p>isSubmitting: {String(outputState.submitFlags.isSubmitting)}</p>\n      <p>isError: {String(outputState.submitFlags.isError)}</p>\n      <p>isSuccess: {String(outputState.submitFlags.isSuccess)}</p>\n      {metaEntries.length > 0 ? (\n        <dl>\n          {metaEntries.map(([key, value]) => (\n            <React.Fragment key={key}>\n              <dt>{key}</dt>\n              <dd>{value === null ? \"null\" : typeof value === \"boolean\" ? String(value) : value}</dd>\n            </React.Fragment>\n          ))}\n        </dl>\n      ) : null}\n      {hasParsedPayload ? <pre>{JSON.stringify(outputState.parsedPayload, null, 2)}</pre> : null}\n      {outputState.error ? <p>{outputState.error.message}</p> : null}\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/docs/src/components/playground/ResultPanel.tsx",
    "content": "// apps/docs/src/components/playground/ResultPanel.tsx\nimport React from \"react\";\nimport type { OutputState } from \"./types\";\n\ninterface ResultPanelProps {\n  outputState: OutputState;\n}\n\nfunction formatZodErrors(error: { message: string }): string[] {\n  // The error message from react-variant is pre-formatted as \"path: msg\\npath: msg\"\n  return error.message.split(\"\\n\").filter(Boolean);\n}\n\nexport function ResultPanel({ outputState }: ResultPanelProps): React.JSX.Element {\n  const statusClass = `status-${outputState.status}`;\n\n  if (outputState.kind === \"react\") {\n    const { submitFlags, error, parsedPayload, meta } = outputState;\n    const errorLines = error ? formatZodErrors(error) : [];\n    const metaEntries = meta ? Object.entries(meta) : [];\n\n    return (\n      <div>\n        <p className=\"result-eyebrow\">Output</p>\n        <h2>Submit state</h2>\n        <span className={`status-badge ${statusClass}`}>\n          {outputState.statusMessage}\n        </span>\n        <div className=\"result-flags\">\n          <span className=\"flag-key\">isSubmitting</span>\n          <span className={submitFlags.isSubmitting ? \"flag-true\" : \"flag-false\"}>\n            {String(submitFlags.isSubmitting)}\n          </span>\n          <span className=\"flag-key\">isError</span>\n          <span className={submitFlags.isError ? \"flag-true\" : \"flag-false\"}>\n            {String(submitFlags.isError)}\n          </span>\n          <span className=\"flag-key\">isSuccess</span>\n          <span className={submitFlags.isSuccess ? \"flag-true\" : \"flag-false\"}>\n            {String(submitFlags.isSuccess)}\n          </span>\n        </div>\n        {metaEntries.length > 0 && (\n          <dl>\n            {metaEntries.map(([key, value]) => (\n              <React.Fragment key={key}>\n                <dt>{key}</dt>\n                <dd>{value === null ? \"null\" : typeof value === \"boolean\" ? String(value) : value}</dd>\n              </React.Fragment>\n            ))}\n          </dl>\n        )}\n        {errorLines.length > 0 && (\n          <div className=\"result-errors\">\n            {errorLines.map((line, i) => (\n              <p key={i} className=\"result-error-item\">\n                {line}\n              </p>\n            ))}\n          </div>\n        )}\n        {parsedPayload !== null ? (\n          <pre className=\"result-json\">{JSON.stringify(parsedPayload, null, 2)}</pre>\n        ) : (\n          <p className=\"result-empty\">\n            {outputState.status === \"idle\" ? \"Submit the form to see parsed output.\" : \"\"}\n          </p>\n        )}\n      </div>\n    );\n  }\n\n  // standard kind\n  const { errorMessage, parsedPayload, statusMessage } = outputState;\n  return (\n    <div>\n      <p className=\"result-eyebrow\">Output</p>\n      <h2>Parsed result</h2>\n      <span className={`status-badge ${statusClass}`}>{statusMessage}</span>\n      {errorMessage && (\n        <div className=\"result-errors\">\n          <p className=\"result-error-item\">{errorMessage}</p>\n        </div>\n      )}\n      {parsedPayload !== null ? (\n        <pre className=\"result-json\">{JSON.stringify(parsedPayload, null, 2)}</pre>\n      ) : (\n        <p className=\"result-empty\">\n          {outputState.status === \"idle\" ? \"Run the variant to see parsed output.\" : \"\"}\n        </p>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/docs/src/components/playground/StandardResultPanel.tsx",
    "content": "import React from \"react\";\n\nimport type { StandardOutputState } from \"./types\";\n\ninterface StandardResultPanelProps {\n  outputState: StandardOutputState;\n}\n\nexport function StandardResultPanel({ outputState }: StandardResultPanelProps): React.JSX.Element {\n  const hasParsedPayload = outputState.parsedPayload !== null;\n\n  return (\n    <section aria-label=\"Standard output\">\n      <h2>Parsed result</h2>\n      <p>{outputState.statusMessage}</p>\n      {outputState.errorMessage ? <p>{outputState.errorMessage}</p> : null}\n      {hasParsedPayload ? <pre>{JSON.stringify(outputState.parsedPayload, null, 2)}</pre> : null}\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/docs/src/components/playground/VariantHeader.tsx",
    "content": "// apps/docs/src/components/playground/VariantHeader.tsx\nimport React from \"react\";\nimport type { VariantDefinition, VariantId } from \"./types\";\n\ninterface VariantHeaderProps {\n  activeId: VariantId;\n  variants: VariantDefinition[];\n  onSelect: (variantId: VariantId) => void;\n}\n\nexport function VariantHeader({ activeId, variants, onSelect }: VariantHeaderProps): React.JSX.Element {\n  return (\n    <div className=\"pg-tabs\" aria-label=\"Variant switcher\">\n      {variants.map((variant) => (\n        <button\n          key={variant.id}\n          aria-pressed={variant.id === activeId}\n          className=\"pg-tab-btn\"\n          data-variant-id={variant.id}\n          onClick={() => {\n            onSelect(variant.id);\n          }}\n          type=\"button\"\n        >\n          {variant.label}\n        </button>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/docs/src/components/playground/bootstrap/jquery-bootstrap.ts",
    "content": "import $ from \"jquery\";\nimport { installToObjectPlugin } from \"@form2js/jquery\";\n\ntype JQueryWithPlugin = typeof $ & {\n  fn: {\n    toObject?: unknown;\n  };\n};\n\nlet installedPlugin: unknown = null;\n\nexport function ensureJqueryBootstrap(): unknown {\n  const jquery = $ as JQueryWithPlugin;\n\n  if (typeof jquery.fn.toObject !== \"function\") {\n    installToObjectPlugin(jquery);\n  }\n\n  installedPlugin = jquery.fn.toObject ?? null;\n  return installedPlugin;\n}\n"
  },
  {
    "path": "apps/docs/src/components/playground/types.ts",
    "content": "import type { ReactNode } from \"react\";\n\nexport type VariantKind = \"react\" | \"standard\";\nexport type OutputStatus = \"idle\" | \"running\" | \"success\" | \"error\";\nexport type VariantId = \"react\" | \"form\" | \"jquery\" | \"js2form\" | \"core\" | \"form-data\";\n\nexport interface ErrorInfo {\n  message: string;\n  source: \"render\" | \"effect\" | \"bootstrap\" | \"event\" | \"async\";\n  detail?: string;\n}\n\nexport interface ReactOutputState {\n  kind: \"react\";\n  status: OutputStatus;\n  statusMessage: string;\n  submitFlags: {\n    isSubmitting: boolean;\n    isError: boolean;\n    isSuccess: boolean;\n  };\n  error: { message: string; detail?: string } | null;\n  parsedPayload: unknown;\n  meta?: Record<string, ReactNode>;\n}\n\nexport interface StandardOutputState {\n  kind: \"standard\";\n  status: OutputStatus;\n  statusMessage: string;\n  errorMessage: string | null;\n  parsedPayload: unknown;\n}\n\nexport type OutputState = ReactOutputState | StandardOutputState;\n\nexport interface VariantComponentProps {\n  isActive: boolean;\n  onOutputChange: (outputState: OutputState) => void;\n  reportFatalError: (errorInfo: ErrorInfo) => void;\n}\n\nexport interface VariantDefinition {\n  id: VariantId;\n  kind: VariantKind;\n  label: string;\n  summary: string;\n  packages: string[];\n  createInitialOutputState: () => OutputState;\n  Component: (props: VariantComponentProps) => ReactNode;\n}\n"
  },
  {
    "path": "apps/docs/src/components/playground/variant-registry.ts",
    "content": "import type { OutputState, VariantDefinition, VariantId } from \"./types\";\nimport { CoreVariant } from \"./variants/core-variant\";\nimport { FormDataVariant } from \"./variants/form-data-variant\";\nimport { FormVariant } from \"./variants/form-variant\";\nimport { JQueryVariant } from \"./variants/jquery-variant\";\nimport { Js2FormVariant } from \"./variants/js2form-variant\";\nimport { ReactVariant } from \"./variants/react-variant\";\n\nfunction createStandardIdle(statusMessage: string): OutputState {\n  return {\n    kind: \"standard\",\n    status: \"idle\",\n    statusMessage,\n    errorMessage: null,\n    parsedPayload: null\n  };\n}\n\nfunction createReactIdle(statusMessage: string): OutputState {\n  return {\n    kind: \"react\",\n    status: \"idle\",\n    statusMessage,\n    submitFlags: {\n      isSubmitting: false,\n      isError: false,\n      isSuccess: false\n    },\n    error: null,\n    parsedPayload: null\n  };\n}\n\nexport const variants = [\n  {\n    id: \"react\",\n    kind: \"react\",\n    label: \"React\",\n    summary: \"Submit forms with schema-aware async state.\",\n    packages: [\"@form2js/react\"],\n    createInitialOutputState: () => createReactIdle(\"Ready to submit.\")\n  },\n  {\n    id: \"form\",\n    kind: \"standard\",\n    label: \"Form\",\n    summary: \"Parse a plain browser form with @form2js/dom or form2js().\",\n    packages: [\"@form2js/dom\"],\n    createInitialOutputState: () => createStandardIdle(\"Ready to parse the form.\")\n  },\n  {\n    id: \"jquery\",\n    kind: \"standard\",\n    label: \"jQuery\",\n    summary: \"Use the jQuery plugin adapter with selectable modes.\",\n    packages: [\"@form2js/jquery\"],\n    createInitialOutputState: () => createStandardIdle(\"Ready to run the plugin.\")\n  },\n  {\n    id: \"js2form\",\n    kind: \"standard\",\n    label: \"js2form\",\n    summary: \"Apply object data back into form controls.\",\n    packages: [\"@form2js/js2form\"],\n    createInitialOutputState: () => createStandardIdle(\"Ready to apply object data.\")\n  },\n  {\n    id: \"core\",\n    kind: \"standard\",\n    label: \"Core\",\n    summary: \"Parse raw key/value entries into nested objects.\",\n    packages: [\"@form2js/core\"],\n    createInitialOutputState: () => createStandardIdle(\"Ready to parse entry data.\")\n  },\n  {\n    id: \"form-data\",\n    kind: \"standard\",\n    label: \"FormData\",\n    summary: \"Convert FormData-like entries into structured objects.\",\n    packages: [\"@form2js/form-data\"],\n    createInitialOutputState: () => createStandardIdle(\"Ready to parse form data.\")\n  }\n] satisfies Omit<VariantDefinition, \"Component\">[];\n\nexport const VARIANT_IDS = variants.map((variant) => variant.id) satisfies VariantId[];\n\nconst variantComponents: Record<VariantId, VariantDefinition[\"Component\"]> = {\n  react: ReactVariant,\n  form: FormVariant,\n  jquery: JQueryVariant,\n  js2form: Js2FormVariant,\n  core: CoreVariant,\n  \"form-data\": FormDataVariant\n};\n\nexport const variantsById = Object.fromEntries(\n  variants.map((variant) => [\n    variant.id,\n    {\n      ...variant,\n      Component: variantComponents[variant.id]\n    }\n  ])\n) as Record<VariantId, VariantDefinition>;\n"
  },
  {
    "path": "apps/docs/src/components/playground/variants/core-variant.tsx",
    "content": "// apps/docs/src/components/playground/variants/core-variant.tsx\nimport React, { useRef } from \"react\";\nimport { entriesToObject } from \"@form2js/core\";\n\nimport type { StandardOutputState, VariantComponentProps } from \"../types\";\n\nconst INITIAL_ENTRIES_JSON = `[\n  { \"key\": \"person.name.first\", \"value\": \"Moist\" },\n  { \"key\": \"person.name.last\", \"value\": \"von Lipwig\" },\n  { \"key\": \"person.city\", \"value\": \"ankh-morpork\" },\n  { \"key\": \"person.guild\", \"value\": \"thieves\" },\n  { \"key\": \"person.tags[]\", \"value\": \"crime\" },\n  { \"key\": \"person.tags[]\", \"value\": \"banking\" }\n]`;\n\nfunction createErrorState(message: string): StandardOutputState {\n  return { kind: \"standard\", status: \"error\", statusMessage: \"core parse failed.\", errorMessage: message, parsedPayload: null };\n}\n\nfunction createSuccessState(parsedPayload: unknown): StandardOutputState {\n  return { kind: \"standard\", status: \"success\", statusMessage: \"@form2js/core -> entriesToObject(entry objects)\", errorMessage: null, parsedPayload };\n}\n\nfunction formatVariantError(error: unknown): string {\n  if (error instanceof Error) {\n    return error.message;\n  }\n\n  return String(error);\n}\n\nexport function CoreVariant({ onOutputChange }: VariantComponentProps): React.JSX.Element {\n  const jsonInputRef = useRef<HTMLTextAreaElement>(null);\n\n  function handleRun(): void {\n    const jsonInput = jsonInputRef.current;\n    if (!jsonInput) return;\n\n    let parsed: { key: string; value: unknown }[];\n\n    try {\n      parsed = JSON.parse(jsonInput.value) as { key: string; value: unknown }[];\n    } catch {\n      onOutputChange(createErrorState(\"JSON parse error: please provide valid entry-object JSON before parsing core entries.\"));\n      return;\n    }\n\n    try {\n      onOutputChange(createSuccessState(entriesToObject(parsed)));\n    } catch (error: unknown) {\n      onOutputChange(createErrorState(`Core conversion failed: ${formatVariantError(error)}`));\n    }\n  }\n\n  return (\n    <section aria-label=\"Core variant\">\n      <div className=\"pg-field\">\n        <label className=\"pg-label\" htmlFor=\"core-entries\">entry objects</label>\n        <textarea\n          className=\"pg-textarea\"\n          defaultValue={INITIAL_ENTRIES_JSON}\n          id=\"core-entries\"\n          name=\"core-entries-json\"\n          ref={jsonInputRef}\n          rows={10}\n        />\n      </div>\n      <div className=\"pg-btns\">\n        <button className=\"pg-btn\" onClick={handleRun} type=\"button\">Run @form2js/core</button>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/docs/src/components/playground/variants/form-data-variant.tsx",
    "content": "// apps/docs/src/components/playground/variants/form-data-variant.tsx\nimport React, { useRef } from \"react\";\nimport { formDataToObject } from \"@form2js/form-data\";\n\nimport type { StandardOutputState, VariantComponentProps } from \"../types\";\n\nfunction createIdleState(): StandardOutputState {\n  return { kind: \"standard\", status: \"idle\", statusMessage: \"Ready to parse form data.\", errorMessage: null, parsedPayload: null };\n}\n\nfunction createSuccessState(parsedPayload: unknown): StandardOutputState {\n  return { kind: \"standard\", status: \"success\", statusMessage: \"@form2js/form-data -> formDataToObject(form)\", errorMessage: null, parsedPayload };\n}\n\nexport function FormDataVariant({ onOutputChange }: VariantComponentProps): React.JSX.Element {\n  const formRef = useRef<HTMLFormElement>(null);\n\n  function handleSubmit(event: React.SyntheticEvent<HTMLFormElement>): void {\n    event.preventDefault();\n    const form = formRef.current;\n    if (!form) { onOutputChange(createIdleState()); return; }\n    onOutputChange(createSuccessState(formDataToObject(new FormData(form))));\n  }\n\n  function handleReset(): void {\n    formRef.current?.reset();\n    onOutputChange(createIdleState());\n  }\n\n  return (\n    <section aria-label=\"FormData variant\">\n      <form ref={formRef} onSubmit={handleSubmit}>\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"fd-first\">person.name.first</label>\n          <input className=\"pg-input\" defaultValue=\"Tiffany\" id=\"fd-first\" name=\"person.name.first\" type=\"text\" />\n        </div>\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"fd-last\">person.name.last</label>\n          <input className=\"pg-input\" defaultValue=\"Aching\" id=\"fd-last\" name=\"person.name.last\" type=\"text\" />\n        </div>\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"fd-city\">person.city</label>\n          <select className=\"pg-select\" defaultValue=\"quirm\" id=\"fd-city\" name=\"person.city\">\n            <option value=\"ankh-morpork\">Ankh-Morpork</option>\n            <option value=\"lancre\">Lancre</option>\n            <option value=\"quirm\">Quirm</option>\n            <option value=\"sto-lat\">Sto Lat</option>\n          </select>\n        </div>\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"fd-guild\">person.guild</label>\n          <select className=\"pg-select\" defaultValue=\"witches\" id=\"fd-guild\" name=\"person.guild\">\n            <option value=\"watchman\">watchman</option>\n            <option value=\"witches\">witches</option>\n            <option value=\"assassins\">assassins</option>\n            <option value=\"thieves\">thieves</option>\n          </select>\n        </div>\n        <fieldset className=\"pg-fieldset\">\n          <legend>person.tags[]</legend>\n          <label className=\"pg-check-label\">\n            <input defaultChecked name=\"person.tags[]\" type=\"checkbox\" value=\"witch\" />witch\n          </label>\n          <label className=\"pg-check-label\">\n            <input name=\"person.tags[]\" type=\"checkbox\" value=\"shepherding\" />shepherding\n          </label>\n        </fieldset>\n        <div className=\"pg-btns\">\n          <button className=\"pg-btn\" type=\"submit\">Run @form2js/form-data</button>\n          <button className=\"pg-btn pg-btn-secondary\" onClick={handleReset} type=\"button\">Reset</button>\n        </div>\n      </form>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/docs/src/components/playground/variants/form-variant.tsx",
    "content": "// apps/docs/src/components/playground/variants/form-variant.tsx\nimport React, { useRef } from \"react\";\nimport { form2js, formToObject } from \"@form2js/dom\";\n\nimport type { StandardOutputState, VariantComponentProps } from \"../types\";\n\nfunction createIdleState(): StandardOutputState {\n  return { kind: \"standard\", status: \"idle\", statusMessage: \"Ready to parse the form.\", errorMessage: null, parsedPayload: null };\n}\n\nfunction createSuccessState(statusMessage: string, parsedPayload: unknown): StandardOutputState {\n  return { kind: \"standard\", status: \"success\", statusMessage, errorMessage: null, parsedPayload };\n}\n\nexport function FormVariant({ onOutputChange }: VariantComponentProps): React.JSX.Element {\n  const formRef = useRef<HTMLFormElement>(null);\n\n  function handleSubmit(event: React.SyntheticEvent<HTMLFormElement>): void {\n    event.preventDefault();\n    const form = formRef.current;\n    if (!form) { onOutputChange(createIdleState()); return; }\n    onOutputChange(createSuccessState(\"@form2js/dom -> formToObject(form)\", formToObject(form)));\n  }\n\n  function handleLegacyRun(): void {\n    const form = formRef.current;\n    if (!form) { onOutputChange(createIdleState()); return; }\n    onOutputChange(createSuccessState(\"@form2js/dom -> form2js(form)\", form2js(form)));\n  }\n\n  function handleReset(): void {\n    formRef.current?.reset();\n    onOutputChange(createIdleState());\n  }\n\n  return (\n    <section aria-label=\"Form variant\">\n      <form ref={formRef} onSubmit={handleSubmit}>\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"fv-first\">person.name.first</label>\n          <input className=\"pg-input\" defaultValue=\"Esme\" id=\"fv-first\" name=\"person.name.first\" type=\"text\" />\n        </div>\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"fv-last\">person.name.last</label>\n          <input className=\"pg-input\" defaultValue=\"Weatherwax\" id=\"fv-last\" name=\"person.name.last\" type=\"text\" />\n        </div>\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"fv-city\">person.city</label>\n          <select className=\"pg-select\" defaultValue=\"lancre\" id=\"fv-city\" name=\"person.city\">\n            <option value=\"ankh-morpork\">Ankh-Morpork</option>\n            <option value=\"lancre\">Lancre</option>\n            <option value=\"quirm\">Quirm</option>\n            <option value=\"sto-lat\">Sto Lat</option>\n          </select>\n        </div>\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"fv-guild\">person.guild</label>\n          <select className=\"pg-select\" defaultValue=\"witches\" id=\"fv-guild\" name=\"person.guild\">\n            <option value=\"watchman\">watchman</option>\n            <option value=\"witches\">witches</option>\n            <option value=\"assassins\">assassins</option>\n            <option value=\"thieves\">thieves</option>\n          </select>\n        </div>\n        <fieldset className=\"pg-fieldset\">\n          <legend>person.tags[]</legend>\n          <label className=\"pg-check-label\">\n            <input defaultChecked name=\"person.tags[]\" type=\"checkbox\" value=\"witch\" />witch\n          </label>\n          <label className=\"pg-check-label\">\n            <input defaultChecked name=\"person.tags[]\" type=\"checkbox\" value=\"headology\" />headology\n          </label>\n          <label className=\"pg-check-label\">\n            <input name=\"person.tags[]\" type=\"checkbox\" value=\"crime\" />crime\n          </label>\n        </fieldset>\n        <label className=\"pg-check-label\" style={{ marginBottom: \"1rem\" }}>\n          <input name=\"person.approved\" type=\"checkbox\" value=\"true\" />person.approved\n        </label>\n        <div className=\"pg-btns\">\n          <button className=\"pg-btn\" type=\"submit\">Run @form2js/dom</button>\n          <button className=\"pg-btn pg-btn-secondary\" onClick={handleReset} type=\"button\">Reset</button>\n        </div>\n      </form>\n      <div style={{ marginTop: \"0.75rem\" }}>\n        <button className=\"pg-btn pg-btn-secondary\" onClick={handleLegacyRun} type=\"button\">\n          Run form2js() (legacy)\n        </button>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/docs/src/components/playground/variants/jquery-variant.tsx",
    "content": "// apps/docs/src/components/playground/variants/jquery-variant.tsx\nimport React, { useEffect, useRef } from \"react\";\nimport $ from \"jquery\";\n\nimport { ensureJqueryBootstrap } from \"../bootstrap/jquery-bootstrap\";\nimport type { StandardOutputState, VariantComponentProps } from \"../types\";\n\ntype JQueryToObjectOptions = { mode?: \"first\" | \"all\" | \"combine\" };\ntype JQueryCollectionWithToObject = JQuery & {\n  toObject?: (options?: JQueryToObjectOptions) => unknown;\n};\n\nfunction createIdleState(): StandardOutputState {\n  return { kind: \"standard\", status: \"idle\", statusMessage: \"Ready to run the plugin.\", errorMessage: null, parsedPayload: null };\n}\n\nfunction createSuccessState(statusMessage: string, parsedPayload: unknown): StandardOutputState {\n  return { kind: \"standard\", status: \"success\", statusMessage, errorMessage: null, parsedPayload };\n}\n\nfunction getParsedPayload(\n  root: HTMLDivElement,\n  mode: \"first\" | \"all\" | \"combine\"\n): unknown {\n  const collection = $(root).find(\".jq-slice\") as JQueryCollectionWithToObject;\n  return collection.toObject?.({ mode }) ?? null;\n}\n\nexport function JQueryVariant({ onOutputChange }: VariantComponentProps): React.JSX.Element {\n  const sourceRef = useRef<HTMLDivElement>(null);\n  const modeRef = useRef<HTMLSelectElement>(null);\n\n  useEffect(() => { ensureJqueryBootstrap(); }, []);\n\n  function handleRun(): void {\n    ensureJqueryBootstrap();\n    const root = sourceRef.current;\n    const mode = (modeRef.current?.value ?? \"combine\") as \"first\" | \"all\" | \"combine\";\n    if (!root) return;\n    const parsedPayload = getParsedPayload(root, mode);\n    onOutputChange(createSuccessState(`@form2js/jquery -> $(\".jq-slice\").toObject({ mode: \"${mode}\" })`, parsedPayload));\n  }\n\n  function handleReset(): void {\n    const root = sourceRef.current;\n    if (root) {\n      root.querySelectorAll<HTMLFormElement>(\"form\").forEach((form) => {\n        form.reset();\n      });\n    }\n    onOutputChange(createIdleState());\n  }\n\n  return (\n    <section aria-label=\"jQuery variant\">\n      <div ref={sourceRef}>\n        <form className=\"jq-slice\">\n          <div className=\"pg-field\">\n            <label className=\"pg-label\">person.first</label>\n            <input className=\"pg-input\" defaultValue=\"Gytha\" name=\"person.first\" type=\"text\" />\n          </div>\n          <div className=\"pg-field\">\n            <label className=\"pg-label\">person.last</label>\n            <input className=\"pg-input\" defaultValue=\"Ogg\" name=\"person.last\" type=\"text\" />\n          </div>\n        </form>\n        <form className=\"jq-slice\">\n          <div className=\"pg-field\">\n            <label className=\"pg-label\">person.city</label>\n            <select className=\"pg-select\" defaultValue=\"lancre\" name=\"person.city\">\n              <option value=\"ankh-morpork\">Ankh-Morpork</option>\n              <option value=\"lancre\">Lancre</option>\n              <option value=\"quirm\">Quirm</option>\n            </select>\n          </div>\n          <div className=\"pg-field\">\n            <label className=\"pg-label\">person.guild</label>\n            <select className=\"pg-select\" defaultValue=\"witches\" name=\"person.guild\">\n              <option value=\"watchman\">watchman</option>\n              <option value=\"witches\">witches</option>\n              <option value=\"assassins\">assassins</option>\n            </select>\n          </div>\n        </form>\n      </div>\n      <div className=\"pg-field\" style={{ marginTop: \"0.75rem\" }}>\n        <label className=\"pg-label\" htmlFor=\"jq-mode\">Mode</label>\n        <select className=\"pg-select\" defaultValue=\"combine\" id=\"jq-mode\" name=\"jquery-mode\" ref={modeRef}>\n          <option value=\"first\">first</option>\n          <option value=\"all\">all</option>\n          <option value=\"combine\">combine</option>\n        </select>\n      </div>\n      <div className=\"pg-btns\">\n        <button className=\"pg-btn\" onClick={handleRun} type=\"button\">Run @form2js/jquery</button>\n        <button className=\"pg-btn pg-btn-secondary\" onClick={handleReset} type=\"button\">Reset</button>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/docs/src/components/playground/variants/js2form-variant.tsx",
    "content": "// apps/docs/src/components/playground/variants/js2form-variant.tsx\nimport React, { useRef } from \"react\";\nimport { formToObject } from \"@form2js/dom\";\nimport { objectToForm } from \"@form2js/js2form\";\n\nimport type { StandardOutputState, VariantComponentProps } from \"../types\";\n\nconst INITIAL_JSON = `{\n  \"person\": {\n    \"name\": {\n      \"first\": \"Tiffany\",\n      \"last\": \"Aching\"\n    },\n    \"city\": \"quirm\",\n    \"tags\": [\"witch\"]\n  }\n}`;\n\nfunction createIdleState(): StandardOutputState {\n  return { kind: \"standard\", status: \"idle\", statusMessage: \"Ready to apply object data.\", errorMessage: null, parsedPayload: null };\n}\n\nfunction createErrorState(message: string): StandardOutputState {\n  return { kind: \"standard\", status: \"error\", statusMessage: \"js2form apply failed.\", errorMessage: message, parsedPayload: null };\n}\n\nfunction createSuccessState(parsedPayload: unknown): StandardOutputState {\n  return { kind: \"standard\", status: \"success\", statusMessage: \"@form2js/js2form -> objectToForm(...), then formToObject(...)\", errorMessage: null, parsedPayload };\n}\n\nfunction formatVariantError(error: unknown): string {\n  if (error instanceof Error) {\n    return error.message;\n  }\n\n  return String(error);\n}\n\nexport function Js2FormVariant({ onOutputChange }: VariantComponentProps): React.JSX.Element {\n  const formRef = useRef<HTMLFormElement>(null);\n  const jsonInputRef = useRef<HTMLTextAreaElement>(null);\n\n  function handleApply(): void {\n    const form = formRef.current;\n    const jsonInput = jsonInputRef.current;\n    if (!form || !jsonInput) { onOutputChange(createIdleState()); return; }\n\n    let parsed: unknown;\n\n    try {\n      parsed = JSON.parse(jsonInput.value) as unknown;\n    } catch {\n      onOutputChange(createErrorState(\"JSON parse error: please provide valid JSON before applying js2form.\"));\n      return;\n    }\n\n    try {\n      objectToForm(form, parsed);\n      onOutputChange(createSuccessState(formToObject(form)));\n    } catch (error: unknown) {\n      onOutputChange(createErrorState(`js2form runtime error: ${formatVariantError(error)}`));\n    }\n  }\n\n  function handleReset(): void {\n    const form = formRef.current;\n    const jsonInput = jsonInputRef.current;\n    if (form) form.reset();\n    if (jsonInput) jsonInput.value = INITIAL_JSON;\n    onOutputChange(createIdleState());\n  }\n\n  return (\n    <section aria-label=\"js2form variant\">\n      <div className=\"pg-field\">\n        <label className=\"pg-label\" htmlFor=\"j2f-json\">input.json</label>\n        <textarea className=\"pg-textarea\" defaultValue={INITIAL_JSON} id=\"j2f-json\" name=\"js2form-json\" ref={jsonInputRef} rows={10} />\n      </div>\n      <form ref={formRef}>\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"j2f-first\">person.name.first</label>\n          <input className=\"pg-input\" defaultValue=\"Esme\" id=\"j2f-first\" name=\"person.name.first\" type=\"text\" />\n        </div>\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"j2f-last\">person.name.last</label>\n          <input className=\"pg-input\" defaultValue=\"Weatherwax\" id=\"j2f-last\" name=\"person.name.last\" type=\"text\" />\n        </div>\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"j2f-city\">person.city</label>\n          <select className=\"pg-select\" defaultValue=\"lancre\" id=\"j2f-city\" name=\"person.city\">\n            <option value=\"ankh-morpork\">Ankh-Morpork</option>\n            <option value=\"lancre\">Lancre</option>\n            <option value=\"quirm\">Quirm</option>\n          </select>\n        </div>\n        <fieldset className=\"pg-fieldset\">\n          <legend>person.tags[]</legend>\n          <label className=\"pg-check-label\">\n            <input defaultChecked name=\"person.tags[]\" type=\"checkbox\" value=\"witch\" />witch\n          </label>\n          <label className=\"pg-check-label\">\n            <input defaultChecked name=\"person.tags[]\" type=\"checkbox\" value=\"headology\" />headology\n          </label>\n        </fieldset>\n      </form>\n      <div className=\"pg-btns\">\n        <button className=\"pg-btn\" onClick={handleApply} type=\"button\">Apply js2form</button>\n        <button className=\"pg-btn pg-btn-secondary\" onClick={handleReset} type=\"button\">Reset form</button>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/docs/src/components/playground/variants/react-variant.tsx",
    "content": "// apps/docs/src/components/playground/variants/react-variant.tsx\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { z } from \"zod\";\nimport { useForm2js } from \"@form2js/react\";\n\nimport type { ReactOutputState, VariantComponentProps } from \"../types\";\n\nconst SubmitPayloadSchema = z.object({\n  person: z.object({\n    name: z.object({\n      first: z.string().min(1, \"First name is required.\"),\n      last: z.string().min(1, \"Last name is required.\")\n    }),\n    email: z.string().regex(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/, \"Email must be valid.\"),\n    age: z.coerce.number().int().min(18, \"Age must be at least 18.\"),\n    guild: z.string().min(1, \"Guild is required.\"),\n    interests: z.array(z.string()).default([])\n  })\n});\n\ntype SubmitPayload = z.infer<typeof SubmitPayloadSchema>;\n\nfunction sleep(ms: number): Promise<void> {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction formatError(error: unknown): string {\n  if (error instanceof z.ZodError) {\n    return error.issues.map((issue) => `${issue.path.join(\".\") || \"root\"}: ${issue.message}`).join(\"\\n\");\n  }\n  if (error instanceof Error) return error.message;\n  return String(error);\n}\n\nfunction createOutputState(\n  isSubmitting: boolean,\n  isError: boolean,\n  isSuccess: boolean,\n  error: unknown,\n  lastSuccessfulPayload: SubmitPayload | null\n): ReactOutputState {\n  let status: ReactOutputState[\"status\"] = \"idle\";\n  let statusMessage = \"Ready to submit.\";\n  if (isSubmitting) { status = \"running\"; statusMessage = \"Callback running\"; }\n  else if (isError)  { status = \"error\";   statusMessage = \"Submit failed\"; }\n  else if (isSuccess){ status = \"success\"; statusMessage = \"Callback resolved\"; }\n\n  return {\n    kind: \"react\",\n    status,\n    statusMessage,\n    submitFlags: { isSubmitting, isError, isSuccess },\n    error: isError ? { message: formatError(error) } : null,\n    parsedPayload: lastSuccessfulPayload,\n    meta: { submitMode: \"onSubmit\", validationEnabled: true }\n  };\n}\n\nexport function ReactVariant({ onOutputChange }: VariantComponentProps): React.JSX.Element {\n  const [lastSuccessfulPayload, setLastSuccessfulPayload] = useState<SubmitPayload | null>(null);\n  const onOutputChangeRef = useRef(onOutputChange);\n  const forceErrorRef = useRef(false);\n  const formRef = useRef<HTMLFormElement>(null);\n\n  const { onSubmit, isSubmitting, isError, error, isSuccess, reset } = useForm2js(\n    async (data: SubmitPayload) => {\n      await sleep(850);\n      if (forceErrorRef.current) {\n        forceErrorRef.current = false;\n        throw new Error(\"Simulated server error.\");\n      }\n      setLastSuccessfulPayload(data);\n    },\n    { schema: SubmitPayloadSchema }\n  );\n\n  useEffect(() => { onOutputChangeRef.current = onOutputChange; }, [onOutputChange]);\n\n  useEffect(() => {\n    onOutputChangeRef.current(createOutputState(isSubmitting, isError, isSuccess, error, lastSuccessfulPayload));\n  }, [error, isError, isSubmitting, isSuccess, lastSuccessfulPayload]);\n\n  useEffect(() => {\n    if (!isSubmitting && (isError || isSuccess)) {\n      forceErrorRef.current = false;\n    }\n  }, [isError, isSubmitting, isSuccess]);\n\n  function handleReset(): void {\n    forceErrorRef.current = false;\n    reset();\n    setLastSuccessfulPayload(null);\n  }\n\n  function handleForceError(): void {\n    const form = formRef.current;\n    if (!form) {\n      return;\n    }\n\n    if (!form.reportValidity()) {\n      forceErrorRef.current = false;\n      return;\n    }\n\n    forceErrorRef.current = true;\n    form.requestSubmit();\n  }\n\n  return (\n    <section aria-label=\"React variant\">\n      <form\n        ref={formRef}\n        onSubmit={(event) => { void onSubmit(event); }}\n      >\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"rv-first\">person.name.first</label>\n          <input className=\"pg-input\" defaultValue=\"Sam\" id=\"rv-first\" name=\"person.name.first\" type=\"text\" />\n        </div>\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"rv-last\">person.name.last</label>\n          <input className=\"pg-input\" defaultValue=\"Vimes\" id=\"rv-last\" name=\"person.name.last\" type=\"text\" />\n        </div>\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"rv-email\">person.email</label>\n          <input className=\"pg-input\" defaultValue=\"sam.vimes@ankh-morpork.gov\" id=\"rv-email\" name=\"person.email\" type=\"email\" />\n        </div>\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"rv-age\">person.age</label>\n          <input className=\"pg-input\" defaultValue={45} id=\"rv-age\" min={0} name=\"person.age\" type=\"number\" />\n        </div>\n        <div className=\"pg-field\">\n          <label className=\"pg-label\" htmlFor=\"rv-guild\">person.guild</label>\n          <select className=\"pg-select\" defaultValue=\"watchman\" id=\"rv-guild\" name=\"person.guild\">\n            <option value=\"watchman\">watchman</option>\n            <option value=\"witches\">witches</option>\n            <option value=\"assassins\">assassins</option>\n            <option value=\"thieves\">thieves</option>\n          </select>\n        </div>\n        <fieldset className=\"pg-fieldset\">\n          <legend>person.interests[]</legend>\n          <label className=\"pg-check-label\">\n            <input defaultChecked name=\"person.interests[]\" type=\"checkbox\" value=\"city-watch\" />\n            city-watch\n          </label>\n          <label className=\"pg-check-label\">\n            <input defaultChecked name=\"person.interests[]\" type=\"checkbox\" value=\"cigars\" />\n            cigars\n          </label>\n          <label className=\"pg-check-label\">\n            <input name=\"person.interests[]\" type=\"checkbox\" value=\"headology\" />\n            headology\n          </label>\n        </fieldset>\n        <div className=\"pg-btns\">\n          <button className=\"pg-btn\" disabled={isSubmitting} type=\"submit\">\n            {isSubmitting ? \"Submitting…\" : \"Submit\"}\n          </button>\n          <button className=\"pg-btn pg-btn-secondary\" onClick={handleReset} type=\"button\">\n            Reset state\n          </button>\n          <button\n            className=\"pg-btn pg-btn-danger\"\n            disabled={isSubmitting}\n            onClick={handleForceError}\n            type=\"button\"\n          >\n            {isSubmitting ? \"Submitting…\" : \"Force Error\"}\n          </button>\n        </div>\n      </form>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/docs/src/env.d.ts",
    "content": "/// <reference types=\"astro/client\" />\n"
  },
  {
    "path": "apps/docs/src/layouts/ApiDocsLayout.astro",
    "content": "---\nimport { ApiPackageNav } from \"../components/api/ApiPackageNav\";\nimport { ApiToc } from \"../components/api/ApiToc\";\nimport type { ApiPackageEntry } from \"../lib/api-packages\";\nimport type { ApiHeading } from \"../lib/api-docs-source\";\nimport DocsShell from \"./DocsShell.astro\";\n\ninterface Props {\n  title: string;\n  introHtml: string;\n  bodyHtml: string;\n  headings: ApiHeading[];\n  packages: ApiPackageEntry[];\n  activePackageSlug?: ApiPackageEntry[\"slug\"];\n}\n\nconst { title, introHtml, bodyHtml, headings, packages, activePackageSlug } =\n  Astro.props;\nconst basePath = import.meta.env.BASE_URL;\n---\n\n<DocsShell title={`${title} | form2js`}>\n  <section class=\"api-docs\">\n    <aside class=\"api-docs__nav\">\n      <ApiPackageNav\n        activeSlug={activePackageSlug}\n        basePath={basePath}\n        packages={packages}\n      />\n    </aside>\n    <article class=\"api-docs__content\">\n      <header class=\"api-docs__header\">\n        <p class=\"hero__eyebrow\">API Docs</p>\n        <h1>{title}</h1>\n        {introHtml ? <div class=\"api-docs__intro\" set:html={introHtml} /> : null}\n      </header>\n      <slot name=\"summary\" />\n      <div class=\"api-docs__body\" set:html={bodyHtml} />\n    </article>\n    {headings.length > 0 ? (\n      <aside class=\"api-docs__sidebar\">\n        <ApiToc client:load headings={headings} />\n      </aside>\n    ) : null}\n  </section>\n</DocsShell>\n"
  },
  {
    "path": "apps/docs/src/layouts/DesignShell.astro",
    "content": "---\n// apps/docs/src/layouts/DesignShell.astro\nimport { apiDocsPath } from \"../lib/site-routes\";\nimport \"../styles/landing.css\";\nimport \"../styles/playground.css\";\n\ninterface Props {\n  title?: string;\n  theme: \"dark-brutalism\" | \"outrun-sunset\" | \"terminal-noir\" | \"linear-dark\" | \"editorial\";\n}\n\nconst { title = \"form2js\", theme } = Astro.props;\nconst basePath = import.meta.env.BASE_URL;\n\nconst THEME_FONTS: Record<string, string> = {\n  \"dark-brutalism\":\n    \"https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800;900&family=DM+Mono:wght@300;400;500&display=swap\",\n  \"outrun-sunset\":\n    \"https://fonts.googleapis.com/css2?family=Dela+Gothic+One&family=Syne:wght@700;900&family=DM+Mono:wght@300;400;500&display=swap\",\n  \"terminal-noir\":\n    \"https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap\",\n  \"linear-dark\":\n    \"https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700;900&family=DM+Mono:wght@300;400;500&display=swap\",\n  editorial:\n    \"https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=DM+Mono:wght@300;400;500&display=swap\",\n};\n\nconst fontsHref = THEME_FONTS[theme];\n---\n\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>{title} — form2js</title>\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin />\n    {fontsHref && <link href={fontsHref} rel=\"stylesheet\" />}\n  </head>\n  <body>\n    <header class=\"site-header\">\n      <a class=\"site-brand\" href={basePath}>\n        <span class=\"site-brand__badge\">f2j</span>\n        form2js\n      </a>\n      <nav class=\"site-nav\" aria-label=\"Primary\">\n        <a href={apiDocsPath(basePath)}>API Docs</a>\n        <a href=\"https://github.com/maxatwork/form2js\">Github</a>\n      </nav>\n    </header>\n    <main>\n      <slot />\n    </main>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/docs/src/layouts/DocsShell.astro",
    "content": "---\nimport \"../styles/global.css\";\nimport { homepagePath, migrationGuidePath } from \"../lib/site-routes\";\n\ninterface Props {\n  title?: string;\n}\n\nconst { title = \"form2js\" } = Astro.props;\nconst basePath = import.meta.env.BASE_URL;\n---\n\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>{title}</title>\n  </head>\n  <body>\n    <div class=\"site-shell\">\n      <header class=\"site-header\">\n        <a class=\"site-brand\" href={homepagePath(basePath)}>\n          <span class=\"site-brand__logo\">f2j</span>\n          <span class=\"site-brand__name\">form2js</span>\n        </a>\n        <nav class=\"site-nav\" aria-label=\"Primary\">\n          <a href={homepagePath(basePath)}>Overview</a>\n          <a href={migrationGuidePath(basePath)}>Migration</a>\n          <a href=\"https://github.com/maxatwork/form2js\">Github</a>\n        </nav>\n      </header>\n      <main>\n        <slot />\n      </main>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/docs/src/lib/api-docs-source.ts",
    "content": "import { readFile } from \"node:fs/promises\";\nimport rehypeStringify from \"rehype-stringify\";\nimport remarkGfm from \"remark-gfm\";\nimport remarkParse from \"remark-parse\";\nimport remarkRehype from \"remark-rehype\";\nimport { unified } from \"unified\";\n\nimport {\n  apiIndexMarkdownPath,\n  getApiPackageByMarkdownBasename\n} from \"./api-packages\";\nimport {\n  apiDocsPath,\n  apiPackageDocsPath,\n  homepagePath,\n  migrationGuidePath\n} from \"./site-routes\";\n\nexport interface ApiHeading {\n  depth: 2 | 3;\n  slug: string;\n  text: string;\n}\n\nexport interface ApiDocsSource {\n  title: string;\n  introMarkdown: string;\n  introHtml: string;\n  bodyMarkdown: string;\n  bodyHtml: string;\n  headings: ApiHeading[];\n}\n\ninterface ParseOptions {\n  basePath: string;\n}\n\ntype MarkdownNode = {\n  type: string;\n  url?: string;\n  depth?: number;\n  value?: string;\n  children?: MarkdownNode[];\n  position?: {\n    start?: { offset?: number };\n    end?: { offset?: number };\n  };\n  data?: Record<string, unknown>;\n};\n\nfunction slugify(text: string): string {\n  const slug = text\n    .toLowerCase()\n    .replace(/[`\"]/g, \"\")\n    .replace(/[^a-z0-9]+/g, \"-\")\n    .replace(/^-+|-+$/g, \"\")\n    .replace(/-{2,}/g, \"-\");\n\n  return slug || \"section\";\n}\n\nfunction rewriteMarkdownLink(url: string, basePath: string): string {\n  if (\n    url.startsWith(\"#\") ||\n    url.startsWith(\"http://\") ||\n    url.startsWith(\"https://\") ||\n    url.startsWith(\"mailto:\")\n  ) {\n    return url;\n  }\n\n  const [pathname, hash] = url.split(\"#\");\n  const suffix = hash ? `#${hash}` : \"\";\n  const normalizedPathname = pathname\n    .replace(/^(?:\\.\\.\\/)?docs\\//, \"\")\n    .replace(/^\\.\\//, \"\");\n\n  if (\n    normalizedPathname === \"README.md\" ||\n    pathname === \"../README.md\"\n  ) {\n    return `${homepagePath(basePath)}${suffix}`;\n  }\n\n  if (normalizedPathname === \"migrate.md\") {\n    return `${migrationGuidePath(basePath)}${suffix}`;\n  }\n\n  if (normalizedPathname === \"api.md\" || normalizedPathname === \"api-index.md\") {\n    return `${apiDocsPath(basePath)}${suffix}`;\n  }\n\n  const apiPackage = getApiPackageByMarkdownBasename(normalizedPathname);\n  if (apiPackage) {\n    return `${apiPackageDocsPath(basePath, apiPackage.slug)}${suffix}`;\n  }\n\n  return url;\n}\n\nfunction visit(\n  node: MarkdownNode,\n  callback: (currentNode: MarkdownNode) => void\n): void {\n  callback(node);\n\n  for (const child of node.children ?? []) {\n    visit(child, callback);\n  }\n}\n\nfunction collectText(node: MarkdownNode): string {\n  if (node.type === \"text\" || node.type === \"inlineCode\") {\n    return node.value ?? \"\";\n  }\n\n  return (node.children ?? []).map((child) => collectText(child)).join(\"\");\n}\n\nfunction renderMarkdownNodes(nodes: MarkdownNode[]): string {\n  if (nodes.length === 0) {\n    return \"\";\n  }\n\n  const processor = unified().use(remarkRehype).use(rehypeStringify);\n  const htmlTree = processor.runSync({\n    type: \"root\",\n    children: nodes,\n  } as never);\n\n  return processor.stringify(htmlTree).trim();\n}\n\nfunction extractMarkdownSlice(\n  markdown: string,\n  startOffset: number,\n  endOffset?: number\n): string {\n  return markdown.slice(startOffset, endOffset).trim();\n}\n\nexport function parseApiDocsMarkdown(\n  markdown: string,\n  options: ParseOptions\n): ApiDocsSource {\n  const parser = unified().use(remarkParse).use(remarkGfm);\n  const tree = parser.parse(markdown) as MarkdownNode;\n  const rootChildren = tree.children ?? [];\n  const titleNode = rootChildren[0];\n\n  if (titleNode?.type !== \"heading\" || titleNode.depth !== 1) {\n    throw new Error(\"API docs markdown must start with an H1 heading.\");\n  }\n\n  const title = collectText(titleNode);\n  const contentNodes = rootChildren.slice(1);\n  const firstBodyNodeIndex = contentNodes.findIndex(\n    (node) => node.type === \"heading\" && node.depth === 2\n  );\n  const introNodes =\n    firstBodyNodeIndex === -1\n      ? contentNodes\n      : contentNodes.slice(0, firstBodyNodeIndex);\n  const bodyNodes =\n    firstBodyNodeIndex === -1 ? [] : contentNodes.slice(firstBodyNodeIndex);\n  const introStartOffset = titleNode.position?.end?.offset ?? 0;\n  const bodyStartOffset = bodyNodes[0]?.position?.start?.offset;\n  const introMarkdown = extractMarkdownSlice(\n    markdown,\n    introStartOffset,\n    bodyStartOffset\n  );\n  const bodyMarkdown =\n    bodyStartOffset === undefined\n      ? \"\"\n      : extractMarkdownSlice(markdown, bodyStartOffset);\n  const headings: ApiHeading[] = [];\n  const slugCounts = new Map<string, number>();\n\n  for (const node of [...introNodes, ...bodyNodes]) {\n    visit(node, (currentNode) => {\n      if (currentNode.type === \"link\" && currentNode.url) {\n        currentNode.url = rewriteMarkdownLink(\n          currentNode.url,\n          options.basePath\n        );\n      }\n\n      if (\n        currentNode.type !== \"heading\" ||\n        (currentNode.depth !== 2 && currentNode.depth !== 3)\n      ) {\n        return;\n      }\n\n      if (!bodyNodes.includes(node)) {\n        return;\n      }\n\n      const text = collectText(currentNode);\n      const baseSlug = slugify(text);\n      const nextCount = (slugCounts.get(baseSlug) ?? 0) + 1;\n      slugCounts.set(baseSlug, nextCount);\n      const slug = nextCount === 1 ? baseSlug : `${baseSlug}-${nextCount}`;\n\n      currentNode.data = {\n        ...(currentNode.data ?? {}),\n        hProperties: {\n          id: slug,\n        },\n      };\n\n      headings.push({\n        depth: currentNode.depth,\n        slug,\n        text,\n      });\n    });\n  }\n\n  return {\n    title,\n    introMarkdown,\n    introHtml: renderMarkdownNodes(introNodes),\n    bodyMarkdown,\n    bodyHtml: renderMarkdownNodes(bodyNodes),\n    headings,\n  };\n}\n\nexport async function loadApiDocsSource(\n  options: { basePath?: string; markdownPath?: string } = {}\n): Promise<ApiDocsSource> {\n  const markdownPath = options.markdownPath ?? apiIndexMarkdownPath;\n  const markdown = await readFile(markdownPath, \"utf8\");\n\n  return parseApiDocsMarkdown(markdown, {\n    basePath: options.basePath ?? \"/\",\n  });\n}\n"
  },
  {
    "path": "apps/docs/src/lib/api-packages.ts",
    "content": "import path from \"node:path\";\n\nexport type ApiPackageSlug =\n  | \"core\"\n  | \"dom\"\n  | \"form-data\"\n  | \"react\"\n  | \"js2form\"\n  | \"jquery\";\n\nexport interface ApiPackageEntry {\n  slug: ApiPackageSlug;\n  packageName: string;\n  summary: string;\n  markdownPath: string;\n}\n\nfunction resolveDocsPath(filename: string): string {\n  return path.resolve(process.cwd(), \"..\", \"..\", \"docs\", filename);\n}\n\nexport const apiIndexMarkdownPath = resolveDocsPath(\"api-index.md\");\n\nexport const apiPackages: ApiPackageEntry[] = [\n  {\n    slug: \"core\",\n    packageName: \"@form2js/core\",\n    summary: \"Turn path-like key/value pairs into nested objects and flatten them back into entries.\",\n    markdownPath: resolveDocsPath(\"api-core.md\")\n  },\n  {\n    slug: \"dom\",\n    packageName: \"@form2js/dom\",\n    summary: \"Parse browser form controls into an object while preserving native submission behavior.\",\n    markdownPath: resolveDocsPath(\"api-dom.md\")\n  },\n  {\n    slug: \"form-data\",\n    packageName: \"@form2js/form-data\",\n    summary: \"Parse FormData and tuple entries with the same path rules used by the core parser.\",\n    markdownPath: resolveDocsPath(\"api-form-data.md\")\n  },\n  {\n    slug: \"react\",\n    packageName: \"@form2js/react\",\n    summary: \"Handle React form submission with parsed payloads, optional schema validation, and submit state.\",\n    markdownPath: resolveDocsPath(\"api-react.md\")\n  },\n  {\n    slug: \"js2form\",\n    packageName: \"@form2js/js2form\",\n    summary: \"Push nested object data back into matching DOM form controls.\",\n    markdownPath: resolveDocsPath(\"api-js2form.md\")\n  },\n  {\n    slug: \"jquery\",\n    packageName: \"@form2js/jquery\",\n    summary: \"Install a jQuery plugin wrapper around the DOM parser for legacy form handling flows.\",\n    markdownPath: resolveDocsPath(\"api-jquery.md\")\n  }\n];\n\nexport function getApiPackageBySlug(slug: string): ApiPackageEntry | undefined {\n  return apiPackages.find((entry) => entry.slug === slug);\n}\n\nexport function getApiPackageByMarkdownBasename(\n  basename: string\n): ApiPackageEntry | undefined {\n  return apiPackages.find(\n    (entry) => path.basename(entry.markdownPath) === basename\n  );\n}\n"
  },
  {
    "path": "apps/docs/src/lib/site-routes.ts",
    "content": "function normalizeBase(basePath: string): string {\n  if (basePath === \"/\") {\n    return \"/\";\n  }\n\n  const withLeadingSlash = basePath.startsWith(\"/\") ? basePath : `/${basePath}`;\n  return withLeadingSlash.endsWith(\"/\") ? withLeadingSlash : `${withLeadingSlash}/`;\n}\n\nexport function homepagePath(basePath: string): string {\n  return normalizeBase(basePath);\n}\n\nexport function migrationGuidePath(basePath: string): string {\n  return `${normalizeBase(basePath)}migrate/`;\n}\n\nexport function apiDocsPath(basePath: string): string {\n  return `${normalizeBase(basePath)}api/`;\n}\n\nexport function apiPackageDocsPath(basePath: string, slug: string): string {\n  return `${apiDocsPath(basePath)}${encodeURIComponent(slug)}/`;\n}\n\nexport function homepageVariantPath(basePath: string, variant: string): string {\n  return `${normalizeBase(basePath)}?variant=${encodeURIComponent(variant)}`;\n}\n"
  },
  {
    "path": "apps/docs/src/pages/api/[package].astro",
    "content": "---\nimport ApiDocsLayout from \"../../layouts/ApiDocsLayout.astro\";\nimport { apiPackages, type ApiPackageEntry } from \"../../lib/api-packages\";\nimport { loadApiDocsSource } from \"../../lib/api-docs-source\";\n\ninterface Props {\n  apiPackage: ApiPackageEntry;\n}\n\nexport function getStaticPaths() {\n  return apiPackages.map((apiPackage) => ({\n    params: { package: apiPackage.slug },\n    props: { apiPackage }\n  }));\n}\n\nconst { apiPackage } = Astro.props;\nconst apiDocsSource = await loadApiDocsSource({\n  basePath: import.meta.env.BASE_URL,\n  markdownPath: apiPackage.markdownPath\n});\n---\n\n<ApiDocsLayout\n  activePackageSlug={apiPackage.slug}\n  bodyHtml={apiDocsSource.bodyHtml}\n  headings={apiDocsSource.headings}\n  introHtml={apiDocsSource.introHtml}\n  packages={apiPackages}\n  title={apiDocsSource.title}\n/>\n"
  },
  {
    "path": "apps/docs/src/pages/api/index.astro",
    "content": "---\nimport { ApiPackageSummaryList } from \"../../components/api/ApiPackageSummaryList\";\nimport ApiDocsLayout from \"../../layouts/ApiDocsLayout.astro\";\nimport { apiPackages } from \"../../lib/api-packages\";\nimport { loadApiDocsSource } from \"../../lib/api-docs-source\";\n\nconst apiDocsSource = await loadApiDocsSource({\n  basePath: import.meta.env.BASE_URL\n});\n---\n\n<ApiDocsLayout\n  bodyHtml={apiDocsSource.bodyHtml}\n  headings={apiDocsSource.headings}\n  introHtml={apiDocsSource.introHtml}\n  packages={apiPackages}\n  title={apiDocsSource.title}\n>\n  <ApiPackageSummaryList\n    slot=\"summary\"\n    basePath={import.meta.env.BASE_URL}\n    packages={apiPackages}\n  />\n</ApiDocsLayout>\n"
  },
  {
    "path": "apps/docs/src/pages/index.astro",
    "content": "---\n// apps/docs/src/pages/1.astro\nimport \"../styles/themes/dark-brutalism.css\";\nimport DesignShell from \"../layouts/DesignShell.astro\";\nimport Hero from \"../components/landing/Hero.astro\";\nimport InstallSection from \"../components/landing/InstallSection.astro\";\nimport ApiDocsCta from \"../components/landing/ApiDocsCta.astro\";\nimport { PlaygroundShell } from \"../components/playground/PlaygroundShell\";\n---\n\n<DesignShell title=\"form2js - form serialization library\" theme=\"dark-brutalism\">\n  <Hero />\n  <section class=\"content-wrap section\" id=\"playground\">\n    <p class=\"section-eyebrow\">Playground</p>\n    <h2 class=\"section-title\">Try it live</h2>\n    <PlaygroundShell client:load />\n  </section>\n  <InstallSection />\n  <ApiDocsCta />\n</DesignShell>\n\n"
  },
  {
    "path": "apps/docs/src/pages/migrate.astro",
    "content": "---\nimport path from \"node:path\";\n\nimport { ApiToc } from \"../components/api/ApiToc\";\nimport { loadApiDocsSource } from \"../lib/api-docs-source\";\nimport DocsShell from \"../layouts/DocsShell.astro\";\n\nconst migrationSource = await loadApiDocsSource({\n  basePath: import.meta.env.BASE_URL,\n  markdownPath: path.resolve(process.cwd(), \"..\", \"..\", \"docs\", \"migrate.md\")\n});\n---\n\n<DocsShell title={`${migrationSource.title} | form2js`}>\n  <section class=\"api-docs api-docs--content-sidebar\">\n    <article class=\"api-docs__content\">\n      <header class=\"api-docs__header\">\n        <p class=\"hero__eyebrow\">Migration Guide</p>\n        <h1>{migrationSource.title}</h1>\n        {migrationSource.introHtml ? (\n          <div class=\"api-docs__intro\" set:html={migrationSource.introHtml} />\n        ) : null}\n      </header>\n      <div class=\"api-docs__body\" set:html={migrationSource.bodyHtml} />\n    </article>\n    {migrationSource.headings.length > 0 ? (\n      <aside class=\"api-docs__sidebar\">\n        <ApiToc client:load headings={migrationSource.headings} />\n      </aside>\n    ) : null}\n  </section>\n</DocsShell>\n"
  },
  {
    "path": "apps/docs/src/styles/global.css",
    "content": ":root {\n  color-scheme: dark;\n  --bg: #0c0e14;\n  --panel: #12151e;\n  --panel-2: #1a1d2b;\n  --text: #e4e6ef;\n  --muted: #8b8fa3;\n  --accent: #00e5c8;\n  --border: rgba(255, 255, 255, 0.08);\n  --mono: \"JetBrains Mono\", \"Fira Code\", monospace;\n  --ui: \"Inter\", \"Segoe UI\", sans-serif;\n}\n\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n}\n\nhtml,\nbody {\n  margin: 0;\n  min-height: 100%;\n}\n\nbody {\n  background: var(--bg);\n  color: var(--text);\n  font-family: var(--ui);\n}\n\na {\n  color: inherit;\n}\n\n.site-shell {\n  min-height: 100vh;\n}\n\n.site-header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: 1rem;\n  padding: 1rem 1.5rem;\n  border-bottom: 1px solid var(--border);\n  background: var(--panel);\n}\n\n.site-brand {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.75rem;\n  text-decoration: none;\n}\n\n.site-brand__logo {\n  padding: 0.2rem 0.5rem;\n  border: 1px solid rgba(0, 229, 200, 0.2);\n  border-radius: 0.4rem;\n  color: var(--accent);\n  background: rgba(0, 229, 200, 0.08);\n  font-family: var(--mono);\n}\n\n.site-brand__name {\n  font-weight: 600;\n}\n\n.site-nav {\n  display: inline-flex;\n  gap: 1rem;\n}\n\n.site-nav a {\n  color: var(--muted);\n  text-decoration: none;\n}\n\n.site-nav a:hover,\n.site-nav a:focus-visible {\n  color: var(--text);\n}\n\n.hero {\n  width: min(72rem, calc(100% - 3rem));\n  margin: 0 auto;\n  padding: 4rem 0;\n}\n\n.hero__eyebrow {\n  margin: 0 0 0.75rem;\n  color: var(--accent);\n  font-family: var(--mono);\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n}\n\n.hero h1 {\n  margin: 0;\n  font-size: clamp(2.5rem, 8vw, 5rem);\n}\n\n.hero p:last-child {\n  max-width: 42rem;\n  color: var(--muted);\n  line-height: 1.6;\n}\n\n.api-docs {\n  width: min(96rem, calc(100% - 3rem));\n  margin: 0 auto;\n  padding: 3rem 0 4rem;\n  display: grid;\n  grid-template-columns: 16rem minmax(0, 1fr) 18rem;\n  gap: 2rem;\n}\n\n.api-docs--content-sidebar {\n  grid-template-columns: minmax(0, 1fr) 18rem;\n}\n\n.api-docs__nav,\n.api-docs__sidebar {\n  position: sticky;\n  top: 1rem;\n  align-self: start;\n}\n\n.api-docs__content {\n  min-width: 0;\n}\n\n.api-docs__header h1 {\n  margin: 0;\n  font-size: clamp(2rem, 6vw, 3.75rem);\n}\n\n.api-docs__intro,\n.api-docs__body {\n  color: var(--muted);\n  line-height: 1.75;\n}\n\n.api-docs__intro {\n  margin-top: 1.5rem;\n}\n\n.api-docs__body {\n  margin-top: 2rem;\n}\n\n.api-package-nav,\n.api-toc,\n.api-package-summary {\n  border: 1px solid var(--border);\n  border-radius: 1rem;\n  background: var(--panel);\n}\n\n.api-docs__body h2,\n.api-docs__body h3 {\n  color: var(--text);\n  scroll-margin-top: 6rem;\n}\n\n.api-docs__body pre {\n  overflow-x: auto;\n  padding: 1rem 1.25rem;\n  border: 1px solid var(--border);\n  border-radius: 1rem;\n  background: var(--panel);\n}\n\n.api-docs__body code {\n  font-family: var(--mono);\n}\n\n.api-docs__body table {\n  width: 100%;\n  border-collapse: collapse;\n  margin: 1.5rem 0;\n}\n\n.api-docs__body th,\n.api-docs__body td {\n  padding: 0.75rem;\n  text-align: left;\n  border-bottom: 1px solid var(--border);\n  vertical-align: top;\n}\n\n.api-package-nav {\n  padding: 1rem;\n}\n\n.api-package-nav__eyebrow,\n.api-toc__eyebrow {\n  margin: 0 0 0.75rem;\n  color: var(--accent);\n  font-family: var(--mono);\n  text-transform: uppercase;\n  letter-spacing: 0.08em;\n  font-size: 0.8rem;\n}\n\n.api-package-nav__list,\n.api-toc__list,\n.api-toc__sublist {\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n\n.api-package-nav__list {\n  display: grid;\n  gap: 0.5rem;\n}\n\n.api-package-nav__link {\n  display: block;\n  padding: 0.6rem 0.75rem;\n  border-radius: 0.75rem;\n  color: var(--muted);\n  text-decoration: none;\n}\n\n.api-package-nav__link[aria-current=\"page\"] {\n  background: color-mix(in srgb, var(--accent) 18%, transparent);\n  color: var(--text);\n}\n\n.api-toc {\n  padding: 1rem;\n}\n\n.api-toc__list,\n.api-toc__sublist {\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n\n.api-toc__list {\n  display: grid;\n  gap: 0.75rem;\n}\n\n.api-toc__sublist {\n  margin-top: 0.5rem;\n  padding-left: 0.75rem;\n  display: grid;\n  gap: 0.4rem;\n}\n\n.api-toc__link,\n.api-toc__sublink {\n  color: var(--muted);\n  text-decoration: none;\n}\n\n.api-toc__link[aria-current=\"true\"],\n.api-toc__sublink[aria-current=\"true\"] {\n  color: var(--text);\n}\n\n.api-package-summary-list {\n  margin-top: 2rem;\n}\n\n.api-package-summary-list h2 {\n  margin-bottom: 1rem;\n}\n\n.api-package-summary-list__items {\n  display: grid;\n  gap: 1rem;\n}\n\n.api-package-summary {\n  padding: 1.25rem;\n}\n\n.api-package-summary h3 {\n  margin: 0 0 0.5rem;\n}\n\n.api-package-summary p {\n  margin: 0;\n  color: var(--muted);\n  line-height: 1.6;\n}\n\n@media (max-width: 900px) {\n  .api-docs {\n    grid-template-columns: 1fr;\n  }\n\n  .api-docs__nav {\n    position: static;\n    order: -1;\n  }\n\n  .api-docs__sidebar {\n    position: static;\n  }\n}\n"
  },
  {
    "path": "apps/docs/src/styles/landing.css",
    "content": "/* apps/docs/src/styles/landing.css */\n*,\n*::before,\n*::after {\n  box-sizing: border-box;\n  margin: 0;\n  padding: 0;\n}\n\nhtml,\nbody {\n  min-height: 100%;\n}\n\nbody {\n  background: var(--c-bg);\n  color: var(--c-text);\n  font-family: var(--f-ui);\n}\n\na {\n  color: inherit;\n}\n\n/* ── Nav ── */\n.site-header {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 1rem 2.5rem;\n  border-bottom: 1px solid var(--c-border);\n}\n\n.site-brand {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.6rem;\n  text-decoration: none;\n  color: var(--c-text);\n  font-family: var(--f-display);\n  font-weight: 700;\n  font-size: 1rem;\n}\n\n.site-brand__badge {\n  padding: 0.15rem 0.5rem;\n  background: var(--c-accent-glow);\n  border: 1px solid color-mix(in srgb, var(--c-accent) 35%, transparent);\n  color: var(--c-accent);\n  font-family: var(--f-mono);\n  font-size: 0.75rem;\n  font-weight: 700;\n  letter-spacing: 0.03em;\n}\n\n.site-nav {\n  display: flex;\n  align-items: center;\n  gap: 1.5rem;\n}\n\n.site-nav a {\n  color: var(--c-muted);\n  text-decoration: none;\n  font-size: 0.85rem;\n  transition: color 0.15s;\n}\n.site-nav a:hover {\n  color: var(--c-text);\n}\n\n/* ── Hero ── */\n.hero {\n  width: min(76rem, calc(100% - 3rem));\n  margin: 0 auto;\n  padding: 5rem 0 3.5rem;\n}\n\n.hero__eyebrow {\n  font-family: var(--f-mono);\n  font-size: 0.72rem;\n  letter-spacing: 0.18em;\n  text-transform: uppercase;\n  color: var(--c-accent);\n  margin-bottom: 1.25rem;\n}\n\n.hero__title {\n  font-family: var(--f-display);\n  font-size: clamp(3rem, 9vw, 6rem);\n  font-weight: 900;\n  line-height: 0.92;\n  letter-spacing: -0.02em;\n  color: var(--c-text);\n  margin-bottom: 1.25rem;\n}\n\n.hero__desc {\n  font-size: 0.95rem;\n  color: var(--c-muted);\n  line-height: 1.7;\n  max-width: 48ch;\n  margin-bottom: 2rem;\n  font-family: var(--f-ui);\n}\n\n.hero__actions {\n  display: flex;\n  gap: 0.75rem;\n  flex-wrap: wrap;\n  align-items: center;\n}\n\n.hero__install {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.5rem;\n  padding: 0.5rem 0.9rem;\n  background: var(--c-panel);\n  border: 1px solid var(--c-border);\n  font-family: var(--f-mono);\n  font-size: 0.8rem;\n  color: var(--c-text);\n}\n\n.hero__install-prefix {\n  color: var(--c-muted);\n}\n\n.btn-primary {\n  display: inline-block;\n  padding: 0.55rem 1.5rem;\n  background: var(--c-accent);\n  color: var(--c-bg);\n  font-family: var(--f-ui);\n  font-size: 0.85rem;\n  font-weight: 700;\n  text-decoration: none;\n  border: none;\n  cursor: pointer;\n  transition: opacity 0.15s;\n}\n.btn-primary:hover {\n  opacity: 0.85;\n}\n\n.btn-ghost {\n  display: inline-block;\n  padding: 0.55rem 1.5rem;\n  background: transparent;\n  color: var(--c-muted);\n  font-family: var(--f-ui);\n  font-size: 0.85rem;\n  font-weight: 600;\n  text-decoration: none;\n  border: 1px solid var(--c-border);\n  cursor: pointer;\n  transition: color 0.15s, border-color 0.15s;\n}\n.btn-ghost:hover {\n  color: var(--c-text);\n  border-color: var(--c-text);\n}\n\n/* ── Page sections ── */\n.content-wrap {\n  width: min(76rem, calc(100% - 3rem));\n  margin: 0 auto;\n}\n\n.section {\n  padding: 3rem 0;\n  border-top: 1px solid var(--c-border);\n}\n\n.section-eyebrow {\n  font-family: var(--f-mono);\n  font-size: 0.7rem;\n  letter-spacing: 0.18em;\n  text-transform: uppercase;\n  color: var(--c-accent);\n  margin-bottom: 0.75rem;\n}\n\n.section-title {\n  font-family: var(--f-display);\n  font-size: clamp(1.5rem, 4vw, 2.5rem);\n  font-weight: 900;\n  color: var(--c-text);\n  margin-bottom: 0.75rem;\n}\n\n.section-desc {\n  font-size: 0.9rem;\n  color: var(--c-muted);\n  line-height: 1.65;\n  max-width: 54ch;\n  margin-bottom: 1.5rem;\n}\n\n/* ── Install section ── */\n.install-grid {\n  display: grid;\n  grid-template-columns: 1fr 1.4fr;\n  gap: 3rem;\n  align-items: start;\n}\n\n@media (max-width: 680px) {\n  .install-grid {\n    grid-template-columns: 1fr;\n  }\n  .hero {\n    padding: 3rem 0 2.5rem;\n  }\n  .site-header {\n    padding: 1rem 1.25rem;\n  }\n}\n\n.code-block {\n  background: var(--c-panel-2);\n  border: 1px solid var(--c-border);\n  padding: 1.25rem 1.5rem;\n  font-family: var(--f-mono);\n  font-size: 0.78rem;\n  line-height: 1.65;\n  overflow-x: auto;\n  color: var(--c-muted);\n}\n\n.code-block .kw {\n  color: var(--c-accent);\n}\n.code-block .fn {\n  color: var(--c-accent-2);\n}\n.code-block .str {\n  color: var(--c-text);\n  opacity: 0.7;\n}\n.code-block .cmt {\n  color: var(--c-muted);\n  font-style: italic;\n}\n\n.install-cmd {\n  font-family: var(--f-mono);\n  font-size: 0.82rem;\n  color: var(--c-accent);\n  margin-bottom: 0.5rem;\n  display: block;\n}\n.install-cmd::before {\n  content: \"$ \";\n  color: var(--c-muted);\n}\n"
  },
  {
    "path": "apps/docs/src/styles/playground.css",
    "content": "/* apps/docs/src/styles/playground.css */\n\n/* ── Shell ── */\n.pg-shell {\n  border: 1px solid var(--c-border);\n  overflow: hidden;\n}\n\n/* ── Tab bar ── */\n.pg-tabs {\n  display: flex;\n  border-bottom: 1px solid var(--c-border);\n  background: var(--c-panel);\n  overflow-x: auto;\n  scrollbar-width: none;\n}\n.pg-tabs::-webkit-scrollbar { display: none; }\n\n.pg-tab-btn {\n  padding: 0.6rem 1rem;\n  background: transparent;\n  border: none;\n  border-bottom: 2px solid transparent;\n  margin-bottom: -1px;\n  color: var(--c-muted);\n  font-family: var(--f-mono);\n  font-size: 0.72rem;\n  font-weight: 600;\n  letter-spacing: 0.06em;\n  cursor: pointer;\n  white-space: nowrap;\n  flex-shrink: 0;\n  transition: color 0.15s;\n}\n.pg-tab-btn[aria-pressed=\"true\"] {\n  color: var(--c-accent);\n  border-bottom-color: var(--c-accent);\n}\n.pg-tab-btn:hover:not([aria-pressed=\"true\"]) { color: var(--c-text); }\n\n/* ── Two-column split ── */\n.pg-body {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  min-height: 340px;\n}\n\n@media (max-width: 760px) {\n  .pg-body { grid-template-columns: 1fr; }\n  .pg-form-col { border-right: none; }\n}\n\n.pg-form-col {\n  padding: 1.5rem;\n  border-right: 1px solid var(--c-border);\n  background: var(--c-panel);\n  overflow-y: auto;\n}\n\n.pg-output-col {\n  padding: 1.5rem;\n  background: var(--c-panel-2);\n  overflow-y: auto;\n}\n\n/* ── Form fields ── */\n.pg-field { margin-bottom: 1rem; }\n\n.pg-label {\n  display: block;\n  font-family: var(--f-mono);\n  font-size: 0.7rem;\n  color: var(--c-muted);\n  margin-bottom: 0.3rem;\n  letter-spacing: 0.05em;\n}\n\n.pg-input,\n.pg-select,\n.pg-textarea {\n  width: 100%;\n  padding: 0.4rem 0.6rem;\n  background: var(--c-bg);\n  border: 1px solid var(--c-border);\n  color: var(--c-text);\n  font-family: var(--f-mono);\n  font-size: 0.8rem;\n  outline: none;\n  transition: border-color 0.15s;\n  -webkit-appearance: none;\n}\n.pg-input:focus,\n.pg-select:focus,\n.pg-textarea:focus { border-color: var(--c-accent); }\n\n.pg-textarea {\n  min-height: 220px;\n  resize: vertical;\n  line-height: 1.55;\n}\n\n.pg-fieldset {\n  border: 1px solid var(--c-border);\n  padding: 0.6rem 0.9rem 0.6rem;\n  margin-bottom: 1rem;\n}\n.pg-fieldset legend {\n  font-family: var(--f-mono);\n  font-size: 0.7rem;\n  color: var(--c-muted);\n  padding: 0 0.3rem;\n  letter-spacing: 0.05em;\n}\n\n.pg-check-label {\n  display: flex;\n  align-items: center;\n  gap: 0.5rem;\n  font-family: var(--f-mono);\n  font-size: 0.78rem;\n  color: var(--c-text);\n  margin: 0.35rem 0;\n  cursor: pointer;\n}\n\n/* ── Buttons ── */\n.pg-btns {\n  display: flex;\n  gap: 0.5rem;\n  flex-wrap: wrap;\n  margin-top: 1.25rem;\n}\n\n.pg-btn {\n  padding: 0.4rem 1rem;\n  background: var(--c-accent);\n  color: var(--c-bg);\n  font-family: var(--f-mono);\n  font-size: 0.73rem;\n  font-weight: 700;\n  letter-spacing: 0.06em;\n  border: none;\n  cursor: pointer;\n  transition: opacity 0.15s;\n}\n.pg-btn:hover { opacity: 0.82; }\n.pg-btn:disabled { opacity: 0.35; cursor: not-allowed; }\n\n.pg-btn-secondary {\n  background: transparent;\n  color: var(--c-muted);\n  border: 1px solid var(--c-border);\n}\n.pg-btn-secondary:hover { color: var(--c-text); border-color: var(--c-text); opacity: 1; }\n\n.pg-btn-danger {\n  background: transparent;\n  color: #ff5555;\n  border: 1px solid rgba(255, 85, 85, 0.3);\n}\n.pg-btn-danger:hover { border-color: #ff5555; opacity: 1; }\n.pg-btn-danger:disabled { opacity: 0.35; cursor: not-allowed; }\n\n/* ── Output panel (ResultPanel) ── */\n.result-eyebrow {\n  font-family: var(--f-mono);\n  font-size: 0.65rem;\n  letter-spacing: 0.18em;\n  text-transform: uppercase;\n  color: var(--c-muted);\n  margin-bottom: 1rem;\n}\n\n.status-badge {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.4rem;\n  font-family: var(--f-mono);\n  font-size: 0.73rem;\n  font-weight: 700;\n  letter-spacing: 0.08em;\n  text-transform: uppercase;\n  margin-bottom: 1rem;\n}\n.status-badge::before {\n  content: '';\n  display: block;\n  width: 7px;\n  height: 7px;\n  border-radius: 50%;\n  background: currentColor;\n  flex-shrink: 0;\n}\n\n.status-idle    { color: var(--c-muted); }\n.status-running { color: var(--c-accent); animation: badge-pulse 1s steps(1) infinite; }\n.status-success { color: #22c55e; }\n.status-error   { color: #ff5555; }\n\n@keyframes badge-pulse {\n  0%, 100% { opacity: 1; }\n  50% { opacity: 0.35; }\n}\n\n.result-flags {\n  display: grid;\n  grid-template-columns: auto 1fr;\n  gap: 0.2rem 0.75rem;\n  font-family: var(--f-mono);\n  font-size: 0.73rem;\n  margin-bottom: 1rem;\n  align-items: baseline;\n}\n.flag-key   { color: var(--c-muted); }\n.flag-true  { color: var(--c-accent); }\n.flag-false { color: var(--c-muted); opacity: 0.5; }\n\n.result-errors {\n  margin-bottom: 1rem;\n  padding: 0.75rem 0.9rem;\n  background: rgba(255, 85, 85, 0.06);\n  border: 1px solid rgba(255, 85, 85, 0.2);\n}\n.result-error-item {\n  font-family: var(--f-mono);\n  font-size: 0.73rem;\n  color: #ff7777;\n  margin: 0.2rem 0;\n}\n\n.result-json {\n  font-family: var(--f-mono);\n  font-size: 0.76rem;\n  color: var(--c-text);\n  background: var(--c-panel);\n  border: 1px solid var(--c-border);\n  padding: 0.75rem 1rem;\n  overflow-x: auto;\n  white-space: pre;\n  line-height: 1.55;\n}\n\n.result-msg {\n  font-family: var(--f-mono);\n  font-size: 0.78rem;\n  color: var(--c-muted);\n  line-height: 1.5;\n}\n\n.result-empty {\n  font-family: var(--f-mono);\n  font-size: 0.73rem;\n  color: var(--c-muted);\n  opacity: 0.5;\n  font-style: italic;\n}\n\n.pg-variant-summary {\n  font-family: var(--f-mono);\n  font-size: 0.72rem;\n  color: var(--c-muted);\n  margin-bottom: 1rem;\n  line-height: 1.5;\n}\n"
  },
  {
    "path": "apps/docs/src/styles/themes/dark-brutalism.css",
    "content": "/* apps/docs/src/styles/themes/dark-brutalism.css */\n:root {\n  color-scheme: dark;\n  --c-bg: #0c0c0c;\n  --c-panel: #111111;\n  --c-panel-2: #0a0a0a;\n  --c-text: #ffffff;\n  --c-muted: #555555;\n  --c-accent: #c6ff00;\n  --c-accent-2: #c6ff00;\n  --c-accent-glow: rgba(198, 255, 0, 0.18);\n  --c-border: #1e1e1e;\n  --f-ui: 'Syne', sans-serif;\n  --f-mono: 'DM Mono', 'Fira Code', monospace;\n  --f-display: 'Syne', sans-serif;\n}\n"
  },
  {
    "path": "apps/docs/src/styles/themes/editorial.css",
    "content": "/* apps/docs/src/styles/themes/editorial.css */\n:root {\n  color-scheme: light;\n  --c-bg: #faf9f6;\n  --c-panel: #f0ece0;\n  --c-panel-2: #1a1a1a;\n  --c-text: #1a1a1a;\n  --c-muted: #888888;\n  --c-accent: #ff6600;\n  --c-accent-2: #1a1a1a;\n  --c-accent-glow: transparent;\n  --c-border: #cccccc;\n  --f-ui: 'Libre Baskerville', Georgia, serif;\n  --f-mono: 'DM Mono', 'Fira Code', monospace;\n  --f-display: 'Bebas Neue', 'Arial Black', sans-serif;\n}\n"
  },
  {
    "path": "apps/docs/src/styles/themes/linear-dark.css",
    "content": "/* apps/docs/src/styles/themes/linear-dark.css */\n:root {\n  color-scheme: dark;\n  --c-bg: #060609;\n  --c-panel: rgba(255, 255, 255, 0.025);\n  --c-panel-2: rgba(255, 255, 255, 0.04);\n  --c-text: #ffffff;\n  --c-muted: rgba(255, 255, 255, 0.4);\n  --c-accent: #5e56ff;\n  --c-accent-2: #9747ff;\n  --c-accent-glow: rgba(94, 86, 255, 0.22);\n  --c-border: rgba(255, 255, 255, 0.07);\n  --f-ui: 'Geist', 'Segoe UI', sans-serif;\n  --f-mono: 'DM Mono', 'Fira Code', monospace;\n  --f-display: 'Geist', 'Segoe UI', sans-serif;\n}\n"
  },
  {
    "path": "apps/docs/src/styles/themes/outrun-sunset.css",
    "content": "/* apps/docs/src/styles/themes/outrun-sunset.css */\n:root {\n  color-scheme: dark;\n  --c-bg: #050a1a;\n  --c-panel: #080f26;\n  --c-panel-2: #060c1e;\n  --c-text: #ffffff;\n  --c-muted: rgba(255, 255, 255, 0.35);\n  --c-accent: #ff8c00;\n  --c-accent-2: #ff2d55;\n  --c-accent-glow: rgba(255, 100, 0, 0.22);\n  --c-border: rgba(255, 100, 0, 0.2);\n  --f-ui: 'Syne', sans-serif;\n  --f-mono: 'DM Mono', 'Fira Code', monospace;\n  --f-display: 'Dela Gothic One', 'Syne', sans-serif;\n}\n"
  },
  {
    "path": "apps/docs/src/styles/themes/terminal-noir.css",
    "content": "/* apps/docs/src/styles/themes/terminal-noir.css */\n:root {\n  color-scheme: dark;\n  --c-bg: #030403;\n  --c-panel: #040c04;\n  --c-panel-2: #020802;\n  --c-text: #22ff44;\n  --c-muted: #2a6b2a;\n  --c-accent: #22ff44;\n  --c-accent-2: #22ff44;\n  --c-accent-glow: rgba(34, 255, 68, 0.18);\n  --c-border: #0f2e0f;\n  --f-ui: 'Space Mono', monospace;\n  --f-mono: 'Space Mono', monospace;\n  --f-display: 'Space Mono', monospace;\n}\n"
  },
  {
    "path": "apps/docs/test/api-docs-page.test.tsx",
    "content": "// @vitest-environment jsdom\n\nimport React from \"react\";\nimport { renderToStaticMarkup } from \"react-dom/server\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { ApiPackageNav } from \"../src/components/api/ApiPackageNav\";\nimport { ApiPackageSummaryList } from \"../src/components/api/ApiPackageSummaryList\";\nimport { ApiToc } from \"../src/components/api/ApiToc\";\n\ndescribe(\"ApiPackageNav\", () => {\n  it(\"renders package links and marks the active package\", () => {\n    const markup = renderToStaticMarkup(\n      <ApiPackageNav\n        activeSlug=\"react\"\n        basePath=\"/form2js/\"\n        packages={[\n          {\n            slug: \"core\",\n            packageName: \"@form2js/core\"\n          },\n          {\n            slug: \"react\",\n            packageName: \"@form2js/react\"\n          }\n        ]}\n      />\n    );\n\n    expect(markup).toContain(\"Packages\");\n    expect(markup).toContain('href=\"/form2js/api/react/\"');\n    expect(markup).toContain('aria-current=\"page\"');\n  });\n});\n\ndescribe(\"ApiPackageSummaryList\", () => {\n  it(\"renders package summaries with package routes\", () => {\n    const markup = renderToStaticMarkup(\n      <ApiPackageSummaryList\n        basePath=\"/form2js/\"\n        packages={[\n          {\n            slug: \"dom\",\n            packageName: \"@form2js/dom\",\n            summary: \"DOM parsing\"\n          }\n        ]}\n      />\n    );\n\n    expect(markup).toContain(\"@form2js/dom\");\n    expect(markup).toContain(\"DOM parsing\");\n    expect(markup).toContain('href=\"/form2js/api/dom/\"');\n  });\n});\n\ndescribe(\"ApiToc\", () => {\n  it(\"renders nested section links and marks the active section\", () => {\n    const markup = renderToStaticMarkup(\n      <ApiToc\n        headings={[\n          { depth: 2, slug: \"package-index\", text: \"Package Index\" },\n          { depth: 3, slug: \"common-tasks\", text: \"Common tasks\" },\n          { depth: 2, slug: \"api\", text: \"API\" }\n        ]}\n        initialActiveSlug=\"common-tasks\"\n      />\n    );\n\n    expect(markup).toContain(\"On this page\");\n    expect(markup).toContain('href=\"#package-index\"');\n    expect(markup).toContain('href=\"#common-tasks\"');\n    expect(markup).toContain('aria-current=\"true\"');\n  });\n});\n"
  },
  {
    "path": "apps/docs/test/api-docs-source.test.ts",
    "content": "import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { describe, expect, it } from \"vitest\";\n\nimport { parseApiDocsMarkdown } from \"../src/lib/api-docs-source\";\n\nconst testDir = path.dirname(fileURLToPath(import.meta.url));\nconst apiIndexMarkdown = readFileSync(path.resolve(testDir, \"../../../docs/api-index.md\"), \"utf8\");\nconst apiCoreMarkdown = readFileSync(path.resolve(testDir, \"../../../docs/api-core.md\"), \"utf8\");\nconst apiDomMarkdown = readFileSync(path.resolve(testDir, \"../../../docs/api-dom.md\"), \"utf8\");\nconst apiFormDataMarkdown = readFileSync(path.resolve(testDir, \"../../../docs/api-form-data.md\"), \"utf8\");\nconst apiJqueryMarkdown = readFileSync(path.resolve(testDir, \"../../../docs/api-jquery.md\"), \"utf8\");\nconst apiJs2formMarkdown = readFileSync(path.resolve(testDir, \"../../../docs/api-js2form.md\"), \"utf8\");\nconst apiReactMarkdown = readFileSync(path.resolve(testDir, \"../../../docs/api-react.md\"), \"utf8\");\nconst migrationMarkdown = readFileSync(path.resolve(testDir, \"../../../docs/migrate.md\"), \"utf8\");\nconst readmeMarkdown = readFileSync(path.resolve(testDir, \"../../../README.md\"), \"utf8\");\n\ndescribe(\"parseApiDocsMarkdown\", () => {\n  it(\"extracts the H1 title, intro copy, headings, and rewrites package markdown links\", () => {\n    const source = parseApiDocsMarkdown(\n      `# React API\n\nIntro with [index](api-index.md).\n\n## Installation\n\nText with [core](api-core.md).\n\n### npm\n\nMore text.\n`,\n      { basePath: \"/form2js/\" }\n    );\n\n    expect(source.title).toBe(\"React API\");\n    expect(source.introMarkdown).toContain(\"Intro with\");\n    expect(source.introHtml).toContain('href=\"/form2js/api/\"');\n    expect(source.bodyHtml).toContain('href=\"/form2js/api/core/\"');\n    expect(source.bodyHtml).toContain('id=\"installation\"');\n    expect(source.bodyHtml).toContain('id=\"npm\"');\n    expect(source.headings).toEqual([\n      { depth: 2, slug: \"installation\", text: \"Installation\" },\n      { depth: 3, slug: \"npm\", text: \"npm\" }\n    ]);\n  });\n\n  it(\"returns an empty intro when the first section starts immediately after the title\", () => {\n    const source = parseApiDocsMarkdown(\n      `# API Title\n\n## Section\n\nText.\n`,\n      { basePath: \"/\" }\n    );\n\n    expect(source.introMarkdown).toBe(\"\");\n    expect(source.introHtml).toBe(\"\");\n  });\n\n  it(\"deduplicates repeated heading slugs\", () => {\n    const source = parseApiDocsMarkdown(\n      `# API Title\n\nIntro\n\n## Repeated Name\n\n### Repeated Name\n\n## Repeated Name\n`,\n      { basePath: \"/\" }\n    );\n\n    expect(source.headings).toEqual([\n      { depth: 2, slug: \"repeated-name\", text: \"Repeated Name\" },\n      { depth: 3, slug: \"repeated-name-2\", text: \"Repeated Name\" },\n      { depth: 2, slug: \"repeated-name-3\", text: \"Repeated Name\" }\n    ]);\n  });\n\n  it(\"throws when the markdown does not start with an H1\", () => {\n    expect(() =>\n      parseApiDocsMarkdown(\n        `## Missing title\n\nText.\n`,\n        { basePath: \"/\" }\n      )\n    ).toThrow(\"API docs markdown must start with an H1 heading.\");\n  });\n\n  it(\"documents the split api markdown sources and updates the readme source link\", () => {\n    expect(apiIndexMarkdown).toContain(\"# form2js API Reference\");\n    expect(apiCoreMarkdown).toContain(\"## Installation\");\n    expect(apiCoreMarkdown).toContain(\"## General Example\");\n    expect(apiCoreMarkdown).toContain(\"## Types and Properties\");\n    expect(apiCoreMarkdown).toContain(\"npm install @form2js/core\");\n    expect(apiCoreMarkdown).toContain(\"### Schema validation\");\n    expect(apiCoreMarkdown).toContain(\"entriesToObject(rawEntries, { schema: PersonSchema })\");\n    expect(apiDomMarkdown).toContain(\"https://unpkg.com/@form2js/dom/dist/standalone.global.js\");\n    expect(apiDomMarkdown).toContain(\"### `useIdIfEmptyName`\");\n    expect(apiDomMarkdown).toContain(\"### `nodeCallback`\");\n    expect(apiFormDataMarkdown).toContain(\"### Schema validation\");\n    expect(apiFormDataMarkdown).toContain(\"formDataToObject(formData, { schema: PersonSchema })\");\n    expect(apiJqueryMarkdown).toContain(\"https://unpkg.com/@form2js/jquery/dist/standalone.global.js\");\n    expect(apiJqueryMarkdown).toContain(\"### `mode: \\\"all\\\"`\");\n    expect(apiJs2formMarkdown).toContain(\"### `shouldClean: false`\");\n    expect(apiJs2formMarkdown).toContain(\"### `useIdIfEmptyName`\");\n    expect(apiReactMarkdown).toContain(\"npm install @form2js/react react\");\n    expect(readmeMarkdown).toContain(\"[API Reference Source](docs/api-index.md)\");\n  });\n\n  it(\"parses the migration guide markdown and rewrites package links\", () => {\n    const source = parseApiDocsMarkdown(migrationMarkdown, {\n      basePath: \"/form2js/\"\n    });\n\n    expect(source.title).toBe(\"Migrate from Legacy form2js\");\n    expect(source.introMarkdown).toContain(\"single `form2js` script\");\n    expect(source.bodyHtml).toContain('href=\"/form2js/api/dom/\"');\n    expect(source.bodyHtml).toContain('href=\"/form2js/api/jquery/\"');\n    expect(source.bodyHtml).toContain('href=\"/form2js/api/form-data/\"');\n    expect(source.headings).toEqual(\n      expect.arrayContaining([\n        expect.objectContaining({ slug: \"quick-chooser\", text: \"Quick Chooser\" }),\n        expect.objectContaining({ slug: \"legacy-api-mapping\", text: \"Legacy API Mapping\" }),\n        expect.objectContaining({ slug: \"where-to-go-now\", text: \"Where To Go Now\" })\n      ])\n    );\n  });\n});\n"
  },
  {
    "path": "apps/docs/test/data-variants.test.tsx",
    "content": "// apps/docs/test/data-variants.test.tsx\n// @vitest-environment jsdom\n\nimport React, { act } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { PlaygroundShell } from \"../src/components/playground/PlaygroundShell\";\nimport type { OutputState, VariantComponentProps } from \"../src/components/playground/types\";\nimport { CoreVariant } from \"../src/components/playground/variants/core-variant\";\nimport { FormDataVariant } from \"../src/components/playground/variants/form-data-variant\";\n\ndeclare global {\n  var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;\n}\n\ninterface RenderResult {\n  container: HTMLDivElement;\n  root: ReturnType<typeof createRoot>;\n  getLastOutputState: () => OutputState | null;\n}\n\nfunction renderVariant(Component: (props: VariantComponentProps) => React.ReactNode): RenderResult {\n  let lastOutputState: OutputState | null = null;\n  const container = document.createElement(\"div\");\n  document.body.append(container);\n  const root = createRoot(container);\n  act(() => {\n    root.render(\n      <Component\n        isActive\n        onOutputChange={(outputState) => { lastOutputState = outputState; }}\n        reportFatalError={(errorInfo) => { throw new Error(`Unexpected fatal error: ${errorInfo.message}`); }}\n      />\n    );\n  });\n  return { container, root, getLastOutputState: () => lastOutputState };\n}\n\ndescribe(\"parser playground variants\", () => {\n  beforeEach(() => { globalThis.IS_REACT_ACT_ENVIRONMENT = true; });\n\n  afterEach(() => {\n    document.body.innerHTML = \"\";\n    vi.restoreAllMocks();\n  });\n\n  it(\"runs the core variant from seeded entry objects (Moist von Lipwig)\", () => {\n    const view = renderVariant(CoreVariant);\n    const jsonInput = view.container.querySelector<HTMLTextAreaElement>('textarea[name=\"core-entries-json\"]');\n    const runButton = [...view.container.querySelectorAll(\"button\")].find((button) =>\n      button.textContent?.includes(\"@form2js/core\")\n    );\n\n    expect(jsonInput?.value).toContain('\"key\": \"person.name.first\"');\n    expect(jsonInput?.value).toContain(\"Moist\");\n\n    act(() => {\n      runButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(view.getLastOutputState()).toEqual({\n      kind: \"standard\",\n      status: \"success\",\n      statusMessage: \"@form2js/core -> entriesToObject(entry objects)\",\n      errorMessage: null,\n      parsedPayload: {\n        person: {\n          city: \"ankh-morpork\",\n          guild: \"thieves\",\n          name: { first: \"Moist\", last: \"von Lipwig\" },\n          tags: [\"crime\", \"banking\"]\n        }\n      }\n    });\n\n    act(() => { view.root.unmount(); });\n  });\n\n  it(\"runs the form-data variant from a submitted form (Tiffany Aching)\", () => {\n    const view = renderVariant(FormDataVariant);\n    const firstNameInput = view.container.querySelector<HTMLInputElement>('input[name=\"person.name.first\"]');\n    const form = view.container.querySelector(\"form\");\n\n    expect(firstNameInput?.value).toBe(\"Tiffany\");\n    expect(form).not.toBeNull();\n\n    act(() => {\n      form?.dispatchEvent(new Event(\"submit\", { bubbles: true, cancelable: true }));\n    });\n\n    expect(view.getLastOutputState()).toEqual({\n      kind: \"standard\",\n      status: \"success\",\n      statusMessage: \"@form2js/form-data -> formDataToObject(form)\",\n      errorMessage: null,\n      parsedPayload: {\n        person: {\n          city: \"quirm\",\n          guild: \"witches\",\n          name: { first: \"Tiffany\", last: \"Aching\" },\n          tags: [\"witch\"]\n        }\n      }\n    });\n\n    act(() => { view.root.unmount(); });\n  });\n\n  it(\"reports runtime merge failures from the core parser instead of labeling them as JSON parse errors\", () => {\n    const view = renderVariant(CoreVariant);\n    const jsonInput = view.container.querySelector<HTMLTextAreaElement>('textarea[name=\"core-entries-json\"]');\n    const runButton = [...view.container.querySelectorAll(\"button\")].find((button) =>\n      button.textContent?.includes(\"@form2js/core\")\n    );\n\n    act(() => {\n      if (!jsonInput) throw new Error(\"Missing core JSON input.\");\n      jsonInput.value = JSON.stringify([\n        { key: \"person\", value: \"watch\" },\n        { key: \"person.name.first\", value: \"Sam\" }\n      ]);\n      jsonInput.dispatchEvent(new Event(\"input\", { bubbles: true }));\n      runButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(view.getLastOutputState()).toEqual({\n      kind: \"standard\",\n      status: \"error\",\n      statusMessage: \"core parse failed.\",\n      errorMessage: \"Core conversion failed: Expected object-like container while setting nested path\",\n      parsedPayload: null\n    });\n\n    act(() => { view.root.unmount(); });\n  });\n\n  it(\"preserves core output when switching away and back through the shell\", () => {\n    const container = document.createElement(\"div\");\n    document.body.append(container);\n    const root = createRoot(container);\n\n    act(() => { root.render(<PlaygroundShell />); });\n\n    const coreButton = container.querySelector('button[data-variant-id=\"core\"]');\n    act(() => { coreButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true })); });\n\n    const runButton = [...container.querySelectorAll(\"button\")].find((button) =>\n      button.textContent?.includes(\"@form2js/core\")\n    );\n    act(() => { runButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true })); });\n\n    expect(container.textContent).toContain(\"@form2js/core -> entriesToObject(entry objects)\");\n    expect(container.textContent).toContain(\"von Lipwig\");\n\n    const formButton = container.querySelector('button[data-variant-id=\"form\"]');\n    act(() => { formButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true })); });\n    act(() => { coreButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true })); });\n\n    expect(container.textContent).toContain(\"@form2js/core -> entriesToObject(entry objects)\");\n    expect(container.textContent).toContain(\"von Lipwig\");\n\n    act(() => { root.unmount(); });\n  });\n});\n"
  },
  {
    "path": "apps/docs/test/docs-pipeline.test.ts",
    "content": "import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { describe, expect, it } from \"vitest\";\n\nconst testDir = path.dirname(fileURLToPath(import.meta.url));\nconst repoRoot = path.resolve(testDir, \"../../..\");\nconst turboConfig = JSON.parse(readFileSync(path.join(repoRoot, \"turbo.json\"), \"utf8\")) as {\n  tasks?: Record<string, { dependsOn?: string[] }>;\n};\n\ndescribe(\"docs CI task graph\", () => {\n  it(\"builds workspace package dependencies before docs lint and typecheck\", () => {\n    expect(turboConfig.tasks?.lint?.dependsOn).toEqual(expect.arrayContaining([\"^build\"]));\n    expect(turboConfig.tasks?.typecheck?.dependsOn).toEqual(expect.arrayContaining([\"^build\"]));\n  });\n});\n"
  },
  {
    "path": "apps/docs/test/docs-root-scripts.test.ts",
    "content": "import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { describe, expect, it } from \"vitest\";\n\nconst testDir = path.dirname(fileURLToPath(import.meta.url));\nconst repoRoot = path.resolve(testDir, \"../../..\");\nconst rootPackageJson = JSON.parse(readFileSync(path.join(repoRoot, \"package.json\"), \"utf8\")) as {\n  scripts?: Record<string, string>;\n};\n\ndescribe(\"root docs scripts\", () => {\n  it(\"builds workspace packages before local docs commands that depend on package dist output\", () => {\n    expect(rootPackageJson.scripts?.docs).toContain(\"npm run build:packages\");\n    expect(rootPackageJson.scripts?.[\"docs:build\"]).toContain(\"npm run build:packages\");\n    expect(rootPackageJson.scripts?.[\"test:docs\"]).toContain(\"npm run build:packages\");\n  });\n});\n"
  },
  {
    "path": "apps/docs/test/homepage-shell.test.ts",
    "content": "import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { describe, expect, it } from \"vitest\";\n\nconst testDir = path.dirname(fileURLToPath(import.meta.url));\nconst homepageSource = readFileSync(path.resolve(testDir, \"../src/pages/index.astro\"), \"utf8\");\nconst installSectionSource = readFileSync(\n  path.resolve(testDir, \"../src/components/landing/InstallSection.astro\"),\n  \"utf8\"\n);\nconst docsShellSource = readFileSync(path.resolve(testDir, \"../src/layouts/DocsShell.astro\"), \"utf8\");\nconst readmeSource = readFileSync(path.resolve(testDir, \"../../../README.md\"), \"utf8\");\n\ndescribe(\"docs homepage shell\", () => {\n  it(\"wires the landing page sections together\", () => {\n    expect(homepageSource).toContain(\"<Hero />\");\n    expect(homepageSource).toContain('id=\"playground\"');\n    expect(homepageSource).toContain(\"<InstallSection />\");\n    expect(homepageSource).toContain(\"<ApiDocsCta />\");\n    expect(readFileSync(path.resolve(testDir, \"../src/components/landing/Hero.astro\"), \"utf8\")).toContain(\n      \"npm install @form2js/react react\"\n    );\n  });\n\n  it(\"includes npm and standalone install guidance for supported variants\", () => {\n    expect(installSectionSource).toContain(\"npm install @form2js/react react\");\n    expect(installSectionSource).toContain(\"https://unpkg.com/@form2js/dom/dist/standalone.global.js\");\n    expect(installSectionSource).toContain(\"https://unpkg.com/@form2js/jquery/dist/standalone.global.js\");\n  });\n\n  it(\"surfaces the migration guide in the shared docs chrome and the README\", () => {\n    expect(docsShellSource).toContain(\"migrationGuidePath\");\n    expect(docsShellSource).toContain(\">Migration<\");\n    expect(readmeSource).toContain(\"Migrating from legacy form2js?\");\n    expect(readmeSource).toContain(\"https://maxatwork.github.io/form2js/migrate/\");\n    expect(readmeSource).toContain(\"## Migration from Legacy\");\n  });\n});\n"
  },
  {
    "path": "apps/docs/test/playground-shell.test.tsx",
    "content": "// @vitest-environment jsdom\n\nimport React, { act } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport type { OutputState, VariantComponentProps, VariantDefinition, VariantId } from \"../src/components/playground/types\";\n\nfunction createReactOutput(statusMessage: string): OutputState {\n  return {\n    kind: \"react\",\n    status: \"success\",\n    statusMessage,\n    submitFlags: {\n      isSubmitting: false,\n      isError: false,\n      isSuccess: true\n    },\n    error: null,\n    parsedPayload: { ok: true },\n    meta: {\n      submitMode: \"onSubmit\",\n      validationEnabled: true\n    }\n  };\n}\n\nfunction createStandardOutput(statusMessage: string): OutputState {\n  return {\n    kind: \"standard\",\n    status: \"success\",\n    statusMessage,\n    errorMessage: null,\n    parsedPayload: { ok: true }\n  };\n}\n\nfunction makeVariant(\n  id: VariantId,\n  kind: VariantDefinition[\"kind\"],\n  label: string,\n  statusMessage: string,\n  options?: {\n    throwWhenActive?: boolean;\n  }\n): VariantDefinition {\n  function Component(props: VariantComponentProps) {\n    if (options?.throwWhenActive && props.isActive) {\n      throw new Error(`${label} render exploded`);\n    }\n\n    return (\n      <div data-testid={`${id}-variant`}>\n        <span>{props.isActive ? `${label} active` : `${label} hidden`}</span>\n        <button\n          onClick={() => {\n            props.onOutputChange(\n              kind === \"react\" ? createReactOutput(statusMessage) : createStandardOutput(statusMessage)\n            );\n          }}\n          type=\"button\"\n        >\n          Emit {label}\n        </button>\n        <button\n          onClick={() => {\n            props.reportFatalError({\n              message: `${label} crashed`,\n              source: \"event\"\n            });\n          }}\n          type=\"button\"\n        >\n          Fail {label}\n        </button>\n      </div>\n    );\n  }\n\n  return {\n    id,\n    kind,\n    label,\n    summary: `${label} summary`,\n    packages: [`@form2js/${id}`],\n    createInitialOutputState: () =>\n      kind === \"react\"\n        ? {\n            kind: \"react\",\n            status: \"idle\",\n            statusMessage: `Ready for ${label}`,\n            submitFlags: {\n              isSubmitting: false,\n              isError: false,\n              isSuccess: false\n            },\n            error: null,\n            parsedPayload: null\n          }\n        : {\n            kind: \"standard\",\n            status: \"idle\",\n            statusMessage: `Ready for ${label}`,\n            errorMessage: null,\n            parsedPayload: null\n          },\n    Component\n  };\n}\n\nfunction resetMockVariants(): void {\n  mockVariantsById.react = makeVariant(\"react\", \"react\", \"React\", \"React complete\");\n}\n\nconst { mockVariantsById } = vi.hoisted(() => ({\n  mockVariantsById: {\n    react: makeVariant(\"react\", \"react\", \"React\", \"React complete\"),\n    form: makeVariant(\"form\", \"standard\", \"Form\", \"Form complete\"),\n    jquery: makeVariant(\"jquery\", \"standard\", \"jQuery\", \"jQuery complete\"),\n    js2form: makeVariant(\"js2form\", \"standard\", \"js2form\", \"js2form complete\"),\n    core: makeVariant(\"core\", \"standard\", \"Core\", \"Core complete\"),\n    \"form-data\": makeVariant(\"form-data\", \"standard\", \"FormData\", \"FormData complete\")\n  } satisfies Record<VariantId, VariantDefinition>\n}));\n\nvi.mock(\"../src/components/playground/variant-registry\", () => ({\n  VARIANT_IDS: [\"react\", \"form\", \"jquery\", \"js2form\", \"core\", \"form-data\"],\n  variantsById: mockVariantsById\n}));\n\nimport { PlaygroundShell } from \"../src/components/playground/PlaygroundShell\";\n\ndeclare global {\n  var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;\n}\n\nlet container: HTMLDivElement;\nlet root: ReturnType<typeof createRoot>;\n\nbeforeEach(() => {\n  globalThis.IS_REACT_ACT_ENVIRONMENT = true;\n  resetMockVariants();\n  vi.spyOn(console, \"error\").mockImplementation(() => undefined);\n  container = document.createElement(\"div\");\n  document.body.append(container);\n  root = createRoot(container);\n  window.history.replaceState({}, \"\", \"/\");\n});\n\nafterEach(() => {\n  act(() => {\n    root.unmount();\n  });\n  container.remove();\n  resetMockVariants();\n  vi.restoreAllMocks();\n});\n\nfunction renderShell(): void {\n  act(() => {\n    root.render(<PlaygroundShell />);\n  });\n}\n\ndescribe(\"PlaygroundShell\", () => {\n  it(\"renders the active variant and chooses the output panel by kind\", () => {\n    window.history.replaceState({}, \"\", \"/?variant=react\");\n\n    renderShell();\n\n    expect(container.textContent).toContain(\"React summary\");\n    expect(container.textContent).toContain(\"React active\");\n    expect(container.textContent).toContain(\"Submit state\");\n    expect(container.textContent).toContain(\"Ready for React\");\n  });\n\n  it(\"updates the query string when switching variants\", () => {\n    window.history.replaceState({}, \"\", \"/docs/start?variant=react#top\");\n\n    renderShell();\n\n    const formButton = container.querySelector('button[data-variant-id=\"form\"]');\n    expect(formButton).not.toBeNull();\n\n    act(() => {\n      formButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(window.location.pathname).toBe(\"/docs/start\");\n    expect(window.location.search).toBe(\"?variant=form\");\n    expect(window.location.hash).toBe(\"\");\n    expect(container.textContent).toContain(\"Form active\");\n    expect(container.textContent).toContain(\"Parsed result\");\n  });\n\n  it(\"does not render placeholder payload output while a variant is idle\", () => {\n    window.history.replaceState({}, \"\", \"/?variant=react\");\n\n    renderShell();\n\n    expect(container.textContent).toContain(\"Ready for React\");\n    expect(container.textContent).not.toContain(\"null\");\n\n    const formButton = container.querySelector('button[data-variant-id=\"form\"]');\n    act(() => {\n      formButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(container.textContent).toContain(\"Ready for Form\");\n    expect(container.textContent).not.toContain(\"null\");\n  });\n\n  it(\"preserves emitted output state when switching away and back\", () => {\n    renderShell();\n\n    const emitReact = [...container.querySelectorAll(\"button\")].find((button) => button.textContent === \"Emit React\");\n    expect(emitReact).not.toBeUndefined();\n\n    act(() => {\n      emitReact?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(container.textContent).toContain(\"React complete\");\n    expect(container.textContent).toContain(\"submitMode\");\n    expect(container.textContent).toContain(\"onSubmit\");\n    expect(container.textContent).toContain(\"validationEnabled\");\n    expect(container.textContent).toContain(\"true\");\n\n    const formButton = container.querySelector('button[data-variant-id=\"form\"]');\n    act(() => {\n      formButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(container.textContent).toContain(\"React hidden\");\n\n    const reactButton = container.querySelector('button[data-variant-id=\"react\"]');\n    act(() => {\n      reactButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(container.textContent).toContain(\"React complete\");\n  });\n\n  it(\"renders a failed-state fallback and keeps it when revisiting the variant\", () => {\n    renderShell();\n\n    const emitReact = [...container.querySelectorAll(\"button\")].find((button) => button.textContent === \"Emit React\");\n    const failReact = [...container.querySelectorAll(\"button\")].find((button) => button.textContent === \"Fail React\");\n    expect(emitReact).not.toBeUndefined();\n    expect(failReact).not.toBeUndefined();\n\n    act(() => {\n      emitReact?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(container.textContent).toContain(\"React complete\");\n\n    act(() => {\n      failReact?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(container.textContent).toContain(\"React crashed\");\n    expect(container.textContent).not.toContain(\"React complete\");\n    expect(container.textContent).not.toContain(\"submitMode\");\n    expect(container.textContent).not.toContain(\"React active\");\n\n    const formButton = container.querySelector('button[data-variant-id=\"form\"]');\n    act(() => {\n      formButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    const reactButton = container.querySelector('button[data-variant-id=\"react\"]');\n    act(() => {\n      reactButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(container.textContent).toContain(\"React crashed\");\n    expect(container.textContent).not.toContain(\"React complete\");\n    expect(container.textContent).not.toContain(\"submitMode\");\n  });\n\n  it(\"isolates a thrown active-variant render failure and keeps the switcher usable\", async () => {\n    mockVariantsById.react = makeVariant(\"react\", \"react\", \"React\", \"React complete\", {\n      throwWhenActive: true\n    });\n\n    renderShell();\n\n    await act(async () => {\n      await Promise.resolve();\n    });\n\n    expect(container.textContent).toContain(\"React failed\");\n    expect(container.textContent).toContain(\"React render exploded\");\n    expect(container.textContent).not.toContain(\"Form failed\");\n\n    const formButton = container.querySelector('button[data-variant-id=\"form\"]');\n    expect(formButton).not.toBeNull();\n\n    act(() => {\n      formButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(container.textContent).toContain(\"Form active\");\n    expect(container.textContent).toContain(\"Parsed result\");\n\n    const reactButton = container.querySelector('button[data-variant-id=\"react\"]');\n    act(() => {\n      reactButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    await act(async () => {\n      await Promise.resolve();\n    });\n\n    expect(container.textContent).toContain(\"React failed\");\n    expect(container.textContent).toContain(\"React render exploded\");\n\n  });\n});\n"
  },
  {
    "path": "apps/docs/test/playground-styles.test.ts",
    "content": "import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport { describe, expect, it } from \"vitest\";\n\nconst stylesheetPath = path.resolve(import.meta.dirname, \"../src/styles/playground.css\");\nconst stylesheet = readFileSync(stylesheetPath, \"utf8\");\n\ndescribe(\"playground responsive styles\", () => {\n  it(\"drops the desktop form-column divider in the one-column mobile layout\", () => {\n    expect(stylesheet).toContain(\"@media (max-width: 760px)\");\n    expect(stylesheet).toContain(\".pg-form-col { border-right: none; }\");\n  });\n});\n"
  },
  {
    "path": "apps/docs/test/react-variant.test.tsx",
    "content": "// apps/docs/test/react-variant.test.tsx\n// @vitest-environment jsdom\n\nimport React, { act } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport type { OutputState, VariantComponentProps } from \"../src/components/playground/types\";\nimport { ReactVariant } from \"../src/components/playground/variants/react-variant\";\n\ndeclare global {\n  var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;\n}\n\ninterface RenderResult {\n  container: HTMLDivElement;\n  root: ReturnType<typeof createRoot>;\n  getLastOutputState: () => OutputState | null;\n}\n\nfunction renderVariant(Component: (props: VariantComponentProps) => React.ReactNode): RenderResult {\n  let lastOutputState: OutputState | null = null;\n  const container = document.createElement(\"div\");\n  document.body.append(container);\n  const root = createRoot(container);\n  act(() => {\n    root.render(\n      <Component\n        isActive\n        onOutputChange={(outputState) => { lastOutputState = outputState; }}\n        reportFatalError={(errorInfo) => { throw new Error(`Unexpected fatal error: ${errorInfo.message}`); }}\n      />\n    );\n  });\n  return { container, root, getLastOutputState: () => lastOutputState };\n}\n\nasync function submitForm(container: HTMLDivElement): Promise<void> {\n  const form = container.querySelector(\"form\");\n  if (!form) throw new Error(\"Missing React variant form.\");\n  await act(async () => {\n    form.dispatchEvent(new Event(\"submit\", { bubbles: true, cancelable: true }));\n    await Promise.resolve();\n  });\n}\n\ndescribe(\"ReactVariant\", () => {\n  beforeEach(() => {\n    globalThis.IS_REACT_ACT_ENVIRONMENT = true;\n    vi.useFakeTimers();\n  });\n\n  afterEach(() => {\n    document.body.innerHTML = \"\";\n    vi.useRealTimers();\n    vi.restoreAllMocks();\n  });\n\n  it(\"submits the seeded valid form and emits react output states\", async () => {\n    const view = renderVariant(ReactVariant);\n    const firstNameInput = view.container.querySelector<HTMLInputElement>('input[name=\"person.name.first\"]');\n    const emailInput = view.container.querySelector<HTMLInputElement>('input[name=\"person.email\"]');\n\n    expect(firstNameInput?.value).toBe(\"Sam\");\n    expect(emailInput?.value).toBe(\"sam.vimes@ankh-morpork.gov\");\n\n    await submitForm(view.container);\n\n    expect(view.getLastOutputState()).toMatchObject({\n      kind: \"react\",\n      status: \"running\",\n      submitFlags: { isSubmitting: true, isError: false, isSuccess: false },\n      parsedPayload: null,\n      meta: { submitMode: \"onSubmit\", validationEnabled: true }\n    });\n\n    await act(async () => {\n      vi.advanceTimersByTime(900);\n      await Promise.resolve();\n    });\n\n    expect(view.getLastOutputState()).toEqual({\n      kind: \"react\",\n      status: \"success\",\n      statusMessage: \"Callback resolved\",\n      submitFlags: { isSubmitting: false, isError: false, isSuccess: true },\n      error: null,\n      parsedPayload: {\n        person: {\n          age: 45,\n          email: \"sam.vimes@ankh-morpork.gov\",\n          guild: \"watchman\",\n          interests: [\"city-watch\", \"cigars\"],\n          name: { first: \"Sam\", last: \"Vimes\" }\n        }\n      },\n      meta: { submitMode: \"onSubmit\", validationEnabled: true }\n    });\n\n    act(() => { view.root.unmount(); });\n  });\n\n  it(\"emits validation failure output when seeded fields become invalid\", async () => {\n    const view = renderVariant(ReactVariant);\n    const emailInput = view.container.querySelector<HTMLInputElement>('input[name=\"person.email\"]');\n\n    act(() => {\n      if (!emailInput) throw new Error(\"Missing email input.\");\n      emailInput.value = \"bad-email\";\n      emailInput.dispatchEvent(new Event(\"input\", { bubbles: true }));\n    });\n\n    await submitForm(view.container);\n\n    expect(view.getLastOutputState()).toEqual({\n      kind: \"react\",\n      status: \"error\",\n      statusMessage: \"Submit failed\",\n      submitFlags: { isSubmitting: false, isError: true, isSuccess: false },\n      error: { message: \"person.email: Email must be valid.\" },\n      parsedPayload: null,\n      meta: { submitMode: \"onSubmit\", validationEnabled: true }\n    });\n\n    act(() => { view.root.unmount(); });\n  });\n\n  it(\"emits callback failure output when the Force Error button is clicked\", async () => {\n    const view = renderVariant(ReactVariant);\n    const forceErrorButton = [...view.container.querySelectorAll(\"button\")].find((button) =>\n      button.textContent?.includes(\"Force Error\")\n    );\n\n    expect(forceErrorButton).toBeDefined();\n\n    act(() => {\n      forceErrorButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(view.getLastOutputState()).toMatchObject({\n      kind: \"react\",\n      status: \"running\"\n    });\n\n    await act(async () => {\n      vi.advanceTimersByTime(900);\n      await Promise.resolve();\n    });\n\n    expect(view.getLastOutputState()).toEqual({\n      kind: \"react\",\n      status: \"error\",\n      statusMessage: \"Submit failed\",\n      submitFlags: { isSubmitting: false, isError: true, isSuccess: false },\n      error: { message: \"Simulated server error.\" },\n      parsedPayload: null,\n      meta: { submitMode: \"onSubmit\", validationEnabled: true }\n    });\n\n    act(() => { view.root.unmount(); });\n  });\n\n  it(\"clears a queued force-error after a validation failure so the next valid submit succeeds\", async () => {\n    const view = renderVariant(ReactVariant);\n    const forceErrorButton = [...view.container.querySelectorAll(\"button\")].find((button) =>\n      button.textContent?.includes(\"Force Error\")\n    );\n    const emailInput = view.container.querySelector<HTMLInputElement>('input[name=\"person.email\"]');\n\n    expect(forceErrorButton).toBeDefined();\n\n    act(() => {\n      if (!emailInput) throw new Error(\"Missing email input.\");\n      emailInput.value = \"bad-email\";\n      emailInput.dispatchEvent(new Event(\"input\", { bubbles: true }));\n      forceErrorButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    act(() => {\n      if (!emailInput) throw new Error(\"Missing email input.\");\n      emailInput.value = \"sam.vimes@ankh-morpork.gov\";\n      emailInput.dispatchEvent(new Event(\"input\", { bubbles: true }));\n    });\n\n    await submitForm(view.container);\n\n    expect(view.getLastOutputState()).toMatchObject({\n      kind: \"react\",\n      status: \"running\"\n    });\n\n    await act(async () => {\n      vi.advanceTimersByTime(900);\n      await Promise.resolve();\n    });\n\n    expect(view.getLastOutputState()).toMatchObject({\n      kind: \"react\",\n      status: \"success\",\n      statusMessage: \"Callback resolved\",\n      submitFlags: { isSubmitting: false, isError: false, isSuccess: true },\n      error: null,\n      meta: { submitMode: \"onSubmit\", validationEnabled: true }\n    });\n\n    act(() => { view.root.unmount(); });\n  });\n\n  it(\"resets back to idle and clears the last successful payload\", async () => {\n    const view = renderVariant(ReactVariant);\n    const resetButton = [...view.container.querySelectorAll(\"button\")].find((button) =>\n      button.textContent?.includes(\"Reset state\")\n    );\n\n    await submitForm(view.container);\n    await act(async () => {\n      vi.advanceTimersByTime(900);\n      await Promise.resolve();\n    });\n\n    expect(view.getLastOutputState()).toMatchObject({ kind: \"react\", status: \"success\" });\n\n    act(() => {\n      resetButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(view.getLastOutputState()).toEqual({\n      kind: \"react\",\n      status: \"idle\",\n      statusMessage: \"Ready to submit.\",\n      submitFlags: { isSubmitting: false, isError: false, isSuccess: false },\n      error: null,\n      parsedPayload: null,\n      meta: { submitMode: \"onSubmit\", validationEnabled: true }\n    });\n\n    act(() => { view.root.unmount(); });\n  });\n});\n"
  },
  {
    "path": "apps/docs/test/site-routes.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport {\n  apiDocsPath,\n  apiPackageDocsPath,\n  homepagePath,\n  homepageVariantPath,\n  migrationGuidePath\n} from \"../src/lib/site-routes\";\n\ndescribe(\"site routes\", () => {\n  it(\"builds homepage, migration, and api paths under a base path\", () => {\n    expect(homepagePath(\"/form2js/\")).toBe(\"/form2js/\");\n    expect(migrationGuidePath(\"/form2js/\")).toBe(\"/form2js/migrate/\");\n    expect(migrationGuidePath(\"/\")).toBe(\"/migrate/\");\n    expect(apiDocsPath(\"/form2js/\")).toBe(\"/form2js/api/\");\n    expect(apiPackageDocsPath(\"/form2js/\", \"react\")).toBe(\"/form2js/api/react/\");\n    expect(apiPackageDocsPath(\"/\", \"form-data\")).toBe(\"/api/form-data/\");\n  });\n\n  it(\"adds variant query params to the homepage only\", () => {\n    expect(homepageVariantPath(\"/form2js/\", \"react\")).toBe(\"/form2js/?variant=react\");\n    expect(homepageVariantPath(\"/\", \"form-data\")).toBe(\"/?variant=form-data\");\n  });\n});\n"
  },
  {
    "path": "apps/docs/test/standard-variants.test.tsx",
    "content": "// apps/docs/test/standard-variants.test.tsx\n// @vitest-environment jsdom\n\nimport React, { act } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport type { OutputState, VariantComponentProps } from \"../src/components/playground/types\";\nimport { ensureJqueryBootstrap } from \"../src/components/playground/bootstrap/jquery-bootstrap\";\nimport { FormVariant } from \"../src/components/playground/variants/form-variant\";\nimport { JQueryVariant } from \"../src/components/playground/variants/jquery-variant\";\nimport { Js2FormVariant } from \"../src/components/playground/variants/js2form-variant\";\n\ndeclare global {\n  var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;\n}\n\ninterface RenderResult {\n  container: HTMLDivElement;\n  root: ReturnType<typeof createRoot>;\n  getLastOutputState: () => OutputState | null;\n  render: () => void;\n}\n\nfunction renderVariant(Component: (props: VariantComponentProps) => React.ReactNode): RenderResult {\n  let lastOutputState: OutputState | null = null;\n  const container = document.createElement(\"div\");\n  document.body.append(container);\n  const root = createRoot(container);\n\n  function render(): void {\n    act(() => {\n      root.render(\n        <Component\n          isActive\n          onOutputChange={(outputState) => { lastOutputState = outputState; }}\n          reportFatalError={(errorInfo) => { throw new Error(`Unexpected fatal error: ${errorInfo.message}`); }}\n        />\n      );\n    });\n  }\n\n  render();\n  return { container, root, render, getLastOutputState: () => lastOutputState };\n}\n\ndescribe(\"standard playground variants\", () => {\n  beforeEach(() => { globalThis.IS_REACT_ACT_ENVIRONMENT = true; });\n\n  afterEach(() => {\n    document.body.innerHTML = \"\";\n    vi.restoreAllMocks();\n  });\n\n  it(\"runs the form variant with seeded controls via form submit\", () => {\n    const view = renderVariant(FormVariant);\n\n    const firstNameInput = view.container.querySelector<HTMLInputElement>('input[name=\"person.name.first\"]');\n    const lastNameInput = view.container.querySelector<HTMLInputElement>('input[name=\"person.name.last\"]');\n    const form = view.container.querySelector(\"form\");\n\n    expect(firstNameInput?.value).toBe(\"Esme\");\n    expect(lastNameInput?.value).toBe(\"Weatherwax\");\n    expect(form).not.toBeNull();\n\n    act(() => {\n      form?.dispatchEvent(new Event(\"submit\", { bubbles: true, cancelable: true }));\n    });\n\n    expect(view.getLastOutputState()).toEqual({\n      kind: \"standard\",\n      status: \"success\",\n      statusMessage: \"@form2js/dom -> formToObject(form)\",\n      errorMessage: null,\n      parsedPayload: {\n        person: {\n          city: \"lancre\",\n          guild: \"witches\",\n          name: { first: \"Esme\", last: \"Weatherwax\" },\n          tags: [\"witch\", \"headology\"]\n        }\n      }\n    });\n\n    act(() => { view.root.unmount(); });\n  });\n\n  it(\"runs the jquery variant in combine mode and installs the plugin idempotently\", () => {\n    const beforeInstall = ensureJqueryBootstrap();\n    const secondInstall = ensureJqueryBootstrap();\n    expect(secondInstall).toBe(beforeInstall);\n\n    const view = renderVariant(JQueryVariant);\n    const modeSelect = view.container.querySelector<HTMLSelectElement>('select[name=\"jquery-mode\"]');\n    const runButton = [...view.container.querySelectorAll(\"button\")].find((button) =>\n      button.textContent?.includes(\"@form2js/jquery\")\n    );\n\n    expect(modeSelect?.value).toBe(\"combine\");\n\n    act(() => {\n      runButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(view.getLastOutputState()).toEqual({\n      kind: \"standard\",\n      status: \"success\",\n      statusMessage: '@form2js/jquery -> $(\".jq-slice\").toObject({ mode: \"combine\" })',\n      errorMessage: null,\n      parsedPayload: {\n        person: {\n          city: \"lancre\",\n          first: \"Gytha\",\n          guild: \"witches\",\n          last: \"Ogg\"\n        }\n      }\n    });\n\n    act(() => { view.root.unmount(); });\n  });\n\n  it(\"applies js2form data, reports invalid JSON, and resets back to idle\", () => {\n    const view = renderVariant(Js2FormVariant);\n    const jsonInput = view.container.querySelector<HTMLTextAreaElement>('textarea[name=\"js2form-json\"]');\n    const firstNameInput = view.container.querySelector<HTMLInputElement>('input[name=\"person.name.first\"]');\n    const applyButton = [...view.container.querySelectorAll(\"button\")].find((button) =>\n      button.textContent?.includes(\"Apply js2form\")\n    );\n    const resetButton = [...view.container.querySelectorAll(\"button\")].find((button) =>\n      button.textContent?.includes(\"Reset form\")\n    );\n\n    expect(jsonInput?.value).toContain('\"first\": \"Tiffany\"');\n    expect(firstNameInput?.value).toBe(\"Esme\");\n\n    act(() => {\n      if (!jsonInput) throw new Error(\"Missing js2form JSON input.\");\n      jsonInput.value = \"{\";\n      jsonInput.dispatchEvent(new Event(\"input\", { bubbles: true }));\n      applyButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(view.getLastOutputState()).toEqual({\n      kind: \"standard\",\n      status: \"error\",\n      statusMessage: \"js2form apply failed.\",\n      errorMessage: \"JSON parse error: please provide valid JSON before applying js2form.\",\n      parsedPayload: null\n    });\n\n    act(() => {\n      if (!jsonInput) throw new Error(\"Missing js2form JSON input.\");\n      jsonInput.value = `{\n  \"person\": {\n    \"name\": { \"first\": \"Tiffany\", \"last\": \"Aching\" },\n    \"city\": \"quirm\",\n    \"tags\": [\"witch\"]\n  }\n}`;\n      jsonInput.dispatchEvent(new Event(\"input\", { bubbles: true }));\n      applyButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(view.getLastOutputState()).toEqual({\n      kind: \"standard\",\n      status: \"success\",\n      statusMessage: \"@form2js/js2form -> objectToForm(...), then formToObject(...)\",\n      errorMessage: null,\n      parsedPayload: {\n        person: { city: \"quirm\", name: { first: \"Tiffany\", last: \"Aching\" }, tags: [\"witch\"] }\n      }\n    });\n\n    expect(firstNameInput?.value).toBe(\"Tiffany\");\n\n    act(() => {\n      resetButton?.dispatchEvent(new MouseEvent(\"click\", { bubbles: true }));\n    });\n\n    expect(firstNameInput?.value).toBe(\"Esme\");\n    expect(view.getLastOutputState()).toEqual({\n      kind: \"standard\",\n      status: \"idle\",\n      statusMessage: \"Ready to apply object data.\",\n      errorMessage: null,\n      parsedPayload: null\n    });\n\n    act(() => { view.root.unmount(); });\n  });\n});\n"
  },
  {
    "path": "apps/docs/test/variant-registry.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { VARIANT_IDS, variantsById } from \"../src/components/playground/variant-registry\";\n\ndescribe(\"variant registry\", () => {\n  it(\"registers all expected variants\", () => {\n    expect(VARIANT_IDS).toEqual([\n      \"react\",\n      \"form\",\n      \"jquery\",\n      \"js2form\",\n      \"core\",\n      \"form-data\"\n    ]);\n  });\n\n  it(\"provides seeded idle output state for every variant\", () => {\n    for (const id of VARIANT_IDS) {\n      const variant = variantsById[id];\n\n      expect(variant.id).toBe(id);\n      expect(variant.label.length).toBeGreaterThan(0);\n      expect(variant.summary.length).toBeGreaterThan(0);\n      expect(variant.packages.length).toBeGreaterThan(0);\n      expect(variant.createInitialOutputState().status).toBe(\"idle\");\n      expect(variant.createInitialOutputState().kind).toBe(variant.kind);\n    }\n  });\n});\n"
  },
  {
    "path": "apps/docs/test-e2e/api-docs.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\n\ntest(\"api docs index links to package pages and package toc anchors work on mobile\", async ({ page }) => {\n  await page.setViewportSize({ width: 390, height: 844 });\n  await page.goto(\"/api/\");\n\n  await expect(page.getByRole(\"heading\", { name: \"form2js API Reference\" })).toBeVisible();\n  await expect(\n    page.getByLabel(\"API packages\").getByRole(\"link\", { name: \"@form2js/react\" })\n  ).toBeVisible();\n\n  await page.getByLabel(\"API packages\").getByRole(\"link\", { name: \"@form2js/react\" }).click();\n  await expect(page).toHaveURL(/\\/api\\/react\\/$/);\n  await expect(page.getByRole(\"heading\", { name: \"@form2js/react\" })).toBeVisible();\n  await expect(page.getByText(\"On this page\")).toBeVisible();\n\n  await page.getByLabel(\"On this page\").getByRole(\"link\", { name: \"Installation\" }).click();\n  await expect(page).toHaveURL(/#installation$/);\n});\n"
  },
  {
    "path": "apps/docs/test-e2e/homepage.spec.ts",
    "content": "import { expect, test } from \"@playwright/test\";\n\ntest(\"homepage switcher supports keyboard navigation and preserves parser output\", async ({ page }) => {\n  await page.goto(\"/\");\n\n  await expect(page.getByRole(\"link\", { name: \"API Docs\", exact: true })).toBeVisible();\n  await expect(page.getByRole(\"button\", { name: \"Core\" })).toBeVisible();\n\n  const coreButton = page.locator('button[data-variant-id=\"core\"]');\n  await coreButton.focus();\n  await coreButton.press(\"Space\");\n  await expect(coreButton).toHaveAttribute(\"aria-pressed\", \"true\");\n  await expect(page.locator(\"#install-cmd\")).toContainText(\"npm install @form2js/core\");\n  await expect(page.locator(\"#install-snippet\")).toContainText(\"entriesToObject\");\n  await page.getByRole(\"button\", { name: \"Run @form2js/core\" }).click();\n\n  const resultJson = page.locator(\".result-json\");\n  await expect(page.getByText(\"@form2js/core -> entriesToObject(entry objects)\")).toBeVisible();\n  await expect(resultJson).toContainText(\"von Lipwig\");\n\n  await page.locator('button[data-variant-id=\"form\"]').click();\n  await page.locator('button[data-variant-id=\"core\"]').click();\n\n  await expect(page.getByText(\"@form2js/core -> entriesToObject(entry objects)\")).toBeVisible();\n  await expect(resultJson).toContainText(\"von Lipwig\");\n});\n\ntest(\"fault-injected variant failure keeps the switcher usable\", async ({ page }) => {\n  await page.goto(\"/?variant=react&__fault=react:render\");\n\n  await expect(page.getByText(\"React failed to load.\")).toBeVisible();\n  await expect(page.getByText(\"Injected render fault for React\")).toBeVisible();\n\n  await page.locator('button[data-variant-id=\"form\"]').click();\n  await expect(page.getByRole(\"button\", { name: \"Run @form2js/dom\" })).toBeVisible();\n});\n"
  },
  {
    "path": "apps/docs/tsconfig.json",
    "content": "{\n  \"extends\": \"astro/tsconfigs/strict\",\n  \"include\": [\n    \".astro/types.d.ts\",\n    \"src/**/*\",\n    \"test/**/*\",\n    \"test-e2e/**/*\"\n  ]\n}\n"
  },
  {
    "path": "apps/docs/vitest.config.ts",
    "content": "import { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  test: {\n    include: [\"test/**/*.{test,spec}.{ts,tsx}\"],\n    exclude: [\"test-e2e/**\"]\n  }\n});\n"
  },
  {
    "path": "changeset/config.json",
    "content": "{\n  \"note\": \"Canonical changesets config lives at .changeset/config.json\"\n}\n"
  },
  {
    "path": "docs/api-core.md",
    "content": "# @form2js/core\n\n`@form2js/core` is the path parsing engine behind the rest of the package family. Use it when you already have key/value entries, need to turn them into nested objects, or want to flatten nested data back into entry form.\n\n## Installation\n\n```bash\nnpm install @form2js/core\n```\n\nStandalone/global build is not shipped for this package.\n\n## General Example\n\n```ts\nimport { entriesToObject, objectToEntries } from \"@form2js/core\";\n\nconst data = entriesToObject([\n  { key: \"person.name.first\", value: \"Esme\" },\n  { key: \"person.roles[]\", value: \"witch\" },\n]);\n\nconst flat = objectToEntries(data);\n```\n\n## Types and Properties\n\n### Exported Surface\n\n| Export | Kind | What it does |\n| --- | --- | --- |\n| `createMergeContext` | function | Creates merge state used while parsing indexed arrays. |\n| `setPathValue` | function | Applies one path/value into an object tree. |\n| `entriesToObject` | function | Main parser for iterable entries. |\n| `objectToEntries` | function | Flattens nested object/array data into `{ key, value }` entries. |\n| `processNameValues` | function | Compatibility helper for `{ name, value }` input. |\n| `Entry`, `EntryInput`, `EntryValue`, `NameValuePair`, `ObjectTree`, `ParseOptions`, `MergeContext`, `MergeOptions`, `SchemaValidator`, `ValidationOptions`, `InferSchemaOutput` | types | Public type surface for parser inputs, options, and results. |\n\n```ts\nexport function createMergeContext(): MergeContext;\n\nexport function setPathValue(\n  target: ObjectTree,\n  path: string,\n  value: EntryValue,\n  options?: MergeOptions\n): ObjectTree;\n\nexport function entriesToObject(entries: Iterable<EntryInput>, options?: ParseOptions): ObjectTree;\nexport function entriesToObject<TSchema extends SchemaValidator>(\n  entries: Iterable<EntryInput>,\n  options: ParseOptions & { schema: TSchema }\n): InferSchemaOutput<TSchema>;\n\nexport function objectToEntries(value: unknown): Entry[];\n\nexport function processNameValues(\n  nameValues: Iterable<NameValuePair>,\n  skipEmpty?: boolean,\n  delimiter?: string\n): ObjectTree;\n```\n\n### Options And Defaults\n\n| Option | Default | Where | Why this matters |\n| --- | --- | --- | --- |\n| `delimiter` | `\".\"` | `entriesToObject`, `setPathValue`, `processNameValues` | Controls how dot-like path chunks are split. |\n| `skipEmpty` | `true` | `entriesToObject`, `processNameValues` | Drops `\"\"` and `null` values unless you opt out. |\n| `allowUnsafePathSegments` | `false` | `entriesToObject`, `setPathValue` | Blocks prototype-pollution path segments unless you explicitly trust the source. |\n| `schema` | unset | `entriesToObject` | Runs `schema.parse(parsedObject)` and returns schema output type. |\n| `context` | fresh merge context | `setPathValue` | Keeps indexed array compaction stable across multiple writes. |\n\n### Schema validation\n\nUse `schema` when you want parsing and validation in the same step. The parser only requires a structural `{ parse(unknown) }` contract, so this works with Zod and similar validators.\n\n```ts\nimport { z } from \"zod\";\nimport { entriesToObject } from \"@form2js/core\";\n\nconst PersonSchema = z.object({\n  person: z.object({\n    age: z.coerce.number().int().min(0),\n    email: z.string().email()\n  })\n});\n\nconst rawEntries = [\n  { key: \"person.age\", value: \"17\" },\n  { key: \"person.email\", value: \"esk@example.com\" }\n];\n\nconst result = entriesToObject(rawEntries, { schema: PersonSchema });\n```\n\n### `skipEmpty: false`\n\nOpt out of the default empty-value filtering when blank strings are meaningful in your payload.\n\n```ts\nimport { entriesToObject } from \"@form2js/core\";\n\nconst result = entriesToObject(\n  [{ key: \"person.nickname\", value: \"\" }],\n  { skipEmpty: false }\n);\n```\n\n### Behavior Notes\n\n- Indexed array keys are compacted by encounter order, not preserved by numeric index.\n- `EntryInput` accepts `[key, value]`, `{ key, value }`, and `{ name, value }`.\n- If `schema` is provided, parser output is passed to `schema.parse()` and schema errors are rethrown.\n- `objectToEntries` emits bracket indexes for arrays such as `emails[0]` and only serializes own enumerable properties.\n"
  },
  {
    "path": "docs/api-dom.md",
    "content": "# @form2js/dom\n\n`@form2js/dom` solves the browser side of the problem: walk a form or DOM subtree, extract the submitted values, and return the parsed object. Use it when you want native form semantics without writing the extraction logic yourself.\n\n## Installation\n\n```bash\nnpm install @form2js/dom\n```\n\nStandalone via `unpkg`:\n\n```html\n<script src=\"https://unpkg.com/@form2js/dom/dist/standalone.global.js\"></script>\n<script>\n  const data = formToObject(formElement);\n  // or form2js(formElement)\n</script>\n```\n\n## General Example\n\n```ts\nimport { formToObject } from \"@form2js/dom\";\n\nconst result = formToObject(document.getElementById(\"profileForm\"), {\n  useIdIfEmptyName: true,\n  getDisabled: false,\n});\n```\n\n## Types and Properties\n\n### Exported Surface\n\n| Export | Kind | What it does |\n| --- | --- | --- |\n| `NodeCallbackResult` | interface | Custom extraction payload (`name` or `key` plus `value`). |\n| `FormToObjectNodeCallback` | type | Callback type used during node walk. |\n| `ExtractOptions` | interface | Options for pair extraction only. |\n| `FormToObjectOptions` | interface | Extraction options plus parser options. |\n| `RootNodeInput` | type | Supported root inputs such as `id`, `Node`, `NodeList`, arrays, and collections. |\n| `extractPairs` | function | Traverses DOM and returns path/value entries. |\n| `formToObject` | function | High-level parser from DOM to object tree. |\n| `form2js` | function | Compatibility wrapper around `formToObject`. |\n\n```ts\nexport interface NodeCallbackResult {\n  name?: string;\n  key?: string;\n  value: unknown;\n}\n\nexport const SKIP_NODE: unique symbol;\n\nexport type FormToObjectNodeCallback = (\n  node: Node\n) => NodeCallbackResult | typeof SKIP_NODE | false | null | undefined;\n\nexport interface ExtractOptions {\n  nodeCallback?: FormToObjectNodeCallback;\n  useIdIfEmptyName?: boolean;\n  getDisabled?: boolean;\n  document?: Document;\n}\n\nexport interface FormToObjectOptions extends ExtractOptions, ParseOptions {}\n\nexport function extractPairs(rootNode: RootNodeInput, options?: ExtractOptions): Entry[];\nexport function formToObject(rootNode: RootNodeInput, options?: FormToObjectOptions): ObjectTree;\n```\n\n### Options And Defaults\n\n| Option | Default | Where | Why this matters |\n| --- | --- | --- | --- |\n| `delimiter` | `\".\"` | `formToObject`, `form2js` | Matches parser path semantics. |\n| `skipEmpty` | `true` | `formToObject`, `form2js` | Skips `\"\"` and `null` values by default. |\n| `allowUnsafePathSegments` | `false` | `formToObject`, `form2js` | Rejects unsafe path segments before object merging. |\n| `useIdIfEmptyName` | `false` | extraction and wrappers | Lets `id` act as field key when `name` is empty. |\n| `getDisabled` | `false` | extraction and wrappers | Disabled controls, including disabled fieldset descendants, are ignored unless enabled explicitly. |\n| `nodeCallback` | unset | extraction and wrappers | Use it for custom field extraction from specific nodes. |\n| `document` | ambient/global document | extraction and wrappers | Required outside browser globals. |\n\n### `useIdIfEmptyName`\n\nEnable this when a form control is keyed by `id` rather than `name`, which is common in older markup or UI builders.\n\n```ts\nimport { formToObject } from \"@form2js/dom\";\n\nconst result = formToObject(document.getElementById(\"profileForm\"), {\n  useIdIfEmptyName: true\n});\n```\n\n### `nodeCallback`\n\nUse `nodeCallback` to rewrite or skip specific nodes before the default extraction logic runs.\n\n```ts\nimport { formToObject, SKIP_NODE } from \"@form2js/dom\";\n\nconst result = formToObject(document.getElementById(\"profileForm\"), {\n  nodeCallback(node) {\n    if (!(node instanceof HTMLInputElement)) {\n      return;\n    }\n\n    if (node.type === \"hidden\" && node.name === \"csrfToken\") {\n      return SKIP_NODE;\n    }\n\n    if (node.name === \"person.age\") {\n      return { key: node.name, value: Number(node.value) };\n    }\n  }\n});\n```\n\n### Behavior Notes\n\n- `select name=\"colors[]\"` is emitted as key `colors`; the trailing `[]` is removed for selects.\n- Checkbox and radio values follow native browser submission semantics:\n  - checked controls emit their string `value`\n  - unchecked controls are omitted\n  - omitted indexed controls do not reserve compacted array slots, so preserve row identity with another submitted field when it matters\n- Button-like inputs (`button`, `reset`, `submit`, `image`) are excluded from extraction.\n- You can merge multiple roots (`NodeList`, arrays, `HTMLCollection`) into one object.\n- If the callback returns `SKIP_NODE`, that node is excluded from extraction entirely.\n- If the callback returns `{ key | name, value }`, that value is used directly for that node.\n"
  },
  {
    "path": "docs/api-form-data.md",
    "content": "# @form2js/form-data\n\n`@form2js/form-data` is the server-friendly adapter for the same parsing rules used by the DOM package. Use it when your input is a `FormData` instance or a plain iterable of form-like key/value tuples.\n\n## Installation\n\n```bash\nnpm install @form2js/form-data\n```\n\nStandalone/global build is not shipped for this package.\n\n## General Example\n\n```ts\nimport { formDataToObject } from \"@form2js/form-data\";\n\nconst result = formDataToObject([\n  [\"person.name.first\", \"Sam\"],\n  [\"person.roles[]\", \"captain\"],\n]);\n```\n\n## Types and Properties\n\n### Exported Surface\n\n| Export | Kind | What it does |\n| --- | --- | --- |\n| `KeyValueEntryInput` | type alias | Alias of core `EntryInput`. |\n| `FormDataToObjectOptions` | interface | Parser options for form-data conversion. |\n| `entriesToObject` | function | Adapter to the core parser. |\n| `formDataToObject` | function | Parses `FormData` or iterable form-data entries. |\n| `EntryInput`, `ObjectTree`, `ParseOptions`, `SchemaValidator`, `ValidationOptions`, `InferSchemaOutput` | type re-export | Core types re-exported for convenience. |\n\n```ts\nexport type KeyValueEntryInput = EntryInput;\n\nexport interface FormDataToObjectOptions extends ParseOptions {}\n\nexport function entriesToObject(entries: Iterable<KeyValueEntryInput>, options?: ParseOptions): ObjectTree;\nexport function formDataToObject(\n  formData: FormData | Iterable<readonly [string, FormDataEntryValue]>,\n  options?: FormDataToObjectOptions\n): ObjectTree;\n```\n\n### Options And Defaults\n\n| Option | Default | Why this matters |\n| --- | --- | --- |\n| `delimiter` | `\".\"` | Keeps path splitting aligned with core and DOM behavior. |\n| `skipEmpty` | `true` | Drops empty string and `null` values unless disabled. |\n| `allowUnsafePathSegments` | `false` | Rejects unsafe path segments before object merging. |\n| `schema` | unset | Runs `schema.parse(parsedObject)` after parsing and returns schema output type. |\n\n### Schema validation\n\nUse the same schema pattern on `FormData` input when you want validated server-side parsing without importing `@form2js/core` separately.\n\n```ts\nimport { z } from \"zod\";\nimport { formDataToObject } from \"@form2js/form-data\";\n\nconst PersonSchema = z.object({\n  person: z.object({\n    age: z.coerce.number().int().min(0)\n  })\n});\n\nconst formData = new FormData();\nformData.set(\"person.age\", \"12\");\n\nconst result = formDataToObject(formData, { schema: PersonSchema });\n```\n\n### Behavior Notes\n\n- Parsing rules are the same as `@form2js/core`.\n- Accepts either a real `FormData` object or any iterable of readonly key/value tuples.\n- Schema validation is optional and uses only a structural `{ parse(unknown) }` contract.\n"
  },
  {
    "path": "docs/api-index.md",
    "content": "# form2js API Reference\n\nThis section is for developers who want the exact API surface of the current `@form2js/*` packages, plus the defaults and edge cases that usually matter when you wire forms into real applications.\n\nIf you want the broader project overview first, start with [README.md](README.md).\n\n## Who this is for\n\n- Developers choosing between the `@form2js/*` packages\n- Teams migrating from the legacy `form2js` flow to package-specific APIs\n- Anyone who needs exact options, exported types, and behavior notes\n\n## Package Guide\n\n- [`@form2js/core`](api-core.md): parse path-like entries into nested objects and flatten them back out\n- [`@form2js/dom`](api-dom.md): turn browser form controls into an object\n- [`@form2js/form-data`](api-form-data.md): parse `FormData` or tuple entries with the same path rules\n- [`@form2js/react`](api-react.md): handle React form submission with parsing, validation, and submit state\n- [`@form2js/js2form`](api-js2form.md): push nested object data back into form controls\n- [`@form2js/jquery`](api-jquery.md): install a jQuery plugin on top of the DOM parser\n\n## Shared Naming Rules\n\nThese rules apply across parser-based packages such as `core`, `dom`, and `form-data`.\n\n- Dot paths build nested objects: `person.name.first` becomes `{ person: { name: { first: ... } } }`\n- Repeated `[]` pushes into arrays in encounter order: `roles[]`\n- Indexed arrays are compacted in first-seen order: `items[8]`, `items[5]` becomes indexes `0`, `1`\n- Rails-style brackets are supported: `rails[field][value]`\n- By default, empty string and `null` are skipped (`skipEmpty: true`)\n- Unsafe key path segments (`__proto__`, `prototype`, `constructor`) are rejected by default\n"
  },
  {
    "path": "docs/api-jquery.md",
    "content": "# @form2js/jquery\n\n`@form2js/jquery` is the legacy-friendly adapter for projects that still rely on jQuery forms. Use it when you want `$.fn.toObject()` on top of the DOM parser without rewriting the rest of the form handling code.\n\n## Installation\n\n```bash\nnpm install @form2js/jquery jquery\n```\n\nStandalone via `unpkg`:\n\n```html\n<script src=\"https://code.jquery.com/jquery-3.7.1.min.js\"></script>\n<script src=\"https://unpkg.com/@form2js/jquery/dist/standalone.global.js\"></script>\n<script>\n  const data = $(\"#profileForm\").toObject({ mode: \"first\" });\n</script>\n```\n\n## General Example\n\n```ts\nimport $ from \"jquery\";\nimport { installToObjectPlugin } from \"@form2js/jquery\";\n\ninstallToObjectPlugin($);\n\nconst data = $(\"#profileForm\").toObject({ mode: \"first\" });\n```\n\n## Types and Properties\n\n### Exported Surface\n\n| Export | Kind | What it does |\n| --- | --- | --- |\n| `ToObjectMode` | type | `\"first\" | \"all\" | \"combine\"` |\n| `ToObjectOptions` | interface | Plugin options mapped to `@form2js/dom` behavior. |\n| `installToObjectPlugin` | function | Adds `toObject()` to `$.fn` if missing. |\n| `maybeAutoInstallPlugin` | function | Installs the plugin only when a jQuery-like scope is detected. |\n\n```ts\nexport type ToObjectMode = \"first\" | \"all\" | \"combine\";\n\nexport interface ToObjectOptions {\n  mode?: ToObjectMode;\n  delimiter?: string;\n  skipEmpty?: boolean;\n  allowUnsafePathSegments?: boolean;\n  nodeCallback?: FormToObjectNodeCallback;\n  useIdIfEmptyName?: boolean;\n  getDisabled?: boolean;\n}\n\nexport function installToObjectPlugin($: JQueryLike): void;\nexport function maybeAutoInstallPlugin(scope?: unknown): void;\n```\n\n### Options And Defaults\n\n| Option | Default | Why this matters |\n| --- | --- | --- |\n| `mode` | `\"first\"` | Controls whether you parse one match, all matches, or merge all matches. |\n| `delimiter` | `\".\"` | Same path splitting behavior as the other packages. |\n| `skipEmpty` | `true` | Keeps default parser behavior for empty values. |\n| `allowUnsafePathSegments` | `false` | Rejects unsafe path segments before object merging. |\n| `useIdIfEmptyName` | `false` | Lets the plugin fall back to `id` where needed. |\n| `getDisabled` | `false` | Disabled controls are skipped unless enabled. |\n| `nodeCallback` | unset | Hook for custom extraction through the DOM package semantics. |\n\n### `mode: \"all\"`\n\nUse `all` when the selector can match multiple forms or repeated field groups and you want one parsed object per match.\n\n```ts\nconst result = $(\".profile-form\").toObject({ mode: \"all\" });\n```\n\n### Behavior Notes\n\n- `installToObjectPlugin` is idempotent; it does not overwrite an existing `$.fn.toObject`.\n- `mode: \"all\"` returns an array of objects, one per matched element.\n- `mode: \"combine\"` passes all matched root nodes together into the DOM parser.\n"
  },
  {
    "path": "docs/api-js2form.md",
    "content": "# @form2js/js2form\n\n`@form2js/js2form` moves data in the opposite direction: take a nested object and write it into matching form controls. Use it when you need to prefill a form, restore saved draft state, or sync object data back into existing DOM controls.\n\n## Installation\n\n```bash\nnpm install @form2js/js2form\n```\n\nStandalone/global build is not shipped for this package.\n\n## General Example\n\n```ts\nimport { objectToForm } from \"@form2js/js2form\";\n\nobjectToForm(\"profileForm\", {\n  person: {\n    name: { first: \"Tiffany\", last: \"Aching\" },\n    roles: [\"witch\"],\n  },\n});\n```\n\n## Types and Properties\n\n### Exported Surface\n\n| Export | Kind | What it does |\n| --- | --- | --- |\n| `RootNodeInput` | type | Root as element id, node, `null`, or `undefined`. |\n| `ObjectToFormNodeCallback` | type | Write-time callback for per-node assignment control. |\n| `ObjectToFormOptions` | interface | Options for name normalization, cleaning, and document resolution. |\n| `SupportedField`, `SupportedFieldCollection`, `FieldMap` | types | Field typing used by mapping and assignment helpers. |\n| `flattenDataForForm` | function | Flattens object data to an entry list. |\n| `mapFieldsByName` | function | Builds a normalized name-to-field mapping. |\n| `objectToForm` | function | Populates matching fields from object data. |\n| `js2form` | function | Compatibility wrapper around `objectToForm`. |\n| `normalizeName` | function | Normalizes field names and compacts indexed arrays. |\n\n```ts\nexport interface ObjectToFormOptions {\n  delimiter?: string;\n  nodeCallback?: ObjectToFormNodeCallback;\n  useIdIfEmptyName?: boolean;\n  shouldClean?: boolean;\n  document?: Document;\n}\n\nexport function flattenDataForForm(data: unknown): Entry[];\nexport function mapFieldsByName(\n  rootNode: RootNodeInput,\n  options?: Pick<ObjectToFormOptions, \"delimiter\" | \"useIdIfEmptyName\" | \"shouldClean\" | \"document\">\n): FieldMap;\nexport function objectToForm(rootNode: RootNodeInput, data: unknown, options?: ObjectToFormOptions): void;\n```\n\n### Options And Defaults\n\n| Option | Default | Where | Why this matters |\n| --- | --- | --- | --- |\n| `delimiter` | `\".\"` | `objectToForm`, `mapFieldsByName`, `js2form` | Must match how your input keys are structured. |\n| `useIdIfEmptyName` | `false` | `objectToForm`, `mapFieldsByName`, `js2form` | Useful when form controls are keyed by `id` instead of `name`. |\n| `shouldClean` | `true` | `objectToForm`, `mapFieldsByName` | Clears form state before applying incoming values. |\n| `document` | ambient/global document | all root-resolving APIs | Needed when running with a DOM shim. |\n| `nodeCallback` | unset | `objectToForm`, `js2form` options | Called before default assignment; return `false` to skip default assignment for that node. |\n\n### `shouldClean: false`\n\nDisable cleaning when you want to layer partial data onto an existing form without clearing unrelated controls first.\n\n```ts\nimport { objectToForm } from \"@form2js/js2form\";\n\nobjectToForm(\n  \"profileForm\",\n  {\n    person: {\n      name: { first: \"Tiffany\" }\n    }\n  },\n  { shouldClean: false }\n);\n```\n\n### `useIdIfEmptyName`\n\nMatch fields by `id` when the markup does not provide stable `name` attributes.\n\n```ts\nimport { objectToForm } from \"@form2js/js2form\";\n\nobjectToForm(\n  document.getElementById(\"profileForm\"),\n  {\n    firstName: \"Agnes\"\n  },\n  { useIdIfEmptyName: true }\n);\n```\n\n### Behavior Notes\n\n- `objectToForm` is a no-op when the root cannot be resolved.\n- Checkbox and radio groups are matched with `[]` and non-`[]` name fallbacks.\n- Name normalization compacts sparse indexes to sequential indexes during matching.\n- For multi-select names like `colors[]`, matching includes `[]` and bare-name fallbacks without creating one map key per option.\n- Form updates set values, checked state, and selected state, but do not dispatch synthetic events.\n"
  },
  {
    "path": "docs/api-react.md",
    "content": "# @form2js/react\n\n`@form2js/react` wraps the form-data parser in a React submit hook. Use it when you want a single `onSubmit` handler that parses form input, optionally validates it with a schema, and tracks submit state without wiring your own state machine.\n\n## Installation\n\n```bash\nnpm install @form2js/react react\n```\n\nStandalone/global build is not shipped for this package.\n\n## General Example\n\n```tsx\nimport { z } from \"zod\";\nimport { useForm2js } from \"@form2js/react\";\n\nconst schema = z.object({\n  person: z.object({\n    email: z.string().email()\n  })\n});\n\nexport function SignupForm(): React.JSX.Element {\n  const { onSubmit, isSubmitting, isError, error, isSuccess, reset } = useForm2js(\n    async (data) => {\n      await sendFormData(data);\n    },\n    { schema }\n  );\n\n  return (\n    <form\n      onSubmit={(event) => {\n        void onSubmit(event);\n      }}\n    >\n      <input name=\"person.email\" type=\"email\" defaultValue=\"sam.vimes@ankh.city\" />\n      <button type=\"submit\" disabled={isSubmitting}>\n        {isSubmitting ? \"Saving...\" : \"Save\"}\n      </button>\n      {isError ? <p>{String(error)}</p> : null}\n      {isSuccess ? <p>Saved</p> : null}\n      <button type=\"button\" onClick={reset}>\n        Reset state\n      </button>\n    </form>\n  );\n}\n```\n\n## Types and Properties\n\n### Exported Surface\n\n| Export | Kind | What it does |\n| --- | --- | --- |\n| `UseForm2jsData` | type | Infers submit payload from the optional schema. |\n| `UseForm2jsSubmit` | type | Submit callback signature. |\n| `UseForm2jsOptions` | interface | Parser options plus optional schema. |\n| `UseForm2jsResult` | interface | Hook return state and handlers. |\n| `useForm2js` | function | Creates submit handler and submit state machine for forms. |\n\n```ts\nexport type UseForm2jsSubmit<TSchema extends SchemaValidator | undefined = undefined> = (\n  data: UseForm2jsData<TSchema>\n) => Promise<void> | void;\n\nexport interface UseForm2jsOptions<TSchema extends SchemaValidator | undefined = undefined>\n  extends ParseOptions {\n  schema?: TSchema;\n}\n\nexport interface UseForm2jsResult {\n  onSubmit: (event: SyntheticEvent<HTMLFormElement, SubmitEvent>) => Promise<void>;\n  isSubmitting: boolean;\n  isError: boolean;\n  error: unknown;\n  isSuccess: boolean;\n  reset: () => void;\n}\n```\n\n### Options And Defaults\n\n| Option | Default | Why this matters |\n| --- | --- | --- |\n| `delimiter` | `\".\"` | Keeps parser path splitting aligned with the other packages. |\n| `skipEmpty` | `true` | Drops empty string and `null` values unless disabled. |\n| `allowUnsafePathSegments` | `false` | Keeps parser hardened by default. |\n| `schema` | unset | If set, the parsed payload is run through `schema.parse(...)` before the submit callback. |\n\n### Behavior Notes\n\n- `onSubmit` always calls `event.preventDefault()`.\n- Re-submit attempts are ignored while a submit promise is still pending.\n- Validation and submit errors are both surfaced through `error` and `isError`.\n- `reset()` clears `isError`, `error`, and `isSuccess`.\n"
  },
  {
    "path": "docs/migrate.md",
    "content": "# Migrate from Legacy form2js\n\nIf you built around the old single `form2js` script or the archived jQuery plugin flow, the main change is that modern form2js is now a small package family. You install only the part you need instead of pulling one browser-era bundle into every environment.\n\nThe legacy code and historical examples still live in the [legacy branch](https://github.com/maxatwork/form2js/tree/legacy), but new work should move to the current packages and docs.\n\n## Quick Chooser\n\n| If your legacy code does this | Use now | Notes |\n| --- | --- | --- |\n| `form2js(form)` in the browser | [`@form2js/dom`](api-dom.md) | Closest direct replacement. Exports both `formToObject()` and a compatibility `form2js()` wrapper. |\n| `$(\"#form\").toObject()` in jQuery | [`@form2js/jquery`](api-jquery.md) | Keeps the plugin shape while using the modern DOM parser underneath. |\n| Parse `FormData` on the server or in browser pipelines | [`@form2js/form-data`](api-form-data.md) | Best fit for fetch actions, loaders, workers, and Node. |\n| Handle submit state in React | [`@form2js/react`](api-react.md) | Wraps form parsing in a hook with async submit state and optional schema validation. |\n| Push object data back into a form | [`@form2js/js2form`](api-js2form.md) | Modern replacement for the old \"object back into fields\" helpers around the ecosystem. |\n| Work directly with path/value entries | [`@form2js/core`](api-core.md) | Lowest-level parser and formatter. |\n\n## What Changed\n\n- The archived project exposed one browser-oriented `form2js(rootNode, delimiter, skipEmpty, nodeCallback, useIdIfEmptyName)` entry point.\n- The current project splits that behavior by environment and responsibility.\n- Browser DOM extraction lives in `@form2js/dom`.\n- jQuery compatibility lives in `@form2js/jquery`.\n- `FormData`, React, object-to-form, and low-level entry parsing each have their own package.\n\nThat split is the point of the rewrite: smaller installs, clearer environment boundaries, and first-class TypeScript/ESM support without making every user drag along legacy browser assumptions.\n\n## Legacy API Mapping\n\nLegacy browser code usually looked like this:\n\n```js\nvar data = form2js(rootNode, \".\", true, nodeCallback, false);\n```\n\nModern browser code should usually look like this:\n\n```ts\nimport { formToObject } from \"@form2js/dom\";\n\nconst data = formToObject(rootNode, {\n  delimiter: \".\",\n  skipEmpty: true,\n  nodeCallback,\n  useIdIfEmptyName: false\n});\n```\n\nIf you want the smallest possible migration diff, `@form2js/dom` also exports a compatibility wrapper:\n\n```ts\nimport { form2js } from \"@form2js/dom\";\n\nconst data = form2js(rootNode, \".\", true, nodeCallback, false);\n```\n\nParameter mapping:\n\n| Legacy parameter | Modern equivalent |\n| --- | --- |\n| `rootNode` | `rootNode` |\n| `delimiter` | `options.delimiter` |\n| `skipEmpty` | `options.skipEmpty` |\n| `nodeCallback` | `options.nodeCallback` |\n| `useIdIfEmptyName` | `options.useIdIfEmptyName` |\n\nThe main migration decision is not the parameter mapping. It is choosing the right package for the environment where parsing now happens.\n\n## Browser Migration\n\nFor plain browser forms, install `@form2js/dom`:\n\n```bash\nnpm install @form2js/dom\n```\n\nModule usage:\n\n```ts\nimport { formToObject } from \"@form2js/dom\";\n\nconst data = formToObject(document.getElementById(\"profileForm\"));\n```\n\nStandalone usage is still available for the DOM package:\n\n```html\n<script src=\"https://unpkg.com/@form2js/dom/dist/standalone.global.js\"></script>\n<script>\n  const data = formToObject(document.getElementById(\"profileForm\"));\n  // or form2js(document.getElementById(\"profileForm\"))\n</script>\n```\n\n## jQuery Migration\n\nIf your codebase still expects `$.fn.toObject()`, move to `@form2js/jquery` instead of rebuilding that glue yourself.\n\n```bash\nnpm install @form2js/jquery jquery\n```\n\n```ts\nimport $ from \"jquery\";\nimport { installToObjectPlugin } from \"@form2js/jquery\";\n\ninstallToObjectPlugin($);\n\nconst data = $(\"#profileForm\").toObject({ mode: \"first\" });\n```\n\nStandalone usage is also available:\n\n```html\n<script src=\"https://code.jquery.com/jquery-3.7.1.min.js\"></script>\n<script src=\"https://unpkg.com/@form2js/jquery/dist/standalone.global.js\"></script>\n<script>\n  const data = $(\"#profileForm\").toObject({ mode: \"combine\" });\n</script>\n```\n\n## Behavior Differences To Check\n\n- `skipEmpty` still defaults to `true`, so empty strings and `null` values are skipped unless you opt out.\n- Disabled controls are ignored by default. Set `getDisabled: true` only if you really want them parsed.\n- Unsafe path segments such as `__proto__`, `prototype`, and `constructor` are rejected by default in the modern parser.\n- Only `@form2js/dom` and `@form2js/jquery` ship standalone browser globals. The other packages are module-only.\n- React and `FormData` use cases now have dedicated packages instead of being squeezed through the DOM entry point.\n\n## Where To Go Now\n\nIf the legacy code used browser DOM access only because that was the only option at the time, this is the modern package map:\n\n- Use [`@form2js/form-data`](api-form-data.md) when your app already has `FormData`, request entries, or server-side action handlers.\n- Use [`@form2js/react`](api-react.md) when you want submit-state handling around parsing in React.\n- Use [`@form2js/js2form`](api-js2form.md) when you need to populate forms from nested objects.\n- Use [`@form2js/core`](api-core.md) when you already have raw key/value pairs and just need the parser rules.\n\n## Migration Checklist\n\n1. Identify whether the old code is DOM-based, jQuery-based, React-based, or really just `FormData` processing.\n2. Swap the legacy package or script include for the specific current package.\n3. Move old positional arguments to an options object where appropriate.\n4. Re-test any custom `nodeCallback` logic and any flows that depend on disabled or empty fields.\n5. Replace browser-only parsing with `@form2js/form-data` or `@form2js/react` when the parsing no longer needs direct DOM traversal.\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport js from \"@eslint/js\";\nimport tseslint from \"typescript-eslint\";\n\nconst tsconfigRootDir = path.dirname(fileURLToPath(import.meta.url));\n\nexport default tseslint.config(\n  {\n    ignores: [\n      \"**/dist/**\",\n      \"**/coverage/**\",\n      \"**/node_modules/**\",\n      \".turbo/**\",\n      \"package-lock.json\"\n    ]\n  },\n  js.configs.recommended,\n  ...tseslint.configs.strictTypeChecked,\n  ...tseslint.configs.stylisticTypeChecked,\n  {\n    files: [\"**/*.ts\", \"**/*.tsx\", \"**/*.cts\", \"**/*.mts\"],\n    languageOptions: {\n      parserOptions: {\n        projectService: true,\n        tsconfigRootDir\n      }\n    },\n    rules: {\n      \"@typescript-eslint/consistent-type-imports\": [\n        \"error\",\n        {\n          \"prefer\": \"type-imports\"\n        }\n      ],\n      \"@typescript-eslint/no-explicit-any\": \"error\",\n      \"@typescript-eslint/no-empty-object-type\": \"off\",\n      \"@typescript-eslint/no-unsafe-assignment\": \"error\",\n      \"@typescript-eslint/no-unsafe-call\": \"error\",\n      \"@typescript-eslint/no-unsafe-member-access\": \"error\",\n      \"@typescript-eslint/no-unsafe-argument\": \"error\",\n      \"@typescript-eslint/no-unsafe-return\": \"error\",\n      \"@typescript-eslint/prefer-regexp-exec\": \"off\",\n      \"@typescript-eslint/prefer-nullish-coalescing\": \"off\",\n      \"@typescript-eslint/restrict-template-expressions\": \"off\",\n      \"@typescript-eslint/consistent-type-definitions\": \"off\",\n      \"@typescript-eslint/no-unnecessary-condition\": \"off\",\n      \"@typescript-eslint/no-unnecessary-type-assertion\": \"off\",\n      \"@typescript-eslint/prefer-for-of\": \"off\",\n      \"@typescript-eslint/consistent-indexed-object-style\": \"off\",\n      \"@typescript-eslint/no-base-to-string\": \"off\",\n      \"@typescript-eslint/non-nullable-type-assertion-style\": \"off\"\n    }\n  }\n);\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"form2js-monorepo\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"packageManager\": \"npm@11.6.2\",\n  \"workspaces\": [\n    \"packages/*\",\n    \"apps/*\"\n  ],\n  \"engines\": {\n    \"node\": \">=18\"\n  },\n  \"scripts\": {\n    \"build\": \"turbo run build\",\n    \"build:packages\": \"turbo run build --filter=@form2js/*\",\n    \"docs\": \"npm run build:packages && npm -w @form2js/docs run dev\",\n    \"docs:build\": \"npm run build:packages && npm -w @form2js/docs run build\",\n    \"test\": \"npm run test:packages && npm run test:integration && npm run test:docs\",\n    \"test:docs\": \"npm run build:packages && DOCS_E2E_PORT=4329 npm -w @form2js/docs run test:e2e\",\n    \"test:packages\": \"turbo run test\",\n    \"test:integration\": \"vitest run test/integration\",\n    \"lint\": \"turbo run lint\",\n    \"typecheck\": \"turbo run typecheck\",\n    \"clean\": \"turbo run clean && rimraf node_modules .turbo\",\n    \"pack:dry-run\": \"npm_config_cache=/tmp/form2js-npm-cache npm pack --dry-run --workspace @form2js/core && npm_config_cache=/tmp/form2js-npm-cache npm pack --dry-run --workspace @form2js/dom && npm_config_cache=/tmp/form2js-npm-cache npm pack --dry-run --workspace @form2js/form-data && npm_config_cache=/tmp/form2js-npm-cache npm pack --dry-run --workspace @form2js/js2form && npm_config_cache=/tmp/form2js-npm-cache npm pack --dry-run --workspace @form2js/jquery && npm_config_cache=/tmp/form2js-npm-cache npm pack --dry-run --workspace @form2js/react\",\n    \"changeset\": \"changeset\",\n    \"release\": \"changeset publish\",\n    \"scope:rewrite\": \"node scripts/rewrite-scope.mjs\",\n    \"bump-version\": \"node scripts/bump-version.mjs\"\n  },\n  \"overrides\": {\n    \"minimatch\": {\n      \"brace-expansion\": \"1.1.13\"\n    },\n    \"@typescript-eslint/typescript-estree\": {\n      \"minimatch\": {\n        \"brace-expansion\": \"2.0.3\"\n      }\n    },\n    \"micromatch\": {\n      \"picomatch\": \"2.3.2\"\n    },\n    \"astro\": {\n      \"@rollup/pluginutils\": {\n        \"picomatch\": \"4.0.4\"\n      }\n    },\n    \"vite\": {\n      \"picomatch\": \"4.0.4\"\n    },\n    \"@rollup/pluginutils\": {\n      \"picomatch\": \"4.0.4\"\n    },\n    \"tinyglobby\": {\n      \"picomatch\": \"4.0.4\"\n    },\n    \"vitest\": {\n      \"picomatch\": \"4.0.4\"\n    },\n    \"smol-toml\": \"1.6.1\",\n    \"yaml\": \"2.8.3\"\n  },\n  \"devDependencies\": {\n    \"@changesets/cli\": \"^2.29.7\",\n    \"@eslint/js\": \"^9.22.0\",\n    \"@types/jsdom\": \"^21.1.7\",\n    \"eslint\": \"^9.22.0\",\n    \"jsdom\": \"^26.0.0\",\n    \"picomatch\": \"^4.0.4\",\n    \"rimraf\": \"^6.0.1\",\n    \"tsup\": \"^8.5.0\",\n    \"tsx\": \"^4.20.5\",\n    \"turbo\": \"^2.5.6\",\n    \"typescript\": \"^5.9.2\",\n    \"typescript-eslint\": \"^8.35.1\",\n    \"vitest\": \"^4.1.2\"\n  }\n}\n"
  },
  {
    "path": "packages/core/CHANGELOG.md",
    "content": "# @form2js/core\n\n## 3.3.0\n\n### Minor Changes\n\n- Restore Rails-style bracket path compatibility across parsing and form population, align DOM checkbox and radio handling with native browser submission behavior, and add explicit `SKIP_NODE` callback support for DOM extraction.\n\n## 3.2.2\n\n### Patch Changes\n\n- Republish the package metadata and package-level README improvements across all public packages after fixing publish configuration.\n\n## 3.2.1\n\n### Patch Changes\n\n- Add npm package metadata (`license`, `bugs`, `keywords`) and publish package-level README files for all public packages.\n\n## 3.2.0\n\n### Minor Changes\n\n- 4d2f923: Added optional schema validation support (structural `parse`) to parsing APIs in core and form-data, plus a new `@form2js/react` package with `useForm2js` for async submit state management in React forms.\n\n## 3.1.1\n\n### Patch Changes\n\n- 77b8543: Hardened parser security by rejecting unsafe key path segments by default and aligned DOM extraction with HTML semantics (disabled fieldsets, no button-like inputs). Added a writer hook, improved multi-select mapping, tightened ESLint, added a version bump script, and aligned packages to 3.1.0 with updated docs, tests, and repo/website links.\n\n      New Features\n          Added allowUnsafePathSegments option across core, dom, form-data, and jQuery plugin to explicitly allow trusted unsafe segments.\n          Added nodeCallback to objectToForm/js2form; returning false skips default assignment for that node.\n          Improved multi-select handling: matches names with [] and bare-name fallbacks without per-option keys.\n          Added bump-version script to sync workspace versions and ranges; bumped packages to 3.1.0.\n          Enabled vitest for examples and added smoke tests; updated tsconfig and lint script.\n\n      Bug Fixes\n          Prevented prototype pollution by blocking \"proto\", \"prototype\", and \"constructor\" path tokens by default.\n          objectToEntries now serializes only own enumerable properties.\n          DOM extraction respects disabled fieldset rules (legend exception) and excludes button-like inputs even when skipEmpty is false.\n          ESLint config now errors on unsafe TypeScript operations (no-unsafe-* rules).\n"
  },
  {
    "path": "packages/core/README.md",
    "content": "# @form2js/core\n\nCore path parsing and object transformation logic for form-shaped data.\n\n## Install\n\n```bash\nnpm install @form2js/core\n```\n\n## Minimal usage\n\n```ts\nimport { entriesToObject } from \"@form2js/core\";\n\nconst data = entriesToObject([\n  { key: \"person.name.first\", value: \"Vimes\" },\n  { key: \"person.tags[]\", value: \"watch\" },\n]);\n```\n\nFor guides, playground examples, and API details, see the docs site:\nhttps://maxatwork.github.io/form2js/?variant=core\n\nLicense: MIT\n"
  },
  {
    "path": "packages/core/package.json",
    "content": "{\n  \"name\": \"@form2js/core\",\n  \"version\": \"3.4.0\",\n  \"description\": \"Core path parsing and object transformation logic for form2js.\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/maxatwork/form2js\",\n  \"homepage\": \"https://maxatwork.github.io/form2js/\",\n  \"bugs\": {\n    \"url\": \"https://github.com/maxatwork/form2js/issues\"\n  },\n  \"keywords\": [\n    \"form\",\n    \"forms\",\n    \"serialization\",\n    \"deserialization\",\n    \"form-data\",\n    \"json\",\n    \"parser\",\n    \"object-transform\"\n  ],\n  \"type\": \"module\",\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\",\n      \"require\": \"./dist/index.cjs\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"peerDependencies\": {\n    \"zod\": \"^3.22.0 || ^4.0.0\"\n  },\n  \"peerDependenciesMeta\": {\n    \"zod\": {\n      \"optional\": true\n    }\n  },\n  \"engines\": {\n    \"node\": \">=18\"\n  },\n  \"scripts\": {\n    \"build\": \"tsup --config tsup.config.ts\",\n    \"test\": \"vitest run\",\n    \"lint\": \"eslint \\\"src/**/*.ts\\\" \\\"test/**/*.ts\\\"\",\n    \"typecheck\": \"tsc -p tsconfig.json --noEmit\",\n    \"clean\": \"rimraf dist\"\n  }\n}\n"
  },
  {
    "path": "packages/core/src/index.ts",
    "content": "import type {\n  Entry,\n  EntryInput,\n  EntryValue,\n  InferSchemaOutput,\n  MergeContext,\n  MergeOptions,\n  NameValuePair,\n  ObjectTree,\n  ParseOptions,\n  SchemaValidator,\n  ValidationOptions\n} from \"./types\";\n\nconst SUB_ARRAY_REGEXP = /^\\[\\d+?\\]/;\nconst SUB_OBJECT_REGEXP = /^[a-zA-Z_][a-zA-Z_0-9]*/;\nconst PATH_TOKEN_REGEXP = /[a-zA-Z_][a-zA-Z0-9_]*/g;\nconst UNSAFE_PATH_SEGMENTS = new Set([\"__proto__\", \"prototype\", \"constructor\"]);\n\ninterface BracketMatch {\n  content: string;\n  index: number;\n  text: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return typeof value === \"object\" && value !== null;\n}\n\nfunction createIndexRecord(): Record<string, unknown> {\n  return Object.create(null) as Record<string, unknown>;\n}\n\nfunction hasOwnRecordValue(record: Record<string, unknown>, key: string): boolean {\n  return Object.prototype.hasOwnProperty.call(record, key);\n}\n\nfunction getOwnRecordValue(record: Record<string, unknown>, key: string): unknown {\n  return hasOwnRecordValue(record, key) ? record[key] : undefined;\n}\n\nfunction setOwnRecordValue(record: Record<string, unknown>, key: string, value: unknown): void {\n  Object.defineProperty(record, key, {\n    configurable: true,\n    enumerable: true,\n    value,\n    writable: true\n  });\n}\n\nfunction findBracketMatches(input: string): BracketMatch[] {\n  const matches: BracketMatch[] = [];\n  let cursor = 0;\n\n  while (cursor < input.length) {\n    const startIndex = input.indexOf(\"[\", cursor);\n    if (startIndex === -1) {\n      break;\n    }\n\n    const endIndex = input.indexOf(\"]\", startIndex + 1);\n    if (endIndex === -1) {\n      break;\n    }\n\n    matches.push({\n      content: input.slice(startIndex + 1, endIndex),\n      index: startIndex,\n      text: input.slice(startIndex, endIndex + 1)\n    });\n    cursor = endIndex + 1;\n  }\n\n  return matches;\n}\n\nfunction normalizeEntry(entry: EntryInput): Entry {\n  if (Array.isArray(entry) && typeof entry[0] === \"string\") {\n    const tupleEntry = entry as readonly [string, EntryValue];\n    return { key: tupleEntry[0], value: tupleEntry[1] };\n  }\n\n  if (\"key\" in entry && typeof entry.key === \"string\") {\n    return { key: entry.key, value: entry.value };\n  }\n\n  if (\"name\" in entry && typeof entry.name === \"string\") {\n    return { key: entry.name, value: entry.value };\n  }\n\n  throw new TypeError(\"Invalid entry. Expected [key, value], { key, value }, or { name, value }.\");\n}\n\nfunction shouldSkipValue(value: EntryValue, skipEmpty: boolean): boolean {\n  return skipEmpty && (value === \"\" || value === null);\n}\n\nfunction findUnsafePathToken(part: string): string | null {\n  const tokens = part.match(PATH_TOKEN_REGEXP);\n  if (!tokens) {\n    return null;\n  }\n\n  for (const token of tokens) {\n    if (UNSAFE_PATH_SEGMENTS.has(token)) {\n      return token;\n    }\n  }\n\n  return null;\n}\n\nfunction assertPathIsSafe(nameParts: string[], allowUnsafePathSegments: boolean): void {\n  if (allowUnsafePathSegments) {\n    return;\n  }\n\n  for (const namePart of nameParts) {\n    const unsafeToken = findUnsafePathToken(namePart);\n    if (unsafeToken) {\n      throw new TypeError(\n        `Unsafe path segment \"${unsafeToken}\" is not allowed. ` +\n          \"Pass allowUnsafePathSegments: true only for trusted input.\"\n      );\n    }\n  }\n}\n\nfunction splitNameIntoParts(name: string, delimiter: string): string[] {\n  const rawParts = name.split(delimiter);\n  const nameParts: string[] = [];\n\n  for (const rawPart of rawParts) {\n    const bracketMatches = findBracketMatches(rawPart);\n    if (bracketMatches.length === 0) {\n      nameParts.push(rawPart);\n      continue;\n    }\n\n    let currentPart = \"\";\n    let cursor = 0;\n\n    for (const match of bracketMatches) {\n      const literalText = rawPart.slice(cursor, match.index ?? cursor);\n      if (literalText !== \"\") {\n        currentPart += literalText;\n      }\n\n      const bracketContent = match.content;\n      const isArraySegment = bracketContent === \"\" || /^\\d+$/.test(bracketContent);\n\n      if (isArraySegment) {\n        if (currentPart !== \"\" && currentPart.endsWith(\"]\")) {\n          nameParts.push(currentPart);\n          currentPart = \"\";\n        }\n\n        currentPart = `${currentPart}[${bracketContent}]`;\n      } else {\n        if (currentPart !== \"\") {\n          nameParts.push(currentPart);\n        }\n\n        currentPart = bracketContent;\n      }\n\n      cursor = match.index + match.text.length;\n    }\n\n    const trailingText = rawPart.slice(cursor);\n    if (trailingText !== \"\") {\n      currentPart += trailingText;\n    }\n\n    if (currentPart !== \"\") {\n      nameParts.push(currentPart);\n    }\n  }\n\n  return nameParts;\n}\n\nfunction ensureNamedArray(container: unknown, arrayName: string): unknown[] {\n  if (arrayName === \"\" && Array.isArray(container)) {\n    return container;\n  }\n\n  if (!isRecord(container)) {\n    throw new TypeError(\"Expected object-like container when creating array path\");\n  }\n\n  const existingValue = getOwnRecordValue(container, arrayName);\n  if (Array.isArray(existingValue)) {\n    return existingValue;\n  }\n\n  const newArray: unknown[] = [];\n  setOwnRecordValue(container, arrayName, newArray);\n  return newArray;\n}\n\nfunction pushToNamedArray(container: unknown, arrayName: string, value: unknown): unknown {\n  const targetArray = ensureNamedArray(container, arrayName);\n  targetArray.push(value);\n  return targetArray[targetArray.length - 1];\n}\n\nexport function createMergeContext(): MergeContext {\n  return { arrays: createIndexRecord() as MergeContext[\"arrays\"] };\n}\n\nexport function setPathValue(\n  target: ObjectTree,\n  path: string,\n  value: EntryValue,\n  options: MergeOptions = {}\n): ObjectTree {\n  const delimiter = options.delimiter ?? \".\";\n  const context = options.context ?? createMergeContext();\n  const allowUnsafePathSegments = options.allowUnsafePathSegments ?? false;\n  const nameParts = splitNameIntoParts(path, delimiter);\n  assertPathIsSafe(nameParts, allowUnsafePathSegments);\n\n  let currResult: unknown = target;\n  let arrayNameFull = \"\";\n\n  for (let partIndex = 0; partIndex < nameParts.length; partIndex += 1) {\n    const namePart = nameParts[partIndex] ?? \"\";\n    const isLast = partIndex === nameParts.length - 1;\n\n    if (namePart.includes(\"[]\") && isLast) {\n      const arrayName = namePart.slice(0, namePart.indexOf(\"[\"));\n      arrayNameFull += arrayName;\n\n      pushToNamedArray(currResult, arrayName, value);\n      continue;\n    }\n\n    if (namePart.includes(\"[\")) {\n      const arrayName = namePart.slice(0, namePart.indexOf(\"[\"));\n      const arrayIndex = namePart.replace(/(^([a-z_]+)?\\[)|(\\]$)/gi, \"\");\n\n      arrayNameFull += `_${arrayName}_${arrayIndex}`;\n\n      if (arrayName !== \"\") {\n        ensureNamedArray(currResult, arrayName);\n      }\n\n      const existingArrayMap = getOwnRecordValue(context.arrays, arrayNameFull);\n      const arrayMap = isRecord(existingArrayMap) ? existingArrayMap : createIndexRecord();\n      if (!isRecord(existingArrayMap)) {\n        setOwnRecordValue(context.arrays, arrayNameFull, arrayMap);\n      }\n\n      if (isLast) {\n        const inserted = pushToNamedArray(currResult, arrayName, value);\n        setOwnRecordValue(arrayMap, arrayIndex, inserted);\n      } else if (getOwnRecordValue(arrayMap, arrayIndex) === undefined) {\n        const nextNamePart = nameParts[partIndex + 1] ?? \"\";\n        const nextContainer = /^[0-9a-z_]+\\[?/i.test(nextNamePart) ? {} : [];\n\n        const inserted = pushToNamedArray(currResult, arrayName, nextContainer);\n        setOwnRecordValue(arrayMap, arrayIndex, inserted);\n      }\n\n      currResult = getOwnRecordValue(arrayMap, arrayIndex);\n      continue;\n    }\n\n    arrayNameFull += namePart;\n\n    if (!isRecord(currResult)) {\n      throw new TypeError(\"Expected object-like container while setting nested path\");\n    }\n\n    if (!isLast) {\n      if (getOwnRecordValue(currResult, namePart) === undefined) {\n        setOwnRecordValue(currResult, namePart, {});\n      }\n\n      currResult = getOwnRecordValue(currResult, namePart);\n    } else {\n      setOwnRecordValue(currResult, namePart, value);\n    }\n  }\n\n  return target;\n}\n\nexport function entriesToObject(entries: Iterable<EntryInput>, options?: ParseOptions): ObjectTree;\nexport function entriesToObject<TSchema extends SchemaValidator>(\n  entries: Iterable<EntryInput>,\n  options: ParseOptions & { schema: TSchema }\n): InferSchemaOutput<TSchema>;\nexport function entriesToObject(\n  entries: Iterable<EntryInput>,\n  options: ParseOptions & ValidationOptions = {}\n): unknown {\n  const delimiter = options.delimiter ?? \".\";\n  const skipEmpty = options.skipEmpty ?? true;\n  const allowUnsafePathSegments = options.allowUnsafePathSegments ?? false;\n  const context = createMergeContext();\n  const result: ObjectTree = {};\n\n  for (const rawEntry of entries) {\n    const entry = normalizeEntry(rawEntry);\n\n    if (shouldSkipValue(entry.value, skipEmpty)) {\n      continue;\n    }\n\n    setPathValue(result, entry.key, entry.value, {\n      delimiter,\n      context,\n      allowUnsafePathSegments\n    });\n  }\n\n  if (options.schema) {\n    return options.schema.parse(result);\n  }\n\n  return result;\n}\n\nfunction objectToNameValues(obj: unknown): NameValuePair[] {\n  const result: NameValuePair[] = [];\n\n  if (obj === null || obj === undefined) {\n    result.push({ name: \"\", value: null });\n    return result;\n  }\n\n  if (typeof obj === \"string\" || typeof obj === \"number\" || typeof obj === \"boolean\") {\n    result.push({ name: \"\", value: obj });\n    return result;\n  }\n\n  if (Array.isArray(obj)) {\n    for (let index = 0; index < obj.length; index += 1) {\n      const name = `[${index}]`;\n      result.push(...getSubValues(obj[index], name));\n    }\n    return result;\n  }\n\n  if (isRecord(obj)) {\n    for (const key of Object.keys(obj)) {\n      result.push(...getSubValues(obj[key], key));\n    }\n  }\n\n  return result;\n}\n\nfunction getSubValues(subObject: unknown, name: string): NameValuePair[] {\n  const result: NameValuePair[] = [];\n  const tempResult = objectToNameValues(subObject);\n\n  for (const item of tempResult) {\n    let itemName = name;\n\n    if (SUB_ARRAY_REGEXP.test(item.name)) {\n      itemName += item.name;\n    } else if (SUB_OBJECT_REGEXP.test(item.name)) {\n      itemName += `.${item.name}`;\n    }\n\n    result.push({ name: itemName, value: item.value });\n  }\n\n  return result;\n}\n\nexport function objectToEntries(value: unknown): Entry[] {\n  return objectToNameValues(value).map((item) => ({\n    key: item.name,\n    value: item.value\n  }));\n}\n\nexport function processNameValues(\n  nameValues: Iterable<NameValuePair>,\n  skipEmpty = true,\n  delimiter = \".\"\n): ObjectTree {\n  const entries: Entry[] = [];\n\n  for (const pair of nameValues) {\n    entries.push({ key: pair.name, value: pair.value });\n  }\n\n  return entriesToObject(entries, { skipEmpty, delimiter });\n}\n\nexport type {\n  Entry,\n  EntryInput,\n  EntryValue,\n  InferSchemaOutput,\n  MergeContext,\n  MergeOptions,\n  NameValuePair,\n  ObjectTree,\n  ParseOptions,\n  SchemaValidator,\n  ValidationOptions\n} from \"./types\";\n"
  },
  {
    "path": "packages/core/src/types.ts",
    "content": "export type EntryValue = unknown;\n\nexport interface Entry {\n  key: string;\n  value: EntryValue;\n}\n\nexport interface NameValuePair {\n  name: string;\n  value: EntryValue;\n}\n\nexport interface ParseOptions {\n  delimiter?: string;\n  skipEmpty?: boolean;\n  allowUnsafePathSegments?: boolean;\n}\n\nexport interface SchemaValidator<TOutput = unknown> {\n  parse(input: unknown): TOutput;\n}\n\nexport type InferSchemaOutput<TSchema> = TSchema extends SchemaValidator<infer TOutput>\n  ? TOutput\n  : never;\n\nexport interface ValidationOptions<TSchema extends SchemaValidator = SchemaValidator> {\n  schema?: TSchema;\n}\n\nexport interface MergeContext {\n  arrays: Record<string, Record<string, unknown>>;\n}\n\nexport interface MergeOptions {\n  delimiter?: string;\n  context?: MergeContext;\n  allowUnsafePathSegments?: boolean;\n}\n\nexport type ObjectTree = Record<string, unknown>;\n\nexport type EntryInput =\n  | Entry\n  | NameValuePair\n  | readonly [string, EntryValue]\n  | {\n      key: string;\n      value: EntryValue;\n    }\n  | {\n      name: string;\n      value: EntryValue;\n    };\n"
  },
  {
    "path": "packages/core/test/core.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { entriesToObject, objectToEntries, processNameValues, setPathValue } from \"../src/index\";\n\ndescribe(\"entriesToObject\", () => {\n  it(\"accepts tuple entries directly\", () => {\n    const result = entriesToObject([\n      [\"person.name.first\", \"John\"],\n      [\"person.name.last\", \"Doe\"]\n    ]);\n\n    expect(result).toEqual({\n      person: {\n        name: {\n          first: \"John\",\n          last: \"Doe\"\n        }\n      }\n    });\n  });\n\n  it(\"accepts name/value object entries directly\", () => {\n    const result = entriesToObject([\n      { name: \"person.name.first\", value: \"John\" },\n      { name: \"person.name.last\", value: \"Doe\" }\n    ]);\n\n    expect(result).toEqual({\n      person: {\n        name: {\n          first: \"John\",\n          last: \"Doe\"\n        }\n      }\n    });\n  });\n\n  it(\"builds nested objects with dot notation\", () => {\n    const result = entriesToObject([\n      { key: \"person.name.first\", value: \"John\" },\n      { key: \"person.name.last\", value: \"Doe\" }\n    ]);\n\n    expect(result).toEqual({\n      person: {\n        name: {\n          first: \"John\",\n          last: \"Doe\"\n        }\n      }\n    });\n  });\n\n  it(\"builds array values with [] syntax\", () => {\n    const result = entriesToObject([\n      { key: \"person.favFood[]\", value: \"steak\" },\n      { key: \"person.favFood[]\", value: \"chicken\" }\n    ]);\n\n    expect(result).toEqual({\n      person: {\n        favFood: [\"steak\", \"chicken\"]\n      }\n    });\n  });\n\n  it(\"keeps indexed arrays in first-seen order\", () => {\n    const result = entriesToObject([\n      { key: \"person.friends[5].name\", value: \"Neo\" },\n      { key: \"person.friends[3].name\", value: \"Smith\" }\n    ]);\n\n    expect(result).toEqual({\n      person: {\n        friends: [{ name: \"Neo\" }, { name: \"Smith\" }]\n      }\n    });\n  });\n\n  it(\"supports rails style object paths\", () => {\n    const result = entriesToObject([\n      { key: \"rails[field1][foo]\", value: \"baz\" },\n      { key: \"rails[field1][bar]\", value: \"qux\" }\n    ]);\n\n    expect(result).toEqual({\n      rails: {\n        field1: {\n          foo: \"baz\",\n          bar: \"qux\"\n        }\n      }\n    });\n  });\n\n  it(\"supports rails style keys with underscores and one-character names\", () => {\n    const result = entriesToObject(\n      [\n        { key: \"data[Topic][topic_id]\", value: \"1\" },\n        { key: \"person.ruby[field2][f]\", value: \"baz\" }\n      ],\n      { skipEmpty: false }\n    );\n\n    expect(result).toEqual({\n      data: {\n        Topic: {\n          topic_id: \"1\"\n        }\n      },\n      person: {\n        ruby: {\n          field2: {\n            f: \"baz\"\n          }\n        }\n      }\n    });\n  });\n\n  it(\"supports single-bracket rails object segments at the root\", () => {\n    const result = entriesToObject([{ key: \"testitem[test_property]\", value: \"ok\" }], {\n      skipEmpty: false\n    });\n\n    expect(result).toEqual({\n      testitem: {\n        test_property: \"ok\"\n      }\n    });\n  });\n\n  it(\"supports mixed indexed rails arrays and nested object traversal\", () => {\n    const result = entriesToObject(\n      [\n        { key: \"tables[1][features][0][title]\", value: \"Feature A\" },\n        { key: \"something[something][title]\", value: \"Nested\" },\n        { key: \"something[description]\", value: \"Test\" }\n      ],\n      { skipEmpty: false }\n    );\n\n    expect(result).toEqual({\n      tables: [\n        {\n          features: [\n            {\n              title: \"Feature A\"\n            }\n          ]\n        }\n      ],\n      something: {\n        something: {\n          title: \"Nested\"\n        },\n        description: \"Test\"\n      }\n    });\n  });\n\n  it(\"supports consecutive indexed segments for nested arrays\", () => {\n    const result = entriesToObject([{ key: \"foo[0][1][bar]\", value: \"baz\" }], {\n      skipEmpty: false\n    });\n\n    expect(result).toEqual({\n      foo: [\n        [\n          {\n            bar: \"baz\"\n          }\n        ]\n      ]\n    });\n  });\n\n  it(\"skips empty and null values by default\", () => {\n    const result = entriesToObject([\n      { key: \"a\", value: \"\" },\n      { key: \"b\", value: null },\n      { key: \"c\", value: \"ok\" }\n    ]);\n\n    expect(result).toEqual({ c: \"ok\" });\n  });\n\n  it(\"can keep empty values\", () => {\n    const result = entriesToObject(\n      [\n        { key: \"a\", value: \"\" },\n        { key: \"b\", value: null },\n        { key: \"c\", value: \"ok\" }\n      ],\n      { skipEmpty: false }\n    );\n\n    expect(result).toEqual({\n      a: \"\",\n      b: null,\n      c: \"ok\"\n    });\n  });\n\n  it(\"rejects unsafe path segments by default\", () => {\n    expect(() =>\n      entriesToObject([{ key: \"__proto__.polluted\", value: \"yes\" }], {\n        skipEmpty: false\n      })\n    ).toThrow(/Unsafe path segment/);\n  });\n\n  it(\"can opt into unsafe path segments for trusted inputs\", () => {\n    const target = Object.create(null) as Record<string, unknown>;\n\n    setPathValue(target, \"__proto__.polluted\", \"yes\", {\n      allowUnsafePathSegments: true\n    });\n\n    expect(target.__proto__).toEqual({ polluted: \"yes\" });\n  });\n\n  it(\"stores trusted unsafe path segments as own properties on plain objects\", () => {\n    const target = {} as Record<string, unknown>;\n\n    setPathValue(target, \"__proto__.polluted\", \"yes\", {\n      allowUnsafePathSegments: true\n    });\n\n    expect(Object.getPrototypeOf(target)).toBe(Object.prototype);\n    expect(Object.prototype.hasOwnProperty.call(target, \"__proto__\")).toBe(true);\n    expect(target.__proto__).toEqual({ polluted: \"yes\" });\n    expect(({} as Record<string, unknown>).polluted).toBeUndefined();\n  });\n\n  it(\"does not treat trusted array index markers as prototype access\", () => {\n    const target = {} as Record<string, unknown>;\n\n    setPathValue(target, \"items[__proto__].title\", \"safe\", {\n      allowUnsafePathSegments: true\n    });\n\n    expect(Object.getPrototypeOf(target)).toBe(Object.prototype);\n    expect(({} as Record<string, unknown>).title).toBeUndefined();\n    expect(Object.prototype.hasOwnProperty.call(target.items as Record<string, unknown>, \"__proto__\")).toBe(true);\n    expect((target.items as Record<string, unknown>).__proto__).toEqual({ title: \"safe\" });\n  });\n\n  it(\"validates and transforms using schema.parse when schema is provided\", () => {\n    const schema = {\n      parse(value: unknown) {\n        const record = value as { person?: { age?: string } };\n        return {\n          person: {\n            age: Number(record.person?.age ?? \"0\")\n          }\n        };\n      }\n    };\n\n    const result = entriesToObject([{ key: \"person.age\", value: \"42\" }], { schema });\n\n    expect(result).toEqual({\n      person: {\n        age: 42\n      }\n    });\n  });\n\n  it(\"throws validation errors from schema.parse\", () => {\n    const schema = {\n      parse() {\n        throw new Error(\"Validation failed\");\n      }\n    };\n\n    expect(() =>\n      entriesToObject([{ key: \"person.name\", value: \"Neo\" }], {\n        schema\n      })\n    ).toThrow(\"Validation failed\");\n  });\n\n  it(\"throws for invalid entry shapes\", () => {\n    expect(() =>\n      entriesToObject([{ value: \"missing-key\" } as unknown as { key: string; value: unknown }])\n    ).toThrow(\"Invalid entry\");\n  });\n});\n\ndescribe(\"processNameValues\", () => {\n  it(\"keeps compatibility name field\", () => {\n    const result = processNameValues([\n      { name: \"person.name.first\", value: \"John\" },\n      { name: \"person.name.last\", value: \"Doe\" }\n    ]);\n\n    expect(result).toEqual({\n      person: {\n        name: {\n          first: \"John\",\n          last: \"Doe\"\n        }\n      }\n    });\n  });\n});\n\ndescribe(\"objectToEntries\", () => {\n  it(\"flattens nested objects and arrays\", () => {\n    const result = objectToEntries({\n      person: {\n        name: {\n          first: \"John\"\n        },\n        emails: [\"a@example.com\", \"b@example.com\"]\n      }\n    });\n\n    expect(result).toContainEqual({ key: \"person.name.first\", value: \"John\" });\n    expect(result).toContainEqual({ key: \"person.emails[0]\", value: \"a@example.com\" });\n    expect(result).toContainEqual({ key: \"person.emails[1]\", value: \"b@example.com\" });\n  });\n\n  it(\"only serializes own enumerable properties\", () => {\n    const value = Object.create({ leaked: \"secret\" }) as Record<string, unknown>;\n    value.safe = \"ok\";\n\n    const result = objectToEntries(value);\n\n    expect(result).toEqual([{ key: \"safe\", value: \"ok\" }]);\n  });\n});\n"
  },
  {
    "path": "packages/core/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"lib\": [\"ES2020\"]\n  },\n  \"include\": [\"src/**/*.ts\", \"test/**/*.ts\", \"tsup.config.ts\"]\n}\n"
  },
  {
    "path": "packages/core/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: {\n    index: \"src/index.ts\"\n  },\n  format: [\"esm\", \"cjs\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  target: \"es2020\"\n});\n"
  },
  {
    "path": "packages/dom/CHANGELOG.md",
    "content": "# @form2js/dom\n\n## 3.2.0\n\n### Minor Changes\n\n- Restore Rails-style bracket path compatibility across parsing and form population, align DOM checkbox and radio handling with native browser submission behavior, and add explicit `SKIP_NODE` callback support for DOM extraction.\n\n### Patch Changes\n\n- Updated dependencies\n  - @form2js/core@3.3.0\n\n## 3.1.4\n\n### Patch Changes\n\n- Republish the package metadata and package-level README improvements across all public packages after fixing publish configuration.\n- Updated dependencies\n  - @form2js/core@3.2.2\n\n## 3.1.3\n\n### Patch Changes\n\n- Add npm package metadata (`license`, `bugs`, `keywords`) and publish package-level README files for all public packages.\n- Updated dependencies\n  - @form2js/core@3.2.1\n\n## 3.1.2\n\n### Patch Changes\n\n- Updated dependencies [4d2f923]\n  - @form2js/core@3.2.0\n\n## 3.1.1\n\n### Patch Changes\n\n- 77b8543: Hardened parser security by rejecting unsafe key path segments by default and aligned DOM extraction with HTML semantics (disabled fieldsets, no button-like inputs). Added a writer hook, improved multi-select mapping, tightened ESLint, added a version bump script, and aligned packages to 3.1.0 with updated docs, tests, and repo/website links.\n\n      New Features\n          Added allowUnsafePathSegments option across core, dom, form-data, and jQuery plugin to explicitly allow trusted unsafe segments.\n          Added nodeCallback to objectToForm/js2form; returning false skips default assignment for that node.\n          Improved multi-select handling: matches names with [] and bare-name fallbacks without per-option keys.\n          Added bump-version script to sync workspace versions and ranges; bumped packages to 3.1.0.\n          Enabled vitest for examples and added smoke tests; updated tsconfig and lint script.\n\n      Bug Fixes\n          Prevented prototype pollution by blocking \"proto\", \"prototype\", and \"constructor\" path tokens by default.\n          objectToEntries now serializes only own enumerable properties.\n          DOM extraction respects disabled fieldset rules (legend exception) and excludes button-like inputs even when skipEmpty is false.\n          ESLint config now errors on unsafe TypeScript operations (no-unsafe-* rules).\n\n- Updated dependencies [77b8543]\n  - @form2js/core@3.1.1\n"
  },
  {
    "path": "packages/dom/README.md",
    "content": "# @form2js/dom\n\nParse browser form controls into structured objects.\n\n## Install\n\n```bash\nnpm install @form2js/dom\n```\n\n## Minimal usage\n\n```ts\nimport { formToObject } from \"@form2js/dom\";\n\nconst result = formToObject(document.getElementById(\"profileForm\"));\n```\n\nFor guides, playground examples, and API details, see the docs site:\nhttps://maxatwork.github.io/form2js/?variant=form\n\nLicense: MIT\n"
  },
  {
    "path": "packages/dom/package.json",
    "content": "{\n  \"name\": \"@form2js/dom\",\n  \"version\": \"3.4.0\",\n  \"description\": \"DOM extraction adapters for form2js.\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/maxatwork/form2js\",\n  \"homepage\": \"https://maxatwork.github.io/form2js/\",\n  \"bugs\": {\n    \"url\": \"https://github.com/maxatwork/form2js/issues\"\n  },\n  \"keywords\": [\n    \"form\",\n    \"forms\",\n    \"serialization\",\n    \"deserialization\",\n    \"form-data\",\n    \"json\",\n    \"dom\",\n    \"form-to-object\",\n    \"browser\"\n  ],\n  \"type\": \"module\",\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"browser\": \"./dist/standalone.js\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\",\n      \"require\": \"./dist/index.cjs\"\n    },\n    \"./standalone\": {\n      \"default\": \"./dist/standalone.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"engines\": {\n    \"node\": \">=18\"\n  },\n  \"dependencies\": {\n    \"@form2js/core\": \"3.4.0\"\n  },\n  \"scripts\": {\n    \"build\": \"tsup --config tsup.config.ts\",\n    \"test\": \"vitest run\",\n    \"lint\": \"eslint \\\"src/**/*.ts\\\" \\\"test/**/*.ts\\\"\",\n    \"typecheck\": \"tsc -p tsconfig.json --noEmit\",\n    \"clean\": \"rimraf dist\"\n  }\n}\n"
  },
  {
    "path": "packages/dom/src/index.ts",
    "content": "import { entriesToObject, type Entry, type ObjectTree, type ParseOptions } from \"@form2js/core\";\n\nexport interface NodeCallbackResult {\n  name?: string;\n  key?: string;\n  value: unknown;\n}\n\nexport const SKIP_NODE = Symbol(\"form2js.skipNode\");\n\nexport type FormToObjectNodeCallback = (node: Node) => NodeCallbackResult | typeof SKIP_NODE | false | null | undefined;\n\nexport interface ExtractOptions {\n  nodeCallback?: FormToObjectNodeCallback;\n  useIdIfEmptyName?: boolean;\n  getDisabled?: boolean;\n  document?: Document;\n}\n\nexport interface FormToObjectOptions extends ExtractOptions, ParseOptions {}\n\nexport type RootNodeInput =\n  | string\n  | Node\n  | NodeListOf<Node>\n  | Node[]\n  | HTMLCollection\n  | null\n  | undefined;\n\nfunction isNodeObject(value: unknown): value is Node {\n  return (\n    typeof value === \"object\" &&\n    value !== null &&\n    \"nodeType\" in value &&\n    \"nodeName\" in value\n  );\n}\n\nfunction isElementNode(node: Node): node is Element {\n  return node.nodeType === 1;\n}\n\nfunction nodeNameIs(node: Node, expected: string): boolean {\n  return node.nodeName.toUpperCase() === expected;\n}\n\nfunction isInputNode(node: Node): node is HTMLInputElement {\n  return nodeNameIs(node, \"INPUT\");\n}\n\nfunction isTextareaNode(node: Node): node is HTMLTextAreaElement {\n  return nodeNameIs(node, \"TEXTAREA\");\n}\n\nfunction isSelectNode(node: Node): node is HTMLSelectElement {\n  return nodeNameIs(node, \"SELECT\");\n}\n\nfunction isNodeDisabled(node: Node): boolean {\n  return \"disabled\" in node && Boolean((node as { disabled?: boolean }).disabled);\n}\n\nfunction isFormControlNode(node: Node): node is HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement {\n  return isInputNode(node) || isTextareaNode(node) || isSelectNode(node);\n}\n\nfunction getFirstLegendChild(fieldset: Element): Element | null {\n  for (let index = 0; index < fieldset.children.length; index += 1) {\n    const child = fieldset.children[index];\n    if (child?.nodeName.toUpperCase() === \"LEGEND\") {\n      return child;\n    }\n  }\n\n  return null;\n}\n\nfunction isDisabledByAncestorFieldset(node: Element): boolean {\n  let ancestor: Element | null = node.parentElement;\n\n  while (ancestor) {\n    const isDisabledFieldset =\n      nodeNameIs(ancestor, \"FIELDSET\") && \"disabled\" in ancestor && Boolean((ancestor as { disabled?: boolean }).disabled);\n\n    if (isDisabledFieldset) {\n      const firstLegend = getFirstLegendChild(ancestor);\n      if (firstLegend?.contains(node)) {\n        ancestor = ancestor.parentElement;\n        continue;\n      }\n\n      return true;\n    }\n\n    ancestor = ancestor.parentElement;\n  }\n\n  return false;\n}\n\nfunction isEffectivelyDisabledControl(node: Node): boolean {\n  if (!isFormControlNode(node)) {\n    return false;\n  }\n\n  if (isNodeDisabled(node)) {\n    return true;\n  }\n\n  return isDisabledByAncestorFieldset(node);\n}\n\nfunction isButtonLikeInputType(inputType: string): boolean {\n  return /^(button|reset|submit|image)$/i.test(inputType);\n}\n\nfunction isNodeListLike(value: RootNodeInput): value is Node[] | NodeListOf<Node> | HTMLCollection {\n  if (!value || typeof value === \"string\") {\n    return false;\n  }\n\n  return !isNodeObject(value) && typeof value === \"object\" && \"length\" in value;\n}\n\nfunction getDocumentFromRoot(rootNode: RootNodeInput, fallback?: Document): Document {\n  if (fallback) {\n    return fallback;\n  }\n\n  if (typeof document !== \"undefined\") {\n    return document;\n  }\n\n  if (isNodeObject(rootNode) && rootNode.ownerDocument) {\n    return rootNode.ownerDocument;\n  }\n\n  if (isNodeListLike(rootNode) && rootNode.length > 0) {\n    const firstNode = rootNode[0];\n    if (isNodeObject(firstNode) && firstNode.ownerDocument) {\n      return firstNode.ownerDocument;\n    }\n  }\n\n  throw new Error(\"No document available. Provide options.document when running outside a browser.\");\n}\n\nfunction resolveRootNode(rootNode: RootNodeInput, options: ExtractOptions): RootNodeInput {\n  if (typeof rootNode !== \"string\") {\n    return rootNode;\n  }\n\n  const doc = getDocumentFromRoot(rootNode, options.document);\n  return doc.getElementById(rootNode);\n}\n\nfunction getFieldName(node: Node, useIdIfEmptyName: boolean): string {\n  if (!isElementNode(node)) {\n    return \"\";\n  }\n\n  const namedNode = node as Element & { name?: string; id?: string };\n\n  if (namedNode.name && namedNode.name !== \"\") {\n    return namedNode.name;\n  }\n\n  if (useIdIfEmptyName && namedNode.id && namedNode.id !== \"\") {\n    return namedNode.id;\n  }\n\n  return \"\";\n}\n\nfunction getSelectedOptionValue(selectNode: HTMLSelectElement): string | string[] {\n  if (!selectNode.multiple) {\n    return selectNode.value;\n  }\n\n  const result: string[] = [];\n  const options = selectNode.getElementsByTagName(\"option\");\n\n  for (let index = 0; index < options.length; index += 1) {\n    const option = options[index];\n    if (option?.selected) {\n      result.push(option.value);\n    }\n  }\n\n  return result;\n}\n\nfunction getFieldValue(fieldNode: Node, getDisabled: boolean): unknown {\n  if (isInputNode(fieldNode) || isTextareaNode(fieldNode)) {\n    const textLikeNode = fieldNode as HTMLInputElement | HTMLTextAreaElement;\n\n    if (isNodeDisabled(fieldNode) && !getDisabled) {\n      return null;\n    }\n\n    if (isInputNode(fieldNode)) {\n      const inputType = fieldNode.type.toLowerCase();\n\n      if (isButtonLikeInputType(inputType)) {\n        return null;\n      }\n\n      switch (inputType) {\n        case \"radio\":\n        case \"checkbox\":\n          if (fieldNode.checked) {\n            return fieldNode.value;\n          }\n\n          break;\n\n        default:\n          return fieldNode.value;\n      }\n\n      return null;\n    }\n\n    return textLikeNode.value;\n  }\n\n  if (isSelectNode(fieldNode)) {\n    if (isNodeDisabled(fieldNode) && !getDisabled) {\n      return null;\n    }\n\n    return getSelectedOptionValue(fieldNode);\n  }\n\n  return null;\n}\n\nfunction getSubFormValues(rootNode: Node, options: ExtractOptions): Entry[] {\n  const result: Entry[] = [];\n  let currentNode: ChildNode | null = rootNode.firstChild;\n\n  while (currentNode) {\n    const extractedValues = extractNodeValues(currentNode, options);\n    if (extractedValues !== SKIP_NODE) {\n      result.push(...extractedValues);\n    }\n    currentNode = currentNode.nextSibling;\n  }\n\n  return result;\n}\n\nfunction extractNodeValues(node: Node, options: ExtractOptions): Entry[] | typeof SKIP_NODE {\n  if (isEffectivelyDisabledControl(node) && !options.getDisabled) {\n    return [];\n  }\n\n  const fieldName = getFieldName(node, options.useIdIfEmptyName ?? false);\n  const callbackResult = options.nodeCallback?.(node);\n\n  if (callbackResult === SKIP_NODE) {\n    return SKIP_NODE;\n  }\n\n  if (callbackResult && (callbackResult.name || callbackResult.key)) {\n    const key = callbackResult.key ?? callbackResult.name ?? \"\";\n    if (key !== \"\") {\n      return [{ key, value: callbackResult.value }];\n    }\n  }\n\n  if (fieldName !== \"\" && (isInputNode(node) || isTextareaNode(node))) {\n    const fieldValue = getFieldValue(node, options.getDisabled ?? false);\n    if (fieldValue === null) {\n      return [];\n    }\n\n    return [{ key: fieldName, value: fieldValue }];\n  }\n\n  if (fieldName !== \"\" && isSelectNode(node)) {\n    const fieldValue = getFieldValue(node, options.getDisabled ?? false);\n    return [{ key: fieldName.replace(/\\[\\]$/, \"\"), value: fieldValue }];\n  }\n\n  return getSubFormValues(node, options);\n}\n\nfunction getFormValues(rootNode: Node, options: ExtractOptions): Entry[] {\n  const directResult = extractNodeValues(rootNode, options);\n  if (directResult === SKIP_NODE) {\n    return [];\n  }\n\n  if (directResult.length > 0) {\n    return directResult;\n  }\n\n  return getSubFormValues(rootNode, options);\n}\n\nexport function extractPairs(rootNode: RootNodeInput, options: ExtractOptions = {}): Entry[] {\n  const resolvedRoot = resolveRootNode(rootNode, options);\n\n  if (!resolvedRoot) {\n    return [];\n  }\n\n  if (isNodeListLike(resolvedRoot)) {\n    const result: Entry[] = [];\n\n    for (let index = 0; index < resolvedRoot.length; index += 1) {\n      const currentNode = resolvedRoot[index];\n      if (isNodeObject(currentNode)) {\n        result.push(...getFormValues(currentNode, options));\n      }\n    }\n\n    return result;\n  }\n\n  if (isNodeObject(resolvedRoot)) {\n    return getFormValues(resolvedRoot, options);\n  }\n\n  return [];\n}\n\nexport function formToObject(rootNode: RootNodeInput, options: FormToObjectOptions = {}): ObjectTree {\n  const pairs = extractPairs(rootNode, options);\n  const parseOptions: ParseOptions = {};\n\n  if (options.delimiter !== undefined) {\n    parseOptions.delimiter = options.delimiter;\n  }\n\n  if (options.skipEmpty !== undefined) {\n    parseOptions.skipEmpty = options.skipEmpty;\n  }\n\n  if (options.allowUnsafePathSegments !== undefined) {\n    parseOptions.allowUnsafePathSegments = options.allowUnsafePathSegments;\n  }\n\n  return entriesToObject(pairs, parseOptions);\n}\n\nexport function form2js(\n  rootNode: RootNodeInput,\n  delimiter?: string,\n  skipEmpty?: boolean,\n  nodeCallback?: FormToObjectNodeCallback,\n  useIdIfEmptyName = false,\n  getDisabled = false,\n  allowUnsafePathSegments = false\n): ObjectTree {\n  const normalizedOptions: FormToObjectOptions = {\n    useIdIfEmptyName,\n    getDisabled,\n    allowUnsafePathSegments\n  };\n\n  if (delimiter !== undefined) {\n    normalizedOptions.delimiter = delimiter;\n  }\n\n  if (skipEmpty !== undefined) {\n    normalizedOptions.skipEmpty = skipEmpty;\n  }\n\n  if (nodeCallback !== undefined) {\n    normalizedOptions.nodeCallback = nodeCallback;\n  }\n\n  return formToObject(rootNode, normalizedOptions);\n}\n"
  },
  {
    "path": "packages/dom/src/standalone.ts",
    "content": "import { SKIP_NODE, form2js, formToObject } from \"./index\";\n\ninterface DomStandaloneGlobals {\n  formToObject?: typeof formToObject;\n  form2js?: typeof form2js;\n  SKIP_NODE?: typeof SKIP_NODE;\n}\n\nconst scope = globalThis as typeof globalThis & DomStandaloneGlobals;\n\nscope.formToObject = formToObject;\nscope.form2js = form2js;\nscope.SKIP_NODE = SKIP_NODE;\n\nexport { SKIP_NODE, form2js, formToObject };\n"
  },
  {
    "path": "packages/dom/test/dom.test.ts",
    "content": "// @vitest-environment jsdom\n\nimport { describe, expect, it } from \"vitest\";\nimport { SKIP_NODE, extractPairs, form2js, formToObject } from \"../src/index\";\n\ndescribe(\"extractPairs\", () => {\n  it(\"extracts input/select values\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input type=\"text\" name=\"person.name.first\" value=\"John\" />\n        <input type=\"text\" name=\"person.name.last\" value=\"Doe\" />\n        <select name=\"person.colors[]\" multiple>\n          <option value=\"red\" selected>red</option>\n          <option value=\"blue\">blue</option>\n          <option value=\"green\" selected>green</option>\n        </select>\n      </form>\n    `;\n\n    const form = document.getElementById(\"testForm\") as HTMLFormElement;\n    const result = extractPairs(form);\n\n    expect(result).toContainEqual({ key: \"person.name.first\", value: \"John\" });\n    expect(result).toContainEqual({ key: \"person.name.last\", value: \"Doe\" });\n    expect(result).toContainEqual({ key: \"person.colors\", value: [\"red\", \"green\"] });\n  });\n\n  it(\"extracts nested form controls inside arbitrary container markup\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <div class=\"wrapper\">\n          <section>\n            <input type=\"text\" name=\"person.name.first\" value=\"John\" />\n          </section>\n        </div>\n      </form>\n    `;\n\n    const form = document.getElementById(\"testForm\") as HTMLFormElement;\n    const result = extractPairs(form);\n\n    expect(result).toEqual([{ key: \"person.name.first\", value: \"John\" }]);\n  });\n\n  it(\"supports callback extraction\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <div id=\"person.callbackTest\">hello world</div>\n      </form>\n    `;\n\n    const form = document.getElementById(\"testForm\") as HTMLFormElement;\n    const result = extractPairs(form, {\n      nodeCallback(node) {\n        if (node instanceof HTMLDivElement && node.id === \"person.callbackTest\") {\n          return { name: node.id, value: node.textContent };\n        }\n\n        return false;\n      }\n    });\n\n    expect(result).toEqual([{ key: \"person.callbackTest\", value: \"hello world\" }]);\n  });\n\n  it(\"supports explicit callback exclusion without overloading falsy returns\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input name=\"person.changed\" value=\"yes\" data-changed=\"true\" />\n        <input name=\"person.unchanged\" value=\"no\" />\n      </form>\n    `;\n\n    const form = document.getElementById(\"testForm\") as HTMLFormElement;\n    const result = extractPairs(form, {\n      nodeCallback(node) {\n        if (!(node instanceof HTMLInputElement)) {\n          return false;\n        }\n\n        if (node.dataset.changed === \"true\") {\n          return null;\n        }\n\n        return SKIP_NODE;\n      }\n    });\n\n    expect(result).toEqual([{ key: \"person.changed\", value: \"yes\" }]);\n  });\n\n  it(\"excludes an entire supplied root when the callback returns SKIP_NODE for that root\", () => {\n    document.body.innerHTML = `\n      <section id=\"wrapper\">\n        <input name=\"person.changed\" value=\"yes\" />\n      </section>\n    `;\n\n    const wrapper = document.getElementById(\"wrapper\") as HTMLElement;\n    const result = extractPairs(wrapper, {\n      nodeCallback(node) {\n        if (node === wrapper) {\n          return SKIP_NODE;\n        }\n\n        return false;\n      }\n    });\n\n    expect(result).toEqual([]);\n  });\n\n  it(\"returns empty pairs when the root cannot be resolved\", () => {\n    expect(extractPairs(\"missing-form\")).toEqual([]);\n  });\n});\n\ndescribe(\"formToObject\", () => {\n  it(\"keeps checkbox and radio values aligned with native form submission\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input type=\"checkbox\" name=\"person.checkboxTrueChecked\" value=\"true\" checked />\n        <input type=\"checkbox\" name=\"person.checkboxTrueUnchecked\" value=\"true\" />\n        <input type=\"checkbox\" name=\"person.checkboxFalseChecked\" value=\"false\" checked />\n        <input type=\"checkbox\" name=\"person.checkboxFalseUnchecked\" value=\"false\" />\n        <input type=\"radio\" name=\"person.radio\" value=\"false\" checked />\n      </form>\n    `;\n\n    const form = document.getElementById(\"testForm\") as HTMLFormElement;\n    const result = formToObject(form);\n\n    expect(result).toEqual({\n      person: {\n        checkboxTrueChecked: \"true\",\n        checkboxFalseChecked: \"false\",\n        radio: \"false\"\n      }\n    });\n  });\n\n  it(\"uses the browser default on value for checked checkboxes without an explicit value\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input type=\"checkbox\" name=\"enabled\" checked />\n        <input type=\"checkbox\" name=\"disabled\" />\n      </form>\n    `;\n\n    const form = document.getElementById(\"testForm\") as HTMLFormElement;\n\n    expect(extractPairs(form)).toEqual([{ key: \"enabled\", value: \"on\" }]);\n    expect(formToObject(form)).toEqual({\n      enabled: \"on\"\n    });\n  });\n\n  it(\"compacts sparse indexed checkbox rows after omitting unchecked controls\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input type=\"checkbox\" name=\"items[0].selected\" value=\"true\" />\n        <input type=\"checkbox\" name=\"items[1].selected\" value=\"true\" checked />\n      </form>\n    `;\n\n    const form = document.getElementById(\"testForm\") as HTMLFormElement;\n\n    expect(formToObject(form)).toEqual({\n      items: [{ selected: \"true\" }]\n    });\n  });\n\n  it(\"does not coerce an empty checked radio option to false when true and false siblings exist\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input type=\"radio\" name=\"state\" value=\"\" checked />\n        <input type=\"radio\" name=\"state\" value=\"true\" />\n        <input type=\"radio\" name=\"state\" value=\"false\" />\n      </form>\n    `;\n\n    const form = document.getElementById(\"testForm\") as HTMLFormElement;\n    const result = formToObject(form, { skipEmpty: false });\n\n    expect(result).toEqual({\n      state: \"\"\n    });\n  });\n\n  it(\"supports id fallback and disabled field extraction\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input id=\"person.name.first\" name=\"\" value=\"John\" />\n        <input id=\"person.name.last\" disabled value=\"Doe\" />\n      </form>\n    `;\n\n    const form = document.getElementById(\"testForm\") as HTMLFormElement;\n\n    const withoutDisabled = formToObject(form, {\n      useIdIfEmptyName: true\n    });\n\n    expect(withoutDisabled).toEqual({\n      person: {\n        name: {\n          first: \"John\"\n        }\n      }\n    });\n\n    const withDisabled = formToObject(form, {\n      useIdIfEmptyName: true,\n      getDisabled: true\n    });\n\n    expect(withDisabled).toEqual({\n      person: {\n        name: {\n          first: \"John\",\n          last: \"Doe\"\n        }\n      }\n    });\n  });\n\n  it(\"respects disabled fieldset semantics\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <fieldset disabled>\n          <legend><input name=\"insideLegend\" value=\"legend-value\" /></legend>\n          <input name=\"outsideLegend\" value=\"blocked-value\" />\n        </fieldset>\n      </form>\n    `;\n\n    const form = document.getElementById(\"testForm\") as HTMLFormElement;\n\n    const withoutDisabled = formToObject(form);\n    expect(withoutDisabled).toEqual({\n      insideLegend: \"legend-value\"\n    });\n\n    const withDisabled = formToObject(form, { getDisabled: true });\n    expect(withDisabled).toEqual({\n      insideLegend: \"legend-value\",\n      outsideLegend: \"blocked-value\"\n    });\n  });\n\n  it(\"skips button-like input fields even when skipEmpty is false\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input type=\"submit\" name=\"submitButton\" value=\"Submit\" />\n        <input type=\"button\" name=\"plainButton\" value=\"Click\" />\n        <input type=\"text\" name=\"person.name\" value=\"Trinity\" />\n      </form>\n    `;\n\n    const form = document.getElementById(\"testForm\") as HTMLFormElement;\n    const result = formToObject(form, { skipEmpty: false });\n\n    expect(result).toEqual({\n      person: {\n        name: \"Trinity\"\n      }\n    });\n  });\n\n  it(\"rejects unsafe path segments from field names\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input name=\"__proto__.polluted\" value=\"yes\" />\n      </form>\n    `;\n\n    const form = document.getElementById(\"testForm\") as HTMLFormElement;\n\n    expect(() => formToObject(form)).toThrow(/Unsafe path segment/);\n  });\n\n  it(\"can combine NodeList roots\", () => {\n    document.body.innerHTML = `\n      <form class=\"part\"><input name=\"person.first\" value=\"Neo\" /></form>\n      <form class=\"part\"><input name=\"person.last\" value=\"Anderson\" /></form>\n    `;\n\n    const roots = document.querySelectorAll(\".part\");\n    const result = formToObject(roots);\n\n    expect(result).toEqual({\n      person: {\n        first: \"Neo\",\n        last: \"Anderson\"\n      }\n    });\n  });\n\n  it(\"can combine HTMLCollection roots\", () => {\n    document.body.innerHTML = `\n      <form class=\"part\"><input name=\"person.first\" value=\"Neo\" /></form>\n      <form class=\"part\"><input name=\"person.last\" value=\"Anderson\" /></form>\n    `;\n\n    const roots = document.getElementsByClassName(\"part\");\n    const result = formToObject(roots);\n\n    expect(result).toEqual({\n      person: {\n        first: \"Neo\",\n        last: \"Anderson\"\n      }\n    });\n  });\n\n  it(\"resolves string roots against an explicit document\", () => {\n    const customDocument = document.implementation.createHTMLDocument(\"custom\");\n    customDocument.body.innerHTML = `\n      <form id=\"testForm\">\n        <input name=\"person/name\" value=\"Sam\" />\n      </form>\n    `;\n\n    const result = formToObject(\"testForm\", {\n      document: customDocument,\n      delimiter: \"/\"\n    });\n\n    expect(result).toEqual({\n      person: {\n        name: \"Sam\"\n      }\n    });\n  });\n\n  it(\"keeps compatibility wrapper\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input name=\"person.name\" value=\"Trinity\" />\n      </form>\n    `;\n\n    const result = form2js(\"testForm\");\n\n    expect(result).toEqual({\n      person: {\n        name: \"Trinity\"\n      }\n    });\n  });\n\n  it(\"returns an empty object when the root cannot be resolved\", () => {\n    expect(formToObject(\"missing-form\")).toEqual({});\n  });\n});\n\ndescribe(\"standalone entry\", () => {\n  it(\"attaches browser globals\", async () => {\n    const scope = globalThis as typeof globalThis & {\n      formToObject?: unknown;\n      form2js?: unknown;\n      SKIP_NODE?: unknown;\n    };\n\n    Reflect.deleteProperty(scope, \"formToObject\");\n    Reflect.deleteProperty(scope, \"form2js\");\n    Reflect.deleteProperty(scope, \"SKIP_NODE\");\n\n    await import(\"../src/standalone\");\n\n    expect(typeof scope.formToObject).toBe(\"function\");\n    expect(typeof scope.form2js).toBe(\"function\");\n    expect(typeof scope.SKIP_NODE).toBe(\"symbol\");\n  });\n});\n"
  },
  {
    "path": "packages/dom/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"]\n  },\n  \"include\": [\"src/**/*.ts\", \"test/**/*.ts\", \"tsup.config.ts\"]\n}\n"
  },
  {
    "path": "packages/dom/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig([\n  {\n    entry: {\n      index: \"src/index.ts\"\n    },\n    format: [\"esm\", \"cjs\"],\n    dts: true,\n    sourcemap: true,\n    clean: true,\n    target: \"es2020\"\n  },\n  {\n    entry: {\n      standalone: \"src/standalone.ts\"\n    },\n    format: [\"iife\"],\n    globalName: \"form2jsDom\",\n    sourcemap: true,\n    clean: false,\n    target: \"es2020\"\n  }\n]);\n"
  },
  {
    "path": "packages/form-data/CHANGELOG.md",
    "content": "# @form2js/form-data\n\n## 3.2.3\n\n### Patch Changes\n\n- Updated dependencies\n  - @form2js/core@3.3.0\n\n## 3.2.2\n\n### Patch Changes\n\n- Republish the package metadata and package-level README improvements across all public packages after fixing publish configuration.\n- Updated dependencies\n  - @form2js/core@3.2.2\n\n## 3.2.1\n\n### Patch Changes\n\n- Add npm package metadata (`license`, `bugs`, `keywords`) and publish package-level README files for all public packages.\n- Updated dependencies\n  - @form2js/core@3.2.1\n\n## 3.2.0\n\n### Minor Changes\n\n- 4d2f923: Added optional schema validation support (structural `parse`) to parsing APIs in core and form-data, plus a new `@form2js/react` package with `useForm2js` for async submit state management in React forms.\n\n### Patch Changes\n\n- Updated dependencies [4d2f923]\n  - @form2js/core@3.2.0\n\n## 3.1.1\n\n### Patch Changes\n\n- 77b8543: Hardened parser security by rejecting unsafe key path segments by default and aligned DOM extraction with HTML semantics (disabled fieldsets, no button-like inputs). Added a writer hook, improved multi-select mapping, tightened ESLint, added a version bump script, and aligned packages to 3.1.0 with updated docs, tests, and repo/website links.\n\n      New Features\n          Added allowUnsafePathSegments option across core, dom, form-data, and jQuery plugin to explicitly allow trusted unsafe segments.\n          Added nodeCallback to objectToForm/js2form; returning false skips default assignment for that node.\n          Improved multi-select handling: matches names with [] and bare-name fallbacks without per-option keys.\n          Added bump-version script to sync workspace versions and ranges; bumped packages to 3.1.0.\n          Enabled vitest for examples and added smoke tests; updated tsconfig and lint script.\n\n      Bug Fixes\n          Prevented prototype pollution by blocking \"proto\", \"prototype\", and \"constructor\" path tokens by default.\n          objectToEntries now serializes only own enumerable properties.\n          DOM extraction respects disabled fieldset rules (legend exception) and excludes button-like inputs even when skipEmpty is false.\n          ESLint config now errors on unsafe TypeScript operations (no-unsafe-* rules).\n\n- Updated dependencies [77b8543]\n  - @form2js/core@3.1.1\n"
  },
  {
    "path": "packages/form-data/README.md",
    "content": "# @form2js/form-data\n\nConvert `FormData` and entry lists into structured objects.\n\n## Install\n\n```bash\nnpm install @form2js/form-data\n```\n\n## Minimal usage\n\n```ts\nimport { formDataToObject } from \"@form2js/form-data\";\n\nconst result = formDataToObject([\n  [\"person.name.first\", \"Sam\"],\n  [\"person.roles[]\", \"captain\"],\n]);\n```\n\nFor guides, playground examples, and API details, see the docs site:\nhttps://maxatwork.github.io/form2js/?variant=form-data\n\nLicense: MIT\n"
  },
  {
    "path": "packages/form-data/package.json",
    "content": "{\n  \"name\": \"@form2js/form-data\",\n  \"version\": \"3.4.0\",\n  \"description\": \"FormData adapters for form2js.\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/maxatwork/form2js\",\n  \"homepage\": \"https://maxatwork.github.io/form2js/\",\n  \"bugs\": {\n    \"url\": \"https://github.com/maxatwork/form2js/issues\"\n  },\n  \"keywords\": [\n    \"form\",\n    \"forms\",\n    \"serialization\",\n    \"deserialization\",\n    \"form-data\",\n    \"json\",\n    \"formdata\",\n    \"entries\",\n    \"server\"\n  ],\n  \"type\": \"module\",\n  \"sideEffects\": false,\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\",\n      \"require\": \"./dist/index.cjs\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"engines\": {\n    \"node\": \">=18\"\n  },\n  \"dependencies\": {\n    \"@form2js/core\": \"3.4.0\"\n  },\n  \"scripts\": {\n    \"build\": \"tsup --config tsup.config.ts\",\n    \"test\": \"vitest run\",\n    \"lint\": \"eslint \\\"src/**/*.ts\\\" \\\"test/**/*.ts\\\"\",\n    \"typecheck\": \"tsc -p tsconfig.json --noEmit\",\n    \"clean\": \"rimraf dist\"\n  }\n}\n"
  },
  {
    "path": "packages/form-data/src/index.ts",
    "content": "import {\n  entriesToObject as coreEntriesToObject,\n  type EntryInput,\n  type InferSchemaOutput,\n  type ObjectTree,\n  type ParseOptions,\n  type SchemaValidator,\n  type ValidationOptions\n} from \"@form2js/core\";\n\nexport type KeyValueEntryInput = EntryInput;\n\nexport interface FormDataToObjectOptions extends ParseOptions {}\n\nexport function entriesToObject(entries: Iterable<KeyValueEntryInput>, options?: ParseOptions): ObjectTree;\nexport function entriesToObject<TSchema extends SchemaValidator>(\n  entries: Iterable<KeyValueEntryInput>,\n  options: ParseOptions & { schema: TSchema }\n): InferSchemaOutput<TSchema>;\nexport function entriesToObject(\n  entries: Iterable<KeyValueEntryInput>,\n  options: ParseOptions & ValidationOptions = {}\n): unknown {\n  return coreEntriesToObject(entries, options);\n}\n\nexport function formDataToObject(\n  formData: FormData | Iterable<readonly [string, FormDataEntryValue]>,\n  options?: FormDataToObjectOptions\n): ObjectTree;\nexport function formDataToObject<TSchema extends SchemaValidator>(\n  formData: FormData | Iterable<readonly [string, FormDataEntryValue]>,\n  options: FormDataToObjectOptions & { schema: TSchema }\n): InferSchemaOutput<TSchema>;\nexport function formDataToObject(\n  formData: FormData | Iterable<readonly [string, FormDataEntryValue]>,\n  options: FormDataToObjectOptions & ValidationOptions = {}\n): unknown {\n  const entries =\n    formData instanceof FormData ? formData.entries() : formData;\n\n  return coreEntriesToObject(entries, options);\n}\n\nexport type {\n  EntryInput,\n  InferSchemaOutput,\n  ObjectTree,\n  ParseOptions,\n  SchemaValidator,\n  ValidationOptions\n} from \"@form2js/core\";\n"
  },
  {
    "path": "packages/form-data/test/form-data.test.ts",
    "content": "// @vitest-environment jsdom\n\nimport { describe, expect, it } from \"vitest\";\nimport { entriesToObject, formDataToObject } from \"../src/index\";\n\ndescribe(\"formDataToObject\", () => {\n  it(\"converts FormData entries into structured objects\", () => {\n    const formData = new FormData();\n    formData.append(\"person.name.first\", \"John\");\n    formData.append(\"person.name.last\", \"Doe\");\n    formData.append(\"person.colors[]\", \"red\");\n    formData.append(\"person.colors[]\", \"blue\");\n\n    const result = formDataToObject(formData);\n\n    expect(result).toEqual({\n      person: {\n        name: {\n          first: \"John\",\n          last: \"Doe\"\n        },\n        colors: [\"red\", \"blue\"]\n      }\n    });\n  });\n\n  it(\"supports generic iterable entries\", () => {\n    const result = formDataToObject([\n      [\"user.id\", \"1\"],\n      [\"user.roles[]\", \"admin\"],\n      [\"user.roles[]\", \"editor\"]\n    ]);\n\n    expect(result).toEqual({\n      user: {\n        id: \"1\",\n        roles: [\"admin\", \"editor\"]\n      }\n    });\n  });\n\n  it(\"rejects unsafe path segments by default\", () => {\n    expect(() =>\n      formDataToObject([[\"__proto__.polluted\", \"yes\"]], {\n        skipEmpty: false\n      })\n    ).toThrow(/Unsafe path segment/);\n  });\n\n  it(\"validates and transforms parsed form data with schema.parse\", () => {\n    const schema = {\n      parse(value: unknown) {\n        const record = value as { user?: { id?: string } };\n        return {\n          user: {\n            id: Number(record.user?.id ?? \"0\")\n          }\n        };\n      }\n    };\n\n    const result = formDataToObject([[\"user.id\", \"7\"]], { schema });\n\n    expect(result).toEqual({\n      user: {\n        id: 7\n      }\n    });\n  });\n\n  it(\"propagates schema.parse validation errors\", () => {\n    const schema = {\n      parse() {\n        throw new Error(\"Invalid payload\");\n      }\n    };\n\n    expect(() =>\n      formDataToObject([[\"user.id\", \"7\"]], {\n        schema\n      })\n    ).toThrow(\"Invalid payload\");\n  });\n});\n\ndescribe(\"entriesToObject adapter\", () => {\n  it(\"accepts key/value object entries\", () => {\n    const result = entriesToObject([\n      { key: \"profile.email\", value: \"neo@example.com\" },\n      { key: \"profile.active\", value: true }\n    ]);\n\n    expect(result).toEqual({\n      profile: {\n        email: \"neo@example.com\",\n        active: true\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "packages/form-data/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"]\n  },\n  \"include\": [\"src/**/*.ts\", \"test/**/*.ts\", \"tsup.config.ts\"]\n}\n"
  },
  {
    "path": "packages/form-data/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: {\n    index: \"src/index.ts\"\n  },\n  format: [\"esm\", \"cjs\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  target: \"es2020\"\n});\n"
  },
  {
    "path": "packages/jquery/CHANGELOG.md",
    "content": "# @form2js/jquery\n\n## 3.1.5\n\n### Patch Changes\n\n- Updated dependencies\n  - @form2js/dom@3.2.0\n\n## 3.1.4\n\n### Patch Changes\n\n- Republish the package metadata and package-level README improvements across all public packages after fixing publish configuration.\n- Updated dependencies\n  - @form2js/dom@3.1.4\n\n## 3.1.3\n\n### Patch Changes\n\n- Add npm package metadata (`license`, `bugs`, `keywords`) and publish package-level README files for all public packages.\n- Updated dependencies\n  - @form2js/dom@3.1.3\n\n## 3.1.2\n\n### Patch Changes\n\n- @form2js/dom@3.1.2\n\n## 3.1.1\n\n### Patch Changes\n\n- 77b8543: Hardened parser security by rejecting unsafe key path segments by default and aligned DOM extraction with HTML semantics (disabled fieldsets, no button-like inputs). Added a writer hook, improved multi-select mapping, tightened ESLint, added a version bump script, and aligned packages to 3.1.0 with updated docs, tests, and repo/website links.\n\n      New Features\n          Added allowUnsafePathSegments option across core, dom, form-data, and jQuery plugin to explicitly allow trusted unsafe segments.\n          Added nodeCallback to objectToForm/js2form; returning false skips default assignment for that node.\n          Improved multi-select handling: matches names with [] and bare-name fallbacks without per-option keys.\n          Added bump-version script to sync workspace versions and ranges; bumped packages to 3.1.0.\n          Enabled vitest for examples and added smoke tests; updated tsconfig and lint script.\n\n      Bug Fixes\n          Prevented prototype pollution by blocking \"proto\", \"prototype\", and \"constructor\" path tokens by default.\n          objectToEntries now serializes only own enumerable properties.\n          DOM extraction respects disabled fieldset rules (legend exception) and excludes button-like inputs even when skipEmpty is false.\n          ESLint config now errors on unsafe TypeScript operations (no-unsafe-* rules).\n\n- Updated dependencies [77b8543]\n  - @form2js/dom@3.1.1\n"
  },
  {
    "path": "packages/jquery/README.md",
    "content": "# @form2js/jquery\n\nUse form2js through a jQuery plugin adapter.\n\n## Install\n\n```bash\nnpm install @form2js/jquery jquery\n```\n\n## Minimal usage\n\n```ts\nimport $ from \"jquery\";\nimport { installToObjectPlugin } from \"@form2js/jquery\";\n\ninstallToObjectPlugin($);\nconst data = $(\"#profileForm\").toObject({ mode: \"first\" });\n```\n\nFor guides, playground examples, and API details, see the docs site:\nhttps://maxatwork.github.io/form2js/?variant=jquery\n\nLicense: MIT\n"
  },
  {
    "path": "packages/jquery/package.json",
    "content": "{\n  \"name\": \"@form2js/jquery\",\n  \"version\": \"3.4.0\",\n  \"description\": \"jQuery plugin adapter for form2js.\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/maxatwork/form2js\",\n  \"homepage\": \"https://maxatwork.github.io/form2js/\",\n  \"bugs\": {\n    \"url\": \"https://github.com/maxatwork/form2js/issues\"\n  },\n  \"keywords\": [\n    \"form\",\n    \"forms\",\n    \"serialization\",\n    \"deserialization\",\n    \"form-data\",\n    \"json\",\n    \"jquery\",\n    \"jquery-plugin\"\n  ],\n  \"type\": \"module\",\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"browser\": \"./dist/standalone.js\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\",\n      \"require\": \"./dist/index.cjs\"\n    },\n    \"./standalone\": {\n      \"default\": \"./dist/standalone.js\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"engines\": {\n    \"node\": \">=18\"\n  },\n  \"peerDependencies\": {\n    \"jquery\": \">=3.0.0\"\n  },\n  \"dependencies\": {\n    \"@form2js/dom\": \"3.4.0\"\n  },\n  \"devDependencies\": {\n    \"@types/jquery\": \"^3.5.32\",\n    \"jquery\": \"^3.7.1\"\n  },\n  \"scripts\": {\n    \"build\": \"tsup --config tsup.config.ts\",\n    \"test\": \"vitest run\",\n    \"lint\": \"eslint \\\"src/**/*.ts\\\" \\\"test/**/*.ts\\\"\",\n    \"typecheck\": \"tsc -p tsconfig.json --noEmit\",\n    \"clean\": \"rimraf dist\"\n  }\n}\n"
  },
  {
    "path": "packages/jquery/src/index.ts",
    "content": "import { form2js, type FormToObjectNodeCallback, type RootNodeInput } from \"@form2js/dom\";\n\nexport type ToObjectMode = \"first\" | \"all\" | \"combine\";\n\nexport interface ToObjectOptions {\n  mode?: ToObjectMode;\n  delimiter?: string;\n  skipEmpty?: boolean;\n  allowUnsafePathSegments?: boolean;\n  nodeCallback?: FormToObjectNodeCallback;\n  useIdIfEmptyName?: boolean;\n  getDisabled?: boolean;\n}\n\ninterface JQueryCollectionLike {\n  get(index: number): unknown;\n  each(callback: (this: unknown, index: number, element: unknown) => void): unknown;\n}\n\ninterface JQueryLike {\n  fn: object;\n  extend?: (target: object, ...sources: object[]) => object;\n}\n\nfunction isNodeObject(value: unknown): value is Node {\n  return typeof value === \"object\" && value !== null && \"nodeType\" in value && \"nodeName\" in value;\n}\n\nfunction getFnObject($: JQueryLike): Record<string, unknown> {\n  return $.fn as Record<string, unknown>;\n}\n\ninterface ResolvedToObjectOptions {\n  mode: ToObjectMode;\n  delimiter: string;\n  skipEmpty: boolean;\n  allowUnsafePathSegments: boolean;\n  nodeCallback?: FormToObjectNodeCallback;\n  useIdIfEmptyName: boolean;\n  getDisabled: boolean;\n}\n\nfunction applySettings(options?: ToObjectOptions): ResolvedToObjectOptions {\n  const settings: ResolvedToObjectOptions = {\n    mode: options?.mode ?? \"first\",\n    delimiter: options?.delimiter ?? \".\",\n    skipEmpty: options?.skipEmpty ?? true,\n    allowUnsafePathSegments: options?.allowUnsafePathSegments ?? false,\n    useIdIfEmptyName: options?.useIdIfEmptyName ?? false,\n    getDisabled: options?.getDisabled ?? false\n  };\n\n  if (options?.nodeCallback) {\n    settings.nodeCallback = options.nodeCallback;\n  }\n\n  return settings;\n}\n\nfunction isJQueryLike(value: unknown): value is JQueryLike {\n  return typeof value === \"function\" || (typeof value === \"object\" && value !== null && \"fn\" in value);\n}\n\nexport function installToObjectPlugin($: JQueryLike): void {\n  if (!$.fn) {\n    throw new TypeError(\"jQuery-like object with fn is required\");\n  }\n\n  const fnObject = getFnObject($);\n\n  if (typeof fnObject.toObject === \"function\") {\n    return;\n  }\n\n  fnObject.toObject = function toObject(this: JQueryCollectionLike, options?: ToObjectOptions): unknown {\n    const settings = applySettings(options);\n\n    switch (settings.mode) {\n      case \"all\": {\n        const result: unknown[] = [];\n        this.each(function eachMatched() {\n          result.push(\n            form2js(\n              this as RootNodeInput,\n              settings.delimiter,\n              settings.skipEmpty,\n              settings.nodeCallback,\n              settings.useIdIfEmptyName,\n              settings.getDisabled,\n              settings.allowUnsafePathSegments\n            )\n          );\n        });\n\n        return result;\n      }\n\n      case \"combine\": {\n        const roots: Node[] = [];\n        this.each(function eachMatched() {\n          if (isNodeObject(this)) {\n            roots.push(this);\n          }\n        });\n\n        return form2js(\n          roots,\n          settings.delimiter,\n          settings.skipEmpty,\n          settings.nodeCallback,\n          settings.useIdIfEmptyName,\n          settings.getDisabled,\n          settings.allowUnsafePathSegments\n        );\n      }\n\n      case \"first\":\n      default:\n        return form2js(\n          this.get(0) as RootNodeInput,\n          settings.delimiter,\n          settings.skipEmpty,\n          settings.nodeCallback,\n          settings.useIdIfEmptyName,\n          settings.getDisabled,\n          settings.allowUnsafePathSegments\n        );\n    }\n  };\n}\n\nexport function maybeAutoInstallPlugin(scope: unknown = globalThis): void {\n  if (!isJQueryLike(scope)) {\n    return;\n  }\n\n  const jqueryLike = scope as JQueryLike;\n  if (jqueryLike.fn) {\n    installToObjectPlugin(jqueryLike);\n  }\n}\n"
  },
  {
    "path": "packages/jquery/src/standalone.ts",
    "content": "import { maybeAutoInstallPlugin } from \"./index\";\n\ninterface JQueryGlobal {\n  jQuery?: unknown;\n}\n\nconst scope = globalThis as typeof globalThis & JQueryGlobal;\n\nif (scope.jQuery) {\n  maybeAutoInstallPlugin(scope.jQuery);\n}\n\nexport { installToObjectPlugin, maybeAutoInstallPlugin } from \"./index\";\n"
  },
  {
    "path": "packages/jquery/test/jquery.test.ts",
    "content": "// @vitest-environment jsdom\n\nimport { describe, expect, it } from \"vitest\";\nimport { installToObjectPlugin, maybeAutoInstallPlugin } from \"../src/index\";\n\ntype ToObjectOptions = {\n  mode?: \"first\" | \"all\" | \"combine\";\n  delimiter?: string;\n  skipEmpty?: boolean;\n  allowUnsafePathSegments?: boolean;\n  useIdIfEmptyName?: boolean;\n  getDisabled?: boolean;\n  nodeCallback?: (node: Node) => { name?: string; key?: string; value: unknown } | false;\n};\n\ntype ToObjectResult = unknown;\n\ntype StubCollection = {\n  length: number;\n  get(index: number): Element | undefined;\n  each(callback: (this: Element, index: number, element: Element) => void): StubCollection;\n  toObject?: (options?: ToObjectOptions) => ToObjectResult;\n};\n\ntype SelectorInput = string | Element | Element[];\n\ntype StubJQuery = ((input: SelectorInput) => StubCollection) & {\n  fn: Record<string, unknown>;\n  extend(target: object, ...sources: object[]): object;\n};\n\nfunction createCollection(elements: Element[], fn: Record<string, unknown>): StubCollection {\n  const collection: StubCollection = {\n    length: elements.length,\n    get(index: number): Element | undefined {\n      return elements[index];\n    },\n    each(callback) {\n      for (let index = 0; index < elements.length; index += 1) {\n        const element = elements[index];\n        if (element) {\n          callback.call(element, index, element);\n        }\n      }\n      return this;\n    }\n  };\n\n  Object.setPrototypeOf(collection, fn);\n  return collection;\n}\n\nfunction createStubJQuery(): StubJQuery {\n  const fn: Record<string, unknown> = {};\n\n  const $ = ((input: SelectorInput): StubCollection => {\n    if (typeof input === \"string\") {\n      return createCollection(Array.from(document.querySelectorAll(input)), fn);\n    }\n\n    if (Array.isArray(input)) {\n      return createCollection(input, fn);\n    }\n\n    return createCollection([input], fn);\n  }) as StubJQuery;\n\n  $.fn = fn;\n  $.extend = (target, ...sources) => {\n    Object.assign(target, ...sources);\n    return target;\n  };\n  return $;\n}\n\ndescribe(\"installToObjectPlugin\", () => {\n  it(\"supports legacy first/all/combine modes\", () => {\n    document.body.innerHTML = `\n      <form class=\"part\" id=\"f1\"><input name=\"person.first\" value=\"Neo\" /></form>\n      <form class=\"part\" id=\"f2\"><input name=\"person.last\" value=\"Anderson\" /></form>\n    `;\n\n    const $ = createStubJQuery();\n    installToObjectPlugin($);\n\n    const first = $(\".part\").toObject?.({ mode: \"first\" });\n    const all = $(\".part\").toObject?.({ mode: \"all\" });\n    const combine = $(\".part\").toObject?.({ mode: \"combine\" });\n\n    expect(first).toEqual({ person: { first: \"Neo\" } });\n    expect(all).toEqual([{ person: { first: \"Neo\" } }, { person: { last: \"Anderson\" } }]);\n    expect(combine).toEqual({ person: { first: \"Neo\", last: \"Anderson\" } });\n  });\n\n  it(\"can be auto-installed\", () => {\n    const $ = createStubJQuery();\n    maybeAutoInstallPlugin($);\n\n    expect(typeof $.fn.toObject).toBe(\"function\");\n  });\n\n  it(\"forwards parsing options through the plugin\", () => {\n    document.body.innerHTML = `\n      <form id=\"profile\">\n        <input id=\"person/name\" name=\"\" value=\"Neo\" />\n        <input id=\"person/nickname\" name=\"\" value=\"\" />\n        <input id=\"person/role\" name=\"\" value=\"captain\" disabled />\n      </form>\n    `;\n\n    const $ = createStubJQuery();\n    installToObjectPlugin($);\n\n    const result = $(\"#profile\").toObject?.({\n      delimiter: \"/\",\n      skipEmpty: false,\n      useIdIfEmptyName: true,\n      getDisabled: true\n    });\n\n    expect(result).toEqual({\n      person: {\n        name: \"Neo\",\n        nickname: \"\",\n        role: \"captain\"\n      }\n    });\n  });\n\n  it(\"forwards nodeCallback and allowUnsafePathSegments through the plugin\", () => {\n    document.body.innerHTML = `\n      <form id=\"unsafe\">\n        <div id=\"profile.callback\">hello world</div>\n        <input name=\"__proto__.polluted\" value=\"yes\" />\n      </form>\n    `;\n\n    const $ = createStubJQuery();\n    installToObjectPlugin($);\n\n    const result = $(\"#unsafe\").toObject?.({\n      allowUnsafePathSegments: true,\n      nodeCallback(node) {\n        if (node instanceof HTMLDivElement && node.id === \"profile.callback\") {\n          return { name: node.id, value: node.textContent };\n        }\n\n        return false;\n      }\n    }) as { profile: { callback: string } } & Record<string, unknown>;\n\n    expect(result.profile.callback).toBe(\"hello world\");\n    expect(result.__proto__).toEqual({ polluted: \"yes\" });\n  });\n});\n\ndescribe(\"standalone entry\", () => {\n  it(\"auto-installs plugin from global jQuery\", async () => {\n    const $ = createStubJQuery();\n    const scope = globalThis as typeof globalThis & { jQuery?: StubJQuery };\n\n    scope.jQuery = $;\n    await import(\"../src/standalone\");\n\n    expect(typeof $.fn.toObject).toBe(\"function\");\n  });\n});\n"
  },
  {
    "path": "packages/jquery/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"types\": [\"vitest/globals\", \"jquery\"]\n  },\n  \"include\": [\"src/**/*.ts\", \"test/**/*.ts\", \"tsup.config.ts\"]\n}\n"
  },
  {
    "path": "packages/jquery/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig([\n  {\n    entry: {\n      index: \"src/index.ts\"\n    },\n    format: [\"esm\", \"cjs\"],\n    dts: true,\n    sourcemap: true,\n    clean: true,\n    target: \"es2020\"\n  },\n  {\n    entry: {\n      standalone: \"src/standalone.ts\"\n    },\n    format: [\"iife\"],\n    globalName: \"form2jsJquery\",\n    sourcemap: true,\n    clean: false,\n    target: \"es2020\",\n    external: [\"jquery\"]\n  }\n]);\n"
  },
  {
    "path": "packages/js2form/CHANGELOG.md",
    "content": "# @form2js/js2form\n\n## 3.2.0\n\n### Minor Changes\n\n- Restore Rails-style bracket path compatibility across parsing and form population, align DOM checkbox and radio handling with native browser submission behavior, and add explicit `SKIP_NODE` callback support for DOM extraction.\n\n### Patch Changes\n\n- Updated dependencies\n  - @form2js/core@3.3.0\n\n## 3.1.4\n\n### Patch Changes\n\n- Republish the package metadata and package-level README improvements across all public packages after fixing publish configuration.\n- Updated dependencies\n  - @form2js/core@3.2.2\n\n## 3.1.3\n\n### Patch Changes\n\n- Add npm package metadata (`license`, `bugs`, `keywords`) and publish package-level README files for all public packages.\n- Updated dependencies\n  - @form2js/core@3.2.1\n\n## 3.1.2\n\n### Patch Changes\n\n- Updated dependencies [4d2f923]\n  - @form2js/core@3.2.0\n\n## 3.1.1\n\n### Patch Changes\n\n- 77b8543: Hardened parser security by rejecting unsafe key path segments by default and aligned DOM extraction with HTML semantics (disabled fieldsets, no button-like inputs). Added a writer hook, improved multi-select mapping, tightened ESLint, added a version bump script, and aligned packages to 3.1.0 with updated docs, tests, and repo/website links.\n\n      New Features\n          Added allowUnsafePathSegments option across core, dom, form-data, and jQuery plugin to explicitly allow trusted unsafe segments.\n          Added nodeCallback to objectToForm/js2form; returning false skips default assignment for that node.\n          Improved multi-select handling: matches names with [] and bare-name fallbacks without per-option keys.\n          Added bump-version script to sync workspace versions and ranges; bumped packages to 3.1.0.\n          Enabled vitest for examples and added smoke tests; updated tsconfig and lint script.\n\n      Bug Fixes\n          Prevented prototype pollution by blocking \"proto\", \"prototype\", and \"constructor\" path tokens by default.\n          objectToEntries now serializes only own enumerable properties.\n          DOM extraction respects disabled fieldset rules (legend exception) and excludes button-like inputs even when skipEmpty is false.\n          ESLint config now errors on unsafe TypeScript operations (no-unsafe-* rules).\n\n- Updated dependencies [77b8543]\n  - @form2js/core@3.1.1\n"
  },
  {
    "path": "packages/js2form/README.md",
    "content": "# @form2js/js2form\n\nPopulate form controls from plain object data.\n\n## Install\n\n```bash\nnpm install @form2js/js2form\n```\n\n## Minimal usage\n\n```ts\nimport { objectToForm } from \"@form2js/js2form\";\n\nobjectToForm(document.getElementById(\"profileForm\"), {\n  person: { name: { first: \"Tiffany\", last: \"Aching\" } },\n});\n```\n\nFor guides, playground examples, and API details, see the docs site:\nhttps://maxatwork.github.io/form2js/?variant=js2form\n\nLicense: MIT\n"
  },
  {
    "path": "packages/js2form/package.json",
    "content": "{\n  \"name\": \"@form2js/js2form\",\n  \"version\": \"3.4.0\",\n  \"description\": \"Populate DOM forms from plain objects.\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/maxatwork/form2js\",\n  \"homepage\": \"https://maxatwork.github.io/form2js/\",\n  \"bugs\": {\n    \"url\": \"https://github.com/maxatwork/form2js/issues\"\n  },\n  \"keywords\": [\n    \"form\",\n    \"forms\",\n    \"serialization\",\n    \"deserialization\",\n    \"form-data\",\n    \"json\",\n    \"object-to-form\",\n    \"populate-form\"\n  ],\n  \"type\": \"module\",\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\",\n      \"require\": \"./dist/index.cjs\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"engines\": {\n    \"node\": \">=18\"\n  },\n  \"dependencies\": {\n    \"@form2js/core\": \"3.4.0\"\n  },\n  \"scripts\": {\n    \"build\": \"tsup --config tsup.config.ts\",\n    \"test\": \"vitest run\",\n    \"lint\": \"eslint \\\"src/**/*.ts\\\" \\\"test/**/*.ts\\\"\",\n    \"typecheck\": \"tsc -p tsconfig.json --noEmit\",\n    \"clean\": \"rimraf dist\"\n  }\n}\n"
  },
  {
    "path": "packages/js2form/src/index.ts",
    "content": "import { objectToEntries, type Entry } from \"@form2js/core\";\n\nconst ARRAY_ITEM_REGEXP = /\\[[0-9]+?\\]$/;\nconst LAST_INDEXED_ARRAY_REGEXP = /(.*)(\\[)([0-9]*)(\\])$/;\nconst ARRAY_OF_ARRAYS_REGEXP = /\\[([0-9]+)\\]\\[([0-9]+)\\]/g;\n\nexport type RootNodeInput = string | Node | null | undefined;\nexport type ObjectToFormNodeCallback = ((node: Node) => unknown) | null | undefined;\n\nexport interface ObjectToFormOptions {\n  delimiter?: string;\n  nodeCallback?: ObjectToFormNodeCallback;\n  useIdIfEmptyName?: boolean;\n  shouldClean?: boolean;\n  document?: Document;\n}\n\nexport type SupportedField = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;\nexport type SupportedFieldCollection = SupportedField | SupportedField[];\nexport type FieldMap = Record<string, SupportedFieldCollection>;\n\ntype ArrayIndexesMap = Record<\n  string,\n  {\n    lastIndex: number;\n    indexes: Record<string, number>;\n    emptyIndexGroup?: {\n      index: number;\n      seenSuffixes: Set<string>;\n    };\n  }\n>;\n\ninterface BracketMatch {\n  content: string;\n  index: number;\n  text: string;\n}\n\nfunction findBracketMatches(input: string): BracketMatch[] {\n  const matches: BracketMatch[] = [];\n  let cursor = 0;\n\n  while (cursor < input.length) {\n    const startIndex = input.indexOf(\"[\", cursor);\n    if (startIndex === -1) {\n      break;\n    }\n\n    const endIndex = input.indexOf(\"]\", startIndex + 1);\n    if (endIndex === -1) {\n      break;\n    }\n\n    matches.push({\n      content: input.slice(startIndex + 1, endIndex),\n      index: startIndex,\n      text: input.slice(startIndex, endIndex + 1)\n    });\n    cursor = endIndex + 1;\n  }\n\n  return matches;\n}\n\nfunction isNodeObject(value: unknown): value is Node {\n  return typeof value === \"object\" && value !== null && \"nodeType\" in value && \"nodeName\" in value;\n}\n\nfunction isElementNode(node: Node): node is Element {\n  return node.nodeType === 1;\n}\n\nfunction nodeNameIs(node: Node, expected: string): boolean {\n  return node.nodeName.toUpperCase() === expected;\n}\n\nfunction isInputNode(node: Node): node is HTMLInputElement {\n  return nodeNameIs(node, \"INPUT\");\n}\n\nfunction isTextareaNode(node: Node): node is HTMLTextAreaElement {\n  return nodeNameIs(node, \"TEXTAREA\");\n}\n\nfunction isSelectNode(node: Node): node is HTMLSelectElement {\n  return nodeNameIs(node, \"SELECT\");\n}\n\nfunction getDocumentFromRoot(rootNode: RootNodeInput, fallback?: Document): Document {\n  if (fallback) {\n    return fallback;\n  }\n\n  if (typeof document !== \"undefined\") {\n    return document;\n  }\n\n  if (isNodeObject(rootNode) && rootNode.ownerDocument) {\n    return rootNode.ownerDocument;\n  }\n\n  throw new Error(\"No document available. Provide options.document when running outside a browser.\");\n}\n\nfunction resolveRootNode(rootNode: RootNodeInput, options: ObjectToFormOptions): Node | null {\n  if (!rootNode) {\n    return null;\n  }\n\n  if (typeof rootNode !== \"string\") {\n    return isNodeObject(rootNode) ? rootNode : null;\n  }\n\n  const doc = getDocumentFromRoot(rootNode, options.document);\n  return doc.getElementById(rootNode);\n}\n\nfunction isSupportedField(node: Node): node is SupportedField {\n  return isInputNode(node) || isTextareaNode(node) || isSelectNode(node);\n}\n\nfunction shouldSkipNodeAssignment(node: Node, nodeCallback: ObjectToFormNodeCallback): boolean {\n  return Boolean(nodeCallback && nodeCallback(node) === false);\n}\n\nfunction normalizeName(name: string, delimiter: string, arrayIndexes: ArrayIndexesMap): string {\n  let nameToNormalize = name;\n  const rawChunks = name.split(delimiter);\n  const normalizedRawChunks: string[] = [];\n\n  for (const rawChunk of rawChunks) {\n    const bracketMatches = findBracketMatches(rawChunk);\n    if (bracketMatches.length === 0) {\n      normalizedRawChunks.push(rawChunk);\n      continue;\n    }\n\n    let currentChunk = \"\";\n    let cursor = 0;\n\n    for (const match of bracketMatches) {\n      const literalText = rawChunk.slice(cursor, match.index ?? cursor);\n      if (literalText !== \"\") {\n        currentChunk += literalText;\n      }\n\n      const bracketContent = match.content;\n      const isArraySegment = bracketContent === \"\" || /^\\d+$/.test(bracketContent);\n\n      if (isArraySegment) {\n        if (currentChunk !== \"\" && currentChunk.endsWith(\"]\")) {\n          normalizedRawChunks.push(currentChunk);\n          currentChunk = \"\";\n        }\n\n        currentChunk = `${currentChunk}[${bracketContent}]`;\n      } else {\n        if (currentChunk !== \"\") {\n          normalizedRawChunks.push(currentChunk);\n        }\n\n        currentChunk = bracketContent;\n      }\n\n      cursor = match.index + match.text.length;\n    }\n\n    const trailingText = rawChunk.slice(cursor);\n    if (trailingText !== \"\") {\n      currentChunk += trailingText;\n    }\n\n    if (currentChunk !== \"\") {\n      normalizedRawChunks.push(currentChunk);\n    }\n  }\n\n  if (normalizedRawChunks.length > 0) {\n    nameToNormalize = normalizedRawChunks.join(delimiter);\n  }\n\n  const normalizedNameChunks: string[] = [];\n  const chunks = nameToNormalize.replace(ARRAY_OF_ARRAYS_REGEXP, \"[$1].[$2]\").split(delimiter);\n\n  for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {\n    const currentChunk = chunks[chunkIndex] ?? \"\";\n    normalizedNameChunks.push(currentChunk);\n\n    const nameMatches = currentChunk.match(LAST_INDEXED_ARRAY_REGEXP);\n    if (!nameMatches) {\n      continue;\n    }\n\n    let currentNormalizedName = normalizedNameChunks.join(delimiter);\n    const currentIndex = currentNormalizedName.replace(LAST_INDEXED_ARRAY_REGEXP, \"$3\");\n    currentNormalizedName = currentNormalizedName.replace(LAST_INDEXED_ARRAY_REGEXP, \"$1\");\n\n    const arrayIndexInfo = (arrayIndexes[currentNormalizedName] ??= {\n      lastIndex: -1,\n      indexes: {}\n    });\n\n    if (currentIndex === \"\") {\n      const remainingPath = chunks.slice(chunkIndex + 1).join(delimiter);\n      const currentGroup = arrayIndexInfo.emptyIndexGroup;\n\n      if (\n        !currentGroup ||\n        remainingPath === \"\" ||\n        currentGroup.seenSuffixes.has(remainingPath)\n      ) {\n        arrayIndexInfo.lastIndex += 1;\n        arrayIndexInfo.emptyIndexGroup = {\n          index: arrayIndexInfo.lastIndex,\n          seenSuffixes: new Set(remainingPath === \"\" ? [] : [remainingPath])\n        };\n      } else {\n        currentGroup.seenSuffixes.add(remainingPath);\n      }\n    } else if (arrayIndexInfo.indexes[currentIndex] === undefined) {\n      arrayIndexInfo.lastIndex += 1;\n      arrayIndexInfo.indexes[currentIndex] = arrayIndexInfo.lastIndex;\n    }\n\n    const newIndex =\n      currentIndex === \"\"\n        ? (arrayIndexInfo.emptyIndexGroup?.index ?? 0)\n        : arrayIndexInfo.indexes[currentIndex];\n    normalizedNameChunks[normalizedNameChunks.length - 1] = currentChunk.replace(\n      LAST_INDEXED_ARRAY_REGEXP,\n      `$1$2${newIndex}$4`\n    );\n  }\n\n  return normalizedNameChunks.join(delimiter).replace(\"].[\", \"][\");\n}\n\nfunction mergeField(result: FieldMap, key: string, value: SupportedFieldCollection): void {\n  const existing = result[key];\n\n  if (!existing) {\n    result[key] = value;\n    return;\n  }\n\n  if (Array.isArray(existing)) {\n    if (Array.isArray(value)) {\n      existing.push(...value);\n    } else {\n      existing.push(value);\n    }\n    return;\n  }\n\n  if (Array.isArray(value)) {\n    result[key] = [existing, ...value];\n    return;\n  }\n\n  result[key] = value;\n}\n\nfunction getFields(\n  rootNode: Node,\n  useIdIfEmptyName: boolean,\n  delimiter: string,\n  arrayIndexes: ArrayIndexesMap,\n  shouldClean: boolean\n): FieldMap {\n  const result: FieldMap = {};\n  let currentNode: ChildNode | null = rootNode.firstChild;\n\n  while (currentNode) {\n    let name = \"\";\n\n    if (isElementNode(currentNode)) {\n      const namedNode = currentNode as Element & { name?: string; id?: string };\n      if (namedNode.name && namedNode.name !== \"\") {\n        name = namedNode.name;\n      } else if (useIdIfEmptyName && namedNode.id && namedNode.id !== \"\") {\n        name = namedNode.id;\n      }\n    }\n\n    if (name === \"\") {\n      const subFields = getFields(currentNode, useIdIfEmptyName, delimiter, arrayIndexes, shouldClean);\n      for (const [subFieldName, subFieldValue] of Object.entries(subFields)) {\n        mergeField(result, subFieldName, subFieldValue);\n      }\n    } else if (isSelectNode(currentNode)) {\n      const options = currentNode.getElementsByTagName(\"option\");\n\n      for (let optionIndex = 0; optionIndex < options.length; optionIndex += 1) {\n        if (shouldClean) {\n          const option = options[optionIndex];\n          if (option) {\n            option.selected = false;\n          }\n        }\n      }\n\n      const normalizedName = normalizeName(name, delimiter, arrayIndexes);\n      result[normalizedName] = currentNode;\n\n      const arraySyntaxName = normalizedName.replace(ARRAY_ITEM_REGEXP, \"[]\");\n      if (arraySyntaxName !== normalizedName) {\n        result[arraySyntaxName] = currentNode;\n      }\n\n      const bareArrayName = normalizedName.replace(ARRAY_ITEM_REGEXP, \"\");\n      if (bareArrayName !== normalizedName) {\n        result[bareArrayName] = currentNode;\n      }\n    } else if (isInputNode(currentNode) && /CHECKBOX|RADIO/i.test(currentNode.type)) {\n      if (shouldClean) {\n        currentNode.checked = false;\n      }\n\n      const normalizedName = normalizeName(name, delimiter, arrayIndexes).replace(ARRAY_ITEM_REGEXP, \"[]\");\n\n      if (!result[normalizedName]) {\n        result[normalizedName] = [];\n      }\n\n      const existing = result[normalizedName];\n      if (Array.isArray(existing)) {\n        existing.push(currentNode);\n      } else {\n        result[normalizedName] = [existing, currentNode];\n      }\n    } else if (isSupportedField(currentNode)) {\n      if (shouldClean) {\n        currentNode.value = \"\";\n      }\n\n      const normalizedName = normalizeName(name, delimiter, arrayIndexes);\n      result[normalizedName] = currentNode;\n    }\n\n    currentNode = currentNode.nextSibling;\n  }\n\n  return result;\n}\n\nfunction setValue(\n  field: SupportedFieldCollection,\n  value: unknown,\n  nodeCallback: ObjectToFormNodeCallback\n): void {\n  if (Array.isArray(field)) {\n    for (const inputNode of field) {\n      if (shouldSkipNodeAssignment(inputNode, nodeCallback)) {\n        continue;\n      }\n\n      if (isInputNode(inputNode) && (inputNode.value === String(value) || value === true)) {\n        inputNode.checked = true;\n      }\n    }\n    return;\n  }\n\n  if (shouldSkipNodeAssignment(field, nodeCallback)) {\n    return;\n  }\n\n  if (isInputNode(field) || isTextareaNode(field)) {\n    field.value = String(value ?? \"\");\n    return;\n  }\n\n  if (isSelectNode(field)) {\n    const options = field.getElementsByTagName(\"option\");\n    for (let index = 0; index < options.length; index += 1) {\n      const option = options[index];\n      if (!option) {\n        continue;\n      }\n\n      if (option.value === String(value)) {\n        option.selected = true;\n        if (field.multiple) {\n          break;\n        }\n      } else if (!field.multiple) {\n        option.selected = false;\n      }\n    }\n  }\n}\n\nfunction toPathEntries(data: unknown): Entry[] {\n  return objectToEntries(data).map((entry) => ({\n    key: entry.key,\n    value: entry.value\n  }));\n}\n\nexport function flattenDataForForm(data: unknown): Entry[] {\n  return toPathEntries(data);\n}\n\nexport function mapFieldsByName(\n  rootNode: RootNodeInput,\n  options: Pick<ObjectToFormOptions, \"delimiter\" | \"useIdIfEmptyName\" | \"shouldClean\" | \"document\"> = {}\n): FieldMap {\n  const resolvedRoot = resolveRootNode(rootNode, options);\n  if (!resolvedRoot) {\n    return {};\n  }\n\n  return getFields(\n    resolvedRoot,\n    options.useIdIfEmptyName ?? false,\n    options.delimiter ?? \".\",\n    {},\n    options.shouldClean ?? true\n  );\n}\n\nexport function objectToForm(rootNode: RootNodeInput, data: unknown, options: ObjectToFormOptions = {}): void {\n  const resolvedRoot = resolveRootNode(rootNode, options);\n  if (!resolvedRoot) {\n    return;\n  }\n\n  const delimiter = options.delimiter ?? \".\";\n  const nodeCallback = options.nodeCallback ?? null;\n  const fieldValues = toPathEntries(data);\n  const formFieldsByName = getFields(\n    resolvedRoot,\n    options.useIdIfEmptyName ?? false,\n    delimiter,\n    {},\n    options.shouldClean ?? true\n  );\n\n  for (const fieldValue of fieldValues) {\n    const fieldName = fieldValue.key;\n    const value = fieldValue.value;\n\n    if (formFieldsByName[fieldName]) {\n      setValue(formFieldsByName[fieldName], value, nodeCallback);\n      continue;\n    }\n\n    const arraySyntaxName = fieldName.replace(ARRAY_ITEM_REGEXP, \"[]\");\n    if (formFieldsByName[arraySyntaxName]) {\n      setValue(formFieldsByName[arraySyntaxName], value, nodeCallback);\n      continue;\n    }\n\n    const bareArrayName = fieldName.replace(ARRAY_ITEM_REGEXP, \"\");\n    if (formFieldsByName[bareArrayName]) {\n      setValue(formFieldsByName[bareArrayName], value, nodeCallback);\n    }\n  }\n}\n\nexport function js2form(\n  rootNode: RootNodeInput,\n  data: unknown,\n  delimiter = \".\",\n  nodeCallback: ObjectToFormNodeCallback = null,\n  useIdIfEmptyName = false\n): void {\n  objectToForm(rootNode, data, {\n    delimiter,\n    nodeCallback,\n    useIdIfEmptyName\n  });\n}\n\nexport { normalizeName };\nexport type { Entry } from \"@form2js/core\";\n"
  },
  {
    "path": "packages/js2form/test/js2form.test.ts",
    "content": "// @vitest-environment jsdom\n\nimport { describe, expect, it } from \"vitest\";\nimport {\n  flattenDataForForm,\n  js2form,\n  mapFieldsByName,\n  objectToForm,\n} from \"../src/index\";\n\ndescribe(\"objectToForm\", () => {\n  it(\"populates text, checkbox, radio and select fields\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input name=\"person.name.first\" />\n        <input type=\"checkbox\" name=\"person.favFood[]\" value=\"steak\" />\n        <input type=\"checkbox\" name=\"person.favFood[]\" value=\"pizza\" />\n        <input type=\"radio\" name=\"person.gender\" value=\"male\" />\n        <input type=\"radio\" name=\"person.gender\" value=\"female\" />\n        <select name=\"person.city\">\n          <option value=\"msk\">Moscow</option>\n          <option value=\"paris\">Paris</option>\n        </select>\n      </form>\n    `;\n\n    objectToForm(\"testForm\", {\n      person: {\n        name: { first: \"Jane\" },\n        favFood: [\"steak\"],\n        gender: \"female\",\n        city: \"paris\",\n      },\n    });\n\n    const firstName = document.querySelector(\n      \"input[name='person.name.first']\"\n    ) as HTMLInputElement;\n    const steak = document.querySelector(\n      \"input[name='person.favFood[]'][value='steak']\"\n    ) as HTMLInputElement;\n    const pizza = document.querySelector(\n      \"input[name='person.favFood[]'][value='pizza']\"\n    ) as HTMLInputElement;\n    const female = document.querySelector(\n      \"input[name='person.gender'][value='female']\"\n    ) as HTMLInputElement;\n    const city = document.querySelector(\n      \"select[name='person.city']\"\n    ) as HTMLSelectElement;\n\n    expect(firstName.value).toBe(\"Jane\");\n    expect(steak.checked).toBe(true);\n    expect(pizza.checked).toBe(false);\n    expect(female.checked).toBe(true);\n    expect(city.value).toBe(\"paris\");\n  });\n\n  it(\"keeps compatibility wrapper\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input name=\"foo.name.first\" />\n      </form>\n    `;\n\n    js2form(\"testForm\", { foo: { name: { first: \"Neo\" } } });\n\n    const input = document.querySelector(\n      \"input[name='foo.name.first']\"\n    ) as HTMLInputElement;\n    expect(input.value).toBe(\"Neo\");\n  });\n\n  it(\"supports nodeCallback by skipping default assignment when callback returns false\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input name=\"person.name.first\" />\n      </form>\n    `;\n\n    objectToForm(\n      \"testForm\",\n      { person: { name: { first: \"Jane\" } } },\n      {\n        nodeCallback(node) {\n          if (node instanceof HTMLInputElement && node.name === \"person.name.first\") {\n            node.value = \"from-callback\";\n            return false;\n          }\n\n          return null;\n        }\n      }\n    );\n\n    const input = document.querySelector(\n      \"input[name='person.name.first']\"\n    ) as HTMLInputElement;\n    expect(input.value).toBe(\"from-callback\");\n  });\n\n  it(\"populates multi-select values from arrays\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <select name=\"person.colors[]\" multiple>\n          <option value=\"red\">red</option>\n          <option value=\"blue\">blue</option>\n          <option value=\"green\">green</option>\n        </select>\n      </form>\n    `;\n\n    objectToForm(\"testForm\", {\n      person: {\n        colors: [\"red\", \"blue\"]\n      }\n    });\n\n    const red = document.querySelector(\"option[value='red']\") as HTMLOptionElement;\n    const blue = document.querySelector(\"option[value='blue']\") as HTMLOptionElement;\n    const green = document.querySelector(\"option[value='green']\") as HTMLOptionElement;\n\n    expect(red.selected).toBe(true);\n    expect(blue.selected).toBe(true);\n    expect(green.selected).toBe(false);\n  });\n\n  it(\"clears existing values by default and preserves them when shouldClean is false\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input name=\"person.name.first\" value=\"Sam\" />\n        <input name=\"person.name.last\" value=\"\" />\n        <input type=\"checkbox\" name=\"person.favFood[]\" value=\"steak\" checked />\n        <input type=\"checkbox\" name=\"person.favFood[]\" value=\"pizza\" />\n      </form>\n    `;\n\n    objectToForm(\"testForm\", {\n      person: {\n        name: { last: \"Vimes\" },\n        favFood: [\"pizza\"]\n      }\n    });\n\n    const firstAfterClean = document.querySelector(\"input[name='person.name.first']\") as HTMLInputElement;\n    const lastAfterClean = document.querySelector(\"input[name='person.name.last']\") as HTMLInputElement;\n    const steakAfterClean = document.querySelector(\n      \"input[name='person.favFood[]'][value='steak']\"\n    ) as HTMLInputElement;\n    const pizzaAfterClean = document.querySelector(\n      \"input[name='person.favFood[]'][value='pizza']\"\n    ) as HTMLInputElement;\n\n    expect(firstAfterClean.value).toBe(\"\");\n    expect(lastAfterClean.value).toBe(\"Vimes\");\n    expect(steakAfterClean.checked).toBe(false);\n    expect(pizzaAfterClean.checked).toBe(true);\n\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input name=\"person.name.first\" value=\"Sam\" />\n        <input name=\"person.name.last\" value=\"\" />\n        <input type=\"checkbox\" name=\"person.favFood[]\" value=\"steak\" checked />\n        <input type=\"checkbox\" name=\"person.favFood[]\" value=\"pizza\" />\n      </form>\n    `;\n\n    objectToForm(\n      \"testForm\",\n      {\n        person: {\n          name: { last: \"Vimes\" },\n          favFood: [\"pizza\"]\n        }\n      },\n      { shouldClean: false }\n    );\n\n    const firstPreserved = document.querySelector(\"input[name='person.name.first']\") as HTMLInputElement;\n    const lastPreserved = document.querySelector(\"input[name='person.name.last']\") as HTMLInputElement;\n    const steakPreserved = document.querySelector(\n      \"input[name='person.favFood[]'][value='steak']\"\n    ) as HTMLInputElement;\n    const pizzaPreserved = document.querySelector(\n      \"input[name='person.favFood[]'][value='pizza']\"\n    ) as HTMLInputElement;\n\n    expect(firstPreserved.value).toBe(\"Sam\");\n    expect(lastPreserved.value).toBe(\"Vimes\");\n    expect(steakPreserved.checked).toBe(true);\n    expect(pizzaPreserved.checked).toBe(true);\n  });\n\n  it(\"populates nested table fields (issue #23 regression)\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <table>\n          <tbody>\n            <tr>\n              <th>Phone</th>\n              <td><input type=\"text\" id=\"phone\" name=\"phone\" /></td>\n            </tr>\n            <tr>\n              <th>Address</th>\n              <td>\n                <table id=\"addressInfo\" class=\"condensed-table\">\n                  <tr>\n                    <th>Street 1</th>\n                    <td><input type=\"text\" id=\"address.street1\" name=\"address.street1\" /></td>\n                  </tr>\n                  <tr>\n                    <th>Street 2</th>\n                    <td><input type=\"text\" id=\"address.street2\" name=\"address.street2\" /></td>\n                  </tr>\n                  <tr>\n                    <th>City</th>\n                    <td><input type=\"text\" id=\"address.city\" name=\"address.city\" /></td>\n                  </tr>\n                  <tr>\n                    <th>State</th>\n                    <td><input type=\"text\" id=\"address.state\" name=\"address.state\" /></td>\n                  </tr>\n                  <tr>\n                    <th>Zip</th>\n                    <td><input type=\"text\" id=\"address.zip\" name=\"address.zip\" /></td>\n                  </tr>\n                  <tr>\n                    <th>Country</th>\n                    <td><input type=\"text\" id=\"address.country\" name=\"address.country\" /></td>\n                  </tr>\n                </table>\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </form>\n    `;\n\n    objectToForm(\"testForm\", {\n      phone: \"555-0123\",\n      address: {\n        street1: \"123 Test St\",\n        street2: \"Suite 5\",\n        city: \"Portland\",\n        state: \"OR\",\n        zip: \"97201\",\n        country: \"US\"\n      }\n    });\n\n    expect((document.querySelector(\"input[name='phone']\") as HTMLInputElement).value).toBe(\"555-0123\");\n    expect((document.querySelector(\"input[name='address.street1']\") as HTMLInputElement).value).toBe(\n      \"123 Test St\"\n    );\n    expect((document.querySelector(\"input[name='address.street2']\") as HTMLInputElement).value).toBe(\"Suite 5\");\n    expect((document.querySelector(\"input[name='address.city']\") as HTMLInputElement).value).toBe(\"Portland\");\n    expect((document.querySelector(\"input[name='address.state']\") as HTMLInputElement).value).toBe(\"OR\");\n    expect((document.querySelector(\"input[name='address.zip']\") as HTMLInputElement).value).toBe(\"97201\");\n    expect((document.querySelector(\"input[name='address.country']\") as HTMLInputElement).value).toBe(\"US\");\n  });\n\n  it(\"supports id fallback and explicit document resolution\", () => {\n    const customDocument = document.implementation.createHTMLDocument(\"custom\");\n    customDocument.body.innerHTML = `\n      <form id=\"testForm\">\n        <input id=\"person.name.first\" name=\"\" />\n      </form>\n    `;\n\n    objectToForm(\n      \"testForm\",\n      {\n        person: {\n          name: {\n            first: \"Neo\"\n          }\n        }\n      },\n      {\n        useIdIfEmptyName: true,\n        document: customDocument\n      }\n    );\n\n    const input = customDocument.getElementById(\"person.name.first\") as HTMLInputElement;\n    expect(input.value).toBe(\"Neo\");\n  });\n\n  it(\"handles literal name and id field names\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input name=\"name\" />\n        <input name=\"id\" />\n      </form>\n    `;\n\n    objectToForm(\"testForm\", {\n      name: \"Form Name\",\n      id: \"form-id\"\n    });\n\n    expect((document.querySelector(\"input[name='name']\") as HTMLInputElement).value).toBe(\"Form Name\");\n    expect((document.querySelector(\"input[name='id']\") as HTMLInputElement).value).toBe(\"form-id\");\n  });\n\n  it(\"writes empty strings for null values instead of the literal null string\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input name=\"person.nickname\" value=\"existing\" />\n      </form>\n    `;\n\n    objectToForm(\"testForm\", {\n      person: {\n        nickname: null\n      }\n    });\n\n    expect((document.querySelector(\"input[name='person.nickname']\") as HTMLInputElement).value).toBe(\"\");\n  });\n\n  it(\"populates rails-style field names\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input name=\"a[b][c]\" />\n      </form>\n    `;\n\n    objectToForm(\"testForm\", {\n      a: {\n        b: {\n          c: \"value\"\n        }\n      }\n    });\n\n    const input = document.querySelector(\"input[name='a[b][c]']\") as HTMLInputElement;\n    expect(input.value).toBe(\"value\");\n  });\n\n  it(\"keeps sibling rails array object fields on the same synthetic index\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input name=\"items[][title]\" />\n        <input name=\"items[][description]\" />\n      </form>\n    `;\n\n    objectToForm(\"testForm\", {\n      items: [\n        {\n          title: \"First\",\n          description: \"Desc\"\n        }\n      ]\n    });\n\n    expect((document.querySelector(\"input[name='items[][title]']\") as HTMLInputElement).value).toBe(\"First\");\n    expect((document.querySelector(\"input[name='items[][description]']\") as HTMLInputElement).value).toBe(\n      \"Desc\"\n    );\n  });\n});\n\ndescribe(\"low-level helpers\", () => {\n  it(\"maps fields by normalized names\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input type=\"checkbox\" name=\"items[5].name\" value=\"a\" />\n        <input type=\"checkbox\" name=\"items[1].name\" value=\"b\" />\n      </form>\n    `;\n\n    const fields = mapFieldsByName(\"testForm\", { shouldClean: false });\n    expect(Object.keys(fields)).toContain(\"items[0].name\");\n    expect(Object.keys(fields)).toContain(\"items[1].name\");\n  });\n\n  it(\"flattens object data to path entries\", () => {\n    const entries = flattenDataForForm({ foo: { bar: [\"a\", \"b\"] } });\n\n    expect(entries).toContainEqual({ key: \"foo.bar[0]\", value: \"a\" });\n    expect(entries).toContainEqual({ key: \"foo.bar[1]\", value: \"b\" });\n  });\n\n  it(\"maps multi-select names without creating one key per option\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <select name=\"person.colors[]\" multiple>\n          <option value=\"red\">red</option>\n          <option value=\"blue\">blue</option>\n          <option value=\"green\">green</option>\n        </select>\n      </form>\n    `;\n\n    const fields = mapFieldsByName(\"testForm\", { shouldClean: false });\n    expect(Object.keys(fields)).toContain(\"person.colors[]\");\n    expect(Object.keys(fields)).not.toContain(\"person.colors[1]\");\n    expect(Object.keys(fields)).not.toContain(\"person.colors[2]\");\n  });\n\n  it(\"preserves literal suffixes around indexed bracket groups\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input name=\"items[5]_id\" value=\"alpha\" />\n      </form>\n    `;\n\n    const fields = mapFieldsByName(\"testForm\", { shouldClean: false });\n    expect(Object.keys(fields)).toContain(\"items[5]_id\");\n  });\n\n  it(\"preserves unmatched bracket-heavy field names\", () => {\n    document.body.innerHTML = `\n      <form id=\"testForm\">\n        <input name=\"filters[[[[value\" value=\"alpha\" />\n      </form>\n    `;\n\n    const fields = mapFieldsByName(\"testForm\", { shouldClean: false });\n    expect(Object.keys(fields)).toContain(\"filters[[[[value\");\n  });\n});\n"
  },
  {
    "path": "packages/js2form/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"]\n  },\n  \"include\": [\"src/**/*.ts\", \"test/**/*.ts\", \"tsup.config.ts\"]\n}\n"
  },
  {
    "path": "packages/js2form/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: {\n    index: \"src/index.ts\"\n  },\n  format: [\"esm\", \"cjs\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  target: \"es2020\"\n});\n"
  },
  {
    "path": "packages/react/CHANGELOG.md",
    "content": "# @form2js/react\n\n## 3.2.3\n\n### Patch Changes\n\n- @form2js/form-data@3.2.3\n\n## 3.2.2\n\n### Patch Changes\n\n- Republish the package metadata and package-level README improvements across all public packages after fixing publish configuration.\n- Updated dependencies\n  - @form2js/form-data@3.2.2\n\n## 3.2.1\n\n### Patch Changes\n\n- Add npm package metadata (`license`, `bugs`, `keywords`) and publish package-level README files for all public packages.\n- Updated dependencies\n  - @form2js/form-data@3.2.1\n\n## 3.2.0\n\n### Minor Changes\n\n- 4d2f923: Added optional schema validation support (structural `parse`) to parsing APIs in core and form-data, plus a new `@form2js/react` package with `useForm2js` for async submit state management in React forms.\n\n### Patch Changes\n\n- Updated dependencies [4d2f923]\n  - @form2js/form-data@3.2.0\n\n## 3.1.1\n\n### Patch Changes\n\n- Initial package scaffold.\n"
  },
  {
    "path": "packages/react/README.md",
    "content": "# @form2js/react\n\nHandle form submission with a small React hook built on form2js parsing.\n\n## Install\n\n```bash\nnpm install @form2js/react react\n```\n\n## Minimal usage\n\n```tsx\nimport { useForm2js } from \"@form2js/react\";\n\nexport function ProfileForm(): React.JSX.Element {\n  const { onSubmit, isSubmitting } = useForm2js(async (data) => {\n    await saveProfile(data);\n  });\n\n  return (\n    <form\n      onSubmit={(event) => {\n        void onSubmit(event);\n      }}\n    >\n      <input name=\"person.name.first\" defaultValue=\"Sam\" />\n      <button disabled={isSubmitting}>Save</button>\n    </form>\n  );\n}\n```\n\nFor guides, playground examples, and API details, see the docs site:\nhttps://maxatwork.github.io/form2js/?variant=react\n\nLicense: MIT\n"
  },
  {
    "path": "packages/react/package.json",
    "content": "{\n  \"name\": \"@form2js/react\",\n  \"version\": \"3.4.0\",\n  \"description\": \"React hook adapter for form2js.\",\n  \"license\": \"MIT\",\n  \"repository\": \"https://github.com/maxatwork/form2js\",\n  \"homepage\": \"https://maxatwork.github.io/form2js/\",\n  \"bugs\": {\n    \"url\": \"https://github.com/maxatwork/form2js/issues\"\n  },\n  \"keywords\": [\n    \"form\",\n    \"forms\",\n    \"serialization\",\n    \"deserialization\",\n    \"form-data\",\n    \"json\",\n    \"react\",\n    \"hook\",\n    \"submit\"\n  ],\n  \"type\": \"module\",\n  \"sideEffects\": false,\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.js\",\n  \"types\": \"./dist/index.d.ts\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"import\": \"./dist/index.js\",\n      \"require\": \"./dist/index.cjs\"\n    }\n  },\n  \"files\": [\n    \"dist\"\n  ],\n  \"engines\": {\n    \"node\": \">=18\"\n  },\n  \"peerDependencies\": {\n    \"react\": \">=18.0.0\"\n  },\n  \"dependencies\": {\n    \"@form2js/form-data\": \"3.4.0\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"^19.1.12\",\n    \"@types/react-dom\": \"^19.1.9\",\n    \"react\": \"^19.1.1\",\n    \"react-dom\": \"^19.1.1\"\n  },\n  \"scripts\": {\n    \"build\": \"tsup --config tsup.config.ts\",\n    \"test\": \"vitest run\",\n    \"lint\": \"eslint \\\"src/**/*.ts\\\" \\\"test/**/*.ts\\\"\",\n    \"typecheck\": \"tsc -p tsconfig.json --noEmit\",\n    \"clean\": \"rimraf dist\"\n  }\n}\n"
  },
  {
    "path": "packages/react/src/index.ts",
    "content": "import { formDataToObject, type InferSchemaOutput, type ObjectTree, type ParseOptions, type SchemaValidator } from \"@form2js/form-data\";\nimport { useCallback, useRef, useState, type SyntheticEvent } from \"react\";\n\nexport type UseForm2jsData<TSchema extends SchemaValidator | undefined> =\n  TSchema extends SchemaValidator ? InferSchemaOutput<TSchema> : ObjectTree;\n\nexport type UseForm2jsSubmit<TSchema extends SchemaValidator | undefined = undefined> = (\n  data: UseForm2jsData<TSchema>\n) => Promise<void> | void;\n\nexport interface UseForm2jsOptions<TSchema extends SchemaValidator | undefined = undefined>\n  extends ParseOptions {\n  schema?: TSchema;\n}\n\nexport interface UseForm2jsResult {\n  onSubmit: (event: SyntheticEvent<HTMLFormElement, SubmitEvent>) => Promise<void>;\n  isSubmitting: boolean;\n  isError: boolean;\n  error: unknown;\n  isSuccess: boolean;\n  reset: () => void;\n}\n\nfunction buildParseOptions(options: ParseOptions): ParseOptions {\n  const parseOptions: ParseOptions = {};\n\n  if (options.delimiter !== undefined) {\n    parseOptions.delimiter = options.delimiter;\n  }\n\n  if (options.skipEmpty !== undefined) {\n    parseOptions.skipEmpty = options.skipEmpty;\n  }\n\n  if (options.allowUnsafePathSegments !== undefined) {\n    parseOptions.allowUnsafePathSegments = options.allowUnsafePathSegments;\n  }\n\n  return parseOptions;\n}\n\nexport function useForm2js<TSchema extends SchemaValidator | undefined = undefined>(\n  submit: UseForm2jsSubmit<TSchema>,\n  options: UseForm2jsOptions<TSchema> = {}\n): UseForm2jsResult {\n  const [isSubmitting, setIsSubmitting] = useState(false);\n  const [isError, setIsError] = useState(false);\n  const [error, setError] = useState<unknown>(null);\n  const [isSuccess, setIsSuccess] = useState(false);\n  const isSubmittingRef = useRef(false);\n  const { allowUnsafePathSegments, delimiter, schema, skipEmpty } = options;\n\n  const reset = useCallback(() => {\n    setIsError(false);\n    setError(null);\n    setIsSuccess(false);\n  }, []);\n\n  const onSubmit = useCallback(\n    async (event: SyntheticEvent<HTMLFormElement, SubmitEvent>) => {\n      event.preventDefault();\n\n      if (isSubmittingRef.current) {\n        return;\n      }\n\n      isSubmittingRef.current = true;\n      setIsSubmitting(true);\n      setIsError(false);\n      setError(null);\n      setIsSuccess(false);\n\n      try {\n        const parseOptions = buildParseOptions(options);\n        const formData = new FormData(event.currentTarget);\n\n        const data = schema\n          ? formDataToObject(formData, { ...parseOptions, schema })\n          : formDataToObject(formData, parseOptions);\n\n        await submit(data as UseForm2jsData<TSchema>);\n        setIsSuccess(true);\n      } catch (submitError: unknown) {\n        setIsError(true);\n        setError(submitError);\n      } finally {\n        setIsSubmitting(false);\n        isSubmittingRef.current = false;\n      }\n    },\n    [allowUnsafePathSegments, delimiter, schema, skipEmpty, submit]\n  );\n\n  return {\n    onSubmit,\n    isSubmitting,\n    isError,\n    error,\n    isSuccess,\n    reset\n  };\n}\n"
  },
  {
    "path": "packages/react/test/react.test.ts",
    "content": "// @vitest-environment jsdom\n\nimport React, { act } from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport type { SchemaValidator } from \"@form2js/form-data\";\nimport { useForm2js, type UseForm2jsOptions, type UseForm2jsResult, type UseForm2jsSubmit } from \"../src/index\";\n\nconst reactActScope = globalThis as typeof globalThis & {\n  IS_REACT_ACT_ENVIRONMENT?: boolean;\n};\nreactActScope.IS_REACT_ACT_ENVIRONMENT = true;\n\ninterface HarnessProps<TSchema extends SchemaValidator | undefined = undefined> {\n  submit: UseForm2jsSubmit<TSchema>;\n  options?: UseForm2jsOptions<TSchema>;\n  onSnapshot: (state: UseForm2jsResult) => void;\n  renderFields?: () => React.ReactNode;\n}\n\nfunction Harness<TSchema extends SchemaValidator | undefined = undefined>(\n  props: HarnessProps<TSchema>\n): React.ReactElement {\n  const state = useForm2js(props.submit, props.options);\n  props.onSnapshot(state);\n  const fields =\n    props.renderFields?.() ??\n    React.createElement(\"input\", { name: \"person.name\", defaultValue: \"Neo\" });\n\n  return React.createElement(\n    \"form\",\n    { onSubmit: state.onSubmit },\n    fields,\n    React.createElement(\"button\", { type: \"submit\" }, \"Submit\")\n  );\n}\n\ninterface MountedHarness {\n  root: Root;\n  container: HTMLDivElement;\n}\n\nconst mountedHarnesses: MountedHarness[] = [];\n\nafterEach(() => {\n  for (const mounted of mountedHarnesses) {\n    act(() => {\n      mounted.root.unmount();\n    });\n    mounted.container.remove();\n  }\n\n  mountedHarnesses.length = 0;\n});\n\nfunction renderHarness<TSchema extends SchemaValidator | undefined = undefined>(\n  submit: UseForm2jsSubmit<TSchema>,\n  options?: UseForm2jsOptions<TSchema>,\n  renderFields?: () => React.ReactNode\n): {\n  form: HTMLFormElement;\n  getState: () => UseForm2jsResult;\n} {\n  const container = document.createElement(\"div\");\n  document.body.appendChild(container);\n  const root = createRoot(container);\n  mountedHarnesses.push({ root, container });\n\n  let latestState: UseForm2jsResult | null = null;\n\n  const harnessProps: HarnessProps<TSchema> = {\n    submit,\n    onSnapshot(state) {\n      latestState = state;\n    }\n  };\n\n  if (options !== undefined) {\n    harnessProps.options = options;\n  }\n\n  if (renderFields !== undefined) {\n    harnessProps.renderFields = renderFields;\n  }\n\n  act(() => {\n    root.render(React.createElement(Harness, harnessProps));\n  });\n\n  const form = container.querySelector(\"form\");\n  if (!(form instanceof HTMLFormElement)) {\n    throw new Error(\"Harness form was not rendered\");\n  }\n\n  return {\n    form,\n    getState() {\n      if (!latestState) {\n        throw new Error(\"Hook state snapshot is not available\");\n      }\n\n      return latestState;\n    }\n  };\n}\n\nfunction dispatchSubmit(form: HTMLFormElement): void {\n  act(() => {\n    form.dispatchEvent(new Event(\"submit\", { bubbles: true, cancelable: true }));\n  });\n}\n\nasync function submitAndFlush(form: HTMLFormElement): Promise<void> {\n  await act(async () => {\n    form.dispatchEvent(new Event(\"submit\", { bubbles: true, cancelable: true }));\n    await Promise.resolve();\n  });\n}\n\nfunction createDeferred<TValue = undefined>(): {\n  promise: Promise<TValue>;\n  resolve: (value: TValue) => void;\n  reject: (reason?: unknown) => void;\n} {\n  let resolve!: (value: TValue) => void;\n  let reject!: (reason?: unknown) => void;\n\n  const promise = new Promise<TValue>((innerResolve, innerReject) => {\n    resolve = innerResolve;\n    reject = innerReject;\n  });\n\n  return { promise, resolve, reject };\n}\n\ndescribe(\"useForm2js\", () => {\n  it(\"submits validated data and marks submit as successful\", async () => {\n    const schema = {\n      parse(value: unknown) {\n        const record = value as { person?: { name?: string } };\n        return {\n          profileName: (record.person?.name ?? \"\").toUpperCase()\n        };\n      }\n    };\n    const received: unknown[] = [];\n    const submit = vi.fn((data: { profileName: string }) => {\n      received.push(data);\n    });\n\n    const { form, getState } = renderHarness(submit, { schema });\n    await submitAndFlush(form);\n\n    expect(submit).toHaveBeenCalledTimes(1);\n    expect(received).toEqual([{ profileName: \"NEO\" }]);\n    expect(getState().isSubmitting).toBe(false);\n    expect(getState().isError).toBe(false);\n    expect(getState().error).toBeNull();\n    expect(getState().isSuccess).toBe(true);\n  });\n\n  it(\"captures validation errors and does not call submit callback\", async () => {\n    const schema = {\n      parse() {\n        throw new Error(\"Invalid payload\");\n      }\n    };\n    const submit = vi.fn(() => Promise.resolve());\n\n    const { form, getState } = renderHarness(submit, { schema });\n    await submitAndFlush(form);\n\n    expect(submit).not.toHaveBeenCalled();\n    expect(getState().isSubmitting).toBe(false);\n    expect(getState().isError).toBe(true);\n    expect(getState().isSuccess).toBe(false);\n    expect(getState().error).toBeInstanceOf(Error);\n  });\n\n  it(\"captures submit errors from async callback\", async () => {\n    const submit = vi.fn(() => Promise.reject(new Error(\"Network failed\")));\n\n    const { form, getState } = renderHarness(submit);\n    await submitAndFlush(form);\n\n    expect(submit).toHaveBeenCalledTimes(1);\n    expect(getState().isError).toBe(true);\n    expect(getState().isSuccess).toBe(false);\n    expect(getState().error).toBeInstanceOf(Error);\n  });\n\n  it(\"ignores duplicate submit attempts while request is in flight\", async () => {\n    const deferred = createDeferred();\n    const submit = vi.fn(() => deferred.promise);\n\n    const { form, getState } = renderHarness(submit);\n\n    dispatchSubmit(form);\n    dispatchSubmit(form);\n\n    expect(submit).toHaveBeenCalledTimes(1);\n    expect(getState().isSubmitting).toBe(true);\n\n    deferred.resolve(undefined);\n    await act(async () => {\n      await deferred.promise;\n      await Promise.resolve();\n    });\n\n    expect(getState().isSubmitting).toBe(false);\n  });\n\n  it(\"reset clears error and success flags\", async () => {\n    const submit = vi.fn(() => Promise.reject(new Error(\"submit failed\")));\n\n    const { form, getState } = renderHarness(submit);\n    await submitAndFlush(form);\n    expect(getState().isError).toBe(true);\n\n    act(() => {\n      getState().reset();\n    });\n\n    expect(getState().isError).toBe(false);\n    expect(getState().error).toBeNull();\n    expect(getState().isSuccess).toBe(false);\n  });\n\n  it(\"submits parsed object when schema is omitted\", async () => {\n    const received: unknown[] = [];\n    const submit = vi.fn((data: unknown) => {\n      received.push(data);\n    });\n\n    const { form, getState } = renderHarness(submit);\n    await submitAndFlush(form);\n\n    expect(submit).toHaveBeenCalledTimes(1);\n    expect(received).toEqual([\n      {\n        person: {\n          name: \"Neo\"\n        }\n      }\n    ]);\n    expect(getState().isSuccess).toBe(true);\n  });\n\n  it(\"forwards delimiter and skipEmpty options to the parser\", async () => {\n    const received: unknown[] = [];\n    const submit = vi.fn((data: unknown) => {\n      received.push(data);\n    });\n\n    const { form } = renderHarness(\n      submit,\n      {\n        delimiter: \"/\",\n        skipEmpty: false\n      },\n      () =>\n        React.createElement(\n          React.Fragment,\n          null,\n          React.createElement(\"input\", { name: \"person/name\", defaultValue: \"Neo\" }),\n          React.createElement(\"input\", { name: \"person/alias\", defaultValue: \"\" })\n        )\n    );\n\n    await submitAndFlush(form);\n\n    expect(received).toEqual([\n      {\n        person: {\n          name: \"Neo\",\n          alias: \"\"\n        }\n      }\n    ]);\n  });\n\n  it(\"forwards allowUnsafePathSegments to the parser\", async () => {\n    const submit = vi.fn(() => Promise.resolve());\n\n    const { form, getState } = renderHarness(\n      submit,\n      {\n        allowUnsafePathSegments: true\n      },\n      () => React.createElement(\"input\", { name: \"__proto__.polluted\", defaultValue: \"yes\" })\n    );\n\n    await submitAndFlush(form);\n\n    expect(submit).toHaveBeenCalledTimes(1);\n    expect(getState().isError).toBe(false);\n    expect(getState().isSuccess).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/react/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"]\n  },\n  \"include\": [\"src/**/*.ts\", \"test/**/*.ts\", \"tsup.config.ts\"]\n}\n"
  },
  {
    "path": "packages/react/tsup.config.ts",
    "content": "import { defineConfig } from \"tsup\";\n\nexport default defineConfig({\n  entry: {\n    index: \"src/index.ts\"\n  },\n  format: [\"esm\", \"cjs\"],\n  dts: true,\n  sourcemap: true,\n  clean: true,\n  target: \"es2020\"\n});\n"
  },
  {
    "path": "scripts/bump-version.mjs",
    "content": "#!/usr/bin/env node\n\nimport { readFile, writeFile, readdir } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nconst VERSION_PARTS_RE = /^(\\d+)\\.(\\d+)\\.(\\d+)$/;\nconst SEMVER_RE = /^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$/;\n\nconst BUMP_TYPES = new Set([\"patch\", \"minor\", \"major\"]);\nconst DEP_FIELDS = [\n  \"dependencies\",\n  \"devDependencies\",\n  \"peerDependencies\",\n  \"optionalDependencies\",\n];\n\nfunction usage(exitCode = 1) {\n  console.error(\n    \"Usage: npm run bump-version -- [patch|minor|major|<exact-semver>]\"\n  );\n  process.exit(exitCode);\n}\n\nfunction nextVersion(currentVersion, bumpType) {\n  const parts = VERSION_PARTS_RE.exec(currentVersion);\n  if (!parts) {\n    throw new Error(\n      `Version ${JSON.stringify(\n        currentVersion\n      )} is not x.y.z; use an exact version instead.`\n    );\n  }\n\n  let major = Number(parts[1]);\n  let minor = Number(parts[2]);\n  let patch = Number(parts[3]);\n\n  if (bumpType === \"major\") {\n    major += 1;\n    minor = 0;\n    patch = 0;\n  } else if (bumpType === \"minor\") {\n    minor += 1;\n    patch = 0;\n  } else {\n    patch += 1;\n  }\n\n  return `${major}.${minor}.${patch}`;\n}\n\nasync function expandWorkspacePattern(pattern) {\n  if (!pattern.endsWith(\"/*\")) {\n    throw new Error(\n      `Unsupported workspace pattern ${JSON.stringify(\n        pattern\n      )}; only trailing /* patterns are supported.`\n    );\n  }\n\n  const baseDir = pattern.slice(0, -2);\n  const absoluteBaseDir = path.resolve(baseDir);\n  const entries = await readdir(absoluteBaseDir, { withFileTypes: true });\n  return entries\n    .filter((entry) => entry.isDirectory())\n    .map((entry) => path.join(baseDir, entry.name));\n}\n\nasync function readJson(filePath) {\n  const raw = await readFile(filePath, \"utf8\");\n  return JSON.parse(raw);\n}\n\nasync function readJsonIfExists(filePath) {\n  try {\n    return await readJson(filePath);\n  } catch (error) {\n    if (error && typeof error === \"object\" && error.code === \"ENOENT\") {\n      return null;\n    }\n\n    throw error;\n  }\n}\n\nfunction rewriteDependencyRange(currentRange, targetVersion) {\n  const clean = currentRange.trim();\n\n  if (clean.startsWith(\"workspace:\")) {\n    const workspaceRange = clean.slice(\"workspace:\".length);\n    if (workspaceRange.startsWith(\"^\")) return `workspace:^${targetVersion}`;\n    if (workspaceRange.startsWith(\"~\")) return `workspace:~${targetVersion}`;\n    if (workspaceRange === \"*\" || workspaceRange === \"^\" || workspaceRange === \"~\") {\n      return clean;\n    }\n    return `workspace:${targetVersion}`;\n  }\n\n  if (clean.startsWith(\"^\")) return `^${targetVersion}`;\n  if (clean.startsWith(\"~\")) return `~${targetVersion}`;\n  return targetVersion;\n}\n\nasync function main() {\n  const input = process.argv[2];\n  if (!input || input === \"--help\" || input === \"-h\") {\n    usage(input ? 0 : 1);\n  }\n\n  const rootManifest = await readJson(path.resolve(\"package.json\"));\n  const workspacePatterns = Array.isArray(rootManifest.workspaces)\n    ? rootManifest.workspaces\n    : rootManifest.workspaces?.packages;\n\n  if (!Array.isArray(workspacePatterns) || workspacePatterns.length === 0) {\n    throw new Error(\"No workspaces found in root package.json.\");\n  }\n\n  const workspaceDirs = (\n    await Promise.all(workspacePatterns.map((pattern) => expandWorkspacePattern(pattern)))\n  ).flat();\n\n  const packagePaths = workspaceDirs.map((dir) => path.resolve(dir, \"package.json\"));\n  const manifests = (\n    await Promise.all(\n      packagePaths.map(async (packagePath) => {\n        const data = await readJsonIfExists(packagePath);\n        return data ? { packagePath, data } : null;\n      })\n    )\n  ).filter(Boolean);\n\n  const workspacePackageNames = new Set(\n    manifests.map((manifest) => manifest.data.name).filter(Boolean)\n  );\n\n  const versionedManifests = manifests.filter(\n    (manifest) => typeof manifest.data.version === \"string\"\n  );\n  if (versionedManifests.length === 0) {\n    throw new Error(\"No versioned workspace packages were found.\");\n  }\n\n  const uniqueVersions = [...new Set(versionedManifests.map((m) => m.data.version))];\n  const currentVersion = uniqueVersions[0];\n  if (BUMP_TYPES.has(input) && uniqueVersions.length !== 1) {\n    throw new Error(\n      `Workspace versions are not aligned: ${uniqueVersions.join(\n        \", \"\n      )}. Use an exact version to set all packages explicitly.`\n    );\n  }\n\n  const targetVersion = BUMP_TYPES.has(input) ? nextVersion(currentVersion, input) : input;\n  if (!SEMVER_RE.test(targetVersion)) {\n    throw new Error(\n      `Invalid version input ${JSON.stringify(\n        input\n      )}. Use patch/minor/major or an exact semver version.`\n    );\n  }\n\n  for (const manifest of manifests) {\n    if (typeof manifest.data.version === \"string\") {\n      manifest.data.version = targetVersion;\n    }\n\n    for (const field of DEP_FIELDS) {\n      const deps = manifest.data[field];\n      if (!deps || typeof deps !== \"object\") continue;\n\n      for (const [depName, depRange] of Object.entries(deps)) {\n        if (\n          workspacePackageNames.has(depName) &&\n          typeof depRange === \"string\"\n        ) {\n          deps[depName] = rewriteDependencyRange(depRange, targetVersion);\n        }\n      }\n    }\n  }\n\n  await Promise.all(\n    manifests.map(({ packagePath, data }) =>\n      writeFile(packagePath, `${JSON.stringify(data, null, 2)}\\n`, \"utf8\")\n    )\n  );\n\n  console.log(`Bumped workspace package versions to ${targetVersion}`);\n}\n\nmain().catch((error) => {\n  console.error(`bump-version failed: ${error.message}`);\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/rewrite-scope.mjs",
    "content": "#!/usr/bin/env node\nimport { promises as fs } from \"node:fs\";\nimport path from \"node:path\";\n\nconst ROOT = process.cwd();\nconst SOURCE_SCOPE = \"@form2js\";\nconst SKIP_DIRS = new Set([\".git\", \"node_modules\", \"dist\", \"coverage\", \".turbo\"]);\nconst TEXT_EXTENSIONS = new Set([\n  \".ts\",\n  \".mts\",\n  \".cts\",\n  \".js\",\n  \".mjs\",\n  \".cjs\",\n  \".json\",\n  \".md\",\n  \".html\"\n]);\n\nfunction parseArgs(argv) {\n  const args = { scope: \"\", dryRun: false };\n\n  for (let i = 0; i < argv.length; i += 1) {\n    const part = argv[i];\n\n    if (part === \"--dry-run\") {\n      args.dryRun = true;\n      continue;\n    }\n\n    if (part === \"--scope\") {\n      args.scope = argv[i + 1] ?? \"\";\n      i += 1;\n      continue;\n    }\n\n    if (part.startsWith(\"--scope=\")) {\n      args.scope = part.slice(\"--scope=\".length);\n      continue;\n    }\n  }\n\n  return args;\n}\n\nfunction assertScope(scope) {\n  if (!scope.startsWith(\"@\") || scope.includes(\"/\")) {\n    throw new Error(\"Scope must look like @your-scope\");\n  }\n\n  if (scope === SOURCE_SCOPE) {\n    throw new Error(\"New scope is the same as current scope @form2js\");\n  }\n}\n\nasync function walk(dir, files = []) {\n  const entries = await fs.readdir(dir, { withFileTypes: true });\n\n  for (const entry of entries) {\n    const fullPath = path.join(dir, entry.name);\n\n    if (entry.isDirectory()) {\n      if (SKIP_DIRS.has(entry.name)) {\n        continue;\n      }\n\n      await walk(fullPath, files);\n      continue;\n    }\n\n    files.push(fullPath);\n  }\n\n  return files;\n}\n\nfunction rewriteDependencyMap(map, targetScope) {\n  if (!map) {\n    return { changed: false, value: map };\n  }\n\n  let changed = false;\n  const next = {};\n\n  for (const [name, version] of Object.entries(map)) {\n    if (name.startsWith(`${SOURCE_SCOPE}/`)) {\n      const newName = name.replace(`${SOURCE_SCOPE}/`, `${targetScope}/`);\n      next[newName] = version;\n      changed = true;\n    } else {\n      next[name] = version;\n    }\n  }\n\n  return { changed, value: next };\n}\n\nasync function rewritePackageJson(filePath, targetScope, dryRun) {\n  const source = await fs.readFile(filePath, \"utf8\");\n  const parsed = JSON.parse(source);\n  let changed = false;\n\n  if (typeof parsed.name === \"string\" && parsed.name.startsWith(`${SOURCE_SCOPE}/`)) {\n    parsed.name = parsed.name.replace(`${SOURCE_SCOPE}/`, `${targetScope}/`);\n    changed = true;\n  }\n\n  for (const field of [\"dependencies\", \"devDependencies\", \"peerDependencies\", \"optionalDependencies\"]) {\n    const result = rewriteDependencyMap(parsed[field], targetScope);\n    if (result.changed) {\n      parsed[field] = result.value;\n      changed = true;\n    }\n  }\n\n  if (!changed) {\n    return false;\n  }\n\n  if (!dryRun) {\n    await fs.writeFile(filePath, `${JSON.stringify(parsed, null, 2)}\\n`, \"utf8\");\n  }\n\n  return true;\n}\n\nasync function rewriteTextFile(filePath, targetScope, dryRun) {\n  const source = await fs.readFile(filePath, \"utf8\");\n  const updated = source.replaceAll(`${SOURCE_SCOPE}/`, `${targetScope}/`);\n\n  if (source === updated) {\n    return false;\n  }\n\n  if (!dryRun) {\n    await fs.writeFile(filePath, updated, \"utf8\");\n  }\n\n  return true;\n}\n\nasync function main() {\n  const { scope, dryRun } = parseArgs(process.argv.slice(2));\n\n  if (!scope) {\n    throw new Error(\"Usage: node scripts/rewrite-scope.mjs --scope @your-scope [--dry-run]\");\n  }\n\n  assertScope(scope);\n\n  const files = await walk(ROOT);\n  const changedFiles = [];\n\n  for (const filePath of files) {\n    if (filePath.endsWith(\"package-lock.json\")) {\n      continue;\n    }\n\n    const relative = path.relative(ROOT, filePath);\n\n    if (path.basename(filePath) === \"package.json\") {\n      if (await rewritePackageJson(filePath, scope, dryRun)) {\n        changedFiles.push(relative);\n      }\n      continue;\n    }\n\n    const extension = path.extname(filePath);\n    if (!TEXT_EXTENSIONS.has(extension)) {\n      continue;\n    }\n\n    if (await rewriteTextFile(filePath, scope, dryRun)) {\n      changedFiles.push(relative);\n    }\n  }\n\n  if (changedFiles.length === 0) {\n    console.log(\"No files required changes.\");\n    return;\n  }\n\n  console.log(`${dryRun ? \"[dry-run] \" : \"\"}Updated ${changedFiles.length} file(s):`);\n  for (const file of changedFiles) {\n    console.log(`- ${file}`);\n  }\n}\n\nmain().catch((error) => {\n  console.error(error.message);\n  process.exitCode = 1;\n});\n"
  },
  {
    "path": "test/integration/bump-version.test.ts",
    "content": "import { mkdtempSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { spawnSync } from \"node:child_process\";\n\nimport { describe, expect, it } from \"vitest\";\n\nconst repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"..\");\nconst scriptPath = path.join(repoRoot, \"scripts\", \"bump-version.mjs\");\n\nfunction writeJson(filePath: string, value: unknown) {\n  writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\\n`, \"utf8\");\n}\n\ndescribe(\"bump-version script\", () => {\n  it(\"skips workspace directories that do not contain a package manifest\", () => {\n    const tempDir = mkdtempSync(path.join(os.tmpdir(), \"form2js-bump-version-\"));\n\n    mkdirSync(path.join(tempDir, \"packages\", \"core\"), { recursive: true });\n    mkdirSync(path.join(tempDir, \"apps\", \"docs\"), { recursive: true });\n    mkdirSync(path.join(tempDir, \"apps\", \"examples\"), { recursive: true });\n\n    writeJson(path.join(tempDir, \"package.json\"), {\n      name: \"fixture-monorepo\",\n      private: true,\n      workspaces: [\"packages/*\", \"apps/*\"],\n    });\n\n    writeJson(path.join(tempDir, \"packages\", \"core\", \"package.json\"), {\n      name: \"@form2js/core\",\n      version: \"1.0.0\",\n    });\n\n    writeJson(path.join(tempDir, \"apps\", \"docs\", \"package.json\"), {\n      name: \"@form2js/docs\",\n      dependencies: {\n        \"@form2js/core\": \"1.0.0\",\n      },\n    });\n\n    const result = spawnSync(process.execPath, [scriptPath, \"1.2.3\"], {\n      cwd: tempDir,\n      encoding: \"utf8\",\n    });\n\n    expect(result.status).toBe(0);\n\n    const bumpedCore = JSON.parse(\n      readFileSync(path.join(tempDir, \"packages\", \"core\", \"package.json\"), \"utf8\")\n    ) as { version: string };\n    const docsManifest = JSON.parse(\n      readFileSync(path.join(tempDir, \"apps\", \"docs\", \"package.json\"), \"utf8\")\n    ) as { dependencies: { \"@form2js/core\": string } };\n\n    expect(bumpedCore.version).toBe(\"1.2.3\");\n    expect(docsManifest.dependencies[\"@form2js/core\"]).toBe(\"1.2.3\");\n  });\n});\n"
  },
  {
    "path": "test/integration/dependency-security.test.ts",
    "content": "import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { describe, expect, it } from \"vitest\";\n\ntype Lockfile = {\n  packages?: Record<string, { version?: string }>;\n};\n\nconst repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"..\");\nconst lockfilePath = path.join(repoRoot, \"package-lock.json\");\n\nfunction readLockfile(): Lockfile {\n  return JSON.parse(readFileSync(lockfilePath, \"utf8\")) as Lockfile;\n}\n\nfunction compareSemver(left: string, right: string): number {\n  const leftParts = left.split(\"-\", 1)[0].split(\".\").map(Number);\n  const rightParts = right.split(\"-\", 1)[0].split(\".\").map(Number);\n  const length = Math.max(leftParts.length, rightParts.length);\n\n  for (let index = 0; index < length; index += 1) {\n    const difference = (leftParts[index] ?? 0) - (rightParts[index] ?? 0);\n\n    if (difference !== 0) {\n      return difference;\n    }\n  }\n\n  return 0;\n}\n\nfunction isVulnerableVersion(name: string, version: string): boolean {\n  const [major = 0] = version.split(\".\", 1).map(Number);\n\n  if (name === \"brace-expansion\") {\n    if (major === 1) {\n      return compareSemver(version, \"1.1.13\") < 0;\n    }\n\n    if (major === 2) {\n      return compareSemver(version, \"2.0.3\") < 0;\n    }\n\n    if (major === 4) {\n      return true;\n    }\n\n    if (major === 5) {\n      return compareSemver(version, \"5.0.5\") < 0;\n    }\n\n    return false;\n  }\n\n  if (name === \"picomatch\") {\n    if (major === 2) {\n      return compareSemver(version, \"2.3.2\") < 0;\n    }\n\n    if (major === 4) {\n      return compareSemver(version, \"4.0.4\") < 0;\n    }\n\n    return false;\n  }\n\n  if (name === \"smol-toml\") {\n    return compareSemver(version, \"1.6.1\") < 0;\n  }\n\n  if (name === \"vite\") {\n    if (major === 6) {\n      return compareSemver(version, \"6.4.2\") < 0;\n    }\n\n    if (major === 7) {\n      return compareSemver(version, \"7.3.2\") < 0;\n    }\n\n    if (major === 8) {\n      return compareSemver(version, \"8.0.5\") < 0;\n    }\n\n    return false;\n  }\n\n  if (name === \"defu\") {\n    if (major === 6) {\n      return compareSemver(version, \"6.1.5\") < 0;\n    }\n\n    return false;\n  }\n\n  if (name === \"yaml\" && major === 2) {\n    return compareSemver(version, \"2.8.3\") < 0;\n  }\n\n  return false;\n}\n\ndescribe(\"dependency security\", () => {\n  it(\"does not leave known vulnerable dependency versions in the lockfile\", () => {\n    const lockfile = readLockfile();\n    const packages = lockfile.packages ?? {};\n    const vulnerablePackages = [\"brace-expansion\", \"defu\", \"picomatch\", \"smol-toml\", \"vite\", \"yaml\"];\n\n    for (const [packagePath, packageInfo] of Object.entries(packages)) {\n      const packageName = packagePath.split(\"/\").at(-1);\n      const version = packageInfo.version;\n\n      if (!packageName || !version || !vulnerablePackages.includes(packageName)) {\n        continue;\n      }\n\n      expect(\n        isVulnerableVersion(packageName, version),\n        `${packagePath} resolves to vulnerable ${packageName}@${version}`\n      ).toBe(false);\n    }\n  });\n});\n"
  },
  {
    "path": "test/integration/form-flow.test.ts",
    "content": "// @vitest-environment jsdom\n\nimport { describe, expect, it } from \"vitest\";\nimport { formToObject } from \"@form2js/dom\";\nimport { formDataToObject } from \"@form2js/form-data\";\nimport { objectToForm } from \"@form2js/js2form\";\n\ndescribe(\"integration: full data flow\", () => {\n  it(\"supports form -> object -> form roundtrip\", () => {\n    document.body.innerHTML = `\n      <form id=\"source\">\n        <input name=\"person.name.first\" value=\"Neo\" />\n        <input name=\"person.name.last\" value=\"Anderson\" />\n        <input type=\"checkbox\" name=\"person.roles[]\" value=\"admin\" checked />\n        <input type=\"checkbox\" name=\"person.roles[]\" value=\"operator\" checked />\n      </form>\n      <form id=\"target\">\n        <input name=\"person.name.first\" />\n        <input name=\"person.name.last\" />\n        <input type=\"checkbox\" name=\"person.roles[]\" value=\"admin\" />\n        <input type=\"checkbox\" name=\"person.roles[]\" value=\"operator\" />\n      </form>\n    `;\n\n    const source = document.getElementById(\"source\");\n    const target = document.getElementById(\"target\");\n\n    const sourceObject = formToObject(source);\n    objectToForm(target, sourceObject);\n    const roundTripped = formToObject(target);\n\n    expect(roundTripped).toEqual(sourceObject);\n  });\n\n  it(\"accepts FormData with same path semantics\", () => {\n    const formData = new FormData();\n    formData.append(\"items[8].name\", \"A\");\n    formData.append(\"items[5].name\", \"B\");\n\n    const result = formDataToObject(formData);\n\n    expect(result).toEqual({\n      items: [{ name: \"A\" }, { name: \"B\" }]\n    });\n  });\n});\n"
  },
  {
    "path": "test/integration/package-metadata.test.ts",
    "content": "import { existsSync, readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { describe, expect, it } from \"vitest\";\n\ntype PackageManifest = {\n  bugs?: { url?: string } | string;\n  homepage?: string;\n  keywords?: string[];\n  license?: string;\n  repository?: { url?: string } | string;\n};\n\nconst repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"..\");\n\nconst packages = [\n  \"packages/core\",\n  \"packages/dom\",\n  \"packages/form-data\",\n  \"packages/js2form\",\n  \"packages/jquery\",\n  \"packages/react\",\n] as const;\n\nfunction readManifest(packageDir: string): PackageManifest {\n  const manifestPath = path.join(repoRoot, packageDir, \"package.json\");\n  return JSON.parse(readFileSync(manifestPath, \"utf8\")) as PackageManifest;\n}\n\ndescribe(\"published package metadata\", () => {\n  for (const packageDir of packages) {\n    it(`${packageDir} includes npm-facing metadata and a package README`, () => {\n      const manifest = readManifest(packageDir);\n      const readmePath = path.join(repoRoot, packageDir, \"README.md\");\n\n      expect(existsSync(readmePath), `${packageDir} is missing README.md`).toBe(true);\n      expect(manifest.license, `${packageDir} is missing license`).toBeTruthy();\n      expect(manifest.homepage, `${packageDir} is missing homepage`).toBeTruthy();\n      expect(manifest.repository, `${packageDir} is missing repository`).toBeTruthy();\n      expect(manifest.bugs, `${packageDir} is missing bugs`).toBeTruthy();\n      expect(manifest.keywords, `${packageDir} is missing keywords`).toBeInstanceOf(Array);\n      expect(manifest.keywords?.length ?? 0, `${packageDir} has no keywords`).toBeGreaterThan(0);\n    });\n  }\n});\n"
  },
  {
    "path": "test/integration/workflow-node-version.test.ts",
    "content": "import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { describe, expect, it } from \"vitest\";\n\nconst repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), \"..\", \"..\");\n\nfunction readWorkflow(relativePath: string): string {\n  return readFileSync(path.join(repoRoot, relativePath), \"utf8\");\n}\n\ndescribe(\"workflow node versions\", () => {\n  it(\"runs docs-related GitHub workflows on a supported Node.js version\", () => {\n    const ciWorkflow = readWorkflow(\".github/workflows/ci.yml\");\n    const pagesWorkflow = readWorkflow(\".github/workflows/pages.yml\");\n\n    expect(ciWorkflow).toContain(\"node-version: [22.14.0]\");\n    expect(ciWorkflow).toContain(\"name: docs-e2e (node 22.14.0)\");\n    expect(ciWorkflow).toContain(\"node-version: 22.14.0\");\n    expect(pagesWorkflow).toContain(\"node-version: 22.14.0\");\n  });\n});\n"
  },
  {
    "path": "tsconfig.base.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@form2js/core\": [\"packages/core/src/index.ts\"],\n      \"@form2js/dom\": [\"packages/dom/src/index.ts\"],\n      \"@form2js/form-data\": [\"packages/form-data/src/index.ts\"],\n      \"@form2js/js2form\": [\"packages/js2form/src/index.ts\"],\n      \"@form2js/jquery\": [\"packages/jquery/src/index.ts\"],\n      \"@form2js/react\": [\"packages/react/src/index.ts\"]\n    },\n    \"target\": \"ES2020\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"strict\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"noImplicitOverride\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"skipLibCheck\": true,\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"sourceMap\": true,\n    \"esModuleInterop\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"types\": [\n      \"vitest/globals\"\n    ]\n  }\n}\n"
  },
  {
    "path": "turbo.json",
    "content": "{\n  \"$schema\": \"https://turbo.build/schema.json\",\n  \"tasks\": {\n    \"build\": {\n      \"dependsOn\": [\n        \"^build\"\n      ],\n      \"outputs\": [\n        \"dist/**\"\n      ]\n    },\n    \"test\": {\n      \"dependsOn\": [\n        \"^build\"\n      ],\n      \"outputs\": []\n    },\n    \"lint\": {\n      \"dependsOn\": [\n        \"^build\"\n      ],\n      \"outputs\": []\n    },\n    \"typecheck\": {\n      \"dependsOn\": [\n        \"^build\",\n        \"^typecheck\"\n      ],\n      \"outputs\": []\n    },\n    \"clean\": {\n      \"cache\": false\n    }\n  }\n}\n"
  }
]