@aglyn/aglyn 1.0.0-beta.199 → 1.0.0-beta.200
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 +144 -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 +151 -1
- package/src/lib/app-utils/crm.js +388 -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-index.generated.js +40 -40
- 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/forms.d.ts +83 -3
- package/src/lib/app-utils/forms.js +67 -0
- package/src/lib/app-utils/forms.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-metadata.d.ts +79 -0
- package/src/lib/app-utils/media-metadata.js +74 -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/server.d.ts +2 -0
- package/src/lib/app-utils/server.js +2 -0
- package/src/lib/app-utils/server.js.map +1 -1
|
@@ -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[];
|
|
@@ -141,6 +141,35 @@
|
|
|
141
141
|
nameReversed: nameSearchReversed(name)
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* The search fields a document NAMED BY `displayName` carries (AGL-3321):
|
|
146
|
+
* the site artifacts (screens of every kind, layouts, reusable components,
|
|
147
|
+
* templates) and the marketplace listings.
|
|
148
|
+
*
|
|
149
|
+
* `nameSearchFields` is for documents whose name IS `name`. These documents
|
|
150
|
+
* name themselves `displayName`, and have since before search existed, so
|
|
151
|
+
* the keys ride beside it under the same three names every list declares —
|
|
152
|
+
* `lowerPath: 'nameLower'`, `tokensPath: 'nameTokens'`,
|
|
153
|
+
* `reversedPath: 'nameReversed'` — and one list query grammar reads them all.
|
|
154
|
+
*
|
|
155
|
+
* `displayName` is NOT returned. A create that carries no name must not gain
|
|
156
|
+
* an empty one: readers fall back with `displayName ?? id`, which an empty
|
|
157
|
+
* string defeats. But the KEYS are always returned, empty for a missing name,
|
|
158
|
+
* so a create stamps them unconditionally — an `orderBy('nameLower')` drops
|
|
159
|
+
* any document that lacks the field, and a document created without a name
|
|
160
|
+
* is still one a list must reach.
|
|
161
|
+
*
|
|
162
|
+
* ⚠️ Spread at every write that sets `displayName`, and at every create —
|
|
163
|
+
* and nowhere else: a partial update that stamped keys for a name it did not
|
|
164
|
+
* write would make the document unfindable by the name it still shows.
|
|
165
|
+
*/ export function displayNameSearchFields(displayName) {
|
|
166
|
+
const name = typeof displayName === 'string' ? displayName : '';
|
|
167
|
+
return {
|
|
168
|
+
nameLower: nameSearchKey(name),
|
|
169
|
+
nameTokens: nameSearchTokens(name),
|
|
170
|
+
nameReversed: nameSearchReversed(name)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
144
173
|
/**
|
|
145
174
|
* The normalizers a list query plan (`planListQuery` in
|
|
146
175
|
* `@aglyn/shared-ui-jsx/const/list-query-plan`) turns typed values into keys
|
|
@@ -152,5 +181,73 @@
|
|
|
152
181
|
reversed: (value)=>nameSearchReversed(value),
|
|
153
182
|
maxPrefix: NAME_TOKEN_MAX_PREFIX
|
|
154
183
|
};
|
|
184
|
+
/**
|
|
185
|
+
* What joins a scope token to a search prefix in a SCOPED search token
|
|
186
|
+
* (AGL-3321): `host:abc~acm`.
|
|
187
|
+
*
|
|
188
|
+
* A list under a scope clause (`visibleTo array-contains-any [...]`) has
|
|
189
|
+
* spent Firestore's one array clause on the scope, so its search cannot be a
|
|
190
|
+
* second `array-contains`. The list query plan folds the typed word INTO the scope
|
|
191
|
+
* clause instead (`ListQueryDeclaration.search.scoped`), asking for each
|
|
192
|
+
* scope token joined to the word; the writers stamp every scope token
|
|
193
|
+
* joined to every prefix, through {@link scopedSearchTokens}. A declaration
|
|
194
|
+
* names this constant as its `join`, so the writer and the reader cannot
|
|
195
|
+
* disagree about the character between the two halves.
|
|
196
|
+
*
|
|
197
|
+
* ⛔ Security rules cannot prove a folded query for a reader limited to some
|
|
198
|
+
* sites. Rules like `canReadScoped` grant a list from
|
|
199
|
+
* `visibleTo.hasAny(<the reader's scopes>)`, and a query that constrains only
|
|
200
|
+
* the scoped-token field says nothing about `visibleTo`, so Firestore denies
|
|
201
|
+
* it (pinned in cloud/rules-tests/firestore-rules.test.mjs). A folded search
|
|
202
|
+
* is therefore for readers the rules admit WITHOUT a `visibleTo` term (an
|
|
203
|
+
* org-wide member, whose rule short-circuits) or for an Admin-SDK route that
|
|
204
|
+
* checks the reader's scope itself. A partial-reach collaborator's search
|
|
205
|
+
* stays beside the `visibleTo` clause instead, as a prefix range on one
|
|
206
|
+
* ordered key (media: `nameLower`; inbox leads: `email`).
|
|
207
|
+
*
|
|
208
|
+
* `~` appears in no scope token (`org`, `host:{id}`), so the split is never
|
|
209
|
+
* ambiguous; a prefix that itself holds a `~` is matched whole on both sides
|
|
210
|
+
* and needs no escaping.
|
|
211
|
+
*/ export const SCOPED_SEARCH_JOIN = '~';
|
|
212
|
+
/**
|
|
213
|
+
* The `search.scoped` half of a list declaration, over the array the
|
|
214
|
+
* writers stamp with {@link scopedSearchTokens}.
|
|
215
|
+
*/ export function scopedSearch(tokensPath) {
|
|
216
|
+
return {
|
|
217
|
+
tokensPath,
|
|
218
|
+
join: SCOPED_SEARCH_JOIN
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Every scope a record is visible to, joined to every search prefix it
|
|
223
|
+
* carries: `[org~a, org~ac, host:x~a, …]`, in scope order then token order,
|
|
224
|
+
* with no duplicates (AGL-3321).
|
|
225
|
+
*
|
|
226
|
+
* `visibleTo` is read defensively — a record's stored array — and keeps
|
|
227
|
+
* every non-empty string in it; anything else answers no scope, and so no
|
|
228
|
+
* scoped tokens: a record visible to nobody is found by no scoped search.
|
|
229
|
+
* `tokens` is whatever the record's plain search array holds, so a list that
|
|
230
|
+
* searches several fields passes their merged prefixes rather than one
|
|
231
|
+
* name's. Each is stamped beside the plain array on every write that sets
|
|
232
|
+
* either the searched text or `visibleTo`.
|
|
233
|
+
*
|
|
234
|
+
* The count is the product of the two, so a record placed on thirty sites
|
|
235
|
+
* with a long name carries thousands of entries. That stays far inside
|
|
236
|
+
* Firestore's per-document index-entry ceiling, and it is the price of a
|
|
237
|
+
* search a scoped reader can run at all.
|
|
238
|
+
*/ export function scopedSearchTokens(visibleTo, tokens) {
|
|
239
|
+
if (!Array.isArray(visibleTo)) return [];
|
|
240
|
+
const scopes = [
|
|
241
|
+
...new Set(visibleTo.filter((entry)=>typeof entry === 'string' && entry.length > 0))
|
|
242
|
+
];
|
|
243
|
+
const words = [
|
|
244
|
+
...new Set(tokens.filter((token)=>typeof token === 'string' && token))
|
|
245
|
+
];
|
|
246
|
+
const scoped = [];
|
|
247
|
+
for (const scope of scopes){
|
|
248
|
+
for (const word of words)scoped.push(`${scope}${SCOPED_SEARCH_JOIN}${word}`);
|
|
249
|
+
}
|
|
250
|
+
return scoped;
|
|
251
|
+
}
|
|
155
252
|
|
|
156
253
|
//# sourceMappingURL=name-search.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/name-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\n/**\n * Normalized search key for a human display name (AGL-835).\n *\n * Firestore has no case-insensitive query, so name search is done by storing\n * a normalized `nameLower` alongside the display name and running a prefix\n * range query against it: `orderBy(nameLower) startAt(q) endAt(q + '')`.\n * For that to match, the stored key and the typed query MUST be normalized the\n * same way — so both the write paths (screen/host creates and renames) and the\n * switcher's query builder run the raw text through this one function.\n *\n * Normalization is deliberately minimal and reversible-in-spirit: lower-case,\n * trim, and collapse internal whitespace. Diacritics are intentionally NOT\n * stripped — a user who types \"café\" should match the stored \"café\", and one\n * who types \"cafe\" is doing a different search; folding accents here would make\n * the prefix range silently disagree with the key. Returns '' for nullish or\n * blank input, which callers treat as \"no searchable name / skip the field\".\n */\nexport function nameSearchKey(name: string | null | undefined): string {\n return (name ?? '').trim().replace(/\\s+/g, ' ').toLowerCase()\n}\n\n/**\n * The longest word-prefix a token may be.\n *\n * Every prefix of every word is stored, so the token count grows with name\n * LENGTH rather than word count. Twelve characters is past the point where a\n * search box narrows anything — a reader who has typed twelve characters has\n * already found it — and it keeps a long name from spending a hundred index\n * entries on the tail of one word.\n */\nexport const NAME_TOKEN_MAX_PREFIX = 12\n\n/** Ceiling on tokens per document, so a pathological name cannot bloat the index. */\nexport const NAME_TOKEN_LIMIT = 120\n\n/**\n * Word-prefix tokens for `array-contains` search (AGL-2501).\n *\n * `nameSearchKey` supports a PREFIX range, which is anchored at the start of\n * the whole name: \"acme\" finds \"Acme Coffee\" and \"coffee\" does not. That is\n * the wrong shape for a search box, where the word a reader remembers is\n * rarely the first one.\n *\n * Firestore cannot answer `contains` on a string, but it can answer\n * `array-contains` on a field the write path prepared. Storing every prefix\n * of every WORD turns \"does this name contain a word starting with X\" into a\n * single indexed equality:\n *\n * \"Acme Coffee\" → a, ac, acm, acme, c, co, cof, coff, coffe, coffee\n *\n * So \"cof\" finds it, and so does \"acme\". What it still cannot do is match\n * mid-word — \"offee\" is not a prefix of any word — which is the honest edge\n * of doing this without a search service.\n *\n * Normalized through `nameSearchKey` so the stored tokens and the typed query\n * agree on case, trimming and internal whitespace; diacritics are kept, for\n * the reason given there.\n */\nexport function nameSearchTokens(name: string | null | undefined): string[] {\n const key = nameSearchKey(name)\n if (!key) return []\n const tokens = new Set<string>()\n for (const word of key.split(' ')) {\n if (!word) continue\n const capped = word.slice(0, NAME_TOKEN_MAX_PREFIX)\n for (let end = 1; end <= capped.length; end += 1) {\n tokens.add(capped.slice(0, end))\n if (tokens.size >= NAME_TOKEN_LIMIT) return [...tokens]\n }\n }\n return [...tokens]\n}\n\n/**\n * The one token a typed query becomes.\n *\n * `array-contains` takes a single value, and Firestore allows only one such\n * clause per query — so a multi-word query cannot be an AND on the server.\n * The FIRST word is used, which is what a reader is narrowing by when they\n * type \"acme cof\": they see the Acme results and read the rest themselves.\n * Capped to the same prefix length the tokens were written at, or a longer\n * query would match a token that was never stored.\n */\nexport function nameSearchToken(query: string | null | undefined): string {\n const key = nameSearchKey(query)\n if (!key) return ''\n return (key.split(' ')[0] ?? '').slice(0, NAME_TOKEN_MAX_PREFIX)\n}\n\n/**\n * The normalized name, reversed, so \"ends with\" becomes a prefix range.\n *\n * Firestore has one string operator that is not equality: the range. That\n * gives \"starts with\" directly — `>= q` and `<= q + ''` over\n * `nameLower` — and gives \"ends with\" nothing at all, because a range is\n * anchored at the front of the stored value.\n *\n * Reversing the stored key moves the end to the front. \"Acme Coffee\" is\n * stored as \"eeffoc emca\", and a search for names ending \"coffee\" becomes a\n * prefix range for \"eeffoc\" — the same query, on the same kind of index.\n *\n * Reversed by CODEPOINT (`[...key]`), not by UTF-16 unit: `split('')` cuts\n * surrogate pairs in half, so an emoji or a non-BMP character in a workspace\n * name would reverse into two lone surrogates and never match anything.\n */\nexport function nameSearchReversed(name: string | null | undefined): string {\n return [...nameSearchKey(name)].reverse().join('')\n}\n\n/**\n * The three search fields that travel with a `name`, as one object.\n *\n * Every collection whose list is SEARCHED on the server carries them, and\n * carrying only some is the failure mode worth designing against: `orderBy`\n * drops a document missing the field it sorts by, and `array-contains` drops\n * one missing the array. Either way the record still LISTS normally, so the\n * gap shows up only as a search that quietly cannot find one row.\n *\n * ⚠️ Spread this at every write that sets `name`, and only where `name` is\n * actually being written — a partial write that stamped an empty key over a\n * real one would make the document unfindable by the name it still displays.\n */\nexport function nameSearchFields(name: string): {\n name: string\n nameLower: string\n nameTokens: string[]\n nameReversed: string\n} {\n // `name` is INCLUDED so this is a drop-in replacement for writing `{ name }`.\n // A helper that returned only the derived keys would be spread beside the\n // name at four call sites, and the one that forgot would write search keys\n // for a name it never stored.\n return {\n name,\n nameLower: nameSearchKey(name),\n nameTokens: nameSearchTokens(name),\n nameReversed: nameSearchReversed(name),\n }\n}\n\n/**\n * The normalizers a list query plan (`planListQuery` in\n * `@aglyn/shared-ui-jsx/const/list-query-plan`) turns typed values into keys\n * with: the same functions the writers stamp `nameLower`, `nameTokens` and\n * `nameReversed` with, so a query asks for what was stored (AGL-3321).\n */\nexport const nameSearchNormalizers = {\n key: (value: string) => nameSearchKey(value),\n token: (value: string) => nameSearchToken(value),\n reversed: (value: string) => nameSearchReversed(value),\n maxPrefix: NAME_TOKEN_MAX_PREFIX,\n} as const\n\n"],"names":["nameSearchKey","name","trim","replace","toLowerCase","NAME_TOKEN_MAX_PREFIX","NAME_TOKEN_LIMIT","nameSearchTokens","key","tokens","Set","word","split","capped","slice","end","length","add","size","nameSearchToken","query","nameSearchReversed","reverse","join","nameSearchFields","nameLower","nameTokens","nameReversed","nameSearchNormalizers","value","token","reversed","maxPrefix"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASA,cAAcC,IAA+B;IAC3D,OAAO,CAACA,eAAAA,OAAQ,IAAIC,IAAI,GAAGC,OAAO,CAAC,QAAQ,KAAKC,WAAW;AAC7D;AAEA;;;;;;;;CAQC,GACD,OAAO,MAAMC,wBAAwB,GAAE;AAEvC,mFAAmF,GACnF,OAAO,MAAMC,mBAAmB,IAAG;AAEnC;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,SAASC,iBAAiBN,IAA+B;IAC9D,MAAMO,MAAMR,cAAcC;IAC1B,IAAI,CAACO,KAAK,OAAO,EAAE;IACnB,MAAMC,SAAS,IAAIC;IACnB,KAAK,MAAMC,QAAQH,IAAII,KAAK,CAAC,KAAM;QACjC,IAAI,CAACD,MAAM;QACX,MAAME,SAASF,KAAKG,KAAK,CAAC,GAAGT;QAC7B,IAAK,IAAIU,MAAM,GAAGA,OAAOF,OAAOG,MAAM,EAAED,OAAO,EAAG;YAChDN,OAAOQ,GAAG,CAACJ,OAAOC,KAAK,CAAC,GAAGC;YAC3B,IAAIN,OAAOS,IAAI,IAAIZ,kBAAkB,OAAO;mBAAIG;aAAO;QACzD;IACF;IACA,OAAO;WAAIA;KAAO;AACpB;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASU,gBAAgBC,KAAgC;QAGtDZ;IAFR,MAAMA,MAAMR,cAAcoB;IAC1B,IAAI,CAACZ,KAAK,OAAO;IACjB,OAAO,EAACA,cAAAA,IAAII,KAAK,CAAC,IAAI,CAAC,EAAE,YAAjBJ,cAAqB,IAAIM,KAAK,CAAC,GAAGT;AAC5C;AAEA;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASgB,mBAAmBpB,IAA+B;IAChE,OAAO;WAAID,cAAcC;KAAM,CAACqB,OAAO,GAAGC,IAAI,CAAC;AACjD;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,iBAAiBvB,IAAY;IAM3C,8EAA8E;IAC9E,0EAA0E;IAC1E,2EAA2E;IAC3E,8BAA8B;IAC9B,OAAO;QACLA;QACAwB,WAAWzB,cAAcC;QACzByB,YAAYnB,iBAAiBN;QAC7B0B,cAAcN,mBAAmBpB;IACnC;AACF;AAEA;;;;;CAKC,GACD,OAAO,MAAM2B,wBAAwB;IACnCpB,KAAK,CAACqB,QAAkB7B,cAAc6B;IACtCC,OAAO,CAACD,QAAkBV,gBAAgBU;IAC1CE,UAAU,CAACF,QAAkBR,mBAAmBQ;IAChDG,WAAW3B;AACb,EAAU"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/name-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\n/**\n * Normalized search key for a human display name (AGL-835).\n *\n * Firestore has no case-insensitive query, so name search is done by storing\n * a normalized `nameLower` alongside the display name and running a prefix\n * range query against it: `orderBy(nameLower) startAt(q) endAt(q + '')`.\n * For that to match, the stored key and the typed query MUST be normalized the\n * same way — so both the write paths (screen/host creates and renames) and the\n * switcher's query builder run the raw text through this one function.\n *\n * Normalization is deliberately minimal and reversible-in-spirit: lower-case,\n * trim, and collapse internal whitespace. Diacritics are intentionally NOT\n * stripped — a user who types \"café\" should match the stored \"café\", and one\n * who types \"cafe\" is doing a different search; folding accents here would make\n * the prefix range silently disagree with the key. Returns '' for nullish or\n * blank input, which callers treat as \"no searchable name / skip the field\".\n */\nexport function nameSearchKey(name: string | null | undefined): string {\n return (name ?? '').trim().replace(/\\s+/g, ' ').toLowerCase()\n}\n\n/**\n * The longest word-prefix a token may be.\n *\n * Every prefix of every word is stored, so the token count grows with name\n * LENGTH rather than word count. Twelve characters is past the point where a\n * search box narrows anything — a reader who has typed twelve characters has\n * already found it — and it keeps a long name from spending a hundred index\n * entries on the tail of one word.\n */\nexport const NAME_TOKEN_MAX_PREFIX = 12\n\n/** Ceiling on tokens per document, so a pathological name cannot bloat the index. */\nexport const NAME_TOKEN_LIMIT = 120\n\n/**\n * Word-prefix tokens for `array-contains` search (AGL-2501).\n *\n * `nameSearchKey` supports a PREFIX range, which is anchored at the start of\n * the whole name: \"acme\" finds \"Acme Coffee\" and \"coffee\" does not. That is\n * the wrong shape for a search box, where the word a reader remembers is\n * rarely the first one.\n *\n * Firestore cannot answer `contains` on a string, but it can answer\n * `array-contains` on a field the write path prepared. Storing every prefix\n * of every WORD turns \"does this name contain a word starting with X\" into a\n * single indexed equality:\n *\n * \"Acme Coffee\" → a, ac, acm, acme, c, co, cof, coff, coffe, coffee\n *\n * So \"cof\" finds it, and so does \"acme\". What it still cannot do is match\n * mid-word — \"offee\" is not a prefix of any word — which is the honest edge\n * of doing this without a search service.\n *\n * Normalized through `nameSearchKey` so the stored tokens and the typed query\n * agree on case, trimming and internal whitespace; diacritics are kept, for\n * the reason given there.\n */\nexport function nameSearchTokens(name: string | null | undefined): string[] {\n const key = nameSearchKey(name)\n if (!key) return []\n const tokens = new Set<string>()\n for (const word of key.split(' ')) {\n if (!word) continue\n const capped = word.slice(0, NAME_TOKEN_MAX_PREFIX)\n for (let end = 1; end <= capped.length; end += 1) {\n tokens.add(capped.slice(0, end))\n if (tokens.size >= NAME_TOKEN_LIMIT) return [...tokens]\n }\n }\n return [...tokens]\n}\n\n/**\n * The one token a typed query becomes.\n *\n * `array-contains` takes a single value, and Firestore allows only one such\n * clause per query — so a multi-word query cannot be an AND on the server.\n * The FIRST word is used, which is what a reader is narrowing by when they\n * type \"acme cof\": they see the Acme results and read the rest themselves.\n * Capped to the same prefix length the tokens were written at, or a longer\n * query would match a token that was never stored.\n */\nexport function nameSearchToken(query: string | null | undefined): string {\n const key = nameSearchKey(query)\n if (!key) return ''\n return (key.split(' ')[0] ?? '').slice(0, NAME_TOKEN_MAX_PREFIX)\n}\n\n/**\n * The normalized name, reversed, so \"ends with\" becomes a prefix range.\n *\n * Firestore has one string operator that is not equality: the range. That\n * gives \"starts with\" directly — `>= q` and `<= q + ''` over\n * `nameLower` — and gives \"ends with\" nothing at all, because a range is\n * anchored at the front of the stored value.\n *\n * Reversing the stored key moves the end to the front. \"Acme Coffee\" is\n * stored as \"eeffoc emca\", and a search for names ending \"coffee\" becomes a\n * prefix range for \"eeffoc\" — the same query, on the same kind of index.\n *\n * Reversed by CODEPOINT (`[...key]`), not by UTF-16 unit: `split('')` cuts\n * surrogate pairs in half, so an emoji or a non-BMP character in a workspace\n * name would reverse into two lone surrogates and never match anything.\n */\nexport function nameSearchReversed(name: string | null | undefined): string {\n return [...nameSearchKey(name)].reverse().join('')\n}\n\n/**\n * The three search fields that travel with a `name`, as one object.\n *\n * Every collection whose list is SEARCHED on the server carries them, and\n * carrying only some is the failure mode worth designing against: `orderBy`\n * drops a document missing the field it sorts by, and `array-contains` drops\n * one missing the array. Either way the record still LISTS normally, so the\n * gap shows up only as a search that quietly cannot find one row.\n *\n * ⚠️ Spread this at every write that sets `name`, and only where `name` is\n * actually being written — a partial write that stamped an empty key over a\n * real one would make the document unfindable by the name it still displays.\n */\nexport function nameSearchFields(name: string): {\n name: string\n nameLower: string\n nameTokens: string[]\n nameReversed: string\n} {\n // `name` is INCLUDED so this is a drop-in replacement for writing `{ name }`.\n // A helper that returned only the derived keys would be spread beside the\n // name at four call sites, and the one that forgot would write search keys\n // for a name it never stored.\n return {\n name,\n nameLower: nameSearchKey(name),\n nameTokens: nameSearchTokens(name),\n nameReversed: nameSearchReversed(name),\n }\n}\n\n/**\n * The search fields a document NAMED BY `displayName` carries (AGL-3321):\n * the site artifacts (screens of every kind, layouts, reusable components,\n * templates) and the marketplace listings.\n *\n * `nameSearchFields` is for documents whose name IS `name`. These documents\n * name themselves `displayName`, and have since before search existed, so\n * the keys ride beside it under the same three names every list declares —\n * `lowerPath: 'nameLower'`, `tokensPath: 'nameTokens'`,\n * `reversedPath: 'nameReversed'` — and one list query grammar reads them all.\n *\n * `displayName` is NOT returned. A create that carries no name must not gain\n * an empty one: readers fall back with `displayName ?? id`, which an empty\n * string defeats. But the KEYS are always returned, empty for a missing name,\n * so a create stamps them unconditionally — an `orderBy('nameLower')` drops\n * any document that lacks the field, and a document created without a name\n * is still one a list must reach.\n *\n * ⚠️ Spread at every write that sets `displayName`, and at every create —\n * and nowhere else: a partial update that stamped keys for a name it did not\n * write would make the document unfindable by the name it still shows.\n */\nexport function displayNameSearchFields(displayName: unknown): {\n nameLower: string\n nameTokens: string[]\n nameReversed: string\n} {\n const name = typeof displayName === 'string' ? displayName : ''\n return {\n nameLower: nameSearchKey(name),\n nameTokens: nameSearchTokens(name),\n nameReversed: nameSearchReversed(name),\n }\n}\n\n/**\n * The normalizers a list query plan (`planListQuery` in\n * `@aglyn/shared-ui-jsx/const/list-query-plan`) turns typed values into keys\n * with: the same functions the writers stamp `nameLower`, `nameTokens` and\n * `nameReversed` with, so a query asks for what was stored (AGL-3321).\n */\nexport const nameSearchNormalizers = {\n key: (value: string) => nameSearchKey(value),\n token: (value: string) => nameSearchToken(value),\n reversed: (value: string) => nameSearchReversed(value),\n maxPrefix: NAME_TOKEN_MAX_PREFIX,\n} as const\n\n\n/**\n * What joins a scope token to a search prefix in a SCOPED search token\n * (AGL-3321): `host:abc~acm`.\n *\n * A list under a scope clause (`visibleTo array-contains-any [...]`) has\n * spent Firestore's one array clause on the scope, so its search cannot be a\n * second `array-contains`. The list query plan folds the typed word INTO the scope\n * clause instead (`ListQueryDeclaration.search.scoped`), asking for each\n * scope token joined to the word; the writers stamp every scope token\n * joined to every prefix, through {@link scopedSearchTokens}. A declaration\n * names this constant as its `join`, so the writer and the reader cannot\n * disagree about the character between the two halves.\n *\n * ⛔ Security rules cannot prove a folded query for a reader limited to some\n * sites. Rules like `canReadScoped` grant a list from\n * `visibleTo.hasAny(<the reader's scopes>)`, and a query that constrains only\n * the scoped-token field says nothing about `visibleTo`, so Firestore denies\n * it (pinned in cloud/rules-tests/firestore-rules.test.mjs). A folded search\n * is therefore for readers the rules admit WITHOUT a `visibleTo` term (an\n * org-wide member, whose rule short-circuits) or for an Admin-SDK route that\n * checks the reader's scope itself. A partial-reach collaborator's search\n * stays beside the `visibleTo` clause instead, as a prefix range on one\n * ordered key (media: `nameLower`; inbox leads: `email`).\n *\n * `~` appears in no scope token (`org`, `host:{id}`), so the split is never\n * ambiguous; a prefix that itself holds a `~` is matched whole on both sides\n * and needs no escaping.\n */\nexport const SCOPED_SEARCH_JOIN = '~'\n\n/**\n * The `search.scoped` half of a list declaration, over the array the\n * writers stamp with {@link scopedSearchTokens}.\n */\nexport function scopedSearch(tokensPath: string): {\n tokensPath: string\n join: typeof SCOPED_SEARCH_JOIN\n} {\n return { tokensPath, join: SCOPED_SEARCH_JOIN }\n}\n\n/**\n * Every scope a record is visible to, joined to every search prefix it\n * carries: `[org~a, org~ac, host:x~a, …]`, in scope order then token order,\n * with no duplicates (AGL-3321).\n *\n * `visibleTo` is read defensively — a record's stored array — and keeps\n * every non-empty string in it; anything else answers no scope, and so no\n * scoped tokens: a record visible to nobody is found by no scoped search.\n * `tokens` is whatever the record's plain search array holds, so a list that\n * searches several fields passes their merged prefixes rather than one\n * name's. Each is stamped beside the plain array on every write that sets\n * either the searched text or `visibleTo`.\n *\n * The count is the product of the two, so a record placed on thirty sites\n * with a long name carries thousands of entries. That stays far inside\n * Firestore's per-document index-entry ceiling, and it is the price of a\n * search a scoped reader can run at all.\n */\nexport function scopedSearchTokens(\n visibleTo: unknown,\n tokens: readonly string[],\n): string[] {\n if (!Array.isArray(visibleTo)) return []\n const scopes = [\n ...new Set(\n visibleTo.filter(\n (entry): entry is string => typeof entry === 'string' && entry.length > 0,\n ),\n ),\n ]\n const words = [...new Set(tokens.filter((token) => typeof token === 'string' && token))]\n const scoped: string[] = []\n for (const scope of scopes) {\n for (const word of words) scoped.push(`${scope}${SCOPED_SEARCH_JOIN}${word}`)\n }\n return scoped\n}\n"],"names":["nameSearchKey","name","trim","replace","toLowerCase","NAME_TOKEN_MAX_PREFIX","NAME_TOKEN_LIMIT","nameSearchTokens","key","tokens","Set","word","split","capped","slice","end","length","add","size","nameSearchToken","query","nameSearchReversed","reverse","join","nameSearchFields","nameLower","nameTokens","nameReversed","displayNameSearchFields","displayName","nameSearchNormalizers","value","token","reversed","maxPrefix","SCOPED_SEARCH_JOIN","scopedSearch","tokensPath","scopedSearchTokens","visibleTo","Array","isArray","scopes","filter","entry","words","scoped","scope","push"],"mappings":"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"}
|
|
@@ -31,6 +31,7 @@ export * from './collection-entries';
|
|
|
31
31
|
export * from './collection-kind';
|
|
32
32
|
export * from './collection-slug';
|
|
33
33
|
export * from './content-authors';
|
|
34
|
+
export * from './content-query-fields';
|
|
34
35
|
export * from './content-schema-type';
|
|
35
36
|
export * from './content-author-profile';
|
|
36
37
|
export * from './url-slug';
|
|
@@ -133,6 +134,7 @@ export * from './resolve-named-tokens';
|
|
|
133
134
|
export * from './screen-route';
|
|
134
135
|
export * from './host-naming';
|
|
135
136
|
export * from './name-search';
|
|
137
|
+
export * from './artifact-list-keys';
|
|
136
138
|
export * from './name-match';
|
|
137
139
|
export * from './api-plugins';
|
|
138
140
|
export * from './plugin-api-rate-limit';
|
|
@@ -34,6 +34,7 @@ export * from "./collection-entries.js";
|
|
|
34
34
|
export * from "./collection-kind.js";
|
|
35
35
|
export * from "./collection-slug.js";
|
|
36
36
|
export * from "./content-authors.js";
|
|
37
|
+
export * from "./content-query-fields.js";
|
|
37
38
|
export * from "./content-schema-type.js";
|
|
38
39
|
export * from "./content-author-profile.js";
|
|
39
40
|
export * from "./url-slug.js";
|
|
@@ -266,6 +267,7 @@ export * from "./resolve-named-tokens.js";
|
|
|
266
267
|
export * from "./screen-route.js";
|
|
267
268
|
export * from "./host-naming.js";
|
|
268
269
|
export * from "./name-search.js";
|
|
270
|
+
export * from "./artifact-list-keys.js";
|
|
269
271
|
export * from "./name-match.js";
|
|
270
272
|
export * from "./api-plugins.js";
|
|
271
273
|
export * from "./plugin-api-rate-limit.js";
|