[
  {
    "path": ".actrc",
    "content": "# For runs-on: ubuntu-latest\n-P ubuntu-latest=shivammathur/node:latest"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n  pull_request:\n\njobs:\n  style:\n    if: github.event_name == 'push'\n    runs-on: ubuntu-latest\n    name: Code Style\n\n    steps:\n    - name: Checkout code\n      uses: actions/checkout@v4\n\n    - name: Setup PHP\n      uses: shivammathur/setup-php@v2\n      with:\n        php-version: 8.3\n\n    - name: Install dependencies\n      run: composer install --prefer-dist --no-interaction --no-progress\n\n    - name: Run PHP CS Fixer\n      run: vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --allow-risky=yes\n\n    - name: Commit changes\n      uses: stefanzweifel/git-auto-commit-action@v5\n      with:\n        commit_message: Fix styling\n\n  unit:\n    runs-on: ubuntu-latest\n\n    strategy:\n      fail-fast: true\n      matrix:\n        php: [ 8.4, 8.3 ]\n\n    name: Unit & Feature — PHP ${{ matrix.php }}\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v4\n\n    - name: Setup PHP\n      uses: shivammathur/setup-php@v2\n      with:\n        php-version: ${{ matrix.php }}\n\n    - name: Get Composer cache dir\n      id: composer-cache\n      run: echo \"dir=$(composer config cache-files-dir)\" >> $GITHUB_OUTPUT\n\n    - name: Cache Composer dependencies\n      uses: actions/cache@v4\n      with:\n        path: ${{ steps.composer-cache.outputs.dir }}\n        key: php-${{ matrix.php }}-composer-${{ hashFiles('composer.lock') }}\n        restore-keys: php-${{ matrix.php }}-composer-\n\n    - name: Install dependencies\n      run: composer install --prefer-dist --no-interaction --no-progress\n\n    - name: Run unit & feature tests\n      run: vendor/bin/pest --testsuite=\"Unit Suite,Feature Suite\"\n\n  browser:\n    runs-on: ubuntu-latest\n    name: Browser E2E\n\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v4\n\n    - name: Setup PHP\n      uses: shivammathur/setup-php@v2\n      with:\n        php-version: 8.3\n\n    - name: Setup Node.js\n      uses: actions/setup-node@v4\n      with:\n        node-version: 20\n        cache: npm\n        cache-dependency-path: package-lock.json\n\n    - name: Get Composer cache dir\n      id: composer-cache\n      run: echo \"dir=$(composer config cache-files-dir)\" >> $GITHUB_OUTPUT\n\n    - name: Cache Composer dependencies\n      uses: actions/cache@v4\n      with:\n        path: ${{ steps.composer-cache.outputs.dir }}\n        key: php-8.3-composer-${{ hashFiles('composer.lock') }}\n        restore-keys: php-8.3-composer-\n\n    - name: Install PHP dependencies\n      run: composer install --prefer-dist --no-interaction --no-progress\n\n    - name: Cache Playwright browsers\n      uses: actions/cache@v4\n      id: playwright-cache\n      with:\n        path: ~/.cache/ms-playwright\n        key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}-chromium\n        restore-keys: ${{ runner.os }}-playwright-\n\n    - name: Install Playwright\n      run: npm ci && npx playwright install --with-deps chromium\n\n    - name: Run browser tests\n      run: vendor/bin/pest --group=browser\n"
  },
  {
    "path": ".gitignore",
    "content": "/vendor\n/node_modules\n/.phpunit.result.cache\n/.php-cs-fixer.cache\n/tests/compiled/**/*.php\n/tests/compiled/*.php\n/tests/Browser/Screenshots\n/tests/Benchmark/profile-report.html\n.claude\n"
  },
  {
    "path": ".php-cs-fixer.dist.php",
    "content": "<?php\n\n$finder = Symfony\\Component\\Finder\\Finder::create()\n    ->name('*.php')\n    ->notName('*.blade.php')\n    ->ignoreDotFiles(true)\n    ->ignoreVCS(true)\n    ->in([\n        __DIR__ . '/src',\n        __DIR__ . '/tests',\n    ]);\n\n$config = new PhpCsFixer\\Config();\n\nreturn $config->setRules([\n        '@PSR12' => true,\n        'array_syntax' => ['syntax' => 'short'],\n        'ordered_imports' => ['sort_algorithm' => 'alpha'],\n        'no_unused_imports' => true,\n        'not_operator_with_successor_space' => true,\n        'trailing_comma_in_multiline' => true,\n        'phpdoc_scalar' => true,\n        'unary_operator_spaces' => true,\n        'phpdoc_single_line_var_spacing' => true,\n        'phpdoc_var_without_name' => true,\n        'class_attributes_separation' => ['elements' => ['method' => 'one']],\n        'method_argument_space' => [\n            'on_multiline' => 'ensure_fully_multiline',\n            'keep_multiple_spaces_after_comma' => true,\n        ],\n        'single_trait_insert_per_statement' => true,\n    ])\n    ->setFinder($finder);"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## [Unreleased](https://github.com/clickfwd/yoyo/compare/0.15.0...develop)\n\n## [0.15.0 (2026-04-10)](https://github.com/clickfwd/yoyo/compare/0.14.0...0.15.0)\n\n### Added\n\n- `Yoyo.dispatch()` and `Yoyo.dispatchTo()` JS API for triggering component events from JavaScript, matching Livewire 3's dispatch API. Thanks to @mucan54 for the original proposal in #35.\n- Named parameter support for JS dispatch — object params like `{ postId: 2 }` map to listener method arguments by name.\n- Required parameter validation for named dispatch params — throws `InvalidArgumentException` if a required parameter is missing.\n- Documentation for all HTMX response header methods available via `$this->response` (`retarget`, `reswap`, `reselect`, `pushUrl`, `replaceUrl`, `location`, `redirect`, `refresh`, `trigger`, `triggerAfterSwap`, `triggerAfterSettle`).\n\n### Fixed\n\n- Fix `test_json` empty-params decode bug when `eventParams` is an empty JSON object (`'{}'`).\n- Add defensive null-guard in `triggerServerEmittedEvent` to prevent TypeError when source element is not inside a component.\n\n### Performance\n\n- Add static cache to `getMethodParametersWithTypes()` for consistency with other `ClassHelpers` caches.\n- Optimize props filtering in `YoyoCompiler` by replacing repeated `in_array` checks with a lookup map.\n- Skip re-processing already compiled nested Yoyo child components (`yoyo:name` + `hx-vals`) during compile.\n\nBenchmark update (5-run averages from `tests/Benchmark/RealWorldBenchmarkTest.php`):\n\n- Listing List (10x3 children): `0.5064 -> 0.4488 ms/op` (`-11.4%`)\n- Listing List (25x3 children): `1.1379 -> 1.0104 ms/op` (`-11.2%`)\n- Listing List (50x3 children): `2.1507 -> 1.9204 ms/op` (`-10.7%`)\n\nOther scenarios remained in the same range with normal benchmark variance.\n\n## [0.14.0 (2025-10-27)](https://github.com/clickfwd/yoyo/compare/0.13.1...0.14.0)\n\n### Breaking Changes\n\n- Minimum PHP version is now 8.0+\n- `illuminate/container` is now an optional dependency\n\n### Added\n\n- Built-in dependency injection container for standalone usage\n\n### Changed\n\n- Yoyo now automatically detects and uses `illuminate/container` if available, otherwise uses built-in container\n\n### Migration Notes\n\nIf you're using Yoyo standalone and need advanced container features, install illuminate/container:\n```bash\ncomposer require illuminate/container\n```\n\n## [0.13.1 (2025-08-15)](https://github.com/clickfwd/yoyo/compare/0.13.0...0.13.1)\n\n- Fix parameter validation to correctly handle optional parameters in component actions\n\n## [0.13.0 (2025-08-15)](https://github.com/clickfwd/yoyo/compare/0.12.0...0.13.0)\n\n- Fix spinners stop working when target is different than current element\n- Add support for variadic parameters in component actions using PHP's `...$params` syntax\n- Add automatic dependency injection for typed parameters in component actions\n- Add new ClassHelpers methods: `methodHasVariadicParameter()` and `getMethodParametersWithTypes()`\n- Enhanced ComponentManager to handle methods with variadic parameters\n- Improved parameter validation to support methods with only typed (DI) parameters\n- Container integration now properly handles mixed regular, typed, and variadic parameters\n\n## [0.12.0 (2025-07-18)](https://github.com/clickfwd/yoyo/compare/0.11.1...0.12.0)\n\n- Add compatibility up to illuminate/container v12\n- Fix deprecated error for  implicit nullable parameter value\n\n## [0.11.1 (2025-05-28)](https://github.com/clickfwd/yoyo/compare/0.11.0...0.11.1)\n\n- Improve parsing of yoyo attribute action arguments and fix error where they are incorreclty converted to null.\n\n## [0.11.0 (2025-02-06)](https://github.com/clickfwd/yoyo/compare/0.10.0...0.11.0)\n\n- Passing attributes to Yoyo\\yoyo_render should only prefix HTMX attributes defined in `YoyoCompiler::YOYO_ATTRIBUTES`.\n\n## [0.10.1 (2024-08-29)](https://github.com/clickfwd/yoyo/compare/0.10.0...0.10.1)\n\n- Change default hx-include to `this` to improve event-to-request delay on forms with large number of elements.\n\n## [0.10.0 (2024-06-20)](https://github.com/clickfwd/yoyo/compare/0.9.1...0.10.0)\n\n- Merge PR to add Falcon framework implementation.\n\n## [0.9.1 (2024-04-16)](https://github.com/clickfwd/yoyo/compare/0.9.0...0.9.1)\n\n- Fix Safari/iOS errors due to evt.target and evt.srcElement now being null.\n- Add support for port in UrlStateManagerService.php\n- PHP 8.2/8.3 compat\n- Fix ResponseHeaders::refresh error due to missing parameter.\n- Fix headers already sent error when setting status code in response\n- Ensure components are compiled only once.\n- Bump htmx to v1.9.4 and include new config options.\n- New Request::set, Request::triggerName and Request::header methods.\n- New Response::reselect method for the HX-Reselect header.\n- New New Yoyo::actionArgs method.\n\n## [0.9.0 (2023-04-02)](https://github.com/clickfwd/yoyo/compare/0.8.1...0.9.0)\n\n## New\n\n- Added Component::actionMatches method.\n- Add response HX header methods that can via accessed in Yoyo component via $this->response:\n    - location\n    - pushUrl\n    - redirect\n    - refresh\n    - replace\n    - reswap\n    - retarget\n    - trigger\n    - triggerAfterSwap\n    - triggerAfterSettle\n\n## Changed\n\n- Added composer support for illuminate/container v9.0\n\n## Fixed \n\n- Regex replacement in Yoyo compiler causing issues due to incorrect replacements.\n\n## [0.8.2 (2021-07-07)](https://github.com/clickfwd/yoyo/compare/0.8.1...0.8.2)\n\n### Changed\n\n    - Updated htmx to v1.8.4.\n    - Add support for new htmx attributes to the Yoyo compiler: `replace-url`, `select-oob`, `validate`.\n    - Add support for PHP 8.1 installs.\n    - Add `yoyo:history=\"remove\"` attribute to allow excluding elements from browser history cache snapshot.\n    - Renamed `Component::addDynamicProperties` to `Component::getDynamicProperties` to make it consistent with other component methods.\n    - Expose all htmx configuration options to Yoyo via `Clickfwd\\Yoyo\\Services\\Configuration`.\n    - Breaking change! Compiler no longer makes buttons, links or inputs reactive by default. Previously any button, link or input would automatically receive the yoyo:get=\"render\" attribute unless it already had a different request attribute. Now it's necessary to explicitly add the yoyo:{method} attribute if you want to make the element reactive. You can also just add an empty `yoyo` attribute if you want to make a request to the default yoyo:get=\"render\" attribute on the component.    \n\n### Fixed\n\n    - Allow queryString parameters with value of zero to be pushed to URL.\n    - Javascript `Yoyo.on` throws undefined error when event detail is of type object.\n    - Issues working with dynamic properties.\n    - Lots of other changes and improvements.\n\n## [0.8.1 (2021-07-07)](https://github.com/clickfwd/yoyo/compare/0.8.0...0.8.1)\n\n### Added\n\n- Support for adding dynamic properties to components via Component::addDynamicProperties method, which returns an array of property names. NOTE: `renamed to Component::getDynamicProperties` in next update.\n\n    This can be useful when the names of the properties are not known in advanced (i.e. coming from the database). The code below shows how to use this together with queryStrings to push the dynamic property values to the URL. The dynamic properties are also available in templates like regular public properties.\n\n    ```php\n    public function addDynamicProperties() \n    {\n        return ['width', 'length'];\n    }\n\n    public function getQueryString()\n    {\n        return array_merge($this->queryString, $this->addDynamicProperties());\n    }\n    ```\n\n\n## [0.8.0 (2021-07-07)](https://github.com/clickfwd/yoyo/compare/0.7.5...0.8.0)\n\n### Changed\n\n- Links that trigger Yoyo requests now automatically update the browser URL and push the component state to the browser history.\n\n## [0.7.5 (2021-05-28)](https://github.com/clickfwd/yoyo/compare/0.7.4...0.7.5)\n\n### Fixed\n\n- Error retrieving parameter names for component action.\n\n## [0.7.4 (2021-05-20)](https://github.com/clickfwd/yoyo/compare/0.7.3...0.7.4)\n\n### Fixed\n\n- Various fixes\n\n## [0.7.3 (2021-04-04)](https://github.com/clickfwd/yoyo/compare/0.7.2...0.7.3)\n\n### Fixed\n\n- Allow component listeners to trigger the default `refresh` action.\n\n    ```php\n    protected $listeners = ['updated' => 'refresh'];\n    ```\n\n## [0.7.2 (2021-03-22)](https://github.com/clickfwd/yoyo/compare/0.7.1...0.7.2)\n\n### Fixed\n\n- Initial component history snapshot taken even for components that don't push changes to the URL via `queryString`.\n\n## [0.7.1 (2021-03-14)](https://github.com/clickfwd/yoyo/compare/0.7.0...0.7.1)\n\n### Added\n\n- Updated htmx to v1.3.1\n- Component `emitToWithSelector` method to differentiate from `emitTo`. `emitTo` targets Yoyo components specifically, while `emitToWithSelector` can target elements using a CSS selector.\n- Component `skipRenderAndRemove` method to allow removing components from the page.\n- Component `addSwapModifiers` method to dynamically set [swap modifers](https://htmx.org/attributes/hx-swap/) when updating components. \n- Additional component lifecycle hooks\n    - initialize - on component initialization, allows adding properties, setting listeners, etc.\n    - mount - after component initialization\n    - rendering - before component render method\n    - rendered - after component render method, receives component output\n- Allow traits to implement lifecycle hooks which run after the component's equivalent. For example, a `trait WithValidation` in addition to adding its own properties and methods can also implement hooks with the format:\n    - `initializeWithValidation`\n    - `mountWithValidation`\n    - `renderingWithValidation`\n    - `renderedWithValidation`\n- Depedency Injection for lifecycle hooks and listener methods.\n- Yoyo\\abort, Yoyo\\abort_if, Yoyo\\abort_unless functions allows throwing exceptions within components to stop execution while still sending any queued events back up to the browser.\n- Namespace support for view templates and component classes\n- Support for new htmx `hx-headers` attribute via `yoyo:headers`\n- Tests for Blade and Twig\n\n### Changed\n\n- Automatically re-spawn dynamically created target elements if these are removed on swap. \n\n    When `yoyo:target=\"#some-element\"` is used with an ID and the target element doesn't exist, Yoyo automatically creates an empty `<div id=\"some-element\"></div>` and appends it to the document body.\n- Refactored component resolver\n- Events are sent to the browser even when throwing an exception within a component.\n- Components are resolved from the container.\n\n### Fixed\n\n- Cannot use Array property as prop.\n- Component props not persisted in POST request updates.\n- Variables passed directly to `render` method leaking to component props.\n"
  },
  {
    "path": "LICENSE.md",
    "content": "## MIT License\n\nCopyright © ClickFWD\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# Yoyo\n\nYoyo is a full-stack PHP framework that you can use on any project to create rich dynamic interfaces using server-rendered HTML.\n\nWith Yoyo, you create reactive components that are seamlessly updated without the need to write any Javascript code.\n\nYoyo ships with a simple templating system, and  offers out-of-the-box support for [Blade](https://laravel.com/docs/8.x/blade), without having to use Laravel, and [Twig](https://twig.symfony.com/).\n\nInspired by [Laravel Livewire](https://laravel-livewire.com/) and [Sprig](https://putyourlightson.com/plugins/sprig), and using [htmx](https://htmx.org/).\n\n## 🚀 Yoyo Demo Apps  \n\nCheck out the [Yoyo Demo App](https://app.getyoyo.dev) to get a better idea of what you can build with Yoyo. It showcases many different types of Yoyo components. You can also clone and install the demo apps:\n\n- [Yoyo Blade App](https://github.com/clickfwd/yoyo-blade-app)\n- [Yoyo Laravel App](https://github.com/clickfwd/yoyo-laravel-app)\n- [Yoyo PHP template App](https://github.com/clickfwd/yoyo-app)\n- [Yoyo Twig App](https://github.com/clickfwd/yoyo-twig-app)\n\n## Documentation \n\n- [How it Works](#how-it-works)\n- [Installation](#installation)\n- [Updating](#updating)\n- [Configuring Yoyo](#configuring-yoyo)\n- [Creating Components](#creating-components)\n- [Rendering Components](#rendering-components)\n- [Properties](#properties)\n- [Actions](#actions)\n- [View Data](#view-data)\n- [Computed Properties](#computed-properties)\n- [Events](#events)\n- [Redirecting](#redirecting)\n- [Component Props](#component-props)\n- [Query String](#query-string)\n- [Loading States](#loading-states)\n- [Using Blade](#using-blade)\n- [Using Twig](#using-twig)\n- [License](#license)\n\n## How it Works\n\nYoyo components are rendered on page load and can be individually updated, without the need for page-reloads, based on user interaction and specific events.\n\nComponent update requests are sent directly to a Yoyo-designated route, where it processes the request and then sends the updated component HTML partial back to the browser.\n\nYoyo can update the browser URL state and trigger browser events straight from the server.\n\nBelow you can see what a Counter component looks like:\n\n**Component class**\n\n```php\n# /app/Yoyo/Counter.php\n\n<?php \nnamespace App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass Counter extends Component\n{\n\tpublic $count = 0;\n\t\n\tprotected $props = ['count'];\n\n    public function increment()\n    {\n        $this->count++;\n    }\n}\n```\n\n**Component template**\n\n```html\n<!-- /app/resources/views/yoyo/counter.php -->\n\n<div>\n\n\t<button yoyo:get=\"increment\">+</button>\n\t\n\t<span><?php echo $count; ?></span>\n\n</div>\n```\n\nYes, it's that simple! One thing to note above is the use of the protected property `$props`. This indicates to Yoyo that the `count` variable, which is not explicitly available within the template, should be persisted and updated in every request.\n\n\n## Installation\n\n### Install the Package\n\n```bash\ncomposer require clickfwd/yoyo\n```\n\n#### Phalcon Framework Installation\n\nFor phalcon, you need to add di\n\n```php\n$di->register(new \\Clickfwd\\Yoyo\\YoyoPhalconServiceProvider());\t\n```\n\nand you need to add router:\n\n```php\n$router->add('/yoyo', [\n            'controller' => 'yoyo',\n            'action' => 'handle',\n        ]);\n```\n\nand you should create a controller and inherit from `Clickfwd\\Yoyo\\PhalconController` class.\n\n\n## Updating\n\nAfter performing the usual `composer update`, remember to also update the `yoyo.js` script per the [Load Assets](#load-assets) instructions.\n\n## Configuring Yoyo\n\nIt's necessary to bootstrap Yoyo with a few configuration settings. This code should run when rendering and updating components.\n\n```php\nuse Clickfwd\\Yoyo\\View;\nuse Clickfwd\\Yoyo\\ViewProviders\\YoyoViewProvider;\nuse Clickfwd\\Yoyo\\Yoyo;\n\n$yoyo = new Yoyo();\n\n$yoyo->configure([\n  'url' => '/yoyo',\n  'scriptsPath' => 'app/resources/assets/js/',\n  'namespace' => 'App\\\\Yoyo\\\\'\n]);\n\n// Register the native Yoyo view provider \n// Pass the Yoyo components' template directory path in the constructor\n\n$yoyo->registerViewProvider(function() {\n  return new YoyoViewProvider(new View(__DIR__.'/resources/views/yoyo'));\n});\n```\n\n**'url'**\n\nAbsolute or relative URL that will be used to request component updates.\n\n**'scriptsPath'**\n\nThe location where you copied the `yoyo.js` script. \n\n**'namespace'**\n\nThis is the PHP class namespace that will be used to discover auto-loaded dynamic components (components that use a PHP class). \n\nIf the namespace is not provided or components are in different namespaces, you need to register them manually:\n\n```php\n$yoyo->registerComponents([\n    'counter' => App\\Yoyo\\Counter::class,\n];\n```\n\nYou are required to load the component classes at run time, either using a `require` statement to load the component's PHP class file, or by including your component namespaces in you project's `composer.json`.\n\nAnonymous components don't need to be registered, but the template name needs to match the component name.\n\n### Dependency Injection Container\n\nYoyo includes a built-in container and automatically detects Laravel's `illuminate/container` if installed.\n\n**Simple dependency injection:**\n\n```php\nclass UserProfile extends Component\n{\n    public function mount(UserRepository $users, $userId)\n    {\n        $this->user = $users->find($userId);\n    }\n}\n```\n\n**For advanced container features**, install `illuminate/container` (automatically detected):\n\n```bash\ncomposer require illuminate/container\n```\n\n**Using a custom PSR-11 container:**\n\n```php\nuse Clickfwd\\Yoyo\\ContainerResolver;\n\nContainerResolver::setPreferred($myContainer);\n```\n\n### Load Assets\n\nFind `yoyo.js` in the following vendor path and copy it to your project's public assets directory.\n\n```file\n/vendor/clickfwd/yoyo/src/assets/js/yoyo.js \n```\n\nTo load the necessary scripts in your template add the following code inside the `<head>` tag:\n\n```php\n<?php yoyo_scripts(); ?>\n```\n\n## Creating Components\n\nDynamic components require a class and a template. When using the Blade and Twig view providers, you can also use inline views, where the component markup is returned directly in the component's `render` method.\n\nAnonymous components allow creating components with just a template file.\n\nTo create a simple search component that retrieves results from the server and updates itself, create the component template:\n\n```html\n// resources/views/yoyo/search.php\n\n<form>\n    <input type=\"text\" name=\"query\" value=\"<?php echo $query ?? ''; ?>\">\n    <button type=\"submit\">Submit</button>\n</form>\n```\n\nYoyo will render the component output and compile it to add the necessary attributes that makes it dynamic and reactive. \n\nWhen you submit the form, posted data is automatically made available within the component template. The template code can be expanded to show a list of results, or an empty state:\n\n```php\n<?php\n$query = $query ?? '';\n$entries = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];\n$results = array_filter($entries, function($entry) use ($query) {\n    return $query && strpos($entry, $query) !== false;\n});\n?>\n```\n\n```html\n<form>\n    <input type=\"text\" name=\"query\" value=\"<?php echo $query; ?>\">\n    <button type=\"submit\">Submit</button>\n</form>\n    \n<ul>\n    <?php if ($query && empty($results)): ?>\n        <li>No results found</li>\n    <?php endif; ?>\n    \n    <?php foreach ($results as $entry): ?>\n        <li><?php echo $entry; ?></li>\n    <?php endforeach; ?>\n</ul>\n```\n\nThe `$results` array can be populated from any source (i.e. database, API, etc.)\n\nThe example can be converted into a live search input, with a 300ms debounce to minimize the number of requests. Replace the `form` tag with:\n\n```html\n<input yoyo:on=\"keyup delay:300ms changed\" type=\"text\" name=\"query\" value=\"<?php echo $query; ?>\" />\n```\n\nThe `yoyo:on=\"keyup delay:300ms change\"` directive tells Yoyo to make a request on the keyup event, with a 300ms debounce, and only if the input text changed. \n\nNow let's turn this into a dynamic component using a class.\n\n```php\n# /app/Yoyo/Search\n\n<?php\n\nnamespace App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass Search extends Component\n{\n\tpublic $query;\n\t\n\tprotected $queryString = ['query'];\n\t\n\tpublic function render()\n\t{\n\t\t$query = $this->query;\n\t\n\t\t// Perform your database query\n\t\t$entries = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];\n\t\n\t\t$results = array_filter($entries, function($entry) use ($query) {\n\t\t\treturn $query && stripos($entry, $query) !== false;\n\t\t});\n\t\n\t  // Render the component view\n\t\treturn $this->view('search',['results' => $results]);\n\t}\n}\n```\n\nAnd the template:\n\n```html\n<!-- /app/resources/views/yoyo/search.php -->\n\n<input yoyo:on=\"keyup delay:300ms changed\" type=\"text\" name=\"query\" value=\"<?php echo $query; ?>\" />\n\n<ul yoyo:ignore>\n    <?php if ($query && empty($results)): ?>\n        <li>No results found</li>\n    <?php endif; ?>\n    \n    <?php foreach ($results as $entry): ?>\n        <li><?php echo $entry; ?></li>\n    <?php endforeach; ?>\n</ul>\n```\n\nA couple of things to note here that are covered in more detail in other sections.\n\n1. The component class includes a `queryString` property that tells Yoyo to automatically include the queryString values in the browser URL after a component update. If you re-load the page with the `query` value in the URL, you'll automatically see the search results on the page.\n2. Yoyo will automatically make available component class public properties as template variables. This allows using `$this->query` to access the search keyword in the component and `$query` in the template.\n\nWhen you compare this search example to the counter example at the beginning, you can see that there are no action methods (i.e. increment, decrement). A component update will always default to the `render` method, unless an action is specified via one of the method attributes (i.e. yoyo:get, yoyo:post, etc.). In that case, the action method always runs before the render method.\n\n## Rendering Components\n\nThere are two instances when components are rendered. On page load, and on component updates.\n\n### Rendering on Page Load\n\nTo render any component on page load within your templates, use the `yoyo_render` function and pass the component name as the first parameter.\n\n```php\n<?php echo yoyo_render('search'); ?>\n```\n\nFor dynamic components, the component name is a hyphenated version of the class name (i.e. LiveSearch → live-search). If you register components while bootstrapping Yoyo using the `registerComponents` method, then you can use the registered alias as the component name.\n\n```php\n$yoyo->registerComponent('search', App\\Yoyo\\LiveSearch::class);\n```\n\nFor anonymous components, the component name should match the template name without the file extension. So if the template name is `form.php`, the component can be rendered with:\n\n```php\n<?php echo yoyo_render('form'); ?>\n```\n\n### Rendering on updates\n\nUse the `yoyo_update` function to automatically process the component request and output the updated component.\n\n```php\n<?php echo yoyo_update(); ?>\n```\n\nYou need to add this function call for requests routed to the Yoyo `url` used in the initial configuration.\n\n## Properties\n\nIn dynamic components, all public properties in the component class are automatically made available to the view and tracked in component updates.\n\n```php\nclass HelloWorld extends Component\n{\n    public $message = 'Hello World!';\n}\n```\n\n```html\n<div>\n    <h1><?php echo $message; ?></h1>\n    <!-- Will output \"Hello World!\" -->\n</div>\n```\n\nPublic properties should only be of type: `string`, `int`, `array`, `boolean`, and should not contain any sensitive information because they can be used in component requests to keep the data in sync.\n\n### Initializing Properties\n\nYou can initialize properties using the `mount` method of your component which runs right after the component is instantiated, and before the `render` method.\n\n```php\nclass HelloWorld extends Component\n{\n    public $message;\n\n    public function mount()\n    {\n        $this->message = 'Hello World!';\n    }\n}\n```\n\n### Data Binding\n\nYou can automatically bind, or synchronize, the value of an HTML element with a component public property.\n\n```php\nclass HelloWorld extends Component\n{\n    public $message = 'Hello World!';\n}\n```\n\n```html\n<div>\n    <input yoyo name=\"message\" type=\"text\" value=\"<?php echo $message; ?>\">\n    <h1><?php echo $message;?></h1>\n</div>\n``` \n\nAdding the `yoyo` attribute to any input will instantly make it reactive. Any changes to the input will be updated in the component.\n\nBy the default, the natural event of an element will be used as the event trigger. \n\n- input, textarea and select elements are triggered on the change event.\n- form elements are triggered on the submit event.\n- All other elements are triggered on the click event.\n\nYou can modify this behavior using the `yoyo:on` directive which accepts multiple events separated by comma:\n\n ```html\n <input yoyo:on=\"keyup\" name=\"message\" type=\"text\" value=\"<?php echo $message; ?>\">\n ```\n\n### Debouncing and Throttling Requests\n\nThe are several ways to limit the requests to update components.\n\n**`delay`** - debounces the request so it's made only after the specified period passes after the last trigger.\n\n```html\n<input yoyo:on=\"keyup delay:300ms\" name=\"message\" type=\"text\" value=\"<?php echo $message; ?>\">\n```\n\n**`throttle`** limits request to one dwithin the specified interval.\n\n```html\n<input yoyo:on=\"input throttle:2s\" name=\"message\" type=\"text\" value=\"<?php echo $message; ?>\">\n```\n\n**`changed`** - only makes the request when the input value has changed.\n\n```html\n<input yoyo:on=\"keyup delay:300ms changed\" name=\"message\" type=\"text\" value=\"<?php echo $message; ?>\">\n```\n\n## Actions\n\nAn action is a request made to a Yoyo component method to update (re-render) it as a result of a user interaction or page event (click, mouseover, scroll, load, etc.).\n\nThe `render` method is the default action when one is not provided explicitly. You can also override it in the component class to change the template name or when you need to send additional variables to the template in addition to the public properties.\n\n```php\npublic function render() \n{\n\treturn $this->view($this->componentName, ['foo' => 'bar']);\n}\n```\n\nTo specify an action you use one of the available action directives with the name of the action as the value.\n\n- `yoyo:get`\n- `yoyo:post`\n- `yoyo:put`\n- `yoyo:patch`\n- `yoyo:delete`\n\nFor example:\n\n```php\nclass Review extends Component\n{\n    public Review $review;\n\n    public function helpful()\n    {\n        $this->review->userFoundHelpful($userId);\n    }\n}\n```\n\n```html\n<div>\n    <button yoyo:on=\"click\" yoyo:get=\"helpful\">Found Helpful</button>\n</div>\n```\n\nAll components automatically listen for the `refresh` event and trigger the `render` action to refresh the component state.\n\n### Passing Data to Actions\n\nYou can include additional data to send to the server on component update requests using the `yoyo:vals` directive which accepts a JSON encoded list of name-value pairs.\n\n```html\n<button yoyo:on=\"click\" yoyo:get=\"helpful\" yoyo:vals='{\"reviewId\":100}'>Found Helpful</button>\n\n<!-- Or use the encode_vals helper function to pass an array of name-value pairs -->\n<button yoyo:on=\"click\" yoyo:get=\"helpful\" yoyo:vals='<?php Yoyo\\encode_vals([\"reviewId\"=> 100]); ?>'>Found Helpful</button>\n```\n\nYou can also use `yoyo:val.name` for individual values. kebab-case variable names are automatically converted to camel-case.\n\n```html\n<button yoyo:on=\"click\" yoyo:get=\"helpful\" yoyo:val.review-id=\"100\">Found Helpful</button>\n```\n\nYoyo will automatically track and send component public properties and input values with every request. \n\n```php\nclass Review extends Component {\n\n\tpublic $reviewId;\n\n\tpublic function helpful()\n\t{\n\t\t// access reviewId via $this->reviewId\n\t}\n}\n```\n\nYou can also pass extra parameters to an action as arguments using an expression, without having to define them as public properties in the component:\n\n```html\n<button yoyo:get=\"addToCart(<?php echo $productId; ?>, '<?php echo $style; ?>')\">\n    Add Todo\n</button>\n```\n\nExtra parameters passed to an action are made available to the component method as regular arguments:\n\n```php\npublic function addToCart($productId, $style)\n{\n    // ...\n}\n```\n\n### Actions Without a Response\n\nSometimes you may want to use a component action only to make changes to a database and trigger events, without rendering a response. You can use the component `skipRender` method for this:\n\n```php\npublic function savePost() \n{\n\t// Store the post to the database\n\n\t// Send event to the browser to close modal, or trigger a notification\n\t$this->emitSelf('PostSaved');\n\n\t// Skip template rendering\n\t$this->skipRender();\n}\n```\n\n## View Data\n\nSometimes you want to send data to a view without declaring the variable as a public property. You can do this by defining a render method in your component and passing a data array as the second argument:\n\n```php\npublic function render() \n{\n\treturn $this->view($this->componentName, ['foo' => 'bar']);\n}\n```\n\nThen access the $foo variable in your template.\n\nYou can also send data to the component view using the `set` method in any component action. For example:\n\n```php\npublic function increment()\n{\n\t$this->set('foo', 'bar');\n\t// or\n\t$this->set(['foo' => 'bar']);\n}\n```\n\n## Computed Properties\n\n```php\nclass HelloWorld extends Component\n{\n\tpublic $message = 'Hello World!';\n\t\n   \t// Computed Property\n\tpublic function getHelloWorldProperty()\n\t{\n\t\treturn $message;\n\t}\n\n   \t// Computed Property with argument\n\tpublic function getErrorsProperty($name)\n\t{\n\t\treturn [\n\t\t\t'title' => 'Please enter a title',\n\t\t\t'description' => 'Please enter a description',\n\t\t][$name] ?? null;\n\t}\n}\n```\n\t\nNow, you can access `$this->hello_world` from either the component's class or template:\n\n```php\n<div>\n\t<h1><?php echo $this->hello_world ;?></h1>\n\t<!-- Will output \"Hello World!\" -->\n</div>\n```\n\nComputed properties with arguments behave like normal class methods that you can call in your templates:\n\n```php\n<div>\n\t<h1><?php echo $this->errors('title') ;?></h1>\n\t<!-- Will output \"Please enter a title\" -->\n</div>\n```\n\nThe output of computed properties is cached within the same component request, allowing you to perform complex tasks like querying the database and not duplicating the tasks if the property is accessed multiple times. If you need to clear the cache for a computed property:\n\n```php\n// Clear all computed properties, including those with arguments\n$this->forgetComputed();\n\n// Clear a single property\n$this->forgetComputed($property);\n\n// Clear multiple properties\n$this->forgetComputed([$property1, $property2]);\n\n// Clear a single computed property with arguments\n$this->forgetComputedWithArgs($property, $arg1, $arg2);\n```\n\n## Component Props\n\nYoyo can persist and update variables in requests without the need to explicitly include an input element.\n\nFor an anonymous component, it's possible to specify the props directly in the component root node using a comma-separated list of variable names and this allows implementing a counter without the need for a component class:\n\n```php\n<?php $count = $count ?? 0 ; ?>\n<div yoyo:props=\"count\">\n\t<button yoyo:val.count=\"<?php echo $count + 1; ?>\">+</button> \n    <p><?php echo $count; ?></p>\n</div>\n```\n\nBy adding the `yoyo:props=\"count\"`, Yoyo knows to automatically include the value of `count` in every request.\n\nFor dynamic components, there's no need to use the `yoyo:props` attribute because we use the protected method $props in the component class with an array of variable names.\n\n```php\nclass Counter extends Component\n{\n\tpublic $count = 0;\n\t\n\tprotected $props = ['count'];\n\n    public function increment()\n    {\n        $this->count++;\n    }\n}\n```\n\nSince the `$count` variable is also defined as a public property, it's already available in the template and the value is incremented throgh the `increment` method in the component class without having to use `yoyo:val.count`.\n\n```html\n<div>\n\t<button yoyo:get=\"increment\">+</button>\n\t<span><?php echo $count; ?></span>\n</div>\n```\n\n## Query String\n\nComponents have the ability to automatically update the browser's query string on state changes. \n\n```php\nclass Search extends Component\n{\n\tpublic $query;\n\t\n\tprotected $queryString = ['query'];\n}\n```\n\nYoyo is smart enough to automatically remove the query string when the current state value matches the property's default value.\n\nFor example, in a pagination component, you don't need the `?page=1` query string to appear in the URL.\n\n```php\nclass Posts extends Component\n{\n\tpublic $page = 1;\n\t\n\tprotected $queryString = ['page'];\n}\n```\n\n## Loading States\n\nUpdating Yoyo components requires an Ajax request to the server and depending on what the component does, the response time will vary. The `yoyo:spinning` directive allows you to do all sorts of cool things when a component is updating to provide a visual indicator to end-users.\n\n### Toggling Elements During Loading States\n\nTo show an element at the start of a Yoyo update request and hide it again when the update is complete:\n\n```html\n<div>\n    <button yoyo:post=\"submit\">Submit</button>\n\n    <div yoyo:spinning>\n        Processing your submission...\n    </div>\n</div>\n```\n\nYoyo adds some CSS to the page to automatically hide the element with the `yoyo:spinning` directive.\n\nTo hide a visible element while the component is updating you can add the `remove` modifier:\n\n```html\n<div>\n    <button yoyo:post=\"submit\">Submit</button>\n\n    <div yoyo:spinning.remove>\n        Text hidden while updating ...\n    </div>\n</div>\n```\n\n## Delaying Loading States\n\nSome actions may update quickly and showing a loading state in these cases may be more of a distraction. The `delay` modifier ensures that the loading state changes are applied only after 200ms if the component hasn't finished updating.\n\n```html\n<div>\n    <button yoyo:post=\"submit\">Submit</button>\n\n    <div yoyo:spinning.delay>\n        Processing your submission...\n    </div>\n</div>\n```\n\n### Targeting Specific Actions\n\nIf you need to toggle different indicators for different component actions, you can add the `yoyo:spin-on` directive and pass a comma separated list of action names. For example:\n\n```html\n<div>\n\t<button yoyo:get=\"edit\">Edit</button>\n\n\t<button yoyo:get=\"like\">Like</button>\n\n    <div yoyo:spinning yoyo:spin-on=\"edit\">\n        Show for edit action\n    </div>\n\n    <div yoyo:spinning yoyo:spin-on=\"like\">\n        Show for like action\n    </div>\n\n    <div yoyo:spinning yoyo:spin-on=\"edit, like\">\n        Show for edit and like actions\n    </div>\n\n</div>\n```\n\n## Toggling Element CSS Classes\n\nInstead of toggling the visibility of an element you can also add specific CSS classes while the component updates. Use the `class` modifier and include the space-separated class names as the attribute value:\n\n```html\n<div>\n    <button yoyo:post=\"submit\" yoyo:spinning.class=\"text-gray-300\">\n\t\tSubmit\n\t</button>\n</div>\n```\n\nYou can also remove specific class names by adding the `remove` modifier:\n\n```html\n<div>\n    <button yoyo:post=\"submit\" yoyo:spinning.class.remove=\"bg-blue-200\" class=\"bg-blue-200\">\n\t\tSubmit\n\t</button>\n</div>\n```\n\n## Toggling Element Attributes\n\nSimilar to CSS class toggling, you can also add or remove attributes while the component is updating.\n\n```html\n<div>\n    <button yoyo:post=\"submit\" yoyo:spinning.attr=\"disabled\">\n\t\tSubmit\n\t</button>\n</div>\n```\n\n## Events\n\nEvents are a great way to establish communication between Yoyo components on the same page, where one or more components can listen to events fired by another component.\n\nEvents can be fired from component methods and templates using a variety of emit methods.\n\nAll emit methods accept any number of arguments that allow sending data (string, number, array) to listeners.\n\n### Emitting an Event to All Yoyo Components\n\nFrom a component method.\n\n```php\npublic function increment()\n{\n\t$this->count++;\n\t\t\n\t$this->emit('counter-updated', $count);\n}\n```\n\nFrom a template\n\n```php\n<?php $this->emit('counter-updated', $count) ; ?>\n```\n\n### Emitting an Event to Parent Components\n\nWhen dealing with nested components you can emit events to parents and not children or sibling components.\n\n```php\n$this->emitUp('postWascreated', $arg1, $arg2);\n```\n\n### Emitting an Event to a Specific Component\n\nWhen you need to emit an event to a specific component using the component name (e.g. `cart`).\n\n```php\n$this->emitTo('cart', 'productAddedToCart', $arg1, $arg2);\n```\n\n### Emitting an Event to an Element Using a Selector\n\nThe `emitTo` method also works with selectors. When a component is not found, the selector is used instead. Emitting events using selectors doesn't support passing arguments. \n\n```php\n$this->emitTo('.cart', 'productAddedToCart');\n$this->emitTo('#cart', 'productAddedToCart');\n$this->emitTo('.post-100', 'saved');\n```\n\n#### Emitting an Event to Itself\n\nWhen you need to emit an event on the same component.\n\n```php\n$this->emitSelf('productAddedToCart', $arg1, $arg2);\n```\n\n### Listening for Events\n\nTo register listeners in Yoyo, use the `$listeners` protected property of the component.\n\nListeners are a key->value pair where the key is the event to listen for, and the value is the method to call on the component. If the event and method are the same, you can leave out the key.\n\n```php\nclass Counter extends Component {\n\n\tpublic $message;\n\n\tprotected $listeners = ['counter-updated' => 'showNewCount'];\n\n\tprotected function showNewCount($count)\n\t{\n\t\t$this->message = \"The new count is: $count\";\n\t}\n}\n```\n\n### Listening For Events In JavaScript\n\nYoyo allows registering event listeners for component emitted events:\n\n```js\n<script>\nYoyo.on('productAddedToCart', id => {\n\talert('A product was added to the cart with ID:' + id\n});\n</script>\n```\n\nWith this feature you can control toasters, alerts, modals, etc. directly from a component action on the server by emitting the event and listening for it on the browser.\n\n### Dispatching Events From JavaScript\n\nYou can also trigger component events directly from your custom JavaScript using the `Yoyo.dispatch()` method. This is useful when you need to interact with other JavaScript libraries or custom browser APIs.\n\n```js\n// Dispatch an event to all Yoyo components listening for it\nYoyo.dispatch('post-created');\n\n// You can also pass named parameters to the event listener\nYoyo.dispatch('post-created', { postId: 2 });\n\n// Dispatch an event to a specific component by name\nYoyo.dispatchTo('dashboard', 'post-created', { postId: 2 });\n```\n\nParameters are passed as named arguments that match the listener method's parameter names:\n\n```php\nprotected $listeners = [\n    'post-created' => 'handlePostCreated',\n];\n\npublic function handlePostCreated($postId)\n{\n    // $postId will be 2\n}\n```\n\n### Dispatching Browser Events\n\nIn addition to allowing components to communicate with each other, you can also send browser window events directly from a component method or template:\n\n```php\n// passing single value\n$this->dispatchBrowserEvent('counter-updated', $count);\n\n// Passing an array\n$this->dispatchBrowserEvent('counter-updated', ['count' => $count]);\n```\n\nAnd listen for the event anywhere on the page:\n\n```js\n<script>\nwindow.addEventListener('counter-updated', event => {\n\t// Reading a single value\n\talert('Counter is now: ' + event.detail);\n\n\t// Reading from an array\n\talert('Counter is now: ' + event.detail.count);\n})\n</script>\n```\n## Redirecting\n\nSometimes you may want to redirect the user to a different page after performing an action within a Yoyo component.\n\n```php\nclass Registration extends Component\n{\n    public function register()\n    {\n\t// Create the user \n\n\t$this->redirect('/welcome');\n    }\n}\n```\n\n## Response Headers\n\nYoyo components have access to `$this->response` which provides methods for controlling how HTMX handles the response. These map directly to [HTMX response headers](https://htmx.org/reference/#response_headers).\n\n### Retargeting\n\nOverride which element receives the swap:\n\n```php\npublic function save()\n{\n    // Swap the response into a different element instead of the component itself\n    $this->response->retarget('#notification-area');\n}\n```\n\n### Changing Swap Strategy\n\nOverride the swap strategy for the response:\n\n```php\npublic function update()\n{\n    // Use innerHTML instead of the default outerHTML swap\n    $this->response->reswap('innerHTML');\n}\n```\n\n### Selecting Response Content\n\nSelect a subset of the response HTML to swap:\n\n```php\npublic function load()\n{\n    // Only swap the #content portion of the response\n    $this->response->reselect('#content');\n}\n```\n\n### URL Management\n\nPush or replace the browser URL without a full page reload:\n\n```php\npublic function navigate()\n{\n    // Push a new URL to browser history\n    $this->response->pushUrl('/new-page');\n}\n\npublic function filter()\n{\n    // Replace the current URL without adding a history entry\n    $this->response->replaceUrl('/results?q=search');\n}\n```\n\n### Client-Side Navigation\n\nPerform a client-side redirect (AJAX-style, no full reload) or a full redirect:\n\n```php\npublic function softRedirect()\n{\n    // AJAX navigation — loads content without a full page reload\n    $this->response->location('/dashboard');\n}\n\npublic function fullRedirect()\n{\n    // Full page redirect via HX-Redirect header\n    $this->response->redirect('/login');\n}\n```\n\n> **Note:** `$this->response->redirect()` sets the `HX-Redirect` header (HTMX native redirect). This is different from `$this->redirect()` which uses Yoyo's own `Yoyo-Redirect` header. Both achieve a full page redirect but through different mechanisms.\n\n### Triggering Client-Side Events\n\nTrigger browser events from the server that JavaScript can listen for:\n\n```php\npublic function save()\n{\n    // Trigger immediately after the response is received\n    $this->response->trigger('item-saved');\n\n    // Trigger after the swap is complete\n    $this->response->triggerAfterSwap('swap-complete');\n\n    // Trigger after the settle phase (CSS transitions finished)\n    $this->response->triggerAfterSettle('settle-complete');\n}\n```\n\nListen for these events in JavaScript:\n\n```js\ndocument.body.addEventListener('item-saved', function() {\n    // Show a toast notification, update a counter, etc.\n});\n```\n\n### Full Page Refresh\n\nForce a full page refresh from a component action:\n\n```php\npublic function reset()\n{\n    $this->response->refresh();\n}\n```\n\n## Using Blade\n\nYou can use Yoyo with Laravel's [Blade](https://laravel.com/docs/8.x/blade) templating engine, without having to use Laravel.\n\n### Installation\n\nTo get started install the following packages in your project:\n\n```bash\ncomposer require clickfwd/yoyo\ncomposer require jenssegers/blade\n```\n\n### Configuration\n\nCreate a Blade instance and set it as the view provider for Yoyo. We also add the `YoyoServiceProvider` for Blade.\n\nThis code should run when rendering and updating components.\n\n```php\n<?php\n\nuse Clickfwd\\Yoyo\\Blade\\Application;\nuse Clickfwd\\Yoyo\\Blade\\YoyoServiceProvider;\nuse Clickfwd\\Yoyo\\ViewProviders\\BladeViewProvider;\nuse Clickfwd\\Yoyo\\Yoyo;\nuse Illuminate\\Contracts\\Foundation\\Application as ApplicationContract;\nuse Illuminate\\Contracts\\View\\Factory as ViewFactory;\nuse Illuminate\\Support\\Fluent;\nuse Jenssegers\\Blade\\Blade;\n\ndefine('APP_PATH', __DIR__);\n\n$yoyo = new Yoyo();\n\n$yoyo->configure([\n  'url' => 'yoyo',\n  'scriptsPath' => APP_PATH.'/app/resources/assets/js/',\n  'namespace' => 'App\\\\Yoyo\\\\',\n]);\n\n// Create a Blade instance\n\n$app = Application::getInstance();\n\n$app->bind(ApplicationContract::class, Application::class);\n\n// Needed for Blade anonymous components\n\n$app->alias('view', ViewFactory::class);\n\n$app->extend('config', function (array $config) {\n    return new Fluent($config);\n});\n\n$blade = new Blade(\n    [\n        APP_PATH.'/resources/views',\n        APP_PATH.'/resources/views/yoyo',\n        APP_PATH.'/resources/views/components',\n    ],\n    APP_PATH.'/../cache',\n    $app\n);\n\n$app->bind('view', function () use ($blade) {\n    return $blade;\n});\n\n(new YoyoServiceProvider($app))->boot();\n\n// Optionally register Blade components\n\n$blade->compiler()->components([\n    // 'button' => 'button',\n]);\n\n// Register Blade view provider for Yoyo\n\n$yoyo->registerViewProvider(function() use ($blade) {\n    return new BladeViewProvider($blade);\n});\n```\n\n### Load Assets\n\nFind `yoyo.js` in the following vendor path and copy it to your project's public assets directory.\n\n```file\n/vendor/clickfwd/yoyo/src/assets/js/yoyo.js \n```\n\nTo load the necessary scripts in your Blade template you can use the `yoyo_scripts` directive in the `<head>` tag:\n\n```blade\n@yoyo_scripts\n```\n\n### Rendering a Blade View\n\nYou can use the Blade instance to render any Blade view.\n\n```\n$blade = \\Clickfwd\\Yoyo\\Yoyo::getViewProvider()->getProviderInstance();\n\necho $blade->render('home');\n```\n\n### Rendering Yoyo Blade Components\n\nTo render Yoyo components inside Blade views, use the `@yoyo` directive.\n\n```blade\n@yoyo('search')\n```\n\n### Updating Yoyo Blade Components\n\nTo update Yoyo components in the Yoyo-designated route.\n\n```php\necho (new \\Clickfwd\\Yoyo\\Blade\\Yoyo())->update();\n```\n\n### Inline Views\n\nWhen dealing with simple templates, you can create components without a template file and instead return an inline view in the component's `render` method.\n\n```php\nclass HelloWorld extends Component\n{\n    public $message = 'Hello World!';\n}\n\npublic function render()\n{\n\treturn <<<'yoyo'\n\t\t<div>\n\t\t    <input yoyo name=\"message\" type=\"text\" value=\"{{ $message }}\">\n\t\t    <h1>{{ $message }}</h1>\n\t\t</div>\t\t\n\tyoyo;\n}\n```\n\n### Other Blade Features\n\nYoyo implements several Blade directives that can be used within Yoyo component templates.\n\n- `@spinning` and `@endspinning` - Check if a component is being re-rendered. \n\n\t```blade\n\t@spinnning\n\tComponent updated\n\t@endspinning\n\t\n\t@spinning($liked == 1)\n\tComponent updated and liked == 1\n\t@endspinning\n\t```\n\n- All event methods are available as directives within blade components\n\n\t```blade\n\t@emit('eventName', ['foo' => 'bar']);\n\t@emitUp('eventName', ['foo' => 'bar']);\n\t@emitSelf('eventName', ['foo' => 'bar']);\n\t@emitTo('component-name', 'eventName', ['foo' => 'bar']);\n\t```\n    \n- Computed properties\n\n\t```php\n\tclass HelloWorld extends Component\n\t{\n\t    public $message = 'Hello World!';\n\n\t    public function getHelloWorldProperty()\n\t    {\n\t\t    return $message;\n\t    }\n\t}\n\t```\n\n\t```blade\n\t<div>\n\t    <h1>{{ $this->hello_world }}</h1>\n\t    <!-- Will output \"Hello World!\" -->\n\t</div>\n\t```\n\n## Using Twig\n\nYou can use Yoyo with Symfony's [Twig](https://twig.symfony.com/) templating engine.\n\n### Installation\n\nTo get started install the following packages in your project:\n\n```bash\ncomposer require clickfwd/yoyo\ncomposer require twig/twig\n```\n\n### Configuration \n\nCreate a Twig instance and set it as the view provider for Yoyo. We also add the `YoyoTwigExtension` to Twig.\n\nThis code should run when rendering and updating components.\n\n```php\nuse Clickfwd\\Yoyo\\Twig\\YoyoTwigExtension;\nuse Clickfwd\\Yoyo\\ViewProviders\\TwigViewProvider;\nuse Clickfwd\\Yoyo\\Yoyo;\nuse Twig\\Extension\\DebugExtension;\n\ndefine('APP_PATH', __DIR__);\n\n$yoyo = new Yoyo();\n\n$yoyo->configure([\n  'url' => 'yoyo',\n  'scriptsPath' => APP_PATH.'/app/resources/assets/js/',\n  'namespace' => 'App\\\\Yoyo\\\\',\n]);\n\n$loader = new \\Twig\\Loader\\FilesystemLoader([\n  APP_PATH.'/resources/views',\n  APP_PATH.'/resources/views/yoyo',\n]);\n\n$twig = new \\Twig\\Environment($loader, [\n  'cache' => APP_PATH.'/../cache',\n  'auto_reload' => true,\n  // 'debug' => true\n]);\n\n// Add Yoyo's Twig Extension\n\n$twig->addExtension(new YoyoTwigExtension());\n\n// Register Twig view provider for Yoyo\n\n$yoyo->registerViewProvider(function() use ($twig) {\n  return new TwigViewProvider($twig);\n});\n```\n\n### Load Assets\n\nFind `yoyo.js` in the following vendor path and copy it to your project's public assets directory.\n\n```file\n/vendor/clickfwd/yoyo/src/assets/js/yoyo.js \n```\n\nTo load the necessary scripts in your Twig template you can use the `yoyo_scripts` function in the `<head>` tag:\n\n```twig\n{{ yoyo_scripts() }}\n```\n\n### Rendering a Twig View\n\nYou can use the Twig instance to render any Twig view.\n\n```\n$twig = \\Clickfwd\\Yoyo\\Yoyo::getViewProvider()->getProviderInstance();\n\necho $twig->render('home');\n```\n\n### Rendering Yoyo Twig Components\n\nTo render Yoyo components inside Twig views, use the `yoyo` function.\n\n```twig\nyoyo('search')\n```\n\n### Updating Yoyo Twig Components\n\nTo update Yoyo components in the Yoyo-designated route.\n\n```php\necho (new \\Clickfwd\\Yoyo\\Yoyo())->update();\n```\n\n### Inline Views\n\nWhen dealing with simple templates, you can create components without a template file and instead return an inline view in the component's `render` method.\n\n```php\nclass HelloWorld extends Component\n{\n    public $message = 'Hello World!';\n}\n\npublic function render()\n{\n\treturn <<<'twig'\n\t\t<div>\n\t\t    <input yoyo name=\"message\" type=\"text\" value=\"{{ message }}\">\n\t\t    <h1>{{ message }}</h1>\n\t\t</div>\t\t\n\ttwig;\n}\n```\n\n### Other Twig Features\n\nYoyo adds a few functions and variables that can be used within Yoyo component templates.\n\n- The `spinning` variable can be used to check if a component is being re-rendered. \n\n\t```twig\n\t{% if spinning %}\n\tComponent updated\n\t{% endif %}\n\t```\t\n\n- All event methods are available as functions within blade components\n\t\n\t```twig\n\t{{ emit('eventName', {'foo':'bar'}) }}\n\t{{ emitUp('eventName', {'foo':'bar'}) }}\n\t{{ emitSelf('eventName', {'foo':'bar'}) }}\n\t{{ emitTo('component-name', 'eventName', {'foo':'bar'}) }}\n\t```\n\t\n- Computed properties\n\n\t```php\n\tclass HelloWorld extends Component\n\t{\n\t    public $message = 'Hello World!';\n\n\t    public function getHelloWorldProperty()\n\t    {\n\t\t    return $this->message;\n\t    }\n\t}\n\t```\n\t\n\t```twig\n\t<div>\n\t\t<h1>{{ this.hello_world }}</h1>\n\t\t<!-- Will output \"Hello World!\" -->\n\t</div>\n\t```\n\n\n## License\n\nCopyright © ClickFWD\n\nYoyo is open-sourced software licensed under the [MIT license](LICENSE.md).\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"clickfwd/yoyo\",\n    \"description\": \"Framework to build dynamic interfaces with seamless communication between frontend and backend.\",\n    \"keywords\": [\n        \"framework\",\n        \"yoyo\"\n    ],\n    \"license\": \"MIT\",\n    \"homepage\": \"https://github.com/Clickfwd/yoyo\",\n    \"support\": {\n        \"issues\": \"https://github.com/Clickfwd/yoyo/issues\",\n        \"source\": \"https://github.com/Clickfwd/yoyo\"\n    },\n    \"authors\": [\n        {\n            \"name\": \"Clickfwd\"\n        }\n    ],\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"require\": {\n        \"php\": \"^8.0\",\n        \"psr/container\": \"^1.1.1|^2.0.1\"\n    },\n    \"suggest\": {\n        \"illuminate/container\": \"Required for advanced dependency injection or Blade integration\"\n    },\n    \"require-dev\": {\n        \"phpunit/phpunit\": \"^12.0\",\n        \"pestphp/pest\": \"^4.0\",\n        \"pestphp/pest-plugin-browser\": \"^4.0\",\n        \"jenssegers/blade\": \"^2.0\",\n        \"symfony/var-dumper\": \"^5.2|^6.0|^7.0\",\n        \"spatie/ray\": \"^1.20\",\n        \"twig/twig\": \"^3.4\",\n        \"illuminate/container\": \"^8.0||^9.0||^10.0||^11.0||^12.0\",\n        \"friendsofphp/php-cs-fixer\": \"^3.39\"\n    },\n    \"autoload\": {\n        \"files\": [\n            \"src/yoyo/helpers.php\"\n        ],\n        \"psr-4\": {\n            \"Clickfwd\\\\Yoyo\\\\\": \"src/yoyo\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Tests\\\\App\\\\\": \"tests/app\",\n            \"Tests\\\\AppAnother\\\\\": \"tests/app-another\",\n            \"Tests\\\\Browser\\\\\": \"tests/Browser\",\n            \"Tests\\\\Browser\\\\Components\\\\\": \"tests/Browser/Components\"\n        }\n    },\n    \"config\": {\n        \"allow-plugins\": {\n            \"pestphp/pest-plugin\": true\n        }\n    }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"yoyo-tests\",\n  \"private\": true,\n  \"scripts\": {\n    \"test:browser\": \"./vendor/bin/pest --testsuite Browser\",\n    \"test:browser:headed\": \"./vendor/bin/pest --testsuite Browser --headed\"\n  },\n  \"devDependencies\": {\n    \"playwright\": \"^1.54.1\"\n  }\n}\n"
  },
  {
    "path": "phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNamespaceSchemaLocation=\"./vendor/phpunit/phpunit/phpunit.xsd\"\n         bootstrap=\"vendor/autoload.php\"\n         colors=\"true\"\n>\n    <testsuites>\n        <testsuite name=\"Unit Suite\">\n            <directory suffix=\"Test.php\">./tests/Unit</directory>\n        </testsuite>\n        <testsuite name=\"Feature Suite\">\n            <directory suffix=\"Test.php\">./tests/Feature</directory>\n        </testsuite>\n        <testsuite name=\"Browser\">\n            <directory suffix=\"Test.php\">./tests/Browser</directory>\n        </testsuite>\n        <testsuite name=\"Benchmark\">\n            <directory suffix=\"Test.php\">./tests/Benchmark</directory>\n        </testsuite>\n    </testsuites>\n    <source>\n        <include>\n            <directory suffix=\".php\">./src</directory>\n        </include>\n    </source>\n</phpunit>"
  },
  {
    "path": "src/assets/js/yoyo.js",
    "content": "; (function (global, factory) {\n\tif (typeof define === 'function' && define.amd) {\n\t\tdefine([], factory)\n\t} else {\n\t\tglobal.Yoyo = factory()\n\t}\n})(typeof self !== 'undefined' ? self : this, function () {\n\treturn (function () {\n\t\t'use strict'\n\n\t\twindow.YoyoEngine = window.htmx\n\n\t\twindow.addEventListener('popstate', (event) => {\n\t\t\tevent?.state?.yoyo?.forEach((state) =>\n\t\t\t\trestoreComponentStateFromHistory(state)\n\t\t\t)\n\t\t})\n\n\t\tvar Yoyo = {\n\t\t\turl: null,\n\t\t\tconfig(options) {\n\t\t\t\tObject.keys(options).forEach((key) => {\n\t\t\t\t\tYoyoEngine.config[key] = options[key]\n\t\t\t\t})\n\t\t\t},\n\t\t\ton(name, callback) {\n\t\t\t\tYoyoEngine.on(window, name, (event) => {\n\t\t\t\t\tdelete event.detail.elt\n\t\t\t\t\tcallback(event.detail)\n\t\t\t\t})\n\t\t\t},\n\t\t\tdispatch(eventName, params = null) {\n\t\t\t\tthis.processEmitEvents(document.body, [\n\t\t\t\t\t{ event: eventName, params: params }\n\t\t\t\t])\n\t\t\t},\n\t\t\tdispatchTo(componentName, eventName, params = null) {\n\t\t\t\tif (!/^[a-zA-Z0-9._-]+$/.test(componentName)) return\n\t\t\t\tthis.processEmitEvents(document.body, [\n\t\t\t\t\t{ event: eventName, params: params, component: componentName }\n\t\t\t\t])\n\t\t\t},\n\t\t\tcreateNonExistentIdTarget(targetId) {\n\t\t\t\t// Dynamically create non-existent target IDs by appending them to document body\n\t\t\t\tif (\n\t\t\t\t\ttargetId &&\n\t\t\t\t\ttargetId[0] == '#' &&\n\t\t\t\t\tdocument.querySelector(targetId) === null\n\t\t\t\t) {\n\t\t\t\t\tlet targetDiv = document.createElement('div')\n\t\t\t\t\ttargetDiv.setAttribute('id', targetId.replace('#', ''))\n\t\t\t\t\tdocument.body.appendChild(targetDiv)\n\t\t\t\t}\n\t\t\t},\n\t\t\tafterProcessNode(evt) {\n\t\t\t\t// Create non-existent target\n\t\t\t\tif (evt.detail.elt) {\n\t\t\t\t\tthis.createNonExistentIdTarget(\n\t\t\t\t\t\tevt.detail.elt.getAttribute('hx-target')\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\t// Initialize spinners\n\t\t\t\tlet component\n\n\t\t\t\tif (!evt.detail.elt || !isComponent(evt.detail.elt)) {\n\t\t\t\t\t// For innerHTML swap find the component root node\n\t\t\t\t\tcomponent = YoyoEngine.closest(\n\t\t\t\t\t\tevt.detail.elt,\n\t\t\t\t\t\t'[hx-swap~=innerHTML]'\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\tcomponent = getComponent(evt.detail.elt)\n\t\t\t\t}\n\n\t\t\t\t// Fallback: try to find the nearest component from evt.detail.elt or evt.detail.target\n\t\t\t\tif (!component && evt.detail.elt) {\n\t\t\t\t\tcomponent = getComponent(evt.detail.elt);\n\t\t\t\t}\n\t\t\t\tif (!component && evt.detail.target) {\n\t\t\t\t\tcomponent = getComponent(evt.detail.target);\n\t\t\t\t}\n\n\t\t\t\tif (!component) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tinitializeComponentSpinners(component)\n\t\t\t},\n\t\t\tbootstrapRequest(evt) {\n\t\t\t\tconst elt = evt.detail.elt\n\t\t\t\tlet component = getComponent(elt)\n\t\t\t\tconst componentName = getComponentName(component)\n\n\t\t\t\tif (evt.detail.path === document.location.href) {\n\t\t\t\t\tevt.detail.path = 'render'\n\t\t\t\t}\n\n\t\t\t\t// Includes the commonly-used X-Requested-With header that identifies ajax requests in many backend frameworks\n\t\t\t\tevt.detail.headers['X-Requested-With'] = 'XMLHttpRequest'\n\n\t\t\t\tconst action = getActionAndParseArguments(evt.detail)\n\n\t\t\t\tevt.detail.parameters[\n\t\t\t\t\t'component'\n\t\t\t\t] = `${componentName}/${action}`\n\t\t\t\tevt.detail.path = Yoyo.url\n\n\t\t\t\t// Make request info available to other events\n\t\t\t\tcomponentAddYoyoData(component, { action })\n\n\t\t\t\teventsMiddleware(evt)\n\t\t\t},\n\t\t\tprocessRedirectHeader(xhr) {\n\t\t\t\tif (xhr.getAllResponseHeaders().match(/Yoyo-Redirect:/i)) {\n\t\t\t\t\tconst url = xhr.getResponseHeader('Yoyo-Redirect')\n\t\t\t\t\tif (url) {\n\t\t\t\t\t\twindow.location = url\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t},\n\t\t\tprocessEmitEvents(elt, events) {\n\t\t\t\tif (!events || events == '[]') return\n\n\t\t\t\tevents = typeof events == 'string' ? JSON.parse(events) : events\n\n\t\t\t\tyoyoEventCache.clear()\n\n\t\t\t\tevents.forEach((event) => {\n\t\t\t\t\ttriggerServerEmittedEvent(elt, event)\n\t\t\t\t})\n\t\t\t},\n\t\t\tprocessBrowserEvents(events) {\n\t\t\t\tif (!events) return\n\n\t\t\t\tevents = typeof events == 'string' ? JSON.parse(events) : events\n\n\t\t\t\tevents.forEach((event) => {\n\t\t\t\t\twindow.dispatchEvent(\n\t\t\t\t\t\tnew CustomEvent(event.event, {\n\t\t\t\t\t\t\tdetail: event.params,\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t})\n\t\t\t},\n\t\t\tbeforeRequestActions(elt) {\n\t\t\t\tlet component = getComponent(elt)\n\n\t\t\t\tspinningStart(component)\n\t\t\t},\n\t\t\tafterOnLoadActions(evt) {\n\t\t\t\tlet component = getComponentById(evt.detail.target.id)\n\n\t\t\t\tif (!component) {\n\t\t\t\t\tif (!evt.detail.elt) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\t// Needed when using yoyo:select to replace a specific part of the response\n\t\t\t\t\t// so stop spinning callbacks are run to remove animations in the parts of the component\n\t\t\t\t\t// that were not replaced\n\t\t\t\t\tcomponent = getComponent(evt.detail.elt)\n\t\t\t\t\tif (component) {\n\t\t\t\t\t\tspinningStop(component)\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tcomponentCopyYoyoDataFromTo(evt.detail.target, component)\n\n\t\t\t\t// For 204 No Content responses or empty responses, explicitly stop spinners\n\t\t\t\t// since no DOM swap occurs that would naturally clean up spinner states\n\t\t\t\tconst xhr = evt.detail.xhr\n\t\t\t\tif (xhr.status === 204 || !xhr.responseText) {\n\t\t\t\t\tspinningStop(component)\n\t\t\t\t}\n\n\t\t\t\t// This isn't needed at this time because the CSS classes/attributes are\n\t\t\t\t// automatically removed when a component is updated from the server\n\t\t\t\t// however, could be useful to improve transitions in the future. It would\n\t\t\t\t// be necessary to add back spinner classes before new HTML is swapped in\n\t\t\t\t// spinningStop(component)\n\n\t\t\t\t// Timeout needed for targets outside of Yoyo component\n\t\t\t\tsetTimeout(() => {\n\t\t\t\t\tremoveEventListenerData(component)\n\t\t\t\t}, 125)\n\t\t\t},\n\t\t\tafterSettleActions(evt) {\n\t\t\t\t/// HISTORY\n\t\t\t\t// At this time, browser history support only works with outerHTML swaps\n\t\t\t\tconst component = getComponentById(evt.detail.elt.id)\n\n\t\t\t\tif (!component) return\n\n\t\t\t\tconst xhr = evt.detail.xhr\n\t\t\t\t// Browser history automatically enabled for components with queryStrings\n\t\t\t\tlet history = component.hasAttribute('yoyo:history')\n\t\t\t\tlet pushedUrl = xhr.getResponseHeader('Yoyo-Push')\n\t\t\t\tlet triggerId =\n\t\t\t\t\tevt.detail.requestConfig?.triggerEltInfo?.id ||\n\t\t\t\t\tevt.detail.requestConfig.headers['HX-Trigger']\n\t\t\t\tlet href = triggerId\n\t\t\t\t\t? evt.detail.requestConfig?.triggerEltInfo?.href\n\t\t\t\t\t: false\n\n\t\t\t\t// If reactive element has an href tag, override history setting and add the component and href to browser history\n\t\t\t\tif (triggerId && href) {\n\t\t\t\t\tpushedUrl = href\n\t\t\t\t\thistory = true\n\t\t\t\t}\n\n\t\t\t\tconst url =\n\t\t\t\t\tpushedUrl !== null ? pushedUrl : window.location.href\n\n\t\t\t\tif (\n\t\t\t\t\t!history ||\n\t\t\t\t\t!pushedUrl ||\n\t\t\t\t\tcomponent?.__yoyo?.replayingHistory\n\t\t\t\t) {\n\t\t\t\t\tcomponent.__yoyo.replayingHistory = false\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tcomponentAddYoyoData(component, {\n\t\t\t\t\teffects: {\n\t\t\t\t\t\tbrowserEvents:\n\t\t\t\t\t\t\txhr.getResponseHeader('Yoyo-Browser-Event'),\n\t\t\t\t\t\temitEvents: xhr.getResponseHeader('Yoyo-Emit'),\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\tconst componentName = getComponentName(component)\n\n\t\t\t\tYoyoEngine.findAll(\n\t\t\t\t\tevt.detail.target,\n\t\t\t\t\t'[yoyo\\\\:history=remove]'\n\t\t\t\t).forEach((node) => node.remove())\n\n\t\t\t\t// Before pushing a component to the browser history, we need to take a snapshot\n\t\t\t\t// of its initial rendered-HTML to store it in the current state\n\t\t\t\t// This also works for components loaded dynamically onto the page, like modals\n\t\t\t\tif (!componentAlreadyInCurrentHistoryState(component)) {\n\t\t\t\t\tupdateState(\n\t\t\t\t\t\t'replaceState',\n\t\t\t\t\t\tdocument.location.href,\n\t\t\t\t\t\tcomponent,\n\t\t\t\t\t\ttrue,\n\t\t\t\t\t\tevt.detail.target.outerHTML\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\tif (\n\t\t\t\t\t!history?.state?.yoyo ||\n\t\t\t\t\thistory?.state?.initialState ||\n\t\t\t\t\turl !== window.location.href\n\t\t\t\t) {\n\t\t\t\t\tupdateState('pushState', url, component)\n\t\t\t\t} else {\n\t\t\t\t\tupdateState('replaceState', url, component)\n\t\t\t\t}\n\t\t\t},\n\t\t}\n\n\t\t/**\n\t\t * Tracking for elements receiving multiple emitted events to only trigger the first one\n\t\t */\n\t\tlet yoyoEventCache = new Set()\n\n\t\tlet yoyoSpinners = {}\n\n\t\tfunction getActionAndParseArguments(detail) {\n\t\t\tconst m = detail.path.match(/^(.+?)\\((.*)\\)$/);\n\t\t\tif (!m) {\n\t\t\t\t// no “(…)” → action is the full path\n\t\t\t\treturn detail.path;\n\t\t\t}\n\n\t\t\tconst [, actionName, rawArgs] = m;\n\t\t\tconst args = rawArgs.trim() === ''\n\t\t\t\t? []\n\t\t\t\t: rawArgs\n\t\t\t\t\t.split(/\\s*,\\s*/)       // split on commas + trim\n\t\t\t\t\t.map(parseArg);\n\n\t\t\tdetail.parameters.actionArgs = JSON.stringify(args);\n\t\t\treturn actionName;\n\t\t}\n\n\t\tfunction parseArg(token) {\n\t\t\t// quoted string?\n\t\t\tif (/^['\"].*['\"]$/.test(token)) {\n\t\t\t\treturn token.slice(1, -1);\n\t\t\t}\n\t\t\t// boolean literals?\n\t\t\tif (token === 'true') return true;\n\t\t\tif (token === 'false') return false;\n\t\t\t// try number\n\t\t\tconst num = Number(token);\n\t\t\tif (!isNaN(num) && isFinite(num)) {\n\t\t\t\treturn num;\n\t\t\t}\n\t\t\t// fallback to raw string\n\t\t\treturn token;\n\t\t}\n\n\t\tfunction isComponent(elt) {\n\t\t\treturn elt?.hasAttribute('yoyo:name')\n\t\t}\n\n\t\tfunction getComponent(elt) {\n\t\t\tlet component = elt.closest('[yoyo\\\\:name]')\n\t\t\tif (component) {\n\t\t\t\tcomponent.__yoyo = component?.__yoyo || {}\n\t\t\t}\n\t\t\treturn component\n\t\t}\n\n\t\tfunction getAllcomponents() {\n\t\t\treturn document.querySelectorAll('[yoyo\\\\:name]')\n\t\t}\n\n\t\tfunction getComponentById(componentId) {\n\t\t\tif (!componentId) return null\n\n\t\t\tconst component = document.querySelector(`#${componentId}`)\n\n\t\t\treturn isComponent(component) ? component : null\n\t\t}\n\n\t\tfunction getComponentName(component) {\n\t\t\treturn component.getAttribute('yoyo:name')\n\t\t}\n\n\t\tfunction getComponentFingerprint(component) {\n\t\t\treturn `${getComponentName(\n\t\t\t\tcomponent\n\t\t\t)}:${getComponentIndex(component)}`\n\t\t}\n\n\t\tfunction getComponentsByName(name) {\n\t\t\treturn Array.from(\n\t\t\t\tdocument.querySelectorAll(`[yoyo\\\\:name=\"${name}\"]`)\n\t\t\t)\n\t\t}\n\n\t\t// Index as it appears on the page relative to other same-named components\n\t\tfunction getComponentIndex(component) {\n\t\t\tconst name = getComponentName(component)\n\t\t\tconst components = getComponentsByName(name)\n\t\t\treturn components.indexOf(component)\n\t\t}\n\n\t\tfunction getAncestorcomponents(selector) {\n\t\t\tlet ancestor = getComponent(document.querySelector(selector))\n\t\t\tlet ancestors = []\n\n\t\t\twhile (ancestor) {\n\t\t\t\tancestors.push(ancestor)\n\t\t\t\tancestor = getComponent(ancestor.parentElement)\n\t\t\t}\n\n\t\t\t// Remove the current component\n\t\t\tancestors.shift()\n\t\t\treturn ancestors\n\t\t}\n\n\t\tfunction shouldTriggerYoyoEvent(elt, eventName) {\n\t\t\tlet key\n\t\t\tif (isComponent(elt)) {\n\t\t\t\tkey = `${elt.id}${eventName}`\n\t\t\t} else if (elt.selector !== undefined) {\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\tif (key && !yoyoEventCache.has(key)) {\n\t\t\t\tyoyoEventCache.add(key)\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\treturn false\n\t\t}\n\n\t\tfunction eventsMiddleware(evt) {\n\t\t\tconst component = getComponent(evt.detail.elt)\n\t\t\tconst componentName = getComponentName(component)\n\t\t\tconst eventData = component.__yoyo.eventListener\n\n\t\t\tif (!eventData) return\n\n\t\t\tevt.detail.parameters[\n\t\t\t\t'component'\n\t\t\t] = `${componentName}/${eventData.name}`\n\n\t\t\tif (eventData.params) {\n\t\t\t\tdelete eventData.params.elt\n\t\t\t}\n\n\t\t\tevt.detail.parameters = {\n\t\t\t\t...evt.detail.parameters,\n\t\t\t\t...{\n\t\t\t\t\teventParams: eventData.params\n\t\t\t\t\t\t? JSON.stringify(eventData.params)\n\t\t\t\t\t\t: [],\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\n\t\tfunction addEmittedEventParametersToListenerComponent(\n\t\t\tcomponent,\n\t\t\tevent,\n\t\t\tparams\n\t\t) {\n\t\t\t// Check if Yoyo component is listening for the event\n\t\t\tlet componentListeningFor = component\n\t\t\t\t.getAttribute('hx-trigger')\n\t\t\t\t.split(',')\n\t\t\t\t.filter((name) => name.trim())\n\n\t\t\tif (componentListeningFor.indexOf(event) === -1) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tcomponentAddYoyoData(component, {\n\t\t\t\teventListener: { name: event, params: params },\n\t\t\t})\n\t\t}\n\n\t\tfunction triggerServerEmittedEvent(elt, event) {\n\t\t\tconst component = getComponent(elt)\n\t\t\tconst eventName = event.event\n\t\t\tconst params = event.params\n\t\t\tconst selector = event.selector || null\n\t\t\tconst componentName = event.component || null\n\t\t\tconst propagation = event.propagation || null\n\t\t\tlet elements\n\n\t\t\tif (!component && (propagation === 'self' || selector)) return\n\n\t\t\t// emit\n\t\t\tif (!selector && !componentName) {\n\t\t\t\telements = getAllcomponents()\n\t\t\t} else if (componentName) {\n\t\t\t\t// emitUp\n\t\t\t\tif (propagation == 'ancestorsOnly') {\n\t\t\t\t\telements = getAncestorcomponents(selector)\n\t\t\t\t\t// emitSelf\n\t\t\t\t} else if (propagation == 'self') {\n\t\t\t\t\telements = [component]\n\t\t\t\t\t// emitTo\n\t\t\t\t} else {\n\t\t\t\t\telements = getComponentsByName(componentName)\n\t\t\t\t}\n\t\t\t\t// emitWithSelector, excludes current component to allow replication without udpating the current component twice\n\t\t\t} else if (selector) {\n\t\t\t\telements = document.querySelectorAll(selector)\n\t\t\t\telements = Array.from(elements).filter(\n\t\t\t\t\t(element) => !component.contains(element)\n\t\t\t\t)\n\t\t\t\telements.forEach((elt) => (elt.selector = selector))\n\t\t\t}\n\n\t\t\tif (elements.length) {\n\t\t\t\telements.forEach((elt) => {\n\t\t\t\t\tif (shouldTriggerYoyoEvent(elt, eventName)) {\n\t\t\t\t\t\taddEmittedEventParametersToListenerComponent(\n\t\t\t\t\t\t\tgetComponent(elt),\n\t\t\t\t\t\t\teventName,\n\t\t\t\t\t\t\tparams\n\t\t\t\t\t\t)\n\t\t\t\t\t\tYoyoEngine.trigger(elt, eventName, params)\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tfunction removeEventListenerData(component) {\n\t\t\tdelete component.__yoyo.eventListener\n\t\t}\n\n\t\t/**\n\t\t * Component loading state spinners\n\t\t */\n\n\t\tfunction spinningStart(component) {\n\t\t\tconst componentId = component.id\n\n\t\t\tif (!yoyoSpinners[componentId]) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tlet spinningElts = yoyoSpinners[componentId].generic || []\n\n\t\t\tspinningElts = spinningElts.concat(\n\t\t\t\tyoyoSpinners[componentId]?.actions[component.__yoyo.action] ||\n\t\t\t\t[]\n\t\t\t)\n\n\t\t\tdelete yoyoSpinners[component.id]\n\n\t\t\tspinningElts.forEach((directive) => {\n\t\t\t\tconst spinnerElt = directive.elt\n\t\t\t\tif (directive.modifiers.includes('class')) {\n\t\t\t\t\tlet classes = directive.value.split(' ').filter(Boolean)\n\n\t\t\t\t\tdoAndSetCallbackOnElToUndo(\n\t\t\t\t\t\tcomponent,\n\t\t\t\t\t\tdirective,\n\t\t\t\t\t\t() => directive.elt.classList.add(...classes),\n\t\t\t\t\t\t() => spinnerElt.classList.remove(...classes)\n\t\t\t\t\t)\n\t\t\t\t} else if (directive.modifiers.includes('attr')) {\n\t\t\t\t\tdoAndSetCallbackOnElToUndo(\n\t\t\t\t\t\tcomponent,\n\t\t\t\t\t\tdirective,\n\t\t\t\t\t\t() => directive.elt.setAttribute(directive.value, true),\n\t\t\t\t\t\t() => spinnerElt.removeAttribute(directive.value)\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\tdoAndSetCallbackOnElToUndo(\n\t\t\t\t\t\tcomponent,\n\t\t\t\t\t\tdirective,\n\t\t\t\t\t\t() => (spinnerElt.style.display = 'inline-block'),\n\t\t\t\t\t\t() => (spinnerElt.style.display = 'none')\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\n\t\tfunction spinningStop(component) {\n\t\t\twhile (component.__yoyo_on_finish_loading.length > 0) {\n\t\t\t\tcomponent.__yoyo_on_finish_loading.shift()()\n\t\t\t}\n\t\t}\n\n\t\tfunction initializeComponentSpinners(component) {\n\t\t\tconst componentId = component.id\n\t\t\tcomponent.__yoyo_on_finish_loading = []\n\n\t\t\twalk(component, (elt) => {\n\t\t\t\tconst directive = extractModifiersAndValue(elt, 'spinning')\n\t\t\t\tif (directive) {\n\t\t\t\t\tconst yoyoSpinOnAction = elt.getAttribute('yoyo:spin-on')\n\t\t\t\t\tif (yoyoSpinOnAction) {\n\t\t\t\t\t\tyoyoSpinOnAction\n\t\t\t\t\t\t\t.replace(' ', '')\n\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t.forEach((action) => {\n\t\t\t\t\t\t\t\taddActionSpinner(componentId, action, directive)\n\t\t\t\t\t\t\t})\n\t\t\t\t\t} else {\n\t\t\t\t\t\taddGenericSpinner(componentId, directive)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\n\t\tfunction checkSpinnerInitialized(componentId, action) {\n\t\t\tyoyoSpinners[componentId] = yoyoSpinners[componentId] || {\n\t\t\t\tactions: {},\n\t\t\t\tgeneric: [],\n\t\t\t}\n\t\t\tif (\n\t\t\t\taction &&\n\t\t\t\tyoyoSpinners?.[componentId]?.actions?.[action] === undefined\n\t\t\t) {\n\t\t\t\tyoyoSpinners[componentId].actions[action] = []\n\t\t\t}\n\t\t}\n\n\t\tfunction addActionSpinner(componentId, action, directive) {\n\t\t\tcheckSpinnerInitialized(componentId, action)\n\t\t\tyoyoSpinners[componentId].actions[action].push(directive)\n\t\t}\n\n\t\tfunction addGenericSpinner(componentId, directive) {\n\t\t\tcheckSpinnerInitialized(componentId)\n\t\t\tyoyoSpinners[componentId].generic.push(directive)\n\t\t}\n\n\t\t// https://github.com/livewire/livewire\n\t\tfunction doAndSetCallbackOnElToUndo(\n\t\t\tel,\n\t\t\tdirective,\n\t\t\tdoCallback,\n\t\t\tundoCallback\n\t\t) {\n\t\t\tif (directive.modifiers.includes('remove'))\n\t\t\t\t[doCallback, undoCallback] = [undoCallback, doCallback]\n\n\t\t\tif (directive.modifiers.includes('delay')) {\n\t\t\t\tlet timeout = setTimeout(() => {\n\t\t\t\t\tdoCallback()\n\t\t\t\t\tel.__yoyo_on_finish_loading.push(() => undoCallback())\n\t\t\t\t}, 200)\n\n\t\t\t\tel.__yoyo_on_finish_loading.push(() => clearTimeout(timeout))\n\t\t\t} else {\n\t\t\t\tdoCallback()\n\t\t\t\tel.__yoyo_on_finish_loading.push(() => undoCallback())\n\t\t\t}\n\t\t}\n\n\t\tfunction componentAlreadyInCurrentHistoryState(component) {\n\t\t\tif (!history?.state?.yoyo) return false\n\n\t\t\thistory.state.yoyo.forEach((state) => {\n\t\t\t\tif (state.fingerprint == getComponentFingerprint(component)) {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t})\n\n\t\t\treturn false\n\t\t}\n\n\t\t/**\n\t\t * Component state caching for browser history\n\t\t */\n\n\t\tfunction updateState(\n\t\t\tmethod,\n\t\t\turl,\n\t\t\tcomponent,\n\t\t\tinitialState,\n\t\t\toriginalHTML\n\t\t) {\n\t\t\tconst id = component.id\n\t\t\tconst componentName = getComponentName(component)\n\t\t\tconst componentIndex = getComponentIndex(component)\n\t\t\tconst fingerprint = getComponentFingerprint(component)\n\t\t\tconst html = originalHTML ? originalHTML : component.outerHTML\n\t\t\tconst effects = component.__yoyo.effects || {}\n\n\t\t\tconst newState = {\n\t\t\t\turl,\n\t\t\t\tid,\n\t\t\t\tcomponentName,\n\t\t\t\tcomponentIndex,\n\t\t\t\tfingerprint,\n\t\t\t\thtml,\n\t\t\t\teffects,\n\t\t\t\tinitialState,\n\t\t\t}\n\n\t\t\tconst stateArray =\n\t\t\t\tmethod == 'pushState'\n\t\t\t\t\t? [newState]\n\t\t\t\t\t: replaceStateByComponentIndex(newState)\n\n\t\t\thistory[method](\n\t\t\t\t{ yoyo: stateArray, initialState: initialState },\n\t\t\t\t'',\n\t\t\t\turl\n\t\t\t)\n\t\t}\n\n\t\tfunction replaceStateByComponentIndex(newState) {\n\t\t\tlet stateArray = history?.state?.yoyo || []\n\t\t\tlet fingerprintFound = false\n\t\t\tstateArray.map((state) => {\n\t\t\t\tif (state.fingerprint == newState.fingerprint) {\n\t\t\t\t\tfingerprintFound = true\n\t\t\t\t\treturn newState\n\t\t\t\t}\n\n\t\t\t\treturn state\n\t\t\t})\n\n\t\t\tif (!fingerprintFound) {\n\t\t\t\tstateArray.push(newState)\n\t\t\t}\n\n\t\t\treturn stateArray\n\t\t}\n\n\t\tfunction restoreComponentStateFromHistory(state) {\n\t\t\tconst componentName = state.componentName\n\t\t\tconst componentsWithSameName = getComponentsByName(componentName)\n\t\t\tlet component = componentsWithSameName[state.componentIndex]\n\n\t\t\t// If the component cannot be found by index, try a simple ID check\n\t\t\t// This is needed for components dynamically added to the page, like modals\n\t\t\t// and it works when the component id is pre-determined (i.e. not randomly generated)\n\t\t\tif (!component) {\n\t\t\t\tcomponent = getComponentById(state.id)\n\n\t\t\t\tif (!component) return\n\t\t\t}\n\n\t\t\tvar parser = new DOMParser()\n\t\t\tvar cached = parser.parseFromString(state.html, 'text/html').body\n\t\t\t\t.firstElementChild\n\n\t\t\tcomponent.replaceWith(cached)\n\n\t\t\thtmx.process(cached)\n\n\t\t\t// Trigger full server refresh when coming back to the original state\n\t\t\t// so server-sent events on the render/refresh method are run\n\t\t\tif (state.initialState) {\n\t\t\t\tcomponentAddYoyoData(cached, { replayingHistory: true })\n\t\t\t\tYoyoEngine.trigger(cached, 'refresh')\n\t\t\t} else {\n\t\t\t\tYoyo.processBrowserEvents(state?.effects?.browserEvents)\n\t\t\t\tYoyo.processEmitEvents(component, state?.effects?.emitEvents)\n\t\t\t}\n\t\t}\n\n\t\tfunction componentCopyYoyoDataFromTo(from, to) {\n\t\t\tto.__yoyo = from?.__yoyo || {}\n\t\t\tto.__yoyo_on_finish_loading = from?.__yoyo_on_finish_loading || []\n\t\t}\n\n\t\tfunction componentAddYoyoData(component, data) {\n\t\t\tif (!data) return\n\t\t\tcomponent.__yoyo = Object.assign(component.__yoyo, data)\n\t\t}\n\n\t\t// https://github.com/alpinejs/alpine/\n\t\tfunction walk(el, callback) {\n\t\t\tif (callback(el) === false) return\n\n\t\t\tlet node = el.firstElementChild\n\n\t\t\twhile (node) {\n\t\t\t\twalk(node, callback)\n\n\t\t\t\tnode = node.nextElementSibling\n\t\t\t}\n\t\t}\n\n\t\tfunction extractModifiersAndValue(elt, type) {\n\t\t\tconst attr = elt\n\t\t\t\t.getAttributeNames()\n\t\t\t\t// Filter only the Yoyo spinning directives.\n\t\t\t\t.filter((name) => name.match(new RegExp(`yoyo:${type}`)))\n\n\t\t\tif (attr.length) {\n\t\t\t\tconst name = attr[0]\n\t\t\t\tconst [ntype, ...modifiers] = name\n\t\t\t\t\t.replace(new RegExp(`yoyo:${type}`), '')\n\t\t\t\t\t.split('.')\n\n\t\t\t\tconst value = elt.getAttribute(name)\n\t\t\t\treturn { elt, name, value, modifiers }\n\t\t\t}\n\n\t\t\treturn false\n\t\t}\n\n\t\treturn Yoyo\n\t})()\n})\n\nYoyoEngine.defineExtension('yoyo', {\n\tonEvent: function (name, evt) {\n\t\tif (name === 'htmx:afterProcessNode') {\n\t\t\tYoyo.afterProcessNode(evt)\n\t\t}\n\n\t\tif (name === 'htmx:configRequest') {\n\t\t\tif (!evt.detail.elt) return\n\n\t\t\tYoyo.bootstrapRequest(evt)\n\t\t}\n\n\t\tif (name === 'htmx:beforeRequest') {\n\t\t\tif (!Yoyo.url) {\n\t\t\t\tconsole.error('The yoyo URL needs to be defined')\n\t\t\t\tevt.preventDefault()\n\t\t\t}\n\n\t\t\tYoyo.beforeRequestActions(evt.detail.elt)\n\t\t}\n\n\t\tif (name === 'htmx:afterOnLoad') {\n\t\t\tYoyo.afterOnLoadActions(evt)\n\n\t\t\tconst xhr = evt.detail.xhr\n\n\t\t\tYoyo.processEmitEvents(\n\t\t\t\tevt.detail.elt,\n\t\t\t\txhr.getResponseHeader('Yoyo-Emit')\n\t\t\t)\n\n\t\t\tYoyo.processBrowserEvents(\n\t\t\t\txhr.getResponseHeader('Yoyo-Browser-Event')\n\t\t\t)\n\n\t\t\tYoyo.processRedirectHeader(xhr)\n\n\t\t\t// Re-spawn targets removed from the page and take into account swap delays\n\t\t\tlet modifier = xhr.getResponseHeader('Yoyo-Swap-Modifier')\n\t\t\tif (!modifier) return\n\t\t\tlet swap = modifier.match(/swap:([0-9.]+)s/)\n\t\t\tlet time = swap[1] ? swap[1] * 1000 + 1 : 0\n\t\t\tsetTimeout(() => {\n\t\t\t\tif (\n\t\t\t\t\t!evt.detail.target.isConnected &&\n\t\t\t\t\tdocument.querySelector(\n\t\t\t\t\t\t`[hx-target=\"#${evt.detail.target.id}\"]`\n\t\t\t\t\t)\n\t\t\t\t) {\n\t\t\t\t\tYoyo.createNonExistentIdTarget(`#${evt.detail.target.id}`)\n\t\t\t\t}\n\t\t\t}, time)\n\t\t}\n\n\t\tif (name === 'htmx:beforeSwap') {\n\t\t\tif (!evt.detail.elt) return\n\n\t\t\t// Add triggering element info to event detail so it can be read in after swap events\n\t\t\t// For example to push the href url to browser history using the href from the element that's no longer present on the page\n\t\t\tlet triggerId =\n\t\t\t\tevt.detail.requestConfig.headers['HX-Trigger'] || null\n\t\t\tlet triggeringElt = htmx.find(`#${triggerId}`)\n\t\t\tif (triggerId && triggeringElt) {\n\t\t\t\tevt.detail.requestConfig.triggerEltInfo = {\n\t\t\t\t\tid: triggerId,\n\t\t\t\t\thref: triggeringElt.getAttribute('href'),\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst modifier =\n\t\t\t\tevt.detail.xhr.getResponseHeader('Yoyo-Swap-Modifier')\n\n\t\t\tif (modifier) {\n\t\t\t\tconst swap =\n\t\t\t\t\tevt.detail.elt.getAttribute('hx-swap') ||\n\t\t\t\t\tYoyoEngine.config.defaultSwapStyle\n\t\t\t\tevt.detail.elt.setAttribute('hx-swap', `${swap} ${modifier}`)\n\t\t\t}\n\n\t\t\tYoyo.processBrowserEvents(\n\t\t\t\tevt.detail.xhr.getResponseHeader('Yoyo-Browser-Event')\n\t\t\t)\n\t\t}\n\n\t\tif (name === 'htmx:afterSettle') {\n\t\t\t// Push component response to history cache\n\t\t\t// Make sure we trigger once for the new element - this was failing in Safari mobile\n\t\t\t// Causing a duplicate snapshot\n\t\t\tif (!evt.detail.elt || !evt.detail.elt.isConnected) return\n\n\t\t\tYoyo.afterSettleActions(evt)\n\t\t}\n\t},\n\n\t// Add support for morphdom swap when using Alpine JS to be able to\n\t// maintain the Alpine component state after a swap\n\tisInlineSwap: function (swapStyle) {\n\t\treturn swapStyle === 'morphdom'\n\t},\n\thandleSwap: function (swapStyle, target, fragment) {\n\t\tif (typeof morphdom === 'function' && swapStyle === 'morphdom') {\n\t\t\tmorphdom(target, fragment.outerHTML, {\n\t\t\t\tonBeforeElUpdated: (from, to) => {\n\t\t\t\t\t// From Livewire - deal with Alpine component updates\n\t\t\t\t\tif (from.__x) {\n\t\t\t\t\t\twindow.Alpine.clone(from.__x, to)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t})\n\n\t\t\treturn [target] // let htmx handle the new content\n\t\t}\n\t},\n})\n"
  },
  {
    "path": "src/yoyo/AnonymousComponent.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nclass AnonymousComponent extends Component\n{\n    public function render()\n    {\n        $data = array_merge($this->variables, $this->request->all());\n\n        return $this->view($this->componentName, $data);\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Blade/Application.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Blade;\n\nuse Closure;\nuse Illuminate\\Container\\Container;\n\nclass Application extends Container\n{\n    protected array $terminatingCallbacks = [];\n\n    public function getNamespace()\n    {\n        return '';\n    }\n\n    public function terminating(Closure $callback)\n    {\n        $this->terminatingCallbacks[] = $callback;\n\n        return $this;\n    }\n\n    public function terminate()\n    {\n        foreach ($this->terminatingCallbacks as $terminatingCallback) {\n            $terminatingCallback();\n        }\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Blade/CreateBladeViewFromString.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Blade;\n\nuse Illuminate\\View\\Component;\n\nclass CreateBladeViewFromString extends Component\n{\n    public function __invoke($view, $contents)\n    {\n        return $this->createBladeViewFromString($view, $contents);\n    }\n\n    public function render()\n    {\n        //\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Blade/YoyoBladeCompilerEngine.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Blade;\n\nuse Illuminate\\View\\Engines\\CompilerEngine as LaravelCompilerEngine;\n\nclass YoyoBladeCompilerEngine extends LaravelCompilerEngine\n{\n    protected $yoyoComponent;\n\n    protected $isRenderingYoyoComponent;\n\n    public function startYoyoRendering($component)\n    {\n        $this->yoyoComponent = $component;\n\n        $this->isRenderingYoyoComponent = true;\n    }\n\n    public function stopYoyoRendering()\n    {\n        $this->isRenderingYoyoComponent = false;\n    }\n\n    /**\n     * /vendor/illuminate/view/Engines/PhpEngine.php.\n     */\n    protected function evaluatePath($__path, $__data)\n    {\n        if (! $this->isRenderingYoyoComponent) {\n            return parent::evaluatePath($__path, $__data);\n        }\n\n        $obLevel = ob_get_level();\n\n        ob_start();\n\n        try {\n            \\Closure::bind(function () use ($__path, $__data) {\n                extract($__data, EXTR_SKIP);\n                include $__path;\n            }, $this->yoyoComponent ? $this->yoyoComponent : $this)();\n        } catch (\\Throwable $e) {\n            $this->handleViewException($e, $obLevel);\n        }\n\n        return ltrim(ob_get_clean());\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Blade/YoyoBladeDirectives.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Blade;\n\nclass YoyoBladeDirectives\n{\n    public function __construct($blade)\n    {\n        $blade->directive('yoyo', [$this, 'yoyo']);\n\n        $blade->directive('yoyo_scripts', [$this, 'yoyo_scripts']);\n\n        $blade->directive('spinning', [$this, 'spinning']);\n\n        $blade->directive('endspinning', [$this, 'endspinning']);\n\n        $blade->directive('emit', [$this, 'emit']);\n\n        $blade->directive('emitTo', [$this, 'emitTo']);\n\n        $blade->directive('emitToWithSelector', [$this, 'emitToWithSelector']);\n\n        $blade->directive('emitSelf', [$this, 'emitToWithSelector']);\n\n        $blade->directive('emitUp', [$this, 'emitToWithSelector']);\n    }\n\n    public function yoyo($expression)\n    {\n        return <<<yoyo\n<?php\n\\$yoyo = \\Clickfwd\\Yoyo\\Yoyo::getInstance();\nif (Yoyo\\is_spinning()) {\n    echo \\$yoyo->mount({$expression})->refresh();\n} else {\n    echo \\$yoyo->mount({$expression})->render();\n}\n?>\nyoyo;\n    }\n\n    public function yoyo_scripts()\n    {\n        return '<?php Yoyo\\yoyo_scripts(); ?>';\n    }\n\n    public function spinning($expression)\n    {\n        return $expression !== ''\n        ? \"<?php if(\\$spinning && {$expression}): ?>\"\n        : '<?php if($spinning): ?>';\n    }\n\n    public function endspinning()\n    {\n        return '<?php endif; ?>';\n    }\n\n    public function emit($expression)\n    {\n        return \"<?php \\$this->emit({$expression}); ?>\";\n    }\n\n    public function emitTo($expression)\n    {\n        return \"<?php \\$this->emitTo({$expression}); ?>\";\n    }\n\n    public function emitToWithSelector($expression)\n    {\n        return \"<?php \\$this->emitToWithSelector({$expression}); ?>\";\n    }\n\n    public function emitSelf($expression)\n    {\n        return \"<?php \\$this->emitSelf({$expression}); ?>\";\n    }\n\n    public function emitUp($expression)\n    {\n        return \"<?php \\$this->emitUp({$expression}); ?>\";\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Blade/YoyoServiceProvider.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Blade;\n\nuse Illuminate\\Support\\ServiceProvider;\n\nclass YoyoServiceProvider extends ServiceProvider\n{\n    public function boot()\n    {\n        $this->registerBladeDirectives();\n        $this->registerViewCompilerEngine();\n    }\n\n    protected function registerViewCompilerEngine()\n    {\n        $this->app->make('view.engine.resolver')->register('blade', function () {\n            return new YoyoBladeCompilerEngine($this->app['blade.compiler']);\n        });\n    }\n\n    protected function registerBladeDirectives()\n    {\n        if (method_exists($this->app->get('view'), 'directive')) {\n            $blade = $this->app->get('view');\n        } else {\n            $blade = $this->app->get('view')->getEngineResolver()->resolve('blade')->getCompiler();\n        }\n\n        new YoyoBladeDirectives($blade);\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Blade/yoyo-view.blade.php",
    "content": "@yoyo($name, $variables, $attributes, $action)"
  },
  {
    "path": "src/yoyo/ClassHelpers.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse ReflectionClass;\nuse ReflectionMethod;\n\nclass ClassHelpers\n{\n    private static array $propertyCache = [];\n\n    private static array $defaultVarCache = [];\n\n    private static array $methodCache = [];\n\n    private static array $traitCache = [];\n\n    private static array $paramTypeCache = [];\n\n    public static function getDefaultPublicVars($instance, $baseClass = null)\n    {\n        $className = get_class($instance);\n        $cacheKey = $className . ':' . ($baseClass ?? '');\n\n        if (isset(static::$defaultVarCache[$cacheKey])) {\n            return static::$defaultVarCache[$cacheKey];\n        }\n\n        $class = new ReflectionClass($className);\n\n        $names = self::getPublicProperties($instance, $baseClass);\n\n        $values = $class->getDefaultProperties();\n\n        return static::$defaultVarCache[$cacheKey] = array_intersect_key($values, array_flip($names));\n    }\n\n    public static function getPublicVars($instance, $baseClass = null)\n    {\n        $publicProperties = self::getPublicProperties($instance, $baseClass);\n\n        $vars = call_user_func('get_object_vars', $instance);\n\n        $publicVars = [];\n\n        foreach ($vars as $key => $value) {\n            if (in_array($key, $publicProperties)) {\n                $publicVars[$key] = $vars[$key];\n            }\n        }\n\n        return $publicVars;\n    }\n\n    public static function getPublicProperties($instance, $baseClass = null)\n    {\n        $className = get_class($instance);\n        $cacheKey = $className . ':' . ($baseClass ?? '');\n\n        if (isset(static::$propertyCache[$cacheKey])) {\n            return static::$propertyCache[$cacheKey];\n        }\n\n        $class = new ReflectionClass($className);\n\n        $properties = $class->getProperties(ReflectionMethod::IS_PUBLIC);\n\n        $publicProperties = [];\n\n        foreach ($properties as $prop) {\n            // Only include the property if it's different from the base class when passed as 2d parameter\n            // This allows extending component classes with public properties\n            if (($baseClass && $prop->class !== $baseClass) || $prop->class == $className) {\n                $publicProperties[] = $prop->name;\n            }\n        }\n\n        return static::$propertyCache[$cacheKey] = $publicProperties;\n    }\n\n    public static function getPublicMethods($instance, $exceptions = [])\n    {\n        $className = is_string($instance) ? $instance : get_class($instance);\n        $cacheKey = $className . ':' . implode(',', $exceptions);\n\n        if (isset(static::$methodCache[$cacheKey])) {\n            return static::$methodCache[$cacheKey];\n        }\n\n        $class = new ReflectionClass($className);\n\n        $methods = $class->getMethods(ReflectionMethod::IS_PUBLIC);\n\n        foreach ($methods as $method) {\n            if ($method->class == $className && ! in_array($method->name, $exceptions)) {\n                $publicMethods[] = $method->name;\n            }\n        }\n\n        return static::$methodCache[$cacheKey] = $publicMethods ?? [];\n    }\n\n    public static function methodIsPrivate($instance, $method)\n    {\n        $reflection = new ReflectionMethod($instance, $method);\n\n        return ! $reflection->isPublic();\n    }\n\n    public static function classImplementsInterface($name, $instance)\n    {\n        $class = new ReflectionClass($name);\n\n        return in_array($instance, $class->getInterfaceNames());\n    }\n\n    /**\n     * Laravel Support helper\n     */\n    public static function classUsesRecursive($class)\n    {\n        if (is_object($class)) {\n            $class = get_class($class);\n        }\n\n        $className = $class;\n\n        if (isset(static::$traitCache[$className])) {\n            return static::$traitCache[$className];\n        }\n\n        $results = [];\n\n        foreach (array_reverse(class_parents($class)) + [$class => $class] as $class) {\n            $results += static::traitUsesRecursive($class);\n        }\n\n        return static::$traitCache[$className] = array_unique($results);\n    }\n\n    /**\n     * Laravel Support helper\n     */\n    public static function traitUsesRecursive($trait)\n    {\n        $traits = class_uses($trait);\n\n        foreach ($traits as $trait) {\n            $traits += static::traitUsesRecursive($trait);\n        }\n\n        return $traits;\n    }\n\n    /**\n     * Laravel Support helper\n     */\n    public static function classBasename($class)\n    {\n        $class = is_object($class) ? get_class($class) : $class;\n\n        return basename(str_replace('\\\\', '/', $class));\n    }\n\n    public static function getMethodParameterNames($class, $method)\n    {\n        $names = [];\n\n        $reflector = new ReflectionClass($class);\n\n        $method = $reflector->getMethod($method);\n\n        foreach ($method->getParameters() as $parameter) {\n            if (! $parameter->getType() || ($parameter->getType() && $parameter->getType()->isBuiltin())) {\n                $names[] = $parameter->getName();\n            }\n        }\n\n        return $names;\n    }\n\n    public static function methodHasVariadicParameter($class, $method)\n    {\n        $reflector = new ReflectionClass($class);\n        $method = $reflector->getMethod($method);\n        $parameters = $method->getParameters();\n\n        if (empty($parameters)) {\n            return false;\n        }\n\n        // Check if the last parameter is variadic\n        $lastParam = end($parameters);\n        return $lastParam->isVariadic();\n    }\n\n    /**\n     * Get all method parameters with type information\n     * Returns an array with 'typed' and 'regular' parameters\n     */\n    public static function getMethodParametersWithTypes($class, $method)\n    {\n        $className = is_object($class) ? get_class($class) : $class;\n        $cacheKey = $className . ':' . $method;\n\n        if (isset(static::$paramTypeCache[$cacheKey])) {\n            return static::$paramTypeCache[$cacheKey];\n        }\n\n        $typed = [];    // Parameters with class type hints (for DI)\n        $regular = [];  // Parameters without type hints or with builtin types\n\n        $reflector = new ReflectionClass($class);\n        $method = $reflector->getMethod($method);\n\n        foreach ($method->getParameters() as $parameter) {\n            $paramInfo = [\n                'name' => $parameter->getName(),\n                'optional' => $parameter->isOptional(),\n                'variadic' => $parameter->isVariadic(),\n            ];\n\n            if (! $parameter->getType() || ($parameter->getType() && $parameter->getType()->isBuiltin())) {\n                // Regular parameter (no type or builtin type)\n                $regular[] = $paramInfo;\n            } else {\n                // Typed parameter (class type hint for DI)\n                $paramInfo['type'] = $parameter->getType()->getName();\n                $typed[] = $paramInfo;\n            }\n        }\n\n        return static::$paramTypeCache[$cacheKey] = [\n            'typed' => $typed,\n            'regular' => $regular,\n        ];\n    }\n\n    public static function flushCache(): void\n    {\n        static::$propertyCache = [];\n        static::$defaultVarCache = [];\n        static::$methodCache = [];\n        static::$traitCache = [];\n        static::$paramTypeCache = [];\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Component.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Concerns\\BrowserEvents;\nuse Clickfwd\\Yoyo\\Concerns\\Redirector;\nuse Clickfwd\\Yoyo\\Exceptions\\BypassRenderMethod;\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentMethodNotFound;\nuse Clickfwd\\Yoyo\\Exceptions\\MissingComponentTemplate;\nuse Clickfwd\\Yoyo\\Interfaces\\ViewProviderInterface;\nuse Clickfwd\\Yoyo\\Services\\Response;\nuse Closure;\nuse ReflectionMethod;\n\nabstract class Component\n{\n    use BrowserEvents;\n    use Redirector;\n\n    protected $yoyo_id;\n\n    protected $componentName;\n\n    protected $componentAction;\n\n    protected $variables;\n\n    protected $request;\n\n    protected $response;\n\n    protected $spinning;\n\n    protected $queryString = [];\n\n    protected $props = [];\n\n    protected $listeners = [];\n\n    protected $omitResponse = false;\n\n    protected $computedPropertyCache = [];\n\n    protected $attributes;\n\n    protected $resolver;\n\n    protected $viewData = [];\n\n    private static $excludePublicMethods = [\n        '__construct',\n        'spinning',\n    ];\n\n    public function __construct(ComponentResolver $resolver, string $id, string $name)\n    {\n        $this->yoyo_id = $id;\n\n        $this->componentName = $name;\n\n        $this->request = Yoyo::request();\n\n        $this->response = Response::getInstance();\n\n        $this->resolver = $resolver;\n    }\n\n    public function spinning(bool $spinning)\n    {\n        $this->spinning = $spinning;\n\n        return $this;\n    }\n\n    public function boot(array $variables, array $attributes)\n    {\n        $data = array_merge($variables, $this->request->all());\n\n        $this->variables = $variables;\n\n        $this->attributes = $attributes;\n\n        $publicProperties = ClassHelpers::getPublicProperties($this, __CLASS__);\n\n        foreach ($publicProperties as $property) {\n            $this->{$property} = $data[$property] ?? $this->{$property};\n        }\n\n        // Set an initial value for dynamic properties\n        foreach ($this->getDynamicProperties() as $property) {\n            $this->{$property} = $data[$property] ?? null;\n        }\n\n        return $this;\n    }\n\n    public function getName()\n    {\n        return $this->componentName;\n    }\n\n    public function getDynamicProperties()\n    {\n        return [];\n    }\n\n    public function getInitialAttributes()\n    {\n        $attributes = $this->attributes;\n\n        return $attributes;\n    }\n\n    public function getVariables()\n    {\n        return $this->variables;\n    }\n\n    public function getQueryParam($key, $default = null)\n    {\n        return $this->request->get($key, $default);\n    }\n\n    public function getQueryString()\n    {\n        return $this->queryString;\n    }\n\n    public function getProps()\n    {\n        return $this->props;\n    }\n\n    public function setAction($action)\n    {\n        $this->componentAction = $action;\n    }\n\n    public function actionMatches($action)\n    {\n        if (is_array($action)) {\n            return in_array($this->componentAction, $action);\n        }\n\n        return $this->componentAction == $action;\n    }\n\n    public function getListeners()\n    {\n        $listeners = [];\n\n        foreach ($this->listeners as $key => $value) {\n            if (is_numeric($key)) {\n                $listeners[$value] = $value;\n            } else {\n                $listeners[$key] = $value;\n            }\n        }\n\n        return $listeners;\n    }\n\n    public function getComponentId()\n    {\n        return $this->yoyo_id;\n    }\n\n    public function set($key, $value = null)\n    {\n        if (is_array($key)) {\n            $this->viewData = array_merge($this->viewData, $key);\n        } else {\n            $this->viewData[$key] = $value;\n        }\n\n        return $this;\n    }\n\n    public function render()\n    {\n        if ($this->omitResponse) {\n            throw new BypassRenderMethod($this->response->getStatusCode());\n        }\n\n        return $this->view($this->componentName);\n    }\n\n    public function addSwapModifiers($modifier)\n    {\n        $this->response->header('Yoyo-Swap-Modifier', $modifier);\n\n        return $this;\n    }\n\n    public function skipRender()\n    {\n        $this->response->status(204);\n        $this->omitResponse = true;\n\n        return $this;\n    }\n\n    public function skipRenderAndRemove($modifier = 'swap:1s')\n    {\n        if ($modifier) {\n            $this->addSwapModifiers($modifier);\n        }\n\n        $this->response->status(200);\n        $this->omitResponse = true;\n\n        return $this;\n    }\n\n    protected function view($template, $vars = []): ViewProviderInterface\n    {\n        $view = $this->resolver->resolveViewProvider();\n\n        if (! $view->exists($template)) {\n            throw new MissingComponentTemplate($template, get_class($this));\n        }\n\n        $view->startYoyoRendering($this);\n\n        // Make public properties and methods available to views\n\n        $vars = array_merge($this->viewVars(), $vars);\n\n        $view->render($template, $vars);\n\n        return $view;\n    }\n\n    public function createViewFromString($content): string\n    {\n        $view = $this->resolver->resolveViewProvider();\n\n        $view->startYoyoRendering($this);\n\n        $html = $view->makeFromString($content, $this->viewVars());\n\n        $view->stopYoyoRendering();\n\n        return $html;\n    }\n\n    protected function viewVars(): array\n    {\n        $vars = [];\n\n        $vars['spinning'] = $this->spinning;\n\n        $properties = ClassHelpers::getPublicVars($this, __CLASS__);\n\n        $properties = array_merge($properties, array_fill_keys($this->getDynamicProperties(), null));\n\n        return array_merge($this->viewData, $vars, $properties);\n    }\n\n    protected function createVariableFromMethod(ReflectionMethod $method)\n    {\n        return $method->getNumberOfParameters() === 0\n                        ? $this->createInvocableVariable($method->getName())\n                        : Closure::fromCallable([$this, $method->getName()]);\n    }\n\n    protected function createInvocableVariable(string $method)\n    {\n        return new InvocableComponentVariable(function () use ($method) {\n            return $this->{$method}();\n        });\n    }\n\n    // For computed properties with arguments\n    // For Twig compatibility, because computed properties are not resolved through __get\n\n    public function __call(string $name, array $arguments)\n    {\n        $key = static::makeCacheKey($name, $arguments);\n\n        if (isset($this->computedPropertyCache[$key])) {\n            return $this->computedPropertyCache[$key];\n        }\n\n        $studlyProperty = YoyoHelpers::studly($name);\n\n        if (method_exists($this, $computedMethodName = 'get'.$studlyProperty.'Property')) {\n            return $this->computedPropertyCache[$key] = call_user_func_array([$this, $computedMethodName], $arguments);\n        }\n\n        throw new ComponentMethodNotFound($this->getName(), $name);\n    }\n\n    public function __get($property)\n    {\n        if (isset($this->computedPropertyCache[$property])) {\n            return $this->computedPropertyCache[$property];\n        }\n\n        $studlyProperty = YoyoHelpers::studly($property);\n\n        if (method_exists($this, $computedMethodName = 'get'.$studlyProperty.'Property')) {\n            return $this->computedPropertyCache[$property] = $this->$computedMethodName();\n        }\n\n        throw new ComponentMethodNotFound($this->getName(), $property);\n    }\n\n    public function forgetComputed($key = null)\n    {\n        if (is_null($key)) {\n            $this->computedPropertyCache = [];\n\n            return;\n        }\n\n        $keys = is_array($key) ? $key : func_get_args();\n\n        foreach ($keys as $keyName) {\n            unset($this->computedPropertyCache[$keyName]);\n        }\n    }\n\n    public function forgetComputedWithArgs($name, ...$args)\n    {\n        $this->forgetComputed(static::makeCacheKey($name, $args));\n    }\n\n    protected static function makeCacheKey($name, $arguments)\n    {\n        return md5($name.json_encode($arguments));\n    }\n}\n"
  },
  {
    "path": "src/yoyo/ComponentManager.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentMethodNotFound;\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentNotFound;\nuse Clickfwd\\Yoyo\\Exceptions\\NonPublicComponentMethodCall;\n\nclass ComponentManager\n{\n    private $id;\n\n    private $name;\n\n    private $request;\n\n    private $component;\n\n    private $resolver;\n\n    private $spinning;\n\n    public function __construct($resolver, $request, $spinning)\n    {\n        $this->request = $request;\n\n        $this->spinning = $spinning;\n\n        $this->resolver = $resolver;\n    }\n\n    public function getDefaultPublicVars()\n    {\n        return ClassHelpers::getDefaultPublicVars($this->component, Component::class);\n    }\n\n    public function getPublicVars()\n    {\n        if ($this->isAnonymousComponent()) {\n            return $this->request->except(['component', YoyoCompiler::yoprefix('id')]);\n        }\n\n        $vars = ClassHelpers::getPublicVars($this->component, Component::class);\n\n        foreach ($this->component->getDynamicProperties() as $name) {\n            $vars[$name] = property_exists($this->component, $name) ? $this->component->{$name} : null;\n        }\n\n        $vars = array_merge($vars, $this->request->startsWith(YoyoCompiler::yoprefix('')));\n\n        return $vars;\n    }\n\n    public function getQueryString()\n    {\n        if ($this->isAnonymousComponent()) {\n            return $this->request->method() == 'GET'\n                    ? array_keys($this->request->except(['component', YoyoCompiler::yoprefix('id')]))\n                    : [];\n        }\n\n        return $this->component->getQueryString();\n    }\n\n    public function getProps()\n    {\n        return $this->component->getProps();\n    }\n\n    public function getListeners()\n    {\n        return $this->component->getListeners();\n    }\n\n    public function process($id, $name, $action, $variables, $attributes)\n    {\n        if (! ($this->component = $this->resolver->resolveComponent($id, $name, $variables))) {\n            throw new ComponentNotFound($name);\n        }\n\n        if ($this->isAnonymousComponent()) {\n            return $this->processAnonymousComponent($variables, $attributes);\n        }\n\n        return $this->processDynamicComponent($action, $variables, $attributes);\n    }\n\n    public function isAnonymousComponent(): bool\n    {\n        return is_a($this->component, AnonymousComponent::class);\n    }\n\n    public function isDynamicComponent(): bool\n    {\n        return ! $this->isAnonymousComponent();\n    }\n\n    private function processDynamicComponent($action, $variables = [], $attributes = [])\n    {\n        $class = get_class($this->component);\n\n        $this->component->setAction($action);\n\n        $isEventListenerAction = false;\n\n        $eventParams = $this->request->get('eventParams', []);\n\n        // Guard: Request::get() returns raw string when test_json decodes to falsy value ([], {})\n        // TODO: Root cause is test_json falsy check in Request::get() — track as separate fix\n        if (is_string($eventParams)) {\n            $decoded = json_decode($eventParams, true, 32);\n            $eventParams = is_array($decoded) ? $decoded : [];\n        }\n\n        $this->component->spinning($this->spinning)->boot($variables, $attributes);\n\n        $hookStack = [\n            'initialize' => ['initialize'],\n            'mount' => ['mount'],\n            'rendering' => ['rendering'],\n            'rendered' => ['rendered'],\n        ];\n\n        $parameters = array_merge($variables, $this->request->all());\n\n        // Build stack of trait lifecycle hooks to run after the component hook of the same name\n        foreach (ClassHelpers::classUsesRecursive($this->component) as $trait) {\n            foreach (array_keys($hookStack) as $hook) {\n                $hookStack[$hook][] = $hook.ClassHelpers::classBasename($trait);\n            }\n        }\n\n        foreach ($hookStack['initialize'] as $method) {\n            if (method_exists($this->component, $method)) {\n                Yoyo::container()->call([$this->component, $method], $parameters);\n            }\n        }\n\n        $listeners = $this->component->getListeners();\n\n        if (! empty($listeners[$action]) || in_array($action, $listeners)) {\n            // If action is an event listener, re-route it to the listener method\n\n            $action = ! empty($listeners[$action]) ? $listeners[$action] : $action;\n\n            $isEventListenerAction = true;\n        } elseif (! method_exists($this->component, $action)) {\n            throw new ComponentMethodNotFound($class, $action);\n        }\n\n        $excludedActions = ClassHelpers::getPublicMethods(Component::class, ['render']);\n\n        if (in_array($action, $excludedActions) ||\n            (! $isEventListenerAction && ClassHelpers::methodIsPrivate($this->component, $action))) {\n            throw new NonPublicComponentMethodCall($class, $action);\n        }\n\n        foreach ($hookStack['mount'] as $method) {\n            if (method_exists($this->component, $method)) {\n                Yoyo::container()->call([$this->component, $method], $parameters);\n            }\n        }\n\n        if (! in_array($action, ['render', 'refresh'])) {\n            $parameters = $isEventListenerAction ? $eventParams : $this->parseActionArguments();\n\n            // Empty params must fall through to existing no-params handling in the else branch\n            if ($isEventListenerAction && is_array($parameters) && ! empty($parameters) && array_values($parameters) !== $parameters) {\n                // Associative array from JS dispatch — validate required params then pass as named args\n                $paramInfo = ClassHelpers::getMethodParametersWithTypes($this->component, $action);\n                $regularParams = $paramInfo['regular'];\n\n                foreach ($regularParams as $param) {\n                    if (! $param['optional'] && ! $param['variadic'] && ! isset($parameters[$param['name']])) {\n                        throw new \\InvalidArgumentException(\n                            \"Missing required parameter [{$param['name']}] for [{$this->name}::{$action}]\"\n                        );\n                    }\n                }\n\n                $args = $parameters;\n            } else {\n                // Get parameter information with types\n                $paramInfo = ClassHelpers::getMethodParametersWithTypes($this->component, $action);\n                $regularParams = $paramInfo['regular'];\n                $typedParams = $paramInfo['typed'];\n\n                // Extract just the names of regular parameters for backwards compatibility\n                $parameterNames = array_column($regularParams, 'name');\n\n                // Check if the last regular parameter is variadic\n                $hasVariadic = ! empty($regularParams) && end($regularParams)['variadic'];\n\n                // Handle variadic parameters\n                if ($hasVariadic && count($parameterNames) > 0) {\n                    $regularParamCount = count($parameterNames) - 1; // Exclude the variadic parameter\n\n                    if (count($parameters) >= $regularParamCount) {\n                        // Split parameters into regular and variadic\n                        $regularParamValues = array_slice($parameters, 0, $regularParamCount);\n                        $variadicParamValues = array_slice($parameters, $regularParamCount);\n\n                        // Create args array with named regular parameters and indexed variadic parameters\n                        $args = [];\n                        for ($i = 0; $i < $regularParamCount; $i++) {\n                            $args[$parameterNames[$i]] = $regularParamValues[$i] ?? null;\n                        }\n\n                        // Add variadic parameters as indexed values (not named)\n                        foreach ($variadicParamValues as $value) {\n                            $args[] = $value;\n                        }\n                    } else {\n                        throw new \\InvalidArgumentException(\"Too few parameters passed to [{$this->name}::{$action}]\");\n                    }\n                } else {\n                    // Check if all regular parameters are optional\n                    $requiredCount = 0;\n                    foreach ($regularParams as $param) {\n                        if (! $param['optional']) {\n                            $requiredCount++;\n                        }\n                    }\n\n                    // Only validate regular parameters (not typed/DI parameters)\n                    if (count($parameters) >= $requiredCount && count($parameters) <= count($parameterNames)) {\n                        // Parameters count is valid (between required and total)\n                        $args = [];\n                        for ($i = 0; $i < count($parameterNames); $i++) {\n                            $args[$parameterNames[$i]] = $parameters[$i] ?? null;\n                        }\n                    } elseif (empty($parameterNames) && empty($parameters)) {\n                        // Method has only typed parameters (or no parameters at all)\n                        $args = [];\n                    } else {\n                        throw new \\InvalidArgumentException(\"Incorrect number of parameters passed to [{$this->name}::{$action}]\");\n                    }\n                }\n            }\n\n            // The container will handle dependency injection for typed parameters\n            $actionResponse = Yoyo::container()->call([$this->component, $action], $args);\n\n            $type = gettype($actionResponse);\n\n            if ($type !== 'string' && $type !== 'NULL') {\n                throw new \\Exception(\"Component [{$class}] action [{$action}] response should be a string, instead was [{$type}]\");\n            }\n        }\n\n        foreach ($hookStack['rendering'] as $method) {\n            if (method_exists($this->component, $method)) {\n                Yoyo::container()->call([$this->component, $method]);\n            }\n        }\n\n        $view = $this->component->render();\n\n        if (is_null($view)) {\n            return '';\n        }\n\n        // For string based templates\n        if (is_string($view)) {\n            $view = $this->component->createViewFromString($view);\n        }\n\n        foreach ($hookStack['rendered'] as $method) {\n            if (method_exists($this->component, $method)) {\n                $view = Yoyo::container()->call([$this->component, $method], ['view' => $view]);\n            }\n        }\n\n        return (string) $view;\n    }\n\n    private function parseActionArguments()\n    {\n        $args = $this->request->get('actionArgs', []);\n\n        return is_array($args) ? $args : [$args];\n    }\n\n    private function processAnonymousComponent($variables = [], $attributes = [])\n    {\n        $this->component->spinning($this->spinning)->boot($variables, $attributes);\n\n        $view = (string) $this->component->render();\n\n        return $view;\n    }\n\n    public function getComponentInstance()\n    {\n        return $this->component;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/ComponentResolver.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Interfaces\\ViewProviderInterface;\nuse Clickfwd\\Yoyo\\Interfaces\\YoyoContainerInterface;\nuse Psr\\Container\\ContainerExceptionInterface;\n\nclass ComponentResolver\n{\n    protected $name = 'default';\n\n    protected $variables;\n\n    protected $registered;\n\n    protected $hints;\n\n    protected $container;\n\n    public function __invoke(YoyoContainerInterface $container, array $registered = [], array $hints = [])\n    {\n        $this->container = $container;\n\n        $this->registered = $registered;\n\n        $this->hints = $hints;\n\n        return $this;\n    }\n\n    public function getName()\n    {\n        return $this->name;\n    }\n\n    public function resolving($id, $name, $variables)\n    {\n    }\n\n    public function resolveComponent($id, $name, $variables): ?Component\n    {\n        $this->resolving($id, $name, $variables);\n\n        if ($instance = $this->resolveDynamic($id, $name)) {\n            return $instance;\n        }\n\n        return $this->resolveAnonymous($id, $name);\n    }\n\n    public function resolveDynamic($id, $name): ?Component\n    {\n        $classNames = [];\n\n        $args = ['resolver' => $this, 'id' => $id, 'name' => $name];\n\n        // Check namespaced components\n        if (strpos($name, ViewProviderInterface::HINT_PATH_DELIMITER) > 0) {\n            [$namespaceAlias, $name] = explode(ViewProviderInterface::HINT_PATH_DELIMITER, $name);\n            if (isset($this->hints[$namespaceAlias])) {\n                foreach ($this->hints[$namespaceAlias] as $namespaceHint) {\n                    $classNames[] = $namespaceHint . '\\\\' . $this->dotNotationToClass($name);\n                }\n            }\n        }\n\n        $classNames[] = $this->registered[$name] ?? null;\n\n        $configurationNamespaces = (array) \\Clickfwd\\Yoyo\\Services\\Configuration::get('namespace');\n\n        foreach ($configurationNamespaces as $namespaceHint) {\n            $classNames[] = $namespaceHint . $this->dotNotationToClass($name);\n        }\n\n        $classNames = array_filter(array_unique($classNames));\n\n        foreach ($classNames as $className) {\n            if (class_exists($className)) {\n                break;\n            }\n        }\n\n        try {\n            return $this->container->make($className, $args);\n        } catch (ContainerExceptionInterface $e) {\n            return null;\n        }\n    }\n\n    public function resolveAnonymous($id, $name): ?Component\n    {\n        $args = ['resolver' => $this, 'id' => $id, 'name' => $name];\n\n        if ($this->registered[$name] ?? null) {\n            $args['name'] = $this->registered[$name] ?? $name;\n\n            return $this->container->make(AnonymousComponent::class, $args);\n        }\n\n        $view = $this->resolveViewProvider();\n\n        if ($view->exists($name)) {\n            return $this->container->make(AnonymousComponent::class, $args);\n        }\n\n        return null;\n    }\n\n    public function dotNotationToClass($name)\n    {\n        return implode('\\\\', array_map(function ($name) {\n            return YoyoHelpers::studly($name);\n        }, explode('.', $name)));\n    }\n\n    public function resolveViewProvider(): ViewProviderInterface\n    {\n        return $this->container->get('yoyo.view.'.$this->getName());\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Concerns/BrowserEvents.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Concerns;\n\nuse Clickfwd\\Yoyo\\Services\\BrowserEventsService;\n\ntrait BrowserEvents\n{\n    public function emit($event, ...$params)\n    {\n        (BrowserEventsService::getInstance())->emit($event, $params);\n    }\n\n    public function emitTo($target, $event, ...$params)\n    {\n        (BrowserEventsService::getInstance())->emitTo($target, $event, $params);\n    }\n\n    public function emitToWithSelector($target, $event, ...$params)\n    {\n        (BrowserEventsService::getInstance())->emitToWithSelector($target, $event, $params);\n    }\n\n    public function emitSelf($event, ...$params)\n    {\n        (BrowserEventsService::getInstance())->emitSelf($event, $params);\n    }\n\n    public function emitUp($event, ...$params)\n    {\n        (BrowserEventsService::getInstance())->emitUp($event, $params);\n    }\n\n    public function dispatchBrowserEvent($event, $params = [])\n    {\n        (BrowserEventsService::getInstance())->dispatchBrowserEvent($event, $params);\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Concerns/Redirector.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Concerns;\n\ntrait Redirector\n{\n    public $redirectTo;\n\n    public function redirect($url)\n    {\n        $this->redirectTo = $url;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Concerns/ResponseHeaders.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Concerns;\n\ntrait ResponseHeaders\n{\n    public function location($path)\n    {\n        $this->header('HX-Location', $path);\n\n        return $this;\n    }\n\n    public function pushUrl($url)\n    {\n        $this->header('HX-Push-Url', $url);\n\n        return $this;\n    }\n\n    public function redirect($url)\n    {\n        $this->header('HX-Redirect', $url);\n\n        return $this;\n    }\n\n    public function refresh()\n    {\n        $this->header('HX-Refresh', 'true');\n\n        return $this;\n    }\n\n    public function replaceUrl($url)\n    {\n        $this->header('HX-Replace-Url', $url);\n\n        return $this;\n    }\n\n    public function reswap($swap)\n    {\n        $this->header('HX-Reswap', $swap);\n\n        return $this;\n    }\n\n    public function reselect($selector)\n    {\n        $this->header('HX-Reselect', $selector);\n\n        return $this;\n    }\n\n    public function retarget($selector)\n    {\n        $this->header('HX-Retarget', $selector);\n\n        return $this;\n    }\n\n    public function trigger($event)\n    {\n        $this->header('HX-Trigger', $event);\n\n        return $this;\n    }\n\n    public function triggerAfterSwap($event)\n    {\n        $this->header('HX-Trigger-After-Swap', $event);\n\n        return $this;\n    }\n\n    public function triggerAfterSettle($event)\n    {\n        $this->header('HX-Trigger-After-Settle', $event);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Concerns/Singleton.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Concerns;\n\ntrait Singleton\n{\n    /**\n     * @var reference to singleton instance\n     */\n    private static $instance;\n\n    /**\n     * Creates a new instance of a singleton class (via late static binding),\n     * accepting a variable-length argument list.\n     *\n     * @return self\n     */\n    final public static function getInstance(...$params)\n    {\n        if (! isset(static::$instance)) {\n            static::$instance = new self(...$params);\n        }\n\n        return static::$instance;\n    }\n\n    /**\n     * Prevents cloning the singleton instance.\n     *\n     * @return void\n     */\n    public function __clone()\n    {\n    }\n\n    /**\n     * Prevents unserializing the singleton instance.\n     *\n     * @return void\n     */\n    public function __wakeup()\n    {\n    }\n}\n"
  },
  {
    "path": "src/yoyo/ContainerResolver.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Containers\\IlluminateContainer;\nuse Clickfwd\\Yoyo\\Containers\\YoyoContainer;\nuse Clickfwd\\Yoyo\\Interfaces\\YoyoContainerInterface;\nuse Illuminate\\Container\\Container;\n\nclass ContainerResolver\n{\n    protected static $preferred = null;\n\n    public static function setPreferred(?YoyoContainerInterface $container)\n    {\n        static::$preferred = $container;\n    }\n\n    public static function getPreferred()\n    {\n        return static::$preferred;\n    }\n\n    public static function resolve(): YoyoContainerInterface\n    {\n        if (static::$preferred) {\n            return static::$preferred;\n        }\n\n        if (class_exists(Container::class)) {\n            return new IlluminateContainer(Container::getInstance());\n        }\n\n        return YoyoContainer::getInstance();\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Containers/IlluminateContainer.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Containers;\n\nuse Clickfwd\\Yoyo\\Interfaces\\YoyoContainerInterface;\nuse Closure;\nuse Illuminate\\Container\\Container;\n\nclass IlluminateContainer implements YoyoContainerInterface\n{\n    protected static $instance;\n\n    protected $container;\n\n    public static function getInstance()\n    {\n        return static::$instance = static::$instance ?? new static(Container::getInstance());\n    }\n\n    public function __construct(Container $container)\n    {\n        $this->container = $container;\n    }\n\n    public function get(string $id)\n    {\n        return $this->container->get($id);\n    }\n\n    public function has(string $id): bool\n    {\n        return $this->container->has($id);\n    }\n\n    public function set(string $id, $value)\n    {\n        if (is_object($value) && ! ($value instanceof Closure)) {\n            return $this->container->instance($id, $value);\n        }\n\n        $this->container->bind($id, $value, true);\n\n        return $value;\n    }\n\n    public function make(string $class, array $args = [])\n    {\n        return $this->container->make($class, $args);\n    }\n\n    public function call($method, array $args = [])\n    {\n        return $this->container->call($method, $args);\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Containers/YoyoContainer.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Containers;\n\nuse Clickfwd\\Yoyo\\Exceptions\\BindingNotFoundException;\nuse Clickfwd\\Yoyo\\Exceptions\\ContainerResolutionException;\nuse Clickfwd\\Yoyo\\Interfaces\\YoyoContainerInterface;\n\nclass YoyoContainer implements YoyoContainerInterface\n{\n    protected static $instance;\n\n    protected $bindings = [];\n\n    public static function getInstance()\n    {\n        return static::$instance = static::$instance ?? new static();\n    }\n\n    public function get(string $id)\n    {\n        if (! $this->has($id)) {\n            throw new BindingNotFoundException(\"[$id] is not bound to the container\");\n        }\n\n        $resolved = $this->bindings[$id];\n\n        if ($resolved instanceof \\Closure) {\n            $this->bindings[$id] = $resolved($this);\n        }\n\n        if (is_string($resolved) && class_exists($resolved)) {\n            return $this->make($resolved);\n        }\n\n        return $this->bindings[$id];\n    }\n\n    public function has(string $id): bool\n    {\n        return isset($this->bindings[$id]);\n    }\n\n    public function set(string $id, $value)\n    {\n        $this->bindings[$id] = $value;\n\n        return $value;\n    }\n\n    public function make(string $class, array $args = [])\n    {\n        try {\n            $class = $this->has($class) ? $this->get($class) : $class;\n            return is_object($class) ? $class : new $class(...$this->extractArgs($class, '__construct', $args));\n        } catch (\\Throwable $e) {\n            throw new ContainerResolutionException(\"[$class] could not be resolved\", $e);\n        }\n    }\n\n    public function call(callable $method, array $args = [])\n    {\n        if (! is_array($method) || count($method) !== 2) {\n            throw new \\InvalidArgumentException(\"Callable must be in [class, method] format\");\n        }\n\n        return $method(...$this->extractArgs($method[0], $method[1], $args));\n    }\n\n    protected function extractArgs($class, $method, $arguments)\n    {\n        try {\n            $result = [];\n            $reflector = new \\ReflectionClass($class);\n            $parameters = $reflector->getMethod($method)->getParameters();\n        } catch (\\ReflectionException $e) {\n            return $arguments;\n        }\n\n        foreach ($parameters as $parameter) {\n            // Variadic arguments\n            if ($parameter->isVariadic()) {\n                return array_merge($result, array_values($arguments));\n            }\n            // Named argument\n            elseif (isset($arguments[$parameter->getName()])) {\n                $result[] = $arguments[$parameter->getName()];\n                unset($arguments[$parameter->getName()]);\n            }\n            // Typed argument\n            elseif ($this->isResolvableType($parameter)) {\n                $result[] = $this->make($parameter->getType()->getName());\n            }\n            // Argument with default value\n            elseif ($parameter->isDefaultValueAvailable()) {\n                $result[] = $parameter->getDefaultValue();\n            }\n            // Nullable argument\n            elseif ($parameter->allowsNull()) {\n                $result[] = null;\n            }\n            // Default - assume explicit arguments\n            else {\n                return $arguments;\n            }\n        }\n\n        return $result;\n    }\n\n    protected function isResolvableType(\\ReflectionParameter $parameter): bool\n    {\n        if ($parameter->getType() instanceof \\ReflectionNamedType) {\n            $name = $parameter->getType()->getName();\n            return $name && (class_exists($name) || interface_exists($name));\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Exceptions/BindingNotFoundException.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nuse Psr\\Container\\NotFoundExceptionInterface;\n\nclass BindingNotFoundException extends \\Exception implements NotFoundExceptionInterface\n{\n    public function __construct(string $message, ?\\Throwable $previous = null)\n    {\n        parent::__construct($message, 0, $previous);\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Exceptions/BypassRenderMethod.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass BypassRenderMethod extends \\Exception\n{\n    public function __construct($statusCode)\n    {\n        parent::__construct('', $statusCode);\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Exceptions/ComponentMethodNotFound.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass ComponentMethodNotFound extends \\Exception\n{\n    public function __construct($component, $method)\n    {\n        parent::__construct(\n            \"Public method [{$method}] not found on Yoyo component [{$component}]\"\n        );\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Exceptions/ComponentNotFound.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass ComponentNotFound extends \\Exception\n{\n    public function __construct($alias)\n    {\n        parent::__construct(\"Yoyo component with alias [$alias] not found.\");\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Exceptions/ContainerResolutionException.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nuse Psr\\Container\\ContainerExceptionInterface;\n\nclass ContainerResolutionException extends \\Exception implements ContainerExceptionInterface\n{\n    public function __construct(string $message, ?\\Throwable $previous = null)\n    {\n        parent::__construct($message, 0, $previous);\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Exceptions/FailedToRegisterComponent.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass FailedToRegisterComponent extends \\Exception\n{\n    public function __construct($alias, $componentClassName)\n    {\n        $message = 'Component registration failed.';\n\n        if ($componentClassName == 'Anonymous') {\n            $message = PHP_EOL.\"[$alias] template not found for Yoyo component [$componentClassName].\";\n        } else {\n            $message = PHP_EOL.\"Yoyo component class [$componentClassName] provided for alias [$alias] not found.\";\n        }\n\n        parent::__construct($message);\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Exceptions/HttpException.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass HttpException extends \\Exception\n{\n    protected $statusCode;\n\n    protected $headers;\n\n    public function __construct(int $statusCode, ?string $message = '', array $headers = [])\n    {\n        $this->statusCode = $statusCode;\n\n        $this->headers = $headers;\n\n        parent::__construct($message, $statusCode);\n    }\n\n    public function getStatusCode()\n    {\n        return $this->statusCode;\n    }\n\n    public function getHeaders()\n    {\n        return $this->headers;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Exceptions/IncompleteComponentParamInRequest.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass IncompleteComponentParamInRequest extends \\Exception\n{\n    public function __construct()\n    {\n        parent::__construct('The component parameter is missing the component name or action.');\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Exceptions/MissingComponentTemplate.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass MissingComponentTemplate extends \\Exception\n{\n    public function __construct($template, $componentName)\n    {\n        parent::__construct(\"Unable to find template [$template] for [$componentName] component.\");\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Exceptions/NonPublicComponentMethodCall.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass NonPublicComponentMethodCall extends \\Exception\n{\n    public function __construct($componentName, $method)\n    {\n        parent::__construct(\"Unable to call non-public method [$method] in Yoyo component [$componentName].\");\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Exceptions/NotFoundHttpException.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Exceptions;\n\nclass NotFoundHttpException extends HttpException\n{\n    public function __construct(?string $message = '', array $headers = [])\n    {\n        parent::__construct(404, $message, $headers);\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Interfaces/RequestInterface.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Interfaces;\n\ninterface RequestInterface\n{\n    public function all();\n\n    public function except($keys);\n\n    public function get($key, $default = null);\n\n    public function drop($key);\n\n    public function method();\n\n    public function fullUrl();\n\n    public function isYoyoRequest();\n\n    public function windUp();\n\n    public function triggerId();\n}\n"
  },
  {
    "path": "src/yoyo/Interfaces/ViewProviderInterface.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Interfaces;\n\ninterface ViewProviderInterface\n{\n    public const HINT_PATH_DELIMITER = '::';\n\n    public function __construct($view);\n\n    public function render($template, $vars = []): self;\n\n    public function makeFromString($content, $vars = []): string;\n\n    public function exists($template): bool;\n\n    public function getProviderInstance();\n\n    public function startYoyoRendering($component): void;\n\n    public function stopYoyoRendering(): void;\n}\n"
  },
  {
    "path": "src/yoyo/Interfaces/YoyoContainerInterface.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Interfaces;\n\nuse Closure;\nuse Psr\\Container\\ContainerInterface;\n\ninterface YoyoContainerInterface extends ContainerInterface\n{\n    /**\n     * Binds value to the container for later resolution\n     *\n     * @param string $id\n     * @param Closure|object|string $value\n     * @return void\n     */\n    public function set(string $id, $value);\n\n    /**\n     * Makes the class with provided attributes\n     *\n     * @param string $class\n     * @param array $args\n     * @return mixed\n     */\n    public function make(string $class, array $args = []);\n\n    /**\n     * Calls the method with provided attributes\n     *\n     * @param callable $method\n     * @param array $args\n     * @return mixed\n     */\n    public function call(callable $method, array $args = []);\n}\n"
  },
  {
    "path": "src/yoyo/InvocableComponentVariable.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Closure;\n\nclass InvocableComponentVariable\n{\n    protected $callable;\n\n    public function __construct(Closure $callable)\n    {\n        $this->callable = $callable;\n    }\n\n    public function __get($key)\n    {\n        return $this->__invoke()->{$key};\n    }\n\n    public function __call($method, $parameters)\n    {\n        return $this->__invoke()->{$method}(...$parameters);\n    }\n\n    public function __invoke()\n    {\n        return call_user_func($this->callable);\n    }\n\n    public function __toString()\n    {\n        return (string) $this->__invoke();\n    }\n}\n"
  },
  {
    "path": "src/yoyo/QueryString.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Services\\Request;\n\nclass QueryString\n{\n    private $defaults;\n\n    private $new;\n\n    private $keys;\n\n    private $currentUrl;\n\n    public function __construct($defaults, $new, $keys)\n    {\n        $this->defaults = $defaults;\n\n        $this->new = $new;\n\n        $this->keys = $keys;\n\n        $this->currentUrl = Yoyo::request()->fullUrl();\n    }\n\n    /**\n     * Used to pass variables to the component request.\n     */\n    public function getQueryParams()\n    {\n        $queryParams = array_merge($this->defaults, $this->new);\n\n        // Filter out keys that are not explicitly set in the component queryString property\n\n        $queryParams = array_intersect_key($queryParams, array_flip($this->keys));\n\n        return $queryParams;\n    }\n\n    /**\n     * Used to update the browser URL state.\n     */\n    public function getPageQueryParams()\n    {\n        if (! $this->currentUrl) {\n            return [];\n        }\n\n        // Filter out keys that are not explicitly set in the component queryString property\n\n        $new = array_intersect_key($this->new, array_flip($this->keys));\n\n        // Get current query string values and merge them with new ones\n\n        $queryString = parse_url(htmlspecialchars_decode($this->currentUrl), PHP_URL_QUERY) ?? '';\n\n        parse_str($queryString, $args);\n\n        $queryParams = array_merge($args, $new);\n\n        // If a query string value matches the default value, remove it from the URL\n\n        foreach ($queryParams as $key => $val) {\n            if (is_object($val) && method_exists($val, 'toArray')) {\n                $queryParams[$key] = $val->toArray();\n            }\n\n            if ((isset($this->defaults[$key]) && $val === $this->defaults[$key]) || $val === '') {\n                unset($queryParams[$key]);\n            }\n        }\n\n        return $queryParams;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Request.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Interfaces\\RequestInterface;\n\nclass Request implements RequestInterface\n{\n    private $request;\n\n    private $server;\n\n    private $dropped = [];\n\n    private $decodedRequest = null;\n\n    public function __construct()\n    {\n        $this->request = $_REQUEST;\n\n        $this->server = $_SERVER;\n    }\n\n    public function mock($request, $server)\n    {\n        $this->request = $request;\n\n        $this->server = $server;\n\n        $this->decodedRequest = null;\n\n        return $this;\n    }\n\n    public function reset()\n    {\n        $this->request = [];\n\n        $this->server = [];\n\n        $this->decodedRequest = null;\n    }\n\n    public function all()\n    {\n        if ($this->decodedRequest !== null) {\n            return $this->decodedRequest;\n        }\n\n        return $this->decodedRequest = array_map(function ($value) {\n            $validJson = false;\n            $decoded = YoyoHelpers::test_json($value, $validJson);\n\n            if ($validJson) {\n                return $decoded;\n            }\n\n            return $value;\n        }, $this->request);\n    }\n\n    public function except($keys)\n    {\n        $keys = is_array($keys) ? $keys : [$keys];\n\n        $all = $this->all();\n\n        $output = [];\n\n        foreach ($all as $key => $value) {\n            if (in_array($key, $keys)) {\n                continue;\n            }\n\n            $output[$key] = $value;\n        }\n\n        return $output;\n    }\n\n    public function only($keys)\n    {\n        return array_intersect_key($this->all(), array_flip($keys));\n    }\n\n    public function get($key, $default = null)\n    {\n        if (in_array($key, $this->dropped)) {\n            return $default;\n        }\n\n        $value = $this->request[$key] ?? $default;\n\n        $validJson = false;\n        $decoded = YoyoHelpers::test_json($value, $validJson);\n\n        if ($validJson) {\n            return $decoded;\n        }\n\n        return $value;\n    }\n\n    public function startsWith($prefix)\n    {\n        $vars = [];\n\n        foreach ($this->all() as $key => $value) {\n            if (strpos($key, $prefix) === 0) {\n                $vars[$key] = $value;\n            }\n        }\n\n        return $vars;\n    }\n\n    public function set($key, $value)\n    {\n        $this->request[$key] = $value;\n\n        $this->decodedRequest = null;\n\n        return $this;\n    }\n\n    public function merge($data)\n    {\n        $this->request = array_merge($this->request, $data);\n\n        $this->decodedRequest = null;\n\n        return $this;\n    }\n\n    public function drop($key)\n    {\n        $this->dropped[] = $key;\n    }\n\n    public function method()\n    {\n        return $this->server['REQUEST_METHOD'] ?? 'GET';\n    }\n\n    public function fullUrl()\n    {\n        if (isset($this->server['HTTP_HX_CURRENT_URL'])) {\n            return $this->server['HTTP_HX_CURRENT_URL'];\n        }\n\n        if (empty($this->server['HTTP_HOST'])) {\n            return null;\n        }\n\n        $protocol = 'http';\n\n        if (isset($this->server['HTTPS']) && $this->server['HTTPS'] === 'on') {\n            $protocol = 'https';\n        }\n\n        $host = $this->server['HTTP_HOST'];\n\n        $path = rtrim($this->server['REQUEST_URI'] ?? '', '?');\n\n        return \"{$protocol}://{$host}{$path}\";\n    }\n\n    public function isYoyoRequest()\n    {\n        return $this->server['HTTP_HX_REQUEST'] ?? false;\n    }\n\n    public function windUp()\n    {\n        unset($this->server['HTTP_HX_REQUEST']);\n    }\n\n    public function triggerId()\n    {\n        return $this->server['HTTP_HX_TRIGGER'];\n    }\n\n    public function triggerName()\n    {\n        return $this->server['HTTP_HX_TRIGGER_NAME'] ?? null;\n    }\n\n    public function header($name)\n    {\n        return $this->server['HTTP_'.strtoupper($name)] ?? null;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Services/BrowserEventsService.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Services;\n\nuse Clickfwd\\Yoyo\\Concerns\\Singleton;\nuse Clickfwd\\Yoyo\\Yoyo;\n\nclass BrowserEventsService\n{\n    use Singleton;\n\n    private $request;\n\n    private $response;\n\n    private $eventQueue = [];\n\n    private $browserEventQueue = [];\n\n    public function __construct()\n    {\n        $this->request = Yoyo::request();\n\n        $this->response = Response::getInstance();\n    }\n\n    public function emit($event, ...$params)\n    {\n        $this->queue($event, $params);\n    }\n\n    public function emitTo($target, $event, ...$params)\n    {\n        $this->queue($event, $params, null, $target);\n    }\n\n    public function emitToWithSelector($target, $event, ...$params)\n    {\n        $this->queue($event, $params, $target);\n    }\n\n    public function emitSelf($event, ...$params)\n    {\n        if ($component = $this->getComponentNameFromRequest()) {\n            $this->queue($event, $params, $selector = null, $component, 'self');\n        }\n    }\n\n    public function emitUp($event, ...$params)\n    {\n        $targetId = $this->request->triggerId();\n        if ($component = $this->getComponentNameFromRequest()) {\n            $this->queue($event, $params, \"#{$targetId}\", $component, 'ancestorsOnly');\n        }\n    }\n\n    public function queue($event, $params, $selector = null, $component = null, $propagation = null)\n    {\n        $params = $params[0];\n\n        $payload = array_filter(compact('event', 'params', 'selector', 'component', 'propagation'));\n\n        $this->eventQueue[] = $payload;\n    }\n\n    public function dispatchBrowserEvent($event, $params = [])\n    {\n        $this->browserEventQueue[] = compact('event', 'params');\n    }\n\n    public function dispatch()\n    {\n        $this->response->header('Yoyo-Emit', json_encode($this->eventQueue));\n\n        $this->response->header('Yoyo-Browser-Event', json_encode($this->browserEventQueue));\n    }\n\n    protected function getComponentNameFromRequest()\n    {\n        if ($name = $this->request->get('component')) {\n            return explode('/', $name)[0];\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Services/Configuration.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Services;\n\nuse Clickfwd\\Yoyo\\Concerns\\Singleton;\n\nclass Configuration\n{\n    use Singleton;\n\n    private static $options;\n\n    public static $htmx = '1.9.4';\n\n    protected static $allowedConfigOptions = [\n        'historyEnabled',\n        'historyCacheSize',\n        'refreshOnHistoryMiss',\n        'defaultSwapStyle',\n        'defaultSwapDelay',\n        'defaultSettleDelay',\n        'includeIndicatorStyles',\n        'indicatorClass',\n        'requestClass',\n        'addedClass',\n        'settlingClass',\n        'swappingClass',\n        'allowEval',\n        'inlineScriptNonce',\n        'attributesToSettle',\n        'withCredentials',\n        'timeout',\n        'wsReconnectDelay',\n        'wsBinaryType',\n        'disableSelector',\n        'useTemplateFragments',\n        'scrollBehavior',\n        'defaultFocusScroll',\n        'getCacheBusterParam',\n        'globalViewTransitions',\n        'methodsThatUseUrlParams',\n    ];\n\n    public function __construct($options)\n    {\n        self::$options = array_merge([\n            'namespace' => 'App\\\\Yoyo\\\\',\n            'defaultSwapStyle' => 'outerHTML',\n            'historyEnabled' => false,\n            'indicatorClass' => 'yoyo-indicator',\n            'requestClass' => 'yoyo-request',\n            'settlingClass' => 'yoyo-settling',\n            'swappingClass' => 'yoyo-swapping',\n        ], $options);\n    }\n\n    public static function get($key, $default = null)\n    {\n        return self::$options[$key] ?? $default;\n    }\n\n    public static function scripts($return = false)\n    {\n        return self::minify(self::javascriptAssets());\n    }\n\n    public static function styles()\n    {\n        return self::minify(self::cssStyle());\n    }\n\n    public static function htmxSrc(): string\n    {\n        if (empty($htmxSrc = self::get('htmx'))) {\n            $htmxSrc = 'https://unpkg.com/htmx.org@'.self::$htmx.'/dist/htmx.min.js';\n        }\n\n        return $htmxSrc;\n    }\n\n    public static function yoyoSrc(): string\n    {\n        return rtrim(self::get('scriptsPath', ''), '/').'/yoyo.js';\n    }\n\n    public static function javascriptAssets(): string\n    {\n        $htmxSrc = self::htmxSrc();\n        $yoyoSrc = self::yoyoSrc();\n        $initCode = self::javascriptInitCode();\n\n        return <<<HTML\n        <script src=\"{$htmxSrc}\"></script>\n        <script src=\"{$yoyoSrc}\"></script>\n        {$initCode}\nHTML;\n    }\n\n    public static function javascriptInitCode($includeScriptTag = true): string\n    {\n        $yoyoRoute = self::get('url', '');\n        $configuration = array_intersect_key(static::$options, array_flip(static::$allowedConfigOptions));\n        $yoyoConfig = json_encode($configuration);\n\n        $yoyoRouteJs = json_encode($yoyoRoute);\n\n        $script = <<<HTML\n        Yoyo.url = {$yoyoRouteJs};\n        Yoyo.config($yoyoConfig);\nHTML;\n\n        if ($includeScriptTag) {\n            $script = \"<script>{$script}</script>\";\n        }\n\n        return $script;\n    }\n\n    public static function cssStyle($includeStyleTag = true)\n    {\n        $style = <<<HTML\n        [yoyo\\:spinning], [yoyo\\:spinning\\.delay] {\n            display: none;\n        }\nHTML;\n\n        if ($includeStyleTag) {\n            $style = \"<style>$style</style>\";\n        }\n\n        return $style;\n    }\n\n    protected static function minify($string)\n    {\n        return preg_replace('~(\\v|\\t|\\s{2,})~m', '', $string);\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Services/PageRedirectService.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Services;\n\nuse Clickfwd\\Yoyo\\Concerns\\Singleton;\n\nclass PageRedirectService\n{\n    use Singleton;\n\n    private $response;\n\n    public function __construct()\n    {\n        $this->response = Response::getInstance();\n    }\n\n    public function redirect($url)\n    {\n        if ($url) {\n            $this->response->header('Yoyo-Redirect', $url);\n        }\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Services/Response.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Services;\n\nuse Clickfwd\\Yoyo\\Concerns;\n\nclass Response\n{\n    use Concerns\\Singleton;\n    use Concerns\\ResponseHeaders;\n\n    protected $headers = [];\n\n    protected $statusCode = 200;\n\n    public function __construct()\n    {\n    }\n\n    public function header($name, $value)\n    {\n        // Sanitize header name and value to prevent header injection\n        $name = str_replace([\"\\r\", \"\\n\", \"\\0\"], '', (string) $name);\n        $value = is_array($value) ? $value : str_replace([\"\\r\", \"\\n\", \"\\0\"], '', (string) $value);\n\n        $this->headers[$name] = $value;\n\n        return $this;\n    }\n\n    public function status($statusCode)\n    {\n        $this->statusCode = $statusCode;\n\n        return $this;\n    }\n\n    public function send(string $content = ''): string\n    {\n        if (! headers_sent()) {\n            foreach ($this->headers as $key => $value) {\n                if (is_array($value)) {\n                    $value = json_encode($value);\n                }\n\n                header(\"$key: $value\");\n            }\n\n            http_response_code($this->statusCode ?? 200);\n        }\n\n        return $content ?: '';\n    }\n\n    public function setHeaders($headers)\n    {\n        $this->headers = array_merge($this->headers, $headers);\n\n        return $this;\n    }\n\n    public function getHeaders()\n    {\n        return $this->headers;\n    }\n\n    public function getStatusCode()\n    {\n        return $this->statusCode;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Services/UrlStateManagerService.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Services;\n\nuse Clickfwd\\Yoyo\\Yoyo;\n\nclass UrlStateManagerService\n{\n    private $request;\n\n    private $currentUrl;\n\n    public function __construct()\n    {\n        $this->request = Yoyo::request();\n\n        $this->currentUrl = $this->request->fullUrl();\n    }\n\n    public function pushState($queryParams)\n    {\n        $response = Response::getInstance();\n\n        if (! $this->currentUrl || $this->request->method() !== 'GET') {\n            return;\n        }\n\n        // Don't override if the component already set an explicit URL\n        $headers = $response->getHeaders();\n        if (isset($headers['HX-Replace-Url']) || isset($headers['HX-Push-Url'])) {\n            return;\n        }\n\n        $parsedUrl = parse_url($this->currentUrl);\n\n        $port = isset($parsedUrl['port']) ? (':'.$parsedUrl['port']) : '';\n        $url = $parsedUrl['scheme'].'://'.$parsedUrl['host'].$port.$parsedUrl['path'].($queryParams ? '?'.http_build_query($queryParams) : '');\n\n        if ($url !== $this->currentUrl) {\n            $response->header('Yoyo-Push', $url);\n        }\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Twig/YoyoTwigExtension.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\Twig;\n\nuse Clickfwd\\Yoyo\\Services\\BrowserEventsService;\nuse Twig\\Extension\\AbstractExtension;\nuse Twig\\Extension\\GlobalsInterface;\nuse Twig\\Markup;\nuse Twig\\TwigFunction;\n\nuse function Yoyo\\yoyo_render;\nuse function Yoyo\\yoyo_scripts;\n\nclass YoyoTwigExtension extends AbstractExtension implements GlobalsInterface\n{\n    public function getFunctions()\n    {\n        return [\n            $this->yoyo_scripts(),\n            $this->yoyo(),\n            $this->emit(),\n            $this->emitTo(),\n            $this->emitToWithSelector(),\n            $this->emitSelf(),\n            $this->emitUp(),\n        ];\n    }\n\n    public function getGlobals(): array\n    {\n        return [\n        ];\n    }\n\n    private function yoyo_scripts()\n    {\n        return new TwigFunction('yoyo_scripts', function (): Markup {\n            return self::raw(yoyo_scripts());\n        });\n    }\n\n    private function yoyo()\n    {\n        return new TwigFunction('yoyo', function ($name, $variables = [], $attributes = []): Markup {\n            $variables = $variables ?? [];\n\n            $attributes = $attributes ?? [];\n\n            $output = yoyo_render($name, $variables, $attributes);\n\n            return self::raw($output);\n        });\n    }\n\n    private function emit()\n    {\n        return new TwigFunction('emit', function ($eventName, $payload = []) {\n            (BrowserEventsService::getInstance())->emit($eventName, $payload);\n        });\n    }\n\n    private function emitTo()\n    {\n        return new TwigFunction('emitTo', function ($target, $eventName, $payload = []) {\n            (BrowserEventsService::getInstance())->emitTo($target, $eventName, $payload);\n        });\n    }\n\n    private function emitToWithSelector()\n    {\n        return new TwigFunction('emitToWithSelector', function ($target, $eventName, $payload = []) {\n            (BrowserEventsService::getInstance())->emitToWithSelector($target, $eventName, $payload);\n        });\n    }\n\n    private function emitSelf()\n    {\n        return new TwigFunction('emitSelf', function ($eventName, $payload = []) {\n            (BrowserEventsService::getInstance())->emitSelf($eventName, $payload);\n        });\n    }\n\n    private function emitUp()\n    {\n        return new TwigFunction('emitUp', function ($eventName, $payload = []) {\n            (BrowserEventsService::getInstance())->emitUp($eventName, $payload);\n        });\n    }\n\n    private static function raw($string)\n    {\n        return new Markup($string, 'UTF-8');\n    }\n}\n"
  },
  {
    "path": "src/yoyo/View.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Interfaces\\ViewProviderInterface;\nuse InvalidArgumentException;\n\nclass View\n{\n    protected $paths;\n\n    protected $views;\n\n    protected $yoyoComponent;\n\n    protected static $hints;\n\n    public function __construct($paths)\n    {\n        $paths = (array) $paths;\n\n        $this->paths = array_map([$this, 'resolvePath'], $paths);\n    }\n\n    /**\n     * Forward method calls to their property closure function equivalent\n     * Used for eventManager emit methods dynamically added to the view class.\n     */\n    public function __call($name, $args)\n    {\n        return call_user_func_array($this->$name, $args);\n    }\n\n    public function startYoyoRendering($component)\n    {\n        $this->yoyoComponent = $component;\n\n        return $this;\n    }\n\n    public function render($name, $vars = []): string\n    {\n        $path = $this->exists($name);\n\n        ob_start();\n\n        \\Closure::bind(function () use ($path, $vars) {\n            extract($vars, EXTR_SKIP);\n            include $path;\n        }, $this->yoyoComponent ? $this->yoyoComponent : $this)();\n\n        return ltrim(ob_get_clean());\n    }\n\n    public function makeFromString($content, $vars = []): string\n    {\n        throw new \\Exception('Views from strings are not supported with the native Yoyo view provider.');\n    }\n\n    public function exists($name)\n    {\n        if (isset($this->views[$name])) {\n            return $this->views[$name];\n        }\n\n        if ($this->hasHintInformation($name = trim($name))) {\n            return $this->views[$name] = $this->findNamespacedView($name);\n        }\n\n        return $this->views[$name] = $this->findInPaths($name, $this->paths);\n    }\n\n    public function addLocation($location)\n    {\n        $this->paths[] = $this->resolvePath($location);\n    }\n\n    public function prependLocation($location)\n    {\n        array_unshift($this->paths, $this->resolvePath($location));\n    }\n\n    protected function findInPaths($name, $paths)\n    {\n        $templatePath = str_replace('.', '/', $name);\n\n        foreach ($paths as $path) {\n            if (file_exists($location = \"{$path}/{$templatePath}.php\")) {\n                return $location;\n            }\n        }\n\n        throw new InvalidArgumentException(\"View [{$name}] not found.\");\n    }\n\n    protected function findNamespacedView($name)\n    {\n        [$namespace, $view] = $this->parseNamespaceSegments($name);\n\n        return $this->findInPaths($view, static::$hints[$namespace]);\n    }\n\n    protected function parseNamespaceSegments($name)\n    {\n        $segments = explode(ViewProviderInterface::HINT_PATH_DELIMITER, $name);\n\n        if (count($segments) !== 2) {\n            throw new InvalidArgumentException(\"View [{$name}] has an invalid name.\");\n        }\n\n        if (! isset(static::$hints[$segments[0]])) {\n            throw new InvalidArgumentException(\"No hint path defined for [{$segments[0]}].\");\n        }\n\n        return $segments;\n    }\n\n    public function addNamespace($namespace, $hints)\n    {\n        $hints = (array) $hints;\n\n        if (isset(static::$hints[$namespace])) {\n            $hints = array_merge(static::$hints[$namespace], $hints);\n        }\n\n        static::$hints[$namespace] = $hints;\n    }\n\n    public function prependNamespace($namespace, $hints)\n    {\n        $hints = (array) $hints;\n\n        if (isset(static::$hints[$namespace])) {\n            $hints = array_merge($hints, static::$hints[$namespace]);\n        }\n\n        static::$hints[$namespace] = $hints;\n    }\n\n    public function hasHintInformation($name)\n    {\n        return strpos($name, ViewProviderInterface::HINT_PATH_DELIMITER) > 0;\n    }\n\n    protected function resolvePath($path)\n    {\n        return realpath($path) ?: $path;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/ViewProviders/BaseViewProvider.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\ViewProviders;\n\nabstract class BaseViewProvider\n{\n    protected $view;\n\n    public function getProviderInstance()\n    {\n        return $this->view;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/ViewProviders/BladeViewProvider.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\ViewProviders;\n\nuse Clickfwd\\Yoyo\\Interfaces\\ViewProviderInterface;\n\nclass BladeViewProvider extends BaseViewProvider implements ViewProviderInterface\n{\n    protected $view;\n\n    protected $template;\n\n    protected $vars;\n\n    protected $engine;\n\n    public function __construct($view)\n    {\n        $this->view = $view;\n    }\n\n    public function startYoyoRendering($component): void\n    {\n        $this->engine = $this->view->getContainer()->get('view.engine.resolver')->resolve('blade');\n\n        $this->engine->startYoyoRendering($component);\n    }\n\n    public function stopYoyoRendering(): void\n    {\n        $this->engine->stopYoyoRendering();\n    }\n\n    public function render($template, $vars = []): ViewProviderInterface\n    {\n        $this->template = $template;\n\n        $this->vars = $vars;\n\n        return $this;\n    }\n\n    public function makeFromString($content, $vars = []): string\n    {\n        $view = $this->view->make((new \\Clickfwd\\Yoyo\\Blade\\CreateBladeViewFromString())($this->view, $content));\n\n        return $view->with($vars)->render();\n    }\n\n    public function exists($template): bool\n    {\n        return $this->view->exists($template);\n    }\n\n    public function getFinder()\n    {\n        return $this->view->getFinder();\n    }\n\n    public function addNamespace($namespace, $hints)\n    {\n        $this->getFinder()->addNamespace($namespace, $hints);\n\n        return $this;\n    }\n\n    public function prependNamespace($namespace, $hints)\n    {\n        $this->getFinder()->prependNamespace($namespace, $hints);\n\n        return $this;\n    }\n\n    public function addLocation($location)\n    {\n        $this->getFinder()->addLocation($location);\n\n        return $this;\n    }\n\n    public function prependLocation($location)\n    {\n        $this->getFinder()->prependLocation($location);\n\n        return $this;\n    }\n\n    public function __call(string $method, array $params)\n    {\n        return call_user_func_array([$this->view, $method], $params);\n    }\n\n    public function __toString()\n    {\n        $output = (string) $this->view->make($this->template, $this->vars);\n\n        $this->stopYoyoRendering();\n\n        return $output;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/ViewProviders/PhalconViewProvider.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\ViewProviders;\n\nuse Clickfwd\\Yoyo\\Interfaces\\ViewProviderInterface;\n\nclass PhalconViewProvider extends BaseViewProvider implements ViewProviderInterface\n{\n    protected $view;\n\n    protected $template;\n\n    protected $vars;\n\n    private $viewExtention = '.phtml';\n\n    public function __construct($view)\n    {\n        $this->view = $view;\n    }\n\n    public function exists($view): bool\n    {\n        return file_exists($this->view->getViewsDir() . $view . $this->viewExtention);\n    }\n\n    public function render($template, $vars = []): ViewProviderInterface\n    {\n        $this->template = $template;\n        $this->vars = $vars;\n\n        return $this;\n    }\n\n    public function setViewExtention($viewExtention)\n    {\n        $this->viewExtention = $viewExtention;\n\n        return $this;\n    }\n\n    public function makeFromString($content, $vars = []): string\n    {\n        $this->view->start();\n        $this->view->setContent($content);\n        $this->view->setVars($vars);\n        $this->view->finish();\n        return $this->view->render();\n    }\n\n    public function startYoyoRendering($component): void\n    {\n    }\n\n    public function stopYoyoRendering(): void\n    {\n    }\n\n    public function __toString()\n    {\n        return $this->view->render($this->template, $this->vars);\n    }\n}\n"
  },
  {
    "path": "src/yoyo/ViewProviders/TwigViewProvider.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\ViewProviders;\n\nuse Clickfwd\\Yoyo\\Interfaces\\ViewProviderInterface;\n\nclass TwigViewProvider extends BaseViewProvider implements ViewProviderInterface\n{\n    protected $view;\n\n    protected $template;\n\n    protected $vars;\n\n    protected $yoyoComponent;\n\n    public static $twig_template_extension = 'twig';\n\n    public function __construct($view)\n    {\n        $this->view = $view;\n    }\n\n    public function startYoyoRendering($component): void\n    {\n        $this->yoyoComponent = $component;\n    }\n\n    public function stopYoyoRendering(): void\n    {\n        //\n    }\n\n    public function normalizeName($template)\n    {\n        if (strpos($template, ViewProviderInterface::HINT_PATH_DELIMITER) > 0) {\n            [$namespace, $name] = explode(ViewProviderInterface::HINT_PATH_DELIMITER, $template);\n\n            return \"@{$namespace}/{$name}\";\n        }\n\n        return $template;\n    }\n\n    public function render($template, $vars = []): ViewProviderInterface\n    {\n        $this->template = $this->normalizeName($template);\n\n        $this->vars = $vars;\n\n        return $this;\n    }\n\n    public function makeFromString($content, $vars = []): string\n    {\n        $template = $this->view->createTemplate((string) $content);\n\n        return $template->render($vars);\n    }\n\n    public function exists($template): bool\n    {\n        $template = $this->normalizeName($template);\n\n        return $this->getLoader()->exists($template.'.'.self::$twig_template_extension);\n    }\n\n    public function getLoader()\n    {\n        return $this->view->getLoader();\n    }\n\n    public function addNamespace($namespace, $path)\n    {\n        $this->getLoader()->addPath($path, $namespace);\n\n        return $this;\n    }\n\n    public function prependNamespace($namespace, $path)\n    {\n        $this->getLoader()->prependPath($path, $namespace);\n\n        return $this;\n    }\n\n    public function addLocation($location)\n    {\n        $this->getLoader()->addPath($location);\n\n        return $this;\n    }\n\n    public function prependLocation($location)\n    {\n        $this->getLoader()->prependPath($location);\n\n        return $this;\n    }\n\n    public function __call(string $method, array $params)\n    {\n        return call_user_func_array([$this->view, $method], $params);\n    }\n\n    public function __toString()\n    {\n        $this->vars['this'] = $this->yoyoComponent;\n\n        return (string) $this->view->render($this->template.'.'.self::$twig_template_extension, $this->vars);\n    }\n}\n"
  },
  {
    "path": "src/yoyo/ViewProviders/YoyoViewProvider.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo\\ViewProviders;\n\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentNotFound;\nuse Clickfwd\\Yoyo\\Interfaces\\ViewProviderInterface;\nuse InvalidArgumentException;\n\nclass YoyoViewProvider extends BaseViewProvider implements ViewProviderInterface\n{\n    protected $view;\n\n    protected $name;\n\n    protected $vars;\n\n    public function __construct($view)\n    {\n        $this->view = $view;\n    }\n\n    public function startYoyoRendering($component): void\n    {\n        $this->view->startYoyoRendering($component);\n    }\n\n    public function stopYoyoRendering(): void\n    {\n        //\n    }\n\n    public function render($name, $vars = []): ViewProviderInterface\n    {\n        $this->name = $name;\n\n        $this->vars = $vars;\n\n        return $this;\n    }\n\n    public function makeFromString($content, $vars = []): string\n    {\n        return $this->view->makeFromString($content, $vars);\n    }\n\n    public function exists($name): bool\n    {\n        try {\n            return $this->view->exists($name);\n        } catch (InvalidArgumentException $e) {\n            throw new ComponentNotFound($name);\n        }\n    }\n\n    public function addNamespace($namespace, $hints)\n    {\n        $this->view->addNamespace($namespace, $hints);\n\n        return $this;\n    }\n\n    public function prependNamespace($namespace, $hints)\n    {\n        $this->view->prependNamespace($namespace, $hints);\n\n        return $this;\n    }\n\n    public function addLocation($location)\n    {\n        $this->view->addLocation($location);\n\n        return $this;\n    }\n\n    public function prependLocation($location)\n    {\n        $this->view->prependLocation($location);\n\n        return $this;\n    }\n\n    public function __call(string $method, array $params)\n    {\n        return call_user_func_array([$this->view, $method], $params);\n    }\n\n    public function __toString()\n    {\n        return $this->view->render($this->name, $this->vars);\n    }\n}\n"
  },
  {
    "path": "src/yoyo/Yoyo.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Exceptions\\BypassRenderMethod;\nuse Clickfwd\\Yoyo\\Exceptions\\HttpException;\nuse Clickfwd\\Yoyo\\Exceptions\\NotFoundHttpException;\nuse Clickfwd\\Yoyo\\Interfaces\\RequestInterface;\nuse Clickfwd\\Yoyo\\Interfaces\\YoyoContainerInterface;\nuse Clickfwd\\Yoyo\\Services\\BrowserEventsService;\nuse Clickfwd\\Yoyo\\Services\\Configuration;\nuse Clickfwd\\Yoyo\\Services\\PageRedirectService;\nuse Clickfwd\\Yoyo\\Services\\Response;\nuse Clickfwd\\Yoyo\\Services\\UrlStateManagerService;\n\nclass Yoyo\n{\n    private $action;\n\n    private $attributes = [];\n\n    private $id;\n\n    private $name;\n\n    private $variables = [];\n\n    private static $container;\n\n    private static $request;\n\n    private static $registeredComponents = [];\n\n    private static $componentNamespaces = [];\n\n    private static $resolverInstances = [];\n\n    public function __construct(?YoyoContainerInterface $container = null)\n    {\n        static::$container = $container ?? ContainerResolver::resolve();\n    }\n\n    /**\n     * Not really an instance, but we avoid having to call `new` with an empty constructor\n     * Nested components don't work when re-using an instance\n     */\n    public static function getInstance()\n    {\n        return new self(static::$container);\n    }\n\n    public function bindRequest(RequestInterface $request)\n    {\n        static::$request = $request;\n    }\n\n    public static function request()\n    {\n        if (! static::$request) {\n            static::$request = new Request();\n        }\n\n        return static::$request;\n    }\n\n    public function configure($options): void\n    {\n        Configuration::getInstance($options);\n    }\n\n    public static function container()\n    {\n        return static::$container;\n    }\n\n    public function getComponentId($attributes): string\n    {\n        if (isset($attributes['id'])) {\n            $id = $attributes['id'];\n        } else {\n            $id = static::request()->get(YoyoCompiler::yoprefix_value('id'), YoyoCompiler::yoprefix_value(YoyoHelpers::randString()));\n        }\n\n        // Remove the component ID from the request so it's not passed to child components\n        static::request()->drop(YoyoCompiler::yoprefix_value('id'));\n\n        return $id;\n    }\n\n    private function getComponentResolver()\n    {\n        $name = $this->variables[YoyoCompiler::yoprefix('resolver')]\n                            ?? static::request()->get(YoyoCompiler::yoprefix('resolver'));\n\n        if ($name && static::$container->has(\"yoyo.resolver.{$name}\")) {\n            return static::$container->get(\"yoyo.resolver.{$name}\")(static::$container, static::$registeredComponents, static::$componentNamespaces);\n        }\n\n        $resolver = ! $name ? new ComponentResolver() : static::$resolverInstances[$name];\n\n        $name = $name ?? 'default';\n\n        static::$container->set(\"yoyo.resolver.{$name}\", $resolver);\n\n        return $resolver(static::$container, static::$registeredComponents, static::$componentNamespaces);\n    }\n\n    public function registerViewProvider($name, $provider = null)\n    {\n        if (is_null($provider)) {\n            $provider = $name;\n            $name = 'default';\n        }\n\n        static::$container->set(\"yoyo.view.{$name}\", $provider);\n    }\n\n    public function registerViewProviders($providers)\n    {\n        foreach ($providers as $name => $provider) {\n            $this->registerViewProvider($name, $provider);\n        }\n    }\n\n    public static function getViewProvider($name = 'default')\n    {\n        return static::$container->get(\"yoyo.view.{$name}\");\n    }\n\n    public function registerComponentResolver($resolver)\n    {\n        if (is_string($resolver)) {\n            $resolver = self::$container->make($resolver);\n        }\n\n        $this->registerViewProvider($name = $resolver->getName(), function () use ($resolver) {\n            return $resolver->getViewProvider();\n        });\n\n        static::$resolverInstances[$name] = $resolver;\n    }\n\n    /**\n     * Register component namespace hints\n     *\n     * @param string $namespace\n     * @param mixed $class\n     * @return void\n     */\n    public static function componentNamespace(string $namespace, $class): void\n    {\n        $class = array_filter((array) $class);\n\n        static::$componentNamespaces[$namespace] = array_merge(static::$componentNamespaces[$namespace] ?? [], $class);\n    }\n\n    public static function registerComponent($name, $class = null): void\n    {\n        static::$registeredComponents[$name] = $class;\n    }\n\n    public static function registerComponents($components): void\n    {\n        foreach ($components as $name => $class) {\n            if (is_numeric($name)) {\n                $name = $class;\n                $class = null;\n            }\n            static::registerComponent($name, $class);\n        }\n    }\n\n    public static function abort($code, $message = '', array $headers = [])\n    {\n        if ($code == 404) {\n            throw new NotFoundHttpException($message, $headers);\n        }\n\n        throw new HttpException($code, $message, $headers);\n    }\n\n    public function mount($name, $variables = [], $attributes = [], $action = 'render'): self\n    {\n        $this->action($action);\n\n        $this->id = $this->getComponentId($attributes);\n\n        unset($attributes['id']);\n\n        $this->name = $name;\n\n        $this->variables = $variables;\n\n        $this->attributes = $attributes;\n\n        return $this;\n    }\n\n    public function action($action): self\n    {\n        $this->action = $action == 'refresh' ? 'render' : $action;\n\n        return $this;\n    }\n\n    public function actionArgs(...$args)\n    {\n        $this->request()->merge(['actionArgs' => $args]);\n\n        return $this;\n    }\n\n    /**\n     * Renders the component on initial page load.\n     */\n    public function render(): string\n    {\n        return $this->output($spinning = false);\n    }\n\n    /**\n     * Renders the component on dynamic updates (ajax) to send back to the browser.\n     */\n    public function refresh(): string\n    {\n        $output = $this->output($spinning = true);\n\n        return $output;\n    }\n\n    public function update(): string\n    {\n        [$name, $action] = $this->parseUpdateRequest();\n\n        return $this->mount($name, $variables = [], $attributes = [], $action)->refresh();\n    }\n\n    protected function parseUpdateRequest()\n    {\n        $component = static::request()->get('component');\n\n        $parts = array_filter(explode('/', $component));\n\n        if (empty($parts)) {\n            throw new Exceptions\\IncompleteComponentParamInRequest();\n        }\n\n        $name = $parts[0];\n\n        $action = $parts[1] ?? 'render';\n\n        return [$name, $action];\n    }\n\n    public function output($spinning = false)\n    {\n        $variables = [];\n\n        $componentManager = new ComponentManager($this->getComponentResolver(), static::request(), $spinning);\n\n        try {\n            try {\n                $html = $componentManager->process($this->id, $this->name, $this->action ?? YoyoCompiler::COMPONENT_DEFAULT_ACTION, $this->variables, $this->attributes);\n            } finally {\n                if ($componentManager->getComponentInstance()) {\n                    // Get all data needed to pass the rendered HTML through the Yoyo compiler to make it reactive\n\n                    $defaultValues = $componentManager->getDefaultPublicVars();\n\n                    $newValues = $componentManager->getPublicVars();\n\n                    // Get dynamic component public properties anonymous components vars to pass them to the compiler\n                    // Any matching parameter names in yoyo:props will be automatically added to yoyo:vals\n\n                    $variables = array_merge($defaultValues, $newValues);\n\n                    $variables = YoyoHelpers::removeEmptyValues($variables);\n\n                    $listeners = $componentManager->getListeners();\n\n                    $componentType = $componentManager->isDynamicComponent() ? 'dynamic' : 'anonymous';\n\n                    // For dynamic components, filter variables based on component props\n\n                    $props = $componentManager->getProps();\n\n                    $postComponentProcessingActions = function () use ($componentManager, $defaultValues, $newValues) {\n                        $queryStringKeys = $componentManager->getQueryString();\n                        $queryString = new QueryString($defaultValues, $newValues, $queryStringKeys);\n\n                        // Browser URL State\n                        $urlStateManager = new UrlStateManagerService();\n\n                        if ($componentManager->isDynamicComponent()) {\n                            $urlStateManager->pushState($queryString->getPageQueryParams());\n                        }\n\n                        // Browser Events\n                        $eventsService = BrowserEventsService::getInstance();\n                        $eventsService->dispatch();\n\n                        // Browser Redirect\n                        (PageRedirectService::getInstance())->redirect($componentManager->getComponentInstance()->redirectTo);\n                    };\n                }\n            }\n        } catch (BypassRenderMethod $e) {\n            if (isset($postComponentProcessingActions)) {\n                $postComponentProcessingActions();\n            }\n            if ($e->getCode() == 204) {\n                Response::getInstance()->status(204)->send();\n                // Need to throw exception to stop execution and send 204 status code\n                throw $e;\n            } else {\n                return Response::getInstance()->status(200)->send();\n            }\n        } catch (HttpException $e) {\n            if (isset($postComponentProcessingActions)) {\n                $postComponentProcessingActions();\n            }\n            Response::getInstance()->status($e->getStatusCode())->setHeaders($e->getHeaders())->send();\n\n            throw $e;\n        } catch (\\Exception $e) {\n            if (isset($postComponentProcessingActions)) {\n                $postComponentProcessingActions();\n            }\n            Response::getInstance()->send();\n\n            throw $e;\n        }\n\n        $cacheHistory = ! empty(array_filter(\n            $componentManager->getQueryString(),\n            function ($key) {\n                return strpos($key, 'yoyo-id') !== 0;\n            }\n        )\n        );\n\n        $compiledHtml = $this->compile($componentType, $html, $spinning, $variables, $listeners, $props, $cacheHistory);\n\n        if ($spinning) {\n            $postComponentProcessingActions();\n        }\n\n        return (Response::getInstance())->send($compiledHtml);\n    }\n\n    public function compile($componentType, $html, $spinning = null, $variables = [], $listeners = [], $props = [], $cacheHistory = false): string\n    {\n        $spinning = $spinning ?? $this->is_spinning();\n\n        $variables = array_merge($this->variables, $variables);\n\n        $output = (new YoyoCompiler($componentType, $this->id, $this->name, $variables, $this->attributes, $spinning))\n                    ->withHistory($cacheHistory)\n                    ->addComponentListeners($listeners)\n                    ->addComponentProps($props)\n                    ->compile($html);\n\n        return $output;\n    }\n\n    /**\n     * Is this a request to update the component?\n     */\n    private function is_spinning(): bool\n    {\n        $spinning = static::request()->isYoyoRequest();\n\n        // Stop spinning of child components when parent is refreshed\n\n        static::request()->windUp();\n\n        return $spinning;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/YoyoCompiler.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse DOMDocument;\nuse DOMXPath;\n\nclass YoyoCompiler\n{\n    protected $componentType;\n\n    protected $componentId;\n\n    protected $name;\n\n    protected $variables;\n\n    protected $attributes;\n\n    protected $spinning;\n\n    protected $listeners;\n\n    protected $props;\n\n    protected $withHistory;\n\n    protected $idCounter = 1;\n\n    public const HTMX_REQUEST_METHOD_ATTRIBUTES = [\n        'boost',\n        'delete',\n        'get',\n        'patch',\n        'post',\n        'put',\n        'sse',\n        'ws',\n    ];\n\n    public const YOYO_ATTRIBUTES = [\n        'confirm',\n        'disable',\n        'disinherit',\n        'encoding',\n        'ext',\n        'headers',\n        'history-elt',\n        'include',\n        'indicator',\n        'on',\n        'params',\n        'preserve',\n        'prompt',\n        'push-url',\n        'replace-url',\n        'request',\n        'select-oob',\n        'select',\n        'swap-oob',\n        'swap',\n        'sync',\n        'target',\n        'trigger',\n        'validate',\n        'vals',\n    ];\n\n    public const YOYO_TO_HX_ATTRIBUTE_REMAP = [\n        'on' => 'trigger',\n    ];\n\n    public const COMPONENT_DEFAULT_ACTION = 'render';\n\n    public const COMPONENT_WRAPPER_CLASS = 'yoyo-wrapper';\n\n    public const YOYO_PREFIX = 'yoyo';\n\n    public const YOYO_PREFIX_FINDER = 'yoyo-finder';\n\n    public const HTMX_PREFIX = 'hx';\n\n    // Pre-built lookup tables for O(1) checks instead of in_array()\n    private static $yoyoAttributeMap;\n\n    private static $htmxMethodMap;\n\n    // Cached prefix strings to avoid repeated concatenation\n    private static $yoprefixCache = [];\n\n    private static $hxprefixCache = [];\n\n    public function __construct($componentType, $componentId, $name, $variables, $attributes, $spinning)\n    {\n        $this->componentType = $componentType;\n\n        $this->componentId = $componentId;\n\n        $this->name = $name;\n\n        $this->variables = $variables;\n\n        $this->attributes = $attributes;\n\n        $this->spinning = $spinning;\n\n        // Build lookup maps once\n        if (self::$yoyoAttributeMap === null) {\n            self::$yoyoAttributeMap = array_flip(self::YOYO_ATTRIBUTES);\n            self::$htmxMethodMap = array_flip(self::HTMX_REQUEST_METHOD_ATTRIBUTES);\n        }\n    }\n\n    public function addComponentListeners($listeners = [])\n    {\n        $this->listeners = $listeners;\n\n        return $this;\n    }\n\n    public function addComponentProps($props = [])\n    {\n        $this->props = $props;\n\n        return $this;\n    }\n\n    public function withHistory($cacheHistory = false)\n    {\n        $this->withHistory = $cacheHistory;\n\n        return $this;\n    }\n\n    public function compile($html): string\n    {\n        if (! trim($html)) {\n            return $html;\n        }\n\n        // Add yoyo-finder marker attributes for XPath discovery\n        // (XPath cannot query attributes with colons)\n        // Combined regex handles both double and single quoted attributes\n        $prefix = self::YOYO_PREFIX;\n        $prefix_finder = self::YOYO_PREFIX_FINDER;\n\n        $html = preg_replace(\n            [\n                '/ '.$prefix.':(.*)=\"(.*)\"/U',\n                '/ '.$prefix.':(.*)=\\'(.*)\\'/U',\n            ],\n            [\n                \" $prefix_finder $prefix:\\$1=\\\"\\$2\\\"\",\n                \" $prefix_finder $prefix:\\$1='\\$2'\",\n            ],\n            $html\n        );\n\n        // Convert non-ASCII characters to numeric HTML entities only when needed\n        if (preg_match('/[\\x80-\\xff]/', $html)) {\n            $html = mb_encode_numericentity($html, [0x80, 0x10FFFF, 0, ~0], 'UTF-8');\n        }\n\n        $dom = new DOMDocument();\n\n        $internalErrors = libxml_use_internal_errors(true);\n\n        $dom->loadHTML($html);\n\n        libxml_use_internal_errors($internalErrors);\n\n        // Reuse a single XPath instance throughout\n        $xpath = new DOMXPath($dom);\n\n        if (! ($node = $this->getComponentRootNode($xpath))) {\n            $html = $this->getOuterHTML($dom, $xpath);\n\n            unset($dom);\n\n            return $this->compile('<div>'.$html.'</div>');\n        }\n\n        $elements = $xpath->query('//form');\n\n        foreach ($elements as $element) {\n            if (! $element->hasAttribute(self::yoprefix('ignore'))) {\n                $this->addFormBehavior($element, $xpath);\n            }\n        }\n\n        // Prevent infinite loop with on 'load' event on root node with outerHTML swap\n\n        $this->removeOnLoadEventWhenSpinning($node);\n\n        $this->addComponentRootAttributes($node);\n\n        $this->addComponentChildrenAttributes($xpath);\n\n        // Cleanup\n        $node->removeAttribute(self::YOYO_PREFIX_FINDER);\n\n        $doOuterHtmlSwap = ! $this->elementHasAttributeWithValue($node, self::hxprefix('swap'), 'innerHTML');\n\n        if ($this->spinning && ! $doOuterHtmlSwap) {\n            $output = $this->getInnerHTML($dom, $xpath);\n        } else {\n            $output = $this->getOuterHTML($dom, $xpath);\n        }\n\n        return trim($output);\n    }\n\n    protected function addComponentRootAttributes($element)\n    {\n        if ($element->hasAttribute(self::yoprefix('ignore'))) {\n            $element->removeAttribute(self::yoprefix('ignore'));\n\n            return;\n        }\n\n        // Skip when component already compiled\n        if ($element->hasAttribute(self::yoprefix('name')) && $element->hasAttribute(self::hxprefix('vals'))) {\n            return;\n        }\n\n        $element->setAttribute(self::YOYO_PREFIX, '');\n\n        $element->setAttribute(self::YOYO_PREFIX_FINDER, '');\n\n        // Discard generated component ID and use hardcoded one if found\n        $id = $element->getAttribute('id');\n\n        if ($id !== '') {\n            $this->componentId = $id;\n        }\n\n        $this->addRequestMethodAttribute($element, true);\n\n        // Get default attributes\n\n        $attributes = $this->getComponentAttributes($this->componentId);\n\n        // Merge, or in some cases replace, defaults with existing attributes at the root node level\n\n        if (! $element->hasAttribute('id')) {\n            $element->setAttribute('id', $this->componentId);\n        }\n\n        foreach (['target', 'include'] as $attr) {\n            if ($value = $element->getAttribute(self::yoprefix($attr))) {\n                $attributes[$attr] = $value;\n            }\n        }\n\n        // Add yoyo extension attribute and merge existing extensions\n\n        if ($ext = $element->getAttribute(self::yoprefix('ext'))) {\n            $element->removeAttribute(self::yoprefix('ext'));\n            $attributes['ext'] .= ', '.$ext;\n        }\n\n        $class = $element->getAttribute('class');\n\n        $element->setAttribute('class', self::COMPONENT_WRAPPER_CLASS.($class ? ' '.$class : ''));\n\n        $element->setAttribute(self::yoprefix('name'), $this->name);\n\n        if ($this->withHistory) {\n            $element->setAttribute(self::yoprefix('history'), 1);\n        }\n\n        if ($trigger = $element->getAttribute(self::yoprefix('on'))) {\n            $attributes['on'] .= ', '.$trigger;\n        }\n\n        // Process variables\n\n        if ($vars = $element->getAttribute(self::yoprefix('vals'))) {\n            $element->removeAttribute(self::yoprefix('vals'));\n\n            $vars = YoyoHelpers::decode_vals($vars);\n\n            $attributes['vals'] = array_merge($attributes['vals'], $vars);\n        }\n\n        // Process invididual variables added through yoyo:val.key\n\n        $attributes['vals'] = array_merge($attributes['vals'] ?? [], $this->parseIndividualValAttributes($element));\n\n        // Process public props\n\n        if ($props = $element->getAttribute(self::yoprefix('props')) ?: []) {\n            $props = explode(',', str_replace(' ', '', $props));\n            $element->removeAttribute(self::yoprefix('props'));\n        }\n\n        $props = array_merge($props, $this->props, [\n            self::yoprefix('resolver'),\n            self::yoprefix('source'),\n        ]);\n\n        $propsLookup = [];\n        foreach ($props as $prop) {\n            if (is_string($prop) && $prop !== '') {\n                $propsLookup[$prop] = true;\n            }\n        }\n\n        $variables = array_filter($this->variables, function ($key) use ($propsLookup) {\n            return isset($propsLookup[$key]);\n        }, ARRAY_FILTER_USE_KEY);\n\n        $attributes['vals'] = array_merge($attributes['vals'], $variables);\n\n        // Add all attributes\n\n        $attributes['vals'] = YoyoHelpers::encode_vals($attributes['vals']);\n\n        foreach ($attributes as $attr => $value) {\n            if (! $value) {\n                $value = $element->getAttribute(self::yoprefix($attr));\n            }\n\n            if ($value) {\n                $this->remapAndReplaceAttribute($element, $attr, $value);\n            }\n        }\n    }\n\n    protected function addComponentChildrenAttributes($xpath)\n    {\n        $elements = $xpath->query('//*[@'.self::YOYO_PREFIX.']|//*[@'.self::YOYO_PREFIX_FINDER.']');\n        $valPrefixLen = strlen(self::yoprefix('val').'.'); // \"yoyo:val.\" = 9\n\n        foreach ($elements as $key => $element) {\n            // Skip the component root because it's processed separately\n            if ($key == 0) {\n                continue;\n            }\n\n            // Skip already-compiled nested components.\n            // Keep cleanup behavior consistent with the existing loop.\n            if ($element->hasAttribute(self::yoprefix('name')) && $element->hasAttribute(self::hxprefix('vals'))) {\n                $element->removeAttribute(self::YOYO_PREFIX);\n                $element->removeAttribute(self::YOYO_PREFIX_FINDER);\n                continue;\n            }\n\n            // Single pass over element attributes: categorize everything at once\n            // instead of 3 separate passes (addRequestMethodAttribute + yoyo scan + val scan)\n            $yoyoAttrs = [];\n            $valAttrs = [];\n            $valToRemove = [];\n            $hasHxMethod = false;\n            $yoyoMethod = null;\n            $yoyoMethodValue = null;\n            $hasYoyoMarker = false;\n\n            foreach ($element->attributes as $attr) {\n                $name = $attr->name;\n\n                if ($name === self::YOYO_PREFIX) {\n                    $hasYoyoMarker = true;\n                    continue;\n                }\n\n                // Check for existing hx-{method} — skip method assignment if found\n                if (str_starts_with($name, 'hx-') && isset(self::$htmxMethodMap[substr($name, 3)])) {\n                    $hasHxMethod = true;\n                    continue;\n                }\n\n                if (! str_starts_with($name, 'yoyo:')) {\n                    continue;\n                }\n\n                $yoyoAttr = substr($name, 5);\n\n                // yoyo:val.{key} — collect for val processing\n                if (str_starts_with($yoyoAttr, 'val.')) {\n                    $valKey = substr($name, $valPrefixLen);\n                    $valAttrs[YoyoHelpers::camel($valKey, '-')] = YoyoHelpers::decode_val($attr->value);\n                    $valToRemove[] = $name;\n                    continue;\n                }\n\n                // yoyo:{method} — request method to remap\n                if (isset(self::$htmxMethodMap[$yoyoAttr])) {\n                    $yoyoMethod = $yoyoAttr;\n                    $yoyoMethodValue = $attr->value;\n                    continue;\n                }\n\n                // yoyo:{attr} — collect for hx- remapping\n                if (isset(self::$yoyoAttributeMap[$yoyoAttr])) {\n                    $yoyoAttrs[$yoyoAttr] = $attr->value;\n                }\n            }\n\n            // Handle request method\n            if (! $hasHxMethod) {\n                if ($yoyoMethod !== null) {\n                    $element->removeAttribute(self::yoprefix($yoyoMethod));\n                    $element->setAttribute(self::hxprefix($yoyoMethod), $yoyoMethodValue);\n                    $this->checkForIdAttribute($element);\n                } elseif ($hasYoyoMarker) {\n                    $element->setAttribute(self::hxprefix('get'), self::COMPONENT_DEFAULT_ACTION);\n                    $this->checkForIdAttribute($element);\n                }\n            }\n\n            // Remap yoyo: attributes to hx-\n            foreach ($yoyoAttrs as $yoyoAttr => $value) {\n                $this->remapAndReplaceAttribute($element, $yoyoAttr, $value);\n            }\n\n            // Process val attributes\n            if ($valAttrs) {\n                foreach ($valToRemove as $name) {\n                    $element->removeAttribute($name);\n                }\n                $element->setAttribute(self::hxprefix('vals'), YoyoHelpers::encode_vals($valAttrs));\n            }\n\n            // Cleanup\n            $element->removeAttribute(self::YOYO_PREFIX);\n            $element->removeAttribute(self::YOYO_PREFIX_FINDER);\n        }\n    }\n\n    protected function parseIndividualValAttributes($element)\n    {\n        $attributes = [];\n        $valPrefix = self::yoprefix('val').'.';\n        $valPrefixLen = strlen($valPrefix);\n\n        // Collect matching attributes first to avoid modifying\n        // the live DOMNamedNodeMap while iterating\n        $toRemove = [];\n        foreach ($element->attributes as $attr) {\n            $name = $attr->name;\n\n            if (! str_starts_with($name, $valPrefix)) {\n                continue;\n            }\n\n            $key = substr($name, $valPrefixLen);\n            $attributes[YoyoHelpers::camel($key, '-')] = YoyoHelpers::decode_val($attr->value);\n            $toRemove[] = $name;\n        }\n\n        foreach ($toRemove as $name) {\n            $element->removeAttribute($name);\n        }\n\n        return $attributes;\n    }\n\n    protected function removeOnLoadEventWhenSpinning($element)\n    {\n        if ($this->spinning && $element->hasAttribute(self::yoprefix('on'))) {\n            $on = $element->getAttribute(self::yoprefix('on'));\n\n            $events = explode(',', $on);\n\n            $events = array_filter($events, function ($event) {\n                return $event !== 'load';\n            });\n\n            $element->setAttribute(self::yoprefix('on'), implode(',', $events));\n        }\n    }\n\n    protected function addFormBehavior($element, $xpath = null)\n    {\n        if ($element->tagName == 'form' && ! $element->hasAttribute(self::yoprefix('on'))) {\n            $element->setAttribute(self::YOYO_PREFIX, '');\n\n            $element->setAttribute(self::yoprefix('on'), 'submit');\n\n            // If the form has an upload input, set the encoding to multipart/form-data\n\n            if ($xpath === null) {\n                $xpath = new DOMXPath($element->ownerDocument);\n            }\n\n            $inputs = $xpath->query('.//*[@type=\"file\"]', $element);\n\n            if ($inputs->item(0)) {\n                $element->setAttribute(self::yoprefix('encoding'), 'multipart/form-data');\n            }\n\n            // If the form tag doesn't have a method set, set POST by default\n\n            foreach ($element->attributes as $attr) {\n                if (($parts = explode(':', $attr->name))[0] == self::YOYO_PREFIX && ! empty($parts[1])) {\n                    if (isset(self::$htmxMethodMap[$parts[1]])) {\n                        return;\n                    }\n                }\n            }\n\n            $element->setAttribute(self::yoprefix('post'), self::COMPONENT_DEFAULT_ACTION);\n        }\n    }\n\n    protected function checkForIdAttribute($element)\n    {\n        if (! $element->hasAttribute('id')) {\n            $element->setAttribute('id', $this->componentId.'-'.$this->idCounter++);\n        }\n    }\n\n    protected function remapAndReplaceAttribute($element, $attr, $value)\n    {\n        if (str_starts_with($attr, self::YOYO_PREFIX) || isset(self::$yoyoAttributeMap[$attr])) {\n            $element->removeAttribute(self::yoprefix($attr));\n            $remappedAttr = self::YOYO_TO_HX_ATTRIBUTE_REMAP[$attr] ?? $attr;\n            $element->setAttribute(self::hxprefix($remappedAttr), $value);\n        } elseif ($value) {\n            $currentValue = $element->getAttribute($attr);\n            if ($currentValue) {\n                $value = $currentValue . ' ' . $value;\n            }\n            $element->setAttribute($attr, $value);\n        }\n    }\n\n    protected function addRequestMethodAttribute($element, $isRootNode = false)\n    {\n        // Skip if element already has an hx-[request] attribute (no yoyo:[request] which is processed below)\n\n        foreach (self::HTMX_REQUEST_METHOD_ATTRIBUTES as $attr) {\n            $hxattr = self::hxprefix($attr);\n            if ($element->hasAttribute($hxattr)) {\n                return;\n            }\n        }\n\n        // Look for existing method attribute, otherwise set 'get' as default\n\n        foreach (self::HTMX_REQUEST_METHOD_ATTRIBUTES as $attr) {\n            $yoattr = self::yoprefix($attr);\n\n            if ($value = $element->getAttribute($yoattr)) {\n                $element->removeAttribute($yoattr);\n\n                $element->setAttribute(self::hxprefix($attr), $value);\n\n                // Add an ID attribute for elements that trigger requests\n                if (! $isRootNode) {\n                    $this->checkForIdAttribute($element);\n                }\n\n                return;\n            }\n        }\n\n        // Automatically add the default hx-get=\"render\" request to component root nodes and any child with the `yoyo` attribute\n        if ($element->hasAttribute(self::YOYO_PREFIX)) {\n            $element->setAttribute(self::hxprefix('get'), self::COMPONENT_DEFAULT_ACTION);\n            if (! $isRootNode) {\n                // Ensure re-active tags have an ID to improve swapping\n                $this->checkForIdAttribute($element);\n            }\n        }\n    }\n\n    protected function getComponentAttributes($componentId): array\n    {\n        $attributes = array_merge(\n            array_fill_keys(self::YOYO_ATTRIBUTES, ''),\n            [\n                'ext' => 'yoyo',\n                // Adding refresh trigger to prevent default click trigger\n                'on' => 'refresh',\n                'target' => 'this',\n                'include' => \"this\",\n                'vals' => [self::yoprefix_value('id') => $componentId],\n            ],\n            $this->attributes\n        );\n\n        // Include component listeners in trigger attribute\n\n        if (! empty($this->listeners)) {\n            $listeners = array_keys($this->listeners);\n\n            $attributes['on'] .= ','.implode(',', $listeners);\n        }\n\n        return $attributes;\n    }\n\n    public static function yoprefix($attr): string\n    {\n        return self::$yoprefixCache[$attr] ??= self::YOYO_PREFIX.':'.$attr;\n    }\n\n    public static function yoprefix_value($string): string\n    {\n        return self::YOYO_PREFIX.'-'.$string;\n    }\n\n    public static function hxprefix($attr): string\n    {\n        return self::$hxprefixCache[$attr] ??= self::HTMX_PREFIX.'-'.$attr;\n    }\n\n    protected function getComponentRootNode($xpath)\n    {\n        $count = 0;\n\n        foreach ($xpath->query('/html/body/*') as $node) {\n            if ($node->nodeType === XML_ELEMENT_NODE) {\n                $count++;\n            }\n        }\n\n        return $count == 1 ? $node : false;\n    }\n\n    protected static function elementHasAttributeWithValue($element, $attr, $value)\n    {\n        if (! $element->hasAttribute($attr)) {\n            return false;\n        }\n\n        $string = $element->getAttribute($attr);\n\n        return strpos($string, $value) !== false;\n    }\n\n    protected function getOuterHTML($dom, $xpath = null): string\n    {\n        $output = '';\n\n        if ($xpath === null) {\n            $xpath = new DOMXPath($dom);\n        }\n\n        $elements = $xpath->query(\"//*[starts-with(name(@*),'hx-')]\");\n\n        foreach ($elements as $node) {\n            $setDefaultAction = true;\n\n            foreach (['get', 'post', 'put', 'delete', 'patch', 'ws', 'sse'] as $verb) {\n                if ($node->hasAttribute('hx-'.$verb)) {\n                    $setDefaultAction = false;\n\n                    break;\n                }\n            }\n\n            if ($setDefaultAction) {\n                $node->setAttribute('hx-get', 'render');\n            }\n        }\n\n        foreach ($dom->getElementsByTagName('body')->item(0)->childNodes as $node) {\n            $output .= $dom->saveHTML($node);\n        }\n\n        return $output;\n    }\n\n    protected function getInnerHTML($dom, $xpath = null): string\n    {\n        $output = '';\n\n        if ($xpath === null) {\n            $xpath = new DOMXPath($dom);\n        }\n\n        $elements = $xpath->query(\"//*[contains(concat(' ', normalize-space(@class), ' '), ' \".self::COMPONENT_WRAPPER_CLASS.\" ')]\");\n\n        if (! $elements->length) {\n            return $this->getOuterHTML($dom, $xpath);\n        }\n\n        foreach ($elements->item(0)->childNodes as $node) {\n            $output .= $dom->saveHTML($node);\n        }\n\n        return $output;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/YoyoHelpers.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nclass YoyoHelpers\n{\n    public static function encode_vals(array $vars): string\n    {\n        $adjusted = [];\n        foreach ($vars as $key => $val) {\n            $newKey = is_array($val) ? $key.'[]' : $key;\n            $adjusted[$newKey] = $val;\n        }\n\n        return json_encode($adjusted, JSON_UNESCAPED_UNICODE | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS);\n    }\n\n    public static function decode_vals(string $string): array\n    {\n        if (empty($string)) {\n            return [];\n        }\n\n        return json_decode((string) $string, true);\n    }\n\n    public static function decode_val(string $string)\n    {\n        $validJson = false;\n        $json = self::test_json($string, $validJson);\n\n        if ($validJson) {\n            return $json;\n        }\n\n        return $string === '0' ? 0 : $string;\n    }\n\n    public static function test_json($string, ?bool &$validJson = null)\n    {\n        $validJson = false;\n\n        if (is_array($string)) {\n            $validJson = true;\n\n            return $string;\n        }\n\n        if (! is_string($string)) {\n            return null;\n        }\n\n        $decoded = json_decode($string, true);\n\n        if (json_last_error() === JSON_ERROR_NONE) {\n            $validJson = true;\n\n            return $decoded;\n        }\n\n        // Retry after stripping slashes (handles WordPress magic quotes)\n        $unslashed = stripslashes($string);\n\n        if ($unslashed !== $string) {\n            $decoded = json_decode($unslashed, true);\n\n            if (json_last_error() === JSON_ERROR_NONE) {\n                $validJson = true;\n\n                return $decoded;\n            }\n        }\n\n        return null;\n    }\n\n    public static function studly($str, $delimiter = ['-', '_'])\n    {\n        $str = str_replace(' ', '', ucwords(str_replace($delimiter, ' ', $str)));\n\n        return $str;\n    }\n\n    public static function camel($str, $delimiter = ['-', '_'])\n    {\n        return lcfirst(static::studly($str, $delimiter));\n    }\n\n    public static function snake($str, $delimiter = '_')\n    {\n        if (! ctype_lower($str)) {\n            $str = preg_replace('/\\\\s+/u', '', ucwords($str));\n            $str = mb_strtolower(preg_replace('/(.)(?=[A-Z])/u', '$1'.$delimiter, $str), 'UTF-8');\n        }\n\n        return $str;\n    }\n\n    public static function randString($length = 8)\n    {\n        $characters = '0123456789abcdefghijklmnopqrstuvwxyz';\n        $charactersLength = strlen($characters);\n        $randomString = '';\n        for ($i = 0; $i < $length; $i++) {\n            $randomString .= $characters[random_int(0, $charactersLength - 1)];\n        }\n\n        return $randomString;\n    }\n\n    public static function removeEmptyValues(array &$array)\n    {\n        foreach ($array as $key => &$value) {\n            if (is_array($value)) {\n                $value = static::removeEmptyValues($value);\n            }\n\n            if (is_array($value) && empty($value)) {\n                unset($array[$key]);\n            }\n\n            if (is_null($value) || (is_string($value) && ! strlen($value))) {\n                unset($array[$key]);\n            }\n        }\n\n        return $array;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/YoyoPhalconController.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Phalcon\\Mvc\\Controller;\n\nclass YoyoPhalconController extends Controller\n{\n    public function handleAction()\n    {\n        $this->view->disable();\n        /** @var Yoyo $yoyo */\n        $yoyo = $this->di->get('yoyo');\n        $yoyoRequest = new Request();\n        $yoyoRequest->mock($_REQUEST, $_SERVER);\n        $yoyo->bindRequest($yoyoRequest);\n        $this->response->setContent($yoyo->update());\n        return $this->response;\n    }\n}\n"
  },
  {
    "path": "src/yoyo/YoyoPhalconServiceProvider.php",
    "content": "<?php\n\nnamespace Clickfwd\\Yoyo;\n\nuse Clickfwd\\Yoyo\\ViewProviders\\PhalconViewProvider;\nuse Phalcon\\Di\\DiInterface;\nuse Phalcon\\Di\\ServiceProviderInterface;\nuse Phalcon\\Mvc\\View\\Simple as SimpleView;\n\nclass YoyoPhalconServiceProvider implements ServiceProviderInterface\n{\n    private $yoyoConfig = [];\n\n    private $viewExtention = null;\n\n    public function setYoyoConfig($yoyoConfig)\n    {\n        $this->yoyoConfig = $yoyoConfig;\n\n        return $this;\n    }\n\n    public function setViewExtention($viewExtention)\n    {\n        $this->viewExtention = $viewExtention;\n\n        return $this;\n    }\n\n    public function register(DiInterface $di): void\n    {\n        $di->setShared('yoyo', function () use ($di) {\n            $yoyo = new Yoyo();\n            $yoyoConfig = $this->yoyoConfig ?? [\n                'url' => '/yoyo',\n                'namespace' => 'App\\Components\\\\',\n                'scriptsPath' => 'js/',\n            ];\n\n            $yoyo->configure($yoyoConfig);\n            $viewExtention = $this->viewExtention ?? null;\n\n            $yoyo->container()->set('yoyo.view.default', function () use ($di, $viewExtention) {\n                $view = $di->get('view');\n                $simpleView = new SimpleView();\n                $simpleView->setViewsDir($view->getViewsDir());\n                /** @var PhalconViewProvider $viewProvider */\n                $viewProvider = new PhalconViewProvider($simpleView);\n                if ($viewExtention) {\n                    $viewProvider->setViewExtention($this->viewExtention);\n                }\n\n                return $viewProvider;\n            });\n\n            return $yoyo;\n        });\n    }\n}\n"
  },
  {
    "path": "src/yoyo/helpers.php",
    "content": "<?php\n\nnamespace Yoyo;\n\nuse Clickfwd\\Yoyo\\Services\\Configuration;\nuse Clickfwd\\Yoyo\\Yoyo;\n\nif (! function_exists('Yoyo\\yoyo_render')) {\n    function yoyo_render($name, $variables = [], $attributes = []): string\n    {\n        $yoyo = Yoyo::getInstance();\n\n        return $yoyo->mount($name, $variables, $attributes)->render();\n    }\n}\n\nfunction yoyo_scripts($return = false)\n{\n    $output = Configuration::scripts();\n    if ($return) {\n        return $output;\n    }\n    echo $output;\n}\n\nfunction yoyo_styles($return = false)\n{\n    $output = Configuration::styles();\n    if ($return) {\n        return $output;\n    }\n    echo $output;\n}\n\nfunction abort($code, $message = '', array $headers = [])\n{\n    Yoyo::abort($code, $message, $headers);\n}\n\nfunction abort_if($boolean, $code, $message = '', array $headers = [])\n{\n    if ($boolean) {\n        Yoyo::abort($code, $message, $headers);\n    }\n}\n\nfunction abort_unless($boolean, $code, $message = '', array $headers = [])\n{\n    if (! $boolean) {\n        Yoyo::abort($code, $message, $headers);\n    }\n}\n\nfunction encode_vals($vals)\n{\n    echo \\Clickfwd\\Yoyo\\YoyoHelpers::encode_vals($vals);\n}\n\nfunction is_spinning($expression = null)\n{\n    $request = Yoyo::request();\n\n    if ($request->isYoyoRequest()) {\n        if (! $expression) {\n            return true;\n        }\n\n        echo $expression;\n    } elseif (! $expression) {\n        return false;\n    }\n}\n\nfunction not_spinning($expression = null)\n{\n    $request = Yoyo::request();\n\n    if (! $request->isYoyoRequest()) {\n        if (! $expression) {\n            return true;\n        }\n\n        echo $expression;\n    } elseif (! $expression) {\n        return false;\n    }\n}\n"
  },
  {
    "path": "tests/Benchmark/PipelineBenchmarkTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\ClassHelpers;\nuse Clickfwd\\Yoyo\\Component;\nuse Clickfwd\\Yoyo\\ComponentResolver;\nuse Clickfwd\\Yoyo\\Request;\nuse Clickfwd\\Yoyo\\Yoyo;\nuse Tests\\App\\Yoyo\\ComponentWithTrait;\nuse Tests\\App\\Yoyo\\Counter;\n\nuse function Tests\\compile_html;\nuse function Tests\\mockYoyoGetRequest;\nuse function Tests\\render;\nuse function Tests\\resetYoyoRequest;\nuse function Tests\\update;\nuse function Tests\\yoyo_view;\n\nbeforeAll(function () {\n    yoyo_view();\n});\n\nfunction benchmark(string $label, int $iterations, Closure $fn): array\n{\n    // Warmup\n    for ($i = 0; $i < 10; $i++) {\n        $fn();\n    }\n\n    $start = hrtime(true);\n    for ($i = 0; $i < $iterations; $i++) {\n        $fn();\n    }\n    $elapsed = (hrtime(true) - $start) / 1e6; // ms\n\n    $perOp = $elapsed / $iterations;\n\n    fwrite(STDERR, sprintf(\n        \"BENCH [%s] %d iterations: %.3fms total, %.4fms/op\\n\",\n        $label,\n        $iterations,\n        $elapsed,\n        $perOp\n    ));\n\n    return ['label' => $label, 'iterations' => $iterations, 'total_ms' => $elapsed, 'per_op_ms' => $perOp];\n}\n\nfunction createComponentInstance(string $class = Counter::class, string $name = 'counter'): Component\n{\n    $resolver = new ComponentResolver(Yoyo::getInstance());\n\n    return new $class($resolver, 'bench-'.$name, $name);\n}\n\n// --- ClassHelpers Reflection ---\n\ntest('BENCH: getPublicProperties', function () {\n    $component = createComponentInstance();\n\n    $result = benchmark('ClassHelpers::getPublicProperties', 5000, function () use ($component) {\n        ClassHelpers::getPublicProperties($component, Component::class);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: getDefaultPublicVars', function () {\n    $component = createComponentInstance();\n\n    $result = benchmark('ClassHelpers::getDefaultPublicVars', 5000, function () use ($component) {\n        ClassHelpers::getDefaultPublicVars($component, Component::class);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: classUsesRecursive', function () {\n    $result = benchmark('ClassHelpers::classUsesRecursive', 5000, function () {\n        ClassHelpers::classUsesRecursive(ComponentWithTrait::class);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: getPublicMethods', function () {\n    $result = benchmark('ClassHelpers::getPublicMethods', 5000, function () {\n        ClassHelpers::getPublicMethods(Counter::class, ['render']);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\n// --- Request JSON decoding ---\n\ntest('BENCH: Request::all() with JSON values', function () {\n    $_REQUEST = [\n        'name' => 'test',\n        'count' => '5',\n        'data' => '{\"key\":\"value\",\"nested\":{\"a\":1}}',\n        'list' => '[1,2,3]',\n        'plain' => 'hello',\n        'yoyo-id' => 'yoyo-abc123',\n    ];\n    $request = new Request();\n\n    $result = benchmark('Request::all()', 5000, function () use ($request) {\n        $request->all();\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: Request::get() repeated access', function () {\n    $_REQUEST = ['data' => '{\"key\":\"value\"}'];\n    $request = new Request();\n\n    $result = benchmark('Request::get() x3', 5000, function () use ($request) {\n        $request->get('data');\n        $request->get('data');\n        $request->get('data');\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\n// --- Full render pipeline ---\n\ntest('BENCH: full component render (Counter)', function () {\n    $result = benchmark('render(Counter)', 500, function () {\n        render('counter', ['count' => 0]);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: full component update (Counter::increment)', function () {\n    $result = benchmark('update(Counter::increment)', 500, function () {\n        mockYoyoGetRequest('http://localhost/', 'counter/increment');\n        update('counter', 'increment', ['count' => 0]);\n        resetYoyoRequest();\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\n// --- YoyoCompiler ---\n\ntest('BENCH: YoyoCompiler::compile() simple HTML', function () {\n    $html = '<div><p>Hello World</p></div>';\n    $result = benchmark('YoyoCompiler::compile(simple)', 1000, function () use ($html) {\n        compile_html('test', $html);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: YoyoCompiler::compile() with yoyo attributes', function () {\n    $html = '<div><button yoyo:post=\"save\">Save</button><form><input type=\"text\" name=\"title\"></form></div>';\n    $result = benchmark('YoyoCompiler::compile(yoyo+form)', 1000, function () use ($html) {\n        compile_html('test', $html);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: YoyoCompiler::compile() with unicode', function () {\n    $html = '<div><p>日本語テスト Unicode: äöü ñ</p></div>';\n    $result = benchmark('YoyoCompiler::compile(unicode)', 1000, function () use ($html) {\n        compile_html('test', $html);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\n// --- Computed properties ---\n\ntest('BENCH: computed property access', function () {\n    $result = benchmark('render(computed-property)', 500, function () {\n        render('computed-property');\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n"
  },
  {
    "path": "tests/Benchmark/RealWorldBenchmarkTest.php",
    "content": "<?php\n\nuse function Tests\\compile_html;\nuse function Tests\\yoyo_view;\n\nbeforeAll(function () {\n    yoyo_view();\n});\n\nfunction bench_run(string $label, int $iterations, Closure $fn): array\n{\n    // Warmup\n    for ($i = 0; $i < min(10, $iterations); $i++) {\n        $fn();\n    }\n\n    $start = hrtime(true);\n    for ($i = 0; $i < $iterations; $i++) {\n        $fn();\n    }\n    $elapsed = (hrtime(true) - $start) / 1e6;\n    $perOp = $elapsed / $iterations;\n\n    return ['label' => $label, 'iterations' => $iterations, 'total_ms' => $elapsed, 'per_op_ms' => $perOp];\n}\n\n// ---------------------------------------------------------------------------\n//  Helper: build a compiled child component (as the parent compiler sees it)\n// ---------------------------------------------------------------------------\n\nfunction childComponent(string $name, int $id, string $innerHtml, array $vals = []): string\n{\n    $valsJson = json_encode(array_merge(['yoyo-id' => \"{$name}-{$id}\"], $vals));\n\n    return '<div id=\"'.$name.'-'.$id.'\" yoyo=\"\" hx-get=\"render\" class=\"yoyo-wrapper\" yoyo:name=\"'.$name.'\" hx-ext=\"yoyo\" hx-include=\"this\" hx-trigger=\"refresh\" hx-target=\"this\" hx-vals=\\''.$valsJson.'\\'>'.$innerHtml.'</div>';\n}\n\n// ---------------------------------------------------------------------------\n//  Realistic templates\n// ---------------------------------------------------------------------------\n\n// 1. Simple interactive component — counter with 2 buttons\n$counterHtml = <<<'HTML'\n<div id=\"counter\" yoyo:val.count=\"0\">\n    <button yoyo:get=\"decrement\">-</button>\n    <span>0</span>\n    <button yoyo:get=\"increment\">+</button>\n</div>\nHTML;\n\n// 2. Form with validation — registration/contact form\n$formHtml = <<<'HTML'\n<form id=\"register-form\" yoyo:post=\"register\" yoyo:on=\"submit\">\n    <div>\n        <label for=\"name\">Name</label>\n        <input id=\"name\" name=\"name\" type=\"text\" value=\"\" />\n        <span data-error=\"name\">Name is required</span>\n    </div>\n    <div>\n        <label for=\"email\">Email</label>\n        <input id=\"email\" name=\"email\" type=\"email\" value=\"\" />\n        <span data-error=\"email\">Invalid email</span>\n    </div>\n    <div>\n        <label for=\"message\">Message</label>\n        <textarea id=\"message\" name=\"message\"></textarea>\n    </div>\n    <button type=\"submit\">Submit</button>\n</form>\nHTML;\n\n// 3. Listing list with nested child components per row (JReviews pattern)\n//    Parent Yoyo component renders the list; each row has 3 child Yoyo\n//    components already compiled: favorite, mylist, comparison\nfunction buildListingList(int $rows): string\n{\n    $html = '<div id=\"listing-list\" yoyo:val.page=\"1\" yoyo:val.sort=\"newest\">';\n    $html .= '<div class=\"toolbar\">';\n    $html .= '<select yoyo:on=\"change\" yoyo:get=\"refresh\" name=\"sort\">';\n    $html .= '<option value=\"newest\">Newest</option><option value=\"rating\">Rating</option><option value=\"title\">Title</option>';\n    $html .= '</select>';\n    $html .= '<select yoyo:on=\"change\" yoyo:get=\"refresh\" name=\"perpage\">';\n    $html .= '<option value=\"10\">10</option><option value=\"25\">25</option><option value=\"50\">50</option>';\n    $html .= '</select>';\n    $html .= '</div>';\n\n    $html .= '<div class=\"listing-rows\">';\n    for ($i = 1; $i <= $rows; $i++) {\n        $html .= '<div class=\"listing-row\">';\n        $html .= '<div class=\"listing-photo\"><img src=\"listing-'.$i.'.jpg\" /></div>';\n        $html .= '<div class=\"listing-info\">';\n        $html .= '<h3><a href=\"/listing/'.$i.'\">Business Name '.$i.'</a></h3>';\n        $html .= '<p class=\"category\">Category &gt; Subcategory</p>';\n        $html .= '<div class=\"rating\"><span class=\"stars\">★★★★☆</span> <span class=\"count\">(42 reviews)</span></div>';\n        $html .= '<p class=\"address\">123 Main St, City, ST 12345</p>';\n        $html .= '</div>';\n\n        // 3 nested Yoyo child components (already compiled, as the parent sees them)\n        $html .= '<div class=\"listing-actions\">';\n        $html .= childComponent(\n            'favorite',\n            $i,\n            '<button hx-post=\"toggle\" id=\"favorite-'.$i.'-1\" class=\"btn-fav\">♡</button>',\n            ['listingId' => $i, 'isFavorite' => 0]\n        );\n        $html .= childComponent(\n            'mylist',\n            $i,\n            '<button hx-post=\"toggle\" id=\"mylist-'.$i.'-1\" class=\"btn-list\">+ My List</button><span class=\"count\">3 lists</span>',\n            ['listingId' => $i, 'inList' => 0]\n        );\n        $html .= childComponent(\n            'compare',\n            $i,\n            '<button hx-post=\"toggle\" id=\"compare-'.$i.'-1\" class=\"btn-compare\">Compare</button>',\n            ['listingId' => $i, 'inCompare' => 0]\n        );\n        $html .= '</div>';\n\n        $html .= '</div>';\n    }\n    $html .= '</div>';\n\n    // Pagination\n    $html .= '<nav class=\"pagination\">';\n    for ($p = 1; $p <= 5; $p++) {\n        $html .= '<a yoyo:get=\"render\" yoyo:val.page=\"'.$p.'\" class=\"page-link\">'.$p.'</a>';\n    }\n    $html .= '</nav>';\n    $html .= '</div>';\n\n    return $html;\n}\n\n$listingList10 = buildListingList(10);\n$listingList25 = buildListingList(25);\n$listingList50 = buildListingList(50);\n\n// 4. Listing form — complex form with many interactive fields\n$listingFormHtml = '<form id=\"listing-form\" yoyo:post=\"save\" yoyo:on=\"submit\">';\n$listingFormHtml .= '<div class=\"form-section\"><h3>Basic Info</h3>';\n$listingFormHtml .= '<input type=\"text\" name=\"title\" value=\"Business Name\" />';\n$listingFormHtml .= '<textarea name=\"description\">Description here</textarea>';\n$listingFormHtml .= '<select name=\"category\" yoyo:on=\"change\" yoyo:get=\"loadSubcategories\"><option>Cat 1</option><option>Cat 2</option></select>';\n$listingFormHtml .= '<select name=\"subcategory\"><option>Sub 1</option></select>';\n$listingFormHtml .= '</div>';\n$listingFormHtml .= '<div class=\"form-section\"><h3>Location</h3>';\n$listingFormHtml .= '<input type=\"text\" name=\"address\" value=\"123 Main St\" />';\n$listingFormHtml .= '<input type=\"text\" name=\"city\" value=\"City\" />';\n$listingFormHtml .= '<input type=\"text\" name=\"state\" value=\"ST\" />';\n$listingFormHtml .= '<input type=\"text\" name=\"zip\" value=\"12345\" />';\n$listingFormHtml .= '<input type=\"text\" name=\"phone\" value=\"555-1234\" />';\n$listingFormHtml .= '<input type=\"url\" name=\"website\" value=\"https://example.com\" />';\n$listingFormHtml .= '</div>';\n// Custom fields section with various interactive field types\n$listingFormHtml .= '<div class=\"form-section\"><h3>Custom Fields</h3>';\nfor ($f = 1; $f <= 8; $f++) {\n    $listingFormHtml .= '<div class=\"field-group\">';\n    $listingFormHtml .= '<label>Custom Field '.$f.'</label>';\n    if ($f % 3 === 0) {\n        $listingFormHtml .= '<select name=\"field'.$f.'\" yoyo:on=\"change\" yoyo:get=\"fieldDependency\" yoyo:val.field-id=\"'.$f.'\">';\n        $listingFormHtml .= '<option>Option A</option><option>Option B</option><option>Option C</option>';\n        $listingFormHtml .= '</select>';\n    } elseif ($f % 3 === 1) {\n        $listingFormHtml .= '<input type=\"text\" name=\"field'.$f.'\" value=\"Value '.$f.'\" />';\n    } else {\n        $listingFormHtml .= '<textarea name=\"field'.$f.'\">Content '.$f.'</textarea>';\n    }\n    $listingFormHtml .= '</div>';\n}\n$listingFormHtml .= '</div>';\n// Media upload section with nested Yoyo component\n$listingFormHtml .= '<div class=\"form-section\"><h3>Media</h3>';\n$listingFormHtml .= childComponent(\n    'media-upload',\n    1,\n    '<div class=\"dropzone\" hx-post=\"upload\" id=\"media-upload-1-1\"><input type=\"file\" name=\"photos[]\" multiple /><p>Drop files here</p></div>'\n    .'<div class=\"preview\"><div class=\"thumb\"><img src=\"photo1.jpg\"/><button hx-post=\"removePhoto\" id=\"media-upload-1-2\">×</button></div></div>',\n    ['listingId' => 1, 'maxFiles' => 10]\n);\n$listingFormHtml .= '</div>';\n$listingFormHtml .= '<div class=\"form-actions\">';\n$listingFormHtml .= '<button type=\"submit\">Save Listing</button>';\n$listingFormHtml .= '<button type=\"button\" yoyo:get=\"preview\">Preview</button>';\n$listingFormHtml .= '</div>';\n$listingFormHtml .= '</form>';\n\n// 5. Admin browse page — CP table with status toggles, edit actions per row\nfunction buildAdminTable(int $rows): string\n{\n    $html = '<div id=\"browse-listings\" yoyo:val.page=\"1\">';\n    $html .= '<div class=\"filters\">';\n    $html .= '<input type=\"search\" name=\"title\" yoyo:on=\"input\" yoyo:get=\"refresh\" yoyo:sync=\"this:replace\" placeholder=\"Search\" />';\n    $html .= '<select name=\"category\" yoyo:on=\"change\" yoyo:get=\"refresh\"><option>All</option><option>Cat 1</option><option>Cat 2</option></select>';\n    $html .= '<select name=\"status\" yoyo:on=\"change\" yoyo:get=\"refresh\"><option>All</option><option>Published</option><option>Unpublished</option></select>';\n    $html .= '<select name=\"ordering\" yoyo:on=\"change\" yoyo:get=\"refresh\"><option>Latest</option><option>Title</option><option>Rating</option></select>';\n    $html .= '</div>';\n\n    $html .= '<table><thead><tr><th>ID</th><th>Title</th><th>Author</th><th>Category</th><th>Reviews</th><th>Featured</th><th>Status</th><th>Actions</th></tr></thead><tbody>';\n    for ($i = 1; $i <= $rows; $i++) {\n        $html .= '<tr>';\n        $html .= '<td>'.$i.'</td>';\n        $html .= '<td><a href=\"/admin/listing/'.$i.'\">Business '.$i.'</a><div class=\"subtitle\">Category &gt; Sub</div></td>';\n        $html .= '<td>User '.$i.'<br/><small>Jan '.($i % 28 + 1).', 2025</small></td>';\n        $html .= '<td>Category Name</td>';\n        $html .= '<td>★ 4.'.($i % 10).' ('.$i.' reviews)</td>';\n        $html .= '<td><button yoyo:post=\"toggleFeatured('.$i.')\">'.($i % 3 === 0 ? '★' : '☆').'</button></td>';\n        $html .= '<td><button yoyo:post=\"togglePublished('.$i.')\">'.($i % 2 === 0 ? 'Published' : 'Unpublished').'</button></td>';\n        $html .= '<td><button yoyo:post=\"edit('.$i.')\">Edit</button> <button yoyo:delete=\"remove('.$i.')\" yoyo:confirm=\"Delete this listing?\">Delete</button></td>';\n        $html .= '</tr>';\n    }\n    $html .= '</tbody></table>';\n    $html .= '<nav>';\n    for ($p = 1; $p <= 5; $p++) {\n        $html .= '<a yoyo:get=\"render\" yoyo:val.page=\"'.$p.'\">'.$p.'</a>';\n    }\n    $html .= '</nav></div>';\n\n    return $html;\n}\n\n$adminTable25 = buildAdminTable(25);\n$adminTable50 = buildAdminTable(50);\n\n// ---------------------------------------------------------------------------\n//  Benchmarks\n// ---------------------------------------------------------------------------\n\ntest('BENCH: counter — simple interactive component', function () use ($counterHtml) {\n    $result = bench_run('Counter (2 buttons)', 1000, fn () => compile_html('counter', $counterHtml));\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: form — registration with validation', function () use ($formHtml) {\n    $result = bench_run('Form (inputs + submit)', 1000, fn () => compile_html('form', $formHtml));\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: listing list — 10 rows × 3 child components', function () use ($listingList10) {\n    $result = bench_run('Listing List (10×3 children)', 200, fn () => compile_html('listing-list', $listingList10));\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: listing list — 25 rows × 3 child components', function () use ($listingList25) {\n    $result = bench_run('Listing List (25×3 children)', 200, fn () => compile_html('listing-list', $listingList25));\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: listing list — 50 rows × 3 child components', function () use ($listingList50) {\n    $result = bench_run('Listing List (50×3 children)', 100, fn () => compile_html('listing-list', $listingList50));\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: listing form — complex form with fields + media upload', function () use ($listingFormHtml) {\n    $result = bench_run('Listing Form (fields + media)', 500, fn () => compile_html('listing-form', $listingFormHtml));\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: admin table — 25 rows with toggles + actions', function () use ($adminTable25) {\n    $result = bench_run('Admin Table (25 rows)', 200, fn () => compile_html('browse-listings', $adminTable25));\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: admin table — 50 rows with toggles + actions', function () use ($adminTable50) {\n    $result = bench_run('Admin Table (50 rows)', 100, fn () => compile_html('browse-listings', $adminTable50));\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\n// --- Summary ---\n\ntest('BENCH: summary', function () use ($counterHtml, $formHtml, $listingList10, $listingList25, $listingList50, $listingFormHtml, $adminTable25, $adminTable50) {\n    $scenarios = [\n        ['Counter (2 buttons)', 1000, fn () => compile_html('counter', $counterHtml)],\n        ['Form (inputs + submit)', 1000, fn () => compile_html('form', $formHtml)],\n        ['Listing List (10×3 children)', 200, fn () => compile_html('listing-list', $listingList10)],\n        ['Listing List (25×3 children)', 200, fn () => compile_html('listing-list', $listingList25)],\n        ['Listing List (50×3 children)', 100, fn () => compile_html('listing-list', $listingList50)],\n        ['Listing Form (fields + media)', 500, fn () => compile_html('listing-form', $listingFormHtml)],\n        ['Admin Table (25 rows)', 200, fn () => compile_html('browse-listings', $adminTable25)],\n        ['Admin Table (50 rows)', 100, fn () => compile_html('browse-listings', $adminTable50)],\n    ];\n\n    fwrite(STDERR, \"\\n\");\n    fwrite(STDERR, \"  ┌──────────────────────────────────────┬────────┬──────────────┐\\n\");\n    fwrite(STDERR, \"  │ Scenario                             │  ops   │    ms/op     │\\n\");\n    fwrite(STDERR, \"  ├──────────────────────────────────────┼────────┼──────────────┤\\n\");\n\n    foreach ($scenarios as [$label, $iterations, $fn]) {\n        $r = bench_run($label, $iterations, $fn);\n        fwrite(STDERR, sprintf(\n            \"  │ %-36s │ %5d  │ %10.4f   │\\n\",\n            $label,\n            $r['iterations'],\n            $r['per_op_ms']\n        ));\n    }\n\n    fwrite(STDERR, \"  └──────────────────────────────────────┴────────┴──────────────┘\\n\\n\");\n\n    expect(true)->toBeTrue();\n})->group('benchmark');\n"
  },
  {
    "path": "tests/Benchmark/YoyoCompilerBenchmarkTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\YoyoCompiler;\n\nuse function Tests\\compile_html;\n\nbeforeAll(function () {\n    Tests\\yoyo_view();\n});\n\nfunction bench(string $label, int $iterations, Closure $fn): array\n{\n    // Warmup\n    for ($i = 0; $i < min(10, $iterations); $i++) {\n        $fn();\n    }\n\n    $start = hrtime(true);\n    for ($i = 0; $i < $iterations; $i++) {\n        $fn();\n    }\n    $elapsed = (hrtime(true) - $start) / 1e6;\n    $perOp = $elapsed / $iterations;\n\n    fwrite(STDERR, sprintf(\n        \"  %-55s %6d ops  %8.3fms total  %8.4fms/op\\n\",\n        $label,\n        $iterations,\n        $elapsed,\n        $perOp,\n    ));\n\n    return ['label' => $label, 'iterations' => $iterations, 'total_ms' => $elapsed, 'per_op_ms' => $perOp];\n}\n\n// --- Phase breakdown: isolate individual steps of compile() ---\n\ntest('BENCH: preg_replace yoyo prefix finder', function () {\n    $html = '<div yoyo:get=\"action\" yoyo:trigger=\"click\" yoyo:target=\"#foo\"><button yoyo:post=\"save\" yoyo:confirm=\"Sure?\">Save</button></div>';\n    $prefix = YoyoCompiler::YOYO_PREFIX;\n    $finder = YoyoCompiler::YOYO_PREFIX_FINDER;\n\n    $result = bench('preg_replace (prefix finder)', 5000, function () use ($html, $prefix, $finder) {\n        preg_replace('/ '.$prefix.':(.*)=\"(.*)\"/U', \" $finder $prefix:\\$1=\\\"\\$2\\\"\", $html);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: mb_encode_numericentity (ASCII only)', function () {\n    $html = '<div><p>Hello World</p><button>Click me</button></div>';\n\n    $result = bench('mb_encode_numericentity (ASCII)', 5000, function () use ($html) {\n        mb_encode_numericentity($html, [0x80, 0x10FFFF, 0, ~0], 'UTF-8');\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: mb_encode_numericentity (unicode)', function () {\n    $html = '<div><p>极简、极速、极致 海豚PHP áéíóü café naïve</p></div>';\n\n    $result = bench('mb_encode_numericentity (unicode)', 5000, function () use ($html) {\n        mb_encode_numericentity($html, [0x80, 0x10FFFF, 0, ~0], 'UTF-8');\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: DOMDocument loadHTML', function () {\n    $html = '<html><body><div><p>Hello</p><button>Click</button></div></body></html>';\n\n    $result = bench('DOMDocument::loadHTML', 5000, function () use ($html) {\n        $dom = new DOMDocument();\n        $internalErrors = libxml_use_internal_errors(true);\n        $dom->loadHTML($html);\n        libxml_use_internal_errors($internalErrors);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: DOMDocument loadHTML + saveHTML', function () {\n    $html = '<html><body><div><p>Hello</p><button>Click</button></div></body></html>';\n\n    $result = bench('DOMDocument load+save', 5000, function () use ($html) {\n        $dom = new DOMDocument();\n        $internalErrors = libxml_use_internal_errors(true);\n        $dom->loadHTML($html);\n        libxml_use_internal_errors($internalErrors);\n        foreach ($dom->getElementsByTagName('body')->item(0)->childNodes as $node) {\n            $dom->saveHTML($node);\n        }\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: DOMXPath query', function () {\n    $dom = new DOMDocument();\n    $dom->loadHTML('<html><body><div><form><input type=\"file\"/></form><button yoyo-finder yoyo=\"\">Test</button></div></body></html>');\n    $xpath = new DOMXPath($dom);\n\n    $result = bench('DOMXPath::query', 5000, function () use ($xpath) {\n        $xpath->query('//form');\n        $xpath->query('//*[@yoyo]|//*[@yoyo-finder]');\n        $xpath->query('/html/body/*');\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\n// --- Full compile at different complexity levels ---\n\ntest('BENCH: compile() minimal HTML', function () {\n    $html = '<div>Hello</div>';\n    $result = bench('compile(minimal)', 2000, function () use ($html) {\n        compile_html('test', $html);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: compile() with 5 yoyo children', function () {\n    $html = '<div>';\n    for ($i = 0; $i < 5; $i++) {\n        $html .= '<button yoyo:get=\"action'.$i.'\" yoyo:target=\"#out\" yoyo:confirm=\"Sure?\">Btn '.$i.'</button>';\n    }\n    $html .= '</div>';\n\n    $result = bench('compile(5 yoyo children)', 1000, function () use ($html) {\n        compile_html('test', $html);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: compile() with 20 yoyo children', function () {\n    $html = '<div>';\n    for ($i = 0; $i < 20; $i++) {\n        $html .= '<button yoyo:get=\"action'.$i.'\" yoyo:target=\"#out\" yoyo:confirm=\"Sure?\">Btn '.$i.'</button>';\n    }\n    $html .= '</div>';\n\n    $result = bench('compile(20 yoyo children)', 500, function () use ($html) {\n        compile_html('test', $html);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: compile() with form + file inputs', function () {\n    $html = '<div><form><input type=\"text\" name=\"title\"/><input type=\"file\" name=\"doc\"/><textarea name=\"body\"></textarea><button type=\"submit\">Save</button></form></div>';\n\n    $result = bench('compile(form+file)', 1000, function () use ($html) {\n        compile_html('test', $html);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: compile() with yoyo:vals JSON', function () {\n    $html = '<div id=\"foo\" yoyo:vals=\\'{\"count\":0,\"filter\":\"active\",\"page\":1,\"sort\":\"name\"}\\'><p>Content</p></div>';\n\n    $result = bench('compile(yoyo:vals JSON)', 1000, function () use ($html) {\n        compile_html('test', $html);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: compile() with multiple yoyo:val attributes', function () {\n    $html = '<div id=\"foo\" yoyo:val.count=\"0\" yoyo:val.filter=\"active\" yoyo:val.page=\"1\" yoyo:val.sort-field=\"name\"><p>Content</p></div>';\n\n    $result = bench('compile(4x yoyo:val attrs)', 1000, function () use ($html) {\n        compile_html('test', $html);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: compile() realistic component (todo-list sized)', function () {\n    $html = '<div id=\"todo-list\">';\n    $html .= '<form yoyo:post=\"add\"><input type=\"text\" name=\"task\" placeholder=\"Add task\"/></form>';\n    $html .= '<ul>';\n    for ($i = 0; $i < 10; $i++) {\n        $html .= '<li>';\n        $html .= '<input type=\"checkbox\" yoyo:post=\"toggle\" yoyo:val.id=\"'.$i.'\"/>';\n        $html .= '<span>Task '.$i.'</span>';\n        $html .= '<button yoyo:delete=\"remove\" yoyo:val.id=\"'.$i.'\" yoyo:confirm=\"Delete?\">×</button>';\n        $html .= '</li>';\n    }\n    $html .= '</ul>';\n    $html .= '<div yoyo:get=\"filter\" yoyo:target=\"#todo-list\" yoyo:trigger=\"click\">All | Active | Done</div>';\n    $html .= '</div>';\n\n    $result = bench('compile(realistic todo-list)', 500, function () use ($html) {\n        compile_html('test', $html);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: compile() large table (50 rows)', function () {\n    $html = '<div id=\"data-table\"><table><thead><tr><th>ID</th><th>Name</th><th>Action</th></tr></thead><tbody>';\n    for ($i = 0; $i < 50; $i++) {\n        $html .= '<tr><td>'.$i.'</td><td>Item '.$i.'</td>';\n        $html .= '<td><button yoyo:post=\"edit\" yoyo:val.id=\"'.$i.'\">Edit</button>';\n        $html .= '<button yoyo:delete=\"remove\" yoyo:val.id=\"'.$i.'\" yoyo:confirm=\"Sure?\">Delete</button></td></tr>';\n    }\n    $html .= '</tbody></table></div>';\n\n    $result = bench('compile(50-row table)', 200, function () use ($html) {\n        compile_html('test', $html);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\ntest('BENCH: compile() innerHTML swap (spinning)', function () {\n    $html = '<div yoyo:swap=\"innerHTML\"><p>Content 1</p><p>Content 2</p><p>Content 3</p></div>';\n\n    $result = bench('compile(innerHTML swap, spinning)', 1000, function () use ($html) {\n        compile_html('test', $html, $spinning = true);\n    });\n    expect($result['total_ms'])->toBeGreaterThan(0);\n})->group('benchmark');\n\n// --- Summary ---\n\ntest('BENCH: compile cost breakdown summary', function () {\n    fwrite(STDERR, \"\\n  --- Compile Cost Breakdown ---\\n\");\n\n    // Minimal\n    $html = '<div>Hello</div>';\n    $simple = bench('  SUMMARY: minimal div', 2000, function () use ($html) {\n        compile_html('test', $html);\n    });\n\n    // With prefix regex\n    $html = '<div yoyo:get=\"action\"><button yoyo:post=\"save\">Save</button></div>';\n    $attrs = bench('  SUMMARY: with yoyo: attrs', 2000, function () use ($html) {\n        compile_html('test', $html);\n    });\n\n    // With vals\n    $html = '<div id=\"x\" yoyo:val.a=\"1\" yoyo:val.b=\"2\" yoyo:val.c=\"3\"><p>Content</p></div>';\n    $vals = bench('  SUMMARY: with yoyo:val attrs', 2000, function () use ($html) {\n        compile_html('test', $html);\n    });\n\n    fwrite(STDERR, sprintf(\n        \"\\n  Cost of yoyo attrs: +%.4fms/op (%.0f%% overhead)\\n\",\n        $attrs['per_op_ms'] - $simple['per_op_ms'],\n        (($attrs['per_op_ms'] / $simple['per_op_ms']) - 1) * 100\n    ));\n    fwrite(STDERR, sprintf(\n        \"  Cost of val parsing: +%.4fms/op (%.0f%% overhead vs minimal)\\n\\n\",\n        $vals['per_op_ms'] - $simple['per_op_ms'],\n        (($vals['per_op_ms'] / $simple['per_op_ms']) - 1) * 100\n    ));\n\n    expect($simple['per_op_ms'])->toBeLessThan(1.0);\n})->group('benchmark');\n"
  },
  {
    "path": "tests/Benchmark/profile-compiler.php",
    "content": "<?php\n\n/**\n * YoyoCompiler Performance Profiler\n *\n * Generates an HTML report with visual breakdown of where time is spent.\n * Run: php tests/Benchmark/profile-compiler.php\n * Opens: tests/Benchmark/profile-report.html\n */\n\nrequire __DIR__ . '/../../vendor/autoload.php';\nrequire __DIR__ . '/../Helpers.php';\n\nTests\\yoyo_view();\n\n// ─── Helpers ───────────────────────────────────────────────────────\n\nfunction bench(string $label, int $iterations, Closure $fn): array\n{\n    // Warmup\n    for ($i = 0; $i < min(10, $iterations); $i++) {\n        $fn();\n    }\n\n    $start = hrtime(true);\n    for ($i = 0; $i < $iterations; $i++) {\n        $fn();\n    }\n    $elapsed = (hrtime(true) - $start) / 1e6;\n\n    return [\n        'label' => $label,\n        'iterations' => $iterations,\n        'total_ms' => $elapsed,\n        'per_op_ms' => $elapsed / $iterations,\n    ];\n}\n\nfunction buildHtml(int $rows): string\n{\n    $html = '<div id=\"data-table\"><table><tbody>';\n    for ($i = 0; $i < $rows; $i++) {\n        $html .= '<tr><td>' . $i . '</td><td>Item ' . $i . '</td>';\n        $html .= '<td><button yoyo:post=\"edit\" yoyo:val.id=\"' . $i . '\">Edit</button>';\n        $html .= '<button yoyo:delete=\"remove\" yoyo:val.id=\"' . $i . '\" yoyo:confirm=\"Sure?\">Del</button></td></tr>';\n    }\n    $html .= '</tbody></table></div>';\n    return $html;\n}\n\nfunction buildTodoHtml(): string\n{\n    $html = '<div id=\"todo-list\">';\n    $html .= '<form yoyo:post=\"add\"><input type=\"text\" name=\"task\" placeholder=\"Add task\"/></form>';\n    $html .= '<ul>';\n    for ($i = 0; $i < 10; $i++) {\n        $html .= '<li>';\n        $html .= '<input type=\"checkbox\" yoyo:post=\"toggle\" yoyo:val.id=\"' . $i . '\"/>';\n        $html .= '<span>Task ' . $i . '</span>';\n        $html .= '<button yoyo:delete=\"remove\" yoyo:val.id=\"' . $i . '\" yoyo:confirm=\"Delete?\">×</button>';\n        $html .= '</li>';\n    }\n    $html .= '</ul>';\n    $html .= '<div yoyo:get=\"filter\" yoyo:target=\"#todo-list\" yoyo:trigger=\"click\">All | Active | Done</div>';\n    $html .= '</div>';\n    return $html;\n}\n\n// ─── Phase profiling for a given HTML ──────────────────────────────\n\nfunction profilePhases(string $html, int $iters): array\n{\n    $prefix = 'yoyo';\n    $finder = 'yoyo-finder';\n\n    // Phase 1: Regex (yoyo-finder injection)\n    $phase1 = bench('Regex: yoyo-finder injection', $iters, function () use ($html, $prefix, $finder) {\n        preg_replace(\n            ['/ ' . $prefix . ':(.*)=\"(.*)\"/U', '/ ' . $prefix . ':(.*)=\\'(.*)\\'/U'],\n            [\" $finder $prefix:\\$1=\\\"\\$2\\\"\", \" $finder $prefix:\\$1='\\$2'\"],\n            $html\n        );\n    });\n\n    // Prepare HTML after regex\n    $regexed = preg_replace(\n        ['/ ' . $prefix . ':(.*)=\"(.*)\"/U', '/ ' . $prefix . ':(.*)=\\'(.*)\\'/U'],\n        [\" $finder $prefix:\\$1=\\\"\\$2\\\"\", \" $finder $prefix:\\$1='\\$2'\"],\n        $html\n    );\n\n    // Phase 2: DOM parse\n    $phase2 = bench('DOM: loadHTML', $iters, function () use ($regexed) {\n        $dom = new DOMDocument();\n        $e = libxml_use_internal_errors(true);\n        $dom->loadHTML($regexed);\n        libxml_use_internal_errors($e);\n    });\n\n    // Phase 3: XPath creation + queries\n    $dom = new DOMDocument();\n    libxml_use_internal_errors(true);\n    $dom->loadHTML($regexed);\n    libxml_use_internal_errors(false);\n\n    $phase3 = bench('XPath: create + 3 queries', $iters, function () use ($dom) {\n        $xpath = new DOMXPath($dom);\n        $xpath->query('/html/body/*');\n        $xpath->query('//form');\n        $xpath->query('//*[@yoyo]|//*[@yoyo-finder]');\n    });\n\n    // Count reactive elements\n    $xpath = new DOMXPath($dom);\n    $children = $xpath->query('//*[@yoyo]|//*[@yoyo-finder]');\n    $childCount = max(0, $children->length - 1);\n\n    // Phase 4: Per-element attribute scanning (3 passes currently)\n    $phase4 = bench('Children: attr scanning (3-pass)', $iters, function () use ($children) {\n        foreach ($children as $key => $el) {\n            if ($key == 0) {\n                continue;\n            }\n            // Pass 1: addRequestMethodAttribute - check hx-* (8 checks)\n            foreach (['boost', 'delete', 'get', 'patch', 'post', 'put', 'sse', 'ws'] as $m) {\n                $el->hasAttribute('hx-' . $m);\n            }\n            // Pass 1b: addRequestMethodAttribute - check yoyo:* (8 checks)\n            foreach (['boost', 'delete', 'get', 'patch', 'post', 'put', 'sse', 'ws'] as $m) {\n                $el->getAttribute('yoyo:' . $m);\n            }\n            // Pass 2: scan for yoyo: attributes\n            foreach ($el->attributes as $a) {\n                str_starts_with($a->name, 'yoyo:');\n            }\n            // Pass 3: scan for yoyo:val. attributes\n            foreach ($el->attributes as $a) {\n                str_starts_with($a->name, 'yoyo:val.');\n            }\n        }\n    });\n\n    // Phase 5: DOM mutations (setAttribute/removeAttribute per element)\n    $phase5 = bench('Children: DOM mutations (set/remove)', $iters, function () use ($children) {\n        foreach ($children as $key => $el) {\n            if ($key == 0) {\n                continue;\n            }\n            // Typical: remove yoyo:post, set hx-post, remove yoyo:val.id, set hx-vals\n            $el->setAttribute('hx-post', 'edit');\n            $el->removeAttribute('hx-post');\n            $el->setAttribute('hx-vals', '{\"id\":1}');\n            $el->removeAttribute('hx-vals');\n        }\n    });\n\n    // Phase 6: encode/decode vals\n    $phase6 = bench('Vals: encode + decode per element', $iters, function () use ($childCount) {\n        for ($i = 0; $i < $childCount; $i++) {\n            Clickfwd\\Yoyo\\YoyoHelpers::decode_val((string) $i);\n            Clickfwd\\Yoyo\\YoyoHelpers::camel('sort-field', '-');\n            Clickfwd\\Yoyo\\YoyoHelpers::encode_vals(['id' => $i]);\n        }\n    });\n\n    // Phase 7: getOuterHTML (extra XPath + method check)\n    $phase7 = bench('Output: getOuterHTML XPath + check', $iters, function () use ($dom) {\n        $xpath2 = new DOMXPath($dom);\n        $matched = $xpath2->query(\"//*[starts-with(name(@*),'hx-')]\");\n        foreach ($matched as $n) {\n            foreach (['get', 'post', 'put', 'delete', 'patch', 'ws', 'sse'] as $v) {\n                if ($n->hasAttribute('hx-' . $v)) {\n                    break;\n                }\n            }\n        }\n    });\n\n    // Phase 8: saveHTML serialization\n    $phase8 = bench('Output: saveHTML (serialize)', $iters, function () use ($dom) {\n        $output = '';\n        foreach ($dom->getElementsByTagName('body')->item(0)->childNodes as $n) {\n            $output .= $dom->saveHTML($n);\n        }\n    });\n\n    // Full compile\n    $full = bench('FULL: compile()', $iters, function () use ($html) {\n        Tests\\compile_html('test', $html);\n    });\n\n    return [\n        'child_count' => $childCount,\n        'full' => $full,\n        'phases' => [$phase1, $phase2, $phase3, $phase4, $phase5, $phase6, $phase7, $phase8],\n    ];\n}\n\n// ─── Scaling analysis ──────────────────────────────────────────────\n\nfunction profileScaling(): array\n{\n    $sizes = [0, 1, 5, 10, 20, 50];\n    $results = [];\n\n    foreach ($sizes as $rows) {\n        $html = $rows === 0 ? '<div>Hello</div>' : buildHtml($rows);\n        $iters = $rows <= 5 ? 2000 : ($rows <= 20 ? 1000 : 500);\n        $r = bench(\"$rows rows\", $iters, function () use ($html) {\n            Tests\\compile_html('test', $html);\n        });\n        $r['rows'] = $rows;\n        $r['reactive_elements'] = $rows * 2;\n        $results[] = $r;\n    }\n\n    return $results;\n}\n\n// ─── Run everything ────────────────────────────────────────────────\n\nfwrite(STDERR, \"Profiling 50-row table...\\n\");\n$table50 = profilePhases(buildHtml(50), 500);\n\nfwrite(STDERR, \"Profiling realistic todo-list...\\n\");\n$todoList = profilePhases(buildTodoHtml(), 1000);\n\nfwrite(STDERR, \"Profiling minimal component...\\n\");\n$minimal = profilePhases('<div>Hello</div>', 2000);\n\nfwrite(STDERR, \"Profiling scaling behavior...\\n\");\n$scaling = profileScaling();\n\n// ─── Generate HTML report ──────────────────────────────────────────\n\n$phaseColors = [\n    '#e74c3c', // 1 regex - red\n    '#3498db', // 2 DOM parse - blue\n    '#2ecc71', // 3 XPath - green\n    '#f39c12', // 4 attr scan - orange (HOT)\n    '#9b59b6', // 5 DOM mutations - purple\n    '#1abc9c', // 6 vals encode - teal\n    '#e67e22', // 7 getOuterHTML - dark orange\n    '#95a5a6', // 8 saveHTML - gray\n];\n\nfunction phaseBar(array $profile, array $colors): string\n{\n    $full = $profile['full']['per_op_ms'];\n    $sum = 0;\n    $segments = [];\n\n    foreach ($profile['phases'] as $i => $phase) {\n        $pct = ($phase['per_op_ms'] / $full) * 100;\n        $sum += $phase['per_op_ms'];\n        $segments[] = sprintf(\n            '<div class=\"seg\" style=\"width:%.1f%%;background:%s\" title=\"%s: %.4fms (%.1f%%)\"></div>',\n            $pct,\n            $colors[$i],\n            htmlspecialchars($phase['label']),\n            $phase['per_op_ms'],\n            $pct\n        );\n    }\n\n    $unaccounted = $full - $sum;\n    $uPct = ($unaccounted / $full) * 100;\n    $segments[] = sprintf(\n        '<div class=\"seg\" style=\"width:%.1f%%;background:#bdc3c7\" title=\"Other overhead: %.4fms (%.1f%%)\"></div>',\n        $uPct,\n        $unaccounted,\n        $uPct\n    );\n\n    return implode('', $segments);\n}\n\nfunction phaseTable(array $profile): string\n{\n    global $phaseColors;\n    $full = $profile['full']['per_op_ms'];\n    $rows = '';\n    $sum = 0;\n\n    foreach ($profile['phases'] as $i => $phase) {\n        $pct = ($phase['per_op_ms'] / $full) * 100;\n        $sum += $phase['per_op_ms'];\n        $bar = str_repeat('█', max(1, (int) round($pct / 2)));\n        $rows .= sprintf(\n            '<tr><td><span class=\"dot\" style=\"background:%s\"></span>%s</td><td class=\"num\">%.4f</td><td class=\"num\">%.1f%%</td><td class=\"bar-cell\"><div class=\"mini-bar\" style=\"width:%.1f%%;background:%s\"></div></td></tr>',\n            $phaseColors[$i],\n            htmlspecialchars($phase['label']),\n            $phase['per_op_ms'],\n            $pct,\n            $pct,\n            $phaseColors[$i]\n        );\n    }\n\n    $unaccounted = $full - $sum;\n    $uPct = ($unaccounted / $full) * 100;\n    $rows .= sprintf(\n        '<tr><td><span class=\"dot\" style=\"background:#bdc3c7\"></span>Other (root attrs, form, overhead)</td><td class=\"num\">%.4f</td><td class=\"num\">%.1f%%</td><td class=\"bar-cell\"><div class=\"mini-bar\" style=\"width:%.1f%%;background:#bdc3c7\"></div></td></tr>',\n        $unaccounted,\n        $uPct,\n        $uPct\n    );\n\n    $rows .= sprintf(\n        '<tr class=\"total\"><td>Total compile()</td><td class=\"num\">%.4f</td><td class=\"num\">100%%</td><td></td></tr>',\n        $full\n    );\n\n    return $rows;\n}\n\nfunction scalingChart(array $scaling): string\n{\n    $maxMs = 0;\n    foreach ($scaling as $r) {\n        $maxMs = max($maxMs, $r['per_op_ms']);\n    }\n\n    $bars = '';\n    foreach ($scaling as $r) {\n        $pct = ($r['per_op_ms'] / $maxMs) * 100;\n        $label = $r['rows'] === 0 ? 'minimal' : $r['rows'] . ' rows';\n        $bars .= sprintf(\n            '<div class=\"scale-row\"><span class=\"scale-label\">%s<br><small>%d elems</small></span><div class=\"scale-bar-wrap\"><div class=\"scale-bar\" style=\"width:%.1f%%\"></div><span class=\"scale-val\">%.3fms</span></div></div>',\n            $label,\n            $r['reactive_elements'],\n            $pct,\n            $r['per_op_ms']\n        );\n    }\n    return $bars;\n}\n\n$html = <<<HTML\n<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<title>YoyoCompiler Performance Profile</title>\n<style>\n* { box-sizing: border-box; margin: 0; padding: 0; }\nbody { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; padding: 2rem; line-height: 1.5; }\nh1 { font-size: 1.6rem; margin-bottom: 0.5rem; color: #fff; }\nh2 { font-size: 1.2rem; margin: 2rem 0 0.8rem; color: #a8b2d1; border-bottom: 1px solid #333; padding-bottom: 0.4rem; }\nh3 { font-size: 1rem; margin: 1.2rem 0 0.5rem; color: #8892b0; }\n.meta { color: #666; font-size: 0.85rem; margin-bottom: 2rem; }\n.card { background: #16213e; border-radius: 8px; padding: 1.2rem; margin-bottom: 1.5rem; }\n.stacked-bar { display: flex; height: 36px; border-radius: 4px; overflow: hidden; margin: 0.5rem 0; }\n.seg { height: 100%; min-width: 1px; transition: opacity 0.2s; cursor: help; }\n.seg:hover { opacity: 0.8; }\ntable { width: 100%; border-collapse: collapse; font-size: 0.85rem; }\nth { text-align: left; color: #8892b0; font-weight: 500; padding: 0.4rem 0.6rem; border-bottom: 1px solid #333; }\ntd { padding: 0.4rem 0.6rem; border-bottom: 1px solid #222; }\n.num { text-align: right; font-variant-numeric: tabular-nums; font-family: 'SF Mono', Menlo, monospace; }\n.total td { font-weight: 600; border-top: 2px solid #444; color: #fff; }\n.dot { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 6px; vertical-align: middle; }\n.bar-cell { width: 35%; }\n.mini-bar { height: 14px; border-radius: 2px; min-width: 2px; }\n.scale-row { display: flex; align-items: center; margin: 0.4rem 0; }\n.scale-label { width: 90px; font-size: 0.8rem; text-align: right; padding-right: 12px; color: #8892b0; }\n.scale-label small { color: #555; }\n.scale-bar-wrap { flex: 1; display: flex; align-items: center; }\n.scale-bar { height: 24px; background: linear-gradient(90deg, #3498db, #e74c3c); border-radius: 3px; min-width: 3px; }\n.scale-val { margin-left: 8px; font-family: 'SF Mono', Menlo, monospace; font-size: 0.8rem; color: #a8b2d1; }\n.insight { background: #0f3460; border-left: 3px solid #f39c12; padding: 0.8rem 1rem; border-radius: 0 4px 4px 0; margin: 1rem 0; font-size: 0.9rem; }\n.insight strong { color: #f39c12; }\n.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }\n@media (max-width: 900px) { .cols { grid-template-columns: 1fr; } }\n.opt-table td:first-child { font-weight: 500; }\n.opt-table .save { color: #2ecc71; }\n.opt-table .cost { color: #e74c3c; }\n</style>\n</head>\n<body>\n<h1>YoyoCompiler — Performance Profile</h1>\n<p class=\"meta\">PHP {PHP_VERSION} · {$table50['full']['iterations']} iterations · {$table50['child_count']} reactive elements in 50-row table</p>\n\n<h2>1. Phase Breakdown — 50-Row Table ({$table50['child_count']} reactive elements)</h2>\n<div class=\"card\">\n    <h3>Time Distribution (hover for details)</h3>\n    <div class=\"stacked-bar\">{PHASE_BAR_TABLE50}</div>\n    <table>\n        <thead><tr><th>Phase</th><th>ms/op</th><th>%</th><th>Distribution</th></tr></thead>\n        <tbody>{PHASE_TABLE_TABLE50}</tbody>\n    </table>\n</div>\n\n<div class=\"insight\">\n    <strong>Key finding:</strong> Attribute scanning (3 separate passes per element) is the #1 scaling cost at {SCAN_PCT}%.\n    Combined with the redundant XPath in getOuterHTML ({OUTER_PCT}%), these two account for {COMBINED_PCT}% of compile time —\n    and both are optimizable.\n</div>\n\n<h2>2. Phase Breakdown Comparison</h2>\n<div class=\"cols\">\n    <div class=\"card\">\n        <h3>Realistic Todo-List ({$todoList['child_count']} elements)</h3>\n        <div class=\"stacked-bar\">{PHASE_BAR_TODO}</div>\n        <table>\n            <thead><tr><th>Phase</th><th>ms/op</th><th>%</th><th></th></tr></thead>\n            <tbody>{PHASE_TABLE_TODO}</tbody>\n        </table>\n    </div>\n    <div class=\"card\">\n        <h3>Minimal Component (0 elements)</h3>\n        <div class=\"stacked-bar\">{PHASE_BAR_MINIMAL}</div>\n        <table>\n            <thead><tr><th>Phase</th><th>ms/op</th><th>%</th><th></th></tr></thead>\n            <tbody>{PHASE_TABLE_MINIMAL}</tbody>\n        </table>\n    </div>\n</div>\n\n<h2>3. Scaling: Compile Time vs Component Size</h2>\n<div class=\"card\">\n    <div>{SCALING_CHART}</div>\n</div>\n\n<div class=\"insight\">\n    <strong>Scaling is linear</strong> with reactive element count. Each element adds ~{PER_ELEM_COST}ms.\n    The fixed overhead (regex + DOM parse + XPath + serialize) is ~{FIXED_COST}ms regardless of size.\n</div>\n\n<h2>4. Optimization Opportunities</h2>\n<div class=\"card\">\n    <table class=\"opt-table\">\n        <thead><tr><th>Optimization</th><th>Target</th><th>Expected Impact</th></tr></thead>\n        <tbody>\n            <tr>\n                <td>Single-pass attribute scan</td>\n                <td>Children: attr scanning ({SCAN_PCT}%)</td>\n                <td class=\"save\">Eliminate 2 of 3 passes → ~{SCAN_SAVE}% total savings</td>\n            </tr>\n            <tr>\n                <td>Eliminate redundant XPath in getOuterHTML</td>\n                <td>Output: getOuterHTML ({OUTER_PCT}%)</td>\n                <td class=\"save\">Reuse existing XPath → ~{OUTER_SAVE}% savings</td>\n            </tr>\n            <tr>\n                <td>Inline addRequestMethodAttribute for children</td>\n                <td>Part of attr scanning</td>\n                <td class=\"save\">Avoid 16 DOM calls per element → included in single-pass</td>\n            </tr>\n            <tr>\n                <td>PHP 8.4 Dom\\HTMLDocument</td>\n                <td>DOM: loadHTML ({PARSE_PCT}%)</td>\n                <td class=\"cost\">Actually ~20% slower for small/medium HTML</td>\n            </tr>\n        </tbody>\n    </table>\n</div>\n\n<p class=\"meta\" style=\"margin-top: 2rem; text-align: center;\">Generated by profile-compiler.php</p>\n</body>\n</html>\nHTML;\n\n// Fill in placeholders\n$scanPct = sprintf('%.1f', ($table50['phases'][3]['per_op_ms'] / $table50['full']['per_op_ms']) * 100);\n$outerPct = sprintf('%.1f', ($table50['phases'][6]['per_op_ms'] / $table50['full']['per_op_ms']) * 100);\n$combinedPct = sprintf('%.1f', (float)$scanPct + (float)$outerPct);\n$parsePct = sprintf('%.1f', ($table50['phases'][1]['per_op_ms'] / $table50['full']['per_op_ms']) * 100);\n\n// Per-element cost = (50-row total - minimal total) / 100 elements\n$perElemCost = sprintf('%.4f', ($scaling[5]['per_op_ms'] - $scaling[0]['per_op_ms']) / 100);\n$fixedCost = sprintf('%.3f', $scaling[0]['per_op_ms']);\n\n$replacements = [\n    '{PHASE_BAR_TABLE50}' => phaseBar($table50, $phaseColors),\n    '{PHASE_TABLE_TABLE50}' => phaseTable($table50),\n    '{PHASE_BAR_TODO}' => phaseBar($todoList, $phaseColors),\n    '{PHASE_TABLE_TODO}' => phaseTable($todoList),\n    '{PHASE_BAR_MINIMAL}' => phaseBar($minimal, $phaseColors),\n    '{PHASE_TABLE_MINIMAL}' => phaseTable($minimal),\n    '{SCALING_CHART}' => scalingChart($scaling),\n    '{SCAN_PCT}' => $scanPct,\n    '{OUTER_PCT}' => $outerPct,\n    '{COMBINED_PCT}' => $combinedPct,\n    '{PARSE_PCT}' => $parsePct,\n    '{SCAN_SAVE}' => sprintf('%.0f', (float)$scanPct * 0.7),\n    '{OUTER_SAVE}' => $outerPct,\n    '{PER_ELEM_COST}' => $perElemCost,\n    '{FIXED_COST}' => $fixedCost,\n];\n\n$html = str_replace(array_keys($replacements), array_values($replacements), $html);\n\n$outputPath = __DIR__ . '/profile-report.html';\nfile_put_contents($outputPath, $html);\nfwrite(STDERR, \"\\nReport saved to: $outputPath\\n\");\n"
  },
  {
    "path": "tests/Browser/BrowserServer.php",
    "content": "<?php\n\nnamespace Tests\\Browser;\n\n/**\n * Manages the PHP built-in server for browser tests.\n * Auto-starts if not already running, auto-stops on shutdown.\n */\nclass BrowserServer\n{\n    private static ?self $instance = null;\n\n    /** @var resource|null */\n    private $process = null;\n\n    /** @var array<int, resource> */\n    private array $pipes = [];\n\n    public const HOST = 'localhost';\n\n    public const PORT = 8765;\n\n    private function __construct()\n    {\n    }\n\n    public static function url(): string\n    {\n        return sprintf('http://%s:%d', self::HOST, self::PORT);\n    }\n\n    /**\n     * Ensure the server is running. Safe to call multiple times.\n     */\n    public static function ensureRunning(): void\n    {\n        if (self::$instance !== null) {\n            return;\n        }\n\n        self::$instance = new self();\n\n        // Already running externally (manual start)?\n        if (self::isListening()) {\n            return;\n        }\n\n        self::$instance->start();\n\n        register_shutdown_function([self::class, 'stop']);\n    }\n\n    /**\n     * Start the PHP built-in server.\n     */\n    private function start(): void\n    {\n        $router = realpath(__DIR__.'/server/index.php');\n\n        if ($router === false) {\n            throw new \\RuntimeException('Browser test server router not found at tests/Browser/server/index.php');\n        }\n\n        $cmd = sprintf(\n            'php -S %s:%d %s',\n            self::HOST,\n            self::PORT,\n            escapeshellarg($router)\n        );\n\n        $this->process = proc_open(\n            $cmd,\n            [\n                0 => ['pipe', 'r'],\n                1 => ['pipe', 'w'],\n                2 => ['pipe', 'w'],\n            ],\n            $this->pipes,\n            dirname($router)\n        );\n\n        if (! is_resource($this->process)) {\n            throw new \\RuntimeException('Failed to start browser test server');\n        }\n\n        // Wait for server to accept connections (up to 5 seconds)\n        for ($i = 0; $i < 50; $i++) {\n            if (self::isListening()) {\n                return;\n            }\n\n            usleep(100_000); // 100ms\n        }\n\n        self::stop();\n        throw new \\RuntimeException(\n            sprintf('Browser test server failed to start on %s:%d within 5 seconds', self::HOST, self::PORT)\n        );\n    }\n\n    /**\n     * Check if the server port is accepting connections.\n     */\n    public static function isListening(): bool\n    {\n        $sock = @fsockopen(self::HOST, self::PORT, $errno, $errstr, 0.5);\n\n        if ($sock) {\n            fclose($sock);\n\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * Stop the server if we started it.\n     */\n    public static function stop(): void\n    {\n        if (self::$instance === null) {\n            return;\n        }\n\n        $instance = self::$instance;\n\n        if (is_resource($instance->process)) {\n            // Close stdin to signal shutdown\n            if (isset($instance->pipes[0]) && is_resource($instance->pipes[0])) {\n                fclose($instance->pipes[0]);\n            }\n\n            proc_terminate($instance->process);\n            proc_close($instance->process);\n            $instance->process = null;\n        }\n\n        self::$instance = null;\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Components/ActionButton.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ActionButton extends Component\n{\n    public $label;\n\n    protected $props = ['label'];\n\n    public function fire()\n    {\n        $this->emit('notification');\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Components/Counter.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass Counter extends Component\n{\n    public $count = 0;\n\n    protected $queryString = ['count'];\n\n    protected $props = ['count'];\n\n    public function increment()\n    {\n        $this->count++;\n    }\n\n    public function decrement()\n    {\n        $this->count--;\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Components/DeleteItem.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass DeleteItem extends Component\n{\n    public $itemId;\n\n    public $title;\n\n    protected $props = ['itemId', 'title'];\n\n    public function delete()\n    {\n        $this->skipRender();\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Components/DispatchBystander.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass DispatchBystander extends Component\n{\n    public $label = 'unchanged';\n\n    protected $props = ['label'];\n}\n"
  },
  {
    "path": "tests/Browser/Components/DispatchListener.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass DispatchListener extends Component\n{\n    public $message = '';\n\n    protected $listeners = [\n        'post-created' => 'handlePostCreated',\n        'status-changed' => 'handleStatusChanged',\n        'simple-refresh' => 'handleSimpleRefresh',\n    ];\n\n    public function handlePostCreated($postId)\n    {\n        $this->message = \"Post created with ID: {$postId}\";\n    }\n\n    public function handleStatusChanged($status, $reason = 'none')\n    {\n        $this->message = \"Status: {$status}, Reason: {$reason}\";\n    }\n\n    public function handleSimpleRefresh()\n    {\n        $this->message = 'Refreshed without params';\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Components/FavoriteButton.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass FavoriteButton extends Component\n{\n    public $itemId;\n\n    public $isFavorited = 0;\n\n    protected $props = ['itemId', 'isFavorited'];\n\n    public function toggle()\n    {\n        $this->isFavorited = $this->isFavorited ? 0 : 1;\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Components/Form.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass Form extends Component\n{\n    public $name = '';\n\n    public $email = '';\n\n    public $success = false;\n\n    public $errors = [];\n\n    public function register()\n    {\n        $this->errors = [];\n\n        if (empty($this->name)) {\n            $this->errors['name'] = 'Name is required.';\n        }\n\n        if (empty($this->email)) {\n            $this->errors['email'] = 'Email is required.';\n        }\n\n        if (empty($this->errors)) {\n            $this->success = true;\n        }\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Components/LiveSearch.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass LiveSearch extends Component\n{\n    public $q = '';\n\n    protected $queryString = ['q'];\n\n    protected $results = [];\n\n    private static $data = [\n        ['title' => 'PHP Basics'],\n        ['title' => 'PHP Advanced'],\n        ['title' => 'JavaScript Guide'],\n        ['title' => 'CSS Flexbox'],\n        ['title' => 'HTML Forms'],\n        ['title' => 'Laravel Framework'],\n        ['title' => 'Yoyo Components'],\n        ['title' => 'Alpine JS'],\n    ];\n\n    protected function getResultsProperty()\n    {\n        if (! $this->q) {\n            return [];\n        }\n\n        return array_filter(self::$data, function ($item) {\n            return stripos($item['title'], $this->q) !== false;\n        });\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Components/ModalTrigger.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ModalTrigger extends Component\n{\n    public $isOpen = 0;\n\n    public $modalTitle = '';\n\n    public function openModal()\n    {\n        $this->isOpen = 1;\n    }\n\n    public function closeModal()\n    {\n        $this->isOpen = 0;\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Components/MultiScreen.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass MultiScreen extends Component\n{\n    public $message = '';\n\n    public function open()\n    {\n        // Renders the form screen via actionMatches('open')\n    }\n\n    public function submit()\n    {\n        // Renders the success screen via actionMatches('submit')\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Components/NotificationBadge.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass NotificationBadge extends Component\n{\n    public $count = 0;\n\n    protected $props = ['count'];\n\n    protected $listeners = ['notification' => 'onNotification'];\n\n    public function onNotification()\n    {\n        $this->count = $this->count + 1;\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Components/NullProp.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass NullProp extends Component\n{\n    public $iconSlot = null;\n\n    public $enabled = false;\n\n    public $clicks = 0;\n\n    protected $props = ['iconSlot', 'enabled'];\n\n    public function increment()\n    {\n        $this->clicks++;\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Components/Pagination.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass Pagination extends Component\n{\n    public $page = 1;\n\n    protected $queryString = ['page'];\n\n    protected $props = ['page'];\n\n    private $perPage = 3;\n\n    private $totalItems = 12;\n\n    protected function getResultsProperty()\n    {\n        $items = [];\n\n        for ($i = 1; $i <= $this->totalItems; $i++) {\n            $items[] = ['title' => \"Item $i\"];\n        }\n\n        $offset = ($this->page - 1) * $this->perPage;\n\n        return array_slice($items, $offset, $this->perPage);\n    }\n\n    protected function getTotalPagesProperty()\n    {\n        return (int) ceil($this->totalItems / $this->perPage);\n    }\n\n    protected function getStartProperty()\n    {\n        return (($this->page - 1) * $this->perPage) + 1;\n    }\n\n    protected function getEndProperty()\n    {\n        return min($this->page * $this->perPage, $this->totalItems);\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Components/ResponseHeaders.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ResponseHeaders extends Component\n{\n    public $message = 'initial';\n\n    public function doRetarget()\n    {\n        $this->response->retarget('#retarget-receiver');\n        $this->message = 'retargeted content';\n    }\n\n    public function doReswap()\n    {\n        $this->response->reswap('innerHTML');\n        $this->message = 'inner-swapped';\n    }\n\n    public function doTrigger()\n    {\n        $this->response->trigger('custom-event');\n        $this->message = 'triggered';\n    }\n\n    public function doTriggerAfterSettle()\n    {\n        $this->response->triggerAfterSettle('settle-event');\n        $this->message = 'settled';\n    }\n\n    public function doPushUrl()\n    {\n        $this->response->pushUrl('/pushed-path');\n        $this->message = 'url-pushed';\n    }\n\n    public function doReplaceUrl()\n    {\n        $this->response->replaceUrl('/replaced-path');\n        $this->message = 'url-replaced';\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Components/StatusDropdown.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass StatusDropdown extends Component\n{\n    public $itemId;\n\n    public $status = 'draft';\n\n    public $isOpen = false;\n\n    public $newStatus;\n\n    protected $props = ['itemId', 'status'];\n\n    public function toggleMenu()\n    {\n        $this->isOpen = ! $this->isOpen;\n    }\n\n    public function setStatus()\n    {\n        $this->status = $this->newStatus;\n        $this->isOpen = false;\n    }\n}\n"
  },
  {
    "path": "tests/Browser/Components/TodoList.php",
    "content": "<?php\n\nnamespace Tests\\Browser\\Components;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass TodoList extends Component\n{\n    public $filter = '';\n\n    protected $props = ['filter'];\n\n    protected $queryString = ['filter'];\n\n    // Simple in-memory storage via session\n    public function mount()\n    {\n        if (session_status() === PHP_SESSION_NONE) {\n            session_start();\n        }\n\n        if (! isset($_SESSION['todos'])) {\n            $_SESSION['todos'] = [\n                ['id' => 1, 'title' => 'Build a framework', 'completed' => false],\n                ['id' => 2, 'title' => 'Buy groceries', 'completed' => false],\n                ['id' => 3, 'title' => 'Write tests', 'completed' => true],\n            ];\n        }\n    }\n\n    public function add()\n    {\n        $title = trim($this->request->get('task', ''));\n\n        if ($title) {\n            $id = max(array_column($_SESSION['todos'], 'id')) + 1;\n            $_SESSION['todos'][] = ['id' => $id, 'title' => $title, 'completed' => false];\n        }\n    }\n\n    public function toggle()\n    {\n        $id = (int) $this->request->get('id', 0);\n\n        foreach ($_SESSION['todos'] as &$todo) {\n            if ($todo['id'] === $id) {\n                $todo['completed'] = ! $todo['completed'];\n                break;\n            }\n        }\n    }\n\n    public function delete()\n    {\n        $id = (int) $this->request->get('id', 0);\n\n        $_SESSION['todos'] = array_values(array_filter($_SESSION['todos'], function ($todo) use ($id) {\n            return $todo['id'] !== $id;\n        }));\n    }\n\n    protected function getEntriesProperty()\n    {\n        $todos = $_SESSION['todos'] ?? [];\n\n        if ($this->filter === 'active') {\n            return array_filter($todos, fn ($t) => ! $t['completed']);\n        }\n\n        if ($this->filter === 'completed') {\n            return array_filter($todos, fn ($t) => $t['completed']);\n        }\n\n        return $todos;\n    }\n\n    protected function getCountProperty()\n    {\n        return count($_SESSION['todos'] ?? []);\n    }\n\n    protected function getActiveCountProperty()\n    {\n        return count(array_filter($_SESSION['todos'] ?? [], fn ($t) => ! $t['completed']));\n    }\n}\n"
  },
  {
    "path": "tests/Browser/CounterTest.php",
    "content": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders with initial count of 0', function () {\n    $this->visit(BASE_URL.'/counter')\n        ->assertVisible('#counter')\n        ->assertSeeIn('[data-count]', '0');\n});\n\nit('increments count when clicking +', function () {\n    $this->visit(BASE_URL.'/counter')\n        ->assertSeeIn('[data-count]', '0')\n        ->click('[data-action=\"increment\"]')\n        ->assertSeeIn('[data-count]', '1');\n});\n\nit('decrements count when clicking -', function () {\n    $this->visit(BASE_URL.'/counter')\n        ->click('[data-action=\"decrement\"]')\n        ->assertSeeIn('[data-count]', '-1');\n});\n\nit('maintains state across multiple actions', function () {\n    $page = $this->visit(BASE_URL.'/counter');\n\n    for ($i = 0; $i < 3; $i++) {\n        $page->click('[data-action=\"increment\"]')\n            ->assertSeeIn('[data-count]', (string) ($i + 1))\n            ->wait(0.3);\n    }\n});\n\nit('updates query string with count value', function () {\n    $this->visit(BASE_URL.'/counter')\n        ->click('[data-action=\"increment\"]')\n        ->assertQueryStringHas('count', '1');\n});\n"
  },
  {
    "path": "tests/Browser/CrossComponentEventsTest.php",
    "content": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders badge and action buttons', function () {\n    $this->visit(BASE_URL.'/events')\n        ->assertVisible('#events-test')\n        ->assertVisible('#badge')\n        ->assertSeeIn('#badge [data-count]', '0')\n        ->assertVisible('#btn-email')\n        ->assertVisible('#btn-add');\n});\n\nit('increments badge count when action button emits event', function () {\n    $page = $this->visit(BASE_URL.'/events')\n        ->assertSeeIn('#badge [data-count]', '0');\n\n    $page->click('#btn-email [data-action=\"fire\"]')\n        ->assertSeeIn('#badge [data-count]', '1');\n});\n\nit('increments badge count from multiple buttons', function () {\n    $page = $this->visit(BASE_URL.'/events')\n        ->assertSeeIn('#badge [data-count]', '0');\n\n    $page->click('#btn-email [data-action=\"fire\"]')\n        ->assertSeeIn('#badge [data-count]', '1')\n        ->wait(0.3);\n\n    $page->click('#btn-add [data-action=\"fire\"]')\n        ->assertSeeIn('#badge [data-count]', '2');\n});\n\nit('badge has notification listener in hx-trigger', function () {\n    $this->visit(BASE_URL.'/events')\n        ->assertSourceHas('hx-trigger=\"refresh,notification\"');\n});\n"
  },
  {
    "path": "tests/Browser/DispatchTest.php",
    "content": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('registers listener events in hx-trigger attribute', function () {\n    $this->visit(BASE_URL.'/dispatch')\n        ->assertSourceHas('yoyo:name=\"dispatch-listener\"')\n        ->assertSourceHas('hx-trigger=\"refresh,post-created,status-changed,simple-refresh\"');\n});\n\nit('dispatches event with single named param', function () {\n    $this->visit(BASE_URL.'/dispatch')\n        ->click('#btn-dispatch')\n        ->assertSeeIn('#message', 'Post created with ID: 42');\n});\n\nit('dispatches event with multiple named params', function () {\n    $this->visit(BASE_URL.'/dispatch')\n        ->click('#btn-dispatch-multi')\n        ->assertSeeIn('#message', 'Status: active, Reason: manual');\n});\n\nit('dispatches targeted event via dispatchTo', function () {\n    $this->visit(BASE_URL.'/dispatch')\n        ->click('#btn-dispatch-to')\n        ->assertSeeIn('#message', 'Post created with ID: 7');\n});\n\nit('dispatches event without params', function () {\n    $this->visit(BASE_URL.'/dispatch')\n        ->click('#btn-dispatch-no-params')\n        ->assertSeeIn('#message', 'Refreshed without params');\n});\n\nit('does not update non-listening bystander component', function () {\n    $this->visit(BASE_URL.'/dispatch')\n        ->assertSeeIn('#bystander', 'unchanged')\n        ->click('#btn-dispatch')\n        ->assertSeeIn('#message', 'Post created with ID: 42')\n        ->assertSeeIn('#bystander', 'unchanged');\n});\n\nit('dispatchTo non-existent component is a silent no-op', function () {\n    $page = $this->visit(BASE_URL.'/dispatch');\n\n    $page->click('#btn-dispatch-nonexistent');\n    $page->wait(1);\n\n    // Listener should still have empty message — component was not updated\n    $page->assertSourceHas('<span id=\"message\"></span>');\n});\n"
  },
  {
    "path": "tests/Browser/FormTest.php",
    "content": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders with empty fields', function () {\n    $this->visit(BASE_URL.'/form')\n        ->assertVisible('#form')\n        ->assertVisible('input#name')\n        ->assertVisible('input#email')\n        ->assertVisible('button[type=\"submit\"]');\n});\n\nit('shows validation errors when submitting empty form', function () {\n    $this->visit(BASE_URL.'/form')\n        ->click('button[type=\"submit\"]')\n        ->assertVisible('[data-error=\"name\"]')\n        ->assertSee('Name is required')\n        ->assertVisible('[data-error=\"email\"]')\n        ->assertSee('Email is required');\n});\n\nit('shows success message after valid submission', function () {\n    $this->visit(BASE_URL.'/form')\n        ->fill('input#name', 'John Doe')\n        ->fill('input#email', 'john@example.com')\n        ->click('button[type=\"submit\"]')\n        ->assertVisible('[data-success]')\n        ->assertSee('Thank you for registering!');\n});\n\nit('replaces form with success state', function () {\n    $this->visit(BASE_URL.'/form')\n        ->fill('input#name', 'Jane')\n        ->fill('input#email', 'jane@test.com')\n        ->click('button[type=\"submit\"]')\n        ->assertMissing('input#name')\n        ->assertMissing('button[type=\"submit\"]')\n        ->assertSee('Thank you for registering!');\n});\n"
  },
  {
    "path": "tests/Browser/InfrastructureTest.php",
    "content": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('includes htmx script', function () {\n    $this->visit(BASE_URL.'/counter')\n        ->assertSourceHas('htmx');\n});\n\nit('includes yoyo.js script', function () {\n    $this->visit(BASE_URL.'/counter')\n        ->assertSourceHas('yoyo.js');\n});\n\nit('initializes Yoyo configuration in JavaScript', function () {\n    $this->visit(BASE_URL.'/counter')\n        ->assertSourceHas('Yoyo.url')\n        ->assertSourceHas('Yoyo.config(');\n});\n\nit('includes yoyo spinning CSS', function () {\n    $this->visit(BASE_URL.'/counter')\n        ->assertSourceHas('yoyo\\\\:spinning');\n});\n\nit('compiles yoyo: attributes to hx- attributes', function () {\n    $this->visit(BASE_URL.'/counter')\n        ->assertSourceHas('hx-get=\"increment\"')\n        ->assertSourceHas('hx-get=\"decrement\"');\n});\n\nit('adds yoyo wrapper attributes to component root', function () {\n    $this->visit(BASE_URL.'/counter')\n        ->assertSourceHas('yoyo:name=\"counter\"')\n        ->assertSourceHas('hx-ext=\"yoyo\"')\n        ->assertSourceHas('hx-include=\"this\"');\n});\n"
  },
  {
    "path": "tests/Browser/LiveSearchTest.php",
    "content": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders with empty search input', function () {\n    $this->visit(BASE_URL.'/live-search')\n        ->assertVisible('#live-search')\n        ->assertVisible('input[name=\"q\"]')\n        ->assertMissing('[data-results]');\n});\n\nit('shows matching results when typing', function () {\n    $this->visit(BASE_URL.'/live-search')\n        ->typeSlowly('input[name=\"q\"]', 'php', 100)\n        ->assertVisible('[data-results]')\n        ->assertSeeIn('[data-results]', 'PHP Basics')\n        ->assertSeeIn('[data-results]', 'PHP Advanced');\n});\n\nit('shows no results message for unmatched query', function () {\n    $this->visit(BASE_URL.'/live-search')\n        ->typeSlowly('input[name=\"q\"]', 'zzzzz', 100)\n        ->assertVisible('[data-no-results]')\n        ->assertSee('No results found');\n});\n\nit('filters results based on query', function () {\n    $this->visit(BASE_URL.'/live-search')\n        ->typeSlowly('input[name=\"q\"]', 'yoyo', 100)\n        ->assertSeeIn('[data-results]', 'Yoyo Components')\n        ->assertDontSeeIn('[data-results]', 'PHP Basics');\n});\n"
  },
  {
    "path": "tests/Browser/ModalTest.php",
    "content": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders without modal visible', function () {\n    $this->visit(BASE_URL.'/modal')\n        ->assertVisible('#modal-trigger')\n        ->assertVisible('[data-action=\"open\"]')\n        ->assertNotPresent('[data-modal]');\n});\n\nit('opens modal on button click', function () {\n    $this->visit(BASE_URL.'/modal')\n        ->assertNotPresent('[data-modal]')\n        ->click('[data-action=\"open\"]')\n        ->assertVisible('[data-modal]')\n        ->assertSeeIn('[data-modal-title]', 'Modal Content')\n        ->assertSee('This is the modal body.');\n});\n\nit('closes modal on close button click', function () {\n    $page = $this->visit(BASE_URL.'/modal');\n\n    $page->click('[data-action=\"open\"]')\n        ->assertVisible('[data-modal]')\n        ->wait(0.3);\n\n    $page->click('[data-action=\"close\"]')\n        ->assertNotPresent('[data-modal]');\n});\n\nit('can reopen modal after closing', function () {\n    $page = $this->visit(BASE_URL.'/modal');\n\n    $page->click('[data-action=\"open\"]')\n        ->assertVisible('[data-modal]')\n        ->wait(0.3);\n\n    $page->click('[data-action=\"close\"]')\n        ->assertNotPresent('[data-modal]')\n        ->wait(0.3);\n\n    $page->click('[data-action=\"open\"]')\n        ->assertVisible('[data-modal]');\n});\n\nit('uses native dialog element', function () {\n    $this->visit(BASE_URL.'/modal')\n        ->click('[data-action=\"open\"]')\n        ->assertSourceHas('<dialog')\n        ->assertSourceHas('data-modal');\n});\n"
  },
  {
    "path": "tests/Browser/MultiScreenTest.php",
    "content": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders initial screen with open button', function () {\n    $this->visit(BASE_URL.'/multi-screen')\n        ->assertVisible('#wizard')\n        ->assertVisible('[data-screen=\"initial\"]')\n        ->assertSeeIn('[data-info]', 'Ready to begin')\n        ->assertVisible('[data-action=\"open\"]');\n});\n\nit('transitions to form screen on open', function () {\n    $this->visit(BASE_URL.'/multi-screen')\n        ->assertVisible('[data-screen=\"initial\"]')\n        ->click('[data-action=\"open\"]')\n        ->assertVisible('[data-screen=\"form\"]')\n        ->assertMissing('[data-screen=\"initial\"]')\n        ->assertVisible('input[name=\"message\"]')\n        ->assertVisible('[data-action=\"submit\"]')\n        ->assertVisible('[data-action=\"cancel\"]');\n});\n\nit('transitions to success screen on submit', function () {\n    $page = $this->visit(BASE_URL.'/multi-screen');\n\n    $page->click('[data-action=\"open\"]')\n        ->assertVisible('[data-screen=\"form\"]')\n        ->wait(0.3);\n\n    $page->fill('input[name=\"message\"]', 'Hello World')\n        ->click('[data-action=\"submit\"]')\n        ->assertVisible('[data-screen=\"success\"]')\n        ->assertMissing('[data-screen=\"form\"]')\n        ->assertSeeIn('[data-result]', 'Submitted: Hello World');\n});\n\nit('returns to initial screen on cancel', function () {\n    $page = $this->visit(BASE_URL.'/multi-screen');\n\n    $page->click('[data-action=\"open\"]')\n        ->assertVisible('[data-screen=\"form\"]')\n        ->wait(0.3);\n\n    $page->click('[data-action=\"cancel\"]')\n        ->assertVisible('[data-screen=\"initial\"]')\n        ->assertMissing('[data-screen=\"form\"]');\n});\n\nit('returns to initial screen from success via reset', function () {\n    $page = $this->visit(BASE_URL.'/multi-screen');\n\n    $page->click('[data-action=\"open\"]')\n        ->assertVisible('[data-screen=\"form\"]')\n        ->wait(0.3);\n\n    $page->fill('input[name=\"message\"]', 'Test')\n        ->click('[data-action=\"submit\"]')\n        ->assertVisible('[data-screen=\"success\"]')\n        ->wait(0.3);\n\n    $page->click('[data-action=\"reset\"]')\n        ->assertVisible('[data-screen=\"initial\"]')\n        ->assertMissing('[data-screen=\"success\"]');\n});\n"
  },
  {
    "path": "tests/Browser/NullPropTest.php",
    "content": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('preserves PHP null prop after request roundtrip', function () {\n    $this->visit(BASE_URL.'/null-prop')\n        ->assertVisible('#null-prop')\n        ->assertAttribute('[data-icon-slot-type]', 'data-icon-slot-type', 'NULL')\n        ->assertSeeIn('[data-icon-slot-display]', 'NULL_OK')\n        ->click('[data-action=\"increment\"]')\n        ->assertSeeIn('[data-clicks]', '1')\n        ->assertAttribute('[data-icon-slot-type]', 'data-icon-slot-type', 'NULL')\n        ->assertSeeIn('[data-icon-slot-display]', 'NULL_OK');\n});\n\nit('preserves PHP false prop after request roundtrip', function () {\n    $this->visit(BASE_URL.'/null-prop')\n        ->assertAttribute('[data-enabled-type]', 'data-enabled-type', 'boolean')\n        ->assertSeeIn('[data-enabled-display]', 'FALSE_OK')\n        ->click('[data-action=\"increment\"]')\n        ->assertSeeIn('[data-clicks]', '1')\n        ->assertAttribute('[data-enabled-type]', 'data-enabled-type', 'boolean')\n        ->assertSeeIn('[data-enabled-display]', 'FALSE_OK');\n});\n\nit('emits JSON null and false in button hx-vals', function () {\n    $this->visit(BASE_URL.'/null-prop')\n        ->assertAttributeContains('[data-action=\"increment\"]', 'hx-vals', '\"iconSlot\":null')\n        ->assertAttributeContains('[data-action=\"increment\"]', 'hx-vals', '\"enabled\":false')\n        ->assertAttributeDoesntContain('[data-action=\"increment\"]', 'hx-vals', '\"iconSlot\":\"null\"')\n        ->assertAttributeDoesntContain('[data-action=\"increment\"]', 'hx-vals', '\"enabled\":\"false\"');\n});\n"
  },
  {
    "path": "tests/Browser/PaginationTest.php",
    "content": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders first page of results', function () {\n    $this->visit(BASE_URL.'/pagination')\n        ->assertVisible('#pagination')\n        ->assertVisible('[data-results]')\n        ->assertSeeIn('[data-page-info]', 'Showing 1 to 3')\n        ->assertSeeIn('[data-results]', 'Item 1');\n});\n\nit('navigates to page 2', function () {\n    $this->visit(BASE_URL.'/pagination')\n        ->click('[data-page=\"2\"]')\n        ->assertSeeIn('[data-page-info]', 'Showing 4 to 6')\n        ->assertSeeIn('[data-results]', 'Item 4');\n});\n\nit('navigates to last page', function () {\n    $this->visit(BASE_URL.'/pagination')\n        ->click('[data-page=\"4\"]')\n        ->assertSeeIn('[data-page-info]', 'Showing 10 to 12')\n        ->assertSeeIn('[data-results]', 'Item 12');\n});\n\nit('updates query string with page number', function () {\n    $this->visit(BASE_URL.'/pagination')\n        ->click('[data-page=\"3\"]')\n        ->assertQueryStringHas('page', '3');\n});\n\nit('highlights active page', function () {\n    $this->visit(BASE_URL.'/pagination')\n        ->assertAttribute('[data-page=\"1\"]', 'class', 'active');\n});\n"
  },
  {
    "path": "tests/Browser/ProductListTest.php",
    "content": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders all products with their own component instances', function () {\n    $this->visit(BASE_URL.'/product-list')\n        ->assertVisible('#product-list')\n        ->assertVisible('#fav-1')\n        ->assertVisible('#fav-2')\n        ->assertVisible('#fav-3')\n        ->assertVisible('#status-1')\n        ->assertVisible('#status-2')\n        ->assertVisible('#status-3');\n});\n\nit('renders each favorite button with unfavorited state', function () {\n    $this->visit(BASE_URL.'/product-list')\n        ->assertSeeIn('#fav-1', '☆')\n        ->assertSeeIn('#fav-2', '☆')\n        ->assertSeeIn('#fav-3', '☆');\n});\n\nit('renders each status dropdown with correct initial status', function () {\n    $this->visit(BASE_URL.'/product-list')\n        ->assertSeeIn('#status-1 [data-status]', 'Active')\n        ->assertSeeIn('#status-2 [data-status]', 'Draft')\n        ->assertSeeIn('#status-3 [data-status]', 'Archived');\n});\n\nit('toggles favorite on one item without affecting others', function () {\n    $page = $this->visit(BASE_URL.'/product-list')\n        ->assertSeeIn('#fav-1', '☆')\n        ->assertSeeIn('#fav-2', '☆');\n\n    $page->click('#fav-1 [data-action=\"toggle\"]')\n        ->assertSeeIn('#fav-1', '★')\n        ->assertSeeIn('#fav-2', '☆');\n});\n\nit('toggles favorite back to unfavorited', function () {\n    $page = $this->visit(BASE_URL.'/product-list')\n        ->assertSeeIn('#fav-2', '☆');\n\n    $page->click('#fav-2 [data-action=\"toggle\"]')\n        ->assertSeeIn('#fav-2', '★')\n        ->wait(0.3);\n\n    $page->click('#fav-2 [data-action=\"toggle\"]')\n        ->assertSeeIn('#fav-2', '☆');\n});\n\nit('can favorite multiple items independently', function () {\n    $page = $this->visit(BASE_URL.'/product-list')\n        ->assertSeeIn('#fav-1', '☆');\n\n    $page->click('#fav-1 [data-action=\"toggle\"]')\n        ->assertSeeIn('#fav-1', '★')\n        ->wait(0.3);\n\n    $page->click('#fav-3 [data-action=\"toggle\"]')\n        ->assertSeeIn('#fav-3', '★');\n\n    $page->assertSeeIn('#fav-2', '☆');\n});\n\nit('opens status dropdown menu', function () {\n    $page = $this->visit(BASE_URL.'/product-list');\n\n    $page->assertNotPresent('#status-1 [data-menu]');\n\n    $page->click('#status-1 [data-action=\"toggle-menu\"]')\n        ->assertVisible('#status-1 [data-menu]')\n        ->assertSeeIn('#status-1 [data-option=\"active\"]', 'Active')\n        ->assertSeeIn('#status-1 [data-option=\"draft\"]', 'Draft')\n        ->assertSeeIn('#status-1 [data-option=\"archived\"]', 'Archived');\n});\n\nit('changes status via dropdown without affecting other items', function () {\n    $page = $this->visit(BASE_URL.'/product-list');\n\n    $page->click('#status-1 [data-action=\"toggle-menu\"]')\n        ->assertVisible('#status-1 [data-menu]')\n        ->wait(0.3);\n\n    $page->click('#status-1 [data-option=\"draft\"]')\n        ->assertSeeIn('#status-1 [data-status]', 'Draft')\n        ->assertNotPresent('#status-1 [data-menu]')\n        ->assertSeeIn('#status-2 [data-status]', 'Draft')\n        ->assertSeeIn('#status-3 [data-status]', 'Archived');\n});\n\nit('each dropdown operates independently', function () {\n    $page = $this->visit(BASE_URL.'/product-list');\n\n    $page->click('#status-3 [data-action=\"toggle-menu\"]')\n        ->assertVisible('#status-3 [data-menu]')\n        ->wait(0.3);\n\n    $page->click('#status-3 [data-option=\"active\"]')\n        ->assertSeeIn('#status-3 [data-status]', 'Active');\n\n    $page->assertSeeIn('#status-1 [data-status]', 'Active')\n        ->assertSeeIn('#status-2 [data-status]', 'Draft');\n});\n\nit('each component has unique yoyo IDs in the DOM', function () {\n    $this->visit(BASE_URL.'/product-list')\n        ->assertSourceHas('id=\"fav-1\"')\n        ->assertSourceHas('id=\"fav-2\"')\n        ->assertSourceHas('id=\"fav-3\"')\n        ->assertSourceHas('id=\"status-1\"')\n        ->assertSourceHas('id=\"status-2\"')\n        ->assertSourceHas('id=\"status-3\"')\n        ->assertSourceHas('yoyo:name=\"favorite-button\"')\n        ->assertSourceHas('yoyo:name=\"status-dropdown\"');\n});\n"
  },
  {
    "path": "tests/Browser/ResponseHeadersTest.php",
    "content": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders response headers test page', function () {\n    $this->visit(BASE_URL.'/response-headers')\n        ->assertVisible('#response-headers-test')\n        ->assertSeeIn('#rh-message', 'initial')\n        ->assertSeeIn('#retarget-content', 'original');\n});\n\nit('retargets response to a different element via HX-Retarget', function () {\n    // HX-Retarget with outerHTML swap replaces #retarget-receiver with the component response\n    $this->visit(BASE_URL.'/response-headers')\n        ->assertSeeIn('#retarget-content', 'original')\n        ->click('#btn-retarget')\n        ->assertSee('retargeted content');\n});\n\nit('changes swap strategy to innerHTML via HX-Reswap', function () {\n    // Default swap is outerHTML — the component root is replaced entirely.\n    // With HX-Reswap: innerHTML, the component root stays and content goes inside it.\n    $this->visit(BASE_URL.'/response-headers')\n        ->click('#btn-reswap')\n        ->assertSeeIn('#response-headers', 'inner-swapped');\n});\n\nit('triggers client-side event via HX-Trigger', function () {\n    $this->visit(BASE_URL.'/response-headers')\n        ->click('#btn-trigger')\n        ->assertSeeIn('#event-log', 'custom-event,');\n});\n\nit('triggers client-side event after settle via HX-Trigger-After-Settle', function () {\n    $this->visit(BASE_URL.'/response-headers')\n        ->click('#btn-trigger-settle')\n        ->assertSeeIn('#event-log', 'settle-event,');\n});\n\nit('executes pushUrl action and re-renders component', function () {\n    // URL push is not assertable in browser tests because historyEnabled is false\n    // in the test server config. Feature tests verify the HX-Push-Url header is set.\n    $this->visit(BASE_URL.'/response-headers')\n        ->click('#btn-push-url')\n        ->assertSeeIn('#rh-message', 'url-pushed');\n});\n\nit('executes replaceUrl action and re-renders component', function () {\n    // Same as pushUrl — HX-Replace-Url header verified in feature tests.\n    $this->visit(BASE_URL.'/response-headers')\n        ->click('#btn-replace-url')\n        ->assertSeeIn('#rh-message', 'url-replaced');\n});\n"
  },
  {
    "path": "tests/Browser/SkipRenderTest.php",
    "content": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders deleteable items', function () {\n    $this->visit(BASE_URL.'/skip-render')\n        ->assertVisible('#skip-render-test')\n        ->assertVisible('#item-1')\n        ->assertVisible('#item-2')\n        ->assertSeeIn('#item-1 [data-title]', 'First Item')\n        ->assertSeeIn('#item-2 [data-title]', 'Second Item');\n});\n\nit('keeps component in DOM after skipRender (204)', function () {\n    $page = $this->visit(BASE_URL.'/skip-render')\n        ->assertSeeIn('#item-1 [data-title]', 'First Item');\n\n    // Click delete — should return 204, no swap\n    $page->click('#item-1 [data-action=\"delete\"]')\n        ->waitForEvent('networkidle');\n\n    // Component should still be in the DOM (204 = no swap)\n    $page->assertVisible('#item-1')\n        ->assertSeeIn('#item-1 [data-title]', 'First Item');\n});\n\nit('does not affect other components on 204', function () {\n    $page = $this->visit(BASE_URL.'/skip-render')\n        ->assertSeeIn('#item-2 [data-title]', 'Second Item');\n\n    $page->click('#item-1 [data-action=\"delete\"]')\n        ->waitForEvent('networkidle');\n\n    $page->assertVisible('#item-2')\n        ->assertSeeIn('#item-2 [data-title]', 'Second Item');\n});\n"
  },
  {
    "path": "tests/Browser/TodoListTest.php",
    "content": "<?php\n\nrequire __DIR__.'/bootstrap.php';\n\nit('renders with default entries', function () {\n    $this->visit(BASE_URL.'/todo-list')\n        ->assertVisible('#todo-list')\n        ->assertVisible('[data-entries]')\n        ->assertSee('Build a framework')\n        ->assertSee('Buy groceries')\n        ->assertSee('Write tests');\n});\n\nit('shows active item count', function () {\n    $this->visit(BASE_URL.'/todo-list')\n        ->assertVisible('[data-active-count]')\n        ->assertSeeIn('[data-active-count]', 'items left');\n});\n\nit('adds a new todo item', function () {\n    $this->visit(BASE_URL.'/todo-list')\n        ->fill('input[name=\"task\"]', 'New browser test task')\n        ->keys('input[name=\"task\"]', ['Enter'])\n        ->assertSee('New browser test task');\n});\n\nit('toggles todo completion', function () {\n    $this->visit(BASE_URL.'/todo-list')\n        ->click('[data-todo-id=\"1\"] input[type=\"checkbox\"]')\n        ->assertChecked('[data-todo-id=\"1\"] input[type=\"checkbox\"]');\n});\n\nit('deletes a todo item', function () {\n    $this->visit(BASE_URL.'/todo-list')\n        ->assertSee('Buy groceries')\n        ->click('[data-todo-id=\"2\"] [data-delete]')\n        ->assertDontSee('Buy groceries');\n});\n"
  },
  {
    "path": "tests/Browser/bootstrap.php",
    "content": "<?php\n\n// Shared bootstrap for browser tests.\n// Each test file requires this to start the server and define BASE_URL.\n\nuse Tests\\Browser\\BrowserServer;\n\nBrowserServer::ensureRunning();\n\nif (! defined('BASE_URL')) {\n    define('BASE_URL', BrowserServer::url());\n}\n"
  },
  {
    "path": "tests/Browser/server/index.php",
    "content": "<?php\n\n// Minimal Yoyo test server for isolated component browser tests.\n// Usage: php -S localhost:8765 tests/Browser/server/index.php\n\nrequire __DIR__.'/../../../vendor/autoload.php';\n\nuse Clickfwd\\Yoyo\\View;\nuse Clickfwd\\Yoyo\\ViewProviders\\YoyoViewProvider;\nuse Clickfwd\\Yoyo\\Yoyo;\n\n$yoyo = new Yoyo();\n\n$yoyo->configure([\n    'url' => '/yoyo',\n    'namespace' => 'Tests\\\\Browser\\\\Components\\\\',\n]);\n\n$yoyo->registerViewProvider(function () {\n    return new YoyoViewProvider(new View(__DIR__.'/views'));\n});\n\n$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);\n\n// Yoyo AJAX endpoint\nif ($uri === '/yoyo') {\n    echo $yoyo->update();\n    exit;\n}\n\n// Serve yoyo.js from src\nif ($uri === '/yoyo.js' || $uri === '/assets/js/yoyo.js') {\n    header('Content-Type: application/javascript');\n    readfile(__DIR__.'/../../../src/assets/js/yoyo.js');\n    exit;\n}\n\n// Component isolation pages\n$page = ltrim($uri, '/') ?: 'index';\n\n$pagePath = __DIR__.'/pages/'.$page.'.php';\nif (file_exists($pagePath)) {\n    ob_start();\n    include $pagePath;\n    $content = ob_get_clean();\n    echo $content;\n    exit;\n}\n\nhttp_response_code(404);\necho '404 Not Found';\n"
  },
  {
    "path": "tests/Browser/server/layout.php",
    "content": "<?php\n\n/**\n * Minimal layout for browser test pages.\n * Includes Yoyo scripts/styles and renders a single component in isolation.\n */\nfunction render_page(string $title, string $componentHtml): void\n{\n    ?>\n<!DOCTYPE html>\n<html>\n<head>\n    <meta charset=\"UTF-8\">\n    <title><?php echo $title; ?></title>\n    <?php Yoyo\\yoyo_styles(); ?>\n    <?php Yoyo\\yoyo_scripts(); ?>\n</head>\n<body>\n    <div id=\"app\">\n        <?php echo $componentHtml; ?>\n    </div>\n</body>\n</html>\n    <?php\n}\n"
  },
  {
    "path": "tests/Browser/server/pages/counter.php",
    "content": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nrender_page('Counter', Yoyo\\yoyo_render('counter'));\n"
  },
  {
    "path": "tests/Browser/server/pages/dispatch.php",
    "content": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nob_start();\n?>\n<div id=\"dispatch-test\">\n    <div data-listener-area>\n        <?php echo Yoyo\\yoyo_render('dispatch-listener', [], ['id' => 'dispatch-listener']); ?>\n    </div>\n    <div data-bystander-area>\n        <?php echo Yoyo\\yoyo_render('dispatch-bystander', [\n            'label' => 'unchanged',\n        ], ['id' => 'dispatch-bystander']); ?>\n    </div>\n    <div data-buttons>\n        <button id=\"btn-dispatch\" onclick=\"Yoyo.dispatch('post-created', { postId: 42 })\">Dispatch</button>\n        <button id=\"btn-dispatch-to\" onclick=\"Yoyo.dispatchTo('dispatch-listener', 'post-created', { postId: 7 })\">DispatchTo</button>\n        <button id=\"btn-dispatch-multi\" onclick=\"Yoyo.dispatch('status-changed', { status: 'active', reason: 'manual' })\">Multi Params</button>\n        <button id=\"btn-dispatch-no-params\" onclick=\"Yoyo.dispatch('simple-refresh')\">No Params</button>\n        <button id=\"btn-dispatch-nonexistent\" onclick=\"Yoyo.dispatchTo('nonexistent', 'post-created', { postId: 1 })\">Nonexistent</button>\n    </div>\n</div>\n<?php\n\nrender_page('Dispatch', ob_get_clean());\n"
  },
  {
    "path": "tests/Browser/server/pages/events.php",
    "content": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nob_start();\n?>\n<div id=\"events-test\">\n    <div data-badge-area>\n        <?php echo Yoyo\\yoyo_render('notification-badge', [\n            'count' => 0,\n        ], ['id' => 'badge']); ?>\n    </div>\n    <div data-buttons>\n        <?php echo Yoyo\\yoyo_render('action-button', [\n            'label' => 'Send Email',\n        ], ['id' => 'btn-email']); ?>\n        <?php echo Yoyo\\yoyo_render('action-button', [\n            'label' => 'Add Item',\n        ], ['id' => 'btn-add']); ?>\n    </div>\n</div>\n<?php\n\nrender_page('Events', ob_get_clean());\n"
  },
  {
    "path": "tests/Browser/server/pages/form.php",
    "content": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nrender_page('Form', Yoyo\\yoyo_render('form'));\n"
  },
  {
    "path": "tests/Browser/server/pages/index.php",
    "content": "<?php\n\nrequire __DIR__.'/../layout.php';\n\n$links = [\n    'counter' => 'Counter',\n    'live-search' => 'Live Search',\n    'form' => 'Form',\n    'todo-list' => 'Todo List',\n    'pagination' => 'Pagination',\n];\n\n$html = '<h1>Yoyo Browser Test Components</h1><ul>';\nforeach ($links as $path => $label) {\n    $html .= \"<li><a href=\\\"/$path\\\">$label</a></li>\";\n}\n$html .= '</ul>';\n\nrender_page('Yoyo Browser Tests', $html);\n"
  },
  {
    "path": "tests/Browser/server/pages/live-search.php",
    "content": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nrender_page('Live Search', Yoyo\\yoyo_render('live-search'));\n"
  },
  {
    "path": "tests/Browser/server/pages/modal.php",
    "content": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nob_start();\n?>\n<div id=\"modal-test\">\n    <?php echo Yoyo\\yoyo_render('modal-trigger', [], ['id' => 'modal-trigger']); ?>\n</div>\n<?php\n\nrender_page('Modal', ob_get_clean());\n"
  },
  {
    "path": "tests/Browser/server/pages/multi-screen.php",
    "content": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nob_start();\n?>\n<div id=\"multi-screen-test\">\n    <?php echo Yoyo\\yoyo_render('multi-screen', [], ['id' => 'wizard']); ?>\n</div>\n<?php\n\nrender_page('Multi Screen', ob_get_clean());\n"
  },
  {
    "path": "tests/Browser/server/pages/null-prop.php",
    "content": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nrender_page('Null Prop', Yoyo\\yoyo_render('null-prop'));\n"
  },
  {
    "path": "tests/Browser/server/pages/pagination.php",
    "content": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nrender_page('Pagination', Yoyo\\yoyo_render('pagination'));\n"
  },
  {
    "path": "tests/Browser/server/pages/product-list.php",
    "content": "<?php\n\nrequire __DIR__.'/../layout.php';\n\n// Simulated product data — each row gets its own Yoyo components\n$products = [\n    ['id' => 1, 'name' => 'Widget Alpha', 'status' => 'active'],\n    ['id' => 2, 'name' => 'Widget Beta',  'status' => 'draft'],\n    ['id' => 3, 'name' => 'Widget Gamma', 'status' => 'archived'],\n];\n\nob_start();\n?>\n<table id=\"product-list\">\n    <thead><tr><th>Name</th><th>Favorite</th><th>Status</th></tr></thead>\n    <tbody>\n    <?php foreach ($products as $product): ?>\n        <tr data-product=\"<?php echo $product['id']; ?>\">\n            <td><?php echo $product['name']; ?></td>\n            <td>\n                <?php echo Yoyo\\yoyo_render('favorite-button', [\n                    'itemId' => $product['id'],\n                    'isFavorited' => 0,\n                ], ['id' => 'fav-'.$product['id']]); ?>\n            </td>\n            <td>\n                <?php echo Yoyo\\yoyo_render('status-dropdown', [\n                    'itemId' => $product['id'],\n                    'status' => $product['status'],\n                ], ['id' => 'status-'.$product['id']]); ?>\n            </td>\n        </tr>\n    <?php endforeach; ?>\n    </tbody>\n</table>\n<?php\n\nrender_page('Product List', ob_get_clean());\n"
  },
  {
    "path": "tests/Browser/server/pages/response-headers.php",
    "content": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nob_start();\n?>\n<div id=\"response-headers-test\">\n    <div data-component-area>\n        <?php echo Yoyo\\yoyo_render('response-headers', [], ['id' => 'response-headers']); ?>\n    </div>\n    <div id=\"retarget-receiver\">\n        <span id=\"retarget-content\">original</span>\n    </div>\n    <div id=\"event-log\"></div>\n    <script>\n        // Listen for HX-Trigger events and log them to #event-log\n        document.body.addEventListener('custom-event', function() {\n            document.getElementById('event-log').textContent += 'custom-event,';\n        });\n        document.body.addEventListener('settle-event', function() {\n            document.getElementById('event-log').textContent += 'settle-event,';\n        });\n    </script>\n</div>\n<?php\n\nrender_page('Response Headers', ob_get_clean());\n"
  },
  {
    "path": "tests/Browser/server/pages/skip-render.php",
    "content": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nob_start();\n?>\n<div id=\"skip-render-test\">\n    <div data-items>\n        <?php echo Yoyo\\yoyo_render('delete-item', [\n            'itemId' => 1,\n            'title' => 'First Item',\n        ], ['id' => 'item-1']); ?>\n        <?php echo Yoyo\\yoyo_render('delete-item', [\n            'itemId' => 2,\n            'title' => 'Second Item',\n        ], ['id' => 'item-2']); ?>\n    </div>\n</div>\n<?php\n\nrender_page('Skip Render', ob_get_clean());\n"
  },
  {
    "path": "tests/Browser/server/pages/todo-list.php",
    "content": "<?php\n\nrequire __DIR__.'/../layout.php';\n\nrender_page('Todo List', Yoyo\\yoyo_render('todo-list'));\n"
  },
  {
    "path": "tests/Browser/server/views/action-button.php",
    "content": "<div data-component=\"action-button\">\n    <button data-action=\"fire\" yoyo:get=\"fire\"><?php echo htmlspecialchars($label); ?></button>\n</div>\n"
  },
  {
    "path": "tests/Browser/server/views/counter.php",
    "content": "<div id=\"counter\">\n    <button data-action=\"decrement\" yoyo:get=\"decrement\">-</button>\n    <span data-count><?php echo $count; ?></span>\n    <button data-action=\"increment\" yoyo:get=\"increment\">+</button>\n</div>\n"
  },
  {
    "path": "tests/Browser/server/views/delete-item.php",
    "content": "<div data-component=\"delete-item\" data-item=\"<?php echo $itemId; ?>\">\n    <span data-title><?php echo htmlspecialchars($title); ?></span>\n    <button data-action=\"delete\" yoyo:get=\"delete\">Delete</button>\n</div>\n"
  },
  {
    "path": "tests/Browser/server/views/dispatch-bystander.php",
    "content": "<div id=\"dispatch-bystander\">\n    <span id=\"bystander\"><?php echo $label; ?></span>\n</div>\n"
  },
  {
    "path": "tests/Browser/server/views/dispatch-listener.php",
    "content": "<div id=\"dispatch-listener\">\n    <span id=\"message\"><?php echo $message; ?></span>\n</div>\n"
  },
  {
    "path": "tests/Browser/server/views/favorite-button.php",
    "content": "<div data-component=\"favorite\" data-item=\"<?php echo $itemId; ?>\">\n    <button\n        data-action=\"toggle\"\n        data-favorited=\"<?php echo $isFavorited; ?>\"\n        yoyo:get=\"toggle\"\n    ><?php echo $isFavorited ? '★' : '☆'; ?></button>\n</div>\n"
  },
  {
    "path": "tests/Browser/server/views/form.php",
    "content": "<?php if ($success): ?>\n    <div id=\"form\" data-success>\n        <p>Thank you for registering!</p>\n    </div>\n<?php else: ?>\n    <form id=\"form\" yoyo:post=\"register\" yoyo:on=\"submit\">\n        <div>\n            <label for=\"name\">Name</label>\n            <input id=\"name\" name=\"name\" type=\"text\"\n                value=\"<?php echo htmlspecialchars($name ?? ''); ?>\" />\n            <?php if (! empty($errors['name'])): ?>\n                <span data-error=\"name\"><?php echo $errors['name']; ?></span>\n            <?php endif; ?>\n        </div>\n\n        <div>\n            <label for=\"email\">Email</label>\n            <input id=\"email\" name=\"email\" type=\"email\"\n                value=\"<?php echo htmlspecialchars($email ?? ''); ?>\" />\n            <?php if (! empty($errors['email'])): ?>\n                <span data-error=\"email\"><?php echo $errors['email']; ?></span>\n            <?php endif; ?>\n        </div>\n\n        <button type=\"submit\">Submit</button>\n    </form>\n<?php endif; ?>\n"
  },
  {
    "path": "tests/Browser/server/views/live-search.php",
    "content": "<div id=\"live-search\" yoyo:trigger=\"input delay:300ms from:input[name='q']\">\n    <input type=\"text\" name=\"q\" value=\"<?php echo htmlspecialchars($q ?? ''); ?>\"\n        placeholder=\"Search...\" />\n\n    <?php if ($this->results): ?>\n        <ul data-results>\n            <?php foreach ($this->results as $row): ?>\n                <li><?php echo htmlspecialchars($row['title']); ?></li>\n            <?php endforeach; ?>\n        </ul>\n    <?php elseif ($q): ?>\n        <p data-no-results>No results found</p>\n    <?php endif; ?>\n</div>\n"
  },
  {
    "path": "tests/Browser/server/views/modal-trigger.php",
    "content": "<div data-component=\"modal-trigger\">\n    <button data-action=\"open\" yoyo:get=\"openModal\">Open Modal</button>\n<?php if ($isOpen): ?>\n    <dialog data-modal open>\n        <h2 data-modal-title>Modal Content</h2>\n        <p>This is the modal body.</p>\n        <button data-action=\"close\" yoyo:get=\"closeModal\">Close</button>\n    </dialog>\n<?php endif; ?>\n</div>\n"
  },
  {
    "path": "tests/Browser/server/views/multi-screen.php",
    "content": "<div data-component=\"multi-screen\">\n<?php if ($this->actionMatches(['render', 'closeModal'])): ?>\n    <div data-screen=\"initial\">\n        <p data-info>Ready to begin</p>\n        <button data-action=\"open\" yoyo:get=\"open\">Open Form</button>\n    </div>\n<?php elseif ($this->actionMatches('open')): ?>\n    <div data-screen=\"form\">\n        <input type=\"text\" name=\"message\" value=\"<?php echo htmlspecialchars($message); ?>\" placeholder=\"Enter message\" />\n        <button data-action=\"submit\" yoyo:post=\"submit\">Submit</button>\n        <button data-action=\"cancel\" yoyo:get=\"render\">Cancel</button>\n    </div>\n<?php elseif ($this->actionMatches('submit')): ?>\n    <div data-screen=\"success\">\n        <p data-result>Submitted: <?php echo htmlspecialchars($message); ?></p>\n        <button data-action=\"reset\" yoyo:get=\"render\">Start Over</button>\n    </div>\n<?php endif; ?>\n</div>\n"
  },
  {
    "path": "tests/Browser/server/views/notification-badge.php",
    "content": "<div data-component=\"badge\">\n    <span data-count><?php echo $count; ?></span>\n</div>\n"
  },
  {
    "path": "tests/Browser/server/views/null-prop.php",
    "content": "<div id=\"null-prop\">\n    <span data-icon-slot-type=\"<?php echo gettype($iconSlot); ?>\"></span>\n    <span data-icon-slot-display><?php echo $iconSlot === null ? 'NULL_OK' : 'GOT:'.var_export($iconSlot, true); ?></span>\n    <span data-enabled-type=\"<?php echo gettype($enabled); ?>\"></span>\n    <span data-enabled-display><?php echo $enabled === false ? 'FALSE_OK' : 'GOT:'.var_export($enabled, true); ?></span>\n    <span data-clicks><?php echo $clicks; ?></span>\n    <button\n        data-action=\"increment\"\n        yoyo:get=\"increment\"\n        yoyo:val.icon-slot=\"null\"\n        yoyo:val.enabled=\"false\"\n    >+</button>\n</div>\n"
  },
  {
    "path": "tests/Browser/server/views/pagination.php",
    "content": "<div id=\"pagination\">\n    <?php if ($this->results): ?>\n        <ul data-results>\n            <?php foreach ($this->results as $item): ?>\n                <li><?php echo htmlspecialchars($item['title']); ?></li>\n            <?php endforeach; ?>\n        </ul>\n\n        <div data-page-info>\n            Showing <?php echo $this->start; ?> to <?php echo $this->end; ?>\n            of <?php echo 12; ?> results\n        </div>\n\n        <nav data-nav>\n            <?php for ($i = 1; $i <= $this->totalPages; $i++): ?>\n                <a href=\"?page=<?php echo $i; ?>\"\n                    yoyo:get=\"render\"\n                    yoyo:val.page=\"<?php echo $i; ?>\"\n                    data-page=\"<?php echo $i; ?>\"\n                    class=\"<?php echo $page == $i ? 'active' : ''; ?>\">\n                    <?php echo $i; ?>\n                </a>\n            <?php endfor; ?>\n        </nav>\n    <?php endif; ?>\n</div>\n"
  },
  {
    "path": "tests/Browser/server/views/response-headers.php",
    "content": "<div id=\"response-headers\">\n    <span id=\"rh-message\"><?php echo $message; ?></span>\n    <button id=\"btn-retarget\" yoyo:get=\"doRetarget\">Retarget</button>\n    <button id=\"btn-reswap\" yoyo:get=\"doReswap\">Reswap</button>\n    <button id=\"btn-trigger\" yoyo:get=\"doTrigger\">Trigger</button>\n    <button id=\"btn-trigger-settle\" yoyo:get=\"doTriggerAfterSettle\">TriggerAfterSettle</button>\n    <button id=\"btn-push-url\" yoyo:get=\"doPushUrl\">PushUrl</button>\n    <button id=\"btn-replace-url\" yoyo:get=\"doReplaceUrl\">ReplaceUrl</button>\n</div>\n"
  },
  {
    "path": "tests/Browser/server/views/status-dropdown.php",
    "content": "<div data-component=\"status\" data-item=\"<?php echo $itemId; ?>\">\n    <button\n        data-action=\"toggle-menu\"\n        data-status=\"<?php echo $status; ?>\"\n        yoyo:get=\"toggleMenu\"\n    ><?php echo ucfirst($status); ?> ▾</button>\n\n    <?php if ($isOpen): ?>\n    <ul data-menu>\n        <?php foreach (['draft', 'active', 'archived'] as $option): ?>\n        <li>\n            <button\n                data-option=\"<?php echo $option; ?>\"\n                yoyo:get=\"setStatus\"\n                yoyo:val.new-status=\"<?php echo $option; ?>\"\n                <?php echo $option === $status ? 'data-selected' : ''; ?>\n            ><?php echo ucfirst($option); ?></button>\n        </li>\n        <?php endforeach; ?>\n    </ul>\n    <?php endif; ?>\n</div>\n"
  },
  {
    "path": "tests/Browser/server/views/todo-list.php",
    "content": "<div id=\"todo-list\">\n    <div>\n        <input type=\"text\" name=\"task\" placeholder=\"What needs to be done?\"\n            yoyo:on=\"keydown[key=='Enter']\" yoyo:post=\"add\" />\n    </div>\n\n    <?php if ($this->entries): ?>\n        <ul data-entries>\n            <?php foreach ($this->entries as $entry): ?>\n                <li data-todo-id=\"<?php echo $entry['id']; ?>\">\n                    <input type=\"checkbox\" yoyo:get=\"toggle\" yoyo:val.id=\"<?php echo $entry['id']; ?>\"\n                        <?php echo $entry['completed'] ? 'checked' : ''; ?> />\n                    <span class=\"<?php echo $entry['completed'] ? 'completed' : ''; ?>\">\n                        <?php echo htmlspecialchars($entry['title']); ?>\n                    </span>\n                    <button yoyo:get=\"delete\" yoyo:val.id=\"<?php echo $entry['id']; ?>\" data-delete>x</button>\n                </li>\n            <?php endforeach; ?>\n        </ul>\n\n        <footer>\n            <span data-active-count><?php echo $this->activeCount; ?> items left</span>\n            <div>\n                <button yoyo:get=\"render\" yoyo:val.filter=\"\">All</button>\n                <button yoyo:get=\"render\" yoyo:val.filter=\"active\">Active</button>\n                <button yoyo:get=\"render\" yoyo:val.filter=\"completed\">Completed</button>\n            </div>\n        </footer>\n    <?php endif; ?>\n</div>\n"
  },
  {
    "path": "tests/Feature/AnonymousComponentTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentNotFound;\n\nuse function Tests\\render;\nuse function Tests\\update;\nuse function Tests\\yoyo_view;\n\nbeforeAll(function () {\n    yoyo_view();\n});\n\nit('throws exception when template not found', function () {\n    render('random');\n})->throws(ComponentNotFound::class);\n\nit('renders anonymous component', function () {\n    expect(render('foo'))->toContain('default foo');\n});\n\nit('updates anonymous component', function () {\n    expect(update('foo'))->toContain('default bar');\n});\n\nit('loads anonymous component with a registered alias', function () {\n    \\Clickfwd\\Yoyo\\Yoyo::registerComponent('awesome', 'registered-anon');\n    expect(render('awesome'))->toContain('id=\"registered-anon\"');\n});\n\nit('renders anonymous component in sub-directory', function () {\n    expect(render('account.login'))->toContain('app/resources/views/yoyo/account/login.php');\n});\n"
  },
  {
    "path": "tests/Feature/BladeTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Yoyo;\n\nuse function Tests\\htmlformat;\nuse function Tests\\render;\nuse function Tests\\response;\nuse function Tests\\update;\nuse function Tests\\yoyo_blade;\n\nbeforeAll(function () {\n    yoyo_blade();\n});\n\nit('renders anonymous component', function () {\n    expect(render('foo'))->toContain('blade foo');\n});\n\nit('updates anonymous component', function () {\n    expect(update('foo'))->toContain('blade bar');\n});\n\nit('renders anonymous component form different location', function () {\n    $view = Yoyo::getInstance()->getViewProvider();\n    $view->getFinder()->flush();\n    $view->prependLocation(__DIR__.'/../app-another/views');\n    expect(render('foo'))->toContain('blade foo from another app');\n});\n\nit('renders anonymous component using a view namespace', function () {\n    $view = Yoyo::getInstance()->getViewProvider();\n    $view->getFinder()->flush();\n    $view->addNamespace('packagename', __DIR__.'/../app-another/views');\n    expect(render('packagename::foo'))->toContain('blade foo from another app');\n});\n\nit('renders dynamic component using a view and class namespace', function () {\n    $view = Yoyo::getInstance()->getViewProvider();\n    $view->getFinder()->flush();\n    $view->addNamespace('packagename', __DIR__.'/../app-another/views');\n    Yoyo::getInstance()->componentNamespace('packagename', 'Tests\\\\AppAnother\\\\Yoyo');\n    expect(render('packagename::counter', ['count' => 3]))->toContain('The count is now 3');\n});\n\nit('can render nested components with @yoyo directive', function () {\n    $output = render('parent', ['data' => [1, 2, 3]], ['id' => 'parent']);\n    expect(htmlformat($output))->toEqual(response('nested.blade'));\n});\n\nit('renders anonymous component in subdirectory', function () {\n    expect(render('account.login'))->toContain('blade:app/resources/views/yoyo/account/login.php');\n});\n\nit('renders dynamic component in subdirectory', function () {\n    expect(render('account.register'))->toContain('blade:Please register to access this page');\n});\n\nit('renders blade template within Yoyo', function () {\n    $view = Yoyo::getInstance()->getViewProvider();\n    $view->getFinder()->flush();\n    $view->addLocation(__DIR__.'/../app/resources/views');\n    expect(render('layout-a'))->toContain('app/resources/views/components/select.blade.php');\n});\n\nit('renders namespaced blade template within Yoyo', function () {\n    $view = Yoyo::getInstance()->getViewProvider();\n    $view->getFinder()->flush();\n    $view->addLocation(__DIR__.'/../app/resources/views');\n    $view->addNamespace('packagename', __DIR__.'/../app-another/views');\n    expect(render('layout-b'))->toContain('app-another/views/components/input.blade.php');\n});\n"
  },
  {
    "path": "tests/Feature/ComponentLifecycleTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Exceptions\\NonPublicComponentMethodCall;\nuse Clickfwd\\Yoyo\\Services\\BrowserEventsService;\nuse Clickfwd\\Yoyo\\Services\\PageRedirectService;\nuse Clickfwd\\Yoyo\\Services\\Response;\n\nuse function Tests\\headers;\nuse function Tests\\mockYoyoGetRequest;\nuse function Tests\\render;\nuse function Tests\\resetYoyoRequest;\nuse function Tests\\update;\nuse function Tests\\yoyo_update;\nuse function Tests\\yoyo_view;\n\nuses()->group('component-lifecycle');\n\nbeforeAll(function () {\n    yoyo_view();\n});\n\nbeforeEach(function () {\n    // Reset singleton services to prevent cross-test pollution\n    foreach ([BrowserEventsService::class, PageRedirectService::class, Response::class] as $class) {\n        $ref = new ReflectionClass($class);\n        $prop = $ref->getProperty('instance');\n        $prop->setAccessible(true);\n        $prop->setValue(null, null);\n    }\n});\n\n// --- Listeners ---\n\nit('renders component with listeners in trigger attribute', function () {\n    $output = render('component-with-listeners');\n    expect($output)\n        ->toContain('itemAdded')\n        ->toContain('id=\"component-with-listeners\"');\n});\n\nit('includes mapped listener events in trigger', function () {\n    $output = render('component-with-listeners');\n    // Both itemAdded and refresh should be in the trigger attribute\n    expect($output)->toContain('itemAdded');\n});\n\n// --- Computed properties with arguments ---\n\nit('renders computed property with arguments', function () {\n    $output = render('component-with-computed-args');\n    expect($output)\n        ->toContain('Hello, Alice!')\n        ->toContain('Hello, Bob!');\n});\n\n// --- Redirect ---\n\nit('sets redirect property via redirect method', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-redirect/save', 'component-with-redirect');\n\n    $output = yoyo_update();\n\n    $responseHeaders = headers();\n\n    resetYoyoRequest();\n\n    expect($responseHeaders)->toHaveKey('Yoyo-Redirect', '/success');\n});\n\n// --- Emit and browser events ---\n\nit('emits events via component action', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-emit/doEmit', 'component-with-emit');\n\n    $output = yoyo_update();\n\n    $responseHeaders = headers();\n\n    resetYoyoRequest();\n\n    $events = json_decode($responseHeaders['Yoyo-Emit'], true);\n    expect($events)->toBeArray();\n    expect($events[0]['event'])->toBe('testEvent');\n    // Params are wrapped in an array due to variadic forwarding in BrowserEvents trait\n    expect($events[0]['params'])->toBeArray();\n    expect($events[0]['params'][0])->toMatchArray(['key' => 'value']);\n});\n\nit('emits targeted events via emitTo', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-emit/doEmitTo', 'component-with-emit');\n\n    $output = yoyo_update();\n\n    $responseHeaders = headers();\n\n    resetYoyoRequest();\n\n    $events = json_decode($responseHeaders['Yoyo-Emit'], true);\n    expect($events)->toBeArray();\n    expect($events[0])->toMatchArray([\n        'event' => 'targetEvent',\n        'component' => 'other-component',\n    ]);\n});\n\nit('dispatches browser events', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-emit/doBrowserEvent', 'component-with-emit');\n\n    $output = yoyo_update();\n\n    $responseHeaders = headers();\n\n    resetYoyoRequest();\n\n    $browserEvents = json_decode($responseHeaders['Yoyo-Browser-Event'], true);\n    expect($browserEvents)->toBeArray();\n    expect($browserEvents[0])->toMatchArray([\n        'event' => 'notification',\n        'params' => ['message' => 'done'],\n    ]);\n});\n\n// --- Swap modifiers ---\n\nit('adds swap modifier headers via component action', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-swap-modifiers/doSwap', 'component-with-swap-modifiers');\n\n    $output = yoyo_update();\n\n    $responseHeaders = headers();\n\n    resetYoyoRequest();\n\n    expect($responseHeaders)->toHaveKey('Yoyo-Swap-Modifier', 'transition:true swap:500ms');\n});\n\n// --- Counter state management ---\n\nit('increments counter and emits event', function () {\n    $output = update('counter', 'increment');\n    expect($output)->toContain('The count is now 1');\n});\n\nit('renders counter with custom initial value', function () {\n    $output = render('counter', ['count' => 10]);\n    expect($output)->toContain('The count is now 10');\n});\n\n// --- Protected method blocking ---\n\nit('prevents calling boot method directly', function () {\n    update('counter', 'boot');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('prevents calling getName method as action', function () {\n    update('counter', 'getName');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('prevents calling getComponentId method as action', function () {\n    update('counter', 'getComponentId');\n})->throws(NonPublicComponentMethodCall::class);\n\n// --- Component set() method ---\n\nit('passes view data set via set() method', function () {\n    $output = render('set-view-data');\n    expect($output)->toContain('bar-baz');\n});\n\n// --- Sub-directory components ---\n\nit('resolves component class in sub-directory via dot notation', function () {\n    $output = render('account.register');\n    expect($output)->toContain('Please register to access this page');\n});\n"
  },
  {
    "path": "tests/Feature/ComponentResolverTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Yoyo;\nuse Tests\\App\\Resolvers\\BladeComponentResolver;\nuse Tests\\App\\Resolvers\\CustomComponentResolver;\nuse Tests\\App\\Resolvers\\TwigComponentResolver;\n\nuse function Tests\\render;\n\nit('can use multiple view providers using component resolvers', function () {\n    $yoyo = Yoyo::getInstance();\n    $yoyo->registerComponentResolver(new CustomComponentResolver());\n    $yoyo->registerComponentResolver(new BladeComponentResolver());\n    $yoyo->registerComponentResolver(new TwigComponentResolver());\n\n    expect(render('foo', ['yoyo:resolver' => 'custom']))->toContain('default foo')\n        ->and(render('foo', ['yoyo:resolver' => 'blade']))->toContain('blade foo')\n        ->and(render('foo', ['yoyo:resolver' => 'twig']))->toContain('twig foo');\n});\n"
  },
  {
    "path": "tests/Feature/DispatchEventTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentMethodNotFound;\nuse Clickfwd\\Yoyo\\Services\\BrowserEventsService;\nuse Clickfwd\\Yoyo\\Services\\Response;\n\nuse function Tests\\mockYoyoGetRequest;\nuse function Tests\\resetYoyoRequest;\nuse function Tests\\yoyo_update;\nuse function Tests\\yoyo_view;\n\nuses()->group('dispatch');\n\nbeforeAll(function () {\n    yoyo_view();\n});\n\nbeforeEach(function () {\n    // Reset singletons for clean state (mirrors ComponentLifecycleTest)\n    $ref = new ReflectionClass(BrowserEventsService::class);\n    $prop = $ref->getProperty('instance');\n    $prop->setAccessible(true);\n    $prop->setValue(null, null);\n\n    $ref = new ReflectionClass(Response::class);\n    $prop = $ref->getProperty('instance');\n    $prop->setAccessible(true);\n    $prop->setValue(null, null);\n});\n\nafterEach(function () {\n    resetYoyoRequest();\n});\n\n// =============================================================================\n// JS Dispatch - Associative (named) params: Yoyo.dispatch('event', { key: val })\n// =============================================================================\n\nit('handles JS dispatch with single named parameter', function () {\n    // Simulates: Yoyo.dispatch('post-created', { postId: 42 })\n    mockYoyoGetRequest('http://example.com/', 'dispatch-listener/post-created', '', [\n        'eventParams' => json_encode(['postId' => 42]),\n    ]);\n\n    $output = yoyo_update();\n\n    expect($output)->toContain('Post created with ID: 42');\n});\n\nit('handles JS dispatch with multiple named parameters', function () {\n    // Simulates: Yoyo.dispatch('status-changed', { status: 'active', reason: 'manual' })\n    mockYoyoGetRequest('http://example.com/', 'dispatch-listener/status-changed', '', [\n        'eventParams' => json_encode(['status' => 'active', 'reason' => 'manual']),\n    ]);\n\n    $output = yoyo_update();\n\n    expect($output)->toContain('Status: active, Reason: manual');\n});\n\nit('handles JS dispatch with named params where optional param is omitted', function () {\n    // Simulates: Yoyo.dispatch('status-changed', { status: 'paused' })\n    // 'reason' has a default value of 'none', should use it\n    mockYoyoGetRequest('http://example.com/', 'dispatch-listener/status-changed', '', [\n        'eventParams' => json_encode(['status' => 'paused']),\n    ]);\n\n    $output = yoyo_update();\n\n    expect($output)->toContain('Status: paused, Reason: none');\n});\n\n// =============================================================================\n// JS Dispatch - No params: Yoyo.dispatch('event')\n// =============================================================================\n\nit('handles JS dispatch without parameters', function () {\n    // Simulates: Yoyo.dispatch('simple-refresh') with explicit empty object\n    // Must use stdClass to produce JSON object '{}' matching JS JSON.stringify({})\n    // json_encode([]) produces '[]' which is the server-side emit case — different path\n    mockYoyoGetRequest('http://example.com/', 'dispatch-listener/simple-refresh', '', [\n        'eventParams' => json_encode(new stdClass()),\n    ]);\n\n    $output = yoyo_update();\n\n    expect($output)->toContain('Refreshed without params');\n});\n\n// =============================================================================\n// JS Dispatch - Missing required parameter\n// =============================================================================\n\nit('throws exception when required named parameter is missing', function () {\n    // Simulates: Yoyo.dispatch('status-changed', { reason: 'manual' })\n    // 'status' is required but missing from the payload\n    mockYoyoGetRequest('http://example.com/', 'dispatch-listener/status-changed', '', [\n        'eventParams' => json_encode(['reason' => 'manual']),\n    ]);\n\n    yoyo_update();\n})->throws(\\InvalidArgumentException::class);\n\n// =============================================================================\n// Server-side emit - Sequential (positional) params (existing behavior)\n// =============================================================================\n\nit('handles server-side emit with sequential parameters', function () {\n    // Simulates server-side: $this->emit('post-created', 99)\n    // Server emit sends params as numerically indexed array\n    mockYoyoGetRequest('http://example.com/', 'dispatch-listener/post-created', '', [\n        'eventParams' => json_encode([99]),\n    ]);\n\n    $output = yoyo_update();\n\n    expect($output)->toContain('Post created with ID: 99');\n});\n\nit('handles server-side emit with multiple sequential parameters', function () {\n    // Simulates: $this->emit('multi-param', 'My Title', 'My Body', 5)\n    mockYoyoGetRequest('http://example.com/', 'dispatch-listener/multi-param', '', [\n        'eventParams' => json_encode(['My Title', 'My Body', 5]),\n    ]);\n\n    $output = yoyo_update();\n\n    expect($output)->toContain('Title: My Title, Body: My Body, Category: 5');\n});\n\n// =============================================================================\n// Security - only declared listeners can be triggered\n// =============================================================================\n\nit('rejects dispatch to non-listener event name', function () {\n    // Event name 'nonExistentEvent' is not declared in $listeners\n    // Exception fires during listener routing, before eventParams are read\n    mockYoyoGetRequest('http://example.com/', 'dispatch-listener/nonExistentEvent', '', []);\n\n    yoyo_update();\n})->throws(ComponentMethodNotFound::class);\n"
  },
  {
    "path": "tests/Feature/DynamicComponentTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Exceptions\\BypassRenderMethod;\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentMethodNotFound;\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentNotFound;\nuse Clickfwd\\Yoyo\\Exceptions\\HttpException;\nuse Clickfwd\\Yoyo\\Yoyo;\n\nuse function Tests\\encode_vals;\nuse function Tests\\htmlformat;\nuse function Tests\\hxattr;\nuse function Tests\\mockYoyoGetRequest;\nuse function Tests\\mockYoyoPostRequest;\nuse function Tests\\render;\nuse function Tests\\resetYoyoRequest;\nuse function Tests\\response;\nuse function Tests\\update;\nuse function Tests\\yoprefix_value;\nuse function Tests\\yoyo_update;\nuse function Tests\\yoyo_view;\n\nuses()->group('unit-dynamic');\n\nbeforeAll(function () {\n    yoyo_view();\n});\n\nit('throws expection on component class not found', function () {\n    render('random');\n})->throws(ComponentNotFound::class);\n\nit('renders counter component', function () {\n    $vars = encode_vals([\n        yoprefix_value('id') => 'counter',\n        'count' => 0,\n    ]);\n\n    expect(render('counter'))->toContain(hxattr('vals', $vars));\n});\n\nit('uses passed variable value set in component', function () {\n    $vars = encode_vals([yoprefix_value('id') => 'counter', 'count' => 3]);\n\n    expect(render('counter', ['count' => 3]))->toContain(hxattr('vals', $vars));\n});\n\nit('updates component', function () {\n    expect(update('counter', 'increment'))->toContain('The count is now 1');\n});\n\nit('uses passed variable value in component action', function () {\n    $vars = encode_vals([yoprefix_value('id') => 'counter', 'count' => 1]);\n\n    expect(update('counter', 'increment'))->toContain(hxattr('vals', $vars));\n});\n\nit('throws exception when component method not found', function () {\n    update('counter', 'random');\n})->throws(ComponentMethodNotFound::class);\n\nit('uses a computed property', function () {\n    $output = render('computed-property');\n    expect(htmlformat($output))->toEqual(response('computed-property'));\n});\n\nit('uses the computed property cache', function () {\n    $output = render('computed-property-cache');\n    expect(htmlformat($output))->toEqual(response('computed-property-cache'));\n});\n\nit('can set component view data', function () {\n    expect(render('set-view-data'))->toContain('bar-baz');\n});\n\n\nit('passes action parameters to component method arguments', function () {\n    mockYoyoGetRequest('http://example.com/', 'action-arguments/someAction', '', [\n        'actionArgs' => [1,'foo'],\n    ]);\n\n    $output = yoyo_update();\n\n    resetYoyoRequest();\n\n    expect(htmlformat($output))->toEqual(response('action-arguments'));\n});\n\nit('loads dynamic component with registered alias', function () {\n    Yoyo::registerComponent('registeredalias', \\Tests\\App\\Yoyo\\Registered::class);\n    expect(render('registeredalias'))->toContain('id=\"registered\"');\n});\n\nit('returns empty response with 204 status on skipRender', function () {\n    expect(render('empty-response'))->toBeEmpty()->and(http_response_code())->toBe(204);\n})->throws(BypassRenderMethod::class);\n\nit('returns empty response with 200 status on skipRenderAndReplace', function () {\n    expect(render('empty-response-and-remove'))->toBeEmpty()->and(http_response_code())->toBe(200);\n});\n\nit('dynamically resolves class and named arguments in mount method', function () {\n    mockYoyoGetRequest('http://example.com/', 'ependency-injection-class-with-named-argument-mapping', '', [\n        'id' => 100,\n    ]);\n\n    expect(render('dependency-injection-class-with-named-argument-mapping'))->toContain('the comment title-100');\n\n    resetYoyoRequest();\n});\n\nit('executes trait lifecycle hooks', function () {\n    expect(render('component-with-trait'))->toContain('{ComponentWithTrait} saw that {mountWithFramework} was here');\n});\n\nit('it aborts component execution and throws an exception', function () {\n    try {\n        render('abort');\n    } catch (HttpException $e) {\n        expect($e->getHeaders())->toMatchArray(['foo' => 'bar'])\n            ->and($e->getStatusCode())->toBe(404)\n            ->and($e->getMessage())->toBe('not found');\n\n        throw $e;\n    }\n})->throws(HttpException::class);\n\nit('renders dynamic component in sub-directory', function () {\n    expect(render('account.register'))->toContain('Please register to access this page');\n});\n\nit('renders component using dynamic properties', function () {\n    $vars = encode_vals([\n        yoprefix_value('id') => 'counter',\n        'count' => '',\n    ]);\n\n    expect(render('counter_dynamic_properties'))->toContain(hxattr('vals', $vars));\n});\n\nit('updates component with dynamic properties', function () {\n    expect(update('counter_dynamic_properties', 'increment'))->toContain('The count is now 1');\n});\n\n// Variadic Parameters Tests\nit('handles variadic parameters with no arguments', function () {\n    mockYoyoPostRequest('/', 'variadic-parameters/onlyVariadic', 'variadic-parameters', [\n        'actionArgs' => [],\n    ]);\n\n    expect(yoyo_update())->toContain('Received: []');\n});\n\nit('handles variadic parameters with multiple arguments', function () {\n    mockYoyoPostRequest('/', 'variadic-parameters/onlyVariadic', 'variadic-parameters', [\n        'actionArgs' => ['arg1', 'arg2', 'arg3'],\n    ]);\n\n    expect(yoyo_update())->toContain('Received: [\"arg1\",\"arg2\",\"arg3\"]');\n});\n\nit('handles mixed regular and variadic parameters', function () {\n    mockYoyoPostRequest('/', 'variadic-parameters/mixedVariadic', 'variadic-parameters', [\n        'actionArgs' => ['first', 'second', 'third'],\n    ]);\n\n    expect(yoyo_update())->toContain('First: first, Rest: [\"second\",\"third\"]');\n});\n\nit('handles optional and variadic parameters', function () {\n    mockYoyoPostRequest('/', 'variadic-parameters/optionalAndVariadic', 'variadic-parameters', [\n        'actionArgs' => ['required_value', 'optional_value', 'extra1', 'extra2'],\n    ]);\n\n    expect(yoyo_update())->toContain('Required: required_value, Optional: optional_value, Extra: [\"extra1\",\"extra2\"]');\n});\n\n// Dependency Injection Tests\nit('handles action with only typed parameters', function () {\n    mockYoyoPostRequest('/', 'dependency-injection-action/onlyTyped', 'dependency-injection-action', [\n        'actionArgs' => [],\n    ]);\n\n    expect(yoyo_update())->toContain('Post title: the comment title');\n});\n\nit('handles action with multiple typed parameters', function () {\n    mockYoyoPostRequest('/', 'dependency-injection-action/multipleTyped', 'dependency-injection-action', [\n        'actionArgs' => [],\n    ]);\n\n    expect(yoyo_update())->toContain('Post: the comment title, Comment: the comment body');\n});\n\nit('handles action with mixed typed and regular parameters', function () {\n    mockYoyoPostRequest('/', 'dependency-injection-action/mixedTypedAndRegular', 'dependency-injection-action', [\n        'actionArgs' => [123, 'inactive'],\n    ]);\n\n    expect(yoyo_update())->toContain('Post: the comment title, ID: 123, Status: inactive');\n});\n\nit('handles action with typed and variadic parameters', function () {\n    mockYoyoPostRequest('/', 'dependency-injection-action/typedWithVariadic', 'dependency-injection-action', [\n        'actionArgs' => ['php', 'laravel', 'yoyo'],\n    ]);\n\n    expect(yoyo_update())->toContain('Post: the comment title, Tags: [\"php\",\"laravel\",\"yoyo\"]');\n});\n\nit('handles action with typed and optional regular parameter without value', function () {\n    mockYoyoPostRequest('/', 'dependency-injection-action/typedWithOptional', 'dependency-injection-action', [\n        'actionArgs' => [],\n    ]);\n\n    expect(yoyo_update())->toContain('Post: the comment title, Status: default');\n});\n\nit('handles action with typed and optional regular parameter with value', function () {\n    mockYoyoPostRequest('/', 'dependency-injection-action/typedWithOptional', 'dependency-injection-action', [\n        'actionArgs' => ['active'],\n    ]);\n\n    expect(yoyo_update())->toContain('Post: the comment title, Status: active');\n});\n"
  },
  {
    "path": "tests/Feature/NamespacedComponentTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Yoyo;\nuse Illuminate\\Container\\Container;\nuse Tests\\App\\Resolvers\\BladeComponentResolver;\n\nuse function Tests\\render;\n\nit('can render namespaced anonymous component', function () {\n    $view = Yoyo::getInstance()->getViewProvider();\n    $view->addNamespace('packagename', __DIR__.'/../app-another/views');\n    expect(render('packagename::foo'))->toContain('other foo from another app');\n});\n\nit('can render namespaced dynamic component', function () {\n    $yoyo = Yoyo::getInstance();\n    $view = $yoyo->getViewProvider();\n    $view->addNamespace('packagename', __DIR__.'/../app-another/views');\n    Yoyo::getInstance()->componentNamespace('packagename', 'Tests\\\\AppAnother\\\\Yoyo');\n    expect(render('packagename::counter', ['count' => 3]))->toContain('The count is now 3');\n});\n\nit('can render namespaced anonymous component with custom resolver', function () {\n    $yoyo = Yoyo::getInstance();\n    Container::getInstance()->flush();\n    $yoyo->registerComponentResolver(new BladeComponentResolver());\n    $view = $yoyo->getViewProvider('blade');\n    $view->addNamespace('packagename', __DIR__.'/../app-another/views');\n    expect(render('packagename::foo', [\n        'yoyo:resolver' => 'blade',\n    ]))->toContain('blade foo from another app');\n});\n\nit('can render namespaced dynamic component with custom resolver', function () {\n    $yoyo = Yoyo::getInstance();\n    Container::getInstance()->flush();\n    $yoyo->registerComponentResolver(new BladeComponentResolver());\n    $view = $yoyo->getViewProvider('blade');\n    $view->addNamespace('packagename', __DIR__.'/../app-another/views');\n    $yoyo->componentNamespace('packagename', 'Tests\\\\AppAnother\\\\Yoyo');\n    expect(render('packagename::counter', [\n        'yoyo:resolver' => 'blade',\n        'count' => 3,\n    ]))->toContain('The count is now 3');\n});\n"
  },
  {
    "path": "tests/Feature/NestedComponentTest.php",
    "content": "<?php\n\nuse function Tests\\htmlformat;\nuse function Tests\\render;\nuse function Tests\\response;\nuse function Tests\\yoyo_view;\n\nuses()->group('unit-nested');\n\nbeforeAll(function () {\n    yoyo_view();\n});\n\nit('can render nested components', function () {\n    $output = render('parent', ['data' => [1, 2, 3]], ['id' => 'parent']);\n    expect(htmlformat($output))->toEqual(response('nested'));\n});\n"
  },
  {
    "path": "tests/Feature/ResponseHeadersComponentTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Services\\BrowserEventsService;\nuse Clickfwd\\Yoyo\\Services\\Response;\n\nuse function Tests\\headers;\nuse function Tests\\mockYoyoGetRequest;\nuse function Tests\\resetYoyoRequest;\nuse function Tests\\yoyo_update;\nuse function Tests\\yoyo_view;\n\nuses()->group('response-headers');\n\nbeforeAll(function () {\n    yoyo_view();\n});\n\nbeforeEach(function () {\n    $ref = new ReflectionClass(BrowserEventsService::class);\n    $prop = $ref->getProperty('instance');\n    $prop->setAccessible(true);\n    $prop->setValue(null, null);\n\n    $ref = new ReflectionClass(Response::class);\n    $prop = $ref->getProperty('instance');\n    $prop->setAccessible(true);\n    $prop->setValue(null, null);\n});\n\nafterEach(function () {\n    resetYoyoRequest();\n});\n\n// --- Retarget ---\n\nit('sets HX-Retarget header via component action', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-response-headers/doRetarget', 'component-with-response-headers');\n\n    $output = yoyo_update();\n\n    $responseHeaders = headers();\n\n    expect($responseHeaders)->toHaveKey('HX-Retarget', '#other-target');\n    expect($output)->toContain('retargeted');\n});\n\n// --- Reswap ---\n\nit('sets HX-Reswap header via component action', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-response-headers/doReswap', 'component-with-response-headers');\n\n    $output = yoyo_update();\n\n    $responseHeaders = headers();\n\n    expect($responseHeaders)->toHaveKey('HX-Reswap', 'innerHTML');\n    expect($output)->toContain('reswapped');\n});\n\n// --- Reselect ---\n\nit('sets HX-Reselect header via component action', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-response-headers/doReselect', 'component-with-response-headers');\n\n    $output = yoyo_update();\n\n    $responseHeaders = headers();\n\n    expect($responseHeaders)->toHaveKey('HX-Reselect', '#selected-part');\n    expect($output)->toContain('reselected');\n});\n\n// --- Location ---\n\nit('sets HX-Location header via component action', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-response-headers/doLocation', 'component-with-response-headers');\n\n    yoyo_update();\n\n    $responseHeaders = headers();\n\n    expect($responseHeaders)->toHaveKey('HX-Location', '/new-location');\n});\n\n// --- Push URL ---\n\nit('sets HX-Push-Url header via component action', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-response-headers/doPushUrl', 'component-with-response-headers');\n\n    $output = yoyo_update();\n\n    $responseHeaders = headers();\n\n    expect($responseHeaders)->toHaveKey('HX-Push-Url', '/pushed-url');\n    expect($output)->toContain('url-pushed');\n});\n\n// --- Replace URL ---\n\nit('sets HX-Replace-Url header via component action', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-response-headers/doReplaceUrl', 'component-with-response-headers');\n\n    $output = yoyo_update();\n\n    $responseHeaders = headers();\n\n    expect($responseHeaders)->toHaveKey('HX-Replace-Url', '/replaced-url');\n    expect($output)->toContain('url-replaced');\n});\n\n// --- Redirect (Response-level, not Component-level) ---\n\nit('sets HX-Redirect header via response object', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-response-headers/doRedirect', 'component-with-response-headers');\n\n    yoyo_update();\n\n    $responseHeaders = headers();\n\n    expect($responseHeaders)->toHaveKey('HX-Redirect', '/redirected');\n});\n\n// --- Refresh ---\n\nit('sets HX-Refresh header via component action', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-response-headers/doRefresh', 'component-with-response-headers');\n\n    yoyo_update();\n\n    $responseHeaders = headers();\n\n    expect($responseHeaders)->toHaveKey('HX-Refresh', 'true');\n});\n\n// --- Trigger ---\n\nit('sets HX-Trigger header via component action', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-response-headers/doTrigger', 'component-with-response-headers');\n\n    $output = yoyo_update();\n\n    $responseHeaders = headers();\n\n    expect($responseHeaders)->toHaveKey('HX-Trigger', 'custom-event');\n    expect($output)->toContain('triggered');\n});\n\n// --- Trigger After Swap ---\n\nit('sets HX-Trigger-After-Swap header via component action', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-response-headers/doTriggerAfterSwap', 'component-with-response-headers');\n\n    $output = yoyo_update();\n\n    $responseHeaders = headers();\n\n    expect($responseHeaders)->toHaveKey('HX-Trigger-After-Swap', 'swap-event');\n    expect($output)->toContain('trigger-after-swap');\n});\n\n// --- Trigger After Settle ---\n\nit('sets HX-Trigger-After-Settle header via component action', function () {\n    mockYoyoGetRequest('http://example.com/', 'component-with-response-headers/doTriggerAfterSettle', 'component-with-response-headers');\n\n    $output = yoyo_update();\n\n    $responseHeaders = headers();\n\n    expect($responseHeaders)->toHaveKey('HX-Trigger-After-Settle', 'settle-event');\n    expect($output)->toContain('trigger-after-settle');\n});\n"
  },
  {
    "path": "tests/Feature/TwigTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Yoyo;\n\nuse function Tests\\htmlformat;\nuse function Tests\\render;\nuse function Tests\\response;\nuse function Tests\\update;\nuse function Tests\\yoyo_twig;\n\nbeforeAll(function () {\n    yoyo_twig();\n});\n\nit('discovers and renders anonymous foo component', function () {\n    expect(render('foo'))->toContain('twig foo');\n});\n\nit('updates anonymous component', function () {\n    expect(update('foo'))->toContain('twig bar');\n});\n\nit('can render anonymous component form different location', function () {\n    $view = Yoyo::getInstance()->getViewProvider();\n    $view->prependLocation(__DIR__.'/../app-another/views');\n    expect(render('foo'))->toContain('twig foo from another app');\n});\n\nit('can render anonymous component using a view namespace', function () {\n    $view = Yoyo::getInstance()->getViewProvider();\n    $view->addNamespace('packagename', __DIR__.'/../app-another/views');\n    expect(render('packagename::foo'))->toContain('twig foo from another app');\n});\n\nit('can render dynamic component using a view and class namespace', function () {\n    $view = Yoyo::getInstance()->getViewProvider();\n    $view->addNamespace('packagename', __DIR__.'/../app-another/views');\n    Yoyo::getInstance()->componentNamespace('packagename', 'Tests\\\\AppAnother\\\\Yoyo');\n    expect(render('packagename::counter', ['count' => 3]))->toContain('The count is now 3');\n});\n\nit('can render nested components with yoyo function', function () {\n    $output = render('parent', ['data' => [1, 2, 3]], ['id' => 'parent']);\n    expect(htmlformat($output))->toEqual(response('nested.twig'));\n});\n"
  },
  {
    "path": "tests/Helpers.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse Clickfwd\\Yoyo\\Services\\Response;\nuse Clickfwd\\Yoyo\\View;\nuse Clickfwd\\Yoyo\\ViewProviders\\YoyoViewProvider;\nuse Clickfwd\\Yoyo\\Yoyo;\nuse Clickfwd\\Yoyo\\YoyoCompiler;\nuse Clickfwd\\Yoyo\\YoyoHelpers;\n\nrequire_once __DIR__.'/HelpersBlade.php';\nrequire_once __DIR__.'/HelpersTwig.php';\n\nfunction yoyo_view()\n{\n    yoyo_instance()->registerViewProvider(function () {\n        return new YoyoViewProvider(new View(__DIR__.'/app/resources/views/yoyo'));\n    });\n}\n\nfunction yoyo_instance()\n{\n    $yoyo = Yoyo::getInstance();\n\n    return $yoyo;\n}\n\nfunction compile_html($name, $html, $spinning = false)\n{\n    $yoyo = yoyo_instance();\n\n    return normalizeDomOutput($yoyo->mount($name)->compile('anonymous', $html, $spinning));\n}\n\nfunction compile_html_with_vars($name, $html, $vars, $spinning = false)\n{\n    $yoyo = yoyo_instance();\n\n    return normalizeDomOutput($yoyo->mount($name, $vars)->compile('anonymous', $html, $spinning));\n}\n\nfunction render($name, $variables = [], $attributes = [])\n{\n    $yoyo = yoyo_instance();\n\n    return normalizeDomOutput($yoyo->mount($name, $variables, $attributes)->render());\n}\n\nfunction update($name, $action = 'render', $variables = [], $attributes = [])\n{\n    $yoyo = yoyo_instance();\n\n    return normalizeDomOutput($yoyo->mount($name, $variables, $attributes, $action)->refresh());\n}\n\nfunction yoyo_update()\n{\n    return normalizeDomOutput((yoyo_instance())->update());\n}\n\n/**\n * Normalize DOMDocument attribute encoding for cross-PHP-version compatibility.\n * PHP 8.3+ outputs: hx-vals=\"{&quot;key&quot;:&quot;val&quot;}\"\n * PHP 8.0-8.2 outputs: hx-vals='{\"key\":\"val\"}'\n * This normalizes to the single-quoted format.\n */\nfunction normalizeDomOutput($html)\n{\n    return preg_replace_callback(\n        '/hx-vals=\"([^\"]*)\"/',\n        function ($m) {\n            return \"hx-vals='\" . str_replace('&quot;', '\"', $m[1]) . \"'\";\n        },\n        $html\n    );\n}\n\nfunction mockYoyoGetRequest($url, $component, $target = '', $parameters = [])\n{\n    $request = array_merge([\n        'component' => $component,\n    ], $parameters);\n\n    $server = [\n        'REQUEST_METHOD' => 'GET',\n        'HTTP_HX_REQUEST' => true,\n        'HTTP_HX_CURRENT_URL' => $url,\n        'HTTP_HX_TARGET' => $target,\n    ];\n\n    $requestService = Yoyo::request()->mock($request, $server);\n\n    return $requestService;\n}\n\nfunction mockYoyoPostRequest($url, $component, $target = '', $parameters = [])\n{\n    $request = array_merge([\n        'component' => $component,\n    ], $parameters);\n\n    $server = [\n        'REQUEST_METHOD' => 'POST',\n        'HTTP_HX_REQUEST' => true,\n        'HTTP_HX_CURRENT_URL' => $url,\n        'HTTP_HX_TARGET' => $target,\n    ];\n\n    $requestService = Yoyo::request()->mock($request, $server);\n\n    return $requestService;\n}\n\nfunction resetYoyoRequest()\n{\n    Yoyo::request()->reset();\n}\n\nfunction headers()\n{\n    return (Response::getInstance())->getHeaders();\n}\n\nfunction hxattr($name, $value = '')\n{\n    return YoyoCompiler::hxprefix($name).addValue($value);\n}\n\nfunction yoattr($name, $value = '')\n{\n    return YoyoCompiler::yoprefix($name).addValue($value);\n}\n\nfunction yoprefix_value($value)\n{\n    return YoyoCompiler::yoprefix_value($value);\n}\n\nfunction encode_vals($vars)\n{\n    return YoyoHelpers::encode_vals($vars);\n}\n\nfunction addValue($value = '')\n{\n    if (! $value) {\n        return '';\n    }\n\n    if (YoyoHelpers::test_json($value)) {\n        return \"='\".$value.\"'\";\n    }\n\n    return '=\"'.$value.'\"';\n}\n\nfunction response($filename)\n{\n    $output = file_get_contents(__DIR__.\"/responses/$filename.html\");\n\n    return htmlformat($output);\n}\n\nfunction htmlformat($html)\n{\n    $html = preg_replace('!\\s+!', ' ', $html);\n    $html = preg_replace('/\\>\\s+\\</m', '><', $html);\n\n    return $html;\n}\n"
  },
  {
    "path": "tests/HelpersBlade.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse Clickfwd\\Yoyo\\Blade\\Application;\nuse Clickfwd\\Yoyo\\Blade\\YoyoServiceProvider;\nuse Clickfwd\\Yoyo\\ViewProviders\\BladeViewProvider;\nuse Illuminate\\Container\\Container;\nuse Illuminate\\Contracts\\Foundation\\Application as ApplicationContract;\nuse Illuminate\\Contracts\\View\\Factory as ViewFactory;\nuse Illuminate\\Support\\Fluent;\nuse Jenssegers\\Blade\\Blade;\n\nfunction yoyo_blade()\n{\n    $blade = blade();\n\n    yoyo_instance()->registerViewProvider(function () use ($blade) {\n        return new BladeViewProvider($blade);\n    });\n}\n\nfunction blade()\n{\n    // Use Application (which has terminating()) instead of base Container\n    $app = new Application();\n    Container::setInstance($app);\n\n    $app->bind(ApplicationContract::class, Application::class);\n\n    $app->alias('view', ViewFactory::class);\n\n    $app->extend('config', function ($config) {\n        return is_array($config) ? new Fluent($config) : $config;\n    });\n\n    $blade = new Blade(__DIR__.'/app/resources/views/yoyo', __DIR__.'/compiled', $app);\n\n    $app->bind('view', function () use ($blade) {\n        return $blade;\n    });\n\n    (new YoyoServiceProvider($app))->boot();\n\n    return $blade;\n}\n"
  },
  {
    "path": "tests/HelpersTwig.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse Clickfwd\\Yoyo\\Twig\\YoyoTwigExtension;\nuse Clickfwd\\Yoyo\\ViewProviders\\TwigViewProvider;\n\nfunction yoyo_twig()\n{\n    $yoyo = yoyo_instance();\n\n    $yoyo->registerViewProvider(function () {\n        return new TwigViewProvider(twig());\n    });\n}\n\nfunction twig()\n{\n    $loader = new \\Twig\\Loader\\FilesystemLoader([\n        __DIR__.'/app/resources/views/yoyo',\n      ]);\n\n    $twig = new \\Twig\\Environment($loader, [\n        'cache' => __DIR__.'/compiled',\n        'auto_reload' => true,\n        // 'debug' => true\n    ]);\n\n    // Add Yoyo's Twig Extension\n    $twig->addExtension(new YoyoTwigExtension());\n\n    return $twig;\n}\n"
  },
  {
    "path": "tests/InitYoyoContainer.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\ContainerResolver;\nuse Clickfwd\\Yoyo\\Containers\\YoyoContainer;\nuse Clickfwd\\Yoyo\\Yoyo;\n\nContainerResolver::setPreferred(YoyoContainer::getInstance());\n\n$yoyo = new Yoyo(YoyoContainer::getInstance());\n$yoyo->configure([\n    'namespace' => 'Tests\\\\App\\\\Yoyo\\\\',\n]);\n"
  },
  {
    "path": "tests/Pest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Yoyo;\n\n$yoyo = new Yoyo();\n\n$yoyo->configure([\n    'namespace' => 'Tests\\\\App\\\\Yoyo\\\\',\n]);\n\nuses()->group('browser')->in('Browser');\n"
  },
  {
    "path": "tests/Unit/BrowserEventsServiceTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Services\\BrowserEventsService;\nuse Clickfwd\\Yoyo\\Services\\Response;\nuse Clickfwd\\Yoyo\\Yoyo;\n\nuses()->group('browser-events');\n\nbeforeEach(function () {\n    // Reset singleton instances for clean state\n    $ref = new ReflectionClass(BrowserEventsService::class);\n    $prop = $ref->getProperty('instance');\n    $prop->setAccessible(true);\n    $prop->setValue(null, null);\n\n    $ref = new ReflectionClass(Response::class);\n    $prop = $ref->getProperty('instance');\n    $prop->setAccessible(true);\n    $prop->setValue(null, null);\n\n    Yoyo::request()->mock([], [\n        'REQUEST_METHOD' => 'GET',\n        'HTTP_HX_REQUEST' => true,\n    ]);\n});\n\nafterEach(function () {\n    Yoyo::request()->reset();\n});\n\nit('emits an event with params', function () {\n    $service = BrowserEventsService::getInstance();\n    $service->emit('testEvent', ['key' => 'value']);\n    $service->dispatch();\n\n    $headers = Response::getInstance()->getHeaders();\n    $events = json_decode($headers['Yoyo-Emit'], true);\n\n    expect($events)->toHaveCount(1);\n    expect($events[0]['event'])->toBe('testEvent');\n    expect($events[0]['params'])->toBe(['key' => 'value']);\n});\n\nit('emits targeted event with emitTo', function () {\n    $service = BrowserEventsService::getInstance();\n    $service->emitTo('target-component', 'updateEvent', ['id' => 42]);\n    $service->dispatch();\n\n    $headers = Response::getInstance()->getHeaders();\n    $events = json_decode($headers['Yoyo-Emit'], true);\n\n    expect($events)->toHaveCount(1);\n    expect($events[0])->toMatchArray([\n        'event' => 'updateEvent',\n        'component' => 'target-component',\n    ]);\n});\n\nit('emits to selector with emitToWithSelector', function () {\n    $service = BrowserEventsService::getInstance();\n    $service->emitToWithSelector('#my-element', 'selectorEvent', ['data' => true]);\n    $service->dispatch();\n\n    $headers = Response::getInstance()->getHeaders();\n    $events = json_decode($headers['Yoyo-Emit'], true);\n\n    expect($events)->toHaveCount(1);\n    expect($events[0])->toMatchArray([\n        'event' => 'selectorEvent',\n        'selector' => '#my-element',\n    ]);\n});\n\nit('emits self-targeted event', function () {\n    Yoyo::request()->mock([\n        'component' => 'my-component/action',\n    ], [\n        'REQUEST_METHOD' => 'GET',\n        'HTTP_HX_REQUEST' => true,\n    ]);\n\n    $service = BrowserEventsService::getInstance();\n    $service->emitSelf('selfEvent', ['status' => 'ok']);\n    $service->dispatch();\n\n    $headers = Response::getInstance()->getHeaders();\n    $events = json_decode($headers['Yoyo-Emit'], true);\n\n    expect($events)->toHaveCount(1);\n    expect($events[0])->toMatchArray([\n        'event' => 'selfEvent',\n        'component' => 'my-component',\n        'propagation' => 'self',\n    ]);\n});\n\nit('does not emit self when no component in request', function () {\n    Yoyo::request()->mock([], [\n        'REQUEST_METHOD' => 'GET',\n        'HTTP_HX_REQUEST' => true,\n    ]);\n\n    $service = BrowserEventsService::getInstance();\n    $service->emitSelf('selfEvent', ['status' => 'ok']);\n    $service->dispatch();\n\n    $headers = Response::getInstance()->getHeaders();\n    $events = json_decode($headers['Yoyo-Emit'], true);\n\n    expect($events)->toHaveCount(0);\n});\n\nit('dispatches browser event', function () {\n    $service = BrowserEventsService::getInstance();\n    $service->dispatchBrowserEvent('show-modal', ['title' => 'Confirm']);\n    $service->dispatch();\n\n    $headers = Response::getInstance()->getHeaders();\n    $browserEvents = json_decode($headers['Yoyo-Browser-Event'], true);\n\n    expect($browserEvents)->toHaveCount(1);\n    expect($browserEvents[0])->toMatchArray([\n        'event' => 'show-modal',\n        'params' => ['title' => 'Confirm'],\n    ]);\n});\n\nit('queues multiple events', function () {\n    $service = BrowserEventsService::getInstance();\n    $service->emit('event1', ['a' => 1]);\n    $service->emit('event2', ['b' => 2]);\n    $service->emit('event3', ['c' => 3]);\n    $service->dispatch();\n\n    $headers = Response::getInstance()->getHeaders();\n    $events = json_decode($headers['Yoyo-Emit'], true);\n\n    expect($events)->toHaveCount(3);\n    expect($events[0]['event'])->toBe('event1');\n    expect($events[1]['event'])->toBe('event2');\n    expect($events[2]['event'])->toBe('event3');\n});\n\nit('queues multiple browser events', function () {\n    $service = BrowserEventsService::getInstance();\n    $service->dispatchBrowserEvent('toast', ['msg' => 'saved']);\n    $service->dispatchBrowserEvent('scroll', ['to' => 'top']);\n    $service->dispatch();\n\n    $headers = Response::getInstance()->getHeaders();\n    $browserEvents = json_decode($headers['Yoyo-Browser-Event'], true);\n\n    expect($browserEvents)->toHaveCount(2);\n});\n\nit('dispatches empty arrays when no events queued', function () {\n    $service = BrowserEventsService::getInstance();\n    $service->dispatch();\n\n    $headers = Response::getInstance()->getHeaders();\n\n    expect(json_decode($headers['Yoyo-Emit'], true))->toBe([]);\n    expect(json_decode($headers['Yoyo-Browser-Event'], true))->toBe([]);\n});\n"
  },
  {
    "path": "tests/Unit/BrowserEventsTest.php",
    "content": "<?php\n\nuse function Tests\\headers;\nuse function Tests\\mockYoyoGetRequest;\nuse function Tests\\yoyo_update;\nuse function Tests\\yoyo_view;\n\nbeforeAll(function () {\n    yoyo_view();\n});\n\nit('emits browser event', function () {\n    mockYoyoGetRequest('http://example.com/', 'counter/increment');\n\n    yoyo_update();\n\n    $headers = headers();\n\n    expect($headers)->toHaveKey('Yoyo-Emit');\n\n    expect($headers['Yoyo-Emit'])->toEqual('[{\"event\":\"counter:updated\",\"params\":[{\"count\":1}]}]');\n})->group('headers');\n"
  },
  {
    "path": "tests/Unit/ClassHelpersTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\ClassHelpers;\nuse Clickfwd\\Yoyo\\Component;\nuse Clickfwd\\Yoyo\\ComponentResolver;\nuse Clickfwd\\Yoyo\\Yoyo;\nuse Tests\\App\\Yoyo\\ComponentWithTrait;\nuse Tests\\App\\Yoyo\\ComputedProperty;\nuse Tests\\App\\Yoyo\\Counter;\n\nfunction resolveComponent(string $class = Counter::class, string $name = 'counter'): Component\n{\n    $resolver = new ComponentResolver(Yoyo::getInstance());\n\n    return new $class($resolver, 'test-'.$name, $name);\n}\n\nbeforeEach(function () {\n    ClassHelpers::flushCache();\n});\n\nit('returns public properties excluding base class', function () {\n    $component = resolveComponent();\n    $props = ClassHelpers::getPublicProperties($component, Component::class);\n    expect($props)->toContain('count');\n    expect($props)->not->toContain('yoyo_id');\n});\n\nit('returns same result on repeated calls', function () {\n    $component = resolveComponent();\n    $first = ClassHelpers::getPublicProperties($component, Component::class);\n    $second = ClassHelpers::getPublicProperties($component, Component::class);\n    expect($first)->toEqual($second);\n});\n\nit('returns default public vars', function () {\n    $component = resolveComponent();\n    $defaults = ClassHelpers::getDefaultPublicVars($component, Component::class);\n    expect($defaults)->toHaveKey('count');\n    expect($defaults['count'])->toBe(0);\n});\n\nit('returns current public vars after mutation', function () {\n    $component = resolveComponent();\n    $component->count = 5;\n    $vars = ClassHelpers::getPublicVars($component, Component::class);\n    expect($vars['count'])->toBe(5);\n});\n\nit('returns public methods excluding base class', function () {\n    $methods = ClassHelpers::getPublicMethods(Counter::class, ['render']);\n    expect($methods)->toContain('increment');\n});\n\nit('discovers traits recursively', function () {\n    $traits = ClassHelpers::classUsesRecursive(ComponentWithTrait::class);\n    expect($traits)->not->toBeEmpty();\n});\n\nit('returns class basename from FQCN', function () {\n    expect(ClassHelpers::classBasename(Counter::class))->toBe('Counter');\n});\n\nit('detects non-private methods', function () {\n    expect(ClassHelpers::methodIsPrivate(Counter::class, 'increment'))->toBeFalse();\n});\n\nit('gets method parameter names', function () {\n    $names = ClassHelpers::getMethodParameterNames(Counter::class, 'increment');\n    expect($names)->toBeArray();\n});\n\nit('returns method parameters with types', function () {\n    $params = ClassHelpers::getMethodParametersWithTypes(Counter::class, 'increment');\n    expect($params)->toHaveKey('typed');\n    expect($params)->toHaveKey('regular');\n});\n\nit('detects variadic parameters', function () {\n    expect(ClassHelpers::methodHasVariadicParameter(Counter::class, 'increment'))->toBeFalse();\n});\n\n// --- Caching tests ---\n\nit('returns cached result on second call (strict identity)', function () {\n    $component = resolveComponent();\n\n    $first = ClassHelpers::getPublicProperties($component, Component::class);\n    $second = ClassHelpers::getPublicProperties($component, Component::class);\n\n    expect($first)->toBe($second);\n});\n\nit('separates cache by class name', function () {\n    $counter = resolveComponent(Counter::class, 'counter');\n    $computed = resolveComponent(ComputedProperty::class, 'computed-property');\n\n    $counterProps = ClassHelpers::getPublicProperties($counter, Component::class);\n    $computedProps = ClassHelpers::getPublicProperties($computed, Component::class);\n\n    expect($counterProps)->not->toEqual($computedProps);\n});\n\nit('flushCache clears all caches', function () {\n    $component = resolveComponent();\n    ClassHelpers::getPublicProperties($component, Component::class);\n    ClassHelpers::getDefaultPublicVars($component, Component::class);\n    ClassHelpers::getPublicMethods(Counter::class, ['render']);\n    ClassHelpers::classUsesRecursive(ComponentWithTrait::class);\n\n    ClassHelpers::flushCache();\n\n    // After flush, a new call should still return correct results\n    $props = ClassHelpers::getPublicProperties($component, Component::class);\n    expect($props)->toContain('count');\n});\n\nit('caches getDefaultPublicVars result', function () {\n    $component = resolveComponent();\n\n    $first = ClassHelpers::getDefaultPublicVars($component, Component::class);\n    $second = ClassHelpers::getDefaultPublicVars($component, Component::class);\n\n    expect($first)->toBe($second);\n});\n\nit('caches classUsesRecursive result', function () {\n    $first = ClassHelpers::classUsesRecursive(ComponentWithTrait::class);\n    $second = ClassHelpers::classUsesRecursive(ComponentWithTrait::class);\n\n    expect($first)->toBe($second);\n});\n\nit('caches getPublicMethods result', function () {\n    $first = ClassHelpers::getPublicMethods(Counter::class, ['render']);\n    $second = ClassHelpers::getPublicMethods(Counter::class, ['render']);\n\n    expect($first)->toBe($second);\n});\n"
  },
  {
    "path": "tests/Unit/ComponentResolverTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\ContainerResolver;\n\nit('resolves dynamic component', function () {\n    $resolver = (new Clickfwd\\Yoyo\\ComponentResolver())(ContainerResolver::resolve());\n\n    expect($resolver->resolveDynamic('counter', 'counter'))->toBeInstanceOf(Tests\\App\\Yoyo\\Counter::class);\n});\n\nit('resolves anonymous component', function () {\n    $resolver = (new Clickfwd\\Yoyo\\ComponentResolver())(ContainerResolver::resolve());\n\n    expect($resolver->resolveAnonymous('foo', 'foo'))->toBeInstanceOf(Clickfwd\\Yoyo\\AnonymousComponent::class);\n});\n\nit('resolves namespaced dynamic component', function () {\n    $namespaces = ['packagename' => [\n        'Tests\\\\AppAnother\\\\Yoyo',\n    ]];\n\n    $resolver = (new Clickfwd\\Yoyo\\ComponentResolver())(ContainerResolver::resolve(), [], $namespaces);\n\n    expect($resolver->resolveDynamic('foo', 'packagename::counter'))\n        ->toBeInstanceOf(Tests\\AppAnother\\Yoyo\\Counter::class);\n});\n"
  },
  {
    "path": "tests/Unit/ComponentTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\ComponentResolver;\nuse Clickfwd\\Yoyo\\ContainerResolver;\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentMethodNotFound;\n\nuses()->group('component');\n\nfunction makeComponent(string $class, string $id = 'test', string $name = 'test'): \\Clickfwd\\Yoyo\\Component\n{\n    $container = ContainerResolver::resolve();\n    $resolver = (new ComponentResolver())($container);\n\n    return new $class($resolver, $id, $name);\n}\n\n// --- getListeners ---\n\nit('normalizes numeric listener keys to key=value pairs', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\ComponentWithListeners::class);\n\n    $listeners = $component->getListeners();\n\n    expect($listeners)->toHaveKey('itemAdded', 'onItemAdded');\n    expect($listeners)->toHaveKey('refresh', 'refresh');\n});\n\n// --- set() method ---\n\nit('sets single view data key', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class);\n    $component->set('foo', 'bar');\n\n    $ref = new ReflectionClass($component);\n    $prop = $ref->getProperty('viewData');\n    $prop->setAccessible(true);\n\n    expect($prop->getValue($component))->toMatchArray(['foo' => 'bar']);\n});\n\nit('sets multiple view data keys via array', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class);\n    $component->set(['a' => 1, 'b' => 2]);\n\n    $ref = new ReflectionClass($component);\n    $prop = $ref->getProperty('viewData');\n    $prop->setAccessible(true);\n\n    expect($prop->getValue($component))->toMatchArray(['a' => 1, 'b' => 2]);\n});\n\nit('merges view data on subsequent calls', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class);\n    $component->set('x', 1);\n    $component->set('y', 2);\n\n    $ref = new ReflectionClass($component);\n    $prop = $ref->getProperty('viewData');\n    $prop->setAccessible(true);\n\n    expect($prop->getValue($component))->toMatchArray(['x' => 1, 'y' => 2]);\n});\n\n// --- actionMatches ---\n\nit('matches single action string', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class);\n    $component->setAction('increment');\n\n    expect($component->actionMatches('increment'))->toBeTrue();\n    expect($component->actionMatches('decrement'))->toBeFalse();\n});\n\nit('matches action from array', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class);\n    $component->setAction('increment');\n\n    expect($component->actionMatches(['increment', 'decrement']))->toBeTrue();\n    expect($component->actionMatches(['save', 'delete']))->toBeFalse();\n});\n\n// --- Computed property caching ---\n\nit('caches computed property value', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\ComputedPropertyCache::class, 'test', 'computed-property-cache');\n    $component->boot([], []);\n\n    // First call returns 1 and caches it\n    $first = $component->testCount;\n    // Second call returns cached value (still 1, not 2)\n    $second = $component->testCount;\n\n    expect($first)->toBe(1);\n    expect($second)->toBe(1);\n});\n\nit('clears all computed property cache', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\ComputedPropertyCache::class, 'test', 'computed-property-cache');\n    $component->boot([], []);\n\n    $first = $component->testCount;\n    expect($first)->toBe(1);\n\n    $component->forgetComputed();\n\n    // After clearing cache, it recalculates\n    $second = $component->testCount;\n    expect($second)->toBe(2);\n});\n\nit('clears specific computed property cache', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\ComputedPropertyCache::class, 'test', 'computed-property-cache');\n    $component->boot([], []);\n\n    $first = $component->testCount;\n    expect($first)->toBe(1);\n\n    $component->forgetComputed('testCount');\n\n    $second = $component->testCount;\n    expect($second)->toBe(2);\n});\n\nit('clears multiple computed property caches via args', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\ComputedPropertyCache::class, 'test', 'computed-property-cache');\n    $component->boot([], []);\n\n    $component->testCount;\n    $component->forgetComputed('testCount', 'otherKey');\n\n    $second = $component->testCount;\n    expect($second)->toBe(2);\n});\n\n// --- Computed property with arguments (__call) ---\n\nit('calls computed property with arguments', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\ComponentWithComputedArgs::class, 'test', 'component-with-computed-args');\n    $component->boot([], []);\n\n    expect($component->greeting('Alice'))->toBe('Hello, Alice!');\n    expect($component->greeting('Bob'))->toBe('Hello, Bob!');\n});\n\nit('caches computed property with arguments by arg hash', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\ComponentWithComputedArgs::class, 'test', 'component-with-computed-args');\n    $component->boot([], []);\n\n    $first = $component->expensive();\n    $second = $component->expensive();\n\n    expect($first)->toBe($second);\n});\n\nit('uses different cache keys for different arguments', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\ComponentWithComputedArgs::class, 'test', 'component-with-computed-args');\n    $component->boot([], []);\n\n    $resultAlice = $component->greeting('Alice');\n    $resultBob = $component->greeting('Bob');\n\n    expect($resultAlice)->toBe('Hello, Alice!');\n    expect($resultBob)->toBe('Hello, Bob!');\n    expect($resultAlice)->not->toBe($resultBob);\n});\n\nit('clears computed property cache with arguments', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\ComponentWithComputedArgs::class, 'test', 'component-with-computed-args');\n    $component->boot([], []);\n\n    $first = $component->expensive();\n    $component->forgetComputedWithArgs('expensive');\n\n    // After clearing, next call recalculates and returns a different value\n    $second = $component->expensive();\n    expect($second)->not->toBe($first);\n});\n\n// --- __get and __call throw on unknown ---\n\nit('throws ComponentMethodNotFound for unknown property', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class, 'test', 'counter');\n    $component->boot([], []);\n    $component->nonExistentProperty;\n})->throws(ComponentMethodNotFound::class);\n\nit('throws ComponentMethodNotFound for unknown method call', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class, 'test', 'counter');\n    $component->boot([], []);\n    $component->nonExistentMethod();\n})->throws(ComponentMethodNotFound::class);\n\n// --- spinning() ---\n\nit('sets spinning state and returns self', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class);\n    $result = $component->spinning(true);\n\n    expect($result)->toBe($component);\n\n    $ref = new ReflectionClass($component);\n    $prop = $ref->getProperty('spinning');\n    $prop->setAccessible(true);\n\n    expect($prop->getValue($component))->toBeTrue();\n});\n\n// --- boot() ---\n\nit('sets public properties from variables', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class);\n    $component->boot(['count' => 42], []);\n\n    expect($component->count)->toBe(42);\n});\n\nit('preserves default value when variable not provided', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class);\n    $component->boot([], []);\n\n    expect($component->count)->toBe(0);\n});\n\n// --- getName, getComponentId ---\n\nit('returns component name', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class, 'counter-id', 'counter');\n    expect($component->getName())->toBe('counter');\n});\n\nit('returns component id', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class, 'counter-id', 'counter');\n    expect($component->getComponentId())->toBe('counter-id');\n});\n\n// --- getQueryString, getProps ---\n\nit('returns query string configuration', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class);\n    expect($component->getQueryString())->toBe(['count']);\n});\n\nit('returns props configuration', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class);\n    expect($component->getProps())->toBe(['count']);\n});\n\n// --- getVariables, getInitialAttributes ---\n\nit('returns variables after boot', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class);\n    $component->boot(['count' => 5], []);\n    expect($component->getVariables())->toBe(['count' => 5]);\n});\n\nit('returns initial attributes after boot', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class);\n    $component->boot([], ['class' => 'my-class']);\n    expect($component->getInitialAttributes())->toBe(['class' => 'my-class']);\n});\n\n// --- Redirect trait ---\n\nit('stores redirect URL', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class);\n    $result = $component->redirect('/some-page');\n\n    expect($result)->toBe($component);\n    expect($component->redirectTo)->toBe('/some-page');\n});\n\nit('redirect defaults to null', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class);\n    expect($component->redirectTo)->toBeNull();\n});\n\n// --- Dynamic properties ---\n\nit('returns empty array for getDynamicProperties by default', function () {\n    $component = makeComponent(Tests\\App\\Yoyo\\Counter::class);\n    expect($component->getDynamicProperties())->toBe([]);\n});\n"
  },
  {
    "path": "tests/Unit/ConfigurationTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Services\\Configuration;\n\n// Tests work against the configuration set in Pest.php (namespace = Tests\\App\\Yoyo\\)\n\nit('returns configured value with get()', function () {\n    expect(Configuration::get('namespace'))->toBe('Tests\\\\App\\\\Yoyo\\\\');\n});\n\nit('returns default when key not found', function () {\n    expect(Configuration::get('nonexistent', 'fallback'))->toBe('fallback');\n});\n\nit('returns null when key not found and no default', function () {\n    expect(Configuration::get('nonexistent'))->toBeNull();\n});\n\nit('provides default config values', function () {\n    expect(Configuration::get('defaultSwapStyle'))->toBe('outerHTML');\n    expect(Configuration::get('historyEnabled'))->toBeFalse();\n    expect(Configuration::get('indicatorClass'))->toBe('yoyo-indicator');\n    expect(Configuration::get('requestClass'))->toBe('yoyo-request');\n    expect(Configuration::get('settlingClass'))->toBe('yoyo-settling');\n    expect(Configuration::get('swappingClass'))->toBe('yoyo-swapping');\n});\n\nit('generates htmx source URL with default version', function () {\n    $src = Configuration::htmxSrc();\n\n    expect($src)->toContain('htmx.org@');\n    expect($src)->toContain('/dist/htmx.min.js');\n});\n\nit('generates yoyo source path with default config', function () {\n    $src = Configuration::yoyoSrc();\n\n    expect($src)->toContain('yoyo.js');\n});\n\nit('generates JavaScript assets with script tags', function () {\n    $assets = Configuration::javascriptAssets();\n\n    expect($assets)->toContain('<script src=');\n    expect($assets)->toContain('htmx');\n    expect($assets)->toContain('yoyo.js');\n});\n\nit('generates JavaScript init code with script tag by default', function () {\n    $code = Configuration::javascriptInitCode();\n\n    expect($code)->toContain('<script>');\n    expect($code)->toContain('Yoyo.url');\n    expect($code)->toContain('Yoyo.config(');\n});\n\nit('generates JavaScript init code without script tag', function () {\n    $code = Configuration::javascriptInitCode(false);\n\n    expect($code)->not->toContain('<script>');\n    expect($code)->toContain('Yoyo.url');\n});\n\nit('excludes non-allowed config options from JavaScript config', function () {\n    $code = Configuration::javascriptInitCode(false);\n\n    // namespace and url are not in allowedConfigOptions\n    expect($code)->not->toContain('Tests\\\\\\\\App\\\\\\\\Yoyo');\n    // Allowed options should be present\n    expect($code)->toContain('outerHTML'); // defaultSwapStyle\n    expect($code)->toContain('yoyo-indicator'); // indicatorClass\n});\n\nit('generates CSS styles with style tag', function () {\n    $styles = Configuration::cssStyle();\n\n    expect($styles)->toContain('<style>');\n    expect($styles)->toContain('yoyo\\\\:spinning');\n    expect($styles)->toContain('display: none');\n});\n\nit('generates CSS styles without style tag', function () {\n    $styles = Configuration::cssStyle(false);\n\n    expect($styles)->not->toContain('<style>');\n    expect($styles)->toContain('display: none');\n});\n\nit('minifies scripts output', function () {\n    $scripts = Configuration::scripts();\n\n    // Should not contain tabs or multiple spaces\n    expect($scripts)->not->toMatch('/\\t/');\n    expect($scripts)->not->toMatch('/\\s{2,}/');\n});\n\nit('minifies styles output', function () {\n    $styles = Configuration::styles();\n\n    expect($styles)->not->toMatch('/\\t/');\n});\n\nit('uses json_encode for URL value in JavaScript init code', function () {\n    $code = Configuration::javascriptInitCode(false);\n\n    // URL should be double-quoted via json_encode, not single-quoted\n    expect($code)->toMatch('/Yoyo\\.url = \".*\";/');\n    expect($code)->not->toMatch(\"/Yoyo\\\\.url = '.*';/\");\n});\n"
  },
  {
    "path": "tests/Unit/ContainerResolverTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\ContainerResolver;\nuse Clickfwd\\Yoyo\\Containers\\IlluminateContainer;\nuse Clickfwd\\Yoyo\\Containers\\YoyoContainer;\n\nit('resolves to illuminate container when available', function () {\n    $oldPreferred  = ContainerResolver::getPreferred();\n\n    // Reset preferred\n    ContainerResolver::setPreferred(null);\n    $container = ContainerResolver::resolve();\n    expect($container)->toBeInstanceOf(IlluminateContainer::class);\n\n    // Cleanup\n    ContainerResolver::setPreferred($oldPreferred);\n});\n\nit('uses preferred container when set', function () {\n    $oldPreferred  = ContainerResolver::getPreferred();\n\n    // Prefer YoyoContainer\n    $preferred = YoyoContainer::getInstance();\n    ContainerResolver::setPreferred($preferred);\n    $container = ContainerResolver::resolve();\n    expect($container)->toBe($preferred);\n\n    // Prefer IlluminateContainer\n    $preferred = IlluminateContainer::getInstance();\n    ContainerResolver::setPreferred($preferred);\n    $container = ContainerResolver::resolve();\n    expect($container)->toBe($preferred);\n\n    // Cleanup\n    ContainerResolver::setPreferred($oldPreferred);\n});\n"
  },
  {
    "path": "tests/Unit/ExceptionsTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Exceptions\\BindingNotFoundException;\nuse Clickfwd\\Yoyo\\Exceptions\\BypassRenderMethod;\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentMethodNotFound;\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentNotFound;\nuse Clickfwd\\Yoyo\\Exceptions\\ContainerResolutionException;\nuse Clickfwd\\Yoyo\\Exceptions\\FailedToRegisterComponent;\nuse Clickfwd\\Yoyo\\Exceptions\\HttpException;\nuse Clickfwd\\Yoyo\\Exceptions\\IncompleteComponentParamInRequest;\nuse Clickfwd\\Yoyo\\Exceptions\\NonPublicComponentMethodCall;\nuse Clickfwd\\Yoyo\\Exceptions\\NotFoundHttpException;\n\n// --- HttpException ---\n\nit('creates HttpException with status code and message', function () {\n    $e = new HttpException(500, 'Internal error', ['X-Custom' => 'value']);\n\n    expect($e->getStatusCode())->toBe(500);\n    expect($e->getMessage())->toBe('Internal error');\n    expect($e->getHeaders())->toBe(['X-Custom' => 'value']);\n    expect($e->getCode())->toBe(500);\n});\n\nit('creates HttpException with empty message', function () {\n    $e = new HttpException(403);\n\n    expect($e->getStatusCode())->toBe(403);\n    expect($e->getMessage())->toBe('');\n    expect($e->getHeaders())->toBe([]);\n});\n\n// --- NotFoundHttpException ---\n\nit('creates NotFoundHttpException with 404 status', function () {\n    $e = new NotFoundHttpException('Page not found', ['Retry-After' => '60']);\n\n    expect($e->getStatusCode())->toBe(404);\n    expect($e->getMessage())->toBe('Page not found');\n    expect($e->getHeaders())->toBe(['Retry-After' => '60']);\n});\n\nit('creates NotFoundHttpException with defaults', function () {\n    $e = new NotFoundHttpException();\n\n    expect($e->getStatusCode())->toBe(404);\n    expect($e->getMessage())->toBe('');\n});\n\n// --- BypassRenderMethod ---\n\nit('creates BypassRenderMethod with status code', function () {\n    $e = new BypassRenderMethod(204);\n\n    expect($e->getCode())->toBe(204);\n    expect($e->getMessage())->toBe('');\n});\n\nit('creates BypassRenderMethod with 200 status', function () {\n    $e = new BypassRenderMethod(200);\n\n    expect($e->getCode())->toBe(200);\n});\n\n// --- ComponentMethodNotFound ---\n\nit('creates ComponentMethodNotFound with descriptive message', function () {\n    $e = new ComponentMethodNotFound('Counter', 'nonExistent');\n\n    expect($e->getMessage())->toContain('nonExistent');\n    expect($e->getMessage())->toContain('Counter');\n    expect($e->getMessage())->toContain('Public method');\n});\n\n// --- ComponentNotFound ---\n\nit('creates ComponentNotFound with component alias', function () {\n    $e = new ComponentNotFound('missing-component');\n\n    expect($e->getMessage())->toContain('missing-component');\n    expect($e->getMessage())->toContain('not found');\n});\n\n// --- NonPublicComponentMethodCall ---\n\nit('creates NonPublicComponentMethodCall with descriptive message', function () {\n    $e = new NonPublicComponentMethodCall('Counter', 'secret');\n\n    expect($e->getMessage())->toContain('Counter');\n    expect($e->getMessage())->toContain('secret');\n    expect($e->getMessage())->toContain('non-public');\n});\n\n// --- IncompleteComponentParamInRequest ---\n\nit('creates IncompleteComponentParamInRequest with default message', function () {\n    $e = new IncompleteComponentParamInRequest();\n\n    expect($e->getMessage())->toContain('component parameter');\n    expect($e->getMessage())->toContain('missing');\n});\n\n// --- FailedToRegisterComponent ---\n\nit('creates FailedToRegisterComponent for anonymous component', function () {\n    $e = new FailedToRegisterComponent('my-alias', 'Anonymous');\n\n    expect($e->getMessage())->toContain('my-alias');\n    expect($e->getMessage())->toContain('Anonymous');\n    expect($e->getMessage())->toContain('template not found');\n});\n\nit('creates FailedToRegisterComponent for class-based component', function () {\n    $e = new FailedToRegisterComponent('my-alias', 'App\\\\Yoyo\\\\MyComponent');\n\n    expect($e->getMessage())->toContain('my-alias');\n    expect($e->getMessage())->toContain('App\\\\Yoyo\\\\MyComponent');\n    expect($e->getMessage())->toContain('class');\n    expect($e->getMessage())->toContain('not found');\n});\n\n// --- BindingNotFoundException ---\n\nit('creates BindingNotFoundException with message and previous exception', function () {\n    $previous = new \\RuntimeException('original');\n    $e = new BindingNotFoundException('binding not found', $previous);\n\n    expect($e->getMessage())->toBe('binding not found');\n    expect($e->getPrevious())->toBe($previous);\n    expect($e)->toBeInstanceOf(\\Psr\\Container\\NotFoundExceptionInterface::class);\n});\n\n// --- ContainerResolutionException ---\n\nit('creates ContainerResolutionException with message and previous exception', function () {\n    $previous = new \\RuntimeException('original');\n    $e = new ContainerResolutionException('resolution failed', $previous);\n\n    expect($e->getMessage())->toBe('resolution failed');\n    expect($e->getPrevious())->toBe($previous);\n    expect($e)->toBeInstanceOf(\\Psr\\Container\\ContainerExceptionInterface::class);\n});\n"
  },
  {
    "path": "tests/Unit/IlluminateContainerTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Containers\\IlluminateContainer;\nuse Illuminate\\Container\\Container;\n\nit('delegates to illuminate container', function () {\n    $illuminate = Container::getInstance();\n    $illuminate->bind('test', fn () => 'value');\n\n    $container = IlluminateContainer::getInstance();\n\n    expect($container->get('test'))->toBe('value');\n});\n"
  },
  {
    "path": "tests/Unit/InvocableComponentVariableTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\InvocableComponentVariable;\n\nit('invokes the callable when used as a function', function () {\n    $variable = new InvocableComponentVariable(function () {\n        return 'hello';\n    });\n\n    expect($variable())->toBe('hello');\n});\n\nit('converts to string by invoking the callable', function () {\n    $variable = new InvocableComponentVariable(function () {\n        return 'world';\n    });\n\n    expect((string) $variable)->toBe('world');\n});\n\nit('delegates property access to the invoked result', function () {\n    $obj = new stdClass();\n    $obj->name = 'test';\n\n    $variable = new InvocableComponentVariable(function () use ($obj) {\n        return $obj;\n    });\n\n    expect($variable->name)->toBe('test');\n});\n\nit('delegates method calls to the invoked result', function () {\n    $obj = new class () {\n        public function greet()\n        {\n            return 'hi';\n        }\n    };\n\n    $variable = new InvocableComponentVariable(function () use ($obj) {\n        return $obj;\n    });\n\n    expect($variable->greet())->toBe('hi');\n});\n\nit('passes arguments to delegated method calls', function () {\n    $obj = new class () {\n        public function add($a, $b)\n        {\n            return $a + $b;\n        }\n    };\n\n    $variable = new InvocableComponentVariable(function () use ($obj) {\n        return $obj;\n    });\n\n    expect($variable->add(2, 3))->toBe(5);\n});\n\nit('handles numeric return values in toString', function () {\n    $variable = new InvocableComponentVariable(function () {\n        return 42;\n    });\n\n    expect((string) $variable)->toBe('42');\n});\n"
  },
  {
    "path": "tests/Unit/PageRedirectServiceTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Services\\PageRedirectService;\nuse Clickfwd\\Yoyo\\Services\\Response;\n\nuses()->group('services');\n\nbeforeEach(function () {\n    // Reset singleton instances\n    $ref = new ReflectionClass(PageRedirectService::class);\n    $prop = $ref->getProperty('instance');\n    $prop->setAccessible(true);\n    $prop->setValue(null, null);\n\n    $ref = new ReflectionClass(Response::class);\n    $prop = $ref->getProperty('instance');\n    $prop->setAccessible(true);\n    $prop->setValue(null, null);\n});\n\nit('sets redirect header for valid URL', function () {\n    $service = PageRedirectService::getInstance();\n    $service->redirect('/dashboard');\n\n    $headers = Response::getInstance()->getHeaders();\n    expect($headers)->toHaveKey('Yoyo-Redirect', '/dashboard');\n});\n\nit('sets redirect header for absolute URL', function () {\n    $service = PageRedirectService::getInstance();\n    $service->redirect('https://example.com/page');\n\n    $headers = Response::getInstance()->getHeaders();\n    expect($headers)->toHaveKey('Yoyo-Redirect', 'https://example.com/page');\n});\n\nit('does not set header for null URL', function () {\n    $service = PageRedirectService::getInstance();\n    $service->redirect(null);\n\n    $headers = Response::getInstance()->getHeaders();\n    expect($headers)->not->toHaveKey('Yoyo-Redirect');\n});\n\nit('does not set header for empty string URL', function () {\n    $service = PageRedirectService::getInstance();\n    $service->redirect('');\n\n    $headers = Response::getInstance()->getHeaders();\n    expect($headers)->not->toHaveKey('Yoyo-Redirect');\n});\n\nit('returns singleton instance', function () {\n    $a = PageRedirectService::getInstance();\n    $b = PageRedirectService::getInstance();\n    expect($a)->toBe($b);\n});\n"
  },
  {
    "path": "tests/Unit/ProtectedMethodTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Exceptions\\NonPublicComponentMethodCall;\n\nuse function Tests\\update;\n\nit('throws exception when requesting a protected component action', function () {\n    update('protected-methods', 'secret');\n})->throws(NonPublicComponentMethodCall::class)->group('notpublic');\n"
  },
  {
    "path": "tests/Unit/PushUrlStateTest.php",
    "content": "<?php\n\nuse function Tests\\headers;\nuse function Tests\\mockYoyoGetRequest;\nuse function Tests\\yoyo_update;\n\nit('pushes new URL state', function () {\n    mockYoyoGetRequest('http://example.com/', 'counter/increment');\n\n    yoyo_update();\n\n    $headers = headers();\n\n    expect($headers)->toHaveKey('Yoyo-Push');\n\n    expect($headers['Yoyo-Push'])->toEqual('http://example.com/?count=1');\n})->group('headers');\n"
  },
  {
    "path": "tests/Unit/QueryStringTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\QueryString;\nuse Clickfwd\\Yoyo\\Request;\nuse Clickfwd\\Yoyo\\Yoyo;\n\nbeforeEach(function () {\n    $request = new Request();\n    Yoyo::getInstance()->bindRequest($request);\n    $request->mock([], ['HTTP_HX_CURRENT_URL' => 'http://example.com/page']);\n});\n\nit('returns only declared query string keys', function () {\n    $qs = new QueryString(\n        ['count' => 0, 'name' => 'default'],\n        ['count' => 5, 'name' => 'test', 'extra' => 'ignored'],\n        ['count', 'name']\n    );\n\n    $params = $qs->getQueryParams();\n\n    expect($params)->toHaveKey('count');\n    expect($params)->toHaveKey('name');\n    expect($params)->not->toHaveKey('extra');\n});\n\nit('merges defaults with new values in getQueryParams', function () {\n    $qs = new QueryString(\n        ['count' => 0],\n        ['count' => 5],\n        ['count']\n    );\n\n    expect($qs->getQueryParams())->toBe(['count' => 5]);\n});\n\nit('returns empty array when no URL is available', function () {\n    $request = new Request();\n    Yoyo::getInstance()->bindRequest($request);\n    $request->mock([], []);\n\n    $qs = new QueryString(['count' => 0], ['count' => 5], ['count']);\n\n    expect($qs->getPageQueryParams())->toBe([]);\n});\n\nit('removes params matching default values from page query params', function () {\n    $qs = new QueryString(\n        ['count' => 0],\n        ['count' => 0],\n        ['count']\n    );\n\n    $params = $qs->getPageQueryParams();\n\n    expect($params)->not->toHaveKey('count');\n});\n\nit('removes empty string values from page query params', function () {\n    $qs = new QueryString(\n        ['filter' => ''],\n        ['filter' => ''],\n        ['filter']\n    );\n\n    $params = $qs->getPageQueryParams();\n\n    expect($params)->not->toHaveKey('filter');\n});\n\nit('preserves existing query string params from URL', function () {\n    $request = new Request();\n    Yoyo::getInstance()->bindRequest($request);\n    $request->mock([], ['HTTP_HX_CURRENT_URL' => 'http://example.com/page?existing=value']);\n\n    $qs = new QueryString(\n        ['count' => 0],\n        ['count' => 5],\n        ['count']\n    );\n\n    $params = $qs->getPageQueryParams();\n\n    expect($params)->toHaveKey('existing');\n    expect($params['existing'])->toBe('value');\n    expect($params)->toHaveKey('count');\n    expect($params['count'])->toBe(5);\n});\n\nit('keeps params that differ from defaults', function () {\n    $qs = new QueryString(\n        ['count' => 0],\n        ['count' => 10],\n        ['count']\n    );\n\n    $params = $qs->getPageQueryParams();\n\n    expect($params)->toHaveKey('count');\n    expect($params['count'])->toBe(10);\n});\n\nit('filters new values to only declared keys in page query params', function () {\n    $qs = new QueryString(\n        ['count' => 0],\n        ['count' => 5, 'secret' => 'hidden'],\n        ['count']\n    );\n\n    $params = $qs->getPageQueryParams();\n\n    expect($params)->not->toHaveKey('secret');\n});\n"
  },
  {
    "path": "tests/Unit/RegressionTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\ClassHelpers;\nuse Clickfwd\\Yoyo\\Component;\nuse Clickfwd\\Yoyo\\ComponentResolver;\nuse Clickfwd\\Yoyo\\QueryString;\nuse Clickfwd\\Yoyo\\Request;\nuse Clickfwd\\Yoyo\\Yoyo;\nuse Clickfwd\\Yoyo\\YoyoHelpers;\nuse Tests\\App\\Yoyo\\Counter;\n\n// --- Operator precedence fix in ClassHelpers::getPublicProperties ---\n\nit('applies correct operator precedence with baseClass filter', function () {\n    ClassHelpers::flushCache();\n\n    $resolver = new ComponentResolver(Yoyo::getInstance());\n    $component = new Counter($resolver, 'test-counter', 'counter');\n\n    // With baseClass: should exclude base class properties but include subclass properties\n    $props = ClassHelpers::getPublicProperties($component, Component::class);\n\n    expect($props)->toContain('count');\n    expect($props)->not->toContain('redirectTo'); // From Redirector trait on base class\n});\n\nit('returns all public properties when no baseClass is provided', function () {\n    ClassHelpers::flushCache();\n\n    $resolver = new ComponentResolver(Yoyo::getInstance());\n    $component = new Counter($resolver, 'test-counter', 'counter');\n\n    // Without baseClass: should include properties from all classes including Component\n    $props = ClassHelpers::getPublicProperties($component, null);\n\n    // Should include at least the subclass property\n    expect($props)->toContain('count');\n});\n\n// --- fullUrl() using $this->server instead of raw $_SERVER ---\n\nit('uses mocked server data in fullUrl() instead of raw $_SERVER', function () {\n    $request = new Request();\n\n    $request->mock([], [\n        'HTTP_HOST' => 'mocked.example.com',\n        'REQUEST_URI' => '/mocked-path',\n        'HTTPS' => 'on',\n    ]);\n\n    $url = $request->fullUrl();\n\n    expect($url)->toBe('https://mocked.example.com/mocked-path');\n});\n\nit('returns HX_CURRENT_URL when available from mocked server', function () {\n    $request = new Request();\n\n    $request->mock([], [\n        'HTTP_HX_CURRENT_URL' => 'https://htmx.example.com/page',\n        'HTTP_HOST' => 'other.com',\n    ]);\n\n    expect($request->fullUrl())->toBe('https://htmx.example.com/page');\n});\n\nit('returns null when no host is available', function () {\n    $request = new Request();\n\n    $request->mock([], []);\n\n    expect($request->fullUrl())->toBeNull();\n});\n\nit('constructs http URL when HTTPS is not set', function () {\n    $request = new Request();\n\n    $request->mock([], [\n        'HTTP_HOST' => 'example.com',\n        'REQUEST_URI' => '/path?query',\n    ]);\n\n    expect($request->fullUrl())->toBe('http://example.com/path?query');\n});\n\n// --- test_json: preserves legitimate backslashes, handles WordPress magic quotes ---\n\nit('decodes WordPress magic-quoted JSON via stripslashes fallback', function () {\n    // WordPress wp_magic_quotes() adds backslashes to $_REQUEST values.\n    // HTMX sends: [\"jreviews-cp::pages.browse-listings\"]\n    // After magic quotes: [\\\"jreviews-cp::pages.browse-listings\\\"]\n    $magicQuoted = addslashes('[\"jreviews-cp::pages.browse-listings\"]');\n    $decoded = YoyoHelpers::test_json($magicQuoted);\n\n    expect($decoded)->toBe(['jreviews-cp::pages.browse-listings']);\n});\n\nit('does not strip legitimate backslashes from JSON values', function () {\n    // A JSON string containing a backslash in a value (e.g., a Windows path)\n    $json = '{\"path\":\"C:\\\\\\\\Users\\\\\\\\test\"}';\n    $decoded = YoyoHelpers::test_json($json);\n\n    expect($decoded)->toBe(['path' => 'C:\\\\Users\\\\test']);\n});\n\nit('still decodes valid JSON without backslashes', function () {\n    $json = '{\"key\":\"value\",\"number\":42}';\n    $decoded = YoyoHelpers::test_json($json);\n\n    expect($decoded)->toBe(['key' => 'value', 'number' => 42]);\n});\n\nit('returns null for non-JSON strings', function () {\n    expect(YoyoHelpers::test_json('just a string'))->toBeNull();\n    expect(YoyoHelpers::test_json(''))->toBeNull();\n});\n\nit('returns array input as-is from test_json', function () {\n    $input = ['already' => 'decoded'];\n    expect(YoyoHelpers::test_json($input))->toBe($input);\n});\n\nit('returns null for non-string non-array input', function () {\n    expect(YoyoHelpers::test_json(42))->toBeNull();\n    expect(YoyoHelpers::test_json(null))->toBeNull();\n    expect(YoyoHelpers::test_json(true))->toBeNull();\n});\n\n// --- randString uses random_int (not predictable rand) ---\n\nit('generates string of correct length', function () {\n    expect(strlen(YoyoHelpers::randString(8)))->toBe(8);\n    expect(strlen(YoyoHelpers::randString(16)))->toBe(16);\n    expect(strlen(YoyoHelpers::randString(1)))->toBe(1);\n});\n\nit('generates only alphanumeric lowercase characters', function () {\n    $result = YoyoHelpers::randString(100);\n    expect($result)->toMatch('/^[0-9a-z]+$/');\n});\n\nit('generates different strings on consecutive calls', function () {\n    $results = [];\n    for ($i = 0; $i < 20; $i++) {\n        $results[] = YoyoHelpers::randString(16);\n    }\n\n    // All should be unique (extremely high probability with 16-char strings)\n    expect(count(array_unique($results)))->toBe(20);\n});\n\n// --- Configuration URL escaping ---\n\nit('uses json_encode for URL in JavaScript init code', function () {\n    $initCode = \\Clickfwd\\Yoyo\\Services\\Configuration::javascriptInitCode(false);\n\n    // The URL value should be json_encode'd (double-quoted), not single-quoted interpolation\n    // json_encode('') produces '\"\"', so output should be: Yoyo.url = \"\";\n    expect($initCode)->toMatch('/Yoyo\\.url = \".*\";/');\n    expect($initCode)->not->toMatch(\"/Yoyo\\\\.url = '.*';/\");\n});\n\n// --- QueryString operator precedence fix ---\n\nit('removes query params matching defaults with correct precedence', function () {\n    $_SERVER = ['HTTP_HX_CURRENT_URL' => 'http://example.com/?count=5'];\n\n    $request = new Request();\n    Yoyo::getInstance()->bindRequest($request);\n    $request->mock([], ['HTTP_HX_CURRENT_URL' => 'http://example.com/?count=5']);\n\n    $qs = new QueryString(\n        ['count' => 0],      // defaults\n        ['count' => 0],      // new (matches default)\n        ['count']             // keys\n    );\n\n    $pageParams = $qs->getPageQueryParams();\n\n    // count=0 matches the default, so it should be removed\n    expect($pageParams)->not->toHaveKey('count');\n});\n\nit('keeps query params that differ from defaults', function () {\n    $request = new Request();\n    Yoyo::getInstance()->bindRequest($request);\n    $request->mock([], ['HTTP_HX_CURRENT_URL' => 'http://example.com/']);\n\n    $qs = new QueryString(\n        ['count' => 0],\n        ['count' => 5],\n        ['count']\n    );\n\n    $pageParams = $qs->getPageQueryParams();\n\n    expect($pageParams)->toHaveKey('count');\n    expect($pageParams['count'])->toBe(5);\n});\n"
  },
  {
    "path": "tests/Unit/RequestTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Request;\n\nbeforeEach(function () {\n    $_REQUEST = ['name' => 'test', 'count' => '5', 'data' => '{\"key\":\"value\"}'];\n    $_SERVER = ['REQUEST_METHOD' => 'GET'];\n});\n\nit('returns all values with JSON decoded', function () {\n    $request = new Request();\n    $all = $request->all();\n    expect($all['name'])->toBe('test');\n    expect($all['data'])->toBe(['key' => 'value']);\n});\n\nit('returns same result on repeated all() calls', function () {\n    $request = new Request();\n    $first = $request->all();\n    $second = $request->all();\n    expect($first)->toEqual($second);\n});\n\nit('decodes JSON on get()', function () {\n    $request = new Request();\n    expect($request->get('data'))->toBe(['key' => 'value']);\n});\n\nit('returns default for missing key', function () {\n    $request = new Request();\n    expect($request->get('missing', 'default'))->toBe('default');\n});\n\nit('excludes specified keys', function () {\n    $request = new Request();\n    $result = $request->except(['name']);\n    expect($result)->not->toHaveKey('name');\n    expect($result)->toHaveKey('count');\n});\n\nit('returns only specified keys', function () {\n    $request = new Request();\n    $result = $request->only(['name']);\n    expect($result)->toHaveKey('name');\n    expect($result)->not->toHaveKey('count');\n});\n\nit('filters by prefix', function () {\n    $_REQUEST = ['yoyo:id' => '123', 'yoyo:name' => 'test', 'other' => 'val'];\n    $request = new Request();\n    $result = $request->startsWith('yoyo:');\n    expect($result)->toHaveKey('yoyo:id');\n    expect($result)->toHaveKey('yoyo:name');\n    expect($result)->not->toHaveKey('other');\n});\n\nit('reflects set() in subsequent get()', function () {\n    $request = new Request();\n    $request->set('new_key', 'new_val');\n    expect($request->get('new_key'))->toBe('new_val');\n});\n\nit('reflects merge() in subsequent all()', function () {\n    $request = new Request();\n    $request->merge(['extra' => 'data']);\n    $all = $request->all();\n    expect($all)->toHaveKey('extra');\n});\n\nit('respects dropped keys', function () {\n    $request = new Request();\n    $request->drop('name');\n    expect($request->get('name', 'default'))->toBe('default');\n});\n\nit('returns request method', function () {\n    $request = new Request();\n    expect($request->method())->toBe('GET');\n});\n\nit('detects yoyo request', function () {\n    $_SERVER = ['HTTP_HX_REQUEST' => true];\n    $request = new Request();\n    expect($request->isYoyoRequest())->toBeTruthy();\n});\n\nit('returns header value', function () {\n    $_SERVER = ['HTTP_CUSTOM_HEADER' => 'value'];\n    $request = new Request();\n    expect($request->header('CUSTOM_HEADER'))->toBe('value');\n});\n\nit('resets all data', function () {\n    $request = new Request();\n    $request->reset();\n    expect($request->all())->toBeEmpty();\n});\n\n// --- Cache invalidation tests ---\n\nit('invalidates all() cache after set()', function () {\n    $request = new Request();\n    $before = $request->all();\n\n    $request->set('new_key', 'new_val');\n    $after = $request->all();\n\n    expect($after)->toHaveKey('new_key');\n    expect($before)->not->toHaveKey('new_key');\n});\n\nit('invalidates all() cache after merge()', function () {\n    $request = new Request();\n    $before = $request->all();\n\n    $request->merge(['merged' => 'data']);\n    $after = $request->all();\n\n    expect($after)->toHaveKey('merged');\n});\n\nit('invalidates all() cache after reset()', function () {\n    $request = new Request();\n    $request->all(); // populate cache\n    $request->reset();\n\n    expect($request->all())->toBeEmpty();\n});\n\nit('invalidates all() cache after mock()', function () {\n    $request = new Request();\n    $request->all(); // populate cache\n\n    $request->mock(['mocked' => 'value'], ['REQUEST_METHOD' => 'POST']);\n    $after = $request->all();\n\n    expect($after)->toHaveKey('mocked');\n    expect($after)->not->toHaveKey('name');\n});\n\nit('returns cached all() result (strict identity)', function () {\n    $request = new Request();\n    $first = $request->all();\n    $second = $request->all();\n\n    expect($first)->toBe($second);\n});\n\n// --- JSON scalar null/false decoding (regression) ---\n\nit('decodes JSON scalar null in all()', function () {\n    $_REQUEST = ['iconSlot' => 'null'];\n    $request = new Request();\n    $all = $request->all();\n\n    expect($all)->toHaveKey('iconSlot');\n    expect($all['iconSlot'])->toBeNull();\n});\n\nit('decodes JSON scalar null in get()', function () {\n    $_REQUEST = ['iconSlot' => 'null'];\n    $request = new Request();\n\n    expect($request->get('iconSlot', 'fallback'))->toBeNull();\n});\n\nit('decodes JSON scalar false in all()', function () {\n    $_REQUEST = ['enabled' => 'false'];\n    $request = new Request();\n    $all = $request->all();\n\n    expect($all['enabled'])->toBeFalse();\n});\n\nit('decodes JSON scalar false in get()', function () {\n    $_REQUEST = ['enabled' => 'false'];\n    $request = new Request();\n\n    expect($request->get('enabled'))->toBeFalse();\n});\n\nit('keeps non-JSON strings unchanged in all()', function () {\n    $_REQUEST = ['greeting' => 'hello'];\n    $request = new Request();\n    $all = $request->all();\n\n    expect($all['greeting'])->toBe('hello');\n});\n"
  },
  {
    "path": "tests/Unit/ResponseHeadersTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Services\\Response;\n\nit('sets HX-Location header', function () {\n    $response = new Response();\n    $result = $response->location('/new-path');\n\n    expect($response->getHeaders())->toHaveKey('HX-Location');\n    expect($response->getHeaders()['HX-Location'])->toBe('/new-path');\n    expect($result)->toBe($response); // fluent interface\n});\n\nit('sets HX-Push-Url header', function () {\n    $response = new Response();\n    $response->pushUrl('/pushed');\n\n    expect($response->getHeaders()['HX-Push-Url'])->toBe('/pushed');\n});\n\nit('sets HX-Redirect header', function () {\n    $response = new Response();\n    $response->redirect('/redirected');\n\n    expect($response->getHeaders()['HX-Redirect'])->toBe('/redirected');\n});\n\nit('sets HX-Refresh header', function () {\n    $response = new Response();\n    $response->refresh();\n\n    expect($response->getHeaders()['HX-Refresh'])->toBe('true');\n});\n\nit('sets HX-Replace-Url header', function () {\n    $response = new Response();\n    $response->replaceUrl('/replaced');\n\n    expect($response->getHeaders()['HX-Replace-Url'])->toBe('/replaced');\n});\n\nit('sets HX-Reswap header', function () {\n    $response = new Response();\n    $response->reswap('innerHTML');\n\n    expect($response->getHeaders()['HX-Reswap'])->toBe('innerHTML');\n});\n\nit('sets HX-Reselect header', function () {\n    $response = new Response();\n    $response->reselect('#content');\n\n    expect($response->getHeaders()['HX-Reselect'])->toBe('#content');\n});\n\nit('sets HX-Retarget header', function () {\n    $response = new Response();\n    $response->retarget('#target');\n\n    expect($response->getHeaders()['HX-Retarget'])->toBe('#target');\n});\n\nit('sets HX-Trigger header', function () {\n    $response = new Response();\n    $response->trigger('myEvent');\n\n    expect($response->getHeaders()['HX-Trigger'])->toBe('myEvent');\n});\n\nit('sets HX-Trigger-After-Swap header', function () {\n    $response = new Response();\n    $response->triggerAfterSwap('swapEvent');\n\n    expect($response->getHeaders()['HX-Trigger-After-Swap'])->toBe('swapEvent');\n});\n\nit('sets HX-Trigger-After-Settle header', function () {\n    $response = new Response();\n    $response->triggerAfterSettle('settleEvent');\n\n    expect($response->getHeaders()['HX-Trigger-After-Settle'])->toBe('settleEvent');\n});\n\n// --- Response class tests ---\n\nit('sets and retrieves status code', function () {\n    $response = new Response();\n    $response->status(404);\n\n    expect($response->getStatusCode())->toBe(404);\n});\n\nit('defaults to 200 status code', function () {\n    $response = new Response();\n\n    expect($response->getStatusCode())->toBe(200);\n});\n\nit('returns content from send()', function () {\n    $response = new Response();\n    $result = $response->send('hello');\n\n    expect($result)->toBe('hello');\n});\n\nit('returns empty string from send() with no content', function () {\n    $response = new Response();\n    $result = $response->send();\n\n    expect($result)->toBe('');\n});\n\nit('merges headers with setHeaders()', function () {\n    $response = new Response();\n    $response->header('X-First', 'one');\n    $response->setHeaders(['X-Second' => 'two', 'X-Third' => 'three']);\n\n    $headers = $response->getHeaders();\n\n    expect($headers)->toHaveKey('X-First');\n    expect($headers)->toHaveKey('X-Second');\n    expect($headers)->toHaveKey('X-Third');\n});\n\nit('supports fluent interface chaining', function () {\n    $response = new Response();\n\n    $result = $response\n        ->status(201)\n        ->header('X-Custom', 'value')\n        ->location('/path')\n        ->pushUrl('/url');\n\n    expect($result)->toBe($response);\n    expect($response->getStatusCode())->toBe(201);\n    expect($response->getHeaders())->toHaveKey('X-Custom');\n    expect($response->getHeaders())->toHaveKey('HX-Location');\n    expect($response->getHeaders())->toHaveKey('HX-Push-Url');\n});\n"
  },
  {
    "path": "tests/Unit/SecurityTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Exceptions\\ComponentMethodNotFound;\nuse Clickfwd\\Yoyo\\Exceptions\\NonPublicComponentMethodCall;\nuse Clickfwd\\Yoyo\\Request;\nuse Clickfwd\\Yoyo\\Services\\Response;\n\nuse function Tests\\mockYoyoGetRequest;\nuse function Tests\\resetYoyoRequest;\nuse function Tests\\update;\nuse function Tests\\yoyo_update;\nuse function Tests\\yoyo_view;\n\nbeforeAll(function () {\n    yoyo_view();\n});\n\n// --- Header injection prevention ---\n\nit('strips newlines from header names', function () {\n    $response = new Response();\n    $response->header(\"X-Custom\\r\\nInjected: bad\", 'value');\n\n    $headers = $response->getHeaders();\n\n    // The key should have newlines stripped\n    expect($headers)->toHaveKey('X-CustomInjected: bad');\n    expect($headers)->not->toHaveKey(\"X-Custom\\r\\nInjected: bad\");\n});\n\nit('strips newlines from header values', function () {\n    $response = new Response();\n    $response->header('X-Custom', \"value\\r\\nInjected-Header: evil\");\n\n    $headers = $response->getHeaders();\n\n    expect($headers['X-Custom'])->toBe('valueInjected-Header: evil');\n    expect($headers['X-Custom'])->not->toContain(\"\\r\\n\");\n});\n\nit('strips null bytes from header values', function () {\n    $response = new Response();\n    $response->header('X-Custom', \"value\\0hidden\");\n\n    $headers = $response->getHeaders();\n\n    expect($headers['X-Custom'])->toBe('valuehidden');\n});\n\nit('preserves array header values without string sanitization', function () {\n    $response = new Response();\n    $response->header('Yoyo-Emit', ['event' => 'test']);\n\n    $headers = $response->getHeaders();\n\n    expect($headers['Yoyo-Emit'])->toBe(['event' => 'test']);\n});\n\n// --- Component action invocation security ---\n\nit('prevents calling boot method via action', function () {\n    update('counter', 'boot');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('prevents calling setAction method via action', function () {\n    update('counter', 'setAction');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('prevents calling getComponentId method via action', function () {\n    update('counter', 'getComponentId');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('prevents calling getVariables method via action', function () {\n    update('counter', 'getVariables');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('prevents calling getName method via action', function () {\n    update('counter', 'getName');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('prevents calling getListeners method via action', function () {\n    update('counter', 'getListeners');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('prevents calling getProps method via action', function () {\n    update('counter', 'getProps');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('prevents calling getQueryString method via action', function () {\n    update('counter', 'getQueryString');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('prevents calling spinning method via action', function () {\n    update('counter', 'spinning');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('prevents calling set method via action', function () {\n    update('counter', 'set');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('prevents calling forgetComputed method via action', function () {\n    update('counter', 'forgetComputed');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('prevents calling skipRender method via action', function () {\n    update('counter', 'skipRender');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('prevents calling emit method via action', function () {\n    update('counter', 'emit');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('prevents calling __construct method via action', function () {\n    update('counter', '__construct');\n})->throws(NonPublicComponentMethodCall::class);\n\nit('allows calling user-defined public action', function () {\n    $output = update('counter', 'increment');\n    expect($output)->toContain('The count is now 1');\n});\n\nit('throws ComponentMethodNotFound for non-existent method', function () {\n    update('counter', 'nonExistentMethod');\n})->throws(ComponentMethodNotFound::class);\n\n// --- Request data isolation ---\n\nit('isolates mocked request data from real $_SERVER', function () {\n    $request = new Request();\n\n    // Mock with no HTTP_HOST - should return null for fullUrl\n    $request->mock(['component' => 'test'], []);\n\n    expect($request->method())->toBe('GET'); // No REQUEST_METHOD defaults to GET\n    expect($request->fullUrl())->toBeNull(); // No HTTP_HOST means null URL\n\n    // Mock with full server data\n    $request->mock(\n        ['component' => 'test'],\n        ['HTTP_HOST' => 'mocked.test', 'REQUEST_METHOD' => 'POST', 'REQUEST_URI' => '/mocked']\n    );\n\n    expect($request->method())->toBe('POST');\n    expect($request->fullUrl())->toBe('http://mocked.test/mocked');\n\n    // Mock again with different host - should use new mocked data, not previous\n    $request->mock(\n        [],\n        ['HTTP_HOST' => 'other.test', 'REQUEST_URI' => '/other']\n    );\n\n    expect($request->fullUrl())->toBe('http://other.test/other');\n});\n\n// --- Component method access via URL path ---\n\nit('prevents accessing protected methods through URL action parameter', function () {\n    mockYoyoGetRequest('http://example.com/', 'protected-methods/secret');\n\n    expect(fn () => yoyo_update())->toThrow(NonPublicComponentMethodCall::class);\n\n    resetYoyoRequest();\n});\n"
  },
  {
    "path": "tests/Unit/UrlStateManagerServiceTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Services\\Response;\nuse Clickfwd\\Yoyo\\Services\\UrlStateManagerService;\nuse Clickfwd\\Yoyo\\Yoyo;\n\nuses()->group('services');\n\nbeforeEach(function () {\n    $ref = new ReflectionClass(Response::class);\n    $prop = $ref->getProperty('instance');\n    $prop->setAccessible(true);\n    $prop->setValue(null, null);\n});\n\nafterEach(function () {\n    Yoyo::request()->reset();\n});\n\nit('sets push state header when URL changes', function () {\n    Yoyo::request()->mock([], [\n        'REQUEST_METHOD' => 'GET',\n        'HTTP_HX_CURRENT_URL' => 'http://example.com/page',\n    ]);\n\n    $service = new UrlStateManagerService();\n    $service->pushState(['count' => 1]);\n\n    $headers = Response::getInstance()->getHeaders();\n    expect($headers)->toHaveKey('Yoyo-Push');\n    expect($headers['Yoyo-Push'])->toContain('count=1');\n});\n\nit('does not set push header on POST requests', function () {\n    Yoyo::request()->mock([], [\n        'REQUEST_METHOD' => 'POST',\n        'HTTP_HX_CURRENT_URL' => 'http://example.com/page',\n    ]);\n\n    $service = new UrlStateManagerService();\n    $service->pushState(['count' => 1]);\n\n    $headers = Response::getInstance()->getHeaders();\n    expect($headers)->not->toHaveKey('Yoyo-Push');\n});\n\nit('does not set push header when URL is null', function () {\n    Yoyo::request()->mock([], [\n        'REQUEST_METHOD' => 'GET',\n    ]);\n\n    $service = new UrlStateManagerService();\n    $service->pushState(['count' => 1]);\n\n    $headers = Response::getInstance()->getHeaders();\n    expect($headers)->not->toHaveKey('Yoyo-Push');\n});\n\nit('does not set push header when URL stays the same', function () {\n    Yoyo::request()->mock([], [\n        'REQUEST_METHOD' => 'GET',\n        'HTTP_HX_CURRENT_URL' => 'http://example.com/page',\n    ]);\n\n    $service = new UrlStateManagerService();\n    $service->pushState([]);\n\n    $headers = Response::getInstance()->getHeaders();\n    expect($headers)->not->toHaveKey('Yoyo-Push');\n});\n\nit('preserves port in push URL', function () {\n    Yoyo::request()->mock([], [\n        'REQUEST_METHOD' => 'GET',\n        'HTTP_HX_CURRENT_URL' => 'http://localhost:8080/page',\n    ]);\n\n    $service = new UrlStateManagerService();\n    $service->pushState(['foo' => 'bar']);\n\n    $headers = Response::getInstance()->getHeaders();\n    expect($headers['Yoyo-Push'])->toContain('localhost:8080');\n});\n\nit('does not set push header when HX-Replace-Url is already set', function () {\n    Yoyo::request()->mock([], [\n        'REQUEST_METHOD' => 'GET',\n        'HTTP_HX_CURRENT_URL' => 'http://example.com/page',\n    ]);\n\n    Response::getInstance()->replaceUrl('/custom-url');\n\n    $service = new UrlStateManagerService();\n    $service->pushState(['count' => 1]);\n\n    $headers = Response::getInstance()->getHeaders();\n    expect($headers)->toHaveKey('HX-Replace-Url');\n    expect($headers)->not->toHaveKey('Yoyo-Push');\n});\n\nit('does not set push header when HX-Push-Url is already set', function () {\n    Yoyo::request()->mock([], [\n        'REQUEST_METHOD' => 'GET',\n        'HTTP_HX_CURRENT_URL' => 'http://example.com/page',\n    ]);\n\n    Response::getInstance()->pushUrl('/custom-url');\n\n    $service = new UrlStateManagerService();\n    $service->pushState(['count' => 1]);\n\n    $headers = Response::getInstance()->getHeaders();\n    expect($headers)->toHaveKey('HX-Push-Url');\n    expect($headers)->not->toHaveKey('Yoyo-Push');\n});\n\nit('builds URL without query string when no params', function () {\n    Yoyo::request()->mock([], [\n        'REQUEST_METHOD' => 'GET',\n        'HTTP_HX_CURRENT_URL' => 'http://example.com/page?old=param',\n    ]);\n\n    $service = new UrlStateManagerService();\n    $service->pushState([]);\n\n    $headers = Response::getInstance()->getHeaders();\n    // When pushState with empty params, the new URL has no query string\n    // This differs from current URL so it should set the header\n    expect($headers)->toHaveKey('Yoyo-Push');\n    expect($headers['Yoyo-Push'])->toBe('http://example.com/page');\n});\n"
  },
  {
    "path": "tests/Unit/ViewTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\View;\n\nit('renders a template with variables', function () {\n    $view = new View(__DIR__.'/../app/resources/views/yoyo');\n\n    $output = $view->render('foo', ['spinning' => false]);\n\n    expect($output)->toContain('default foo');\n});\n\nit('renders a template in spinning state', function () {\n    $view = new View(__DIR__.'/../app/resources/views/yoyo');\n\n    $output = $view->render('foo', ['spinning' => true]);\n\n    expect($output)->toContain('default bar');\n});\n\nit('finds existing template via exists()', function () {\n    $view = new View(__DIR__.'/../app/resources/views/yoyo');\n\n    $path = $view->exists('foo');\n\n    expect($path)->toBeString();\n    expect($path)->toContain('foo.php');\n});\n\nit('caches template path on repeated exists() calls', function () {\n    $view = new View(__DIR__.'/../app/resources/views/yoyo');\n\n    $first = $view->exists('foo');\n    $second = $view->exists('foo');\n\n    expect($first)->toBe($second);\n});\n\nit('throws exception for non-existent template', function () {\n    $view = new View(__DIR__.'/../app/resources/views/yoyo');\n\n    $view->exists('nonexistent-template');\n})->throws(InvalidArgumentException::class);\n\nit('supports dot notation for subdirectories', function () {\n    $view = new View(__DIR__.'/../app/resources/views/yoyo');\n\n    $path = $view->exists('account.login');\n\n    expect($path)->toContain('account/login.php');\n});\n\nit('adds location and finds templates there', function () {\n    $view = new View(__DIR__.'/../app/resources/views/yoyo');\n    $view->addLocation(__DIR__.'/../app-another/views');\n\n    // The original location should still work\n    $path = $view->exists('counter');\n    expect($path)->toContain('app/resources/views/yoyo/counter.php');\n});\n\nit('prepends location with higher priority', function () {\n    $view = new View(__DIR__.'/../app/resources/views/yoyo');\n    $view->prependLocation(__DIR__.'/../app-another/views');\n\n    // The prepended location should take priority\n    $output = $view->render('foo', ['spinning' => false]);\n    expect($output)->toContain('other foo from another app');\n});\n\nit('supports namespaced views', function () {\n    $view = new View(__DIR__.'/../app/resources/views/yoyo');\n    $view->addNamespace('pkg', __DIR__.'/../app-another/views');\n\n    $path = $view->exists('pkg::foo');\n\n    expect($path)->toContain('app-another/views/foo.php');\n});\n\nit('detects hint information in view names', function () {\n    $view = new View(__DIR__.'/../app/resources/views/yoyo');\n\n    expect($view->hasHintInformation('pkg::foo'))->toBeTrue();\n    expect($view->hasHintInformation('foo'))->toBeFalse();\n});\n\nit('throws exception for unknown namespace', function () {\n    $view = new View(__DIR__.'/../app/resources/views/yoyo');\n\n    $view->exists('unknown::foo');\n})->throws(InvalidArgumentException::class);\n\nit('throws exception for invalid namespaced format', function () {\n    $view = new View(__DIR__.'/../app/resources/views/yoyo');\n    $view->addNamespace('pkg', __DIR__.'/../app-another/views');\n\n    $view->exists('pkg::a::b');\n})->throws(InvalidArgumentException::class);\n\nit('prepends namespace hints with higher priority', function () {\n    $view = new View(__DIR__.'/../app/resources/views/yoyo');\n    $view->addNamespace('pkg', __DIR__.'/../app/resources/views/yoyo');\n    $view->prependNamespace('pkg', __DIR__.'/../app-another/views');\n\n    $output = $view->render('pkg::foo', ['spinning' => false]);\n    expect($output)->toContain('other foo from another app');\n});\n\nit('throws exception for makeFromString with native view provider', function () {\n    $view = new View(__DIR__.'/../app/resources/views/yoyo');\n\n    $view->makeFromString('content', []);\n})->throws(\\Exception::class, 'Views from strings are not supported');\n\nit('accepts multiple paths in constructor', function () {\n    $view = new View([\n        __DIR__.'/../app/resources/views/yoyo',\n        __DIR__.'/../app-another/views',\n    ]);\n\n    // Should find templates from first path\n    $path = $view->exists('counter');\n    expect($path)->toContain('counter.php');\n});\n"
  },
  {
    "path": "tests/Unit/YoyoCompileTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\YoyoCompiler;\n\nuse function Tests\\compile_html;\nuse function Tests\\compile_html_with_vars;\nuse function Tests\\encode_vals;\nuse function Tests\\hxattr;\nuse function Tests\\yoattr;\nuse function Tests\\yoprefix_value;\n\nuses()->group('compiler');\n\nit('uses hardcoded id attribute', function () {\n    expect(compile_html('test', '<div id=\"test\"></div>'))->toContain('id=\"test\"');\n});\n\nit('adds yoyo:id to component root', function () {\n    expect(compile_html('test', '<div id=\"test\"></div>'))\n        ->toContain(hxattr('vals', encode_vals([yoprefix_value('id') => 'test'])));\n});\n\nit('includes component `name` attribute', function () {\n    expect(compile_html('foo', '<div></div>'))\n        ->toContain(yoattr('name', 'foo'));\n});\n\nit('includes all attributes', function () {\n    $name = 'test';\n\n    expect(compile_html($name, '<div></div>'))\n        ->toContain(YoyoCompiler::YOYO_PREFIX.'=\"\"')\n        ->toContain(hxattr('get', YoyoCompiler::COMPONENT_DEFAULT_ACTION))\n        ->toContain('class=\"'.YoyoCompiler::COMPONENT_WRAPPER_CLASS.'\"')\n        ->toContain(hxattr('trigger', 'refresh'))\n        ->toContain(hxattr('ext', YoyoCompiler::YOYO_PREFIX))\n        ->toContain(hxattr('target', 'this'))\n        ->toContain(hxattr('vals'))\n        ->toContain(yoattr('name', $name))\n        ->toMatch('/id=\"'.YoyoCompiler::YOYO_PREFIX.'-[a-z0-9]+/i');\n});\n\nit('wraps output in a new div element when multiple child nodes found', function () {\n    expect(compile_html('foo', '<div></div><div></div>'))\n        ->toMatch('/<div .*><div><\\/div><div><\\/div><\\/div>/');\n});\n\nit('excludes root node on innerHTML swap', function () {\n    $html = compile_html('foo', '<div '.yoattr('swap', 'innerHTML').'><div>Inner Text</div></div>', $spinning = true);\n\n    expect($html)->toEqual('<div>Inner Text</div>');\n});\n\nit('includes root node on outerHTML swap', function () {\n    $html = compile_html('foo', '<div '.yoattr('swap', 'outerHTML').'><div>Inner Text</div></div>', $spinning = true);\n\n    expect($html)->toMatch('/<div.*'.hxattr('swap', 'outerHTML').'.*><div>Inner Text<\\/div><\\/div>/');\n});\n\nit('skips elements with `yoyo:ignore` attribute', function () {\n    expect(compile_html('foo', '<div '.yoattr('ignore').'>Foo</div>'))\n        ->toEqual('<div>Foo</div>');\n});\n\nit('skips elements with `yoyo:ignore` attribute on update', function () {\n    expect(compile_html('foo', '<div '.yoattr('ignore').'>Foo</div>', $spinning = true))\n        ->toEqual('<div>Foo</div>');\n});\n\nit('includes additional extensions', function () {\n    expect(compile_html('foo', '<div '.yoattr('ext', 'new-ext').'></div>'))\n        ->toMatch('/'.hxattr('ext', 'yoyo, new-ext').'/');\n});\n\nit('detects file inputs and adds the `encoding` attribute to the root form', function () {\n    expect(compile_html('foo', '<form><input type=\"file\"/></form>'))\n        ->toMatch('/'.hxattr('encoding', 'multipart\\/form-data').'/');\n});\n\nit('detects file inputs and adds the `encoding` attribute to child forms', function () {\n    expect(compile_html('foo', '<div><form><input type=\"file\"/></form></div>'))\n        ->toMatch('/'.hxattr('encoding', 'multipart\\/form-data').'/');\n});\n\nit('adds trigger method and `id` to elements with yoyo tag', function () {\n    expect(compile_html('foo', '<div><button yoyo>foo</button></div>'))\n        ->toMatch('/\\<button hx-get=\"render\" id=\"yoyo-(.*)-1\">foo\\<\\/button\\>/');\n});\n\nit('preserves reactive element `id` if already present', function () {\n    expect(compile_html('foo', '<div><button yoyo id=\"a\">foo</button></div>'))\n        ->toMatch('/\\<button id=\"a\" hx-get=\"render\">foo\\<\\/button\\>/');\n});\n\nit('parses and merges yoyo:vals attribute in root node', function () {\n    expect(compile_html('foo', '<div id=\"foo\" yoyo:vals=\\'{\"foo\":\"bar\"}\\'></div>'))\n    ->toContain(hxattr('vals', encode_vals([yoprefix_value('id') => 'foo', 'foo' => 'bar'])));\n});\n\nit('parses and merges single yoyo:val attribute in root node', function () {\n    expect(compile_html('foo', '<div id=\"foo\" yoyo:val.count=\"1\"></div>'))\n        ->toContain(hxattr('vals', encode_vals([yoprefix_value('id') => 'foo', 'count' => 1])));\n});\n\nit('converts kebab-case val key to camel-case', function () {\n    expect(compile_html('foo', '<div id=\"foo\" yoyo:val.filter-foo=\"bar\"></div>'))\n        ->toContain(hxattr('vals', encode_vals([yoprefix_value('id') => 'foo', 'filterFoo' => 'bar'])));\n});\n\nit('parses and merges single yoyo:val attribute in child node', function () {\n    expect(compile_html('foo', '<div><button yoyo:val.count=\"1\"></button></div>'))\n        ->toContain(hxattr('vals', encode_vals(['count' => 1])));\n});\n\nit('parses and merges single yoyo:val attribute in root and child nodes', function () {\n    expect(compile_html('foo', '<div id=\"parent\" yoyo:val.foo=\"1\"><button yoyo:val.bar=\"1\"></button></div>'))\n        ->toContain(hxattr('vals', encode_vals([yoprefix_value('id') => 'parent', 'foo' => 1])))\n        ->toContain(hxattr('vals', encode_vals(['bar' => 1])));\n});\n\nit('parses yoyo:val zero value as integer', function () {\n    expect(compile_html('foo', '<div id=\"foo\" yoyo:val.foo=\"0\"></div>'))\n        ->toContain(hxattr('vals', encode_vals([yoprefix_value('id') => 'foo', 'foo' => 0])));\n});\n\nit('includes declared props as vals', function () {\n    expect(compile_html_with_vars('foo', '<div id=\"foo\" yoyo:props=\"foo\"></div>', ['foo' => 'bar']))\n        ->toContain(hxattr('vals', encode_vals([yoprefix_value('id') => 'foo', 'foo' => 'bar'])));\n});\n\nit('correctly compiles component with non-ascii characters', function () {\n    expect(compile_html('foo', '<div><p>áéíóü</p></div>'))\n        ->toContain('áéíóü');\n});\n\nit('correctly compiles component with Chinese characters', function () {\n    expect(compile_html('foo', '<div><p>极简、极速、极致、 海豚PHP、PHP开发框架、后台框架</p></div>'))\n        ->toContain('极简、极速、极致、 海豚PHP、PHP开发框架、后台框架');\n});\n"
  },
  {
    "path": "tests/Unit/YoyoCompilerEdgeCasesTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\YoyoCompiler;\n\nuse function Tests\\compile_html;\nuse function Tests\\compile_html_with_vars;\nuse function Tests\\encode_vals;\nuse function Tests\\hxattr;\nuse function Tests\\yoattr;\nuse function Tests\\yoprefix_value;\n\nuses()->group('compiler-edge-cases');\n\n// --- Empty and whitespace inputs ---\n\nit('returns empty string for empty input', function () {\n    expect(compile_html('foo', ''))->toBe('');\n});\n\nit('returns whitespace-only input as-is', function () {\n    expect(compile_html('foo', '   '))->toBe('   ');\n});\n\n// --- Root node detection ---\n\nit('wraps multiple sibling elements in a div', function () {\n    $html = compile_html('foo', '<span>a</span><span>b</span>');\n    expect($html)->toMatch('/<div [^>]*class=\"yoyo-wrapper[^\"]*\"[^>]*><span>a<\\/span><span>b<\\/span><\\/div>/');\n});\n\nit('wraps text node with sibling element in a div', function () {\n    $html = compile_html('foo', '<p>a</p><p>b</p><p>c</p>');\n    expect($html)->toMatch('/<div [^>]*>.*<\\/div>/s');\n});\n\nit('does not re-wrap a single root element', function () {\n    $html = compile_html('foo', '<section>content</section>');\n    expect($html)->toMatch('/<section [^>]*>content<\\/section>/');\n    expect($html)->not->toContain('<div><section');\n});\n\n// --- Form behavior ---\n\nit('auto-adds submit trigger and post method to forms', function () {\n    $html = compile_html('foo', '<div><form><input name=\"x\"/></form></div>');\n    expect($html)\n        ->toContain(hxattr('trigger', 'submit'))\n        ->toContain(hxattr('post', YoyoCompiler::COMPONENT_DEFAULT_ACTION));\n});\n\nit('does not override existing yoyo:post on form', function () {\n    $html = compile_html('foo', '<div><form yoyo:post=\"customAction\"><input name=\"x\"/></form></div>');\n    expect($html)\n        ->toContain(hxattr('post', 'customAction'))\n        ->not->toContain(hxattr('post', YoyoCompiler::COMPONENT_DEFAULT_ACTION));\n});\n\nit('does not override existing yoyo:put on form', function () {\n    $html = compile_html('foo', '<div><form yoyo:put=\"save\"><input name=\"x\"/></form></div>');\n    expect($html)->toContain(hxattr('put', 'save'));\n});\n\nit('skips form behavior when yoyo:ignore is present', function () {\n    $html = compile_html('foo', '<div><form yoyo:ignore><input name=\"x\"/></form></div>');\n    expect($html)->not->toContain(hxattr('trigger', 'submit'));\n});\n\n// --- Attribute remapping ---\n\nit('remaps yoyo:on to hx-trigger', function () {\n    $html = compile_html('foo', '<div yoyo:on=\"click\"></div>');\n    // yoyo:on on root becomes part of trigger attribute (merged with 'refresh')\n    expect($html)->toContain(hxattr('trigger'));\n});\n\nit('remaps yoyo:on in child elements to hx-trigger', function () {\n    $html = compile_html('foo', '<div><button yoyo yoyo:on=\"click\">Go</button></div>');\n    expect($html)->toContain(hxattr('trigger', 'click'));\n});\n\nit('compiles yoyo:target on child elements', function () {\n    $html = compile_html('foo', '<div><button yoyo yoyo:target=\"#output\">Go</button></div>');\n    expect($html)->toContain(hxattr('target', '#output'));\n});\n\nit('compiles yoyo:swap on child elements', function () {\n    $html = compile_html('foo', '<div><button yoyo yoyo:swap=\"outerHTML\">Go</button></div>');\n    expect($html)->toContain(hxattr('swap', 'outerHTML'));\n});\n\nit('compiles yoyo:confirm on child elements', function () {\n    $html = compile_html('foo', '<div><a yoyo yoyo:confirm=\"Sure?\">Delete</a></div>');\n    expect($html)->toContain(hxattr('confirm', 'Sure?'));\n});\n\nit('compiles yoyo:indicator on child elements', function () {\n    $html = compile_html('foo', '<div><button yoyo yoyo:indicator=\"#spinner\">Go</button></div>');\n    expect($html)->toContain(hxattr('indicator', '#spinner'));\n});\n\n// --- Request method attributes ---\n\nit('converts yoyo:get to hx-get on child elements', function () {\n    $html = compile_html('foo', '<div><button yoyo:get=\"loadMore\">More</button></div>');\n    expect($html)->toContain(hxattr('get', 'loadMore'));\n});\n\nit('converts yoyo:post to hx-post on child elements', function () {\n    $html = compile_html('foo', '<div><button yoyo:post=\"save\">Save</button></div>');\n    expect($html)->toContain(hxattr('post', 'save'));\n});\n\nit('converts yoyo:delete to hx-delete on child elements', function () {\n    $html = compile_html('foo', '<div><button yoyo:delete=\"remove\">Delete</button></div>');\n    expect($html)->toContain(hxattr('delete', 'remove'));\n});\n\nit('does not add default hx-get to child when hx-post already exists', function () {\n    $html = compile_html('foo', '<div><button hx-post=\"save\">Save</button></div>');\n    // The button should not get hx-get since it already has hx-post\n    // But the root div still gets hx-get as the component default\n    expect($html)->toMatch('/<button hx-post=\"save\">Save<\\/button>/');\n});\n\n// --- Spinning behavior ---\n\nit('removes load event from root trigger when spinning', function () {\n    $html = compile_html('foo', '<div yoyo:on=\"load\"></div>', $spinning = true);\n    // The load event should be removed when spinning\n    expect($html)->not->toContain('load');\n});\n\nit('preserves non-load events when spinning', function () {\n    $html = compile_html('foo', '<div yoyo:on=\"click,load\"></div>', $spinning = true);\n    expect($html)->toContain('click');\n});\n\n// --- ID generation ---\n\nit('assigns incremental IDs to reactive child elements', function () {\n    $html = compile_html('foo', '<div><button yoyo>A</button><button yoyo>B</button></div>');\n    expect($html)->toMatch('/id=\"yoyo-[a-z0-9]+-1\"/');\n    expect($html)->toMatch('/id=\"yoyo-[a-z0-9]+-2\"/');\n});\n\nit('preserves existing ID on reactive child elements', function () {\n    $html = compile_html('foo', '<div><button yoyo id=\"my-btn\">A</button></div>');\n    expect($html)->toContain('id=\"my-btn\"');\n    expect($html)->not->toMatch('/id=\"yoyo-[a-z0-9]+-1\"/');\n});\n\n// --- Listener support ---\n\nit('adds listeners to root trigger attribute', function () {\n    $compiler = new YoyoCompiler('dynamic', 'test-id', 'test', [], [], false);\n    $compiler->addComponentListeners(['itemAdded' => 'refresh', 'itemRemoved' => 'removeItem']);\n    $compiler->addComponentProps([]);\n    $html = $compiler->compile('<div id=\"test\"></div>');\n    expect($html)\n        ->toContain('itemAdded')\n        ->toContain('itemRemoved');\n});\n\n// --- History caching ---\n\nit('adds yoyo:history attribute when withHistory is true', function () {\n    $compiler = new YoyoCompiler('dynamic', 'test-id', 'test', [], [], false);\n    $compiler->withHistory(true);\n    $compiler->addComponentProps([]);\n    $html = $compiler->compile('<div id=\"test\"></div>');\n    // DOMDocument may use double quotes, so check for the attribute presence\n    expect($html)->toMatch('/yoyo:history=\"1\"/');\n});\n\nit('does not add yoyo:history attribute when withHistory is false', function () {\n    $compiler = new YoyoCompiler('dynamic', 'test-id', 'test', [], [], false);\n    $compiler->withHistory(false);\n    $compiler->addComponentProps([]);\n    $html = $compiler->compile('<div id=\"test\"></div>');\n    expect($html)->not->toContain(yoattr('history'));\n});\n\n// --- CSS class handling ---\n\nit('preserves existing CSS class on root element', function () {\n    $html = compile_html('foo', '<div class=\"my-class\">content</div>');\n    expect($html)->toContain('class=\"yoyo-wrapper my-class\"');\n});\n\nit('adds wrapper class when no class exists', function () {\n    $html = compile_html('foo', '<div>content</div>');\n    expect($html)->toContain('class=\"yoyo-wrapper\"');\n});\n\n// --- Props passing ---\n\nit('excludes non-declared props from vals', function () {\n    $html = compile_html_with_vars('foo', '<div id=\"foo\"></div>', ['secret' => 'value', 'extra' => 123]);\n    // Only yoyo-id should be in vals, not secret or extra (they're not in yoyo:props)\n    expect($html)->not->toContain('\"secret\"');\n    expect($html)->not->toContain('\"extra\"');\n});\n\nit('includes only declared props in vals', function () {\n    $html = compile_html_with_vars('foo', '<div id=\"foo\" yoyo:props=\"color\"></div>', ['color' => 'blue', 'size' => 'large']);\n    expect($html)->toContain(hxattr('vals', encode_vals([yoprefix_value('id') => 'foo', 'color' => 'blue'])));\n    expect($html)->not->toContain('\"size\"');\n});\n\n// --- Special HTML content ---\n\nit('handles HTML entities in content', function () {\n    $html = compile_html('foo', '<div><p>&amp; &lt; &gt;</p></div>');\n    expect($html)->toContain('&amp;');\n});\n\nit('handles nested elements deeply', function () {\n    $html = compile_html('foo', '<div><ul><li><a href=\"#\">Link</a></li></ul></div>');\n    expect($html)->toContain('<ul><li><a href=\"#\">Link</a></li></ul>');\n});\n\nit('handles boolean attributes', function () {\n    $html = compile_html('foo', '<div><input type=\"text\" required disabled/></div>');\n    expect($html)->toContain('required');\n    expect($html)->toContain('disabled');\n});\n\n// --- Single-quoted yoyo attributes ---\n\nit('handles single-quoted yoyo attributes', function () {\n    $html = compile_html('foo', \"<div><button yoyo:get='loadMore'>More</button></div>\");\n    expect($html)->toContain(hxattr('get', 'loadMore'));\n});\n\n// --- Skip already-compiled components ---\n\nit('skips already-compiled component roots', function () {\n    // Simulate a component that's already been compiled (has both yoyo:name and hx-vals)\n    $html = compile_html('foo', '<div yoyo:name=\"foo\" hx-vals=\\'{\"yoyo-id\":\"foo\"}\\'>content</div>');\n    // Should not double-add attributes\n    expect($html)->toContain('content');\n});\n\n// --- Static helper methods ---\n\nit('generates correct yoyo prefix', function () {\n    expect(YoyoCompiler::yoprefix('get'))->toBe('yoyo:get');\n    expect(YoyoCompiler::yoprefix('trigger'))->toBe('yoyo:trigger');\n    expect(YoyoCompiler::yoprefix('ignore'))->toBe('yoyo:ignore');\n});\n\nit('generates correct yoyo prefix value', function () {\n    expect(YoyoCompiler::yoprefix_value('id'))->toBe('yoyo-id');\n    expect(YoyoCompiler::yoprefix_value('resolver'))->toBe('yoyo-resolver');\n});\n\nit('generates correct hx prefix', function () {\n    expect(YoyoCompiler::hxprefix('get'))->toBe('hx-get');\n    expect(YoyoCompiler::hxprefix('trigger'))->toBe('hx-trigger');\n    expect(YoyoCompiler::hxprefix('vals'))->toBe('hx-vals');\n});\n\n// --- Target and include overrides ---\n\nit('allows overriding target on root element', function () {\n    $html = compile_html('foo', '<div yoyo:target=\"#other\">content</div>');\n    expect($html)->toContain(hxattr('target', '#other'));\n});\n\nit('allows overriding include on root element', function () {\n    $html = compile_html('foo', '<div yoyo:include=\"closest form\">content</div>');\n    expect($html)->toContain(hxattr('include', 'closest form'));\n});\n"
  },
  {
    "path": "tests/Unit/YoyoContainerTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\Containers\\IlluminateContainer;\nuse Clickfwd\\Yoyo\\Containers\\YoyoContainer;\nuse Clickfwd\\Yoyo\\Exceptions\\BindingNotFoundException;\nuse Clickfwd\\Yoyo\\Exceptions\\ContainerResolutionException;\nuse Clickfwd\\Yoyo\\Interfaces\\YoyoContainerInterface;\n\nit('can set and get bindings', function () {\n    $container = new YoyoContainer();\n    $container->set('test', 'value');\n\n    expect($container->get('test'))->toBe('value');\n    expect($container->has('test'))->toBeTrue();\n});\n\nit('can make classes with dependencies', function () {\n    $container = new YoyoContainer();\n\n    $instance = $container->make(Tests\\App\\Yoyo\\Counter::class, ['id' => 'test', 'name' => 'test']);\n\n    expect($instance)->toBeInstanceOf(Tests\\App\\Yoyo\\Counter::class);\n});\n\nit('handles nullable parameters correctly', function () {\n    $container = new YoyoContainer();\n\n    $class = new class (null) {\n        public ?string $optional;\n\n        public function __construct(?string $optional)\n        {\n            $this->optional = $optional;\n        }\n    };\n\n    $instance = $container->make(get_class($class)); // Injects null implicitly\n    expect($instance)->toBeObject();\n    expect($instance->optional)->toBeNull();\n\n    $instance = $container->make(get_class($class), ['optional' => 'test']);\n    expect($instance)->toBeObject();\n    expect($instance->optional)->toBe('test');\n});\n\nit('handles interfaces bound to container', function () {\n    $container = new YoyoContainer();\n\n    $class = new class ($container) {\n        public YoyoContainerInterface $container;\n\n        public function __construct(YoyoContainerInterface $container)\n        {\n            $this->container = $container;\n        }\n    };\n\n    // Binds class name to interface\n    $container->set(YoyoContainerInterface::class, YoyoContainer::class);\n    $instance = $container->make(get_class($class));\n    expect($instance)->toBeObject();\n    expect($instance->container)->toBeInstanceOf(YoyoContainer::class);\n\n    // Binds class instance to interface\n    $container->set(YoyoContainerInterface::class, IlluminateContainer::getInstance());\n    $instance = $container->make(get_class($class));\n    expect($instance)->toBeObject();\n    expect($instance->container)->toBeInstanceOf(IlluminateContainer::class);\n});\n\nit('resolves closures as singletons', function () {\n    $container = new YoyoContainer();\n    $counter = 0;\n\n    $container->set('service', function () use (&$counter) {\n        $counter++;\n        return new stdClass();\n    });\n\n    $first = $container->get('service');\n    $second = $container->get('service');\n\n    expect($counter)->toBe(1); // Closure only called once\n    expect($first)->toBe($second); // Same instance\n});\n\nit('throws exception for failed resolution', function () {\n    $container = new YoyoContainer();\n\n    $container->make(Foo::class);\n})->throws(ContainerResolutionException::class);\n\nit('throws exception for invalid bindings', function () {\n    $container = new YoyoContainer();\n\n    $container->get(Foo::class);\n})->throws(BindingNotFoundException::class);\n"
  },
  {
    "path": "tests/Unit/YoyoHelpersTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\YoyoHelpers;\n\n// --- encode_vals ---\n\nit('encodes simple key-value pairs to JSON', function () {\n    $result = YoyoHelpers::encode_vals(['name' => 'test', 'count' => 5]);\n    $decoded = json_decode($result, true);\n\n    expect($decoded)->toBe(['name' => 'test', 'count' => 5]);\n});\n\nit('appends [] suffix for array values in encode_vals', function () {\n    $result = YoyoHelpers::encode_vals(['tags' => ['php', 'yoyo']]);\n    $decoded = json_decode($result, true);\n\n    expect($decoded)->toHaveKey('tags[]');\n    expect($decoded['tags[]'])->toBe(['php', 'yoyo']);\n});\n\nit('encodes unicode characters without escaping', function () {\n    $result = YoyoHelpers::encode_vals(['name' => '日本語']);\n\n    expect($result)->toContain('日本語');\n});\n\nit('escapes HTML-sensitive characters in encode_vals', function () {\n    $result = YoyoHelpers::encode_vals(['html' => '<script>alert(\"xss\")</script>']);\n\n    // Should escape quotes, tags, ampersands\n    expect($result)->not->toContain('<script>');\n    expect($result)->not->toContain('\"xss\"');\n});\n\nit('handles empty array in encode_vals', function () {\n    expect(YoyoHelpers::encode_vals([]))->toBe('[]');\n});\n\n// --- decode_vals ---\n\nit('decodes valid JSON string to array', function () {\n    $result = YoyoHelpers::decode_vals('{\"key\":\"value\",\"num\":42}');\n    expect($result)->toBe(['key' => 'value', 'num' => 42]);\n});\n\nit('returns empty array for empty string in decode_vals', function () {\n    expect(YoyoHelpers::decode_vals(''))->toBe([]);\n});\n\n// --- decode_val ---\n\nit('decodes JSON value via decode_val', function () {\n    expect(YoyoHelpers::decode_val('{\"key\":\"value\"}'))->toBe(['key' => 'value']);\n});\n\nit('converts string \"0\" to integer 0 in decode_val', function () {\n    expect(YoyoHelpers::decode_val('0'))->toBe(0);\n});\n\nit('returns non-JSON string as-is from decode_val', function () {\n    expect(YoyoHelpers::decode_val('hello'))->toBe('hello');\n});\n\nit('decodes JSON scalar null to PHP null in decode_val', function () {\n    expect(YoyoHelpers::decode_val('null'))->toBeNull();\n});\n\nit('decodes JSON scalar false to PHP false in decode_val', function () {\n    expect(YoyoHelpers::decode_val('false'))->toBeFalse();\n});\n\nit('decodes JSON scalar true to PHP true in decode_val', function () {\n    expect(YoyoHelpers::decode_val('true'))->toBeTrue();\n});\n\n// --- test_json ---\n\nit('flags JSON scalar null as valid in test_json', function () {\n    $valid = false;\n    $decoded = YoyoHelpers::test_json('null', $valid);\n\n    expect($valid)->toBeTrue();\n    expect($decoded)->toBeNull();\n});\n\nit('flags JSON scalar false as valid in test_json', function () {\n    $valid = false;\n    $decoded = YoyoHelpers::test_json('false', $valid);\n\n    expect($valid)->toBeTrue();\n    expect($decoded)->toBeFalse();\n});\n\nit('flags JSON object as valid in test_json', function () {\n    $valid = false;\n    $decoded = YoyoHelpers::test_json('{\"a\":1}', $valid);\n\n    expect($valid)->toBeTrue();\n    expect($decoded)->toBe(['a' => 1]);\n});\n\nit('marks invalid JSON string as not valid in test_json', function () {\n    $valid = false;\n    $decoded = YoyoHelpers::test_json('hello', $valid);\n\n    expect($valid)->toBeFalse();\n    expect($decoded)->toBeNull();\n});\n\nit('treats array input as already-decoded valid JSON in test_json', function () {\n    $valid = false;\n    $decoded = YoyoHelpers::test_json(['a' => 1], $valid);\n\n    expect($valid)->toBeTrue();\n    expect($decoded)->toBe(['a' => 1]);\n});\n\nit('marks non-string non-array input as not valid in test_json', function () {\n    $valid = false;\n    $decoded = YoyoHelpers::test_json(123, $valid);\n\n    expect($valid)->toBeFalse();\n    expect($decoded)->toBeNull();\n});\n\n// --- studly ---\n\nit('converts kebab-case to StudlyCase', function () {\n    expect(YoyoHelpers::studly('foo-bar'))->toBe('FooBar');\n    expect(YoyoHelpers::studly('foo-bar-baz'))->toBe('FooBarBaz');\n});\n\nit('converts snake_case to StudlyCase', function () {\n    expect(YoyoHelpers::studly('foo_bar'))->toBe('FooBar');\n});\n\nit('handles already StudlyCase input', function () {\n    expect(YoyoHelpers::studly('FooBar'))->toBe('FooBar');\n});\n\nit('handles single word in studly', function () {\n    expect(YoyoHelpers::studly('foo'))->toBe('Foo');\n});\n\nit('supports custom delimiter in studly', function () {\n    expect(YoyoHelpers::studly('foo.bar', '.'))->toBe('FooBar');\n});\n\n// --- camel ---\n\nit('converts kebab-case to camelCase', function () {\n    expect(YoyoHelpers::camel('foo-bar'))->toBe('fooBar');\n    expect(YoyoHelpers::camel('foo-bar-baz'))->toBe('fooBarBaz');\n});\n\nit('converts snake_case to camelCase', function () {\n    expect(YoyoHelpers::camel('foo_bar'))->toBe('fooBar');\n});\n\nit('handles single word in camel', function () {\n    expect(YoyoHelpers::camel('foo'))->toBe('foo');\n});\n\nit('supports custom delimiter in camel', function () {\n    expect(YoyoHelpers::camel('foo.bar', '.'))->toBe('fooBar');\n});\n\n// --- snake ---\n\nit('converts camelCase to snake_case', function () {\n    expect(YoyoHelpers::snake('fooBar'))->toBe('foo_bar');\n    expect(YoyoHelpers::snake('fooBarBaz'))->toBe('foo_bar_baz');\n});\n\nit('converts StudlyCase to snake_case', function () {\n    expect(YoyoHelpers::snake('FooBar'))->toBe('foo_bar');\n});\n\nit('returns already lowercase string unchanged', function () {\n    expect(YoyoHelpers::snake('foobar'))->toBe('foobar');\n});\n\nit('supports custom delimiter in snake', function () {\n    expect(YoyoHelpers::snake('fooBar', '-'))->toBe('foo-bar');\n});\n\n// --- removeEmptyValues ---\n\nit('removes null values', function () {\n    $array = ['a' => 'value', 'b' => null, 'c' => 'other'];\n    YoyoHelpers::removeEmptyValues($array);\n\n    expect($array)->toBe(['a' => 'value', 'c' => 'other']);\n});\n\nit('removes empty string values', function () {\n    $array = ['a' => 'value', 'b' => '', 'c' => 'other'];\n    YoyoHelpers::removeEmptyValues($array);\n\n    expect($array)->toBe(['a' => 'value', 'c' => 'other']);\n});\n\nit('removes empty nested arrays', function () {\n    $array = ['a' => 'value', 'b' => ['nested' => null]];\n    YoyoHelpers::removeEmptyValues($array);\n\n    expect($array)->toBe(['a' => 'value']);\n});\n\nit('preserves zero values', function () {\n    $array = ['a' => 0, 'b' => '0', 'c' => false];\n    YoyoHelpers::removeEmptyValues($array);\n\n    expect($array)->toHaveKey('a');\n    expect($array)->toHaveKey('b');\n});\n\nit('handles already empty array', function () {\n    $array = [];\n    $result = YoyoHelpers::removeEmptyValues($array);\n\n    expect($result)->toBe([]);\n});\n"
  },
  {
    "path": "tests/Unit/YoyoViewProviderTest.php",
    "content": "<?php\n\nuse Clickfwd\\Yoyo\\View;\nuse Clickfwd\\Yoyo\\ViewProviders\\YoyoViewProvider;\n\ntest('can render a template', function () {\n    $view = new YoyoViewProvider(new View(__DIR__.'/../app/resources/views/yoyo'));\n    expect((string) $view->render('foo', ['spinning' => false]))->toContain('default foo');\n});\n\ntest('can render a template with a prepended location with higher priority', function () {\n    $view = new YoyoViewProvider(new View(__DIR__.'/../app/resources/views/yoyo'));\n    $view->prependLocation(__DIR__.'/../app-another/views');\n    expect((string) $view->render('foo'))->toContain('other foo from another app');\n});\n\ntest('can render a template with custom namespace', function () {\n    $view = new YoyoViewProvider(new View(__DIR__.'/../app/resources/views/yoyo'));\n    $view->addNamespace('packagename', __DIR__.'/../app-another/views');\n    expect((string) $view->render('packagename::foo'))->toContain('other foo from another app');\n});\n"
  },
  {
    "path": "tests/app/Comment.php",
    "content": "<?php\n\nnamespace Tests\\App;\n\nclass Comment\n{\n    public function title()\n    {\n        return 'the comment title';\n    }\n\n    public function body()\n    {\n        return 'the comment body';\n    }\n}\n"
  },
  {
    "path": "tests/app/Post.php",
    "content": "<?php\n\nnamespace Tests\\App;\n\nclass Post\n{\n    protected $comment;\n\n    public function __construct(Comment $comment)\n    {\n        $this->comment = $comment;\n    }\n\n    public function title()\n    {\n        return $this->comment->title();\n    }\n}\n"
  },
  {
    "path": "tests/app/Resolvers/BladeComponentResolver.php",
    "content": "<?php\n\nnamespace Tests\\App\\Resolvers;\n\nuse Clickfwd\\Yoyo\\ComponentResolver;\nuse Clickfwd\\Yoyo\\ViewProviders\\BladeViewProvider;\n\nuse function Tests\\blade;\n\nclass BladeComponentResolver extends ComponentResolver\n{\n    protected $name = 'blade';\n\n    public function getViewProvider()\n    {\n        return new BladeViewProvider(blade());\n    }\n}\n"
  },
  {
    "path": "tests/app/Resolvers/CustomComponentResolver.php",
    "content": "<?php\n\nnamespace Tests\\App\\Resolvers;\n\nuse Clickfwd\\Yoyo\\ComponentResolver;\nuse Clickfwd\\Yoyo\\View;\nuse Clickfwd\\Yoyo\\ViewProviders\\YoyoViewProvider;\n\nclass CustomComponentResolver extends ComponentResolver\n{\n    protected $name = 'custom';\n\n    public function getViewProvider()\n    {\n        return new YoyoViewProvider(new View(__DIR__.'/../resources/views/yoyo'));\n    }\n}\n"
  },
  {
    "path": "tests/app/Resolvers/TwigComponentResolver.php",
    "content": "<?php\n\nnamespace Tests\\App\\Resolvers;\n\nuse Clickfwd\\Yoyo\\ComponentResolver;\nuse Clickfwd\\Yoyo\\ViewProviders\\TwigViewProvider;\n\nuse function Tests\\twig;\n\nclass TwigComponentResolver extends ComponentResolver\n{\n    protected $name = 'twig';\n\n    public function getViewProvider()\n    {\n        return new TwigViewProvider(twig());\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/Abort.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nuse function Yoyo\\abort;\n\nclass Abort extends Component\n{\n    public function initialize()\n    {\n        abort(404, 'not found', ['foo' => 'bar']);\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/Account/Register.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo\\Account;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass Register extends Component\n{\n    public $message = 'Please register to access this page';\n}\n"
  },
  {
    "path": "tests/app/Yoyo/ActionArguments.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ActionArguments extends Component\n{\n    protected $a;\n\n    protected $b;\n\n    public function someAction($a, $b)\n    {\n        $this->a = $a;\n\n        $this->b = $b;\n    }\n\n    public function render()\n    {\n        return $this->view('action-arguments', ['a' => $this->a, 'b' => $this->b]);\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/ComponentWithComputedArgs.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComponentWithComputedArgs extends Component\n{\n    public $prefix = 'Hello';\n\n    protected function getGreetingProperty($name = 'World')\n    {\n        return \"{$this->prefix}, {$name}!\";\n    }\n\n    protected function getExpensiveProperty()\n    {\n        static $callCount = 0;\n        $callCount++;\n\n        return \"called:{$callCount}\";\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/ComponentWithEmit.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComponentWithEmit extends Component\n{\n    public function doEmit()\n    {\n        $this->emit('testEvent', ['key' => 'value']);\n    }\n\n    public function doEmitTo()\n    {\n        $this->emitTo('other-component', 'targetEvent', ['id' => 1]);\n    }\n\n    public function doBrowserEvent()\n    {\n        $this->dispatchBrowserEvent('notification', ['message' => 'done']);\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/ComponentWithListeners.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComponentWithListeners extends Component\n{\n    public $message = '';\n\n    protected $listeners = [\n        'itemAdded' => 'onItemAdded',\n        'refresh',\n    ];\n\n    public function onItemAdded()\n    {\n        $this->message = 'item was added';\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/ComponentWithRedirect.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComponentWithRedirect extends Component\n{\n    public function save()\n    {\n        $this->redirect('/success');\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/ComponentWithResponseHeaders.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComponentWithResponseHeaders extends Component\n{\n    public $message = 'initial';\n\n    public function doRetarget()\n    {\n        $this->response->retarget('#other-target');\n        $this->message = 'retargeted';\n    }\n\n    public function doReswap()\n    {\n        $this->response->reswap('innerHTML');\n        $this->message = 'reswapped';\n    }\n\n    public function doReselect()\n    {\n        $this->response->reselect('#selected-part');\n        $this->message = 'reselected';\n    }\n\n    public function doLocation()\n    {\n        $this->response->location('/new-location');\n    }\n\n    public function doPushUrl()\n    {\n        $this->response->pushUrl('/pushed-url');\n        $this->message = 'url-pushed';\n    }\n\n    public function doReplaceUrl()\n    {\n        $this->response->replaceUrl('/replaced-url');\n        $this->message = 'url-replaced';\n    }\n\n    public function doRedirect()\n    {\n        $this->response->redirect('/redirected');\n    }\n\n    public function doRefresh()\n    {\n        $this->response->refresh();\n    }\n\n    public function doTrigger()\n    {\n        $this->response->trigger('custom-event');\n        $this->message = 'triggered';\n    }\n\n    public function doTriggerAfterSwap()\n    {\n        $this->response->triggerAfterSwap('swap-event');\n        $this->message = 'trigger-after-swap';\n    }\n\n    public function doTriggerAfterSettle()\n    {\n        $this->response->triggerAfterSettle('settle-event');\n        $this->message = 'trigger-after-settle';\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/ComponentWithSwapModifiers.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComponentWithSwapModifiers extends Component\n{\n    public function doSwap()\n    {\n        $this->addSwapModifiers('transition:true swap:500ms');\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/ComponentWithTrait.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComponentWithTrait extends Component\n{\n    use WithFramework;\n\n    public $output;\n\n    public function mount()\n    {\n        $this->output = 'Component saw that ';\n    }\n}\n\ntrait WithFramework\n{\n    public function mountWithFramework()\n    {\n        $this->output .= '{mountWithFramework} was here';\n    }\n\n    public function renderedWithFramework($view)\n    {\n        return str_replace(\"Component\", \"{ComponentWithTrait}\", $view);\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/ComputedProperty.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComputedProperty extends Component\n{\n    public $foo = 'bar';\n\n    protected function getFooBarProperty()\n    {\n        return $this->foo;\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/ComputedPropertyCache.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ComputedPropertyCache extends Component\n{\n    protected $count = 1;\n\n    public function getTestCountProperty()\n    {\n        return $this->count++;\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/Counter.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass Counter extends Component\n{\n    public $count = 0;\n\n    protected $queryString = ['count'];\n\n    protected $props = ['count'];\n\n    public function increment()\n    {\n        $this->count++;\n\n        $this->emit('counter:updated', ['count' => $this->count]);\n    }\n\n    public function getCurrentCountProperty()\n    {\n        return 'The count is now '.$this->count;\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/CounterDynamicProperties.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\n#[\\AllowDynamicProperties]\nclass CounterDynamicProperties extends Component\n{\n    public function getQueryString()\n    {\n        return $this->getDynamicProperties();\n    }\n\n    /**\n     * The 'count' property value is not known ahead of time and can be set programatically;\n     *\n     * @return void\n     */\n    public function getDynamicProperties()\n    {\n        return ['count'];\n    }\n\n    public function increment()\n    {\n        $this->count++;\n    }\n\n    public function getCurrentCountProperty()\n    {\n        return 'The count is now '.$this->count;\n    }\n\n    public function render()\n    {\n        return $this->view('counter');\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/DependencyInjectionAction.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\nuse Tests\\App\\Comment;\nuse Tests\\App\\Post;\n\nclass DependencyInjectionAction extends Component\n{\n    public $result = '';\n\n    /**\n     * Test action with only typed parameters (dependency injection)\n     */\n    public function onlyTyped(Post $post)\n    {\n        $this->result = 'Post title: ' . $post->title();\n    }\n\n    /**\n     * Test action with multiple typed parameters\n     */\n    public function multipleTyped(Post $post, Comment $comment)\n    {\n        $this->result = 'Post: ' . $post->title() . ', Comment: ' . $comment->body();\n    }\n\n    /**\n     * Test action with mixed typed and regular parameters\n     */\n    public function mixedTypedAndRegular(Post $post, $id, $status = 'active')\n    {\n        $this->result = \"Post: {$post->title()}, ID: {$id}, Status: {$status}\";\n    }\n\n    /**\n     * Test action with typed and variadic parameters\n     */\n    public function typedWithVariadic(Post $post, ...$tags)\n    {\n        $this->result = \"Post: {$post->title()}, Tags: \" . json_encode($tags);\n    }\n\n    /**\n     * Test action with typed and optional regular parameter\n     */\n    public function typedWithOptional(Post $post, ?string $status = null)\n    {\n        $statusText = $status ?? 'default';\n        $this->result = \"Post: {$post->title()}, Status: {$statusText}\";\n    }\n\n    public function render()\n    {\n        return $this->view('dependency-injection-action', ['result' => $this->result]);\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/DependencyInjectionClassWithNamedArgumentMapping.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\nuse Tests\\App\\Post;\n\nclass DependencyInjectionClassWithNamedArgumentMapping extends Component\n{\n    protected $id;\n\n    protected $post;\n\n    // $foo variable passed to component is automaticaly injected in Post::__constructor\n    // using dependency injection\n    public function mount(Post $post, $id)\n    {\n        $this->id = $id;\n\n        $this->post = $post;\n    }\n\n    public function getOutputProperty()\n    {\n        return $this->post->title().'-'.$this->id;\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/DispatchListener.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass DispatchListener extends Component\n{\n    public $message = '';\n\n    public $postId = 0;\n\n    public $status = 'idle';\n\n    protected $listeners = [\n        'post-created' => 'handlePostCreated',\n        'status-changed' => 'handleStatusChanged',\n        'simple-refresh' => 'handleSimpleRefresh',\n        'multi-param' => 'handleMultiParam',\n    ];\n\n    public function handlePostCreated($postId)\n    {\n        $this->postId = $postId;\n        $this->message = \"Post created with ID: {$postId}\";\n    }\n\n    public function handleStatusChanged($status, $reason = 'none')\n    {\n        $this->status = $status;\n        $this->message = \"Status: {$status}, Reason: {$reason}\";\n    }\n\n    public function handleSimpleRefresh()\n    {\n        $this->message = 'Refreshed without params';\n    }\n\n    public function handleMultiParam($title, $body, $categoryId)\n    {\n        $this->message = \"Title: {$title}, Body: {$body}, Category: {$categoryId}\";\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/EmptyResponse.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass EmptyResponse extends Component\n{\n    public function mount()\n    {\n        return $this->skipRender();\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/EmptyResponseAndRemove.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass EmptyResponseAndRemove extends Component\n{\n    public function mount()\n    {\n        return $this->skipRenderAndRemove();\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/ProtectedMethods.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass ProtectedMethods extends Component\n{\n    protected function secret()\n    {\n        // Cannot be accessed through direct request\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/Registered.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass Registered extends Component\n{\n    public function render()\n    {\n        return $this->view('registered');\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/SetViewData.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass SetViewData extends Component\n{\n    public function mount()\n    {\n        $this->set('foo', 'bar');\n\n        $this->set(['bar' => 'baz']);\n    }\n}\n"
  },
  {
    "path": "tests/app/Yoyo/VariadicParameters.php",
    "content": "<?php\n\nnamespace Tests\\App\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass VariadicParameters extends Component\n{\n    public $result = '';\n\n    /**\n     * Test method with only variadic parameters\n     */\n    public function onlyVariadic(...$params)\n    {\n        $this->result = 'Received: ' . json_encode($params);\n    }\n\n    /**\n     * Test method with regular and variadic parameters\n     */\n    public function mixedVariadic($first, ...$rest)\n    {\n        $this->result = \"First: {$first}, Rest: \" . json_encode($rest);\n    }\n\n    /**\n     * Test method with optional and variadic parameters\n     */\n    public function optionalAndVariadic($required, $optional = 'default', ...$extra)\n    {\n        $this->result = \"Required: {$required}, Optional: {$optional}, Extra: \" . json_encode($extra);\n    }\n\n    public function render()\n    {\n        return $this->view('variadic-parameters', ['result' => $this->result]);\n    }\n}\n"
  },
  {
    "path": "tests/app/resources/views/components/select.blade.php",
    "content": "app/resources/views/components/select.blade.php"
  },
  {
    "path": "tests/app/resources/views/yoyo/account/login.blade.php",
    "content": "blade:app/resources/views/yoyo/account/login.php"
  },
  {
    "path": "tests/app/resources/views/yoyo/account/login.php",
    "content": "app/resources/views/yoyo/account/login.php"
  },
  {
    "path": "tests/app/resources/views/yoyo/account/register.blade.php",
    "content": "blade:<?php echo $message; ?>"
  },
  {
    "path": "tests/app/resources/views/yoyo/account/register.php",
    "content": "<?php\n\necho $message;\n"
  },
  {
    "path": "tests/app/resources/views/yoyo/action-arguments.php",
    "content": "<div id=\"action-arguments\">\n    <button yoyo:get=\"someAction(1, 'foo')\">Click</button>\n    <span><?php echo $a; ?><?php echo $b; ?></span>\n</div>"
  },
  {
    "path": "tests/app/resources/views/yoyo/child.blade.php",
    "content": "<p yoyo:props=\"id\">blade:{{ $id }}</p>"
  },
  {
    "path": "tests/app/resources/views/yoyo/child.php",
    "content": "<p yoyo:props=\"id\"><?php echo $id; ?></p>"
  },
  {
    "path": "tests/app/resources/views/yoyo/child.twig",
    "content": "<p yoyo:props=\"id\">twig:{{ id }}</p>"
  },
  {
    "path": "tests/app/resources/views/yoyo/component-with-computed-args.php",
    "content": "<div id=\"component-with-computed-args\">\n    <p><?php echo $this->greeting('Alice'); ?></p>\n    <p><?php echo $this->greeting('Bob'); ?></p>\n</div>\n"
  },
  {
    "path": "tests/app/resources/views/yoyo/component-with-emit.php",
    "content": "<div id=\"component-with-emit\">\n    <p>Emit test</p>\n</div>\n"
  },
  {
    "path": "tests/app/resources/views/yoyo/component-with-listeners.php",
    "content": "<div id=\"component-with-listeners\">\n    <p><?php echo $message; ?></p>\n</div>\n"
  },
  {
    "path": "tests/app/resources/views/yoyo/component-with-redirect.php",
    "content": "<div id=\"component-with-redirect\">\n    <p>Redirect test</p>\n</div>\n"
  },
  {
    "path": "tests/app/resources/views/yoyo/component-with-response-headers.php",
    "content": "<div id=\"component-with-response-headers\">\n    <span id=\"rh-message\"><?php echo $message; ?></span>\n    <div id=\"selected-part\">selected content</div>\n</div>\n"
  },
  {
    "path": "tests/app/resources/views/yoyo/component-with-swap-modifiers.php",
    "content": "<div id=\"component-with-swap-modifiers\">\n    <p>Swap modifiers test</p>\n</div>\n"
  },
  {
    "path": "tests/app/resources/views/yoyo/component-with-trait.php",
    "content": "<div><?php echo $output; ?></div>"
  },
  {
    "path": "tests/app/resources/views/yoyo/computed-property-cache.php",
    "content": "<p id=\"computed-cache\"><?php $this->test_count;\necho $this->test_count; ?></p>"
  },
  {
    "path": "tests/app/resources/views/yoyo/computed-property.php",
    "content": "<p id=\"computed\"><?php echo $this->foo_bar; ?></p>"
  },
  {
    "path": "tests/app/resources/views/yoyo/counter.php",
    "content": "<div id=\"counter\" yoyo:val.count=\"<?php echo $count; ?>\">\n    <button yoyo:get=\"increment\">+</button>\n    <span><?php echo $count; ?></span>\n    <span><?php echo $this->currentCount; ?></span>\n</div>"
  },
  {
    "path": "tests/app/resources/views/yoyo/dependency-injection-action.php",
    "content": "<div>\n    <div id=\"result\"><?php echo $result; ?></div>\n</div>"
  },
  {
    "path": "tests/app/resources/views/yoyo/dependency-injection-class-with-named-argument-mapping.php",
    "content": "<div><?php echo $this->output; ?></div>"
  },
  {
    "path": "tests/app/resources/views/yoyo/dispatch-listener.php",
    "content": "<div id=\"dispatch-listener\">\n    <span id=\"message\"><?php echo $message; ?></span>\n    <span id=\"post-id\"><?php echo $postId; ?></span>\n    <span id=\"status\"><?php echo $status; ?></span>\n</div>\n"
  },
  {
    "path": "tests/app/resources/views/yoyo/foo.blade.php",
    "content": "<div>\n    @spinning \n        blade bar \n    @else \n        blade foo \n    @endspinning\n</div>"
  },
  {
    "path": "tests/app/resources/views/yoyo/foo.php",
    "content": "<div>\n    <?php echo ! $spinning ? 'default foo' : 'default bar'; ?>\n</div>"
  },
  {
    "path": "tests/app/resources/views/yoyo/foo.twig",
    "content": "<div>\n    {% if not spinning %}\n        twig foo\n    {% else %}\n        twig bar\n    {% endif %}\n</div>"
  },
  {
    "path": "tests/app/resources/views/yoyo/layout-a.blade.php",
    "content": "<div>\n    <x-select />\n</div>"
  },
  {
    "path": "tests/app/resources/views/yoyo/layout-b.blade.php",
    "content": "<div>\n    <x-packagename::input />\n</div>"
  },
  {
    "path": "tests/app/resources/views/yoyo/parent.blade.php",
    "content": "<div yoyo:props=\"data\">\n@foreach ($data as $id)\n    \n    @yoyo('child', ['id' => $id], ['id'=>'child-'.$id])\n\n@endforeach\n</div>"
  },
  {
    "path": "tests/app/resources/views/yoyo/parent.php",
    "content": "<div yoyo:props=\"data\">\n<?php foreach ($data as $id): ?>\n\n    <?php echo Yoyo\\yoyo_render('child', ['id' => $id], ['id' => 'child-'.$id]); ?>\n\n<?php endforeach; ?>\n</div>"
  },
  {
    "path": "tests/app/resources/views/yoyo/parent.twig",
    "content": "<div yoyo:props=\"data\">\n{% for id in data %}\n\n    {{ yoyo( 'child', {'id': id}, {'id': 'child-'~id} ) }}\n\n{% endfor %}\n</div>\n"
  },
  {
    "path": "tests/app/resources/views/yoyo/registered-anon.php",
    "content": "<div id=\"registered-anon\"></div>"
  },
  {
    "path": "tests/app/resources/views/yoyo/registered.php",
    "content": "<div id=\"registered\"></div>"
  },
  {
    "path": "tests/app/resources/views/yoyo/set-view-data.php",
    "content": "<div id=\"set-view-data\">\n    <?php echo $foo; ?>-<?php echo $bar; ?>\n</div>"
  },
  {
    "path": "tests/app/resources/views/yoyo/variadic-parameters.php",
    "content": "<div>\n    <div id=\"result\"><?php echo $result; ?></div>\n</div>"
  },
  {
    "path": "tests/app-another/Yoyo/Counter.php",
    "content": "<?php\n\nnamespace Tests\\AppAnother\\Yoyo;\n\nuse Clickfwd\\Yoyo\\Component;\n\nclass Counter extends Component\n{\n    public $count = 0;\n\n    protected $queryString = ['count'];\n\n    protected $props = ['count'];\n\n    public function increment()\n    {\n        $this->count++;\n\n        $this->emit('counter:updated', ['count' => $this->count]);\n    }\n\n    public function getCurrentCountProperty()\n    {\n        return 'The count is now '.$this->count;\n    }\n}\n"
  },
  {
    "path": "tests/app-another/views/components/input.blade.php",
    "content": "app-another/views/components/input.blade.php"
  },
  {
    "path": "tests/app-another/views/counter.blade.php",
    "content": "<div id=\"counter\" yoyo:val.count=\"{{ $count }}\">\n    <button yoyo:get=\"increment\">+</button>\n    <span>{{ $count }}</span>\n    <span>{{ $this->currentCount }}</span>\n</div>"
  },
  {
    "path": "tests/app-another/views/counter.php",
    "content": "<div id=\"counter\" yoyo:val.count=\"<?php echo $count; ?>\">\n    <button yoyo:get=\"increment\">+</button>\n    <span><?php echo $count; ?></span>\n    <span><?php echo $this->currentCount; ?></span>\n</div>"
  },
  {
    "path": "tests/app-another/views/counter.twig",
    "content": "<div id=\"counter\" yoyo:val.count=\"{{ count }}\">\n    <button yoyo:get=\"increment\">+</button>\n    <button yoyo:get=\"decrement\">-</button>\n    <span>{{ count }}</span>\n    <span>{{ this.currentCount }}</span>\n</div>"
  },
  {
    "path": "tests/app-another/views/foo.blade.php",
    "content": "<div>blade foo from another app</div>"
  },
  {
    "path": "tests/app-another/views/foo.php",
    "content": "<div>other foo from another app</div>"
  },
  {
    "path": "tests/app-another/views/foo.twig",
    "content": "<div>twig foo from another app</div>"
  },
  {
    "path": "tests/compiled/.gitkeep",
    "content": ""
  },
  {
    "path": "tests/responses/action-arguments.html",
    "content": "<div id=\"action-arguments\" yoyo=\"\" hx-get=\"render\" class=\"yoyo-wrapper\" yoyo:name=\"action-arguments\" hx-ext=\"yoyo\"\n    hx-include=\"this\" hx-trigger=\"refresh\" hx-target=\"this\" hx-vals='{\"yoyo-id\":\"action-arguments\"}'>\n    <button hx-get=\"someAction(1, 'foo')\" id=\"action-arguments-1\">Click</button><span>1foo</span>\n</div>"
  },
  {
    "path": "tests/responses/computed-property-cache.html",
    "content": "<p id=\"computed-cache\" yoyo=\"\" hx-get=\"render\" class=\"yoyo-wrapper\" yoyo:name=\"computed-property-cache\" hx-ext=\"yoyo\"\n    hx-include=\"this\" hx-trigger=\"refresh\" hx-target=\"this\" hx-vals='{\"yoyo-id\":\"computed-cache\"}'>1</p>"
  },
  {
    "path": "tests/responses/computed-property.html",
    "content": "<p id=\"computed\" yoyo=\"\" hx-get=\"render\" class=\"yoyo-wrapper\" yoyo:name=\"computed-property\" hx-ext=\"yoyo\"\n    hx-include=\"this\" hx-trigger=\"refresh\" hx-target=\"this\" hx-vals='{\"yoyo-id\":\"computed\"}'>bar</p>"
  },
  {
    "path": "tests/responses/nested.blade.html",
    "content": "<div yoyo=\"\" hx-get=\"render\" id=\"parent\" class=\"yoyo-wrapper\" yoyo:name=\"parent\" hx-ext=\"yoyo\" hx-include=\"this\"\n    hx-trigger=\"refresh\" hx-target=\"this\" hx-vals='{\"yoyo-id\":\"parent\",\"data[]\":[1,2,3]}'>\n    <p hx-get=\"render\" id=\"child-1\" class=\"yoyo-wrapper\" yoyo:name=\"child\" hx-ext=\"yoyo\" hx-include=\"this\"\n        hx-trigger=\"refresh\" hx-target=\"this\" hx-vals='{\"yoyo-id\":\"child-1\",\"id\":1}'>blade:1</p>\n    <p hx-get=\"render\" id=\"child-2\" class=\"yoyo-wrapper\" yoyo:name=\"child\" hx-ext=\"yoyo\" hx-include=\"this\"\n        hx-trigger=\"refresh\" hx-target=\"this\" hx-vals='{\"yoyo-id\":\"child-2\",\"id\":2}'>blade:2</p>\n    <p hx-get=\"render\" id=\"child-3\" class=\"yoyo-wrapper\" yoyo:name=\"child\" hx-ext=\"yoyo\" hx-include=\"this\"\n        hx-trigger=\"refresh\" hx-target=\"this\" hx-vals='{\"yoyo-id\":\"child-3\",\"id\":3}'>blade:3</p>\n</div>"
  },
  {
    "path": "tests/responses/nested.html",
    "content": "<div yoyo=\"\" hx-get=\"render\" id=\"parent\" class=\"yoyo-wrapper\" yoyo:name=\"parent\" hx-ext=\"yoyo\" hx-include=\"this\"\n    hx-trigger=\"refresh\" hx-target=\"this\" hx-vals='{\"yoyo-id\":\"parent\",\"data[]\":[1,2,3]}'>\n    <p hx-get=\"render\" id=\"child-1\" class=\"yoyo-wrapper\" yoyo:name=\"child\" hx-ext=\"yoyo\" hx-include=\"this\"\n        hx-trigger=\"refresh\" hx-target=\"this\" hx-vals='{\"yoyo-id\":\"child-1\",\"id\":1}'>1</p>\n    <p hx-get=\"render\" id=\"child-2\" class=\"yoyo-wrapper\" yoyo:name=\"child\" hx-ext=\"yoyo\" hx-include=\"this\"\n        hx-trigger=\"refresh\" hx-target=\"this\" hx-vals='{\"yoyo-id\":\"child-2\",\"id\":2}'>2</p>\n    <p hx-get=\"render\" id=\"child-3\" class=\"yoyo-wrapper\" yoyo:name=\"child\" hx-ext=\"yoyo\" hx-include=\"this\"\n        hx-trigger=\"refresh\" hx-target=\"this\" hx-vals='{\"yoyo-id\":\"child-3\",\"id\":3}'>3</p>\n</div>"
  },
  {
    "path": "tests/responses/nested.twig.html",
    "content": "<div yoyo=\"\" hx-get=\"render\" id=\"parent\" class=\"yoyo-wrapper\" yoyo:name=\"parent\" hx-ext=\"yoyo\" hx-include=\"this\"\n    hx-trigger=\"refresh\" hx-target=\"this\" hx-vals='{\"yoyo-id\":\"parent\",\"data[]\":[1,2,3]}'>\n    <p hx-get=\"render\" id=\"child-1\" class=\"yoyo-wrapper\" yoyo:name=\"child\" hx-ext=\"yoyo\" hx-include=\"this\"\n        hx-trigger=\"refresh\" hx-target=\"this\" hx-vals='{\"yoyo-id\":\"child-1\",\"id\":1}'>twig:1</p>\n    <p hx-get=\"render\" id=\"child-2\" class=\"yoyo-wrapper\" yoyo:name=\"child\" hx-ext=\"yoyo\" hx-include=\"this\"\n        hx-trigger=\"refresh\" hx-target=\"this\" hx-vals='{\"yoyo-id\":\"child-2\",\"id\":2}'>twig:2</p>\n    <p hx-get=\"render\" id=\"child-3\" class=\"yoyo-wrapper\" yoyo:name=\"child\" hx-ext=\"yoyo\" hx-include=\"this\"\n        hx-trigger=\"refresh\" hx-target=\"this\" hx-vals='{\"yoyo-id\":\"child-3\",\"id\":3}'>twig:3</p>\n</div>"
  }
]