{"language":"rust","files":[{"path":"Cargo.toml","content":"[package]\nname = \"app\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[lib]\npath = \"src/lib.rs\"\n\n[[bin]]\nname = \"app\"\npath = \"src/main.rs\"\n\n[dependencies]\naxum = { version = \"0.8\", features = [\"ws\"] }\ntokio = { version = \"1\", features = [\"rt-multi-thread\", \"macros\", \"net\", \"sync\", \"time\"] }\ntower-http = { version = \"0.6\", features = [\"fs\"] }\nfutures-util = \"0.3\"\nrusqlite = { version = \"0.33\", features = [\"bundled\"] }\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\nbase64 = \"0.22\"\nregex = \"1\"\nchrono = { version = \"0.4\", features = [\"serde\"] }\n\n[dev-dependencies]\naxum-test = \"18\"\n# Temporary cap: time 0.3.48 (2026-06-12) added an impl that conflicts\n# (E0119) with cookie 0.18.1's blanket `From<T> for Expiration`; cookie\n# reaches this tree only via axum-test. Drop the cap once cookie ships\n# a fix or time yanks/repairs 0.3.48.\ntime = \">=0.3, <0.3.48\"\n"},{"path":"README.md","content":"# Roundhouse → rust\n\nTranspiled from a Rails source app by [Roundhouse](https://rubys.github.io/roundhouse/).\n\nRunning it serves the blog on http://localhost:3000 (set `PORT` to override), with live Turbo Stream updates over the `/cable` WebSocket.\n\n## Prerequisites\n- Rust 1.85+ (`cargo`)\n- SQLite (system library)\n\n## Build\n```sh\ncargo build --release\n```\n\n## Run\n```sh\n./target/release/app\n```\n\n## Test\n```sh\ncargo test\n```\n\n## End-to-end\nBrowser smoke tests (Playwright). Needs Node.js 18+ and the `sqlite3` CLI; run after the Build steps above — the test config boots the server and seeds `db/seed.sql` itself:\n```sh\ncd e2e\nnpm install\nnpx playwright install chromium\nnpx playwright test\n```\n\n## Regenerate\n```sh\nroundhouse --target rust -o <output-dir> <input-app>\n```\n"},{"path":"db/seed.sql","content":"-- Seed data for the demo blog, for self-contained reproduction from the\n-- published archive (the archive is text-only, so no binary DB is shipped).\n-- Schema matches config/schema.rb (the binary's Schema.load! is idempotent).\n-- Regenerate from a seeded fixture with scripts/… or sqlite3 .dump if data changes.\n--\n-- This file ships in EVERY target archive (injected by src/project.rs\n-- target_files), so the seed step is language-agnostic — `sqlite3 <db> <\n-- db/seed.sql` populates a fresh DB regardless of which target serves it.\n-- INSERTs name their columns explicitly so the file stays valid even if a\n-- target emits its schema columns in a different order than another.\nCREATE TABLE IF NOT EXISTS articles (id INTEGER PRIMARY KEY AUTOINCREMENT, body TEXT, created_at TEXT NOT NULL, title TEXT, updated_at TEXT NOT NULL);\nCREATE TABLE IF NOT EXISTS comments (id INTEGER PRIMARY KEY AUTOINCREMENT, article_id INTEGER NOT NULL, body TEXT, commenter TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);\nCREATE INDEX IF NOT EXISTS index_comments_on_article_id ON comments (article_id);\nINSERT INTO articles (id, title, body, created_at, updated_at) VALUES (1,'Getting Started with Rails','Rails is a web application framework running on the Ruby programming language. It makes building web apps faster and easier with conventions over configuration.','2026-05-15 21:14:56.300213','2026-05-15 21:14:56.300213');\nINSERT INTO articles (id, title, body, created_at, updated_at) VALUES (2,'Understanding MVC Architecture','MVC stands for Model-View-Controller. Models handle data and business logic, Views display information to users, and Controllers coordinate between them.','2026-05-15 21:14:56.382238','2026-05-15 21:14:56.382238');\nINSERT INTO articles (id, title, body, created_at, updated_at) VALUES (3,'Ruby2JS: Rails Everywhere','Ruby2JS transpiles Ruby to JavaScript, enabling Rails applications to run in browsers, on Node.js, and at the edge. Same code, different runtimes.','2026-05-15 21:14:56.386016','2026-05-15 21:14:56.386016');\nINSERT INTO comments (id, article_id, commenter, body, created_at, updated_at) VALUES (1,1,'Alice','Great introduction! Rails really does make development faster.','2026-05-15 21:14:56.328046','2026-05-15 21:14:56.328046');\nINSERT INTO comments (id, article_id, commenter, body, created_at, updated_at) VALUES (2,1,'Bob','I love how Rails handles database migrations automatically.','2026-05-15 21:14:56.379600','2026-05-15 21:14:56.379600');\nINSERT INTO comments (id, article_id, commenter, body, created_at, updated_at) VALUES (3,2,'Carol','This pattern really helps keep code organized!','2026-05-15 21:14:56.383950','2026-05-15 21:14:56.383950');\n"},{"path":"e2e/action_cable.spec.js","content":"import { test, expect } from '@playwright/test'\n\n// article_3 is seeded with zero comments. Assertions are scoped to *our*\n// uniquely-worded comment so this can run in parallel with the Turbo Stream\n// test, which also posts comments on the same article.\nconst ARTICLE_PATH = '/articles/3'\nconst COMMENTER = 'Cable Bot'\nconst BODY = 'Action Cable broadcast smoke-test comment'\n\ntest('a new comment broadcasts live to other viewers via Action Cable', async ({ browser }) => {\n  // Two independent contexts = two separate viewers of the same article.\n  const observerCtx = await browser.newContext()\n  const actorCtx = await browser.newContext()\n  const observer = await observerCtx.newPage()\n  const actor = await actorCtx.newPage()\n\n  const observerRow = observer.locator('#comments > div').filter({ hasText: BODY })\n  const actorRow = actor.locator('#comments > div').filter({ hasText: BODY })\n\n  try {\n    await observer.goto(ARTICLE_PATH)\n    await actor.goto(ARTICLE_PATH)\n    await expect(observerRow).toHaveCount(0)\n\n    // Wait until the observer's Turbo Stream subscription is confirmed, so the\n    // broadcast can't be missed by a not-yet-connected websocket.\n    await expect(observer.locator('turbo-cable-stream-source')).toHaveAttribute('connected', '')\n\n    // The observer must never navigate; this marker proves the row arrives over\n    // the websocket rather than via a reload.\n    await observer.evaluate(() => { window.__noNav = true })\n\n    // The actor submits a comment.\n    await actor.getByLabel('Commenter').fill(COMMENTER)\n    await actor.getByLabel('Body').fill(BODY)\n    await actor.getByRole('button', { name: 'Add Comment' }).click()\n\n    // The observer receives it live, exactly once, without having navigated.\n    await expect(observerRow).toHaveCount(1)\n    await expect(observerRow).toBeVisible()\n    expect(await observer.evaluate(() => window.__noNav)).toBe(true)\n\n    // Cleanup: delete the comment from the actor page (accept the Turbo confirm).\n    actor.on('dialog', dialog => dialog.accept())\n    await actorRow.getByRole('button', { name: 'Delete' }).click()\n    await expect(actorRow).toHaveCount(0)\n\n    // The removal broadcasts too — the observer's row disappears, leaving no residue.\n    await expect(observerRow).toHaveCount(0)\n  } finally {\n    await observerCtx.close()\n    await actorCtx.close()\n  }\n})\n"},{"path":"e2e/flash.spec.js","content":"import { test, expect } from '@playwright/test'\n\n// Flash shows exactly once, then is swept. Trigger a `redirect_to … notice:`\n// → the redirect target renders the \"successfully updated\" notice → a later\n// navigation must NOT show it again. The SWEEP (second assertion) is the\n// regression that matters: a sticky flash re-renders the notice on every\n// page (the bug that motivated moving the sweep into ActionDispatch::Flash).\n//\n// Why EDIT (not create): the suite runs `fullyParallel` against ONE shared\n// server + DB, and `index.spec.js` pins the seeded set to exactly three\n// articles with specific titles + comment counts. Creating an article would\n// race that exact-count assertion; editing an existing one keeps the count,\n// every title, and every comment count fixed — only the body (which no spec\n// asserts) changes — so flash.spec stays isolated from its neighbours. The\n// sweep is checked by reloading the article's own show page, not the index,\n// to steer clear of index.spec entirely.\n//\n// Scoping (via E2E_SKIP in each target's README ## End-to-end block): runs\n// only on cookie-backed, per-session targets (ruby, jruby, go, rust). The\n// in-memory-flash targets (crystal, typescript) share ONE global flash slot,\n// which races with the comment specs' `redirect_to … notice:` under\n// `fullyParallel`; the remaining targets don't wire flash yet. As a target\n// gains per-session flash, drop it from that skip list.\ntest('flash notice shows once then is swept', async ({ page }) => {\n  await page.goto('/articles/1/edit')\n\n  // Keep the title (index.spec pins it to this exact value); only the body\n  // changes, which no spec asserts. An update with a valid body redirects\n  // with the notice regardless of whether any field actually changed.\n  await page.getByLabel('Title').fill('Getting Started with Rails')\n  await page.getByLabel('Body').fill('Edited by the flash e2e spec to exercise the show-once sweep.')\n  await page.getByRole('button', { name: 'Update Article' }).click()\n\n  // Redirected to the article — the notice renders once.\n  await expect(page.locator('#notice')).toHaveText('Article was successfully updated.')\n\n  // Navigate again — the notice must be gone (swept), not sticky.\n  await page.goto('/articles/1')\n  await expect(page.locator('#notice')).toHaveCount(0)\n})\n"},{"path":"e2e/index.spec.js","content":"import { test, expect } from '@playwright/test'\n\n// The seeded database renders three articles, newest-first (prepend order).\n// Each entry pins the DOM id, the title link text, and the comment-count\n// label shown beside the title.\nconst expectedArticles = [\n  { id: 'article_3', title: 'Ruby2JS: Rails Everywhere',      comments: '(0 comments)' },\n  { id: 'article_2', title: 'Understanding MVC Architecture', comments: '(1 comment)' },\n  { id: 'article_1', title: 'Getting Started with Rails',     comments: '(2 comments)' },\n]\n\ntest('index lists the three seeded articles in order with correct comment counts', async ({ page }) => {\n  await page.goto('/')\n\n  // Exactly three articles render in the list.\n  const rows = page.locator('#articles > div')\n  await expect(rows).toHaveCount(expectedArticles.length)\n\n  // Index positionally so the displayed order is asserted, not just presence.\n  for (let i = 0; i < expectedArticles.length; i++) {\n    const { id, title, comments } = expectedArticles[i]\n    const row = rows.nth(i)\n    await expect(row).toHaveAttribute('id', id)\n    await expect(row.locator('h2 a')).toHaveText(title)\n    await expect(row.locator(`#comments_count_${id}`)).toHaveText(comments)\n  }\n})\n"},{"path":"e2e/package.json","content":"{\n  \"name\": \"app-e2e\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"description\": \"Playwright end-to-end smoke tests for this archive — see ../README.md\",\n  \"scripts\": {\n    \"test\": \"playwright test\"\n  },\n  \"devDependencies\": {\n    \"@playwright/test\": \"^1.49.0\"\n  }\n}\n"},{"path":"e2e/playwright.config.js","content":"import { defineConfig, devices } from '@playwright/test'\n\n// Generated by Roundhouse. Self-contained: `webServer` boots the app\n// (built per ../README.md) and global-setup.js seeds ../db/seed.sql.\n// E2E_SKIP is a space/comma list of spec basenames to skip.\nconst SKIP = (process.env.E2E_SKIP || '').split(/[\\s,]+/).filter(Boolean)\n\nexport default defineConfig({\n  testDir: '.',\n  testIgnore: SKIP.map(name => `**/${name}*.spec.js`),\n  fullyParallel: true,\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 2 : 0,\n  reporter: process.env.CI ? [['github'], ['list']] : 'list',\n  use: {\n    baseURL: 'http://localhost:3000',\n    trace: 'on-first-retry',\n  },\n  // seed.js runs INSIDE the webServer command (not globalSetup —\n  // Playwright boots the webServer first) so the server opens an\n  // already-seeded DB.\n  webServer: {\n    command: 'node e2e/seed.js && ./target/release/app',\n    cwd: '..',\n    url: 'http://localhost:3000/articles',\n    reuseExistingServer: !process.env.CI,\n    timeout: 120_000,\n  },\n  projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],\n})\n"},{"path":"e2e/seed.js","content":"// Generated by Roundhouse. Seeds the server's DB from ../db/seed.sql\n// (sqlite3 CLI). Runs as the first half of playwright.config.js's\n// webServer command, so the server boots against an already-seeded\n// DB (some targets self-seed demo data on an empty one, with\n// different row timestamps than the canonical seed). Idempotent:\n// skips when articles already exist, so re-runs don't double-seed.\n// For a truly fresh run, delete storage/development.sqlite3 (or re-extract the archive).\nimport { execFileSync } from 'node:child_process'\nimport { mkdirSync, readFileSync } from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')\nconst db = path.join(root, 'storage/development.sqlite3')\nconst seed = path.join(root, 'db', 'seed.sql')\n\nmkdirSync(path.dirname(db), { recursive: true })\nlet count = 0\ntry {\n  count = Number(execFileSync('sqlite3', [db, 'SELECT COUNT(*) FROM articles'],\n    { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim())\n} catch { /* missing file or table — seed below */ }\nif (count === 0) {\n  execFileSync('sqlite3', [db], { input: readFileSync(seed, 'utf8') })\n  console.log(`seed.js: seeded ${db} from db/seed.sql`)\n} else {\n  console.log(`seed.js: db already seeded (${count} articles)`)\n}\n"},{"path":"e2e/tailwind.spec.js","content":"import { test, expect } from '@playwright/test'\n\ntest('tailwind is compiled and applied', async ({ page }) => {\n  await page.goto('/')\n\n  // Stronger check: the compiled stylesheet actually serves (catches a broken\n  // asset pipeline directly). Propshaft fingerprints the filename, so derive\n  // the digested href from the page instead of hardcoding it.\n  const hrefs = await page\n    .locator('link[rel=\"stylesheet\"]')\n    .evaluateAll(links => links.map(l => l.getAttribute('href')))\n  const tailwindHref = hrefs.find(h => /tailwind/.test(h)) ?? hrefs[0]\n  expect(tailwindHref, 'expected a stylesheet <link> on the page').toBeTruthy()\n\n  const res = await page.request.get(tailwindHref)\n  expect(res.status()).toBe(200)\n  expect(res.headers()['content-type']).toContain('css')\n\n  // The \"New article\" button (an <a> styled via Tailwind utilities) is the probe.\n  const button = page.getByRole('link', { name: 'New article' })\n  const s = await button.evaluate(el => {\n    const c = getComputedStyle(el)\n    return { display: c.display, background: c.backgroundColor,\n             radius: c.borderRadius, padding: c.paddingTop }\n  })\n\n  // Utilities applied, not browser defaults (an unstyled <a> would be\n  // display:inline, transparent background, no radius, no padding):\n  expect(s.display).toBe('block')                    // `block`\n  expect(s.radius).toBe('6px')                       // `rounded-md`\n  expect(s.padding).toBe('10px')                     // `py-2.5`\n  expect(s.background).not.toBe('rgba(0, 0, 0, 0)')  // not transparent\n\n  // The distinctive color token resolved (bg-blue-600):\n  expect(s.background).toBe('oklch(0.546 0.245 262.881)')\n})\n"},{"path":"e2e/turbo_comment.spec.js","content":"import { test, expect } from '@playwright/test'\n\n// article_3 (\"Ruby2JS: Rails Everywhere\") is seeded with zero comments. The\n// assertions are scoped to *our* uniquely-worded comment (not the whole list)\n// so this test can run in parallel with the Action Cable test, which also\n// posts comments on the same article.\nconst ARTICLE_PATH = '/articles/3'\nconst COMMENTER = 'Playwright Bot'\nconst BODY = 'Turbo stream smoke-test comment'\n\ntest('adding a comment shows the new row via Turbo without a full reload', async ({ page }) => {\n  await page.goto(ARTICLE_PATH)\n\n  const ours = page.locator('#comments > div').filter({ hasText: BODY })\n  await expect(ours).toHaveCount(0) // not present yet\n\n  // Marker on window: a Turbo Drive visit preserves it; a full page reload wipes it.\n  await page.evaluate(() => { window.__noFullReload = true })\n\n  await page.getByLabel('Commenter').fill(COMMENTER)\n  await page.getByLabel('Body').fill(BODY)\n  await page.getByRole('button', { name: 'Add Comment' }).click()\n\n  // Our new comment row appears exactly once (no redirect/broadcast duplicate).\n  await expect(ours).toHaveCount(1)\n  await expect(ours).toBeVisible()\n\n  // Turbo handled the redirect as a Drive visit, not a full browser reload.\n  expect(await page.evaluate(() => window.__noFullReload)).toBe(true)\n\n  // Cleanup: delete the comment we added, accepting the Turbo confirm dialog.\n  page.on('dialog', dialog => dialog.accept())\n  await ours.getByRole('button', { name: 'Delete' }).click()\n  await expect(ours).toHaveCount(0) // gone — no residue left behind\n})\n"},{"path":"e2e/validation.spec.js","content":"import { test, expect } from '@playwright/test'\n\ntest('new article form shows a validation error for a too-short body', async ({ page }) => {\n  await page.goto('/articles/new')\n\n  // Title only needs presence; body must be at least 10 chars. A short body\n  // (9 chars) trips the length validation, so create re-renders :new (422).\n  await page.getByLabel('Title').fill('Hi')\n  await page.getByLabel('Body').fill('too short')\n\n  await page.getByRole('button', { name: 'Create Article' }).click()\n\n  // The error summary appears with the specific failure message.\n  const errors = page.locator('#error_explanation')\n  await expect(errors).toBeVisible()\n  await expect(errors).toContainText('prohibited this article from being saved')\n  await expect(errors).toContainText('Body is too short (minimum is 10 characters)')\n})\n"},{"path":"src/action_controller_base.rs","content":"// Generated from runtime/ruby/action_controller/base.rb at app emit time.\n// Do not edit by hand — edit the source `.rb` and re-run emit.\n\nuse crate::flash::Flash;\nuse crate::session::Session;\nuse crate::param_value::ParamValue;\nuse crate::errors_ext::raise;\nuse crate::errors_ext::NotImplementedError;\n\nstatic STATUS_CODES: std::sync::LazyLock<std::collections::HashMap<&'static str, i64>> = std::sync::LazyLock::new(|| std::collections::HashMap::from([(\"ok\", 200_i64), (\"created\", 201_i64), (\"accepted\", 202_i64), (\"no_content\", 204_i64), (\"moved_permanently\", 301_i64), (\"found\", 302_i64), (\"see_other\", 303_i64), (\"not_modified\", 304_i64), (\"bad_request\", 400_i64), (\"unauthorized\", 401_i64), (\"forbidden\", 403_i64), (\"not_found\", 404_i64), (\"unprocessable_entity\", 422_i64), (\"unprocessable_content\", 422_i64), (\"internal_server_error\", 500_i64)]));\n\n#[derive(Clone, Default)]\npub struct Base {\n    pub params: std::collections::HashMap<String, ParamValue>,\n    pub session: Session,\n    pub flash: Flash,\n    pub request_method: String,\n    pub request_path: String,\n    pub request_format: String,\n    pub status: i64,\n    pub body: String,\n    pub location: Option<String>,\n    pub content_type: String,\n}\n\nimpl Base {\n    pub fn params(&self) -> std::collections::HashMap<String, ParamValue> {\n        self.params.clone()\n    }\n\n    pub fn set_params(&mut self, value: std::collections::HashMap<String, ParamValue>) {\n        self.params = value\n    }\n\n    pub fn session(&self) -> Session {\n        self.session.clone()\n    }\n\n    pub fn set_session(&mut self, value: Session) {\n        self.session = value\n    }\n\n    pub fn flash(&self) -> Flash {\n        self.flash.clone()\n    }\n\n    pub fn set_flash(&mut self, value: Flash) {\n        self.flash = value\n    }\n\n    pub fn request_method(&self) -> String {\n        self.request_method.clone()\n    }\n\n    pub fn set_request_method(&mut self, value: &str) {\n        self.request_method = (value).to_string()\n    }\n\n    pub fn request_path(&self) -> String {\n        self.request_path.clone()\n    }\n\n    pub fn set_request_path(&mut self, value: &str) {\n        self.request_path = (value).to_string()\n    }\n\n    pub fn request_format(&self) -> String {\n        self.request_format.clone()\n    }\n\n    pub fn set_request_format(&mut self, value: &str) {\n        self.request_format = (value).to_string()\n    }\n\n    pub fn status(&self) -> i64 {\n        self.status.clone()\n    }\n\n    pub fn body(&self) -> String {\n        self.body.clone()\n    }\n\n    pub fn location(&self) -> Option<String> {\n        self.location.clone()\n    }\n\n    pub fn content_type(&self) -> String {\n        self.content_type.clone()\n    }\n\n    pub fn new() -> Self {\n        let mut params: std::collections::HashMap<String, ParamValue> = std::collections::HashMap::new();\n        let mut session: Session = Session::new();\n        let mut flash: Flash = Flash::new();\n        let mut status: i64 = 200_i64;\n        let mut body: String = (\"\").to_string();\n        let mut location: Option<String> = None;\n        let mut request_format: String = \"html\".to_string();\n        let mut content_type: String = (\"text/html; charset=utf-8\").to_string();\n        let request_method: String = String::new();\n        let request_path: String = String::new();\n        Self { params, session, flash, request_method, request_path, request_format, status, body, location, content_type }\n    }\n\n    pub fn process_action(&self, _action_name: &str) {\n        raise(NotImplementedError, \"process_action must be overridden by subclass\");\n    }\n\n    pub fn render(&mut self, body: &str, status: &str, content_type: Option<String>, location: Option<String>) {\n        self.body = (body).to_string();\n        self.status = Self::resolve_status(&(status));\n        if !(content_type.is_none()) { self.content_type = (content_type.clone().unwrap()).to_string() };\n        if !(location.is_none()) { self.location = Some((location.clone().unwrap()).to_string()) };\n    }\n\n    pub fn redirect_to(&mut self, path: &str, notice: Option<String>, alert: Option<String>, status: &str) {\n        self.location = Some((path).to_string());\n        self.status = Self::resolve_status(&(status));\n        if !(notice.is_none()) { self.flash.set(\"notice\", Some(notice.clone().unwrap())) };\n        if !(alert.is_none()) { self.flash.set(\"alert\", Some(alert.clone().unwrap())) };\n    }\n\n    pub fn head(&mut self, status: &str, content_type: Option<String>) {\n        self.status = Self::resolve_status(&(status));\n        self.body = (\"\").to_string();\n        if !(content_type.is_none()) { self.content_type = (content_type.clone().unwrap()).to_string() };\n    }\n\n    pub fn resolve_status(s: &str) -> i64 {\n        STATUS_CODES.get(s).cloned().unwrap_or(200_i64)\n    }\n}\n"},{"path":"src/active_record_adapter.rs","content":"//! Abstract `ActiveRecordAdapter` trait — the rust analog of\n//! crystal's `abstract class ActiveRecordAdapter` and TS's\n//! `interface ActiveRecordAdapter`. Hand-written for Phase 3.\n//!\n//! The 9-method contract `runtime/ruby/active_record/base.rb` calls\n//! against `ActiveRecord.adapter`. Every concrete adapter (production\n//! sqlite, in-memory framework-test, future libsql/D1) implements it.\n//!\n//! Return shapes are `serde_json::Value` because the abstract slot is\n//! polymorphic — concrete adapters produce concrete row types\n//! (`HashMap<String, rusqlite::Value>` for sqlite, an in-memory\n//! `TestRow` for the framework-test adapter), and the only common\n//! surface is the untyped JSON tree. The transpiled `Base` methods\n//! that call into the adapter feed the result through\n//! `instantiate(row)` which subclasses override with concrete-typed\n//! per-column extraction.\n\nuse serde_json::Value;\nuse std::collections::HashMap;\n\n// Row shape: `HashMap<String, Value>` (not `Value`). The transpiled\n// `runtime/ruby/active_record/base.rb` types rows as\n// `Hash[String, untyped]` and feeds them to `Self::instantiate(row)`\n// whose RBS signature is `(Hash[String, untyped]) -> Base`. Returning\n// a bare `Value` would force the transpile to insert an `as_object`-\n// style coercion at every call site — handing the trait the\n// pre-shaped HashMap keeps the body-typer's view aligned with what\n// the runtime delivers.\n//\n// Parameters use owned `String` (not `&str`) so transpiled call sites\n// can pass the result of `Self::table_name() -> String` directly.\n// Rust idiom would prefer `&str`, but emit-side auto-borrow at every\n// call site is a much wider change than the one-time alloc cost here.\npub type Row = HashMap<String, Value>;\n\npub trait ActiveRecordAdapter: Send + Sync {\n    fn all(&self, table_name: String) -> Vec<Row>;\n    fn find(&self, table_name: String, id: i64) -> Option<Row>;\n    fn r#where(&self, table_name: String, conditions: HashMap<String, Value>) -> Vec<Row>;\n    fn count(&self, table_name: String) -> i64;\n    fn exists(&self, table_name: String, id: i64) -> bool;\n    fn insert(&self, table_name: String, attributes: HashMap<String, Value>) -> i64;\n    fn update(&self, table_name: String, id: i64, attributes: HashMap<String, Value>);\n    fn delete(&self, table_name: String, id: i64);\n    fn truncate(&self, table_name: String);\n}\n"},{"path":"src/active_record_base.rs","content":"// Generated from runtime/ruby/active_record/base.rb at app emit time.\n// Do not edit by hand — edit the source `.rb` and re-run emit.\n\nuse crate::active_record_adapter::ActiveRecordAdapter;\nuse crate::adapter_interface::AdapterInterface;\nuse crate::errors_ext::raise;\nuse crate::errors_ext::name;\nuse crate::errors_ext::NotImplementedError;\nuse crate::errors_ext::RecordNotFound;\nuse crate::errors_ext::RecordInvalid;\n\n#[derive(Clone, Default)]\npub struct Base {\n    pub id: i64,\n    pub errors: Vec<String>,\n    pub persisted: bool,\n    pub destroyed: bool,\n}\n\nimpl Base {\n    pub fn id(&self) -> i64 {\n        self.id.clone()\n    }\n\n    pub fn set_id(&mut self, value: i64) {\n        self.id = value\n    }\n\n    pub fn errors(&self) -> Vec<String> {\n        self.errors.clone()\n    }\n\n    pub fn new(_attrs: std::collections::HashMap<String, serde_json::Value>) -> Self {\n        let mut id: i64 = 0_i64;\n        let mut errors: Vec<String> = vec![];\n        let mut persisted: bool = false;\n        let mut destroyed: bool = false;\n        Self { id, errors, persisted, destroyed }\n    }\n\n    pub fn table_name() -> String {\n        raise(NotImplementedError, format!(\"{}.table_name must be overridden\", name()))\n    }\n\n    pub fn schema_columns() -> Vec<String> {\n        raise(NotImplementedError, format!(\"{}.schema_columns must be overridden\", name()))\n    }\n\n    pub fn instantiate(_row: std::collections::HashMap<String, serde_json::Value>) -> Base {\n        raise(NotImplementedError, format!(\"{}.instantiate must be overridden\", name()))\n    }\n\n    pub fn _adapter_find_by_id(id: i64) -> Option<Base> {\n        let Some(row) = ActiveRecord::adapter().find(Self::table_name(), id) else { return None };\n        Some(Self::instantiate(row.clone().clone()))\n    }\n\n    pub fn _adapter_all() -> Vec<Base> {\n        ActiveRecord::adapter().all(Self::table_name()).into_iter().map(|row| { Self::instantiate(row.clone()) }).collect::<Vec<_>>()\n    }\n\n    pub fn _adapter_insert() -> i64 {\n        0_i64\n    }\n\n    pub fn _adapter_update() {\n    }\n\n    pub fn _adapter_delete() {\n    }\n\n    pub fn _adapter_count() -> i64 {\n        ActiveRecord::adapter().count(Self::table_name())\n    }\n\n    pub fn _adapter_exists_by_id(id: i64) -> bool {\n        ActiveRecord::adapter().exists(Self::table_name(), id)\n    }\n\n    pub fn _adapter_truncate() {\n        ActiveRecord::adapter().truncate(Self::table_name())\n    }\n\n    pub fn _adapter_reload() -> Option<Base> {\n        None\n    }\n\n    pub fn attributes() -> std::collections::HashMap<String, serde_json::Value> {\n        std::collections::HashMap::new()\n    }\n\n    pub fn get_index(&self, _name: &str) -> serde_json::Value {\n        raise(NotImplementedError, \"[] must be overridden by subclass\")\n    }\n\n    pub fn set_index(&self, _name: &str, _value: serde_json::Value) {\n        raise(NotImplementedError, \"[]= must be overridden by subclass\")\n    }\n\n    pub fn assign_from_row(_row: std::collections::HashMap<String, serde_json::Value>) {\n    }\n\n    pub fn dom_prefix(&self) -> String {\n        raise(NotImplementedError, \"dom_prefix must be overridden by subclass\")\n    }\n\n    pub fn persisted(&self) -> bool {\n        self.persisted.clone()\n    }\n\n    pub fn new_record(&self) -> bool {\n        !(self.persisted)\n    }\n\n    pub fn destroyed(&self) -> bool {\n        self.destroyed.clone()\n    }\n\n    pub fn mark_persisted_bang(&mut self) {\n        self.persisted = true;\n        self.destroyed = false;\n    }\n\n    pub fn all() -> Vec<Base> {\n        Self::_adapter_all()\n    }\n\n    pub fn find(id: i64) -> Base {\n        let Some(result) = Self::_adapter_find_by_id(id) else { raise(RecordNotFound, format!(\"Couldn't find {} with id={}\", name(), id)) };\n        result.clone()\n    }\n\n    pub fn find_by(conditions: std::collections::HashMap<String, serde_json::Value>) -> Option<Base> {\n        let mut rows = ActiveRecord::adapter().r#where(Self::table_name(), conditions.clone());\n        if (rows.len() as i64) == 0_i64 { return None };\n        Some(Self::instantiate(rows.clone()[(0_i64) as usize].clone()))\n    }\n\n    pub fn r#where(conditions: std::collections::HashMap<String, serde_json::Value>) -> Vec<Base> {\n        ActiveRecord::adapter().r#where(Self::table_name(), conditions.clone()).into_iter().map(|row| { Self::instantiate(row.clone()) }).collect::<Vec<_>>()\n    }\n\n    pub fn count() -> i64 {\n        Self::_adapter_count()\n    }\n\n    pub fn exists(id: i64) -> bool {\n        Self::_adapter_exists_by_id(id)\n    }\n\n    pub fn destroy_all() -> Vec<Base> {\n        let mut records = Self::all();\n        records.clone().iter_mut().for_each(|r| { r.destroy(); });\n        records.clone()\n    }\n\n    pub fn create(attrs: std::collections::HashMap<String, serde_json::Value>) -> Base {\n        let mut instance = Self::new(attrs);\n        instance.save();\n        instance.clone()\n    }\n\n    pub fn create_bang(attrs: std::collections::HashMap<String, serde_json::Value>) -> Base {\n        let mut instance = Self::new(attrs);\n        if !(instance.save()) { raise(RecordInvalid, instance.clone()) };\n        instance.clone()\n    }\n\n    pub fn last() -> Option<Base> {\n        let mut records = Self::all();\n        if !(records.is_empty()) { Some(records.clone()[records.clone().len() - 1_usize].clone()) } else { None }\n    }\n\n    pub fn save(&mut self) -> bool {\n        Self::before_validation();\n        let ok = self.valid();\n        Self::after_validation();\n        if !(ok) { return false };\n        Self::before_save();\n        if self.new_record() { Self::before_create();\n        self.fill_timestamps(true);\n        self.id = Self::_adapter_insert();\n        self.persisted = true;\n        Self::after_create();\n        Self::after_create_commit() } else { Self::before_update();\n        self.fill_timestamps(false);\n        Self::_adapter_update();\n        Self::after_update();\n        Self::after_update_commit() };\n        Self::after_save();\n        Self::after_save_commit();\n        Self::after_commit();\n        true\n    }\n\n    pub fn save_bang(&mut self) -> Base {\n        if !(self.save()) { raise(RecordInvalid, self) };\n        self.clone()\n    }\n\n    pub fn destroy(&mut self) -> Base {\n        if !(self.persisted()) { return self.clone() };\n        Self::before_destroy();\n        Self::_adapter_delete();\n        self.persisted = false;\n        self.destroyed = true;\n        Self::after_destroy();\n        Self::after_destroy_commit();\n        Self::after_commit();\n        self.clone()\n    }\n\n    pub fn reload(&self) -> Base {\n        Self::_adapter_reload();\n        self.clone()\n    }\n\n    pub fn before_validation() {\n    }\n\n    pub fn after_validation() {\n    }\n\n    pub fn before_save() {\n    }\n\n    pub fn after_save() {\n    }\n\n    pub fn before_create() {\n    }\n\n    pub fn after_create() {\n    }\n\n    pub fn before_update() {\n    }\n\n    pub fn after_update() {\n    }\n\n    pub fn before_destroy() {\n    }\n\n    pub fn after_destroy() {\n    }\n\n    pub fn after_commit() {\n    }\n\n    pub fn after_create_commit() {\n    }\n\n    pub fn after_update_commit() {\n    }\n\n    pub fn after_destroy_commit() {\n    }\n\n    pub fn after_save_commit() {\n    }\n\n    pub fn after_touch() {\n    }\n\n    pub fn validate() {\n    }\n\n    pub fn fill_timestamps(&mut self, creating: bool) {\n        let mut cols = Self::schema_columns();\n        let now = chrono::Utc::now().to_rfc3339();\n        if cols.iter().any(|__c| __c == \"updated_at\") { self.set_index(\"updated_at\", serde_json::Value::from(now.clone())) };\n        if creating && cols.iter().any(|__c| __c == \"created_at\") { self.set_index(\"created_at\", serde_json::Value::from(now.clone())) };\n    }\n\n    pub fn valid(&mut self) -> bool {\n        self.errors = vec![];\n        Self::validate();\n        self.errors.is_empty()\n    }\n}\n\npub struct ActiveRecord;\n\nstatic ADAPTER: std::sync::Mutex<Option<AdapterInterface>> = std::sync::Mutex::new(None);\n\nimpl ActiveRecord {\n    pub fn adapter() -> AdapterInterface {\n        ADAPTER.lock().unwrap().clone().unwrap_or_default()\n    }\n\n    pub fn set_adapter(value: AdapterInterface) {\n        *ADAPTER.lock().unwrap() = Some(value)\n    }\n}\n"},{"path":"src/adapter_interface.rs","content":"//! `AdapterInterface` — the concrete type the transpiled\n//! `ActiveRecord.adapter` slot uses. Wraps an `Arc<dyn ActiveRecordAdapter>`\n//! so the module-singleton emit's slot template\n//! (`Mutex<Option<AdapterInterface>>` + `.clone().unwrap_or_default()`)\n//! works without per-target rust2 emit branching.\n//!\n//! Why a wrapper: the `runtime/ruby/active_record/base.rbs` types\n//! `ActiveRecord.adapter` as `AdapterInterface` (the analyzer registers\n//! that class with the 9-method contract — `all/find/where/count/exists?/\n//! insert/update/delete/truncate`). Transpiled call sites\n//! (`ActiveRecord::adapter().find(...)`) need a *single* concrete type\n//! that:\n//!   - Is `Clone` (the slot template does `.clone()` on the mutex guard).\n//!   - Has a `Default` (the template falls back to `Default::default()`\n//!     when the slot is `None`).\n//!   - Forwards every adapter method to whatever concrete impl was\n//!     installed at boot (sqlite, framework-test, libsql, ...).\n//!\n//! A bare `Arc<dyn ActiveRecordAdapter>` lacks `Default`. Wrapping it\n//! lets us provide a panicking-on-call \"not configured\" default\n//! (matches the call-time error you'd get if the boot path forgot to\n//! install an adapter — earlier than e.g. a SQL error).\n//!\n//! Install at boot:\n//!     ActiveRecord::set_adapter(AdapterInterface::new(SqliteAdapter::open(\"./db.sqlite\")));\n\nuse std::collections::HashMap;\nuse std::sync::Arc;\n\nuse serde_json::Value;\n\nuse crate::active_record_adapter::{ActiveRecordAdapter, Row};\n\nstruct NotConfigured;\nimpl ActiveRecordAdapter for NotConfigured {\n    fn all(&self, _t: String) -> Vec<Row> {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn find(&self, _t: String, _id: i64) -> Option<Row> {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn r#where(&self, _t: String, _c: HashMap<String, Value>) -> Vec<Row> {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn count(&self, _t: String) -> i64 {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn exists(&self, _t: String, _id: i64) -> bool {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn insert(&self, _t: String, _a: HashMap<String, Value>) -> i64 {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn update(&self, _t: String, _id: i64, _a: HashMap<String, Value>) {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn delete(&self, _t: String, _id: i64) {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n    fn truncate(&self, _t: String) {\n        panic!(\"ActiveRecord.adapter was not set before use\")\n    }\n}\n\n#[derive(Clone)]\npub struct AdapterInterface(Arc<dyn ActiveRecordAdapter + Send + Sync>);\n\nimpl Default for AdapterInterface {\n    fn default() -> Self {\n        Self(Arc::new(NotConfigured))\n    }\n}\n\nimpl AdapterInterface {\n    pub fn new<A>(adapter: A) -> Self\n    where\n        A: ActiveRecordAdapter + Send + Sync + 'static,\n    {\n        Self(Arc::new(adapter))\n    }\n}\n\nimpl ActiveRecordAdapter for AdapterInterface {\n    fn all(&self, t: String) -> Vec<Row> {\n        self.0.all(t)\n    }\n    fn find(&self, t: String, id: i64) -> Option<Row> {\n        self.0.find(t, id)\n    }\n    fn r#where(&self, t: String, c: HashMap<String, Value>) -> Vec<Row> {\n        self.0.r#where(t, c)\n    }\n    fn count(&self, t: String) -> i64 {\n        self.0.count(t)\n    }\n    fn exists(&self, t: String, id: i64) -> bool {\n        self.0.exists(t, id)\n    }\n    fn insert(&self, t: String, a: HashMap<String, Value>) -> i64 {\n        self.0.insert(t, a)\n    }\n    fn update(&self, t: String, id: i64, a: HashMap<String, Value>) {\n        self.0.update(t, id, a)\n    }\n    fn delete(&self, t: String, id: i64) {\n        self.0.delete(t, id)\n    }\n    fn truncate(&self, t: String) {\n        self.0.truncate(t)\n    }\n}\n"},{"path":"src/broadcasts.rs","content":"//! Turbo Streams broadcasts shim.\n//!\n//! The model lowerer's `broadcasts_to` expansion (see\n//! `src/lower/broadcasts.rs`) produces calls like\n//! `Broadcasts.prepend(stream: \"x\", target: \"y\", html: \"...\")`\n//! from inside model callback methods (`after_create`, etc.).\n//! rust2 emits the kwargs as a `HashMap<String, Value>`, so each\n//! shim method here accepts the unified hash shape and pulls\n//! the named fields out.\n//!\n//! State lives in a thread-local log so framework tests can assert\n//! on what got emitted; production installs a broadcaster via\n//! `install_broadcaster` that fans fragments out to the cable\n//! websocket. Mirrors `runtime/crystal/broadcasts.cr` member-for-\n//! member at the shim level.\n//!\n//! For Phase 5 the production path is a no-op stub — Cable wiring\n//! arrives in a later phase.\n\nuse std::cell::RefCell;\nuse std::collections::HashMap;\n\nuse serde_json::Value;\n\nthread_local! {\n    /// In-memory broadcast log: `(action, stream, target, html)`\n    /// tuples in emission order. Tests inspect this after running\n    /// model callbacks; production reads it through `log()` if a\n    /// fan-out plugin needs the trail.\n    static LOG: RefCell<Vec<(String, String, String, String)>> =\n        const { RefCell::new(Vec::new()) };\n}\n\n/// `Broadcasts` namespace — `pub struct Broadcasts;` + impl gives\n/// the same `Broadcasts::method(...)` call shape the lowered model\n/// callbacks emit.\npub struct Broadcasts;\n\nimpl Broadcasts {\n    /// Reset the in-memory log. Framework tests call this between\n    /// assertions; production typically doesn't.\n    pub fn reset_log_bang() {\n        LOG.with(|c| c.borrow_mut().clear());\n    }\n\n    /// Snapshot the log as a fresh Vec.\n    pub fn log() -> Vec<(String, String, String, String)> {\n        LOG.with(|c| c.borrow().clone())\n    }\n\n    pub fn append(attrs: HashMap<String, Value>) {\n        Self::record(\"append\", &attrs);\n    }\n\n    pub fn prepend(attrs: HashMap<String, Value>) {\n        Self::record(\"prepend\", &attrs);\n    }\n\n    pub fn replace(attrs: HashMap<String, Value>) {\n        Self::record(\"replace\", &attrs);\n    }\n\n    pub fn remove(attrs: HashMap<String, Value>) {\n        Self::record(\"remove\", &attrs);\n    }\n\n    fn record(action: &str, attrs: &HashMap<String, Value>) {\n        let stream = attrs.get(\"stream\").and_then(|v| v.as_str()).unwrap_or(\"\").to_string();\n        let target = attrs.get(\"target\").and_then(|v| v.as_str()).unwrap_or(\"\").to_string();\n        let html = attrs.get(\"html\").and_then(|v| v.as_str()).unwrap_or(\"\").to_string();\n        LOG.with(|c| {\n            c.borrow_mut().push((\n                action.to_string(),\n                stream.clone(),\n                target.clone(),\n                html.clone(),\n            ))\n        });\n        // Forward to the live Action Cable fan-out. The model callbacks\n        // (`broadcasts_to` lowering) hand us the already-rendered partial\n        // as `html`; compose the `<turbo-stream>` wrapper and fan it out to\n        // every subscriber of `stream`. Without this the production path is\n        // a no-op (only the in-memory LOG is populated) and a subscribed\n        // `<turbo-cable-stream-source>` never sees the create/destroy\n        // broadcast — the e2e action_cable spec. The cable server fans out\n        // via per-subscriber mpsc channels, so this is safe to call from\n        // any axum worker thread.\n        let fragment = crate::cable::turbo_stream_html(action, &target, &html);\n        crate::cable::CABLE.broadcast(&stream, &fragment);\n    }\n}\n"},{"path":"src/cable.rs","content":"//! Action Cable server + Turbo Streams broadcaster.\n//!\n//! Mirrors the TS runtime's `CableServer` + `/cable` handler.\n//! Ported from railcar's proven implementation (pings + the\n//! `actioncable-v1-json` subprotocol are both known-good there).\n//!\n//! Two halves:\n//!   1. Broadcasting — models call `broadcast_prepend_to` /\n//!      `broadcast_replace_to` / `broadcast_remove_to`, which render\n//!      the appropriate `<turbo-stream>` element and push it to\n//!      every subscriber of the given channel. A partial-renderer\n//!      registry (`register_partial`) lets the runtime reach back\n//!      into the generated `views::*` functions without this file\n//!      having to know model-specific types.\n//!   2. WebSocket — `cable_handler` upgrades incoming requests,\n//!      sends a welcome frame, pings every 3s, and on `subscribe`\n//!      commands decodes Turbo's signed-stream-name blob to recover\n//!      the channel name, then registers the socket's outbound mpsc\n//!      sender with the global `CABLE` registry.\n//!\n//! Generated code (Phase B of the cable work) calls these via\n//! `crate::cable::...` from `impl Broadcaster for <Model>` blocks\n//! produced by the emitter's `broadcasts_to` translation.\n//!\n//! The `actioncable-v1-json` subprotocol spec:\n//!   https://github.com/rails/rails/blob/main/actioncable/lib/action_cable/server/worker.rb\n//! Frames used here: `welcome`, `ping`, `confirm_subscription`,\n//! `message`. Rejections + `unsubscribe` commands aren't needed\n//! for the current broadcast paths.\n\nuse std::collections::HashMap;\nuse std::sync::{LazyLock, RwLock};\nuse std::time::{SystemTime, UNIX_EPOCH};\n\nuse axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};\nuse axum::response::IntoResponse;\nuse base64::Engine;\nuse futures_util::{SinkExt, StreamExt};\nuse serde_json::{json, Value};\nuse tokio::sync::mpsc;\n\n// ── Partial-renderer registry ──────────────────────────────────\n//\n// Models register a closure that renders an instance identified by\n// id into its Turbo Stream partial HTML. Kept as a runtime lookup\n// (rather than parameterising Broadcaster on the model type) so\n// broadcasts called on associations — e.g., `comment.article`'s\n// replace broadcast — can find the parent's partial without the\n// child model needing to know the parent's view module.\n\npub type RenderPartialFn = Box<dyn Fn(i64) -> String + Send + Sync>;\n\nstatic PARTIAL_RENDERERS: LazyLock<RwLock<HashMap<String, RenderPartialFn>>> =\n    LazyLock::new(|| RwLock::new(HashMap::new()));\n\n/// Register a partial renderer for `type_name` (the model class\n/// name, e.g. `\"Article\"`). The closure receives a record id and\n/// returns the rendered partial HTML, or empty string on miss.\npub fn register_partial(type_name: &str, f: impl Fn(i64) -> String + Send + Sync + 'static) {\n    PARTIAL_RENDERERS\n        .write()\n        .expect(\"cable partial renderers poisoned\")\n        .insert(type_name.to_string(), Box::new(f));\n}\n\n/// Look up and invoke a registered partial renderer. Returns a\n/// placeholder div when no renderer is registered — tests can\n/// assert on the fallback rather than panicking.\npub fn render_partial(type_name: &str, id: i64) -> String {\n    let table = PARTIAL_RENDERERS\n        .read()\n        .expect(\"cable partial renderers poisoned\");\n    match table.get(type_name) {\n        Some(f) => f(id),\n        None => format!(\"<div>{} #{}</div>\", type_name, id),\n    }\n}\n\n// ── Turbo Streams rendering ────────────────────────────────────\n\n/// Render a single `<turbo-stream>` element. Empty content collapses\n/// to a self-closing template (used by `remove` actions).\npub fn turbo_stream_html(action: &str, target: &str, content: &str) -> String {\n    if content.is_empty() {\n        format!(\n            r#\"<turbo-stream action=\"{}\" target=\"{}\"></turbo-stream>\"#,\n            action, target\n        )\n    } else {\n        format!(\n            r#\"<turbo-stream action=\"{}\" target=\"{}\"><template>{}</template></turbo-stream>\"#,\n            action, target, content,\n        )\n    }\n}\n\n/// Rails convention: `<singular>_<id>`. Naive depluralise — strips\n/// a trailing `s` if present. Matches railcar's `dom_id_for`.\nfn dom_id_for(table_name: &str, id: i64) -> String {\n    let singular = table_name.strip_suffix('s').unwrap_or(table_name);\n    format!(\"{}_{}\", singular, id)\n}\n\n// ── Broadcast helpers ──────────────────────────────────────────\n\n/// Replace the target element with the record's partial. Defaults\n/// `target` to `<singular>_<id>` when caller passes an empty\n/// string (matches Rails' `broadcast_replace_to` with no explicit\n/// target).\npub fn broadcast_replace_to(\n    table_name: &str,\n    id: i64,\n    type_name: &str,\n    channel: &str,\n    target: &str,\n) {\n    let target = if target.is_empty() {\n        dom_id_for(table_name, id)\n    } else {\n        target.to_string()\n    };\n    let html = render_partial(type_name, id);\n    let stream = turbo_stream_html(\"replace\", &target, &html);\n    CABLE.broadcast(channel, &stream);\n}\n\n/// Prepend the record's partial into the target container.\n/// Defaults `target` to the table name (the scaffold's `<ul\n/// id=\"articles\">` convention).\npub fn broadcast_prepend_to(\n    table_name: &str,\n    id: i64,\n    type_name: &str,\n    channel: &str,\n    target: &str,\n) {\n    let target = if target.is_empty() {\n        table_name.to_string()\n    } else {\n        target.to_string()\n    };\n    let html = render_partial(type_name, id);\n    let stream = turbo_stream_html(\"prepend\", &target, &html);\n    CABLE.broadcast(channel, &stream);\n}\n\n/// Append the record's partial into the target container. Same\n/// default-target rule as prepend.\npub fn broadcast_append_to(\n    table_name: &str,\n    id: i64,\n    type_name: &str,\n    channel: &str,\n    target: &str,\n) {\n    let target = if target.is_empty() {\n        table_name.to_string()\n    } else {\n        target.to_string()\n    };\n    let html = render_partial(type_name, id);\n    let stream = turbo_stream_html(\"append\", &target, &html);\n    CABLE.broadcast(channel, &stream);\n}\n\n/// Remove the target element. Target defaults to `<singular>_<id>`\n/// so `broadcast_remove_to(channel)` on a record deletes its own\n/// DOM node.\npub fn broadcast_remove_to(table_name: &str, id: i64, channel: &str, target: &str) {\n    let target = if target.is_empty() {\n        dom_id_for(table_name, id)\n    } else {\n        target.to_string()\n    };\n    let stream = turbo_stream_html(\"remove\", &target, \"\");\n    CABLE.broadcast(channel, &stream);\n}\n\n// ── CableServer ────────────────────────────────────────────────\n\nstruct Subscriber {\n    /// Outbound channel to this socket's send task. Cloned into the\n    /// registry; the socket task owns the receiver half.\n    tx: mpsc::UnboundedSender<String>,\n    /// The raw identifier string the client sent on subscribe. We\n    /// echo it back in every broadcast so Turbo can route the\n    /// message to the right `<turbo-cable-stream-source>` element.\n    identifier: String,\n}\n\npub struct CableServer {\n    channels: RwLock<HashMap<String, Vec<Subscriber>>>,\n}\n\n/// Process-wide registry. One per server; fine as a static because\n/// the server runs one app per process (same assumption as\n/// `server::LAYOUT_FN`).\npub static CABLE: LazyLock<CableServer> = LazyLock::new(|| CableServer {\n    channels: RwLock::new(HashMap::new()),\n});\n\nimpl CableServer {\n    fn subscribe(&self, channel: &str, tx: mpsc::UnboundedSender<String>, identifier: &str) {\n        self.channels\n            .write()\n            .expect(\"cable channels poisoned\")\n            .entry(channel.to_string())\n            .or_default()\n            .push(Subscriber {\n                tx,\n                identifier: identifier.to_string(),\n            });\n    }\n\n    /// Drop any subscribers whose `tx` matches the given pointer.\n    /// Called on socket close. We compare by pointer rather than\n    /// PartialEq because `UnboundedSender` doesn't implement it;\n    /// each subscriber holds a distinct sender, so pointer identity\n    /// is sufficient and avoids threading a subscriber id through\n    /// the WebSocket task.\n    fn unsubscribe(&self, tx_ptr: usize) {\n        let mut channels = self.channels.write().expect(\"cable channels poisoned\");\n        for subs in channels.values_mut() {\n            subs.retain(|s| &s.tx as *const _ as usize != tx_ptr);\n        }\n        channels.retain(|_, subs| !subs.is_empty());\n    }\n\n    /// Push `html` as a Turbo Stream `message` frame to every\n    /// subscriber on `channel`. Dropped senders are ignored — the\n    /// subscriber will be cleaned up on the close-driven\n    /// `unsubscribe` path.\n    pub fn broadcast(&self, channel: &str, html: &str) {\n        let channels = self.channels.read().expect(\"cable channels poisoned\");\n        if let Some(subs) = channels.get(channel) {\n            for sub in subs {\n                let frame = json!({\n                    \"type\": \"message\",\n                    \"identifier\": sub.identifier,\n                    \"message\": html,\n                })\n                .to_string();\n                let _ = sub.tx.send(frame);\n            }\n        }\n    }\n}\n\n// ── WebSocket handler ──────────────────────────────────────────\n\n/// Axum handler for `GET /cable`. Negotiates the\n/// `actioncable-v1-json` subprotocol (Turbo's client requires the\n/// echo) and hands off to the per-socket task.\npub async fn cable_handler(ws: WebSocketUpgrade) -> impl IntoResponse {\n    ws.protocols([\"actioncable-v1-json\"])\n        .on_upgrade(handle_socket)\n}\n\nasync fn handle_socket(socket: WebSocket) {\n    let (mut sender, mut receiver) = socket.split();\n\n    // Welcome frame — the Action Cable client waits for this before\n    // it sends its first `subscribe`.\n    if sender\n        .send(Message::Text(\n            json!({\"type\": \"welcome\"}).to_string().into(),\n        ))\n        .await\n        .is_err()\n    {\n        return;\n    }\n\n    // Single outbound channel merges broadcasts + pings onto the\n    // shared sender half. Cloning the tx into the ping task and\n    // the registry lets each source push independently without\n    // locking the socket writer.\n    let (tx, mut rx) = mpsc::unbounded_channel::<String>();\n    let tx_ptr = &tx as *const _ as usize;\n\n    let ping_tx = tx.clone();\n    let ping_task = tokio::spawn(async move {\n        let mut interval = tokio::time::interval(std::time::Duration::from_secs(3));\n        // First tick fires immediately; skip it so we don't ping\n        // before the welcome + confirm_subscription round-trip.\n        interval.tick().await;\n        loop {\n            interval.tick().await;\n            let ts = SystemTime::now()\n                .duration_since(UNIX_EPOCH)\n                .map(|d| d.as_secs() as i64)\n                .unwrap_or(0);\n            let frame = json!({\"type\": \"ping\", \"message\": ts}).to_string();\n            if ping_tx.send(frame).is_err() {\n                break;\n            }\n        }\n    });\n\n    let send_task = tokio::spawn(async move {\n        while let Some(frame) = rx.recv().await {\n            if sender.send(Message::Text(frame.into())).await.is_err() {\n                break;\n            }\n        }\n    });\n\n    while let Some(Ok(msg)) = receiver.next().await {\n        let Message::Text(text) = msg else { continue };\n        let Ok(payload) = serde_json::from_str::<Value>(&text) else {\n            continue;\n        };\n        if payload.get(\"command\").and_then(Value::as_str) != Some(\"subscribe\") {\n            continue;\n        }\n        let Some(identifier) = payload.get(\"identifier\").and_then(Value::as_str) else {\n            continue;\n        };\n        let Some(channel) = decode_channel(identifier) else {\n            continue;\n        };\n        CABLE.subscribe(&channel, tx.clone(), identifier);\n        let confirm = json!({\n            \"type\": \"confirm_subscription\",\n            \"identifier\": identifier,\n        })\n        .to_string();\n        let _ = tx.send(confirm);\n    }\n\n    ping_task.abort();\n    send_task.abort();\n    CABLE.unsubscribe(tx_ptr);\n}\n\n/// Recover the channel name from Turbo's `signed_stream_name`.\n/// The identifier is a JSON blob like\n/// `{\"channel\":\"Turbo::StreamsChannel\",\"signed_stream_name\":\"<base64>--<digest>\"}`;\n/// the base64 segment holds a JSON-encoded channel name (e.g.\n/// `\"articles\"`). If either decode fails we fall back to the raw\n/// identifier so tests can subscribe by literal channel string.\nfn decode_channel(identifier: &str) -> Option<String> {\n    let id_json = serde_json::from_str::<Value>(identifier).ok()?;\n    let signed = id_json\n        .get(\"signed_stream_name\")\n        .and_then(Value::as_str)?;\n    let base64_part = signed.split(\"--\").next().unwrap_or(\"\");\n    let decoded_bytes = base64::engine::general_purpose::STANDARD\n        .decode(base64_part)\n        .ok()?;\n    let decoded = std::str::from_utf8(&decoded_bytes).ok()?;\n    serde_json::from_str::<String>(decoded).ok()\n}\n\n// ── Broadcaster trait ──────────────────────────────────────────\n\n/// Implemented on models with `broadcasts_to` declarations. The\n/// emitter's `broadcasts_to` translation (Phase B) generates these\n/// implementations; the runtime calls them from the generated\n/// `save()` / `destroy()` methods at the end of a successful\n/// persist.\n///\n/// Kept separate from `Model` so that models without any broadcast\n/// hooks don't need a stub impl — the emitter only emits this for\n/// models that declare `broadcasts_to`, and the save/destroy\n/// codegen conditionally calls into it.\npub trait Broadcaster {\n    fn after_save(&self);\n    fn after_delete(&self);\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn turbo_stream_html_wraps_content_in_template() {\n        let got = turbo_stream_html(\"replace\", \"article_1\", \"<div>hi</div>\");\n        assert_eq!(\n            got,\n            r#\"<turbo-stream action=\"replace\" target=\"article_1\"><template><div>hi</div></template></turbo-stream>\"#\n        );\n    }\n\n    #[test]\n    fn turbo_stream_html_self_closes_when_empty() {\n        let got = turbo_stream_html(\"remove\", \"article_1\", \"\");\n        assert_eq!(\n            got,\n            r#\"<turbo-stream action=\"remove\" target=\"article_1\"></turbo-stream>\"#\n        );\n    }\n\n    #[test]\n    fn dom_id_for_strips_trailing_s() {\n        assert_eq!(dom_id_for(\"articles\", 7), \"article_7\");\n        assert_eq!(dom_id_for(\"comment\", 3), \"comment_3\");\n    }\n\n    #[test]\n    fn render_partial_falls_back_when_unregistered() {\n        // Use a distinct type name so parallel tests don't collide.\n        let got = render_partial(\"UnregisteredNoise\", 99);\n        assert_eq!(got, \"<div>UnregisteredNoise #99</div>\");\n    }\n\n    #[test]\n    fn decode_channel_recovers_plain_base64_name() {\n        // Construct a signed stream name for `\"articles\"`.\n        let inner = serde_json::to_string(\"articles\").unwrap();\n        let b64 =\n            base64::engine::general_purpose::STANDARD.encode(inner.as_bytes());\n        let signed = format!(\"{}--unsigned\", b64);\n        let identifier = serde_json::json!({\n            \"channel\": \"Turbo::StreamsChannel\",\n            \"signed_stream_name\": signed,\n        })\n        .to_string();\n        assert_eq!(decode_channel(&identifier).as_deref(), Some(\"articles\"));\n    }\n\n    #[test]\n    fn decode_channel_returns_none_on_bad_input() {\n        assert!(decode_channel(\"not json\").is_none());\n        assert!(decode_channel(r#\"{\"no\":\"signed\"}\"#).is_none());\n    }\n}\n"},{"path":"src/controllers/application_controller.rs","content":"#[allow(unused_imports)]\nuse crate::action_controller_base::{self, Base};\n#[allow(unused_imports)]\nuse crate::flash::Flash;\n#[allow(unused_imports)]\nuse crate::session::Session;\n#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::route_helpers::{self, RouteHelpers};\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n#[allow(unused_imports)]\nuse crate::http::RubyToS;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::views::*;\n#[allow(unused_imports)]\nuse crate::errors_ext::{raise, NotImplementedError, RecordNotFound, RecordInvalid};\n#[derive(Clone, Default)]\npub struct ApplicationController {\n}\n\nimpl ApplicationController {\n}\n\nimpl ApplicationController {\n    pub fn render(&self, content: String) {\n        crate::http::response_set_body(content);\n    }\n    pub fn render_with(&self, content: String, opts: std::collections::HashMap<String, crate::param_value::ParamValue>) {\n        let content_type = opts.get(\"content_type\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        let status = opts.get(\"status\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        let status_code = status.as_deref().map(crate::http::status_name_to_code_pub);\n        crate::http::response_set_body_with(content, content_type, status_code);\n    }\n    pub fn request_format(&self) -> String { crate::http::request_format_get() }\n    pub fn redirect_to(&self, url: String, opts: std::collections::HashMap<String, crate::param_value::ParamValue>) {\n        // Carry `notice:` / `alert:` to the next request via the\n        // flash cookie (the per-action wrapper sweeps FLASH_OUT into\n        // Set-Cookie). Recorded in a thread-local rather than on\n        // `self.flash` because not every controller carries a flash\n        // field, but any controller can redirect with a notice.\n        if let Some(n) = opts.get(\"notice\").and_then(|v| v.as_str()) { crate::http::flash_out_set(\"notice\", n); }\n        if let Some(a) = opts.get(\"alert\").and_then(|v| v.as_str()) { crate::http::flash_out_set(\"alert\", a); }\n        let status = opts.get(\"status\").and_then(|v| v.as_str()).unwrap_or(\"see_other\");\n        crate::http::response_set_redirect(url, crate::http::status_name_to_code_pub(status));\n    }\n    pub fn head(&self, status: &str, opts: std::collections::HashMap<String, serde_json::Value>) {\n        let content_type = opts.get(\"content_type\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        crate::http::response_set_head(status, content_type);\n    }\n}\n"},{"path":"src/controllers/articles_controller.rs","content":"#[allow(unused_imports)]\nuse crate::action_controller_base::{self, Base};\n#[allow(unused_imports)]\nuse crate::flash::Flash;\n#[allow(unused_imports)]\nuse crate::session::Session;\n#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::route_helpers::{self, RouteHelpers};\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n#[allow(unused_imports)]\nuse crate::http::RubyToS;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::views::*;\n#[allow(unused_imports)]\nuse crate::errors_ext::{raise, NotImplementedError, RecordNotFound, RecordInvalid};\n#[derive(Clone, Default)]\npub struct ArticlesController {\n    pub articles: Vec<Article>,\n    pub article: Article,\n    pub flash: Flash,\n    pub params: std::collections::HashMap<String, serde_json::Value>,\n}\n\nimpl ArticlesController {\n    pub fn process_action(&mut self, action_name: &str) {\n        match action_name {\n                \"index\" => { self.index() },\n                \"show\" => { self.show() },\n                \"new\" => { self.new_action() },\n                \"edit\" => { self.edit() },\n                \"create\" => { self.create() },\n                \"update\" => { self.update() },\n                \"destroy\" => { self.destroy() }\n                _ => (),\n            }\n    }\n\n    pub fn index(&mut self) {\n        let stmt = Db::prepare(&(format!(\"{}{}\", \"SELECT id, body, created_at, title, updated_at FROM articles\", \" ORDER BY created_at DESC\")));\n        let mut results = vec![];\n        while Db::step(stmt) {\n            results.push(Article::from_stmt(stmt));\n        };\n        Db::finalize(stmt);\n        let __comments_ids = results.clone().into_iter().map(|a| { a.id() }).collect::<Vec<_>>();\n        let __comments_stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", \"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments WHERE article_id IN (\", Db::escape_int_list(__comments_ids)), \")\")));\n        let mut __comments_loaded = vec![];\n        while Db::step(__comments_stmt) {\n            __comments_loaded.push(Comment::from_stmt(__comments_stmt));\n        };\n        Db::finalize(__comments_stmt);\n        results.clone().iter_mut().for_each(|a| {\n            let mut __comments_group = vec![];\n            __comments_loaded.clone().iter_mut().for_each(|r| { if r.article_id() == a.id() { __comments_group.push(r.clone()) }; });\n            a._preload_comments(__comments_group.clone());;\n        });\n        self.articles = results.clone();\n        if self.request_format() == \"json\" { self.render_with(Articles::index_json(self.articles.clone()), std::collections::HashMap::from([(\"content_type\", \"application/json\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } else { self.render(Articles::index(self.articles.clone(), self.flash.get(\"notice\"), self.flash.get(\"alert\"))) };\n    }\n\n    pub fn show(&mut self) {\n        self.article = Article::find(self.params.get(\"id\").cloned().unwrap_or(serde_json::Value::from(\"0\")).ruby_to_s().parse::<i64>().unwrap_or(0));\n        if self.request_format() == \"json\" { self.render_with(Articles::show_json(self.article.clone()), std::collections::HashMap::from([(\"content_type\", \"application/json\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } else { self.render(Articles::show(self.article.clone(), self.flash.get(\"notice\"), self.flash.get(\"alert\"))) };\n    }\n\n    pub fn new_action(&mut self) {\n        self.article = Article::new(std::collections::HashMap::new());\n        self.render(Articles::new(self.article.clone(), self.flash.get(\"notice\"), self.flash.get(\"alert\")));\n    }\n\n    pub fn edit(&mut self) {\n        self.article = Article::find(self.params.get(\"id\").cloned().unwrap_or(serde_json::Value::from(\"0\")).ruby_to_s().parse::<i64>().unwrap_or(0));\n        self.render(Articles::edit(self.article.clone(), self.flash.get(\"notice\"), self.flash.get(\"alert\")));\n    }\n\n    pub fn create(&mut self) {\n        self.article = Article::from_params(self.article_params());\n        if self.article.save() { if self.request_format() == \"json\" { self.render_with(Articles::show_json(self.article.clone()), std::collections::HashMap::from([(\"status\", \"created\".to_string()), (\"location\", RouteHelpers::article_path(self.article.id())), (\"content_type\", (\"application/json\").to_string())]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } else { self.redirect_to(RouteHelpers::article_path(self.article.id()), std::collections::HashMap::from([(\"notice\", \"Article was successfully created.\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } } else { self.render_with(Articles::new(self.article.clone(), self.flash.get(\"notice\"), self.flash.get(\"alert\")), std::collections::HashMap::from([(\"status\", \"unprocessable_content\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) };\n    }\n\n    pub fn update(&mut self) {\n        self.article = Article::find(self.params.get(\"id\").cloned().unwrap_or(serde_json::Value::from(\"0\")).ruby_to_s().parse::<i64>().unwrap_or(0));\n        if self.article.update(self.article_params()) { if self.request_format() == \"json\" { self.render_with(Articles::show_json(self.article.clone()), std::collections::HashMap::from([(\"status\", \"ok\".to_string()), (\"location\", RouteHelpers::article_path(self.article.id())), (\"content_type\", (\"application/json\").to_string())]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } else { self.redirect_to(RouteHelpers::article_path(self.article.id()), std::collections::HashMap::from([(\"notice\", \"Article was successfully updated.\"), (\"status\", \"see_other\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } } else { self.render_with(Articles::edit(self.article.clone(), self.flash.get(\"notice\"), self.flash.get(\"alert\")), std::collections::HashMap::from([(\"status\", \"unprocessable_content\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) };\n    }\n\n    pub fn destroy(&mut self) {\n        self.article = Article::find(self.params.get(\"id\").cloned().unwrap_or(serde_json::Value::from(\"0\")).ruby_to_s().parse::<i64>().unwrap_or(0));\n        self.article.destroy();\n        if self.request_format() == \"json\" { self.head(\"no_content\", std::collections::HashMap::from([(\"content_type\", \"application/json\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } else { self.redirect_to(RouteHelpers::articles_path(), std::collections::HashMap::from([(\"notice\", \"Article was successfully destroyed.\"), (\"status\", \"see_other\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) };\n    }\n\n    pub fn article_params(&self) -> ArticleParams {\n        ArticleParams::from_raw(self.params.clone())\n    }\n}\n\nimpl ArticlesController {\n    pub fn render(&self, content: String) {\n        crate::http::response_set_body(content);\n    }\n    pub fn render_with(&self, content: String, opts: std::collections::HashMap<String, crate::param_value::ParamValue>) {\n        let content_type = opts.get(\"content_type\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        let status = opts.get(\"status\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        let status_code = status.as_deref().map(crate::http::status_name_to_code_pub);\n        crate::http::response_set_body_with(content, content_type, status_code);\n    }\n    pub fn request_format(&self) -> String { crate::http::request_format_get() }\n    pub fn redirect_to(&self, url: String, opts: std::collections::HashMap<String, crate::param_value::ParamValue>) {\n        // Carry `notice:` / `alert:` to the next request via the\n        // flash cookie (the per-action wrapper sweeps FLASH_OUT into\n        // Set-Cookie). Recorded in a thread-local rather than on\n        // `self.flash` because not every controller carries a flash\n        // field, but any controller can redirect with a notice.\n        if let Some(n) = opts.get(\"notice\").and_then(|v| v.as_str()) { crate::http::flash_out_set(\"notice\", n); }\n        if let Some(a) = opts.get(\"alert\").and_then(|v| v.as_str()) { crate::http::flash_out_set(\"alert\", a); }\n        let status = opts.get(\"status\").and_then(|v| v.as_str()).unwrap_or(\"see_other\");\n        crate::http::response_set_redirect(url, crate::http::status_name_to_code_pub(status));\n    }\n    pub fn head(&self, status: &str, opts: std::collections::HashMap<String, serde_json::Value>) {\n        let content_type = opts.get(\"content_type\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        crate::http::response_set_head(status, content_type);\n    }\n}\n\n// ── rust2 wedge 2c.2: axum handler wrappers ──\n// Per-action free fns axum's Router can dispatch into. Build the\n// controller via Default, call the action, and translate the\n// thread-local response state into an `axum::response::Response`.\npub async fn _axum_index(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, headers: axum::http::HeaderMap) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut c = ArticlesController::default();\n    c.flash = crate::http::flash_from_request(&headers);\n    c.index();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\npub async fn _axum_new(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, headers: axum::http::HeaderMap) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut c = ArticlesController::default();\n    c.flash = crate::http::flash_from_request(&headers);\n    c.new_action();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\npub async fn _axum_create(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, headers: axum::http::HeaderMap, axum::extract::Form(form): axum::extract::Form<std::collections::HashMap<String, String>>) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut params = crate::http::params_from_form(form);\n    let mut c = ArticlesController { params, ..Default::default() };\n    c.flash = crate::http::flash_from_request(&headers);\n    c.create();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\npub async fn _axum_show(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, headers: axum::http::HeaderMap, axum::extract::Path(id_raw): axum::extract::Path<String>) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut params: std::collections::HashMap<String, serde_json::Value> = std::collections::HashMap::new();\n    let id: i64 = id_raw.strip_suffix(\".json\").unwrap_or(&id_raw).parse().unwrap_or(0);\n    params.insert(\"id\".to_string(), serde_json::Value::from(id));\n    let mut c = ArticlesController { params, ..Default::default() };\n    c.flash = crate::http::flash_from_request(&headers);\n    c.show();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\npub async fn _axum_edit(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, headers: axum::http::HeaderMap, axum::extract::Path(id_raw): axum::extract::Path<String>) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut params: std::collections::HashMap<String, serde_json::Value> = std::collections::HashMap::new();\n    let id: i64 = id_raw.strip_suffix(\".json\").unwrap_or(&id_raw).parse().unwrap_or(0);\n    params.insert(\"id\".to_string(), serde_json::Value::from(id));\n    let mut c = ArticlesController { params, ..Default::default() };\n    c.flash = crate::http::flash_from_request(&headers);\n    c.edit();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\npub async fn _axum_update(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, headers: axum::http::HeaderMap, axum::extract::Path(id_raw): axum::extract::Path<String>, axum::extract::Form(form): axum::extract::Form<std::collections::HashMap<String, String>>) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut params = crate::http::params_from_form(form);\n    let id: i64 = id_raw.strip_suffix(\".json\").unwrap_or(&id_raw).parse().unwrap_or(0);\n    params.insert(\"id\".to_string(), serde_json::Value::from(id));\n    let mut c = ArticlesController { params, ..Default::default() };\n    c.flash = crate::http::flash_from_request(&headers);\n    c.update();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\npub async fn _axum_destroy(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, headers: axum::http::HeaderMap, axum::extract::Path(id_raw): axum::extract::Path<String>) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut params: std::collections::HashMap<String, serde_json::Value> = std::collections::HashMap::new();\n    let id: i64 = id_raw.strip_suffix(\".json\").unwrap_or(&id_raw).parse().unwrap_or(0);\n    params.insert(\"id\".to_string(), serde_json::Value::from(id));\n    let mut c = ArticlesController { params, ..Default::default() };\n    c.flash = crate::http::flash_from_request(&headers);\n    c.destroy();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\n"},{"path":"src/controllers/comments_controller.rs","content":"#[allow(unused_imports)]\nuse crate::action_controller_base::{self, Base};\n#[allow(unused_imports)]\nuse crate::flash::Flash;\n#[allow(unused_imports)]\nuse crate::session::Session;\n#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::route_helpers::{self, RouteHelpers};\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n#[allow(unused_imports)]\nuse crate::http::RubyToS;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::views::*;\n#[allow(unused_imports)]\nuse crate::errors_ext::{raise, NotImplementedError, RecordNotFound, RecordInvalid};\n#[derive(Clone, Default)]\npub struct CommentsController {\n    pub article: Article,\n    pub comment: Comment,\n    pub params: std::collections::HashMap<String, serde_json::Value>,\n}\n\nimpl CommentsController {\n    pub fn process_action(&mut self, action_name: &str) {\n        match action_name {\n                \"create\" => { self.create() },\n                \"destroy\" => { self.destroy() }\n                _ => (),\n            }\n    }\n\n    pub fn create(&mut self) {\n        self.article = Article::find(self.params.get(\"article_id\").cloned().unwrap_or(serde_json::Value::from(\"0\")).ruby_to_s().parse::<i64>().unwrap_or(0));\n        self.comment = Comment::from_params(self.comment_params());\n        self.comment.set_article_id(self.article.id());\n        if self.comment.save() { self.redirect_to(RouteHelpers::article_path(self.article.id()), std::collections::HashMap::from([(\"notice\", \"Comment was successfully created.\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) } else { self.redirect_to(RouteHelpers::article_path(self.article.id()), std::collections::HashMap::from([(\"alert\", \"Could not create comment.\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()) };\n    }\n\n    pub fn destroy(&mut self) {\n        self.article = Article::find(self.params.get(\"article_id\").cloned().unwrap_or(serde_json::Value::from(\"0\")).ruby_to_s().parse::<i64>().unwrap_or(0));\n        self.comment = Comment::find(self.params.get(\"id\").cloned().unwrap_or(serde_json::Value::from(\"0\")).ruby_to_s().parse::<i64>().unwrap_or(0));\n        if self.comment.article_id() != self.article.id() { self.head(\"not_found\", std::collections::HashMap::new());\n        return; } else {  };\n        self.comment.destroy();\n        self.redirect_to(RouteHelpers::article_path(self.article.id()), std::collections::HashMap::from([(\"notice\", \"Comment was successfully deleted.\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    }\n\n    pub fn comment_params(&self) -> CommentParams {\n        CommentParams::from_raw(self.params.clone())\n    }\n}\n\nimpl CommentsController {\n    pub fn render(&self, content: String) {\n        crate::http::response_set_body(content);\n    }\n    pub fn render_with(&self, content: String, opts: std::collections::HashMap<String, crate::param_value::ParamValue>) {\n        let content_type = opts.get(\"content_type\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        let status = opts.get(\"status\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        let status_code = status.as_deref().map(crate::http::status_name_to_code_pub);\n        crate::http::response_set_body_with(content, content_type, status_code);\n    }\n    pub fn request_format(&self) -> String { crate::http::request_format_get() }\n    pub fn redirect_to(&self, url: String, opts: std::collections::HashMap<String, crate::param_value::ParamValue>) {\n        // Carry `notice:` / `alert:` to the next request via the\n        // flash cookie (the per-action wrapper sweeps FLASH_OUT into\n        // Set-Cookie). Recorded in a thread-local rather than on\n        // `self.flash` because not every controller carries a flash\n        // field, but any controller can redirect with a notice.\n        if let Some(n) = opts.get(\"notice\").and_then(|v| v.as_str()) { crate::http::flash_out_set(\"notice\", n); }\n        if let Some(a) = opts.get(\"alert\").and_then(|v| v.as_str()) { crate::http::flash_out_set(\"alert\", a); }\n        let status = opts.get(\"status\").and_then(|v| v.as_str()).unwrap_or(\"see_other\");\n        crate::http::response_set_redirect(url, crate::http::status_name_to_code_pub(status));\n    }\n    pub fn head(&self, status: &str, opts: std::collections::HashMap<String, serde_json::Value>) {\n        let content_type = opts.get(\"content_type\").and_then(|v| v.as_str()).map(|s| s.to_string());\n        crate::http::response_set_head(status, content_type);\n    }\n}\n\n// ── rust2 wedge 2c.2: axum handler wrappers ──\n// Per-action free fns axum's Router can dispatch into. Build the\n// controller via Default, call the action, and translate the\n// thread-local response state into an `axum::response::Response`.\npub async fn _axum_create(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, axum::extract::Path(article_id_raw): axum::extract::Path<String>, axum::extract::Form(form): axum::extract::Form<std::collections::HashMap<String, String>>) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut params = crate::http::params_from_form(form);\n    let article_id: i64 = article_id_raw.strip_suffix(\".json\").unwrap_or(&article_id_raw).parse().unwrap_or(0);\n    params.insert(\"article_id\".to_string(), serde_json::Value::from(article_id));\n    let mut c = CommentsController { params, ..Default::default() };\n    c.create();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\npub async fn _axum_destroy(axum::extract::Extension(_fmt): axum::extract::Extension<crate::http::RequestFormatExt>, axum::extract::Path((article_id_raw, id_raw)): axum::extract::Path<(String, String)>) -> axum::response::Response {\n    crate::http::response_clear();\n    crate::http::request_format_set(_fmt.0);\n    let mut params: std::collections::HashMap<String, serde_json::Value> = std::collections::HashMap::new();\n    let article_id: i64 = article_id_raw.strip_suffix(\".json\").unwrap_or(&article_id_raw).parse().unwrap_or(0);\n    params.insert(\"article_id\".to_string(), serde_json::Value::from(article_id));\n    let id: i64 = id_raw.strip_suffix(\".json\").unwrap_or(&id_raw).parse().unwrap_or(0);\n    params.insert(\"id\".to_string(), serde_json::Value::from(id));\n    let mut c = CommentsController { params, ..Default::default() };\n    c.destroy();\n    let mut __resp = crate::http::response_into_axum(crate::http::response_take());\n    crate::http::apply_flash_cookie(&mut __resp, &crate::http::flash_out_take());\n    __resp\n}\n"},{"path":"src/controllers/mod.rs","content":"// Generated by Roundhouse (rust2).\n\npub mod application_controller;\npub mod articles_controller;\npub mod comments_controller;\npub use application_controller::ApplicationController;\npub use articles_controller::ArticlesController;\npub use comments_controller::CommentsController;\n"},{"path":"src/db.rs","content":"//! Roundhouse Rust DB runtime.\n//!\n//! Hand-written helpers the Rust emitter copies verbatim into each\n//! generated project as `src/db.rs`. Owns the per-test SQLite\n//! connection and hides rusqlite borrowing from the generated code\n//! — save/destroy/count/find all go through `with_conn`.\n//!\n//! Two entry points:\n//!   - `setup_test_db(schema)` — thread-local `:memory:` connection\n//!     for tests. Each test re-installs a fresh DB so prior-test\n//!     state doesn't bleed across.\n//!   - `open_production_db(path, schema)` — file-backed connections\n//!     installed into a process-wide pool (`OnceLock<Vec<Mutex<\n//!     Connection>>>`). Used by `main.rs` on server startup.\n//!     Pool size = `DATABASE_POOL_SIZE` env var, defaulting to\n//!     `std::thread::available_parallelism()`. SQLite is opened in\n//!     WAL mode so N readers actually proceed in parallel.\n//!     `with_conn` reaches either slot — test thread-local first,\n//!     then the production pool (try_lock each entry; fall back to\n//!     blocking-lock on slot 0).\n\nuse std::cell::{Cell, RefCell};\nuse std::collections::HashMap;\nuse std::fs;\nuse std::path::Path;\nuse std::sync::{Mutex, OnceLock};\n\nuse rusqlite::types::Value;\nuse rusqlite::Connection;\n\nthread_local! {\n    /// The connection the current thread's test (or request handler)\n    /// uses. `None` until `setup_test_db` initializes it.\n    static CONN: RefCell<Option<Connection>> = const { RefCell::new(None) };\n}\n\n/// Process-wide sqlite connection pool for the production server.\n/// `axum` handlers run on a multi-thread tokio runtime so per-thread\n/// thread-locals don't work; each slot in this Vec is an independent\n/// `Connection` guarded by its own `Mutex`, and `with_conn` picks\n/// whichever slot it can `try_lock` first. SQLite is opened in WAL\n/// mode (see `open_production_db`), which is what makes N readers\n/// actually proceed in parallel. Pool size defaults to\n/// `std::thread::available_parallelism()`; override with\n/// `DATABASE_POOL_SIZE`.\nstatic PROD_POOL: OnceLock<Vec<Mutex<Connection>>> = OnceLock::new();\n\n/// rusqlite per-connection prepared-statement cache capacity\n/// (roundhouse#12). Each `Connection` keeps an LRU of compiled statements\n/// keyed by SQL; `prepare_cached` reuses them, skipping the re-parse the\n/// blog's fixed query set otherwise paid on every request. Inlined\n/// literals make id-bearing queries key per-id, so the LRU also bounds\n/// memory — evicted statements are finalized by rusqlite, no leak.\n/// Placeholder binding (the planned follow-on) makes the key the static\n/// query shape. Default rusqlite capacity is 16; raise it to comfortably\n/// hold the blog's working set.\nconst STMT_CACHE_CAP: usize = 128;\n\n/// Initialize a fresh `:memory:` SQLite database on the current\n/// thread, run the supplied schema DDL, and install the connection\n/// so later `with_conn` calls can reach it. Replaces any connection\n/// left over from a previous test that ran on the same thread.\n///\n/// `schema_sql` is the generated `crate::schema_sql::CREATE_TABLES`\n/// string — passed explicitly so this file can stay target-agnostic\n/// and compile standalone in the Roundhouse repo's runtime tree.\npub fn setup_test_db(schema_sql: &str) {\n    let conn = Connection::open_in_memory().expect(\"open :memory: sqlite\");\n    conn.set_prepared_statement_cache_capacity(STMT_CACHE_CAP);\n    conn.execute_batch(schema_sql).expect(\"run schema SQL\");\n    CONN.with(|c| *c.borrow_mut() = Some(conn));\n}\n\n/// Borrow the current thread's test connection, falling back to\n/// the process-wide production connection. Panics if neither is\n/// installed — callers are generated code that runs inside either\n/// a test (whose setup already called `setup_test_db`) or a live\n/// request (whose `main.rs` already called `open_production_db`).\npub fn with_conn<R, F: FnOnce(&Connection) -> R>(f: F) -> R {\n    // Check the test-connection slot first — lets a production-\n    // configured binary still run unit tests against a per-thread\n    // in-memory DB. `CONN.with` runs its closure synchronously so\n    // we can carry the closure-ownership handoff explicitly: if\n    // the test slot has a connection, run `f` there and return\n    // Ok(result); otherwise return Err(f) and run `f` against the\n    // production mutex below.\n    let result: Result<R, F> = CONN.with(|c| {\n        let borrowed = c.borrow();\n        match borrowed.as_ref() {\n            Some(conn) => Ok(f(conn)),\n            None => Err(f),\n        }\n    });\n    match result {\n        Ok(out) => out,\n        Err(f) => {\n            let pool = PROD_POOL.get().expect(\n                \"db not initialized; call setup_test_db or open_production_db first\",\n            );\n            for slot in pool {\n                if let Ok(guard) = slot.try_lock() {\n                    return f(&guard);\n                }\n            }\n            let guard = pool[0].lock().expect(\"prod DB mutex poisoned\");\n            f(&guard)\n        }\n    }\n}\n\n/// Open a file-backed sqlite database for the production server,\n/// apply the schema DDL idempotently, and install it as the\n/// process-wide connection. Creates intermediate directories if\n/// needed — `better-sqlite3` creates the file but not its parent\n/// dir, and rusqlite mirrors that behavior; the TS runtime hit\n/// the same gotcha during smoke test, so we preempt it here.\n///\n/// `schema_sql` is the generated `schema_sql::CREATE_TABLES`\n/// string. The emitter produces `CREATE TABLE IF NOT EXISTS`\n/// directly, so re-opening an existing DB no-ops over the\n/// already-present tables.\npub fn open_production_db(path: &str, schema_sql: &str) {\n    let pool_size = std::env::var(\"DATABASE_POOL_SIZE\")\n        .ok()\n        .and_then(|s| s.parse::<usize>().ok())\n        .filter(|n| *n > 0)\n        .unwrap_or_else(|| {\n            std::thread::available_parallelism()\n                .map(|n| n.get())\n                .unwrap_or(4)\n        });\n    open_production_pool(path, schema_sql, pool_size);\n}\n\n/// Pool builder shared by `open_production_db` and the unit tests.\n/// Kept separate so tests can pick an explicit pool size without\n/// reaching for `std::env::set_var` (which is `unsafe` under\n/// Rust edition 2024).\npub fn open_production_pool(path: &str, schema_sql: &str, pool_size: usize) {\n    // Turn off SQLite's global memory-status accounting before the library\n    // initializes. With it on (the bundled-build default), every internal\n    // malloc/free takes the process-wide `mem0` mutex, which serializes all\n    // pool connections under load — profiled at ~65% of thread-time blocked\n    // on that lock at c=64 (roundhouse#32). sqlite3_config is only effective\n    // pre-initialization; if something already opened a connection it returns\n    // SQLITE_MISUSE and accounting simply stays on (correct, just slower), so\n    // the result is deliberately ignored.\n    unsafe {\n        rusqlite::ffi::sqlite3_config(rusqlite::ffi::SQLITE_CONFIG_MEMSTATUS, 0i32);\n    }\n    if path != \":memory:\" {\n        if let Some(parent) = Path::new(path).parent() {\n            if !parent.as_os_str().is_empty() && !parent.exists() {\n                fs::create_dir_all(parent).expect(\"mkdir db parent\");\n            }\n        }\n    }\n    let mut conns: Vec<Mutex<Connection>> = Vec::with_capacity(pool_size);\n    for _ in 0..pool_size {\n        let conn = Connection::open(path).expect(\"open sqlite db\");\n        conn.set_prepared_statement_cache_capacity(STMT_CACHE_CAP);\n        conn.pragma_update(None, \"journal_mode\", \"WAL\")\n            .expect(\"enable WAL\");\n        conn.pragma_update(None, \"foreign_keys\", \"ON\")\n            .expect(\"enable foreign keys\");\n        // CREATE TABLE IF NOT EXISTS is idempotent on file-backed DBs;\n        // applying per-conn is what lets a `:memory:` pool work, since\n        // each `:memory:` is an independent database.\n        conn.execute_batch(schema_sql).expect(\"apply schema\");\n        conns.push(Mutex::new(conn));\n    }\n    PROD_POOL\n        .set(conns)\n        .map_err(|_| \"PROD_POOL already initialized\")\n        .expect(\"set PROD_POOL\");\n}\n\n// ── Low-level prepare/step/column API ───────────────────────────────\n//\n// Per-statement state lives in a thread-local `STATEMENTS` table; the\n// opaque `i64` stmt id indexes into it. Rows materialize on `prepare`\n// — rusqlite's `Statement`/`Rows` borrow chain from `Connection` is\n// awkward to thread through a `RefCell<HashMap<i64, _>>`, so we eat\n// the up-front allocation in exchange for a self-contained per-stmt\n// entry. Matches `runtime/crystal/db.cr`'s `Roundhouse::Db` API\n// shape so the lowered model bodies (`Db.prepare(sql)`, `Db.step?\n// (stmt)`, etc., from `src/lower/model_to_library/adapter_emit.rs`)\n// emit the same calls under both targets.\n\n/// A materialized prepared-statement entry: all rows fetched up front\n/// + a cursor position + the most recently-stepped row snapshot.\nstruct StmtEntry {\n    rows: Vec<Vec<Value>>,\n    pos: usize,\n    current: Option<Vec<Value>>,\n}\n\nthread_local! {\n    static STATEMENTS: RefCell<HashMap<i64, StmtEntry>> =\n        RefCell::new(HashMap::new());\n    static NEXT_STMT_ID: Cell<i64> = const { Cell::new(0) };\n    static LAST_INSERT_ROWID: Cell<i64> = const { Cell::new(0) };\n}\n\n/// `Db` namespace — the lowerer (`src/lower/model_to_library/\n/// adapter_emit.rs` + `src/lower/arel/visitor.rs`) emits\n/// `Db.prepare(sql)` / `Db.step?(stmt)` / `Db.column_int(stmt, i)`\n/// against the synthesized per-model adapter methods. Mirrors the\n/// Crystal target's `Roundhouse::Db` module member-for-member.\npub struct Db;\n\nimpl Db {\n    /// Run a one-shot DDL/INSERT/UPDATE/DELETE. Captures\n    /// `last_insert_rowid` so the subsequent accessor returns the\n    /// freshly-inserted id (the typical `Db.exec(insert_sql);\n    /// id = Db.last_insert_rowid` shape in lowered persistence).\n    pub fn exec(sql: &str) {\n        with_conn(|conn| {\n            conn.execute_batch(sql).expect(\"Db::exec\");\n            LAST_INSERT_ROWID.with(|c| c.set(conn.last_insert_rowid()));\n        });\n    }\n\n    /// Prepare a SELECT, materialize every row, return the opaque\n    /// stmt id. Subsequent `step` / `column_*` / `finalize` calls take\n    /// the id by value. `prepare_cached` reuses the connection's compiled\n    /// statement (roundhouse#12) — the parse is skipped on a cache hit;\n    /// rusqlite resets the cached statement on checkout and returns it to\n    /// the LRU when the `CachedStatement` drops at the end of this closure.\n    pub fn prepare(sql: &str) -> i64 {\n        let rows: Vec<Vec<Value>> = with_conn(|conn| {\n            let mut stmt = conn.prepare_cached(sql).expect(\"Db::prepare\");\n            let n_cols = stmt.column_count();\n            let mut out: Vec<Vec<Value>> = Vec::new();\n            let mut rows = stmt.query([]).expect(\"Db::prepare query\");\n            while let Some(row) = rows.next().expect(\"Db::prepare step\") {\n                let mut col_vec = Vec::with_capacity(n_cols);\n                for i in 0..n_cols {\n                    let v: Value = row.get(i).expect(\"Db::prepare col\");\n                    col_vec.push(v);\n                }\n                out.push(col_vec);\n            }\n            out\n        });\n        let id = NEXT_STMT_ID.with(|c| {\n            let n = c.get() + 1;\n            c.set(n);\n            n\n        });\n        STATEMENTS.with(|s| {\n            s.borrow_mut().insert(\n                id,\n                StmtEntry { rows, pos: 0, current: None },\n            );\n        });\n        id\n    }\n\n    /// Advance the cursor. Snapshots the current row into the entry\n    /// and returns true; clears the snapshot + returns false when\n    /// exhausted. Idempotent on unknown stmt ids (returns false).\n    pub fn step(stmt_id: i64) -> bool {\n        STATEMENTS.with(|s| {\n            let mut map = s.borrow_mut();\n            let Some(entry) = map.get_mut(&stmt_id) else { return false };\n            if entry.pos < entry.rows.len() {\n                entry.current = Some(entry.rows[entry.pos].clone());\n                entry.pos += 1;\n                true\n            } else {\n                entry.current = None;\n                false\n            }\n        })\n    }\n\n    /// Read an integer column from the row most recently stepped.\n    /// NULL coerces to 0 (matches Crystal/TS shims); non-Int variants\n    /// best-effort coerce.\n    pub fn column_int(stmt_id: i64, i: i64) -> i64 {\n        STATEMENTS.with(|s| {\n            let map = s.borrow();\n            let Some(entry) = map.get(&stmt_id) else { return 0 };\n            let Some(row) = entry.current.as_ref() else { return 0 };\n            match row.get(i as usize) {\n                Some(Value::Integer(v)) => *v,\n                Some(Value::Real(v)) => *v as i64,\n                Some(Value::Text(t)) => t.parse().unwrap_or(0),\n                _ => 0,\n            }\n        })\n    }\n\n    /// Read a text column. NULL → \"\" (matches Crystal/TS); numeric\n    /// variants stringify.\n    pub fn column_text(stmt_id: i64, i: i64) -> String {\n        STATEMENTS.with(|s| {\n            let map = s.borrow();\n            let Some(entry) = map.get(&stmt_id) else { return String::new() };\n            let Some(row) = entry.current.as_ref() else { return String::new() };\n            match row.get(i as usize) {\n                Some(Value::Text(t)) => t.clone(),\n                Some(Value::Integer(v)) => v.to_string(),\n                Some(Value::Real(v)) => v.to_string(),\n                Some(Value::Blob(b)) => String::from_utf8_lossy(b).into_owned(),\n                _ => String::new(),\n            }\n        })\n    }\n\n    /// Read a boolean column. SQLite stores booleans as 0/1 integers\n    /// — widen to bool. Nulls coerce to false.\n    pub fn column_bool(stmt_id: i64, i: i64) -> bool {\n        Self::column_int(stmt_id, i) != 0\n    }\n\n    /// Drop the stmt-table entry. Idempotent on unknown ids.\n    pub fn finalize(stmt_id: i64) {\n        STATEMENTS.with(|s| {\n            s.borrow_mut().remove(&stmt_id);\n        });\n    }\n\n    /// Last-row-id from the most recent `exec`. SQLite-specific.\n    pub fn last_insert_rowid() -> i64 {\n        LAST_INSERT_ROWID.with(|c| c.get())\n    }\n\n    /// SQL-quote a string literal. Single quotes doubled per SQLite's\n    /// escape rule; no other byte transforms.\n    pub fn escape_string(s: &str) -> String {\n        let mut out = String::with_capacity(s.len() + 2);\n        out.push('\\'');\n        for ch in s.chars() {\n            if ch == '\\'' {\n                out.push_str(\"''\");\n            } else {\n                out.push(ch);\n            }\n        }\n        out.push('\\'');\n        out\n    }\n\n    /// Render an integer for SQL inlining.\n    pub fn escape_int(n: i64) -> String {\n        n.to_string()\n    }\n\n    /// Render an integer list for `IN (...)` eager-load batches (issue\n    /// #27). An empty list yields \"NULL\" so `IN (NULL)` is valid SQL\n    /// matching no rows — an empty `IN ()` is a syntax error.\n    pub fn escape_int_list(ids: Vec<i64>) -> String {\n        if ids.is_empty() {\n            return \"NULL\".to_string();\n        }\n        ids.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(\", \")\n    }\n\n    /// SQLite stores booleans as 0/1 integers.\n    pub fn escape_bool(b: bool) -> String {\n        (if b { \"1\" } else { \"0\" }).to_string()\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    const TINY_SCHEMA: &str = r#\"\nCREATE TABLE widgets (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  name TEXT\n);\n\"#;\n\n    #[test]\n    fn setup_installs_connection_with_schema() {\n        setup_test_db(TINY_SCHEMA);\n        let row_count = with_conn(|c| {\n            c.execute(\"INSERT INTO widgets (name) VALUES ('a'), ('b')\", [])\n                .expect(\"insert\")\n        });\n        assert_eq!(row_count, 2);\n        let count: i64 = with_conn(|c| {\n            c.query_row(\"SELECT COUNT(*) FROM widgets\", [], |r| r.get(0))\n                .expect(\"count\")\n        });\n        assert_eq!(count, 2);\n    }\n\n    #[test]\n    fn production_pool_serves_parallel_readers() {\n        // Force a 4-slot pool against per-conn `:memory:` databases.\n        // Each slot is independent, so we use a query that doesn't\n        // depend on shared state — purpose is to confirm `with_conn`\n        // can hand out slots concurrently without deadlocking, and\n        // that `try_lock` picks an idle slot under contention.\n        open_production_pool(\":memory:\", TINY_SCHEMA, 4);\n\n        let handles: Vec<_> = (0..16)\n            .map(|i| {\n                std::thread::spawn(move || {\n                    // Read a literal so each slot's empty :memory: is fine.\n                    let n: i64 = with_conn(|c| {\n                        c.query_row(\"SELECT ?1 + 1\", [i as i64], |r| r.get(0))\n                            .expect(\"query\")\n                    });\n                    assert_eq!(n, i as i64 + 1);\n                })\n            })\n            .collect();\n        for h in handles {\n            h.join().expect(\"worker join\");\n        }\n    }\n\n    #[test]\n    fn setup_replaces_previous_connection() {\n        setup_test_db(TINY_SCHEMA);\n        with_conn(|c| {\n            c.execute(\"INSERT INTO widgets (name) VALUES ('stale')\", [])\n                .expect(\"insert\")\n        });\n        setup_test_db(TINY_SCHEMA);\n        let count: i64 = with_conn(|c| {\n            c.query_row(\"SELECT COUNT(*) FROM widgets\", [], |r| r.get(0))\n                .expect(\"count\")\n        });\n        assert_eq!(count, 0, \"new connection should start empty\");\n    }\n}\n"},{"path":"src/errors_ext.rs","content":"//! Per-target error/raise primitives the transpiled framework runtime\n//! reaches via bare-token emit.\n//!\n//! `raise(KIND, payload)` is the Ruby-shape `raise Klass, \"...\"`\n//! emitted by the transpile pipeline. Rust has no `raise` keyword and\n//! the trio of error classes (`NotImplementedError`, `RecordNotFound`,\n//! `RecordInvalid`) doesn't transpile cleanly yet — Display + Error\n//! synthesis for `class < StandardError` is a separate emit feature.\n//!\n//! Phase 3 stub: a single `FrameworkError` enum, three module-level\n//! consts the transpile's bare tokens map to (via the `imports` field\n//! in `RUST_RUNTIME`), and a `raise` function generic over the payload\n//! type. The function returns `!` so call sites in non-Unit-returning\n//! methods compile (`!` coerces to any type).\n//!\n//! Payload is intentionally `T` (no Debug/Display bound) because the\n//! emitted code passes structs (`self`, `Base` instances) that don't\n//! derive Debug yet. Lost message content is acceptable for the\n//! contract-marker raise calls (table_name, instantiate, etc.) — the\n//! panic still surfaces the kind, which is enough to diagnose a\n//! missing subclass override.\n\n#[derive(Debug, Clone, Copy)]\npub enum FrameworkError {\n    NotImplemented,\n    RecordNotFound,\n    RecordInvalid,\n}\n\n#[allow(non_upper_case_globals)]\npub const NotImplementedError: FrameworkError = FrameworkError::NotImplemented;\n#[allow(non_upper_case_globals)]\npub const RecordNotFound: FrameworkError = FrameworkError::RecordNotFound;\n#[allow(non_upper_case_globals)]\npub const RecordInvalid: FrameworkError = FrameworkError::RecordInvalid;\n\n/// Ruby-shape `raise Klass, payload`. Panics with the framework\n/// error kind; payload is accepted but discarded.\n///\n/// Returns `!` so the caller can use it as the body of any-typed\n/// method (`fn table_name() -> String { raise(...) }` compiles).\npub fn raise<T>(kind: FrameworkError, _payload: T) -> ! {\n    panic!(\"FrameworkError::{:?}\", kind);\n}\n\n/// Placeholder for Ruby's `self.name` (class name) inside emitted\n/// class methods. The transpile lowers bare `name` calls to a free\n/// function reference; until the emit-side rewrites these to\n/// per-class string literals or `std::any::type_name::<Self>()`,\n/// this stub gives the references something to resolve to.\npub fn name() -> &'static str {\n    \"Base\"\n}\n\n// Thread-local validation-error buffer the AR shim's `save` path\n// reads through. Every emitted model's `validate` body pushes\n// user-visible messages here — rust2 emit rewrites the lowered\n// `self.errors() << msg` shape to `validation_errors_push(msg)` so\n// the messages survive the `errors() returns owned Vec<String>` shim\n// that would otherwise drop them. `save` clears the buffer before\n// running validate and reads `validation_errors_is_empty()` to\n// decide whether to persist.\n//\n// Per-thread keeps tests independent (cargo test runs tests on\n// separate threads); a global Mutex would cross-contaminate.\nthread_local! {\n    static VALIDATION_ERRORS: std::cell::RefCell<Vec<String>> =\n        std::cell::RefCell::new(Vec::new());\n}\n\npub fn validation_errors_clear() {\n    VALIDATION_ERRORS.with(|c| c.borrow_mut().clear());\n}\n\npub fn validation_errors_push(msg: String) {\n    VALIDATION_ERRORS.with(|c| c.borrow_mut().push(msg));\n}\n\npub fn validation_errors_is_empty() -> bool {\n    VALIDATION_ERRORS.with(|c| c.borrow().is_empty())\n}\n\npub fn validation_errors_snapshot() -> Vec<String> {\n    VALIDATION_ERRORS.with(|c| c.borrow().clone())\n}\n"},{"path":"src/fixtures/articles.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\npub struct ArticlesFixtures;\n\nimpl ArticlesFixtures {\n    pub fn one() -> Article {\n        Article::find(1_i64)\n    }\n\n    pub fn two() -> Article {\n        Article::find(2_i64)\n    }\n\n    pub fn _fixtures_load_bang() {\n        let mut instance = Article::new(std::collections::HashMap::new());\n        instance.set_id(1_i64);\n        instance.set_title(\"Getting Started with Rails\");\n        instance.set_body(\"Rails is a web application framework running on the Ruby programming language.\");\n        instance.save();\n        instance = Article::new(std::collections::HashMap::new());\n        instance.set_id(2_i64);\n        instance.set_title(\"Understanding MVC Architecture\");\n        instance.set_body(\"MVC stands for Model-View-Controller. Models handle data and business logic.\");\n        instance.save();\n    }\n}\n\n// Wedge 2c.3 bare-fn compat shims — delegate to the impl.\npub fn one() -> Article { ArticlesFixtures::one() }\npub fn two() -> Article { ArticlesFixtures::two() }\n"},{"path":"src/fixtures/comments.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\npub struct CommentsFixtures;\n\nimpl CommentsFixtures {\n    pub fn one() -> Comment {\n        Comment::find(1_i64)\n    }\n\n    pub fn two() -> Comment {\n        Comment::find(2_i64)\n    }\n\n    pub fn _fixtures_load_bang() {\n        let mut instance = Comment::new(std::collections::HashMap::new());\n        instance.set_id(1_i64);\n        instance.set_article_id(1_i64);\n        instance.set_commenter(\"Alice\");\n        instance.set_body(\"Great introduction! Rails really does make development faster.\");\n        instance.save();\n        instance = Comment::new(std::collections::HashMap::new());\n        instance.set_id(2_i64);\n        instance.set_article_id(2_i64);\n        instance.set_commenter(\"Bob\");\n        instance.set_body(\"This pattern really helps keep code organized!\");\n        instance.save();\n    }\n}\n\n// Wedge 2c.3 bare-fn compat shims — delegate to the impl.\npub fn one() -> Comment { CommentsFixtures::one() }\npub fn two() -> Comment { CommentsFixtures::two() }\n"},{"path":"src/fixtures/mod.rs","content":"// Generated by Roundhouse (rust2).\n\npub mod articles;\npub mod comments;\npub use articles::ArticlesFixtures;\npub use comments::CommentsFixtures;\n\n/// Per-test entry point. Brings up a fresh in-memory SQLite,\n/// runs the schema DDL, and loads every fixture in declaration\n/// order. Tests call this as their first line; repeat calls on\n/// the same thread reset to a clean slate.\npub fn setup() {\n    crate::db::setup_test_db(crate::schema_sql::CREATE_TABLES);\n    ArticlesFixtures::_fixtures_load_bang();\n    CommentsFixtures::_fixtures_load_bang();\n}\n"},{"path":"src/flash.rs","content":"//! ActionDispatch::Flash — per-app flash store with typed\n//! `notice`/`alert` fields plus HWIA-shape shim methods.\n//!\n//! Hand-written for rust2 Phase 3 (sibling of\n//! `runtime/ruby/action_dispatch/flash.rb` and\n//! `runtime/typescript/`'s transpiled `flash.ts`). The transpile\n//! pipeline produces broken Rust for this file's HWIA shim methods\n//! (Index trait, self-indexing); hand-writing avoids fighting those\n//! emit bugs. The struct surface matches the typed-targets contract\n//! (`is_flash_name` in `view_to_library/extra_params.rs` declares the\n//! closed `notice`/`alert` field set).\n\nuse std::collections::HashMap;\n\n#[derive(Debug, Default, Clone)]\npub struct Flash {\n    pub notice: Option<String>,\n    pub alert: Option<String>,\n}\n\nimpl Flash {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    pub fn from_persisted(other: Option<&HashMap<String, String>>) -> Self {\n        let mut flash = Self::default();\n        if let Some(map) = other {\n            if let Some(v) = map.get(\"notice\") {\n                flash.notice = Some(v.clone());\n            }\n            if let Some(v) = map.get(\"alert\") {\n                flash.alert = Some(v.clone());\n            }\n        }\n        flash\n    }\n\n    pub fn get(&self, key: &str) -> Option<String> {\n        match key {\n            \"notice\" => self.notice.clone(),\n            \"alert\" => self.alert.clone(),\n            _ => None,\n        }\n    }\n\n    pub fn set(&mut self, key: &str, value: Option<String>) {\n        match key {\n            \"notice\" => self.notice = value,\n            \"alert\" => self.alert = value,\n            _ => {}\n        }\n    }\n\n    pub fn fetch(&self, key: &str, default: Option<String>) -> Option<String> {\n        self.get(key).or(default)\n    }\n\n    pub fn key(&self, key: &str) -> bool {\n        self.get(key).is_some()\n    }\n\n    pub fn has_key(&self, key: &str) -> bool {\n        self.key(key)\n    }\n\n    pub fn include(&self, key: &str) -> bool {\n        self.key(key)\n    }\n\n    pub fn delete(&mut self, key: &str) -> Option<String> {\n        match key {\n            \"notice\" => self.notice.take(),\n            \"alert\" => self.alert.take(),\n            _ => None,\n        }\n    }\n\n    pub fn len(&self) -> usize {\n        let mut n = 0;\n        if self.notice.is_some() {\n            n += 1;\n        }\n        if self.alert.is_some() {\n            n += 1;\n        }\n        n\n    }\n\n    pub fn is_empty(&self) -> bool {\n        self.notice.is_none() && self.alert.is_none()\n    }\n\n    pub fn keys(&self) -> Vec<&'static str> {\n        let mut out = Vec::new();\n        if self.notice.is_some() {\n            out.push(\"notice\");\n        }\n        if self.alert.is_some() {\n            out.push(\"alert\");\n        }\n        out\n    }\n\n    pub fn to_h(&self) -> HashMap<String, String> {\n        let mut out = HashMap::new();\n        if let Some(v) = &self.notice {\n            out.insert(\"notice\".to_string(), v.clone());\n        }\n        if let Some(v) = &self.alert {\n            out.insert(\"alert\".to_string(), v.clone());\n        }\n        out\n    }\n}\n"},{"path":"src/hash_ext.rs","content":"//! HashMap helpers bridging Ruby's `Hash#merge` semantics.\n//!\n//! Ruby `hash.merge(other)` returns a new hash with `other`'s entries\n//! layered on top. The transpiled framework runtime emits this on\n//! HashMap-typed receivers with mixed K/V types — typically a literal\n//! built from `(&str, &str)` or `(&str, String)` pairs merged with a\n//! parameter-typed `HashMap<String, serde_json::Value>`. Generic Rust\n//! merge traits can't bridge that K/V variance.\n//!\n//! `merge_attrs` is the pragmatic landing zone: it accepts any pair-\n//! iterator on both sides whose K is `Into<String>` and V is\n//! `Into<serde_json::Value>`, and produces a unified\n//! `HashMap<String, serde_json::Value>`. That matches the\n//! transpiled-runtime usage where the merged map is consumed by\n//! `render_attrs`, `r#where`, etc. — call sites that don't need to\n//! preserve the literal's narrower K/V types.\n\nuse serde_json::Value;\nuse std::collections::HashMap;\n\npub fn merge_attrs<I1, K1, V1, I2, K2, V2>(base: I1, other: I2) -> HashMap<String, Value>\nwhere\n    I1: IntoIterator<Item = (K1, V1)>,\n    K1: Into<String>,\n    V1: Into<Value>,\n    I2: IntoIterator<Item = (K2, V2)>,\n    K2: Into<String>,\n    V2: Into<Value>,\n{\n    let mut out: HashMap<String, Value> = HashMap::new();\n    for (k, v) in base {\n        out.insert(k.into(), v.into());\n    }\n    for (k, v) in other {\n        out.insert(k.into(), v.into());\n    }\n    out\n}\n"},{"path":"src/http.rs","content":"//! Roundhouse Rust HTTP runtime.\n//!\n//! Hand-written, shipped alongside generated code (copied in by the\n//! Rust emitter as `src/http.rs`). Provides the controller-facing\n//! types + helpers the emitter assumes exist: a `Params` wrapper over\n//! Rails-style bracketed form parameters, a `Redirect` convenience, a\n//! small `ViewCtx` carrying flash + request context to views, and\n//! re-exports of axum's `Html` / `Response` / `IntoResponse` so\n//! emitted action signatures can reference them through a single\n//! `crate::http::*` path.\n//!\n//! Axum is the HTTP framework (chosen to match railcar's precedent +\n//! the surrounding Rust ecosystem's gravity). Actions return `impl\n//! IntoResponse`; form bodies extract via `axum::extract::Form`\n//! wrapping a per-controller `#[derive(Deserialize)]` struct.\n\nuse std::collections::HashMap;\n\npub use axum::response::{Html, IntoResponse, Redirect, Response};\n\n/// Wrapper over the flat HashMap form body that axum's `Form`\n/// extractor produces. Rails posts nested keys like `article[title]=\n/// Foo&article[body]=Bar`; this type provides the `.expect(scope,\n/// &[keys])` accessor used by emitted strong-params helpers and the\n/// `[key]` lookup used by `params[:id]` style access.\n#[derive(Debug, Default, Clone)]\npub struct Params {\n    inner: HashMap<String, String>,\n}\n\nimpl Params {\n    pub fn new(inner: HashMap<String, String>) -> Self {\n        Self { inner }\n    }\n\n    /// Rails `params[:id]` / `params[\"id\"]` — return the raw string\n    /// value for a top-level key. Missing keys return an empty\n    /// string (matches Rails' `params[:missing]` returning nil when\n    /// later coerced; for Phase 4d's ID parsing, use `.int(key)`).\n    pub fn get(&self, key: &str) -> &str {\n        self.inner.get(key).map(|s| s.as_str()).unwrap_or(\"\")\n    }\n\n    /// Parse a param as an `i64`. Used in place of the Ruby\n    /// `params[:id]` which is string-typed but always gets coerced\n    /// to an integer for DB lookup. Returns 0 on missing/unparsable.\n    pub fn int(&self, key: &str) -> i64 {\n        self.inner.get(key).and_then(|s| s.parse().ok()).unwrap_or(0)\n    }\n\n    /// Strong-params extractor: pull every `scope[field]` key out of\n    /// the flat form body and return a new `HashMap<String, String>`\n    /// keyed on `field`. Emitted strong-params helpers use this to\n    /// populate their typed struct's fields.\n    ///\n    /// `params.expect(article: [:title, :body])` in Rails lowers to\n    /// `params.expect(\"article\", &[\"title\", \"body\"])` in emitted\n    /// Rust, and the returned map is consumed by the model's\n    /// from-params constructor.\n    pub fn expect(&self, scope: &str, keys: &[&str]) -> HashMap<String, String> {\n        let prefix = format!(\"{scope}[\");\n        let mut out = HashMap::new();\n        for key in keys {\n            let full = format!(\"{prefix}{key}]\");\n            if let Some(v) = self.inner.get(&full) {\n                out.insert((*key).to_string(), v.clone());\n            }\n        }\n        out\n    }\n}\n\nimpl From<HashMap<String, String>> for Params {\n    fn from(inner: HashMap<String, String>) -> Self {\n        Self::new(inner)\n    }\n}\n\n/// Convenience: emit `crate::http::redirect(&path)` from a path\n/// helper's result. Wraps axum's `Redirect::to` with the 303 See\n/// Other status that Rails uses for create/update/destroy redirects.\npub fn redirect(path: &str) -> Redirect {\n    Redirect::to(path)\n}\n\n/// Convenience: emit `crate::http::html(body)` to wrap a view's\n/// String output as an HTML response. Same as `Html(body)` but one\n/// import shorter at call sites.\npub fn html(body: String) -> Html<String> {\n    Html(body)\n}\n\n/// Error response with HTTP 422 (unprocessable entity) — Rails'\n/// convention for validation failures on create/update. Emitters\n/// wrap a view render in this on the `else` branch of `@model.save`.\npub fn unprocessable(body: String) -> (axum::http::StatusCode, Html<String>) {\n    (axum::http::StatusCode::UNPROCESSABLE_ENTITY, Html(body))\n}\n\n/// Context threaded through view functions. Phase 4d minimum: flash\n/// notice (read in every view via `notice.present?`). Later: current\n/// user, csrf token, request path, locale, etc.\n#[derive(Debug, Default, Clone)]\npub struct ViewCtx {\n    pub notice: Option<String>,\n}\n\nimpl ViewCtx {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    pub fn with_notice(notice: impl Into<String>) -> Self {\n        Self { notice: Some(notice.into()) }\n    }\n}\n\n// ── rust2 controller-action response state ──────────────────────\n//\n// Rails controllers thread response data through implicit state —\n// `render`, `redirect_to`, `head`, and `response.headers[…] = …`\n// each accumulate into the controller's response object, which the\n// framework serializes to the HTTP body after the action returns.\n// Rust2's emit shape carries the controller as `impl X { pub fn\n// show(&mut self) }` — `&mut self` methods returning `()`. That\n// signature isn't compatible with axum's free-fn-extractor-then-\n// IntoResponse contract.\n//\n// Bridge: emit per-action axum wrapper free fns that clear this\n// thread-local, build the controller, call the action, then\n// translate the accumulated `ControllerResponse` into an\n// `axum::response::Response`. The AC::Base shim's `render` /\n// `render_with` / `redirect_to` / `head` methods (today no-ops\n// emitted at `src/emit/rust2.rs:~782`) become thin writers to\n// this state.\n//\n// Per-thread because axum dispatches each request on a tokio task\n// that's pinned to one thread for the duration of an action body\n// (controller bodies are sync `&mut self` methods — no `.await`\n// inside, so thread affinity holds). A future migration to async\n// action bodies would need a per-task storage shape (extension\n// types, task_local!, etc.).\n\n#[derive(Debug, Clone)]\npub struct ControllerResponse {\n    pub status: u16,\n    pub body: String,\n    pub content_type: String,\n    /// Set when `redirect_to` fires; the wrapper emits a 3xx with\n    /// this as the `Location` header instead of an HTML body.\n    pub location: Option<String>,\n}\n\nimpl Default for ControllerResponse {\n    fn default() -> Self {\n        Self {\n            status: 200,\n            body: String::new(),\n            content_type: \"text/html; charset=utf-8\".to_string(),\n            location: None,\n        }\n    }\n}\n\nthread_local! {\n    static RESPONSE: std::cell::RefCell<ControllerResponse> =\n        std::cell::RefCell::new(ControllerResponse::default());\n    static REQUEST_FORMAT: std::cell::RefCell<String> =\n        std::cell::RefCell::new(String::from(\"html\"));\n    // Flash the CURRENT action set (`redirect_to … notice:` /\n    // `flash[:x] = …`) — carried to the NEXT request via the rh_flash\n    // cookie. Only what the action set lands here; the incoming flash\n    // (loaded into the controller's `flash` field for display) is never\n    // copied in, so it naturally shows exactly once. Set synchronously\n    // inside the per-action wrapper (no `.await` between the controller\n    // body and `flash_out_take`), so thread affinity holds — same\n    // discipline as `RESPONSE`.\n    static FLASH_OUT: std::cell::RefCell<std::collections::HashMap<String, String>> =\n        std::cell::RefCell::new(std::collections::HashMap::new());\n}\n\n/// Request extension carrying the inferred format (\"html\"/\"json\").\n/// Set by the `request_format_middleware` in `server.rs` after it\n/// strips a `.json` suffix off the URI; read by the per-action axum\n/// wrappers (extracted via `axum::extract::Extension<RequestFormatExt>`)\n/// and threaded into the thread-local before the controller body runs.\n#[derive(Clone, Debug)]\npub struct RequestFormatExt(pub String);\n\n/// Stash the inferred format on the per-task thread-local. The axum\n/// wrapper calls this synchronously immediately before the controller\n/// action body — `AC::Base#request_format` (emitted as a shim method\n/// on each controller) reads it back via `request_format_get`. No\n/// `.await` between set and read, so thread affinity holds.\npub fn request_format_set(format: String) {\n    REQUEST_FORMAT.with(|r| *r.borrow_mut() = format);\n}\n\n/// Read the current request's format. Called by the controller-shim\n/// `request_format()` method; defaults to `\"html\"` if no middleware\n/// has populated it (e.g. unit tests instantiating a controller\n/// directly).\npub fn request_format_get() -> String {\n    REQUEST_FORMAT.with(|r| r.borrow().clone())\n}\n\n/// Tag every request with its inferred format (\"html\" or \"json\") as\n/// an extension before it reaches the per-action handler. The\n/// emitted router attaches this as a `.layer()` so both `axum::serve`\n/// (production) and `axum_test::TestServer` (controller tests) share\n/// one wiring path.\n///\n/// Why an Extension and not a URI rewrite: in axum 0.8 `Router::layer`\n/// wraps each route's handler — route matching + `Path<...>`\n/// extraction happens *before* the layer runs, so URI rewrites here\n/// are too late to affect routing. The router emit registers explicit\n/// `.json`-suffixed entries for parameterless paths\n/// (`src/emit/rust2.rs::render_axum_router_body`); parameterized\n/// paths capture the `.json` tail as part of the segment (e.g.\n/// `id=\"1.json\"`), and the action wrapper strips the suffix before\n/// parsing the id as `i64`. This layer just surfaces the inferred\n/// format so the `if self.request_format() == \"json\"` branch dispatches\n/// the JSON jbuilder view.\npub async fn request_format_middleware(\n    mut req: axum::extract::Request,\n    next: axum::middleware::Next,\n) -> axum::response::Response {\n    let format = if req.uri().path().ends_with(\".json\") {\n        \"json\"\n    } else {\n        \"html\"\n    };\n    req.extensions_mut().insert(RequestFormatExt(format.to_string()));\n    next.run(req).await\n}\n\n/// Reset the thread-local to defaults. Called at the top of each\n/// axum wrapper so a prior action's state doesn't leak into the\n/// current request.\npub fn response_clear() {\n    RESPONSE.with(|r| *r.borrow_mut() = ControllerResponse::default());\n    // Reset the carried-flash accumulator alongside the response so a\n    // prior request's `redirect_to … notice:` can't leak into this one.\n    FLASH_OUT.with(|f| f.borrow_mut().clear());\n}\n\n// ── flash: cookie-backed, per-session storage adapter ───────────\n//\n// The rust2 server is a storage adapter for the \"show exactly once\"\n// flash lifecycle: the action that sets a flash (`redirect_to …\n// notice:`) records it in the FLASH_OUT thread-local, which the\n// per-action wrapper writes to the `rh_flash` cookie; the follow-on\n// request reloads it into the controller's `flash` field for display\n// (via `flash_from_request`) and sets no new flash, so the cookie is\n// cleared and the notice shows once. Per-browser by construction —\n// parallel sessions never share a flash slot.\n\n/// Cookie name carrying flash between requests.\nconst FLASH_COOKIE: &str = \"rh_flash\";\n\n/// Record a flash the current action set (notice/alert only — the\n/// closed key set the lowerer recognizes). Called by the controller\n/// `redirect_to` shim (and any `flash[:x] = …` lowering). The map is\n/// swept into the response cookie by `apply_flash_cookie`.\npub fn flash_out_set(key: &str, value: &str) {\n    if key == \"notice\" || key == \"alert\" {\n        FLASH_OUT.with(|f| {\n            f.borrow_mut().insert(key.to_string(), value.to_string());\n        });\n    }\n}\n\n/// Take (and reset) the flash the action set this request. Called by\n/// the per-action wrapper after the controller body returns.\npub fn flash_out_take() -> std::collections::HashMap<String, String> {\n    FLASH_OUT.with(|f| std::mem::take(&mut *f.borrow_mut()))\n}\n\n/// Build a `Flash` from the incoming `rh_flash` cookie so views can\n/// render `notice` / `alert`. Absent or unparseable cookie → empty\n/// flash (the first request in a session carries none).\npub fn flash_from_request(headers: &axum::http::HeaderMap) -> crate::flash::Flash {\n    let map = read_flash_cookie(headers);\n    crate::flash::Flash::from_persisted(Some(&map))\n}\n\n/// Write the carried-forward flash onto the response as a `Set-Cookie`\n/// header. An empty map clears the cookie (Max-Age=0) so a notice shown\n/// once doesn't stick. Survives the `layout_wrap` middleware (which\n/// preserves response headers) and redirect pass-through.\npub fn apply_flash_cookie(\n    resp: &mut axum::response::Response,\n    persisted: &std::collections::HashMap<String, String>,\n) {\n    let cookie = if persisted.is_empty() {\n        format!(\"{FLASH_COOKIE}=; Path=/; Max-Age=0; HttpOnly\")\n    } else {\n        // Deterministic key order; values percent-encoded so the\n        // urlencoded `key=value&…` structure (and cookie-octet rules)\n        // survive arbitrary notice text.\n        let mut parts: Vec<String> = Vec::new();\n        for k in [\"notice\", \"alert\"] {\n            if let Some(v) = persisted.get(k) {\n                parts.push(format!(\"{k}={}\", pct_encode(v)));\n            }\n        }\n        format!(\"{FLASH_COOKIE}={}; Path=/; HttpOnly\", parts.join(\"&\"))\n    };\n    if let Ok(hv) = axum::http::HeaderValue::from_str(&cookie) {\n        resp.headers_mut()\n            .append(axum::http::header::SET_COOKIE, hv);\n    }\n}\n\n/// Parse the `rh_flash` cookie out of the request headers into a\n/// String-keyed map (the shape `Flash::from_persisted` reloads from).\nfn read_flash_cookie(\n    headers: &axum::http::HeaderMap,\n) -> std::collections::HashMap<String, String> {\n    let mut out = std::collections::HashMap::new();\n    let raw = match headers\n        .get(axum::http::header::COOKIE)\n        .and_then(|v| v.to_str().ok())\n    {\n        Some(s) => s,\n        None => return out,\n    };\n    // Cookie header is `name=value; name2=value2; …`. Find our jar.\n    for pair in raw.split(';') {\n        let pair = pair.trim();\n        if let Some(val) = pair.strip_prefix(\"rh_flash=\") {\n            for kv in val.split('&') {\n                if let Some((k, v)) = kv.split_once('=') {\n                    if k == \"notice\" || k == \"alert\" {\n                        let decoded = pct_decode(v);\n                        if !decoded.is_empty() {\n                            out.insert(k.to_string(), decoded);\n                        }\n                    }\n                }\n            }\n        }\n    }\n    out\n}\n\n/// Minimal percent-encoder over the unreserved set — keeps the cookie\n/// value within RFC 6265 cookie-octets and our `&`/`=` delimiters\n/// unambiguous (both are escaped when they appear in a value).\nfn pct_encode(s: &str) -> String {\n    let mut out = String::with_capacity(s.len());\n    for b in s.bytes() {\n        match b {\n            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {\n                out.push(b as char)\n            }\n            _ => out.push_str(&format!(\"%{b:02X}\")),\n        }\n    }\n    out\n}\n\n/// Inverse of `pct_encode`. Unknown/malformed `%` sequences pass\n/// through literally rather than erroring.\nfn pct_decode(s: &str) -> String {\n    let bytes = s.as_bytes();\n    let mut out: Vec<u8> = Vec::with_capacity(bytes.len());\n    let mut i = 0;\n    while i < bytes.len() {\n        if bytes[i] == b'%' && i + 3 <= bytes.len() {\n            if let Ok(byte) = u8::from_str_radix(&s[i + 1..i + 3], 16) {\n                out.push(byte);\n                i += 3;\n                continue;\n            }\n        }\n        out.push(bytes[i]);\n        i += 1;\n    }\n    String::from_utf8_lossy(&out).into_owned()\n}\n\n/// `render(content)` — stash the body string. Defaults already\n/// have 200/text-html, so a bare render is fully wired.\npub fn response_set_body(body: String) {\n    RESPONSE.with(|r| r.borrow_mut().body = body);\n}\n\n/// `render_with(content, opts)` — body + content_type, optionally\n/// status. Honors common `opts` keys (`content_type`, `status`).\n/// Unknown keys ignored; the AC::Base shim's call site already\n/// strips the Ruby-only knobs.\npub fn response_set_body_with(body: String, content_type: Option<String>, status: Option<u16>) {\n    RESPONSE.with(|r| {\n        let mut resp = r.borrow_mut();\n        resp.body = body;\n        if let Some(ct) = content_type {\n            resp.content_type = ct;\n        }\n        if let Some(st) = status {\n            resp.status = st;\n        }\n    });\n}\n\n/// `redirect_to(path, opts)` — 303 See Other by default; the\n/// `status: :see_other` opt matches Rails' default convention for\n/// post-mutation redirects (avoids form re-submit on back/refresh).\npub fn response_set_redirect(location: String, status: u16) {\n    RESPONSE.with(|r| {\n        let mut resp = r.borrow_mut();\n        resp.status = status;\n        resp.location = Some(location);\n        resp.body = String::new();\n    });\n}\n\n/// `head(name, opts)` — Rails-style status symbol → numeric code.\n/// Body stays empty. Symbol names mirror `Rack::Utils::SYMBOL_TO_STATUS_CODE`.\npub fn response_set_head(status_name: &str, content_type: Option<String>) {\n    let code = status_name_to_code(status_name);\n    RESPONSE.with(|r| {\n        let mut resp = r.borrow_mut();\n        resp.status = code;\n        resp.body = String::new();\n        if let Some(ct) = content_type {\n            resp.content_type = ct;\n        }\n    });\n}\n\n/// Snapshot + reset — used by the per-action axum wrapper to read\n/// out the state immediately after the action returns. Returns\n/// owned value so the borrow on the thread-local is short.\npub fn response_take() -> ControllerResponse {\n    RESPONSE.with(|r| std::mem::take(&mut *r.borrow_mut()))\n}\n\n/// Translate a thread-local response into an `axum::response::Response`.\n/// Redirect-shaped state produces a 3xx with `Location`; otherwise\n/// emits the body with the recorded content-type + status.\npub fn response_into_axum(resp: ControllerResponse) -> axum::response::Response {\n    use axum::http::StatusCode;\n    use axum::response::IntoResponse;\n    let status = StatusCode::from_u16(resp.status).unwrap_or(StatusCode::OK);\n    if let Some(location) = resp.location {\n        let mut response = (status, ()).into_response();\n        if let Ok(hv) = axum::http::HeaderValue::from_str(&location) {\n            response.headers_mut().insert(axum::http::header::LOCATION, hv);\n        }\n        return response;\n    }\n    let body = resp.body;\n    let content_type = resp.content_type;\n    let mut response = (status, body).into_response();\n    if let Ok(hv) = axum::http::HeaderValue::from_str(&content_type) {\n        response\n            .headers_mut()\n            .insert(axum::http::header::CONTENT_TYPE, hv);\n    }\n    response\n}\n\n/// Public alias for `status_name_to_code` — exposed for the AC::Base\n/// shim emitted in `src/emit/rust2.rs`, which reaches it from\n/// outside the crate-private `http` module. Same semantics; just\n/// a re-export that survives module privacy.\npub fn status_name_to_code_pub(name: &str) -> u16 {\n    status_name_to_code(name)\n}\n\n/// Ruby `Object#to_s` analog. Rails' `inner_v.to_s` in\n/// `ActionView::ViewHelpers#render_attrs` ships through any\n/// Hash[String, untyped]; on strict-typed targets the `untyped`\n/// alias resolves to `serde_json::Value`, whose `Display` /\n/// `to_string()` emits a JSON serialization (so\n/// `Value::String(\"reload\").to_string()` becomes `\"\\\"reload\\\"\"`,\n/// not `reload`). Ruby's `String#to_s` is identity — bare string.\n///\n/// `RubyToS` bridges: implementations cover the three recv types\n/// rust2 emit lowers `untyped`-receiver `.to_s` Sends to (`str` /\n/// `String` / `serde_json::Value`). Rust resolves the impl at\n/// compile time via auto-deref, so the rust2 dispatch can emit\n/// `(recv).ruby_to_s()` uniformly without distinguishing closure\n/// params (genuinely `&String` at runtime, body-typer marks\n/// `Untyped`) from value-typed locals (genuinely `&Value`).\n///\n/// Used at every call site in `runtime/ruby/action_view/view_helpers.rb`\n/// that produces an attribute / data-attribute / link tag value;\n/// the lowered IR's `.to_s` Sends on Untyped recvs route through\n/// this trait by the recv-Ty-aware bridge in\n/// `src/emit/rust2/expr/send/dispatch.rs`.\npub trait RubyToS {\n    fn ruby_to_s(&self) -> String;\n}\n\nimpl RubyToS for str {\n    fn ruby_to_s(&self) -> String {\n        self.to_string()\n    }\n}\n\nimpl RubyToS for String {\n    fn ruby_to_s(&self) -> String {\n        self.clone()\n    }\n}\n\nimpl RubyToS for serde_json::Value {\n    fn ruby_to_s(&self) -> String {\n        match self {\n            serde_json::Value::String(s) => s.clone(),\n            serde_json::Value::Null => String::new(),\n            other => other.to_string(),\n        }\n    }\n}\n\n/// Translate a flat axum-`Form<HashMap<String, String>>` body into\n/// the nested params shape Rails controllers expect. Form names of\n/// the shape `article[title]=Foo` land at `params[\"article\"]\n/// [\"title\"] = \"Foo\"`; top-level names pass through unchanged.\n///\n/// One level of bracket-nesting only — scaffold blog forms don't\n/// reach deeper. The lowered `<Resource>Params::from_raw` factory\n/// always looks up a single nested scope (`params.get(resource)`)\n/// then individual fields under it, so the single-level shape\n/// covers every emitted call site today. Deep nesting\n/// (`comment[article_attributes][title]`) becomes a follow-on if\n/// `accepts_nested_attributes_for` lands.\npub fn params_from_form(\n    form: HashMap<String, String>,\n) -> HashMap<String, serde_json::Value> {\n    let mut out: HashMap<String, serde_json::Value> = HashMap::new();\n    for (k, v) in form {\n        if let (Some(open), Some(close)) = (k.find('['), k.rfind(']')) {\n            if close > open {\n                let scope = &k[..open];\n                let inner = &k[open + 1..close];\n                let entry = out\n                    .entry(scope.to_string())\n                    .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));\n                if let serde_json::Value::Object(map) = entry {\n                    map.insert(inner.to_string(), serde_json::Value::from(v));\n                    continue;\n                }\n            }\n        }\n        out.insert(k, serde_json::Value::from(v));\n    }\n    out\n}\n\n/// Rails status-symbol → HTTP code. Subset matching the names the\n/// scaffold emit reaches (`:ok`, `:no_content`, `:not_found`,\n/// `:unprocessable_entity`, `:see_other`). Unknown names fall back\n/// to 200 OK — the controller path that emits an unknown symbol is\n/// generally a bug the caller will see via behavior, not a route\n/// the framework should silently 500 on.\nfn status_name_to_code(name: &str) -> u16 {\n    match name {\n        \"ok\" => 200,\n        \"created\" => 201,\n        \"accepted\" => 202,\n        \"no_content\" => 204,\n        \"moved_permanently\" => 301,\n        \"found\" => 302,\n        \"see_other\" => 303,\n        \"not_modified\" => 304,\n        \"temporary_redirect\" => 307,\n        \"permanent_redirect\" => 308,\n        \"bad_request\" => 400,\n        \"unauthorized\" => 401,\n        \"forbidden\" => 403,\n        \"not_found\" => 404,\n        \"unprocessable_entity\" | \"unprocessable_content\" => 422,\n        \"internal_server_error\" => 500,\n        _ => 200,\n    }\n}\n"},{"path":"src/importmap.rs","content":"pub struct Importmap;\n\nimpl Importmap {\n    pub fn pins() -> Vec<serde_json::Value> {\n        vec![serde_json::Value::Object(std::collections::HashMap::from([(\"name\", \"application\"), (\"path\", \"/assets/application.js\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect()), serde_json::Value::Object(std::collections::HashMap::from([(\"name\", \"@hotwired/turbo-rails\"), (\"path\", \"/assets/turbo.min.js\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect()), serde_json::Value::Object(std::collections::HashMap::from([(\"name\", \"@hotwired/stimulus\"), (\"path\", \"/assets/stimulus.min.js\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect()), serde_json::Value::Object(std::collections::HashMap::from([(\"name\", \"@hotwired/stimulus-loading\"), (\"path\", \"/assets/stimulus-loading.js\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect()), serde_json::Value::Object(std::collections::HashMap::from([(\"name\", \"controllers/application\"), (\"path\", \"/assets/controllers/application.js\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect()), serde_json::Value::Object(std::collections::HashMap::from([(\"name\", \"controllers/hello_controller\"), (\"path\", \"/assets/controllers/hello_controller.js\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect()), serde_json::Value::Object(std::collections::HashMap::from([(\"name\", \"controllers\"), (\"path\", \"/assets/controllers/index.js\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect())]\n    }\n\n    pub fn entry() -> String {\n        (\"application\").to_string()\n    }\n}\n"},{"path":"src/inflector.rs","content":"// Generated from runtime/ruby/inflector.rb at app emit time.\n// Do not edit by hand — edit the source `.rb` and re-run emit.\n\npub struct Inflector;\n\nimpl Inflector {\n    pub fn pluralize(count: i64, word: &str) -> String {\n        if count == 1_i64 { format!(\"1 {}\", word.clone()) } else { format!(\"{} {}s\", count, word.clone()) }\n    }\n}\n"},{"path":"src/json_builder.rs","content":"// Generated from runtime/ruby/json_builder.rb at app emit time.\n// Do not edit by hand — edit the source `.rb` and re-run emit.\n\nuse crate::http::RubyToS;\n\nstatic ESCAPES: std::sync::LazyLock<std::collections::HashMap<&'static str, &'static str>> = std::sync::LazyLock::new(|| std::collections::HashMap::from([(\"\\\\\", \"\\\\\\\\\"), (\"\\\"\", \"\\\\\\\"\"), (\"\\n\", \"\\\\n\"), (\"\\r\", \"\\\\r\"), (\"\\t\", \"\\\\t\"), (\"\\u{8}\", \"\\\\b\"), (\"\\u{c}\", \"\\\\f\")]));\nstatic ESCAPE_PATTERN: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| regex::Regex::new(\"[\\\\\\\\\\\"\\\\n\\\\r\\\\t\\\\x08\\\\f]\").unwrap());\n\npub struct JsonBuilder;\n\nimpl JsonBuilder {\n    pub fn encode_string(s: &str) -> String {\n        ESCAPE_PATTERN.replace_all(&s, |__caps: &regex::Captures| -> String { (*ESCAPES.get(&__caps[0]).unwrap_or(&\"\")).to_string() }).into_owned()\n    }\n\n    pub fn encode_value(v: serde_json::Value) -> String {\n        if v.clone().is_null() { return (\"null\").to_string() };\n        if v.clone().is_boolean() { return (\"true\").to_string() };\n        if v.clone().is_boolean() { return (\"false\").to_string() };\n        if v.clone().is_i64() { return v.as_i64().unwrap().to_string() };\n        if v.clone().is_f64() { return v.as_f64().unwrap().to_string() };\n        if v.clone().is_string() { return format!(\"\\\"{}\\\"\", Self::encode_string(&(v.as_str().unwrap()))) };\n        format!(\"\\\"{}\\\"\", Self::encode_string(&(v.ruby_to_s())))\n    }\n\n    pub fn encode_datetime(s: Option<String>) -> String {\n        let Some(s) = s else { return (\"null\").to_string() };\n        let mut str = s.to_string();\n        if (str.len() as i64) < 19_i64 { return format!(\"\\\"{}\\\"\", Self::encode_string(&(str.clone()))) };\n        let date = (&str.clone()[(0_i64) as usize..((0_i64) + (10_i64)) as usize]).to_string();\n        let time = (&str.clone()[(11_i64) as usize..((11_i64) + (8_i64)) as usize]).to_string();\n        let mut ms = (\"000\").to_string();\n        if (str.len() as i64) > 20_i64 && (&str.clone()[(19_i64) as usize..((19_i64) + (1_i64)) as usize]).to_string() == \".\" { { let frac = &str.clone()[(20_i64) as usize..];\n        let mut padded = format!(\"{}000\", frac);\n        ms = (&padded[(0_i64) as usize..((0_i64) + (3_i64)) as usize]).to_string() } };\n        format!(\"\\\"{}T{}.{}Z\\\"\", date, time, ms)\n    }\n}\n"},{"path":"src/lib.rs","content":"// Generated by Roundhouse (rust2).\n\npub mod action_controller_base;\npub mod active_record_adapter;\npub mod active_record_base;\npub mod adapter_interface;\npub mod broadcasts;\npub mod cable;\npub mod controllers;\npub mod db;\npub mod errors_ext;\n#[cfg(test)]\npub mod fixtures;\npub mod flash;\npub mod hash_ext;\npub mod http;\npub mod importmap;\npub mod inflector;\npub mod json_builder;\npub mod models;\npub mod param_value;\npub mod route_helpers;\npub mod router;\npub mod schema_sql;\npub mod server;\npub mod session;\n#[cfg(test)]\npub mod test_support;\n#[cfg(test)]\npub mod tests;\npub mod view_helpers;\npub mod views;\n"},{"path":"src/main.rs","content":"// Generated by Roundhouse (rust2).\n\nuse app::{router, schema_sql, server};\n\n#[tokio::main]\nasync fn main() {\n    let db_path = std::env::var(\"DATABASE_PATH\").ok();\n    let port = std::env::var(\"PORT\").ok().and_then(|s| s.parse().ok());\n    server::start(\n        router::router(),\n        server::StartOptions {\n            db_path,\n            port,\n            schema_sql: schema_sql::CREATE_TABLES,\n            layout: Some(app::views::layouts::render_layout),\n        },\n    )\n    .await;\n}\n"},{"path":"src/models/application_record.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\npub struct ApplicationRecord;\n\nimpl ApplicationRecord {\n    pub fn r#abstract() -> bool {\n        true\n    }\n}\n"},{"path":"src/models/article.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n#[derive(Clone, Default)]\npub struct Article {\n    pub id: i64,\n    pub body: String,\n    pub created_at: String,\n    pub title: String,\n    pub updated_at: String,\n    pub comments_cache: Vec<Comment>,\n    pub comments_loaded: bool,\n}\n\nimpl Article {\n    pub fn id(&self) -> i64 {\n        self.id\n    }\n\n    pub fn set_id(&mut self, value: i64) {\n        self.id = value\n    }\n\n    pub fn body(&self) -> String {\n        self.body.clone()\n    }\n\n    pub fn set_body(&mut self, value: &str) {\n        self.body = (value).to_string()\n    }\n\n    pub fn created_at(&self) -> String {\n        self.created_at.clone()\n    }\n\n    pub fn set_created_at(&mut self, value: &str) {\n        self.created_at = (value).to_string()\n    }\n\n    pub fn title(&self) -> String {\n        self.title.clone()\n    }\n\n    pub fn set_title(&mut self, value: &str) {\n        self.title = (value).to_string()\n    }\n\n    pub fn updated_at(&self) -> String {\n        self.updated_at.clone()\n    }\n\n    pub fn set_updated_at(&mut self, value: &str) {\n        self.updated_at = (value).to_string()\n    }\n\n    pub fn table_name() -> String {\n        (\"articles\").to_string()\n    }\n\n    pub fn schema_columns() -> Vec<String> {\n        vec![\"id\".to_string(), \"body\".to_string(), \"created_at\".to_string(), \"title\".to_string(), \"updated_at\".to_string()]\n    }\n\n    pub fn instantiate(row: std::collections::HashMap<String, serde_json::Value>) -> Article {\n        let mut instance = Article::from_row(ArticleRow::from_raw(row.clone()));\n        instance.mark_persisted_bang();\n        instance.clone()\n    }\n\n    pub fn from_row(row: ArticleRow) -> Article {\n        let mut instance = Article::new(std::collections::HashMap::new());\n        instance.set_id(row.id());\n        instance.set_body(&(row.body()));\n        instance.set_created_at(&(row.created_at()));\n        instance.set_title(&(row.title()));\n        instance.set_updated_at(&(row.updated_at()));\n        instance.clone()\n    }\n\n    pub fn from_stmt(stmt: i64) -> Article {\n        let mut instance = Article::new(std::collections::HashMap::new());\n        instance.set_id(Db::column_int(stmt, 0_i64));\n        instance.set_body(&(Db::column_text(stmt, 1_i64)));\n        instance.set_created_at(&(Db::column_text(stmt, 2_i64)));\n        instance.set_title(&(Db::column_text(stmt, 3_i64)));\n        instance.set_updated_at(&(Db::column_text(stmt, 4_i64)));\n        instance.mark_persisted_bang();\n        instance.clone()\n    }\n\n    pub fn assign_from_row(&mut self, row: std::collections::HashMap<String, serde_json::Value>) {\n        self.set_id((row.clone()[\"id\"]).as_i64().unwrap());\n        self.set_body((row.clone()[\"body\"]).as_str().unwrap());\n        self.set_created_at((row.clone()[\"created_at\"]).as_str().unwrap());\n        self.set_title((row.clone()[\"title\"]).as_str().unwrap());\n        self.set_updated_at((row.clone()[\"updated_at\"]).as_str().unwrap());\n    }\n\n    pub fn new(attrs: std::collections::HashMap<String, serde_json::Value>) -> Self {\n        /* TODO rust2: ExprNode::Discriminant(22) */;\n        let id = (attrs.clone().get(\"id\").cloned().unwrap_or(serde_json::Value::from(0_i64))).as_i64().unwrap();\n        let body = (attrs.clone().get(\"body\").cloned().unwrap_or(serde_json::Value::from(\"\"))).as_str().unwrap().to_string();\n        let created_at = (attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::from(\"\"))).as_str().unwrap().to_string();\n        let title = (attrs.clone().get(\"title\").cloned().unwrap_or(serde_json::Value::from(\"\"))).as_str().unwrap().to_string();\n        let updated_at = (attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::from(\"\"))).as_str().unwrap().to_string();\n        let mut comments_cache: Vec<Comment> = vec![];\n        let mut comments_loaded: bool = false;\n        Self { id, body, created_at, title, updated_at, comments_cache, comments_loaded }\n    }\n\n    pub fn attributes(&self) -> std::collections::HashMap<String, serde_json::Value> {\n        std::collections::HashMap::from([(\"body\".to_string(), serde_json::Value::from(self.body.clone())), (\"created_at\".to_string(), serde_json::Value::from(self.created_at.clone())), (\"title\".to_string(), serde_json::Value::from(self.title.clone())), (\"updated_at\".to_string(), serde_json::Value::from(self.updated_at.clone()))])\n    }\n\n    pub fn get_index(&self, name: &str) -> serde_json::Value {\n        match name {\n                \"id\" => { serde_json::Value::from(self.id) },\n                \"body\" => { serde_json::Value::from(self.body.clone()) },\n                \"created_at\" => { serde_json::Value::from(self.created_at.clone()) },\n                \"title\" => { serde_json::Value::from(self.title.clone()) },\n                \"updated_at\" => { serde_json::Value::from(self.updated_at.clone()) }\n                _ => serde_json::Value::Null,\n            }\n    }\n\n    pub fn set_index(&mut self, name: &str, value: serde_json::Value) {\n        match name {\n                \"id\" => { self.id = (value.clone()).as_i64().unwrap() },\n                \"body\" => { self.body = (value.clone()).as_str().unwrap().to_string().to_string() },\n                \"created_at\" => { self.created_at = (value.clone()).as_str().unwrap().to_string().to_string() },\n                \"title\" => { self.title = (value.clone()).as_str().unwrap().to_string().to_string() },\n                \"updated_at\" => { self.updated_at = (value.clone()).as_str().unwrap().to_string().to_string() }\n                _ => (),\n            }\n    }\n\n    pub fn update(&mut self, p: ArticleParams) -> bool {\n        if !({ let _ = p.title(); false }) { self.set_title(&(p.title())) };\n        if !({ let _ = p.body(); false }) { self.set_body(&(p.body())) };\n        self.save()\n    }\n\n    pub fn _adapter_find_by_id(id: i64) -> Option<Article> {\n        let stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"SELECT id, body, created_at, title, updated_at FROM articles\", \" WHERE \"), \"id = \"), Db::escape_int(id)), \" LIMIT 1\")));\n        let mut result: Option<Article> = None;\n        if Db::step(stmt) { result = Some(Article::from_stmt(stmt)) };\n        Db::finalize(stmt);\n        result\n    }\n\n    pub fn _adapter_all() -> Vec<Article> {\n        let stmt = Db::prepare(\"SELECT id, body, created_at, title, updated_at FROM articles\");\n        let mut results = vec![];\n        while Db::step(stmt) {\n            results.push(Article::from_stmt(stmt))\n        };\n        Db::finalize(stmt);\n        results.clone()\n    }\n\n    pub fn _adapter_insert(&self) -> i64 {\n        Db::exec(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"INSERT INTO articles (body, created_at, title, updated_at) VALUES (\", Db::escape_string(&(self.body))), \", \"), Db::escape_string(&(self.created_at))), \", \"), Db::escape_string(&(self.title))), \", \"), Db::escape_string(&(self.updated_at))), \")\")));\n        Db::last_insert_rowid()\n    }\n\n    pub fn _adapter_update(&self) {\n        Db::exec(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"UPDATE articles SET \", \"body = \"), Db::escape_string(&(self.body))), \", created_at = \"), Db::escape_string(&(self.created_at))), \", title = \"), Db::escape_string(&(self.title))), \", updated_at = \"), Db::escape_string(&(self.updated_at))), \" WHERE \"), \"id = \"), Db::escape_int(self.id))));\n    }\n\n    pub fn _adapter_delete(&self) {\n        Db::exec(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"DELETE FROM articles\", \" WHERE \"), \"id = \"), Db::escape_int(self.id))));\n    }\n\n    pub fn _adapter_count() -> i64 {\n        let stmt = Db::prepare(\"SELECT COUNT(*) FROM articles\");\n        Db::step(stmt);\n        let result = Db::column_int(stmt, 0_i64);\n        Db::finalize(stmt);\n        result\n    }\n\n    pub fn _adapter_exists_by_id(id: i64) -> bool {\n        let stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"SELECT 1 FROM articles\", \" WHERE \"), \"id = \"), Db::escape_int(id)), \" LIMIT 1\")));\n        let result = Db::step(stmt);\n        Db::finalize(stmt);\n        result\n    }\n\n    pub fn _adapter_truncate() {\n        Db::exec(\"DELETE FROM articles\");\n        Db::exec(\"DELETE FROM sqlite_sequence WHERE name = 'articles'\");\n    }\n\n    pub fn _adapter_reload(&mut self) -> Article {\n        let stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", \"SELECT id, body, created_at, title, updated_at FROM articles WHERE id = \", Db::escape_int(self.id)), \" LIMIT 1\")));\n        if Db::step(stmt) { { self.id = Db::column_int(stmt, 0_i64);\n        self.body = Db::column_text(stmt, 1_i64).to_string();\n        self.created_at = Db::column_text(stmt, 2_i64).to_string();\n        self.title = Db::column_text(stmt, 3_i64).to_string();\n        self.updated_at = Db::column_text(stmt, 4_i64).to_string();\n        self.mark_persisted_bang() } };\n        Db::finalize(stmt);\n        self.clone()\n    }\n\n    pub fn from_params(p: ArticleParams) -> Article {\n        let mut instance = Article::new(std::collections::HashMap::new());\n        instance.set_title(&(p.title()));\n        instance.set_body(&(p.body()));\n        instance.clone()\n    }\n\n    pub fn validate(&self) {\n        if false || self.title.is_empty() { crate::errors_ext::validation_errors_push((\"Title can't be blank\").to_string()) };\n        if false || self.body.is_empty() { crate::errors_ext::validation_errors_push((\"Body can't be blank\").to_string()) };\n        if !(false) { { let mut len = self.body.len() as i64;\n        if len < 10_i64 { crate::errors_ext::validation_errors_push((\"Body is too short (minimum is 10 characters)\").to_string()) }; } };\n    }\n\n    pub fn comments(&self) -> Vec<Comment> {\n        if self.comments_loaded { return self.comments_cache.clone() };\n        let stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments\", \" WHERE \"), \"article_id = \"), Db::escape_int(self.id))));\n        let mut results = vec![];\n        while Db::step(stmt) {\n            results.push(Comment::from_stmt(stmt))\n        };\n        Db::finalize(stmt);\n        results.clone()\n    }\n\n    pub fn _preload_comments(&mut self, list: Vec<Comment>) {\n        self.comments_cache = list;\n        self.comments_loaded = true;\n    }\n\n    pub fn before_destroy(&self) {\n        self.comments().iter_mut().for_each(|c| { c.destroy(); });\n    }\n\n    pub fn dom_prefix() -> String {\n        (\"article\").to_string()\n    }\n\n    pub fn after_create_commit(&self) {\n        Broadcasts::prepend(std::collections::HashMap::from([(\"stream\", (\"articles\").to_string()), (\"target\", (\"articles\").to_string()), (\"html\", Articles::article(self.clone(), None, None))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    }\n\n    pub fn after_update_commit(&self) {\n        Broadcasts::replace(std::collections::HashMap::from([(\"stream\", (\"articles\").to_string()), (\"target\", format!(\"article_{}\", self.id)), (\"html\", Articles::article(self.clone(), None, None))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    }\n\n    pub fn after_destroy_commit(&self) {\n        Broadcasts::remove(std::collections::HashMap::from([(\"stream\", (\"articles\").to_string()), (\"target\", format!(\"article_{}\", self.id))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    }\n}\n\nimpl Article {\npub fn mark_persisted_bang(&mut self) { }\npub fn errors(&self) -> Vec<String> { crate::errors_ext::validation_errors_snapshot() }\npub fn save(&mut self) -> bool {\ncrate::errors_ext::validation_errors_clear();\nself.validate();\nif !crate::errors_ext::validation_errors_is_empty() { return false; }\nif self.id == 0 { self.id = self._adapter_insert(); self.after_create_commit(); }\nelse if Self::_adapter_exists_by_id(self.id) { self._adapter_update(); self.after_update_commit(); }\nelse { let _ = self._adapter_insert(); self.after_create_commit(); }\ntrue\n}\npub fn destroy(&mut self) { self.before_destroy(); self._adapter_delete(); self.after_destroy_commit(); }\npub fn exists(id: i64) -> bool { Self::_adapter_exists_by_id(id) }\npub fn persisted(&self) -> bool { self.id != 0 }\npub fn find(id: i64) -> Self { Self::_adapter_find_by_id(id).expect(\"record not found\") }\npub fn count() -> i64 { Self::_adapter_count() }\npub fn all() -> Vec<Article> { Self::_adapter_all() }\npub fn last() -> Option<Article> { Self::_adapter_all().last().cloned() }\npub fn reload(&mut self) { let _ = self._adapter_reload(); }\npub fn create(attrs: std::collections::HashMap<String, serde_json::Value>) -> Article { let mut m = Self::new(attrs); m.save(); m }\n}\n"},{"path":"src/models/article_params.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n#[derive(Clone, Default)]\npub struct ArticleParams {\n    pub title: String,\n    pub body: String,\n}\n\nimpl ArticleParams {\n    pub fn new() -> Self {\n        let mut title: String = (\"\").to_string();\n        let mut body: String = (\"\").to_string();\n        Self { title, body }\n    }\n\n    pub fn title(&self) -> String {\n        self.title.clone()\n    }\n\n    pub fn set_title(&mut self, value: &str) {\n        self.title = (value).to_string()\n    }\n\n    pub fn body(&self) -> String {\n        self.body.clone()\n    }\n\n    pub fn set_body(&mut self, value: &str) {\n        self.body = (value).to_string()\n    }\n\n    pub fn from_raw(params: std::collections::HashMap<String, ParamValue>) -> ArticleParams {\n        let mut raw_sub = params.get(\"article\").cloned().unwrap_or(serde_json::Value::Object(serde_json::Map::new()));\n        let mut sub = if raw_sub.clone().is_object() { raw_sub.clone().as_object().cloned().unwrap_or_default().into_iter().collect::<std::collections::HashMap<String, serde_json::Value>>() } else { std::collections::HashMap::new() };\n        let mut instance = ArticleParams::new();\n        let mut raw_title = sub.clone().get(\"title\").cloned().unwrap_or(serde_json::Value::from(\"\"));\n        instance.set_title(if raw_title.clone().is_string() { raw_title.as_str().unwrap() } else { \"\" });\n        let mut raw_body = sub.clone().get(\"body\").cloned().unwrap_or(serde_json::Value::from(\"\"));\n        instance.set_body(if raw_body.clone().is_string() { raw_body.as_str().unwrap() } else { \"\" });\n        instance.clone()\n    }\n\n    pub fn to_h(&self) -> std::collections::HashMap<String, String> {\n        std::collections::HashMap::from([(\"title\".to_string(), self.title.clone()), (\"body\".to_string(), self.body.clone())])\n    }\n}\n"},{"path":"src/models/article_row.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n#[derive(Clone, Default)]\npub struct ArticleRow {\n    pub id: i64,\n    pub body: String,\n    pub created_at: String,\n    pub title: String,\n    pub updated_at: String,\n}\n\nimpl ArticleRow {\n    pub fn id(&self) -> i64 {\n        self.id\n    }\n\n    pub fn set_id(&mut self, value: i64) {\n        self.id = value\n    }\n\n    pub fn body(&self) -> String {\n        self.body.clone()\n    }\n\n    pub fn set_body(&mut self, value: &str) {\n        self.body = (value).to_string()\n    }\n\n    pub fn created_at(&self) -> String {\n        self.created_at.clone()\n    }\n\n    pub fn set_created_at(&mut self, value: &str) {\n        self.created_at = (value).to_string()\n    }\n\n    pub fn title(&self) -> String {\n        self.title.clone()\n    }\n\n    pub fn set_title(&mut self, value: &str) {\n        self.title = (value).to_string()\n    }\n\n    pub fn updated_at(&self) -> String {\n        self.updated_at.clone()\n    }\n\n    pub fn set_updated_at(&mut self, value: &str) {\n        self.updated_at = (value).to_string()\n    }\n\n    pub fn new() -> Self {\n        let mut id: i64 = 0_i64;\n        let mut body: String = (\"\").to_string();\n        let mut created_at: String = (\"\").to_string();\n        let mut title: String = (\"\").to_string();\n        let mut updated_at: String = (\"\").to_string();\n        Self { id, body, created_at, title, updated_at }\n    }\n\n    pub fn from_raw(row: std::collections::HashMap<String, serde_json::Value>) -> ArticleRow {\n        let mut instance = ArticleRow::new();\n        instance.set_id((row.clone().get(\"id\").cloned().unwrap_or(serde_json::Value::from(0_i64))).as_i64().unwrap());\n        instance.set_body((row.clone()[\"body\"]).as_str().unwrap());\n        instance.set_created_at((row.clone()[\"created_at\"]).as_str().unwrap());\n        instance.set_title((row.clone()[\"title\"]).as_str().unwrap());\n        instance.set_updated_at((row.clone()[\"updated_at\"]).as_str().unwrap());\n        instance.clone()\n    }\n}\n"},{"path":"src/models/comment.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n#[derive(Clone, Default)]\npub struct Comment {\n    pub id: i64,\n    pub article_id: i64,\n    pub body: String,\n    pub commenter: String,\n    pub created_at: String,\n    pub updated_at: String,\n}\n\nimpl Comment {\n    pub fn id(&self) -> i64 {\n        self.id\n    }\n\n    pub fn set_id(&mut self, value: i64) {\n        self.id = value\n    }\n\n    pub fn article_id(&self) -> i64 {\n        self.article_id\n    }\n\n    pub fn set_article_id(&mut self, value: i64) {\n        self.article_id = value\n    }\n\n    pub fn body(&self) -> String {\n        self.body.clone()\n    }\n\n    pub fn set_body(&mut self, value: &str) {\n        self.body = (value).to_string()\n    }\n\n    pub fn commenter(&self) -> String {\n        self.commenter.clone()\n    }\n\n    pub fn set_commenter(&mut self, value: &str) {\n        self.commenter = (value).to_string()\n    }\n\n    pub fn created_at(&self) -> String {\n        self.created_at.clone()\n    }\n\n    pub fn set_created_at(&mut self, value: &str) {\n        self.created_at = (value).to_string()\n    }\n\n    pub fn updated_at(&self) -> String {\n        self.updated_at.clone()\n    }\n\n    pub fn set_updated_at(&mut self, value: &str) {\n        self.updated_at = (value).to_string()\n    }\n\n    pub fn table_name() -> String {\n        (\"comments\").to_string()\n    }\n\n    pub fn schema_columns() -> Vec<String> {\n        vec![\"id\".to_string(), \"article_id\".to_string(), \"body\".to_string(), \"commenter\".to_string(), \"created_at\".to_string(), \"updated_at\".to_string()]\n    }\n\n    pub fn instantiate(row: std::collections::HashMap<String, serde_json::Value>) -> Comment {\n        let mut instance = Comment::from_row(CommentRow::from_raw(row.clone()));\n        instance.mark_persisted_bang();\n        instance.clone()\n    }\n\n    pub fn from_row(row: CommentRow) -> Comment {\n        let mut instance = Comment::new(std::collections::HashMap::new());\n        instance.set_id(row.id());\n        instance.set_article_id(row.article_id());\n        instance.set_body(&(row.body()));\n        instance.set_commenter(&(row.commenter()));\n        instance.set_created_at(&(row.created_at()));\n        instance.set_updated_at(&(row.updated_at()));\n        instance.clone()\n    }\n\n    pub fn from_stmt(stmt: i64) -> Comment {\n        let mut instance = Comment::new(std::collections::HashMap::new());\n        instance.set_id(Db::column_int(stmt, 0_i64));\n        instance.set_article_id(Db::column_int(stmt, 1_i64));\n        instance.set_body(&(Db::column_text(stmt, 2_i64)));\n        instance.set_commenter(&(Db::column_text(stmt, 3_i64)));\n        instance.set_created_at(&(Db::column_text(stmt, 4_i64)));\n        instance.set_updated_at(&(Db::column_text(stmt, 5_i64)));\n        instance.mark_persisted_bang();\n        instance.clone()\n    }\n\n    pub fn assign_from_row(&mut self, row: std::collections::HashMap<String, serde_json::Value>) {\n        self.set_id((row.clone()[\"id\"]).as_i64().unwrap());\n        self.set_article_id((row.clone()[\"article_id\"]).as_i64().unwrap());\n        self.set_body((row.clone()[\"body\"]).as_str().unwrap());\n        self.set_commenter((row.clone()[\"commenter\"]).as_str().unwrap());\n        self.set_created_at((row.clone()[\"created_at\"]).as_str().unwrap());\n        self.set_updated_at((row.clone()[\"updated_at\"]).as_str().unwrap());\n    }\n\n    pub fn new(attrs: std::collections::HashMap<String, serde_json::Value>) -> Self {\n        /* TODO rust2: ExprNode::Discriminant(22) */;\n        let id = (attrs.clone().get(\"id\").cloned().unwrap_or(serde_json::Value::from(0_i64))).as_i64().unwrap();\n        let article_id = (attrs.clone().get(\"article_id\").cloned().unwrap_or(serde_json::Value::from(0_i64))).as_i64().unwrap();\n        let body = (attrs.clone().get(\"body\").cloned().unwrap_or(serde_json::Value::from(\"\"))).as_str().unwrap().to_string();\n        let commenter = (attrs.clone().get(\"commenter\").cloned().unwrap_or(serde_json::Value::from(\"\"))).as_str().unwrap().to_string();\n        let created_at = (attrs.clone().get(\"created_at\").cloned().unwrap_or(serde_json::Value::from(\"\"))).as_str().unwrap().to_string();\n        let updated_at = (attrs.clone().get(\"updated_at\").cloned().unwrap_or(serde_json::Value::from(\"\"))).as_str().unwrap().to_string();\n        Self { id, article_id, body, commenter, created_at, updated_at }\n    }\n\n    pub fn attributes(&self) -> std::collections::HashMap<String, serde_json::Value> {\n        std::collections::HashMap::from([(\"article_id\".to_string(), serde_json::Value::from(self.article_id)), (\"body\".to_string(), serde_json::Value::from(self.body.clone())), (\"commenter\".to_string(), serde_json::Value::from(self.commenter.clone())), (\"created_at\".to_string(), serde_json::Value::from(self.created_at.clone())), (\"updated_at\".to_string(), serde_json::Value::from(self.updated_at.clone()))])\n    }\n\n    pub fn get_index(&self, name: &str) -> serde_json::Value {\n        match name {\n                \"id\" => { serde_json::Value::from(self.id) },\n                \"article_id\" => { serde_json::Value::from(self.article_id) },\n                \"body\" => { serde_json::Value::from(self.body.clone()) },\n                \"commenter\" => { serde_json::Value::from(self.commenter.clone()) },\n                \"created_at\" => { serde_json::Value::from(self.created_at.clone()) },\n                \"updated_at\" => { serde_json::Value::from(self.updated_at.clone()) }\n                _ => serde_json::Value::Null,\n            }\n    }\n\n    pub fn set_index(&mut self, name: &str, value: serde_json::Value) {\n        match name {\n                \"id\" => { self.id = (value.clone()).as_i64().unwrap() },\n                \"article_id\" => { self.article_id = (value.clone()).as_i64().unwrap() },\n                \"body\" => { self.body = (value.clone()).as_str().unwrap().to_string().to_string() },\n                \"commenter\" => { self.commenter = (value.clone()).as_str().unwrap().to_string().to_string() },\n                \"created_at\" => { self.created_at = (value.clone()).as_str().unwrap().to_string().to_string() },\n                \"updated_at\" => { self.updated_at = (value.clone()).as_str().unwrap().to_string().to_string() }\n                _ => (),\n            }\n    }\n\n    pub fn update(&mut self, p: CommentParams) -> bool {\n        if !({ let _ = p.commenter(); false }) { self.set_commenter(&(p.commenter())) };\n        if !({ let _ = p.body(); false }) { self.set_body(&(p.body())) };\n        self.save()\n    }\n\n    pub fn _adapter_find_by_id(id: i64) -> Option<Comment> {\n        let stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments\", \" WHERE \"), \"id = \"), Db::escape_int(id)), \" LIMIT 1\")));\n        let mut result: Option<Comment> = None;\n        if Db::step(stmt) { result = Some(Comment::from_stmt(stmt)) };\n        Db::finalize(stmt);\n        result\n    }\n\n    pub fn _adapter_all() -> Vec<Comment> {\n        let stmt = Db::prepare(\"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments\");\n        let mut results = vec![];\n        while Db::step(stmt) {\n            results.push(Comment::from_stmt(stmt))\n        };\n        Db::finalize(stmt);\n        results.clone()\n    }\n\n    pub fn _adapter_insert(&self) -> i64 {\n        Db::exec(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"INSERT INTO comments (article_id, body, commenter, created_at, updated_at) VALUES (\", Db::escape_int(self.article_id)), \", \"), Db::escape_string(&(self.body))), \", \"), Db::escape_string(&(self.commenter))), \", \"), Db::escape_string(&(self.created_at))), \", \"), Db::escape_string(&(self.updated_at))), \")\")));\n        Db::last_insert_rowid()\n    }\n\n    pub fn _adapter_update(&self) {\n        Db::exec(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"UPDATE comments SET \", \"article_id = \"), Db::escape_int(self.article_id)), \", body = \"), Db::escape_string(&(self.body))), \", commenter = \"), Db::escape_string(&(self.commenter))), \", created_at = \"), Db::escape_string(&(self.created_at))), \", updated_at = \"), Db::escape_string(&(self.updated_at))), \" WHERE \"), \"id = \"), Db::escape_int(self.id))));\n    }\n\n    pub fn _adapter_delete(&self) {\n        Db::exec(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"DELETE FROM comments\", \" WHERE \"), \"id = \"), Db::escape_int(self.id))));\n    }\n\n    pub fn _adapter_count() -> i64 {\n        let stmt = Db::prepare(\"SELECT COUNT(*) FROM comments\");\n        Db::step(stmt);\n        let result = Db::column_int(stmt, 0_i64);\n        Db::finalize(stmt);\n        result\n    }\n\n    pub fn _adapter_exists_by_id(id: i64) -> bool {\n        let stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"SELECT 1 FROM comments\", \" WHERE \"), \"id = \"), Db::escape_int(id)), \" LIMIT 1\")));\n        let result = Db::step(stmt);\n        Db::finalize(stmt);\n        result\n    }\n\n    pub fn _adapter_truncate() {\n        Db::exec(\"DELETE FROM comments\");\n        Db::exec(\"DELETE FROM sqlite_sequence WHERE name = 'comments'\");\n    }\n\n    pub fn _adapter_reload(&mut self) -> Comment {\n        let stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", \"SELECT id, article_id, body, commenter, created_at, updated_at FROM comments WHERE id = \", Db::escape_int(self.id)), \" LIMIT 1\")));\n        if Db::step(stmt) { { self.id = Db::column_int(stmt, 0_i64);\n        self.article_id = Db::column_int(stmt, 1_i64);\n        self.body = Db::column_text(stmt, 2_i64).to_string();\n        self.commenter = Db::column_text(stmt, 3_i64).to_string();\n        self.created_at = Db::column_text(stmt, 4_i64).to_string();\n        self.updated_at = Db::column_text(stmt, 5_i64).to_string();\n        self.mark_persisted_bang() } };\n        Db::finalize(stmt);\n        self.clone()\n    }\n\n    pub fn from_params(p: CommentParams) -> Comment {\n        let mut instance = Comment::new(std::collections::HashMap::new());\n        instance.set_commenter(&(p.commenter()));\n        instance.set_body(&(p.body()));\n        instance.clone()\n    }\n\n    pub fn validate(&self) {\n        if false || self.commenter.is_empty() { crate::errors_ext::validation_errors_push((\"Commenter can't be blank\").to_string()) };\n        if false || self.body.is_empty() { crate::errors_ext::validation_errors_push((\"Body can't be blank\").to_string()) };\n        if false || self.article_id == 0_i64 || !(Article::exists(self.article_id)) { crate::errors_ext::validation_errors_push((\"Article must exist\").to_string()) };\n    }\n\n    pub fn article(&self) -> Option<Article> {\n        if !(self.article_id == 0_i64) { { let stmt = Db::prepare(&(format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"SELECT id, body, created_at, title, updated_at FROM articles\", \" WHERE \"), \"id = \"), Db::escape_int(self.article_id)), \" LIMIT 1\")));\n        let mut result: Option<Article> = None;\n        if Db::step(stmt) { result = Some(Article::from_stmt(stmt)) };\n        Db::finalize(stmt);\n        result } } else { None }\n    }\n\n    pub fn dom_prefix() -> String {\n        (\"comment\").to_string()\n    }\n\n    pub fn after_create_commit(&self) {\n        Broadcasts::append(std::collections::HashMap::from([(\"stream\", format!(\"article_{}_comments\", self.article_id)), (\"target\", (\"comments\").to_string()), (\"html\", Comments::comment(self.clone(), None, None))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n        let Some(parent) = self.article() else { return };\n        Broadcasts::replace(std::collections::HashMap::from([(\"stream\", (\"articles\").to_string()), (\"target\", format!(\"article_{}\", parent.id())), (\"html\", Articles::article(parent.clone().clone(), None, None))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    }\n\n    pub fn after_update_commit(&self) {\n        Broadcasts::replace(std::collections::HashMap::from([(\"stream\", format!(\"article_{}_comments\", self.article_id)), (\"target\", format!(\"comment_{}\", self.id)), (\"html\", Comments::comment(self.clone(), None, None))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    }\n\n    pub fn after_destroy_commit(&self) {\n        Broadcasts::remove(std::collections::HashMap::from([(\"stream\", format!(\"article_{}_comments\", self.article_id)), (\"target\", format!(\"comment_{}\", self.id))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n        let Some(parent) = self.article() else { return };\n        Broadcasts::replace(std::collections::HashMap::from([(\"stream\", (\"articles\").to_string()), (\"target\", format!(\"article_{}\", parent.id())), (\"html\", Articles::article(parent.clone().clone(), None, None))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    }\n}\n\nimpl Comment {\npub fn mark_persisted_bang(&mut self) { }\npub fn errors(&self) -> Vec<String> { crate::errors_ext::validation_errors_snapshot() }\npub fn save(&mut self) -> bool {\ncrate::errors_ext::validation_errors_clear();\nself.validate();\nif !crate::errors_ext::validation_errors_is_empty() { return false; }\nif self.id == 0 { self.id = self._adapter_insert(); self.after_create_commit(); }\nelse if Self::_adapter_exists_by_id(self.id) { self._adapter_update(); self.after_update_commit(); }\nelse { let _ = self._adapter_insert(); self.after_create_commit(); }\ntrue\n}\npub fn destroy(&mut self) { self._adapter_delete(); self.after_destroy_commit(); }\npub fn exists(id: i64) -> bool { Self::_adapter_exists_by_id(id) }\npub fn persisted(&self) -> bool { self.id != 0 }\npub fn find(id: i64) -> Self { Self::_adapter_find_by_id(id).expect(\"record not found\") }\npub fn count() -> i64 { Self::_adapter_count() }\npub fn all() -> Vec<Comment> { Self::_adapter_all() }\npub fn last() -> Option<Comment> { Self::_adapter_all().last().cloned() }\npub fn reload(&mut self) { let _ = self._adapter_reload(); }\npub fn create(attrs: std::collections::HashMap<String, serde_json::Value>) -> Comment { let mut m = Self::new(attrs); m.save(); m }\n}\n"},{"path":"src/models/comment_params.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n#[derive(Clone, Default)]\npub struct CommentParams {\n    pub commenter: String,\n    pub body: String,\n}\n\nimpl CommentParams {\n    pub fn new() -> Self {\n        let mut commenter: String = (\"\").to_string();\n        let mut body: String = (\"\").to_string();\n        Self { commenter, body }\n    }\n\n    pub fn commenter(&self) -> String {\n        self.commenter.clone()\n    }\n\n    pub fn set_commenter(&mut self, value: &str) {\n        self.commenter = (value).to_string()\n    }\n\n    pub fn body(&self) -> String {\n        self.body.clone()\n    }\n\n    pub fn set_body(&mut self, value: &str) {\n        self.body = (value).to_string()\n    }\n\n    pub fn from_raw(params: std::collections::HashMap<String, ParamValue>) -> CommentParams {\n        let mut raw_sub = params.get(\"comment\").cloned().unwrap_or(serde_json::Value::Object(serde_json::Map::new()));\n        let mut sub = if raw_sub.clone().is_object() { raw_sub.clone().as_object().cloned().unwrap_or_default().into_iter().collect::<std::collections::HashMap<String, serde_json::Value>>() } else { std::collections::HashMap::new() };\n        let mut instance = CommentParams::new();\n        let mut raw_commenter = sub.clone().get(\"commenter\").cloned().unwrap_or(serde_json::Value::from(\"\"));\n        instance.set_commenter(if raw_commenter.clone().is_string() { raw_commenter.as_str().unwrap() } else { \"\" });\n        let mut raw_body = sub.clone().get(\"body\").cloned().unwrap_or(serde_json::Value::from(\"\"));\n        instance.set_body(if raw_body.clone().is_string() { raw_body.as_str().unwrap() } else { \"\" });\n        instance.clone()\n    }\n\n    pub fn to_h(&self) -> std::collections::HashMap<String, String> {\n        std::collections::HashMap::from([(\"commenter\".to_string(), self.commenter.clone()), (\"body\".to_string(), self.body.clone())])\n    }\n}\n"},{"path":"src/models/comment_row.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n#[derive(Clone, Default)]\npub struct CommentRow {\n    pub id: i64,\n    pub article_id: i64,\n    pub body: String,\n    pub commenter: String,\n    pub created_at: String,\n    pub updated_at: String,\n}\n\nimpl CommentRow {\n    pub fn id(&self) -> i64 {\n        self.id\n    }\n\n    pub fn set_id(&mut self, value: i64) {\n        self.id = value\n    }\n\n    pub fn article_id(&self) -> i64 {\n        self.article_id\n    }\n\n    pub fn set_article_id(&mut self, value: i64) {\n        self.article_id = value\n    }\n\n    pub fn body(&self) -> String {\n        self.body.clone()\n    }\n\n    pub fn set_body(&mut self, value: &str) {\n        self.body = (value).to_string()\n    }\n\n    pub fn commenter(&self) -> String {\n        self.commenter.clone()\n    }\n\n    pub fn set_commenter(&mut self, value: &str) {\n        self.commenter = (value).to_string()\n    }\n\n    pub fn created_at(&self) -> String {\n        self.created_at.clone()\n    }\n\n    pub fn set_created_at(&mut self, value: &str) {\n        self.created_at = (value).to_string()\n    }\n\n    pub fn updated_at(&self) -> String {\n        self.updated_at.clone()\n    }\n\n    pub fn set_updated_at(&mut self, value: &str) {\n        self.updated_at = (value).to_string()\n    }\n\n    pub fn new() -> Self {\n        let mut id: i64 = 0_i64;\n        let mut article_id: i64 = 0_i64;\n        let mut body: String = (\"\").to_string();\n        let mut commenter: String = (\"\").to_string();\n        let mut created_at: String = (\"\").to_string();\n        let mut updated_at: String = (\"\").to_string();\n        Self { id, article_id, body, commenter, created_at, updated_at }\n    }\n\n    pub fn from_raw(row: std::collections::HashMap<String, serde_json::Value>) -> CommentRow {\n        let mut instance = CommentRow::new();\n        instance.set_id((row.clone().get(\"id\").cloned().unwrap_or(serde_json::Value::from(0_i64))).as_i64().unwrap());\n        instance.set_article_id((row.clone().get(\"article_id\").cloned().unwrap_or(serde_json::Value::from(0_i64))).as_i64().unwrap());\n        instance.set_body((row.clone()[\"body\"]).as_str().unwrap());\n        instance.set_commenter((row.clone()[\"commenter\"]).as_str().unwrap());\n        instance.set_created_at((row.clone()[\"created_at\"]).as_str().unwrap());\n        instance.set_updated_at((row.clone()[\"updated_at\"]).as_str().unwrap());\n        instance.clone()\n    }\n}\n"},{"path":"src/models/mod.rs","content":"// Generated by Roundhouse (rust2).\n\npub mod application_record;\npub mod article;\npub mod article_params;\npub mod article_row;\npub mod comment;\npub mod comment_params;\npub mod comment_row;\npub use application_record::ApplicationRecord;\npub use article::Article;\npub use article_params::ArticleParams;\npub use article_row::ArticleRow;\npub use comment::Comment;\npub use comment_params::CommentParams;\npub use comment_row::CommentRow;\n"},{"path":"src/param_value.rs","content":"//! Recursive Rails-params value type — the rust analog of\n//! `runtime/typescript/param_value.ts` and `runtime/crystal/param_value.cr`.\n//!\n//! Rails request params shape as a recursive tree of String,\n//! Vec<ParamValue>, or HashMap<String, ParamValue>. The TS sibling\n//! declares it as a union of (string | string[] | { [k: string]:\n//! ParamValue }); Crystal as `alias ParamValue = String | Hash(String,\n//! ParamValue) | Array(ParamValue)`.\n//!\n//! In rust2 Phase 3 this is a re-export alias to `serde_json::Value`\n//! — same recursive shape, already familiar to every emit path that\n//! lowers `untyped`. Concrete enum can replace this later if the\n//! typed-value discipline gets tighter (Ty::Untyped reform).\n\npub type ParamValue = serde_json::Value;\n"},{"path":"src/route_helpers.rs","content":"pub struct RouteHelpers;\n\nimpl RouteHelpers {\n    pub fn root_path() -> String {\n        (\"/\").to_string()\n    }\n\n    pub fn articles_path() -> String {\n        (\"/articles\").to_string()\n    }\n\n    pub fn new_article_path() -> String {\n        (\"/articles/new\").to_string()\n    }\n\n    pub fn article_path(id: i64) -> String {\n        format!(\"/articles/{}\", id)\n    }\n\n    pub fn edit_article_path(id: i64) -> String {\n        format!(\"/articles/{}/edit\", id)\n    }\n\n    pub fn article_comments_path(article_id: i64) -> String {\n        format!(\"/articles/{}/comments\", article_id)\n    }\n\n    pub fn article_comment_path(article_id: i64, id: i64) -> String {\n        format!(\"/articles/{}/comments/{}\", article_id, id)\n    }\n}\n\n// Wedge 2c.3 bare-fn compat shims — delegate to `impl RouteHelpers`.\npub fn root_path() -> String { RouteHelpers::root_path() }\npub fn articles_path() -> String { RouteHelpers::articles_path() }\npub fn new_article_path() -> String { RouteHelpers::new_article_path() }\npub fn article_path(id: i64) -> String { RouteHelpers::article_path(id) }\npub fn edit_article_path(id: i64) -> String { RouteHelpers::edit_article_path(id) }\npub fn article_comments_path(article_id: i64) -> String { RouteHelpers::article_comments_path(article_id) }\npub fn article_comment_path(article_id: i64, id: i64) -> String { RouteHelpers::article_comment_path(article_id, id) }\n"},{"path":"src/router.rs","content":"// Generated from runtime/ruby/action_dispatch/router.rb at app emit time.\n// Do not edit by hand — edit the source `.rb` and re-run emit.\n\n#[derive(Clone, Default)]\npub struct Route {\n    pub verb: String,\n    pub pattern: String,\n    pub controller: String,\n    pub action: String,\n}\n\nimpl Route {\n    pub fn verb(&self) -> String {\n        self.verb.clone()\n    }\n\n    pub fn pattern(&self) -> String {\n        self.pattern.clone()\n    }\n\n    pub fn controller(&self) -> String {\n        self.controller.clone()\n    }\n\n    pub fn action(&self) -> String {\n        self.action.clone()\n    }\n\n    pub fn new(verb: &str, pattern: &str, controller: &str, action: &str) -> Self {\n        let mut verb: String = (verb).to_string();\n        let mut pattern: String = (pattern).to_string();\n        let mut controller: String = (controller).to_string();\n        let mut action: String = (action).to_string();\n        Self { verb, pattern, controller, action }\n    }\n}\n\n#[derive(Clone, Default)]\npub struct MatchResult {\n    pub controller: String,\n    pub action: String,\n    pub path_params: std::collections::HashMap<String, String>,\n}\n\nimpl MatchResult {\n    pub fn controller(&self) -> String {\n        self.controller.clone()\n    }\n\n    pub fn action(&self) -> String {\n        self.action.clone()\n    }\n\n    pub fn path_params(&self) -> std::collections::HashMap<String, String> {\n        self.path_params.clone()\n    }\n\n    pub fn new(controller: &str, action: &str, path_params: std::collections::HashMap<String, String>) -> Self {\n        let mut controller: String = (controller).to_string();\n        let mut action: String = (action).to_string();\n        let mut path_params: std::collections::HashMap<String, String> = path_params;\n        Self { controller, action, path_params }\n    }\n}\n\npub struct Router;\n\nimpl Router {\n    pub fn r#match(method: &str, path: &str, table: Vec<Route>) -> Option<MatchResult> {\n        let method_upcase = method.to_string().to_uppercase();\n        let mut i = 0_i64;\n        while i < table.len() as i64 {\n            let mut route = table.clone()[(i) as usize].clone();\n            if route.verb().to_string() == method_upcase { { let mut params = Self::match_pattern(&(route.pattern().to_string()), &(path));\n            if !(params.is_none()) { return Some(MatchResult::new(&(route.controller()), &(route.action()), params.clone().unwrap())) } } };\n            /* TODO rust2: ExprNode::Discriminant(16) */\n        };\n        None\n    }\n\n    pub fn match_pattern(pattern: &str, path: &str) -> Option<std::collections::HashMap<String, String>> {\n        let mut pattern_parts = pattern.split(\"/\").collect::<Vec<&str>>();\n        let mut path_parts = path.split(\"/\").collect::<Vec<&str>>();\n        if (pattern_parts.len() as i64) != path_parts.len() as i64 { return None };\n        let mut params: std::collections::HashMap<String, String> = std::collections::HashMap::new();\n        let mut i = 0_i64;\n        while i < pattern_parts.len() as i64 {\n            let mut pp = pattern_parts.clone()[(i) as usize].clone();\n            let ap = path_parts.clone()[(i) as usize].clone();\n            if pp.starts_with(\":\") { { params.clone().insert((&pp.clone()[(1_i64) as usize..]).to_string(), (ap.clone()).to_string()); } } else { if pp.clone() != ap.clone() { return None } };\n            /* TODO rust2: ExprNode::Discriminant(16) */\n        };\n        Some(params.clone())\n    }\n}\n\n// rust2 wedge 2c.2: concrete axum router.\n#[allow(dead_code)]\npub fn router() -> axum::Router {\n    axum::Router::new()\n        .route(\"/\", axum::routing::get(crate::controllers::articles_controller::_axum_index))\n        .route(\"/.json\", axum::routing::get(crate::controllers::articles_controller::_axum_index))\n        .route(\"/articles\", axum::routing::get(crate::controllers::articles_controller::_axum_index).post(crate::controllers::articles_controller::_axum_create))\n        .route(\"/articles.json\", axum::routing::get(crate::controllers::articles_controller::_axum_index).post(crate::controllers::articles_controller::_axum_create))\n        .route(\"/articles/new\", axum::routing::get(crate::controllers::articles_controller::_axum_new))\n        .route(\"/articles/new.json\", axum::routing::get(crate::controllers::articles_controller::_axum_new))\n        .route(\"/articles/{article_id}/comments\", axum::routing::post(crate::controllers::comments_controller::_axum_create))\n        .route(\"/articles/{article_id}/comments/{id}\", axum::routing::delete(crate::controllers::comments_controller::_axum_destroy))\n        .route(\"/articles/{id}\", axum::routing::get(crate::controllers::articles_controller::_axum_show).patch(crate::controllers::articles_controller::_axum_update).delete(crate::controllers::articles_controller::_axum_destroy))\n        .route(\"/articles/{id}/edit\", axum::routing::get(crate::controllers::articles_controller::_axum_edit))\n        .layer(axum::middleware::from_fn(crate::http::request_format_middleware))\n}\n"},{"path":"src/schema_sql.rs","content":"// Generated by Roundhouse (rust2).\n\npub const CREATE_TABLES: &str = r#\"\nCREATE TABLE IF NOT EXISTS articles (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  body TEXT,\n  created_at TEXT NOT NULL,\n  title TEXT,\n  updated_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS comments (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  article_id INTEGER NOT NULL,\n  body TEXT,\n  commenter TEXT,\n  created_at TEXT NOT NULL,\n  updated_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS index_comments_on_article_id ON comments (article_id);\n\"#;\n"},{"path":"src/server.rs","content":"//! Roundhouse Rust server runtime.\n//!\n//! Hand-written, shipped alongside generated code (copied in by the\n//! Rust emitter as `src/server.rs`). The emitted `main.rs` calls\n//! `start(router, opts)` to open the production DB, apply\n//! schema, install middleware, and run axum.\n//!\n//! Middleware stack (outer → inner):\n//!   - `layout_wrap` — wraps HTML responses in the full document\n//!     shell (Tailwind + importmap + Action Cable meta). Mirrors\n//!     the TS runtime's `renderLayout`.\n//!   - `method_override` — Rails forms POST `_method=patch|\n//!     put|delete`; we read the form body, rewrite the request\n//!     method, and re-inject the body so downstream `axum::Form`\n//!     extractors still work.\n//!\n//! `start` also mounts `GET /cable` onto the axum router, handing\n//! the upgrade off to `crate::cable::cable_handler`. The route is\n//! always registered — apps that don't use Turbo Streams simply\n//! never receive a client connection, and the handler is cheap\n//! (one OnceLock hashmap check on subscribe).\n\nuse std::net::SocketAddr;\n\nuse axum::{\n    body::Body,\n    extract::Request,\n    http::{header, HeaderValue, Method, StatusCode},\n    middleware::{self, Next},\n    response::Response,\n    routing::get,\n    Router,\n};\nuse tower_http::services::ServeDir;\n\nuse crate::cable;\nuse crate::db;\nuse crate::view_helpers;\n\npub struct StartOptions<'a> {\n    /// File path for the sqlite DB. Defaults to\n    /// `./storage/development.sqlite3`.\n    pub db_path: Option<String>,\n    /// Listener port. Defaults to 3000 or `PORT` env var.\n    pub port: Option<u16>,\n    /// Schema SQL to apply on startup — typically\n    /// `crate::schema_sql::CREATE_TABLES`.\n    pub schema_sql: &'a str,\n    /// Layout renderer — the emitted `render_layouts_application`\n    /// (or equivalent). Called after each non-redirect response\n    /// with the inner view body already stashed via\n    /// `view_helpers::set_yield`. When `None`, the layout-wrap\n    /// middleware falls back to the minimal synthesized shell\n    /// below. Applies to apps that don't emit a layouts/\n    /// application ERB template (e.g. tiny-blog).\n    pub layout: Option<fn() -> String>,\n}\n\n/// Process-wide layout renderer, set by `start`. Read by the\n/// `layout_wrap` middleware. Axum middleware fns can't capture\n/// runtime state cleanly without boxing + extensions, so we use\n/// a static slot — the server runs one app per process.\nstatic LAYOUT_FN: std::sync::OnceLock<fn() -> String> = std::sync::OnceLock::new();\n\n/// Start the server. Opens DB, applies schema, layers middleware\n/// on top of the caller-supplied router, and runs axum until the\n/// process exits.\npub async fn start(router: Router, opts: StartOptions<'_>) {\n    let db_path = opts\n        .db_path\n        .unwrap_or_else(|| \"storage/development.sqlite3\".to_string());\n    let port: u16 = opts.port.unwrap_or_else(|| {\n        std::env::var(\"PORT\")\n            .ok()\n            .and_then(|s| s.parse().ok())\n            .unwrap_or(3000)\n    });\n\n    if let Some(layout) = opts.layout {\n        let _ = LAYOUT_FN.set(layout);\n    }\n\n    db::open_production_db(&db_path, opts.schema_sql);\n\n    // Static assets: serve `static/assets/<name>` for `/assets/*`\n    // requests via tower-http's ServeDir. Mirrors Rails' Propshaft URL\n    // shape — the importmap pins and `stylesheet_link_tag(\"tailwind\")`\n    // both point at /assets/<name>. `bin/rh transpile rust` writes the\n    // actual files (Tailwind compile output, turbo.min.js copy) into\n    // `static/assets/`. ServeDir returns 404 on miss without consuming\n    // the request, so other routes still resolve normally.\n    let app = router\n        .nest_service(\"/assets\", ServeDir::new(\"static/assets\"))\n        .route(\"/cable\", get(cable::cable_handler))\n        .layer(middleware::from_fn(layout_wrap));\n    // `method_override` must run BEFORE path+method routing. Applied as a\n    // `Router::layer` it would wrap the *matched* per-route service — but\n    // axum decides a POST to a delete-only path is a 405 during routing,\n    // so the layer (which does run) is already bound to the 405 service\n    // and rewriting the verb there can't re-route (the e2e turbo/cable\n    // DELETE cleanup 405s). Wrapping the whole router as the fallback of\n    // an outer, route-less Router puts the override ahead of all real\n    // routing, so `next.run()` re-enters routing with the corrected verb.\n    let app = Router::new()\n        .fallback_service(app)\n        .layer(middleware::from_fn(method_override));\n\n    let addr: SocketAddr = ([127, 0, 0, 1], port).into();\n    let listener = tokio::net::TcpListener::bind(addr)\n        .await\n        .expect(\"bind listener\");\n    println!(\"Roundhouse server listening on http://localhost:{}\", port);\n    axum::serve(listener, app).await.expect(\"axum serve\");\n}\n\n// ── method override middleware ─────────────────────────────────\n\n/// Rails scaffold forms submit as POST with a hidden `_method`\n/// field when the real verb is PATCH / PUT / DELETE (browsers\n/// don't natively support those in form elements). We consume the\n/// form body, check for `_method`, rewrite the request method, and\n/// re-inject the buffered body so the downstream `Form` extractor\n/// still reads the params.\nasync fn method_override(req: Request, next: Next) -> Response {\n    if req.method() != Method::POST {\n        return next.run(req).await;\n    }\n    let content_type = req\n        .headers()\n        .get(header::CONTENT_TYPE)\n        .and_then(|v| v.to_str().ok())\n        .unwrap_or(\"\")\n        .to_string();\n    if !content_type.starts_with(\"application/x-www-form-urlencoded\") {\n        return next.run(req).await;\n    }\n\n    let (mut parts, body) = req.into_parts();\n    let bytes = match axum::body::to_bytes(body, 16 * 1024 * 1024).await {\n        Ok(b) => b,\n        Err(_) => {\n            return Response::builder()\n                .status(StatusCode::BAD_REQUEST)\n                .body(Body::from(\"body too large\"))\n                .unwrap();\n        }\n    };\n\n    // Scan for `_method=<verb>` in the urlencoded body. We only\n    // look for the first hit — scaffold forms emit a single\n    // _method field; more than that would be a bug upstream.\n    let body_str = std::str::from_utf8(&bytes).unwrap_or(\"\");\n    let mut override_verb: Option<Method> = None;\n    for pair in body_str.split('&') {\n        let (k, v) = match pair.split_once('=') {\n            Some(kv) => kv,\n            None => continue,\n        };\n        if k == \"_method\" {\n            let upper = v.to_ascii_uppercase();\n            override_verb = match upper.as_str() {\n                \"PATCH\" => Some(Method::PATCH),\n                \"PUT\" => Some(Method::PUT),\n                \"DELETE\" => Some(Method::DELETE),\n                _ => None,\n            };\n            break;\n        }\n    }\n    if let Some(m) = override_verb {\n        parts.method = m;\n    }\n\n    let new_req = Request::from_parts(parts, Body::from(bytes));\n    next.run(new_req).await\n}\n\n// ── layout wrap middleware ─────────────────────────────────────\n\n/// Wrap HTML-typed response bodies in the document shell. Only\n/// touches 2xx + 422 responses with `text/html` content type;\n/// redirects pass through untouched, as do non-HTML responses (the\n/// WebSocket upgrade, any JSON endpoints).\nasync fn layout_wrap(req: Request, next: Next) -> Response {\n    // Wipe any stale yield/slot state before the handler runs.\n    // Axum's multi-thread runtime means each worker thread has\n    // its own thread-local; reset covers the current worker.\n    view_helpers::reset_render_state();\n\n    let res = next.run(req).await;\n\n    let status = res.status();\n    let is_html = res\n        .headers()\n        .get(header::CONTENT_TYPE)\n        .and_then(|v| v.to_str().ok())\n        .map(|ct| ct.starts_with(\"text/html\"))\n        .unwrap_or(false);\n\n    // Pass through non-HTML + redirects.\n    if !is_html {\n        return res;\n    }\n    if status.is_redirection() {\n        return res;\n    }\n\n    let (mut parts, body) = res.into_parts();\n    let bytes = match axum::body::to_bytes(body, 16 * 1024 * 1024).await {\n        Ok(b) => b,\n        Err(_) => {\n            return Response::builder()\n                .status(StatusCode::INTERNAL_SERVER_ERROR)\n                .body(Body::from(\"response body too large\"))\n                .unwrap();\n        }\n    };\n    let inner = std::str::from_utf8(&bytes)\n        .map(str::to_string)\n        .unwrap_or_default();\n\n    // If the app has an emitted layout (`opts.layout` was set in\n    // `start`), stash the inner body for `<%= yield %>` and invoke\n    // the layout. Otherwise fall back to the minimal synthesized\n    // shell so apps without an ERB layout still render.\n    let wrapped = if let Some(layout) = LAYOUT_FN.get() {\n        view_helpers::set_yield(&inner);\n        layout()\n    } else {\n        render_layout(&inner)\n    };\n\n    parts.headers.remove(header::CONTENT_LENGTH);\n    parts.headers.insert(\n        header::CONTENT_TYPE,\n        HeaderValue::from_static(\"text/html; charset=utf-8\"),\n    );\n    Response::from_parts(parts, Body::from(wrapped))\n}\n\n/// The document shell. Asset paths point at `/assets/tailwind.css`\n/// + `/assets/turbo.min.js`, served by `tower-http`'s `ServeDir`\n/// mounted on `/assets` (see `start`). `bin/rh transpile rust` is\n/// expected to have populated `static/assets/` with the Tailwind\n/// compile output + a copy of turbo.min.js; without that step the\n/// page is unstyled but functional. Plain `@hotwired/turbo` (not\n/// `@hotwired/turbo-rails`) avoids the latter's transitive\n/// `@rails/actioncable/src` lookup, which would 404 in the browser\n/// — our cable handler at `/cable` matches turbo's default URL.\n/// Inline data-URI favicon suppresses the no-icon 404 on each\n/// page load. Used only as a fallback when no emitter layout is\n/// supplied via `StartOptions::layout`; the emitted Layouts\n/// module overrides this for apps that have `app/views/layouts/\n/// application.{erb,rb}`.\nfn render_layout(body: &str) -> String {\n    format!(\n        r##\"<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>Roundhouse App</title>\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n    <link rel=\"icon\" href=\"data:,\">\n    <link rel=\"stylesheet\" href=\"/assets/tailwind.css\">\n    <script type=\"importmap\">\n    {{\n      \"imports\": {{\n        \"@hotwired/turbo\": \"/assets/turbo.min.js\"\n      }}\n    }}\n    </script>\n    <script type=\"module\">import \"@hotwired/turbo\";</script>\n  </head>\n  <body>\n    <main class=\"container mx-auto mt-8 px-5 flex flex-col\">\n      {}\n    </main>\n  </body>\n</html>\n\"##,\n        body,\n    )\n}\n"},{"path":"src/session.rs","content":"//! ActionDispatch::Session — per-app session store. Empty by default\n//! (real-blog uses no session keys); HWIA-shape shim methods route\n//! through an internal HashMap so apps that introduce session keys\n//! can grow the surface without a runtime rewrite.\n//!\n//! Hand-written for rust2 Phase 3 (sibling of\n//! `runtime/ruby/action_dispatch/session.rb`). The transpile pipeline\n//! produces broken Rust for this file's shim methods; hand-writing\n//! avoids fighting those emit bugs.\n\nuse std::collections::HashMap;\n\n#[derive(Debug, Default, Clone)]\npub struct Session {\n    data: HashMap<String, String>,\n}\n\nimpl Session {\n    pub fn new() -> Self {\n        Self::default()\n    }\n\n    pub fn from_persisted(other: Option<&HashMap<String, String>>) -> Self {\n        let mut session = Self::default();\n        if let Some(map) = other {\n            for (k, v) in map {\n                session.data.insert(k.clone(), v.clone());\n            }\n        }\n        session\n    }\n\n    pub fn get(&self, key: &str) -> Option<String> {\n        self.data.get(key).cloned()\n    }\n\n    pub fn set(&mut self, key: &str, value: String) {\n        self.data.insert(key.to_string(), value);\n    }\n\n    pub fn fetch(&self, key: &str, default: Option<String>) -> Option<String> {\n        self.get(key).or(default)\n    }\n\n    pub fn key(&self, key: &str) -> bool {\n        self.data.contains_key(key)\n    }\n\n    pub fn has_key(&self, key: &str) -> bool {\n        self.key(key)\n    }\n\n    pub fn include(&self, key: &str) -> bool {\n        self.key(key)\n    }\n\n    pub fn delete(&mut self, key: &str) -> Option<String> {\n        self.data.remove(key)\n    }\n\n    pub fn len(&self) -> usize {\n        self.data.len()\n    }\n\n    pub fn is_empty(&self) -> bool {\n        self.data.is_empty()\n    }\n\n    pub fn to_h(&self) -> HashMap<String, String> {\n        self.data.clone()\n    }\n}\n"},{"path":"src/test_support.rs","content":"//! Roundhouse Rust test-support runtime.\n//!\n//! Hand-written, shipped alongside generated code (copied in by the\n//! Rust emitter as `src/test_support.rs`). Emitted controller tests\n//! use this surface through the `TestResponseExt` trait, so the\n//! assertion call sites stay stable while the implementation can\n//! evolve.\n//!\n//! Phase 4d ships substring-match implementations of `assert_select`\n//! — matches railcar's choice, zero extra deps, good-enough for the\n//! scaffold blog's HTML assertions. A later upgrade to a real CSS\n//! selector engine (the `scraper` crate, `html5ever`, or similar)\n//! only needs to touch this file — emitted tests call the trait\n//! methods and are insulated from the rendering strategy.\n\nuse axum_test::TestResponse;\n\npub trait TestResponseExt {\n    /// `assert_response :success` — status 200 OK.\n    fn assert_ok(&self);\n\n    /// `assert_response :unprocessable_entity` — status 422.\n    fn assert_unprocessable(&self);\n\n    /// `assert_response <status>` for any concrete status.\n    fn assert_status(&self, code: u16);\n\n    /// `assert_redirected_to <path>`. Checks the response is a 3xx\n    /// redirect and the `Location` header matches the expected path.\n    /// Phase 4d substring-matches the path in Location to forgive\n    /// absolute-vs-relative URL differences; a stricter check can\n    /// swap in later without touching emitted tests.\n    fn assert_redirected_to(&self, path: &str);\n\n    /// `assert_select <selector>` — response body contains an\n    /// element matching the (very) loose selector form. Substring\n    /// match on the opening tag or `id=` / `class=` attribute\n    /// fragment. Covers the scaffold blog's shapes:\n    ///   \"h1\"             → contains \"<h1\"\n    ///   \"#articles\"      → contains `id=\"articles\"`\n    ///   \".p-4\"           → contains `class=\"... p-4 ...\"` (as substring)\n    ///   \"form\"           → contains \"<form\"\n    fn assert_select(&self, selector: &str);\n\n    /// `assert_select <selector>, <text>` — the `selector` check\n    /// above *and* the response body contains `text`. Phase 4d\n    /// doesn't verify the text lives inside the selector match\n    /// (would require structural parsing); a later scraper-backed\n    /// impl can tighten this.\n    fn assert_select_text(&self, selector: &str, text: &str);\n\n    /// `assert_select <selector>, minimum: N` — response body\n    /// contains at least `n` occurrences of the selector fragment.\n    /// Again substring-counted in Phase 4d.\n    fn assert_select_min(&self, selector: &str, n: usize);\n}\n\nimpl TestResponseExt for TestResponse {\n    fn assert_ok(&self) {\n        assert_eq!(self.status_code(), 200, \"expected 200 OK\");\n    }\n\n    fn assert_unprocessable(&self) {\n        assert_eq!(self.status_code(), 422, \"expected 422 Unprocessable Entity\");\n    }\n\n    fn assert_status(&self, code: u16) {\n        assert_eq!(self.status_code().as_u16(), code, \"expected status {code}\");\n    }\n\n    fn assert_redirected_to(&self, path: &str) {\n        assert!(\n            self.status_code().is_redirection(),\n            \"expected redirection, got {}\",\n            self.status_code(),\n        );\n        let location = self\n            .headers()\n            .get(axum::http::header::LOCATION)\n            .and_then(|v| v.to_str().ok())\n            .unwrap_or(\"\");\n        assert!(\n            location.contains(path),\n            \"expected Location to contain {path:?}, got {location:?}\",\n        );\n    }\n\n    fn assert_select(&self, selector: &str) {\n        let body = self.text();\n        let fragment = selector_fragment(selector);\n        assert!(\n            body.contains(&fragment),\n            \"expected body to match selector {selector:?} (looked for substring {fragment:?})\",\n        );\n    }\n\n    fn assert_select_text(&self, selector: &str, text: &str) {\n        self.assert_select(selector);\n        let body = self.text();\n        assert!(\n            body.contains(text),\n            \"expected body to contain text {text:?} under selector {selector:?}\",\n        );\n    }\n\n    fn assert_select_min(&self, selector: &str, n: usize) {\n        let body = self.text();\n        let fragment = selector_fragment(selector);\n        let count = body.matches(&fragment).count();\n        assert!(\n            count >= n,\n            \"expected at least {n} matches for selector {selector:?} (fragment {fragment:?}), got {count}\",\n        );\n    }\n}\n\n/// Map a loose selector to a substring fragment that probably appears\n/// in matching HTML. Phase 4d: handles `#id`, `.class`, `tag`, and\n/// the first element of compound selectors like `\"#comments .p-4\"`\n/// (splits on whitespace, picks the first chunk). Every match is a\n/// substring search — false positives are possible but the blog's\n/// HTML is narrow enough that the tests are a reliable signal.\nfn selector_fragment(selector: &str) -> String {\n    let first = selector.split_whitespace().next().unwrap_or(\"\");\n    if let Some(id) = first.strip_prefix('#') {\n        format!(\"id=\\\"{id}\\\"\")\n    } else if let Some(class) = first.strip_prefix('.') {\n        format!(\"{class}\\\"\")\n    } else {\n        format!(\"<{first}\")\n    }\n}\n"},{"path":"src/tests/article.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n\n#[allow(unused_imports)]\nuse crate::fixtures::*;\n// Generated by Roundhouse (rust2).\n\n#[test]\npub fn test_creates_an_article_with_valid_attributes() {\n        crate::fixtures::setup();\n    let mut article = ArticlesFixtures::one();\n    if { let _ = article.id(); false } { panic!(\"{}\", \"refute_nil failed\") };\n    if \"Getting Started with Rails\" != article.title() { panic!(\"{}\", \"assert_equal failed\") };\n}\n\n#[test]\npub fn test_validates_title_presence() {\n        crate::fixtures::setup();\n    let mut article = Article::new(std::collections::HashMap::from([(\"title\", \"\"), (\"body\", \"Valid body content here.\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    if article.save() { panic!(\"{}\", \"refute failed\") };\n}\n\n#[test]\npub fn test_validates_body_minimum_length() {\n        crate::fixtures::setup();\n    let mut article = Article::new(std::collections::HashMap::from([(\"title\", \"Valid Title\"), (\"body\", \"Short\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    if article.save() { panic!(\"{}\", \"refute failed\") };\n}\n\n#[test]\npub fn test_destroys_comments_when_article_is_destroyed() {\n        crate::fixtures::setup();\n    let mut article = ArticlesFixtures::one();\n    let __diff_before = Comment::count();\n    article.destroy();\n    let mut __diff_after = Comment::count();\n    if __diff_after - __diff_before != -1_i64 { panic!(\"{}\", \"Comment.count didn't change by -1\") };\n}\n"},{"path":"src/tests/articles_controller_test.rs","content":"// Generated by Roundhouse.\n\n#[allow(unused_imports)]\nuse crate::fixtures;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::route_helpers;\n#[allow(unused_imports)]\nuse crate::test_support::TestResponseExt;\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_get_index() {\n    // \"should get index\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let resp = server.get(&route_helpers::articles_path()).await;\n    resp.assert_ok();\n    resp.assert_select_text(\"h1\", &\"Articles\");\n    resp.assert_select(\"#articles\");\n    resp.assert_select_min(\"h2\", 1_i64 as usize);\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_get_new() {\n    // \"should get new\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let resp = server.get(&route_helpers::new_article_path()).await;\n    resp.assert_ok();\n    resp.assert_select(\"form\");\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_create_article() {\n    // \"should create article\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let _before = Article::count();\n    let resp = server.post(&route_helpers::articles_path()).form(&std::collections::HashMap::<String, String>::from([(\"article[body]\".to_string(), \"A sufficiently long body for validation.\".to_string()), (\"article[title]\".to_string(), \"New Title\".to_string())])).await;\n    let _after = Article::count();\n    assert_eq!(_after - _before, 1);\n    resp.assert_redirected_to(&route_helpers::article_path(Article::last().unwrap().id));\n    assert_eq!(\"New Title\", Article::last().unwrap().title);\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_not_create_article_with_invalid_params() {\n    // \"should not create article with invalid params\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let _before = Article::count();\n    let resp = server.post(&route_helpers::articles_path()).form(&std::collections::HashMap::<String, String>::from([(\"article[title]\".to_string(), \"\".to_string()), (\"article[body]\".to_string(), \"\".to_string())])).await;\n    let _after = Article::count();\n    assert_eq!(_after - _before, 0);\n    resp.assert_unprocessable();\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_show_article() {\n    // \"should show article\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let resp = server.get(&route_helpers::article_path(article.id)).await;\n    resp.assert_ok();\n    resp.assert_select_text(\"h1\", &article.title);\n    resp.assert_select_text(\"h2\", &\"Comments\");\n    resp.assert_select_min(\"#comments .p-4\", 1_i64 as usize);\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_get_edit() {\n    // \"should get edit\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let resp = server.get(&route_helpers::edit_article_path(article.id)).await;\n    resp.assert_ok();\n    resp.assert_select(\"form\");\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_update_article() {\n    // \"should update article\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let resp = server.patch(&route_helpers::article_path(article.id)).form(&std::collections::HashMap::<String, String>::from([(\"article[body]\".to_string(), article.body.to_string()), (\"article[title]\".to_string(), \"Updated Title\".to_string())])).await;\n    resp.assert_redirected_to(&route_helpers::article_path(article.id));\n    article.reload();\n    assert_eq!(\"Updated Title\", article.title);\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_not_update_article_with_invalid_params() {\n    // \"should not update article with invalid params\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let resp = server.patch(&route_helpers::article_path(article.id)).form(&std::collections::HashMap::<String, String>::from([(\"article[title]\".to_string(), \"\".to_string()), (\"article[body]\".to_string(), \"\".to_string())])).await;\n    resp.assert_unprocessable();\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_destroy_article() {\n    // \"should destroy article\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let _before = Article::count();\n    let resp = server.delete(&route_helpers::article_path(article.id)).await;\n    let _after = Article::count();\n    assert_eq!(_after - _before, -1);\n    resp.assert_redirected_to(&route_helpers::articles_path());\n}\n"},{"path":"src/tests/comment.rs","content":"#[allow(unused_imports)]\nuse crate::param_value::ParamValue;\n#[allow(unused_imports)]\nuse crate::db::Db;\n#[allow(unused_imports)]\nuse crate::broadcasts::Broadcasts;\n// Sibling-model glob so cross-file refs (Article ↔ Comment, the\n// `<Model>Row` typed-row pair) resolve through the `pub use` chain\n// that `emit_models_mod_rs` lays into `src/models/mod.rs`. Rust\n// doesn't auto-import siblings — the lowerer leaves bare `Article`\n// / `Comment` / `ArticleRow` / `CommentRow` references at every\n// `Comment.belongs_to :article`, `has_many :comments`, and\n// `instantiate(row)` call site; without this line each of those\n// E0433s independently.\n#[allow(unused_imports)]\nuse crate::models::*;\n// View modules (Phase 5b stubs). The model lowerer's broadcasts_to\n// expansion emits `Articles::article(self)` / `Comments::comment\n// (self)` partial renders inside `after_*_commit` callback bodies;\n// the actual view emit isn't yet wired through rust2, so each\n// LibraryClass produced by `lower_views_to_library_classes` lands\n// here as a fully-generic `String::new()` stub. Replace with real\n// view emit when Phase 5b lands.\n#[allow(unused_imports)]\nuse crate::views::*;\n\n#[allow(unused_imports)]\nuse crate::fixtures::*;\n// Generated by Roundhouse (rust2).\n\n#[test]\npub fn test_creates_a_comment_on_an_article() {\n        crate::fixtures::setup();\n    let mut comment = CommentsFixtures::one();\n    if { let _ = comment.id(); false } { panic!(\"{}\", \"refute_nil failed\") };\n    if ArticlesFixtures::one().id() != comment.article_id() { panic!(\"{}\", \"assert_equal failed\") };\n}\n\n#[test]\npub fn test_belongs_to_article_association() {\n        crate::fixtures::setup();\n    let mut article = ArticlesFixtures::one();\n    let mut comment = Comment::create(std::collections::HashMap::from([((\"article_id\").to_string(), serde_json::Value::from(article.id())), ((\"commenter\").to_string(), serde_json::Value::from((\"Commenter\").to_string())), ((\"body\").to_string(), serde_json::Value::from((\"Comment body text.\").to_string()))]));\n    if article.id() != comment.article_id() { panic!(\"{}\", \"assert_equal failed\") };\n}\n\n#[test]\npub fn test_requires_commenter() {\n        crate::fixtures::setup();\n    let mut article = ArticlesFixtures::one();\n    let mut comment = Comment::new(std::collections::HashMap::from([((\"article_id\").to_string(), serde_json::Value::from(article.id())), ((\"body\").to_string(), serde_json::Value::from((\"Comment without commenter\").to_string()))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    if comment.save() { panic!(\"{}\", \"refute failed\") };\n}\n\n#[test]\npub fn test_requires_body() {\n        crate::fixtures::setup();\n    let mut article = ArticlesFixtures::one();\n    let mut comment = Comment::new(std::collections::HashMap::from([((\"article_id\").to_string(), serde_json::Value::from(article.id())), ((\"commenter\").to_string(), serde_json::Value::from((\"Someone\").to_string()))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    if comment.save() { panic!(\"{}\", \"refute failed\") };\n}\n\n#[test]\npub fn test_requires_valid_article() {\n        crate::fixtures::setup();\n    let mut comment = Comment::new(std::collections::HashMap::from([((\"commenter\").to_string(), serde_json::Value::from(\"Test\")), ((\"body\").to_string(), serde_json::Value::from(\"A test comment.\")), ((\"article_id\").to_string(), serde_json::Value::from(999999_i64))]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>());\n    if comment.save() { panic!(\"{}\", \"refute failed\") };\n}\n"},{"path":"src/tests/comments_controller_test.rs","content":"// Generated by Roundhouse.\n\n#[allow(unused_imports)]\nuse crate::fixtures;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::route_helpers;\n#[allow(unused_imports)]\nuse crate::test_support::TestResponseExt;\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_create_comment() {\n    // \"should create comment\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let _before = Comment::count();\n    let resp = server.post(&route_helpers::article_comments_path(article.id)).form(&std::collections::HashMap::<String, String>::from([(\"comment[commenter]\".to_string(), \"Test\".to_string()), (\"comment[body]\".to_string(), \"A test comment.\".to_string())])).await;\n    let _after = Comment::count();\n    assert_eq!(_after - _before, 1);\n    resp.assert_redirected_to(&route_helpers::article_path(article.id));\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_not_create_comment_with_invalid_params() {\n    // \"should not create comment with invalid params\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let _before = Comment::count();\n    let resp = server.post(&route_helpers::article_comments_path(article.id)).form(&std::collections::HashMap::<String, String>::from([(\"comment[commenter]\".to_string(), \"\".to_string()), (\"comment[body]\".to_string(), \"\".to_string())])).await;\n    let _after = Comment::count();\n    assert_eq!(_after - _before, 0);\n    resp.assert_redirected_to(&route_helpers::article_path(article.id));\n}\n\n#[tokio::test(flavor = \"multi_thread\")]\n#[allow(unused_mut, unused_variables)]\nasync fn should_destroy_comment() {\n    // \"should destroy comment\"\n    fixtures::setup();\n    let server = axum_test::TestServer::new(crate::router::router()).unwrap();\n    let mut article = fixtures::articles::one();\n    let mut comment = fixtures::comments::one();\n    let _before = Comment::count();\n    let resp = server.delete(&route_helpers::article_comment_path(article.id, comment.id)).await;\n    let _after = Comment::count();\n    assert_eq!(_after - _before, -1);\n    resp.assert_redirected_to(&route_helpers::article_path(article.id));\n}\n"},{"path":"src/tests/mod.rs","content":"// Generated by Roundhouse (rust2).\n\npub mod article;\npub mod articles_controller_test;\npub mod comment;\npub mod comments_controller_test;\n"},{"path":"src/view_helpers.rs","content":"// Generated from runtime/ruby/action_view/view_helpers.rb at app emit time.\n// Do not edit by hand — edit the source `.rb` and re-run emit.\n\nuse crate::active_record_base::Base;\nuse crate::hash_ext::merge_attrs;\nuse crate::http::RubyToS;\n\nstatic HTML_ESCAPES: std::sync::LazyLock<std::collections::HashMap<&'static str, &'static str>> = std::sync::LazyLock::new(|| std::collections::HashMap::from([(\"&\", \"&amp;\"), (\"<\", \"&lt;\"), (\">\", \"&gt;\"), (\"\\\"\", \"&quot;\"), (\"'\", \"&#39;\")]));\nstatic HTML_ESCAPE_PATTERN: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| regex::Regex::new(\"[&<>\\\"']\").unwrap());\n\npub struct ViewHelpers;\n\nthread_local! {\n    static SLOTS: std::cell::RefCell<Option<std::collections::HashMap<String, String>>> = const { std::cell::RefCell::new(None) };\n}\n\nimpl ViewHelpers {\n    pub fn reset_slots_bang() {\n        SLOTS.with(|__s| *__s.borrow_mut() = Some(std::collections::HashMap::new()))\n    }\n\n    pub fn content_for_set(slot: &str, value: &str) {\n        { SLOTS.with(|__s| __s.borrow_mut().get_or_insert_with(std::collections::HashMap::new).insert((slot).to_string(), (value).to_string())); };\n    }\n\n    pub fn content_for_get(slot: &str) -> Option<String> {\n        SLOTS.with(|__s| __s.borrow().as_ref().and_then(|__m| __m.get(slot).cloned()))\n    }\n\n    pub fn get_slot(slot: &str) -> String {\n        SLOTS.with(|__s| __s.borrow().as_ref().and_then(|__m| __m.get(slot).cloned())).unwrap_or((\"\").to_string())\n    }\n\n    pub fn get_yield() -> String {\n        SLOTS.with(|__s| __s.borrow().as_ref().and_then(|__m| __m.get(\"__body__\").cloned())).unwrap_or((\"\").to_string())\n    }\n\n    pub fn set_yield(content: &str) {\n        { SLOTS.with(|__s| __s.borrow_mut().get_or_insert_with(std::collections::HashMap::new).insert((\"__body__\").to_string(), (content).to_string())); };\n    }\n\n    pub fn html_escape(s: &str) -> String {\n        HTML_ESCAPE_PATTERN.replace_all(&s, |__caps: &regex::Captures| -> String { (*HTML_ESCAPES.get(&__caps[0]).unwrap_or(&\"\")).to_string() }).into_owned()\n    }\n\n    pub fn truncate(s: &str, length: i64, omission: &str) -> String {\n        if (s.len() as i64) <= length { return (s.clone()).to_string() };\n        let mut cutoff = length - omission.len() as i64;\n        if cutoff < 0_i64 { cutoff = 0_i64 };\n        format!(\"{}{}\", (&s.clone()[(0_i64) as usize..((0_i64) + (cutoff)) as usize]).to_string(), omission.clone())\n    }\n\n    pub fn dom_id(record: Base, suffix: Option<String>) -> String {\n        if suffix.is_none() { format!(\"{}_{}\", record.dom_prefix(), record.id()) } else { format!(\"{}_{}_{}\", suffix.clone().unwrap(), record.dom_prefix(), record.id()) }\n    }\n\n    pub fn link_to(text: &str, href: &str, opts: std::collections::HashMap<String, serde_json::Value>) -> String {\n        let attrs = Self::render_attrs(merge_attrs(std::collections::HashMap::from([(\"href\", href)]), opts.clone()));\n        format!(\"<a{}>{}</a>\", attrs, Self::html_escape(&(text)))\n    }\n\n    pub fn button_to(text: &str, href: &str, opts: std::collections::HashMap<String, serde_json::Value>) -> String {\n        let mut method = opts.clone().get(\"method\").cloned();\n        let form_class = opts.clone().get(\"form_class\").cloned();\n        let mut inner_opts = opts.clone().clone().clone();\n        inner_opts.remove(\"method\");\n        inner_opts.remove(\"form_class\");\n        let mut form_attrs = std::collections::HashMap::from([(\"action\", (href).to_string()), (\"method\", (\"post\").to_string())]).clone();\n        { form_attrs.clone().insert(\"class\", form_class.unwrap_or(serde_json::Value::from(\"button_to\")).to_string()); };\n        let button_attrs = Self::render_attrs(merge_attrs(std::collections::HashMap::from([(\"type\", \"submit\")]), inner_opts.clone()));\n        let method_input = if !(method.is_none()) && method.clone().unwrap().ruby_to_s() != \"post\" { format!(\"<input type=\\\"hidden\\\" name=\\\"_method\\\" value=\\\"{}\\\">\", (method.clone().unwrap()).ruby_to_s()) } else { (\"\").to_string() };\n        let auth_token_input = \"<input type=\\\"hidden\\\" name=\\\"authenticity_token\\\" value=\\\"\\\">\";\n        format!(\"<form{}>{}<button{}>{}</button>{}</form>\", Self::render_attrs(form_attrs.clone().into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()), method_input, button_attrs, Self::html_escape(&(text)), auth_token_input)\n    }\n\n    pub fn csrf_meta_tags() -> String {\n        (\"<meta name=\\\"csrf-param\\\" content=\\\"authenticity_token\\\" />\\n<meta name=\\\"csrf-token\\\" content=\\\"\\\" />\").to_string()\n    }\n\n    pub fn csp_meta_tag() -> String {\n        (\"\").to_string()\n    }\n\n    pub fn stylesheet_link_tag(name: &str, opts: std::collections::HashMap<String, serde_json::Value>) -> String {\n        let href = format!(\"/assets/{}.css\", name);\n        let attrs = Self::render_attrs(merge_attrs(std::collections::HashMap::from([(\"rel\", (\"stylesheet\").to_string()), (\"href\", href)]), opts.clone()));\n        format!(\"<link{}>\", attrs)\n    }\n\n    pub fn javascript_importmap_tags(pins: Option<Vec<serde_json::Value>>, entry: &str) -> String {\n        if pins.is_none() || pins.clone().unwrap().is_empty() { { let mut json = (\"{\\n  \\\"imports\\\": {\\n    \\\"@hotwired/turbo\\\": \\\"/assets/turbo.min.js\\\"\\n  }\\n}\").to_string();\n        return format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", format!(\"{}{}\", \"<script type=\\\"importmap\\\" data-turbo-track=\\\"reload\\\">\", json.clone()), \"</script>\"), \"\\n\"), \"<link rel=\\\"modulepreload\\\" href=\\\"/assets/turbo.min.js\\\">\"), \"\\n\"), \"<script type=\\\"module\\\">import \\\"@hotwired/turbo\\\"</script>\") } };\n        let import_lines = pins.clone().iter().flatten().map(|p| { format!(\"    \\\"{}\\\": \\\"{}\\\"\", (p.clone()[\"name\"]).ruby_to_s(), (p.clone()[\"path\"]).ruby_to_s()) }).collect::<Vec<_>>().join(\",\\n\");\n        let mut json = format!(\"{}{}\", format!(\"{}{}\", \"{\\n  \\\"imports\\\": {\\n\", import_lines), \"\\n  }\\n}\");\n        let mut parts = vec![];\n        parts.push(format!(\"{}{}\", format!(\"{}{}\", \"<script type=\\\"importmap\\\" data-turbo-track=\\\"reload\\\">\", json.clone()), \"</script>\"));\n        pins.clone().iter().flatten().for_each(|p| { parts.push(format!(\"<link rel=\\\"modulepreload\\\" href=\\\"{}\\\">\", (p.clone()[\"path\"]).ruby_to_s())); });\n        parts.push(format!(\"<script type=\\\"module\\\">import \\\"{}\\\"</script>\", entry));\n        parts.join(\"\\n\")\n    }\n\n    pub fn turbo_stream_from(stream: serde_json::Value) -> String {\n        let encoded = { use base64::Engine; base64::engine::general_purpose::STANDARD.encode(serde_json::to_string(&stream).unwrap()) };\n        format!(\"<turbo-cable-stream-source channel=\\\"Turbo::StreamsChannel\\\" signed-stream-name=\\\"{}--unsigned\\\"></turbo-cable-stream-source>\", encoded)\n    }\n\n    pub fn csrf_token_hidden_input() -> String {\n        (\"<input type=\\\"hidden\\\" name=\\\"authenticity_token\\\" value=\\\"\\\">\").to_string()\n    }\n\n    pub fn method_override_input(method: &str) -> String {\n        let mut method_str = method.to_string();\n        if method_str.clone() == \"get\" || method_str.clone() == \"post\" { (\"\").to_string() } else { format!(\"<input type=\\\"hidden\\\" name=\\\"_method\\\" value=\\\"{}\\\">\", method_str.clone()) }\n    }\n\n    pub fn optional_value_attr(value: serde_json::Value) -> String {\n        if value.clone().is_null() || value.ruby_to_s().is_empty() { (\"\").to_string() } else { format!(\" value=\\\"{}\\\"\", Self::html_escape(&(value.ruby_to_s()))) }\n    }\n\n    pub fn escape_or_empty(value: serde_json::Value) -> String {\n        if value.clone().is_null() { (\"\").to_string() } else { Self::html_escape(&(value.ruby_to_s())) }\n    }\n\n    pub fn render_attrs(attrs: std::collections::HashMap<String, serde_json::Value>) -> String {\n        if attrs.is_empty() { return (\"\").to_string() };\n        let mut pairs = vec![];\n        { let _m = attrs.clone(); let mut _items: Vec<_> = _m.iter().collect(); _items.sort_by(|a, b| a.0.cmp(b.0)); _items.into_iter().for_each(|(k, v)| {\n            if v.clone().is_null() { /* TODO rust2: ExprNode::Discriminant(23) */ };\n            let name = k.to_string();\n            if v.clone().is_object() { v.clone().as_object().unwrap().iter().for_each(|(inner_k, inner_v)| {\n                if inner_v.clone().is_null() { /* TODO rust2: ExprNode::Discriminant(23) */ };\n                let inner_name = inner_k.ruby_to_s().replace(\"_\", \"-\");\n                pairs.push(format!(\" {}-{}=\\\"{}\\\"\", name.clone(), inner_name, Self::html_escape(&(inner_v.ruby_to_s()))))\n            }) } else { pairs.push(format!(\" {}=\\\"{}\\\"\", name.clone(), Self::html_escape(&(v.ruby_to_s())))) }\n        }); };\n        pairs.join(\"\")\n    }\n}\n\n// rust2 compat: bare-fn wrappers consumed by server.rs.\npub fn reset_render_state() { ViewHelpers::reset_slots_bang() }\npub fn set_yield(content: &str) { ViewHelpers::set_yield(content) }\npub fn get_yield() -> String { ViewHelpers::get_yield() }\n"},{"path":"src/views/articles.rs","content":"#[allow(unused_imports)]\nuse std::fmt::Write as _;\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n#[allow(unused_imports)]\nuse crate::route_helpers::{self, RouteHelpers};\n#[allow(unused_imports)]\nuse crate::inflector::{self, Inflector};\n#[allow(unused_imports)]\nuse crate::importmap::{self, Importmap};\n#[allow(unused_imports)]\nuse crate::json_builder::{self, JsonBuilder};\n#[allow(unused_imports)]\nuse crate::http::RubyToS;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::views::*;\npub struct Articles;\n\nimpl Articles {\n    pub fn article(article: Article, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        io.push_str(\"<div id=\\\"\");\n        io.push_str(&format!(\"article_{}\", article.clone().id()));\n        io.push_str(\"\\\" class=\\\"flex flex-col sm:flex-row justify-between items-center pb-5 sm:pb-0\\\">\\n  <div class=\\\"p-4 border rounded mb-4 flex-grow\\\">\\n    <h2 class=\\\"text-xl font-bold\\\">\\n      \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::article_path(article.id()))), \"text-blue-600 hover:underline\", ViewHelpers::html_escape(&(article.title()))).ok(); };\n        io.push_str(\"\\n      <span id=\\\"\");\n        io.push_str(&format!(\"comments_count_article_{}\", article.clone().id()));\n        io.push_str(\"\\\" class=\\\"text-gray-500 text-sm font-normal ml-2\\\">\\n        (\");\n        io.push_str(&Inflector::pluralize(article.comments().len() as i64, \"comment\"));\n        io.push_str(\")\\n      </span>\\n    </h2>\\n    <p class=\\\"text-gray-700 mt-2\\\">\");\n        io.push_str(&ViewHelpers::html_escape(&(ViewHelpers::truncate(&(article.body()), 100_i64, \"...\"))));\n        io.push_str(\"</p>\\n  </div>\\n  <div class=\\\"w-full sm:w-auto flex flex-col sm:flex-row space-x-2 space-y-2\\\">\\n    \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::article_path(article.id()))), \"w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium\", \"Show\").ok(); };\n        io.push_str(\"\\n    \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::edit_article_path(article.id()))), \"w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium\", \"Edit\").ok(); };\n        io.push_str(\"\\n    \");\n        { write!(io, \"<form action=\\\"{}\\\" method=\\\"post\\\" class=\\\"{}\\\">{}<button type=\\\"submit\\\" class=\\\"{}\\\" data-turbo-confirm=\\\"{}\\\">{}</button>{}</form>\", ViewHelpers::html_escape(&(RouteHelpers::article_path(article.id()))), \"button_to\", ViewHelpers::method_override_input(\"delete\"), \"w-full sm:w-auto rounded-md px-3.5 py-2.5 text-white bg-red-600 hover:bg-red-500 font-medium cursor-pointer\", \"Are you sure?\", \"Destroy\", ViewHelpers::csrf_token_hidden_input()).ok(); };\n        io.push_str(\"\\n  </div>\\n</div>\\n\");\n        io\n    }\n\n    pub fn form(article: Article, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        let mut form_method = if article.persisted() { \"patch\" } else { \"post\" };\n        { write!(io, \"<form action=\\\"{}\\\" accept-charset=\\\"UTF-8\\\" method=\\\"post\\\" class=\\\"{}\\\">\", ViewHelpers::html_escape(&(if article.persisted() { RouteHelpers::article_path(article.id()) } else { RouteHelpers::articles_path() })), \"contents\").ok(); };\n        io.push_str(&ViewHelpers::method_override_input(&(form_method.clone())));\n        io.push_str(&ViewHelpers::csrf_token_hidden_input());\n        io.push_str(\"\\n\");\n        if !(article.errors().is_empty()) { { io.push_str(\"    <div id=\\\"error_explanation\\\" class=\\\"bg-red-50 text-red-500 px-3 py-2 font-medium rounded-md mt-3\\\">\\n      <h2>\");\n        io.push_str(&Inflector::pluralize(article.errors().len() as i64, \"error\"));\n        io.push_str(\" prohibited this article from being saved:</h2>\\n\\n      <ul class=\\\"list-disc ml-6\\\">\\n        \");\n        article.errors().iter_mut().for_each(|error| {\n            io.push_str(\"\\n          <li>\");\n            io.push_str(&ViewHelpers::html_escape(&(error)));\n            io.push_str(\"</li>\\n        \");\n        });\n        io.push_str(\"\\n      </ul>\\n    </div>\\n\") } };\n        io.push_str(\"\\n  <div class=\\\"my-5\\\">\\n    \");\n        { write!(io, \"<label for=\\\"article_title\\\">Title</label>\").ok(); };\n        io.push_str(\"\\n    \");\n        { write!(io, \"<input type=\\\"text\\\" name=\\\"article[title]\\\" id=\\\"article_title\\\"{} class=\\\"{}\\\">\", ViewHelpers::optional_value_attr(article.clone().get_index(\"title\")), \"block shadow-sm rounded-md border px-3 py-2 mt-2 w-full border-gray-400 focus:outline-blue-600\").ok(); };\n        io.push_str(\"\\n  </div>\\n\\n  <div class=\\\"my-5\\\">\\n    \");\n        { write!(io, \"<label for=\\\"article_body\\\">Body</label>\").ok(); };\n        io.push_str(\"\\n    \");\n        { write!(io, \"<textarea name=\\\"article[body]\\\" id=\\\"article_body\\\" rows=\\\"{}\\\" class=\\\"{}\\\">{}</textarea>\", \"4\", \"block shadow-sm rounded-md border px-3 py-2 mt-2 w-full border-gray-400 focus:outline-blue-600\", ViewHelpers::escape_or_empty(article.clone().get_index(\"body\"))).ok(); };\n        io.push_str(\"\\n  </div>\\n\\n  <div class=\\\"inline\\\">\\n    \");\n        { write!(io, \"<input type=\\\"submit\\\" name=\\\"commit\\\" value=\\\"{}\\\" data-disable-with=\\\"{}\\\" class=\\\"{}\\\">\", if form_method.clone() == \"patch\" { \"Update Article\" } else { \"Create Article\" }, if form_method.clone() == \"patch\" { \"Update Article\" } else { \"Create Article\" }, \"w-full sm:w-auto rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white inline-block font-medium cursor-pointer\").ok(); };\n        io.push_str(\"\\n  </div>\\n\");\n        io.push_str(\"</form>\");\n        io\n    }\n\n    pub fn edit(article: Article, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        ViewHelpers::content_for_set(\"title\", \"Editing article\");\n        io.push_str(\"\\n<div class=\\\"md:w-2/3 w-full\\\">\\n  <h1 class=\\\"font-bold text-4xl\\\">Editing article</h1>\\n\\n  \");\n        io.push_str(&Articles::form(article.clone().clone(), None, None));\n        io.push_str(\"\\n\\n  \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::article_path(article.id()))), \"w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium\", \"Show this article\").ok(); };\n        io.push_str(\"\\n  \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::articles_path())), \"w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium\", \"Back to articles\").ok(); };\n        io.push_str(\"\\n</div>\\n\");\n        io\n    }\n\n    pub fn index(articles: Vec<Article>, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        io.push_str(&ViewHelpers::turbo_stream_from(serde_json::Value::from(\"articles\")));\n        io.push_str(\"\\n\\n\");\n        ViewHelpers::content_for_set(\"title\", \"Articles\");\n        io.push_str(\"\\n<div class=\\\"w-full\\\">\\n\");\n        if !(notice.is_none()) && !(notice.clone().unwrap().is_empty()) { { io.push_str(\"    <p class=\\\"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block\\\" id=\\\"notice\\\">\");\n        io.push_str(&ViewHelpers::html_escape(&(notice.clone().unwrap())));\n        io.push_str(\"</p>\\n\") } };\n        io.push_str(\"\\n  <div class=\\\"flex justify-between items-center\\\">\\n    <h1 class=\\\"font-bold text-4xl\\\">Articles</h1>\\n    \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::new_article_path())), \"rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white block font-medium\", \"New article\").ok(); };\n        io.push_str(\"\\n  </div>\\n\\n  <div id=\\\"articles\\\" class=\\\"min-w-full divide-y divide-gray-200 space-y-5\\\">\\n\");\n        if !(articles.is_empty()) { io.push_str(\"      \");\n        articles.clone().iter_mut().for_each(|a| { io.push_str(&Articles::article(a.clone(), None, None)); });\n        io.push_str(\"\\n\") } else { io.push_str(\"      <p class=\\\"text-center my-10\\\">No articles found.</p>\\n\") };\n        io.push_str(\"  </div>\\n</div>\\n\");\n        io\n    }\n\n    pub fn new(article: Article, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        ViewHelpers::content_for_set(\"title\", \"New article\");\n        io.push_str(\"\\n<div class=\\\"md:w-2/3 w-full\\\">\\n  <h1 class=\\\"font-bold text-4xl\\\">New article</h1>\\n\\n  \");\n        io.push_str(&Articles::form(article.clone(), None, None));\n        io.push_str(\"\\n\\n  \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::articles_path())), \"w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium\", \"Back to articles\").ok(); };\n        io.push_str(\"\\n</div>\\n\");\n        io\n    }\n\n    pub fn show(article: Article, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        ViewHelpers::content_for_set(\"title\", \"Showing article\");\n        io.push_str(\"\\n<div class=\\\"md:w-2/3 w-full\\\">\\n\");\n        if !(notice.is_none()) && !(notice.clone().unwrap().is_empty()) { { io.push_str(\"    <p class=\\\"py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block\\\" id=\\\"notice\\\">\");\n        io.push_str(&ViewHelpers::html_escape(&(notice.clone().unwrap())));\n        io.push_str(\"</p>\\n\") } };\n        io.push_str(\"\\n  <h1 class=\\\"font-bold text-4xl\\\">\");\n        io.push_str(&ViewHelpers::html_escape(&(article.title())));\n        io.push_str(\"</h1>\\n\\n  <div class=\\\"my-4\\\">\\n    <p class=\\\"text-gray-700\\\">\");\n        io.push_str(&ViewHelpers::html_escape(&(article.body())));\n        io.push_str(\"</p>\\n  </div>\\n\\n  \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::edit_article_path(article.id()))), \"w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium\", \"Edit this article\").ok(); };\n        io.push_str(\"\\n  \");\n        { write!(io, \"<a href=\\\"{}\\\" class=\\\"{}\\\">{}</a>\", ViewHelpers::html_escape(&(RouteHelpers::articles_path())), \"w-full sm:w-auto text-center mt-2 sm:mt-0 sm:ml-2 rounded-md px-3.5 py-2.5 bg-gray-100 hover:bg-gray-50 inline-block font-medium\", \"Back to articles\").ok(); };\n        io.push_str(\"\\n  \");\n        { write!(io, \"<form action=\\\"{}\\\" method=\\\"post\\\" class=\\\"{}\\\">{}<button type=\\\"submit\\\" class=\\\"{}\\\" data-turbo-confirm=\\\"{}\\\">{}</button>{}</form>\", ViewHelpers::html_escape(&(RouteHelpers::article_path(article.id()))), \"sm:inline-block mt-2 sm:mt-0 sm:ml-2\", ViewHelpers::method_override_input(\"delete\"), \"w-full rounded-md px-3.5 py-2.5 text-white bg-red-600 hover:bg-red-500 font-medium cursor-pointer\", \"Are you sure?\", \"Destroy this article\", ViewHelpers::csrf_token_hidden_input()).ok(); };\n        io.push_str(\"\\n</div>\\n\\n<hr class=\\\"my-8\\\">\\n\\n<h2 class=\\\"text-xl font-bold mb-4\\\">Comments</h2>\\n\\n\");\n        io.push_str(&ViewHelpers::turbo_stream_from(serde_json::Value::from(format!(\"article_{}_comments\", article.id()))));\n        io.push_str(\"\\n\\n<div id=\\\"comments\\\" class=\\\"space-y-4 mb-8\\\">\\n  \");\n        article.comments().iter_mut().for_each(|c| { io.push_str(&Comments::comment(c.clone(), None, None)); });\n        io.push_str(\"\\n</div>\\n\\n<h3 class=\\\"text-lg font-semibold mb-2\\\">Add a Comment</h3>\\n\\n\");\n        let mut form_record = Comment::new(std::collections::HashMap::new());\n        let form_method = \"post\";\n        { write!(io, \"<form action=\\\"{}\\\" accept-charset=\\\"UTF-8\\\" method=\\\"post\\\" class=\\\"{}\\\">\", ViewHelpers::html_escape(&(RouteHelpers::article_comments_path(article.id()))), \"space-y-4\").ok(); };\n        io.push_str(&ViewHelpers::method_override_input(&(form_method)));\n        io.push_str(&ViewHelpers::csrf_token_hidden_input());\n        io.push_str(\"\\n  <div>\\n    \");\n        { write!(io, \"<label for=\\\"comment_commenter\\\" class=\\\"{}\\\">Commenter</label>\", \"block font-medium\").ok(); };\n        io.push_str(\"\\n    \");\n        { write!(io, \"<input type=\\\"text\\\" name=\\\"comment[commenter]\\\" id=\\\"comment_commenter\\\"{} class=\\\"{}\\\">\", ViewHelpers::optional_value_attr(form_record.clone().get_index(\"commenter\")), \"block w-full border rounded p-2\").ok(); };\n        io.push_str(\"\\n  </div>\\n  <div>\\n    \");\n        { write!(io, \"<label for=\\\"comment_body\\\" class=\\\"{}\\\">Body</label>\", \"block font-medium\").ok(); };\n        io.push_str(\"\\n    \");\n        { write!(io, \"<textarea name=\\\"comment[body]\\\" id=\\\"comment_body\\\" rows=\\\"{}\\\" class=\\\"{}\\\">{}</textarea>\", \"3\", \"block w-full border rounded p-2\", ViewHelpers::escape_or_empty(form_record.clone().get_index(\"body\"))).ok(); };\n        io.push_str(\"\\n  </div>\\n  \");\n        { write!(io, \"<input type=\\\"submit\\\" name=\\\"commit\\\" value=\\\"{}\\\" data-disable-with=\\\"{}\\\" class=\\\"{}\\\">\", \"Add Comment\", \"Add Comment\", \"bg-blue-600 text-white px-4 py-2 rounded\").ok(); };\n        io.push_str(\"\\n\");\n        io.push_str(\"</form>\");\n        io\n    }\n\n    pub fn article_json(article: Article) -> String {\n        let mut io = String::new();\n        io.push_str(\"{\");\n        io.push_str(\"\\\"id\\\":\");\n        io.push_str(&JsonBuilder::encode_value(serde_json::Value::from(article.id())));\n        io.push_str(\",\");\n        io.push_str(\"\\\"title\\\":\");\n        io.push_str(&JsonBuilder::encode_value(serde_json::Value::from(article.title())));\n        io.push_str(\",\");\n        io.push_str(\"\\\"body\\\":\");\n        io.push_str(&JsonBuilder::encode_value(serde_json::Value::from(article.body())));\n        io.push_str(\",\");\n        io.push_str(\"\\\"created_at\\\":\");\n        io.push_str(&JsonBuilder::encode_datetime(Some(article.created_at())).to_string());\n        io.push_str(\",\");\n        io.push_str(\"\\\"updated_at\\\":\");\n        io.push_str(&JsonBuilder::encode_datetime(Some(article.updated_at())).to_string());\n        io.push_str(\",\");\n        io.push_str(\"\\\"url\\\":\");\n        io.push_str(&JsonBuilder::encode_value(serde_json::Value::from(format!(\"{}{}\", RouteHelpers::article_path(article.id()), \".json\"))));\n        io.push_str(\"}\");\n        io\n    }\n\n    pub fn index_json(articles: Vec<Article>) -> String {\n        let mut io = String::new();\n        io.push_str(\"[\");\n        io.push_str(&articles.into_iter().map(|article| { Articles::article_json(article.clone()) }).collect::<Vec<_>>().join(\",\"));\n        io.push_str(\"]\");\n        io\n    }\n\n    pub fn show_json(article: Article) -> String {\n        let mut io = String::new();\n        io.push_str(&Articles::article_json(article.clone()));\n        io\n    }\n}\n"},{"path":"src/views/comments.rs","content":"#[allow(unused_imports)]\nuse std::fmt::Write as _;\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n#[allow(unused_imports)]\nuse crate::route_helpers::{self, RouteHelpers};\n#[allow(unused_imports)]\nuse crate::inflector::{self, Inflector};\n#[allow(unused_imports)]\nuse crate::importmap::{self, Importmap};\n#[allow(unused_imports)]\nuse crate::json_builder::{self, JsonBuilder};\n#[allow(unused_imports)]\nuse crate::http::RubyToS;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::views::*;\npub struct Comments;\n\nimpl Comments {\n    pub fn comment(comment: Comment, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        io.push_str(\"<div id=\\\"\");\n        io.push_str(&format!(\"comment_{}\", comment.clone().id()));\n        io.push_str(\"\\\" class=\\\"p-4 bg-gray-50 rounded\\\">\\n  <p class=\\\"font-semibold\\\">\");\n        io.push_str(&ViewHelpers::html_escape(&(comment.commenter())));\n        io.push_str(\"</p>\\n  <p class=\\\"text-gray-700\\\">\");\n        io.push_str(&ViewHelpers::html_escape(&(comment.body())));\n        io.push_str(\"</p>\\n  \");\n        { write!(io, \"<form action=\\\"{}\\\" method=\\\"post\\\" class=\\\"{}\\\">{}<button type=\\\"submit\\\" class=\\\"{}\\\" data-turbo-confirm=\\\"{}\\\">{}</button>{}</form>\", ViewHelpers::html_escape(&(RouteHelpers::article_comment_path(comment.article_id(), comment.id()))), \"button_to\", ViewHelpers::method_override_input(\"delete\"), \"text-red-600 text-sm mt-2\", \"Are you sure?\", \"Delete\", ViewHelpers::csrf_token_hidden_input()).ok(); };\n        io.push_str(\"\\n</div>\\n\");\n        io\n    }\n}\n"},{"path":"src/views/layouts.rs","content":"#[allow(unused_imports)]\nuse std::fmt::Write as _;\n#[allow(unused_imports)]\nuse crate::view_helpers::{self, ViewHelpers};\n#[allow(unused_imports)]\nuse crate::route_helpers::{self, RouteHelpers};\n#[allow(unused_imports)]\nuse crate::inflector::{self, Inflector};\n#[allow(unused_imports)]\nuse crate::importmap::{self, Importmap};\n#[allow(unused_imports)]\nuse crate::json_builder::{self, JsonBuilder};\n#[allow(unused_imports)]\nuse crate::http::RubyToS;\n#[allow(unused_imports)]\nuse crate::models::*;\n#[allow(unused_imports)]\nuse crate::views::*;\npub struct Layouts;\n\nimpl Layouts {\n    pub fn application(body: &str, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        io.push_str(\"<!DOCTYPE html>\\n<html>\\n  <head>\\n    <title>\");\n        io.push_str(&ViewHelpers::html_escape(&(ViewHelpers::content_for_get(\"title\").unwrap_or(\"Real Blog\".to_string()))));\n        io.push_str(\"</title>\\n    <meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\">\\n    <meta name=\\\"apple-mobile-web-app-capable\\\" content=\\\"yes\\\">\\n    <meta name=\\\"application-name\\\" content=\\\"Real Blog\\\">\\n    <meta name=\\\"mobile-web-app-capable\\\" content=\\\"yes\\\">\\n    \");\n        io.push_str(&ViewHelpers::csrf_meta_tags());\n        io.push_str(\"\\n    \");\n        io.push_str(&ViewHelpers::csp_meta_tag());\n        io.push_str(\"\\n\\n    \");\n        io.push_str(&ViewHelpers::get_slot(\"head\").to_string());\n        io.push_str(\"\\n\\n\\n    <link rel=\\\"icon\\\" href=\\\"/icon.png\\\" type=\\\"image/png\\\">\\n    <link rel=\\\"icon\\\" href=\\\"/icon.svg\\\" type=\\\"image/svg+xml\\\">\\n    <link rel=\\\"apple-touch-icon\\\" href=\\\"/icon.png\\\">\\n\\n    \");\n        io.push_str(&format!(\"{}{}\", format!(\"{}{}\", ViewHelpers::stylesheet_link_tag(\"application\", std::collections::HashMap::from([(\"data-turbo-track\", \"reload\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>()), \"\\n\"), ViewHelpers::stylesheet_link_tag(\"tailwind\", std::collections::HashMap::from([(\"data-turbo-track\", \"reload\")]).into_iter().map(|(k, v)| (k.to_string(), serde_json::Value::from(v))).collect::<std::collections::HashMap<String, serde_json::Value>>())));\n        io.push_str(\"\\n    \");\n        io.push_str(&ViewHelpers::javascript_importmap_tags(Some(Importmap::pins()), &(Importmap::entry())));\n        io.push_str(\"\\n  </head>\\n\\n  <body>\\n    <main class=\\\"container mx-auto mt-28 px-5 flex flex-col\\\">\\n      \");\n        io.push_str(&body);\n        io.push_str(\"\\n    </main>\\n  </body>\\n</html>\\n\");\n        io\n    }\n\n    pub fn mailer(body: &str, notice: Option<String>, alert: Option<String>) -> String {\n        let mut io = String::new();\n        io.push_str(\"<!DOCTYPE html>\\n<html>\\n  <head>\\n    <meta http-equiv=\\\"Content-Type\\\" content=\\\"text/html; charset=utf-8\\\">\\n    <style>\\n      /* Email styles need to be inline */\\n    </style>\\n  </head>\\n\\n  <body>\\n    \");\n        io.push_str(&body);\n        io.push_str(\"\\n  </body>\\n</html>\\n\");\n        io\n    }\n}\n\n// Wedge 2c.4 layout bridge — server runtime slot is\n// `fn() -> String` (no args), so wrap the 3-arg\n// template signature with a thread-local body read.\npub fn render_layout() -> String {\n    let body = crate::view_helpers::ViewHelpers::get_yield();\n    Layouts::application(&body, None, None)\n}\n"},{"path":"src/views/mod.rs","content":"// Generated by Roundhouse (rust2).\n\npub mod articles;\npub mod comments;\npub mod layouts;\npub use articles::Articles;\npub use comments::Comments;\npub use layouts::Layouts;\n"},{"path":"static/assets/application.js","content":"// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails\nimport \"@hotwired/turbo-rails\"\nimport \"controllers\"\n"},{"path":"static/assets/controllers/application.js","content":"import { Application } from \"@hotwired/stimulus\"\n\nconst application = Application.start()\n\n// Configure Stimulus development experience\napplication.debug = false\nwindow.Stimulus   = application\n\nexport { application }\n"},{"path":"static/assets/controllers/hello_controller.js","content":"import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n  connect() {\n    this.element.textContent = \"Hello World!\"\n  }\n}\n"},{"path":"static/assets/controllers/index.js","content":"// Import and register all your controllers from the importmap via controllers/**/*_controller\nimport { application } from \"controllers/application\"\nimport { eagerLoadControllersFrom } from \"@hotwired/stimulus-loading\"\neagerLoadControllersFrom(\"controllers\", application)\n"},{"path":"static/assets/stimulus-loading.js","content":"// FIXME: es-module-shim won't shim the dynamic import without this explicit import\nimport \"@hotwired/stimulus\"\n\nconst controllerAttribute = \"data-controller\"\n\n// Eager load all controllers registered beneath the `under` path in the import map to the passed application instance.\nexport function eagerLoadControllersFrom(under, application) {\n  const paths = Object.keys(parseImportmapJson()).filter(path => path.match(new RegExp(`^${under}/.*_controller$`)))\n  paths.forEach(path => registerControllerFromPath(path, under, application))\n}\n\nfunction parseImportmapJson() {\n  return JSON.parse(document.querySelector(\"script[type=importmap]\").text).imports\n}\n\nfunction registerControllerFromPath(path, under, application) {\n  const name = path\n    .replace(new RegExp(`^${under}/`), \"\")\n    .replace(\"_controller\", \"\")\n    .replace(/\\//g, \"--\")\n    .replace(/_/g, \"-\")\n\n  if (canRegisterController(name, application)) {\n    import(path)\n      .then(module => registerController(name, module, application))\n      .catch(error => console.error(`Failed to register controller: ${name} (${path})`, error))\n  }\n}\n\n\n// Lazy load controllers registered beneath the `under` path in the import map to the passed application instance.\nexport function lazyLoadControllersFrom(under, application, element = document) {\n  lazyLoadExistingControllers(under, application, element)\n  lazyLoadNewControllers(under, application, element)\n}\n\nfunction lazyLoadExistingControllers(under, application, element) {\n  queryControllerNamesWithin(element).forEach(controllerName => loadController(controllerName, under, application))\n}\n\nfunction lazyLoadNewControllers(under, application, element) {\n  new MutationObserver((mutationsList) => {\n    for (const { attributeName, target, type } of mutationsList) {\n      switch (type) {\n        case \"attributes\": {\n          if (attributeName == controllerAttribute && target.getAttribute(controllerAttribute)) {\n            extractControllerNamesFrom(target).forEach(controllerName => loadController(controllerName, under, application))\n          }\n        }\n\n        case \"childList\": {\n          lazyLoadExistingControllers(under, application, target)\n        }\n      }\n    }\n  }).observe(element, { attributeFilter: [controllerAttribute], subtree: true, childList: true })\n}\n\nfunction queryControllerNamesWithin(element) {\n  return Array.from(element.querySelectorAll(`[${controllerAttribute}]`)).map(extractControllerNamesFrom).flat()\n}\n\nfunction extractControllerNamesFrom(element) {\n  return element.getAttribute(controllerAttribute).split(/\\s+/).filter(content => content.length)\n}\n\nfunction loadController(name, under, application) {\n  if (canRegisterController(name, application)) {\n    import(controllerFilename(name, under))\n      .then(module => registerController(name, module, application))\n      .catch(error => console.error(`Failed to autoload controller: ${name}`, error))\n  }\n}\n\nfunction controllerFilename(name, under) {\n  return `${under}/${name.replace(/--/g, \"/\").replace(/-/g, \"_\")}_controller`\n}\n\nfunction registerController(name, module, application) {\n  if (canRegisterController(name, application)) {\n    application.register(name, module.default)\n  }\n}\n\nfunction canRegisterController(name, application){\n  return !application.router.modulesByIdentifier.has(name)\n}\n"},{"path":"static/assets/stimulus.min.js","content":"//= link ./stimulus-autoloader.js\n//= link ./stimulus-importmap-autoloader.js\n//= link ./stimulus-loading.js\nclass e{constructor(e,t,s){this.eventTarget=e,this.eventName=t,this.eventOptions=s,this.unorderedBindings=new Set}connect(){this.eventTarget.addEventListener(this.eventName,this,this.eventOptions)}disconnect(){this.eventTarget.removeEventListener(this.eventName,this,this.eventOptions)}bindingConnected(e){this.unorderedBindings.add(e)}bindingDisconnected(e){this.unorderedBindings.delete(e)}handleEvent(e){const t=function(e){if(\"immediatePropagationStopped\"in e)return e;{const{stopImmediatePropagation:t}=e;return Object.assign(e,{immediatePropagationStopped:!1,stopImmediatePropagation(){this.immediatePropagationStopped=!0,t.call(this)}})}}(e);for(const e of this.bindings){if(t.immediatePropagationStopped)break;e.handleEvent(t)}}hasBindings(){return this.unorderedBindings.size>0}get bindings(){return Array.from(this.unorderedBindings).sort(((e,t)=>{const s=e.index,r=t.index;return s<r?-1:s>r?1:0}))}}class t{constructor(e){this.application=e,this.eventListenerMaps=new Map,this.started=!1}start(){this.started||(this.started=!0,this.eventListeners.forEach((e=>e.connect())))}stop(){this.started&&(this.started=!1,this.eventListeners.forEach((e=>e.disconnect())))}get eventListeners(){return Array.from(this.eventListenerMaps.values()).reduce(((e,t)=>e.concat(Array.from(t.values()))),[])}bindingConnected(e){this.fetchEventListenerForBinding(e).bindingConnected(e)}bindingDisconnected(e,t=!1){this.fetchEventListenerForBinding(e).bindingDisconnected(e),t&&this.clearEventListenersForBinding(e)}handleError(e,t,s={}){this.application.handleError(e,`Error ${t}`,s)}clearEventListenersForBinding(e){const t=this.fetchEventListenerForBinding(e);t.hasBindings()||(t.disconnect(),this.removeMappedEventListenerFor(e))}removeMappedEventListenerFor(e){const{eventTarget:t,eventName:s,eventOptions:r}=e,n=this.fetchEventListenerMapForEventTarget(t),i=this.cacheKey(s,r);n.delete(i),0==n.size&&this.eventListenerMaps.delete(t)}fetchEventListenerForBinding(e){const{eventTarget:t,eventName:s,eventOptions:r}=e;return this.fetchEventListener(t,s,r)}fetchEventListener(e,t,s){const r=this.fetchEventListenerMapForEventTarget(e),n=this.cacheKey(t,s);let i=r.get(n);return i||(i=this.createEventListener(e,t,s),r.set(n,i)),i}createEventListener(t,s,r){const n=new e(t,s,r);return this.started&&n.connect(),n}fetchEventListenerMapForEventTarget(e){let t=this.eventListenerMaps.get(e);return t||(t=new Map,this.eventListenerMaps.set(e,t)),t}cacheKey(e,t){const s=[e];return Object.keys(t).sort().forEach((e=>{s.push(`${t[e]?\"\":\"!\"}${e}`)})),s.join(\":\")}}const s={stop:({event:e,value:t})=>(t&&e.stopPropagation(),!0),prevent:({event:e,value:t})=>(t&&e.preventDefault(),!0),self:({event:e,value:t,element:s})=>!t||s===e.target},r=/^(?:(?:([^.]+?)\\+)?(.+?)(?:\\.(.+?))?(?:@(window|document))?->)?(.+?)(?:#([^:]+?))(?::(.+))?$/;function n(e){return\"window\"==e?window:\"document\"==e?document:void 0}function i(e){return e.replace(/(?:[_-])([a-z0-9])/g,((e,t)=>t.toUpperCase()))}function o(e){return i(e.replace(/--/g,\"-\").replace(/__/g,\"_\"))}function a(e){return e.charAt(0).toUpperCase()+e.slice(1)}function c(e){return e.replace(/([A-Z])/g,((e,t)=>`-${t.toLowerCase()}`))}function l(e){return null!=e}function h(e,t){return Object.prototype.hasOwnProperty.call(e,t)}const u=[\"meta\",\"ctrl\",\"alt\",\"shift\"];const d={a:()=>\"click\",button:()=>\"click\",form:()=>\"submit\",details:()=>\"toggle\",input:e=>\"submit\"==e.getAttribute(\"type\")?\"click\":\"input\",select:()=>\"change\",textarea:()=>\"input\"};function m(e){throw new Error(e)}function g(e){try{return JSON.parse(e)}catch(t){return e}}class p{constructor(e,t){this.context=e,this.action=t}get index(){return this.action.index}get eventTarget(){return this.action.eventTarget}get eventOptions(){return this.action.eventOptions}get identifier(){return this.context.identifier}handleEvent(e){const t=this.prepareActionEvent(e);this.willBeInvokedByEvent(e)&&this.applyEventModifiers(t)&&this.invokeWithEvent(t)}get eventName(){return this.action.eventName}get method(){const e=this.controller[this.methodName];if(\"function\"==typeof e)return e;throw new Error(`Action \"${this.action}\" references undefined method \"${this.methodName}\"`)}applyEventModifiers(e){const{element:t}=this.action,{actionDescriptorFilters:s}=this.context.application,{controller:r}=this.context;let n=!0;for(const[i,o]of Object.entries(this.eventOptions))if(i in s){const a=s[i];n=n&&a({name:i,value:o,event:e,element:t,controller:r})}return n}prepareActionEvent(e){return Object.assign(e,{params:this.action.params})}invokeWithEvent(e){const{target:t,currentTarget:s}=e;try{this.method.call(this.controller,e),this.context.logDebugActivity(this.methodName,{event:e,target:t,currentTarget:s,action:this.methodName})}catch(t){const{identifier:s,controller:r,element:n,index:i}=this,o={identifier:s,controller:r,element:n,index:i,event:e};this.context.handleError(t,`invoking action \"${this.action}\"`,o)}}willBeInvokedByEvent(e){const t=e.target;return!(e instanceof KeyboardEvent&&this.action.shouldIgnoreKeyboardEvent(e))&&(!(e instanceof MouseEvent&&this.action.shouldIgnoreMouseEvent(e))&&(this.element===t||(t instanceof Element&&this.element.contains(t)?this.scope.containsElement(t):this.scope.containsElement(this.action.element))))}get controller(){return this.context.controller}get methodName(){return this.action.methodName}get element(){return this.scope.element}get scope(){return this.context.scope}}class f{constructor(e,t){this.mutationObserverInit={attributes:!0,childList:!0,subtree:!0},this.element=e,this.started=!1,this.delegate=t,this.elements=new Set,this.mutationObserver=new MutationObserver((e=>this.processMutations(e)))}start(){this.started||(this.started=!0,this.mutationObserver.observe(this.element,this.mutationObserverInit),this.refresh())}pause(e){this.started&&(this.mutationObserver.disconnect(),this.started=!1),e(),this.started||(this.mutationObserver.observe(this.element,this.mutationObserverInit),this.started=!0)}stop(){this.started&&(this.mutationObserver.takeRecords(),this.mutationObserver.disconnect(),this.started=!1)}refresh(){if(this.started){const e=new Set(this.matchElementsInTree());for(const t of Array.from(this.elements))e.has(t)||this.removeElement(t);for(const t of Array.from(e))this.addElement(t)}}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){\"attributes\"==e.type?this.processAttributeChange(e.target,e.attributeName):\"childList\"==e.type&&(this.processRemovedNodes(e.removedNodes),this.processAddedNodes(e.addedNodes))}processAttributeChange(e,t){this.elements.has(e)?this.delegate.elementAttributeChanged&&this.matchElement(e)?this.delegate.elementAttributeChanged(e,t):this.removeElement(e):this.matchElement(e)&&this.addElement(e)}processRemovedNodes(e){for(const t of Array.from(e)){const e=this.elementFromNode(t);e&&this.processTree(e,this.removeElement)}}processAddedNodes(e){for(const t of Array.from(e)){const e=this.elementFromNode(t);e&&this.elementIsActive(e)&&this.processTree(e,this.addElement)}}matchElement(e){return this.delegate.matchElement(e)}matchElementsInTree(e=this.element){return this.delegate.matchElementsInTree(e)}processTree(e,t){for(const s of this.matchElementsInTree(e))t.call(this,s)}elementFromNode(e){if(e.nodeType==Node.ELEMENT_NODE)return e}elementIsActive(e){return e.isConnected==this.element.isConnected&&this.element.contains(e)}addElement(e){this.elements.has(e)||this.elementIsActive(e)&&(this.elements.add(e),this.delegate.elementMatched&&this.delegate.elementMatched(e))}removeElement(e){this.elements.has(e)&&(this.elements.delete(e),this.delegate.elementUnmatched&&this.delegate.elementUnmatched(e))}}class b{constructor(e,t,s){this.attributeName=t,this.delegate=s,this.elementObserver=new f(e,this)}get element(){return this.elementObserver.element}get selector(){return`[${this.attributeName}]`}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get started(){return this.elementObserver.started}matchElement(e){return e.hasAttribute(this.attributeName)}matchElementsInTree(e){const t=this.matchElement(e)?[e]:[],s=Array.from(e.querySelectorAll(this.selector));return t.concat(s)}elementMatched(e){this.delegate.elementMatchedAttribute&&this.delegate.elementMatchedAttribute(e,this.attributeName)}elementUnmatched(e){this.delegate.elementUnmatchedAttribute&&this.delegate.elementUnmatchedAttribute(e,this.attributeName)}elementAttributeChanged(e,t){this.delegate.elementAttributeValueChanged&&this.attributeName==t&&this.delegate.elementAttributeValueChanged(e,t)}}function v(e,t,s){O(e,t).add(s)}function y(e,t,s){O(e,t).delete(s),A(e,t)}function O(e,t){let s=e.get(t);return s||(s=new Set,e.set(t,s)),s}function A(e,t){const s=e.get(t);null!=s&&0==s.size&&e.delete(t)}class E{constructor(){this.valuesByKey=new Map}get keys(){return Array.from(this.valuesByKey.keys())}get values(){return Array.from(this.valuesByKey.values()).reduce(((e,t)=>e.concat(Array.from(t))),[])}get size(){return Array.from(this.valuesByKey.values()).reduce(((e,t)=>e+t.size),0)}add(e,t){v(this.valuesByKey,e,t)}delete(e,t){y(this.valuesByKey,e,t)}has(e,t){const s=this.valuesByKey.get(e);return null!=s&&s.has(t)}hasKey(e){return this.valuesByKey.has(e)}hasValue(e){return Array.from(this.valuesByKey.values()).some((t=>t.has(e)))}getValuesForKey(e){const t=this.valuesByKey.get(e);return t?Array.from(t):[]}getKeysForValue(e){return Array.from(this.valuesByKey).filter((([t,s])=>s.has(e))).map((([e,t])=>e))}}class w extends E{constructor(){super(),this.keysByValue=new Map}get values(){return Array.from(this.keysByValue.keys())}add(e,t){super.add(e,t),v(this.keysByValue,t,e)}delete(e,t){super.delete(e,t),y(this.keysByValue,t,e)}hasValue(e){return this.keysByValue.has(e)}getKeysForValue(e){const t=this.keysByValue.get(e);return t?Array.from(t):[]}}class M{constructor(e,t,s,r){this._selector=t,this.details=r,this.elementObserver=new f(e,this),this.delegate=s,this.matchesByElement=new E}get started(){return this.elementObserver.started}get selector(){return this._selector}set selector(e){this._selector=e,this.refresh()}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get element(){return this.elementObserver.element}matchElement(e){const{selector:t}=this;if(t){const s=e.matches(t);return this.delegate.selectorMatchElement?s&&this.delegate.selectorMatchElement(e,this.details):s}return!1}matchElementsInTree(e){const{selector:t}=this;if(t){const s=this.matchElement(e)?[e]:[],r=Array.from(e.querySelectorAll(t)).filter((e=>this.matchElement(e)));return s.concat(r)}return[]}elementMatched(e){const{selector:t}=this;t&&this.selectorMatched(e,t)}elementUnmatched(e){const t=this.matchesByElement.getKeysForValue(e);for(const s of t)this.selectorUnmatched(e,s)}elementAttributeChanged(e,t){const{selector:s}=this;if(s){const t=this.matchElement(e),r=this.matchesByElement.has(s,e);t&&!r?this.selectorMatched(e,s):!t&&r&&this.selectorUnmatched(e,s)}}selectorMatched(e,t){this.delegate.selectorMatched(e,t,this.details),this.matchesByElement.add(t,e)}selectorUnmatched(e,t){this.delegate.selectorUnmatched(e,t,this.details),this.matchesByElement.delete(t,e)}}class k{constructor(e,t){this.element=e,this.delegate=t,this.started=!1,this.stringMap=new Map,this.mutationObserver=new MutationObserver((e=>this.processMutations(e)))}start(){this.started||(this.started=!0,this.mutationObserver.observe(this.element,{attributes:!0,attributeOldValue:!0}),this.refresh())}stop(){this.started&&(this.mutationObserver.takeRecords(),this.mutationObserver.disconnect(),this.started=!1)}refresh(){if(this.started)for(const e of this.knownAttributeNames)this.refreshAttribute(e,null)}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){const t=e.attributeName;t&&this.refreshAttribute(t,e.oldValue)}refreshAttribute(e,t){const s=this.delegate.getStringMapKeyForAttribute(e);if(null!=s){this.stringMap.has(e)||this.stringMapKeyAdded(s,e);const r=this.element.getAttribute(e);if(this.stringMap.get(e)!=r&&this.stringMapValueChanged(r,s,t),null==r){const t=this.stringMap.get(e);this.stringMap.delete(e),t&&this.stringMapKeyRemoved(s,e,t)}else this.stringMap.set(e,r)}}stringMapKeyAdded(e,t){this.delegate.stringMapKeyAdded&&this.delegate.stringMapKeyAdded(e,t)}stringMapValueChanged(e,t,s){this.delegate.stringMapValueChanged&&this.delegate.stringMapValueChanged(e,t,s)}stringMapKeyRemoved(e,t,s){this.delegate.stringMapKeyRemoved&&this.delegate.stringMapKeyRemoved(e,t,s)}get knownAttributeNames(){return Array.from(new Set(this.currentAttributeNames.concat(this.recordedAttributeNames)))}get currentAttributeNames(){return Array.from(this.element.attributes).map((e=>e.name))}get recordedAttributeNames(){return Array.from(this.stringMap.keys())}}class N{constructor(e,t,s){this.attributeObserver=new b(e,t,this),this.delegate=s,this.tokensByElement=new E}get started(){return this.attributeObserver.started}start(){this.attributeObserver.start()}pause(e){this.attributeObserver.pause(e)}stop(){this.attributeObserver.stop()}refresh(){this.attributeObserver.refresh()}get element(){return this.attributeObserver.element}get attributeName(){return this.attributeObserver.attributeName}elementMatchedAttribute(e){this.tokensMatched(this.readTokensForElement(e))}elementAttributeValueChanged(e){const[t,s]=this.refreshTokensForElement(e);this.tokensUnmatched(t),this.tokensMatched(s)}elementUnmatchedAttribute(e){this.tokensUnmatched(this.tokensByElement.getValuesForKey(e))}tokensMatched(e){e.forEach((e=>this.tokenMatched(e)))}tokensUnmatched(e){e.forEach((e=>this.tokenUnmatched(e)))}tokenMatched(e){this.delegate.tokenMatched(e),this.tokensByElement.add(e.element,e)}tokenUnmatched(e){this.delegate.tokenUnmatched(e),this.tokensByElement.delete(e.element,e)}refreshTokensForElement(e){const t=this.tokensByElement.getValuesForKey(e),s=this.readTokensForElement(e),r=function(e,t){const s=Math.max(e.length,t.length);return Array.from({length:s},((s,r)=>[e[r],t[r]]))}(t,s).findIndex((([e,t])=>{return r=t,!((s=e)&&r&&s.index==r.index&&s.content==r.content);var s,r}));return-1==r?[[],[]]:[t.slice(r),s.slice(r)]}readTokensForElement(e){const t=this.attributeName;return function(e,t,s){return e.trim().split(/\\s+/).filter((e=>e.length)).map(((e,r)=>({element:t,attributeName:s,content:e,index:r})))}(e.getAttribute(t)||\"\",e,t)}}class F{constructor(e,t,s){this.tokenListObserver=new N(e,t,this),this.delegate=s,this.parseResultsByToken=new WeakMap,this.valuesByTokenByElement=new WeakMap}get started(){return this.tokenListObserver.started}start(){this.tokenListObserver.start()}stop(){this.tokenListObserver.stop()}refresh(){this.tokenListObserver.refresh()}get element(){return this.tokenListObserver.element}get attributeName(){return this.tokenListObserver.attributeName}tokenMatched(e){const{element:t}=e,{value:s}=this.fetchParseResultForToken(e);s&&(this.fetchValuesByTokenForElement(t).set(e,s),this.delegate.elementMatchedValue(t,s))}tokenUnmatched(e){const{element:t}=e,{value:s}=this.fetchParseResultForToken(e);s&&(this.fetchValuesByTokenForElement(t).delete(e),this.delegate.elementUnmatchedValue(t,s))}fetchParseResultForToken(e){let t=this.parseResultsByToken.get(e);return t||(t=this.parseToken(e),this.parseResultsByToken.set(e,t)),t}fetchValuesByTokenForElement(e){let t=this.valuesByTokenByElement.get(e);return t||(t=new Map,this.valuesByTokenByElement.set(e,t)),t}parseToken(e){try{return{value:this.delegate.parseValueForToken(e)}}catch(e){return{error:e}}}}class B{constructor(e,t){this.context=e,this.delegate=t,this.bindingsByAction=new Map}start(){this.valueListObserver||(this.valueListObserver=new F(this.element,this.actionAttribute,this),this.valueListObserver.start())}stop(){this.valueListObserver&&(this.valueListObserver.stop(),delete this.valueListObserver,this.disconnectAllActions())}get element(){return this.context.element}get identifier(){return this.context.identifier}get actionAttribute(){return this.schema.actionAttribute}get schema(){return this.context.schema}get bindings(){return Array.from(this.bindingsByAction.values())}connectAction(e){const t=new p(this.context,e);this.bindingsByAction.set(e,t),this.delegate.bindingConnected(t)}disconnectAction(e){const t=this.bindingsByAction.get(e);t&&(this.bindingsByAction.delete(e),this.delegate.bindingDisconnected(t))}disconnectAllActions(){this.bindings.forEach((e=>this.delegate.bindingDisconnected(e,!0))),this.bindingsByAction.clear()}parseValueForToken(e){const t=class{constructor(e,t,s,r){this.element=e,this.index=t,this.eventTarget=s.eventTarget||e,this.eventName=s.eventName||function(e){const t=e.tagName.toLowerCase();if(t in d)return d[t](e)}(e)||m(\"missing event name\"),this.eventOptions=s.eventOptions||{},this.identifier=s.identifier||m(\"missing identifier\"),this.methodName=s.methodName||m(\"missing method name\"),this.keyFilter=s.keyFilter||\"\",this.schema=r}static forToken(e,t){return new this(e.element,e.index,function(e){const t=e.trim().match(r)||[];let s=t[2],i=t[3];return i&&![\"keydown\",\"keyup\",\"keypress\"].includes(s)&&(s+=`.${i}`,i=\"\"),{eventTarget:n(t[4]),eventName:s,eventOptions:t[7]?(o=t[7],o.split(\":\").reduce(((e,t)=>Object.assign(e,{[t.replace(/^!/,\"\")]:!/^!/.test(t)})),{})):{},identifier:t[5],methodName:t[6],keyFilter:t[1]||i};var o}(e.content),t)}toString(){const e=this.keyFilter?`.${this.keyFilter}`:\"\",t=this.eventTargetName?`@${this.eventTargetName}`:\"\";return`${this.eventName}${e}${t}->${this.identifier}#${this.methodName}`}shouldIgnoreKeyboardEvent(e){if(!this.keyFilter)return!1;const t=this.keyFilter.split(\"+\");if(this.keyFilterDissatisfied(e,t))return!0;const s=t.filter((e=>!u.includes(e)))[0];return!!s&&(h(this.keyMappings,s)||m(`contains unknown key filter: ${this.keyFilter}`),this.keyMappings[s].toLowerCase()!==e.key.toLowerCase())}shouldIgnoreMouseEvent(e){if(!this.keyFilter)return!1;const t=[this.keyFilter];return!!this.keyFilterDissatisfied(e,t)}get params(){const e={},t=new RegExp(`^data-${this.identifier}-(.+)-param$`,\"i\");for(const{name:s,value:r}of Array.from(this.element.attributes)){const n=s.match(t),o=n&&n[1];o&&(e[i(o)]=g(r))}return e}get eventTargetName(){return(e=this.eventTarget)==window?\"window\":e==document?\"document\":void 0;var e}get keyMappings(){return this.schema.keyMappings}keyFilterDissatisfied(e,t){const[s,r,n,i]=u.map((e=>t.includes(e)));return e.metaKey!==s||e.ctrlKey!==r||e.altKey!==n||e.shiftKey!==i}}.forToken(e,this.schema);if(t.identifier==this.identifier)return t}elementMatchedValue(e,t){this.connectAction(t)}elementUnmatchedValue(e,t){this.disconnectAction(t)}}class C{constructor(e,t){this.context=e,this.receiver=t,this.stringMapObserver=new k(this.element,this),this.valueDescriptorMap=this.controller.valueDescriptorMap}start(){this.stringMapObserver.start(),this.invokeChangedCallbacksForDefaultValues()}stop(){this.stringMapObserver.stop()}get element(){return this.context.element}get controller(){return this.context.controller}getStringMapKeyForAttribute(e){if(e in this.valueDescriptorMap)return this.valueDescriptorMap[e].name}stringMapKeyAdded(e,t){const s=this.valueDescriptorMap[t];this.hasValue(e)||this.invokeChangedCallback(e,s.writer(this.receiver[e]),s.writer(s.defaultValue))}stringMapValueChanged(e,t,s){const r=this.valueDescriptorNameMap[t];null!==e&&(null===s&&(s=r.writer(r.defaultValue)),this.invokeChangedCallback(t,e,s))}stringMapKeyRemoved(e,t,s){const r=this.valueDescriptorNameMap[e];this.hasValue(e)?this.invokeChangedCallback(e,r.writer(this.receiver[e]),s):this.invokeChangedCallback(e,r.writer(r.defaultValue),s)}invokeChangedCallbacksForDefaultValues(){for(const{key:e,name:t,defaultValue:s,writer:r}of this.valueDescriptors)null==s||this.controller.data.has(e)||this.invokeChangedCallback(t,r(s),void 0)}invokeChangedCallback(e,t,s){const r=`${e}Changed`,n=this.receiver[r];if(\"function\"==typeof n){const r=this.valueDescriptorNameMap[e];try{const e=r.reader(t);let i=s;s&&(i=r.reader(s)),n.call(this.receiver,e,i)}catch(e){throw e instanceof TypeError&&(e.message=`Stimulus Value \"${this.context.identifier}.${r.name}\" - ${e.message}`),e}}}get valueDescriptors(){const{valueDescriptorMap:e}=this;return Object.keys(e).map((t=>e[t]))}get valueDescriptorNameMap(){const e={};return Object.keys(this.valueDescriptorMap).forEach((t=>{const s=this.valueDescriptorMap[t];e[s.name]=s})),e}hasValue(e){const t=`has${a(this.valueDescriptorNameMap[e].name)}`;return this.receiver[t]}}class ${constructor(e,t){this.context=e,this.delegate=t,this.targetsByName=new E}start(){this.tokenListObserver||(this.tokenListObserver=new N(this.element,this.attributeName,this),this.tokenListObserver.start())}stop(){this.tokenListObserver&&(this.disconnectAllTargets(),this.tokenListObserver.stop(),delete this.tokenListObserver)}tokenMatched({element:e,content:t}){this.scope.containsElement(e)&&this.connectTarget(e,t)}tokenUnmatched({element:e,content:t}){this.disconnectTarget(e,t)}connectTarget(e,t){var s;this.targetsByName.has(t,e)||(this.targetsByName.add(t,e),null===(s=this.tokenListObserver)||void 0===s||s.pause((()=>this.delegate.targetConnected(e,t))))}disconnectTarget(e,t){var s;this.targetsByName.has(t,e)&&(this.targetsByName.delete(t,e),null===(s=this.tokenListObserver)||void 0===s||s.pause((()=>this.delegate.targetDisconnected(e,t))))}disconnectAllTargets(){for(const e of this.targetsByName.keys)for(const t of this.targetsByName.getValuesForKey(e))this.disconnectTarget(t,e)}get attributeName(){return`data-${this.context.identifier}-target`}get element(){return this.context.element}get scope(){return this.context.scope}}function T(e,t){const s=x(e);return Array.from(s.reduce(((e,s)=>(function(e,t){const s=e[t];return Array.isArray(s)?s:[]}(s,t).forEach((t=>e.add(t))),e)),new Set))}function S(e,t){return x(e).reduce(((e,s)=>(e.push(...function(e,t){const s=e[t];return s?Object.keys(s).map((e=>[e,s[e]])):[]}(s,t)),e)),[])}function x(e){const t=[];for(;e;)t.push(e),e=Object.getPrototypeOf(e);return t.reverse()}class D{constructor(e,t){this.started=!1,this.context=e,this.delegate=t,this.outletsByName=new E,this.outletElementsByName=new E,this.selectorObserverMap=new Map,this.attributeObserverMap=new Map}start(){this.started||(this.outletDefinitions.forEach((e=>{this.setupSelectorObserverForOutlet(e),this.setupAttributeObserverForOutlet(e)})),this.started=!0,this.dependentContexts.forEach((e=>e.refresh())))}refresh(){this.selectorObserverMap.forEach((e=>e.refresh())),this.attributeObserverMap.forEach((e=>e.refresh()))}stop(){this.started&&(this.started=!1,this.disconnectAllOutlets(),this.stopSelectorObservers(),this.stopAttributeObservers())}stopSelectorObservers(){this.selectorObserverMap.size>0&&(this.selectorObserverMap.forEach((e=>e.stop())),this.selectorObserverMap.clear())}stopAttributeObservers(){this.attributeObserverMap.size>0&&(this.attributeObserverMap.forEach((e=>e.stop())),this.attributeObserverMap.clear())}selectorMatched(e,t,{outletName:s}){const r=this.getOutlet(e,s);r&&this.connectOutlet(r,e,s)}selectorUnmatched(e,t,{outletName:s}){const r=this.getOutletFromMap(e,s);r&&this.disconnectOutlet(r,e,s)}selectorMatchElement(e,{outletName:t}){const s=this.selector(t),r=this.hasOutlet(e,t),n=e.matches(`[${this.schema.controllerAttribute}~=${t}]`);return!!s&&(r&&n&&e.matches(s))}elementMatchedAttribute(e,t){const s=this.getOutletNameFromOutletAttributeName(t);s&&this.updateSelectorObserverForOutlet(s)}elementAttributeValueChanged(e,t){const s=this.getOutletNameFromOutletAttributeName(t);s&&this.updateSelectorObserverForOutlet(s)}elementUnmatchedAttribute(e,t){const s=this.getOutletNameFromOutletAttributeName(t);s&&this.updateSelectorObserverForOutlet(s)}connectOutlet(e,t,s){var r;this.outletElementsByName.has(s,t)||(this.outletsByName.add(s,e),this.outletElementsByName.add(s,t),null===(r=this.selectorObserverMap.get(s))||void 0===r||r.pause((()=>this.delegate.outletConnected(e,t,s))))}disconnectOutlet(e,t,s){var r;this.outletElementsByName.has(s,t)&&(this.outletsByName.delete(s,e),this.outletElementsByName.delete(s,t),null===(r=this.selectorObserverMap.get(s))||void 0===r||r.pause((()=>this.delegate.outletDisconnected(e,t,s))))}disconnectAllOutlets(){for(const e of this.outletElementsByName.keys)for(const t of this.outletElementsByName.getValuesForKey(e))for(const s of this.outletsByName.getValuesForKey(e))this.disconnectOutlet(s,t,e)}updateSelectorObserverForOutlet(e){const t=this.selectorObserverMap.get(e);t&&(t.selector=this.selector(e))}setupSelectorObserverForOutlet(e){const t=this.selector(e),s=new M(document.body,t,this,{outletName:e});this.selectorObserverMap.set(e,s),s.start()}setupAttributeObserverForOutlet(e){const t=this.attributeNameForOutletName(e),s=new b(this.scope.element,t,this);this.attributeObserverMap.set(e,s),s.start()}selector(e){return this.scope.outlets.getSelectorForOutletName(e)}attributeNameForOutletName(e){return this.scope.schema.outletAttributeForScope(this.identifier,e)}getOutletNameFromOutletAttributeName(e){return this.outletDefinitions.find((t=>this.attributeNameForOutletName(t)===e))}get outletDependencies(){const e=new E;return this.router.modules.forEach((t=>{T(t.definition.controllerConstructor,\"outlets\").forEach((s=>e.add(s,t.identifier)))})),e}get outletDefinitions(){return this.outletDependencies.getKeysForValue(this.identifier)}get dependentControllerIdentifiers(){return this.outletDependencies.getValuesForKey(this.identifier)}get dependentContexts(){const e=this.dependentControllerIdentifiers;return this.router.contexts.filter((t=>e.includes(t.identifier)))}hasOutlet(e,t){return!!this.getOutlet(e,t)||!!this.getOutletFromMap(e,t)}getOutlet(e,t){return this.application.getControllerForElementAndIdentifier(e,t)}getOutletFromMap(e,t){return this.outletsByName.getValuesForKey(t).find((t=>t.element===e))}get scope(){return this.context.scope}get schema(){return this.context.schema}get identifier(){return this.context.identifier}get application(){return this.context.application}get router(){return this.application.router}}class L{constructor(e,t){this.logDebugActivity=(e,t={})=>{const{identifier:s,controller:r,element:n}=this;t=Object.assign({identifier:s,controller:r,element:n},t),this.application.logDebugActivity(this.identifier,e,t)},this.module=e,this.scope=t,this.controller=new e.controllerConstructor(this),this.bindingObserver=new B(this,this.dispatcher),this.valueObserver=new C(this,this.controller),this.targetObserver=new $(this,this),this.outletObserver=new D(this,this);try{this.controller.initialize(),this.logDebugActivity(\"initialize\")}catch(e){this.handleError(e,\"initializing controller\")}}connect(){this.bindingObserver.start(),this.valueObserver.start(),this.targetObserver.start(),this.outletObserver.start();try{this.controller.connect(),this.logDebugActivity(\"connect\")}catch(e){this.handleError(e,\"connecting controller\")}}refresh(){this.outletObserver.refresh()}disconnect(){try{this.controller.disconnect(),this.logDebugActivity(\"disconnect\")}catch(e){this.handleError(e,\"disconnecting controller\")}this.outletObserver.stop(),this.targetObserver.stop(),this.valueObserver.stop(),this.bindingObserver.stop()}get application(){return this.module.application}get identifier(){return this.module.identifier}get schema(){return this.application.schema}get dispatcher(){return this.application.dispatcher}get element(){return this.scope.element}get parentElement(){return this.element.parentElement}handleError(e,t,s={}){const{identifier:r,controller:n,element:i}=this;s=Object.assign({identifier:r,controller:n,element:i},s),this.application.handleError(e,`Error ${t}`,s)}targetConnected(e,t){this.invokeControllerMethod(`${t}TargetConnected`,e)}targetDisconnected(e,t){this.invokeControllerMethod(`${t}TargetDisconnected`,e)}outletConnected(e,t,s){this.invokeControllerMethod(`${o(s)}OutletConnected`,e,t)}outletDisconnected(e,t,s){this.invokeControllerMethod(`${o(s)}OutletDisconnected`,e,t)}invokeControllerMethod(e,...t){const s=this.controller;\"function\"==typeof s[e]&&s[e](...t)}}function V(e){return function(e,t){const s=I(e),r=function(e,t){return K(t).reduce(((s,r)=>{const n=function(e,t,s){const r=Object.getOwnPropertyDescriptor(e,s);if(!r||!(\"value\"in r)){const e=Object.getOwnPropertyDescriptor(t,s).value;return r&&(e.get=r.get||e.get,e.set=r.set||e.set),e}}(e,t,r);return n&&Object.assign(s,{[r]:n}),s}),{})}(e.prototype,t);return Object.defineProperties(s.prototype,r),s}(e,function(e){return T(e,\"blessings\").reduce(((t,s)=>{const r=s(e);for(const e in r){const s=t[e]||{};t[e]=Object.assign(s,r[e])}return t}),{})}(e))}const K=\"function\"==typeof Object.getOwnPropertySymbols?e=>[...Object.getOwnPropertyNames(e),...Object.getOwnPropertySymbols(e)]:Object.getOwnPropertyNames,I=(()=>{function e(e){function t(){return Reflect.construct(e,arguments,new.target)}return t.prototype=Object.create(e.prototype,{constructor:{value:t}}),Reflect.setPrototypeOf(t,e),t}try{return function(){const t=e((function(){this.a.call(this)}));t.prototype.a=function(){},new t}(),e}catch(e){return e=>class extends e{}}})();class j{constructor(e,t){this.application=e,this.definition=function(e){return{identifier:e.identifier,controllerConstructor:V(e.controllerConstructor)}}(t),this.contextsByScope=new WeakMap,this.connectedContexts=new Set}get identifier(){return this.definition.identifier}get controllerConstructor(){return this.definition.controllerConstructor}get contexts(){return Array.from(this.connectedContexts)}connectContextForScope(e){const t=this.fetchContextForScope(e);this.connectedContexts.add(t),t.connect()}disconnectContextForScope(e){const t=this.contextsByScope.get(e);t&&(this.connectedContexts.delete(t),t.disconnect())}fetchContextForScope(e){let t=this.contextsByScope.get(e);return t||(t=new L(this,e),this.contextsByScope.set(e,t)),t}}class U{constructor(e){this.scope=e}has(e){return this.data.has(this.getDataKey(e))}get(e){return this.getAll(e)[0]}getAll(e){const t=this.data.get(this.getDataKey(e))||\"\";return t.match(/[^\\s]+/g)||[]}getAttributeName(e){return this.data.getAttributeNameForKey(this.getDataKey(e))}getDataKey(e){return`${e}-class`}get data(){return this.scope.data}}class P{constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get(e){const t=this.getAttributeNameForKey(e);return this.element.getAttribute(t)}set(e,t){const s=this.getAttributeNameForKey(e);return this.element.setAttribute(s,t),this.get(e)}has(e){const t=this.getAttributeNameForKey(e);return this.element.hasAttribute(t)}delete(e){if(this.has(e)){const t=this.getAttributeNameForKey(e);return this.element.removeAttribute(t),!0}return!1}getAttributeNameForKey(e){return`data-${this.identifier}-${c(e)}`}}class R{constructor(e){this.warnedKeysByObject=new WeakMap,this.logger=e}warn(e,t,s){let r=this.warnedKeysByObject.get(e);r||(r=new Set,this.warnedKeysByObject.set(e,r)),r.has(t)||(r.add(t),this.logger.warn(s,e))}}function z(e,t){return`[${e}~=\"${t}\"]`}class _{constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return null!=this.find(e)}find(...e){return e.reduce(((e,t)=>e||this.findTarget(t)||this.findLegacyTarget(t)),void 0)}findAll(...e){return e.reduce(((e,t)=>[...e,...this.findAllTargets(t),...this.findAllLegacyTargets(t)]),[])}findTarget(e){const t=this.getSelectorForTargetName(e);return this.scope.findElement(t)}findAllTargets(e){const t=this.getSelectorForTargetName(e);return this.scope.findAllElements(t)}getSelectorForTargetName(e){return z(this.schema.targetAttributeForScope(this.identifier),e)}findLegacyTarget(e){const t=this.getLegacySelectorForTargetName(e);return this.deprecate(this.scope.findElement(t),e)}findAllLegacyTargets(e){const t=this.getLegacySelectorForTargetName(e);return this.scope.findAllElements(t).map((t=>this.deprecate(t,e)))}getLegacySelectorForTargetName(e){const t=`${this.identifier}.${e}`;return z(this.schema.targetAttribute,t)}deprecate(e,t){if(e){const{identifier:s}=this,r=this.schema.targetAttribute,n=this.schema.targetAttributeForScope(s);this.guide.warn(e,`target:${t}`,`Please replace ${r}=\"${s}.${t}\" with ${n}=\"${t}\". The ${r} attribute is deprecated and will be removed in a future version of Stimulus.`)}return e}get guide(){return this.scope.guide}}class q{constructor(e,t){this.scope=e,this.controllerElement=t}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return null!=this.find(e)}find(...e){return e.reduce(((e,t)=>e||this.findOutlet(t)),void 0)}findAll(...e){return e.reduce(((e,t)=>[...e,...this.findAllOutlets(t)]),[])}getSelectorForOutletName(e){const t=this.schema.outletAttributeForScope(this.identifier,e);return this.controllerElement.getAttribute(t)}findOutlet(e){const t=this.getSelectorForOutletName(e);if(t)return this.findElement(t,e)}findAllOutlets(e){const t=this.getSelectorForOutletName(e);return t?this.findAllElements(t,e):[]}findElement(e,t){return this.scope.queryElements(e).filter((s=>this.matchesElement(s,e,t)))[0]}findAllElements(e,t){return this.scope.queryElements(e).filter((s=>this.matchesElement(s,e,t)))}matchesElement(e,t,s){const r=e.getAttribute(this.scope.schema.controllerAttribute)||\"\";return e.matches(t)&&r.split(\" \").includes(s)}}class W{constructor(e,t,s,r){this.targets=new _(this),this.classes=new U(this),this.data=new P(this),this.containsElement=e=>e.closest(this.controllerSelector)===this.element,this.schema=e,this.element=t,this.identifier=s,this.guide=new R(r),this.outlets=new q(this.documentScope,t)}findElement(e){return this.element.matches(e)?this.element:this.queryElements(e).find(this.containsElement)}findAllElements(e){return[...this.element.matches(e)?[this.element]:[],...this.queryElements(e).filter(this.containsElement)]}queryElements(e){return Array.from(this.element.querySelectorAll(e))}get controllerSelector(){return z(this.schema.controllerAttribute,this.identifier)}get isDocumentScope(){return this.element===document.documentElement}get documentScope(){return this.isDocumentScope?this:new W(this.schema,document.documentElement,this.identifier,this.guide.logger)}}class J{constructor(e,t,s){this.element=e,this.schema=t,this.delegate=s,this.valueListObserver=new F(this.element,this.controllerAttribute,this),this.scopesByIdentifierByElement=new WeakMap,this.scopeReferenceCounts=new WeakMap}start(){this.valueListObserver.start()}stop(){this.valueListObserver.stop()}get controllerAttribute(){return this.schema.controllerAttribute}parseValueForToken(e){const{element:t,content:s}=e;return this.parseValueForElementAndIdentifier(t,s)}parseValueForElementAndIdentifier(e,t){const s=this.fetchScopesByIdentifierForElement(e);let r=s.get(t);return r||(r=this.delegate.createScopeForElementAndIdentifier(e,t),s.set(t,r)),r}elementMatchedValue(e,t){const s=(this.scopeReferenceCounts.get(t)||0)+1;this.scopeReferenceCounts.set(t,s),1==s&&this.delegate.scopeConnected(t)}elementUnmatchedValue(e,t){const s=this.scopeReferenceCounts.get(t);s&&(this.scopeReferenceCounts.set(t,s-1),1==s&&this.delegate.scopeDisconnected(t))}fetchScopesByIdentifierForElement(e){let t=this.scopesByIdentifierByElement.get(e);return t||(t=new Map,this.scopesByIdentifierByElement.set(e,t)),t}}class H{constructor(e){this.application=e,this.scopeObserver=new J(this.element,this.schema,this),this.scopesByIdentifier=new E,this.modulesByIdentifier=new Map}get element(){return this.application.element}get schema(){return this.application.schema}get logger(){return this.application.logger}get controllerAttribute(){return this.schema.controllerAttribute}get modules(){return Array.from(this.modulesByIdentifier.values())}get contexts(){return this.modules.reduce(((e,t)=>e.concat(t.contexts)),[])}start(){this.scopeObserver.start()}stop(){this.scopeObserver.stop()}loadDefinition(e){this.unloadIdentifier(e.identifier);const t=new j(this.application,e);this.connectModule(t);const s=e.controllerConstructor.afterLoad;s&&s.call(e.controllerConstructor,e.identifier,this.application)}unloadIdentifier(e){const t=this.modulesByIdentifier.get(e);t&&this.disconnectModule(t)}getContextForElementAndIdentifier(e,t){const s=this.modulesByIdentifier.get(t);if(s)return s.contexts.find((t=>t.element==e))}proposeToConnectScopeForElementAndIdentifier(e,t){const s=this.scopeObserver.parseValueForElementAndIdentifier(e,t);s?this.scopeObserver.elementMatchedValue(s.element,s):console.error(`Couldn't find or create scope for identifier: \"${t}\" and element:`,e)}handleError(e,t,s){this.application.handleError(e,t,s)}createScopeForElementAndIdentifier(e,t){return new W(this.schema,e,t,this.logger)}scopeConnected(e){this.scopesByIdentifier.add(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.connectContextForScope(e)}scopeDisconnected(e){this.scopesByIdentifier.delete(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.disconnectContextForScope(e)}connectModule(e){this.modulesByIdentifier.set(e.identifier,e);this.scopesByIdentifier.getValuesForKey(e.identifier).forEach((t=>e.connectContextForScope(t)))}disconnectModule(e){this.modulesByIdentifier.delete(e.identifier);this.scopesByIdentifier.getValuesForKey(e.identifier).forEach((t=>e.disconnectContextForScope(t)))}}const Z={controllerAttribute:\"data-controller\",actionAttribute:\"data-action\",targetAttribute:\"data-target\",targetAttributeForScope:e=>`data-${e}-target`,outletAttributeForScope:(e,t)=>`data-${e}-${t}-outlet`,keyMappings:Object.assign(Object.assign({enter:\"Enter\",tab:\"Tab\",esc:\"Escape\",space:\" \",up:\"ArrowUp\",down:\"ArrowDown\",left:\"ArrowLeft\",right:\"ArrowRight\",home:\"Home\",end:\"End\",page_up:\"PageUp\",page_down:\"PageDown\"},G(\"abcdefghijklmnopqrstuvwxyz\".split(\"\").map((e=>[e,e])))),G(\"0123456789\".split(\"\").map((e=>[e,e]))))};function G(e){return e.reduce(((e,[t,s])=>Object.assign(Object.assign({},e),{[t]:s})),{})}class Q{constructor(e=document.documentElement,r=Z){this.logger=console,this.debug=!1,this.logDebugActivity=(e,t,s={})=>{this.debug&&this.logFormattedMessage(e,t,s)},this.element=e,this.schema=r,this.dispatcher=new t(this),this.router=new H(this),this.actionDescriptorFilters=Object.assign({},s)}static start(e,t){const s=new this(e,t);return s.start(),s}async start(){await new Promise((e=>{\"loading\"==document.readyState?document.addEventListener(\"DOMContentLoaded\",(()=>e())):e()})),this.logDebugActivity(\"application\",\"starting\"),this.dispatcher.start(),this.router.start(),this.logDebugActivity(\"application\",\"start\")}stop(){this.logDebugActivity(\"application\",\"stopping\"),this.dispatcher.stop(),this.router.stop(),this.logDebugActivity(\"application\",\"stop\")}register(e,t){this.load({identifier:e,controllerConstructor:t})}registerActionOption(e,t){this.actionDescriptorFilters[e]=t}load(e,...t){(Array.isArray(e)?e:[e,...t]).forEach((e=>{e.controllerConstructor.shouldLoad&&this.router.loadDefinition(e)}))}unload(e,...t){(Array.isArray(e)?e:[e,...t]).forEach((e=>this.router.unloadIdentifier(e)))}get controllers(){return this.router.contexts.map((e=>e.controller))}getControllerForElementAndIdentifier(e,t){const s=this.router.getContextForElementAndIdentifier(e,t);return s?s.controller:null}handleError(e,t,s){var r;this.logger.error(\"%s\\n\\n%o\\n\\n%o\",t,e,s),null===(r=window.onerror)||void 0===r||r.call(window,t,\"\",0,0,e)}logFormattedMessage(e,t,s={}){s=Object.assign({application:this},s),this.logger.groupCollapsed(`${e} #${t}`),this.logger.log(\"details:\",Object.assign({},s)),this.logger.groupEnd()}}function X(e,t,s){return e.application.getControllerForElementAndIdentifier(t,s)}function Y(e,t,s){let r=X(e,t,s);return r||(e.application.router.proposeToConnectScopeForElementAndIdentifier(t,s),r=X(e,t,s),r||void 0)}function ee([e,t],s){return function(e){const{token:t,typeDefinition:s}=e,r=`${c(t)}-value`,n=function(e){const{controller:t,token:s,typeDefinition:r}=e,n=function(e){const{controller:t,token:s,typeObject:r}=e,n=l(r.type),i=l(r.default),o=n&&i,a=n&&!i,c=!n&&i,h=te(r.type),u=se(e.typeObject.default);if(a)return h;if(c)return u;if(h!==u){throw new Error(`The specified default value for the Stimulus Value \"${t?`${t}.${s}`:s}\" must match the defined type \"${h}\". The provided default value of \"${r.default}\" is of type \"${u}\".`)}if(o)return h}({controller:t,token:s,typeObject:r}),i=se(r),o=te(r),a=n||i||o;if(a)return a;throw new Error(`Unknown value type \"${t?`${t}.${r}`:s}\" for \"${s}\" value`)}(e);return{type:n,key:r,name:i(r),get defaultValue(){return function(e){const t=te(e);if(t)return re[t];const s=h(e,\"default\"),r=h(e,\"type\"),n=e;if(s)return n.default;if(r){const{type:e}=n,t=te(e);if(t)return re[t]}return e}(s)},get hasCustomDefaultValue(){return void 0!==se(s)},reader:ne[n],writer:ie[n]||ie.default}}({controller:s,token:e,typeDefinition:t})}function te(e){switch(e){case Array:return\"array\";case Boolean:return\"boolean\";case Number:return\"number\";case Object:return\"object\";case String:return\"string\"}}function se(e){switch(typeof e){case\"boolean\":return\"boolean\";case\"number\":return\"number\";case\"string\":return\"string\"}return Array.isArray(e)?\"array\":\"[object Object]\"===Object.prototype.toString.call(e)?\"object\":void 0}const re={get array(){return[]},boolean:!1,number:0,get object(){return{}},string:\"\"},ne={array(e){const t=JSON.parse(e);if(!Array.isArray(t))throw new TypeError(`expected value of type \"array\" but instead got value \"${e}\" of type \"${se(t)}\"`);return t},boolean:e=>!(\"0\"==e||\"false\"==String(e).toLowerCase()),number:e=>Number(e.replace(/_/g,\"\")),object(e){const t=JSON.parse(e);if(null===t||\"object\"!=typeof t||Array.isArray(t))throw new TypeError(`expected value of type \"object\" but instead got value \"${e}\" of type \"${se(t)}\"`);return t},string:e=>e},ie={default:function(e){return`${e}`},array:oe,object:oe};function oe(e){return JSON.stringify(e)}class ae{constructor(e){this.context=e}static get shouldLoad(){return!0}static afterLoad(e,t){}get application(){return this.context.application}get scope(){return this.context.scope}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get targets(){return this.scope.targets}get outlets(){return this.scope.outlets}get classes(){return this.scope.classes}get data(){return this.scope.data}initialize(){}connect(){}disconnect(){}dispatch(e,{target:t=this.element,detail:s={},prefix:r=this.identifier,bubbles:n=!0,cancelable:i=!0}={}){const o=new CustomEvent(r?`${r}:${e}`:e,{detail:s,bubbles:n,cancelable:i});return t.dispatchEvent(o),o}}ae.blessings=[function(e){return T(e,\"classes\").reduce(((e,t)=>{return Object.assign(e,{[`${s=t}Class`]:{get(){const{classes:e}=this;if(e.has(s))return e.get(s);{const t=e.getAttributeName(s);throw new Error(`Missing attribute \"${t}\"`)}}},[`${s}Classes`]:{get(){return this.classes.getAll(s)}},[`has${a(s)}Class`]:{get(){return this.classes.has(s)}}});var s}),{})},function(e){return T(e,\"targets\").reduce(((e,t)=>{return Object.assign(e,{[`${s=t}Target`]:{get(){const e=this.targets.find(s);if(e)return e;throw new Error(`Missing target element \"${s}\" for \"${this.identifier}\" controller`)}},[`${s}Targets`]:{get(){return this.targets.findAll(s)}},[`has${a(s)}Target`]:{get(){return this.targets.has(s)}}});var s}),{})},function(e){const t=S(e,\"values\"),s={valueDescriptorMap:{get(){return t.reduce(((e,t)=>{const s=ee(t,this.identifier),r=this.data.getAttributeNameForKey(s.key);return Object.assign(e,{[r]:s})}),{})}}};return t.reduce(((e,t)=>Object.assign(e,function(e,t){const s=ee(e,t),{key:r,name:n,reader:i,writer:o}=s;return{[n]:{get(){const e=this.data.get(r);return null!==e?i(e):s.defaultValue},set(e){void 0===e?this.data.delete(r):this.data.set(r,o(e))}},[`has${a(n)}`]:{get(){return this.data.has(r)||s.hasCustomDefaultValue}}}}(t))),s)},function(e){return T(e,\"outlets\").reduce(((e,t)=>Object.assign(e,function(e){const t=o(e);return{[`${t}Outlet`]:{get(){const t=this.outlets.find(e),s=this.outlets.getSelectorForOutletName(e);if(t){const s=Y(this,t,e);if(s)return s;throw new Error(`The provided outlet element is missing an outlet controller \"${e}\" instance for host controller \"${this.identifier}\"`)}throw new Error(`Missing outlet element \"${e}\" for host controller \"${this.identifier}\". Stimulus couldn't find a matching outlet element using selector \"${s}\".`)}},[`${t}Outlets`]:{get(){const t=this.outlets.findAll(e);return t.length>0?t.map((t=>{const s=Y(this,t,e);if(s)return s;console.warn(`The provided outlet element is missing an outlet controller \"${e}\" instance for host controller \"${this.identifier}\"`,t)})).filter((e=>e)):[]}},[`${t}OutletElement`]:{get(){const t=this.outlets.find(e),s=this.outlets.getSelectorForOutletName(e);if(t)return t;throw new Error(`Missing outlet element \"${e}\" for host controller \"${this.identifier}\". Stimulus couldn't find a matching outlet element using selector \"${s}\".`)}},[`${t}OutletElements`]:{get(){return this.outlets.findAll(e)}},[`has${a(t)}Outlet`]:{get(){return this.outlets.has(e)}}}}(t))),{})}],ae.targets=[],ae.outlets=[],ae.values={};export{Q as Application,b as AttributeObserver,L as Context,ae as Controller,f as ElementObserver,w as IndexedMultimap,E as Multimap,M as SelectorObserver,k as StringMapObserver,N as TokenListObserver,F as ValueListObserver,v as add,Z as defaultSchema,y as del,O as fetch,A as prune};\n//# sourceMappingURL=stimulus.min.js.map\n"},{"path":"static/assets/tailwind.css","content":"/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */\n@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-green-50:oklch(98.2% .018 155.826);--color-green-500:oklch(72.3% .219 149.579);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-700:oklch(37.3% .034 259.733);--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.fixed{position:fixed}.sticky{position:sticky}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.my-4{margin-block:calc(var(--spacing) * 4)}.my-5{margin-block:calc(var(--spacing) * 5)}.my-8{margin-block:calc(var(--spacing) * 8)}.my-10{margin-block:calc(var(--spacing) * 10)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-28{margin-top:calc(var(--spacing) * 28)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-6{margin-left:calc(var(--spacing) * 6)}.block{display:block}.contents{display:contents}.flex{display:flex}.inline{display:inline}.inline-block{display:inline-block}.w-full{width:100%}.min-w-full{min-width:100%}.flex-grow{flex-grow:1}.cursor-pointer{cursor:pointer}.list-disc{list-style-type:disc}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}.rounded{border-radius:.25rem}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-gray-400{border-color:var(--color-gray-400)}.border-red-400{border-color:var(--color-red-400)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-green-50{background-color:var(--color-green-50)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-600{background-color:var(--color-red-600)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\\.5{padding-block:calc(var(--spacing) * 2.5)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.text-center{text-align:center}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-blue-600{color:var(--color-blue-600)}.text-gray-500{color:var(--color-gray-500)}.text-gray-700{color:var(--color-gray-700)}.text-green-500{color:var(--color-green-500)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-white{color:var(--color-white)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@media (hover:hover){.hover\\:bg-blue-500:hover{background-color:var(--color-blue-500)}.hover\\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\\:bg-red-500:hover{background-color:var(--color-red-500)}.hover\\:underline:hover{text-decoration-line:underline}}.focus\\:outline-blue-600:focus{outline-color:var(--color-blue-600)}.focus\\:outline-red-600:focus{outline-color:var(--color-red-600)}@media (min-width:40rem){.sm\\:mt-0{margin-top:calc(var(--spacing) * 0)}.sm\\:ml-2{margin-left:calc(var(--spacing) * 2)}.sm\\:inline-block{display:inline-block}.sm\\:w-auto{width:auto}.sm\\:flex-row{flex-direction:row}.sm\\:pb-0{padding-bottom:calc(var(--spacing) * 0)}}@media (min-width:48rem){.md\\:w-2\\/3{width:66.6667%}}}@property --tw-space-y-reverse{syntax:\"*\";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:\"*\";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:\"*\";inherits:false;initial-value:0}@property --tw-border-style{syntax:\"*\";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:\"*\";inherits:false}@property --tw-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:\"*\";inherits:false}@property --tw-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:\"*\";inherits:false}@property --tw-inset-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:\"*\";inherits:false}@property --tw-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:\"*\";inherits:false}@property --tw-inset-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:\"*\";inherits:false}@property --tw-ring-offset-width{syntax:\"<length>\";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:\"*\";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}"},{"path":"static/assets/turbo.min.js","content":"/*!\nTurbo 8.0.23\nCopyright © 2026 37signals LLC\n */\nconst e={eager:\"eager\",lazy:\"lazy\"};class t extends HTMLElement{static delegateConstructor=void 0;loaded=Promise.resolve();static get observedAttributes(){return[\"disabled\",\"loading\",\"src\"]}constructor(){super(),this.delegate=new t.delegateConstructor(this)}connectedCallback(){this.delegate.connect()}disconnectedCallback(){this.delegate.disconnect()}reload(){return this.delegate.sourceURLReloaded()}attributeChangedCallback(e){\"loading\"==e?this.delegate.loadingStyleChanged():\"src\"==e?this.delegate.sourceURLChanged():\"disabled\"==e&&this.delegate.disabledChanged()}get src(){return this.getAttribute(\"src\")}set src(e){e?this.setAttribute(\"src\",e):this.removeAttribute(\"src\")}get refresh(){return this.getAttribute(\"refresh\")}set refresh(e){e?this.setAttribute(\"refresh\",e):this.removeAttribute(\"refresh\")}get shouldReloadWithMorph(){return this.src&&\"morph\"===this.refresh}get loading(){return function(t){if(\"lazy\"===t.toLowerCase())return e.lazy;return e.eager}(this.getAttribute(\"loading\")||\"\")}set loading(e){e?this.setAttribute(\"loading\",e):this.removeAttribute(\"loading\")}get disabled(){return this.hasAttribute(\"disabled\")}set disabled(e){e?this.setAttribute(\"disabled\",\"\"):this.removeAttribute(\"disabled\")}get autoscroll(){return this.hasAttribute(\"autoscroll\")}set autoscroll(e){e?this.setAttribute(\"autoscroll\",\"\"):this.removeAttribute(\"autoscroll\")}get complete(){return!this.delegate.isLoading}get isActive(){return this.ownerDocument===document&&!this.isPreview}get isPreview(){return this.ownerDocument?.documentElement?.hasAttribute(\"data-turbo-preview\")}}const s={enabled:!0,progressBarDelay:500,unvisitableExtensions:new Set([\".7z\",\".aac\",\".apk\",\".avi\",\".bmp\",\".bz2\",\".css\",\".csv\",\".deb\",\".dmg\",\".doc\",\".docx\",\".exe\",\".gif\",\".gz\",\".heic\",\".heif\",\".ico\",\".iso\",\".jpeg\",\".jpg\",\".js\",\".json\",\".m4a\",\".mkv\",\".mov\",\".mp3\",\".mp4\",\".mpeg\",\".mpg\",\".msi\",\".ogg\",\".ogv\",\".pdf\",\".pkg\",\".png\",\".ppt\",\".pptx\",\".rar\",\".rtf\",\".svg\",\".tar\",\".tif\",\".tiff\",\".txt\",\".wav\",\".webm\",\".webp\",\".wma\",\".wmv\",\".xls\",\".xlsx\",\".xml\",\".zip\"])};function r(e){if(\"false\"==e.getAttribute(\"data-turbo-eval\"))return e;{const t=document.createElement(\"script\"),s=w();return s&&(t.nonce=s),t.textContent=e.textContent,t.async=!1,function(e,t){for(const{name:s,value:r}of t.attributes)e.setAttribute(s,r)}(t,e),t}}function i(e,{target:t,cancelable:s,detail:r}={}){const i=new CustomEvent(e,{cancelable:s,bubbles:!0,composed:!0,detail:r});return t&&t.isConnected?t.dispatchEvent(i):document.documentElement.dispatchEvent(i),i}function n(e){e.preventDefault(),e.stopImmediatePropagation()}function o(){return\"hidden\"===document.visibilityState?c():a()}function a(){return new Promise((e=>requestAnimationFrame((()=>e()))))}function c(){return new Promise((e=>setTimeout((()=>e()),0)))}function l(e=\"\"){return(new DOMParser).parseFromString(e,\"text/html\")}function h(e,...t){const s=function(e,t){return e.reduce(((e,s,r)=>e+s+(null==t[r]?\"\":t[r])),\"\")}(e,t).replace(/^\\n/,\"\").split(\"\\n\"),r=s[0].match(/^\\s+/),i=r?r[0].length:0;return s.map((e=>e.slice(i))).join(\"\\n\")}function d(){return Array.from({length:36}).map(((e,t)=>8==t||13==t||18==t||23==t?\"-\":14==t?\"4\":19==t?(Math.floor(4*Math.random())+8).toString(16):Math.floor(16*Math.random()).toString(16))).join(\"\")}function u(e,...t){for(const s of t.map((t=>t?.getAttribute(e))))if(\"string\"==typeof s)return s;return null}function m(...e){for(const t of e)\"turbo-frame\"==t.localName&&t.setAttribute(\"busy\",\"\"),t.setAttribute(\"aria-busy\",\"true\")}function p(...e){for(const t of e)\"turbo-frame\"==t.localName&&t.removeAttribute(\"busy\"),t.removeAttribute(\"aria-busy\")}function f(e,t=2e3){return new Promise((s=>{const r=()=>{e.removeEventListener(\"error\",r),e.removeEventListener(\"load\",r),s()};e.addEventListener(\"load\",r,{once:!0}),e.addEventListener(\"error\",r,{once:!0}),setTimeout(s,t)}))}function g(e){switch(e){case\"replace\":return history.replaceState;case\"advance\":case\"restore\":return history.pushState}}function b(...e){const t=u(\"data-turbo-action\",...e);return function(e){return\"advance\"==e||\"replace\"==e||\"restore\"==e}(t)?t:null}function v(e){return document.querySelector(`meta[name=\"${e}\"]`)}function S(e){const t=v(e);return t&&t.content}function w(){const e=v(\"csp-nonce\");if(e){const{nonce:t,content:s}=e;return\"\"==t?s:t}}function y(e,t){if(e instanceof Element)return e.closest(t)||y(e.assignedSlot||e.getRootNode()?.host,t)}function E(e){return!!e&&null==e.closest(\"[inert], :disabled, [hidden], details:not([open]), dialog:not([open])\")&&\"function\"==typeof e.focus}function A(e){return Array.from(e.querySelectorAll(\"[autofocus]\")).find(E)}function R(e){if(\"_blank\"===e)return!1;if(e){for(const t of document.getElementsByName(e))if(t instanceof HTMLIFrameElement)return!1;return!0}return!0}function T(e){const t=y(e,\"a[href], a[xlink\\\\:href]\");if(!t)return null;if(t.href.startsWith(\"#\"))return null;if(t.hasAttribute(\"download\"))return null;const s=t.getAttribute(\"target\");return s&&\"_self\"!==s?null:t}const L={\"aria-disabled\":{beforeSubmit:e=>{e.setAttribute(\"aria-disabled\",\"true\"),e.addEventListener(\"click\",n)},afterSubmit:e=>{e.removeAttribute(\"aria-disabled\"),e.removeEventListener(\"click\",n)}},disabled:{beforeSubmit:e=>e.disabled=!0,afterSubmit:e=>e.disabled=!1}};const C=new class{#e=null;constructor(e){Object.assign(this,e)}get submitter(){return this.#e}set submitter(e){this.#e=L[e]||e}}({mode:\"on\",submitter:\"disabled\"}),P={drive:s,forms:C};function k(e){return new URL(e.toString(),document.baseURI)}function M(e){let t;return e.hash?e.hash.slice(1):(t=e.href.match(/#(.*)$/))?t[1]:void 0}function F(e,t){return k(t?.getAttribute(\"formaction\")||e.getAttribute(\"action\")||e.action)}function I(e){return(function(e){return function(e){return e.pathname.split(\"/\").slice(1)}(e).slice(-1)[0]}(e).match(/\\.[^.]*$/)||[])[0]||\"\"}function q(e,t){return function(e,t){const s=O(t.origin+t.pathname);return O(e.href)===s||e.href.startsWith(s)}(e,t)&&!P.drive.unvisitableExtensions.has(I(e))}function H(e){return k(e.getAttribute(\"href\")||\"\")}function B(e){return function(e){const t=M(e);return null!=t?e.href.slice(0,-(t.length+1)):e.href}(e)}function N(e,t){return k(e).href==k(t).href}function O(e){return e.endsWith(\"/\")?e:e+\"/\"}class D{constructor(e){this.response=e}get succeeded(){return this.response.ok}get failed(){return!this.succeeded}get clientError(){return this.statusCode>=400&&this.statusCode<=499}get serverError(){return this.statusCode>=500&&this.statusCode<=599}get redirected(){return this.response.redirected}get location(){return k(this.response.url)}get isHTML(){return this.contentType&&this.contentType.match(/^(?:text\\/([^\\s;,]+\\b)?html|application\\/xhtml\\+xml)\\b/)}get statusCode(){return this.response.status}get contentType(){return this.header(\"Content-Type\")}get responseText(){return this.response.clone().text()}get responseHTML(){return this.isHTML?this.response.clone().text():Promise.resolve(void 0)}header(e){return this.response.headers.get(e)}}class x extends Set{constructor(e){super(),this.maxSize=e}add(e){if(this.size>=this.maxSize){const e=this.values().next().value;this.delete(e)}super.add(e)}}const V=new x(20);function W(e,t={}){const s=new Headers(t.headers||{}),r=d();return V.add(r),s.append(\"X-Turbo-Request-Id\",r),window.fetch(e,{...t,headers:s})}function U(e){switch(e.toLowerCase()){case\"get\":return _.get;case\"post\":return _.post;case\"put\":return _.put;case\"patch\":return _.patch;case\"delete\":return _.delete}}const _={get:\"get\",post:\"post\",put:\"put\",patch:\"patch\",delete:\"delete\"};function $(e){switch(e.toLowerCase()){case j.multipart:return j.multipart;case j.plain:return j.plain;default:return j.urlEncoded}}const j={urlEncoded:\"application/x-www-form-urlencoded\",multipart:\"multipart/form-data\",plain:\"text/plain\"};class z{abortController=new AbortController;#t=e=>{};constructor(e,t,s,r=new URLSearchParams,i=null,n=j.urlEncoded){const[o,a]=K(k(s),t,r,n);this.delegate=e,this.url=o,this.target=i,this.fetchOptions={credentials:\"same-origin\",redirect:\"follow\",method:t.toUpperCase(),headers:{...this.defaultHeaders},body:a,signal:this.abortSignal,referrer:this.delegate.referrer?.href},this.enctype=n}get method(){return this.fetchOptions.method}set method(e){const t=this.isSafe?this.url.searchParams:this.fetchOptions.body||new FormData,s=U(e)||_.get;this.url.search=\"\";const[r,i]=K(this.url,s,t,this.enctype);this.url=r,this.fetchOptions.body=i,this.fetchOptions.method=s.toUpperCase()}get headers(){return this.fetchOptions.headers}set headers(e){this.fetchOptions.headers=e}get body(){return this.isSafe?this.url.searchParams:this.fetchOptions.body}set body(e){this.fetchOptions.body=e}get location(){return this.url}get params(){return this.url.searchParams}get entries(){return this.body?Array.from(this.body.entries()):[]}cancel(){this.abortController.abort()}async perform(){const{fetchOptions:e}=this;this.delegate.prepareRequest(this);const t=await this.#s(e);try{this.delegate.requestStarted(this),t.detail.fetchRequest?this.response=t.detail.fetchRequest.response:this.response=W(this.url.href,e);const s=await this.response;return await this.receive(s)}catch(e){if(\"AbortError\"!==e.name)throw this.#r(e)&&this.delegate.requestErrored(this,e),e}finally{this.delegate.requestFinished(this)}}async receive(e){const t=new D(e);return i(\"turbo:before-fetch-response\",{cancelable:!0,detail:{fetchResponse:t},target:this.target}).defaultPrevented?this.delegate.requestPreventedHandlingResponse(this,t):t.succeeded?this.delegate.requestSucceededWithResponse(this,t):this.delegate.requestFailedWithResponse(this,t),t}get defaultHeaders(){return{Accept:\"text/html, application/xhtml+xml\"}}get isSafe(){return G(this.method)}get abortSignal(){return this.abortController.signal}acceptResponseType(e){this.headers.Accept=[e,this.headers.Accept].join(\", \")}async#s(e){const t=new Promise((e=>this.#t=e)),s=i(\"turbo:before-fetch-request\",{cancelable:!0,detail:{fetchOptions:e,url:this.url,resume:this.#t},target:this.target});return this.url=s.detail.url,s.defaultPrevented&&await t,s}#r(e){return!i(\"turbo:fetch-request-error\",{target:this.target,cancelable:!0,detail:{request:this,error:e}}).defaultPrevented}}function G(e){return U(e)==_.get}function K(e,t,s,r){const i=Array.from(s).length>0?new URLSearchParams(J(s)):e.searchParams;return G(t)?[X(e,i),null]:r==j.urlEncoded?[e,i]:[e,s]}function J(e){const t=[];for(const[s,r]of e)r instanceof File||t.push([s,r]);return t}function X(e,t){const s=new URLSearchParams(J(t));return e.search=s.toString(),e}class Y{started=!1;constructor(e,t){this.delegate=e,this.element=t,this.intersectionObserver=new IntersectionObserver(this.intersect)}start(){this.started||(this.started=!0,this.intersectionObserver.observe(this.element))}stop(){this.started&&(this.started=!1,this.intersectionObserver.unobserve(this.element))}intersect=e=>{const t=e.slice(-1)[0];t?.isIntersecting&&this.delegate.elementAppearedInViewport(this.element)}}class Q{static contentType=\"text/vnd.turbo-stream.html\";static wrap(e){return\"string\"==typeof e?new this(function(e){const t=document.createElement(\"template\");return t.innerHTML=e,t.content}(e)):e}constructor(e){this.fragment=function(e){for(const t of e.querySelectorAll(\"turbo-stream\")){const e=document.importNode(t,!0);for(const t of e.templateElement.content.querySelectorAll(\"script\"))t.replaceWith(r(t));t.replaceWith(e)}return e}(e)}}const Z=e=>e;class ee{keys=[];entries={};#i;constructor(e,t=Z){this.size=e,this.#i=t}has(e){return this.#i(e)in this.entries}get(e){if(this.has(e)){const t=this.read(e);return this.touch(e),t}}put(e,t){return this.write(e,t),this.touch(e),t}clear(){for(const e of Object.keys(this.entries))this.evict(e)}read(e){return this.entries[this.#i(e)]}write(e,t){this.entries[this.#i(e)]=t}touch(e){e=this.#i(e);const t=this.keys.indexOf(e);t>-1&&this.keys.splice(t,1),this.keys.unshift(e),this.trim()}trim(){for(const e of this.keys.splice(this.size))this.evict(e)}evict(e){delete this.entries[e]}}const te=1e4,se=new class extends ee{#n=null;#o={};constructor(e=1,t=100){super(e,B),this.prefetchDelay=t}putLater(e,t,s){this.#n=setTimeout((()=>{t.perform(),this.put(e,t,s),this.#n=null}),this.prefetchDelay)}put(e,t,s=te){super.put(e,t),this.#o[B(e)]=new Date((new Date).getTime()+s)}clear(){super.clear(),this.#n&&clearTimeout(this.#n)}evict(e){super.evict(e),delete this.#o[e]}has(e){if(super.has(e)){const t=this.#o[B(e)];return t&&t>Date.now()}return!1}},re={initialized:\"initialized\",requesting:\"requesting\",waiting:\"waiting\",receiving:\"receiving\",stopping:\"stopping\",stopped:\"stopped\"};class ie{state=re.initialized;static confirmMethod(e){return Promise.resolve(confirm(e))}constructor(e,t,s,r=!1){const i=function(e,t){const s=t?.getAttribute(\"formmethod\")||e.getAttribute(\"method\")||\"\";return U(s.toLowerCase())||_.get}(t,s),n=function(e,t){const s=k(e);G(t)&&(s.search=\"\");return s}(function(e,t){const s=\"string\"==typeof e.action?e.action:null;return t?.hasAttribute(\"formaction\")?t.getAttribute(\"formaction\")||\"\":e.getAttribute(\"action\")||s||\"\"}(t,s),i),o=function(e,t){const s=new FormData(e),r=t?.getAttribute(\"name\"),i=t?.getAttribute(\"value\");r&&s.append(r,i||\"\");return s}(t,s),a=function(e,t){return $(t?.getAttribute(\"formenctype\")||e.enctype)}(t,s);this.delegate=e,this.formElement=t,this.submitter=s,this.fetchRequest=new z(this,i,n,o,t,a),this.mustRedirect=r}get method(){return this.fetchRequest.method}set method(e){this.fetchRequest.method=e}get action(){return this.fetchRequest.url.toString()}set action(e){this.fetchRequest.url=k(e)}get body(){return this.fetchRequest.body}get enctype(){return this.fetchRequest.enctype}get isSafe(){return this.fetchRequest.isSafe}get location(){return this.fetchRequest.url}async start(){const{initialized:e,requesting:t}=re,s=u(\"data-turbo-confirm\",this.submitter,this.formElement);if(\"string\"==typeof s){const e=\"function\"==typeof P.forms.confirm?P.forms.confirm:ie.confirmMethod;if(!await e(s,this.formElement,this.submitter))return}if(this.state==e)return this.state=t,this.fetchRequest.perform()}stop(){const{stopping:e,stopped:t}=re;if(this.state!=e&&this.state!=t)return this.state=e,this.fetchRequest.cancel(),!0}prepareRequest(e){if(!e.isSafe){const t=function(e){if(null!=e){const t=(document.cookie?document.cookie.split(\"; \"):[]).find((t=>t.startsWith(e)));if(t){const e=t.split(\"=\").slice(1).join(\"=\");return e?decodeURIComponent(e):void 0}}}(S(\"csrf-param\"))||S(\"csrf-token\");t&&(e.headers[\"X-CSRF-Token\"]=t)}this.requestAcceptsTurboStreamResponse(e)&&e.acceptResponseType(Q.contentType)}requestStarted(e){this.state=re.waiting,this.submitter&&P.forms.submitter.beforeSubmit(this.submitter),this.setSubmitsWith(),m(this.formElement),i(\"turbo:submit-start\",{target:this.formElement,detail:{formSubmission:this}}),this.delegate.formSubmissionStarted(this)}requestPreventedHandlingResponse(e,t){se.clear(),this.result={success:t.succeeded,fetchResponse:t}}requestSucceededWithResponse(e,t){if(t.clientError||t.serverError)this.delegate.formSubmissionFailedWithResponse(this,t);else if(se.clear(),this.requestMustRedirect(e)&&function(e){return 200==e.statusCode&&!e.redirected}(t)){const e=new Error(\"Form responses must redirect to another location\");this.delegate.formSubmissionErrored(this,e)}else this.state=re.receiving,this.result={success:!0,fetchResponse:t},this.delegate.formSubmissionSucceededWithResponse(this,t)}requestFailedWithResponse(e,t){this.result={success:!1,fetchResponse:t},this.delegate.formSubmissionFailedWithResponse(this,t)}requestErrored(e,t){this.result={success:!1,error:t},this.delegate.formSubmissionErrored(this,t)}requestFinished(e){this.state=re.stopped,this.submitter&&P.forms.submitter.afterSubmit(this.submitter),this.resetSubmitterText(),p(this.formElement),i(\"turbo:submit-end\",{target:this.formElement,detail:{formSubmission:this,...this.result}}),this.delegate.formSubmissionFinished(this)}setSubmitsWith(){if(this.submitter&&this.submitsWith)if(this.submitter.matches(\"button\"))this.originalSubmitText=this.submitter.innerHTML,this.submitter.innerHTML=this.submitsWith;else if(this.submitter.matches(\"input\")){const e=this.submitter;this.originalSubmitText=e.value,e.value=this.submitsWith}}resetSubmitterText(){if(this.submitter&&this.originalSubmitText)if(this.submitter.matches(\"button\"))this.submitter.innerHTML=this.originalSubmitText;else if(this.submitter.matches(\"input\")){this.submitter.value=this.originalSubmitText}}requestMustRedirect(e){return!e.isSafe&&this.mustRedirect}requestAcceptsTurboStreamResponse(e){return!e.isSafe||function(e,...t){return t.some((t=>t&&t.hasAttribute(e)))}(\"data-turbo-stream\",this.submitter,this.formElement)}get submitsWith(){return this.submitter?.getAttribute(\"data-turbo-submits-with\")}}class ne{constructor(e){this.element=e}get activeElement(){return this.element.ownerDocument.activeElement}get children(){return[...this.element.children]}hasAnchor(e){return null!=this.getElementForAnchor(e)}getElementForAnchor(e){return e?this.element.querySelector(`[id='${e}'], a[name='${e}']`):null}get isConnected(){return this.element.isConnected}get firstAutofocusableElement(){return A(this.element)}get permanentElements(){return ae(this.element)}getPermanentElementById(e){return oe(this.element,e)}getPermanentElementMapForSnapshot(e){const t={};for(const s of this.permanentElements){const{id:r}=s,i=e.getPermanentElementById(r);i&&(t[r]=[s,i])}return t}}function oe(e,t){return e.querySelector(`#${t}[data-turbo-permanent]`)}function ae(e){return e.querySelectorAll(\"[id][data-turbo-permanent]\")}class ce{started=!1;constructor(e,t){this.delegate=e,this.eventTarget=t}start(){this.started||(this.eventTarget.addEventListener(\"submit\",this.submitCaptured,!0),this.started=!0)}stop(){this.started&&(this.eventTarget.removeEventListener(\"submit\",this.submitCaptured,!0),this.started=!1)}submitCaptured=()=>{this.eventTarget.removeEventListener(\"submit\",this.submitBubbled,!1),this.eventTarget.addEventListener(\"submit\",this.submitBubbled,!1)};submitBubbled=e=>{if(!e.defaultPrevented){const t=e.target instanceof HTMLFormElement?e.target:void 0,s=e.submitter||void 0;t&&function(e,t){const s=t?.getAttribute(\"formmethod\")||e.getAttribute(\"method\");return\"dialog\"!=s}(t,s)&&function(e,t){const s=t?.getAttribute(\"formtarget\")||e.getAttribute(\"target\");return R(s)}(t,s)&&this.delegate.willSubmitForm(t,s)&&(e.preventDefault(),e.stopImmediatePropagation(),this.delegate.formSubmitted(t,s))}}}class le{#a=e=>{};#c=e=>{};constructor(e,t){this.delegate=e,this.element=t}scrollToAnchor(e){const t=this.snapshot.getElementForAnchor(e);t?(this.focusElement(t),this.scrollToElement(t)):this.scrollToPosition({x:0,y:0})}scrollToAnchorFromLocation(e){this.scrollToAnchor(M(e))}scrollToElement(e){e.scrollIntoView()}focusElement(e){e instanceof HTMLElement&&(e.hasAttribute(\"tabindex\")?e.focus():(e.setAttribute(\"tabindex\",\"-1\"),e.focus(),e.removeAttribute(\"tabindex\")))}scrollToPosition({x:e,y:t}){this.scrollRoot.scrollTo(e,t)}scrollToTop(){this.scrollToPosition({x:0,y:0})}get scrollRoot(){return window}async render(e){const{isPreview:t,shouldRender:s,willRender:r,newSnapshot:i}=e,n=r;if(s)try{this.renderPromise=new Promise((e=>this.#a=e)),this.renderer=e,await this.prepareToRenderSnapshot(e);const s=new Promise((e=>this.#c=e)),r={resume:this.#c,render:this.renderer.renderElement,renderMethod:this.renderer.renderMethod};this.delegate.allowsImmediateRender(i,r)||await s,await this.renderSnapshot(e),this.delegate.viewRenderedSnapshot(i,t,this.renderer.renderMethod),this.delegate.preloadOnLoadLinksForView(this.element),this.finishRenderingSnapshot(e)}finally{delete this.renderer,this.#a(void 0),delete this.renderPromise}else n&&this.invalidate(e.reloadReason)}invalidate(e){this.delegate.viewInvalidated(e)}async prepareToRenderSnapshot(e){this.markAsPreview(e.isPreview),await e.prepareToRender()}markAsPreview(e){e?this.element.setAttribute(\"data-turbo-preview\",\"\"):this.element.removeAttribute(\"data-turbo-preview\")}markVisitDirection(e){this.element.setAttribute(\"data-turbo-visit-direction\",e)}unmarkVisitDirection(){this.element.removeAttribute(\"data-turbo-visit-direction\")}async renderSnapshot(e){await e.render()}finishRenderingSnapshot(e){e.finishRendering()}}class he extends le{missing(){this.element.innerHTML='<strong class=\"turbo-frame-error\">Content missing</strong>'}get snapshot(){return new ne(this.element)}}class de{constructor(e,t){this.delegate=e,this.element=t}start(){this.element.addEventListener(\"click\",this.clickBubbled),document.addEventListener(\"turbo:click\",this.linkClicked),document.addEventListener(\"turbo:before-visit\",this.willVisit)}stop(){this.element.removeEventListener(\"click\",this.clickBubbled),document.removeEventListener(\"turbo:click\",this.linkClicked),document.removeEventListener(\"turbo:before-visit\",this.willVisit)}clickBubbled=e=>{this.clickEventIsSignificant(e)?this.clickEvent=e:delete this.clickEvent};linkClicked=e=>{this.clickEvent&&this.clickEventIsSignificant(e)&&this.delegate.shouldInterceptLinkClick(e.target,e.detail.url,e.detail.originalEvent)&&(this.clickEvent.preventDefault(),e.preventDefault(),this.delegate.linkClickIntercepted(e.target,e.detail.url,e.detail.originalEvent)),delete this.clickEvent};willVisit=e=>{delete this.clickEvent};clickEventIsSignificant(e){const t=e.composed?e.target?.parentElement:e.target,s=T(t)||t;return s instanceof Element&&s.closest(\"turbo-frame, html\")==this.element}}class ue{started=!1;constructor(e,t){this.delegate=e,this.eventTarget=t}start(){this.started||(this.eventTarget.addEventListener(\"click\",this.clickCaptured,!0),this.started=!0)}stop(){this.started&&(this.eventTarget.removeEventListener(\"click\",this.clickCaptured,!0),this.started=!1)}clickCaptured=()=>{this.eventTarget.removeEventListener(\"click\",this.clickBubbled,!1),this.eventTarget.addEventListener(\"click\",this.clickBubbled,!1)};clickBubbled=e=>{if(e instanceof MouseEvent&&this.clickEventIsSignificant(e)){const t=T(e.composedPath&&e.composedPath()[0]||e.target);if(t&&R(t.target)){const s=H(t);this.delegate.willFollowLinkToLocation(t,s,e)&&(e.preventDefault(),this.delegate.followedLinkToLocation(t,s))}}};clickEventIsSignificant(e){return!(e.target&&e.target.isContentEditable||e.defaultPrevented||e.which>1||e.altKey||e.ctrlKey||e.metaKey||e.shiftKey)}}class me{constructor(e,t){this.delegate=e,this.linkInterceptor=new ue(this,t)}start(){this.linkInterceptor.start()}stop(){this.linkInterceptor.stop()}canPrefetchRequestToLocation(e,t){return!1}prefetchAndCacheRequestToLocation(e,t){}willFollowLinkToLocation(e,t,s){return this.delegate.willSubmitFormLinkToLocation(e,t,s)&&(e.hasAttribute(\"data-turbo-method\")||e.hasAttribute(\"data-turbo-stream\"))}followedLinkToLocation(e,t){const s=document.createElement(\"form\");for(const[e,r]of t.searchParams)s.append(Object.assign(document.createElement(\"input\"),{type:\"hidden\",name:e,value:r}));const r=Object.assign(t,{search:\"\"});s.setAttribute(\"data-turbo\",\"true\"),s.setAttribute(\"action\",r.href),s.setAttribute(\"hidden\",\"\");const i=e.getAttribute(\"data-turbo-method\");i&&s.setAttribute(\"method\",i);const n=e.getAttribute(\"data-turbo-frame\");n&&s.setAttribute(\"data-turbo-frame\",n);const o=b(e);o&&s.setAttribute(\"data-turbo-action\",o);const a=e.getAttribute(\"data-turbo-confirm\");a&&s.setAttribute(\"data-turbo-confirm\",a);e.hasAttribute(\"data-turbo-stream\")&&s.setAttribute(\"data-turbo-stream\",\"\"),this.delegate.submittedFormLinkToLocation(e,t,s),document.body.appendChild(s),s.addEventListener(\"turbo:submit-end\",(()=>s.remove()),{once:!0}),requestAnimationFrame((()=>s.requestSubmit()))}}class pe{static async preservingPermanentElements(e,t,s){const r=new this(e,t);r.enter(),await s(),r.leave()}constructor(e,t){this.delegate=e,this.permanentElementMap=t}enter(){for(const e in this.permanentElementMap){const[t,s]=this.permanentElementMap[e];this.delegate.enteringBardo(t,s),this.replaceNewPermanentElementWithPlaceholder(s)}}leave(){for(const e in this.permanentElementMap){const[t]=this.permanentElementMap[e];this.replaceCurrentPermanentElementWithClone(t),this.replacePlaceholderWithPermanentElement(t),this.delegate.leavingBardo(t)}}replaceNewPermanentElementWithPlaceholder(e){const t=function(e){const t=document.createElement(\"meta\");return t.setAttribute(\"name\",\"turbo-permanent-placeholder\"),t.setAttribute(\"content\",e.id),t}(e);e.replaceWith(t)}replaceCurrentPermanentElementWithClone(e){const t=e.cloneNode(!0);e.replaceWith(t)}replacePlaceholderWithPermanentElement(e){const t=this.getPlaceholderById(e.id);t?.replaceWith(e)}getPlaceholderById(e){return this.placeholders.find((t=>t.content==e))}get placeholders(){return[...document.querySelectorAll(\"meta[name=turbo-permanent-placeholder][content]\")]}}class fe{#l=null;static renderElement(e,t){}constructor(e,t,s,r=!0){this.currentSnapshot=e,this.newSnapshot=t,this.isPreview=s,this.willRender=r,this.renderElement=this.constructor.renderElement,this.promise=new Promise(((e,t)=>this.resolvingFunctions={resolve:e,reject:t}))}get shouldRender(){return!0}get shouldAutofocus(){return!0}get reloadReason(){}prepareToRender(){}render(){}finishRendering(){this.resolvingFunctions&&(this.resolvingFunctions.resolve(),delete this.resolvingFunctions)}async preservingPermanentElements(e){await pe.preservingPermanentElements(this,this.permanentElementMap,e)}focusFirstAutofocusableElement(){if(this.shouldAutofocus){const e=this.connectedSnapshot.firstAutofocusableElement;e&&e.focus()}}enteringBardo(e){this.#l||e.contains(this.currentSnapshot.activeElement)&&(this.#l=this.currentSnapshot.activeElement)}leavingBardo(e){e.contains(this.#l)&&this.#l instanceof HTMLElement&&(this.#l.focus(),this.#l=null)}get connectedSnapshot(){return this.newSnapshot.isConnected?this.newSnapshot:this.currentSnapshot}get currentElement(){return this.currentSnapshot.element}get newElement(){return this.newSnapshot.element}get permanentElementMap(){return this.currentSnapshot.getPermanentElementMapForSnapshot(this.newSnapshot)}get renderMethod(){return\"replace\"}}class ge extends fe{static renderElement(e,t){const s=document.createRange();s.selectNodeContents(e),s.deleteContents();const r=t,i=r.ownerDocument?.createRange();i&&(i.selectNodeContents(r),e.appendChild(i.extractContents()))}constructor(e,t,s,r,i,n=!0){super(t,s,r,i,n),this.delegate=e}get shouldRender(){return!0}async render(){await o(),this.preservingPermanentElements((()=>{this.loadFrameElement()})),this.scrollFrameIntoView(),await o(),this.focusFirstAutofocusableElement(),await o(),this.activateScriptElements()}loadFrameElement(){this.delegate.willRenderFrame(this.currentElement,this.newElement),this.renderElement(this.currentElement,this.newElement)}scrollFrameIntoView(){if(this.currentElement.autoscroll||this.newElement.autoscroll){const s=this.currentElement.firstElementChild,r=(e=this.currentElement.getAttribute(\"data-autoscroll-block\"),t=\"end\",\"end\"==e||\"start\"==e||\"center\"==e||\"nearest\"==e?e:t),i=function(e,t){return\"auto\"==e||\"smooth\"==e?e:t}(this.currentElement.getAttribute(\"data-autoscroll-behavior\"),\"auto\");if(s)return s.scrollIntoView({block:r,behavior:i}),!0}var e,t;return!1}activateScriptElements(){for(const e of this.newScriptElements){const t=r(e);e.replaceWith(t)}}get newScriptElements(){return this.currentElement.querySelectorAll(\"script\")}}var be=function(){const e=()=>{},t={morphStyle:\"outerHTML\",callbacks:{beforeNodeAdded:e,afterNodeAdded:e,beforeNodeMorphed:e,afterNodeMorphed:e,beforeNodeRemoved:e,afterNodeRemoved:e,beforeAttributeUpdated:e},head:{style:\"merge\",shouldPreserve:e=>\"true\"===e.getAttribute(\"im-preserve\"),shouldReAppend:e=>\"true\"===e.getAttribute(\"im-re-append\"),shouldRemove:e,afterHeadMorphed:e},restoreFocus:!0};const s=function(){function e(e,t,s,i){if(!1===i.callbacks.beforeNodeAdded(t))return null;if(i.idMap.has(t)){const n=document.createElement(t.tagName);return e.insertBefore(n,s),r(n,t,i),i.callbacks.afterNodeAdded(n),n}{const r=document.importNode(t,!0);return e.insertBefore(r,s),i.callbacks.afterNodeAdded(r),r}}const t=function(){function e(e,t,s){let r=e.idMap.get(t),i=e.idMap.get(s);if(!i||!r)return!1;for(const e of r)if(i.has(e))return!0;return!1}function t(e,t){const s=e,r=t;return s.nodeType===r.nodeType&&s.tagName===r.tagName&&(!s.getAttribute?.(\"id\")||s.getAttribute?.(\"id\")===r.getAttribute?.(\"id\"))}return function(s,r,i,n){let o=null,a=r.nextSibling,c=0,l=i;for(;l&&l!=n;){if(t(l,r)){if(e(s,l,r))return l;null===o&&(s.idMap.has(l)||(o=l))}if(null===o&&a&&t(l,a)&&(c++,a=a.nextSibling,c>=2&&(o=void 0)),s.activeElementAndParents.includes(l))break;l=l.nextSibling}return o||null}}();function s(e,t){if(e.idMap.has(t))o(e.pantry,t,null);else{if(!1===e.callbacks.beforeNodeRemoved(t))return;t.parentNode?.removeChild(t),e.callbacks.afterNodeRemoved(t)}}function i(e,t,r){let i=t;for(;i&&i!==r;){let t=i;i=i.nextSibling,s(e,t)}return i}function n(e,t,s,r){const i=r.target.getAttribute?.(\"id\")===t&&r.target||r.target.querySelector(`[id=\"${t}\"]`)||r.pantry.querySelector(`[id=\"${t}\"]`);return function(e,t){const s=e.getAttribute(\"id\");for(;e=e.parentNode;){let r=t.idMap.get(e);r&&(r.delete(s),r.size||t.idMap.delete(e))}}(i,r),o(e,i,s),i}function o(e,t,s){if(e.moveBefore)try{e.moveBefore(t,s)}catch(r){e.insertBefore(t,s)}else e.insertBefore(t,s)}return function(o,a,c,l=null,h=null){a instanceof HTMLTemplateElement&&c instanceof HTMLTemplateElement&&(a=a.content,c=c.content),l||=a.firstChild;for(const s of c.childNodes){if(l&&l!=h){const e=t(o,s,l,h);if(e){e!==l&&i(o,l,e),r(e,s,o),l=e.nextSibling;continue}}if(s instanceof Element){const e=s.getAttribute(\"id\");if(o.persistentIds.has(e)){const t=n(a,e,l,o);r(t,s,o),l=t.nextSibling;continue}}const c=e(a,s,l,o);c&&(l=c.nextSibling)}for(;l&&l!=h;){const e=l;l=l.nextSibling,s(o,e)}}}(),r=function(){function e(e,s,r,i){const n=s[r];if(n!==e[r]){const o=t(r,e,\"update\",i);o||(e[r]=s[r]),n?o||e.setAttribute(r,\"\"):t(r,e,\"remove\",i)||e.removeAttribute(r)}}function t(e,t,s,r){return!(\"value\"!==e||!r.ignoreActiveValue||t!==document.activeElement)||!1===r.callbacks.beforeAttributeUpdated(e,t,s)}function r(e,t){return!!t.ignoreActiveValue&&e===document.activeElement&&e!==document.body}return function(n,o,a){return a.ignoreActive&&n===document.activeElement?null:(!1===a.callbacks.beforeNodeMorphed(n,o)||(n instanceof HTMLHeadElement&&a.head.ignore||(n instanceof HTMLHeadElement&&\"morph\"!==a.head.style?i(n,o,a):(!function(s,i,n){let o=i.nodeType;if(1===o){const o=s,a=i,c=o.attributes,l=a.attributes;for(const e of l)t(e.name,o,\"update\",n)||o.getAttribute(e.name)!==e.value&&o.setAttribute(e.name,e.value);for(let e=c.length-1;0<=e;e--){const s=c[e];if(s&&!a.hasAttribute(s.name)){if(t(s.name,o,\"remove\",n))continue;o.removeAttribute(s.name)}}r(o,n)||function(s,r,i){if(s instanceof HTMLInputElement&&r instanceof HTMLInputElement&&\"file\"!==r.type){let n=r.value,o=s.value;e(s,r,\"checked\",i),e(s,r,\"disabled\",i),r.hasAttribute(\"value\")?o!==n&&(t(\"value\",s,\"update\",i)||(s.setAttribute(\"value\",n),s.value=n)):t(\"value\",s,\"remove\",i)||(s.value=\"\",s.removeAttribute(\"value\"))}else if(s instanceof HTMLOptionElement&&r instanceof HTMLOptionElement)e(s,r,\"selected\",i);else if(s instanceof HTMLTextAreaElement&&r instanceof HTMLTextAreaElement){let e=r.value,n=s.value;if(t(\"value\",s,\"update\",i))return;e!==n&&(s.value=e),s.firstChild&&s.firstChild.nodeValue!==e&&(s.firstChild.nodeValue=e)}}(o,a,n)}8!==o&&3!==o||s.nodeValue!==i.nodeValue&&(s.nodeValue=i.nodeValue)}(n,o,a),r(n,a)||s(a,n,o))),a.callbacks.afterNodeMorphed(n,o)),n)}}();function i(e,t,s){let r=[],i=[],n=[],o=[],a=new Map;for(const e of t.children)a.set(e.outerHTML,e);for(const t of e.children){let e=a.has(t.outerHTML),r=s.head.shouldReAppend(t),c=s.head.shouldPreserve(t);e||c?r?i.push(t):(a.delete(t.outerHTML),n.push(t)):\"append\"===s.head.style?r&&(i.push(t),o.push(t)):!1!==s.head.shouldRemove(t)&&i.push(t)}o.push(...a.values());let c=[];for(const t of o){let i=document.createRange().createContextualFragment(t.outerHTML).firstChild;if(!1!==s.callbacks.beforeNodeAdded(i)){if(\"href\"in i&&i.href||\"src\"in i&&i.src){let e,t=new Promise((function(t){e=t}));i.addEventListener(\"load\",(function(){e()})),c.push(t)}e.appendChild(i),s.callbacks.afterNodeAdded(i),r.push(i)}}for(const t of i)!1!==s.callbacks.beforeNodeRemoved(t)&&(e.removeChild(t),s.callbacks.afterNodeRemoved(t));return s.head.afterHeadMorphed(e,{added:r,kept:n,removed:i}),c}const n=function(){function e(){const e=document.createElement(\"div\");return e.hidden=!0,document.body.insertAdjacentElement(\"afterend\",e),e}function s(e){let t=[],s=document.activeElement;if(\"BODY\"!==s?.tagName&&e.contains(s))for(;s&&(t.push(s),s!==e);)s=s.parentElement;return t}function r(e){let t=Array.from(e.querySelectorAll(\"[id]\"));return e.getAttribute?.(\"id\")&&t.push(e),t}function i(e,t,s,r){for(const i of r){const r=i.getAttribute(\"id\");if(t.has(r)){let t=i;for(;t;){let i=e.get(t);if(null==i&&(i=new Set,e.set(t,i)),i.add(r),t===s)break;t=t.parentElement}}}}return function(n,o,a){const{persistentIds:c,idMap:l}=function(e,t){const s=r(e),n=r(t),o=function(e,t){let s=new Set,r=new Map;for(const{id:t,tagName:i}of e)r.has(t)?s.add(t):r.set(t,i);let i=new Set;for(const{id:e,tagName:n}of t)i.has(e)?s.add(e):r.get(e)===n&&i.add(e);for(const e of s)i.delete(e);return i}(s,n);let a=new Map;i(a,o,e,s);const c=t.__idiomorphRoot||t;return i(a,o,c,n),{persistentIds:o,idMap:a}}(n,o),h=function(e){let s=Object.assign({},t);return Object.assign(s,e),s.callbacks=Object.assign({},t.callbacks,e.callbacks),s.head=Object.assign({},t.head,e.head),s}(a),d=h.morphStyle||\"outerHTML\";if(![\"innerHTML\",\"outerHTML\"].includes(d))throw`Do not understand how to morph style ${d}`;return{target:n,newContent:o,config:h,morphStyle:d,ignoreActive:h.ignoreActive,ignoreActiveValue:h.ignoreActiveValue,restoreFocus:h.restoreFocus,idMap:l,persistentIds:c,pantry:e(),activeElementAndParents:s(n),callbacks:h.callbacks,head:h.head}}}(),{normalizeElement:o,normalizeParent:a}=function(){const e=new WeakSet;class t{constructor(e){this.originalNode=e,this.realParentNode=e.parentNode,this.previousSibling=e.previousSibling,this.nextSibling=e.nextSibling}get childNodes(){const e=[];let t=this.previousSibling?this.previousSibling.nextSibling:this.realParentNode.firstChild;for(;t&&t!=this.nextSibling;)e.push(t),t=t.nextSibling;return e}querySelectorAll(e){return this.childNodes.reduce(((t,s)=>{if(s instanceof Element){s.matches(e)&&t.push(s);const r=s.querySelectorAll(e);for(let e=0;e<r.length;e++)t.push(r[e])}return t}),[])}insertBefore(e,t){return this.realParentNode.insertBefore(e,t)}moveBefore(e,t){return this.realParentNode.moveBefore(e,t)}get __idiomorphRoot(){return this.originalNode}}return{normalizeElement:function(e){return e instanceof Document?e.documentElement:e},normalizeParent:function s(r){if(null==r)return document.createElement(\"div\");if(\"string\"==typeof r)return s(function(t){let s=new DOMParser,r=t.replace(/<svg(\\s[^>]*>|>)([\\s\\S]*?)<\\/svg>/gim,\"\");if(r.match(/<\\/html>/)||r.match(/<\\/head>/)||r.match(/<\\/body>/)){let i=s.parseFromString(t,\"text/html\");if(r.match(/<\\/html>/))return e.add(i),i;{let t=i.firstChild;return t&&e.add(t),t}}{let r=s.parseFromString(\"<body><template>\"+t+\"</template></body>\",\"text/html\").body.querySelector(\"template\").content;return e.add(r),r}}(r));if(e.has(r))return r;if(r instanceof Node){if(r.parentNode)return new t(r);{const e=document.createElement(\"div\");return e.append(r),e}}{const e=document.createElement(\"div\");for(const t of[...r])e.append(t);return e}}}}();return{morph:function(e,t,r={}){e=o(e);const c=a(t),l=n(e,c,r),h=function(e,t){if(!e.config.restoreFocus)return t();let s=document.activeElement;if(!(s instanceof HTMLInputElement||s instanceof HTMLTextAreaElement))return t();const{id:r,selectionStart:i,selectionEnd:n}=s,o=t();r&&r!==document.activeElement?.getAttribute(\"id\")&&(s=e.target.querySelector(`[id=\"${r}\"]`),s?.focus());s&&!s.selectionEnd&&n&&s.setSelectionRange(i,n);return o}(l,(()=>function(e,t,s,r){if(e.head.block){const n=t.querySelector(\"head\"),o=s.querySelector(\"head\");if(n&&o){const t=i(n,o,e);return Promise.all(t).then((()=>{const t=Object.assign(e,{head:{block:!1,ignore:!0}});return r(t)}))}}return r(e)}(l,e,c,(t=>\"innerHTML\"===t.morphStyle?(s(t,e,c),Array.from(e.childNodes)):function(e,t,r){const i=a(t);return s(e,i,r,t,t.nextSibling),Array.from(i.childNodes)}(t,e,c)))));return l.pantry.remove(),h},defaults:t}}();function ve(e,t,{callbacks:s,...r}={}){be.morph(e,t,{...r,callbacks:new Ee(s)})}function Se(e,t,s={}){ve(e,t.childNodes,{...s,morphStyle:\"innerHTML\"})}function we(e,s){return e instanceof t&&e.shouldReloadWithMorph&&(!s||function(e,t){return t instanceof Element&&\"TURBO-FRAME\"===t.nodeName&&e.id===t.id&&(!t.getAttribute(\"src\")||N(e.src,t.getAttribute(\"src\")))}(e,s))&&!e.closest(\"[data-turbo-permanent]\")}function ye(e){return e.parentElement.closest(\"turbo-frame[src][refresh=morph]\")}class Ee{#h;constructor({beforeNodeMorphed:e}={}){this.#h=e||(()=>!0)}beforeNodeAdded=e=>!(e.id&&e.hasAttribute(\"data-turbo-permanent\")&&document.getElementById(e.id));beforeNodeMorphed=(e,t)=>{if(e instanceof Element){if(!e.hasAttribute(\"data-turbo-permanent\")&&this.#h(e,t)){return!i(\"turbo:before-morph-element\",{cancelable:!0,target:e,detail:{currentElement:e,newElement:t}}).defaultPrevented}return!1}};beforeAttributeUpdated=(e,t,s)=>!i(\"turbo:before-morph-attribute\",{cancelable:!0,target:t,detail:{attributeName:e,mutationType:s}}).defaultPrevented;beforeNodeRemoved=e=>this.beforeNodeMorphed(e);afterNodeMorphed=(e,t)=>{e instanceof Element&&i(\"turbo:morph-element\",{target:e,detail:{currentElement:e,newElement:t}})}}class Ae extends ge{static renderElement(e,t){i(\"turbo:before-frame-morph\",{target:e,detail:{currentElement:e,newElement:t}}),Se(e,t,{callbacks:{beforeNodeMorphed:(t,s)=>!we(t,s)||ye(t)!==e||(t.reload(),!1)}})}async preservingPermanentElements(e){return await e()}}class Re{static animationDuration=300;static get defaultCSS(){return h`\n      .turbo-progress-bar {\n        position: fixed;\n        display: block;\n        top: 0;\n        left: 0;\n        height: 3px;\n        background: #0076ff;\n        z-index: 2147483647;\n        transition:\n          width ${Re.animationDuration}ms ease-out,\n          opacity ${Re.animationDuration/2}ms ${Re.animationDuration/2}ms ease-in;\n        transform: translate3d(0, 0, 0);\n      }\n    `}hiding=!1;value=0;visible=!1;constructor(){this.stylesheetElement=this.createStylesheetElement(),this.progressElement=this.createProgressElement(),this.installStylesheetElement(),this.setValue(0)}show(){this.visible||(this.visible=!0,this.installProgressElement(),this.startTrickling())}hide(){this.visible&&!this.hiding&&(this.hiding=!0,this.fadeProgressElement((()=>{this.uninstallProgressElement(),this.stopTrickling(),this.visible=!1,this.hiding=!1})))}setValue(e){this.value=e,this.refresh()}installStylesheetElement(){document.head.insertBefore(this.stylesheetElement,document.head.firstChild)}installProgressElement(){this.progressElement.style.width=\"0\",this.progressElement.style.opacity=\"1\",document.documentElement.insertBefore(this.progressElement,document.body),this.refresh()}fadeProgressElement(e){this.progressElement.style.opacity=\"0\",setTimeout(e,1.5*Re.animationDuration)}uninstallProgressElement(){this.progressElement.parentNode&&document.documentElement.removeChild(this.progressElement)}startTrickling(){this.trickleInterval||(this.trickleInterval=window.setInterval(this.trickle,Re.animationDuration))}stopTrickling(){window.clearInterval(this.trickleInterval),delete this.trickleInterval}trickle=()=>{this.setValue(this.value+Math.random()/100)};refresh(){requestAnimationFrame((()=>{this.progressElement.style.width=10+90*this.value+\"%\"}))}createStylesheetElement(){const e=document.createElement(\"style\");e.type=\"text/css\",e.textContent=Re.defaultCSS;const t=w();return t&&(e.nonce=t),e}createProgressElement(){const e=document.createElement(\"div\");return e.className=\"turbo-progress-bar\",e}}class Te extends ne{detailsByOuterHTML=this.children.filter((e=>!function(e){const t=e.localName;return\"noscript\"==t}(e))).map((e=>function(e){e.hasAttribute(\"nonce\")&&e.setAttribute(\"nonce\",\"\");return e}(e))).reduce(((e,t)=>{const{outerHTML:s}=t,r=s in e?e[s]:{type:Le(t),tracked:Ce(t),elements:[]};return{...e,[s]:{...r,elements:[...r.elements,t]}}}),{});get trackedElementSignature(){return Object.keys(this.detailsByOuterHTML).filter((e=>this.detailsByOuterHTML[e].tracked)).join(\"\")}getScriptElementsNotInSnapshot(e){return this.getElementsMatchingTypeNotInSnapshot(\"script\",e)}getStylesheetElementsNotInSnapshot(e){return this.getElementsMatchingTypeNotInSnapshot(\"stylesheet\",e)}getElementsMatchingTypeNotInSnapshot(e,t){return Object.keys(this.detailsByOuterHTML).filter((e=>!(e in t.detailsByOuterHTML))).map((e=>this.detailsByOuterHTML[e])).filter((({type:t})=>t==e)).map((({elements:[e]})=>e))}get provisionalElements(){return Object.keys(this.detailsByOuterHTML).reduce(((e,t)=>{const{type:s,tracked:r,elements:i}=this.detailsByOuterHTML[t];return null!=s||r?i.length>1?[...e,...i.slice(1)]:e:[...e,...i]}),[])}getMetaValue(e){const t=this.findMetaElementByName(e);return t?t.getAttribute(\"content\"):null}findMetaElementByName(e){return Object.keys(this.detailsByOuterHTML).reduce(((t,s)=>{const{elements:[r]}=this.detailsByOuterHTML[s];return function(e,t){const s=e.localName;return\"meta\"==s&&e.getAttribute(\"name\")==t}(r,e)?r:t}),0)}}function Le(e){return function(e){const t=e.localName;return\"script\"==t}(e)?\"script\":function(e){const t=e.localName;return\"style\"==t||\"link\"==t&&\"stylesheet\"==e.getAttribute(\"rel\")}(e)?\"stylesheet\":void 0}function Ce(e){return\"reload\"==e.getAttribute(\"data-turbo-track\")}class Pe extends ne{static fromHTMLString(e=\"\"){return this.fromDocument(l(e))}static fromElement(e){return this.fromDocument(e.ownerDocument)}static fromDocument({documentElement:e,body:t,head:s}){return new this(e,t,new Te(s))}constructor(e,t,s){super(t),this.documentElement=e,this.headSnapshot=s}clone(){const e=this.element.cloneNode(!0),t=this.element.querySelectorAll(\"select\"),s=e.querySelectorAll(\"select\");for(const[e,r]of t.entries()){const t=s[e];for(const e of t.selectedOptions)e.selected=!1;for(const e of r.selectedOptions)t.options[e.index].selected=!0}for(const t of e.querySelectorAll('input[type=\"password\"]'))t.value=\"\";for(const t of e.querySelectorAll(\"noscript\"))t.remove();return new Pe(this.documentElement,e,this.headSnapshot)}get lang(){return this.documentElement.getAttribute(\"lang\")}get dir(){return this.documentElement.getAttribute(\"dir\")}get headElement(){return this.headSnapshot.element}get rootLocation(){return k(this.getSetting(\"root\")??\"/\")}get cacheControlValue(){return this.getSetting(\"cache-control\")}get isPreviewable(){return\"no-preview\"!=this.cacheControlValue}get isCacheable(){return\"no-cache\"!=this.cacheControlValue}get isVisitable(){return\"reload\"!=this.getSetting(\"visit-control\")}get prefersViewTransitions(){return(\"true\"===this.getSetting(\"view-transition\")||\"same-origin\"===this.headSnapshot.getMetaValue(\"view-transition\"))&&!window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches}get refreshMethod(){return this.getSetting(\"refresh-method\")}get refreshScroll(){return this.getSetting(\"refresh-scroll\")}getSetting(e){return this.headSnapshot.getMetaValue(`turbo-${e}`)}}class ke{#d=!1;#u=Promise.resolve();renderChange(e,t){return e&&this.viewTransitionsAvailable&&!this.#d?(this.#d=!0,this.#u=this.#u.then((async()=>{await document.startViewTransition(t).finished}))):this.#u=this.#u.then(t),this.#u}get viewTransitionsAvailable(){return document.startViewTransition}}const Me={action:\"advance\",historyChanged:!1,visitCachedSnapshot:()=>{},willRender:!0,updateHistory:!0,shouldCacheSnapshot:!0,acceptsStreamResponse:!1,refresh:{}},Fe=\"visitStart\",Ie=\"requestStart\",qe=\"requestEnd\",He=\"visitEnd\",Be=\"initialized\",Ne=\"started\",Oe=\"canceled\",De=\"failed\",xe=\"completed\",Ve=0,We=-1,Ue=-2,_e={advance:\"forward\",restore:\"back\",replace:\"none\"};class $e{identifier=d();timingMetrics={};followedRedirect=!1;historyChanged=!1;scrolled=!1;shouldCacheSnapshot=!0;acceptsStreamResponse=!1;snapshotCached=!1;state=Be;viewTransitioner=new ke;constructor(e,t,s,r={}){this.delegate=e,this.location=t,this.restorationIdentifier=s||d();const{action:i,historyChanged:n,referrer:o,snapshot:a,snapshotHTML:c,response:l,visitCachedSnapshot:h,willRender:u,updateHistory:m,shouldCacheSnapshot:p,acceptsStreamResponse:f,direction:g,refresh:b}={...Me,...r};this.action=i,this.historyChanged=n,this.referrer=o,this.snapshot=a,this.snapshotHTML=c,this.response=l,this.isPageRefresh=this.view.isPageRefresh(this),this.visitCachedSnapshot=h,this.willRender=u,this.updateHistory=m,this.scrolled=!u,this.shouldCacheSnapshot=p,this.acceptsStreamResponse=f,this.direction=g||_e[i],this.refresh=b}get adapter(){return this.delegate.adapter}get view(){return this.delegate.view}get history(){return this.delegate.history}get restorationData(){return this.history.getRestorationDataForIdentifier(this.restorationIdentifier)}start(){this.state==Be&&(this.recordTimingMetric(Fe),this.state=Ne,this.adapter.visitStarted(this),this.delegate.visitStarted(this))}cancel(){this.state==Ne&&(this.request&&this.request.cancel(),this.cancelRender(),this.state=Oe)}complete(){this.state==Ne&&(this.recordTimingMetric(He),this.adapter.visitCompleted(this),this.state=xe,this.followRedirect(),this.followedRedirect||this.delegate.visitCompleted(this))}fail(){this.state==Ne&&(this.state=De,this.adapter.visitFailed(this),this.delegate.visitCompleted(this))}changeHistory(){if(!this.historyChanged&&this.updateHistory){const e=g(this.location.href===this.referrer?.href?\"replace\":this.action);this.history.update(e,this.location,this.restorationIdentifier),this.historyChanged=!0}}issueRequest(){this.hasPreloadedResponse()?this.simulateRequest():this.shouldIssueRequest()&&!this.request&&(this.request=new z(this,_.get,this.location),this.request.perform())}simulateRequest(){this.response&&(this.startRequest(),this.recordResponse(),this.finishRequest())}startRequest(){this.recordTimingMetric(Ie),this.adapter.visitRequestStarted(this)}recordResponse(e=this.response){if(this.response=e,e){const{statusCode:t}=e;je(t)?this.adapter.visitRequestCompleted(this):this.adapter.visitRequestFailedWithStatusCode(this,t)}}finishRequest(){this.recordTimingMetric(qe),this.adapter.visitRequestFinished(this)}loadResponse(){if(this.response){const{statusCode:e,responseHTML:t}=this.response;this.render((async()=>{if(this.shouldCacheSnapshot&&this.cacheSnapshot(),this.view.renderPromise&&await this.view.renderPromise,je(e)&&null!=t){const e=Pe.fromHTMLString(t);await this.renderPageSnapshot(e,!1),this.adapter.visitRendered(this),this.complete()}else await this.view.renderError(Pe.fromHTMLString(t),this),this.adapter.visitRendered(this),this.fail()}))}}getCachedSnapshot(){const e=this.view.getCachedSnapshotForLocation(this.location)||this.getPreloadedSnapshot();if(e&&(!M(this.location)||e.hasAnchor(M(this.location)))&&(\"restore\"==this.action||e.isPreviewable))return e}getPreloadedSnapshot(){if(this.snapshotHTML)return Pe.fromHTMLString(this.snapshotHTML)}hasCachedSnapshot(){return null!=this.getCachedSnapshot()}loadCachedSnapshot(){const e=this.getCachedSnapshot();if(e){const t=this.shouldIssueRequest();this.render((async()=>{this.cacheSnapshot(),this.isPageRefresh?this.adapter.visitRendered(this):(this.view.renderPromise&&await this.view.renderPromise,await this.renderPageSnapshot(e,t),this.adapter.visitRendered(this),t||this.complete())}))}}followRedirect(){this.redirectedToLocation&&!this.followedRedirect&&this.response?.redirected&&(this.adapter.visitProposedToLocation(this.redirectedToLocation,{action:\"replace\",response:this.response,shouldCacheSnapshot:!1,willRender:!1}),this.followedRedirect=!0)}prepareRequest(e){this.acceptsStreamResponse&&e.acceptResponseType(Q.contentType)}requestStarted(){this.startRequest()}requestPreventedHandlingResponse(e,t){}async requestSucceededWithResponse(e,t){const s=await t.responseHTML,{redirected:r,statusCode:i}=t;null==s?this.recordResponse({statusCode:Ue,redirected:r}):(this.redirectedToLocation=t.redirected?t.location:void 0,this.recordResponse({statusCode:i,responseHTML:s,redirected:r}))}async requestFailedWithResponse(e,t){const s=await t.responseHTML,{redirected:r,statusCode:i}=t;null==s?this.recordResponse({statusCode:Ue,redirected:r}):this.recordResponse({statusCode:i,responseHTML:s,redirected:r})}requestErrored(e,t){this.recordResponse({statusCode:Ve,redirected:!1})}requestFinished(){this.finishRequest()}performScroll(){this.scrolled||this.view.forceReloaded||this.view.shouldPreserveScrollPosition(this)||(\"restore\"==this.action?this.scrollToRestoredPosition()||this.scrollToAnchor()||this.view.scrollToTop():this.scrollToAnchor()||this.view.scrollToTop(),this.scrolled=!0)}scrollToRestoredPosition(){const{scrollPosition:e}=this.restorationData;if(e)return this.view.scrollToPosition(e),!0}scrollToAnchor(){const e=M(this.location);if(null!=e)return this.view.scrollToAnchor(e),!0}recordTimingMetric(e){this.timingMetrics[e]=(new Date).getTime()}getTimingMetrics(){return{...this.timingMetrics}}hasPreloadedResponse(){return\"object\"==typeof this.response}shouldIssueRequest(){return\"restore\"==this.action?!this.hasCachedSnapshot():this.willRender}cacheSnapshot(){this.snapshotCached||(this.view.cacheSnapshot(this.snapshot).then((e=>e&&this.visitCachedSnapshot(e))),this.snapshotCached=!0)}async render(e){this.cancelRender(),await new Promise((e=>{this.frame=\"hidden\"===document.visibilityState?setTimeout((()=>e()),0):requestAnimationFrame((()=>e()))})),await e(),delete this.frame}async renderPageSnapshot(e,t){await this.viewTransitioner.renderChange(this.view.shouldTransitionTo(e),(async()=>{await this.view.renderPage(e,t,this.willRender,this),this.performScroll()}))}cancelRender(){this.frame&&(cancelAnimationFrame(this.frame),delete this.frame)}}function je(e){return e>=200&&e<300}class ze{progressBar=new Re;constructor(e){this.session=e}visitProposedToLocation(e,t){q(e,this.navigator.rootLocation)?this.navigator.startVisit(e,t?.restorationIdentifier||d(),t):window.location.href=e.toString()}visitStarted(e){this.location=e.location,this.redirectedToLocation=null,e.loadCachedSnapshot(),e.issueRequest()}visitRequestStarted(e){this.progressBar.setValue(0),e.hasCachedSnapshot()||\"restore\"!=e.action?this.showVisitProgressBarAfterDelay():this.showProgressBar()}visitRequestCompleted(e){e.loadResponse(),e.response.redirected&&(this.redirectedToLocation=e.redirectedToLocation)}visitRequestFailedWithStatusCode(e,t){switch(t){case Ve:case We:case Ue:return this.reload({reason:\"request_failed\",context:{statusCode:t}});default:return e.loadResponse()}}visitRequestFinished(e){}visitCompleted(e){this.progressBar.setValue(1),this.hideVisitProgressBar()}pageInvalidated(e){this.reload(e)}visitFailed(e){this.progressBar.setValue(1),this.hideVisitProgressBar()}visitRendered(e){}linkPrefetchingIsEnabledForLocation(e){return!0}formSubmissionStarted(e){this.progressBar.setValue(0),this.showFormProgressBarAfterDelay()}formSubmissionFinished(e){this.progressBar.setValue(1),this.hideFormProgressBar()}showVisitProgressBarAfterDelay(){this.visitProgressBarTimeout=window.setTimeout(this.showProgressBar,this.session.progressBarDelay)}hideVisitProgressBar(){this.progressBar.hide(),null!=this.visitProgressBarTimeout&&(window.clearTimeout(this.visitProgressBarTimeout),delete this.visitProgressBarTimeout)}showFormProgressBarAfterDelay(){null==this.formProgressBarTimeout&&(this.formProgressBarTimeout=window.setTimeout(this.showProgressBar,this.session.progressBarDelay))}hideFormProgressBar(){this.progressBar.hide(),null!=this.formProgressBarTimeout&&(window.clearTimeout(this.formProgressBarTimeout),delete this.formProgressBarTimeout)}showProgressBar=()=>{this.progressBar.show()};reload(e){i(\"turbo:reload\",{detail:e}),window.location.href=(this.redirectedToLocation||this.location)?.toString()||window.location.href}get navigator(){return this.session.navigator}}class Ge{selector=\"[data-turbo-temporary]\";started=!1;start(){this.started||(this.started=!0,addEventListener(\"turbo:before-cache\",this.removeTemporaryElements,!1))}stop(){this.started&&(this.started=!1,removeEventListener(\"turbo:before-cache\",this.removeTemporaryElements,!1))}removeTemporaryElements=e=>{for(const e of this.temporaryElements)e.remove()};get temporaryElements(){return[...document.querySelectorAll(this.selector)]}}class Ke{constructor(e,t){this.session=e,this.element=t,this.linkInterceptor=new de(this,t),this.formSubmitObserver=new ce(this,t)}start(){this.linkInterceptor.start(),this.formSubmitObserver.start()}stop(){this.linkInterceptor.stop(),this.formSubmitObserver.stop()}shouldInterceptLinkClick(e,t,s){return this.#m(e)}linkClickIntercepted(e,t,s){const r=this.#p(e);r&&r.delegate.linkClickIntercepted(e,t,s)}willSubmitForm(e,t){return null==e.closest(\"turbo-frame\")&&this.#f(e,t)&&this.#m(e,t)}formSubmitted(e,t){const s=this.#p(e,t);s&&s.delegate.formSubmitted(e,t)}#f(e,t){const s=F(e,t),r=this.element.ownerDocument.querySelector('meta[name=\"turbo-root\"]'),i=k(r?.content??\"/\");return this.#m(e,t)&&q(s,i)}#m(e,t){if(e instanceof HTMLFormElement?this.session.submissionIsNavigatable(e,t):this.session.elementIsNavigatable(e)){const s=this.#p(e,t);return!!s&&s!=e.closest(\"turbo-frame\")}return!1}#p(e,s){const r=s?.getAttribute(\"data-turbo-frame\")||e.getAttribute(\"data-turbo-frame\");if(r&&\"_top\"!=r){const e=this.element.querySelector(`#${r}:not([disabled])`);if(e instanceof t)return e}}}class Je{location;restorationIdentifier=d();restorationData={};started=!1;currentIndex=0;constructor(e){this.delegate=e}start(){this.started||(addEventListener(\"popstate\",this.onPopState,!1),this.currentIndex=history.state?.turbo?.restorationIndex||0,this.started=!0,this.replace(new URL(window.location.href)))}stop(){this.started&&(removeEventListener(\"popstate\",this.onPopState,!1),this.started=!1)}push(e,t){this.update(history.pushState,e,t)}replace(e,t){this.update(history.replaceState,e,t)}update(e,t,s=d()){e===history.pushState&&++this.currentIndex;const r={turbo:{restorationIdentifier:s,restorationIndex:this.currentIndex}};e.call(history,r,\"\",t.href),this.location=t,this.restorationIdentifier=s}getRestorationDataForIdentifier(e){return this.restorationData[e]||{}}updateRestorationData(e){const{restorationIdentifier:t}=this,s=this.restorationData[t];this.restorationData[t]={...s,...e}}assumeControlOfScrollRestoration(){this.previousScrollRestoration||(this.previousScrollRestoration=history.scrollRestoration??\"auto\",history.scrollRestoration=\"manual\")}relinquishControlOfScrollRestoration(){this.previousScrollRestoration&&(history.scrollRestoration=this.previousScrollRestoration,delete this.previousScrollRestoration)}onPopState=e=>{const{turbo:t}=e.state||{};if(this.location=new URL(window.location.href),t){const{restorationIdentifier:e,restorationIndex:s}=t;this.restorationIdentifier=e;const r=s>this.currentIndex?\"forward\":\"back\";this.delegate.historyPoppedToLocationWithRestorationIdentifierAndDirection(this.location,e,r),this.currentIndex=s}else this.currentIndex++,this.delegate.historyPoppedWithEmptyState(this.location)}}class Xe{started=!1;#g=null;constructor(e,t){this.delegate=e,this.eventTarget=t}start(){this.started||(\"loading\"===this.eventTarget.readyState?this.eventTarget.addEventListener(\"DOMContentLoaded\",this.#b,{once:!0}):this.#b())}stop(){this.started&&(this.eventTarget.removeEventListener(\"mouseenter\",this.#v,{capture:!0,passive:!0}),this.eventTarget.removeEventListener(\"mouseleave\",this.#S,{capture:!0,passive:!0}),this.eventTarget.removeEventListener(\"turbo:before-fetch-request\",this.#w,!0),this.started=!1)}#b=()=>{this.eventTarget.addEventListener(\"mouseenter\",this.#v,{capture:!0,passive:!0}),this.eventTarget.addEventListener(\"mouseleave\",this.#S,{capture:!0,passive:!0}),this.eventTarget.addEventListener(\"turbo:before-fetch-request\",this.#w,!0),this.started=!0};#v=e=>{if(\"false\"===S(\"turbo-prefetch\"))return;const t=e.target;if(t.matches&&t.matches(\"a[href]:not([target^=_]):not([download])\")&&this.#y(t)){const e=t,s=H(e);if(this.delegate.canPrefetchRequestToLocation(e,s)){this.#g=e;const r=new z(this,_.get,s,new URLSearchParams,t);r.fetchOptions.priority=\"low\",se.putLater(s,r,this.#E)}}};#S=e=>{e.target===this.#g&&this.#A()};#A=()=>{se.clear(),this.#g=null};#w=e=>{if(\"FORM\"!==e.target.tagName&&\"GET\"===e.detail.fetchOptions.method){const t=se.get(e.detail.url);t&&(e.detail.fetchRequest=t),se.clear()}};prepareRequest(e){const t=e.target;e.headers[\"X-Sec-Purpose\"]=\"prefetch\";const s=t.closest(\"turbo-frame\"),r=t.getAttribute(\"data-turbo-frame\")||s?.getAttribute(\"target\")||s?.id;r&&\"_top\"!==r&&(e.headers[\"Turbo-Frame\"]=r)}requestSucceededWithResponse(){}requestStarted(e){}requestErrored(e){}requestFinished(e){}requestPreventedHandlingResponse(e,t){}requestFailedWithResponse(e,t){}get#E(){return Number(S(\"turbo-prefetch-cache-time\"))||te}#y(e){return!!e.getAttribute(\"href\")&&(!Ye(e)&&(!Qe(e)&&(!Ze(e)&&(!et(e)&&!st(e)))))}}const Ye=e=>e.origin!==document.location.origin||![\"http:\",\"https:\"].includes(e.protocol)||e.hasAttribute(\"target\"),Qe=e=>e.pathname+e.search===document.location.pathname+document.location.search||e.href.startsWith(\"#\"),Ze=e=>{if(\"false\"===e.getAttribute(\"data-turbo-prefetch\"))return!0;if(\"false\"===e.getAttribute(\"data-turbo\"))return!0;const t=y(e,\"[data-turbo-prefetch]\");return!(!t||\"false\"!==t.getAttribute(\"data-turbo-prefetch\"))},et=e=>{const t=e.getAttribute(\"data-turbo-method\");return!(!t||\"get\"===t.toLowerCase())||(!!tt(e)||(!!e.hasAttribute(\"data-turbo-confirm\")||!!e.hasAttribute(\"data-turbo-stream\")))},tt=e=>e.hasAttribute(\"data-remote\")||e.hasAttribute(\"data-behavior\")||e.hasAttribute(\"data-confirm\")||e.hasAttribute(\"data-method\"),st=e=>i(\"turbo:before-prefetch\",{target:e,cancelable:!0}).defaultPrevented;class rt{constructor(e){this.delegate=e}proposeVisit(e,t={}){this.delegate.allowsVisitingLocationWithAction(e,t.action)&&this.delegate.visitProposedToLocation(e,t)}startVisit(e,t,s={}){this.stop(),this.currentVisit=new $e(this,k(e),t,{referrer:this.location,...s}),this.currentVisit.start()}submitForm(e,t){this.stop(),this.formSubmission=new ie(this,e,t,!0),this.formSubmission.start()}stop(){this.formSubmission&&(this.formSubmission.stop(),delete this.formSubmission),this.currentVisit&&(this.currentVisit.cancel(),delete this.currentVisit)}get adapter(){return this.delegate.adapter}get view(){return this.delegate.view}get rootLocation(){return this.view.snapshot.rootLocation}get history(){return this.delegate.history}formSubmissionStarted(e){\"function\"==typeof this.adapter.formSubmissionStarted&&this.adapter.formSubmissionStarted(e)}async formSubmissionSucceededWithResponse(e,t){if(e==this.formSubmission){const s=await t.responseHTML;if(s){const r=e.isSafe;r||this.view.clearSnapshotCache();const{statusCode:i,redirected:n}=t,o={action:this.#R(e,t),shouldCacheSnapshot:r,response:{statusCode:i,responseHTML:s,redirected:n}};this.proposeVisit(t.location,o)}}}async formSubmissionFailedWithResponse(e,t){const s=await t.responseHTML;if(s){const e=Pe.fromHTMLString(s);t.serverError?await this.view.renderError(e,this.currentVisit):await this.view.renderPage(e,!1,!0,this.currentVisit),\"preserve\"!==e.refreshScroll&&this.view.scrollToTop(),this.view.clearSnapshotCache()}}formSubmissionErrored(e,t){console.error(t)}formSubmissionFinished(e){\"function\"==typeof this.adapter.formSubmissionFinished&&this.adapter.formSubmissionFinished(e)}linkPrefetchingIsEnabledForLocation(e){return\"function\"!=typeof this.adapter.linkPrefetchingIsEnabledForLocation||this.adapter.linkPrefetchingIsEnabledForLocation(e)}visitStarted(e){this.delegate.visitStarted(e)}visitCompleted(e){this.delegate.visitCompleted(e),delete this.currentVisit}locationWithActionIsSamePage(e,t){return!1}get location(){return this.history.location}get restorationIdentifier(){return this.history.restorationIdentifier}#R(e,t){const{submitter:s,formElement:r}=e;return b(s,r)||this.#T(t)}#T(e){return e.redirected&&e.location.href===this.location?.href?\"replace\":\"advance\"}}const it=0,nt=1,ot=2,at=3;class ct{stage=it;started=!1;constructor(e){this.delegate=e}start(){this.started||(this.stage==it&&(this.stage=nt),document.addEventListener(\"readystatechange\",this.interpretReadyState,!1),addEventListener(\"pagehide\",this.pageWillUnload,!1),this.started=!0)}stop(){this.started&&(document.removeEventListener(\"readystatechange\",this.interpretReadyState,!1),removeEventListener(\"pagehide\",this.pageWillUnload,!1),this.started=!1)}interpretReadyState=()=>{const{readyState:e}=this;\"interactive\"==e?this.pageIsInteractive():\"complete\"==e&&this.pageIsComplete()};pageIsInteractive(){this.stage==nt&&(this.stage=ot,this.delegate.pageBecameInteractive())}pageIsComplete(){this.pageIsInteractive(),this.stage==ot&&(this.stage=at,this.delegate.pageLoaded())}pageWillUnload=()=>{this.delegate.pageWillUnload()};get readyState(){return document.readyState}}class lt{started=!1;constructor(e){this.delegate=e}start(){this.started||(addEventListener(\"scroll\",this.onScroll,!1),this.onScroll(),this.started=!0)}stop(){this.started&&(removeEventListener(\"scroll\",this.onScroll,!1),this.started=!1)}onScroll=()=>{this.updatePosition({x:window.pageXOffset,y:window.pageYOffset})};updatePosition(e){this.delegate.scrollPositionChanged(e)}}class ht{render({fragment:e}){pe.preservingPermanentElements(this,function(e){const t=ae(document.documentElement),s={};for(const r of t){const{id:t}=r;for(const i of e.querySelectorAll(\"turbo-stream\")){const e=oe(i.templateElement.content,t);e&&(s[t]=[r,e])}}return s}(e),(()=>{!async function(e,t){const s=`turbo-stream-autofocus-${d()}`,r=e.querySelectorAll(\"turbo-stream\"),i=function(e){for(const t of e){const e=A(t.templateElement.content);if(e)return e}return null}(r);let n=null;i&&(n=i.id?i.id:s,i.id=n);t(),await o();if((null==document.activeElement||document.activeElement==document.body)&&n){const e=document.getElementById(n);E(e)&&e.focus(),e&&e.id==s&&e.removeAttribute(\"id\")}}(e,(()=>{!async function(e){const[t,s]=await async function(e,t){const s=t();return e(),await a(),[s,t()]}(e,(()=>document.activeElement)),r=t&&t.id;if(r){const e=document.getElementById(r);E(e)&&e!=s&&e.focus()}}((()=>{document.documentElement.appendChild(e)}))}))}))}enteringBardo(e,t){t.replaceWith(e.cloneNode(!0))}leavingBardo(){}}class dt{sources=new Set;#L=!1;constructor(e){this.delegate=e}start(){this.#L||(this.#L=!0,addEventListener(\"turbo:before-fetch-response\",this.inspectFetchResponse,!1))}stop(){this.#L&&(this.#L=!1,removeEventListener(\"turbo:before-fetch-response\",this.inspectFetchResponse,!1))}connectStreamSource(e){this.streamSourceIsConnected(e)||(this.sources.add(e),e.addEventListener(\"message\",this.receiveMessageEvent,!1))}disconnectStreamSource(e){this.streamSourceIsConnected(e)&&(this.sources.delete(e),e.removeEventListener(\"message\",this.receiveMessageEvent,!1))}streamSourceIsConnected(e){return this.sources.has(e)}inspectFetchResponse=e=>{const t=function(e){const t=e.detail?.fetchResponse;if(t instanceof D)return t}(e);t&&function(e){const t=e.contentType??\"\";return t.startsWith(Q.contentType)}(t)&&(e.preventDefault(),this.receiveMessageResponse(t))};receiveMessageEvent=e=>{this.#L&&\"string\"==typeof e.data&&this.receiveMessageHTML(e.data)};async receiveMessageResponse(e){const t=await e.responseHTML;t&&this.receiveMessageHTML(t)}receiveMessageHTML(e){this.delegate.receivedMessageFromStream(Q.wrap(e))}}class ut extends fe{static renderElement(e,t){const{documentElement:s,body:r}=document;s.replaceChild(t,r)}async render(){this.replaceHeadAndBody(),this.activateScriptElements()}replaceHeadAndBody(){const{documentElement:e,head:t}=document;e.replaceChild(this.newHead,t),this.renderElement(this.currentElement,this.newElement)}activateScriptElements(){for(const e of this.scriptElements){const t=e.parentNode;if(t){const s=r(e);t.replaceChild(s,e)}}}get newHead(){return this.newSnapshot.headSnapshot.element}get scriptElements(){return document.documentElement.querySelectorAll(\"script\")}}class mt extends fe{static renderElement(e,t){document.body&&t instanceof HTMLBodyElement?document.body.replaceWith(t):document.documentElement.appendChild(t)}get shouldRender(){return this.newSnapshot.isVisitable&&this.trackedElementsAreIdentical}get reloadReason(){return this.newSnapshot.isVisitable?this.trackedElementsAreIdentical?void 0:{reason:\"tracked_element_mismatch\"}:{reason:\"turbo_visit_control_is_reload\"}}async prepareToRender(){this.#C(),await this.mergeHead()}async render(){this.willRender&&await this.replaceBody()}finishRendering(){super.finishRendering(),this.isPreview||this.focusFirstAutofocusableElement()}get currentHeadSnapshot(){return this.currentSnapshot.headSnapshot}get newHeadSnapshot(){return this.newSnapshot.headSnapshot}get newElement(){return this.newSnapshot.element}#C(){const{documentElement:e}=this.currentSnapshot,{dir:t,lang:s}=this.newSnapshot;s?e.setAttribute(\"lang\",s):e.removeAttribute(\"lang\"),t?e.setAttribute(\"dir\",t):e.removeAttribute(\"dir\")}async mergeHead(){const e=this.mergeProvisionalElements(),t=this.copyNewHeadStylesheetElements();this.copyNewHeadScriptElements(),await e,await t,this.willRender&&this.removeUnusedDynamicStylesheetElements()}async replaceBody(){await this.preservingPermanentElements((async()=>{this.activateNewBody(),await this.assignNewBody()}))}get trackedElementsAreIdentical(){return this.currentHeadSnapshot.trackedElementSignature==this.newHeadSnapshot.trackedElementSignature}async copyNewHeadStylesheetElements(){const e=[];for(const t of this.newHeadStylesheetElements)e.push(f(t)),document.head.appendChild(t);await Promise.all(e)}copyNewHeadScriptElements(){for(const e of this.newHeadScriptElements)document.head.appendChild(r(e))}removeUnusedDynamicStylesheetElements(){for(const e of this.unusedDynamicStylesheetElements)document.head.removeChild(e)}async mergeProvisionalElements(){const e=[...this.newHeadProvisionalElements];for(const t of this.currentHeadProvisionalElements)this.isCurrentElementInElementList(t,e)||document.head.removeChild(t);for(const t of e)document.head.appendChild(t)}isCurrentElementInElementList(e,t){for(const[s,r]of t.entries()){if(\"TITLE\"==e.tagName){if(\"TITLE\"!=r.tagName)continue;if(e.innerHTML==r.innerHTML)return t.splice(s,1),!0}if(r.isEqualNode(e))return t.splice(s,1),!0}return!1}removeCurrentHeadProvisionalElements(){for(const e of this.currentHeadProvisionalElements)document.head.removeChild(e)}copyNewHeadProvisionalElements(){for(const e of this.newHeadProvisionalElements)document.head.appendChild(e)}activateNewBody(){document.adoptNode(this.newElement),this.removeNoscriptElements(),this.activateNewBodyScriptElements()}removeNoscriptElements(){for(const e of this.newElement.querySelectorAll(\"noscript\"))e.remove()}activateNewBodyScriptElements(){for(const e of this.newBodyScriptElements){const t=r(e);e.replaceWith(t)}}async assignNewBody(){await this.renderElement(this.currentElement,this.newElement)}get unusedDynamicStylesheetElements(){return this.oldHeadStylesheetElements.filter((e=>\"dynamic\"===e.getAttribute(\"data-turbo-track\")))}get oldHeadStylesheetElements(){return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot)}get newHeadStylesheetElements(){return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot)}get newHeadScriptElements(){return this.newHeadSnapshot.getScriptElementsNotInSnapshot(this.currentHeadSnapshot)}get currentHeadProvisionalElements(){return this.currentHeadSnapshot.provisionalElements}get newHeadProvisionalElements(){return this.newHeadSnapshot.provisionalElements}get newBodyScriptElements(){return this.newElement.querySelectorAll(\"script\")}}class pt extends mt{static renderElement(e,t){ve(e,t,{callbacks:{beforeNodeMorphed:(e,t)=>!(we(e,t)&&!ye(e))||(e.reload(),!1)}}),i(\"turbo:morph\",{detail:{currentElement:e,newElement:t}})}async preservingPermanentElements(e){return await e()}get renderMethod(){return\"morph\"}get shouldAutofocus(){return!1}}class ft extends ee{constructor(e){super(e,B)}get snapshots(){return this.entries}}class gt extends le{snapshotCache=new ft(10);lastRenderedLocation=new URL(location.href);forceReloaded=!1;shouldTransitionTo(e){return this.snapshot.prefersViewTransitions&&e.prefersViewTransitions}renderPage(e,t=!1,s=!0,r){const i=new(this.isPageRefresh(r)&&\"morph\"===(r?.refresh?.method||this.snapshot.refreshMethod)?pt:mt)(this.snapshot,e,t,s);return i.shouldRender?r?.changeHistory():this.forceReloaded=!0,this.render(i)}renderError(e,t){t?.changeHistory();const s=new ut(this.snapshot,e,!1);return this.render(s)}clearSnapshotCache(){this.snapshotCache.clear()}async cacheSnapshot(e=this.snapshot){if(e.isCacheable){this.delegate.viewWillCacheSnapshot();const{lastRenderedLocation:t}=this;await c();const s=e.clone();return this.snapshotCache.put(t,s),s}}getCachedSnapshotForLocation(e){return this.snapshotCache.get(e)}isPageRefresh(e){return!e||this.lastRenderedLocation.pathname===e.location.pathname&&\"replace\"===e.action}shouldPreserveScrollPosition(e){return this.isPageRefresh(e)&&\"preserve\"===(e?.refresh?.scroll||this.snapshot.refreshScroll)}get snapshot(){return Pe.fromElement(this.element)}}class bt{selector=\"a[data-turbo-preload]\";constructor(e,t){this.delegate=e,this.snapshotCache=t}start(){\"loading\"===document.readyState?document.addEventListener(\"DOMContentLoaded\",this.#P):this.preloadOnLoadLinksForView(document.body)}stop(){document.removeEventListener(\"DOMContentLoaded\",this.#P)}preloadOnLoadLinksForView(e){for(const t of e.querySelectorAll(this.selector))this.delegate.shouldPreloadLink(t)&&this.preloadURL(t)}async preloadURL(e){const t=new URL(e.href);if(this.snapshotCache.has(t))return;const s=new z(this,_.get,t,new URLSearchParams,e);await s.perform()}prepareRequest(e){e.headers[\"X-Sec-Purpose\"]=\"prefetch\"}async requestSucceededWithResponse(e,t){try{const s=await t.responseHTML,r=Pe.fromHTMLString(s);this.snapshotCache.put(e.url,r)}catch(e){}}requestStarted(e){}requestErrored(e){}requestFinished(e){}requestPreventedHandlingResponse(e,t){}requestFailedWithResponse(e,t){}#P=()=>{this.preloadOnLoadLinksForView(document.body)}}class vt{constructor(e){this.session=e}clear(){this.session.clearCache()}resetCacheControl(){this.#k(\"\")}exemptPageFromCache(){this.#k(\"no-cache\")}exemptPageFromPreview(){this.#k(\"no-preview\")}#k(e){!function(e,t){let s=v(e);s||(s=document.createElement(\"meta\"),s.setAttribute(\"name\",e),document.head.appendChild(s)),s.setAttribute(\"content\",t)}(\"turbo-cache-control\",e)}}function St(e){Object.defineProperties(e,wt)}const wt={absoluteURL:{get(){return this.toString()}}},yt=new class{navigator=new rt(this);history=new Je(this);view=new gt(this,document.documentElement);adapter=new ze(this);pageObserver=new ct(this);cacheObserver=new Ge;linkPrefetchObserver=new Xe(this,document);linkClickObserver=new ue(this,window);formSubmitObserver=new ce(this,document);scrollObserver=new lt(this);streamObserver=new dt(this);formLinkClickObserver=new me(this,document.documentElement);frameRedirector=new Ke(this,document.documentElement);streamMessageRenderer=new ht;cache=new vt(this);enabled=!0;started=!1;#M=150;constructor(e){this.recentRequests=e,this.preloader=new bt(this,this.view.snapshotCache),this.debouncedRefresh=this.refresh,this.pageRefreshDebouncePeriod=this.pageRefreshDebouncePeriod}start(){this.started||(this.pageObserver.start(),this.cacheObserver.start(),this.linkPrefetchObserver.start(),this.formLinkClickObserver.start(),this.linkClickObserver.start(),this.formSubmitObserver.start(),this.scrollObserver.start(),this.streamObserver.start(),this.frameRedirector.start(),this.history.start(),this.preloader.start(),this.started=!0,this.enabled=!0)}disable(){this.enabled=!1}stop(){this.started&&(this.pageObserver.stop(),this.cacheObserver.stop(),this.linkPrefetchObserver.stop(),this.formLinkClickObserver.stop(),this.linkClickObserver.stop(),this.formSubmitObserver.stop(),this.scrollObserver.stop(),this.streamObserver.stop(),this.frameRedirector.stop(),this.history.stop(),this.preloader.stop(),this.started=!1)}registerAdapter(e){this.adapter=e}visit(e,s={}){const r=s.frame?document.getElementById(s.frame):null;if(r instanceof t){const t=s.action||b(r);r.delegate.proposeVisitIfNavigatedWithAction(r,t),r.src=e.toString()}else this.navigator.proposeVisit(k(e),s)}refresh(e,t={}){t=\"string\"==typeof t?{requestId:t}:t;const{method:s,requestId:r,scroll:i}=t,n=r&&this.recentRequests.has(r),o=e===document.baseURI;n||this.navigator.currentVisit||!o||this.visit(e,{action:\"replace\",shouldCacheSnapshot:!1,refresh:{method:s,scroll:i}})}connectStreamSource(e){this.streamObserver.connectStreamSource(e)}disconnectStreamSource(e){this.streamObserver.disconnectStreamSource(e)}renderStreamMessage(e){this.streamMessageRenderer.render(Q.wrap(e))}clearCache(){this.view.clearSnapshotCache()}setProgressBarDelay(e){console.warn(\"Please replace `session.setProgressBarDelay(delay)` with `session.progressBarDelay = delay`. The function is deprecated and will be removed in a future version of Turbo.`\"),this.progressBarDelay=e}set progressBarDelay(e){P.drive.progressBarDelay=e}get progressBarDelay(){return P.drive.progressBarDelay}set drive(e){P.drive.enabled=e}get drive(){return P.drive.enabled}set formMode(e){P.forms.mode=e}get formMode(){return P.forms.mode}get location(){return this.history.location}get restorationIdentifier(){return this.history.restorationIdentifier}get pageRefreshDebouncePeriod(){return this.#M}set pageRefreshDebouncePeriod(e){this.refresh=function(e,t){let s=null;return(...r)=>{clearTimeout(s),s=setTimeout((()=>e.apply(this,r)),t)}}(this.debouncedRefresh.bind(this),e),this.#M=e}shouldPreloadLink(e){const s=e.hasAttribute(\"data-turbo-method\"),r=e.hasAttribute(\"data-turbo-stream\"),i=e.getAttribute(\"data-turbo-frame\"),n=\"_top\"==i?null:document.getElementById(i)||y(e,\"turbo-frame:not([disabled])\");if(s||r||n instanceof t)return!1;{const t=new URL(e.href);return this.elementIsNavigatable(e)&&q(t,this.snapshot.rootLocation)}}historyPoppedToLocationWithRestorationIdentifierAndDirection(e,t,s){this.enabled?this.navigator.startVisit(e,t,{action:\"restore\",historyChanged:!0,direction:s}):this.adapter.pageInvalidated({reason:\"turbo_disabled\"})}historyPoppedWithEmptyState(e){this.history.replace(e),this.view.lastRenderedLocation=e,this.view.cacheSnapshot()}scrollPositionChanged(e){this.history.updateRestorationData({scrollPosition:e})}willSubmitFormLinkToLocation(e,t){return this.elementIsNavigatable(e)&&q(t,this.snapshot.rootLocation)}submittedFormLinkToLocation(){}canPrefetchRequestToLocation(e,t){return this.elementIsNavigatable(e)&&q(t,this.snapshot.rootLocation)&&this.navigator.linkPrefetchingIsEnabledForLocation(t)}willFollowLinkToLocation(e,t,s){return this.elementIsNavigatable(e)&&q(t,this.snapshot.rootLocation)&&this.applicationAllowsFollowingLinkToLocation(e,t,s)}followedLinkToLocation(e,t){const s=this.getActionForLink(e),r=e.hasAttribute(\"data-turbo-stream\");this.visit(t.href,{action:s,acceptsStreamResponse:r})}allowsVisitingLocationWithAction(e,t){return this.applicationAllowsVisitingLocation(e)}visitProposedToLocation(e,t){St(e),this.adapter.visitProposedToLocation(e,t)}visitStarted(e){e.acceptsStreamResponse||(m(document.documentElement),this.view.markVisitDirection(e.direction)),St(e.location),this.notifyApplicationAfterVisitingLocation(e.location,e.action)}visitCompleted(e){this.view.unmarkVisitDirection(),p(document.documentElement),this.notifyApplicationAfterPageLoad(e.getTimingMetrics())}willSubmitForm(e,t){const s=F(e,t);return this.submissionIsNavigatable(e,t)&&q(k(s),this.snapshot.rootLocation)}formSubmitted(e,t){this.navigator.submitForm(e,t)}pageBecameInteractive(){this.view.lastRenderedLocation=this.location,this.notifyApplicationAfterPageLoad()}pageLoaded(){this.history.assumeControlOfScrollRestoration()}pageWillUnload(){this.history.relinquishControlOfScrollRestoration()}receivedMessageFromStream(e){this.renderStreamMessage(e)}viewWillCacheSnapshot(){this.notifyApplicationBeforeCachingSnapshot()}allowsImmediateRender({element:e},t){const s=this.notifyApplicationBeforeRender(e,t),{defaultPrevented:r,detail:{render:i}}=s;return this.view.renderer&&i&&(this.view.renderer.renderElement=i),!r}viewRenderedSnapshot(e,t,s){this.view.lastRenderedLocation=this.history.location,this.notifyApplicationAfterRender(s)}preloadOnLoadLinksForView(e){this.preloader.preloadOnLoadLinksForView(e)}viewInvalidated(e){this.adapter.pageInvalidated(e)}frameLoaded(e){this.notifyApplicationAfterFrameLoad(e)}frameRendered(e,t){this.notifyApplicationAfterFrameRender(e,t)}applicationAllowsFollowingLinkToLocation(e,t,s){return!this.notifyApplicationAfterClickingLinkToLocation(e,t,s).defaultPrevented}applicationAllowsVisitingLocation(e){return!this.notifyApplicationBeforeVisitingLocation(e).defaultPrevented}notifyApplicationAfterClickingLinkToLocation(e,t,s){return i(\"turbo:click\",{target:e,detail:{url:t.href,originalEvent:s},cancelable:!0})}notifyApplicationBeforeVisitingLocation(e){return i(\"turbo:before-visit\",{detail:{url:e.href},cancelable:!0})}notifyApplicationAfterVisitingLocation(e,t){return i(\"turbo:visit\",{detail:{url:e.href,action:t}})}notifyApplicationBeforeCachingSnapshot(){return i(\"turbo:before-cache\")}notifyApplicationBeforeRender(e,t){return i(\"turbo:before-render\",{detail:{newBody:e,...t},cancelable:!0})}notifyApplicationAfterRender(e){return i(\"turbo:render\",{detail:{renderMethod:e}})}notifyApplicationAfterPageLoad(e={}){return i(\"turbo:load\",{detail:{url:this.location.href,timing:e}})}notifyApplicationAfterFrameLoad(e){return i(\"turbo:frame-load\",{target:e})}notifyApplicationAfterFrameRender(e,t){return i(\"turbo:frame-render\",{detail:{fetchResponse:e},target:t,cancelable:!0})}submissionIsNavigatable(e,t){if(\"off\"==P.forms.mode)return!1;{const s=!t||this.elementIsNavigatable(t);return\"optin\"==P.forms.mode?s&&null!=e.closest('[data-turbo=\"true\"]'):s&&this.elementIsNavigatable(e)}}elementIsNavigatable(e){const t=y(e,\"[data-turbo]\"),s=y(e,\"turbo-frame\");return P.drive.enabled||s?!t||\"false\"!=t.getAttribute(\"data-turbo\"):!!t&&\"true\"==t.getAttribute(\"data-turbo\")}getActionForLink(e){return b(e)||\"advance\"}get snapshot(){return this.view.snapshot}}(V),{cache:Et,navigator:At}=yt;function Rt(){yt.start()}function Tt(e){yt.registerAdapter(e)}function Lt(e,t){yt.visit(e,t)}function Ct(e){yt.connectStreamSource(e)}function Pt(e){yt.disconnectStreamSource(e)}function kt(e){yt.renderStreamMessage(e)}function Mt(e){console.warn(\"Please replace `Turbo.setProgressBarDelay(delay)` with `Turbo.config.drive.progressBarDelay = delay`. The top-level function is deprecated and will be removed in a future version of Turbo.`\"),P.drive.progressBarDelay=e}function Ft(e){console.warn(\"Please replace `Turbo.setConfirmMethod(confirmMethod)` with `Turbo.config.forms.confirm = confirmMethod`. The top-level function is deprecated and will be removed in a future version of Turbo.`\"),P.forms.confirm=e}function It(e){console.warn(\"Please replace `Turbo.setFormMode(mode)` with `Turbo.config.forms.mode = mode`. The top-level function is deprecated and will be removed in a future version of Turbo.`\"),P.forms.mode=e}function qt(e,t){pt.renderElement(e,t)}function Ht(e,t){Ae.renderElement(e,t)}var Bt=Object.freeze({__proto__:null,PageRenderer:mt,PageSnapshot:Pe,FrameRenderer:ge,fetch:W,config:P,session:yt,cache:Et,navigator:At,start:Rt,registerAdapter:Tt,visit:Lt,connectStreamSource:Ct,disconnectStreamSource:Pt,renderStreamMessage:kt,setProgressBarDelay:Mt,setConfirmMethod:Ft,setFormMode:It,morphBodyElements:qt,morphTurboFrameElements:Ht,morphChildren:Se,morphElements:ve});class Nt extends Error{}function Ot(e,s){if(e){const r=e.getAttribute(\"src\");if(null!=r&&null!=s&&N(r,s))throw new Error(`Matching <turbo-frame id=\"${e.id}\"> element has a source URL which references itself`);if(e.ownerDocument!==document&&(e=document.importNode(e,!0)),e instanceof t)return e.connectedCallback(),e.disconnectedCallback(),e}}const Dt={after(){this.removeDuplicateTargetSiblings(),this.targetElements.forEach((e=>e.parentElement?.insertBefore(this.templateContent,e.nextSibling)))},append(){this.removeDuplicateTargetChildren(),this.targetElements.forEach((e=>e.append(this.templateContent)))},before(){this.removeDuplicateTargetSiblings(),this.targetElements.forEach((e=>e.parentElement?.insertBefore(this.templateContent,e)))},prepend(){this.removeDuplicateTargetChildren(),this.targetElements.forEach((e=>e.prepend(this.templateContent)))},remove(){this.targetElements.forEach((e=>e.remove()))},replace(){const e=this.getAttribute(\"method\");this.targetElements.forEach((t=>{\"morph\"===e?ve(t,this.templateContent):t.replaceWith(this.templateContent)}))},update(){const e=this.getAttribute(\"method\");this.targetElements.forEach((t=>{\"morph\"===e?Se(t,this.templateContent):(t.innerHTML=\"\",t.append(this.templateContent))}))},refresh(){const e=this.getAttribute(\"method\"),t=this.requestId,s=this.getAttribute(\"scroll\");yt.refresh(this.baseURI,{method:e,requestId:t,scroll:s})}};class xt extends HTMLElement{static async renderElement(e){await e.performAction()}async connectedCallback(){try{await this.render()}catch(e){console.error(e)}finally{this.disconnect()}}async render(){return this.renderPromise??=(async()=>{const e=this.beforeRenderEvent;this.dispatchEvent(e)&&(await o(),await e.detail.render(this))})()}disconnect(){try{this.remove()}catch{}}removeDuplicateTargetChildren(){this.duplicateChildren.forEach((e=>e.remove()))}get duplicateChildren(){const e=this.targetElements.flatMap((e=>[...e.children])).filter((e=>!!e.getAttribute(\"id\"))),t=[...this.templateContent?.children||[]].filter((e=>!!e.getAttribute(\"id\"))).map((e=>e.getAttribute(\"id\")));return e.filter((e=>t.includes(e.getAttribute(\"id\"))))}removeDuplicateTargetSiblings(){this.duplicateSiblings.forEach((e=>e.remove()))}get duplicateSiblings(){const e=this.targetElements.flatMap((e=>[...e.parentElement.children])).filter((e=>!!e.id)),t=[...this.templateContent?.children||[]].filter((e=>!!e.id)).map((e=>e.id));return e.filter((e=>t.includes(e.id)))}get performAction(){if(this.action){const e=Dt[this.action];if(e)return e;this.#F(\"unknown action\")}this.#F(\"action attribute is missing\")}get targetElements(){return this.target?this.targetElementsById:this.targets?this.targetElementsByQuery:void this.#F(\"target or targets attribute is missing\")}get templateContent(){return this.templateElement.content.cloneNode(!0)}get templateElement(){if(null===this.firstElementChild){const e=this.ownerDocument.createElement(\"template\");return this.appendChild(e),e}if(this.firstElementChild instanceof HTMLTemplateElement)return this.firstElementChild;this.#F(\"first child element must be a <template> element\")}get action(){return this.getAttribute(\"action\")}get target(){return this.getAttribute(\"target\")}get targets(){return this.getAttribute(\"targets\")}get requestId(){return this.getAttribute(\"request-id\")}#F(e){throw new Error(`${this.description}: ${e}`)}get description(){return(this.outerHTML.match(/<[^>]+>/)??[])[0]??\"<turbo-stream>\"}get beforeRenderEvent(){return new CustomEvent(\"turbo:before-stream-render\",{bubbles:!0,cancelable:!0,detail:{newStream:this,render:xt.renderElement}})}get targetElementsById(){const e=this.ownerDocument?.getElementById(this.target);return null!==e?[e]:[]}get targetElementsByQuery(){const e=this.ownerDocument?.querySelectorAll(this.targets);return 0!==e.length?Array.prototype.slice.call(e):[]}}class Vt extends HTMLElement{streamSource=null;connectedCallback(){this.streamSource=this.src.match(/^ws{1,2}:/)?new WebSocket(this.src):new EventSource(this.src),Ct(this.streamSource)}disconnectedCallback(){this.streamSource&&(this.streamSource.close(),Pt(this.streamSource))}get src(){return this.getAttribute(\"src\")||\"\"}}t.delegateConstructor=class{fetchResponseLoaded=e=>Promise.resolve();#I=null;#q=()=>{};#H=!1;#B=!1;#N=new Set;#O=!1;action=null;constructor(e){this.element=e,this.view=new he(this,this.element),this.appearanceObserver=new Y(this,this.element),this.formLinkClickObserver=new me(this,this.element),this.linkInterceptor=new de(this,this.element),this.restorationIdentifier=d(),this.formSubmitObserver=new ce(this,this.element)}connect(){this.#H||(this.#H=!0,this.loadingStyle==e.lazy?this.appearanceObserver.start():this.#D(),this.formLinkClickObserver.start(),this.linkInterceptor.start(),this.formSubmitObserver.start())}disconnect(){this.#H&&(this.#H=!1,this.appearanceObserver.stop(),this.formLinkClickObserver.stop(),this.linkInterceptor.stop(),this.formSubmitObserver.stop(),this.element.hasAttribute(\"recurse\")||this.#I?.cancel())}disabledChanged(){this.disabled?this.#I?.cancel():this.loadingStyle==e.eager&&this.#D()}sourceURLChanged(){this.#x(\"src\")||(this.sourceURL||this.#I?.cancel(),this.element.isConnected&&(this.complete=!1),(this.loadingStyle==e.eager||this.#B)&&this.#D())}sourceURLReloaded(){const{refresh:e,src:t}=this.element;return this.#O=t&&\"morph\"===e,this.element.removeAttribute(\"complete\"),this.element.src=null,this.element.src=t,this.element.loaded}loadingStyleChanged(){this.loadingStyle==e.lazy?this.appearanceObserver.start():(this.appearanceObserver.stop(),this.#D())}async#D(){this.enabled&&this.isActive&&!this.complete&&this.sourceURL&&(this.element.loaded=this.#V(k(this.sourceURL)),this.appearanceObserver.stop(),await this.element.loaded,this.#B=!0)}async loadResponse(e){(e.redirected||e.succeeded&&e.isHTML)&&(this.sourceURL=e.response.url);try{const t=await e.responseHTML;if(t){const s=l(t);Pe.fromDocument(s).isVisitable?await this.#W(e,s):await this.#U(e)}}finally{this.#O=!1,this.fetchResponseLoaded=()=>Promise.resolve()}}elementAppearedInViewport(e){this.proposeVisitIfNavigatedWithAction(e,b(e)),this.#D()}willSubmitFormLinkToLocation(e){return this.#_(e)}submittedFormLinkToLocation(e,t,s){const r=this.#p(e);r&&s.setAttribute(\"data-turbo-frame\",r.id)}shouldInterceptLinkClick(e,t,s){return this.#_(e)}linkClickIntercepted(e,t){this.#$(e,t)}willSubmitForm(e,t){return e.closest(\"turbo-frame\")==this.element&&this.#_(e,t)}formSubmitted(e,t){this.formSubmission&&this.formSubmission.stop(),this.formSubmission=new ie(this,e,t);const{fetchRequest:s}=this.formSubmission,r=this.#p(e,t);this.prepareRequest(s,r),this.formSubmission.start()}prepareRequest(e,t=this){e.headers[\"Turbo-Frame\"]=t.id,this.currentNavigationElement?.hasAttribute(\"data-turbo-stream\")&&e.acceptResponseType(Q.contentType)}requestStarted(e){m(this.element)}requestPreventedHandlingResponse(e,t){this.#q()}async requestSucceededWithResponse(e,t){await this.loadResponse(t),this.#q()}async requestFailedWithResponse(e,t){await this.loadResponse(t),this.#q()}requestErrored(e,t){console.error(t),this.#q()}requestFinished(e){p(this.element)}formSubmissionStarted({formElement:e}){m(e,this.#p(e))}formSubmissionSucceededWithResponse(e,t){const s=this.#p(e.formElement,e.submitter);s.delegate.proposeVisitIfNavigatedWithAction(s,b(e.submitter,e.formElement,s)),s.delegate.loadResponse(t),e.isSafe||yt.clearCache()}formSubmissionFailedWithResponse(e,t){this.element.delegate.loadResponse(t),yt.clearCache()}formSubmissionErrored(e,t){console.error(t)}formSubmissionFinished({formElement:e}){p(e,this.#p(e))}allowsImmediateRender({element:e},t){const s=i(\"turbo:before-frame-render\",{target:this.element,detail:{newFrame:e,...t},cancelable:!0}),{defaultPrevented:r,detail:{render:n}}=s;return this.view.renderer&&n&&(this.view.renderer.renderElement=n),!r}viewRenderedSnapshot(e,t,s){}preloadOnLoadLinksForView(e){yt.preloadOnLoadLinksForView(e)}viewInvalidated(){}willRenderFrame(e,t){this.previousFrameElement=e.cloneNode(!0)}visitCachedSnapshot=({element:e})=>{const t=e.querySelector(\"#\"+this.element.id);t&&this.previousFrameElement&&t.replaceChildren(...this.previousFrameElement.children),delete this.previousFrameElement};async#W(e,t){const s=await this.extractForeignFrameElement(t.body),r=this.#O?Ae:ge;if(s){const t=new ne(s),i=new r(this,this.view.snapshot,t,!1,!1);this.view.renderPromise&&await this.view.renderPromise,this.changeHistory(),await this.view.render(i),this.complete=!0,yt.frameRendered(e,this.element),yt.frameLoaded(this.element),await this.fetchResponseLoaded(e)}else this.#j(e)&&this.#z(e)}async#V(e){const t=new z(this,_.get,e,new URLSearchParams,this.element);return this.#I?.cancel(),this.#I=t,new Promise((e=>{this.#q=()=>{this.#q=()=>{},this.#I=null,e()},t.perform()}))}#$(e,t,s){const r=this.#p(e,s);r.delegate.proposeVisitIfNavigatedWithAction(r,b(s,e,r)),this.#G(e,(()=>{r.src=t}))}proposeVisitIfNavigatedWithAction(e,t=null){if(this.action=t,this.action){const t=Pe.fromElement(e).clone(),{visitCachedSnapshot:s}=e.delegate;e.delegate.fetchResponseLoaded=async r=>{if(e.src){const{statusCode:i,redirected:n}=r,o={response:{statusCode:i,redirected:n,responseHTML:await r.responseHTML},visitCachedSnapshot:s,willRender:!1,updateHistory:!1,restorationIdentifier:this.restorationIdentifier,snapshot:t};this.action&&(o.action=this.action),yt.visit(e.src,o)}}}}changeHistory(){if(this.action){const e=g(this.action);yt.history.update(e,k(this.element.src||\"\"),this.restorationIdentifier)}}async#U(e){console.warn(`The response (${e.statusCode}) from <turbo-frame id=\"${this.element.id}\"> is performing a full page visit due to turbo-visit-control.`),await this.#K(e.response)}#j(e){this.element.setAttribute(\"complete\",\"\");const t=e.response;return!i(\"turbo:frame-missing\",{target:this.element,detail:{response:t,visit:async(e,t)=>{e instanceof Response?this.#K(e):yt.visit(e,t)}},cancelable:!0}).defaultPrevented}#z(e){this.view.missing(),this.#J(e)}#J(e){const t=`The response (${e.statusCode}) did not contain the expected <turbo-frame id=\"${this.element.id}\"> and will be ignored. To perform a full page visit instead, set turbo-visit-control to reload.`;throw new Nt(t)}async#K(e){const t=new D(e),s=await t.responseHTML,{location:r,redirected:i,statusCode:n}=t;return yt.visit(r,{response:{redirected:i,statusCode:n,responseHTML:s}})}#p(e,s){const r=u(\"data-turbo-frame\",s,e)||this.element.getAttribute(\"target\"),i=this.#X(r);return i instanceof t?i:this.element}async extractForeignFrameElement(e){let s;const r=CSS.escape(this.id);try{if(s=Ot(e.querySelector(`turbo-frame#${r}`),this.sourceURL),s)return s;if(s=Ot(e.querySelector(`turbo-frame[src][recurse~=${r}]`),this.sourceURL),s)return await s.loaded,await this.extractForeignFrameElement(s)}catch(e){return console.error(e),new t}return null}#Y(e,t){return q(k(F(e,t)),this.rootLocation)}#_(e,t){const s=u(\"data-turbo-frame\",t,e)||this.element.getAttribute(\"target\");if(e instanceof HTMLFormElement&&!this.#Y(e,t))return!1;if(!this.enabled||\"_top\"==s)return!1;if(s){const e=this.#X(s);if(e)return!e.disabled;if(\"_parent\"==s)return!1}return!!yt.elementIsNavigatable(e)&&!(t&&!yt.elementIsNavigatable(t))}get id(){return this.element.id}get disabled(){return this.element.disabled}get enabled(){return!this.disabled}get sourceURL(){if(this.element.src)return this.element.src}set sourceURL(e){this.#Q(\"src\",(()=>{this.element.src=e??null}))}get loadingStyle(){return this.element.loading}get isLoading(){return void 0!==this.formSubmission||void 0!==this.#q()}get complete(){return this.element.hasAttribute(\"complete\")}set complete(e){e?this.element.setAttribute(\"complete\",\"\"):this.element.removeAttribute(\"complete\")}get isActive(){return this.element.isActive&&this.#H}get rootLocation(){const e=this.element.ownerDocument.querySelector('meta[name=\"turbo-root\"]');return k(e?.content??\"/\")}#x(e){return this.#N.has(e)}#Q(e,t){this.#N.add(e),t(),this.#N.delete(e)}#G(e,t){this.currentNavigationElement=e,t(),delete this.currentNavigationElement}#X(e){if(null!=e){const s=\"_parent\"===e?this.element.parentElement.closest(\"turbo-frame\"):document.getElementById(e);if(s instanceof t)return s}}},void 0===customElements.get(\"turbo-frame\")&&customElements.define(\"turbo-frame\",t),void 0===customElements.get(\"turbo-stream\")&&customElements.define(\"turbo-stream\",xt),void 0===customElements.get(\"turbo-stream-source\")&&customElements.define(\"turbo-stream-source\",Vt),(()=>{const e=document.currentScript;if(!e)return;if(e.hasAttribute(\"data-turbo-suppress-warning\"))return;let t=e.parentElement;for(;t;){if(t==document.body)return console.warn(h`\n        You are loading Turbo from a <script> element inside the <body> element. This is probably not what you meant to do!\n\n        Load your application’s JavaScript bundle inside the <head> element instead. <script> elements in <body> are evaluated with each page change.\n\n        For more information, see: https://turbo.hotwired.dev/handbook/building#working-with-script-elements\n\n        ——\n        Suppress this warning by adding a \"data-turbo-suppress-warning\" attribute to: %s\n      `,e.outerHTML);t=t.parentElement}})(),window.Turbo={...Bt,StreamActions:Dt},Rt();var Wt=Object.freeze({__proto__:null,FetchEnctype:j,FetchMethod:_,FetchRequest:z,FetchResponse:D,FrameElement:t,FrameLoadingStyle:e,FrameRenderer:ge,PageRenderer:mt,PageSnapshot:Pe,StreamActions:Dt,StreamElement:xt,StreamSourceElement:Vt,cache:Et,config:P,connectStreamSource:Ct,disconnectStreamSource:Pt,fetch:W,fetchEnctypeFromString:$,fetchMethodFromString:U,isSafe:G,morphBodyElements:qt,morphChildren:Se,morphElements:ve,morphTurboFrameElements:Ht,navigator:At,registerAdapter:Tt,renderStreamMessage:kt,session:yt,setConfirmMethod:Ft,setFormMode:It,setProgressBarDelay:Mt,start:Rt,visit:Lt});let Ut;async function _t(){return Ut||$t(jt().then($t))}function $t(e){return Ut=e}async function jt(){const{createConsumer:e}=await Promise.resolve().then((function(){return fs}));return e()}async function zt(e,t){const{subscriptions:s}=await _t();return s.create(e,t)}var Gt=Object.freeze({__proto__:null,getConsumer:_t,setConsumer:$t,createConsumer:jt,subscribeTo:zt});function Kt(e){return e&&\"object\"==typeof e?e instanceof Date||e instanceof RegExp?e:Array.isArray(e)?e.map(Kt):Object.keys(e).reduce((function(t,s){return t[s[0].toLowerCase()+s.slice(1).replace(/([A-Z]+)/g,(function(e,t){return\"_\"+t.toLowerCase()}))]=Kt(e[s]),t}),{}):e}class Jt extends HTMLElement{static observedAttributes=[\"channel\",\"signed-stream-name\"];async connectedCallback(){Ct(this),this.subscription=await zt(this.channel,{received:this.dispatchMessageEvent.bind(this),connected:this.subscriptionConnected.bind(this),disconnected:this.subscriptionDisconnected.bind(this)})}disconnectedCallback(){Pt(this),this.subscription&&this.subscription.unsubscribe(),this.subscriptionDisconnected()}attributeChangedCallback(){this.subscription&&(this.disconnectedCallback(),this.connectedCallback())}dispatchMessageEvent(e){const t=new MessageEvent(\"message\",{data:e});return this.dispatchEvent(t)}subscriptionConnected(){this.setAttribute(\"connected\",\"\")}subscriptionDisconnected(){this.removeAttribute(\"connected\")}get channel(){return{channel:this.getAttribute(\"channel\"),signed_stream_name:this.getAttribute(\"signed-stream-name\"),...Kt({...this.dataset})}}}void 0===customElements.get(\"turbo-cable-stream-source\")&&customElements.define(\"turbo-cable-stream-source\",Jt),window.Turbo=Wt,addEventListener(\"turbo:before-fetch-request\",(function(e){if(e.target instanceof HTMLFormElement){const{target:t,detail:{fetchOptions:s}}=e;t.addEventListener(\"turbo:submit-start\",(({detail:{formSubmission:{submitter:e}}})=>{const r=function(e){return e instanceof FormData||e instanceof URLSearchParams}(s.body)?s.body:new URLSearchParams,i=function(e,t,s){const r=function(e){return e instanceof HTMLButtonElement||e instanceof HTMLInputElement?\"_method\"===e.name?e.value:e.hasAttribute(\"formmethod\")?e.formMethod:null:null}(e),i=t.get(\"_method\"),n=s.getAttribute(\"method\")||\"get\";return\"string\"==typeof r?r:\"string\"==typeof i?i:n}(e,r,t);/get/i.test(i)||(/post/i.test(i)?r.delete(\"_method\"):r.set(\"_method\",i),s.method=\"post\")}),{once:!0})}}));var Xt={logger:\"undefined\"!=typeof console?console:void 0,WebSocket:\"undefined\"!=typeof WebSocket?WebSocket:void 0},Yt={log(...e){this.enabled&&(e.push(Date.now()),Xt.logger.log(\"[ActionCable]\",...e))}};const Qt=()=>(new Date).getTime(),Zt=e=>(Qt()-e)/1e3;class es{constructor(e){this.visibilityDidChange=this.visibilityDidChange.bind(this),this.connection=e,this.reconnectAttempts=0}start(){this.isRunning()||(this.startedAt=Qt(),delete this.stoppedAt,this.startPolling(),addEventListener(\"visibilitychange\",this.visibilityDidChange),Yt.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`))}stop(){this.isRunning()&&(this.stoppedAt=Qt(),this.stopPolling(),removeEventListener(\"visibilitychange\",this.visibilityDidChange),Yt.log(\"ConnectionMonitor stopped\"))}isRunning(){return this.startedAt&&!this.stoppedAt}recordMessage(){this.pingedAt=Qt()}recordConnect(){this.reconnectAttempts=0,delete this.disconnectedAt,Yt.log(\"ConnectionMonitor recorded connect\")}recordDisconnect(){this.disconnectedAt=Qt(),Yt.log(\"ConnectionMonitor recorded disconnect\")}startPolling(){this.stopPolling(),this.poll()}stopPolling(){clearTimeout(this.pollTimeout)}poll(){this.pollTimeout=setTimeout((()=>{this.reconnectIfStale(),this.poll()}),this.getPollInterval())}getPollInterval(){const{staleThreshold:e,reconnectionBackoffRate:t}=this.constructor;return 1e3*e*Math.pow(1+t,Math.min(this.reconnectAttempts,10))*(1+(0===this.reconnectAttempts?1:t)*Math.random())}reconnectIfStale(){this.connectionIsStale()&&(Yt.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${Zt(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`),this.reconnectAttempts++,this.disconnectedRecently()?Yt.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${Zt(this.disconnectedAt)} s`):(Yt.log(\"ConnectionMonitor reopening\"),this.connection.reopen()))}get refreshedAt(){return this.pingedAt?this.pingedAt:this.startedAt}connectionIsStale(){return Zt(this.refreshedAt)>this.constructor.staleThreshold}disconnectedRecently(){return this.disconnectedAt&&Zt(this.disconnectedAt)<this.constructor.staleThreshold}visibilityDidChange(){\"visible\"===document.visibilityState&&setTimeout((()=>{!this.connectionIsStale()&&this.connection.isOpen()||(Yt.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`),this.connection.reopen())}),200)}}es.staleThreshold=6,es.reconnectionBackoffRate=.15;var ts=es,ss={message_types:{welcome:\"welcome\",disconnect:\"disconnect\",ping:\"ping\",confirmation:\"confirm_subscription\",rejection:\"reject_subscription\"},disconnect_reasons:{unauthorized:\"unauthorized\",invalid_request:\"invalid_request\",server_restart:\"server_restart\",remote:\"remote\"},default_mount_path:\"/cable\",protocols:[\"actioncable-v1-json\",\"actioncable-unsupported\"]};const{message_types:rs,protocols:is}=ss,ns=is.slice(0,is.length-1),os=[].indexOf;class as{constructor(e){this.open=this.open.bind(this),this.consumer=e,this.subscriptions=this.consumer.subscriptions,this.monitor=new ts(this),this.disconnected=!0}send(e){return!!this.isOpen()&&(this.webSocket.send(JSON.stringify(e)),!0)}open(){if(this.isActive())return Yt.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`),!1;{const e=[...is,...this.consumer.subprotocols||[]];return Yt.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${e}`),this.webSocket&&this.uninstallEventHandlers(),this.webSocket=new Xt.WebSocket(this.consumer.url,e),this.installEventHandlers(),this.monitor.start(),!0}}close({allowReconnect:e}={allowReconnect:!0}){if(e||this.monitor.stop(),this.isOpen())return this.webSocket.close()}reopen(){if(Yt.log(`Reopening WebSocket, current state is ${this.getState()}`),!this.isActive())return this.open();try{return this.close()}catch(e){Yt.log(\"Failed to reopen WebSocket\",e)}finally{Yt.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`),setTimeout(this.open,this.constructor.reopenDelay)}}getProtocol(){if(this.webSocket)return this.webSocket.protocol}isOpen(){return this.isState(\"open\")}isActive(){return this.isState(\"open\",\"connecting\")}triedToReconnect(){return this.monitor.reconnectAttempts>0}isProtocolSupported(){return os.call(ns,this.getProtocol())>=0}isState(...e){return os.call(e,this.getState())>=0}getState(){if(this.webSocket)for(let e in Xt.WebSocket)if(Xt.WebSocket[e]===this.webSocket.readyState)return e.toLowerCase();return null}installEventHandlers(){for(let e in this.events){const t=this.events[e].bind(this);this.webSocket[`on${e}`]=t}}uninstallEventHandlers(){for(let e in this.events)this.webSocket[`on${e}`]=function(){}}}as.reopenDelay=500,as.prototype.events={message(e){if(!this.isProtocolSupported())return;const{identifier:t,message:s,reason:r,reconnect:i,type:n}=JSON.parse(e.data);switch(this.monitor.recordMessage(),n){case rs.welcome:return this.triedToReconnect()&&(this.reconnectAttempted=!0),this.monitor.recordConnect(),this.subscriptions.reload();case rs.disconnect:return Yt.log(`Disconnecting. Reason: ${r}`),this.close({allowReconnect:i});case rs.ping:return null;case rs.confirmation:return this.subscriptions.confirmSubscription(t),this.reconnectAttempted?(this.reconnectAttempted=!1,this.subscriptions.notify(t,\"connected\",{reconnected:!0})):this.subscriptions.notify(t,\"connected\",{reconnected:!1});case rs.rejection:return this.subscriptions.reject(t);default:return this.subscriptions.notify(t,\"received\",s)}},open(){if(Yt.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`),this.disconnected=!1,!this.isProtocolSupported())return Yt.log(\"Protocol is unsupported. Stopping monitor and disconnecting.\"),this.close({allowReconnect:!1})},close(e){if(Yt.log(\"WebSocket onclose event\"),!this.disconnected)return this.disconnected=!0,this.monitor.recordDisconnect(),this.subscriptions.notifyAll(\"disconnected\",{willAttemptReconnect:this.monitor.isRunning()})},error(){Yt.log(\"WebSocket onerror event\")}};var cs=as;class ls{constructor(e,t={},s){this.consumer=e,this.identifier=JSON.stringify(t),function(e,t){if(null!=t)for(let s in t){const r=t[s];e[s]=r}}(this,s)}perform(e,t={}){return t.action=e,this.send(t)}send(e){return this.consumer.send({command:\"message\",identifier:this.identifier,data:JSON.stringify(e)})}unsubscribe(){return this.consumer.subscriptions.remove(this)}}var hs=class{constructor(e){this.subscriptions=e,this.pendingSubscriptions=[]}guarantee(e){-1==this.pendingSubscriptions.indexOf(e)?(Yt.log(`SubscriptionGuarantor guaranteeing ${e.identifier}`),this.pendingSubscriptions.push(e)):Yt.log(`SubscriptionGuarantor already guaranteeing ${e.identifier}`),this.startGuaranteeing()}forget(e){Yt.log(`SubscriptionGuarantor forgetting ${e.identifier}`),this.pendingSubscriptions=this.pendingSubscriptions.filter((t=>t!==e))}startGuaranteeing(){this.stopGuaranteeing(),this.retrySubscribing()}stopGuaranteeing(){clearTimeout(this.retryTimeout)}retrySubscribing(){this.retryTimeout=setTimeout((()=>{this.subscriptions&&\"function\"==typeof this.subscriptions.subscribe&&this.pendingSubscriptions.map((e=>{Yt.log(`SubscriptionGuarantor resubscribing ${e.identifier}`),this.subscriptions.subscribe(e)}))}),500)}};class ds{constructor(e){this.consumer=e,this.guarantor=new hs(this),this.subscriptions=[]}create(e,t){const s=\"object\"==typeof e?e:{channel:e},r=new ls(this.consumer,s,t);return this.add(r)}add(e){return this.subscriptions.push(e),this.consumer.ensureActiveConnection(),this.notify(e,\"initialized\"),this.subscribe(e),e}remove(e){return this.forget(e),this.findAll(e.identifier).length||this.sendCommand(e,\"unsubscribe\"),e}reject(e){return this.findAll(e).map((e=>(this.forget(e),this.notify(e,\"rejected\"),e)))}forget(e){return this.guarantor.forget(e),this.subscriptions=this.subscriptions.filter((t=>t!==e)),e}findAll(e){return this.subscriptions.filter((t=>t.identifier===e))}reload(){return this.subscriptions.map((e=>this.subscribe(e)))}notifyAll(e,...t){return this.subscriptions.map((s=>this.notify(s,e,...t)))}notify(e,t,...s){let r;return r=\"string\"==typeof e?this.findAll(e):[e],r.map((e=>\"function\"==typeof e[t]?e[t](...s):void 0))}subscribe(e){this.sendCommand(e,\"subscribe\")&&this.guarantor.guarantee(e)}confirmSubscription(e){Yt.log(`Subscription confirmed ${e}`),this.findAll(e).map((e=>this.guarantor.forget(e)))}sendCommand(e,t){const{identifier:s}=e;return this.consumer.send({command:t,identifier:s})}}class us{constructor(e){this._url=e,this.subscriptions=new ds(this),this.connection=new cs(this),this.subprotocols=[]}get url(){return ms(this._url)}send(e){return this.connection.send(e)}connect(){return this.connection.open()}disconnect(){return this.connection.close({allowReconnect:!1})}ensureActiveConnection(){if(!this.connection.isActive())return this.connection.open()}addSubProtocol(e){this.subprotocols=[...this.subprotocols,e]}}function ms(e){if(\"function\"==typeof e&&(e=e()),e&&!/^wss?:/i.test(e)){const t=document.createElement(\"a\");return t.href=e,t.href=t.href,t.protocol=t.protocol.replace(\"http\",\"ws\"),t.href}return e}function ps(e){const t=document.head.querySelector(`meta[name='action-cable-${e}']`);if(t)return t.getAttribute(\"content\")}var fs=Object.freeze({__proto__:null,Connection:cs,ConnectionMonitor:ts,Consumer:us,INTERNAL:ss,Subscription:ls,Subscriptions:ds,SubscriptionGuarantor:hs,adapters:Xt,createWebSocketURL:ms,logger:Yt,createConsumer:function(e=ps(\"url\")||ss.default_mount_path){return new us(e)},getConfig:ps});export{Wt as Turbo,Gt as cable};\n//# sourceMappingURL=turbo.min.js.map\n"}]}