@aglyn/aglyn 1.0.0-beta.199 → 1.0.0-beta.201
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/activity-search.d.ts +39 -0
- package/src/lib/app-utils/activity-search.js +85 -0
- package/src/lib/app-utils/activity-search.js.map +1 -0
- package/src/lib/app-utils/admin-audit-index.d.ts +104 -0
- package/src/lib/app-utils/admin-audit-index.js +177 -0
- package/src/lib/app-utils/admin-audit-index.js.map +1 -0
- package/src/lib/app-utils/artifact-list-keys.d.ts +51 -0
- package/src/lib/app-utils/artifact-list-keys.js +154 -0
- package/src/lib/app-utils/artifact-list-keys.js.map +1 -0
- package/src/lib/app-utils/content-query-fields.d.ts +69 -0
- package/src/lib/app-utils/content-query-fields.js +79 -0
- package/src/lib/app-utils/content-query-fields.js.map +1 -0
- package/src/lib/app-utils/crm.d.ts +170 -1
- package/src/lib/app-utils/crm.js +414 -1
- package/src/lib/app-utils/crm.js.map +1 -1
- package/src/lib/app-utils/dataset-models.d.ts +75 -24
- package/src/lib/app-utils/dataset-models.js +107 -40
- package/src/lib/app-utils/dataset-models.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +4 -4
- package/src/lib/app-utils/docs-help.generated.js +10 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +89 -75
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/email-search.d.ts +36 -0
- package/src/lib/app-utils/email-search.js +53 -0
- package/src/lib/app-utils/email-search.js.map +1 -0
- package/src/lib/app-utils/email-state.d.ts +7 -3
- package/src/lib/app-utils/email-state.js +22 -6
- package/src/lib/app-utils/email-state.js.map +1 -1
- package/src/lib/app-utils/forms.d.ts +178 -6
- package/src/lib/app-utils/forms.js +197 -0
- package/src/lib/app-utils/forms.js.map +1 -1
- package/src/lib/app-utils/health-report.d.ts +3 -3
- package/src/lib/app-utils/health-report.js +9 -9
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/host-naming.d.ts +12 -7
- package/src/lib/app-utils/host-naming.js +18 -3
- package/src/lib/app-utils/host-naming.js.map +1 -1
- package/src/lib/app-utils/lockdown.d.ts +19 -0
- package/src/lib/app-utils/lockdown.js +21 -0
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/marketplace-listing-visibility.d.ts +7 -6
- package/src/lib/app-utils/marketplace-listing-visibility.js +12 -10
- package/src/lib/app-utils/marketplace-listing-visibility.js.map +1 -1
- package/src/lib/app-utils/media-embedded-fields.d.ts +40 -0
- package/src/lib/app-utils/media-embedded-fields.js +77 -0
- package/src/lib/app-utils/media-embedded-fields.js.map +1 -1
- package/src/lib/app-utils/media-metadata.d.ts +95 -0
- package/src/lib/app-utils/media-metadata.js +94 -0
- package/src/lib/app-utils/media-metadata.js.map +1 -1
- package/src/lib/app-utils/message-search.d.ts +40 -0
- package/src/lib/app-utils/message-search.js +168 -0
- package/src/lib/app-utils/message-search.js.map +1 -0
- package/src/lib/app-utils/name-search.d.ts +83 -0
- package/src/lib/app-utils/name-search.js +97 -0
- package/src/lib/app-utils/name-search.js.map +1 -1
- package/src/lib/app-utils/payment-risk.d.ts +75 -0
- package/src/lib/app-utils/payment-risk.js +139 -0
- package/src/lib/app-utils/payment-risk.js.map +1 -0
- package/src/lib/app-utils/plugin-api-request-host.d.ts +29 -0
- package/src/lib/app-utils/plugin-api-request-host.js +90 -0
- package/src/lib/app-utils/plugin-api-request-host.js.map +1 -0
- package/src/lib/app-utils/plugin-bundle-checks.d.ts +3 -2
- package/src/lib/app-utils/plugin-bundle-checks.js +76 -5
- package/src/lib/app-utils/plugin-bundle-checks.js.map +1 -1
- package/src/lib/app-utils/server.d.ts +2 -0
- package/src/lib/app-utils/server.js +2 -0
- package/src/lib/app-utils/server.js.map +1 -1
- package/src/lib/app-utils/webhook-delivery.js +4 -1
- package/src/lib/app-utils/webhook-delivery.js.map +1 -1
- package/src/lib/plugin-manager/billing-webhook-hooks.d.ts +9 -0
- package/src/lib/plugin-manager/billing-webhook-hooks.js +5 -1
- package/src/lib/plugin-manager/billing-webhook-hooks.js.map +1 -1
- package/src/lib/plugin-manager/plugin-activity-actions.d.ts +9 -1
- package/src/lib/plugin-manager/plugin-activity-actions.js +11 -1
- package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-contact-capture.d.ts +8 -0
- package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
- package/src/lib/plugin-manager/plugin-events.d.ts +22 -0
- package/src/lib/plugin-manager/plugin-events.js.map +1 -1
- package/src/lib/plugin-manager/plugin-jobs.d.ts +8 -4
- package/src/lib/plugin-manager/plugin-jobs.js +8 -4
- package/src/lib/plugin-manager/plugin-jobs.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/media-metadata.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 * Asset metadata helpers (AGL-173): tag normalization shared by the\n * console editor and any future API validation, plus a dependency-free\n * image header parser so the upload route can stamp dimensions without\n * pulling in an image library.\n */\n\nimport { MEDIA_ALT_MAX_LENGTH } from './media-alt'\n\nexport * from './media-alt'\n\nexport const MEDIA_TAG_MAX_COUNT = 20\nexport const MEDIA_TAG_MAX_LENGTH = 40\n\n/**\n * Trim, lowercase, dedupe, and cap tags. Accepts a comma-separated string\n * or an array; empty and oversized entries are dropped.\n */\nexport function normalizeMediaTags(input: string | string[]): string[] {\n const raw = Array.isArray(input) ? input : String(input ?? '').split(',')\n const seen = new Set<string>()\n const tags: string[] = []\n for (const entry of raw) {\n const tag = String(entry ?? '').trim().toLowerCase()\n if (!tag || tag.length > MEDIA_TAG_MAX_LENGTH || seen.has(tag)) continue\n seen.add(tag)\n tags.push(tag)\n if (tags.length >= MEDIA_TAG_MAX_COUNT) break\n }\n return tags\n}\n\n/**\n * What a placement should store for `alt` once the asset's own alt text is\n * taken into account (AGL-1896) — the DAM asset is the DEFAULT, the\n * placement keeps the override.\n *\n * `AglynHostMedia.alt` has existed since AGL-173 and the library drawer has\n * always been able to set it; what never existed is anybody READING it. Every\n * placement surface asked the author to type alt again from scratch, so the\n * same logo on eight pages needed its alt typed eight times and in practice\n * shipped blank — on a customer's published site.\n *\n * ONE function, called at pick time by every surface, rather than a rule\n * re-derived per surface. The three refusals are the whole contract:\n *\n * * **`decorative` wins outright.** It is the field AGL-1305 added to record\n * \"screen readers should skip this\", and `image.tsx` already forces\n * `alt=\"\"` over any alt text when it is on. Inheriting into a node that has\n * declared itself decorative would put text on a node whose renderer\n * discards it — invisible, and misleading to the next author who opens the\n * panel.\n * * **A non-blank placement alt wins.** That is the per-placement override,\n * and clobbering it is the one failure that would make this feature worse\n * than not having it: the author's sentence about THIS placement is better\n * than the asset's generic one by construction.\n * * **A blank asset alt yields nothing.** Never a fabricated default. The\n * file name is not alt text (\"IMG_4021.jpg\" announced to a screen reader is\n * worse than silence), and nothing here has seen the image. Returning\n * `undefined` is what lets callers omit the key entirely rather than\n * writing `alt: ''`, which on a besigner node is itself an authored value.\n *\n * A blank placement alt DOES inherit, deliberately. Presets ship `alt: ''`\n * (see `card.tsx`), so requiring an absent key would have skipped the single\n * commonest authoring path — dropping a preset and pointing its image at a\n * library asset — and left the issue open for the case it was filed about.\n *\n * @returns the alt to store, or `undefined` when the caller should write\n * nothing at all.\n */\nexport function inheritedMediaAlt(options: {\n /** The alt already on the placement — a node prop, a config field. */\n placementAlt?: unknown\n /** The placement's explicit \"skip me\" intent, when it has one. */\n decorative?: unknown\n /** The chosen DAM asset's stored alt text. */\n assetAlt?: unknown\n}): string | undefined {\n const { placementAlt, decorative, assetAlt } = options ?? {}\n if (decorative === true) return undefined\n if (typeof placementAlt === 'string' && placementAlt.trim()) return undefined\n const inherited = typeof assetAlt === 'string' ? assetAlt.trim() : ''\n if (!inherited) return undefined\n // Capped at the same length the library drawer saves through, so an alt\n // that reaches a placement is one the DAM would also have stored.\n return inherited.slice(0, MEDIA_ALT_MAX_LENGTH)\n}\n\n/**\n * Component ids whose renderer reads intrinsic pixel dimensions off the node.\n *\n * Component ids are persisted in screen documents and never renamed, which is\n * what makes matching on them safe. The list is the gate rather than a\n * decoration: a prop written onto an element that does not destructure it\n * reaches `...rest` and is spread onto the DOM, so an unlisted element would\n * gain an invalid `intrinsicwidth` attribute in its published HTML.\n *\n * ⛔ `video` is NOT here, and its absence is a decision rather than an\n * oversight (AGL-2749). A video needs the pair at least as badly as an image\n * does — `preload=\"none\"` means its metadata never arrives until someone\n * presses play, so the element is zero-height for the whole life of the page\n * without it — but it cannot get it from `width`/`height`. Those are read\n * from the bytes by the server and are absent on every video, deliberately:\n * AGL-2742 kept the client-measured triple in its own `video` record so a\n * reader can tell a server measurement from a browser's report.\n * {@link videoMediaProps} is where a video gets its pair.\n */\nconst INTRINSIC_SIZE_COMPONENT_IDS = new Set(['image'])\n\n/**\n * The intrinsic `width`/`height` to copy onto a node when an author picks a\n * library asset for it (AGL-2486).\n *\n * ## Why the copy happens at pick time, and why it is not the last word\n *\n * The pair rides on the node like every other prop, so the editor canvas, and\n * any page whose asset cannot be read, still reserve a box. It is not the last\n * word. A replace rewrites the asset's `width`/`height` and cannot reach the\n * nodes that copied them, so the tenant composition reads every placed\n * image's document, in one projected batch per page, and lays its current\n * pair over this one (`media-asset-facts.ts`, AGL-2833). What is written here\n * is what a page shows when that read cannot answer.\n *\n * ## Why it matters\n *\n * `image.tsx` lays images out with `width: 100%; height: auto`, under which an\n * `<img>` has NO height until its bytes decode. Without an intrinsic pair the\n * browser has no ratio to reserve a box from, so every image on the page\n * shifts the content below it as it lands.\n *\n * ## The rules\n *\n * * **Both or neither.** A browser derives an aspect-ratio only from the\n * pair; a lone `width` is read as a real dimension and reserves a box of\n * the wrong shape, which is worse than reserving none.\n * * **Only for renderers that read them**, see\n * `INTRINSIC_SIZE_COMPONENT_IDS` — otherwise the prop lands on the DOM.\n * * **Finite and positive.** Upload capture is best-effort, so a media\n * document may carry `0`, a partial capture, or nothing at all; `width=\"0\"`\n * collapses the element.\n * * **`{}` when anything is unknown**, never `{ intrinsicWidth: undefined }`.\n * Callers spread the result into a props object that `updateNodeProps`\n * REPLACES wholesale, so a key present with an undefined value would strip\n * a pair a previous pick had correctly stored.\n *\n * @returns the props to spread, or `{}` when the caller should write nothing.\n */\nexport function intrinsicMediaSize(options: {\n /** The element being written to — its persisted component id. */\n componentId?: unknown\n /** The attribute the picker was opened for; only `src` carries an image. */\n propName?: unknown\n /** The chosen asset's stored pixel width. */\n assetWidth?: unknown\n /** The chosen asset's stored pixel height. */\n assetHeight?: unknown\n}): { intrinsicWidth?: number; intrinsicHeight?: number } {\n const { componentId, propName, assetWidth, assetHeight } = options ?? {}\n if (propName !== 'src') return {}\n if (!INTRINSIC_SIZE_COMPONENT_IDS.has(String(componentId ?? ''))) return {}\n const usable = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0\n if (!usable(assetWidth) || !usable(assetHeight)) return {}\n return { intrinsicWidth: assetWidth, intrinsicHeight: assetHeight }\n}\n\n/** The one element that reads the props below off its node. */\nconst VIDEO_COMPONENT_ID = 'video'\n\n/**\n * The video-only companions to {@link intrinsicMediaSize}, copied onto the\n * node when an author picks a video asset (AGL-2741, rewritten against the\n * real document shape in AGL-2749).\n *\n * Same route as the image dimensions, and like theirs not the last word. A\n * replace rewrites the asset's `video` and `poster` records and cannot reach\n * the nodes that copied them, so the tenant composition reads each placed\n * film's document, in the same batch as the images', and lays its current\n * records over these (`media-asset-facts.ts`, AGL-2807). What is written here\n * is what a page shows when that read cannot answer.\n *\n * ## Why a video does not simply use {@link intrinsicMediaSize}\n *\n * A video's pixel dimensions are NOT in `media.width`/`media.height`. Those\n * are read from the bytes by the server and carry a stronger claim than a\n * browser's report, so AGL-2742 kept the client-measured triple in its own\n * `video` record rather than widening them — a reader that folded the two\n * together would lose the ability to tell which it had. A video therefore\n * reaches its intrinsic pair through here, and `intrinsicMediaSize` finds\n * nothing to copy for one.\n *\n * ## The three rules\n *\n * * **Only the `video` element, only its `src`.** A `durationSeconds` spread\n * onto an element that does not destructure it becomes an invalid attribute\n * in the published HTML.\n * * **The duration is stored in SECONDS**, because the author-facing field is\n * in seconds and a person types 63, not 63000. `videoDurationIso8601` takes\n * milliseconds, and the structured-data builder is the one place that\n * converts back.\n * * **`{}` when unknown**, never a key with an `undefined` value: callers\n * spread this into a props object `updateNodeProps` REPLACES wholesale, so\n * a present-but-undefined key strips what an earlier pick stored.\n *\n * `posterFromSource` is a FLAG rather than a url, deliberately. The generated\n * poster is `?poster=1` on the video's own reference, which\n * {@link mediaPosterSrc} builds without reading anything — and that url is\n * explicitly not a promise the poster exists. This flag IS the promise: it is\n * written only when the document records one, which is what lets\n * `videoPosterSrc` offer the derived url to an `<img>` and to a\n * `thumbnailUrl`, where a 404 would be a broken image and a rich result\n * pointing at nothing.\n *\n * An author's own poster is never touched here. It wins at render time\n * instead (`videoPosterSrc`), so re-picking the SOURCE cannot quietly replace\n * a frame somebody chose.\n *\n * @returns the props to spread, or `{}` when the caller should write nothing.\n */\nexport function videoMediaProps(options: {\n /** The element being written to — its persisted component id. */\n componentId?: unknown\n /** The attribute the picker was opened for; only `src` carries a video. */\n propName?: unknown\n /**\n * The chosen asset's `video` record, as {@link normalizeVideoMetadata}\n * bounds it — `durationMs`, `width` and `height`, all three or none.\n */\n assetVideo?: unknown\n /** The chosen asset's generated `poster` record, when it has one. */\n assetPoster?: unknown\n}): {\n durationSeconds?: number\n intrinsicWidth?: number\n intrinsicHeight?: number\n posterFromSource?: boolean\n} {\n const { componentId, propName, assetVideo, assetPoster } = options ?? {}\n if (propName !== 'src') return {}\n if (String(componentId ?? '') !== VIDEO_COMPONENT_ID) return {}\n const patch: {\n durationSeconds?: number\n intrinsicWidth?: number\n intrinsicHeight?: number\n posterFromSource?: boolean\n } = {}\n // Through the DAM's own validator rather than a second reading of the same\n // three numbers: it is all-or-nothing by design, and re-deriving that rule\n // here is how two files come to disagree about what a partial record means.\n const video = normalizeVideoMetadata(assetVideo)\n if (video) {\n // Never rounds to zero: a sub-second clip is still a clip, and a stored\n // `0` reads as \"no duration\" to everything downstream.\n patch.durationSeconds = Math.max(1, Math.round(video.durationMs / 1000))\n patch.intrinsicWidth = video.width\n patch.intrinsicHeight = video.height\n }\n // A poster RECORD, not a poster url — see the note above. Its mere presence\n // on the document is the whole fact being carried.\n if (assetPoster && typeof assetPoster === 'object') {\n patch.posterFromSource = true\n }\n return patch\n}\n\nexport interface ImageDimensions {\n width: number\n height: number\n}\n\nconst readU32BE = (bytes: Uint8Array, offset: number) =>\n (bytes[offset] << 24) |\n (bytes[offset + 1] << 16) |\n (bytes[offset + 2] << 8) |\n bytes[offset + 3]\n\nconst readU16BE = (bytes: Uint8Array, offset: number) =>\n (bytes[offset] << 8) | bytes[offset + 1]\n\nconst readU16LE = (bytes: Uint8Array, offset: number) =>\n bytes[offset] | (bytes[offset + 1] << 8)\n\n/**\n * Reads pixel dimensions from PNG, JPEG, GIF, and WebP headers. Returns\n * null for anything unrecognized or truncated — callers treat dimensions\n * as best-effort metadata, never a gate.\n */\nexport function readImageDimensions(\n bytes: Uint8Array,\n): ImageDimensions | null {\n if (bytes.length < 24) return null\n\n // PNG: 8-byte signature, IHDR width/height at offsets 16/20.\n if (\n bytes[0] === 0x89 &&\n bytes[1] === 0x50 &&\n bytes[2] === 0x4e &&\n bytes[3] === 0x47\n ) {\n const width = readU32BE(bytes, 16)\n const height = readU32BE(bytes, 20)\n return width > 0 && height > 0 ? { width, height } : null\n }\n\n // GIF87a/GIF89a: little-endian dimensions at offsets 6/8.\n if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {\n const width = readU16LE(bytes, 6)\n const height = readU16LE(bytes, 8)\n return width > 0 && height > 0 ? { width, height } : null\n }\n\n // JPEG: scan segments for a SOFn marker (C0–CF except C4/C8/CC).\n if (bytes[0] === 0xff && bytes[1] === 0xd8) {\n let offset = 2\n while (offset + 9 < bytes.length) {\n if (bytes[offset] !== 0xff) {\n offset += 1\n continue\n }\n const marker = bytes[offset + 1]\n if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd9)) {\n offset += 2\n continue\n }\n const length = readU16BE(bytes, offset + 2)\n if (\n marker >= 0xc0 &&\n marker <= 0xcf &&\n marker !== 0xc4 &&\n marker !== 0xc8 &&\n marker !== 0xcc\n ) {\n const height = readU16BE(bytes, offset + 5)\n const width = readU16BE(bytes, offset + 7)\n return width > 0 && height > 0 ? { width, height } : null\n }\n if (length < 2) return null\n offset += 2 + length\n }\n return null\n }\n\n // WebP: RIFF....WEBP then VP8/VP8L/VP8X chunk.\n if (\n bytes[0] === 0x52 &&\n bytes[1] === 0x49 &&\n bytes[2] === 0x46 &&\n bytes[3] === 0x46 &&\n bytes[8] === 0x57 &&\n bytes[9] === 0x45 &&\n bytes[10] === 0x42 &&\n bytes[11] === 0x50\n ) {\n const chunk = String.fromCharCode(\n bytes[12],\n bytes[13],\n bytes[14],\n bytes[15],\n )\n if (chunk === 'VP8X' && bytes.length >= 30) {\n const width = 1 + (bytes[24] | (bytes[25] << 8) | (bytes[26] << 16))\n const height = 1 + (bytes[27] | (bytes[28] << 8) | (bytes[29] << 16))\n return { width, height }\n }\n if (chunk === 'VP8 ' && bytes.length >= 30) {\n const width = readU16LE(bytes, 26) & 0x3fff\n const height = readU16LE(bytes, 28) & 0x3fff\n return width > 0 && height > 0 ? { width, height } : null\n }\n if (chunk === 'VP8L' && bytes.length >= 25) {\n const bits =\n bytes[21] | (bytes[22] << 8) | (bytes[23] << 16) | (bytes[24] << 24)\n const width = (bits & 0x3fff) + 1\n const height = ((bits >> 14) & 0x3fff) + 1\n return { width, height }\n }\n }\n\n return null\n}\n\n/**\n * What the platform knows about a VIDEO asset (AGL-2742).\n *\n * ## Why this is not `width`/`height` on the document beside an image's\n *\n * An image's dimensions are read from its own header by\n * {@link readImageDimensions}, server-side, from bytes the platform holds.\n * None of that is available for video: the dimensions live in a `moov` atom\n * that an MP4 is free to place at the END of the file, so reading them\n * server-side means fetching up to 200 MB back out of Storage to answer a\n * question worth twelve bytes — the exact download `generateStoredMediaVariants`\n * is written to avoid. So these numbers arrive from the BROWSER, which had\n * the file in hand and a decoder already loaded, and they are consequently\n * client data: every field is bounded here before it reaches a document.\n *\n * They live under their own key rather than widening `width`/`height`\n * because the provenance differs and a reader should be able to tell. A\n * `width` on a media document means \"measured from the bytes\"; a\n * `video.width` means \"reported by the uploader's browser\". Folding the two\n * together would make the weaker claim indistinguishable from the stronger\n * one on every asset in the library.\n */\nexport interface MediaVideoMetadata {\n /** Duration in whole milliseconds. Always finite and positive. */\n durationMs: number\n /** Coded frame width in pixels, after any display-aspect correction. */\n width: number\n height: number\n /**\n * A short label for what produced the file, when the browser offered one\n * (`video/mp4; codecs=\"avc1.640028\"` collapses to `avc1.640028`). Absent\n * far more often than present — no browser API reports the codec of a\n * local file, so this is only ever filled from a `MediaCapabilities` probe\n * or a container sniff, and nothing depends on it.\n */\n codec?: string\n}\n\n/**\n * The upper bound on a stored duration: 24 hours.\n *\n * Not a policy about what may be uploaded — the DAM's ceiling is 200 MB of\n * bytes and says nothing about running time. This is the bound past which a\n * number stops being a duration and starts being a bug, and the specific\n * bug it exists for is real: `HTMLMediaElement.duration` is `Infinity` for a\n * stream and for some WebM files until the element has been seeked to the\n * end. `Infinity` fails the finite test below before it reaches this\n * constant, but a browser that reports a plausible-looking 10^12 instead\n * would otherwise write a document claiming a 31-year film.\n */\nexport const MEDIA_VIDEO_MAX_DURATION_MS = 24 * 60 * 60 * 1000\n\n/** The largest coded dimension accepted, matching the 8K ceiling encoders use. */\nexport const MEDIA_VIDEO_MAX_DIMENSION = 16384\n\n/**\n * Bound a browser's report into something safe to store, or refuse it whole.\n *\n * All-or-nothing on purpose. A partial record — a duration with no\n * dimensions — is worse than none: `VideoObject` JSON-LD would emit a\n * `duration` and omit `width`, and a renderer sizing its container from\n * `video.height` would find the key present and the value absent. The three\n * numbers are produced by one `loadedmetadata` event and are meaningful only\n * together, so they are accepted or rejected together.\n *\n * `codec` is the exception and is dropped rather than refused, for the same\n * reason `formatMediaRef` drops a malformed content pin: it is a label\n * nothing branches on, and losing the whole record over it would trade a\n * working poster and a correct aspect ratio for a cosmetic string.\n */\nexport function normalizeVideoMetadata(\n input: unknown,\n): MediaVideoMetadata | null {\n if (!input || typeof input !== 'object') return null\n const source = input as Record<string, unknown>\n const durationMs = Math.round(Number(source['durationMs']))\n const width = Math.round(Number(source['width']))\n const height = Math.round(Number(source['height']))\n if (!Number.isFinite(durationMs) || durationMs <= 0) return null\n if (durationMs > MEDIA_VIDEO_MAX_DURATION_MS) return null\n if (!Number.isFinite(width) || width <= 0 || width > MEDIA_VIDEO_MAX_DIMENSION)\n return null\n if (\n !Number.isFinite(height) ||\n height <= 0 ||\n height > MEDIA_VIDEO_MAX_DIMENSION\n )\n return null\n const rawCodec = source['codec']\n const codec =\n typeof rawCodec === 'string' && /^[\\w.,\\- ]{1,64}$/.test(rawCodec.trim())\n ? rawCodec.trim()\n : undefined\n return codec ? { durationMs, width, height, codec } : { durationMs, width, height }\n}\n\n/**\n * The `duration` a schema.org `VideoObject` wants: an ISO 8601 duration.\n *\n * Whole seconds, and never a fractional component. Google's structured-data\n * documentation accepts `PT1M30S` and treats sub-second precision as noise,\n * and a `PT1M30.437S` in a rich result is a number nobody asked for. A\n * duration under one second rounds UP to `PT1S` rather than to `PT0S`, which\n * would read as \"no duration\" to a consumer that tests for truthiness.\n */\nexport function videoDurationIso8601(\n durationMs: number | undefined | null,\n): string | undefined {\n const total = Math.max(1, Math.round(Number(durationMs) / 1000))\n if (!Number.isFinite(total)) return undefined\n if (!durationMs || Number(durationMs) <= 0) return undefined\n const hours = Math.floor(total / 3600)\n const minutes = Math.floor((total % 3600) / 60)\n const seconds = total % 60\n const parts = [\n hours ? `${hours}H` : '',\n minutes ? `${minutes}M` : '',\n seconds ? `${seconds}S` : '',\n ].join('')\n return `PT${parts || '0S'}`\n}\n"],"names":["MEDIA_ALT_MAX_LENGTH","MEDIA_TAG_MAX_COUNT","MEDIA_TAG_MAX_LENGTH","normalizeMediaTags","input","raw","Array","isArray","String","split","seen","Set","tags","entry","tag","trim","toLowerCase","length","has","add","push","inheritedMediaAlt","options","placementAlt","decorative","assetAlt","undefined","inherited","slice","INTRINSIC_SIZE_COMPONENT_IDS","intrinsicMediaSize","componentId","propName","assetWidth","assetHeight","usable","value","Number","isFinite","intrinsicWidth","intrinsicHeight","VIDEO_COMPONENT_ID","videoMediaProps","assetVideo","assetPoster","patch","video","normalizeVideoMetadata","durationSeconds","Math","max","round","durationMs","width","height","posterFromSource","readU32BE","bytes","offset","readU16BE","readU16LE","readImageDimensions","marker","chunk","fromCharCode","bits","MEDIA_VIDEO_MAX_DURATION_MS","MEDIA_VIDEO_MAX_DIMENSION","source","rawCodec","codec","test","videoDurationIso8601","total","hours","floor","minutes","seconds","parts","join"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/media-metadata.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 * Asset metadata helpers (AGL-173): tag normalization shared by the\n * console editor and any future API validation, plus a dependency-free\n * image header parser so the upload route can stamp dimensions without\n * pulling in an image library.\n */\n\nimport { MEDIA_ALT_MAX_LENGTH } from './media-alt'\nimport { embeddedMetadataIsCurrent, mediaEmbeddedSearchText } from './media-embedded-fields'\nimport { NAME_TOKEN_LIMIT, nameSearchKey, nameSearchToken, nameSearchTokens } from './name-search'\n\nexport * from './media-alt'\n\n/**\n * The family a stored file belongs to, as the media library's Type filter\n * names it (AGL-3327). Stored on the document as `kind`, because a Firestore\n * query can ask for a value and cannot ask for a prefix beside a list's own\n * sort: `kind == 'image'` sits on any query the library builds, where a\n * `contentType` range would take the one range field the query has.\n *\n * Every type media ingress accepts that is not a picture, a film or a PDF is\n * a document — ZIP, Word, Excel, PowerPoint, CSV, text, Markdown, JSON — and\n * so is anything older than that allowlist.\n */\nexport type MediaKind = 'image' | 'video' | 'pdf' | 'document'\n\nexport const MEDIA_KINDS: readonly MediaKind[] = ['image', 'video', 'pdf', 'document']\n\nexport function mediaKindOf(contentType: unknown): MediaKind {\n const type = String(contentType ?? '').trim().toLowerCase()\n if (type.startsWith('image/')) return 'image'\n if (type.startsWith('video/')) return 'video'\n if (type === 'application/pdf') return 'pdf'\n return 'document'\n}\n\n/** Which way a picture or a film is longer, as the Orientation filter asks. */\nexport type MediaOrientation = 'landscape' | 'portrait' | 'square'\n\nexport const MEDIA_ORIENTATIONS: readonly MediaOrientation[] = [\n 'landscape',\n 'portrait',\n 'square',\n]\n\nconst positive = (value: unknown): number | null =>\n typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null\n\n/**\n * The orientation of a stored file (AGL-3327), from the pixel size it\n * carries: a picture's `width` and `height`, or a film's `video.width` and\n * `video.height` (a video's own `width`/`height` are never written — see\n * `videoMediaProps`). Null when neither pair is whole, which a query for any\n * orientation then leaves out rather than guesses at.\n */\nexport function mediaOrientationOf(media: {\n width?: unknown\n height?: unknown\n video?: unknown\n}): MediaOrientation | null {\n const video =\n media.video && typeof media.video === 'object'\n ? (media.video as { width?: unknown; height?: unknown })\n : null\n let width = positive(media.width)\n let height = positive(media.height)\n if (width === null || height === null) {\n width = positive(video?.width)\n height = positive(video?.height)\n }\n if (width === null || height === null) return null\n if (width === height) return 'square'\n return width > height ? 'landscape' : 'portrait'\n}\n\n/**\n * A file name as words (AGL-3327): every run of characters that is not a\n * letter or a digit becomes a space, so `hero-banner_2x.png` is the four\n * words a person would search it by. The shared name search splits on\n * spaces only, which suits a person's name and misses nearly every file's.\n */\nexport function mediaNameWords(fileName: unknown): string {\n return String(fileName ?? '')\n .replace(/[^\\p{L}\\p{N}]+/gu, ' ')\n .trim()\n}\n\n/**\n * The one token a library search asks for (AGL-3327): the first word of what\n * was typed, folded the way `mediaNameWords` folds a stored name, lower-cased\n * and capped as `nameSearchToken` caps it. A query holds one array filter, so\n * a search is one word.\n */\nexport function mediaSearchToken(query: unknown): string {\n return nameSearchToken(mediaNameWords(query))\n}\n\n/** The derived fields every writer of a media document stores beside it. */\nexport interface MediaFilterKeys {\n kind: MediaKind\n /**\n * `nameSearchKey` of the file name: the Name sort, and the \"starts with\"\n * range a search is served by where the scope clause holds the query's one\n * array filter.\n */\n nameLower: string\n /**\n * Word-prefix tokens of the file name's words, then of the details read\n * from inside the file (AGL-3339), for search — at most\n * `NAME_TOKEN_LIMIT`, the name's first.\n */\n nameTokens: string[]\n /** Whether alt text is written — \"missing\" is a value a query can find. */\n hasAlt: boolean\n /** Which way the file is longer, or null when its size is not known. */\n orientation: MediaOrientation | null\n}\n\n/** What `mediaFilterKeys` reads off a media document. */\nexport interface MediaFilterSource {\n fileName?: unknown\n contentType?: unknown\n alt?: unknown\n width?: unknown\n height?: unknown\n video?: unknown\n /** The details read from inside the file (AGL-3339), searched after its name. */\n embeddedMetadata?: unknown\n /** The bytes' digest, which says whether `embeddedMetadata` describes them. */\n contentSha256?: unknown\n}\n\n/**\n * What a library search finds a file by (AGL-3339): every word prefix of its\n * name, then of the details read from inside it — a title, keywords, who\n * made it, where — inside the one `NAME_TOKEN_LIMIT` a document keeps, so a\n * long caption never crowds out the name. Details that describe other bytes\n * than the file's (`embeddedMetadataIsCurrent`) add nothing.\n */\nexport function mediaNameTokens(\n fileName: unknown,\n embeddedMetadata?: unknown,\n contentSha256?: unknown,\n): string[] {\n const tokens = new Set(nameSearchTokens(mediaNameWords(fileName)))\n if (!embeddedMetadataIsCurrent(embeddedMetadata, contentSha256)) return [...tokens]\n for (const token of nameSearchTokens(mediaNameWords(mediaEmbeddedSearchText(embeddedMetadata)))) {\n if (tokens.size >= NAME_TOKEN_LIMIT) break\n tokens.add(token)\n }\n return [...tokens]\n}\n\n/**\n * What the media library filters, sorts and searches by, derived from the\n * fields a document already carries (AGL-3327). Every writer — the upload\n * routes, replace, the REST API, restore, the Details drawer — spreads this\n * beside the fields it writes, derived from the document as it will stand\n * after the write, so a query finds every file by it. The backfill\n * (`tools/scripts/backfill-media-filter-keys.mjs`) stamps the documents\n * written before it existed, through `tools/scripts/lib/media-filter-keys.mjs`;\n * both are held to `media-filter-keys.fixtures.json` beside it.\n */\nexport function mediaFilterKeys(media: MediaFilterSource): MediaFilterKeys {\n const fileName = String(media.fileName ?? '')\n return {\n kind: mediaKindOf(media.contentType),\n nameLower: nameSearchKey(fileName),\n nameTokens: mediaNameTokens(fileName, media.embeddedMetadata, media.contentSha256),\n hasAlt: String(media.alt ?? '').trim().length > 0,\n orientation: mediaOrientationOf(media),\n }\n}\n\nexport const MEDIA_TAG_MAX_COUNT = 20\nexport const MEDIA_TAG_MAX_LENGTH = 40\n\n/**\n * Trim, lowercase, dedupe, and cap tags. Accepts a comma-separated string\n * or an array; empty and oversized entries are dropped.\n */\nexport function normalizeMediaTags(input: string | string[]): string[] {\n const raw = Array.isArray(input) ? input : String(input ?? '').split(',')\n const seen = new Set<string>()\n const tags: string[] = []\n for (const entry of raw) {\n const tag = String(entry ?? '').trim().toLowerCase()\n if (!tag || tag.length > MEDIA_TAG_MAX_LENGTH || seen.has(tag)) continue\n seen.add(tag)\n tags.push(tag)\n if (tags.length >= MEDIA_TAG_MAX_COUNT) break\n }\n return tags\n}\n\n/**\n * What a placement should store for `alt` once the asset's own alt text is\n * taken into account (AGL-1896) — the DAM asset is the DEFAULT, the\n * placement keeps the override.\n *\n * `AglynHostMedia.alt` has existed since AGL-173 and the library drawer has\n * always been able to set it; what never existed is anybody READING it. Every\n * placement surface asked the author to type alt again from scratch, so the\n * same logo on eight pages needed its alt typed eight times and in practice\n * shipped blank — on a customer's published site.\n *\n * ONE function, called at pick time by every surface, rather than a rule\n * re-derived per surface. The three refusals are the whole contract:\n *\n * * **`decorative` wins outright.** It is the field AGL-1305 added to record\n * \"screen readers should skip this\", and `image.tsx` already forces\n * `alt=\"\"` over any alt text when it is on. Inheriting into a node that has\n * declared itself decorative would put text on a node whose renderer\n * discards it — invisible, and misleading to the next author who opens the\n * panel.\n * * **A non-blank placement alt wins.** That is the per-placement override,\n * and clobbering it is the one failure that would make this feature worse\n * than not having it: the author's sentence about THIS placement is better\n * than the asset's generic one by construction.\n * * **A blank asset alt yields nothing.** Never a fabricated default. The\n * file name is not alt text (\"IMG_4021.jpg\" announced to a screen reader is\n * worse than silence), and nothing here has seen the image. Returning\n * `undefined` is what lets callers omit the key entirely rather than\n * writing `alt: ''`, which on a besigner node is itself an authored value.\n *\n * A blank placement alt DOES inherit, deliberately. Presets ship `alt: ''`\n * (see `card.tsx`), so requiring an absent key would have skipped the single\n * commonest authoring path — dropping a preset and pointing its image at a\n * library asset — and left the issue open for the case it was filed about.\n *\n * @returns the alt to store, or `undefined` when the caller should write\n * nothing at all.\n */\nexport function inheritedMediaAlt(options: {\n /** The alt already on the placement — a node prop, a config field. */\n placementAlt?: unknown\n /** The placement's explicit \"skip me\" intent, when it has one. */\n decorative?: unknown\n /** The chosen DAM asset's stored alt text. */\n assetAlt?: unknown\n}): string | undefined {\n const { placementAlt, decorative, assetAlt } = options ?? {}\n if (decorative === true) return undefined\n if (typeof placementAlt === 'string' && placementAlt.trim()) return undefined\n const inherited = typeof assetAlt === 'string' ? assetAlt.trim() : ''\n if (!inherited) return undefined\n // Capped at the same length the library drawer saves through, so an alt\n // that reaches a placement is one the DAM would also have stored.\n return inherited.slice(0, MEDIA_ALT_MAX_LENGTH)\n}\n\n/**\n * Component ids whose renderer reads intrinsic pixel dimensions off the node.\n *\n * Component ids are persisted in screen documents and never renamed, which is\n * what makes matching on them safe. The list is the gate rather than a\n * decoration: a prop written onto an element that does not destructure it\n * reaches `...rest` and is spread onto the DOM, so an unlisted element would\n * gain an invalid `intrinsicwidth` attribute in its published HTML.\n *\n * ⛔ `video` is NOT here, and its absence is a decision rather than an\n * oversight (AGL-2749). A video needs the pair at least as badly as an image\n * does — `preload=\"none\"` means its metadata never arrives until someone\n * presses play, so the element is zero-height for the whole life of the page\n * without it — but it cannot get it from `width`/`height`. Those are read\n * from the bytes by the server and are absent on every video, deliberately:\n * AGL-2742 kept the client-measured triple in its own `video` record so a\n * reader can tell a server measurement from a browser's report.\n * {@link videoMediaProps} is where a video gets its pair.\n */\nconst INTRINSIC_SIZE_COMPONENT_IDS = new Set(['image'])\n\n/**\n * The intrinsic `width`/`height` to copy onto a node when an author picks a\n * library asset for it (AGL-2486).\n *\n * ## Why the copy happens at pick time, and why it is not the last word\n *\n * The pair rides on the node like every other prop, so the editor canvas, and\n * any page whose asset cannot be read, still reserve a box. It is not the last\n * word. A replace rewrites the asset's `width`/`height` and cannot reach the\n * nodes that copied them, so the tenant composition reads every placed\n * image's document, in one projected batch per page, and lays its current\n * pair over this one (`media-asset-facts.ts`, AGL-2833). What is written here\n * is what a page shows when that read cannot answer.\n *\n * ## Why it matters\n *\n * `image.tsx` lays images out with `width: 100%; height: auto`, under which an\n * `<img>` has NO height until its bytes decode. Without an intrinsic pair the\n * browser has no ratio to reserve a box from, so every image on the page\n * shifts the content below it as it lands.\n *\n * ## The rules\n *\n * * **Both or neither.** A browser derives an aspect-ratio only from the\n * pair; a lone `width` is read as a real dimension and reserves a box of\n * the wrong shape, which is worse than reserving none.\n * * **Only for renderers that read them**, see\n * `INTRINSIC_SIZE_COMPONENT_IDS` — otherwise the prop lands on the DOM.\n * * **Finite and positive.** Upload capture is best-effort, so a media\n * document may carry `0`, a partial capture, or nothing at all; `width=\"0\"`\n * collapses the element.\n * * **`{}` when anything is unknown**, never `{ intrinsicWidth: undefined }`.\n * Callers spread the result into a props object that `updateNodeProps`\n * REPLACES wholesale, so a key present with an undefined value would strip\n * a pair a previous pick had correctly stored.\n *\n * @returns the props to spread, or `{}` when the caller should write nothing.\n */\nexport function intrinsicMediaSize(options: {\n /** The element being written to — its persisted component id. */\n componentId?: unknown\n /** The attribute the picker was opened for; only `src` carries an image. */\n propName?: unknown\n /** The chosen asset's stored pixel width. */\n assetWidth?: unknown\n /** The chosen asset's stored pixel height. */\n assetHeight?: unknown\n}): { intrinsicWidth?: number; intrinsicHeight?: number } {\n const { componentId, propName, assetWidth, assetHeight } = options ?? {}\n if (propName !== 'src') return {}\n if (!INTRINSIC_SIZE_COMPONENT_IDS.has(String(componentId ?? ''))) return {}\n const usable = (value: unknown): value is number =>\n typeof value === 'number' && Number.isFinite(value) && value > 0\n if (!usable(assetWidth) || !usable(assetHeight)) return {}\n return { intrinsicWidth: assetWidth, intrinsicHeight: assetHeight }\n}\n\n/** The one element that reads the props below off its node. */\nconst VIDEO_COMPONENT_ID = 'video'\n\n/**\n * The video-only companions to {@link intrinsicMediaSize}, copied onto the\n * node when an author picks a video asset (AGL-2741, rewritten against the\n * real document shape in AGL-2749).\n *\n * Same route as the image dimensions, and like theirs not the last word. A\n * replace rewrites the asset's `video` and `poster` records and cannot reach\n * the nodes that copied them, so the tenant composition reads each placed\n * film's document, in the same batch as the images', and lays its current\n * records over these (`media-asset-facts.ts`, AGL-2807). What is written here\n * is what a page shows when that read cannot answer.\n *\n * ## Why a video does not simply use {@link intrinsicMediaSize}\n *\n * A video's pixel dimensions are NOT in `media.width`/`media.height`. Those\n * are read from the bytes by the server and carry a stronger claim than a\n * browser's report, so AGL-2742 kept the client-measured triple in its own\n * `video` record rather than widening them — a reader that folded the two\n * together would lose the ability to tell which it had. A video therefore\n * reaches its intrinsic pair through here, and `intrinsicMediaSize` finds\n * nothing to copy for one.\n *\n * ## The three rules\n *\n * * **Only the `video` element, only its `src`.** A `durationSeconds` spread\n * onto an element that does not destructure it becomes an invalid attribute\n * in the published HTML.\n * * **The duration is stored in SECONDS**, because the author-facing field is\n * in seconds and a person types 63, not 63000. `videoDurationIso8601` takes\n * milliseconds, and the structured-data builder is the one place that\n * converts back.\n * * **`{}` when unknown**, never a key with an `undefined` value: callers\n * spread this into a props object `updateNodeProps` REPLACES wholesale, so\n * a present-but-undefined key strips what an earlier pick stored.\n *\n * `posterFromSource` is a FLAG rather than a url, deliberately. The generated\n * poster is `?poster=1` on the video's own reference, which\n * {@link mediaPosterSrc} builds without reading anything — and that url is\n * explicitly not a promise the poster exists. This flag IS the promise: it is\n * written only when the document records one, which is what lets\n * `videoPosterSrc` offer the derived url to an `<img>` and to a\n * `thumbnailUrl`, where a 404 would be a broken image and a rich result\n * pointing at nothing.\n *\n * An author's own poster is never touched here. It wins at render time\n * instead (`videoPosterSrc`), so re-picking the SOURCE cannot quietly replace\n * a frame somebody chose.\n *\n * @returns the props to spread, or `{}` when the caller should write nothing.\n */\nexport function videoMediaProps(options: {\n /** The element being written to — its persisted component id. */\n componentId?: unknown\n /** The attribute the picker was opened for; only `src` carries a video. */\n propName?: unknown\n /**\n * The chosen asset's `video` record, as {@link normalizeVideoMetadata}\n * bounds it — `durationMs`, `width` and `height`, all three or none.\n */\n assetVideo?: unknown\n /** The chosen asset's generated `poster` record, when it has one. */\n assetPoster?: unknown\n}): {\n durationSeconds?: number\n intrinsicWidth?: number\n intrinsicHeight?: number\n posterFromSource?: boolean\n} {\n const { componentId, propName, assetVideo, assetPoster } = options ?? {}\n if (propName !== 'src') return {}\n if (String(componentId ?? '') !== VIDEO_COMPONENT_ID) return {}\n const patch: {\n durationSeconds?: number\n intrinsicWidth?: number\n intrinsicHeight?: number\n posterFromSource?: boolean\n } = {}\n // Through the DAM's own validator rather than a second reading of the same\n // three numbers: it is all-or-nothing by design, and re-deriving that rule\n // here is how two files come to disagree about what a partial record means.\n const video = normalizeVideoMetadata(assetVideo)\n if (video) {\n // Never rounds to zero: a sub-second clip is still a clip, and a stored\n // `0` reads as \"no duration\" to everything downstream.\n patch.durationSeconds = Math.max(1, Math.round(video.durationMs / 1000))\n patch.intrinsicWidth = video.width\n patch.intrinsicHeight = video.height\n }\n // A poster RECORD, not a poster url — see the note above. Its mere presence\n // on the document is the whole fact being carried.\n if (assetPoster && typeof assetPoster === 'object') {\n patch.posterFromSource = true\n }\n return patch\n}\n\nexport interface ImageDimensions {\n width: number\n height: number\n}\n\nconst readU32BE = (bytes: Uint8Array, offset: number) =>\n (bytes[offset] << 24) |\n (bytes[offset + 1] << 16) |\n (bytes[offset + 2] << 8) |\n bytes[offset + 3]\n\nconst readU16BE = (bytes: Uint8Array, offset: number) =>\n (bytes[offset] << 8) | bytes[offset + 1]\n\nconst readU16LE = (bytes: Uint8Array, offset: number) =>\n bytes[offset] | (bytes[offset + 1] << 8)\n\n/**\n * Reads pixel dimensions from PNG, JPEG, GIF, and WebP headers. Returns\n * null for anything unrecognized or truncated — callers treat dimensions\n * as best-effort metadata, never a gate.\n */\nexport function readImageDimensions(\n bytes: Uint8Array,\n): ImageDimensions | null {\n if (bytes.length < 24) return null\n\n // PNG: 8-byte signature, IHDR width/height at offsets 16/20.\n if (\n bytes[0] === 0x89 &&\n bytes[1] === 0x50 &&\n bytes[2] === 0x4e &&\n bytes[3] === 0x47\n ) {\n const width = readU32BE(bytes, 16)\n const height = readU32BE(bytes, 20)\n return width > 0 && height > 0 ? { width, height } : null\n }\n\n // GIF87a/GIF89a: little-endian dimensions at offsets 6/8.\n if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {\n const width = readU16LE(bytes, 6)\n const height = readU16LE(bytes, 8)\n return width > 0 && height > 0 ? { width, height } : null\n }\n\n // JPEG: scan segments for a SOFn marker (C0–CF except C4/C8/CC).\n if (bytes[0] === 0xff && bytes[1] === 0xd8) {\n let offset = 2\n while (offset + 9 < bytes.length) {\n if (bytes[offset] !== 0xff) {\n offset += 1\n continue\n }\n const marker = bytes[offset + 1]\n if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd9)) {\n offset += 2\n continue\n }\n const length = readU16BE(bytes, offset + 2)\n if (\n marker >= 0xc0 &&\n marker <= 0xcf &&\n marker !== 0xc4 &&\n marker !== 0xc8 &&\n marker !== 0xcc\n ) {\n const height = readU16BE(bytes, offset + 5)\n const width = readU16BE(bytes, offset + 7)\n return width > 0 && height > 0 ? { width, height } : null\n }\n if (length < 2) return null\n offset += 2 + length\n }\n return null\n }\n\n // WebP: RIFF....WEBP then VP8/VP8L/VP8X chunk.\n if (\n bytes[0] === 0x52 &&\n bytes[1] === 0x49 &&\n bytes[2] === 0x46 &&\n bytes[3] === 0x46 &&\n bytes[8] === 0x57 &&\n bytes[9] === 0x45 &&\n bytes[10] === 0x42 &&\n bytes[11] === 0x50\n ) {\n const chunk = String.fromCharCode(\n bytes[12],\n bytes[13],\n bytes[14],\n bytes[15],\n )\n if (chunk === 'VP8X' && bytes.length >= 30) {\n const width = 1 + (bytes[24] | (bytes[25] << 8) | (bytes[26] << 16))\n const height = 1 + (bytes[27] | (bytes[28] << 8) | (bytes[29] << 16))\n return { width, height }\n }\n if (chunk === 'VP8 ' && bytes.length >= 30) {\n const width = readU16LE(bytes, 26) & 0x3fff\n const height = readU16LE(bytes, 28) & 0x3fff\n return width > 0 && height > 0 ? { width, height } : null\n }\n if (chunk === 'VP8L' && bytes.length >= 25) {\n const bits =\n bytes[21] | (bytes[22] << 8) | (bytes[23] << 16) | (bytes[24] << 24)\n const width = (bits & 0x3fff) + 1\n const height = ((bits >> 14) & 0x3fff) + 1\n return { width, height }\n }\n }\n\n return null\n}\n\n/**\n * What the platform knows about a VIDEO asset (AGL-2742).\n *\n * ## Why this is not `width`/`height` on the document beside an image's\n *\n * An image's dimensions are read from its own header by\n * {@link readImageDimensions}, server-side, from bytes the platform holds.\n * None of that is available for video: the dimensions live in a `moov` atom\n * that an MP4 is free to place at the END of the file, so reading them\n * server-side means fetching up to 200 MB back out of Storage to answer a\n * question worth twelve bytes — the exact download `generateStoredMediaVariants`\n * is written to avoid. So these numbers arrive from the BROWSER, which had\n * the file in hand and a decoder already loaded, and they are consequently\n * client data: every field is bounded here before it reaches a document.\n *\n * They live under their own key rather than widening `width`/`height`\n * because the provenance differs and a reader should be able to tell. A\n * `width` on a media document means \"measured from the bytes\"; a\n * `video.width` means \"reported by the uploader's browser\". Folding the two\n * together would make the weaker claim indistinguishable from the stronger\n * one on every asset in the library.\n */\nexport interface MediaVideoMetadata {\n /** Duration in whole milliseconds. Always finite and positive. */\n durationMs: number\n /** Coded frame width in pixels, after any display-aspect correction. */\n width: number\n height: number\n /**\n * A short label for what produced the file, when the browser offered one\n * (`video/mp4; codecs=\"avc1.640028\"` collapses to `avc1.640028`). Absent\n * far more often than present — no browser API reports the codec of a\n * local file, so this is only ever filled from a `MediaCapabilities` probe\n * or a container sniff, and nothing depends on it.\n */\n codec?: string\n}\n\n/**\n * The upper bound on a stored duration: 24 hours.\n *\n * Not a policy about what may be uploaded — the DAM's ceiling is 200 MB of\n * bytes and says nothing about running time. This is the bound past which a\n * number stops being a duration and starts being a bug, and the specific\n * bug it exists for is real: `HTMLMediaElement.duration` is `Infinity` for a\n * stream and for some WebM files until the element has been seeked to the\n * end. `Infinity` fails the finite test below before it reaches this\n * constant, but a browser that reports a plausible-looking 10^12 instead\n * would otherwise write a document claiming a 31-year film.\n */\nexport const MEDIA_VIDEO_MAX_DURATION_MS = 24 * 60 * 60 * 1000\n\n/** The largest coded dimension accepted, matching the 8K ceiling encoders use. */\nexport const MEDIA_VIDEO_MAX_DIMENSION = 16384\n\n/**\n * Bound a browser's report into something safe to store, or refuse it whole.\n *\n * All-or-nothing on purpose. A partial record — a duration with no\n * dimensions — is worse than none: `VideoObject` JSON-LD would emit a\n * `duration` and omit `width`, and a renderer sizing its container from\n * `video.height` would find the key present and the value absent. The three\n * numbers are produced by one `loadedmetadata` event and are meaningful only\n * together, so they are accepted or rejected together.\n *\n * `codec` is the exception and is dropped rather than refused, for the same\n * reason `formatMediaRef` drops a malformed content pin: it is a label\n * nothing branches on, and losing the whole record over it would trade a\n * working poster and a correct aspect ratio for a cosmetic string.\n */\nexport function normalizeVideoMetadata(\n input: unknown,\n): MediaVideoMetadata | null {\n if (!input || typeof input !== 'object') return null\n const source = input as Record<string, unknown>\n const durationMs = Math.round(Number(source['durationMs']))\n const width = Math.round(Number(source['width']))\n const height = Math.round(Number(source['height']))\n if (!Number.isFinite(durationMs) || durationMs <= 0) return null\n if (durationMs > MEDIA_VIDEO_MAX_DURATION_MS) return null\n if (!Number.isFinite(width) || width <= 0 || width > MEDIA_VIDEO_MAX_DIMENSION)\n return null\n if (\n !Number.isFinite(height) ||\n height <= 0 ||\n height > MEDIA_VIDEO_MAX_DIMENSION\n )\n return null\n const rawCodec = source['codec']\n const codec =\n typeof rawCodec === 'string' && /^[\\w.,\\- ]{1,64}$/.test(rawCodec.trim())\n ? rawCodec.trim()\n : undefined\n return codec ? { durationMs, width, height, codec } : { durationMs, width, height }\n}\n\n/**\n * The `duration` a schema.org `VideoObject` wants: an ISO 8601 duration.\n *\n * Whole seconds, and never a fractional component. Google's structured-data\n * documentation accepts `PT1M30S` and treats sub-second precision as noise,\n * and a `PT1M30.437S` in a rich result is a number nobody asked for. A\n * duration under one second rounds UP to `PT1S` rather than to `PT0S`, which\n * would read as \"no duration\" to a consumer that tests for truthiness.\n */\nexport function videoDurationIso8601(\n durationMs: number | undefined | null,\n): string | undefined {\n const total = Math.max(1, Math.round(Number(durationMs) / 1000))\n if (!Number.isFinite(total)) return undefined\n if (!durationMs || Number(durationMs) <= 0) return undefined\n const hours = Math.floor(total / 3600)\n const minutes = Math.floor((total % 3600) / 60)\n const seconds = total % 60\n const parts = [\n hours ? `${hours}H` : '',\n minutes ? `${minutes}M` : '',\n seconds ? `${seconds}S` : '',\n ].join('')\n return `PT${parts || '0S'}`\n}\n"],"names":["MEDIA_ALT_MAX_LENGTH","embeddedMetadataIsCurrent","mediaEmbeddedSearchText","NAME_TOKEN_LIMIT","nameSearchKey","nameSearchToken","nameSearchTokens","MEDIA_KINDS","mediaKindOf","contentType","type","String","trim","toLowerCase","startsWith","MEDIA_ORIENTATIONS","positive","value","Number","isFinite","mediaOrientationOf","media","video","width","height","mediaNameWords","fileName","replace","mediaSearchToken","query","mediaNameTokens","embeddedMetadata","contentSha256","tokens","Set","token","size","add","mediaFilterKeys","kind","nameLower","nameTokens","hasAlt","alt","length","orientation","MEDIA_TAG_MAX_COUNT","MEDIA_TAG_MAX_LENGTH","normalizeMediaTags","input","raw","Array","isArray","split","seen","tags","entry","tag","has","push","inheritedMediaAlt","options","placementAlt","decorative","assetAlt","undefined","inherited","slice","INTRINSIC_SIZE_COMPONENT_IDS","intrinsicMediaSize","componentId","propName","assetWidth","assetHeight","usable","intrinsicWidth","intrinsicHeight","VIDEO_COMPONENT_ID","videoMediaProps","assetVideo","assetPoster","patch","normalizeVideoMetadata","durationSeconds","Math","max","round","durationMs","posterFromSource","readU32BE","bytes","offset","readU16BE","readU16LE","readImageDimensions","marker","chunk","fromCharCode","bits","MEDIA_VIDEO_MAX_DURATION_MS","MEDIA_VIDEO_MAX_DIMENSION","source","rawCodec","codec","test","videoDurationIso8601","total","hours","floor","minutes","seconds","parts","join"],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/** The most tokens the sender contributes. A name and an address are a few dozen. */
|
|
18
|
+
export declare const MESSAGE_SENDER_TOKENS_MAX = 60;
|
|
19
|
+
/** The most tokens one message's search array holds, the sender's included. */
|
|
20
|
+
export declare const MESSAGE_SEARCH_TOKENS_MAX = 200;
|
|
21
|
+
/** The most distinct words of the message's values that contribute prefixes. */
|
|
22
|
+
export declare const MESSAGE_SEARCH_WORDS_MAX = 40;
|
|
23
|
+
/**
|
|
24
|
+
* Who wrote in, by the field-name convention: the first non-empty value of
|
|
25
|
+
* a name-like field and of an address-like field. Reads the map in the
|
|
26
|
+
* order it is handed, so the first spelling of a reduced key wins.
|
|
27
|
+
*/
|
|
28
|
+
export declare function messageSender(fields: Record<string, unknown> | null | undefined): {
|
|
29
|
+
name?: string;
|
|
30
|
+
email?: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* The two search arrays a message is stamped with — see the block above.
|
|
34
|
+
* Spread onto every write that creates one; a message's values never change
|
|
35
|
+
* after it arrives, so no other write touches them.
|
|
36
|
+
*/
|
|
37
|
+
export declare function messageSearchFields(fields: Record<string, unknown> | null | undefined): {
|
|
38
|
+
senderTokens: string[];
|
|
39
|
+
searchTokens: string[];
|
|
40
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { NAME_TOKEN_MAX_PREFIX, nameSearchKey } from "./name-search.js";
|
|
18
|
+
/*
|
|
19
|
+
* THE SEARCH KEYS OF A MESSAGE A VISITOR SENT (AGL-3321).
|
|
20
|
+
*
|
|
21
|
+
* A message is a free-form map of the fields its author declared — `name`,
|
|
22
|
+
* `Email Address`, `message`, `Company size` — with no guaranteed shape. A
|
|
23
|
+
* list of them is searched on its Firestore query, which cannot look inside
|
|
24
|
+
* a string, so every writer stamps the words as word-prefix token arrays the
|
|
25
|
+
* query asks with `array-contains`:
|
|
26
|
+
*
|
|
27
|
+
* senderTokens the sender's name and address — who wrote in. What the
|
|
28
|
+
* list's From filter asks.
|
|
29
|
+
* searchTokens the sender's tokens, then the words of every value the
|
|
30
|
+
* message carries. What the list's search box asks.
|
|
31
|
+
*
|
|
32
|
+
* The words are the platform's search keys (`nameSearchKey`, prefixes up to
|
|
33
|
+
* `NAME_TOKEN_MAX_PREFIX`), so a typed word becomes the token the query asks
|
|
34
|
+
* through the same `nameSearchNormalizers` every list uses. A word that joins
|
|
35
|
+
* parts with `@ . + _ -` — an address, a domain, a hyphenated name — is also
|
|
36
|
+
* its parts, so `acme` finds `dana@acme.com` and `dana` does too.
|
|
37
|
+
*
|
|
38
|
+
* ## What is capped
|
|
39
|
+
*
|
|
40
|
+
* A message is free text, so the arrays are bounded rather than complete:
|
|
41
|
+
* the first {@link MESSAGE_SEARCH_WORDS_MAX} distinct words of the values
|
|
42
|
+
* contribute, and each array stops at its own ceiling
|
|
43
|
+
* ({@link MESSAGE_SENDER_TOKENS_MAX}, {@link MESSAGE_SEARCH_TOKENS_MAX}) —
|
|
44
|
+
* the sender first, so a long message loses the tail of its text before it
|
|
45
|
+
* loses who sent it. A word past the cap is still in the message; the search
|
|
46
|
+
* box just does not find it.
|
|
47
|
+
*
|
|
48
|
+
* ## Deterministic whatever the map's key order
|
|
49
|
+
*
|
|
50
|
+
* A map's keys come back in a different order from different readers (the
|
|
51
|
+
* Admin SDK sorts them), and a cap makes the result order-sensitive. So the
|
|
52
|
+
* values are read in SORTED key order, and a backfill that re-derives the
|
|
53
|
+
* arrays from a stored message computes exactly what the writer stamped.
|
|
54
|
+
*
|
|
55
|
+
* `tools/scripts/backfill-form-submission-filters.mjs` restates this for the
|
|
56
|
+
* rows written before it, held to `tools/scripts/lib/message-search.fixtures.json`
|
|
57
|
+
* with the spec beside this file.
|
|
58
|
+
*/ /** The most tokens the sender contributes. A name and an address are a few dozen. */ export const MESSAGE_SENDER_TOKENS_MAX = 60;
|
|
59
|
+
/** The most tokens one message's search array holds, the sender's included. */ export const MESSAGE_SEARCH_TOKENS_MAX = 200;
|
|
60
|
+
/** The most distinct words of the message's values that contribute prefixes. */ export const MESSAGE_SEARCH_WORDS_MAX = 40;
|
|
61
|
+
/**
|
|
62
|
+
* Field names that mean "this is who wrote in", most specific first,
|
|
63
|
+
* compared against a key REDUCED to lowercase letters and digits — the same
|
|
64
|
+
* field arrives as `Full Name`, `full_name` and `fullname`.
|
|
65
|
+
*/ const SENDER_NAME_KEYS = [
|
|
66
|
+
'name',
|
|
67
|
+
'fullname',
|
|
68
|
+
'yourname',
|
|
69
|
+
'firstname',
|
|
70
|
+
'contactname'
|
|
71
|
+
];
|
|
72
|
+
const SENDER_EMAIL_KEYS = [
|
|
73
|
+
'email',
|
|
74
|
+
'emailaddress'
|
|
75
|
+
];
|
|
76
|
+
/** The joins a word is also split on, so an address is also its parts. */ const WORD_JOINS = /[@.+_-]/;
|
|
77
|
+
const WORD_PARTS = /[@.+_-]+/;
|
|
78
|
+
/** Punctuation around a word — `cake?`, `(june)` — is not part of it. */ const WORD_EDGES = RegExp("^[^\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376-\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0560-\\u0588\\u05D0-\\u05EA\\u05EF-\\u05F2\\u0620-\\u064A\\u066E-\\u066F\\u0671-\\u06D3\\u06D5\\u06E5-\\u06E6\\u06EE-\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4-\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u0860-\\u086A\\u0870-\\u0887\\u0889-\\u088F\\u08A0-\\u08C9\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F-\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC-\\u09DD\\u09DF-\\u09E1\\u09F0-\\u09F1\\u09FC\\u0A05-\\u0A0A\\u0A0F-\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32-\\u0A33\\u0A35-\\u0A36\\u0A38-\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2-\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0-\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F-\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32-\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C-\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99-\\u0B9A\\u0B9C\\u0B9E-\\u0B9F\\u0BA3-\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C5C-\\u0C5D\\u0C60-\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDC-\\u0CDE\\u0CE0-\\u0CE1\\u0CF1-\\u0CF2\\u0D04-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32-\\u0E33\\u0E40-\\u0E46\\u0E81-\\u0E82\\u0E84\\u0E86-\\u0E8A\\u0E8C-\\u0EA3\\u0EA5\\u0EA7-\\u0EB0\\u0EB2-\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065-\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u1711\\u171F-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1878\\u1880-\\u1884\\u1887-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4C\\u1B83-\\u1BA0\\u1BAE-\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C8A\\u1C90-\\u1CBA\\u1CBD-\\u1CBF\\u1CE9-\\u1CEC\\u1CEE-\\u1CF3\\u1CF5-\\u1CF6\\u1CFA\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183-\\u2184\\u2C00-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2-\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3006\\u3031-\\u3035\\u303B-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312F\\u3131-\\u318E\\u31A0-\\u31BF\\u31F0-\\u31FF\\u3400-\\u4DBF\\u4E00-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A-\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7DC\\uA7F1-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD-\\uA8FE\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5-\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB69\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40-\\uFB41\\uFB43-\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC\\u{10000}-\\u{1000B}\\u{1000D}-\\u{10026}\\u{10028}-\\u{1003A}\\u{1003C}-\\u{1003D}\\u{1003F}-\\u{1004D}\\u{10050}-\\u{1005D}\\u{10080}-\\u{100FA}\\u{10280}-\\u{1029C}\\u{102A0}-\\u{102D0}\\u{10300}-\\u{1031F}\\u{1032D}-\\u{10340}\\u{10342}-\\u{10349}\\u{10350}-\\u{10375}\\u{10380}-\\u{1039D}\\u{103A0}-\\u{103C3}\\u{103C8}-\\u{103CF}\\u{10400}-\\u{1049D}\\u{104B0}-\\u{104D3}\\u{104D8}-\\u{104FB}\\u{10500}-\\u{10527}\\u{10530}-\\u{10563}\\u{10570}-\\u{1057A}\\u{1057C}-\\u{1058A}\\u{1058C}-\\u{10592}\\u{10594}-\\u{10595}\\u{10597}-\\u{105A1}\\u{105A3}-\\u{105B1}\\u{105B3}-\\u{105B9}\\u{105BB}-\\u{105BC}\\u{105C0}-\\u{105F3}\\u{10600}-\\u{10736}\\u{10740}-\\u{10755}\\u{10760}-\\u{10767}\\u{10780}-\\u{10785}\\u{10787}-\\u{107B0}\\u{107B2}-\\u{107BA}\\u{10800}-\\u{10805}\\u{10808}\\u{1080A}-\\u{10835}\\u{10837}-\\u{10838}\\u{1083C}\\u{1083F}-\\u{10855}\\u{10860}-\\u{10876}\\u{10880}-\\u{1089E}\\u{108E0}-\\u{108F2}\\u{108F4}-\\u{108F5}\\u{10900}-\\u{10915}\\u{10920}-\\u{10939}\\u{10940}-\\u{10959}\\u{10980}-\\u{109B7}\\u{109BE}-\\u{109BF}\\u{10A00}\\u{10A10}-\\u{10A13}\\u{10A15}-\\u{10A17}\\u{10A19}-\\u{10A35}\\u{10A60}-\\u{10A7C}\\u{10A80}-\\u{10A9C}\\u{10AC0}-\\u{10AC7}\\u{10AC9}-\\u{10AE4}\\u{10B00}-\\u{10B35}\\u{10B40}-\\u{10B55}\\u{10B60}-\\u{10B72}\\u{10B80}-\\u{10B91}\\u{10C00}-\\u{10C48}\\u{10C80}-\\u{10CB2}\\u{10CC0}-\\u{10CF2}\\u{10D00}-\\u{10D23}\\u{10D4A}-\\u{10D65}\\u{10D6F}-\\u{10D85}\\u{10E80}-\\u{10EA9}\\u{10EB0}-\\u{10EB1}\\u{10EC2}-\\u{10EC7}\\u{10F00}-\\u{10F1C}\\u{10F27}\\u{10F30}-\\u{10F45}\\u{10F70}-\\u{10F81}\\u{10FB0}-\\u{10FC4}\\u{10FE0}-\\u{10FF6}\\u{11003}-\\u{11037}\\u{11071}-\\u{11072}\\u{11075}\\u{11083}-\\u{110AF}\\u{110D0}-\\u{110E8}\\u{11103}-\\u{11126}\\u{11144}\\u{11147}\\u{11150}-\\u{11172}\\u{11176}\\u{11183}-\\u{111B2}\\u{111C1}-\\u{111C4}\\u{111DA}\\u{111DC}\\u{11200}-\\u{11211}\\u{11213}-\\u{1122B}\\u{1123F}-\\u{11240}\\u{11280}-\\u{11286}\\u{11288}\\u{1128A}-\\u{1128D}\\u{1128F}-\\u{1129D}\\u{1129F}-\\u{112A8}\\u{112B0}-\\u{112DE}\\u{11305}-\\u{1130C}\\u{1130F}-\\u{11310}\\u{11313}-\\u{11328}\\u{1132A}-\\u{11330}\\u{11332}-\\u{11333}\\u{11335}-\\u{11339}\\u{1133D}\\u{11350}\\u{1135D}-\\u{11361}\\u{11380}-\\u{11389}\\u{1138B}\\u{1138E}\\u{11390}-\\u{113B5}\\u{113B7}\\u{113D1}\\u{113D3}\\u{11400}-\\u{11434}\\u{11447}-\\u{1144A}\\u{1145F}-\\u{11461}\\u{11480}-\\u{114AF}\\u{114C4}-\\u{114C5}\\u{114C7}\\u{11580}-\\u{115AE}\\u{115D8}-\\u{115DB}\\u{11600}-\\u{1162F}\\u{11644}\\u{11680}-\\u{116AA}\\u{116B8}\\u{11700}-\\u{1171A}\\u{11740}-\\u{11746}\\u{11800}-\\u{1182B}\\u{118A0}-\\u{118DF}\\u{118FF}-\\u{11906}\\u{11909}\\u{1190C}-\\u{11913}\\u{11915}-\\u{11916}\\u{11918}-\\u{1192F}\\u{1193F}\\u{11941}\\u{119A0}-\\u{119A7}\\u{119AA}-\\u{119D0}\\u{119E1}\\u{119E3}\\u{11A00}\\u{11A0B}-\\u{11A32}\\u{11A3A}\\u{11A50}\\u{11A5C}-\\u{11A89}\\u{11A9D}\\u{11AB0}-\\u{11AF8}\\u{11BC0}-\\u{11BE0}\\u{11C00}-\\u{11C08}\\u{11C0A}-\\u{11C2E}\\u{11C40}\\u{11C72}-\\u{11C8F}\\u{11D00}-\\u{11D06}\\u{11D08}-\\u{11D09}\\u{11D0B}-\\u{11D30}\\u{11D46}\\u{11D60}-\\u{11D65}\\u{11D67}-\\u{11D68}\\u{11D6A}-\\u{11D89}\\u{11D98}\\u{11DB0}-\\u{11DDB}\\u{11EE0}-\\u{11EF2}\\u{11F02}\\u{11F04}-\\u{11F10}\\u{11F12}-\\u{11F33}\\u{11FB0}\\u{12000}-\\u{12399}\\u{12480}-\\u{12543}\\u{12F90}-\\u{12FF0}\\u{13000}-\\u{1342F}\\u{13441}-\\u{13446}\\u{13460}-\\u{143FA}\\u{14400}-\\u{14646}\\u{16100}-\\u{1611D}\\u{16800}-\\u{16A38}\\u{16A40}-\\u{16A5E}\\u{16A70}-\\u{16ABE}\\u{16AD0}-\\u{16AED}\\u{16B00}-\\u{16B2F}\\u{16B40}-\\u{16B43}\\u{16B63}-\\u{16B77}\\u{16B7D}-\\u{16B8F}\\u{16D40}-\\u{16D6C}\\u{16E40}-\\u{16E7F}\\u{16EA0}-\\u{16EB8}\\u{16EBB}-\\u{16ED3}\\u{16F00}-\\u{16F4A}\\u{16F50}\\u{16F93}-\\u{16F9F}\\u{16FE0}-\\u{16FE1}\\u{16FE3}\\u{16FF2}-\\u{16FF3}\\u{17000}-\\u{18CD5}\\u{18CFF}-\\u{18D1E}\\u{18D80}-\\u{18DF2}\\u{1AFF0}-\\u{1AFF3}\\u{1AFF5}-\\u{1AFFB}\\u{1AFFD}-\\u{1AFFE}\\u{1B000}-\\u{1B122}\\u{1B132}\\u{1B150}-\\u{1B152}\\u{1B155}\\u{1B164}-\\u{1B167}\\u{1B170}-\\u{1B2FB}\\u{1BC00}-\\u{1BC6A}\\u{1BC70}-\\u{1BC7C}\\u{1BC80}-\\u{1BC88}\\u{1BC90}-\\u{1BC99}\\u{1D400}-\\u{1D454}\\u{1D456}-\\u{1D49C}\\u{1D49E}-\\u{1D49F}\\u{1D4A2}\\u{1D4A5}-\\u{1D4A6}\\u{1D4A9}-\\u{1D4AC}\\u{1D4AE}-\\u{1D4B9}\\u{1D4BB}\\u{1D4BD}-\\u{1D4C3}\\u{1D4C5}-\\u{1D505}\\u{1D507}-\\u{1D50A}\\u{1D50D}-\\u{1D514}\\u{1D516}-\\u{1D51C}\\u{1D51E}-\\u{1D539}\\u{1D53B}-\\u{1D53E}\\u{1D540}-\\u{1D544}\\u{1D546}\\u{1D54A}-\\u{1D550}\\u{1D552}-\\u{1D6A5}\\u{1D6A8}-\\u{1D6C0}\\u{1D6C2}-\\u{1D6DA}\\u{1D6DC}-\\u{1D6FA}\\u{1D6FC}-\\u{1D714}\\u{1D716}-\\u{1D734}\\u{1D736}-\\u{1D74E}\\u{1D750}-\\u{1D76E}\\u{1D770}-\\u{1D788}\\u{1D78A}-\\u{1D7A8}\\u{1D7AA}-\\u{1D7C2}\\u{1D7C4}-\\u{1D7CB}\\u{1DF00}-\\u{1DF1E}\\u{1DF25}-\\u{1DF2A}\\u{1E030}-\\u{1E06D}\\u{1E100}-\\u{1E12C}\\u{1E137}-\\u{1E13D}\\u{1E14E}\\u{1E290}-\\u{1E2AD}\\u{1E2C0}-\\u{1E2EB}\\u{1E4D0}-\\u{1E4EB}\\u{1E5D0}-\\u{1E5ED}\\u{1E5F0}\\u{1E6C0}-\\u{1E6DE}\\u{1E6E0}-\\u{1E6E2}\\u{1E6E4}-\\u{1E6E5}\\u{1E6E7}-\\u{1E6ED}\\u{1E6F0}-\\u{1E6F4}\\u{1E6FE}-\\u{1E6FF}\\u{1E7E0}-\\u{1E7E6}\\u{1E7E8}-\\u{1E7EB}\\u{1E7ED}-\\u{1E7EE}\\u{1E7F0}-\\u{1E7FE}\\u{1E800}-\\u{1E8C4}\\u{1E900}-\\u{1E943}\\u{1E94B}\\u{1EE00}-\\u{1EE03}\\u{1EE05}-\\u{1EE1F}\\u{1EE21}-\\u{1EE22}\\u{1EE24}\\u{1EE27}\\u{1EE29}-\\u{1EE32}\\u{1EE34}-\\u{1EE37}\\u{1EE39}\\u{1EE3B}\\u{1EE42}\\u{1EE47}\\u{1EE49}\\u{1EE4B}\\u{1EE4D}-\\u{1EE4F}\\u{1EE51}-\\u{1EE52}\\u{1EE54}\\u{1EE57}\\u{1EE59}\\u{1EE5B}\\u{1EE5D}\\u{1EE5F}\\u{1EE61}-\\u{1EE62}\\u{1EE64}\\u{1EE67}-\\u{1EE6A}\\u{1EE6C}-\\u{1EE72}\\u{1EE74}-\\u{1EE77}\\u{1EE79}-\\u{1EE7C}\\u{1EE7E}\\u{1EE80}-\\u{1EE89}\\u{1EE8B}-\\u{1EE9B}\\u{1EEA1}-\\u{1EEA3}\\u{1EEA5}-\\u{1EEA9}\\u{1EEAB}-\\u{1EEBB}\\u{20000}-\\u{2A6DF}\\u{2A700}-\\u{2B81D}\\u{2B820}-\\u{2CEAD}\\u{2CEB0}-\\u{2EBE0}\\u{2EBF0}-\\u{2EE5D}\\u{2F800}-\\u{2FA1D}\\u{30000}-\\u{3134A}\\u{31350}-\\u{33479}\\u0030-\\u0039\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE\\u0660-\\u0669\\u06F0-\\u06F9\\u07C0-\\u07C9\\u0966-\\u096F\\u09E6-\\u09EF\\u09F4-\\u09F9\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0B72-\\u0B77\\u0BE6-\\u0BF2\\u0C66-\\u0C6F\\u0C78-\\u0C7E\\u0CE6-\\u0CEF\\u0D58-\\u0D5E\\u0D66-\\u0D78\\u0DE6-\\u0DEF\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F33\\u1040-\\u1049\\u1090-\\u1099\\u1369-\\u137C\\u16EE-\\u16F0\\u17E0-\\u17E9\\u17F0-\\u17F9\\u1810-\\u1819\\u1946-\\u194F\\u19D0-\\u19DA\\u1A80-\\u1A89\\u1A90-\\u1A99\\u1B50-\\u1B59\\u1BB0-\\u1BB9\\u1C40-\\u1C49\\u1C50-\\u1C59\\u2070\\u2074-\\u2079\\u2080-\\u2089\\u2150-\\u2182\\u2185-\\u2189\\u2460-\\u249B\\u24EA-\\u24FF\\u2776-\\u2793\\u2CFD\\u3007\\u3021-\\u3029\\u3038-\\u303A\\u3192-\\u3195\\u3220-\\u3229\\u3248-\\u324F\\u3251-\\u325F\\u3280-\\u3289\\u32B1-\\u32BF\\uA620-\\uA629\\uA6E6-\\uA6EF\\uA830-\\uA835\\uA8D0-\\uA8D9\\uA900-\\uA909\\uA9D0-\\uA9D9\\uA9F0-\\uA9F9\\uAA50-\\uAA59\\uABF0-\\uABF9\\uFF10-\\uFF19\\u{10107}-\\u{10133}\\u{10140}-\\u{10178}\\u{1018A}-\\u{1018B}\\u{102E1}-\\u{102FB}\\u{10320}-\\u{10323}\\u{10341}\\u{1034A}\\u{103D1}-\\u{103D5}\\u{104A0}-\\u{104A9}\\u{10858}-\\u{1085F}\\u{10879}-\\u{1087F}\\u{108A7}-\\u{108AF}\\u{108FB}-\\u{108FF}\\u{10916}-\\u{1091B}\\u{109BC}-\\u{109BD}\\u{109C0}-\\u{109CF}\\u{109D2}-\\u{109FF}\\u{10A40}-\\u{10A48}\\u{10A7D}-\\u{10A7E}\\u{10A9D}-\\u{10A9F}\\u{10AEB}-\\u{10AEF}\\u{10B58}-\\u{10B5F}\\u{10B78}-\\u{10B7F}\\u{10BA9}-\\u{10BAF}\\u{10CFA}-\\u{10CFF}\\u{10D30}-\\u{10D39}\\u{10D40}-\\u{10D49}\\u{10E60}-\\u{10E7E}\\u{10F1D}-\\u{10F26}\\u{10F51}-\\u{10F54}\\u{10FC5}-\\u{10FCB}\\u{11052}-\\u{1106F}\\u{110F0}-\\u{110F9}\\u{11136}-\\u{1113F}\\u{111D0}-\\u{111D9}\\u{111E1}-\\u{111F4}\\u{112F0}-\\u{112F9}\\u{11450}-\\u{11459}\\u{114D0}-\\u{114D9}\\u{11650}-\\u{11659}\\u{116C0}-\\u{116C9}\\u{116D0}-\\u{116E3}\\u{11730}-\\u{1173B}\\u{118E0}-\\u{118F2}\\u{11950}-\\u{11959}\\u{11BF0}-\\u{11BF9}\\u{11C50}-\\u{11C6C}\\u{11D50}-\\u{11D59}\\u{11DA0}-\\u{11DA9}\\u{11DE0}-\\u{11DE9}\\u{11F50}-\\u{11F59}\\u{11FC0}-\\u{11FD4}\\u{12400}-\\u{1246E}\\u{16130}-\\u{16139}\\u{16A60}-\\u{16A69}\\u{16AC0}-\\u{16AC9}\\u{16B50}-\\u{16B59}\\u{16B5B}-\\u{16B61}\\u{16D70}-\\u{16D79}\\u{16E80}-\\u{16E96}\\u{16FF4}-\\u{16FF6}\\u{1CCF0}-\\u{1CCF9}\\u{1D2C0}-\\u{1D2D3}\\u{1D2E0}-\\u{1D2F3}\\u{1D360}-\\u{1D378}\\u{1D7CE}-\\u{1D7FF}\\u{1E140}-\\u{1E149}\\u{1E2F0}-\\u{1E2F9}\\u{1E4F0}-\\u{1E4F9}\\u{1E5F1}-\\u{1E5FA}\\u{1E8C7}-\\u{1E8CF}\\u{1E950}-\\u{1E959}\\u{1EC71}-\\u{1ECAB}\\u{1ECAD}-\\u{1ECAF}\\u{1ECB1}-\\u{1ECB4}\\u{1ED01}-\\u{1ED2D}\\u{1ED2F}-\\u{1ED3D}\\u{1F100}-\\u{1F10C}\\u{1FBF0}-\\u{1FBF9}]+|[^\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376-\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0560-\\u0588\\u05D0-\\u05EA\\u05EF-\\u05F2\\u0620-\\u064A\\u066E-\\u066F\\u0671-\\u06D3\\u06D5\\u06E5-\\u06E6\\u06EE-\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4-\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u0860-\\u086A\\u0870-\\u0887\\u0889-\\u088F\\u08A0-\\u08C9\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F-\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC-\\u09DD\\u09DF-\\u09E1\\u09F0-\\u09F1\\u09FC\\u0A05-\\u0A0A\\u0A0F-\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32-\\u0A33\\u0A35-\\u0A36\\u0A38-\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2-\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0-\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F-\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32-\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C-\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99-\\u0B9A\\u0B9C\\u0B9E-\\u0B9F\\u0BA3-\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C5C-\\u0C5D\\u0C60-\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDC-\\u0CDE\\u0CE0-\\u0CE1\\u0CF1-\\u0CF2\\u0D04-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32-\\u0E33\\u0E40-\\u0E46\\u0E81-\\u0E82\\u0E84\\u0E86-\\u0E8A\\u0E8C-\\u0EA3\\u0EA5\\u0EA7-\\u0EB0\\u0EB2-\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065-\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u1711\\u171F-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1878\\u1880-\\u1884\\u1887-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4C\\u1B83-\\u1BA0\\u1BAE-\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C8A\\u1C90-\\u1CBA\\u1CBD-\\u1CBF\\u1CE9-\\u1CEC\\u1CEE-\\u1CF3\\u1CF5-\\u1CF6\\u1CFA\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183-\\u2184\\u2C00-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2-\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3006\\u3031-\\u3035\\u303B-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312F\\u3131-\\u318E\\u31A0-\\u31BF\\u31F0-\\u31FF\\u3400-\\u4DBF\\u4E00-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A-\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7DC\\uA7F1-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD-\\uA8FE\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5-\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB69\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40-\\uFB41\\uFB43-\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC\\u{10000}-\\u{1000B}\\u{1000D}-\\u{10026}\\u{10028}-\\u{1003A}\\u{1003C}-\\u{1003D}\\u{1003F}-\\u{1004D}\\u{10050}-\\u{1005D}\\u{10080}-\\u{100FA}\\u{10280}-\\u{1029C}\\u{102A0}-\\u{102D0}\\u{10300}-\\u{1031F}\\u{1032D}-\\u{10340}\\u{10342}-\\u{10349}\\u{10350}-\\u{10375}\\u{10380}-\\u{1039D}\\u{103A0}-\\u{103C3}\\u{103C8}-\\u{103CF}\\u{10400}-\\u{1049D}\\u{104B0}-\\u{104D3}\\u{104D8}-\\u{104FB}\\u{10500}-\\u{10527}\\u{10530}-\\u{10563}\\u{10570}-\\u{1057A}\\u{1057C}-\\u{1058A}\\u{1058C}-\\u{10592}\\u{10594}-\\u{10595}\\u{10597}-\\u{105A1}\\u{105A3}-\\u{105B1}\\u{105B3}-\\u{105B9}\\u{105BB}-\\u{105BC}\\u{105C0}-\\u{105F3}\\u{10600}-\\u{10736}\\u{10740}-\\u{10755}\\u{10760}-\\u{10767}\\u{10780}-\\u{10785}\\u{10787}-\\u{107B0}\\u{107B2}-\\u{107BA}\\u{10800}-\\u{10805}\\u{10808}\\u{1080A}-\\u{10835}\\u{10837}-\\u{10838}\\u{1083C}\\u{1083F}-\\u{10855}\\u{10860}-\\u{10876}\\u{10880}-\\u{1089E}\\u{108E0}-\\u{108F2}\\u{108F4}-\\u{108F5}\\u{10900}-\\u{10915}\\u{10920}-\\u{10939}\\u{10940}-\\u{10959}\\u{10980}-\\u{109B7}\\u{109BE}-\\u{109BF}\\u{10A00}\\u{10A10}-\\u{10A13}\\u{10A15}-\\u{10A17}\\u{10A19}-\\u{10A35}\\u{10A60}-\\u{10A7C}\\u{10A80}-\\u{10A9C}\\u{10AC0}-\\u{10AC7}\\u{10AC9}-\\u{10AE4}\\u{10B00}-\\u{10B35}\\u{10B40}-\\u{10B55}\\u{10B60}-\\u{10B72}\\u{10B80}-\\u{10B91}\\u{10C00}-\\u{10C48}\\u{10C80}-\\u{10CB2}\\u{10CC0}-\\u{10CF2}\\u{10D00}-\\u{10D23}\\u{10D4A}-\\u{10D65}\\u{10D6F}-\\u{10D85}\\u{10E80}-\\u{10EA9}\\u{10EB0}-\\u{10EB1}\\u{10EC2}-\\u{10EC7}\\u{10F00}-\\u{10F1C}\\u{10F27}\\u{10F30}-\\u{10F45}\\u{10F70}-\\u{10F81}\\u{10FB0}-\\u{10FC4}\\u{10FE0}-\\u{10FF6}\\u{11003}-\\u{11037}\\u{11071}-\\u{11072}\\u{11075}\\u{11083}-\\u{110AF}\\u{110D0}-\\u{110E8}\\u{11103}-\\u{11126}\\u{11144}\\u{11147}\\u{11150}-\\u{11172}\\u{11176}\\u{11183}-\\u{111B2}\\u{111C1}-\\u{111C4}\\u{111DA}\\u{111DC}\\u{11200}-\\u{11211}\\u{11213}-\\u{1122B}\\u{1123F}-\\u{11240}\\u{11280}-\\u{11286}\\u{11288}\\u{1128A}-\\u{1128D}\\u{1128F}-\\u{1129D}\\u{1129F}-\\u{112A8}\\u{112B0}-\\u{112DE}\\u{11305}-\\u{1130C}\\u{1130F}-\\u{11310}\\u{11313}-\\u{11328}\\u{1132A}-\\u{11330}\\u{11332}-\\u{11333}\\u{11335}-\\u{11339}\\u{1133D}\\u{11350}\\u{1135D}-\\u{11361}\\u{11380}-\\u{11389}\\u{1138B}\\u{1138E}\\u{11390}-\\u{113B5}\\u{113B7}\\u{113D1}\\u{113D3}\\u{11400}-\\u{11434}\\u{11447}-\\u{1144A}\\u{1145F}-\\u{11461}\\u{11480}-\\u{114AF}\\u{114C4}-\\u{114C5}\\u{114C7}\\u{11580}-\\u{115AE}\\u{115D8}-\\u{115DB}\\u{11600}-\\u{1162F}\\u{11644}\\u{11680}-\\u{116AA}\\u{116B8}\\u{11700}-\\u{1171A}\\u{11740}-\\u{11746}\\u{11800}-\\u{1182B}\\u{118A0}-\\u{118DF}\\u{118FF}-\\u{11906}\\u{11909}\\u{1190C}-\\u{11913}\\u{11915}-\\u{11916}\\u{11918}-\\u{1192F}\\u{1193F}\\u{11941}\\u{119A0}-\\u{119A7}\\u{119AA}-\\u{119D0}\\u{119E1}\\u{119E3}\\u{11A00}\\u{11A0B}-\\u{11A32}\\u{11A3A}\\u{11A50}\\u{11A5C}-\\u{11A89}\\u{11A9D}\\u{11AB0}-\\u{11AF8}\\u{11BC0}-\\u{11BE0}\\u{11C00}-\\u{11C08}\\u{11C0A}-\\u{11C2E}\\u{11C40}\\u{11C72}-\\u{11C8F}\\u{11D00}-\\u{11D06}\\u{11D08}-\\u{11D09}\\u{11D0B}-\\u{11D30}\\u{11D46}\\u{11D60}-\\u{11D65}\\u{11D67}-\\u{11D68}\\u{11D6A}-\\u{11D89}\\u{11D98}\\u{11DB0}-\\u{11DDB}\\u{11EE0}-\\u{11EF2}\\u{11F02}\\u{11F04}-\\u{11F10}\\u{11F12}-\\u{11F33}\\u{11FB0}\\u{12000}-\\u{12399}\\u{12480}-\\u{12543}\\u{12F90}-\\u{12FF0}\\u{13000}-\\u{1342F}\\u{13441}-\\u{13446}\\u{13460}-\\u{143FA}\\u{14400}-\\u{14646}\\u{16100}-\\u{1611D}\\u{16800}-\\u{16A38}\\u{16A40}-\\u{16A5E}\\u{16A70}-\\u{16ABE}\\u{16AD0}-\\u{16AED}\\u{16B00}-\\u{16B2F}\\u{16B40}-\\u{16B43}\\u{16B63}-\\u{16B77}\\u{16B7D}-\\u{16B8F}\\u{16D40}-\\u{16D6C}\\u{16E40}-\\u{16E7F}\\u{16EA0}-\\u{16EB8}\\u{16EBB}-\\u{16ED3}\\u{16F00}-\\u{16F4A}\\u{16F50}\\u{16F93}-\\u{16F9F}\\u{16FE0}-\\u{16FE1}\\u{16FE3}\\u{16FF2}-\\u{16FF3}\\u{17000}-\\u{18CD5}\\u{18CFF}-\\u{18D1E}\\u{18D80}-\\u{18DF2}\\u{1AFF0}-\\u{1AFF3}\\u{1AFF5}-\\u{1AFFB}\\u{1AFFD}-\\u{1AFFE}\\u{1B000}-\\u{1B122}\\u{1B132}\\u{1B150}-\\u{1B152}\\u{1B155}\\u{1B164}-\\u{1B167}\\u{1B170}-\\u{1B2FB}\\u{1BC00}-\\u{1BC6A}\\u{1BC70}-\\u{1BC7C}\\u{1BC80}-\\u{1BC88}\\u{1BC90}-\\u{1BC99}\\u{1D400}-\\u{1D454}\\u{1D456}-\\u{1D49C}\\u{1D49E}-\\u{1D49F}\\u{1D4A2}\\u{1D4A5}-\\u{1D4A6}\\u{1D4A9}-\\u{1D4AC}\\u{1D4AE}-\\u{1D4B9}\\u{1D4BB}\\u{1D4BD}-\\u{1D4C3}\\u{1D4C5}-\\u{1D505}\\u{1D507}-\\u{1D50A}\\u{1D50D}-\\u{1D514}\\u{1D516}-\\u{1D51C}\\u{1D51E}-\\u{1D539}\\u{1D53B}-\\u{1D53E}\\u{1D540}-\\u{1D544}\\u{1D546}\\u{1D54A}-\\u{1D550}\\u{1D552}-\\u{1D6A5}\\u{1D6A8}-\\u{1D6C0}\\u{1D6C2}-\\u{1D6DA}\\u{1D6DC}-\\u{1D6FA}\\u{1D6FC}-\\u{1D714}\\u{1D716}-\\u{1D734}\\u{1D736}-\\u{1D74E}\\u{1D750}-\\u{1D76E}\\u{1D770}-\\u{1D788}\\u{1D78A}-\\u{1D7A8}\\u{1D7AA}-\\u{1D7C2}\\u{1D7C4}-\\u{1D7CB}\\u{1DF00}-\\u{1DF1E}\\u{1DF25}-\\u{1DF2A}\\u{1E030}-\\u{1E06D}\\u{1E100}-\\u{1E12C}\\u{1E137}-\\u{1E13D}\\u{1E14E}\\u{1E290}-\\u{1E2AD}\\u{1E2C0}-\\u{1E2EB}\\u{1E4D0}-\\u{1E4EB}\\u{1E5D0}-\\u{1E5ED}\\u{1E5F0}\\u{1E6C0}-\\u{1E6DE}\\u{1E6E0}-\\u{1E6E2}\\u{1E6E4}-\\u{1E6E5}\\u{1E6E7}-\\u{1E6ED}\\u{1E6F0}-\\u{1E6F4}\\u{1E6FE}-\\u{1E6FF}\\u{1E7E0}-\\u{1E7E6}\\u{1E7E8}-\\u{1E7EB}\\u{1E7ED}-\\u{1E7EE}\\u{1E7F0}-\\u{1E7FE}\\u{1E800}-\\u{1E8C4}\\u{1E900}-\\u{1E943}\\u{1E94B}\\u{1EE00}-\\u{1EE03}\\u{1EE05}-\\u{1EE1F}\\u{1EE21}-\\u{1EE22}\\u{1EE24}\\u{1EE27}\\u{1EE29}-\\u{1EE32}\\u{1EE34}-\\u{1EE37}\\u{1EE39}\\u{1EE3B}\\u{1EE42}\\u{1EE47}\\u{1EE49}\\u{1EE4B}\\u{1EE4D}-\\u{1EE4F}\\u{1EE51}-\\u{1EE52}\\u{1EE54}\\u{1EE57}\\u{1EE59}\\u{1EE5B}\\u{1EE5D}\\u{1EE5F}\\u{1EE61}-\\u{1EE62}\\u{1EE64}\\u{1EE67}-\\u{1EE6A}\\u{1EE6C}-\\u{1EE72}\\u{1EE74}-\\u{1EE77}\\u{1EE79}-\\u{1EE7C}\\u{1EE7E}\\u{1EE80}-\\u{1EE89}\\u{1EE8B}-\\u{1EE9B}\\u{1EEA1}-\\u{1EEA3}\\u{1EEA5}-\\u{1EEA9}\\u{1EEAB}-\\u{1EEBB}\\u{20000}-\\u{2A6DF}\\u{2A700}-\\u{2B81D}\\u{2B820}-\\u{2CEAD}\\u{2CEB0}-\\u{2EBE0}\\u{2EBF0}-\\u{2EE5D}\\u{2F800}-\\u{2FA1D}\\u{30000}-\\u{3134A}\\u{31350}-\\u{33479}\\u0030-\\u0039\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE\\u0660-\\u0669\\u06F0-\\u06F9\\u07C0-\\u07C9\\u0966-\\u096F\\u09E6-\\u09EF\\u09F4-\\u09F9\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0B72-\\u0B77\\u0BE6-\\u0BF2\\u0C66-\\u0C6F\\u0C78-\\u0C7E\\u0CE6-\\u0CEF\\u0D58-\\u0D5E\\u0D66-\\u0D78\\u0DE6-\\u0DEF\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F33\\u1040-\\u1049\\u1090-\\u1099\\u1369-\\u137C\\u16EE-\\u16F0\\u17E0-\\u17E9\\u17F0-\\u17F9\\u1810-\\u1819\\u1946-\\u194F\\u19D0-\\u19DA\\u1A80-\\u1A89\\u1A90-\\u1A99\\u1B50-\\u1B59\\u1BB0-\\u1BB9\\u1C40-\\u1C49\\u1C50-\\u1C59\\u2070\\u2074-\\u2079\\u2080-\\u2089\\u2150-\\u2182\\u2185-\\u2189\\u2460-\\u249B\\u24EA-\\u24FF\\u2776-\\u2793\\u2CFD\\u3007\\u3021-\\u3029\\u3038-\\u303A\\u3192-\\u3195\\u3220-\\u3229\\u3248-\\u324F\\u3251-\\u325F\\u3280-\\u3289\\u32B1-\\u32BF\\uA620-\\uA629\\uA6E6-\\uA6EF\\uA830-\\uA835\\uA8D0-\\uA8D9\\uA900-\\uA909\\uA9D0-\\uA9D9\\uA9F0-\\uA9F9\\uAA50-\\uAA59\\uABF0-\\uABF9\\uFF10-\\uFF19\\u{10107}-\\u{10133}\\u{10140}-\\u{10178}\\u{1018A}-\\u{1018B}\\u{102E1}-\\u{102FB}\\u{10320}-\\u{10323}\\u{10341}\\u{1034A}\\u{103D1}-\\u{103D5}\\u{104A0}-\\u{104A9}\\u{10858}-\\u{1085F}\\u{10879}-\\u{1087F}\\u{108A7}-\\u{108AF}\\u{108FB}-\\u{108FF}\\u{10916}-\\u{1091B}\\u{109BC}-\\u{109BD}\\u{109C0}-\\u{109CF}\\u{109D2}-\\u{109FF}\\u{10A40}-\\u{10A48}\\u{10A7D}-\\u{10A7E}\\u{10A9D}-\\u{10A9F}\\u{10AEB}-\\u{10AEF}\\u{10B58}-\\u{10B5F}\\u{10B78}-\\u{10B7F}\\u{10BA9}-\\u{10BAF}\\u{10CFA}-\\u{10CFF}\\u{10D30}-\\u{10D39}\\u{10D40}-\\u{10D49}\\u{10E60}-\\u{10E7E}\\u{10F1D}-\\u{10F26}\\u{10F51}-\\u{10F54}\\u{10FC5}-\\u{10FCB}\\u{11052}-\\u{1106F}\\u{110F0}-\\u{110F9}\\u{11136}-\\u{1113F}\\u{111D0}-\\u{111D9}\\u{111E1}-\\u{111F4}\\u{112F0}-\\u{112F9}\\u{11450}-\\u{11459}\\u{114D0}-\\u{114D9}\\u{11650}-\\u{11659}\\u{116C0}-\\u{116C9}\\u{116D0}-\\u{116E3}\\u{11730}-\\u{1173B}\\u{118E0}-\\u{118F2}\\u{11950}-\\u{11959}\\u{11BF0}-\\u{11BF9}\\u{11C50}-\\u{11C6C}\\u{11D50}-\\u{11D59}\\u{11DA0}-\\u{11DA9}\\u{11DE0}-\\u{11DE9}\\u{11F50}-\\u{11F59}\\u{11FC0}-\\u{11FD4}\\u{12400}-\\u{1246E}\\u{16130}-\\u{16139}\\u{16A60}-\\u{16A69}\\u{16AC0}-\\u{16AC9}\\u{16B50}-\\u{16B59}\\u{16B5B}-\\u{16B61}\\u{16D70}-\\u{16D79}\\u{16E80}-\\u{16E96}\\u{16FF4}-\\u{16FF6}\\u{1CCF0}-\\u{1CCF9}\\u{1D2C0}-\\u{1D2D3}\\u{1D2E0}-\\u{1D2F3}\\u{1D360}-\\u{1D378}\\u{1D7CE}-\\u{1D7FF}\\u{1E140}-\\u{1E149}\\u{1E2F0}-\\u{1E2F9}\\u{1E4F0}-\\u{1E4F9}\\u{1E5F1}-\\u{1E5FA}\\u{1E8C7}-\\u{1E8CF}\\u{1E950}-\\u{1E959}\\u{1EC71}-\\u{1ECAB}\\u{1ECAD}-\\u{1ECAF}\\u{1ECB1}-\\u{1ECB4}\\u{1ED01}-\\u{1ED2D}\\u{1ED2F}-\\u{1ED3D}\\u{1F100}-\\u{1F10C}\\u{1FBF0}-\\u{1FBF9}]+$", "gu");
|
|
79
|
+
/** A value as the text it reads as, or '' for anything that is not text. */ const textOf = (value)=>{
|
|
80
|
+
if (typeof value === 'string') return value;
|
|
81
|
+
if (typeof value === 'number' && Number.isFinite(value)) return String(value);
|
|
82
|
+
if (typeof value === 'boolean') return String(value);
|
|
83
|
+
if (Array.isArray(value)) return value.map(textOf).filter(Boolean).join(' ');
|
|
84
|
+
return '';
|
|
85
|
+
};
|
|
86
|
+
/** Who wrote in, from the message's entries in the order given. */ function senderOf(entries) {
|
|
87
|
+
const reduced = new Map();
|
|
88
|
+
for (const [key, value] of entries){
|
|
89
|
+
const text = textOf(value).trim();
|
|
90
|
+
if (!text) continue;
|
|
91
|
+
const at = key.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
92
|
+
if (!reduced.has(at)) reduced.set(at, text);
|
|
93
|
+
}
|
|
94
|
+
const name = SENDER_NAME_KEYS.map((key)=>reduced.get(key)).find(Boolean);
|
|
95
|
+
const email = SENDER_EMAIL_KEYS.map((key)=>reduced.get(key)).find(Boolean);
|
|
96
|
+
return _extends({}, name ? {
|
|
97
|
+
name
|
|
98
|
+
} : {}, email ? {
|
|
99
|
+
email
|
|
100
|
+
} : {});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Who wrote in, by the field-name convention: the first non-empty value of
|
|
104
|
+
* a name-like field and of an address-like field. Reads the map in the
|
|
105
|
+
* order it is handed, so the first spelling of a reduced key wins.
|
|
106
|
+
*/ export function messageSender(fields) {
|
|
107
|
+
return senderOf(Object.entries(fields != null ? fields : {}));
|
|
108
|
+
}
|
|
109
|
+
/** The words of a text: its key's words without edge punctuation, and their parts. */ function wordsOf(text) {
|
|
110
|
+
const words = [];
|
|
111
|
+
for (const raw of nameSearchKey(text).split(' ')){
|
|
112
|
+
const word = raw.replace(WORD_EDGES, '');
|
|
113
|
+
if (!word) continue;
|
|
114
|
+
words.push(word);
|
|
115
|
+
if (WORD_JOINS.test(word)) {
|
|
116
|
+
for (const part of word.split(WORD_PARTS))if (part && part !== word) words.push(part);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return words;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Every prefix of each word, up to the prefix cap, into `tokens` until `max`
|
|
123
|
+
* — cut exactly as `nameSearchTokens` cuts, so the one token a typed word
|
|
124
|
+
* becomes (`nameSearchToken`) is one of these.
|
|
125
|
+
*/ function addPrefixes(tokens, words, max) {
|
|
126
|
+
for (const word of words){
|
|
127
|
+
const capped = word.slice(0, NAME_TOKEN_MAX_PREFIX);
|
|
128
|
+
for(let end = 1; end <= capped.length; end += 1){
|
|
129
|
+
if (tokens.size >= max) return;
|
|
130
|
+
tokens.add(capped.slice(0, end));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The two search arrays a message is stamped with — see the block above.
|
|
136
|
+
* Spread onto every write that creates one; a message's values never change
|
|
137
|
+
* after it arrives, so no other write touches them.
|
|
138
|
+
*/ export function messageSearchFields(fields) {
|
|
139
|
+
var _sender_name, _sender_email;
|
|
140
|
+
// Sorted entries, never an object rebuilt from them: an object puts
|
|
141
|
+
// integer-like keys first whatever order they were added in.
|
|
142
|
+
const sorted = Object.entries(fields != null ? fields : {}).sort(([left], [right])=>left < right ? -1 : left > right ? 1 : 0);
|
|
143
|
+
const sender = senderOf(sorted);
|
|
144
|
+
const senderSet = new Set();
|
|
145
|
+
addPrefixes(senderSet, [
|
|
146
|
+
...wordsOf((_sender_name = sender.name) != null ? _sender_name : ''),
|
|
147
|
+
...wordsOf((_sender_email = sender.email) != null ? _sender_email : '')
|
|
148
|
+
], MESSAGE_SENDER_TOKENS_MAX);
|
|
149
|
+
const words = new Set();
|
|
150
|
+
for (const word of sorted.flatMap(([, value])=>wordsOf(textOf(value)))){
|
|
151
|
+
if (words.size >= MESSAGE_SEARCH_WORDS_MAX) break;
|
|
152
|
+
words.add(word);
|
|
153
|
+
}
|
|
154
|
+
const search = new Set(senderSet);
|
|
155
|
+
addPrefixes(search, [
|
|
156
|
+
...words
|
|
157
|
+
], MESSAGE_SEARCH_TOKENS_MAX);
|
|
158
|
+
return {
|
|
159
|
+
senderTokens: [
|
|
160
|
+
...senderSet
|
|
161
|
+
],
|
|
162
|
+
searchTokens: [
|
|
163
|
+
...search
|
|
164
|
+
]
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
//# sourceMappingURL=message-search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/message-search.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\nimport { NAME_TOKEN_MAX_PREFIX, nameSearchKey } from './name-search'\n\n/*\n * THE SEARCH KEYS OF A MESSAGE A VISITOR SENT (AGL-3321).\n *\n * A message is a free-form map of the fields its author declared — `name`,\n * `Email Address`, `message`, `Company size` — with no guaranteed shape. A\n * list of them is searched on its Firestore query, which cannot look inside\n * a string, so every writer stamps the words as word-prefix token arrays the\n * query asks with `array-contains`:\n *\n * senderTokens the sender's name and address — who wrote in. What the\n * list's From filter asks.\n * searchTokens the sender's tokens, then the words of every value the\n * message carries. What the list's search box asks.\n *\n * The words are the platform's search keys (`nameSearchKey`, prefixes up to\n * `NAME_TOKEN_MAX_PREFIX`), so a typed word becomes the token the query asks\n * through the same `nameSearchNormalizers` every list uses. A word that joins\n * parts with `@ . + _ -` — an address, a domain, a hyphenated name — is also\n * its parts, so `acme` finds `dana@acme.com` and `dana` does too.\n *\n * ## What is capped\n *\n * A message is free text, so the arrays are bounded rather than complete:\n * the first {@link MESSAGE_SEARCH_WORDS_MAX} distinct words of the values\n * contribute, and each array stops at its own ceiling\n * ({@link MESSAGE_SENDER_TOKENS_MAX}, {@link MESSAGE_SEARCH_TOKENS_MAX}) —\n * the sender first, so a long message loses the tail of its text before it\n * loses who sent it. A word past the cap is still in the message; the search\n * box just does not find it.\n *\n * ## Deterministic whatever the map's key order\n *\n * A map's keys come back in a different order from different readers (the\n * Admin SDK sorts them), and a cap makes the result order-sensitive. So the\n * values are read in SORTED key order, and a backfill that re-derives the\n * arrays from a stored message computes exactly what the writer stamped.\n *\n * `tools/scripts/backfill-form-submission-filters.mjs` restates this for the\n * rows written before it, held to `tools/scripts/lib/message-search.fixtures.json`\n * with the spec beside this file.\n */\n\n/** The most tokens the sender contributes. A name and an address are a few dozen. */\nexport const MESSAGE_SENDER_TOKENS_MAX = 60\n\n/** The most tokens one message's search array holds, the sender's included. */\nexport const MESSAGE_SEARCH_TOKENS_MAX = 200\n\n/** The most distinct words of the message's values that contribute prefixes. */\nexport const MESSAGE_SEARCH_WORDS_MAX = 40\n\n/**\n * Field names that mean \"this is who wrote in\", most specific first,\n * compared against a key REDUCED to lowercase letters and digits — the same\n * field arrives as `Full Name`, `full_name` and `fullname`.\n */\nconst SENDER_NAME_KEYS = ['name', 'fullname', 'yourname', 'firstname', 'contactname']\nconst SENDER_EMAIL_KEYS = ['email', 'emailaddress']\n\n/** The joins a word is also split on, so an address is also its parts. */\nconst WORD_JOINS = /[@.+_-]/\nconst WORD_PARTS = /[@.+_-]+/\n/** Punctuation around a word — `cake?`, `(june)` — is not part of it. */\nconst WORD_EDGES = /^[^\\p{L}\\p{N}]+|[^\\p{L}\\p{N}]+$/gu\n\n/** A value as the text it reads as, or '' for anything that is not text. */\nconst textOf = (value: unknown): string => {\n if (typeof value === 'string') return value\n if (typeof value === 'number' && Number.isFinite(value)) return String(value)\n if (typeof value === 'boolean') return String(value)\n if (Array.isArray(value)) return value.map(textOf).filter(Boolean).join(' ')\n return ''\n}\n\n/** Who wrote in, from the message's entries in the order given. */\nfunction senderOf(entries: ReadonlyArray<readonly [string, unknown]>): {\n name?: string\n email?: string\n} {\n const reduced = new Map<string, string>()\n for (const [key, value] of entries) {\n const text = textOf(value).trim()\n if (!text) continue\n const at = key.toLowerCase().replace(/[^a-z0-9]/g, '')\n if (!reduced.has(at)) reduced.set(at, text)\n }\n const name = SENDER_NAME_KEYS.map((key) => reduced.get(key)).find(Boolean)\n const email = SENDER_EMAIL_KEYS.map((key) => reduced.get(key)).find(Boolean)\n return { ...(name ? { name } : {}), ...(email ? { email } : {}) }\n}\n\n/**\n * Who wrote in, by the field-name convention: the first non-empty value of\n * a name-like field and of an address-like field. Reads the map in the\n * order it is handed, so the first spelling of a reduced key wins.\n */\nexport function messageSender(\n fields: Record<string, unknown> | null | undefined,\n): { name?: string; email?: string } {\n return senderOf(Object.entries(fields ?? {}))\n}\n\n/** The words of a text: its key's words without edge punctuation, and their parts. */\nfunction wordsOf(text: string): string[] {\n const words: string[] = []\n for (const raw of nameSearchKey(text).split(' ')) {\n const word = raw.replace(WORD_EDGES, '')\n if (!word) continue\n words.push(word)\n if (WORD_JOINS.test(word)) {\n for (const part of word.split(WORD_PARTS)) if (part && part !== word) words.push(part)\n }\n }\n return words\n}\n\n/**\n * Every prefix of each word, up to the prefix cap, into `tokens` until `max`\n * — cut exactly as `nameSearchTokens` cuts, so the one token a typed word\n * becomes (`nameSearchToken`) is one of these.\n */\nfunction addPrefixes(tokens: Set<string>, words: readonly string[], max: number): void {\n for (const word of words) {\n const capped = word.slice(0, NAME_TOKEN_MAX_PREFIX)\n for (let end = 1; end <= capped.length; end += 1) {\n if (tokens.size >= max) return\n tokens.add(capped.slice(0, end))\n }\n }\n}\n\n/**\n * The two search arrays a message is stamped with — see the block above.\n * Spread onto every write that creates one; a message's values never change\n * after it arrives, so no other write touches them.\n */\nexport function messageSearchFields(fields: Record<string, unknown> | null | undefined): {\n senderTokens: string[]\n searchTokens: string[]\n} {\n // Sorted entries, never an object rebuilt from them: an object puts\n // integer-like keys first whatever order they were added in.\n const sorted = Object.entries(fields ?? {}).sort(([left], [right]) =>\n left < right ? -1 : left > right ? 1 : 0,\n )\n const sender = senderOf(sorted)\n const senderSet = new Set<string>()\n addPrefixes(\n senderSet,\n [...wordsOf(sender.name ?? ''), ...wordsOf(sender.email ?? '')],\n MESSAGE_SENDER_TOKENS_MAX,\n )\n const words = new Set<string>()\n for (const word of sorted.flatMap(([, value]) => wordsOf(textOf(value)))) {\n if (words.size >= MESSAGE_SEARCH_WORDS_MAX) break\n words.add(word)\n }\n const search = new Set<string>(senderSet)\n addPrefixes(search, [...words], MESSAGE_SEARCH_TOKENS_MAX)\n return { senderTokens: [...senderSet], searchTokens: [...search] }\n}\n"],"names":["NAME_TOKEN_MAX_PREFIX","nameSearchKey","MESSAGE_SENDER_TOKENS_MAX","MESSAGE_SEARCH_TOKENS_MAX","MESSAGE_SEARCH_WORDS_MAX","SENDER_NAME_KEYS","SENDER_EMAIL_KEYS","WORD_JOINS","WORD_PARTS","WORD_EDGES","textOf","value","Number","isFinite","String","Array","isArray","map","filter","Boolean","join","senderOf","entries","reduced","Map","key","text","trim","at","toLowerCase","replace","has","set","name","get","find","email","messageSender","fields","Object","wordsOf","words","raw","split","word","push","test","part","addPrefixes","tokens","max","capped","slice","end","length","size","add","messageSearchFields","sender","sorted","sort","left","right","senderSet","Set","flatMap","search","senderTokens","searchTokens"],"mappings":"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"}
|
|
@@ -115,6 +115,33 @@ export declare function nameSearchFields(name: string): {
|
|
|
115
115
|
nameTokens: string[];
|
|
116
116
|
nameReversed: string;
|
|
117
117
|
};
|
|
118
|
+
/**
|
|
119
|
+
* The search fields a document NAMED BY `displayName` carries (AGL-3321):
|
|
120
|
+
* the site artifacts (screens of every kind, layouts, reusable components,
|
|
121
|
+
* templates) and the marketplace listings.
|
|
122
|
+
*
|
|
123
|
+
* `nameSearchFields` is for documents whose name IS `name`. These documents
|
|
124
|
+
* name themselves `displayName`, and have since before search existed, so
|
|
125
|
+
* the keys ride beside it under the same three names every list declares —
|
|
126
|
+
* `lowerPath: 'nameLower'`, `tokensPath: 'nameTokens'`,
|
|
127
|
+
* `reversedPath: 'nameReversed'` — and one list query grammar reads them all.
|
|
128
|
+
*
|
|
129
|
+
* `displayName` is NOT returned. A create that carries no name must not gain
|
|
130
|
+
* an empty one: readers fall back with `displayName ?? id`, which an empty
|
|
131
|
+
* string defeats. But the KEYS are always returned, empty for a missing name,
|
|
132
|
+
* so a create stamps them unconditionally — an `orderBy('nameLower')` drops
|
|
133
|
+
* any document that lacks the field, and a document created without a name
|
|
134
|
+
* is still one a list must reach.
|
|
135
|
+
*
|
|
136
|
+
* ⚠️ Spread at every write that sets `displayName`, and at every create —
|
|
137
|
+
* and nowhere else: a partial update that stamped keys for a name it did not
|
|
138
|
+
* write would make the document unfindable by the name it still shows.
|
|
139
|
+
*/
|
|
140
|
+
export declare function displayNameSearchFields(displayName: unknown): {
|
|
141
|
+
nameLower: string;
|
|
142
|
+
nameTokens: string[];
|
|
143
|
+
nameReversed: string;
|
|
144
|
+
};
|
|
118
145
|
/**
|
|
119
146
|
* The normalizers a list query plan (`planListQuery` in
|
|
120
147
|
* `@aglyn/shared-ui-jsx/const/list-query-plan`) turns typed values into keys
|
|
@@ -127,3 +154,59 @@ export declare const nameSearchNormalizers: {
|
|
|
127
154
|
readonly reversed: (value: string) => string;
|
|
128
155
|
readonly maxPrefix: 12;
|
|
129
156
|
};
|
|
157
|
+
/**
|
|
158
|
+
* What joins a scope token to a search prefix in a SCOPED search token
|
|
159
|
+
* (AGL-3321): `host:abc~acm`.
|
|
160
|
+
*
|
|
161
|
+
* A list under a scope clause (`visibleTo array-contains-any [...]`) has
|
|
162
|
+
* spent Firestore's one array clause on the scope, so its search cannot be a
|
|
163
|
+
* second `array-contains`. The list query plan folds the typed word INTO the scope
|
|
164
|
+
* clause instead (`ListQueryDeclaration.search.scoped`), asking for each
|
|
165
|
+
* scope token joined to the word; the writers stamp every scope token
|
|
166
|
+
* joined to every prefix, through {@link scopedSearchTokens}. A declaration
|
|
167
|
+
* names this constant as its `join`, so the writer and the reader cannot
|
|
168
|
+
* disagree about the character between the two halves.
|
|
169
|
+
*
|
|
170
|
+
* ⛔ Security rules cannot prove a folded query for a reader limited to some
|
|
171
|
+
* sites. Rules like `canReadScoped` grant a list from
|
|
172
|
+
* `visibleTo.hasAny(<the reader's scopes>)`, and a query that constrains only
|
|
173
|
+
* the scoped-token field says nothing about `visibleTo`, so Firestore denies
|
|
174
|
+
* it (pinned in cloud/rules-tests/firestore-rules.test.mjs). A folded search
|
|
175
|
+
* is therefore for readers the rules admit WITHOUT a `visibleTo` term (an
|
|
176
|
+
* org-wide member, whose rule short-circuits) or for an Admin-SDK route that
|
|
177
|
+
* checks the reader's scope itself. A partial-reach collaborator's search
|
|
178
|
+
* stays beside the `visibleTo` clause instead, as a prefix range on one
|
|
179
|
+
* ordered key (media: `nameLower`; inbox leads: `email`).
|
|
180
|
+
*
|
|
181
|
+
* `~` appears in no scope token (`org`, `host:{id}`), so the split is never
|
|
182
|
+
* ambiguous; a prefix that itself holds a `~` is matched whole on both sides
|
|
183
|
+
* and needs no escaping.
|
|
184
|
+
*/
|
|
185
|
+
export declare const SCOPED_SEARCH_JOIN = "~";
|
|
186
|
+
/**
|
|
187
|
+
* The `search.scoped` half of a list declaration, over the array the
|
|
188
|
+
* writers stamp with {@link scopedSearchTokens}.
|
|
189
|
+
*/
|
|
190
|
+
export declare function scopedSearch(tokensPath: string): {
|
|
191
|
+
tokensPath: string;
|
|
192
|
+
join: typeof SCOPED_SEARCH_JOIN;
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* Every scope a record is visible to, joined to every search prefix it
|
|
196
|
+
* carries: `[org~a, org~ac, host:x~a, …]`, in scope order then token order,
|
|
197
|
+
* with no duplicates (AGL-3321).
|
|
198
|
+
*
|
|
199
|
+
* `visibleTo` is read defensively — a record's stored array — and keeps
|
|
200
|
+
* every non-empty string in it; anything else answers no scope, and so no
|
|
201
|
+
* scoped tokens: a record visible to nobody is found by no scoped search.
|
|
202
|
+
* `tokens` is whatever the record's plain search array holds, so a list that
|
|
203
|
+
* searches several fields passes their merged prefixes rather than one
|
|
204
|
+
* name's. Each is stamped beside the plain array on every write that sets
|
|
205
|
+
* either the searched text or `visibleTo`.
|
|
206
|
+
*
|
|
207
|
+
* The count is the product of the two, so a record placed on thirty sites
|
|
208
|
+
* with a long name carries thousands of entries. That stays far inside
|
|
209
|
+
* Firestore's per-document index-entry ceiling, and it is the price of a
|
|
210
|
+
* search a scoped reader can run at all.
|
|
211
|
+
*/
|
|
212
|
+
export declare function scopedSearchTokens(visibleTo: unknown, tokens: readonly string[]): string[];
|