@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
|
@@ -615,5 +615,82 @@ export function embeddedMetadataReadable(contentType) {
|
|
|
615
615
|
return value;
|
|
616
616
|
}
|
|
617
617
|
}
|
|
618
|
+
/**
|
|
619
|
+
* Whether a stored record still describes the asset's bytes (AGL-3331).
|
|
620
|
+
* Stale when the reader has changed shape since, or when the bytes were
|
|
621
|
+
* replaced by a path that did not re-read them — both are answered by
|
|
622
|
+
* reading the file again, never by showing the old record.
|
|
623
|
+
*/ export function embeddedMetadataIsCurrent(stored, contentSha256) {
|
|
624
|
+
if (!stored || typeof stored !== 'object') return false;
|
|
625
|
+
const record = stored;
|
|
626
|
+
if (record.version !== MEDIA_EMBEDDED_METADATA_VERSION) return false;
|
|
627
|
+
if (!Array.isArray(record.fields)) return false;
|
|
628
|
+
return typeof contentSha256 !== 'string' || record.contentSha256 === contentSha256;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* What a person would type to find a file, most telling first (AGL-3339):
|
|
632
|
+
* its title, its keywords, who made it and where. A search token budget is
|
|
633
|
+
* spent in this order, so a long caption can never crowd out a title. GPS,
|
|
634
|
+
* camera settings, dates and software are left out — nobody searches a
|
|
635
|
+
* library by shutter speed.
|
|
636
|
+
*/ const SEARCHABLE_EMBEDDED_KEYS = [
|
|
637
|
+
'title',
|
|
638
|
+
'keywords',
|
|
639
|
+
'headline',
|
|
640
|
+
'subject',
|
|
641
|
+
'creator',
|
|
642
|
+
'credit',
|
|
643
|
+
'source',
|
|
644
|
+
'city',
|
|
645
|
+
'state',
|
|
646
|
+
'country',
|
|
647
|
+
'category',
|
|
648
|
+
'label',
|
|
649
|
+
'description'
|
|
650
|
+
];
|
|
651
|
+
/**
|
|
652
|
+
* The searchable text of a stored record, for the library's search keys
|
|
653
|
+
* (AGL-3339). Empty for anything that is not a current-shape record, so a
|
|
654
|
+
* caller can pass a document field straight through.
|
|
655
|
+
*/ export function mediaEmbeddedSearchText(stored) {
|
|
656
|
+
const fields = stored == null ? void 0 : stored.fields;
|
|
657
|
+
if (!Array.isArray(fields)) return '';
|
|
658
|
+
const byKey = new Map(fields.map((field)=>[
|
|
659
|
+
field == null ? void 0 : field.key,
|
|
660
|
+
field
|
|
661
|
+
]));
|
|
662
|
+
return SEARCHABLE_EMBEDDED_KEYS.flatMap((key)=>{
|
|
663
|
+
const field = byKey.get(key);
|
|
664
|
+
if (!field) return [];
|
|
665
|
+
return Array.isArray(field.values) ? field.values.map(String) : typeof field.value === 'string' ? [
|
|
666
|
+
field.value
|
|
667
|
+
] : [];
|
|
668
|
+
}).map((text)=>text.trim()).filter(Boolean).join(' ');
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* The stored record as the customer API and the MCP tools return it
|
|
672
|
+
* (AGL-3339), or `null` when there is no current record for these bytes —
|
|
673
|
+
* never an old file's details under a new file's id.
|
|
674
|
+
*
|
|
675
|
+
* `sources` and `editable` stay behind: they answer the drawer's question
|
|
676
|
+
* (can this be written back, and where from), and the API has no call that
|
|
677
|
+
* writes a file's details.
|
|
678
|
+
*/ export function mediaEmbeddedPublicView(stored, contentSha256) {
|
|
679
|
+
if (!embeddedMetadataIsCurrent(stored, contentSha256)) return null;
|
|
680
|
+
return {
|
|
681
|
+
format: stored.format,
|
|
682
|
+
truncated: stored.truncated === true,
|
|
683
|
+
fields: stored.fields.map((field)=>{
|
|
684
|
+
var _field_value;
|
|
685
|
+
return {
|
|
686
|
+
key: field.key,
|
|
687
|
+
label: field.label,
|
|
688
|
+
group: field.group,
|
|
689
|
+
value: Array.isArray(field.values) ? null : (_field_value = field.value) != null ? _field_value : '',
|
|
690
|
+
values: Array.isArray(field.values) ? field.values.map(String) : null
|
|
691
|
+
};
|
|
692
|
+
})
|
|
693
|
+
};
|
|
694
|
+
}
|
|
618
695
|
|
|
619
696
|
//# sourceMappingURL=media-embedded-fields.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/media-embedded-fields.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 * The metadata a file carries INSIDE its own bytes (AGL-3331): EXIF, IPTC\n * and XMP on a photo, the document info of a PDF, the core and custom\n * properties of an Office file, the tags of a video container.\n *\n * ## Why this module is client-safe and the parsers are not\n *\n * The parsers and writers live in `media-embedded-metadata/` and are exposed\n * only through `@aglyn/aglyn/server` — they need `node:zlib` and\n * `node:crypto`, and nothing in a browser ever holds the bytes they read.\n * What the console needs is the SHAPE of what they produced (it is stored on\n * the media document) and the rules for presenting and editing it, which is\n * everything here. Keeping the two apart keeps a PDF parser out of the\n * console bundle.\n *\n * ## Canonical keys\n *\n * A photo's caption can be written in three places at once — XMP\n * `dc:description`, IPTC 2:120 and EXIF `ImageDescription` — and a PDF's in\n * two. The reader folds every spelling of one idea into one canonical key\n * ({@link MEDIA_EMBEDDED_CATALOG}), so the drawer shows ONE \"Description\"\n * rather than three rows that may or may not agree, and the writer fans an\n * edit back out to every place that already holds it (the Metadata Working\n * Group's rule: never leave a stale copy for the next reader to prefer).\n *\n * Anything a file carries that is not in the catalog — an XMP property from\n * a DAM we have never heard of, a PDF custom property, a PNG text chunk —\n * still surfaces, under a namespaced \"other\" key ({@link otherEmbeddedKey})\n * that round-trips to the writer unchanged.\n */\n\n/** Where a value was read from. Several sources can back one field. */\nexport type MediaEmbeddedSource =\n | 'xmp'\n | 'iptc'\n | 'exif'\n | 'icc'\n | 'png'\n | 'comment'\n | 'pdf'\n | 'ooxml'\n | 'mp4'\n | 'webm'\n | 'svg'\n\n/** How the drawer groups fields, in display order. */\nexport type MediaEmbeddedGroup =\n | 'description'\n | 'rights'\n | 'location'\n | 'capture'\n | 'document'\n | 'technical'\n | 'other'\n\nexport const MEDIA_EMBEDDED_GROUP_ORDER: readonly MediaEmbeddedGroup[] = [\n 'description',\n 'rights',\n 'location',\n 'capture',\n 'document',\n 'technical',\n 'other',\n]\n\nexport const MEDIA_EMBEDDED_GROUP_LABELS: Record<MediaEmbeddedGroup, string> = {\n description: 'Description',\n rights: 'Rights',\n location: 'Location',\n capture: 'Camera',\n document: 'Document',\n technical: 'Technical',\n other: 'Other embedded fields',\n}\n\n/**\n * The container a file was read as. `null`-free: a file whose format has no\n * reader is simply not given an `embeddedMetadata` record at all.\n */\nexport type MediaEmbeddedFormat =\n | 'jpeg'\n | 'png'\n | 'webp'\n | 'gif'\n | 'tiff'\n | 'heif'\n | 'svg'\n | 'pdf'\n | 'ooxml'\n | 'mp4'\n | 'webm'\n\n/**\n * The formats whose bytes can take an edit without being re-encoded.\n *\n * Every writer here rewrites METADATA containers only — JPEG APP segments,\n * PNG ancillary chunks, WebP RIFF chunks, a TIFF's IFD, a PDF incremental\n * update, an Office package's property parts — and copies the image data,\n * page content and every other part through byte for byte. A format that\n * cannot promise that (a video's `moov` would move its sample offsets; an\n * SVG is sanitized on every ingress and is its own markup) is read-only.\n */\nexport const MEDIA_EMBEDDED_WRITABLE_FORMATS: ReadonlySet<MediaEmbeddedFormat> =\n new Set(['jpeg', 'png', 'webp', 'tiff', 'pdf', 'ooxml'])\n\n/** How a value is typed, which decides the editor and the validation. */\nexport type MediaEmbeddedValueKind =\n | 'text'\n | 'longText'\n | 'list'\n | 'date'\n | 'rating'\n | 'gps'\n\nexport interface MediaEmbeddedCatalogEntry {\n label: string\n group: MediaEmbeddedGroup\n kind: MediaEmbeddedValueKind\n /**\n * The formats an edit of this key can be written into. Absent means the\n * key is read-only everywhere — a measured fact such as an exposure time\n * or a page count, which describes the bytes rather than annotating them.\n */\n writable?: readonly MediaEmbeddedFormat[]\n /**\n * `gps` only: the one edit offered is removal. Moving a photo's recorded\n * location is not a thing anybody needs from a DAM; taking it OFF a photo\n * before it goes on a public page is.\n */\n removeOnly?: boolean\n /** Format-specific label, where a format names the idea differently. */\n labels?: Partial<Record<MediaEmbeddedFormat, string>>\n /** Format-specific group — a PDF's creation date is not a camera fact. */\n groups?: Partial<Record<MediaEmbeddedFormat, MediaEmbeddedGroup>>\n}\n\nconst IMAGE_WRITABLE: readonly MediaEmbeddedFormat[] = [\n 'jpeg',\n 'png',\n 'webp',\n 'tiff',\n]\nconst IMAGE_AND_PDF: readonly MediaEmbeddedFormat[] = [...IMAGE_WRITABLE, 'pdf']\nconst IMAGE_PDF_OOXML: readonly MediaEmbeddedFormat[] = [\n ...IMAGE_AND_PDF,\n 'ooxml',\n]\n\n/**\n * Every canonical key, in display order within its group.\n *\n * ## Value formats every reader and writer agrees on\n *\n * - `date`: ISO 8601 — `2021-05-03T10:11:12`, with `Z` or `±HH:MM` only when\n * the source recorded a zone (EXIF 2.3's `OffsetTimeOriginal`, a PDF\n * date's `+05'00'`). Never invented: a local camera time stays local.\n * - `list`: an array of strings (creators, keywords).\n * - `gps`: `\"<lat>,<lon>\"` in signed decimal degrees, six places.\n * - `rating`: `\"0\"`–`\"5\"`, XMP's scale; `\"-1\"` is XMP's \"rejected\".\n * - technical values are display strings already (`1/250`, `f/2.8`,\n * `35 mm`) because nothing edits them.\n */\nexport const MEDIA_EMBEDDED_CATALOG = {\n title: {\n label: 'Title',\n group: 'description',\n kind: 'text',\n writable: IMAGE_PDF_OOXML,\n },\n headline: {\n label: 'Headline',\n group: 'description',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n description: {\n label: 'Description',\n group: 'description',\n kind: 'longText',\n writable: IMAGE_PDF_OOXML,\n labels: { pdf: 'Subject', ooxml: 'Comments' },\n },\n subject: {\n label: 'Subject',\n group: 'description',\n kind: 'text',\n writable: ['ooxml'],\n },\n keywords: {\n label: 'Keywords',\n group: 'description',\n kind: 'list',\n writable: IMAGE_PDF_OOXML,\n },\n category: {\n label: 'Category',\n group: 'description',\n kind: 'text',\n writable: ['ooxml'],\n },\n comment: {\n label: 'Comment',\n group: 'description',\n kind: 'longText',\n writable: ['png'],\n },\n instructions: {\n label: 'Instructions',\n group: 'description',\n kind: 'longText',\n writable: IMAGE_WRITABLE,\n },\n label: {\n label: 'Label',\n group: 'description',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n rating: {\n label: 'Rating',\n group: 'description',\n kind: 'rating',\n writable: IMAGE_WRITABLE,\n },\n creator: {\n label: 'Creator',\n group: 'rights',\n kind: 'list',\n writable: IMAGE_PDF_OOXML,\n labels: { pdf: 'Author', ooxml: 'Author' },\n },\n copyright: {\n label: 'Copyright',\n group: 'rights',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n credit: {\n label: 'Credit line',\n group: 'rights',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n source: {\n label: 'Source',\n group: 'rights',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n usageTerms: {\n label: 'Usage terms',\n group: 'rights',\n kind: 'longText',\n writable: IMAGE_WRITABLE,\n },\n webStatement: {\n label: 'Copyright info URL',\n group: 'rights',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n city: {\n label: 'City',\n group: 'location',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n state: {\n label: 'State / province',\n group: 'location',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n country: {\n label: 'Country',\n group: 'location',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n gps: {\n label: 'GPS position',\n group: 'location',\n kind: 'gps',\n writable: IMAGE_WRITABLE,\n removeOnly: true,\n },\n gpsAltitude: { label: 'Altitude', group: 'location', kind: 'text' },\n createdAt: {\n label: 'Date taken',\n group: 'capture',\n kind: 'date',\n writable: IMAGE_AND_PDF,\n labels: {\n pdf: 'Created',\n ooxml: 'Created',\n svg: 'Created',\n mp4: 'Recorded',\n webm: 'Recorded',\n },\n groups: { pdf: 'document', ooxml: 'document', svg: 'document' },\n },\n make: {\n label: 'Camera make',\n group: 'capture',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n model: {\n label: 'Camera model',\n group: 'capture',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n lens: { label: 'Lens', group: 'capture', kind: 'text', writable: IMAGE_WRITABLE },\n exposure: { label: 'Exposure', group: 'capture', kind: 'text' },\n aperture: { label: 'Aperture', group: 'capture', kind: 'text' },\n iso: { label: 'ISO', group: 'capture', kind: 'text' },\n focalLength: { label: 'Focal length', group: 'capture', kind: 'text' },\n flash: { label: 'Flash', group: 'capture', kind: 'text' },\n modifiedAt: {\n label: 'Modified',\n group: 'document',\n kind: 'date',\n },\n software: {\n label: 'Software',\n group: 'document',\n kind: 'text',\n writable: IMAGE_AND_PDF,\n labels: { pdf: 'Created with', ooxml: 'Application' },\n },\n producer: {\n label: 'PDF producer',\n group: 'document',\n kind: 'text',\n writable: ['pdf'],\n },\n lastModifiedBy: { label: 'Last modified by', group: 'document', kind: 'text' },\n company: { label: 'Company', group: 'document', kind: 'text' },\n pageCount: {\n label: 'Pages',\n group: 'document',\n kind: 'text',\n labels: { ooxml: 'Pages / slides' },\n },\n pdfVersion: { label: 'PDF version', group: 'document', kind: 'text' },\n orientation: { label: 'Orientation', group: 'technical', kind: 'text' },\n colorProfile: { label: 'Color profile', group: 'technical', kind: 'text' },\n encoder: { label: 'Encoder', group: 'technical', kind: 'text' },\n} as const satisfies Record<string, MediaEmbeddedCatalogEntry>\n\nexport type MediaEmbeddedCanonicalKey = keyof typeof MEDIA_EMBEDDED_CATALOG\n\n/**\n * The namespaces an \"other\" key can come from. The key round-trips from\n * reader to writer verbatim, so its only job is to be unambiguous:\n *\n * - `xmp|<namespace URI>|<local name>` — a simple XMP property outside the\n * catalog. Keyed by URI, not prefix, because two files may bind the same\n * namespace to different prefixes and must still produce one key.\n * - `png|<keyword>` — a PNG `tEXt`/`zTXt`/`iTXt` chunk.\n * - `pdf|<Name>` — a PDF document-info entry (Acrobat's \"Custom\" tab).\n * - `ooxml|<name>` — an Office custom document property.\n * - `mp4|<atom or freeform name>`, `webm|<TAG>` — video tags, read-only.\n */\nexport type MediaEmbeddedOtherNamespace =\n | 'xmp'\n | 'png'\n | 'pdf'\n | 'ooxml'\n | 'mp4'\n | 'webm'\n\nexport function otherEmbeddedKey(\n namespace: MediaEmbeddedOtherNamespace,\n ...parts: string[]\n): string {\n return [namespace, ...parts].join('|')\n}\n\nexport function parseOtherEmbeddedKey(\n key: string,\n): { namespace: MediaEmbeddedOtherNamespace; parts: string[] } | null {\n const [namespace = '', ...parts] = key.split('|')\n if (!parts.length) return null\n if (!['xmp', 'png', 'pdf', 'ooxml', 'mp4', 'webm'].includes(namespace))\n return null\n return { namespace: namespace as MediaEmbeddedOtherNamespace, parts }\n}\n\n/** The \"other\" namespaces a writer accepts edits for, by format. */\nconst OTHER_WRITABLE: Partial<\n Record<MediaEmbeddedOtherNamespace, readonly MediaEmbeddedFormat[]>\n> = {\n xmp: IMAGE_WRITABLE,\n png: ['png'],\n pdf: ['pdf'],\n ooxml: ['ooxml'],\n}\n\n/** One field as stored on the media document and shown in the drawer. */\nexport interface MediaEmbeddedField {\n /** A {@link MediaEmbeddedCanonicalKey} or an {@link otherEmbeddedKey}. */\n key: string\n label: string\n group: MediaEmbeddedGroup\n /** Single-valued fields. Absent when `values` is set. */\n value?: string\n /** List-valued fields (creators, keywords). */\n values?: string[]\n /** Every block the value was found in, strongest first. */\n sources: MediaEmbeddedSource[]\n /** Whether the drawer may offer an edit for it on this file. */\n editable: boolean\n}\n\n/**\n * `embeddedMetadata` on a media document (AGL-3331). Server-written only —\n * the Firestore rules lock it like every other fact read from the bytes.\n */\nexport interface MediaEmbeddedMetadata {\n /** Bumped when the reader's output changes shape, so a sweep can re-read. */\n version: number\n format: MediaEmbeddedFormat\n fields: MediaEmbeddedField[]\n /** Whether an edit can be written back into this file at all. */\n writable: boolean\n /** Why not, in a sentence for the drawer, when `writable` is false. */\n readOnlyReason?: string\n /** True when fields were dropped to stay inside the stored caps. */\n truncated?: boolean\n /**\n * The `contentSha256` of the bytes this was read from. A record whose\n * digest no longer matches the document's is stale — the file was\n * replaced by a path that did not re-read it — and is re-read on open.\n */\n contentSha256?: string\n}\n\n/** The reader's output version. See {@link MediaEmbeddedMetadata.version}. */\nexport const MEDIA_EMBEDDED_METADATA_VERSION = 1\n\n/** Stored caps: one media document must never approach Firestore's 1 MiB. */\nexport const MEDIA_EMBEDDED_MAX_FIELDS = 150\nexport const MEDIA_EMBEDDED_MAX_VALUE_LENGTH = 2000\nexport const MEDIA_EMBEDDED_MAX_LIST_ITEMS = 100\n/** A single keyword or creator; IPTC's own limit for keywords is 64 bytes. */\nexport const MEDIA_EMBEDDED_MAX_LIST_ITEM_LENGTH = 256\n\n/**\n * An edit to a file's embedded metadata: key → new value, or `null` to\n * remove it from the file. A list-valued key takes an array.\n */\nexport type MediaEmbeddedPatch = Record<string, string | string[] | null>\n\nexport function isCanonicalEmbeddedKey(\n key: string,\n): key is MediaEmbeddedCanonicalKey {\n return Object.prototype.hasOwnProperty.call(MEDIA_EMBEDDED_CATALOG, key)\n}\n\n/** The label a key carries on a given format. */\nexport function embeddedFieldLabel(\n key: string,\n format: MediaEmbeddedFormat,\n fallback?: string,\n): string {\n if (isCanonicalEmbeddedKey(key)) {\n const entry: MediaEmbeddedCatalogEntry = MEDIA_EMBEDDED_CATALOG[key]\n return entry.labels?.[format] ?? entry.label\n }\n return fallback ?? parseOtherEmbeddedKey(key)?.parts.at(-1) ?? key\n}\n\n/** The group a key is shown under on a given format. */\nexport function embeddedFieldGroup(\n key: string,\n format: MediaEmbeddedFormat,\n): MediaEmbeddedGroup {\n if (!isCanonicalEmbeddedKey(key)) return 'other'\n const entry: MediaEmbeddedCatalogEntry = MEDIA_EMBEDDED_CATALOG[key]\n return entry.groups?.[format] ?? entry.group\n}\n\n/** How a key's value is edited. Other keys are free text. */\nexport function embeddedFieldKind(key: string): MediaEmbeddedValueKind {\n return isCanonicalEmbeddedKey(key)\n ? (MEDIA_EMBEDDED_CATALOG[key] as MediaEmbeddedCatalogEntry).kind\n : 'text'\n}\n\n/**\n * Whether an edit of `key` can be written into a file of `format`.\n *\n * The one place the answer lives: the reader stamps `editable` with it, the\n * drawer offers the \"add a field\" choices from it, and the write route\n * refuses anything it says no to — so the three cannot disagree about which\n * fields a JPEG can take.\n */\nexport function embeddedKeyWritable(\n key: string,\n format: MediaEmbeddedFormat,\n): boolean {\n if (!MEDIA_EMBEDDED_WRITABLE_FORMATS.has(format)) return false\n if (isCanonicalEmbeddedKey(key)) {\n const entry: MediaEmbeddedCatalogEntry = MEDIA_EMBEDDED_CATALOG[key]\n return Boolean(entry.writable?.includes(format))\n }\n const other = parseOtherEmbeddedKey(key)\n return Boolean(other && OTHER_WRITABLE[other.namespace]?.includes(format))\n}\n\n/**\n * The canonical keys a file of this format can take, whether or not it\n * holds them yet — what the drawer offers under \"Add a field\".\n */\nexport function embeddedWritableKeys(\n format: MediaEmbeddedFormat,\n): MediaEmbeddedCanonicalKey[] {\n return (Object.keys(MEDIA_EMBEDDED_CATALOG) as MediaEmbeddedCanonicalKey[])\n .filter((key) => embeddedKeyWritable(key, format))\n}\n\n/** A strict ISO 8601 date or date-time, the only date a writer accepts. */\nconst ISO_DATE =\n /^\\d{4}-\\d{2}-\\d{2}(?:T\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,3})?)?(?:Z|[+-]\\d{2}:\\d{2})?)?$/\n\n/**\n * Validate an edit before it reaches a writer, or say why it cannot.\n *\n * Runs in the drawer (to explain) and in the write route (to refuse), so\n * the messages are written for the person who typed the value.\n */\nexport function sanitizeEmbeddedPatch(\n format: MediaEmbeddedFormat,\n patch: unknown,\n): { patch: MediaEmbeddedPatch } | { error: string } {\n if (!MEDIA_EMBEDDED_WRITABLE_FORMATS.has(format)) {\n return { error: 'This kind of file cannot take metadata edits.' }\n }\n if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {\n return { error: 'Nothing to change.' }\n }\n const out: MediaEmbeddedPatch = {}\n for (const [rawKey, raw] of Object.entries(patch as Record<string, unknown>)) {\n const key = rawKey.trim()\n if (!embeddedKeyWritable(key, format)) {\n return {\n error: `${embeddedFieldLabel(key, format)} cannot be written into this file.`,\n }\n }\n const label = embeddedFieldLabel(key, format)\n if (raw === null) {\n out[key] = null\n continue\n }\n const entry: MediaEmbeddedCatalogEntry | undefined = isCanonicalEmbeddedKey(\n key,\n )\n ? MEDIA_EMBEDDED_CATALOG[key]\n : undefined\n if (entry?.removeOnly) {\n return { error: `${label} can only be removed.` }\n }\n const kind = embeddedFieldKind(key)\n if (kind === 'list') {\n const items = (Array.isArray(raw) ? raw : String(raw).split(/[,;\\n]/))\n .map((item) => String(item).trim())\n .filter(Boolean)\n if (items.length > MEDIA_EMBEDDED_MAX_LIST_ITEMS) {\n return { error: `${label} holds at most ${MEDIA_EMBEDDED_MAX_LIST_ITEMS} entries.` }\n }\n if (items.some((item) => item.length > MEDIA_EMBEDDED_MAX_LIST_ITEM_LENGTH)) {\n return { error: `An entry in ${label} is too long.` }\n }\n out[key] = items.length ? [...new Set(items)] : null\n continue\n }\n if (typeof raw !== 'string') {\n return { error: `${label} must be text.` }\n }\n const value = raw.trim()\n if (!value) {\n out[key] = null\n continue\n }\n if (value.length > MEDIA_EMBEDDED_MAX_VALUE_LENGTH) {\n return { error: `${label} is too long.` }\n }\n if (kind === 'date' && !ISO_DATE.test(value)) {\n return { error: `${label} must be a date.` }\n }\n if (kind === 'rating' && !/^(?:-1|[0-5])$/.test(value)) {\n return { error: `${label} must be between 0 and 5.` }\n }\n out[key] = value\n }\n if (!Object.keys(out).length) return { error: 'Nothing to change.' }\n return { patch: out }\n}\n\n/** The value of a field as one line of text. */\nexport function embeddedFieldText(field: MediaEmbeddedField): string {\n return field.values ? field.values.join(', ') : (field.value ?? '')\n}\n\n/**\n * The stored content types a reader exists for — the drawer asks the\n * server to read a file only when this says there is something to find, so\n * a CSV never costs a round trip to learn it has no caption.\n */\nconst READABLE_TYPES = new Set([\n 'image/jpeg',\n 'image/png',\n 'image/webp',\n 'image/gif',\n 'image/tiff',\n 'image/heic',\n 'image/heif',\n 'image/avif',\n 'image/svg+xml',\n 'application/pdf',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n 'video/mp4',\n 'video/quicktime',\n 'video/webm',\n])\n\nexport function embeddedMetadataReadable(contentType: unknown): boolean {\n return READABLE_TYPES.has(\n String(contentType ?? '').toLowerCase().split(';')[0]?.trim() ?? '',\n )\n}\n\n/**\n * A field's value as the drawer shows it — the stored value is the\n * machine form every writer agrees on, and this is the only place it is\n * dressed for a person.\n */\nexport function embeddedFieldDisplay(\n field: Pick<MediaEmbeddedField, 'key' | 'value' | 'values'>,\n locale?: string,\n): string {\n if (field.values) return field.values.join(', ')\n const value = field.value ?? ''\n switch (embeddedFieldKind(field.key)) {\n case 'date': {\n const dateOnly = /^\\d{4}-\\d{2}-\\d{2}$/.test(value)\n // A date-only value parsed by `Date` is UTC midnight, which renders as\n // the previous day west of Greenwich; noon keeps it on its own date.\n const parsed = new Date(dateOnly ? `${value}T12:00:00` : value)\n if (Number.isNaN(parsed.getTime())) return value\n return parsed.toLocaleString(locale, {\n dateStyle: 'medium',\n ...(dateOnly ? {} : { timeStyle: 'short' }),\n })\n }\n case 'gps': {\n const [lat, lon] = value.split(',').map(Number)\n if (!Number.isFinite(lat) || !Number.isFinite(lon)) return value\n const part = (n: number, positive: string, negative: string) =>\n `${Math.abs(n).toFixed(5)}° ${n < 0 ? negative : positive}`\n return `${part(lat as number, 'N', 'S')}, ${part(lon as number, 'E', 'W')}`\n }\n case 'rating': {\n const stars = Number(value)\n if (stars === -1) return 'Rejected'\n if (!Number.isInteger(stars) || stars < 0 || stars > 5) return value\n return '★'.repeat(stars) + '☆'.repeat(5 - stars)\n }\n default:\n return value\n }\n}\n"],"names":["MEDIA_EMBEDDED_GROUP_ORDER","MEDIA_EMBEDDED_GROUP_LABELS","description","rights","location","capture","document","technical","other","MEDIA_EMBEDDED_WRITABLE_FORMATS","Set","IMAGE_WRITABLE","IMAGE_AND_PDF","IMAGE_PDF_OOXML","MEDIA_EMBEDDED_CATALOG","title","label","group","kind","writable","headline","labels","pdf","ooxml","subject","keywords","category","comment","instructions","rating","creator","copyright","credit","source","usageTerms","webStatement","city","state","country","gps","removeOnly","gpsAltitude","createdAt","svg","mp4","webm","groups","make","model","lens","exposure","aperture","iso","focalLength","flash","modifiedAt","software","producer","lastModifiedBy","company","pageCount","pdfVersion","orientation","colorProfile","encoder","otherEmbeddedKey","namespace","parts","join","parseOtherEmbeddedKey","key","split","length","includes","OTHER_WRITABLE","xmp","png","MEDIA_EMBEDDED_METADATA_VERSION","MEDIA_EMBEDDED_MAX_FIELDS","MEDIA_EMBEDDED_MAX_VALUE_LENGTH","MEDIA_EMBEDDED_MAX_LIST_ITEMS","MEDIA_EMBEDDED_MAX_LIST_ITEM_LENGTH","isCanonicalEmbeddedKey","Object","prototype","hasOwnProperty","call","embeddedFieldLabel","format","fallback","entry","at","embeddedFieldGroup","embeddedFieldKind","embeddedKeyWritable","has","Boolean","embeddedWritableKeys","keys","filter","ISO_DATE","sanitizeEmbeddedPatch","patch","error","Array","isArray","out","rawKey","raw","entries","trim","undefined","items","String","map","item","some","value","test","embeddedFieldText","field","values","READABLE_TYPES","embeddedMetadataReadable","contentType","toLowerCase","embeddedFieldDisplay","locale","dateOnly","parsed","Date","Number","isNaN","getTime","toLocaleString","dateStyle","timeStyle","lat","lon","isFinite","part","n","positive","negative","Math","abs","toFixed","stars","isInteger","repeat"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/media-embedded-fields.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 * The metadata a file carries INSIDE its own bytes (AGL-3331): EXIF, IPTC\n * and XMP on a photo, the document info of a PDF, the core and custom\n * properties of an Office file, the tags of a video container.\n *\n * ## Why this module is client-safe and the parsers are not\n *\n * The parsers and writers live in `media-embedded-metadata/` and are exposed\n * only through `@aglyn/aglyn/server` — they need `node:zlib` and\n * `node:crypto`, and nothing in a browser ever holds the bytes they read.\n * What the console needs is the SHAPE of what they produced (it is stored on\n * the media document) and the rules for presenting and editing it, which is\n * everything here. Keeping the two apart keeps a PDF parser out of the\n * console bundle.\n *\n * ## Canonical keys\n *\n * A photo's caption can be written in three places at once — XMP\n * `dc:description`, IPTC 2:120 and EXIF `ImageDescription` — and a PDF's in\n * two. The reader folds every spelling of one idea into one canonical key\n * ({@link MEDIA_EMBEDDED_CATALOG}), so the drawer shows ONE \"Description\"\n * rather than three rows that may or may not agree, and the writer fans an\n * edit back out to every place that already holds it (the Metadata Working\n * Group's rule: never leave a stale copy for the next reader to prefer).\n *\n * Anything a file carries that is not in the catalog — an XMP property from\n * a DAM we have never heard of, a PDF custom property, a PNG text chunk —\n * still surfaces, under a namespaced \"other\" key ({@link otherEmbeddedKey})\n * that round-trips to the writer unchanged.\n */\n\n/** Where a value was read from. Several sources can back one field. */\nexport type MediaEmbeddedSource =\n | 'xmp'\n | 'iptc'\n | 'exif'\n | 'icc'\n | 'png'\n | 'comment'\n | 'pdf'\n | 'ooxml'\n | 'mp4'\n | 'webm'\n | 'svg'\n\n/** How the drawer groups fields, in display order. */\nexport type MediaEmbeddedGroup =\n | 'description'\n | 'rights'\n | 'location'\n | 'capture'\n | 'document'\n | 'technical'\n | 'other'\n\nexport const MEDIA_EMBEDDED_GROUP_ORDER: readonly MediaEmbeddedGroup[] = [\n 'description',\n 'rights',\n 'location',\n 'capture',\n 'document',\n 'technical',\n 'other',\n]\n\nexport const MEDIA_EMBEDDED_GROUP_LABELS: Record<MediaEmbeddedGroup, string> = {\n description: 'Description',\n rights: 'Rights',\n location: 'Location',\n capture: 'Camera',\n document: 'Document',\n technical: 'Technical',\n other: 'Other embedded fields',\n}\n\n/**\n * The container a file was read as. `null`-free: a file whose format has no\n * reader is simply not given an `embeddedMetadata` record at all.\n */\nexport type MediaEmbeddedFormat =\n | 'jpeg'\n | 'png'\n | 'webp'\n | 'gif'\n | 'tiff'\n | 'heif'\n | 'svg'\n | 'pdf'\n | 'ooxml'\n | 'mp4'\n | 'webm'\n\n/**\n * The formats whose bytes can take an edit without being re-encoded.\n *\n * Every writer here rewrites METADATA containers only — JPEG APP segments,\n * PNG ancillary chunks, WebP RIFF chunks, a TIFF's IFD, a PDF incremental\n * update, an Office package's property parts — and copies the image data,\n * page content and every other part through byte for byte. A format that\n * cannot promise that (a video's `moov` would move its sample offsets; an\n * SVG is sanitized on every ingress and is its own markup) is read-only.\n */\nexport const MEDIA_EMBEDDED_WRITABLE_FORMATS: ReadonlySet<MediaEmbeddedFormat> =\n new Set(['jpeg', 'png', 'webp', 'tiff', 'pdf', 'ooxml'])\n\n/** How a value is typed, which decides the editor and the validation. */\nexport type MediaEmbeddedValueKind =\n | 'text'\n | 'longText'\n | 'list'\n | 'date'\n | 'rating'\n | 'gps'\n\nexport interface MediaEmbeddedCatalogEntry {\n label: string\n group: MediaEmbeddedGroup\n kind: MediaEmbeddedValueKind\n /**\n * The formats an edit of this key can be written into. Absent means the\n * key is read-only everywhere — a measured fact such as an exposure time\n * or a page count, which describes the bytes rather than annotating them.\n */\n writable?: readonly MediaEmbeddedFormat[]\n /**\n * `gps` only: the one edit offered is removal. Moving a photo's recorded\n * location is not a thing anybody needs from a DAM; taking it OFF a photo\n * before it goes on a public page is.\n */\n removeOnly?: boolean\n /** Format-specific label, where a format names the idea differently. */\n labels?: Partial<Record<MediaEmbeddedFormat, string>>\n /** Format-specific group — a PDF's creation date is not a camera fact. */\n groups?: Partial<Record<MediaEmbeddedFormat, MediaEmbeddedGroup>>\n}\n\nconst IMAGE_WRITABLE: readonly MediaEmbeddedFormat[] = [\n 'jpeg',\n 'png',\n 'webp',\n 'tiff',\n]\nconst IMAGE_AND_PDF: readonly MediaEmbeddedFormat[] = [...IMAGE_WRITABLE, 'pdf']\nconst IMAGE_PDF_OOXML: readonly MediaEmbeddedFormat[] = [\n ...IMAGE_AND_PDF,\n 'ooxml',\n]\n\n/**\n * Every canonical key, in display order within its group.\n *\n * ## Value formats every reader and writer agrees on\n *\n * - `date`: ISO 8601 — `2021-05-03T10:11:12`, with `Z` or `±HH:MM` only when\n * the source recorded a zone (EXIF 2.3's `OffsetTimeOriginal`, a PDF\n * date's `+05'00'`). Never invented: a local camera time stays local.\n * - `list`: an array of strings (creators, keywords).\n * - `gps`: `\"<lat>,<lon>\"` in signed decimal degrees, six places.\n * - `rating`: `\"0\"`–`\"5\"`, XMP's scale; `\"-1\"` is XMP's \"rejected\".\n * - technical values are display strings already (`1/250`, `f/2.8`,\n * `35 mm`) because nothing edits them.\n */\nexport const MEDIA_EMBEDDED_CATALOG = {\n title: {\n label: 'Title',\n group: 'description',\n kind: 'text',\n writable: IMAGE_PDF_OOXML,\n },\n headline: {\n label: 'Headline',\n group: 'description',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n description: {\n label: 'Description',\n group: 'description',\n kind: 'longText',\n writable: IMAGE_PDF_OOXML,\n labels: { pdf: 'Subject', ooxml: 'Comments' },\n },\n subject: {\n label: 'Subject',\n group: 'description',\n kind: 'text',\n writable: ['ooxml'],\n },\n keywords: {\n label: 'Keywords',\n group: 'description',\n kind: 'list',\n writable: IMAGE_PDF_OOXML,\n },\n category: {\n label: 'Category',\n group: 'description',\n kind: 'text',\n writable: ['ooxml'],\n },\n comment: {\n label: 'Comment',\n group: 'description',\n kind: 'longText',\n writable: ['png'],\n },\n instructions: {\n label: 'Instructions',\n group: 'description',\n kind: 'longText',\n writable: IMAGE_WRITABLE,\n },\n label: {\n label: 'Label',\n group: 'description',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n rating: {\n label: 'Rating',\n group: 'description',\n kind: 'rating',\n writable: IMAGE_WRITABLE,\n },\n creator: {\n label: 'Creator',\n group: 'rights',\n kind: 'list',\n writable: IMAGE_PDF_OOXML,\n labels: { pdf: 'Author', ooxml: 'Author' },\n },\n copyright: {\n label: 'Copyright',\n group: 'rights',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n credit: {\n label: 'Credit line',\n group: 'rights',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n source: {\n label: 'Source',\n group: 'rights',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n usageTerms: {\n label: 'Usage terms',\n group: 'rights',\n kind: 'longText',\n writable: IMAGE_WRITABLE,\n },\n webStatement: {\n label: 'Copyright info URL',\n group: 'rights',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n city: {\n label: 'City',\n group: 'location',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n state: {\n label: 'State / province',\n group: 'location',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n country: {\n label: 'Country',\n group: 'location',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n gps: {\n label: 'GPS position',\n group: 'location',\n kind: 'gps',\n writable: IMAGE_WRITABLE,\n removeOnly: true,\n },\n gpsAltitude: { label: 'Altitude', group: 'location', kind: 'text' },\n createdAt: {\n label: 'Date taken',\n group: 'capture',\n kind: 'date',\n writable: IMAGE_AND_PDF,\n labels: {\n pdf: 'Created',\n ooxml: 'Created',\n svg: 'Created',\n mp4: 'Recorded',\n webm: 'Recorded',\n },\n groups: { pdf: 'document', ooxml: 'document', svg: 'document' },\n },\n make: {\n label: 'Camera make',\n group: 'capture',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n model: {\n label: 'Camera model',\n group: 'capture',\n kind: 'text',\n writable: IMAGE_WRITABLE,\n },\n lens: { label: 'Lens', group: 'capture', kind: 'text', writable: IMAGE_WRITABLE },\n exposure: { label: 'Exposure', group: 'capture', kind: 'text' },\n aperture: { label: 'Aperture', group: 'capture', kind: 'text' },\n iso: { label: 'ISO', group: 'capture', kind: 'text' },\n focalLength: { label: 'Focal length', group: 'capture', kind: 'text' },\n flash: { label: 'Flash', group: 'capture', kind: 'text' },\n modifiedAt: {\n label: 'Modified',\n group: 'document',\n kind: 'date',\n },\n software: {\n label: 'Software',\n group: 'document',\n kind: 'text',\n writable: IMAGE_AND_PDF,\n labels: { pdf: 'Created with', ooxml: 'Application' },\n },\n producer: {\n label: 'PDF producer',\n group: 'document',\n kind: 'text',\n writable: ['pdf'],\n },\n lastModifiedBy: { label: 'Last modified by', group: 'document', kind: 'text' },\n company: { label: 'Company', group: 'document', kind: 'text' },\n pageCount: {\n label: 'Pages',\n group: 'document',\n kind: 'text',\n labels: { ooxml: 'Pages / slides' },\n },\n pdfVersion: { label: 'PDF version', group: 'document', kind: 'text' },\n orientation: { label: 'Orientation', group: 'technical', kind: 'text' },\n colorProfile: { label: 'Color profile', group: 'technical', kind: 'text' },\n encoder: { label: 'Encoder', group: 'technical', kind: 'text' },\n} as const satisfies Record<string, MediaEmbeddedCatalogEntry>\n\nexport type MediaEmbeddedCanonicalKey = keyof typeof MEDIA_EMBEDDED_CATALOG\n\n/**\n * The namespaces an \"other\" key can come from. The key round-trips from\n * reader to writer verbatim, so its only job is to be unambiguous:\n *\n * - `xmp|<namespace URI>|<local name>` — a simple XMP property outside the\n * catalog. Keyed by URI, not prefix, because two files may bind the same\n * namespace to different prefixes and must still produce one key.\n * - `png|<keyword>` — a PNG `tEXt`/`zTXt`/`iTXt` chunk.\n * - `pdf|<Name>` — a PDF document-info entry (Acrobat's \"Custom\" tab).\n * - `ooxml|<name>` — an Office custom document property.\n * - `mp4|<atom or freeform name>`, `webm|<TAG>` — video tags, read-only.\n */\nexport type MediaEmbeddedOtherNamespace =\n | 'xmp'\n | 'png'\n | 'pdf'\n | 'ooxml'\n | 'mp4'\n | 'webm'\n\nexport function otherEmbeddedKey(\n namespace: MediaEmbeddedOtherNamespace,\n ...parts: string[]\n): string {\n return [namespace, ...parts].join('|')\n}\n\nexport function parseOtherEmbeddedKey(\n key: string,\n): { namespace: MediaEmbeddedOtherNamespace; parts: string[] } | null {\n const [namespace = '', ...parts] = key.split('|')\n if (!parts.length) return null\n if (!['xmp', 'png', 'pdf', 'ooxml', 'mp4', 'webm'].includes(namespace))\n return null\n return { namespace: namespace as MediaEmbeddedOtherNamespace, parts }\n}\n\n/** The \"other\" namespaces a writer accepts edits for, by format. */\nconst OTHER_WRITABLE: Partial<\n Record<MediaEmbeddedOtherNamespace, readonly MediaEmbeddedFormat[]>\n> = {\n xmp: IMAGE_WRITABLE,\n png: ['png'],\n pdf: ['pdf'],\n ooxml: ['ooxml'],\n}\n\n/** One field as stored on the media document and shown in the drawer. */\nexport interface MediaEmbeddedField {\n /** A {@link MediaEmbeddedCanonicalKey} or an {@link otherEmbeddedKey}. */\n key: string\n label: string\n group: MediaEmbeddedGroup\n /** Single-valued fields. Absent when `values` is set. */\n value?: string\n /** List-valued fields (creators, keywords). */\n values?: string[]\n /** Every block the value was found in, strongest first. */\n sources: MediaEmbeddedSource[]\n /** Whether the drawer may offer an edit for it on this file. */\n editable: boolean\n}\n\n/**\n * `embeddedMetadata` on a media document (AGL-3331). Server-written only —\n * the Firestore rules lock it like every other fact read from the bytes.\n */\nexport interface MediaEmbeddedMetadata {\n /** Bumped when the reader's output changes shape, so a sweep can re-read. */\n version: number\n format: MediaEmbeddedFormat\n fields: MediaEmbeddedField[]\n /** Whether an edit can be written back into this file at all. */\n writable: boolean\n /** Why not, in a sentence for the drawer, when `writable` is false. */\n readOnlyReason?: string\n /** True when fields were dropped to stay inside the stored caps. */\n truncated?: boolean\n /**\n * The `contentSha256` of the bytes this was read from. A record whose\n * digest no longer matches the document's is stale — the file was\n * replaced by a path that did not re-read it — and is re-read on open.\n */\n contentSha256?: string\n}\n\n/** The reader's output version. See {@link MediaEmbeddedMetadata.version}. */\nexport const MEDIA_EMBEDDED_METADATA_VERSION = 1\n\n/** Stored caps: one media document must never approach Firestore's 1 MiB. */\nexport const MEDIA_EMBEDDED_MAX_FIELDS = 150\nexport const MEDIA_EMBEDDED_MAX_VALUE_LENGTH = 2000\nexport const MEDIA_EMBEDDED_MAX_LIST_ITEMS = 100\n/** A single keyword or creator; IPTC's own limit for keywords is 64 bytes. */\nexport const MEDIA_EMBEDDED_MAX_LIST_ITEM_LENGTH = 256\n\n/**\n * An edit to a file's embedded metadata: key → new value, or `null` to\n * remove it from the file. A list-valued key takes an array.\n */\nexport type MediaEmbeddedPatch = Record<string, string | string[] | null>\n\nexport function isCanonicalEmbeddedKey(\n key: string,\n): key is MediaEmbeddedCanonicalKey {\n return Object.prototype.hasOwnProperty.call(MEDIA_EMBEDDED_CATALOG, key)\n}\n\n/** The label a key carries on a given format. */\nexport function embeddedFieldLabel(\n key: string,\n format: MediaEmbeddedFormat,\n fallback?: string,\n): string {\n if (isCanonicalEmbeddedKey(key)) {\n const entry: MediaEmbeddedCatalogEntry = MEDIA_EMBEDDED_CATALOG[key]\n return entry.labels?.[format] ?? entry.label\n }\n return fallback ?? parseOtherEmbeddedKey(key)?.parts.at(-1) ?? key\n}\n\n/** The group a key is shown under on a given format. */\nexport function embeddedFieldGroup(\n key: string,\n format: MediaEmbeddedFormat,\n): MediaEmbeddedGroup {\n if (!isCanonicalEmbeddedKey(key)) return 'other'\n const entry: MediaEmbeddedCatalogEntry = MEDIA_EMBEDDED_CATALOG[key]\n return entry.groups?.[format] ?? entry.group\n}\n\n/** How a key's value is edited. Other keys are free text. */\nexport function embeddedFieldKind(key: string): MediaEmbeddedValueKind {\n return isCanonicalEmbeddedKey(key)\n ? (MEDIA_EMBEDDED_CATALOG[key] as MediaEmbeddedCatalogEntry).kind\n : 'text'\n}\n\n/**\n * Whether an edit of `key` can be written into a file of `format`.\n *\n * The one place the answer lives: the reader stamps `editable` with it, the\n * drawer offers the \"add a field\" choices from it, and the write route\n * refuses anything it says no to — so the three cannot disagree about which\n * fields a JPEG can take.\n */\nexport function embeddedKeyWritable(\n key: string,\n format: MediaEmbeddedFormat,\n): boolean {\n if (!MEDIA_EMBEDDED_WRITABLE_FORMATS.has(format)) return false\n if (isCanonicalEmbeddedKey(key)) {\n const entry: MediaEmbeddedCatalogEntry = MEDIA_EMBEDDED_CATALOG[key]\n return Boolean(entry.writable?.includes(format))\n }\n const other = parseOtherEmbeddedKey(key)\n return Boolean(other && OTHER_WRITABLE[other.namespace]?.includes(format))\n}\n\n/**\n * The canonical keys a file of this format can take, whether or not it\n * holds them yet — what the drawer offers under \"Add a field\".\n */\nexport function embeddedWritableKeys(\n format: MediaEmbeddedFormat,\n): MediaEmbeddedCanonicalKey[] {\n return (Object.keys(MEDIA_EMBEDDED_CATALOG) as MediaEmbeddedCanonicalKey[])\n .filter((key) => embeddedKeyWritable(key, format))\n}\n\n/** A strict ISO 8601 date or date-time, the only date a writer accepts. */\nconst ISO_DATE =\n /^\\d{4}-\\d{2}-\\d{2}(?:T\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,3})?)?(?:Z|[+-]\\d{2}:\\d{2})?)?$/\n\n/**\n * Validate an edit before it reaches a writer, or say why it cannot.\n *\n * Runs in the drawer (to explain) and in the write route (to refuse), so\n * the messages are written for the person who typed the value.\n */\nexport function sanitizeEmbeddedPatch(\n format: MediaEmbeddedFormat,\n patch: unknown,\n): { patch: MediaEmbeddedPatch } | { error: string } {\n if (!MEDIA_EMBEDDED_WRITABLE_FORMATS.has(format)) {\n return { error: 'This kind of file cannot take metadata edits.' }\n }\n if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {\n return { error: 'Nothing to change.' }\n }\n const out: MediaEmbeddedPatch = {}\n for (const [rawKey, raw] of Object.entries(patch as Record<string, unknown>)) {\n const key = rawKey.trim()\n if (!embeddedKeyWritable(key, format)) {\n return {\n error: `${embeddedFieldLabel(key, format)} cannot be written into this file.`,\n }\n }\n const label = embeddedFieldLabel(key, format)\n if (raw === null) {\n out[key] = null\n continue\n }\n const entry: MediaEmbeddedCatalogEntry | undefined = isCanonicalEmbeddedKey(\n key,\n )\n ? MEDIA_EMBEDDED_CATALOG[key]\n : undefined\n if (entry?.removeOnly) {\n return { error: `${label} can only be removed.` }\n }\n const kind = embeddedFieldKind(key)\n if (kind === 'list') {\n const items = (Array.isArray(raw) ? raw : String(raw).split(/[,;\\n]/))\n .map((item) => String(item).trim())\n .filter(Boolean)\n if (items.length > MEDIA_EMBEDDED_MAX_LIST_ITEMS) {\n return { error: `${label} holds at most ${MEDIA_EMBEDDED_MAX_LIST_ITEMS} entries.` }\n }\n if (items.some((item) => item.length > MEDIA_EMBEDDED_MAX_LIST_ITEM_LENGTH)) {\n return { error: `An entry in ${label} is too long.` }\n }\n out[key] = items.length ? [...new Set(items)] : null\n continue\n }\n if (typeof raw !== 'string') {\n return { error: `${label} must be text.` }\n }\n const value = raw.trim()\n if (!value) {\n out[key] = null\n continue\n }\n if (value.length > MEDIA_EMBEDDED_MAX_VALUE_LENGTH) {\n return { error: `${label} is too long.` }\n }\n if (kind === 'date' && !ISO_DATE.test(value)) {\n return { error: `${label} must be a date.` }\n }\n if (kind === 'rating' && !/^(?:-1|[0-5])$/.test(value)) {\n return { error: `${label} must be between 0 and 5.` }\n }\n out[key] = value\n }\n if (!Object.keys(out).length) return { error: 'Nothing to change.' }\n return { patch: out }\n}\n\n/** The value of a field as one line of text. */\nexport function embeddedFieldText(field: MediaEmbeddedField): string {\n return field.values ? field.values.join(', ') : (field.value ?? '')\n}\n\n/**\n * The stored content types a reader exists for — the drawer asks the\n * server to read a file only when this says there is something to find, so\n * a CSV never costs a round trip to learn it has no caption.\n */\nconst READABLE_TYPES = new Set([\n 'image/jpeg',\n 'image/png',\n 'image/webp',\n 'image/gif',\n 'image/tiff',\n 'image/heic',\n 'image/heif',\n 'image/avif',\n 'image/svg+xml',\n 'application/pdf',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n 'video/mp4',\n 'video/quicktime',\n 'video/webm',\n])\n\nexport function embeddedMetadataReadable(contentType: unknown): boolean {\n return READABLE_TYPES.has(\n String(contentType ?? '').toLowerCase().split(';')[0]?.trim() ?? '',\n )\n}\n\n/**\n * A field's value as the drawer shows it — the stored value is the\n * machine form every writer agrees on, and this is the only place it is\n * dressed for a person.\n */\nexport function embeddedFieldDisplay(\n field: Pick<MediaEmbeddedField, 'key' | 'value' | 'values'>,\n locale?: string,\n): string {\n if (field.values) return field.values.join(', ')\n const value = field.value ?? ''\n switch (embeddedFieldKind(field.key)) {\n case 'date': {\n const dateOnly = /^\\d{4}-\\d{2}-\\d{2}$/.test(value)\n // A date-only value parsed by `Date` is UTC midnight, which renders as\n // the previous day west of Greenwich; noon keeps it on its own date.\n const parsed = new Date(dateOnly ? `${value}T12:00:00` : value)\n if (Number.isNaN(parsed.getTime())) return value\n return parsed.toLocaleString(locale, {\n dateStyle: 'medium',\n ...(dateOnly ? {} : { timeStyle: 'short' }),\n })\n }\n case 'gps': {\n const [lat, lon] = value.split(',').map(Number)\n if (!Number.isFinite(lat) || !Number.isFinite(lon)) return value\n const part = (n: number, positive: string, negative: string) =>\n `${Math.abs(n).toFixed(5)}° ${n < 0 ? negative : positive}`\n return `${part(lat as number, 'N', 'S')}, ${part(lon as number, 'E', 'W')}`\n }\n case 'rating': {\n const stars = Number(value)\n if (stars === -1) return 'Rejected'\n if (!Number.isInteger(stars) || stars < 0 || stars > 5) return value\n return '★'.repeat(stars) + '☆'.repeat(5 - stars)\n }\n default:\n return value\n }\n}\n\n/**\n * Whether a stored record still describes the asset's bytes (AGL-3331).\n * Stale when the reader has changed shape since, or when the bytes were\n * replaced by a path that did not re-read them — both are answered by\n * reading the file again, never by showing the old record.\n */\nexport function embeddedMetadataIsCurrent(\n stored: unknown,\n contentSha256: unknown,\n): stored is MediaEmbeddedMetadata {\n if (!stored || typeof stored !== 'object') return false\n const record = stored as Partial<MediaEmbeddedMetadata>\n if (record.version !== MEDIA_EMBEDDED_METADATA_VERSION) return false\n if (!Array.isArray(record.fields)) return false\n return typeof contentSha256 !== 'string' || record.contentSha256 === contentSha256\n}\n\n/**\n * What a person would type to find a file, most telling first (AGL-3339):\n * its title, its keywords, who made it and where. A search token budget is\n * spent in this order, so a long caption can never crowd out a title. GPS,\n * camera settings, dates and software are left out — nobody searches a\n * library by shutter speed.\n */\nconst SEARCHABLE_EMBEDDED_KEYS: readonly MediaEmbeddedCanonicalKey[] = [\n 'title',\n 'keywords',\n 'headline',\n 'subject',\n 'creator',\n 'credit',\n 'source',\n 'city',\n 'state',\n 'country',\n 'category',\n 'label',\n 'description',\n]\n\n/**\n * The searchable text of a stored record, for the library's search keys\n * (AGL-3339). Empty for anything that is not a current-shape record, so a\n * caller can pass a document field straight through.\n */\nexport function mediaEmbeddedSearchText(stored: unknown): string {\n const fields = (stored as Partial<MediaEmbeddedMetadata> | null)?.fields\n if (!Array.isArray(fields)) return ''\n const byKey = new Map(fields.map((field) => [field?.key, field]))\n return SEARCHABLE_EMBEDDED_KEYS.flatMap((key) => {\n const field = byKey.get(key)\n if (!field) return []\n return Array.isArray(field.values)\n ? field.values.map(String)\n : typeof field.value === 'string'\n ? [field.value]\n : []\n })\n .map((text) => text.trim())\n .filter(Boolean)\n .join(' ')\n}\n\n/** One field as the customer API publishes it (AGL-3339). */\nexport interface MediaEmbeddedPublicField {\n key: string\n label: string\n group: MediaEmbeddedGroup\n /** The value of a single-valued field; `null` for a list. */\n value: string | null\n /** The items of a list-valued field (keywords, creators); else `null`. */\n values: string[] | null\n}\n\n/** `embeddedMetadata` as the customer API publishes it (AGL-3339). */\nexport interface MediaEmbeddedPublicView {\n format: MediaEmbeddedFormat\n /** True when fields were dropped to stay inside the stored caps. */\n truncated: boolean\n fields: MediaEmbeddedPublicField[]\n}\n\n/**\n * The stored record as the customer API and the MCP tools return it\n * (AGL-3339), or `null` when there is no current record for these bytes —\n * never an old file's details under a new file's id.\n *\n * `sources` and `editable` stay behind: they answer the drawer's question\n * (can this be written back, and where from), and the API has no call that\n * writes a file's details.\n */\nexport function mediaEmbeddedPublicView(\n stored: unknown,\n contentSha256: unknown,\n): MediaEmbeddedPublicView | null {\n if (!embeddedMetadataIsCurrent(stored, contentSha256)) return null\n return {\n format: stored.format,\n truncated: stored.truncated === true,\n fields: stored.fields.map((field) => ({\n key: field.key,\n label: field.label,\n group: field.group,\n value: Array.isArray(field.values) ? null : (field.value ?? ''),\n values: Array.isArray(field.values) ? field.values.map(String) : null,\n })),\n }\n}\n"],"names":["MEDIA_EMBEDDED_GROUP_ORDER","MEDIA_EMBEDDED_GROUP_LABELS","description","rights","location","capture","document","technical","other","MEDIA_EMBEDDED_WRITABLE_FORMATS","Set","IMAGE_WRITABLE","IMAGE_AND_PDF","IMAGE_PDF_OOXML","MEDIA_EMBEDDED_CATALOG","title","label","group","kind","writable","headline","labels","pdf","ooxml","subject","keywords","category","comment","instructions","rating","creator","copyright","credit","source","usageTerms","webStatement","city","state","country","gps","removeOnly","gpsAltitude","createdAt","svg","mp4","webm","groups","make","model","lens","exposure","aperture","iso","focalLength","flash","modifiedAt","software","producer","lastModifiedBy","company","pageCount","pdfVersion","orientation","colorProfile","encoder","otherEmbeddedKey","namespace","parts","join","parseOtherEmbeddedKey","key","split","length","includes","OTHER_WRITABLE","xmp","png","MEDIA_EMBEDDED_METADATA_VERSION","MEDIA_EMBEDDED_MAX_FIELDS","MEDIA_EMBEDDED_MAX_VALUE_LENGTH","MEDIA_EMBEDDED_MAX_LIST_ITEMS","MEDIA_EMBEDDED_MAX_LIST_ITEM_LENGTH","isCanonicalEmbeddedKey","Object","prototype","hasOwnProperty","call","embeddedFieldLabel","format","fallback","entry","at","embeddedFieldGroup","embeddedFieldKind","embeddedKeyWritable","has","Boolean","embeddedWritableKeys","keys","filter","ISO_DATE","sanitizeEmbeddedPatch","patch","error","Array","isArray","out","rawKey","raw","entries","trim","undefined","items","String","map","item","some","value","test","embeddedFieldText","field","values","READABLE_TYPES","embeddedMetadataReadable","contentType","toLowerCase","embeddedFieldDisplay","locale","dateOnly","parsed","Date","Number","isNaN","getTime","toLocaleString","dateStyle","timeStyle","lat","lon","isFinite","part","n","positive","negative","Math","abs","toFixed","stars","isInteger","repeat","embeddedMetadataIsCurrent","stored","contentSha256","record","version","fields","SEARCHABLE_EMBEDDED_KEYS","mediaEmbeddedSearchText","byKey","Map","flatMap","get","text","mediaEmbeddedPublicView","truncated"],"mappings":"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"}
|
|
@@ -15,6 +15,101 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
export * from './media-alt';
|
|
18
|
+
/**
|
|
19
|
+
* The family a stored file belongs to, as the media library's Type filter
|
|
20
|
+
* names it (AGL-3327). Stored on the document as `kind`, because a Firestore
|
|
21
|
+
* query can ask for a value and cannot ask for a prefix beside a list's own
|
|
22
|
+
* sort: `kind == 'image'` sits on any query the library builds, where a
|
|
23
|
+
* `contentType` range would take the one range field the query has.
|
|
24
|
+
*
|
|
25
|
+
* Every type media ingress accepts that is not a picture, a film or a PDF is
|
|
26
|
+
* a document — ZIP, Word, Excel, PowerPoint, CSV, text, Markdown, JSON — and
|
|
27
|
+
* so is anything older than that allowlist.
|
|
28
|
+
*/
|
|
29
|
+
export type MediaKind = 'image' | 'video' | 'pdf' | 'document';
|
|
30
|
+
export declare const MEDIA_KINDS: readonly MediaKind[];
|
|
31
|
+
export declare function mediaKindOf(contentType: unknown): MediaKind;
|
|
32
|
+
/** Which way a picture or a film is longer, as the Orientation filter asks. */
|
|
33
|
+
export type MediaOrientation = 'landscape' | 'portrait' | 'square';
|
|
34
|
+
export declare const MEDIA_ORIENTATIONS: readonly MediaOrientation[];
|
|
35
|
+
/**
|
|
36
|
+
* The orientation of a stored file (AGL-3327), from the pixel size it
|
|
37
|
+
* carries: a picture's `width` and `height`, or a film's `video.width` and
|
|
38
|
+
* `video.height` (a video's own `width`/`height` are never written — see
|
|
39
|
+
* `videoMediaProps`). Null when neither pair is whole, which a query for any
|
|
40
|
+
* orientation then leaves out rather than guesses at.
|
|
41
|
+
*/
|
|
42
|
+
export declare function mediaOrientationOf(media: {
|
|
43
|
+
width?: unknown;
|
|
44
|
+
height?: unknown;
|
|
45
|
+
video?: unknown;
|
|
46
|
+
}): MediaOrientation | null;
|
|
47
|
+
/**
|
|
48
|
+
* A file name as words (AGL-3327): every run of characters that is not a
|
|
49
|
+
* letter or a digit becomes a space, so `hero-banner_2x.png` is the four
|
|
50
|
+
* words a person would search it by. The shared name search splits on
|
|
51
|
+
* spaces only, which suits a person's name and misses nearly every file's.
|
|
52
|
+
*/
|
|
53
|
+
export declare function mediaNameWords(fileName: unknown): string;
|
|
54
|
+
/**
|
|
55
|
+
* The one token a library search asks for (AGL-3327): the first word of what
|
|
56
|
+
* was typed, folded the way `mediaNameWords` folds a stored name, lower-cased
|
|
57
|
+
* and capped as `nameSearchToken` caps it. A query holds one array filter, so
|
|
58
|
+
* a search is one word.
|
|
59
|
+
*/
|
|
60
|
+
export declare function mediaSearchToken(query: unknown): string;
|
|
61
|
+
/** The derived fields every writer of a media document stores beside it. */
|
|
62
|
+
export interface MediaFilterKeys {
|
|
63
|
+
kind: MediaKind;
|
|
64
|
+
/**
|
|
65
|
+
* `nameSearchKey` of the file name: the Name sort, and the "starts with"
|
|
66
|
+
* range a search is served by where the scope clause holds the query's one
|
|
67
|
+
* array filter.
|
|
68
|
+
*/
|
|
69
|
+
nameLower: string;
|
|
70
|
+
/**
|
|
71
|
+
* Word-prefix tokens of the file name's words, then of the details read
|
|
72
|
+
* from inside the file (AGL-3339), for search — at most
|
|
73
|
+
* `NAME_TOKEN_LIMIT`, the name's first.
|
|
74
|
+
*/
|
|
75
|
+
nameTokens: string[];
|
|
76
|
+
/** Whether alt text is written — "missing" is a value a query can find. */
|
|
77
|
+
hasAlt: boolean;
|
|
78
|
+
/** Which way the file is longer, or null when its size is not known. */
|
|
79
|
+
orientation: MediaOrientation | null;
|
|
80
|
+
}
|
|
81
|
+
/** What `mediaFilterKeys` reads off a media document. */
|
|
82
|
+
export interface MediaFilterSource {
|
|
83
|
+
fileName?: unknown;
|
|
84
|
+
contentType?: unknown;
|
|
85
|
+
alt?: unknown;
|
|
86
|
+
width?: unknown;
|
|
87
|
+
height?: unknown;
|
|
88
|
+
video?: unknown;
|
|
89
|
+
/** The details read from inside the file (AGL-3339), searched after its name. */
|
|
90
|
+
embeddedMetadata?: unknown;
|
|
91
|
+
/** The bytes' digest, which says whether `embeddedMetadata` describes them. */
|
|
92
|
+
contentSha256?: unknown;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* What a library search finds a file by (AGL-3339): every word prefix of its
|
|
96
|
+
* name, then of the details read from inside it — a title, keywords, who
|
|
97
|
+
* made it, where — inside the one `NAME_TOKEN_LIMIT` a document keeps, so a
|
|
98
|
+
* long caption never crowds out the name. Details that describe other bytes
|
|
99
|
+
* than the file's (`embeddedMetadataIsCurrent`) add nothing.
|
|
100
|
+
*/
|
|
101
|
+
export declare function mediaNameTokens(fileName: unknown, embeddedMetadata?: unknown, contentSha256?: unknown): string[];
|
|
102
|
+
/**
|
|
103
|
+
* What the media library filters, sorts and searches by, derived from the
|
|
104
|
+
* fields a document already carries (AGL-3327). Every writer — the upload
|
|
105
|
+
* routes, replace, the REST API, restore, the Details drawer — spreads this
|
|
106
|
+
* beside the fields it writes, derived from the document as it will stand
|
|
107
|
+
* after the write, so a query finds every file by it. The backfill
|
|
108
|
+
* (`tools/scripts/backfill-media-filter-keys.mjs`) stamps the documents
|
|
109
|
+
* written before it existed, through `tools/scripts/lib/media-filter-keys.mjs`;
|
|
110
|
+
* both are held to `media-filter-keys.fixtures.json` beside it.
|
|
111
|
+
*/
|
|
112
|
+
export declare function mediaFilterKeys(media: MediaFilterSource): MediaFilterKeys;
|
|
18
113
|
export declare const MEDIA_TAG_MAX_COUNT = 20;
|
|
19
114
|
export declare const MEDIA_TAG_MAX_LENGTH = 40;
|
|
20
115
|
/**
|
|
@@ -19,7 +19,101 @@
|
|
|
19
19
|
* image header parser so the upload route can stamp dimensions without
|
|
20
20
|
* pulling in an image library.
|
|
21
21
|
*/ import { MEDIA_ALT_MAX_LENGTH } from "./media-alt.js";
|
|
22
|
+
import { embeddedMetadataIsCurrent, mediaEmbeddedSearchText } from "./media-embedded-fields.js";
|
|
23
|
+
import { NAME_TOKEN_LIMIT, nameSearchKey, nameSearchToken, nameSearchTokens } from "./name-search.js";
|
|
22
24
|
export * from "./media-alt.js";
|
|
25
|
+
export const MEDIA_KINDS = [
|
|
26
|
+
'image',
|
|
27
|
+
'video',
|
|
28
|
+
'pdf',
|
|
29
|
+
'document'
|
|
30
|
+
];
|
|
31
|
+
export function mediaKindOf(contentType) {
|
|
32
|
+
const type = String(contentType != null ? contentType : '').trim().toLowerCase();
|
|
33
|
+
if (type.startsWith('image/')) return 'image';
|
|
34
|
+
if (type.startsWith('video/')) return 'video';
|
|
35
|
+
if (type === 'application/pdf') return 'pdf';
|
|
36
|
+
return 'document';
|
|
37
|
+
}
|
|
38
|
+
export const MEDIA_ORIENTATIONS = [
|
|
39
|
+
'landscape',
|
|
40
|
+
'portrait',
|
|
41
|
+
'square'
|
|
42
|
+
];
|
|
43
|
+
const positive = (value)=>typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null;
|
|
44
|
+
/**
|
|
45
|
+
* The orientation of a stored file (AGL-3327), from the pixel size it
|
|
46
|
+
* carries: a picture's `width` and `height`, or a film's `video.width` and
|
|
47
|
+
* `video.height` (a video's own `width`/`height` are never written — see
|
|
48
|
+
* `videoMediaProps`). Null when neither pair is whole, which a query for any
|
|
49
|
+
* orientation then leaves out rather than guesses at.
|
|
50
|
+
*/ export function mediaOrientationOf(media) {
|
|
51
|
+
const video = media.video && typeof media.video === 'object' ? media.video : null;
|
|
52
|
+
let width = positive(media.width);
|
|
53
|
+
let height = positive(media.height);
|
|
54
|
+
if (width === null || height === null) {
|
|
55
|
+
width = positive(video == null ? void 0 : video.width);
|
|
56
|
+
height = positive(video == null ? void 0 : video.height);
|
|
57
|
+
}
|
|
58
|
+
if (width === null || height === null) return null;
|
|
59
|
+
if (width === height) return 'square';
|
|
60
|
+
return width > height ? 'landscape' : 'portrait';
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A file name as words (AGL-3327): every run of characters that is not a
|
|
64
|
+
* letter or a digit becomes a space, so `hero-banner_2x.png` is the four
|
|
65
|
+
* words a person would search it by. The shared name search splits on
|
|
66
|
+
* spaces only, which suits a person's name and misses nearly every file's.
|
|
67
|
+
*/ export function mediaNameWords(fileName) {
|
|
68
|
+
return String(fileName != null ? fileName : '').replace(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}]+", "gu"), ' ').trim();
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The one token a library search asks for (AGL-3327): the first word of what
|
|
72
|
+
* was typed, folded the way `mediaNameWords` folds a stored name, lower-cased
|
|
73
|
+
* and capped as `nameSearchToken` caps it. A query holds one array filter, so
|
|
74
|
+
* a search is one word.
|
|
75
|
+
*/ export function mediaSearchToken(query) {
|
|
76
|
+
return nameSearchToken(mediaNameWords(query));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* What a library search finds a file by (AGL-3339): every word prefix of its
|
|
80
|
+
* name, then of the details read from inside it — a title, keywords, who
|
|
81
|
+
* made it, where — inside the one `NAME_TOKEN_LIMIT` a document keeps, so a
|
|
82
|
+
* long caption never crowds out the name. Details that describe other bytes
|
|
83
|
+
* than the file's (`embeddedMetadataIsCurrent`) add nothing.
|
|
84
|
+
*/ export function mediaNameTokens(fileName, embeddedMetadata, contentSha256) {
|
|
85
|
+
const tokens = new Set(nameSearchTokens(mediaNameWords(fileName)));
|
|
86
|
+
if (!embeddedMetadataIsCurrent(embeddedMetadata, contentSha256)) return [
|
|
87
|
+
...tokens
|
|
88
|
+
];
|
|
89
|
+
for (const token of nameSearchTokens(mediaNameWords(mediaEmbeddedSearchText(embeddedMetadata)))){
|
|
90
|
+
if (tokens.size >= NAME_TOKEN_LIMIT) break;
|
|
91
|
+
tokens.add(token);
|
|
92
|
+
}
|
|
93
|
+
return [
|
|
94
|
+
...tokens
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* What the media library filters, sorts and searches by, derived from the
|
|
99
|
+
* fields a document already carries (AGL-3327). Every writer — the upload
|
|
100
|
+
* routes, replace, the REST API, restore, the Details drawer — spreads this
|
|
101
|
+
* beside the fields it writes, derived from the document as it will stand
|
|
102
|
+
* after the write, so a query finds every file by it. The backfill
|
|
103
|
+
* (`tools/scripts/backfill-media-filter-keys.mjs`) stamps the documents
|
|
104
|
+
* written before it existed, through `tools/scripts/lib/media-filter-keys.mjs`;
|
|
105
|
+
* both are held to `media-filter-keys.fixtures.json` beside it.
|
|
106
|
+
*/ export function mediaFilterKeys(media) {
|
|
107
|
+
var _media_fileName, _media_alt;
|
|
108
|
+
const fileName = String((_media_fileName = media.fileName) != null ? _media_fileName : '');
|
|
109
|
+
return {
|
|
110
|
+
kind: mediaKindOf(media.contentType),
|
|
111
|
+
nameLower: nameSearchKey(fileName),
|
|
112
|
+
nameTokens: mediaNameTokens(fileName, media.embeddedMetadata, media.contentSha256),
|
|
113
|
+
hasAlt: String((_media_alt = media.alt) != null ? _media_alt : '').trim().length > 0,
|
|
114
|
+
orientation: mediaOrientationOf(media)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
23
117
|
export const MEDIA_TAG_MAX_COUNT = 20;
|
|
24
118
|
export const MEDIA_TAG_MAX_LENGTH = 40;
|
|
25
119
|
/**
|