]> Ladyโ€™s Gitweb - Pisces/blob - string.test.js
toIntegerOrInfinity ๐Ÿ”œ toIntegralNumberOrInfinity
[Pisces] / string.test.js
1 // โ™“๐ŸŒŸ Piscฤ“s โˆท string.test.js
2 // ====================================================================
3 //
4 // Copyright ยฉ 2022 Lady [@ Ladyโ€™s Computer].
5 //
6 // This Source Code Form is subject to the terms of the Mozilla Public
7 // License, v. 2.0. If a copy of the MPL was not distributed with this
8 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
9
10 import {
11 assert,
12 assertEquals,
13 assertSpyCall,
14 assertSpyCalls,
15 assertStrictEquals,
16 assertThrows,
17 describe,
18 it,
19 spy,
20 } from "./dev-deps.js";
21 import {
22 asciiLowercase,
23 asciiUppercase,
24 codepoints,
25 codeUnits,
26 getCharacter,
27 join,
28 Matcher,
29 scalarValues,
30 scalarValueString,
31 splitOnASCIIWhitespace,
32 splitOnCommas,
33 stripAndCollapseASCIIWhitespace,
34 stripLeadingAndTrailingASCIIWhitespace,
35 toString,
36 } from "./string.js";
37
38 describe("Matcher", () => {
39 it("[[Construct]] accepts a string first argument", () => {
40 assert(new Matcher(""));
41 });
42
43 it("[[Construct]] accepts a unicode regular expression first argument", () => {
44 assert(new Matcher(/(?:)/u));
45 });
46
47 it("[[Construct]] throws with a nonยทunicode regular expression first argument", () => {
48 assertThrows(() => new Matcher(/(?:)/));
49 });
50
51 it("[[Construct]] creates a callable object", () => {
52 assertStrictEquals(typeof new Matcher(""), "function");
53 });
54
55 it("[[Construct]] creates a new Matcher", () => {
56 assertStrictEquals(
57 Object.getPrototypeOf(new Matcher("")),
58 Matcher.prototype,
59 );
60 });
61
62 it("[[Construct]] creates an object which inherits from RegExp", () => {
63 assert(new Matcher("") instanceof RegExp);
64 });
65
66 it("[[Construct]] throws when provided with a noncallable, nonยทnull third argument", () => {
67 assertThrows(() => new Matcher("", undefined, "failure"));
68 });
69
70 describe("::dotAll", () => {
71 it("[[Get]] returns true when the dotAll flag is present", () => {
72 assertStrictEquals(new Matcher(/(?:)/su).dotAll, true);
73 });
74
75 it("[[Get]] returns false when the dotAll flag is not present", () => {
76 assertStrictEquals(new Matcher(/(?:)/u).dotAll, false);
77 });
78 });
79
80 describe("::exec", () => {
81 it("[[Call]] returns the match object given a complete match", () => {
82 assertEquals(
83 [...new Matcher(/.(?<wow>(?:.(?=.))*)(.)?/u).exec("success")],
84 ["success", "ucces", "s"],
85 );
86 assertEquals(
87 [...new Matcher(
88 /.(?<wow>(?:.(?=.))*)(.)?/u,
89 undefined,
90 ($) => $ === "success",
91 ).exec("success")],
92 ["success", "ucces", "s"],
93 );
94 });
95
96 it("[[Call]] calls the constraint if the match succeeds", () => {
97 const constraint = spy((_) => true);
98 const matcher = new Matcher("(.).*", undefined, constraint);
99 const result = matcher.exec({
100 toString() {
101 return "etaoin";
102 },
103 });
104 assertSpyCalls(constraint, 1);
105 assertSpyCall(constraint, 0, {
106 args: ["etaoin", result, matcher],
107 self: undefined,
108 });
109 });
110
111 it("[[Call]] does not call the constraint if the match fails", () => {
112 const constraint = spy((_) => true);
113 const matcher = new Matcher("", undefined, constraint);
114 matcher.exec("failure");
115 assertSpyCalls(constraint, 0);
116 });
117
118 it("[[Call]] returns null given a partial match", () => {
119 assertStrictEquals(new Matcher("").exec("failure"), null);
120 });
121
122 it("[[Call]] returns null if the constraint fails", () => {
123 assertStrictEquals(
124 new Matcher(".*", undefined, () => false).exec(""),
125 null,
126 );
127 });
128 });
129
130 describe("::global", () => {
131 it("[[Get]] returns true when the global flag is present", () => {
132 assertStrictEquals(new Matcher(/(?:)/gu).global, true);
133 });
134
135 it("[[Get]] returns false when the global flag is not present", () => {
136 assertStrictEquals(new Matcher(/(?:)/u).global, false);
137 });
138 });
139
140 describe("::hasIndices", () => {
141 it("[[Get]] returns true when the hasIndices flag is present", () => {
142 assertStrictEquals(new Matcher(/(?:)/du).hasIndices, true);
143 });
144
145 it("[[Get]] returns false when the hasIndices flag is not present", () => {
146 assertStrictEquals(new Matcher(/(?:)/u).hasIndices, false);
147 });
148 });
149
150 describe("::ignoreCase", () => {
151 it("[[Get]] returns true when the ignoreCase flag is present", () => {
152 assertStrictEquals(new Matcher(/(?:)/iu).ignoreCase, true);
153 });
154
155 it("[[Get]] returns false when the ignoreCase flag is not present", () => {
156 assertStrictEquals(new Matcher(/(?:)/u).ignoreCase, false);
157 });
158 });
159
160 describe("::multiline", () => {
161 it("[[Get]] returns true when the multiline flag is present", () => {
162 assertStrictEquals(new Matcher(/(?:)/mu).multiline, true);
163 });
164
165 it("[[Get]] returns false when the multiline flag is not present", () => {
166 assertStrictEquals(new Matcher(/(?:)/u).multiline, false);
167 });
168 });
169
170 describe("::source", () => {
171 it("[[Get]] returns the RegExp source", () => {
172 assertStrictEquals(new Matcher("").source, "(?:)");
173 assertStrictEquals(new Matcher(/.*/su).source, ".*");
174 });
175 });
176
177 describe("::sticky", () => {
178 it("[[Get]] returns true when the sticky flag is present", () => {
179 assertStrictEquals(new Matcher(/(?:)/uy).sticky, true);
180 });
181
182 it("[[Get]] returns false when the sticky flag is not present", () => {
183 assertStrictEquals(new Matcher(/(?:)/u).sticky, false);
184 });
185 });
186
187 describe("::unicode", () => {
188 it("[[Get]] returns true when the unicode flag is present", () => {
189 assertStrictEquals(new Matcher(/(?:)/u).unicode, true);
190 });
191 });
192
193 describe("~", () => {
194 it("[[Call]] returns true for a complete match", () => {
195 assertStrictEquals(new Matcher("")(""), true);
196 assertStrictEquals(new Matcher(/.*/su)("success\nyay"), true);
197 assertStrictEquals(
198 new Matcher(/.*/su, undefined, ($) => $ === "success")(
199 "success",
200 ),
201 true,
202 );
203 });
204
205 it("[[Call]] calls the constraint if the match succeeds", () => {
206 const constraint = spy((_) => true);
207 const matcher = new Matcher("(.).*", undefined, constraint);
208 matcher("etaoin");
209 assertSpyCalls(constraint, 1);
210 assertEquals(constraint.calls[0].args[0], "etaoin");
211 assertEquals([...constraint.calls[0].args[1]], ["etaoin", "e"]);
212 assertEquals(constraint.calls[0].args[2], matcher);
213 assertEquals(constraint.calls[0].self, undefined);
214 });
215
216 it("[[Call]] does not call the constraint if the match fails", () => {
217 const constraint = spy((_) => true);
218 const matcher = new Matcher("", undefined, constraint);
219 matcher("failure");
220 assertSpyCalls(constraint, 0);
221 });
222
223 it("[[Call]] returns false for a partial match", () => {
224 assertStrictEquals(new Matcher("")("failure"), false);
225 assertStrictEquals(new Matcher(/.*/u)("failure\nno"), false);
226 });
227
228 it("[[Call]] returns false if the constraint fails", () => {
229 assertStrictEquals(
230 new Matcher(".*", undefined, () => false)(""),
231 false,
232 );
233 });
234 });
235
236 describe("~lastIndex", () => {
237 it("[[Get]] returns zero", () => {
238 assertStrictEquals(new Matcher("").lastIndex, 0);
239 });
240
241 it("[[Set]] fails", () => {
242 assertThrows(() => (new Matcher("").lastIndex = 1));
243 });
244 });
245
246 describe("~length", () => {
247 it("[[Get]] returns one", () => {
248 assertStrictEquals(new Matcher("").length, 1);
249 });
250 });
251
252 describe("~name", () => {
253 it("[[Get]] wraps the stringified regular expression if no name was provided", () => {
254 assertStrictEquals(new Matcher("").name, "Matcher(/(?:)/u)");
255 assertStrictEquals(
256 new Matcher(/.*/gsu).name,
257 "Matcher(/.*/gsu)",
258 );
259 });
260
261 it("[[Get]] uses the provided name if one was provided", () => {
262 assertStrictEquals(new Matcher("", "success").name, "success");
263 });
264 });
265 });
266
267 describe("asciiLowercase", () => {
268 it("[[Call]] lowercases (just) AยทSยทCยทIยทI letters", () => {
269 assertStrictEquals(asciiLowercase("aBลฟร†ss Ftษษ‚รŸ"), "abลฟร†ss ftษษ‚รŸ");
270 });
271 });
272
273 describe("asciiUppercase", () => {
274 it("[[Call]] uppercases (just) AยทSยทCยทIยทI letters", () => {
275 assertStrictEquals(asciiUppercase("aBลฟร†ss Ftษษ‚รŸ"), "ABลฟร†SS FTษษ‚รŸ");
276 });
277 });
278
279 describe("codeUnits", () => {
280 it("[[Call]] returns an iterable", () => {
281 assertStrictEquals(
282 typeof codeUnits("")[Symbol.iterator],
283 "function",
284 );
285 });
286
287 it("[[Call]] returns an iterator", () => {
288 assertStrictEquals(typeof codeUnits("").next, "function");
289 });
290
291 it("[[Call]] returns a string code value iterator", () => {
292 assertStrictEquals(
293 codeUnits("")[Symbol.toStringTag],
294 "String Code Value Iterator",
295 );
296 });
297
298 it("[[Call]] iterates over the code units", () => {
299 assertEquals([
300 ...codeUnits("Ii๐ŸŽ™\uDFFF\uDD96\uD83C\uD800๐Ÿ†—โ˜บ"),
301 ], [
302 0x49,
303 0x69,
304 0xD83C,
305 0xDF99,
306 0xDFFF,
307 0xDD96,
308 0xD83C,
309 0xD800,
310 0xD83C,
311 0xDD97,
312 0x263A,
313 ]);
314 });
315 });
316
317 describe("codepoints", () => {
318 it("[[Call]] returns an iterable", () => {
319 assertStrictEquals(
320 typeof codepoints("")[Symbol.iterator],
321 "function",
322 );
323 });
324
325 it("[[Call]] returns an iterator", () => {
326 assertStrictEquals(typeof codepoints("").next, "function");
327 });
328
329 it("[[Call]] returns a string code value iterator", () => {
330 assertStrictEquals(
331 codepoints("")[Symbol.toStringTag],
332 "String Code Value Iterator",
333 );
334 });
335
336 it("[[Call]] iterates over the codepoints", () => {
337 assertEquals([
338 ...codepoints("Ii๐ŸŽ™\uDFFF\uDD96\uD83C\uD800๐Ÿ†—โ˜บ"),
339 ], [
340 0x49,
341 0x69,
342 0x1F399,
343 0xDFFF,
344 0xDD96,
345 0xD83C,
346 0xD800,
347 0x1F197,
348 0x263A,
349 ]);
350 });
351 });
352
353 describe("getCharacter", () => {
354 it("[[Call]] returns the character at the provided position", () => {
355 assertStrictEquals(getCharacter("Ii๐ŸŽ™๐Ÿ†—โ˜บ", 4), "๐Ÿ†—");
356 });
357
358 it("[[Call]] returns a low surrogate if the provided position splits a character", () => {
359 assertStrictEquals(getCharacter("Ii๐ŸŽ™๐Ÿ†—โ˜บ", 5), "\uDD97");
360 });
361
362 it("[[Call]] returns undefined for an outโ€ofโ€bounds index", () => {
363 assertStrictEquals(getCharacter("Ii๐ŸŽ™๐Ÿ†—โ˜บ", -1), void {});
364 assertStrictEquals(getCharacter("Ii๐ŸŽ™๐Ÿ†—โ˜บ", 7), void {});
365 });
366 });
367
368 describe("join", () => {
369 it("[[Call]] joins the provided iterator with the provided separartor", () => {
370 assertStrictEquals(join([1, 2, 3, 4].values(), "โ˜‚"), "1โ˜‚2โ˜‚3โ˜‚4");
371 });
372
373 it('[[Call]] uses "," if no separator is provided', () => {
374 assertStrictEquals(join([1, 2, 3, 4].values()), "1,2,3,4");
375 });
376
377 it("[[Call]] uses the empty sting for nullish values", () => {
378 assertStrictEquals(
379 join([null, , null, undefined].values(), "โ˜‚"),
380 "โ˜‚โ˜‚โ˜‚",
381 );
382 });
383 });
384
385 describe("scalarValueString", () => {
386 it("[[Call]] replaces invalid values", () => {
387 assertStrictEquals(
388 scalarValueString("Ii๐ŸŽ™\uDFFF\uDD96\uD83C\uD800๐Ÿ†—โ˜บ"),
389 "Ii๐ŸŽ™\uFFFD\uFFFD\uFFFD\uFFFD๐Ÿ†—โ˜บ",
390 );
391 });
392 });
393
394 describe("scalarValues", () => {
395 it("[[Call]] returns an iterable", () => {
396 assertStrictEquals(
397 typeof scalarValues("")[Symbol.iterator],
398 "function",
399 );
400 });
401
402 it("[[Call]] returns an iterator", () => {
403 assertStrictEquals(typeof scalarValues("").next, "function");
404 });
405
406 it("[[Call]] returns a string code value iterator", () => {
407 assertStrictEquals(
408 scalarValues("")[Symbol.toStringTag],
409 "String Code Value Iterator",
410 );
411 });
412
413 it("[[Call]] iterates over the scalar values", () => {
414 assertEquals([
415 ...scalarValues("Ii๐ŸŽ™\uDFFF\uDD96\uD83C\uD800๐Ÿ†—โ˜บ"),
416 ], [
417 0x49,
418 0x69,
419 0x1F399,
420 0xFFFD,
421 0xFFFD,
422 0xFFFD,
423 0xFFFD,
424 0x1F197,
425 0x263A,
426 ]);
427 });
428 });
429
430 describe("splitOnASCIIWhitespace", () => {
431 it("[[Call]] splits on sequences of spaces", () => {
432 assertEquals(
433 splitOnASCIIWhitespace("๐Ÿ…ฐ๏ธ ๐Ÿ…ฑ๏ธ ๐Ÿ†Ž ๐Ÿ…พ๏ธ"),
434 ["๐Ÿ…ฐ๏ธ", "๐Ÿ…ฑ๏ธ", "๐Ÿ†Ž", "๐Ÿ…พ๏ธ"],
435 );
436 });
437
438 it("[[Call]] splits on sequences of tabs", () => {
439 assertEquals(
440 splitOnASCIIWhitespace("๐Ÿ…ฐ๏ธ\t\t\t๐Ÿ…ฑ๏ธ\t๐Ÿ†Ž\t\t๐Ÿ…พ๏ธ"),
441 ["๐Ÿ…ฐ๏ธ", "๐Ÿ…ฑ๏ธ", "๐Ÿ†Ž", "๐Ÿ…พ๏ธ"],
442 );
443 });
444
445 it("[[Call]] splits on sequences of carriage returns", () => {
446 assertEquals(
447 splitOnASCIIWhitespace("๐Ÿ…ฐ๏ธ\r\r\r๐Ÿ…ฑ๏ธ\r๐Ÿ†Ž\r\r๐Ÿ…พ๏ธ"),
448 ["๐Ÿ…ฐ๏ธ", "๐Ÿ…ฑ๏ธ", "๐Ÿ†Ž", "๐Ÿ…พ๏ธ"],
449 );
450 });
451
452 it("[[Call]] splits on sequences of newlines", () => {
453 assertEquals(
454 splitOnASCIIWhitespace("๐Ÿ…ฐ๏ธ\r\r\r๐Ÿ…ฑ๏ธ\r๐Ÿ†Ž\r\r๐Ÿ…พ๏ธ"),
455 ["๐Ÿ…ฐ๏ธ", "๐Ÿ…ฑ๏ธ", "๐Ÿ†Ž", "๐Ÿ…พ๏ธ"],
456 );
457 });
458
459 it("[[Call]] splits on sequences of form feeds", () => {
460 assertEquals(
461 splitOnASCIIWhitespace("๐Ÿ…ฐ๏ธ\f\f\f๐Ÿ…ฑ๏ธ\f๐Ÿ†Ž\f\f๐Ÿ…พ๏ธ"),
462 ["๐Ÿ…ฐ๏ธ", "๐Ÿ…ฑ๏ธ", "๐Ÿ†Ž", "๐Ÿ…พ๏ธ"],
463 );
464 });
465
466 it("[[Call]] splits on mixed whitespace", () => {
467 assertEquals(
468 splitOnASCIIWhitespace("๐Ÿ…ฐ๏ธ\f \t\n๐Ÿ…ฑ๏ธ\r\n\r๐Ÿ†Ž\n\f๐Ÿ…พ๏ธ"),
469 ["๐Ÿ…ฐ๏ธ", "๐Ÿ…ฑ๏ธ", "๐Ÿ†Ž", "๐Ÿ…พ๏ธ"],
470 );
471 });
472
473 it("[[Call]] returns an array of just the empty string for the empty string", () => {
474 assertEquals(splitOnASCIIWhitespace(""), [""]);
475 });
476
477 it("[[Call]] returns a single token if there are no spaces", () => {
478 assertEquals(splitOnASCIIWhitespace("abcd"), ["abcd"]);
479 });
480
481 it("[[Call]] does not split on other kinds of whitespace", () => {
482 assertEquals(
483 splitOnASCIIWhitespace("a\u202F\u205F\xa0\v\0\bb"),
484 ["a\u202F\u205F\xa0\v\0\bb"],
485 );
486 });
487
488 it("[[Call]] trims leading and trailing whitespace", () => {
489 assertEquals(
490 splitOnASCIIWhitespace(
491 "\f\r\n\r\n \n\t๐Ÿ…ฐ๏ธ\f \t\n๐Ÿ…ฑ๏ธ\r๐Ÿ†Ž\n\f๐Ÿ…พ๏ธ\n\f",
492 ),
493 ["๐Ÿ…ฐ๏ธ", "๐Ÿ…ฑ๏ธ", "๐Ÿ†Ž", "๐Ÿ…พ๏ธ"],
494 );
495 });
496 });
497
498 describe("splitOnCommas", () => {
499 it("[[Call]] splits on commas", () => {
500 assertEquals(
501 splitOnCommas("๐Ÿ…ฐ๏ธ,๐Ÿ…ฑ๏ธ,๐Ÿ†Ž,๐Ÿ…พ๏ธ"),
502 ["๐Ÿ…ฐ๏ธ", "๐Ÿ…ฑ๏ธ", "๐Ÿ†Ž", "๐Ÿ…พ๏ธ"],
503 );
504 });
505
506 it("[[Call]] returns an array of just the empty string for the empty string", () => {
507 assertEquals(splitOnCommas(""), [""]);
508 });
509
510 it("[[Call]] returns a single token if there are no commas", () => {
511 assertEquals(splitOnCommas("abcd"), ["abcd"]);
512 });
513
514 it("[[Call]] splits into empty strings if there are only commas", () => {
515 assertEquals(splitOnCommas(",,,"), ["", "", "", ""]);
516 });
517
518 it("[[Call]] trims leading and trailing whitespace", () => {
519 assertEquals(
520 splitOnCommas("\f\r\n\r\n \n\t๐Ÿ…ฐ๏ธ,๐Ÿ…ฑ๏ธ,๐Ÿ†Ž,๐Ÿ…พ๏ธ\n\f"),
521 ["๐Ÿ…ฐ๏ธ", "๐Ÿ…ฑ๏ธ", "๐Ÿ†Ž", "๐Ÿ…พ๏ธ"],
522 );
523 assertEquals(
524 splitOnCommas("\f\r\n\r\n \n\t,,,\n\f"),
525 ["", "", "", ""],
526 );
527 });
528
529 it("[[Call]] removes whitespace from the split tokens", () => {
530 assertEquals(
531 splitOnCommas(
532 "\f\r\n\r\n \n\t๐Ÿ…ฐ๏ธ\f , \t\n๐Ÿ…ฑ๏ธ,\r\n\r๐Ÿ†Ž\n\f,๐Ÿ…พ๏ธ\n\f",
533 ),
534 ["๐Ÿ…ฐ๏ธ", "๐Ÿ…ฑ๏ธ", "๐Ÿ†Ž", "๐Ÿ…พ๏ธ"],
535 );
536 assertEquals(
537 splitOnCommas("\f\r\n\r\n \n\t\f , \t\n,\r\n\r\n\f,\n\f"),
538 ["", "", "", ""],
539 );
540 });
541 });
542
543 describe("stripAndCollapseASCIIWhitespace", () => {
544 it("[[Call]] collapses mixed inner whitespace", () => {
545 assertEquals(
546 stripAndCollapseASCIIWhitespace("๐Ÿ…ฐ๏ธ\f \t\n๐Ÿ…ฑ๏ธ\r\n\r๐Ÿ†Ž\n\f๐Ÿ…พ๏ธ"),
547 "๐Ÿ…ฐ๏ธ ๐Ÿ…ฑ๏ธ ๐Ÿ†Ž ๐Ÿ…พ๏ธ",
548 );
549 });
550
551 it("[[Call]] trims leading and trailing whitespace", () => {
552 assertStrictEquals(
553 stripAndCollapseASCIIWhitespace(
554 "\f\r\n\r\n \n\t\f ๐Ÿ…ฐ๏ธ\f \t\n๐Ÿ…ฑ๏ธ\r\n\r๐Ÿ†Ž\n\f๐Ÿ…พ๏ธ\n\f",
555 ),
556 "๐Ÿ…ฐ๏ธ ๐Ÿ…ฑ๏ธ ๐Ÿ†Ž ๐Ÿ…พ๏ธ",
557 );
558 });
559
560 it("[[Call]] returns the empty string for strings of whitespace", () => {
561 assertStrictEquals(
562 stripAndCollapseASCIIWhitespace("\f\r\n\r\n \n\t\f \n\f"),
563 "",
564 );
565 });
566
567 it("[[Call]] does not collapse other kinds of whitespace", () => {
568 assertEquals(
569 stripAndCollapseASCIIWhitespace("a\u202F\u205F\xa0\v\0\bb"),
570 "a\u202F\u205F\xa0\v\0\bb",
571 );
572 });
573 });
574
575 describe("stripLeadingAndTrailingASCIIWhitespace", () => {
576 it("[[Call]] trims leading and trailing whitespace", () => {
577 assertStrictEquals(
578 stripLeadingAndTrailingASCIIWhitespace(
579 "\f\r\n\r\n \n\t\f ๐Ÿ…ฐ๏ธ๐Ÿ…ฑ๏ธ๐Ÿ†Ž๐Ÿ…พ๏ธ\n\f",
580 ),
581 "๐Ÿ…ฐ๏ธ๐Ÿ…ฑ๏ธ๐Ÿ†Ž๐Ÿ…พ๏ธ",
582 );
583 });
584
585 it("[[Call]] returns the empty string for strings of whitespace", () => {
586 assertStrictEquals(
587 stripLeadingAndTrailingASCIIWhitespace("\f\r\n\r\n \n\t\f \n\f"),
588 "",
589 );
590 });
591
592 it("[[Call]] does not trim other kinds of whitespace", () => {
593 assertEquals(
594 stripLeadingAndTrailingASCIIWhitespace(
595 "\v\u202F\u205Fx\0\b\xa0",
596 ),
597 "\v\u202F\u205Fx\0\b\xa0",
598 );
599 });
600
601 it("[[Call]] does not adjust inner whitespace", () => {
602 assertEquals(
603 stripLeadingAndTrailingASCIIWhitespace("a b"),
604 "a b",
605 );
606 });
607 });
608
609 describe("toString", () => {
610 it("[[Call]] converts to a string", () => {
611 assertStrictEquals(
612 toString({
613 toString() {
614 return "success";
615 },
616 }),
617 "success",
618 );
619 });
620
621 it("[[Call]] throws when provided a symbol", () => {
622 assertThrows(() => toString(Symbol()));
623 });
624 });
This page took 0.226525 seconds and 5 git commands to generate.