@getmicdrop/venue-calendar 4.0.109 → 4.0.110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CartView-bMHSoj75.js +2475 -0
- package/dist/CartView-bMHSoj75.js.map +1 -0
- package/dist/{Checkout-CMEf_qnp.js → Checkout-DDXZc-yj.js} +323 -333
- package/dist/Checkout-DDXZc-yj.js.map +1 -0
- package/dist/{Checkout-Chki2df_.js → Checkout-DG2iHmHC.js} +206 -205
- package/dist/Checkout-DG2iHmHC.js.map +1 -0
- package/dist/{CheckoutTimer-BAaY_60w.js → CheckoutTimer-Bwq6a_Hl.js} +5 -5
- package/dist/{CheckoutTimer-BAaY_60w.js.map → CheckoutTimer-Bwq6a_Hl.js.map} +1 -1
- package/dist/{ClockIcon-Dbbj_qur.js → ClockIcon-ZgeMNNI1.js} +2 -2
- package/dist/{ClockIcon-Dbbj_qur.js.map → ClockIcon-ZgeMNNI1.js.map} +1 -1
- package/dist/{CollectionView-RM30YPOJ.js → CollectionView-1pf2C4kX.js} +78 -77
- package/dist/CollectionView-1pf2C4kX.js.map +1 -0
- package/dist/{Event-kdxMa6OT.js → Event-D3o0fguY.js} +659 -657
- package/dist/Event-D3o0fguY.js.map +1 -0
- package/dist/{EventPage-BRBByBHe.js → EventPage-DqO-AV8i.js} +300 -306
- package/dist/EventPage-DqO-AV8i.js.map +1 -0
- package/dist/{Heading-CSDFD80h.js → Heading-pURRwiIC.js} +2 -2
- package/dist/{Heading-CSDFD80h.js.map → Heading-pURRwiIC.js.map} +1 -1
- package/dist/{ScarcityBadge-B0ca48su.js → ScarcityBadge-ykz32KS9.js} +11 -11
- package/dist/{ScarcityBadge-B0ca48su.js.map → ScarcityBadge-ykz32KS9.js.map} +1 -1
- package/dist/SeriesPage-BmNk80cF.js +34 -0
- package/dist/SeriesPage-BmNk80cF.js.map +1 -0
- package/dist/{Spinner--TOwvv9A.js → Spinner-OGA1hrc5.js} +2 -2
- package/dist/{Spinner--TOwvv9A.js.map → Spinner-OGA1hrc5.js.map} +1 -1
- package/dist/Success-DLlQ54_-.js +896 -0
- package/dist/Success-DLlQ54_-.js.map +1 -0
- package/dist/{Text-WLl_ELy9.js → Text-DMpMLgcU.js} +19 -19
- package/dist/Text-DMpMLgcU.js.map +1 -0
- package/dist/VenueCalendar-ClSRsS6u.js +16474 -0
- package/dist/VenueCalendar-ClSRsS6u.js.map +1 -0
- package/dist/{ViewTicketsEmbed-BOukxQIF.js → ViewTicketsEmbed-5LMG2xwn.js} +86 -85
- package/dist/{ViewTicketsEmbed-BOukxQIF.js.map → ViewTicketsEmbed-5LMG2xwn.js.map} +1 -1
- package/dist/__SKIP_PATHS__-BtJ0tS93.js +10 -0
- package/dist/__SKIP_PATHS__-BtJ0tS93.js.map +1 -0
- package/dist/api/api.cjs +1 -1
- package/dist/api/api.cjs.map +1 -1
- package/dist/api/api.mjs +336 -333
- package/dist/api/api.mjs.map +1 -1
- package/dist/{labels-DXGieQXs.js → labels-Ws4MJjH5.js} +154 -149
- package/dist/labels-Ws4MJjH5.js.map +1 -0
- package/dist/{modalManager.svelte-Dbd1xgTQ.js → modalManager.svelte-BPA2JF6c.js} +2 -2
- package/dist/{modalManager.svelte-Dbd1xgTQ.js.map → modalManager.svelte-BPA2JF6c.js.map} +1 -1
- package/dist/navigation--gZASiSa.js +19 -0
- package/dist/navigation--gZASiSa.js.map +1 -0
- package/dist/{promo-oWi_MKmY.js → promo-vXx1bxQm.js} +2 -2
- package/dist/{promo-oWi_MKmY.js.map → promo-vXx1bxQm.js.map} +1 -1
- package/dist/seo/seo.cjs +1 -1
- package/dist/seo/seo.cjs.map +1 -1
- package/dist/seo/seo.mjs +438 -291
- package/dist/seo/seo.mjs.map +1 -1
- package/dist/seo/types.d.ts +0 -1
- package/dist/{showcase-state-options-DJJdjxvZ.js → showcase-state-options-9SoS5WuT.js} +21 -21
- package/dist/showcase-state-options-9SoS5WuT.js.map +1 -0
- package/dist/{transform-Bgqwj9i-.js → transform-BD9xXAq7.js} +81 -77
- package/dist/transform-BD9xXAq7.js.map +1 -0
- package/dist/types/index.d.ts +18 -14
- package/dist/utm-rT0TPh8G.js +288 -0
- package/dist/utm-rT0TPh8G.js.map +1 -0
- package/dist/venue-calendar.css +1 -1
- package/dist/venue-calendar.es.js +2 -2
- package/dist/venue-calendar.iife.js +49 -49
- package/dist/venue-calendar.iife.js.map +1 -1
- package/dist/venue-calendar.umd.js +44 -44
- package/dist/venue-calendar.umd.js.map +1 -1
- package/package.json +4 -4
- package/dist/CartView-C3N5pw89.js +0 -2381
- package/dist/CartView-C3N5pw89.js.map +0 -1
- package/dist/Checkout-CMEf_qnp.js.map +0 -1
- package/dist/Checkout-Chki2df_.js.map +0 -1
- package/dist/CollectionView-RM30YPOJ.js.map +0 -1
- package/dist/Event-kdxMa6OT.js.map +0 -1
- package/dist/EventPage-BRBByBHe.js.map +0 -1
- package/dist/SeriesPage-CczfbUKO.js +0 -33
- package/dist/SeriesPage-CczfbUKO.js.map +0 -1
- package/dist/Success-DIWUGUPs.js +0 -886
- package/dist/Success-DIWUGUPs.js.map +0 -1
- package/dist/Text-WLl_ELy9.js.map +0 -1
- package/dist/VenueCalendar-CU5N9klc.js +0 -16579
- package/dist/VenueCalendar-CU5N9klc.js.map +0 -1
- package/dist/labels-DXGieQXs.js.map +0 -1
- package/dist/showcase-state-options-DJJdjxvZ.js.map +0 -1
- package/dist/transform-Bgqwj9i-.js.map +0 -1
package/dist/seo/seo.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo.mjs","sources":["../../node_modules/@getmicdrop/svelte-components/dist/utils/timezones.js","../../node_modules/@getmicdrop/svelte-components/dist/richtext/index.js","../../node_modules/@getmicdrop/svelte-components/dist/datetime/local.js","../../src/lib/seo/helpers.ts","../../src/lib/seo/buildEventJsonLd.ts","../../src/lib/seo/buildSeriesJsonLd.ts","../../src/lib/seo/buildCollectionJsonLd.ts","../../node_modules/@getmicdrop/svelte-components/dist/routing/slug.js","../../src/lib/utils/textUtils.ts","../../src/lib/seo/HostSeoController.ts"],"sourcesContent":["/**\n * Timezone utility helpers.\n * Canonical source for timezone defaults and validation.\n */\n/**\n * Returns the default timezone for Micdrop, branded as `IANATimezone`.\n * Used when no explicit timezone is provided to date formatters. Bypass\n * call sites that pass `'America/Los_Angeles'` literally are red-squiggle\n * type errors at canonicals that take `IANATimezone` — the fix is to\n * thread `getDefaultTimezone()` through.\n *\n * @returns Branded IANA timezone (`'America/Los_Angeles'`)\n *\n * @example\n * formatDate(new Date(), {}, 'en-US', getDefaultTimezone())\n *\n * @deprecated Duplicate of the canonical default. Use `getDefaultTimezone`\n * (or the `DEFAULT_TIMEZONE` constant) from `$lib/datetime`\n * (re-exported via `datetime/constants`) — the single source of truth.\n */\nexport function getDefaultTimezone() {\n return 'America/Los_Angeles';\n}\n/**\n * Validates an IANA timezone string.\n * Uses Intl.DateTimeFormat to test if the timezone is recognized.\n *\n * @param tz - Timezone string to validate\n * @returns true if valid IANA timezone, false otherwise\n *\n * @example\n * isValidTimezone('America/Los_Angeles') // true\n * isValidTimezone('Invalid/Zone') // false\n *\n * @deprecated Lenient: `Intl.DateTimeFormat` accepts non-canonical inputs.\n * Use `isValidTimezone` from `$lib/datetime` (stricter — rejects\n * abbreviations like `'PST'`), or `toIANATimezone` from `$lib/types/brands`\n * when you need a branded value.\n */\nexport function isValidTimezone(tz) {\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: tz });\n return true;\n }\n catch {\n return false;\n }\n}\n","/**\n * Rich-text helpers for event / series descriptions.\n *\n * Pure string utilities — NO DOM access and NO external dependencies — so they\n * run safely during SSR (e.g. a `<svelte:head>` meta-description) as well as in\n * the browser. The companion {@link RichTextEditor} produces HTML that already\n * passes through an Eventbrite-parity allowlist; these helpers:\n *\n * - {@link normalizeEventDescriptionHtml} — promote legacy plain-text\n * descriptions to paragraph HTML so they don't render as a single line.\n * - {@link isEventDescriptionEqual} — compare two descriptions\n * whitespace-insensitively, so cosmetic edits don't trip dirty-checking.\n * - {@link stripEventDescriptionHtml} — flatten HTML to plain text (optionally\n * truncated) for meta tags / previews.\n *\n * @module richtext\n */\n/** Block-level closers that should read as a space when flattened to text. */\nconst BLOCK_CLOSE = /<\\/(?:p|div|h[1-6]|li|ul|ol|blockquote|tr)>/gi;\nconst LINE_BREAK = /<br\\s*\\/?>/gi;\nconst ANY_TAG = /<[^>]*>/g;\n/** A leading `<tag>` is a reliable \"this is already HTML\" signal. */\nconst LOOKS_LIKE_HTML = /<[a-z][\\s\\S]*>/i;\nconst NAMED_ENTITIES = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n \""\": '\"',\n \"'\": \"'\",\n \"'\": \"'\",\n \" \": \" \",\n};\nfunction decodeEntities(input) {\n return input\n .replace(/&#x([0-9a-f]+);/gi, (_, hex) => codePointOrEmpty(parseInt(hex, 16)))\n .replace(/&#(\\d+);/g, (_, dec) => codePointOrEmpty(Number(dec)))\n .replace(/&[a-z0-9]+;/gi, (m) => NAMED_ENTITIES[m.toLowerCase()] ?? m);\n}\nfunction codePointOrEmpty(code) {\n return Number.isFinite(code) && code > 0 && code <= 0x10ffff\n ? String.fromCodePoint(code)\n : \"\";\n}\nfunction escapeHtml(input) {\n return input\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\n}\n/**\n * Flatten a (possibly HTML) description to a single line of plain text.\n * When `maxLength` is given the result is truncated on a word boundary and an\n * ellipsis appended. Returns `''` for empty/missing input. SSR-safe.\n */\nexport function stripEventDescriptionHtml(html, maxLength) {\n if (!html)\n return \"\";\n let text = String(html)\n .replace(LINE_BREAK, \" \")\n .replace(BLOCK_CLOSE, \" \")\n .replace(ANY_TAG, \"\");\n text = decodeEntities(text).replace(/\\s+/g, \" \").trim();\n if (maxLength && maxLength > 0 && text.length > maxLength) {\n const clipped = text.slice(0, maxLength);\n const lastSpace = clipped.lastIndexOf(\" \");\n text =\n (lastSpace > 40 ? clipped.slice(0, lastSpace) : clipped).trimEnd() + \"…\";\n }\n return text;\n}\n/**\n * Normalize a stored description for the rich-text editor. Existing HTML is\n * returned as-is (trimmed); legacy plain text is escaped and promoted to\n * paragraphs (blank lines → `<p>`, single newlines → `<br>`) so it renders with\n * its original line structure instead of collapsing to one line. SSR-safe.\n */\nexport function normalizeEventDescriptionHtml(html) {\n if (!html)\n return \"\";\n const value = String(html);\n if (LOOKS_LIKE_HTML.test(value))\n return value.trim();\n return value\n .replace(/\\r\\n/g, \"\\n\")\n .split(/\\n{2,}/)\n .map((block) => block.trim())\n .filter(Boolean)\n .map((block) => `<p>${escapeHtml(block).replace(/\\n/g, \"<br>\")}</p>`)\n .join(\"\");\n}\n/** Collapse a description to a canonical form for equality comparison. */\nfunction canonicalize(html) {\n if (!html)\n return \"\";\n const collapsed = String(html)\n .replace(/>\\s+</g, \"><\")\n .replace(/\\s+/g, \" \")\n .trim();\n // An \"empty\" editor value (stray empty paragraphs / breaks) === no value.\n return /^(?:<p>(?:\\s| |<br\\s*\\/?>)*<\\/p>|<br\\s*\\/?>)*$/i.test(collapsed)\n ? \"\"\n : collapsed;\n}\n/**\n * True when two descriptions are equal ignoring cosmetic whitespace and\n * empty-paragraph noise — used for Save/dirty checking so a no-op edit doesn't\n * keep the form dirty. SSR-safe.\n */\nexport function isEventDescriptionEqual(a, b) {\n return canonicalize(a) === canonicalize(b);\n}\n// Sanitizer + shared allowlist — the security boundary for rendering stored\n// description HTML (which can arrive via CSV import / Eventbrite sync) on the\n// public event page + embed. `RichTextContent` calls this on render; callers\n// rendering description HTML directly with `{@html}` MUST sanitize here first.\nexport { sanitizeEventDescriptionHtml } from \"./sanitize.js\";\nexport { EVENT_DESCRIPTION_TAGS, EVENT_DESCRIPTION_ATTRS, EVENT_DESCRIPTION_LINK_PROTOCOLS, EVENT_DESCRIPTION_WHITELIST, } from \"./allowlist.js\";\n","/**\n * Browser-local date helpers — calendar chrome & `<input type=\"datetime-local\">`.\n *\n * These are the ONE exception to the module's golden rule (\"timezone is always\n * required\"). They operate on a JavaScript `Date` in the **browser's own local\n * timezone**, with NO IANA timezone argument and NO UTC conversion. They exist\n * for the cases where browser-local IS the correct semantic:\n *\n * - Scheduling / calendar-grid chrome that reflects the operator's own clock.\n * - Producing the `YYYY-MM-DD` / `YYYY-MM-DDTHH:mm` strings that native\n * `<input type=\"date\">` / `<input type=\"datetime-local\">` require (those\n * inputs are defined to read/write **local** wall-clock time, never UTC).\n *\n * The distinction from the venue-timezone formatters in `./format.ts` is\n * deliberate and load-bearing — keep it unmistakable:\n *\n * | Concern | Use | From |\n * |---------------------------------|-----------------------------------|---------------|\n * | Venue/event display time | `formatDayOfWeek(utcIso, tz)` | `./format.ts` |\n * | Operator's own clock / inputs | `formatLocalDayOfWeek(date)` | this file |\n *\n * Name-collision resolution (Atlas AUDIT #17): the venue-tz `formatDayOfWeek`\n * (UTC ISO + IANA tz) keeps the canonical name in `format.ts`; the browser-local\n * variant (a `Date`, no tz) is named `formatLocalDayOfWeek` here so the two can\n * never be confused at a call site.\n *\n * @module datetime/local\n */\nimport { getUserLocale } from './timezone.js';\n/**\n * Pads a number to two digits (e.g. `5` -> `\"05\"`).\n */\nfunction pad2(n) {\n return n < 10 ? `0${n}` : String(n);\n}\n/**\n * Current instant, as a `Date` in the **browser's own local clock** — the\n * named-intent replacement for a bare `new Date()` \"now\".\n *\n * Returns the present moment for viewer-local UI: calendar-grid chrome,\n * optimistic timestamps, \"is this in the past?\" checks against the operator's\n * own clock. It is the honest name for the many bare `new Date()` calls that\n * mean \"now, for the browser\" — NOT \"now, for the API\". Anything sent to or\n * reconciled against the backend must go through the venue-timezone helpers in\n * `./format.ts` / `./parse.ts`; this is strictly the local-chrome \"now\".\n *\n * (A `Date` is an absolute epoch instant with no stored zone — \"browser clock\"\n * describes how you read it back: via local wall-clock parts / local\n * formatters.)\n *\n * **Client-only for visible \"now\".** Under SSR / a SvelteKit `load`, this reads\n * the *server* clock — using it for a rendered \"now\" can hydrate to a different\n * value on the client. Call it in browser/`onMount` context (or accept that a\n * server render is a server-time snapshot) for anything the user sees tick.\n *\n * @returns A `Date` for the current instant.\n *\n * @example\n * const stampedAt = browserNow(); // optimistic UI timestamp\n * // event is in the past on the operator's own clock:\n * compareInstants(event.localStart, browserNow()) < 0\n */\nexport function browserNow() {\n return new Date();\n}\n/**\n * Current local date+time as an HTML `<input type=\"datetime-local\">` value.\n *\n * Format: `YYYY-MM-DDTHH:mm` (no seconds, no timezone) — exactly the value\n * format the native control reads and writes. Uses the browser's local clock;\n * does NOT convert to/from UTC.\n *\n * @returns Local-time string in `YYYY-MM-DDTHH:mm` form.\n *\n * @example\n * getCurrentDateTimeLocalFormat() // \"2026-06-09T14:30\" (in the viewer's tz)\n */\nexport function getCurrentDateTimeLocalFormat() {\n const now = browserNow();\n return (`${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())}` +\n `T${pad2(now.getHours())}:${pad2(now.getMinutes())}`);\n}\n/**\n * Formats a `Date` as a `YYYY-MM-DD` string in the **browser's local timezone**.\n *\n * This is the value format for `<input type=\"date\">`. It reads the local\n * wall-clock parts (`getFullYear`/`getMonth`/`getDate`) — it does NOT use\n * `toISOString()` (which would shift to UTC and can roll the date across\n * midnight). Use this when \"the day on the operator's calendar\" is what matters.\n *\n * @param date - The date to format (interpreted in browser-local time).\n * @returns Local date string in `YYYY-MM-DD` form.\n *\n * @example\n * formatLocalDate(new Date(2026, 5, 9)) // \"2026-06-09\"\n */\nexport function formatLocalDate(date) {\n return (`${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`);\n}\n/**\n * The **host-local calendar-day anchor** for a date — local midnight\n * (`00:00:00.000`) of the calendar day the input falls on, in the browser's own\n * timezone.\n *\n * This is the anchor for grid / recurrence / \"same day\" math that starts from\n * the operator's own calendar and re-resolves to the venue timezone downstream.\n * It normalizes away the time-of-day so two instants on the same local day\n * collapse to one comparable anchor — pair it with\n * {@link import('./instant.js').compareInstants} /\n * {@link import('./instant.js').epochEquals} for ordering and same-day checks.\n *\n * It reads local wall-clock parts (`getFullYear` / `getMonth` / `getDate`) — it\n * does NOT use `toISOString()` (which would shift to UTC and can roll the anchor\n * across midnight under a non-UTC offset). Use this when \"the day on the\n * operator's calendar\" is the reference; use the venue-tz helpers in\n * `./format.ts` when the venue's calendar day is what matters.\n *\n * @param date - The date to anchor (interpreted in browser-local time).\n * Defaults to {@link browserNow} (today's local calendar day).\n * @returns A new `Date` at local midnight of that calendar day.\n *\n * @example\n * localCalendarDay(new Date(2026, 5, 9, 23, 30)) // 2026-06-09 00:00 local\n * localCalendarDay() // today at local midnight\n * // are two instants on the same local calendar day?\n * epochEquals(localCalendarDay(a), localCalendarDay(b))\n */\nexport function localCalendarDay(date = browserNow()) {\n return new Date(date.getFullYear(), date.getMonth(), date.getDate());\n}\n/**\n * Formats a `Date`'s weekday in the **browser's local timezone**.\n *\n * The browser-local sibling of the venue-timezone {@link import('./format.js').formatDayOfWeek}\n * (which takes a UTC ISO string + IANA timezone). This one takes a plain `Date`\n * and renders its weekday in local time — for calendar chrome where the\n * operator's own clock is the reference.\n *\n * @param date - The date whose weekday to format (browser-local).\n * @param full - If `true` (default), returns the full name (\"Monday\");\n * if `false`, the short name (\"Mon\").\n * @param locale - BCP-47 locale tag. Defaults to the viewer's resolved locale\n * via {@link import('./timezone.js').getUserLocale}. An app with its own i18n\n * locale should pass it explicitly.\n * @returns Localized weekday name in browser-local time.\n *\n * @example\n * formatLocalDayOfWeek(new Date(2026, 5, 8)) // \"Monday\"\n * formatLocalDayOfWeek(new Date(2026, 5, 8), false) // \"Mon\"\n */\nexport function formatLocalDayOfWeek(date, full = true, locale = getUserLocale()) {\n return new Intl.DateTimeFormat(locale, {\n weekday: full ? 'long' : 'short',\n }).format(date);\n}\n/**\n * Formats a `Date` in the **browser's local timezone** with caller-supplied\n * Intl options — the generic display-label formatter for viewer-local picker\n * chrome (single/range date pickers, month-year headers, day cells).\n *\n * Browser-local sibling of the venue-timezone formatters in `./format.ts`: it\n * renders the Date's local wall-clock parts (NO IANA timezone argument, NO UTC\n * conversion) via `Intl.DateTimeFormat`. This is the honest canonical home for\n * the labels the deprecated `formatDate` in `utils/formatters` used to produce\n * for viewer-local pickers — but it injects NO default field set: the caller's\n * `options` are authoritative (the pickers already pass fully-specified option\n * objects). The `'en-US'` locale default preserves the historical picker output.\n *\n * @param date - The date to format (interpreted in browser-local time).\n * @param options - Intl formatting options (authoritative; no defaults merged).\n * @param locale - BCP-47 locale tag. Defaults to `'en-US'` to match the\n * historical picker label output.\n * @returns The localized label in browser-local time.\n *\n * @example\n * formatLocal(new Date(2026, 5, 9), { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })\n * // \"Tuesday, June 9, 2026\"\n */\nexport function formatLocal(date, options, locale = 'en-US') {\n return new Intl.DateTimeFormat(locale, options).format(date);\n}\n","/**\r\n * Shared helper utilities for JSON-LD builders.\r\n *\r\n * All functions are pure (no DOM, no Svelte, no side effects).\r\n * Extracted verbatim from EventStructuredData.svelte.\r\n *\r\n * @raw-intl-datetimeformat-escape: SEO/JSON-LD implementation. formatDateTimeWithOffset\r\n * must emit Google-required ISO-8601-with-tz-offset (\"2025-07-21T19:00:00-05:00\"), which\r\n * requires Intl.DateTimeFormat.formatToParts(...) with timeZoneName:'shortOffset' to read\r\n * the per-timezone UTC offset. The SC formatDate/DATE_FORMATS canonical produces\r\n * human-readable display strings, not parts-based offset extraction — it cannot express this.\r\n */\r\n\r\nimport { getDefaultTimezone } from '@getmicdrop/svelte-components/utils/timezones';\r\nimport { browserNow } from '@getmicdrop/svelte-components/datetime';\r\nimport type { EventTicketInput, EventPerformerInput } from './types';\r\n\r\n/**\r\n * Escape a JSON string for safe embedding inside an inline\r\n * <script type=\"application/ld+json\"> element.\r\n *\r\n * JSON.stringify does NOT escape `<`, `>` or `&`, so a field value containing\r\n * `</script><script>...` would break out of the script element and execute\r\n * (stored XSS). Replacing those characters with their unicode escape sequences\r\n * keeps the output valid JSON (parsers decode `<` back to `<`) while making\r\n * it impossible to terminate the surrounding <script> tag.\r\n *\r\n * This is the standard JSON-LD-in-<script> hardening; the sibling\r\n * HostSeoController avoids the issue entirely by using element.textContent.\r\n */\r\nexport function escapeJsonLdForScript(json: string): string {\r\n return json\r\n .replace(/</g, '\\\\u003c')\r\n .replace(/>/g, '\\\\u003e')\r\n .replace(/&/g, '\\\\u0026');\r\n}\r\n\r\n/**\r\n * Format datetime to ISO-8601 with timezone offset.\r\n * Google requires: \"2025-07-21T19:00:00-05:00\"\r\n *\r\n * Extracted from EventStructuredData.svelte lines 25-68.\r\n */\r\nexport function formatDateTimeWithOffset(\r\n isoString: string | null | undefined,\r\n tz: string\r\n): string | null {\r\n if (!isoString) return null;\r\n\r\n const date = new Date(isoString); // @datetime-escape: parse UTC instant; rendered in an explicit tz\r\n if (isNaN(date.getTime())) return null;\r\n\r\n // Get the offset in the target timezone\r\n const offsetFormatOptions: Intl.DateTimeFormatOptions = {\r\n year: 'numeric',\r\n month: '2-digit',\r\n day: '2-digit',\r\n hour: '2-digit',\r\n minute: '2-digit',\r\n second: '2-digit',\r\n hour12: false,\r\n timeZoneName: 'shortOffset',\r\n };\r\n\r\n let formatter: Intl.DateTimeFormat;\r\n try {\r\n formatter = new Intl.DateTimeFormat('en-US', { ...offsetFormatOptions, timeZone: tz });\r\n } catch {\r\n // An invalid IANA timezone makes Intl.DateTimeFormat throw a RangeError,\r\n // which would otherwise escape the JSON-LD builders unguarded. Fall back\r\n // to the canonical default timezone — the same invalid-tz fallback the\r\n // rest of the repo uses ($lib/utils/datetime.js formatInTz).\r\n formatter = new Intl.DateTimeFormat('en-US', {\r\n ...offsetFormatOptions,\r\n timeZone: getDefaultTimezone(),\r\n });\r\n }\r\n\r\n const parts = formatter.formatToParts(date);\r\n const get = (type: string) => parts.find(p => p.type === type)?.value || '';\r\n\r\n const year = get('year');\r\n const month = get('month');\r\n const day = get('day');\r\n const hour = get('hour');\r\n const minute = get('minute');\r\n const second = get('second');\r\n const tzOffset = get('timeZoneName'); // e.g., \"GMT-5\" or \"GMT+2\"\r\n\r\n // Convert \"GMT-5\" to \"-05:00\" format\r\n let offset = '+00:00';\r\n if (tzOffset) {\r\n const match = tzOffset.match(/GMT([+-])(\\d+)(?::(\\d+))?/);\r\n if (match) {\r\n const sign = match[1];\r\n const hours = match[2].padStart(2, '0');\r\n const mins = match[3] || '00';\r\n offset = `${sign}${hours}:${mins}`;\r\n }\r\n }\r\n\r\n return `${year}-${month}-${day}T${hour}:${minute}:${second}${offset}`;\r\n}\r\n\r\n/**\r\n * Parse address string into PostalAddress components.\r\n * Input: \"7702 Santa Monica Blvd, West Hollywood, CA 90046\"\r\n * Output: { @type, streetAddress, addressLocality, addressRegion, postalCode, addressCountry }\r\n *\r\n * Extracted from EventStructuredData.svelte lines 75-111.\r\n */\r\nexport function parseAddressToPostal(\r\n addressStr: string | null | undefined\r\n): Record<string, string> | null {\r\n if (!addressStr) return null;\r\n\r\n const parts = addressStr.split(',').map(p => p.trim());\r\n\r\n if (parts.length < 2) {\r\n return {\r\n '@type': 'PostalAddress',\r\n streetAddress: addressStr,\r\n addressCountry: 'US',\r\n };\r\n }\r\n\r\n // Check if last part is \"State ZIP\" (e.g., \"CA 90046\")\r\n const lastPart = parts[parts.length - 1];\r\n const stateZipMatch = lastPart.match(/^([A-Z]{2})\\s+(\\d{5}(?:-\\d{4})?)$/);\r\n\r\n if (stateZipMatch && parts.length >= 3) {\r\n // Standard US address format\r\n return {\r\n '@type': 'PostalAddress',\r\n streetAddress: parts.slice(0, -2).join(', '),\r\n addressLocality: parts[parts.length - 2],\r\n addressRegion: stateZipMatch[1],\r\n postalCode: stateZipMatch[2],\r\n addressCountry: 'US',\r\n };\r\n }\r\n\r\n // Fallback for non-standard formats\r\n return {\r\n '@type': 'PostalAddress',\r\n streetAddress: parts[0],\r\n addressLocality: parts.slice(1).join(', '),\r\n addressCountry: 'US',\r\n };\r\n}\r\n\r\n/**\r\n * Structured postal-address parts, as the public resolve/series payloads now\r\n * expose them: `address` is the STREET line, with city/state/postalCode/country\r\n * as discrete fields (PublicVenueInfo / SeriesVenueDTO).\r\n */\r\nexport interface PostalAddressParts {\r\n address?: string | null; // street line\r\n city?: string | null;\r\n state?: string | null;\r\n postalCode?: string | null;\r\n country?: string | null;\r\n}\r\n\r\n/**\r\n * Build a schema.org PostalAddress, preferring discrete structured fields when\r\n * any locality signal (city / state / postalCode) is present, and otherwise\r\n * falling back to parsing a single comma-separated string (the street `address`\r\n * field if it carries a full address, else `googleLocationNameCache`).\r\n *\r\n * Discrete fields are more reliable than string-splitting — Google requires a\r\n * well-formed PostalAddress for Event rich-result eligibility (online-only\r\n * events lost eligibility June 2025).\r\n */\r\nexport function buildPostalAddress(\r\n parts: PostalAddressParts | null | undefined,\r\n fallback?: string | null\r\n): Record<string, string> | null {\r\n if (parts && (parts.city || parts.state || parts.postalCode)) {\r\n const out: Record<string, string> = { '@type': 'PostalAddress' };\r\n if (parts.address) out.streetAddress = parts.address;\r\n if (parts.city) out.addressLocality = parts.city;\r\n if (parts.state) out.addressRegion = parts.state;\r\n if (parts.postalCode) out.postalCode = parts.postalCode;\r\n out.addressCountry = parts.country || 'US';\r\n return out;\r\n }\r\n // No discrete locality fields — parse the best single string we have. The\r\n // street `address` may legacy-hold a full comma-separated address, so prefer\r\n // it over the location-name cache.\r\n return parseAddressToPostal((parts && parts.address) || fallback || null);\r\n}\r\n\r\n/**\r\n * Map event status to schema.org EventStatusType.\r\n *\r\n * Extracted from EventStructuredData.svelte lines 116-132.\r\n */\r\nexport function getEventStatus(rawStatus: string | null | undefined): string {\r\n if (!rawStatus) return 'https://schema.org/EventScheduled';\r\n\r\n const status = rawStatus.toLowerCase();\r\n\r\n if (status === 'cancelled' || status === 'canceled') {\r\n return 'https://schema.org/EventCancelled';\r\n }\r\n if (status === 'postponed') {\r\n return 'https://schema.org/EventPostponed';\r\n }\r\n if (status === 'rescheduled') {\r\n return 'https://schema.org/EventRescheduled';\r\n }\r\n\r\n return 'https://schema.org/EventScheduled';\r\n}\r\n\r\n/**\r\n * Map ticket availability to schema.org ItemAvailability.\r\n *\r\n * Extracted from EventStructuredData.svelte lines 137-157.\r\n */\r\nexport function getTicketAvailability(ticket: EventTicketInput): string {\r\n const remaining =\r\n ticket.remainingCapacity ?? ticket.quantityRemaining ?? ticket.quantity;\r\n\r\n if (remaining === 0 || ticket.soldOut) {\r\n return 'https://schema.org/SoldOut';\r\n }\r\n\r\n const now = browserNow();\r\n const salesBegin =\r\n ticket.salesBegin ||\r\n ticket.salesStart ||\r\n ticket.saleBegin ||\r\n ticket.onSaleStart;\r\n const salesEnd = ticket.salesEnd || ticket.saleEnd || ticket.onSaleEnd;\r\n\r\n // @datetime-escape: absolute-instant compare of a UTC-Z field vs now — tz-independent\r\n if (salesBegin && new Date(salesBegin) > now) {\r\n return 'https://schema.org/PreOrder';\r\n }\r\n\r\n // @datetime-escape: absolute-instant compare of a UTC-Z field vs now — tz-independent\r\n if (salesEnd && new Date(salesEnd) < now) {\r\n return 'https://schema.org/SoldOut';\r\n }\r\n\r\n return 'https://schema.org/InStock';\r\n}\r\n\r\n/**\r\n * Build a schema.org price-range offer from a low/high price and availability.\r\n *\r\n * The shared primitive behind every JSON-LD offer block: emit an AggregateOffer\r\n * when the range spans two prices, or a single Offer when low === high. Any\r\n * caller-specific fields (a buy `url`, a `validFrom` sales-start) ride along in\r\n * `extra`, spread verbatim after the core fields so each caller reproduces its\r\n * exact prior shape — the event builder passes url/validFrom, the series builder\r\n * passes nothing.\r\n */\r\nexport function buildPriceRangeOffer(\r\n lowPrice: number,\r\n highPrice: number,\r\n availability: string,\r\n extra: Record<string, unknown> = {}\r\n): Record<string, unknown> {\r\n // Use AggregateOffer when there are multiple price points\r\n if (lowPrice !== highPrice) {\r\n return {\r\n '@type': 'AggregateOffer',\r\n lowPrice,\r\n highPrice,\r\n priceCurrency: 'USD',\r\n availability,\r\n ...extra,\r\n };\r\n }\r\n\r\n // Single price point - use Offer\r\n return {\r\n '@type': 'Offer',\r\n price: lowPrice,\r\n priceCurrency: 'USD',\r\n availability,\r\n ...extra,\r\n };\r\n}\r\n\r\n/**\r\n * Build offers from available tickets.\r\n *\r\n * Extracted from EventStructuredData.svelte lines 162-208.\r\n */\r\nexport function buildOffers(\r\n tickets: EventTicketInput[] | null | undefined,\r\n url?: string\r\n): Record<string, unknown> | null {\r\n if (!tickets || tickets.length === 0) return null;\r\n\r\n // Filter to public tickets only (exclude door-only and hidden)\r\n const publicTickets = tickets.filter(t => {\r\n const visibility = t.visibility ?? 0;\r\n const isVisible = visibility === 0 || visibility === 1;\r\n const isPublic = t.salesChannel !== 2;\r\n return isVisible && isPublic;\r\n });\r\n\r\n if (publicTickets.length === 0) return null;\r\n\r\n // Find lowest and highest price\r\n const prices = publicTickets\r\n .map(t => parseFloat(String(t.price ?? 0)))\r\n .filter(p => !isNaN(p));\r\n\r\n if (prices.length === 0) return null;\r\n\r\n const lowPrice = Math.min(...prices);\r\n const highPrice = Math.max(...prices);\r\n const ticket = publicTickets[0];\r\n\r\n return buildPriceRangeOffer(\r\n lowPrice,\r\n highPrice,\r\n getTicketAvailability(ticket),\r\n {\r\n url: url || undefined,\r\n validFrom: ticket?.salesBegin || ticket?.salesStart || undefined,\r\n }\r\n );\r\n}\r\n\r\n/**\r\n * Build performer array from event performers.\r\n *\r\n * Extracted from EventStructuredData.svelte lines 213-237.\r\n */\r\nexport function buildPerformers(\r\n performers: EventPerformerInput[] | null | undefined\r\n): Array<Record<string, string>> | null {\r\n if (!performers || performers.length === 0) return null;\r\n\r\n // Filter to confirmed, visible performers\r\n const confirmed = performers.filter(p => {\r\n const isConfirmed = p.acceptedState === 2;\r\n const isNotHidden = !p.shouldBeHidden;\r\n const profile = p.RosterPerformer?.User?.performerProfile;\r\n const hasName =\r\n profile?.firstName || profile?.lastName || profile?.displayName;\r\n return isConfirmed && isNotHidden && hasName;\r\n });\r\n\r\n if (confirmed.length === 0) return null;\r\n\r\n return confirmed.map(p => {\r\n const profile = p.RosterPerformer?.User?.performerProfile;\r\n const name =\r\n profile?.displayName ||\r\n [profile?.firstName, profile?.lastName].filter(Boolean).join(' ');\r\n\r\n return {\r\n '@type': 'PerformingGroup',\r\n name: name,\r\n };\r\n });\r\n}\r\n","import { getDefaultTimezone } from '@getmicdrop/svelte-components/utils/timezones';\r\nimport { stripEventDescriptionHtml } from '@getmicdrop/svelte-components/richtext';\r\n/**\r\n * Pure function that builds Event JSON-LD structured data for Google Events rich results.\r\n *\r\n * Extracted from EventStructuredData.svelte (lines 242-313).\r\n * Produces identical output to the original $derived.by block.\r\n */\r\n\r\nimport type { EventJsonLdInput, VenueInput, OrganizerInput } from './types';\r\nimport {\r\n formatDateTimeWithOffset,\r\n buildPostalAddress,\r\n getEventStatus,\r\n buildOffers,\r\n buildPerformers,\r\n} from './helpers';\r\n\r\nexport interface BuildEventJsonLdOptions {\r\n event: EventJsonLdInput;\r\n venue: VenueInput;\r\n venueTimeZone?: string;\r\n eventUrl?: string;\r\n organizer?: OrganizerInput;\r\n}\r\n\r\nexport function buildEventJsonLd(\r\n options: BuildEventJsonLdOptions\r\n): Record<string, unknown> | null {\r\n const {\r\n event,\r\n venue,\r\n venueTimeZone = getDefaultTimezone(),\r\n eventUrl,\r\n organizer,\r\n } = options;\r\n\r\n if (!event) return null;\r\n\r\n const startDate = formatDateTimeWithOffset(\r\n event.startDateTime,\r\n venueTimeZone\r\n );\r\n\r\n // Skip if we don't have required fields\r\n if (!event.name || !startDate || !venue?.name) {\r\n return null;\r\n }\r\n\r\n // Google requires a PostalAddress on a physical event's location (online-only\r\n // events lost rich-result eligibility June 2025). Prefer the venue's discrete\r\n // address fields (street/city/state/postalCode), falling back to parsing the\r\n // googleLocationNameCache string. Only attach `address` when we could build\r\n // one — never emit `address: null`. A missing address is a data-pipeline gap\r\n // (the resolve payload must carry the venue address).\r\n const postalAddress = buildPostalAddress(\r\n venue,\r\n venue?.googleLocationNameCache\r\n );\r\n const location: Record<string, unknown> = {\r\n '@type': 'Place',\r\n name: venue.name,\r\n };\r\n if (postalAddress) {\r\n location.address = postalAddress;\r\n }\r\n\r\n const data: Record<string, unknown> = {\r\n '@context': 'https://schema.org',\r\n '@type': 'Event',\r\n name: event.name,\r\n startDate: startDate,\r\n eventStatus: getEventStatus(event.status),\r\n eventAttendanceMode: 'https://schema.org/OfflineEventAttendanceMode',\r\n location,\r\n };\r\n\r\n // Add endDate if available\r\n const endDate = formatDateTimeWithOffset(event.endDateTime, venueTimeZone);\r\n if (endDate) {\r\n data.endDate = endDate;\r\n }\r\n\r\n // Add description\r\n const description = stripEventDescriptionHtml(event.description) || event.eventSummary;\r\n if (description) {\r\n data.description = description;\r\n }\r\n\r\n // Add image - Google recommends multiple aspect ratios\r\n const imageUrl = event.image || event.coverImage || event.ShowImage;\r\n if (imageUrl) {\r\n data.image = [imageUrl];\r\n }\r\n\r\n // Add offers from tickets\r\n const offers = buildOffers(event.availableTickets, eventUrl);\r\n if (offers) {\r\n data.offers = offers;\r\n }\r\n\r\n // Add performers\r\n const performers = buildPerformers(event.performers);\r\n if (performers && performers.length > 0) {\r\n data.performer = performers;\r\n }\r\n\r\n // Add organizer if provided\r\n if (organizer?.name) {\r\n const org: Record<string, string> = {\r\n '@type': 'Organization',\r\n name: organizer.name,\r\n };\r\n if (organizer.url) {\r\n org.url = organizer.url;\r\n }\r\n data.organizer = org;\r\n }\r\n\r\n // Add event URL if provided\r\n if (eventUrl) {\r\n data.url = eventUrl;\r\n }\r\n\r\n return data;\r\n}\r\n","/**\r\n * Pure function that builds EventSeries JSON-LD structured data.\r\n *\r\n * Series use schema.org EventSeries type with subEvent array containing\r\n * individual Event blocks for each occurrence. Each subEvent has its own\r\n * offers built from the simplified series ticket DTOs (different shape\r\n * from the event builder's tickets).\r\n *\r\n * Google Rich Results Test does NOT support EventSeries directly, but\r\n * the individual subEvent Event blocks can qualify for rich results.\r\n */\r\n\r\nimport type { SeriesJsonLdInput } from './types';\r\nimport { stripEventDescriptionHtml } from '@getmicdrop/svelte-components/richtext';\r\nimport {\r\n formatDateTimeWithOffset,\r\n buildPostalAddress,\r\n getEventStatus,\r\n buildPriceRangeOffer,\r\n} from './helpers';\r\n\r\nexport interface BuildSeriesJsonLdOptions {\r\n series: SeriesJsonLdInput;\r\n baseEventUrl?: string; // e.g., \"https://get-micdrop.com/e\" -- used to build subEvent @id URLs\r\n}\r\n\r\n/**\r\n * Map a series ticket status string to schema.org ItemAvailability.\r\n *\r\n * Series tickets use a simplified { name, price, status } shape from the\r\n * backend (SeriesTicketDTO), unlike event tickets which have visibility,\r\n * salesChannel, remainingCapacity, etc.\r\n */\r\nfunction mapSeriesTicketAvailability(status: string): string {\r\n switch (status) {\r\n case 'available':\r\n return 'https://schema.org/InStock';\r\n case 'sold_out':\r\n return 'https://schema.org/SoldOut';\r\n case 'coming_soon':\r\n return 'https://schema.org/PreOrder';\r\n case 'ended':\r\n return 'https://schema.org/SoldOut';\r\n default:\r\n return 'https://schema.org/InStock';\r\n }\r\n}\r\n\r\n/**\r\n * Build offers from series occurrence tickets.\r\n *\r\n * Series tickets have a simpler shape than event tickets: { name, price, status }.\r\n * Uses AggregateOffer for multiple price points, Offer for single price.\r\n */\r\nfunction buildSeriesOccurrenceOffers(\r\n tickets: Array<{ name: string; price: number; status: string }> | undefined,\r\n occurrenceStatus: string | undefined\r\n): Record<string, unknown> | null {\r\n if (!tickets || tickets.length === 0) return null;\r\n\r\n // Filter out sold_out and ended tickets for offer building\r\n const activeTickets = tickets.filter(\r\n t => t.status !== 'sold_out' && t.status !== 'ended'\r\n );\r\n\r\n // If all tickets are sold out / ended, report SoldOut with the price range\r\n if (activeTickets.length === 0) {\r\n const prices = tickets.map(t => t.price).filter(p => !isNaN(p));\r\n if (prices.length === 0) return null;\r\n\r\n return buildPriceRangeOffer(\r\n Math.min(...prices),\r\n Math.max(...prices),\r\n 'https://schema.org/SoldOut'\r\n );\r\n }\r\n\r\n const prices = activeTickets.map(t => t.price).filter(p => !isNaN(p));\r\n if (prices.length === 0) return null;\r\n\r\n return buildPriceRangeOffer(\r\n Math.min(...prices),\r\n Math.max(...prices),\r\n mapSeriesTicketAvailability(activeTickets[0].status)\r\n );\r\n}\r\n\r\nexport function buildSeriesJsonLd(\r\n options: BuildSeriesJsonLdOptions\r\n): Record<string, unknown> | null {\r\n const { series, baseEventUrl } = options;\r\n\r\n // Required fields\r\n if (!series.title) return null;\r\n if (!series.occurrences || series.occurrences.length === 0) return null;\r\n if (!series.venue?.name) return null;\r\n\r\n // Build location — prefer the venue's discrete address fields, falling back to\r\n // parsing googleLocationNameCache (see buildEventJsonLd for the rationale).\r\n const address = buildPostalAddress(\r\n series.venue,\r\n series.venue.googleLocationNameCache\r\n );\r\n const location: Record<string, unknown> = {\r\n '@type': 'Place',\r\n name: series.venue.name,\r\n };\r\n if (address) {\r\n location.address = address;\r\n }\r\n\r\n // Build parent EventSeries\r\n const data: Record<string, unknown> = {\r\n '@context': 'https://schema.org',\r\n '@type': 'EventSeries',\r\n name: series.title,\r\n location,\r\n };\r\n\r\n // Description: prefer description, fall back to eventSummary\r\n const description = stripEventDescriptionHtml(series.description) || series.eventSummary;\r\n if (description) {\r\n data.description = description;\r\n }\r\n\r\n // Image\r\n if (series.image) {\r\n data.image = [series.image];\r\n }\r\n\r\n // Performers: flat displayName array (different from event builder's nested shape)\r\n const performers = series.performers?.filter(p => p.displayName);\r\n if (performers && performers.length > 0) {\r\n data.performer = performers.map(p => ({\r\n '@type': 'PerformingGroup',\r\n name: p.displayName,\r\n }));\r\n }\r\n\r\n // SubEvents: build Event blocks for each occurrence\r\n const subEvents: Record<string, unknown>[] = [];\r\n\r\n for (const occ of series.occurrences) {\r\n const startDate = formatDateTimeWithOffset(\r\n occ.startDateTime,\r\n series.timeZone\r\n );\r\n if (!startDate) continue; // Skip occurrences without valid startDate\r\n\r\n const subEvent: Record<string, unknown> = {\r\n '@type': 'Event',\r\n name: occ.title,\r\n startDate,\r\n eventStatus: getEventStatus(occ.status),\r\n location,\r\n };\r\n\r\n // @id from baseEventUrl\r\n if (baseEventUrl) {\r\n subEvent['@id'] = `${baseEventUrl}/${occ.id}-${occ.slug}`;\r\n }\r\n\r\n // endDate\r\n const endDate = formatDateTimeWithOffset(occ.endDateTime, series.timeZone);\r\n if (endDate) {\r\n subEvent.endDate = endDate;\r\n }\r\n\r\n // Offers from simplified ticket DTOs\r\n const offers = buildSeriesOccurrenceOffers(occ.tickets, occ.status);\r\n if (offers) {\r\n subEvent.offers = offers;\r\n }\r\n\r\n subEvents.push(subEvent);\r\n }\r\n\r\n // If all occurrences were skipped, return null\r\n if (subEvents.length === 0) return null;\r\n\r\n data.subEvent = subEvents;\r\n\r\n return data;\r\n}\r\n","/**\r\n * Pure function that builds CollectionPage JSON-LD for an event collection.\r\n *\r\n * A collection is a curated list of shows — NOT a single event, and NOT a\r\n * Festival (the product can't infer festival intent from a collection named\r\n * \"Detroit Comedy Festival\"; it's still just a collection). We emit a\r\n * schema.org `CollectionPage` whose `mainEntity` is a *summary-page* `ItemList`\r\n * — each `ListItem` is just `{ position, name, url }` pointing at the show's\r\n * canonical `/e/{id}-{slug}` leaf. The full `Event` rich-result markup lives on\r\n * each event's own page (one event = one leaf URL); duplicating full Event\r\n * objects here would create competing entities and trigger missing-field\r\n * (offers/address) warnings on the collection page.\r\n *\r\n * (Previously emitted `Festival` + `subEvent`, which Google's Rich Results\r\n * Test does not support.)\r\n */\r\n\r\nimport type { CollectionJsonLdInput } from './types';\r\nimport { stripEventDescriptionHtml } from '@getmicdrop/svelte-components/richtext';\r\n\r\nexport interface BuildCollectionJsonLdOptions {\r\n collection: CollectionJsonLdInput;\r\n baseEventUrl?: string; // e.g., \"https://get-micdrop.com/e\"\r\n /** Accepted for API compatibility; the summary ItemList carries no dates. */\r\n defaultTimeZone?: string;\r\n}\r\n\r\nexport function buildCollectionJsonLd(\r\n options: BuildCollectionJsonLdOptions,\r\n): Record<string, unknown> | null {\r\n const { collection, baseEventUrl } = options;\r\n\r\n // Required fields\r\n if (!collection.collectionTitle) return null;\r\n if (!collection.events || collection.events.length === 0) return null;\r\n\r\n // Build a summary ListItem per collection member: position + name + the\r\n // canonical leaf url. Prefer the slugged form, else bare `/e/{id}` (the app\r\n // self-heals bare ids to the slugged canonical).\r\n const itemListElement: Record<string, unknown>[] = [];\r\n\r\n for (const event of collection.events) {\r\n if (!event.title) continue; // Skip members with no name to list\r\n\r\n const listItem: Record<string, unknown> = {\r\n '@type': 'ListItem',\r\n position: itemListElement.length + 1,\r\n name: event.title,\r\n };\r\n\r\n if (baseEventUrl) {\r\n listItem.url = event.slug\r\n ? `${baseEventUrl}/${event.id}-${event.slug}`\r\n : `${baseEventUrl}/${event.id}`;\r\n }\r\n\r\n itemListElement.push(listItem);\r\n }\r\n\r\n // If every event was skipped, there's nothing to mark up.\r\n if (itemListElement.length === 0) return null;\r\n\r\n const data: Record<string, unknown> = {\r\n '@context': 'https://schema.org',\r\n '@type': 'CollectionPage',\r\n name: collection.collectionTitle,\r\n };\r\n\r\n // Description: prefer description, fall back to summary\r\n const description = stripEventDescriptionHtml(collection.description) || collection.summary;\r\n if (description) {\r\n data.description = description;\r\n }\r\n\r\n // Cover image\r\n if (collection.coverImage) {\r\n data.image = [collection.coverImage];\r\n }\r\n\r\n data.mainEntity = {\r\n '@type': 'ItemList',\r\n numberOfItems: itemListElement.length,\r\n itemListElement,\r\n };\r\n\r\n return data;\r\n}\r\n","/**\n * Slug generation for routing URLs.\n *\n * This is the canonical slugify implementation for the MicDrop platform.\n * Originally lifted byte-for-byte from micdrop-frontend's local slug helper;\n * that copy has since been deleted in favor of this module, which is now the\n * single source of truth every consumer (MF, VC, future consumers) imports.\n * The algorithm must never change silently (user-visible + bookmarked URLs).\n *\n * @module routing/slug\n */\n/** Max slug length (chars). Google/SEO sweet spot is ≤60–80; we cap at 70. */\nconst MAX_SLUG_LENGTH = 70;\n/**\n * Creates a URL-safe slug from arbitrary text.\n *\n * - NFKD-decomposes accented chars (é → e + combining accent) then strips the\n * combining diacritical marks, so \"Café\" → \"cafe\" (not \"caf\")\n * - lowercases\n * - trims surrounding whitespace\n * - strips characters that are not word chars, whitespace, or hyphens\n * - collapses runs of whitespace / underscores / hyphens into a single hyphen\n * - trims leading/trailing hyphens\n * - caps length at {@link MAX_SLUG_LENGTH} (70) chars, re-trimming a trailing\n * hyphen the cap may have landed on\n *\n * @example\n * toSlug('Comedy Night: Live at The Club!'); // 'comedy-night-live-at-the-club'\n * @example\n * toSlug('Café Comedy'); // 'cafe-comedy'\n */\n// NOTE: declared as `toSlug` (not `slugify`) and re-exported under the public\n// name below. The shared eslint `no-restricted-syntax` rule forbids declaring a\n// binding named `slugify` outside the canonical site; aliasing keeps the public\n// surface (`slugify`) without redefining that identifier here.\n//\n// This module IS the SEO-canonical / bookmarked authority for emitted URLs;\n// every consumer imports it, so the algorithm must stay stable forever.\nfunction toSlug(text) {\n return (text\n .normalize('NFKD') // decompose accented chars: é -> e + combining accent\n .replace(/[\\u0300-\\u036f]/g, '') // strip combining diacritical marks\n .toLowerCase()\n .trim()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/[\\s_-]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, MAX_SLUG_LENGTH)\n // re-trim in case the length cap landed mid-separator\n .replace(/-+$/g, ''));\n}\nexport { toSlug, toSlug as slugify };\n","/**\r\n * Text utility functions: generateSlug, validateNegativeNumber\r\n *\r\n * classNames and truncateTitle re-exported from @getmicdrop/svelte-components\r\n * via ./utils.js for backward compatibility with existing consumers.\r\n */\r\n\r\nimport { slugify } from '@getmicdrop/svelte-components/routing';\r\n\r\n/**\r\n * Generate a URL-friendly slug from a string.\r\n *\r\n * Converged onto the canonical SC `slugify` (NFKD diacritic strip so \"Café\" →\r\n * \"cafe\" not \"caf\", underscore/hyphen run collapse, 70-char SEO cap) — kept\r\n * byte-for-byte in sync with micdrop-frontend's SEO-canonical slug authority.\r\n *\r\n * Safe to swap the algorithm: VC entity routes resolve BY ID (`[id]/[slug]`\r\n * loads via `fetchEventDetails(id)`; the widget's `#{id}-{slug}` hash resolves\r\n * via `/resolve/{id}` with the slug only a backend canonicalize hint). The slug\r\n * is decorative, so a different cosmetic slug never breaks a shared/bookmarked\r\n * link.\r\n *\r\n * The `if (!text)` guard is preserved because SC `slugify` calls `.normalize()`\r\n * directly and would throw on null/undefined; several call sites pass an\r\n * unguarded `event.name`.\r\n */\r\nexport function generateSlug(text: string): string {\r\n if (!text) return '';\r\n return slugify(text);\r\n}\r\n\r\n/**\r\n * Validate that a value is a positive number\r\n * Returns an error message string, or empty string if valid\r\n */\r\nexport function validateNegativeNumber(\r\n value: string | number | null | undefined\r\n): string {\r\n if (value === null || value === undefined || value === '') {\r\n return 'Must be greater than zero.';\r\n }\r\n\r\n const stringValue = String(value).trim();\r\n\r\n if (\r\n stringValue.includes('--') ||\r\n stringValue.includes('++') ||\r\n stringValue.match(/^-+$/) ||\r\n stringValue.match(/^\\++$/)\r\n ) {\r\n return 'Please enter a valid number';\r\n }\r\n\r\n if (stringValue.match(/^-?\\d+-$/)) {\r\n return 'Please enter a valid number';\r\n }\r\n\r\n if ((stringValue.match(/\\./g) || []).length > 1) {\r\n return 'Please enter a valid number';\r\n }\r\n\r\n const numValue = Number(value);\r\n if (isNaN(numValue)) {\r\n return 'Please enter a valid number';\r\n }\r\n\r\n if (numValue < 0 || numValue === 0) {\r\n return 'Price must be greater than zero.';\r\n }\r\n\r\n return '';\r\n}\r\n","import { getDefaultTimezone } from '@getmicdrop/svelte-components/utils/timezones';\r\nimport { stripEventDescriptionHtml } from '@getmicdrop/svelte-components/richtext';\r\n/**\r\n * HostSeoController -- manages JSON-LD structured data, canonical URLs,\r\n * OG tags, meta descriptions, and page title injection into venue pages'\r\n * <head> when the Micdrop calendar widget is embedded.\r\n *\r\n * Follows a save-replace-restore pattern to be a \"good citizen\" on venue pages:\r\n * idle -> listing -> entity -> listing -> ... -> destroyed\r\n *\r\n * Uses textContent (never innerHTML) for JSON-LD script tags to prevent XSS.\r\n */\r\n\r\nimport type { HostSeoControllerOptions } from './types';\r\nimport { buildEventJsonLd } from './buildEventJsonLd';\r\nimport { buildSeriesJsonLd } from './buildSeriesJsonLd';\r\nimport { buildCollectionJsonLd } from './buildCollectionJsonLd';\r\nimport { generateSlug } from '$lib/utils/textUtils';\r\n\r\ntype ControllerState = 'idle' | 'listing' | 'entity' | 'destroyed';\r\n\r\ninterface SavedOriginals {\r\n title: string;\r\n canonicalHref: string | null;\r\n ogTitle: string | null;\r\n ogDescription: string | null;\r\n ogImage: string | null;\r\n ogUrl: string | null;\r\n ogType: string | null;\r\n metaDescription: string | null;\r\n}\r\n\r\n// Minimal event shape for listing mode\r\ninterface ListingEvent {\r\n id: number | string;\r\n name: string;\r\n slug?: string;\r\n startDateTime?: string;\r\n image?: string;\r\n}\r\n\r\n// Venue data passed at runtime\r\ninterface VenueData {\r\n name: string;\r\n address?: string;\r\n googleLocationNameCache?: string;\r\n // Transformed venues carry lowercase `timezone`; raw/host data may use\r\n // PascalCase `timeZone`. Accept both so the venue tz isn't silently dropped.\r\n timeZone?: string;\r\n timezone?: string;\r\n}\r\n\r\nexport class HostSeoController {\r\n /**\r\n * Single active instance. A venue page that mounts >1 calendar embed\r\n * (e.g. \"upcoming shows\" + \"this weekend\") will see the *later*\r\n * controller take over `<head>` — the earlier one is destroyed.\r\n *\r\n * KNOWN LIMITATION: multi-embed pages cannot run two SEO controllers\r\n * in parallel because they would both fight to own `<title>`/OG/etc.\r\n * Pass `data-inject-seo=\"false\"` on all but one embed to opt out.\r\n */\r\n static _activeInstance: HostSeoController | null = null;\r\n\r\n private _state: ControllerState = 'idle';\r\n private _venueName: string;\r\n private _venueAddress: string | undefined;\r\n private _venueTimeZone: string;\r\n private _baseUrl: string;\r\n private _originals: SavedOriginals | null = null;\r\n private _originalsSaved = false;\r\n private _events: ListingEvent[] = [];\r\n\r\n constructor(options: HostSeoControllerOptions) {\r\n // SSR guard\r\n if (typeof document === 'undefined') return;\r\n\r\n // If another instance exists, destroy it first.\r\n if (\r\n HostSeoController._activeInstance &&\r\n HostSeoController._activeInstance !== this\r\n ) {\r\n HostSeoController._activeInstance.destroy();\r\n }\r\n\r\n this._venueName = options.venueName;\r\n this._venueAddress = options.venueAddress;\r\n this._venueTimeZone = options.venueTimeZone || getDefaultTimezone();\r\n this._baseUrl =\r\n options.baseUrl || window.location.origin + window.location.pathname;\r\n\r\n this._saveOriginals();\r\n\r\n HostSeoController._activeInstance = this;\r\n }\r\n\r\n // ---------------------------------------------------------------------------\r\n // Private helpers\r\n // ---------------------------------------------------------------------------\r\n\r\n private _saveOriginals(): void {\r\n if (this._originalsSaved) return;\r\n\r\n const canonical = document.querySelector(\r\n 'link[rel=\"canonical\"]'\r\n ) as HTMLLinkElement | null;\r\n const ogTitle = document.querySelector(\r\n 'meta[property=\"og:title\"]'\r\n ) as HTMLMetaElement | null;\r\n const ogDescription = document.querySelector(\r\n 'meta[property=\"og:description\"]'\r\n ) as HTMLMetaElement | null;\r\n const ogImage = document.querySelector(\r\n 'meta[property=\"og:image\"]'\r\n ) as HTMLMetaElement | null;\r\n const ogUrl = document.querySelector(\r\n 'meta[property=\"og:url\"]'\r\n ) as HTMLMetaElement | null;\r\n const ogType = document.querySelector(\r\n 'meta[property=\"og:type\"]'\r\n ) as HTMLMetaElement | null;\r\n const metaDescription = document.querySelector(\r\n 'meta[name=\"description\"]'\r\n ) as HTMLMetaElement | null;\r\n\r\n this._originals = {\r\n title: document.title,\r\n canonicalHref: canonical?.getAttribute('href') ?? null,\r\n ogTitle: ogTitle?.getAttribute('content') ?? null,\r\n ogDescription: ogDescription?.getAttribute('content') ?? null,\r\n ogImage: ogImage?.getAttribute('content') ?? null,\r\n ogUrl: ogUrl?.getAttribute('content') ?? null,\r\n ogType: ogType?.getAttribute('content') ?? null,\r\n metaDescription: metaDescription?.getAttribute('content') ?? null,\r\n };\r\n\r\n this._originalsSaved = true;\r\n }\r\n\r\n private _injectJsonLd(data: Record<string, unknown>): void {\r\n if (typeof document === 'undefined') return;\r\n\r\n // Remove any existing micdrop JSON-LD\r\n document\r\n .querySelectorAll('script[data-micdrop=\"jsonld\"]')\r\n .forEach(el => el.remove());\r\n\r\n const script = document.createElement('script');\r\n script.setAttribute('type', 'application/ld+json');\r\n script.setAttribute('data-micdrop', 'jsonld');\r\n // Use textContent (NOT innerHTML) to prevent XSS\r\n script.textContent = JSON.stringify(data);\r\n document.head.appendChild(script);\r\n }\r\n\r\n private _setMeta(property: string, content: string): void {\r\n if (typeof document === 'undefined') return;\r\n\r\n if (property.startsWith('og:')) {\r\n // OG tags use property attribute\r\n let el = document.querySelector(\r\n `meta[property=\"${property}\"]`\r\n ) as HTMLMetaElement | null;\r\n if (el) {\r\n el.setAttribute('content', content);\r\n } else {\r\n el = document.createElement('meta');\r\n el.setAttribute('property', property);\r\n el.setAttribute('content', content);\r\n el.setAttribute('data-micdrop', 'og');\r\n document.head.appendChild(el);\r\n }\r\n } else {\r\n // Standard meta tags use name attribute (e.g., description)\r\n let el = document.querySelector(\r\n `meta[name=\"${property}\"]`\r\n ) as HTMLMetaElement | null;\r\n if (el) {\r\n el.setAttribute('content', content);\r\n } else {\r\n el = document.createElement('meta');\r\n el.setAttribute('name', property);\r\n el.setAttribute('content', content);\r\n el.setAttribute('data-micdrop', 'meta');\r\n document.head.appendChild(el);\r\n }\r\n }\r\n }\r\n\r\n private _setCanonical(url: string): void {\r\n if (typeof document === 'undefined') return;\r\n\r\n let el = document.querySelector(\r\n 'link[rel=\"canonical\"]'\r\n ) as HTMLLinkElement | null;\r\n if (el) {\r\n el.setAttribute('href', url);\r\n } else {\r\n el = document.createElement('link');\r\n el.setAttribute('rel', 'canonical');\r\n el.setAttribute('href', url);\r\n el.setAttribute('data-micdrop', 'canonical');\r\n document.head.appendChild(el);\r\n }\r\n }\r\n\r\n private _removeAllInjected(): void {\r\n if (typeof document === 'undefined') return;\r\n document.querySelectorAll('[data-micdrop]').forEach(el => el.remove());\r\n }\r\n\r\n private _restoreOriginals(): void {\r\n if (typeof document === 'undefined' || !this._originals) return;\r\n\r\n // Restore title\r\n document.title = this._originals.title;\r\n\r\n // Restore canonical\r\n if (this._originals.canonicalHref !== null) {\r\n const canonical = document.querySelector(\r\n 'link[rel=\"canonical\"]'\r\n ) as HTMLLinkElement | null;\r\n if (canonical) {\r\n canonical.setAttribute('href', this._originals.canonicalHref);\r\n }\r\n }\r\n\r\n // Restore OG tags\r\n this._restoreMetaProperty('og:title', this._originals.ogTitle);\r\n this._restoreMetaProperty('og:description', this._originals.ogDescription);\r\n this._restoreMetaProperty('og:image', this._originals.ogImage);\r\n this._restoreMetaProperty('og:url', this._originals.ogUrl);\r\n this._restoreMetaProperty('og:type', this._originals.ogType);\r\n\r\n // Restore meta description\r\n this._restoreMetaName('description', this._originals.metaDescription);\r\n\r\n this._originalsSaved = false;\r\n }\r\n\r\n private _restoreMetaProperty(\r\n property: string,\r\n originalContent: string | null\r\n ): void {\r\n const el = document.querySelector(\r\n `meta[property=\"${property}\"]`\r\n ) as HTMLMetaElement | null;\r\n if (originalContent !== null && el) {\r\n // Had original value -- restore it\r\n el.setAttribute('content', originalContent);\r\n } else if (originalContent === null && el?.hasAttribute('data-micdrop')) {\r\n // We created it -- remove it\r\n el.remove();\r\n }\r\n }\r\n\r\n private _restoreMetaName(name: string, originalContent: string | null): void {\r\n const el = document.querySelector(\r\n `meta[name=\"${name}\"]`\r\n ) as HTMLMetaElement | null;\r\n if (originalContent !== null && el) {\r\n el.setAttribute('content', originalContent);\r\n } else if (originalContent === null && el?.hasAttribute('data-micdrop')) {\r\n el.remove();\r\n }\r\n }\r\n\r\n private _buildItemList(events: ListingEvent[]): Record<string, unknown> {\r\n return {\r\n '@context': 'https://schema.org',\r\n '@type': 'ItemList',\r\n itemListElement: events.map((event, i) => {\r\n const slug = event.slug || generateSlug(event.name);\r\n return {\r\n '@type': 'ListItem',\r\n position: i + 1,\r\n url: `${this._baseUrl}?e=${event.id}-${slug}`,\r\n };\r\n }),\r\n };\r\n }\r\n\r\n private _buildEntityUrl(\r\n type: string,\r\n id: string | number,\r\n slug: string\r\n ): string {\r\n const prefix = type === 'event' ? 'e' : type === 'series' ? 's' : 'c';\r\n return `${this._baseUrl}?${prefix}=${id}-${slug}`;\r\n }\r\n\r\n // ---------------------------------------------------------------------------\r\n // Public API\r\n // ---------------------------------------------------------------------------\r\n\r\n /**\r\n * Called when events finish loading from the API.\r\n * Injects an ItemList JSON-LD for the listing. Does not modify OG/canonical/title.\r\n */\r\n onEventsLoaded(events: ListingEvent[], venue?: VenueData): void {\r\n if (typeof document === 'undefined') return;\r\n if (this._state === 'destroyed') return;\r\n\r\n // Update venue info from parameter if provided\r\n if (venue) {\r\n this._venueName = venue.name || this._venueName;\r\n this._venueAddress =\r\n venue.address || venue.googleLocationNameCache || this._venueAddress;\r\n const venueTz = venue.timeZone || venue.timezone;\r\n if (venueTz) {\r\n this._venueTimeZone = venueTz;\r\n }\r\n }\r\n\r\n // Store events for later use (re-injection on back to listing)\r\n this._events = events;\r\n\r\n // Build and inject ItemList JSON-LD\r\n const itemList = this._buildItemList(events);\r\n this._injectJsonLd(itemList);\r\n\r\n this._state = 'listing';\r\n }\r\n\r\n /**\r\n * Called when the user selects an event, series, or collection.\r\n * Replaces ItemList JSON-LD with entity-specific JSON-LD and updates\r\n * all head tags (canonical, OG, meta description, title).\r\n */\r\n onEntitySelected(\r\n type: 'event' | 'series' | 'collection',\r\n data: any,\r\n venue?: VenueData\r\n ): void {\r\n if (typeof document === 'undefined') return;\r\n if (this._state === 'destroyed') return;\r\n\r\n // Save originals if not already saved\r\n this._saveOriginals();\r\n\r\n // Update venue info from parameter if provided\r\n if (venue) {\r\n this._venueName = venue.name || this._venueName;\r\n this._venueAddress =\r\n venue.address || venue.googleLocationNameCache || this._venueAddress;\r\n const venueTz = venue.timeZone || venue.timezone;\r\n if (venueTz) {\r\n this._venueTimeZone = venueTz;\r\n }\r\n }\r\n\r\n let jsonLd: Record<string, unknown> | null = null;\r\n let entityName = '';\r\n // Raw rich-text description (HTML) kept separate from the plain-text\r\n // summary fallback so we can strip the description first and only then fall\r\n // back — a blank rich-text description (`<p><br></p>`) must not mask a real\r\n // summary. (PBN-1)\r\n let entityDescription = '';\r\n let entitySummary = '';\r\n let entityImage = '';\r\n let entityUrl = '';\r\n\r\n if (type === 'event') {\r\n const slug = data.slug || generateSlug(data.name || '');\r\n entityUrl = this._buildEntityUrl(\r\n 'event',\r\n data.id || data.eventID || '',\r\n slug\r\n );\r\n\r\n jsonLd = buildEventJsonLd({\r\n event: data,\r\n venue: {\r\n name: this._venueName,\r\n googleLocationNameCache: this._venueAddress,\r\n },\r\n venueTimeZone: this._venueTimeZone,\r\n eventUrl: entityUrl,\r\n });\r\n\r\n entityName = data.name || '';\r\n entityDescription = data.description || '';\r\n entitySummary = data.eventSummary || '';\r\n entityImage = data.image || data.coverImage || data.ShowImage || '';\r\n } else if (type === 'series') {\r\n const slug = data.slug || generateSlug(data.title || '');\r\n entityUrl = this._buildEntityUrl('series', data.id || '', slug);\r\n\r\n jsonLd = buildSeriesJsonLd({\r\n series: {\r\n ...data,\r\n venue: {\r\n name: this._venueName,\r\n googleLocationNameCache: this._venueAddress,\r\n },\r\n timeZone: this._venueTimeZone,\r\n },\r\n baseEventUrl: entityUrl,\r\n });\r\n\r\n entityName = data.title || '';\r\n entityDescription = data.description || '';\r\n entitySummary = data.eventSummary || '';\r\n entityImage = data.image || '';\r\n } else if (type === 'collection') {\r\n const slug = data.slug || generateSlug(data.collectionTitle || '');\r\n entityUrl = this._buildEntityUrl('collection', data.id || '', slug);\r\n\r\n jsonLd = buildCollectionJsonLd({\r\n collection: data,\r\n baseEventUrl: entityUrl,\r\n defaultTimeZone: this._venueTimeZone,\r\n });\r\n\r\n entityName = data.collectionTitle || '';\r\n entityDescription = data.description || '';\r\n entitySummary = data.summary || '';\r\n entityImage = data.coverImage || '';\r\n }\r\n\r\n // Inject JSON-LD (replaces existing micdrop JSON-LD)\r\n if (jsonLd) {\r\n this._injectJsonLd(jsonLd);\r\n }\r\n\r\n // Update canonical URL\r\n if (entityUrl) {\r\n this._setCanonical(entityUrl);\r\n }\r\n\r\n // Update OG tags\r\n if (entityName) {\r\n this._setMeta('og:title', entityName);\r\n }\r\n // Descriptions are stored as rich-text HTML. Flatten (and truncate) to plain\r\n // text before meta injection — the same canonical strip the /e page uses —\r\n // so raw tags never leak into share cards or search snippets. Strip first,\r\n // then fall back to the summary, so blank rich text doesn't mask it. (PBN-1)\r\n const metaDescription =\r\n stripEventDescriptionHtml(entityDescription, 160) ||\r\n stripEventDescriptionHtml(entitySummary, 160);\r\n if (metaDescription) {\r\n this._setMeta('og:description', metaDescription);\r\n } else {\r\n // No usable description — restore the page's original so a previously\r\n // selected entity's description can't linger on this one.\r\n this._restoreMetaProperty(\r\n 'og:description',\r\n this._originals?.ogDescription ?? null\r\n );\r\n }\r\n if (entityImage) {\r\n this._setMeta('og:image', entityImage);\r\n }\r\n if (entityUrl) {\r\n this._setMeta('og:url', entityUrl);\r\n }\r\n this._setMeta('og:type', 'website');\r\n\r\n // Twitter Card: large image preview\r\n this._setMeta('twitter:card', 'summary_large_image');\r\n\r\n // Update meta description\r\n if (metaDescription) {\r\n this._setMeta('description', metaDescription);\r\n } else {\r\n this._restoreMetaName(\r\n 'description',\r\n this._originals?.metaDescription ?? null\r\n );\r\n }\r\n\r\n // Update page title\r\n if (entityName) {\r\n document.title = `${entityName} | ${this._venueName}`;\r\n }\r\n\r\n this._state = 'entity';\r\n }\r\n\r\n /**\r\n * Called when the user navigates back to the event listing.\r\n * Restores original head elements and re-injects ItemList JSON-LD.\r\n */\r\n onBackToListing(): void {\r\n if (typeof document === 'undefined') return;\r\n if (this._state === 'destroyed') return;\r\n\r\n // Restore all originals\r\n this._restoreOriginals();\r\n\r\n // Remove any remaining injected elements\r\n this._removeAllInjected();\r\n\r\n // Re-inject ItemList JSON-LD if events are available\r\n if (this._events.length > 0) {\r\n const itemList = this._buildItemList(this._events);\r\n this._injectJsonLd(itemList);\r\n }\r\n\r\n // Re-save originals since we just restored them\r\n this._saveOriginals();\r\n\r\n this._state = 'listing';\r\n }\r\n\r\n /**\r\n * Called when the widget is destroyed. Removes all injected elements\r\n * and restores originals. Clears the static active instance.\r\n */\r\n destroy(): void {\r\n if (typeof document === 'undefined') return;\r\n if (this._state === 'destroyed') return;\r\n\r\n this._removeAllInjected();\r\n this._restoreOriginals();\r\n\r\n if (HostSeoController._activeInstance === this) {\r\n HostSeoController._activeInstance = null;\r\n }\r\n\r\n this._state = 'destroyed';\r\n }\r\n}\r\n"],"names":["getDefaultTimezone","BLOCK_CLOSE","LINE_BREAK","ANY_TAG","NAMED_ENTITIES","decodeEntities","input","_","hex","codePointOrEmpty","dec","m","code","stripEventDescriptionHtml","html","maxLength","text","clipped","lastSpace","browserNow","escapeJsonLdForScript","json","formatDateTimeWithOffset","isoString","tz","date","offsetFormatOptions","formatter","parts","get","type","p","year","month","day","hour","minute","second","tzOffset","offset","match","sign","hours","mins","parseAddressToPostal","addressStr","stateZipMatch","buildPostalAddress","fallback","out","getEventStatus","rawStatus","status","getTicketAvailability","ticket","now","salesBegin","salesEnd","buildPriceRangeOffer","lowPrice","highPrice","availability","extra","buildOffers","tickets","url","publicTickets","t","visibility","isVisible","isPublic","prices","buildPerformers","performers","confirmed","isConfirmed","isNotHidden","profile","hasName","buildEventJsonLd","options","event","venue","venueTimeZone","eventUrl","organizer","startDate","postalAddress","location","data","endDate","description","imageUrl","offers","org","mapSeriesTicketAvailability","buildSeriesOccurrenceOffers","occurrenceStatus","activeTickets","buildSeriesJsonLd","series","baseEventUrl","address","subEvents","occ","subEvent","buildCollectionJsonLd","collection","itemListElement","listItem","MAX_SLUG_LENGTH","toSlug","generateSlug","slugify","_HostSeoController","canonical","ogTitle","ogDescription","ogImage","ogUrl","ogType","metaDescription","el","script","property","content","originalContent","name","events","i","slug","id","prefix","venueTz","itemList","jsonLd","entityName","entityDescription","entitySummary","entityImage","entityUrl","HostSeoController"],"mappings":"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","x_google_ignoreList":[0,1,2,7]}
|
|
1
|
+
{"version":3,"file":"seo.mjs","sources":["../../node_modules/@getmicdrop/svelte-components/dist/utils/timezones.js","../../node_modules/@getmicdrop/svelte-components/dist/richtext/index.js","../../node_modules/@getmicdrop/svelte-components/dist/datetime/now.js","../../node_modules/@getmicdrop/svelte-components/dist/errors/classes/AppError.js","../../node_modules/@getmicdrop/svelte-components/dist/errors/classes/ValueGuardError.js","../../node_modules/@getmicdrop/svelte-components/dist/datetime/types.js","../../node_modules/@getmicdrop/svelte-components/dist/types/brands.js","../../node_modules/@getmicdrop/svelte-components/dist/datetime/instant.js","../../src/lib/seo/helpers.ts","../../src/lib/seo/buildEventJsonLd.ts","../../src/lib/seo/buildSeriesJsonLd.ts","../../src/lib/seo/buildCollectionJsonLd.ts","../../node_modules/@getmicdrop/svelte-components/dist/routing/slug.js","../../src/lib/seo/HostSeoController.ts"],"sourcesContent":["/**\n * Timezone utility helpers.\n * Canonical source for timezone defaults and validation.\n */\n/**\n * Returns the default timezone for Micdrop, branded as `IANATimezone`.\n * Used when no explicit timezone is provided to date formatters. Bypass\n * call sites that pass `'America/Los_Angeles'` literally are red-squiggle\n * type errors at canonicals that take `IANATimezone` — the fix is to\n * thread `getDefaultTimezone()` through.\n *\n * @returns Branded IANA timezone (`'America/Los_Angeles'`)\n *\n * @example\n * formatDate(browserNow(), {}, 'en-US', getDefaultTimezone())\n *\n * @deprecated Duplicate of the canonical default. Use `getDefaultTimezone`\n * (or the `DEFAULT_TIMEZONE` constant) from `$lib/datetime`\n * (re-exported via `datetime/constants`) — the single source of truth.\n */\n// no-shadowed-helper: deliberate @deprecated backward-compat shim on the public\n// `/utils/timezones` subpath; its behavior is pinned by\n// datetime-barrel.characterization.test.ts. Sunset by DELETION once consumers\n// migrate off the subpath, not by convergence (that would change the public API /\n// break the pinned tests). Exempted by name for this file in eslint.config.js.\nexport function getDefaultTimezone() {\n return 'America/Los_Angeles';\n}\n/**\n * Validates an IANA timezone string.\n * Uses Intl.DateTimeFormat to test if the timezone is recognized.\n *\n * @param tz - Timezone string to validate\n * @returns true if valid IANA timezone, false otherwise\n *\n * @example\n * isValidTimezone('America/Los_Angeles') // true\n * isValidTimezone('Invalid/Zone') // false\n *\n * @deprecated Lenient: `Intl.DateTimeFormat` accepts non-canonical inputs.\n * Use `isValidTimezone` from `$lib/datetime` (stricter — rejects\n * abbreviations like `'PST'`), or `toIANATimezone` from `$lib/types/brands`\n * when you need a branded value.\n */\n// no-shadowed-helper: deliberate @deprecated LENIENT shim on the public\n// `/utils/timezones` subpath; the lenient behavior (accepts 'PST' etc.) is\n// explicitly pinned by datetime-barrel.characterization.test.ts. The canonical\n// datetime/timezone.ts version is stricter — converging would flip pinned\n// behavior + break the public subpath. Sunset by DELETION after consumer\n// migration. Exempted by name for this file in eslint.config.js.\nexport function isValidTimezone(tz) {\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: tz });\n return true;\n }\n catch {\n return false;\n }\n}\n","/**\n * Rich-text helpers for event / series descriptions.\n *\n * Pure string utilities — NO DOM access and NO external dependencies — so they\n * run safely during SSR (e.g. a `<svelte:head>` meta-description) as well as in\n * the browser. The companion {@link RichTextEditor} produces HTML that already\n * passes through an Eventbrite-parity allowlist; these helpers:\n *\n * - {@link normalizeEventDescriptionHtml} — promote legacy plain-text\n * descriptions to paragraph HTML so they don't render as a single line.\n * - {@link isEventDescriptionEqual} — compare two descriptions\n * whitespace-insensitively, so cosmetic edits don't trip dirty-checking.\n * - {@link stripEventDescriptionHtml} — flatten HTML to plain text (optionally\n * truncated) for meta tags / previews.\n *\n * @module richtext\n */\n/** Block-level closers that should read as a space when flattened to text. */\nconst BLOCK_CLOSE = /<\\/(?:p|div|h[1-6]|li|ul|ol|blockquote|tr)>/gi;\nconst LINE_BREAK = /<br\\s*\\/?>/gi;\nconst ANY_TAG = /<[^>]*>/g;\n/** A leading `<tag>` is a reliable \"this is already HTML\" signal. */\nconst LOOKS_LIKE_HTML = /<[a-z][\\s\\S]*>/i;\nconst NAMED_ENTITIES = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n \""\": '\"',\n \"'\": \"'\",\n \"'\": \"'\",\n \" \": \" \",\n};\nfunction decodeEntities(input) {\n return input\n .replace(/&#x([0-9a-f]+);/gi, (_, hex) => codePointOrEmpty(parseInt(hex, 16)))\n .replace(/&#(\\d+);/g, (_, dec) => codePointOrEmpty(Number(dec)))\n .replace(/&[a-z0-9]+;/gi, (m) => NAMED_ENTITIES[m.toLowerCase()] ?? m);\n}\nfunction codePointOrEmpty(code) {\n return Number.isFinite(code) && code > 0 && code <= 0x10ffff\n ? String.fromCodePoint(code)\n : \"\";\n}\nfunction escapeHtml(input) {\n return input\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\");\n}\n/**\n * Flatten a (possibly HTML) description to a single line of plain text.\n * When `maxLength` is given the result is truncated on a word boundary and an\n * ellipsis appended. Returns `''` for empty/missing input. SSR-safe.\n */\nexport function stripEventDescriptionHtml(html, maxLength) {\n if (!html)\n return \"\";\n let text = String(html)\n .replace(LINE_BREAK, \" \")\n .replace(BLOCK_CLOSE, \" \")\n .replace(ANY_TAG, \"\");\n text = decodeEntities(text).replace(/\\s+/g, \" \").trim();\n if (maxLength && maxLength > 0 && text.length > maxLength) {\n const clipped = text.slice(0, maxLength);\n const lastSpace = clipped.lastIndexOf(\" \");\n text =\n (lastSpace > 40 ? clipped.slice(0, lastSpace) : clipped).trimEnd() + \"…\";\n }\n return text;\n}\n/**\n * Normalize a stored description for the rich-text editor. Existing HTML is\n * returned as-is (trimmed); legacy plain text is escaped and promoted to\n * paragraphs (blank lines → `<p>`, single newlines → `<br>`) so it renders with\n * its original line structure instead of collapsing to one line. SSR-safe.\n */\nexport function normalizeEventDescriptionHtml(html) {\n if (!html)\n return \"\";\n const value = String(html);\n if (LOOKS_LIKE_HTML.test(value))\n return value.trim();\n return value\n .replace(/\\r\\n/g, \"\\n\")\n .split(/\\n{2,}/)\n .map((block) => block.trim())\n .filter(Boolean)\n .map((block) => `<p>${escapeHtml(block).replace(/\\n/g, \"<br>\")}</p>`)\n .join(\"\");\n}\n/** Collapse a description to a canonical form for equality comparison. */\nfunction canonicalize(html) {\n if (!html)\n return \"\";\n const collapsed = String(html)\n .replace(/>\\s+</g, \"><\")\n .replace(/\\s+/g, \" \")\n .trim();\n // An \"empty\" editor value (stray empty paragraphs / breaks) === no value.\n return /^(?:<p>(?:\\s| |<br\\s*\\/?>)*<\\/p>|<br\\s*\\/?>)*$/i.test(collapsed)\n ? \"\"\n : collapsed;\n}\n/**\n * True when two descriptions are equal ignoring cosmetic whitespace and\n * empty-paragraph noise — used for Save/dirty checking so a no-op edit doesn't\n * keep the form dirty. SSR-safe.\n */\nexport function isEventDescriptionEqual(a, b) {\n return canonicalize(a) === canonicalize(b);\n}\n// Sanitizer + shared allowlist — the security boundary for rendering stored\n// description HTML (which can arrive via CSV import / Eventbrite sync) on the\n// public event page + embed. `RichTextContent` calls this on render; callers\n// rendering description HTML directly with `{@html}` MUST sanitize here first.\nexport { sanitizeEventDescriptionHtml } from \"./sanitize.js\";\nexport { EVENT_DESCRIPTION_TAGS, EVENT_DESCRIPTION_ATTRS, EVENT_DESCRIPTION_LINK_PROTOCOLS, EVENT_DESCRIPTION_WHITELIST, } from \"./allowlist.js\";\n","/**\n * `browserNow` — the canon's ONE \"now\" mint, in a dependency-free leaf module.\n *\n * It lives here (not in `./local.ts`, which re-exports it unchanged) because\n * foundation layers BELOW the datetime canon need a canonical now-read too:\n * `errors/classes/AppError.ts` stamps `timestamp` at construction, but\n * `datetime/local.ts` → `timezone.ts` → `types.ts` → `ValueGuardError` →\n * `AppError` — so an AppError import of `local.js` is a circular-import crash\n * (\"Class extends value undefined\"). A leaf with zero imports breaks the cycle\n * while keeping exactly one sanctioned raw `new Date()` now-read in the repo.\n *\n * All consumers keep importing `browserNow` from `$lib/datetime` (or\n * `datetime/local.js`) — this module is plumbing, not a second door.\n */\n/**\n * Current instant, as a `Date` in the **browser's own local clock** — the\n * named-intent replacement for a bare `new Date()` \"now\".\n *\n * See the re-export in `./local.ts` for the full usage contract (viewer-local\n * UI only; SSR reads the server clock).\n *\n * @returns A `Date` for the current instant.\n */\nexport function browserNow() {\n return new Date();\n}\n","/**\n * Base Application Error\n *\n * All custom errors extend this class for consistent error handling.\n * Provides structured error information with serialization support.\n */\n// Deliberately the dependency-free leaf (NOT datetime/local.js): local.ts pulls\n// timezone.ts → datetime/types.ts → ValueGuardError → AppError, a cycle that\n// crashes class initialization (\"Class extends value undefined\").\nimport { browserNow } from '../../datetime/now.js';\nexport class AppError extends Error {\n constructor(message, code, options = {}) {\n super(message);\n this.name = 'AppError';\n this.code = code;\n this.status = options.status ?? 500;\n this.details = options.details;\n this.timestamp = browserNow();\n this.recoverable = options.recoverable ?? true;\n this.cause = options.cause;\n // Maintains proper stack trace\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, this.constructor);\n }\n }\n /**\n * Serialize error for logging/reporting\n */\n toJSON() {\n return {\n name: this.name,\n code: this.code,\n message: this.message,\n status: this.status,\n details: this.details,\n timestamp: this.timestamp.toISOString(),\n recoverable: this.recoverable,\n stack: this.stack,\n cause: this.cause?.message,\n };\n }\n /**\n * Get user-friendly message.\n *\n * Subclasses override this and may consult an optional `ErrorMessageResolver`\n * to localize. The base class returns the raw message and ignores any\n * resolver (the parameter exists only so the signature is uniform across the\n * hierarchy).\n */\n getUserMessage(_resolver) {\n return this.message;\n }\n /**\n * Check if this error type is retryable\n */\n isRetryable() {\n return this.recoverable;\n }\n}\n","/**\n * Value-Guard Error\n *\n * The branch of the AppError tree for low-level value-object invariant guards —\n * a primitive failed a fail-fast check at a data-trust boundary (a malformed\n * IANA timezone, a null price at a server-load mapper). Distinct in intent from\n * `ValidationError` (form/field submission errors with a field map): these guard\n * brand-typed values deep in the utilities, carry no user-submitted field, and\n * fire before any UI ever sees the value.\n *\n * They used to live as a second error family beside the exported canon\n * (`DateTimeError` / `CurrencyFormatError`, each `extends Error`). Peet ruling\n * `sc-value-guard-error-tier`: converge, no separate tier — fold them under\n * `AppError`. Re-parenting them onto this branch buys one tree and one set of\n * tooling for free: `isAppError`, `toJSON` serialization, `getUserMessage`, the\n * ring-buffer logger and the normalizers all now cover them, with no change to\n * any leaf class's own public shape (its `code` / `details` / `value` stay put).\n */\nimport { AppError } from './AppError.js';\nexport class ValueGuardError extends AppError {\n constructor(message, code, options = {}) {\n super(message, code, {\n // 422 Unprocessable — the value itself failed an invariant. Not a 500\n // (nothing crashed server-side) and not retryable (re-running the same\n // bad value fails identically), so recoverable defaults to false.\n status: 422,\n recoverable: false,\n ...options,\n });\n this.name = 'ValueGuardError';\n }\n}\n","/**\n * DateTime Module Type Definitions\n *\n * This module provides type-safe date/time handling for the MicDrop platform.\n * All datetime operations should use these types to ensure consistency.\n *\n * @module datetime/types\n */\n/* eslint-disable no-unused-vars -- exported types/enum members are consumed by sibling modules; no-unused-vars only sees this file. */\nimport { ValueGuardError } from '../errors/classes/ValueGuardError.js';\n/**\n * Error thrown when datetime operations fail.\n *\n * A value-guard branch of the AppError tree (Peet ruling\n * `sc-value-guard-error-tier`): it `extends ValueGuardError extends AppError`,\n * so `instanceof AppError` / `isAppError()` / `toJSON()` all cover it, while its\n * own `code` + `details` shape is preserved unchanged for every caller. `code`\n * and `details` are narrowed via `declare` (their values are set by the\n * `AppError` constructor through `super`, so no field re-initialization runs).\n */\nexport class DateTimeError extends ValueGuardError {\n constructor(message, code, details) {\n super(message, code, { details });\n this.name = 'DateTimeError';\n }\n}\n/**\n * Error codes for datetime operations.\n */\nexport var DateTimeErrorCode;\n(function (DateTimeErrorCode) {\n DateTimeErrorCode[\"INVALID_TIMEZONE\"] = \"INVALID_TIMEZONE\";\n DateTimeErrorCode[\"INVALID_ISO_STRING\"] = \"INVALID_ISO_STRING\";\n DateTimeErrorCode[\"INVALID_DATE\"] = \"INVALID_DATE\";\n DateTimeErrorCode[\"INVALID_TIME\"] = \"INVALID_TIME\";\n DateTimeErrorCode[\"MISSING_TIMEZONE\"] = \"MISSING_TIMEZONE\";\n DateTimeErrorCode[\"PARSE_ERROR\"] = \"PARSE_ERROR\";\n})(DateTimeErrorCode || (DateTimeErrorCode = {}));\n","/**\n * Branded primitive types — the convergent core of the canonical-redefinition\n * defense. Each brand pairs a primitive (number / string) with a tag the\n * compiler tracks but the runtime ignores. The only way a value of `Cents`\n * gets created is by passing through {@link toCents} (or another canonical\n * that returns `Cents`); raw `42` cannot be silently passed where `Cents`\n * is expected.\n *\n * Why this works where audits don't: an audit lists patterns that BYPASS\n * canonicals and greps for them — non-convergent because the catalog lags\n * developer creativity. Brands invert the polarity: they verify a canonical\n * is THE ONLY path that compiles for its concept. New variants of a bypass\n * cannot exist because the type is unforgeable. See the spec section\n * \"negative-space vs positive-space\" for the full rationale.\n *\n * Boundary policy:\n * - Smart constructors (`toCents`, `toDateOnly`, etc.) are the only public\n * way to MINT a brand. They live here.\n * - Casts (`asCents`, etc.) are intended for the API-response-transform\n * layer ONLY — server load functions, store hydration, response\n * mappers. They unsafely tag a primitive as already-validated.\n * - Feature code never sees `as Cents`; if it needs to ingest a raw\n * number it goes through the smart constructor.\n *\n * Real-world precedent: Stripe (Money, Currency), Shopify (MoneyV2),\n * Atlassian. See https://www.learningtypescript.com/articles/branded-types\n */\n// ── Smart constructors ───────────────────────────────────────────────────\n/**\n * Validate and brand a number as `Cents`. Throws on NaN/Infinity/non-int.\n *\n * @example\n * const price = toCents(2550); // Cents — \"$25.50\"\n * const bad = toCents(25.5); // throws: cents must be integer\n */\nexport function toCents(n) {\n if (!Number.isFinite(n)) {\n throw new TypeError(`cents must be a finite number, got ${n}`);\n }\n if (!Number.isInteger(n)) {\n throw new TypeError(`cents must be integer, got ${n}`);\n }\n return n;\n}\n/**\n * Validate and brand a number as `USD`. Allows fractional dollars but\n * rejects non-finite values.\n */\nexport function toUSD(n) {\n if (!Number.isFinite(n)) {\n throw new TypeError(`USD must be a finite number, got ${n}`);\n }\n return n;\n}\n/**\n * Convert dollars (decimal) to `Cents` rounded half-away-from-zero.\n * The conversion is the only place dollar→cent rounding happens; once\n * minted, a `Cents` value is canonical and addition is exact.\n */\nexport function dollarsToCents(dollars) {\n if (!Number.isFinite(dollars)) {\n throw new TypeError(`dollars must be a finite number, got ${dollars}`);\n }\n return Math.round(dollars * 100);\n}\n/** Convert `Cents` back to dollars (decimal). Loses the brand intentionally. */\nexport function centsToDollars(c) {\n return c / 100;\n}\n/**\n * Build a UTC `Date` from calendar parts — the ONE place this module (and\n * `datetime/instant`) turns `Date.UTC` into an instant.\n *\n * `Date.UTC` maps a year of 0–99 to **1900 + year** (ECMA-262's legacy\n * two-digit-year rule), so `Date.UTC(1, 0, 1)` is **1901**-01-01, not year 1.\n * Every guard in this file round-trips its parts through `Date.UTC` and refuses\n * the value when they move — so without the correction below, that legacy rule\n * makes them refuse **every year from 0001 to 0099** as \"not a real calendar\n * date\". They are real, and one of them is load-bearing: `0001-01-01T00:00:00Z`\n * is Go's zero `time.Time`, the backend's \"never set\" marker, which arrives on\n * the wire constantly.\n *\n * This existed as four hand-rolled copies of the same probe (`toDateOnly`,\n * `toUTCTimestamp`, `isUTCTimestamp`, `addDays`) plus a fifth in\n * `datetime/instant`. Fixing one and leaving the others is how the bug walks\n * back in, so they all call this.\n *\n * @param year - Full year (0–99 means years 0–99, NOT 1900–1999).\n * @param month - 1-indexed month.\n * @param day - Day of month.\n * @returns The UTC instant those parts name.\n */\nexport function utcFromParts(year, month, day, hours = 0, minutes = 0, seconds = 0) {\n const probe = new Date(Date.UTC(year, month - 1, day, hours, minutes, seconds));\n if (year >= 0 && year <= 99)\n probe.setUTCFullYear(year);\n return probe;\n}\n/**\n * Validate a `YYYY-MM-DD` string and brand it as `DateOnly`. Only matches\n * exact ISO calendar-date shape; rejects `2026-1-1`, `2026/01/01`, etc.\n */\nexport function toDateOnly(s) {\n if (!/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) {\n throw new TypeError(`DateOnly must be YYYY-MM-DD, got ${JSON.stringify(s)}`);\n }\n // Verify it's a real calendar date (rejects 2026-02-30).\n const [y, m, d] = s.split('-').map(Number);\n const probe = utcFromParts(y, m, d);\n if (probe.getUTCFullYear() !== y ||\n probe.getUTCMonth() !== m - 1 ||\n probe.getUTCDate() !== d) {\n throw new TypeError(`DateOnly must be a real calendar date, got ${s}`);\n }\n return s;\n}\n/**\n * Validate an IANA timezone via `Intl.DateTimeFormat` resolution.\n * Throws for non-canonical identifiers.\n */\nexport function toIANATimezone(s) {\n try {\n // Will throw RangeError on invalid timezones. The probe formats the epoch\n // instant (0) — validation only needs SOME instant, and a number avoids a\n // pointless raw Date construction.\n new Intl.DateTimeFormat('en-US', { timeZone: s }).format(0);\n return s;\n }\n catch {\n throw new TypeError(`IANATimezone must be a valid zone, got ${s}`);\n }\n}\n/**\n * Brand a string as `AssetUrl`. Validates http(s)/data scheme so the\n * result is safe to drop into an `<img src>` without further checks.\n */\nexport function toAssetUrl(s) {\n if (typeof s !== 'string' || s.length === 0) {\n throw new TypeError(`AssetUrl must be non-empty string, got ${s}`);\n }\n if (!s.startsWith('https://') &&\n !s.startsWith('http://') &&\n !s.startsWith('data:') &&\n !s.startsWith('/')) {\n throw new TypeError(`AssetUrl must be http(s):, data:, or path-absolute, got ${s.slice(0, 32)}…`);\n }\n return s;\n}\n/** Identifier-brand smart constructors. Reject 0/negative/NaN. */\nfunction brandId(brand) {\n return (n) => {\n if (!Number.isInteger(n) || n <= 0) {\n throw new TypeError(`${brand} must be a positive integer, got ${n}`);\n }\n return n;\n };\n}\n// eslint-disable-next-line no-unused-vars -- intentionally retained (prop/slot/forward-compat or Svelte-5 reactive binding eslint cannot see)\nexport const toEventId = brandId('EventId');\n// eslint-disable-next-line no-unused-vars -- intentionally retained (prop/slot/forward-compat or Svelte-5 reactive binding eslint cannot see)\nexport const toOrderId = brandId('OrderId');\n// eslint-disable-next-line no-unused-vars -- intentionally retained (prop/slot/forward-compat or Svelte-5 reactive binding eslint cannot see)\nexport const toUserId = brandId('UserId');\n// eslint-disable-next-line no-unused-vars -- intentionally retained (prop/slot/forward-compat or Svelte-5 reactive binding eslint cannot see)\nexport const toVenueId = brandId('VenueId');\n/**\n * Parse-and-brand helpers for entity IDs that arrive as either a `number`\n * (e.g. internal store state) or a `string` (e.g. SvelteKit route params,\n * URL query strings, ad-hoc form inputs). Each accepts the union and routes\n * through the underlying smart constructor, so the positive-integer check\n * still applies — `parseEventId(\"0\")` throws the same way `toEventId(0)` does.\n *\n * Use these at the boundary where the caller has a union-typed ID value\n * (typically a public component prop or callback) and needs to land a brand\n * on the inside. Example: `onBuyTickets(eventId: number | string)` callbacks\n * in public ticket-buying components.\n */\n// eslint-disable-next-line no-unused-vars -- intentionally retained (prop/slot/forward-compat or Svelte-5 reactive binding eslint cannot see)\nfunction parseId(brand, ctor) {\n return (value) => {\n let n;\n if (typeof value === 'string') {\n // Strict integer shape — reject partial numerics (`\"42abc\"`, `\"42.5\"`,\n // `\"1e2\"`, leading whitespace) that parseInt would silently truncate.\n if (!/^-?\\d+$/.test(value)) {\n throw new TypeError(`${brand} must be a numeric integer string, got ${JSON.stringify(value)}`);\n }\n n = Number(value);\n // Reject values above Number.MAX_SAFE_INTEGER — Number(...) rounds them\n // before the constructor sees them, which would brand a malformed\n // deep-link to the wrong entity row instead of failing closed.\n if (!Number.isSafeInteger(n)) {\n throw new TypeError(`${brand} must fit in a safe integer, got ${JSON.stringify(value)}`);\n }\n }\n else {\n n = value;\n // Same safe-integer guard the string branch applies — a number typed\n // value above 2^53 has already lost precision by the time it reaches\n // this function, and branding it would point at the wrong entity row.\n if (Number.isFinite(n) && !Number.isSafeInteger(n) && Number.isInteger(n)) {\n throw new TypeError(`${brand} must fit in a safe integer, got ${value}`);\n }\n }\n if (!Number.isFinite(n)) {\n throw new TypeError(`${brand} must be a numeric value, got ${JSON.stringify(value)}`);\n }\n return ctor(n);\n };\n}\nexport const parseEventId = parseId('EventId', toEventId);\nexport const parseOrderId = parseId('OrderId', toOrderId);\nexport const parseUserId = parseId('UserId', toUserId);\nexport const parseVenueId = parseId('VenueId', toVenueId);\n/**\n * Brand a string as `E164PhoneNumber` after validating the E.164 shape.\n * Throws on input that isn't a `+` followed by digits. This is a *shape*\n * check; for full carrier-aware validation use `isValidE164` from\n * `phoneUtils` (which also returns this brand via type predicate).\n */\nexport function toE164PhoneNumber(s) {\n if (typeof s !== 'string' || !/^\\+[1-9]\\d{6,14}$/.test(s)) {\n throw new TypeError(`E164PhoneNumber must be +<digits>, got ${s}`);\n }\n return s;\n}\n/**\n * Validate and brand an email address. Loose RFC 5322 shape check —\n * matches HTML5 `<input type=\"email\">` semantics. For deliverability\n * checks the value still has to round-trip through the backend.\n */\nexport function toEmailAddress(s) {\n if (typeof s !== 'string' || !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(s)) {\n throw new TypeError(`EmailAddress must be local@domain.tld, got ${s}`);\n }\n return s;\n}\n/**\n * Brand a path-absolute SvelteKit route as `RoutePath`. Rejects external\n * URLs (`http(s)://...`), protocol-relative URLs (`//foo`), and empty\n * strings. The intended call site is a URL builder in `lib/<domain>/navigation`\n * — feature code never mints a `RoutePath` from a literal in the markup.\n */\nexport function toRoutePath(s) {\n if (typeof s !== 'string' || s.length === 0) {\n throw new TypeError(`RoutePath must be non-empty string, got ${s}`);\n }\n if (!s.startsWith('/') || s.startsWith('//')) {\n throw new TypeError(`RoutePath must start with single '/', got ${JSON.stringify(s)}`);\n }\n if (/^\\/\\w+:\\//.test(s)) {\n // catches `/http:/...` accidentally produced by string concat\n throw new TypeError(`RoutePath must not embed a scheme, got ${s}`);\n }\n return s;\n}\n/** Validate and brand a millisecond count as `DurationMs`. */\nexport function toDurationMs(n) {\n if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {\n throw new TypeError(`DurationMs must be non-negative integer, got ${n}`);\n }\n return n;\n}\n/** Convenience builders. Each returns a `DurationMs` so arithmetic stays branded. */\nexport const seconds = (n) => toDurationMs(Math.round(n * 1000));\nexport const minutes = (n) => toDurationMs(Math.round(n * 60000));\nexport const hours = (n) => toDurationMs(Math.round(n * 3600000));\nexport const days = (n) => toDurationMs(Math.round(n * 86400000));\n// ── API-boundary casts ───────────────────────────────────────────────────\n/**\n * Unsafe cast helpers for the API-response-transform layer.\n *\n * Use ONLY at the boundary where untyped API JSON becomes domain types\n * (server load functions, store hydration, response mappers). Feature\n * code uses the smart constructors above. These exist because validating\n * every field at every wire boundary is expensive and the API contract\n * already guarantees the shape.\n *\n * Convention: name them `as*` (matches TypeScript's `as`) so the unsafe\n * cast intent is loud at the call site. Linting rules (Phase B) restrict\n * which files may import these — boundary-only.\n */\nexport const asCents = (n) => n;\nexport const asUSD = (n) => n;\nexport const asDateOnly = (s) => s;\nexport const asIANATimezone = (s) => s;\nexport const asAssetUrl = (s) => s;\nexport const asEventId = (n) => n;\nexport const asOrderId = (n) => n;\nexport const asUserId = (n) => n;\nexport const asVenueId = (n) => n;\nexport const asE164PhoneNumber = (s) => s;\nexport const asEmailAddress = (s) => s;\nexport const asRoutePath = (s) => s;\nexport const asDurationMs = (n) => n;\n// ── Brand algebra ────────────────────────────────────────────────────────\n/**\n * Operations that preserve a numeric brand. Without these, every tax /\n * discount / fee site has to cast back to `number` to add or multiply,\n * which defeats the boundary. The plan calls this the \"algebra of\n * brand-preserving operations\" and ships it alongside the brands.\n *\n * All `Cents` ops round to integer; `USD` ops do not (decimal-dollar by\n * design). For mixed-precision math, convert at the boundary via\n * {@link centsToDollars} / {@link dollarsToCents}.\n */\nexport const addCents = (a, b) => (a + b);\nexport const subCents = (a, b) => (a - b);\nexport const mulCents = (c, n) => Math.round(c * n);\nexport const sumCents = (xs) => xs.reduce((acc, c) => addCents(acc, c), 0);\nexport const addUSD = (a, b) => (a + b);\nexport const subUSD = (a, b) => (a - b);\nexport const mulUSD = (u, n) => (u * n);\nexport const sumUSD = (xs) => xs.reduce((acc, u) => addUSD(acc, u), 0);\n/** Compare two `Cents` values; sortable like a regular comparator. */\nexport const compareCents = (a, b) => a - b;\n/** Compare two `USD` values; sortable like a regular comparator. */\nexport const compareUSD = (a, b) => a - b;\n/**\n * Add `n` calendar days to a `DateOnly`, preserving the brand. Uses UTC\n * arithmetic so DST / timezone changes don't introduce off-by-one. To\n * shift in a specific timezone, project to that zone first.\n */\nexport const addDays = (d, n) => {\n const [y, m, day] = d.split('-').map(Number);\n const probe = utcFromParts(y, m, day);\n probe.setUTCDate(probe.getUTCDate() + n);\n const yy = String(probe.getUTCFullYear()).padStart(4, '0');\n const mm = String(probe.getUTCMonth() + 1).padStart(2, '0');\n const dd = String(probe.getUTCDate()).padStart(2, '0');\n return `${yy}-${mm}-${dd}`;\n};\n/**\n * Difference in calendar days between two `DateOnly` values: `b - a`.\n * Uses UTC midnight anchors so DST never inflates / deflates the count.\n */\nexport const diffDays = (a, b) => {\n const toUtc = (s) => {\n const [y, m, d] = s.split('-').map(Number);\n return utcFromParts(y, m, d).getTime();\n };\n const ms = toUtc(b) - toUtc(a);\n return Math.round(ms / 86400000);\n};\n/**\n * Brand-preserving arithmetic on `DurationMs`. Subtraction clamps at 0\n * since negative durations don't have a domain meaning here.\n */\nexport const addDuration = (a, b) => (a + b);\nexport const subDuration = (a, b) => Math.max(0, a - b);\nexport const mulDuration = (d, n) => Math.max(0, Math.round(d * n));\nexport const compareDuration = (a, b) => a - b;\n// ── Type guards ──────────────────────────────────────────────────────────\n/** Runtime guard: is this value already a valid `Cents`? */\nexport const isCents = (x) => typeof x === 'number' && Number.isInteger(x) && Number.isFinite(x);\n/** Runtime guard: is this value a valid `USD`? */\nexport const isUSD = (x) => typeof x === 'number' && Number.isFinite(x);\n/** Runtime guard: is this value a valid `DateOnly`? */\nexport const isDateOnly = (x) => typeof x === 'string' && /^\\d{4}-\\d{2}-\\d{2}$/.test(x);\n/** Runtime guard: shape-validates an email. */\nexport const isEmailAddress = (x) => typeof x === 'string' && /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(x);\n/** Runtime guard: shape-validates a SvelteKit route path. */\nexport const isRoutePath = (x) => typeof x === 'string' &&\n x.length > 0 &&\n x.startsWith('/') &&\n !x.startsWith('//') &&\n !/^\\/\\w+:\\//.test(x);\n/** Runtime guard: is this value a valid `DurationMs`? */\nexport const isDurationMs = (x) => typeof x === 'number' && Number.isInteger(x) && Number.isFinite(x) && x >= 0;\n// ── Smart constructors for the new brands ────────────────────────────────\n/** Brand a positive integer as `PerformerId`. */\n// eslint-disable-next-line no-unused-vars -- intentionally retained (prop/slot/forward-compat or Svelte-5 reactive binding eslint cannot see)\nexport const toPerformerId = brandId('PerformerId');\n/** Brand a positive integer as `OrganizationId`. */\n// eslint-disable-next-line no-unused-vars -- intentionally retained (prop/slot/forward-compat or Svelte-5 reactive binding eslint cannot see)\nexport const toOrganizationId = brandId('OrganizationId');\n/** Brand a positive integer as `SeriesId`. */\n// eslint-disable-next-line no-unused-vars -- intentionally retained (prop/slot/forward-compat or Svelte-5 reactive binding eslint cannot see)\nexport const toSeriesId = brandId('SeriesId');\n/** Brand a positive integer as `TicketId`. */\n// eslint-disable-next-line no-unused-vars -- intentionally retained (prop/slot/forward-compat or Svelte-5 reactive binding eslint cannot see)\nexport const toTicketId = brandId('TicketId');\n/** Brand a positive integer as `SectionId`. */\n// eslint-disable-next-line no-unused-vars -- intentionally retained (prop/slot/forward-compat or Svelte-5 reactive binding eslint cannot see)\nexport const toSectionId = brandId('SectionId');\n/** Brand a positive integer as `LogId`. */\n// eslint-disable-next-line no-unused-vars -- intentionally retained (prop/slot/forward-compat or Svelte-5 reactive binding eslint cannot see)\nexport const toLogId = brandId('LogId');\n/** Brand a positive integer as `SlotId`. */\n// eslint-disable-next-line no-unused-vars -- intentionally retained (prop/slot/forward-compat or Svelte-5 reactive binding eslint cannot see)\nexport const toSlotId = brandId('SlotId');\n/** Brand a positive integer as `TransactionId`. */\n// eslint-disable-next-line no-unused-vars -- intentionally retained (prop/slot/forward-compat or Svelte-5 reactive binding eslint cannot see)\nexport const toTransactionId = brandId('TransactionId');\n/**\n * Brand a non-empty UUID/code string as `GiftCardId`. Accepts the public\n * gift-card UUID or human-readable code that MF's gift-card routes consume.\n * Rejects empty/whitespace input; intentionally permissive on format so\n * lookup-by-code paths work alongside UUID paths.\n */\nexport function toGiftCardId(s) {\n if (typeof s !== 'string' || s.trim().length === 0) {\n throw new TypeError(`GiftCardId must be a non-empty string, got ${s}`);\n }\n return s;\n}\n/**\n * Brand a non-empty string as `CookieName`. Accepts any non-empty cookie key;\n * intentionally permissive on format so the canonical `COOKIE_NAMES` registry\n * entries AND the per-event templated builders (`checkout-promocode-${id}`)\n * both mint valid names. Rejects empty / whitespace-only input.\n */\nexport function toCookieName(s) {\n if (typeof s !== 'string' || s.trim().length === 0) {\n throw new TypeError(`CookieName must be a non-empty string, got ${s}`);\n }\n return s;\n}\n/**\n * Brand a non-empty string as `RoleId`. Accepts any non-empty string\n * identifier; intentionally permissive on format so route-param values\n * (numeric DB id serialized as string) and any future slug-style\n * migration both work.\n */\nexport function toRoleId(s) {\n if (typeof s !== 'string' || s.trim().length === 0) {\n throw new TypeError(`RoleId must be a non-empty string, got ${s}`);\n }\n return s;\n}\n/**\n * Brand a non-empty string as `CollectionId`. Accepts any non-empty\n * string identifier; intentionally permissive on format so\n * route-param values (numeric DB id serialized as string) AND any\n * future UUID-style migration both work.\n */\nexport function toCollectionId(s) {\n if (typeof s !== 'string' || s.trim().length === 0) {\n throw new TypeError(`CollectionId must be a non-empty string, got ${s}`);\n }\n return s;\n}\n/**\n * Validate an ISO-8601 UTC timestamp and brand it as `UTCTimestamp`.\n *\n * Accepts the canonical wire forms: `2026-05-24T15:30:00Z`,\n * `2026-05-24T15:30:00.000Z`. Explicit `+00:00` offsets are also accepted\n * but normalized to `Z` is recommended on the producer side.\n *\n * Rejects: missing timezone (`2026-05-24T15:30:00`), non-UTC zones\n * (`2026-05-24T15:30:00-07:00`), invalid calendar dates, malformed shape.\n */\nexport function toUTCTimestamp(s) {\n if (typeof s !== 'string') {\n throw new TypeError(`UTCTimestamp must be a string, got ${typeof s}`);\n }\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(?:\\.(\\d{1,3}))?(?:Z|\\+00:00)$/.exec(s);\n if (!m) {\n throw new TypeError(`UTCTimestamp must be ISO-8601 in UTC (e.g. 2026-05-24T15:30:00Z), got ${JSON.stringify(s)}`);\n }\n const [, yy, mm, dd, hh, mi, ss] = m;\n const y = Number(yy), mo = Number(mm), d = Number(dd);\n const h = Number(hh), min = Number(mi), sec = Number(ss);\n // V8's Date.parse normalizes invalid calendar dates (2026-02-30 → 2026-03-02),\n // which would silently brand a typo'd payload. Compare round-tripped UTC\n // components against the source to reject normalization.\n const probe = utcFromParts(y, mo, d, h, min, sec);\n if (probe.getUTCFullYear() !== y ||\n probe.getUTCMonth() !== mo - 1 ||\n probe.getUTCDate() !== d ||\n probe.getUTCHours() !== h ||\n probe.getUTCMinutes() !== min ||\n probe.getUTCSeconds() !== sec) {\n throw new TypeError(`UTCTimestamp must be a real instant, got ${s}`);\n }\n return s;\n}\n/** Set of canonical role values. Updated alongside `RoleValue` whenever a role is added. */\nconst ROLE_VALUES = new Set(['admin', 'editor', 'viewer', 'guest']);\n/**\n * Validate that a string is one of the canonical `RoleValue` literals.\n * Acts as a type narrow at the wire boundary — the response mapper\n * uses this so a backend role string can flow into role-checked code.\n */\nexport function toRoleValue(s) {\n if (!ROLE_VALUES.has(s)) {\n throw new TypeError(`RoleValue must be one of ${[...ROLE_VALUES].join(' | ')}, got ${JSON.stringify(s)}`);\n }\n return s;\n}\n/**\n * Validate an ISO 3166-1 alpha-2 country code (two uppercase ASCII letters).\n * Shape-only — does not check against the real ISO list at runtime; that\n * belongs in the country-options canonical, which mints branded values\n * via this constructor.\n */\nexport function toISO3166Alpha2(s) {\n if (typeof s !== 'string' || !/^[A-Z]{2}$/.test(s)) {\n throw new TypeError(`ISO3166Alpha2 must be two uppercase ASCII letters, got ${JSON.stringify(s)}`);\n }\n return s;\n}\n/**\n * Validate an ISO 4217 currency code (three uppercase ASCII letters).\n * Shape-only — the currency-options canonical mints branded values from\n * the real ISO list.\n */\nexport function toISO4217(s) {\n if (typeof s !== 'string' || !/^[A-Z]{3}$/.test(s)) {\n throw new TypeError(`ISO4217 must be three uppercase ASCII letters, got ${JSON.stringify(s)}`);\n }\n return s;\n}\n/**\n * Brand a millisecond value as `MotionDuration`. Same shape check as\n * `toDurationMs` — the brand is narrower because of its semantic\n * (motion-only), not its representation.\n */\nexport function toMotionDuration(n) {\n if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {\n throw new TypeError(`MotionDuration must be non-negative integer, got ${n}`);\n }\n return n;\n}\n/**\n * Brand a string as `MotionEasing`. The string must be either a CSS\n * cubic-bezier expression, one of the standard named CSS easings, or one\n * of the Svelte easing function names. Validation is shape-only; the\n * canonical `EASING` token table is the source of well-formed values.\n */\nexport function toMotionEasing(s) {\n if (typeof s !== 'string' || s.length === 0) {\n throw new TypeError(`MotionEasing must be a non-empty string, got ${JSON.stringify(s)}`);\n }\n return s;\n}\n/**\n * Brand a Tailwind utility-class string as a `ClassToken` of the given\n * kind. Caller asserts the kind — this is a typing helper for the\n * canonical class-token table; feature code generally does not call it\n * directly. The runtime validates only that the string is non-empty.\n */\nexport function toClassToken(s, _kind) {\n if (typeof s !== 'string' || s.length === 0) {\n throw new TypeError(`ClassToken must be a non-empty string, got ${JSON.stringify(s)}`);\n }\n return s;\n}\n/**\n * Brand a string as a translated, user-facing message. Only Paraglide\n * message functions and canonical formatters should mint this brand;\n * the smart constructor here exists so tests can mint sample values\n * without rigging Paraglide.\n */\nexport function toUserFacingMessage(s) {\n if (typeof s !== 'string') {\n throw new TypeError(`UserFacingMessage must be a string, got ${typeof s}`);\n }\n return s;\n}\n// ── Boundary casts for the new brands ────────────────────────────────────\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asPerformerId = (n) => n;\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asOrganizationId = (n) => n;\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asSeriesId = (n) => n;\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asTicketId = (n) => n;\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asSectionId = (n) => n;\n/**\n * Unsafe boundary cast — use only at API-mapper layer. See header.\n * Accepts `string | number` because the API response carries the field as\n * `number` (integer PK) while route params deliver it as `string`. This is\n * the canonical pattern for mixed-shape entity brands: absorb the wire-shape\n * ambiguity here so everything above the boundary is union-free `CollectionId`.\n */\nexport const asCollectionId = (raw) => String(raw);\n/**\n * ambiguity here so everything above the boundary is union-free `RoleId`.\n */\nexport const asRoleId = (raw) => String(raw);\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asGiftCardId = (s) => s;\n/** Unsafe boundary cast — use only at the cookies-canonical boundary. See header. */\nexport const asCookieName = (s) => s;\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asTransactionId = (n) => n;\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asSlotId = (n) => n;\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asLogId = (n) => n;\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asUTCTimestamp = (s) => s;\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asRoleValue = (s) => s;\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asISO3166Alpha2 = (s) => s;\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asISO4217 = (s) => s;\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asMotionDuration = (n) => n;\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asMotionEasing = (s) => s;\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asClassToken = (s) => s;\n/** Unsafe boundary cast — use only at API-mapper layer. See header. */\nexport const asUserFacingMessage = (s) => s;\n// ── Type guards for the new brands ───────────────────────────────────────\n/** Runtime guard: is this value a canonical `RoleValue`? */\nexport const isRoleValue = (x) => typeof x === 'string' && ROLE_VALUES.has(x);\n/** Runtime guard: is this string a well-shaped UTC ISO timestamp that round-trips through Date? */\nexport const isUTCTimestamp = (x) => {\n if (typeof x !== 'string')\n return false;\n const m = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(?:\\.\\d{1,3})?(?:Z|\\+00:00)$/.exec(x);\n if (!m)\n return false;\n const [, yy, mm, dd, hh, mi, ss] = m;\n const y = Number(yy), mo = Number(mm), d = Number(dd);\n const h = Number(hh), min = Number(mi), sec = Number(ss);\n const probe = utcFromParts(y, mo, d, h, min, sec);\n return (probe.getUTCFullYear() === y &&\n probe.getUTCMonth() === mo - 1 &&\n probe.getUTCDate() === d &&\n probe.getUTCHours() === h &&\n probe.getUTCMinutes() === min &&\n probe.getUTCSeconds() === sec);\n};\n/** Runtime guard: is this string a well-shaped ISO 3166-1 alpha-2 code? */\nexport const isISO3166Alpha2 = (x) => typeof x === 'string' && /^[A-Z]{2}$/.test(x);\n/** Runtime guard: is this string a well-shaped ISO 4217 currency code? */\nexport const isISO4217 = (x) => typeof x === 'string' && /^[A-Z]{3}$/.test(x);\n","/**\n * Absolute instants — construct one from the wire, then order and compare it by\n * epoch milliseconds, timezone-invariantly.\n *\n * A `Date` is an absolute instant (epoch ms); `getTime()` returns that number\n * regardless of any timezone. These helpers are the named-intent home for the\n * raw `a.getTime() - b.getTime()` / `a.getTime() === b.getTime()` compares\n * scattered across the apps — sorting events, deduping timestamps, \"is A before\n * B?\" — where the answer must NOT depend on the viewer's or the venue's zone.\n *\n * They deliberately take no timezone: instant ordering is zone-invariant by\n * construction. For anything that needs *calendar* semantics (same day, same\n * month) in a specific zone, anchor first (see `datetime/local`'s\n * {@link import('./local.js').localCalendarDay}, or the venue-tz helpers in\n * `./format.ts`) and compare the anchors with these.\n *\n * Invalid operands **throw** {@link DateTimeError} (`INVALID_DATE`), matching\n * the rest of `datetime` (format/parse guard `isNaN` and throw). This is the\n * safety upgrade over the raw `.getTime()` compares they replace: a raw\n * subtraction yields `NaN`, which `Array.prototype.sort` treats like `0` and so\n * silently leaves a bad element unsorted — the canonical helper refuses instead.\n *\n * ## The contract: a wire timestamp is in one of THREE states, not two\n *\n * Every call site that reads a date off the wire must keep these apart. They are\n * different states with different correct answers, and collapsing any two of them\n * is how a missing date turns into a sold ticket.\n *\n * | State | What it is | The answer |\n * |---|---|---|\n * | **PRESENT** | a real zone-bearing instant | use it |\n * | **ABSENT** | the field was never set — `null`, `undefined`, `''`, or the backend's Go zero-time marker (the instant `0001-01-01T00:00:00Z`, in any spelling) | **empty means empty**: render a dash, do NOT sort it as a moment, and do NOT let it answer a comparison. \"Has this started?\" is **no**, not yes. It is an ordinary expected state, so the tolerant door never throws on it. |\n * | **MALFORMED** | present, but we cannot read it | **fail closed.** A sale window we cannot read means the ticket is NOT purchasable. Never guess \"open\". |\n *\n * Ask which state you are in with {@link isAbsentTimestamp} — it is the ONLY\n * definition of \"unset\" in the codebase; do not re-derive it from a year check.\n * {@link toInstantOrNull} deliberately flattens ABSENT and MALFORMED to `null`\n * (a caller that just wants an instant does not care why there isn't one), which\n * is exactly why a caller deciding whether to **fail closed** must consult\n * `isAbsentTimestamp` rather than the `null`.\n *\n * ## A wire TIMESTAMP has two spellings — not just the marker\n *\n * The backend emits a `time.Time` two ways, and **both carry real dates**:\n *\n * 1. RFC-3339, from `encoding/json` — `2024-12-30T18:00:00Z`.\n * 2. Go's `time.Time.String()`, from `fmt` — `2024-12-30 18:00:00 +0000 UTC`:\n * a space separator, a colonless numeric offset, and a trailing zone NAME.\n *\n * Both are parsed here, from parts we extract ourselves (never handed to\n * `new Date()` raw, so nothing is engine-dependent — see\n * {@link GO_STRING_PATTERN}). Learning only spelling 1 is how this module got\n * the unset marker wrong twice: first by knowing one spelling of the marker,\n * then — after fixing *that* — by still refusing a perfectly readable date that\n * happened to arrive in spelling 2, which fails a sale window CLOSED on a ticket\n * that has been on sale since 2024. The lesson is not \"the marker has two\n * spellings\". It is **\"timestamps have two spellings\"**.\n *\n * So ABSENT is decided by the **instant**, not by the string: a value is the\n * unset marker when it parses to Go's zero `time.Time` (`0001-01-01T00:00:00Z`),\n * whatever spelling carried it there. A third spelling cannot smuggle the marker\n * past us, because there is no spelling list left to be incomplete.\n *\n * ## An unset date can never answer a comparison — on EITHER door\n *\n * The tolerant door ({@link toInstantOrNull}) reports it as `null`, so it never\n * reaches {@link compareInstants}. The strict door ({@link toInstant}) **refuses\n * it** — that door is for a timestamp that is REQUIRED, and an unset value in a\n * required field is precisely the bug it exists to shout about. Handing back the\n * real year-1 instant it literally names would sort it two millennia early, ahead\n * of every real date, so every \"is this in the past?\" check would answer **yes**\n * for an event whose date nobody ever filled in.\n *\n * To order raw wire fields that may be unset, use {@link compareWireInstants} —\n * do NOT reach through the strict door with\n * `compareInstants(toInstant(a), toInstant(b))`, which is a required-field\n * recipe and will (correctly, loudly) throw on an unset row.\n * (Ruled by Peet on issue #600.)\n *\n * @module datetime/instant\n */\nimport { utcFromParts } from '../types/brands.js';\nimport { browserNow } from './local.js';\nimport { formatDateTimeForAPI } from './parse.js';\nimport { DateTimeError, DateTimeErrorCode } from './types.js';\n/**\n * The wire shapes {@link toInstant} accepts: an ISO-8601 date-time carrying a\n * zone (`Z` or a `±HH:MM` offset, milliseconds optional), or a bare\n * `YYYY-MM-DD` date.\n *\n * **The zone is mandatory whenever a time is present.** A time-bearing string\n * with no zone (`2026-06-09T20:00:00`) is not an instant at all — the language\n * resolves it against the *reader's* clock, so it names 20:00 in Los Angeles and\n * 20:00 in London, eight hours and one calendar day apart. Accepting it would\n * hand back a `Date` whose day depends on who is looking, which is the exact\n * silent viewer-timezone drift this module exists to stop. It is refused with\n * `MISSING_TIMEZONE` — see {@link ZONELESS_DATETIME_PATTERN}.\n *\n * This matches the module's own grammar rather than the language's: the wire\n * contract is UTC (`DATE_FORMATS.API` ends in `'Z'`, `formatDateTimeForAPI`\n * serializes through `toISOString()`), and the sibling brand `toUTCTimestamp`\n * in `types/brands.ts` already refuses the identical string. A zoneless\n * date-time is a `LocalDateTimeString` here — a different type, produced by\n * `parseUTCToLocal` and consumed by `<input type=\"datetime-local\">` — and\n * feeding one in is a type confusion, not a parse.\n *\n * Deliberately NOT \"anything `Date.parse` swallows\": formats outside ISO-8601\n * (`'Dec 25 2023'`, `'2023-12-5'`) are implementation-defined, which is what the\n * sibling `no-date-parse-non-iso-string` rule exists to stop. A wire timestamp\n * that does not look like a wire timestamp is a bug, not an input.\n *\n * The separator must be a literal `T` and an offset must be `±HH:MM` with the\n * colon. The near-miss forms `'2026-06-09 20:00:00Z'` (space) and\n * `'2026-06-09T20:00:00+0700'` (colonless offset) are RFC-3339-ish but are not\n * in ECMA-262's Date Time String Format, so `new Date(...)` falls through to the\n * *implementation-defined* legacy parser for them — the engine decides what they\n * mean. Engine-dependent parsing is the same defect class as viewer-dependent\n * parsing, keyed on the browser instead of the clock, so they are refused rather\n * than quietly handed to whatever V8 happens to do this year. Nothing on the\n * live wire produces them (the backend serializes Go RFC-3339 with `T` and a\n * colon'd offset).\n */\nconst ISO_INSTANT_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})(?:T\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,9})?)?(?:Z|[+-]\\d{2}:\\d{2}))?$/;\n/**\n * An ISO-8601-shaped date-time that carries a time but NO zone — well-formed\n * enough to be a typo'd wire value rather than garbage, so it earns a named\n * `MISSING_TIMEZONE` refusal that says what to fix instead of a generic \"not\n * ISO-8601\".\n *\n * @internal\n */\nconst ZONELESS_DATETIME_PATTERN = /^\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,9})?)?$/;\n/**\n * Whether `YYYY-MM-DD` parts name a day that exists.\n *\n * `Date.parse` does NOT reject an unreal calendar date — V8 quietly normalizes\n * `'2026-02-30'` into March 2nd (the same trap `toUTCTimestamp` guards in\n * `types/brands.ts`). A timestamp that silently becomes a *different day* is the\n * exact failure this module exists to stop, so the parts are round-tripped\n * through `Date.UTC` and refused when they move.\n *\n * `Date.UTC` maps a year of 0–99 to 1900 + year (ECMA-262 legacy two-digit-year\n * behavior), so the naive round-trip reads `0001-01-01` back as 1901 and refuses\n * every year from 0001 to 0099 as \"not a real calendar date\". They are real, and\n * one of them matters: `0001-01-01T00:00:00Z` is Go's zero `time.Time` — the\n * backend's \"unset\" sentinel — which arrives on the wire whenever a date field\n * was never filled in (see `primitives/Time/types.ts`). The year is re-set\n * explicitly so the probe lands where it was asked to.\n *\n * @internal\n */\nfunction isRealCalendarDate(year, month, day) {\n const probe = utcFromParts(year, month, day);\n return (probe.getUTCFullYear() === year &&\n probe.getUTCMonth() === month - 1 &&\n probe.getUTCDate() === day);\n}\n/**\n * The **second** wire spelling of a timestamp: Go's `time.Time.String()` layout,\n * `2006-01-02 15:04:05.999999999 -0700 MST` — a space separator, a colonless\n * numeric offset, and an optional trailing zone NAME.\n *\n * It reaches us whenever the backend `fmt`-prints a `time.Time` into a string\n * field instead of marshalling it (e.g. `lastBookedDate` in\n * `utils/data/table/performersTable.json` — 27 unset markers **and one live date**,\n * `2024-12-30 18:00:00 +0000 UTC`).\n *\n * It is parsed rather than refused, and that is a reversal worth being explicit\n * about. The `T`-separator rule above refuses `'2026-06-09 20:00:00Z'` because\n * handing a non-ECMA-262 string to `new Date()` drops it into the engine's\n * *implementation-defined* legacy parser. That argument is about **who does the\n * parsing**, not about the separator: here the parts are captured by this anchored\n * pattern and reassembled into RFC-3339 by {@link toWireParts}, so `new Date()`\n * only ever sees a string this module built. Nothing is left to the engine.\n *\n * The zone discipline is untouched — the numeric offset is **required**. A\n * space-separated time with no offset (`'2026-06-09 20:00:00'`) still falls\n * through to {@link ZONELESS_DATETIME_PATTERN} and is still refused with\n * `MISSING_TIMEZONE`. The rule was never \"the separator must be a T\"; it is\n * **\"a time must carry a zone, and we must do the parsing ourselves.\"** Both\n * spellings satisfy it.\n *\n * The trailing zone name is decorative: Go prints the numeric offset AND the\n * abbreviation, and the offset is the authority. A monotonic-clock suffix\n * (`m=+0.000000001`), which Go appends to a `time.Now()` value, is tolerated and\n * discarded for the same reason.\n *\n * @internal\n */\nconst GO_STRING_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2})(?:\\.(\\d{1,9}))? ?([+-])(\\d{2}):?(\\d{2})(?: [A-Za-z][A-Za-z0-9_/+-]*)?(?: m=[+-][\\d.]+)?$/;\n/**\n * Go's zero `time.Time{}` as an epoch instant — `0001-01-01T00:00:00Z`.\n *\n * The backend emits this for a timestamp that was **never set**, instead of\n * omitting the field or sending `null`. It is an absent value wearing a parseable\n * date's clothes, and it is all over live wire data.\n *\n * Absence is keyed on this **instant**, not on a list of strings. Every spelling\n * this module can parse that lands here means \"never set\" — which is why a *third*\n * spelling of the marker cannot slip past {@link isAbsentTimestamp}: there is no\n * spelling list left to be incomplete. (The previous fix kept two literal regexes,\n * one per known spelling. That is a list, and a list is a thing you forget to add\n * to — it had already been forgotten once.)\n *\n * No real date can collide with it: nobody books a show in January of the year 1.\n *\n * @internal\n */\nconst GO_ZERO_INSTANT_MS = utcFromParts(1, 1, 1).getTime();\n/**\n * The `YYYY-MM-DD` parts of a wire timestamp plus the RFC-3339 string to parse it\n * from — `null` when the value is in neither wire spelling.\n *\n * Spelling 1 (RFC-3339) is handed straight through: it is already in ECMA-262's\n * Date Time String Format, so `new Date()` parses it by the spec. Spelling 2 (Go's\n * `String()`) is REASSEMBLED from its captured parts into spelling 1 — never\n * passed to `new Date()` as-is — so the engine's legacy parser is never reached.\n * The nanosecond fraction Go can print is truncated to milliseconds, which is all\n * a `Date` carries.\n *\n * @internal\n */\nfunction toWireParts(value) {\n const iso = ISO_INSTANT_PATTERN.exec(value);\n if (iso) {\n const [, year, month, day] = iso;\n return { iso: value, year: +year, month: +month, day: +day };\n }\n const go = GO_STRING_PATTERN.exec(value);\n if (go) {\n const [, year, month, day, hh, mm, ss, frac, sign, offH, offM] = go;\n const ms = frac ? `.${frac.slice(0, 3).padEnd(3, '0')}` : '';\n return {\n iso: `${year}-${month}-${day}T${hh}:${mm}:${ss}${ms}${sign}${offH}:${offM}`,\n year: +year,\n month: +month,\n day: +day,\n };\n }\n return null;\n}\n/**\n * Parse a wire timestamp without throwing — the shared engine under the strict\n * door, the tolerant door, and the absent check.\n *\n * Deliberately knows nothing about the unset marker: it answers \"what instant, if\n * any, does this value name?\", and the marker names a perfectly real one. Keeping\n * the marker's *meaning* out of the parser is what lets {@link isAbsentTimestamp}\n * be defined on the parsed instant without either of them calling the other in a\n * circle.\n *\n * It accepts **every shape the module's doors accept** — the two string spellings\n * AND epoch milliseconds. That is not incidental: it is the whole mechanism.\n * Absence is keyed on the instant a value NAMES, so a value whose shape the parser\n * cannot see is a value the absent check cannot judge. This function used to take\n * a `string`, which silently made \"is this the unset marker?\" unanswerable for a\n * number — and the number door was the one left open (see {@link isGoZeroTime}).\n *\n * Leading/trailing whitespace is tolerated here (a wire field can arrive padded);\n * the strict {@link toInstant} keeps its exact-shape contract by parsing the raw\n * value.\n *\n * @internal\n */\nfunction parseWireInstantQuietly(value) {\n if (typeof value === 'number') {\n if (!Number.isFinite(value))\n return null;\n const fromEpoch = new Date(value);\n return Number.isNaN(fromEpoch.getTime()) ? null : fromEpoch;\n }\n if (typeof value !== 'string')\n return null;\n const parts = toWireParts(value.trim());\n if (!parts)\n return null;\n if (!isRealCalendarDate(parts.year, parts.month, parts.day))\n return null;\n const parsed = new Date(parts.iso);\n return Number.isNaN(parsed.getTime()) ? null : parsed;\n}\n/**\n * Whether a wire value is Go's zero-time sentinel — i.e. the field was never set.\n *\n * Asks the PARSER, not a pattern, and not the value's TYPE: any value of any\n * accepted shape that resolves to {@link GO_ZERO_INSTANT_MS} is the marker.\n *\n * ## The number door — why this no longer looks at `typeof`\n *\n * This function used to open with `if (typeof value !== 'string') return false`.\n * That line is the same bug as the pattern list it replaced, one level up: the\n * list keyed absence on the TEXT, and the `typeof` guard keyed it on the SHAPE.\n * Both ask \"how was this written?\" when the only sound question is \"what instant\n * does this name?\".\n *\n * The cost was concrete. `isAbsentTimestamp`, `toInstant` and\n * {@link toInstantOrNull} all accept `ISODateString | number` — epoch ms is a\n * documented door. An unset date arriving as `-62135596800000` is the year-1\n * instant written as a number, but the guard bounced it before the comparison\n * ever ran, so it was reported PRESENT, parsed to year 1, and handed to the\n * comparison that decides whether a show has already happened. That is the exact\n * bug this module exists to kill, walking in through the one door nobody checked.\n *\n * Note it compares the PARSED instant, not the raw number. Keying on\n * `value === GO_ZERO_INSTANT_MS` would be a third spelling of the same mistake:\n * `new Date()` truncates a fractional epoch toward zero, so `-62135596800000.4`\n * is a different NUMBER naming the identical INSTANT, and a literal-equality\n * check would wave it through.\n *\n * @internal\n */\nfunction isGoZeroTime(value) {\n const at = parseWireInstantQuietly(value);\n return at !== null && at.getTime() === GO_ZERO_INSTANT_MS;\n}\n/**\n * Refuse the unset marker at a REQUIRED-timestamp door.\n *\n * One definition, called from every branch of {@link toInstant}. It exists as a\n * function because the check used to live inline at the bottom of the string\n * path, which meant the number path — which returns earlier — simply never ran\n * it. A guard that each door has to remember to call is a guard one door forgets.\n *\n * @param parsed - The instant the value resolved to.\n * @param value - The raw value, for the error message.\n * @internal\n */\nfunction assertNotUnsetMarker(parsed, value) {\n if (parsed.getTime() !== GO_ZERO_INSTANT_MS)\n return;\n throw new DateTimeError(`toInstant: ${JSON.stringify(value)} is the backend's UNSET marker (Go's zero time.Time), not a moment — ` +\n `the field was never filled in. toInstant is the REQUIRED-timestamp door, so an unset value here is a bug: ` +\n `returning the year-1 instant it names would sort this row ahead of every real date and answer \"yes\" to ` +\n `\"has this started?\". If the field is allowed to be unset, read it with toInstantOrNull (null), ask ` +\n `isAbsentTimestamp why it is missing, and order it with compareWireInstants.`, DateTimeErrorCode.INVALID_ISO_STRING, { value });\n}\n/**\n * Whether a raw wire timestamp is **absent** — the field carries no date at all.\n *\n * True for `null`, `undefined`, `''`, and Go's zero-time sentinel — the instant\n * `0001-01-01T00:00:00Z`, **in any spelling that parses to it** (see\n * {@link GO_ZERO_INSTANT_MS}). It asks the parser what instant the string names\n * and compares that; it does not match the string against a list of known\n * spellings, because a list is a thing you forget to add to.\n *\n * This exists to keep **absent** and **malformed** apart, which\n * {@link toInstantOrNull} deliberately flattens: it answers `null` to both, because\n * a caller that just wants an instant does not care why there isn't one. A caller\n * deciding whether to **fail closed** cares enormously. \"No sale window\" and \"a\n * sale window we could not read\" are opposite instructions — the first means sell\n * without restriction, the second means do not sell — and a guard that cannot tell\n * them apart either stops selling tickets whose window was never set, or sells\n * tickets whose window is corrupt. Both are money bugs, in opposite directions.\n *\n * @param value - A raw wire timestamp field, in any state.\n * @returns `true` when the field carries no date at all.\n *\n * @example\n * // Fail closed on a corrupt window, but not on an absent one:\n * const raw = ticket.salesBegin;\n * const at = toInstantOrNull(raw);\n * if (!isAbsentTimestamp(raw) && !at) return false; // present but unreadable -> do not sell\n * if (at && compareInstants(at, browserNow()) > 0) return false; // window not open yet\n */\nexport function isAbsentTimestamp(value) {\n if (value === null || value === undefined)\n return true;\n if (typeof value === 'string' && value.trim() === '')\n return true;\n return isGoZeroTime(value);\n}\n/**\n * Resolve a wire fallback chain to its first **present** timestamp — the safe\n * replacement for `a || b || c` / `a ?? b ?? c` over date fields.\n *\n * Wire rows spell one concept several ways (`effectiveSalesBegin`, `salesBegin`,\n * `saleStartDate`, …) and call sites collapse them with `||` or `??`. **Both\n * operators are wrong here**, because the backend's unset marker is a non-empty\n * STRING: it is truthy, and it is not nullish. So an unset first field WINS the\n * chain and **shadows the real value behind it** — the chain resolves to \"no\n * date\", and every guard downstream sees an absent window.\n *\n * That is a live oversell, not a theoretical one: a ticket with\n * `effectiveSalesBegin` unset and a real `salesBegin` of 2099 resolves to *no\n * sale window*, so it reads as **on sale today** — a full 73 years early. The\n * same ticket with the effective field simply missing correctly reads\n * `scheduled`. Presence, not truthiness, is the question being asked.\n *\n * Returns `undefined` when every candidate is absent — i.e. the field genuinely\n * was never set anywhere in the chain, which downstream reads as \"no window, no\n * limit\". A MALFORMED value is **present** and is returned, so fail-closed guards\n * still see it and still refuse.\n *\n * @param values - The chain, in precedence order (most specific first).\n * @returns The first value that is not {@link isAbsentTimestamp}, else `undefined`.\n *\n * @example\n * // WRONG — an unset effectiveSalesBegin shadows the real window:\n * const begin = ticket.effectiveSalesBegin || ticket.salesBegin;\n * // RIGHT:\n * const begin = firstPresentTimestamp(ticket.effectiveSalesBegin, ticket.salesBegin);\n */\nexport function firstPresentTimestamp(...values) {\n return values.find(value => !isAbsentTimestamp(value));\n}\n/**\n * Asserts a value is a valid `Date` (a real instant, not `Invalid Date`).\n *\n * Shared with `./calendar.ts`, which validates its wall-day operands the same\n * way — one definition of \"this is a usable instant\" for the whole module.\n *\n * @throws {DateTimeError} `INVALID_DATE` if not a `Date` or its time is `NaN`.\n * @internal\n */\nexport function assertValidInstant(value, label) {\n if (!(value instanceof Date) || Number.isNaN(value.getTime())) {\n throw new DateTimeError(`${label} must be a valid Date`, DateTimeErrorCode.INVALID_DATE, { value });\n }\n}\n/**\n * Compares two instants by absolute epoch time — a zone-invariant comparator\n * for `Array.prototype.sort`.\n *\n * Returns a negative number if `a` is earlier than `b`, `0` if they are the same\n * instant, and a positive number if `a` is later. The result is the epoch-ms\n * difference (`a - b`), matching the raw `.getTime()` subtraction it replaces —\n * but it refuses invalid dates instead of returning a sort-corrupting `NaN`.\n *\n * @param a - First instant.\n * @param b - Second instant.\n * @returns `< 0` if `a` is before `b`, `0` if equal, `> 0` if `a` is after `b`.\n * @throws {DateTimeError} `INVALID_DATE` if either operand is an invalid `Date`.\n *\n * @example\n * events.sort((x, y) => compareInstants(x.start, y.start)); // earliest first\n * events.sort((x, y) => compareInstants(y.start, x.start)); // latest first\n */\nexport function compareInstants(a, b) {\n assertValidInstant(a, 'compareInstants(a)');\n assertValidInstant(b, 'compareInstants(b)');\n return a.getTime() - b.getTime();\n}\n/**\n * Whether two instants are the same absolute moment — a zone-invariant equality\n * check.\n *\n * `true` iff `a` and `b` carry the identical epoch time, replacing raw\n * `a.getTime() === b.getTime()`. Two `Date` objects are equal here when they\n * point at the same instant even if constructed differently (e.g. a UTC ISO\n * string vs. an epoch number); object identity (`a === b`) is NOT used.\n *\n * @param a - First instant.\n * @param b - Second instant.\n * @returns `true` when both represent the same epoch instant.\n * @throws {DateTimeError} `INVALID_DATE` if either operand is an invalid `Date`.\n *\n * @example\n * epochEquals(new Date('2026-06-09T00:00:00Z'), new Date(1780963200000)) // true\n */\nexport function epochEquals(a, b) {\n assertValidInstant(a, 'epochEquals(a)');\n assertValidInstant(b, 'epochEquals(b)');\n return a.getTime() === b.getTime();\n}\n/**\n * Turns a UTC wire timestamp (or epoch milliseconds) into a `Date` instant —\n * the validated, named replacement for a bare `new Date(wireString)`.\n *\n * This is the symmetric inverse of {@link import('./parse.js').formatDateTimeForAPI}\n * and the front door for every compare / sort / interval site: parse here, then\n * order with {@link compareInstants} or {@link epochEquals}.\n *\n * A raw `new Date('garbage')` yields `Invalid Date`, whose `getTime()` is `NaN`\n * — and `NaN` in a comparator is treated as `0`, so the bad element silently\n * stays where it is and the list *looks* sorted. This refuses instead.\n *\n * It does **not** interpret a timezone: the result is an absolute instant. To\n * ask what calendar day it falls on somewhere, hand it to the venue-tz helpers\n * in `./format.ts`.\n *\n * **A time requires a zone.** `'2026-06-09T20:00:00'` — a time with no `Z` and\n * no offset — is REFUSED (`MISSING_TIMEZONE`), because it is not an instant: it\n * resolves on the viewer's clock and lands on a different calendar day in Los\n * Angeles than in London. A bare `'2026-06-09'` IS accepted and resolves to\n * **UTC midnight** (`2026-06-09T00:00:00Z`), per the language — it carries no\n * time to be wrong about and reads the same for every viewer. That asymmetry is\n * deliberate; for a wall day on the *browser's* calendar use\n * `datetime/calendar`'s `localDayFromYMD` instead.\n *\n * **The unset marker is REFUSED here.** This is the door for a timestamp that is\n * REQUIRED, and the backend's zero `time.Time` means the field was never filled\n * in — which is exactly the bug this door exists to shout about. Handing back the\n * real year-1 instant it literally names would sort it ahead of every real date\n * and answer \"yes\" to every \"has this started?\". A wire field that is ALLOWED to\n * be unset is not a required field: read it with {@link toInstantOrNull}, ask\n * {@link isAbsentTimestamp} why it is missing, and order it with\n * {@link compareWireInstants}.\n *\n * @param value - A zone-bearing UTC ISO-8601 wire timestamp, Go's\n * `time.Time.String()` spelling of one, a bare `YYYY-MM-DD` (read as UTC\n * midnight), or epoch milliseconds.\n * @returns The instant it names.\n * @throws {DateTimeError} `MISSING_TIMEZONE` for a time-bearing string with no\n * zone; `INVALID_ISO_STRING` for a string that is absent, is the unset marker,\n * is in neither wire spelling, or names an unreal date; `INVALID_DATE` for a\n * non-finite number or any other type.\n *\n * @example\n * toInstant(1780963200000) // epoch ms\n * toInstant('2026-06-09T19:00:00Z') // wire ISO\n * toInstant('2026-06-09 19:00:00 +0000 UTC') // Go's String() spelling — same instant\n * toInstant('2026-06-09T12:00:00-07:00') // explicit offset\n * toInstant('2026-06-09') // UTC midnight — no time, no ambiguity\n * toInstant('2026-06-09T20:00:00') // throws — a time with no zone is viewer-dependent\n * toInstant('') // throws — an absent timestamp is a bug, not \"now\"\n * toInstant('0001-01-01T00:00:00Z') // throws — the unset marker is not a moment\n * // Sorting REQUIRED dates. For raw wire fields use compareWireInstants instead:\n * events.sort((x, y) => compareInstants(toInstant(x.start), toInstant(y.start)))\n */\nexport function toInstant(value) {\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n throw new DateTimeError(`toInstant: epoch milliseconds must be a finite number, got ${value}`, DateTimeErrorCode.INVALID_DATE, { value });\n }\n const fromEpoch = new Date(value);\n assertValidInstant(fromEpoch, 'toInstant(value)');\n // The number door gets the SAME marker check as the string doors. It used to\n // return here, one line short of it — so the unset instant threw when it was\n // spelled `'0001-01-01T00:00:00Z'` and was handed back as a real moment when\n // it was spelled `-62135596800000`. Same instant, same required field, two\n // different answers depending on how the wire happened to type it.\n assertNotUnsetMarker(fromEpoch, value);\n return fromEpoch;\n }\n if (typeof value !== 'string' || value.trim() === '') {\n throw new DateTimeError(`toInstant: a UTC ISO timestamp or epoch ms is required, got ${JSON.stringify(value)}`, typeof value === 'string'\n ? DateTimeErrorCode.INVALID_ISO_STRING\n : DateTimeErrorCode.INVALID_DATE, { value });\n }\n const parts = toWireParts(value);\n if (!parts) {\n if (ZONELESS_DATETIME_PATTERN.test(value)) {\n throw new DateTimeError(`toInstant: ${JSON.stringify(value)} carries a time but no timezone, so it is not an instant — ` +\n `it would resolve on the viewer's clock and land on a different calendar day for a viewer in ` +\n `Los Angeles than one in London. Send a UTC wire timestamp (2026-06-09T20:00:00Z) or an explicit ` +\n `offset (2026-06-09T13:00:00-07:00). If this is a local wall-clock value, it belongs in ` +\n `datetime/local or parseLocalToUTC(value, timezone), not here.`, DateTimeErrorCode.MISSING_TIMEZONE, { value });\n }\n throw new DateTimeError(`toInstant: not an ISO-8601 timestamp: ${JSON.stringify(value)}`, DateTimeErrorCode.INVALID_ISO_STRING, { value });\n }\n const { year, month, day } = parts;\n if (!isRealCalendarDate(year, month, day)) {\n throw new DateTimeError(`toInstant: ${year}-${month}-${day} is not a real calendar date: ${JSON.stringify(value)}`, DateTimeErrorCode.INVALID_ISO_STRING, { value });\n }\n const parsed = new Date(parts.iso);\n if (Number.isNaN(parsed.getTime())) {\n throw new DateTimeError(`toInstant: ISO-8601 shaped but not a real instant: ${JSON.stringify(value)}`, DateTimeErrorCode.INVALID_ISO_STRING, { value });\n }\n assertNotUnsetMarker(parsed, value);\n return parsed;\n}\n/**\n * The **tolerant** half of the pair: an unusable timestamp becomes `null`\n * instead of throwing.\n *\n * `null` for an absent field (`null` / `undefined` / `''` / Go's zero-time\n * sentinel) AND for one that cannot be parsed. It is the honest replacement for\n * `new Date(maybeMissing)` at a data boundary — a transformer, a filter, a sort\n * over a list of wire rows — where one bad row must not take the whole page down\n * with it. Today those sites lean on `new Date(undefined)` silently becoming\n * `Invalid Date` and every comparison against it coming out `false`; this returns\n * an honest `null` that the type system forces the caller to handle.\n *\n * Tolerant means tolerant in the SAFE direction: it returns `null` for anything\n * {@link toInstant} refuses — including a time-bearing string with no zone. It\n * never hands back a drifted instant whose calendar day depends on the viewer; an\n * unusable value is skipped, not guessed.\n *\n * It agrees with {@link toInstant} about the unset marker — both refuse to call it\n * a moment — and differs only in HOW: strict throws (an unset value in a required\n * field is a bug), tolerant answers `null` (an unset value in an optional field is\n * an ordinary Tuesday). Neither ever hands back the year-1 instant.\n *\n * Reach for {@link toInstant} instead when the timestamp is REQUIRED and a bad\n * one is a bug you want to hear about immediately (it throws).\n *\n * @param value - A UTC ISO-8601 wire timestamp, Go's `time.Time.String()`\n * spelling of one, epoch ms, or an absent value.\n * @returns The instant, or `null` when the field is absent or unusable.\n *\n * @example\n * const salesBegin = toInstantOrNull(raw.SaleStartDate);\n * if (salesBegin && compareInstants(browserNow(), salesBegin) < 0) return 'not-on-sale-yet';\n * toInstantOrNull('0001-01-01T00:00:00Z') // null — Go's \"never set\", not year 1\n * toInstantOrNull('2024-12-30 18:00:00 +0000 UTC') // a real instant — the OTHER wire spelling\n */\nexport function toInstantOrNull(value) {\n if (value === null || value === undefined)\n return null;\n if (typeof value === 'string' && value.trim() === '')\n return null;\n // The unset marker is ABSENT, not a moment (ruled on #600). Empty means empty.\n if (isGoZeroTime(value))\n return null;\n try {\n return toInstant(value);\n }\n catch {\n return null;\n }\n}\n/**\n * Where a row with no readable instant is parked in a wire sort.\n *\n * **DECISION (#600) — an undated row sorts LAST, in both directions.** It is the\n * one-line flip for this behavior: `+1` puts undated rows at the bottom, `-1` at\n * the top. Bottom, because the top of a chronological list is where the most\n * imminent thing lives, and a date nobody ever filled in is not the most imminent\n * thing — reading it as such is precisely the silent wrong answer the three-state\n * contract exists to stop.\n *\n * @internal\n */\nconst UNDATED_SORTS = 1;\n/**\n * Order two RAW WIRE timestamps — the `Array.prototype.sort` comparator for a\n * column that is allowed to be unset.\n *\n * This is the sort recipe for wire data, and it takes the raw fields rather than\n * `Date`s because a raw field is what a sort callback actually holds. The\n * required-field recipe — `compareInstants(toInstant(a), toInstant(b))` — is the\n * WRONG tool here: it throws the moment a row's date was never filled in, which on\n * a list page is a crash. And before {@link toInstant} learned to refuse the unset\n * marker, it was worse than a crash: it silently sorted that row to the TOP, two\n * millennia early, ahead of every real event.\n *\n * A row with no readable instant — ABSENT (never set) or MALFORMED (present but\n * unreadable) — has no place in a chronological order, so it is parked at the END,\n * in source order (`Array.prototype.sort` is stable). See {@link UNDATED_SORTS}.\n *\n * **Presence is settled before direction, which is why `direction` is an argument\n * rather than something you do to the result.** Negating this comparator, or\n * swapping its arguments, to get \"latest first\" would drag every undated row to\n * the top — re-creating the bug in the act of reversing the list. Direction only\n * ever reorders rows that HAVE an instant.\n *\n * @param a - A raw wire timestamp field, in any state.\n * @param b - A raw wire timestamp field, in any state.\n * @param direction - `'asc'` (earliest first, the default) or `'desc'` (latest\n * first). Undated rows sort last either way.\n * @returns `< 0` if `a` sorts first, `0` if the two are interchangeable, `> 0` if\n * `b` sorts first.\n *\n * @example\n * // Earliest first; rows whose date was never set land at the bottom.\n * rows.sort((a, b) => compareWireInstants(a.startDateTime, b.startDateTime));\n * // Latest first; undated rows STILL at the bottom.\n * rows.sort((a, b) => compareWireInstants(a.startDateTime, b.startDateTime, 'desc'));\n */\nexport function compareWireInstants(a, b, direction = 'asc') {\n const at = toInstantOrNull(a);\n const bt = toInstantOrNull(b);\n // Presence first, and independently of direction — an undated row is not\n // \"very early\" or \"very late\", it is out of the running.\n if (at === null)\n return bt === null ? 0 : UNDATED_SORTS;\n if (bt === null)\n return -UNDATED_SORTS;\n return direction === 'desc'\n ? compareInstants(bt, at)\n : compareInstants(at, bt);\n}\n/**\n * Right now, as a UTC wire string — for timestamps that are **sent or stored**,\n * never displayed: telemetry payloads, log records, ICS `DTSTAMP`, build stamps.\n *\n * The named home for `new Date().toISOString()`. Reads the browser clock (via\n * {@link import('./local.js').browserNow}) and serializes through\n * {@link import('./parse.js').formatDateTimeForAPI}, so it carries the same\n * validation as every other wire timestamp the app produces.\n *\n * For anything a user *sees*, this is the wrong function — a rendered time\n * belongs to a venue's timezone and goes through `<Time>` / the `format*` family.\n *\n * @returns The current instant as a UTC ISO-8601 string.\n *\n * @example\n * logger.info('checkout.started', { at: nowISOString() });\n */\nexport function nowISOString() {\n return formatDateTimeForAPI(browserNow());\n}\n","/**\r\n * Shared helper utilities for JSON-LD builders.\r\n *\r\n * All functions are pure (no DOM, no Svelte, no side effects).\r\n * Extracted verbatim from EventStructuredData.svelte.\r\n *\r\n * @raw-intl-datetimeformat-escape: SEO/JSON-LD implementation. formatDateTimeWithOffset\r\n * must emit Google-required ISO-8601-with-tz-offset (\"2025-07-21T19:00:00-05:00\"), which\r\n * requires Intl.DateTimeFormat.formatToParts(...) with timeZoneName:'shortOffset' to read\r\n * the per-timezone UTC offset. The SC formatDate/DATE_FORMATS canonical produces\r\n * human-readable display strings, not parts-based offset extraction — it cannot express this.\r\n */\r\n\r\nimport { getDefaultTimezone } from '@getmicdrop/svelte-components/utils/timezones';\r\nimport {\r\n browserNow,\r\n compareInstants,\r\n toInstantOrNull,\r\n} from '@getmicdrop/svelte-components/datetime';\r\nimport type { EventTicketInput, EventPerformerInput } from './types';\r\n\r\n/**\r\n * Escape a JSON string for safe embedding inside an inline\r\n * <script type=\"application/ld+json\"> element.\r\n *\r\n * JSON.stringify does NOT escape `<`, `>` or `&`, so a field value containing\r\n * `</script><script>...` would break out of the script element and execute\r\n * (stored XSS). Replacing those characters with their unicode escape sequences\r\n * keeps the output valid JSON (parsers decode `<` back to `<`) while making\r\n * it impossible to terminate the surrounding <script> tag.\r\n *\r\n * This is the standard JSON-LD-in-<script> hardening; the sibling\r\n * HostSeoController avoids the issue entirely by using element.textContent.\r\n */\r\nexport function escapeJsonLdForScript(json: string): string {\r\n return json\r\n .replace(/</g, '\\\\u003c')\r\n .replace(/>/g, '\\\\u003e')\r\n .replace(/&/g, '\\\\u0026');\r\n}\r\n\r\n/**\r\n * Format datetime to ISO-8601 with timezone offset.\r\n * Google requires: \"2025-07-21T19:00:00-05:00\"\r\n *\r\n * Extracted from EventStructuredData.svelte lines 25-68.\r\n */\r\nexport function formatDateTimeWithOffset(\r\n isoString: string | null | undefined,\r\n tz: string\r\n): string | null {\r\n if (!isoString) return null;\r\n\r\n const date = new Date(isoString); // @datetime-escape: parse UTC instant; rendered in an explicit tz\r\n if (isNaN(date.getTime())) return null;\r\n\r\n // Get the offset in the target timezone\r\n const offsetFormatOptions: Intl.DateTimeFormatOptions = {\r\n year: 'numeric',\r\n month: '2-digit',\r\n day: '2-digit',\r\n hour: '2-digit',\r\n minute: '2-digit',\r\n second: '2-digit',\r\n hour12: false,\r\n timeZoneName: 'shortOffset',\r\n };\r\n\r\n let formatter: Intl.DateTimeFormat;\r\n try {\r\n formatter = new Intl.DateTimeFormat('en-US', { ...offsetFormatOptions, timeZone: tz });\r\n } catch {\r\n // An invalid IANA timezone makes Intl.DateTimeFormat throw a RangeError,\r\n // which would otherwise escape the JSON-LD builders unguarded. Fall back\r\n // to the canonical default timezone — the same invalid-tz fallback the\r\n // rest of the repo uses ($lib/utils/datetime.js formatInTz).\r\n formatter = new Intl.DateTimeFormat('en-US', {\r\n ...offsetFormatOptions,\r\n timeZone: getDefaultTimezone(),\r\n });\r\n }\r\n\r\n const parts = formatter.formatToParts(date);\r\n const get = (type: string) => parts.find(p => p.type === type)?.value || '';\r\n\r\n const year = get('year');\r\n const month = get('month');\r\n const day = get('day');\r\n const hour = get('hour');\r\n const minute = get('minute');\r\n const second = get('second');\r\n const tzOffset = get('timeZoneName'); // e.g., \"GMT-5\" or \"GMT+2\"\r\n\r\n // Convert \"GMT-5\" to \"-05:00\" format\r\n let offset = '+00:00';\r\n if (tzOffset) {\r\n const match = tzOffset.match(/GMT([+-])(\\d+)(?::(\\d+))?/);\r\n if (match) {\r\n const sign = match[1];\r\n const hours = match[2].padStart(2, '0');\r\n const mins = match[3] || '00';\r\n offset = `${sign}${hours}:${mins}`;\r\n }\r\n }\r\n\r\n return `${year}-${month}-${day}T${hour}:${minute}:${second}${offset}`;\r\n}\r\n\r\n/**\r\n * Parse address string into PostalAddress components.\r\n * Input: \"7702 Santa Monica Blvd, West Hollywood, CA 90046\"\r\n * Output: { @type, streetAddress, addressLocality, addressRegion, postalCode, addressCountry }\r\n *\r\n * Extracted from EventStructuredData.svelte lines 75-111.\r\n */\r\nexport function parseAddressToPostal(\r\n addressStr: string | null | undefined\r\n): Record<string, string> | null {\r\n if (!addressStr) return null;\r\n\r\n const parts = addressStr.split(',').map(p => p.trim());\r\n\r\n if (parts.length < 2) {\r\n return {\r\n '@type': 'PostalAddress',\r\n streetAddress: addressStr,\r\n addressCountry: 'US',\r\n };\r\n }\r\n\r\n // Check if last part is \"State ZIP\" (e.g., \"CA 90046\")\r\n const lastPart = parts[parts.length - 1];\r\n const stateZipMatch = lastPart.match(/^([A-Z]{2})\\s+(\\d{5}(?:-\\d{4})?)$/);\r\n\r\n if (stateZipMatch && parts.length >= 3) {\r\n // Standard US address format\r\n return {\r\n '@type': 'PostalAddress',\r\n streetAddress: parts.slice(0, -2).join(', '),\r\n addressLocality: parts[parts.length - 2],\r\n addressRegion: stateZipMatch[1],\r\n postalCode: stateZipMatch[2],\r\n addressCountry: 'US',\r\n };\r\n }\r\n\r\n // Fallback for non-standard formats\r\n return {\r\n '@type': 'PostalAddress',\r\n streetAddress: parts[0],\r\n addressLocality: parts.slice(1).join(', '),\r\n addressCountry: 'US',\r\n };\r\n}\r\n\r\n/**\r\n * Structured postal-address parts, as the public resolve/series payloads now\r\n * expose them: `address` is the STREET line, with city/state/postalCode/country\r\n * as discrete fields (PublicVenueInfo / SeriesVenueDTO).\r\n */\r\nexport interface PostalAddressParts {\r\n address?: string | null; // street line\r\n city?: string | null;\r\n state?: string | null;\r\n postalCode?: string | null;\r\n country?: string | null;\r\n}\r\n\r\n/**\r\n * Build a schema.org PostalAddress, preferring discrete structured fields when\r\n * any locality signal (city / state / postalCode) is present, and otherwise\r\n * falling back to parsing a single comma-separated string (the street `address`\r\n * field if it carries a full address, else `googleLocationNameCache`).\r\n *\r\n * Discrete fields are more reliable than string-splitting — Google requires a\r\n * well-formed PostalAddress for Event rich-result eligibility (online-only\r\n * events lost eligibility June 2025).\r\n */\r\nexport function buildPostalAddress(\r\n parts: PostalAddressParts | null | undefined,\r\n fallback?: string | null\r\n): Record<string, string> | null {\r\n if (parts && (parts.city || parts.state || parts.postalCode)) {\r\n const out: Record<string, string> = { '@type': 'PostalAddress' };\r\n if (parts.address) out.streetAddress = parts.address;\r\n if (parts.city) out.addressLocality = parts.city;\r\n if (parts.state) out.addressRegion = parts.state;\r\n if (parts.postalCode) out.postalCode = parts.postalCode;\r\n out.addressCountry = parts.country || 'US';\r\n return out;\r\n }\r\n // No discrete locality fields — parse the best single string we have. The\r\n // street `address` may legacy-hold a full comma-separated address, so prefer\r\n // it over the location-name cache.\r\n return parseAddressToPostal((parts && parts.address) || fallback || null);\r\n}\r\n\r\n/**\r\n * Map event status to schema.org EventStatusType.\r\n *\r\n * Extracted from EventStructuredData.svelte lines 116-132.\r\n */\r\nexport function getEventStatus(rawStatus: string | null | undefined): string {\r\n if (!rawStatus) return 'https://schema.org/EventScheduled';\r\n\r\n const status = rawStatus.toLowerCase();\r\n\r\n if (status === 'cancelled' || status === 'canceled') {\r\n return 'https://schema.org/EventCancelled';\r\n }\r\n if (status === 'postponed') {\r\n return 'https://schema.org/EventPostponed';\r\n }\r\n if (status === 'rescheduled') {\r\n return 'https://schema.org/EventRescheduled';\r\n }\r\n\r\n return 'https://schema.org/EventScheduled';\r\n}\r\n\r\n/**\r\n * Map ticket availability to schema.org ItemAvailability.\r\n *\r\n * Extracted from EventStructuredData.svelte lines 137-157.\r\n */\r\nexport function getTicketAvailability(ticket: EventTicketInput): string {\r\n const remaining =\r\n ticket.remainingCapacity ?? ticket.quantityRemaining ?? ticket.quantity;\r\n\r\n if (remaining === 0 || ticket.soldOut) {\r\n return 'https://schema.org/SoldOut';\r\n }\r\n\r\n const now = browserNow();\r\n const salesBegin = toInstantOrNull(\r\n ticket.salesBegin ||\r\n ticket.salesStart ||\r\n ticket.saleBegin ||\r\n ticket.onSaleStart\r\n );\r\n const salesEnd = toInstantOrNull(\r\n ticket.salesEnd || ticket.saleEnd || ticket.onSaleEnd\r\n );\r\n\r\n if (salesBegin && compareInstants(salesBegin, now) > 0) {\r\n return 'https://schema.org/PreOrder';\r\n }\r\n\r\n if (salesEnd && compareInstants(salesEnd, now) < 0) {\r\n return 'https://schema.org/SoldOut';\r\n }\r\n\r\n return 'https://schema.org/InStock';\r\n}\r\n\r\n/**\r\n * Build a schema.org price-range offer from a low/high price and availability.\r\n *\r\n * The shared primitive behind every JSON-LD offer block: emit an AggregateOffer\r\n * when the range spans two prices, or a single Offer when low === high. Any\r\n * caller-specific fields (a buy `url`, a `validFrom` sales-start) ride along in\r\n * `extra`, spread verbatim after the core fields so each caller reproduces its\r\n * exact prior shape — the event builder passes url/validFrom, the series builder\r\n * passes nothing.\r\n */\r\nexport function buildPriceRangeOffer(\r\n lowPrice: number,\r\n highPrice: number,\r\n availability: string,\r\n extra: Record<string, unknown> = {}\r\n): Record<string, unknown> {\r\n // Use AggregateOffer when there are multiple price points\r\n if (lowPrice !== highPrice) {\r\n return {\r\n '@type': 'AggregateOffer',\r\n lowPrice,\r\n highPrice,\r\n priceCurrency: 'USD',\r\n availability,\r\n ...extra,\r\n };\r\n }\r\n\r\n // Single price point - use Offer\r\n return {\r\n '@type': 'Offer',\r\n price: lowPrice,\r\n priceCurrency: 'USD',\r\n availability,\r\n ...extra,\r\n };\r\n}\r\n\r\n/**\r\n * Build offers from available tickets.\r\n *\r\n * Extracted from EventStructuredData.svelte lines 162-208.\r\n */\r\nexport function buildOffers(\r\n tickets: EventTicketInput[] | null | undefined,\r\n url?: string\r\n): Record<string, unknown> | null {\r\n if (!tickets || tickets.length === 0) return null;\r\n\r\n // Filter to public tickets only (exclude door-only and hidden)\r\n const publicTickets = tickets.filter(t => {\r\n const visibility = t.visibility ?? 0;\r\n const isVisible = visibility === 0 || visibility === 1;\r\n const isPublic = t.salesChannel !== 2;\r\n return isVisible && isPublic;\r\n });\r\n\r\n if (publicTickets.length === 0) return null;\r\n\r\n // Find lowest and highest price\r\n const prices = publicTickets\r\n .map(t => parseFloat(String(t.price ?? 0)))\r\n .filter(p => !isNaN(p));\r\n\r\n if (prices.length === 0) return null;\r\n\r\n const lowPrice = Math.min(...prices);\r\n const highPrice = Math.max(...prices);\r\n const ticket = publicTickets[0];\r\n\r\n return buildPriceRangeOffer(\r\n lowPrice,\r\n highPrice,\r\n getTicketAvailability(ticket),\r\n {\r\n url: url || undefined,\r\n validFrom: ticket?.salesBegin || ticket?.salesStart || undefined,\r\n }\r\n );\r\n}\r\n\r\n/**\r\n * Build performer array from event performers.\r\n *\r\n * Extracted from EventStructuredData.svelte lines 213-237.\r\n */\r\nexport function buildPerformers(\r\n performers: EventPerformerInput[] | null | undefined\r\n): Array<Record<string, string>> | null {\r\n if (!performers || performers.length === 0) return null;\r\n\r\n // Filter to confirmed, visible performers\r\n const confirmed = performers.filter(p => {\r\n const isConfirmed = p.acceptedState === 2;\r\n const isNotHidden = !p.shouldBeHidden;\r\n const profile = p.RosterPerformer?.User?.performerProfile;\r\n const hasName =\r\n profile?.firstName || profile?.lastName || profile?.displayName;\r\n return isConfirmed && isNotHidden && hasName;\r\n });\r\n\r\n if (confirmed.length === 0) return null;\r\n\r\n return confirmed.map(p => {\r\n const profile = p.RosterPerformer?.User?.performerProfile;\r\n const name =\r\n profile?.displayName ||\r\n [profile?.firstName, profile?.lastName].filter(Boolean).join(' ');\r\n\r\n return {\r\n '@type': 'PerformingGroup',\r\n name: name,\r\n };\r\n });\r\n}\r\n","import { getDefaultTimezone } from '@getmicdrop/svelte-components/utils/timezones';\r\nimport { stripEventDescriptionHtml } from '@getmicdrop/svelte-components/richtext';\r\n/**\r\n * Pure function that builds Event JSON-LD structured data for Google Events rich results.\r\n *\r\n * Extracted from EventStructuredData.svelte (lines 242-313).\r\n * Produces identical output to the original $derived.by block.\r\n */\r\n\r\nimport type { EventJsonLdInput, VenueInput, OrganizerInput } from './types';\r\nimport {\r\n formatDateTimeWithOffset,\r\n buildPostalAddress,\r\n getEventStatus,\r\n buildOffers,\r\n buildPerformers,\r\n} from './helpers';\r\n\r\nexport interface BuildEventJsonLdOptions {\r\n event: EventJsonLdInput;\r\n venue: VenueInput;\r\n venueTimeZone?: string;\r\n eventUrl?: string;\r\n organizer?: OrganizerInput;\r\n}\r\n\r\nexport function buildEventJsonLd(\r\n options: BuildEventJsonLdOptions\r\n): Record<string, unknown> | null {\r\n const {\r\n event,\r\n venue,\r\n venueTimeZone = getDefaultTimezone(),\r\n eventUrl,\r\n organizer,\r\n } = options;\r\n\r\n if (!event) return null;\r\n\r\n const startDate = formatDateTimeWithOffset(\r\n event.startDateTime,\r\n venueTimeZone\r\n );\r\n\r\n // Skip if we don't have required fields\r\n if (!event.name || !startDate || !venue?.name) {\r\n return null;\r\n }\r\n\r\n // Google requires a PostalAddress on a physical event's location (online-only\r\n // events lost rich-result eligibility June 2025). Prefer the venue's discrete\r\n // address fields (street/city/state/postalCode), falling back to parsing the\r\n // googleLocationNameCache string. Only attach `address` when we could build\r\n // one — never emit `address: null`. A missing address is a data-pipeline gap\r\n // (the resolve payload must carry the venue address).\r\n const postalAddress = buildPostalAddress(\r\n venue,\r\n venue?.googleLocationNameCache\r\n );\r\n const location: Record<string, unknown> = {\r\n '@type': 'Place',\r\n name: venue.name,\r\n };\r\n if (postalAddress) {\r\n location.address = postalAddress;\r\n }\r\n\r\n const data: Record<string, unknown> = {\r\n '@context': 'https://schema.org',\r\n '@type': 'Event',\r\n name: event.name,\r\n startDate: startDate,\r\n eventStatus: getEventStatus(event.status),\r\n eventAttendanceMode: 'https://schema.org/OfflineEventAttendanceMode',\r\n location,\r\n };\r\n\r\n // Add endDate if available\r\n const endDate = formatDateTimeWithOffset(event.endDateTime, venueTimeZone);\r\n if (endDate) {\r\n data.endDate = endDate;\r\n }\r\n\r\n // Add description\r\n const description = stripEventDescriptionHtml(event.description) || event.eventSummary;\r\n if (description) {\r\n data.description = description;\r\n }\r\n\r\n // Add image - Google recommends multiple aspect ratios\r\n const imageUrl = event.image || event.coverImage || event.ShowImage;\r\n if (imageUrl) {\r\n data.image = [imageUrl];\r\n }\r\n\r\n // Add offers from tickets\r\n const offers = buildOffers(event.availableTickets, eventUrl);\r\n if (offers) {\r\n data.offers = offers;\r\n }\r\n\r\n // Add performers\r\n const performers = buildPerformers(event.performers);\r\n if (performers && performers.length > 0) {\r\n data.performer = performers;\r\n }\r\n\r\n // Add organizer if provided\r\n if (organizer?.name) {\r\n const org: Record<string, string> = {\r\n '@type': 'Organization',\r\n name: organizer.name,\r\n };\r\n if (organizer.url) {\r\n org.url = organizer.url;\r\n }\r\n data.organizer = org;\r\n }\r\n\r\n // Add event URL if provided\r\n if (eventUrl) {\r\n data.url = eventUrl;\r\n }\r\n\r\n return data;\r\n}\r\n","/**\r\n * Pure function that builds EventSeries JSON-LD structured data.\r\n *\r\n * Series use schema.org EventSeries type with subEvent array containing\r\n * individual Event blocks for each occurrence. Each subEvent has its own\r\n * offers built from the simplified series ticket DTOs (different shape\r\n * from the event builder's tickets).\r\n *\r\n * Google Rich Results Test does NOT support EventSeries directly, but\r\n * the individual subEvent Event blocks can qualify for rich results.\r\n */\r\n\r\nimport type { SeriesJsonLdInput } from './types';\r\nimport { stripEventDescriptionHtml } from '@getmicdrop/svelte-components/richtext';\r\nimport {\r\n formatDateTimeWithOffset,\r\n buildPostalAddress,\r\n getEventStatus,\r\n buildPriceRangeOffer,\r\n} from './helpers';\r\n\r\nexport interface BuildSeriesJsonLdOptions {\r\n series: SeriesJsonLdInput;\r\n baseEventUrl?: string; // e.g., \"https://get-micdrop.com/e\" -- used to build subEvent @id URLs\r\n}\r\n\r\n/**\r\n * Map a series ticket status string to schema.org ItemAvailability.\r\n *\r\n * Series tickets use a simplified { name, price, status } shape from the\r\n * backend (SeriesTicketDTO), unlike event tickets which have visibility,\r\n * salesChannel, remainingCapacity, etc.\r\n */\r\nfunction mapSeriesTicketAvailability(status: string): string {\r\n switch (status) {\r\n case 'available':\r\n return 'https://schema.org/InStock';\r\n case 'sold_out':\r\n return 'https://schema.org/SoldOut';\r\n case 'coming_soon':\r\n return 'https://schema.org/PreOrder';\r\n case 'ended':\r\n return 'https://schema.org/SoldOut';\r\n default:\r\n return 'https://schema.org/InStock';\r\n }\r\n}\r\n\r\n/**\r\n * Build offers from series occurrence tickets.\r\n *\r\n * Series tickets have a simpler shape than event tickets: { name, price, status }.\r\n * Uses AggregateOffer for multiple price points, Offer for single price.\r\n */\r\nfunction buildSeriesOccurrenceOffers(\r\n tickets: Array<{ name: string; price: number; status: string }> | undefined,\r\n _occurrenceStatus: string | undefined\r\n): Record<string, unknown> | null {\r\n if (!tickets || tickets.length === 0) return null;\r\n\r\n // Filter out sold_out and ended tickets for offer building\r\n const activeTickets = tickets.filter(\r\n t => t.status !== 'sold_out' && t.status !== 'ended'\r\n );\r\n\r\n // If all tickets are sold out / ended, report SoldOut with the price range\r\n if (activeTickets.length === 0) {\r\n const prices = tickets.map(t => t.price).filter(p => !isNaN(p));\r\n if (prices.length === 0) return null;\r\n\r\n return buildPriceRangeOffer(\r\n Math.min(...prices),\r\n Math.max(...prices),\r\n 'https://schema.org/SoldOut'\r\n );\r\n }\r\n\r\n const prices = activeTickets.map(t => t.price).filter(p => !isNaN(p));\r\n if (prices.length === 0) return null;\r\n\r\n return buildPriceRangeOffer(\r\n Math.min(...prices),\r\n Math.max(...prices),\r\n mapSeriesTicketAvailability(activeTickets[0].status)\r\n );\r\n}\r\n\r\nexport function buildSeriesJsonLd(\r\n options: BuildSeriesJsonLdOptions\r\n): Record<string, unknown> | null {\r\n const { series, baseEventUrl } = options;\r\n\r\n // Required fields\r\n if (!series.title) return null;\r\n if (!series.occurrences || series.occurrences.length === 0) return null;\r\n if (!series.venue?.name) return null;\r\n\r\n // Build location — prefer the venue's discrete address fields, falling back to\r\n // parsing googleLocationNameCache (see buildEventJsonLd for the rationale).\r\n const address = buildPostalAddress(\r\n series.venue,\r\n series.venue.googleLocationNameCache\r\n );\r\n const location: Record<string, unknown> = {\r\n '@type': 'Place',\r\n name: series.venue.name,\r\n };\r\n if (address) {\r\n location.address = address;\r\n }\r\n\r\n // Build parent EventSeries\r\n const data: Record<string, unknown> = {\r\n '@context': 'https://schema.org',\r\n '@type': 'EventSeries',\r\n name: series.title,\r\n location,\r\n };\r\n\r\n // Description: prefer description, fall back to eventSummary\r\n const description = stripEventDescriptionHtml(series.description) || series.eventSummary;\r\n if (description) {\r\n data.description = description;\r\n }\r\n\r\n // Image\r\n if (series.image) {\r\n data.image = [series.image];\r\n }\r\n\r\n // Performers: flat displayName array (different from event builder's nested shape)\r\n const performers = series.performers?.filter(p => p.displayName);\r\n if (performers && performers.length > 0) {\r\n data.performer = performers.map(p => ({\r\n '@type': 'PerformingGroup',\r\n name: p.displayName,\r\n }));\r\n }\r\n\r\n // SubEvents: build Event blocks for each occurrence\r\n const subEvents: Record<string, unknown>[] = [];\r\n\r\n for (const occ of series.occurrences) {\r\n const startDate = formatDateTimeWithOffset(\r\n occ.startDateTime,\r\n series.timeZone\r\n );\r\n if (!startDate) continue; // Skip occurrences without valid startDate\r\n\r\n const subEvent: Record<string, unknown> = {\r\n '@type': 'Event',\r\n name: occ.title,\r\n startDate,\r\n eventStatus: getEventStatus(occ.status),\r\n location,\r\n };\r\n\r\n // @id from baseEventUrl\r\n if (baseEventUrl) {\r\n subEvent['@id'] = `${baseEventUrl}/${occ.id}-${occ.slug}`;\r\n }\r\n\r\n // endDate\r\n const endDate = formatDateTimeWithOffset(occ.endDateTime, series.timeZone);\r\n if (endDate) {\r\n subEvent.endDate = endDate;\r\n }\r\n\r\n // Offers from simplified ticket DTOs\r\n const offers = buildSeriesOccurrenceOffers(occ.tickets, occ.status);\r\n if (offers) {\r\n subEvent.offers = offers;\r\n }\r\n\r\n subEvents.push(subEvent);\r\n }\r\n\r\n // If all occurrences were skipped, return null\r\n if (subEvents.length === 0) return null;\r\n\r\n data.subEvent = subEvents;\r\n\r\n return data;\r\n}\r\n","/**\r\n * Pure function that builds CollectionPage JSON-LD for an event collection.\r\n *\r\n * A collection is a curated list of shows — NOT a single event, and NOT a\r\n * Festival (the product can't infer festival intent from a collection named\r\n * \"Detroit Comedy Festival\"; it's still just a collection). We emit a\r\n * schema.org `CollectionPage` whose `mainEntity` is a *summary-page* `ItemList`\r\n * — each `ListItem` is just `{ position, name, url }` pointing at the show's\r\n * canonical `/e/{id}-{slug}` leaf. The full `Event` rich-result markup lives on\r\n * each event's own page (one event = one leaf URL); duplicating full Event\r\n * objects here would create competing entities and trigger missing-field\r\n * (offers/address) warnings on the collection page.\r\n *\r\n * (Previously emitted `Festival` + `subEvent`, which Google's Rich Results\r\n * Test does not support.)\r\n */\r\n\r\nimport type { CollectionJsonLdInput } from './types';\r\nimport { stripEventDescriptionHtml } from '@getmicdrop/svelte-components/richtext';\r\n\r\nexport interface BuildCollectionJsonLdOptions {\r\n collection: CollectionJsonLdInput;\r\n baseEventUrl?: string; // e.g., \"https://get-micdrop.com/e\"\r\n /** Accepted for API compatibility; the summary ItemList carries no dates. */\r\n defaultTimeZone?: string;\r\n}\r\n\r\nexport function buildCollectionJsonLd(\r\n options: BuildCollectionJsonLdOptions,\r\n): Record<string, unknown> | null {\r\n const { collection, baseEventUrl } = options;\r\n\r\n // Required fields\r\n if (!collection.collectionTitle) return null;\r\n if (!collection.events || collection.events.length === 0) return null;\r\n\r\n // Build a summary ListItem per collection member: position + name + the\r\n // canonical leaf url. Prefer the slugged form, else bare `/e/{id}` (the app\r\n // self-heals bare ids to the slugged canonical).\r\n const itemListElement: Record<string, unknown>[] = [];\r\n\r\n for (const event of collection.events) {\r\n if (!event.title) continue; // Skip members with no name to list\r\n\r\n const listItem: Record<string, unknown> = {\r\n '@type': 'ListItem',\r\n position: itemListElement.length + 1,\r\n name: event.title,\r\n };\r\n\r\n if (baseEventUrl) {\r\n listItem.url = event.slug\r\n ? `${baseEventUrl}/${event.id}-${event.slug}`\r\n : `${baseEventUrl}/${event.id}`;\r\n }\r\n\r\n itemListElement.push(listItem);\r\n }\r\n\r\n // If every event was skipped, there's nothing to mark up.\r\n if (itemListElement.length === 0) return null;\r\n\r\n const data: Record<string, unknown> = {\r\n '@context': 'https://schema.org',\r\n '@type': 'CollectionPage',\r\n name: collection.collectionTitle,\r\n };\r\n\r\n // Description: prefer description, fall back to summary\r\n const description = stripEventDescriptionHtml(collection.description) || collection.summary;\r\n if (description) {\r\n data.description = description;\r\n }\r\n\r\n // Cover image\r\n if (collection.coverImage) {\r\n data.image = [collection.coverImage];\r\n }\r\n\r\n data.mainEntity = {\r\n '@type': 'ItemList',\r\n numberOfItems: itemListElement.length,\r\n itemListElement,\r\n };\r\n\r\n return data;\r\n}\r\n","/**\n * Slug generation for routing URLs.\n *\n * This is the canonical slugify implementation for the MicDrop platform.\n * Originally lifted byte-for-byte from micdrop-frontend's local slug helper;\n * that copy has since been deleted in favor of this module, which is now the\n * single source of truth every consumer (MF, VC, future consumers) imports.\n * The algorithm must never change silently (user-visible + bookmarked URLs).\n *\n * @module routing/slug\n */\n/** Max slug length (chars). Google/SEO sweet spot is ≤60–80; we cap at 70. */\nconst MAX_SLUG_LENGTH = 70;\n/**\n * Creates a URL-safe slug from arbitrary text.\n *\n * - NFKD-decomposes accented chars (é → e + combining accent) then strips the\n * combining diacritical marks, so \"Café\" → \"cafe\" (not \"caf\")\n * - lowercases\n * - trims surrounding whitespace\n * - strips characters that are not word chars, whitespace, or hyphens\n * - collapses runs of whitespace / underscores / hyphens into a single hyphen\n * - trims leading/trailing hyphens\n * - caps length at {@link MAX_SLUG_LENGTH} (70) chars, re-trimming a trailing\n * hyphen the cap may have landed on\n *\n * @example\n * toSlug('Comedy Night: Live at The Club!'); // 'comedy-night-live-at-the-club'\n * @example\n * toSlug('Café Comedy'); // 'cafe-comedy'\n */\n// NOTE: declared as `toSlug` (not `slugify`) and re-exported under the public\n// name below. The shared eslint `no-restricted-syntax` rule forbids declaring a\n// binding named `slugify` outside the canonical site; aliasing keeps the public\n// surface (`slugify`) without redefining that identifier here.\n//\n// This module IS the SEO-canonical / bookmarked authority for emitted URLs;\n// every consumer imports it, so the algorithm must stay stable forever.\nfunction toSlug(text) {\n return (text\n .normalize('NFKD') // decompose accented chars: é -> e + combining accent\n .replace(/[\\u0300-\\u036f]/g, '') // strip combining diacritical marks\n .toLowerCase()\n .trim()\n .replace(/[^\\w\\s-]/g, '')\n .replace(/[\\s_-]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, MAX_SLUG_LENGTH)\n // re-trim in case the length cap landed mid-separator\n .replace(/-+$/g, ''));\n}\nexport { toSlug, toSlug as slugify };\n","import { getDefaultTimezone } from '@getmicdrop/svelte-components/utils/timezones';\r\nimport { stripEventDescriptionHtml } from '@getmicdrop/svelte-components/richtext';\r\n/**\r\n * HostSeoController -- manages JSON-LD structured data, canonical URLs,\r\n * OG tags, meta descriptions, and page title injection into venue pages'\r\n * <head> when the Micdrop calendar widget is embedded.\r\n *\r\n * Follows a save-replace-restore pattern to be a \"good citizen\" on venue pages:\r\n * idle -> listing -> entity -> listing -> ... -> destroyed\r\n *\r\n * Uses textContent (never innerHTML) for JSON-LD script tags to prevent XSS.\r\n */\r\n\r\nimport type { HostSeoControllerOptions } from './types';\r\nimport { buildEventJsonLd } from './buildEventJsonLd';\r\nimport { buildSeriesJsonLd } from './buildSeriesJsonLd';\r\nimport { buildCollectionJsonLd } from './buildCollectionJsonLd';\r\nimport { slugify } from '@getmicdrop/svelte-components/routing';\r\n\r\ntype ControllerState = 'idle' | 'listing' | 'entity' | 'destroyed';\r\n\r\ninterface SavedOriginals {\r\n title: string;\r\n canonicalHref: string | null;\r\n ogTitle: string | null;\r\n ogDescription: string | null;\r\n ogImage: string | null;\r\n ogUrl: string | null;\r\n ogType: string | null;\r\n metaDescription: string | null;\r\n}\r\n\r\n// Minimal event shape for listing mode\r\ninterface ListingEvent {\r\n id: number | string;\r\n name: string;\r\n slug?: string;\r\n startDateTime?: string;\r\n image?: string;\r\n}\r\n\r\n// Venue data passed at runtime\r\ninterface VenueData {\r\n name: string;\r\n address?: string;\r\n googleLocationNameCache?: string;\r\n // Transformed venues carry lowercase `timezone`; raw/host data may use\r\n // PascalCase `timeZone`. Accept both so the venue tz isn't silently dropped.\r\n timeZone?: string;\r\n timezone?: string;\r\n}\r\n\r\nexport class HostSeoController {\r\n /**\r\n * Single active instance. A venue page that mounts >1 calendar embed\r\n * (e.g. \"upcoming shows\" + \"this weekend\") will see the *later*\r\n * controller take over `<head>` — the earlier one is destroyed.\r\n *\r\n * KNOWN LIMITATION: multi-embed pages cannot run two SEO controllers\r\n * in parallel because they would both fight to own `<title>`/OG/etc.\r\n * Pass `data-inject-seo=\"false\"` on all but one embed to opt out.\r\n */\r\n static _activeInstance: HostSeoController | null = null;\r\n\r\n private _state: ControllerState = 'idle';\r\n private _venueName: string;\r\n private _venueAddress: string | undefined;\r\n private _venueTimeZone: string;\r\n private _baseUrl: string;\r\n private _originals: SavedOriginals | null = null;\r\n private _originalsSaved = false;\r\n private _events: ListingEvent[] = [];\r\n\r\n constructor(options: HostSeoControllerOptions) {\r\n // SSR guard\r\n if (typeof document === 'undefined') return;\r\n\r\n // If another instance exists, destroy it first.\r\n if (\r\n HostSeoController._activeInstance &&\r\n HostSeoController._activeInstance !== this\r\n ) {\r\n HostSeoController._activeInstance.destroy();\r\n }\r\n\r\n this._venueName = options.venueName;\r\n this._venueAddress = options.venueAddress;\r\n this._venueTimeZone = options.venueTimeZone || getDefaultTimezone();\r\n this._baseUrl =\r\n options.baseUrl || window.location.origin + window.location.pathname;\r\n\r\n this._saveOriginals();\r\n\r\n HostSeoController._activeInstance = this;\r\n }\r\n\r\n // ---------------------------------------------------------------------------\r\n // Private helpers\r\n // ---------------------------------------------------------------------------\r\n\r\n private _saveOriginals(): void {\r\n if (this._originalsSaved) return;\r\n\r\n const canonical = document.querySelector(\r\n 'link[rel=\"canonical\"]'\r\n ) as HTMLLinkElement | null;\r\n const ogTitle = document.querySelector(\r\n 'meta[property=\"og:title\"]'\r\n ) as HTMLMetaElement | null;\r\n const ogDescription = document.querySelector(\r\n 'meta[property=\"og:description\"]'\r\n ) as HTMLMetaElement | null;\r\n const ogImage = document.querySelector(\r\n 'meta[property=\"og:image\"]'\r\n ) as HTMLMetaElement | null;\r\n const ogUrl = document.querySelector(\r\n 'meta[property=\"og:url\"]'\r\n ) as HTMLMetaElement | null;\r\n const ogType = document.querySelector(\r\n 'meta[property=\"og:type\"]'\r\n ) as HTMLMetaElement | null;\r\n const metaDescription = document.querySelector(\r\n 'meta[name=\"description\"]'\r\n ) as HTMLMetaElement | null;\r\n\r\n this._originals = {\r\n title: document.title,\r\n canonicalHref: canonical?.getAttribute('href') ?? null,\r\n ogTitle: ogTitle?.getAttribute('content') ?? null,\r\n ogDescription: ogDescription?.getAttribute('content') ?? null,\r\n ogImage: ogImage?.getAttribute('content') ?? null,\r\n ogUrl: ogUrl?.getAttribute('content') ?? null,\r\n ogType: ogType?.getAttribute('content') ?? null,\r\n metaDescription: metaDescription?.getAttribute('content') ?? null,\r\n };\r\n\r\n this._originalsSaved = true;\r\n }\r\n\r\n private _injectJsonLd(data: Record<string, unknown>): void {\r\n if (typeof document === 'undefined') return;\r\n\r\n // Remove any existing micdrop JSON-LD\r\n document\r\n .querySelectorAll('script[data-micdrop=\"jsonld\"]')\r\n .forEach(el => el.remove());\r\n\r\n const script = document.createElement('script');\r\n script.setAttribute('type', 'application/ld+json');\r\n script.setAttribute('data-micdrop', 'jsonld');\r\n // Use textContent (NOT innerHTML) to prevent XSS\r\n script.textContent = JSON.stringify(data);\r\n document.head.appendChild(script);\r\n }\r\n\r\n private _setMeta(property: string, content: string): void {\r\n if (typeof document === 'undefined') return;\r\n\r\n if (property.startsWith('og:')) {\r\n // OG tags use property attribute\r\n let el = document.querySelector(\r\n `meta[property=\"${property}\"]`\r\n ) as HTMLMetaElement | null;\r\n if (el) {\r\n el.setAttribute('content', content);\r\n } else {\r\n el = document.createElement('meta');\r\n el.setAttribute('property', property);\r\n el.setAttribute('content', content);\r\n el.setAttribute('data-micdrop', 'og');\r\n document.head.appendChild(el);\r\n }\r\n } else {\r\n // Standard meta tags use name attribute (e.g., description)\r\n let el = document.querySelector(\r\n `meta[name=\"${property}\"]`\r\n ) as HTMLMetaElement | null;\r\n if (el) {\r\n el.setAttribute('content', content);\r\n } else {\r\n el = document.createElement('meta');\r\n el.setAttribute('name', property);\r\n el.setAttribute('content', content);\r\n el.setAttribute('data-micdrop', 'meta');\r\n document.head.appendChild(el);\r\n }\r\n }\r\n }\r\n\r\n private _setCanonical(url: string): void {\r\n if (typeof document === 'undefined') return;\r\n\r\n let el = document.querySelector(\r\n 'link[rel=\"canonical\"]'\r\n ) as HTMLLinkElement | null;\r\n if (el) {\r\n el.setAttribute('href', url);\r\n } else {\r\n el = document.createElement('link');\r\n el.setAttribute('rel', 'canonical');\r\n el.setAttribute('href', url);\r\n el.setAttribute('data-micdrop', 'canonical');\r\n document.head.appendChild(el);\r\n }\r\n }\r\n\r\n private _removeAllInjected(): void {\r\n if (typeof document === 'undefined') return;\r\n document.querySelectorAll('[data-micdrop]').forEach(el => el.remove());\r\n }\r\n\r\n private _restoreOriginals(): void {\r\n if (typeof document === 'undefined' || !this._originals) return;\r\n\r\n // Restore title\r\n document.title = this._originals.title;\r\n\r\n // Restore canonical\r\n if (this._originals.canonicalHref !== null) {\r\n const canonical = document.querySelector(\r\n 'link[rel=\"canonical\"]'\r\n ) as HTMLLinkElement | null;\r\n if (canonical) {\r\n canonical.setAttribute('href', this._originals.canonicalHref);\r\n }\r\n }\r\n\r\n // Restore OG tags\r\n this._restoreMetaProperty('og:title', this._originals.ogTitle);\r\n this._restoreMetaProperty('og:description', this._originals.ogDescription);\r\n this._restoreMetaProperty('og:image', this._originals.ogImage);\r\n this._restoreMetaProperty('og:url', this._originals.ogUrl);\r\n this._restoreMetaProperty('og:type', this._originals.ogType);\r\n\r\n // Restore meta description\r\n this._restoreMetaName('description', this._originals.metaDescription);\r\n\r\n this._originalsSaved = false;\r\n }\r\n\r\n private _restoreMetaProperty(\r\n property: string,\r\n originalContent: string | null\r\n ): void {\r\n const el = document.querySelector(\r\n `meta[property=\"${property}\"]`\r\n ) as HTMLMetaElement | null;\r\n if (originalContent !== null && el) {\r\n // Had original value -- restore it\r\n el.setAttribute('content', originalContent);\r\n } else if (originalContent === null && el?.hasAttribute('data-micdrop')) {\r\n // We created it -- remove it\r\n el.remove();\r\n }\r\n }\r\n\r\n private _restoreMetaName(name: string, originalContent: string | null): void {\r\n const el = document.querySelector(\r\n `meta[name=\"${name}\"]`\r\n ) as HTMLMetaElement | null;\r\n if (originalContent !== null && el) {\r\n el.setAttribute('content', originalContent);\r\n } else if (originalContent === null && el?.hasAttribute('data-micdrop')) {\r\n el.remove();\r\n }\r\n }\r\n\r\n private _buildItemList(events: ListingEvent[]): Record<string, unknown> {\r\n return {\r\n '@context': 'https://schema.org',\r\n '@type': 'ItemList',\r\n itemListElement: events.map((event, i) => {\r\n const slug = event.slug || slugify(event.name || '');\r\n return {\r\n '@type': 'ListItem',\r\n position: i + 1,\r\n url: `${this._baseUrl}?e=${event.id}-${slug}`,\r\n };\r\n }),\r\n };\r\n }\r\n\r\n private _buildEntityUrl(\r\n type: string,\r\n id: string | number,\r\n slug: string\r\n ): string {\r\n const prefix = type === 'event' ? 'e' : type === 'series' ? 's' : 'c';\r\n return `${this._baseUrl}?${prefix}=${id}-${slug}`;\r\n }\r\n\r\n // ---------------------------------------------------------------------------\r\n // Public API\r\n // ---------------------------------------------------------------------------\r\n\r\n /**\r\n * Called when events finish loading from the API.\r\n * Injects an ItemList JSON-LD for the listing. Does not modify OG/canonical/title.\r\n */\r\n onEventsLoaded(events: ListingEvent[], venue?: VenueData): void {\r\n if (typeof document === 'undefined') return;\r\n if (this._state === 'destroyed') return;\r\n\r\n // Update venue info from parameter if provided\r\n if (venue) {\r\n this._venueName = venue.name || this._venueName;\r\n this._venueAddress =\r\n venue.address || venue.googleLocationNameCache || this._venueAddress;\r\n const venueTz = venue.timeZone || venue.timezone;\r\n if (venueTz) {\r\n this._venueTimeZone = venueTz;\r\n }\r\n }\r\n\r\n // Store events for later use (re-injection on back to listing)\r\n this._events = events;\r\n\r\n // Build and inject ItemList JSON-LD\r\n const itemList = this._buildItemList(events);\r\n this._injectJsonLd(itemList);\r\n\r\n this._state = 'listing';\r\n }\r\n\r\n /**\r\n * Called when the user selects an event, series, or collection.\r\n * Replaces ItemList JSON-LD with entity-specific JSON-LD and updates\r\n * all head tags (canonical, OG, meta description, title).\r\n */\r\n onEntitySelected(\r\n type: 'event' | 'series' | 'collection',\r\n data: any,\r\n venue?: VenueData\r\n ): void {\r\n if (typeof document === 'undefined') return;\r\n if (this._state === 'destroyed') return;\r\n\r\n // Save originals if not already saved\r\n this._saveOriginals();\r\n\r\n // Update venue info from parameter if provided\r\n if (venue) {\r\n this._venueName = venue.name || this._venueName;\r\n this._venueAddress =\r\n venue.address || venue.googleLocationNameCache || this._venueAddress;\r\n const venueTz = venue.timeZone || venue.timezone;\r\n if (venueTz) {\r\n this._venueTimeZone = venueTz;\r\n }\r\n }\r\n\r\n let jsonLd: Record<string, unknown> | null = null;\r\n let entityName = '';\r\n // Raw rich-text description (HTML) kept separate from the plain-text\r\n // summary fallback so we can strip the description first and only then fall\r\n // back — a blank rich-text description (`<p><br></p>`) must not mask a real\r\n // summary. (PBN-1)\r\n let entityDescription = '';\r\n let entitySummary = '';\r\n let entityImage = '';\r\n let entityUrl = '';\r\n\r\n if (type === 'event') {\r\n const slug = data.slug || slugify(data.name || '');\r\n entityUrl = this._buildEntityUrl(\r\n 'event',\r\n data.id || data.eventID || '',\r\n slug\r\n );\r\n\r\n jsonLd = buildEventJsonLd({\r\n event: data,\r\n venue: {\r\n name: this._venueName,\r\n googleLocationNameCache: this._venueAddress,\r\n },\r\n venueTimeZone: this._venueTimeZone,\r\n eventUrl: entityUrl,\r\n });\r\n\r\n entityName = data.name || '';\r\n entityDescription = data.description || '';\r\n entitySummary = data.eventSummary || '';\r\n entityImage = data.image || data.coverImage || data.ShowImage || '';\r\n } else if (type === 'series') {\r\n const slug = data.slug || slugify(data.title || '');\r\n entityUrl = this._buildEntityUrl('series', data.id || '', slug);\r\n\r\n jsonLd = buildSeriesJsonLd({\r\n series: {\r\n ...data,\r\n venue: {\r\n name: this._venueName,\r\n googleLocationNameCache: this._venueAddress,\r\n },\r\n timeZone: this._venueTimeZone,\r\n },\r\n baseEventUrl: entityUrl,\r\n });\r\n\r\n entityName = data.title || '';\r\n entityDescription = data.description || '';\r\n entitySummary = data.eventSummary || '';\r\n entityImage = data.image || '';\r\n } else if (type === 'collection') {\r\n const slug = data.slug || slugify(data.collectionTitle || '');\r\n entityUrl = this._buildEntityUrl('collection', data.id || '', slug);\r\n\r\n jsonLd = buildCollectionJsonLd({\r\n collection: data,\r\n baseEventUrl: entityUrl,\r\n defaultTimeZone: this._venueTimeZone,\r\n });\r\n\r\n entityName = data.collectionTitle || '';\r\n entityDescription = data.description || '';\r\n entitySummary = data.summary || '';\r\n entityImage = data.coverImage || '';\r\n }\r\n\r\n // Inject JSON-LD (replaces existing micdrop JSON-LD)\r\n if (jsonLd) {\r\n this._injectJsonLd(jsonLd);\r\n }\r\n\r\n // Update canonical URL\r\n if (entityUrl) {\r\n this._setCanonical(entityUrl);\r\n }\r\n\r\n // Update OG tags\r\n if (entityName) {\r\n this._setMeta('og:title', entityName);\r\n }\r\n // Descriptions are stored as rich-text HTML. Flatten (and truncate) to plain\r\n // text before meta injection — the same canonical strip the /e page uses —\r\n // so raw tags never leak into share cards or search snippets. Strip first,\r\n // then fall back to the summary, so blank rich text doesn't mask it. (PBN-1)\r\n const metaDescription =\r\n stripEventDescriptionHtml(entityDescription, 160) ||\r\n stripEventDescriptionHtml(entitySummary, 160);\r\n if (metaDescription) {\r\n this._setMeta('og:description', metaDescription);\r\n } else {\r\n // No usable description — restore the page's original so a previously\r\n // selected entity's description can't linger on this one.\r\n this._restoreMetaProperty(\r\n 'og:description',\r\n this._originals?.ogDescription ?? null\r\n );\r\n }\r\n if (entityImage) {\r\n this._setMeta('og:image', entityImage);\r\n }\r\n if (entityUrl) {\r\n this._setMeta('og:url', entityUrl);\r\n }\r\n this._setMeta('og:type', 'website');\r\n\r\n // Twitter Card: large image preview\r\n this._setMeta('twitter:card', 'summary_large_image');\r\n\r\n // Update meta description\r\n if (metaDescription) {\r\n this._setMeta('description', metaDescription);\r\n } else {\r\n this._restoreMetaName(\r\n 'description',\r\n this._originals?.metaDescription ?? null\r\n );\r\n }\r\n\r\n // Update page title\r\n if (entityName) {\r\n document.title = `${entityName} | ${this._venueName}`;\r\n }\r\n\r\n this._state = 'entity';\r\n }\r\n\r\n /**\r\n * Called when the user navigates back to the event listing.\r\n * Restores original head elements and re-injects ItemList JSON-LD.\r\n */\r\n onBackToListing(): void {\r\n if (typeof document === 'undefined') return;\r\n if (this._state === 'destroyed') return;\r\n\r\n // Restore all originals\r\n this._restoreOriginals();\r\n\r\n // Remove any remaining injected elements\r\n this._removeAllInjected();\r\n\r\n // Re-inject ItemList JSON-LD if events are available\r\n if (this._events.length > 0) {\r\n const itemList = this._buildItemList(this._events);\r\n this._injectJsonLd(itemList);\r\n }\r\n\r\n // Re-save originals since we just restored them\r\n this._saveOriginals();\r\n\r\n this._state = 'listing';\r\n }\r\n\r\n /**\r\n * Called when the widget is destroyed. Removes all injected elements\r\n * and restores originals. Clears the static active instance.\r\n */\r\n destroy(): void {\r\n if (typeof document === 'undefined') return;\r\n if (this._state === 'destroyed') return;\r\n\r\n this._removeAllInjected();\r\n this._restoreOriginals();\r\n\r\n if (HostSeoController._activeInstance === this) {\r\n HostSeoController._activeInstance = null;\r\n }\r\n\r\n this._state = 'destroyed';\r\n }\r\n}\r\n"],"names":["getDefaultTimezone","BLOCK_CLOSE","LINE_BREAK","ANY_TAG","NAMED_ENTITIES","decodeEntities","input","_","hex","codePointOrEmpty","dec","m","code","stripEventDescriptionHtml","html","maxLength","text","clipped","lastSpace","browserNow","AppError","message","options","_resolver","ValueGuardError","DateTimeError","details","DateTimeErrorCode","utcFromParts","year","month","day","hours","minutes","seconds","probe","ISO_INSTANT_PATTERN","ZONELESS_DATETIME_PATTERN","isRealCalendarDate","GO_STRING_PATTERN","GO_ZERO_INSTANT_MS","toWireParts","value","iso","go","hh","mm","ss","frac","sign","offH","offM","ms","parseWireInstantQuietly","fromEpoch","parts","parsed","isGoZeroTime","at","assertNotUnsetMarker","assertValidInstant","label","compareInstants","a","b","toInstant","toInstantOrNull","escapeJsonLdForScript","json","formatDateTimeWithOffset","isoString","tz","date","offsetFormatOptions","formatter","get","type","p","hour","minute","second","tzOffset","offset","match","mins","parseAddressToPostal","addressStr","stateZipMatch","buildPostalAddress","fallback","out","getEventStatus","rawStatus","status","getTicketAvailability","ticket","now","salesBegin","salesEnd","buildPriceRangeOffer","lowPrice","highPrice","availability","extra","buildOffers","tickets","url","publicTickets","t","visibility","isVisible","isPublic","prices","buildPerformers","performers","confirmed","isConfirmed","isNotHidden","profile","hasName","buildEventJsonLd","event","venue","venueTimeZone","eventUrl","organizer","startDate","postalAddress","location","data","endDate","description","imageUrl","offers","org","mapSeriesTicketAvailability","buildSeriesOccurrenceOffers","_occurrenceStatus","activeTickets","buildSeriesJsonLd","series","baseEventUrl","address","subEvents","occ","subEvent","buildCollectionJsonLd","collection","itemListElement","listItem","MAX_SLUG_LENGTH","toSlug","_HostSeoController","canonical","ogTitle","ogDescription","ogImage","ogUrl","ogType","metaDescription","el","script","property","content","originalContent","name","events","slug","slugify","id","prefix","venueTz","itemList","jsonLd","entityName","entityDescription","entitySummary","entityImage","entityUrl","HostSeoController"],"mappings":"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","x_google_ignoreList":[0,1,2,3,4,5,6,7,12]}
|