on main help sign in
separate patp query validation and patp validation a bit
971bd0bJohn Hyde
ui/src/components/SearchBar.svelte+29−10
@@ -1,19 +1,25 @@
1<script lang="ts">
1<script lang="ts">
2− import ValidationErrors from './ValidationProblems.svelte';
2+ import ValidationProblems from './ValidationProblems.svelte';
3
3
4− import { validateIdSearchText, convertIdSearchTextToQuery } from '../lib/id';
4+ import { SearchAnalysis, analyzeSearch } from '../lib/id';
5
5
6− export let searchText: string = 'der';
6+ export let patp: string = undefined;
7 export let searchQuery: RegExp = /~der/;
7 export let searchQuery: RegExp = /~der/;
8− let problems: Array<string> = [];
8+ let searchText = 'der';
9+ let analysis: SearchAnalysis;
9
10
10 $: {
11 $: {
11− problems = validateIdSearchText(searchText);
12+ analysis = analyzeSearch(searchText);
12− if (problems.length === 0) {
13+ if (analysis.queryIsValid) {
13− searchQuery = convertIdSearchTextToQuery(searchText);
14+ searchQuery = analysis.query;
14 } else {
15 } else {
15 searchQuery = undefined;
16 searchQuery = undefined;
16 }
17 }
18+ if (analysis.patpIsValid) {
19+ patp = analysis.patp;
20+ } else {
21+ patp = undefined;
22+ }
17 }
23 }
18</script>
24</script>
19
25
@@ -27,9 +33,22 @@
27 <button class="absolute right-0 w-10 h-10 rounded-full bg-black transition-opacity opacity-10 hover:opacity-40"></button>
33 <button class="absolute right-0 w-10 h-10 rounded-full bg-black transition-opacity opacity-10 hover:opacity-40"></button>
28 </div>
34 </div>
29</div>
35</div>
30−
36+<div class="bg-gray-300 p-2 rounded-lg">
37+ <p>
38+ text: {searchText}
39+ </p>
40+ <p>
41+ query: {searchQuery}
42+ </p>
43+</div>
31−{#if searchText !== '' && problems.length > 0}
44+{#if searchText !== '' && analysis && analysis.queryProblems.length > 0}
32 <div class="flex justify-center">
45 <div class="flex justify-center">
33− <ValidationErrors {problems}></ValidationErrors>
46+ <ValidationProblems type="Query Problems" problems={analysis.queryProblems}></ValidationProblems>
34 </div>
47 </div>
35{/if}
48{/if}
49+
50+<!-- {#if searchText !== '' && analysis && analysis.patpProblems.length > 0}
51+ <div class="flex justify-center">
52+ <ValidationProblems type="ID Problems" problems={analysis.patpProblems}></ValidationProblems>
53+ </div>
54+{/if} -->
ui/src/components/SearchResults.svelte+9−4
@@ -4,13 +4,15 @@
4 import ShipListing from './ShipListing.svelte';
4 import ShipListing from './ShipListing.svelte';
5 import SearchResultNavButtons from './SearchResultNavButtons.svelte';
5 import SearchResultNavButtons from './SearchResultNavButtons.svelte';
6 import ShipView from './ShipView.svelte';
6 import ShipView from './ShipView.svelte';
7− export const searchText : string = '';
8 export let searchQuery : RegExp;
7 export let searchQuery : RegExp;
8+ export let patp : string;
9 let contactsList = [];
9 let contactsList = [];
10 let selectedShipIndex: number;
10 let selectedShipIndex: number;
11
11
12 $: {
12 $: {
13+ if (!patp) {
13− store.search(searchQuery)
14+ store.search(searchQuery)
15+ }
14 // if (searchQuery) {
16 // if (searchQuery) {
15 // store.search(searchQuery)
17 // store.search(searchQuery)
16 // } else {
18 // } else {
@@ -37,13 +39,16 @@
37 }
39 }
38</script>
40</script>
39
41
40−<!-- TODO: find out why this doesn't update as expected -->
42+<!-- should actually switch on whether field is empty -->
43+<!-- searchQuery is undefined when query is invalid -->
41<!-- {#if searchQuery === undefined}
44<!-- {#if searchQuery === undefined}
42 <p class="p-6 rounded-lg bg-white">
45 <p class="p-6 rounded-lg bg-white">
43 Hello, ~{window.ship}, would you like to search?
46 Hello, ~{window.ship}, would you like to search?
44 </p>
47 </p>
45{/if} -->
48{/if} -->
49+{#if patp}
50+ <ShipView {patp} />
46−{#if selectedShip}
51+{:else if selectedShip}
47 {#if contactsList.length > 1}
52 {#if contactsList.length > 1}
48 <SearchResultNavButtons
53 <SearchResultNavButtons
49 index={selectedShipIndex}
54 index={selectedShipIndex}
ui/src/components/ValidationProblems.svelte+2
@@ -1,8 +1,10 @@
1<script lang="ts">
1<script lang="ts">
2 export let problems: Array<string> = [];
2 export let problems: Array<string> = [];
3+ export let type: string = undefined;
3</script>
4</script>
4
5
5<div class="p-4 max-w-md rounded-lg bg-white">
6<div class="p-4 max-w-md rounded-lg bg-white">
7+ {#if type}<b class="w-full text-center">{type}</b>{/if}
6 <p>
8 <p>
7 Sorry, the ID you entered is invalid:
9 Sorry, the ID you entered is invalid:
8 </p>
10 </p>
ui/src/lib/id.ts+348−152
@@ -1,7 +1,298 @@
1+import type { Patp } from '@urbit/api';
2+import { listStrings } from './utils';
3+
4+type Problem = string;
5+
6+class SearchAnalysis {
7+ queryProblems: Problem[] = [];
8+ private _search: string = '';
9+ patpProblems: Problem[] = [];
10+
11+ constructor(search: string) {
12+ this.search = search;
13+ }
14+
15+ get searchIsSigged() {
16+ return sanitizeSearch(this._search)[0] === '~';
17+ }
18+
19+ get query(): RegExp {
20+ let search = this._search;
21+ search = expandAbbreviatedId(search);
22+ if (this.searchIsSigged) search = sig(search);
23+ return convertSearchTextToRegex(search);
24+ }
25+
26+ get patp(): Patp {
27+ return normalizeId(this._search);
28+ }
29+
30+ get queryIsValid(): boolean {
31+ return this.queryProblems.length === 0;
32+ }
33+
34+ get patpIsValid() {
35+ return this.patpProblems.length === 0;
36+ }
37+
38+ get queryAndPatpAreInvalid() {
39+ return !this.queryIsValid && !this.patpIsValid;
40+ }
41+
42+ get search() {
43+ return this._search;
44+ }
45+
46+ set search(search: string) {
47+ this._search = search;
48+ this.analyze();
49+ }
50+
51+ pushQueryProblem(...problems: Problem[]): void {
52+ if (!Object.isFrozen(this.queryProblems)) {
53+ this.queryProblems.push(...problems);
54+ }
55+ }
56+
57+ pushPatpProblem(...problems: Problem[]): void {
58+ if (!Object.isFrozen(this.patpProblems)) {
59+ this.patpProblems.push(...problems);
60+ }
61+ }
62+
63+ pushProblem(...problems: Problem[]): void {
64+ this.pushQueryProblem(...problems);
65+ this.pushPatpProblem(...problems);
66+ }
67+
68+ checkpointQueryValidation(): void {
69+ if (!this.queryIsValid) {
70+ Object.freeze(this.queryProblems);
71+ }
72+ }
73+
74+ checkpointPatpValidation(): void {
75+ if (!this.patpIsValid) {
76+ Object.freeze(this.patpProblems);
77+ }
78+ }
79+
80+ checkpointValidation(): void {
81+ this.checkpointQueryValidation();
82+ this.checkpointPatpValidation();
83+ }
84+
85+ resetAnalysis() {
86+ this.queryProblems = [];
87+ this.patpProblems = [];
88+ }
89+
90+ // mergeAnalysis(analysis: SearchAnalysis): void {
91+ // this.pushQueryProblem(...analysis.queryProblems);
92+ // this.pushPatpProblem(...analysis.patpProblems);
93+ // if (analysis.search) this._search = analysis.search;
94+ // if (analysis.patp) this.patp = analysis.patp;
95+ // }
96+
97+ analyze(): void {
98+ this.resetAnalysis();
99+ let search = normalizeIdAndDesig(this._search);
100+
101+ // Validate patp is not empty
102+ if (search.length === 0) {
103+ this.pushProblem(emptyPatpMessage);
104+ return;
105+ }
106+
107+ // Validate proper use of sig
108+ if (/~/.test(search)) {
109+ this.pushProblem(improperSigMessage);
110+ }
111+
112+ // Validate all characters are legal
113+ let illegalQueryChars = search.match(illegalQueryCharsRegex);
114+ if (illegalQueryChars) {
115+ this.pushQueryProblem(illegalCharsMessage(illegalQueryChars));
116+ }
117+ let illegalPatpChars = search.match(illegalPatpCharsRegex);
118+ if (illegalPatpChars) {
119+ this.pushPatpProblem(illegalCharsMessage(illegalPatpChars));
120+ }
121+ if (/\^/.test(search)) {
122+ this.pushPatpProblem(cannotInferMoonPatpMessage);
123+ }
124+ if (/_/.test(search)) {
125+ this.pushPatpProblem(cannotInferCometPatpMessage);
126+ }
127+
128+ // Validate separators don't appear on boundaries
129+ // This is fine in an unsigged query, but not a patp
130+ // TODO: check for if a query ends in something dumb like ^-
131+ let patp = search;
132+ if (/^[-^_]/.test(patp)) {
133+ this.pushPatpProblem(outsideSeparatorsMessage);
134+ patp = patp.replace(/^[-^_]*/, '');
135+ if (this.searchIsSigged) {
136+ this.pushQueryProblem(outsideSeparatorsMessage);
137+ search = patp;
138+ }
139+ }
140+ if (/[-^_]$/.test(patp)) {
141+ this.pushPatpProblem(outsideSeparatorsMessage);
142+ patp = patp.replace(/[-^_]*$/, '');
143+ }
144+
145+ // Validate word lengths
146+ // TODO: check words for query and patp separately 😥
147+ // because ~min- is min- for query but min for patp
148+ // (b/c already stripped of extra chars)
149+ let words = splitIdIntoWords(patp);
150+ let queryWords = splitIdIntoWords(search);
151+ if (words.length > 1) {
152+ let wrongLengthPatpWords = [];
153+ let wrongLengthQueryWords = [];
154+ let emptyWords = false;
155+ words.forEach((word, i) => {
156+ if (word.length !== 6) {
157+ if (word.length > 0) {
158+ wrongLengthPatpWords.push(word);
159+ if ((i > 0 || this.searchIsSigged) && i < words.length - 1) {
160+ wrongLengthQueryWords.push(word);
161+ }
162+ } else {
163+ emptyWords = true;
164+ }
165+ }
166+ });
167+ if (wrongLengthPatpWords.length > 0) {
168+ this.pushPatpProblem(multipleWordsLengthMessage(wrongLengthPatpWords));
169+ }
170+ if (wrongLengthQueryWords.length > 0) {
171+ this.pushQueryProblem(multipleWordsLengthMessage(wrongLengthQueryWords));
172+ }
173+ if (emptyWords) {
174+ this.pushProblem(emptyWordsMessage);
175+ }
176+ } else {
177+ let word = words[0];
178+ if (word.length !== 3 && word.length !== 6) {
179+ this.pushPatpProblem(singleWordLengthMessage(word));
180+ if (word.length > 6) {
181+ this.pushQueryProblem(singleWordTooLongMessage(word));
182+ }
183+ }
184+ }
185+
186+ this.checkpointValidation();
187+
188+ // Validate remaining structural edge cases
189+ if (words.length >= 5) {
190+ let cometIdRegex = /^([\w*]+-)+-([\w*]+-){3}[\w*]+$/;
191+ if (!cometIdRegex.test(patp)) {
192+ this.pushPatpProblem(invalidCometIdMessage);
193+ }
194+ } else if (words.length > 2) {
195+ let manyPartIdRegex = /^([\w*]+-)+[\w*]+$/;
196+ if (!manyPartIdRegex.test(patp)) {
197+ this.pushPatpProblem(tooLongAbbreviationMessage);
198+ }
199+ } else if (words.length == 2) {
200+ let twoPartIdRegex = /^[\w*]+[-^_][\w*]+$/;
201+ if (!twoPartIdRegex.test(patp)) {
202+ this.pushPatpProblem(tooLongAbbreviationMessage);
203+ }
204+ }
205+
206+ this.checkpointValidation();
207+
208+ // Validate valid prefixes and suffixes
209+ let wrongPatpPrefixes = [];
210+ let wrongPatpSuffixes = [];
211+ let wrongQueryPrefixes = [];
212+ let wrongQuerySuffixes = [];
213+ let wrongQueryFragments = [];
214+ queryWords.forEach((word, i) => {
215+ if (!word) return;
216+ let patpPrefix = word.substring(0, 3);
217+ let patpSuffix = word.substring(3, 6);
218+
219+ // if galaxy
220+ if (!patpSuffix) {
221+ patpSuffix = patpPrefix;
222+ patpPrefix = null;
223+ }
224+ const patpPrefixInvalid = patpPrefix && !prefixSearchValid(patpPrefix);
225+ if (patpPrefixInvalid) {
226+ wrongPatpPrefixes.push(patpPrefix);
227+ }
228+ const patpSuffixInvalid = patpSuffix && !suffixSearchValid(patpSuffix);
229+ if (patpSuffixInvalid) {
230+ wrongPatpSuffixes.push(patpSuffix);
231+ }
232+ const hasLeftNeighboringWord = i > 0 || this.searchIsSigged;
233+ const hasRightNeighboringWord = i < queryWords.length -1;
234+ let minLeftSplit = hasLeftNeighboringWord ? Math.min(3, word.length) : 0;
235+ // if maybe galaxy
236+ if (queryWords.length === 1 && word.length <= 3) minLeftSplit = 0;
237+ const maxRightSplit = hasRightNeighboringWord ? Math.max(word.length - 3, 0) : word.length;
238+
239+ // Please forgive this black magic
240+ const diff = 7 - word.length;
241+ const splitIndexes = [];
242+ for (let j = 0; j < diff; j++) {
243+ let splitIndex = 3 - j;
244+ if (splitIndex >= minLeftSplit && splitIndex <= maxRightSplit) {
245+ splitIndexes.push(splitIndex);
246+ }
247+ }
248+ let wordInvalid = true;
249+ let queryPrefix;
250+ let queryPrefixInvalid;
251+ let querySuffix;
252+ let querySuffixInvalid;
253+ for (let splitIndex of splitIndexes) {
254+ queryPrefix = word.substring(0, splitIndex);
255+ querySuffix = word.substring(splitIndex, word.length);
256+
257+ queryPrefixInvalid = queryPrefix && !prefixSearchValid(queryPrefix, { checkEnd: word.length >= 3 });
258+ querySuffixInvalid = querySuffix && !suffixSearchValid(querySuffix, { checkStart: word.length >= 3 });
259+ let wordValid = (!queryPrefixInvalid && !querySuffixInvalid);
260+ if (wordValid) {
261+ wordInvalid = false;
262+ }
263+ }
264+ if(wordInvalid && !wrongQueryFragments.includes(word)) {
265+ if (splitIndexes.length == 1) {
266+ if (queryPrefixInvalid) wrongQueryPrefixes.push(queryPrefix);
267+ if (querySuffixInvalid) wrongQuerySuffixes.push(querySuffix);
268+ } else {
269+ wrongQueryFragments.push(word);
270+ }
271+ }
272+ });
273+ if (wrongPatpPrefixes.length > 0) {
274+ this.pushPatpProblem(invalidPrefixesMessage(wrongPatpPrefixes));
275+ }
276+ if (wrongPatpSuffixes.length > 0) {
277+ this.pushPatpProblem(invalidSuffixesMessage(wrongPatpSuffixes));
278+ }
279+ if (wrongQueryPrefixes.length > 0) {
280+ this.pushQueryProblem(invalidPrefixesMessage(wrongQueryPrefixes));
281+ }
282+ if (wrongQuerySuffixes.length > 0) {
283+ this.pushQueryProblem(invalidSuffixesMessage(wrongQuerySuffixes));
284+ }
285+ if (wrongQueryFragments.length > 0) {
286+ this.pushQueryProblem(invalidFragmentsMessage(wrongQueryFragments));
287+ }
288+ }
289+}
290+
291+
1const prefixes = ['doz', 'mar', 'bin', 'wan', 'sam', 'lit', 'sig', 'hid', 'fid', 'lis', 'sog', 'dir', 'wac', 'sab', 'wis', 'sib', 'rig', 'sol', 'dop', 'mod', 'fog', 'lid', 'hop', 'dar', 'dor', 'lor', 'hod', 'fol', 'rin', 'tog', 'sil', 'mir', 'hol', 'pas', 'lac', 'rov', 'liv', 'dal', 'sat', 'lib', 'tab', 'han', 'tic', 'pid', 'tor', 'bol', 'fos', 'dot', 'los', 'dil', 'for', 'pil', 'ram', 'tir', 'win', 'tad', 'bic', 'dif', 'roc', 'wid', 'bis', 'das', 'mid', 'lop', 'ril', 'nar', 'dap', 'mol', 'san', 'loc', 'nov', 'sit', 'nid', 'tip', 'sic', 'rop', 'wit', 'nat', 'pan', 'min', 'rit', 'pod', 'mot', 'tam', 'tol', 'sav', 'pos', 'nap', 'nop', 'som', 'fin', 'fon', 'ban', 'mor', 'wor', 'sip', 'ron', 'nor', 'bot', 'wic', 'soc', 'wat', 'dol', 'mag', 'pic', 'dav', 'bid', 'bal', 'tim', 'tas', 'mal', 'lig', 'siv', 'tag', 'pad', 'sal', 'div', 'dac', 'tan', 'sid', 'fab', 'tar', 'mon', 'ran', 'nis', 'wol', 'mis', 'pal', 'las', 'dis', 'map', 'rab', 'tob', 'rol', 'lat', 'lon', 'nod', 'nav', 'fig', 'nom', 'nib', 'pag', 'sop', 'ral', 'bil', 'had', 'doc', 'rid', 'moc', 'pac', 'rav', 'rip', 'fal', 'tod', 'til', 'tin', 'hap', 'mic', 'fan', 'pat', 'tac', 'lab', 'mog', 'sim', 'son', 'pin', 'lom', 'ric', 'tap', 'fir', 'has', 'bos', 'bat', 'poc', 'hac', 'tid', 'hav', 'sap', 'lin', 'dib', 'hos', 'dab', 'bit', 'bar', 'rac', 'par', 'lod', 'dos', 'bor', 'toc', 'hil', 'mac', 'tom', 'dig', 'fil', 'fas', 'mit', 'hob', 'har', 'mig', 'hin', 'rad', 'mas', 'hal', 'rag', 'lag', 'fad', 'top', 'mop', 'hab', 'nil', 'nos', 'mil', 'fop', 'fam', 'dat', 'nol', 'din', 'hat', 'nac', 'ris', 'fot', 'rib', 'hoc', 'nim', 'lar', 'fit', 'wal', 'rap', 'sar', 'nal', 'mos', 'lan', 'don', 'dan', 'lad', 'dov', 'riv', 'bac', 'pol', 'lap', 'tal', 'pit', 'nam', 'bon', 'ros', 'ton', 'fod', 'pon', 'sov', 'noc', 'sor', 'lav', 'mat', 'mip', 'fip'];
292const prefixes = ['doz', 'mar', 'bin', 'wan', 'sam', 'lit', 'sig', 'hid', 'fid', 'lis', 'sog', 'dir', 'wac', 'sab', 'wis', 'sib', 'rig', 'sol', 'dop', 'mod', 'fog', 'lid', 'hop', 'dar', 'dor', 'lor', 'hod', 'fol', 'rin', 'tog', 'sil', 'mir', 'hol', 'pas', 'lac', 'rov', 'liv', 'dal', 'sat', 'lib', 'tab', 'han', 'tic', 'pid', 'tor', 'bol', 'fos', 'dot', 'los', 'dil', 'for', 'pil', 'ram', 'tir', 'win', 'tad', 'bic', 'dif', 'roc', 'wid', 'bis', 'das', 'mid', 'lop', 'ril', 'nar', 'dap', 'mol', 'san', 'loc', 'nov', 'sit', 'nid', 'tip', 'sic', 'rop', 'wit', 'nat', 'pan', 'min', 'rit', 'pod', 'mot', 'tam', 'tol', 'sav', 'pos', 'nap', 'nop', 'som', 'fin', 'fon', 'ban', 'mor', 'wor', 'sip', 'ron', 'nor', 'bot', 'wic', 'soc', 'wat', 'dol', 'mag', 'pic', 'dav', 'bid', 'bal', 'tim', 'tas', 'mal', 'lig', 'siv', 'tag', 'pad', 'sal', 'div', 'dac', 'tan', 'sid', 'fab', 'tar', 'mon', 'ran', 'nis', 'wol', 'mis', 'pal', 'las', 'dis', 'map', 'rab', 'tob', 'rol', 'lat', 'lon', 'nod', 'nav', 'fig', 'nom', 'nib', 'pag', 'sop', 'ral', 'bil', 'had', 'doc', 'rid', 'moc', 'pac', 'rav', 'rip', 'fal', 'tod', 'til', 'tin', 'hap', 'mic', 'fan', 'pat', 'tac', 'lab', 'mog', 'sim', 'son', 'pin', 'lom', 'ric', 'tap', 'fir', 'has', 'bos', 'bat', 'poc', 'hac', 'tid', 'hav', 'sap', 'lin', 'dib', 'hos', 'dab', 'bit', 'bar', 'rac', 'par', 'lod', 'dos', 'bor', 'toc', 'hil', 'mac', 'tom', 'dig', 'fil', 'fas', 'mit', 'hob', 'har', 'mig', 'hin', 'rad', 'mas', 'hal', 'rag', 'lag', 'fad', 'top', 'mop', 'hab', 'nil', 'nos', 'mil', 'fop', 'fam', 'dat', 'nol', 'din', 'hat', 'nac', 'ris', 'fot', 'rib', 'hoc', 'nim', 'lar', 'fit', 'wal', 'rap', 'sar', 'nal', 'mos', 'lan', 'don', 'dan', 'lad', 'dov', 'riv', 'bac', 'pol', 'lap', 'tal', 'pit', 'nam', 'bon', 'ros', 'ton', 'fod', 'pon', 'sov', 'noc', 'sor', 'lav', 'mat', 'mip', 'fip'];
2const suffixes = ['zod', 'nec', 'bud', 'wes', 'sev', 'per', 'sut', 'let', 'ful', 'pen', 'syt', 'dur', 'wep', 'ser', 'wyl', 'sun', 'ryp', 'syx', 'dyr', 'nup', 'heb', 'peg', 'lup', 'dep', 'dys', 'put', 'lug', 'hec', 'ryt', 'tyv', 'syd', 'nex', 'lun', 'mep', 'lut', 'sep', 'pes', 'del', 'sul', 'ped', 'tem', 'led', 'tul', 'met', 'wen', 'byn', 'hex', 'feb', 'pyl', 'dul', 'het', 'mev', 'rut', 'tyl', 'wyd', 'tep', 'bes', 'dex', 'sef', 'wyc', 'bur', 'der', 'nep', 'pur', 'rys', 'reb', 'den', 'nut', 'sub', 'pet', 'rul', 'syn', 'reg', 'tyd', 'sup', 'sem', 'wyn', 'rec', 'meg', 'net', 'sec', 'mul', 'nym', 'tev', 'web', 'sum', 'mut', 'nyx', 'rex', 'teb', 'fus', 'hep', 'ben', 'mus', 'wyx', 'sym', 'sel', 'ruc', 'dec', 'wex', 'syr', 'wet', 'dyl', 'myn', 'mes', 'det', 'bet', 'bel', 'tux', 'tug', 'myr', 'pel', 'syp', 'ter', 'meb', 'set', 'dut', 'deg', 'tex', 'sur', 'fel', 'tud', 'nux', 'rux', 'ren', 'wyt', 'nub', 'med', 'lyt', 'dus', 'neb', 'rum', 'tyn', 'seg', 'lyx', 'pun', 'res', 'red', 'fun', 'rev', 'ref', 'mec', 'ted', 'rus', 'bex', 'leb', 'dux', 'ryn', 'num', 'pyx', 'ryg', 'ryx', 'fep', 'tyr', 'tus', 'tyc', 'leg', 'nem', 'fer', 'mer', 'ten', 'lus', 'nus', 'syl', 'tec', 'mex', 'pub', 'rym', 'tuc', 'fyl', 'lep', 'deb', 'ber', 'mug', 'hut', 'tun', 'byl', 'sud', 'pem', 'dev', 'lur', 'def', 'bus', 'bep', 'run', 'mel', 'pex', 'dyt', 'byt', 'typ', 'lev', 'myl', 'wed', 'duc', 'fur', 'fex', 'nul', 'luc', 'len', 'ner', 'lex', 'rup', 'ned', 'lec', 'ryd', 'lyd', 'fen', 'wel', 'nyd', 'hus', 'rel', 'rud', 'nes', 'hes', 'fet', 'des', 'ret', 'dun', 'ler', 'nyr', 'seb', 'hul', 'ryl', 'lud', 'rem', 'lys', 'fyn', 'wer', 'ryc', 'sug', 'nys', 'nyl', 'lyn', 'dyn', 'dem', 'lux', 'fed', 'sed', 'bec', 'mun', 'lyr', 'tes', 'mud', 'nyt', 'byr', 'sen', 'weg', 'fyr', 'mur', 'tel', 'rep', 'teg', 'pec', 'nel', 'nev', 'fes'];
293const suffixes = ['zod', 'nec', 'bud', 'wes', 'sev', 'per', 'sut', 'let', 'ful', 'pen', 'syt', 'dur', 'wep', 'ser', 'wyl', 'sun', 'ryp', 'syx', 'dyr', 'nup', 'heb', 'peg', 'lup', 'dep', 'dys', 'put', 'lug', 'hec', 'ryt', 'tyv', 'syd', 'nex', 'lun', 'mep', 'lut', 'sep', 'pes', 'del', 'sul', 'ped', 'tem', 'led', 'tul', 'met', 'wen', 'byn', 'hex', 'feb', 'pyl', 'dul', 'het', 'mev', 'rut', 'tyl', 'wyd', 'tep', 'bes', 'dex', 'sef', 'wyc', 'bur', 'der', 'nep', 'pur', 'rys', 'reb', 'den', 'nut', 'sub', 'pet', 'rul', 'syn', 'reg', 'tyd', 'sup', 'sem', 'wyn', 'rec', 'meg', 'net', 'sec', 'mul', 'nym', 'tev', 'web', 'sum', 'mut', 'nyx', 'rex', 'teb', 'fus', 'hep', 'ben', 'mus', 'wyx', 'sym', 'sel', 'ruc', 'dec', 'wex', 'syr', 'wet', 'dyl', 'myn', 'mes', 'det', 'bet', 'bel', 'tux', 'tug', 'myr', 'pel', 'syp', 'ter', 'meb', 'set', 'dut', 'deg', 'tex', 'sur', 'fel', 'tud', 'nux', 'rux', 'ren', 'wyt', 'nub', 'med', 'lyt', 'dus', 'neb', 'rum', 'tyn', 'seg', 'lyx', 'pun', 'res', 'red', 'fun', 'rev', 'ref', 'mec', 'ted', 'rus', 'bex', 'leb', 'dux', 'ryn', 'num', 'pyx', 'ryg', 'ryx', 'fep', 'tyr', 'tus', 'tyc', 'leg', 'nem', 'fer', 'mer', 'ten', 'lus', 'nus', 'syl', 'tec', 'mex', 'pub', 'rym', 'tuc', 'fyl', 'lep', 'deb', 'ber', 'mug', 'hut', 'tun', 'byl', 'sud', 'pem', 'dev', 'lur', 'def', 'bus', 'bep', 'run', 'mel', 'pex', 'dyt', 'byt', 'typ', 'lev', 'myl', 'wed', 'duc', 'fur', 'fex', 'nul', 'luc', 'len', 'ner', 'lex', 'rup', 'ned', 'lec', 'ryd', 'lyd', 'fen', 'wel', 'nyd', 'hus', 'rel', 'rud', 'nes', 'hes', 'fet', 'des', 'ret', 'dun', 'ler', 'nyr', 'seb', 'hul', 'ryl', 'lud', 'rem', 'lys', 'fyn', 'wer', 'ryc', 'sug', 'nys', 'nyl', 'lyn', 'dyn', 'dem', 'lux', 'fed', 'sed', 'bec', 'mun', 'lyr', 'tes', 'mud', 'nyt', 'byr', 'sen', 'weg', 'fyr', 'mur', 'tel', 'rep', 'teg', 'pec', 'nel', 'nev', 'fes'];
3−const illegalIdCharsRegex = /[^-_^~abcdefghilmnoprstuvwxyz]/g;
294+const illegalPatpCharsRegex = /[^-_^~abcdefghilmnoprstuvwxyz]/g;
4−const illegalSearchCharsRegex = /[^-_*^~abcdefghilmnoprstuvwxyz]/g;
295+const illegalQueryCharsRegex = /[^-_*^~abcdefghilmnoprstuvwxyz]/g;
5
296
6const wildWord = '******';
297const wildWord = '******';
7const moonFiller = `${wildWord}-${wildWord}`;
298const moonFiller = `${wildWord}-${wildWord}`;
@@ -10,6 +301,8 @@ const cometFiller = `-${wildWord}-${wildWord}-${wildWord}--${wildWord}-${wildWor
10// Error Messages
301// Error Messages
11const emptyPatpMessage = "ID cannot be empty.";
302const emptyPatpMessage = "ID cannot be empty.";
12const improperSigMessage = "There should only be one ~ in an ID, at the beginning.";
303const improperSigMessage = "There should only be one ~ in an ID, at the beginning.";
304+const cannotInferMoonPatpMessage = "Please enter the full moon ID, rather than using the '^' abbreviation.";
305+const cannotInferCometPatpMessage = "Please enter the full comet ID, rather than using the '_' abbreviation.";
13const emptyWordsMessage = "Separators (- ^ _) cannot appear next to each other, except for a '--' before the last four parts of a comet ID.";
306const emptyWordsMessage = "Separators (- ^ _) cannot appear next to each other, except for a '--' before the last four parts of a comet ID.";
14const separatorsNeighboringMessage = "Separators (- ^ _) cannot appear next to each other, except for a '--' before the last four parts of a comet ID.";
307const separatorsNeighboringMessage = "Separators (- ^ _) cannot appear next to each other, except for a '--' before the last four parts of a comet ID.";
15const outsideSeparatorsMessage = "Separators (- ^ _) cannot appear at the beginning or end of an ID.";
308const outsideSeparatorsMessage = "Separators (- ^ _) cannot appear at the beginning or end of an ID.";
@@ -35,6 +328,10 @@ function singleWordLengthMessage(word: string) {
35 return `'${word}' must be either 3 or 6 letters long (either a galaxy or a star).`;
328 return `'${word}' must be either 3 or 6 letters long (either a galaxy or a star).`;
36}
329}
37
330
331+function singleWordTooLongMessage(word: string) {
332+ return `'${word}' will not match any IDs. It should not be longer than 6 letters.`;
333+}
334+
38function invalidSyllablesMessage(syllables: string[], syllableSet: string) {
335function invalidSyllablesMessage(syllables: string[], syllableSet: string) {
39 let wordListString = listStrings(syllables, true);
336 let wordListString = listStrings(syllables, true);
40 return `${wordListString} did not match any ${syllableSet}.`;
337 return `${wordListString} did not match any ${syllableSet}.`;
@@ -48,6 +345,11 @@ function invalidSuffixesMessage(syllables: string[]) {
48 return invalidSyllablesMessage(syllables, 'suffixes');
345 return invalidSyllablesMessage(syllables, 'suffixes');
49}
346}
50
347
348+function invalidFragmentsMessage(fragments: string[]) {
349+ let wordListString = listStrings(fragments, true);
350+ return `${wordListString} cannot match any prefix/suffix combinations.`;
351+}
352+
51function sig(str: string): string {
353function sig(str: string): string {
52 if (str[0] !== '~') {
354 if (str[0] !== '~') {
53 return '~' + str;
355 return '~' + str;
@@ -62,37 +364,56 @@ function desig(str: string): string {
62 return str;
364 return str;
63}
365}
64
366
65−function normalizeId(patp: string): string {
367+function sanitizeSearch(search: string): string {
66− patp = patp.toLowerCase();
368+ return search.toLowerCase().trim();
67− patp = sig(patp);
68− return patp;
69}
369}
70
370
71−function normalizeIdAndDesig(patp: string): string {
371+function normalizeId(patp: string): string {
72− patp = patp.toLowerCase();
73− patp = desig(patp);
74− return patp;
372+ return sig(sanitizeSearch(patp));
75}
373}
76
374
77−function listStrings(list: string[], includeAnd: boolean = false): string {
375+function normalizeIdAndDesig(patp: string): string {
78− if (includeAnd && list.length > 1) {
376+ return desig(sanitizeSearch(patp));
79− return listStrings(list.slice(0, -1)) + ' and ' + listStrings(list.slice(-1));
80− }
81− return "'" + list.join("', '") + "'";
82}
377}
83
378
84function splitIdIntoWords(str: string): string[] {
379function splitIdIntoWords(str: string): string[] {
85 return str.split(/(?:--|[-_^])/);
380 return str.split(/(?:--|[-_^])/);
86}
381}
87
382
383+function padWithWild(search: string) {
384+ let words = search.split(/[_^]/);
385+ words[0] = '*'.repeat(6 - words[0].length) + words[0];
386+ words[1] += '*'.repeat(6 - words[1].length);
387+ return words.join(search.match(/([_^])/)[1]);
388+}
389+
88−function convertSearchTextToRegex(str: string): RegExp {
390+function convertSearchTextToRegex(str: string, { checkStart = false, checkEnd = false } = {}): RegExp {
89− str = '^' + str.replace(/\*/g, '.') + '$';
391+ str = str.replace(/\*/g, '.');
392+ if (checkStart) {
393+ str = '^' + str;
394+ }
395+ if (checkEnd) {
396+ str = str + '$';
397+ }
90 return new RegExp(str);
398 return new RegExp(str);
91}
399}
92
400
401+function expandAbbreviatedId(patp: string): string {
402+ patp = normalizeIdAndDesig(patp);
403+ if (/\^/.test(patp)) {
404+ patp = padWithWild(patp);
405+ patp = '~' + moonFiller + '-' + patp.replace(/\^/, '-') + '$';
406+ } else if (/_/.test(patp)) {
407+ patp = padWithWild(patp);
408+ patp = '~' + patp.replace('_', cometFiller);
409+ }
410+ return patp;
411+}
412+
413+
93−function syllableSearchValid(search: string, syllables: string[]) {
414+function syllableSearchValid(search: string, syllables: string[], options = {}) {
94 for (let syllable of syllables) {
415 for (let syllable of syllables) {
95− let regex = convertSearchTextToRegex(search);
416+ let regex = convertSearchTextToRegex(search, options);
96 if (regex.test(syllable)) {
417 if (regex.test(syllable)) {
97 return true;
418 return true;
98 }
419 }
@@ -100,149 +421,24 @@ function syllableSearchValid(search: string, syllables: string[]) {
100 return false;
421 return false;
101}
422}
102
423
103−function prefixSearchValid(prefixSearch: string) {
424+function prefixSearchValid(prefixSearch: string, options = {}) {
104− return syllableSearchValid(prefixSearch, prefixes);
425+ return syllableSearchValid(prefixSearch, prefixes, options);
105}
426}
106
427
107−function suffixSearchValid(search: string) {
428+function suffixSearchValid(search: string, options = {}) {
108− return syllableSearchValid(search, suffixes);
429+ return syllableSearchValid(search, suffixes, options);
109}
430}
110
431
111−function validateIdSearchText(patp: string): string[] {
112− return validateId(patp, true);
113−}
114−
115−
116−function validateId(patp: string, isSearchQuery: boolean = false): string[] {
117− let problems = [];
118− patp = normalizeIdAndDesig(patp);
119−
120− // Validate patp is not empty
121− if (patp.length === 0) {
122− problems.push(emptyPatpMessage);
123− return problems;
124− }
125−
126− // Validate proper use of sig
127− if (/~/.test(patp)) {
128− problems.push(improperSigMessage);
129− }
130−
131− // Validate all characters are legal
132− const illegalCharsRegex = isSearchQuery ? illegalSearchCharsRegex : illegalIdCharsRegex;
133− let illegalChars = patp.match(illegalCharsRegex);
134− if (illegalChars) {
135− problems.push(illegalCharsMessage(illegalChars));
136− }
137−
138− // Validate proper use of separators
139− if (/^([-^_].*)?(.*[-^_])?$/.test(patp)) {
140− problems.push(outsideSeparatorsMessage);
141− patp = patp.match(/^[-^_]*(.*?)[-^_]*$/)[1];
142− }
143−
144− // Validate word lengths
145− let words = splitIdIntoWords(patp);
146− if (words.length > 1) {
147− let wrongLengthWords = [];
148− let emptyWords = false;
149− for (let word of words) {
150− if (word.length !== 6) {
151− if (word.length > 0) {
152− wrongLengthWords.push(word);
153− } else {
154− emptyWords = true;
155− }
156− }
157− }
158− if (wrongLengthWords.length > 0) {
159− problems.push(multipleWordsLengthMessage(wrongLengthWords));
160− }
161− if (emptyWords) {
162− problems.push(emptyWordsMessage);
163− }
164− } else {
165− let word = words[0];
166− if (word.length !== 3 && word.length !== 6) {
167− problems.push(singleWordLengthMessage(word));
168− }
169− }
170−
171− // Don't bother checking syllables if there are already problems
172− if (problems.length > 0) {
173− return problems;
174− }
175−
176− // Validate remaining structural edge cases
177− if (words.length >= 5) {
178− let cometIdRegex = /^([\w*]+-)+-([\w*]+-){3}[\w*]+$/;
179− if (!cometIdRegex.test(patp)) {
180− problems.push(invalidCometIdMessage);
181− }
182− } else if (words.length > 2) {
183− let manyPartIdRegex = /^([\w*]+-)+[\w*]+$/;
184− if (!manyPartIdRegex.test(patp)) {
185− problems.push(tooLongAbbreviationMessage);
186− }
187− } else if (words.length == 2) {
188− let twoPartIdRegex = /^[\w*]+[-^_][\w*]+$/;
189− if (!twoPartIdRegex.test(patp)) {
190− problems.push(tooLongAbbreviationMessage);
191− }
192− }
193−
194− // Don't bother checking syllables if there are already problems
195− if (problems.length > 0) {
196− return problems;
197− }
198−
199− // Validate valid prefixes and suffixes
200− let wrongPrefixes = [];
201− let wrongSuffixes = [];
202− for (let word of words) {
203− let prefix = word.substring(0, 3);
204− let suffix = word.substring(3, 6);
205− // if galaxy
206− if (!suffix) {
207− suffix = prefix;
208− prefix = null;
209− }
210− if (prefix && !prefixSearchValid(prefix)) {
211− wrongPrefixes.push(prefix);
212− }
213− if (suffix && !suffixSearchValid(suffix)) {
214− wrongSuffixes.push(suffix);
215− }
216− }
217− if (wrongPrefixes.length > 0) {
218− problems.push(invalidPrefixesMessage(wrongPrefixes));
219− }
220− if (wrongSuffixes.length > 0) {
221− problems.push(invalidSuffixesMessage(wrongSuffixes));
222− }
223
432
224− return problems;
225−}
226−
227−function expandAbbreviatedId(patp: string): string {
228− patp = normalizeIdAndDesig(patp);
229− if (/\^/.test(patp)) {
230− patp = moonFiller + '-' + patp.replace(/\^/, '-');
231− } else if (/_/.test(patp)) {
232− patp = patp.replace('_', cometFiller);
233− }
234− return patp;
235−}
236
433
237−function convertIdSearchTextToQuery(patp: string): RegExp {
434+function analyzeSearch(search: string): SearchAnalysis {
238− return convertSearchTextToRegex(sig(expandAbbreviatedId(patp)));
435+ return new SearchAnalysis(search);
239}
436}
240
437
241export {
438export {
439+ SearchAnalysis,
242 normalizeId,
440 normalizeId,
243 normalizeIdAndDesig,
441 normalizeIdAndDesig,
244− validateIdSearchText,
245− validateId,
246− convertIdSearchTextToQuery,
247 convertSearchTextToRegex,
442 convertSearchTextToRegex,
443+ analyzeSearch,
248};
444};
ui/src/lib/utils.ts+8
@@ -1,7 +1,15 @@
1+function listStrings(list: string[], includeAnd: boolean = false): string {
2+ if (includeAnd && list.length > 1) {
3+ return listStrings(list.slice(0, -1)) + ' and ' + listStrings(list.slice(-1));
4+ }
5+ return "'" + list.join("', '") + "'";
6+}
7+
1function filterObject(obj, predicate) {
8function filterObject(obj, predicate) {
2 return Object.fromEntries(Object.entries(obj).filter(([key, value]) => predicate(key, value)));
9 return Object.fromEntries(Object.entries(obj).filter(([key, value]) => predicate(key, value)));
3}
10}
4
11
5export {
12export {
13+ listStrings,
6 filterObject,
14 filterObject,
7}
15}
ui/src/routes/Home.svelte+3−3
@@ -1,11 +1,11 @@
1<script lang="ts">
1<script lang="ts">
2 import SearchBar from "../components/SearchBar.svelte";
2 import SearchBar from "../components/SearchBar.svelte";
3 import SearchResults from "../components/SearchResults.svelte";
3 import SearchResults from "../components/SearchResults.svelte";
4− let searchText: string;
4+ let patp: string;
5 let searchQuery: RegExp;
5 let searchQuery: RegExp;
6</script>
6</script>
7
7
8<div class="m-8 flex flex-col space-y-8">
8<div class="m-8 flex flex-col space-y-8">
9− <SearchBar bind:searchText bind:searchQuery />
9+ <SearchBar bind:patp bind:searchQuery />
10− <SearchResults bind:searchText {searchQuery}/>
10+ <SearchResults {patp} {searchQuery}/>
11</div>
11</div>
ui/src/routes/ShipViewPage.svelte+6−7
@@ -1,16 +1,15 @@
1<script lang="ts">
1<script lang="ts">
2 import { link, replace } from 'svelte-spa-router'
2 import { link, replace } from 'svelte-spa-router'
3− import { validateId, normalizeId } from '../lib/id';
3+ import { analyzeSearch, normalizeId } from '../lib/id';
4 import { linkToShip } from '../lib/link';
4 import { linkToShip } from '../lib/link';
5 import ShipView from '../components/ShipView.svelte';
5 import ShipView from '../components/ShipView.svelte';
6− import ValidationErrors from '../components/ValidationProblems.svelte';
6+ import ValidationProblems from '../components/ValidationProblems.svelte';
7 export let params: any = {};
7 export let params: any = {};
8
8
9 $: patp = normalizeId(params.patp);
9 $: patp = normalizeId(params.patp);
10− $: problems = validateId(patp);
10+ $: analysis = analyzeSearch(params.patp);
11− $: patpIsValid = problems.length === 0;
12 $: {
11 $: {
13− if (patpIsValid && patp !== params.patp) {
12+ if (analysis.patpIsValid && patp !== params.patp) {
14 replace(linkToShip(patp));
13 replace(linkToShip(patp));
15 }
14 }
16 }
15 }
@@ -18,9 +17,9 @@
18
17
19<div class="m-8 flex flex-col space-y-8">
18<div class="m-8 flex flex-col space-y-8">
20 <a use:link={"/"} class="p-4 bg-white rounded-lg text-center">Go Home</a>
19 <a use:link={"/"} class="p-4 bg-white rounded-lg text-center">Go Home</a>
21− {#if patpIsValid}
20+ {#if analysis.patpIsValid}
22 <ShipView patp={patp} />
21 <ShipView patp={patp} />
23 {:else}
22 {:else}
24− <ValidationErrors {problems} />
23+ <ValidationProblems problems={analysis.patpProblems} />
25 {/if}
24 {/if}
26</div>
25</div>
ui/src/styles/tailwind-output.css+17
@@ -524,6 +524,9 @@ Ensure the default browser behavior of the `hidden` attribute.
524.rounded-lg {
524.rounded-lg {
525 border-radius: 0.5rem;
525 border-radius: 0.5rem;
526}
526}
527+.rounded-sm {
528+ border-radius: 0.125rem;
529+}
527.border-b {
530.border-b {
528 border-bottom-width: 1px;
531 border-bottom-width: 1px;
529}
532}
@@ -535,6 +538,14 @@ Ensure the default browser behavior of the `hidden` attribute.
535 --tw-bg-opacity: 1;
538 --tw-bg-opacity: 1;
536 background-color: rgb(255 255 255 / var(--tw-bg-opacity));
539 background-color: rgb(255 255 255 / var(--tw-bg-opacity));
537}
540}
541+.bg-gray-200 {
542+ --tw-bg-opacity: 1;
543+ background-color: rgb(229 231 235 / var(--tw-bg-opacity));
544+}
545+.bg-gray-300 {
546+ --tw-bg-opacity: 1;
547+ background-color: rgb(209 213 219 / var(--tw-bg-opacity));
548+}
538.p-4 {
549.p-4 {
539 padding: 1rem;
550 padding: 1rem;
540}
551}
@@ -544,6 +555,12 @@ Ensure the default browser behavior of the `hidden` attribute.
544.p-8 {
555.p-8 {
545 padding: 2rem;
556 padding: 2rem;
546}
557}
558+.p-1 {
559+ padding: 0.25rem;
560+}
561+.p-2 {
562+ padding: 0.5rem;
563+}
547.py-2 {
564.py-2 {
548 padding-top: 0.5rem;
565 padding-top: 0.5rem;
549 padding-bottom: 0.5rem;
566 padding-bottom: 0.5rem;
ui/src/types/id.tsnew file+1
@@ -0,0 +1 @@
1+export {};