@aglyn/aglyn 1.0.0-beta.207 → 1.0.0-beta.209
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/package.json +11 -11
- package/src/lib/app-utils/announce-site-wide-change.d.ts +45 -0
- package/src/lib/app-utils/announce-site-wide-change.js +58 -0
- package/src/lib/app-utils/announce-site-wide-change.js.map +1 -0
- package/src/lib/app-utils/docs-index.generated.js +1 -1
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/screen-link-value.d.ts +4 -0
- package/src/lib/app-utils/screen-link-value.js +6 -2
- package/src/lib/app-utils/screen-link-value.js.map +1 -1
|
@@ -164,6 +164,10 @@ export declare function screenRoutesAnswerFor(screens: ScreenRouteMap | undefine
|
|
|
164
164
|
* Navigable protocols only. A stored `javascript:`/`data:` href would
|
|
165
165
|
* execute in visitors' browsers, so the guard the linking components each
|
|
166
166
|
* carried is here instead — one copy, one place to harden.
|
|
167
|
+
*
|
|
168
|
+
* `sms:` sits beside `tel:` (AGL-3388): it opens the visitor's messaging app
|
|
169
|
+
* and executes nothing, the same list `safe-url-scheme.ts` keeps. Leaving it
|
|
170
|
+
* out rendered every "Text us" button as a button with no link.
|
|
167
171
|
*/
|
|
168
172
|
export declare const SAFE_HREF_PATTERN: RegExp;
|
|
169
173
|
/** Of those, the ones that actually leave the site (new-tab decisions). */
|
|
@@ -206,8 +206,12 @@
|
|
|
206
206
|
* Navigable protocols only. A stored `javascript:`/`data:` href would
|
|
207
207
|
* execute in visitors' browsers, so the guard the linking components each
|
|
208
208
|
* carried is here instead — one copy, one place to harden.
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
*
|
|
210
|
+
* `sms:` sits beside `tel:` (AGL-3388): it opens the visitor's messaging app
|
|
211
|
+
* and executes nothing, the same list `safe-url-scheme.ts` keeps. Leaving it
|
|
212
|
+
* out rendered every "Text us" button as a button with no link.
|
|
213
|
+
*/ export const SAFE_HREF_PATTERN = /^(https?:\/\/|mailto:|tel:|sms:|\/|#)/i;
|
|
214
|
+
/** Of those, the ones that actually leave the site (new-tab decisions). */ export const EXTERNAL_HREF_PATTERN = /^(https?:\/\/|mailto:|tel:|sms:)/i;
|
|
211
215
|
/**
|
|
212
216
|
* The id a link value jumps to when a fragment is ALL it holds — `#watch`
|
|
213
217
|
* gives `watch` — or `undefined` for any other value (AGL-2867).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/screen-link-value.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * What a stored link VALUE means, with no React attached (AGL-703).\n *\n * Split out of `screen-link-context.ts`, which calls `createContext` at module\n * scope and is therefore excluded from the `@aglyn/aglyn/server` barrel by\n * design (AGL-405). The parsing is not client-only, though, and the where-used\n * scan runs on the server: without this split the API route would have had to\n * re-spell `'screen:'` and its own trimming rules, which is exactly how two\n * readers start disagreeing about what a link points at.\n *\n * `screen-link-context.ts` re-exports every name here, so existing importers —\n * all of which reach these through the `@aglyn/aglyn` barrel — are unaffected.\n *\n * RESOLUTION lives here for the same reason parsing does (AGL-2740). The\n * Markdown representation of a page is built in a route handler, from the\n * server barrel that excludes `screen-link-context.ts` — so with the resolver\n * on the far side of that boundary it grew a second, shorter one, which read\n * only the `screenId` slot and emitted a `screen:<id>` value verbatim when it\n * arrived in the `href` slot. One resolver, reachable from both barrels, is\n * what stops a non-HTML representation from disagreeing with the page.\n */\n\nimport type { ScreenRouteMap } from './screen-link-context-value'\n\n/**\n * Prefix marking a stored link value as a screen REFERENCE rather than a\n * literal href (AGL-1335).\n *\n * A `Link`-typed component prop stores its value in the same string slot\n * whichever way it was authored, and every value written before the picker\n * existed is a raw path (`/pricing`). A bare screen id is indistinguishable\n * from a relative path that happens to have no slash, so the id-carrying\n * shape is the one that gets the marker: a legacy string keeps meaning\n * exactly what it always meant, and only newly picked values indirect\n * through the routing map.\n */\nexport const SCREEN_LINK_VALUE_PREFIX = 'screen:'\n\n/**\n * Prefix marking a stored link value as a content collection's LISTING page\n * (AGL-2799): `collection:<collectionId>` is the page at `/{collectionSlug}`.\n *\n * The listing is addressed by the collection's id and never by its slug, for\n * the reason a screen is: `/blog` is where the listing happens to be today,\n * and the id is the part a rename cannot move.\n *\n * The value is ALSO the listing's key in the linkable routing map\n * (`linkableScreenRoutes`), so every reader that resolves a screen id against\n * that map — `resolveScreenHref`, the broken-link verdict, the Tabs strip, the\n * Markdown representation of a page — resolves a listing through the same\n * lookup, with no second path to fall out of step. Generated ids contain no\n * colon, so the key cannot collide with a screen id in the same map.\n *\n * Unlike {@link SCREEN_LINK_VALUE_PREFIX}, this marker is written in every\n * slot the value can occupy, the screen slot included: a bare collection id\n * there would be read as a screen id.\n */\nexport const COLLECTION_LINK_VALUE_PREFIX = 'collection:'\n\n/** Wraps a collection id as a stored listing link — see {@link COLLECTION_LINK_VALUE_PREFIX}. */\nexport function formatCollectionLinkValue(collectionId: string): string {\n return `${COLLECTION_LINK_VALUE_PREFIX}${collectionId.trim()}`\n}\n\n/**\n * The collection id a stored listing link names, or `undefined` for any value\n * that is not a listing link.\n */\nexport function parseCollectionLinkValue(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n if (!trimmed.startsWith(COLLECTION_LINK_VALUE_PREFIX)) return undefined\n const id = trimmed.slice(COLLECTION_LINK_VALUE_PREFIX.length).trim()\n return id || undefined\n}\n\n/**\n * Prefix marking a stored link value as a content collection ENTRY (AGL-3118):\n * `entry:<collectionId>/<entryId>` is the page at `/{collectionSlug}/{entrySlug}`.\n *\n * Both halves are ids, for the reason a listing is addressed by its\n * collection's id: an entry's slug and its collection's slug can each be\n * renamed, and the id pair is the part neither rename moves. A link written\n * once follows both.\n *\n * Like the listing form, the value is the stored form in every slot AND the\n * entry's key in the linkable routing map, so `resolveScreenHref` resolves an\n * entry through the same lookup as a screen. Generated ids contain neither a\n * colon nor a slash, so the key cannot collide with a screen id or a listing.\n * Entry keys are not in every map: a page carries the ones it references.\n */\nexport const ENTRY_LINK_VALUE_PREFIX = 'entry:'\n\n/** Wraps an entry's ids as a stored entry link — see {@link ENTRY_LINK_VALUE_PREFIX}. */\nexport function formatEntryLinkValue(collectionId: string, entryId: string): string {\n return `${ENTRY_LINK_VALUE_PREFIX}${collectionId.trim()}/${entryId.trim()}`\n}\n\n/**\n * The collection and entry ids a stored entry link names, or `undefined` for\n * any value that is not exactly one well-formed entry link.\n */\nexport function parseEntryLinkValue(\n value: unknown,\n): { collectionId: string; entryId: string } | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n if (!trimmed.startsWith(ENTRY_LINK_VALUE_PREFIX)) return undefined\n const parts = trimmed\n .slice(ENTRY_LINK_VALUE_PREFIX.length)\n .split('/')\n .map((part) => part.trim())\n if (parts.length !== 2) return undefined\n const [collectionId, entryId] = parts\n if (!collectionId || !entryId) return undefined\n return { collectionId, entryId }\n}\n\n/**\n * Prefix marking a stored link value as a content collection's RSS FEED\n * (AGL-3118): `feed:<collectionId>` is `/{collectionSlug}/rss.xml`.\n *\n * Addressed by the collection's id for the listing's reason, and keyed in the\n * linkable routing map beside the listing it belongs to, so a feed link\n * follows a renamed collection exactly as its listing link does.\n */\nexport const FEED_LINK_VALUE_PREFIX = 'feed:'\n\n/** Wraps a collection id as a stored feed link — see {@link FEED_LINK_VALUE_PREFIX}. */\nexport function formatFeedLinkValue(collectionId: string): string {\n return `${FEED_LINK_VALUE_PREFIX}${collectionId.trim()}`\n}\n\n/**\n * The collection id a stored feed link names, or `undefined` for any value\n * that is not a feed link.\n */\nexport function parseFeedLinkValue(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n if (!trimmed.startsWith(FEED_LINK_VALUE_PREFIX)) return undefined\n const id = trimmed.slice(FEED_LINK_VALUE_PREFIX.length).trim()\n return id || undefined\n}\n\n/** What a link target key names: a screen, or one of a collection's pages. */\nexport type LinkTargetKind = 'screen' | 'collection' | 'entry' | 'feed'\n\n/**\n * The kind of a routing-map key or stored link value. Anything that is not a\n * listing, entry or feed reference is a screen, which is what a bare id in\n * the map always was.\n */\nexport function linkTargetKind(key: string): LinkTargetKind {\n if (parseCollectionLinkValue(key) !== undefined) return 'collection'\n if (parseEntryLinkValue(key) !== undefined) return 'entry'\n if (parseFeedLinkValue(key) !== undefined) return 'feed'\n return 'screen'\n}\n\n/**\n * Wraps a link target as a stored value — see {@link SCREEN_LINK_VALUE_PREFIX}.\n *\n * Takes what a picker option carries: a screen id, which gains the marker, or\n * a listing, entry or feed key, which is already its own stored form and comes\n * back unchanged.\n */\nexport function formatScreenLinkValue(screenId: string): string {\n const collectionId = parseCollectionLinkValue(screenId)\n if (collectionId) return formatCollectionLinkValue(collectionId)\n const entry = parseEntryLinkValue(screenId)\n if (entry) return formatEntryLinkValue(entry.collectionId, entry.entryId)\n const feedCollectionId = parseFeedLinkValue(screenId)\n if (feedCollectionId) return formatFeedLinkValue(feedCollectionId)\n return `${SCREEN_LINK_VALUE_PREFIX}${screenId}`\n}\n\n/**\n * The routing-map key a stored link value references — a screen id, or a\n * listing's `collection:<id>`, an entry's `entry:<collectionId>/<entryId>` or\n * a feed's `feed:<id>` — or `undefined` when the value is a literal href\n * (legacy raw string, external URL, or unset).\n */\nexport function parseScreenLinkValue(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n const collectionId = parseCollectionLinkValue(trimmed)\n if (collectionId) return formatCollectionLinkValue(collectionId)\n const entry = parseEntryLinkValue(trimmed)\n if (entry) return formatEntryLinkValue(entry.collectionId, entry.entryId)\n const feedCollectionId = parseFeedLinkValue(trimmed)\n if (feedCollectionId) return formatFeedLinkValue(feedCollectionId)\n if (!trimmed.startsWith(SCREEN_LINK_VALUE_PREFIX)) return undefined\n const id = trimmed.slice(SCREEN_LINK_VALUE_PREFIX.length).trim()\n return id || undefined\n}\n\n/**\n * Whether a routing map can say anything about this link target (AGL-1893,\n * AGL-2799, AGL-3118).\n *\n * The map holds several kinds of key read from different places — screens\n * from the host document, listings and feeds from the host's collections,\n * entries from whatever a page references — and any half can be empty while\n * another is not: a console that has received one subscription and not yet\n * the other, a site with a blog and no published screen, or a page that\n * references no entry at all. A map with no key of the target's OWN kind has\n * not heard of that kind, so it is no evidence the target is gone.\n *\n * Compared per kind rather than as \"the map is non-empty\" because the loading\n * beat is exactly where the difference shows: the first collection to arrive\n * would otherwise condemn every screen link on the canvas until the host\n * document caught up, and the host document would condemn every listing link\n * until the collections did.\n */\nexport function screenRoutesAnswerFor(\n screens: ScreenRouteMap | undefined,\n target: string | null | undefined,\n): boolean {\n if (!screens || !target) return false\n const key = parseScreenLinkValue(target) ?? target.trim()\n const wanted = linkTargetKind(key)\n for (const candidate in screens) {\n if (linkTargetKind(candidate) === wanted) return true\n }\n return false\n}\n\n/**\n * Navigable protocols only. A stored `javascript:`/`data:` href would\n * execute in visitors' browsers, so the guard the linking components each\n * carried is here instead — one copy, one place to harden.\n */\nexport const SAFE_HREF_PATTERN = /^(https?:\\/\\/|mailto:|tel:|\\/|#)/i\n\n/** Of those, the ones that actually leave the site (new-tab decisions). */\nexport const EXTERNAL_HREF_PATTERN = /^(https?:\\/\\/|mailto:|tel:)/i\n\n/**\n * The id a link value jumps to when a fragment is ALL it holds — `#watch`\n * gives `watch` — or `undefined` for any other value (AGL-2867).\n *\n * A link field takes paths, so it takes `#watch` too, and on the canvas the\n * element looks linked. On the published page the browser looks for an\n * element with that id, and the elements an author places carry no id they\n * can set, so the press goes nowhere. Two fragments are not that trap and are\n * left out: `#top` (any case), which a browser scrolls to the top of the page\n * with no element needed, and a value holding a `{{…}}` binding, whose text is\n * not known until the page renders. A lone `#` names no element either, and\n * means the top of the page.\n */\nexport function bareFragmentOfLinkValue(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n if (!trimmed.startsWith('#') || trimmed.includes('{{')) return undefined\n const fragment = trimmed.slice(1)\n if (!fragment || fragment.toLowerCase() === 'top') return undefined\n return fragment\n}\n\n/**\n * What a link field says while its value is a bare fragment, or `undefined`\n * (AGL-2867). It names the interaction that does what the fragment was\n * reaching for, because the author is looking at the field, not at the docs.\n */\nexport function bareFragmentLinkWarning(value: unknown): string | undefined {\n const fragment = bareFragmentOfLinkValue(value)\n if (fragment === undefined) return undefined\n return (\n `Elements you place carry no id, so #${fragment} goes nowhere on the ` +\n 'published page. To take visitors to an element, clear this and add an ' +\n 'interaction: When clicked → Scroll to element.'\n )\n}\n\n/**\n * {@link bareFragmentLinkWarning} as a form field's `resolveProps`, for a link\n * attribute in a component schema: the field's helper text carries the warning\n * while the value is a bare fragment, and the field's own description\n * otherwise. Computed from the live value, so it appears as the fragment is\n * typed and clears as it is removed.\n */\nexport function bareFragmentLinkFieldProps(\n _props: unknown,\n field: { input?: { value?: unknown } },\n): { helperText?: string } {\n const warning = bareFragmentLinkWarning(field?.input?.value)\n return warning ? { helperText: warning } : {}\n}\n\n/**\n * Turns a screen id into its current href against a routing map, or\n * `undefined` when there is no id or the id has no entry (unpublished or\n * deleted). Pure and hook-free on purpose: an element that resolves ONE\n * target uses `useScreenLink`, but a row of them — the Tabs strip's per-tab\n * links (AGL-1312) — cannot call a hook per item, and the map-to-path\n * contract (root is `'/'`, everything else gains a leading slash) must have\n * exactly one implementation.\n */\nexport function resolveScreenHref(\n screens: ScreenRouteMap | undefined,\n screenId: string | null | undefined,\n): string | undefined {\n if (!screenId) return undefined\n // A value that arrived through a `Link`-typed component prop (AGL-1335)\n // carries the prefix, because there it has to be distinguishable from the\n // raw path strings those props held before the picker existed. Stripping\n // it HERE rather than at each call site is the same \"one resolver\" rule\n // the doc comment above states: every surface that resolves a screen id\n // must accept both spellings, or a prop-fed tab strip would resolve where\n // a prop-fed button did not.\n const id = parseScreenLinkValue(screenId) ?? screenId\n const path = screens?.[id]\n if (path === undefined) return undefined\n return path === '/' ? '/' : `/${path}`\n}\n\n/**\n * Sorts an element's two link inputs into \"a screen id\" and \"a literal\n * href\", tolerating either value arriving in either slot (AGL-1335).\n *\n * Both slots are string props, and a component prop bound with\n * `{{prop.link}}` can be dropped into whichever one the author reached for\n * first. So the ROUTING is driven by the value's shape, not by which field\n * it sits in:\n *\n * - a `screen:`-prefixed value is a screen reference wherever it appears;\n * - an href-shaped value (`/x`, `https://…`, `#a`, `mailto:`) in the screen\n * slot is a literal href — a real screen id never looks like that, and\n * the alternative is a link that silently resolves to nothing;\n * - anything else in the screen slot is a bare screen id, exactly as before.\n *\n * A resolved screen id always wins: `screenId` has taken precedence over\n * `href` since AGL-139, and this must not change which of the two an\n * already-published page follows.\n */\nexport function splitLinkValue(\n screenId: string | null | undefined,\n href: string | null | undefined,\n): { screenId?: string; href?: string } {\n const rawScreen = typeof screenId === 'string' ? screenId.trim() : ''\n const rawHref = typeof href === 'string' ? href.trim() : ''\n const fromScreenSlot = parseScreenLinkValue(rawScreen)\n if (fromScreenSlot) return { screenId: fromScreenSlot }\n if (rawScreen && !SAFE_HREF_PATTERN.test(rawScreen)) {\n return { screenId: rawScreen }\n }\n const fromHrefSlot = parseScreenLinkValue(rawHref)\n if (fromHrefSlot) return { screenId: fromHrefSlot }\n // An href-shaped value in the screen slot beats an empty href slot, and\n // loses to a real one — the screen slot was never meant to hold a path.\n const literal = rawHref || rawScreen\n return literal ? { href: literal } : {}\n}\n\n/**\n * The target half of a markdown-lite link, `[text](target)` — the same shape\n * the parser keeps, so this finds what a reader will be able to click.\n */\nconst MARKDOWN_LINK_TARGET = /\\]\\(([^)\\s]+)\\)/g\n\n/**\n * Whether a stored node tree links to a given screen (AGL-703).\n *\n * DEEP, unlike {@link nodesReferenceComponent} which reads `props.refId` at\n * one known key. A screen id can sit almost anywhere in a prop bag: the\n * `screenId`/`href` pair every linking element declares, a `Link`-typed\n * component prop the author bound to either slot, and — the case a shallow\n * walk would miss entirely — the ITEM ARRAYS a nav strip, a tab set, or a\n * mega menu store their targets in. Those arrays are where a site's\n * navigation actually lives, so a scan that skipped them would report the\n * home page as linked from nowhere.\n *\n * Two accepted spellings, matching {@link splitLinkValue}'s own rules:\n *\n * - `screen:<id>` — the marked form every picked value has written since\n * AGL-1335;\n * - a bare `<id>` — the legacy form, still live on anything authored before\n * the picker.\n *\n * The bare form is the one that could over-match, and it is allowed to. A\n * screen id is a generated 10-character token, so a prop holding that exact\n * string for some unrelated reason is a theoretical case; and this answers\n * \"what might I break\", where naming one extra document costs a second look\n * and missing one costs a dead link on a live site.\n *\n * A MARKDOWN body counts too (AGL-3118). A link written `[text](collection:…)`\n * inside a Markdown element's content, or inside an entry body, renders as a\n * real link on the page, so whatever it names is as genuinely referenced as a\n * link element's `screenId` — and a delete that ignored it would break the\n * sentence silently.\n */\nexport function nodesReferenceScreen(\n nodes: Record<string, unknown> | null | undefined,\n screenId: string,\n): boolean {\n if (!nodes || !screenId) return false\n const matches = (value: unknown): boolean => {\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed === screenId || parseScreenLinkValue(trimmed) === screenId) {\n return true\n }\n // Only strings that hold a markdown link are worth parsing, which on a\n // page is almost none of them.\n if (!trimmed.includes('](')) return false\n for (const match of trimmed.matchAll(MARKDOWN_LINK_TARGET)) {\n const target = (match[1] ?? '').trim()\n if (target === screenId || parseScreenLinkValue(target) === screenId) {\n return true\n }\n }\n return false\n }\n if (Array.isArray(value)) return value.some(matches)\n if (value && typeof value === 'object') {\n return Object.values(value as Record<string, unknown>).some(matches)\n }\n return false\n }\n for (const node of Object.values(nodes)) {\n const props = (node as { props?: unknown } | undefined)?.props\n if (props && matches(props)) return true\n }\n return false\n}\n"],"names":["SCREEN_LINK_VALUE_PREFIX","COLLECTION_LINK_VALUE_PREFIX","formatCollectionLinkValue","collectionId","trim","parseCollectionLinkValue","value","undefined","trimmed","startsWith","id","slice","length","ENTRY_LINK_VALUE_PREFIX","formatEntryLinkValue","entryId","parseEntryLinkValue","parts","split","map","part","FEED_LINK_VALUE_PREFIX","formatFeedLinkValue","parseFeedLinkValue","linkTargetKind","key","formatScreenLinkValue","screenId","entry","feedCollectionId","parseScreenLinkValue","screenRoutesAnswerFor","screens","target","wanted","candidate","SAFE_HREF_PATTERN","EXTERNAL_HREF_PATTERN","bareFragmentOfLinkValue","includes","fragment","toLowerCase","bareFragmentLinkWarning","bareFragmentLinkFieldProps","_props","field","warning","input","helperText","resolveScreenHref","path","splitLinkValue","href","rawScreen","rawHref","fromScreenSlot","test","fromHrefSlot","literal","MARKDOWN_LINK_TARGET","nodesReferenceScreen","nodes","matches","match","matchAll","Array","isArray","some","Object","values","node","props"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/screen-link-value.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * What a stored link VALUE means, with no React attached (AGL-703).\n *\n * Split out of `screen-link-context.ts`, which calls `createContext` at module\n * scope and is therefore excluded from the `@aglyn/aglyn/server` barrel by\n * design (AGL-405). The parsing is not client-only, though, and the where-used\n * scan runs on the server: without this split the API route would have had to\n * re-spell `'screen:'` and its own trimming rules, which is exactly how two\n * readers start disagreeing about what a link points at.\n *\n * `screen-link-context.ts` re-exports every name here, so existing importers —\n * all of which reach these through the `@aglyn/aglyn` barrel — are unaffected.\n *\n * RESOLUTION lives here for the same reason parsing does (AGL-2740). The\n * Markdown representation of a page is built in a route handler, from the\n * server barrel that excludes `screen-link-context.ts` — so with the resolver\n * on the far side of that boundary it grew a second, shorter one, which read\n * only the `screenId` slot and emitted a `screen:<id>` value verbatim when it\n * arrived in the `href` slot. One resolver, reachable from both barrels, is\n * what stops a non-HTML representation from disagreeing with the page.\n */\n\nimport type { ScreenRouteMap } from './screen-link-context-value'\n\n/**\n * Prefix marking a stored link value as a screen REFERENCE rather than a\n * literal href (AGL-1335).\n *\n * A `Link`-typed component prop stores its value in the same string slot\n * whichever way it was authored, and every value written before the picker\n * existed is a raw path (`/pricing`). A bare screen id is indistinguishable\n * from a relative path that happens to have no slash, so the id-carrying\n * shape is the one that gets the marker: a legacy string keeps meaning\n * exactly what it always meant, and only newly picked values indirect\n * through the routing map.\n */\nexport const SCREEN_LINK_VALUE_PREFIX = 'screen:'\n\n/**\n * Prefix marking a stored link value as a content collection's LISTING page\n * (AGL-2799): `collection:<collectionId>` is the page at `/{collectionSlug}`.\n *\n * The listing is addressed by the collection's id and never by its slug, for\n * the reason a screen is: `/blog` is where the listing happens to be today,\n * and the id is the part a rename cannot move.\n *\n * The value is ALSO the listing's key in the linkable routing map\n * (`linkableScreenRoutes`), so every reader that resolves a screen id against\n * that map — `resolveScreenHref`, the broken-link verdict, the Tabs strip, the\n * Markdown representation of a page — resolves a listing through the same\n * lookup, with no second path to fall out of step. Generated ids contain no\n * colon, so the key cannot collide with a screen id in the same map.\n *\n * Unlike {@link SCREEN_LINK_VALUE_PREFIX}, this marker is written in every\n * slot the value can occupy, the screen slot included: a bare collection id\n * there would be read as a screen id.\n */\nexport const COLLECTION_LINK_VALUE_PREFIX = 'collection:'\n\n/** Wraps a collection id as a stored listing link — see {@link COLLECTION_LINK_VALUE_PREFIX}. */\nexport function formatCollectionLinkValue(collectionId: string): string {\n return `${COLLECTION_LINK_VALUE_PREFIX}${collectionId.trim()}`\n}\n\n/**\n * The collection id a stored listing link names, or `undefined` for any value\n * that is not a listing link.\n */\nexport function parseCollectionLinkValue(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n if (!trimmed.startsWith(COLLECTION_LINK_VALUE_PREFIX)) return undefined\n const id = trimmed.slice(COLLECTION_LINK_VALUE_PREFIX.length).trim()\n return id || undefined\n}\n\n/**\n * Prefix marking a stored link value as a content collection ENTRY (AGL-3118):\n * `entry:<collectionId>/<entryId>` is the page at `/{collectionSlug}/{entrySlug}`.\n *\n * Both halves are ids, for the reason a listing is addressed by its\n * collection's id: an entry's slug and its collection's slug can each be\n * renamed, and the id pair is the part neither rename moves. A link written\n * once follows both.\n *\n * Like the listing form, the value is the stored form in every slot AND the\n * entry's key in the linkable routing map, so `resolveScreenHref` resolves an\n * entry through the same lookup as a screen. Generated ids contain neither a\n * colon nor a slash, so the key cannot collide with a screen id or a listing.\n * Entry keys are not in every map: a page carries the ones it references.\n */\nexport const ENTRY_LINK_VALUE_PREFIX = 'entry:'\n\n/** Wraps an entry's ids as a stored entry link — see {@link ENTRY_LINK_VALUE_PREFIX}. */\nexport function formatEntryLinkValue(collectionId: string, entryId: string): string {\n return `${ENTRY_LINK_VALUE_PREFIX}${collectionId.trim()}/${entryId.trim()}`\n}\n\n/**\n * The collection and entry ids a stored entry link names, or `undefined` for\n * any value that is not exactly one well-formed entry link.\n */\nexport function parseEntryLinkValue(\n value: unknown,\n): { collectionId: string; entryId: string } | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n if (!trimmed.startsWith(ENTRY_LINK_VALUE_PREFIX)) return undefined\n const parts = trimmed\n .slice(ENTRY_LINK_VALUE_PREFIX.length)\n .split('/')\n .map((part) => part.trim())\n if (parts.length !== 2) return undefined\n const [collectionId, entryId] = parts\n if (!collectionId || !entryId) return undefined\n return { collectionId, entryId }\n}\n\n/**\n * Prefix marking a stored link value as a content collection's RSS FEED\n * (AGL-3118): `feed:<collectionId>` is `/{collectionSlug}/rss.xml`.\n *\n * Addressed by the collection's id for the listing's reason, and keyed in the\n * linkable routing map beside the listing it belongs to, so a feed link\n * follows a renamed collection exactly as its listing link does.\n */\nexport const FEED_LINK_VALUE_PREFIX = 'feed:'\n\n/** Wraps a collection id as a stored feed link — see {@link FEED_LINK_VALUE_PREFIX}. */\nexport function formatFeedLinkValue(collectionId: string): string {\n return `${FEED_LINK_VALUE_PREFIX}${collectionId.trim()}`\n}\n\n/**\n * The collection id a stored feed link names, or `undefined` for any value\n * that is not a feed link.\n */\nexport function parseFeedLinkValue(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n if (!trimmed.startsWith(FEED_LINK_VALUE_PREFIX)) return undefined\n const id = trimmed.slice(FEED_LINK_VALUE_PREFIX.length).trim()\n return id || undefined\n}\n\n/** What a link target key names: a screen, or one of a collection's pages. */\nexport type LinkTargetKind = 'screen' | 'collection' | 'entry' | 'feed'\n\n/**\n * The kind of a routing-map key or stored link value. Anything that is not a\n * listing, entry or feed reference is a screen, which is what a bare id in\n * the map always was.\n */\nexport function linkTargetKind(key: string): LinkTargetKind {\n if (parseCollectionLinkValue(key) !== undefined) return 'collection'\n if (parseEntryLinkValue(key) !== undefined) return 'entry'\n if (parseFeedLinkValue(key) !== undefined) return 'feed'\n return 'screen'\n}\n\n/**\n * Wraps a link target as a stored value — see {@link SCREEN_LINK_VALUE_PREFIX}.\n *\n * Takes what a picker option carries: a screen id, which gains the marker, or\n * a listing, entry or feed key, which is already its own stored form and comes\n * back unchanged.\n */\nexport function formatScreenLinkValue(screenId: string): string {\n const collectionId = parseCollectionLinkValue(screenId)\n if (collectionId) return formatCollectionLinkValue(collectionId)\n const entry = parseEntryLinkValue(screenId)\n if (entry) return formatEntryLinkValue(entry.collectionId, entry.entryId)\n const feedCollectionId = parseFeedLinkValue(screenId)\n if (feedCollectionId) return formatFeedLinkValue(feedCollectionId)\n return `${SCREEN_LINK_VALUE_PREFIX}${screenId}`\n}\n\n/**\n * The routing-map key a stored link value references — a screen id, or a\n * listing's `collection:<id>`, an entry's `entry:<collectionId>/<entryId>` or\n * a feed's `feed:<id>` — or `undefined` when the value is a literal href\n * (legacy raw string, external URL, or unset).\n */\nexport function parseScreenLinkValue(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n const collectionId = parseCollectionLinkValue(trimmed)\n if (collectionId) return formatCollectionLinkValue(collectionId)\n const entry = parseEntryLinkValue(trimmed)\n if (entry) return formatEntryLinkValue(entry.collectionId, entry.entryId)\n const feedCollectionId = parseFeedLinkValue(trimmed)\n if (feedCollectionId) return formatFeedLinkValue(feedCollectionId)\n if (!trimmed.startsWith(SCREEN_LINK_VALUE_PREFIX)) return undefined\n const id = trimmed.slice(SCREEN_LINK_VALUE_PREFIX.length).trim()\n return id || undefined\n}\n\n/**\n * Whether a routing map can say anything about this link target (AGL-1893,\n * AGL-2799, AGL-3118).\n *\n * The map holds several kinds of key read from different places — screens\n * from the host document, listings and feeds from the host's collections,\n * entries from whatever a page references — and any half can be empty while\n * another is not: a console that has received one subscription and not yet\n * the other, a site with a blog and no published screen, or a page that\n * references no entry at all. A map with no key of the target's OWN kind has\n * not heard of that kind, so it is no evidence the target is gone.\n *\n * Compared per kind rather than as \"the map is non-empty\" because the loading\n * beat is exactly where the difference shows: the first collection to arrive\n * would otherwise condemn every screen link on the canvas until the host\n * document caught up, and the host document would condemn every listing link\n * until the collections did.\n */\nexport function screenRoutesAnswerFor(\n screens: ScreenRouteMap | undefined,\n target: string | null | undefined,\n): boolean {\n if (!screens || !target) return false\n const key = parseScreenLinkValue(target) ?? target.trim()\n const wanted = linkTargetKind(key)\n for (const candidate in screens) {\n if (linkTargetKind(candidate) === wanted) return true\n }\n return false\n}\n\n/**\n * Navigable protocols only. A stored `javascript:`/`data:` href would\n * execute in visitors' browsers, so the guard the linking components each\n * carried is here instead — one copy, one place to harden.\n *\n * `sms:` sits beside `tel:` (AGL-3388): it opens the visitor's messaging app\n * and executes nothing, the same list `safe-url-scheme.ts` keeps. Leaving it\n * out rendered every \"Text us\" button as a button with no link.\n */\nexport const SAFE_HREF_PATTERN = /^(https?:\\/\\/|mailto:|tel:|sms:|\\/|#)/i\n\n/** Of those, the ones that actually leave the site (new-tab decisions). */\nexport const EXTERNAL_HREF_PATTERN = /^(https?:\\/\\/|mailto:|tel:|sms:)/i\n\n/**\n * The id a link value jumps to when a fragment is ALL it holds — `#watch`\n * gives `watch` — or `undefined` for any other value (AGL-2867).\n *\n * A link field takes paths, so it takes `#watch` too, and on the canvas the\n * element looks linked. On the published page the browser looks for an\n * element with that id, and the elements an author places carry no id they\n * can set, so the press goes nowhere. Two fragments are not that trap and are\n * left out: `#top` (any case), which a browser scrolls to the top of the page\n * with no element needed, and a value holding a `{{…}}` binding, whose text is\n * not known until the page renders. A lone `#` names no element either, and\n * means the top of the page.\n */\nexport function bareFragmentOfLinkValue(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n if (!trimmed.startsWith('#') || trimmed.includes('{{')) return undefined\n const fragment = trimmed.slice(1)\n if (!fragment || fragment.toLowerCase() === 'top') return undefined\n return fragment\n}\n\n/**\n * What a link field says while its value is a bare fragment, or `undefined`\n * (AGL-2867). It names the interaction that does what the fragment was\n * reaching for, because the author is looking at the field, not at the docs.\n */\nexport function bareFragmentLinkWarning(value: unknown): string | undefined {\n const fragment = bareFragmentOfLinkValue(value)\n if (fragment === undefined) return undefined\n return (\n `Elements you place carry no id, so #${fragment} goes nowhere on the ` +\n 'published page. To take visitors to an element, clear this and add an ' +\n 'interaction: When clicked → Scroll to element.'\n )\n}\n\n/**\n * {@link bareFragmentLinkWarning} as a form field's `resolveProps`, for a link\n * attribute in a component schema: the field's helper text carries the warning\n * while the value is a bare fragment, and the field's own description\n * otherwise. Computed from the live value, so it appears as the fragment is\n * typed and clears as it is removed.\n */\nexport function bareFragmentLinkFieldProps(\n _props: unknown,\n field: { input?: { value?: unknown } },\n): { helperText?: string } {\n const warning = bareFragmentLinkWarning(field?.input?.value)\n return warning ? { helperText: warning } : {}\n}\n\n/**\n * Turns a screen id into its current href against a routing map, or\n * `undefined` when there is no id or the id has no entry (unpublished or\n * deleted). Pure and hook-free on purpose: an element that resolves ONE\n * target uses `useScreenLink`, but a row of them — the Tabs strip's per-tab\n * links (AGL-1312) — cannot call a hook per item, and the map-to-path\n * contract (root is `'/'`, everything else gains a leading slash) must have\n * exactly one implementation.\n */\nexport function resolveScreenHref(\n screens: ScreenRouteMap | undefined,\n screenId: string | null | undefined,\n): string | undefined {\n if (!screenId) return undefined\n // A value that arrived through a `Link`-typed component prop (AGL-1335)\n // carries the prefix, because there it has to be distinguishable from the\n // raw path strings those props held before the picker existed. Stripping\n // it HERE rather than at each call site is the same \"one resolver\" rule\n // the doc comment above states: every surface that resolves a screen id\n // must accept both spellings, or a prop-fed tab strip would resolve where\n // a prop-fed button did not.\n const id = parseScreenLinkValue(screenId) ?? screenId\n const path = screens?.[id]\n if (path === undefined) return undefined\n return path === '/' ? '/' : `/${path}`\n}\n\n/**\n * Sorts an element's two link inputs into \"a screen id\" and \"a literal\n * href\", tolerating either value arriving in either slot (AGL-1335).\n *\n * Both slots are string props, and a component prop bound with\n * `{{prop.link}}` can be dropped into whichever one the author reached for\n * first. So the ROUTING is driven by the value's shape, not by which field\n * it sits in:\n *\n * - a `screen:`-prefixed value is a screen reference wherever it appears;\n * - an href-shaped value (`/x`, `https://…`, `#a`, `mailto:`) in the screen\n * slot is a literal href — a real screen id never looks like that, and\n * the alternative is a link that silently resolves to nothing;\n * - anything else in the screen slot is a bare screen id, exactly as before.\n *\n * A resolved screen id always wins: `screenId` has taken precedence over\n * `href` since AGL-139, and this must not change which of the two an\n * already-published page follows.\n */\nexport function splitLinkValue(\n screenId: string | null | undefined,\n href: string | null | undefined,\n): { screenId?: string; href?: string } {\n const rawScreen = typeof screenId === 'string' ? screenId.trim() : ''\n const rawHref = typeof href === 'string' ? href.trim() : ''\n const fromScreenSlot = parseScreenLinkValue(rawScreen)\n if (fromScreenSlot) return { screenId: fromScreenSlot }\n if (rawScreen && !SAFE_HREF_PATTERN.test(rawScreen)) {\n return { screenId: rawScreen }\n }\n const fromHrefSlot = parseScreenLinkValue(rawHref)\n if (fromHrefSlot) return { screenId: fromHrefSlot }\n // An href-shaped value in the screen slot beats an empty href slot, and\n // loses to a real one — the screen slot was never meant to hold a path.\n const literal = rawHref || rawScreen\n return literal ? { href: literal } : {}\n}\n\n/**\n * The target half of a markdown-lite link, `[text](target)` — the same shape\n * the parser keeps, so this finds what a reader will be able to click.\n */\nconst MARKDOWN_LINK_TARGET = /\\]\\(([^)\\s]+)\\)/g\n\n/**\n * Whether a stored node tree links to a given screen (AGL-703).\n *\n * DEEP, unlike {@link nodesReferenceComponent} which reads `props.refId` at\n * one known key. A screen id can sit almost anywhere in a prop bag: the\n * `screenId`/`href` pair every linking element declares, a `Link`-typed\n * component prop the author bound to either slot, and — the case a shallow\n * walk would miss entirely — the ITEM ARRAYS a nav strip, a tab set, or a\n * mega menu store their targets in. Those arrays are where a site's\n * navigation actually lives, so a scan that skipped them would report the\n * home page as linked from nowhere.\n *\n * Two accepted spellings, matching {@link splitLinkValue}'s own rules:\n *\n * - `screen:<id>` — the marked form every picked value has written since\n * AGL-1335;\n * - a bare `<id>` — the legacy form, still live on anything authored before\n * the picker.\n *\n * The bare form is the one that could over-match, and it is allowed to. A\n * screen id is a generated 10-character token, so a prop holding that exact\n * string for some unrelated reason is a theoretical case; and this answers\n * \"what might I break\", where naming one extra document costs a second look\n * and missing one costs a dead link on a live site.\n *\n * A MARKDOWN body counts too (AGL-3118). A link written `[text](collection:…)`\n * inside a Markdown element's content, or inside an entry body, renders as a\n * real link on the page, so whatever it names is as genuinely referenced as a\n * link element's `screenId` — and a delete that ignored it would break the\n * sentence silently.\n */\nexport function nodesReferenceScreen(\n nodes: Record<string, unknown> | null | undefined,\n screenId: string,\n): boolean {\n if (!nodes || !screenId) return false\n const matches = (value: unknown): boolean => {\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (trimmed === screenId || parseScreenLinkValue(trimmed) === screenId) {\n return true\n }\n // Only strings that hold a markdown link are worth parsing, which on a\n // page is almost none of them.\n if (!trimmed.includes('](')) return false\n for (const match of trimmed.matchAll(MARKDOWN_LINK_TARGET)) {\n const target = (match[1] ?? '').trim()\n if (target === screenId || parseScreenLinkValue(target) === screenId) {\n return true\n }\n }\n return false\n }\n if (Array.isArray(value)) return value.some(matches)\n if (value && typeof value === 'object') {\n return Object.values(value as Record<string, unknown>).some(matches)\n }\n return false\n }\n for (const node of Object.values(nodes)) {\n const props = (node as { props?: unknown } | undefined)?.props\n if (props && matches(props)) return true\n }\n return false\n}\n"],"names":["SCREEN_LINK_VALUE_PREFIX","COLLECTION_LINK_VALUE_PREFIX","formatCollectionLinkValue","collectionId","trim","parseCollectionLinkValue","value","undefined","trimmed","startsWith","id","slice","length","ENTRY_LINK_VALUE_PREFIX","formatEntryLinkValue","entryId","parseEntryLinkValue","parts","split","map","part","FEED_LINK_VALUE_PREFIX","formatFeedLinkValue","parseFeedLinkValue","linkTargetKind","key","formatScreenLinkValue","screenId","entry","feedCollectionId","parseScreenLinkValue","screenRoutesAnswerFor","screens","target","wanted","candidate","SAFE_HREF_PATTERN","EXTERNAL_HREF_PATTERN","bareFragmentOfLinkValue","includes","fragment","toLowerCase","bareFragmentLinkWarning","bareFragmentLinkFieldProps","_props","field","warning","input","helperText","resolveScreenHref","path","splitLinkValue","href","rawScreen","rawHref","fromScreenSlot","test","fromHrefSlot","literal","MARKDOWN_LINK_TARGET","nodesReferenceScreen","nodes","matches","match","matchAll","Array","isArray","some","Object","values","node","props"],"mappings":"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"}
|