1<!DOCTYPE html> 2<html lang="en"> 3 4<head> 5 6<!-- ================================================================ --> 7<!-- Google tag (gtag.js) -->
vendor: 64 bytes, lines 7-8
7 8<script async src="https://www.googletagmanager.com/gtag/js?id=
8G-BZ8T9K9XYD
vendor: 12 bytes, line 8
8"></script>
9<script> 10
vendor: 133 bytes, lines 10-13
10window.dataLayer = window.dataLayer || []; 11 function gtag(){dataLayer.push(arguments);} 12 gtag('js', new Date()); 13 gtag('config', '
13G-BZ8T9K9XYD
vendor: 4 bytes, line 13
13');
14</script>
14 15<!-- ================================================================ --> 16 17<meta http-equiv="Content-type" content="text/html;charset=UTF-8"> 18<meta name="description" content="On-line word-finder app"> 19<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- mobile-friendly --> 20<meta name="keywords" content="word finder anagram search puzzle"> 21 22<title>Word Finder</title> 23 24<style> 25 26 /* BODY */ 27 body { 28 color: black; 29 background-color: white; 30 font-family: Verdana, sans-serif; 31 font-size: 150%; 32 letter-spacing: 0.25px; 33 line-height: 125%; 34 } 35 h1 { 36 font-weight: bold; 37 color: maroon; 38 font-size: 110%; 39 text-align: center; 40 margin: 40px; 41 } 42 h2 { 43 font-weight: bold; 44 color: maroon; 45 font-size: 105% 46 } 47 h3 { 48 font-weight: bold; 49 color: maroon; 50 font-size: 100% 51 } 52 a:link { 53 color: maroon; 54 } 55 a:visited { 56 color: maroon; 57 } 58 a:active { 59 color: maroon; 60 } 61 62 .rounded { 63 border-radius: 4px; 64 } 65 66 .larger-input-text { 67 /* font-size: 100%; */ 68 font-size: 0.8em; 69 } 70 .boxed-text { 71 border: 1px solid black; 72 padding-top: 4px; 73 padding-bottom: 4px; 74 padding-left: 8px; 75 padding-right: 8px; 76 } 77 .center { 78 text-align: center; 79 } 80 .maroon { 81 color: maroon; 82 } 83 84 /* TABLE */ 85 table { 86 border: none; 87 } 88 /* 89 for debug 90 table { 91 border: 1px solid; 92 border-collapse: collapse; 93 border-color: #808080; 94 } 95 td { 96 border: 1px solid; 97 border-collapse: collapse; 98 border-color: #808080; 99 } 100 */ 101 102 .smallertext { 103 font-size: 80% 104 } 105 106 /* ERROR-HANDLER */ 107 .error { 108 color: red; 109 background-color: white; 110 padding: 10px; 111 margin: 10px; 112 } 113 114 /* TEXT INPUTS */ 115 input { 116 font-weight: bold; 117 color: black; 118 } 119 120 /* BUTTONS */ 121 button { 122 font-weight: bold; 123 background-color: #e8e8e8; 124 border: 0; 125 padding: 4px; 126 border-radius: 4px; 127 margin: 4px; 128 } 129 button:hover { 130 font-weight: bold; 131 background-color: #d8d8d8; 132 border: 0; 133 padding: 4px; 134 border-radius: 4px; 135 margin: 4px; 136 } 137 button:active { 138 font-weight: bold; 139 background-color: #c0c0c0; 140 border: 0; 141 padding: 4px; 142 border-radius: 4px; 143 margin: 4px; 144 } 145 button.selected { 146 font-weight: bold; 147 } 148 button.deselected { 149 font-weight: normal; 150 } 151 button.no-margin { 152 margin: 0px; 153 } 154 button.transparent { 155 font-weight: normal; 156 border: 0; 157 background-color: transparent; 158 } 159 160 /* MODALS */ 161 /* 162 There's a trick here (found on the web). 163 o The modal background covers the *entire screen*. 164 o The modal content is a little box within that. 165 */ 166 .modal-background { 167 display: none; /* Hidden by default */ 168 position: fixed; /* Stay in place */ 169 z-index: 1; /* Sit on top */ 170 left: 0; 171 top: 0; 172 width: 100%; /* Entire screen */ 173 height: 100%; /* Entire screen */ 174 overflow: auto; /* Enable scroll if needed */ 175 background-color: rgb(0, 0, 0); /* Fallback color in case alpha channel isn't supported */ 176 background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity -- stuff "under" the modal is dimmed */ 177 } 178 .modal-content { 179 background-color: white; 180 border-radius: 4px; 181 margin: 15% auto; /* 15% from the top and centered */ 182 padding: 20px; 183 border: 2px solid gray; 184 width: 80%; 185 } 186 187 /* THEME SELECTOR */ 188 .switch { 189 position: relative; 190 display: inline-block; 191 width: 30px; 192 height: 17px; 193 } 194 .switch input { 195 opacity: 0; 196 width: 0; 197 height: 0; 198 } 199 .slider { 200 position: absolute; 201 cursor: pointer; 202 top: 0; 203 left: 0; 204 right: 0; 205 bottom: 0; 206 background-color: #ccc; 207 -webkit-transition: .4s; 208 transition: .4s; 209 } 210 .slider:before { 211 position: absolute; 212 content: ""; 213 height: 13px; 214 width: 13px; 215 left: 2px; 216 bottom: 2px; 217 background-color: white; 218 -webkit-transition: .4s; 219 transition: .4s; 220 } 221 input:checked + .slider { 222 background-color: #2196F3; 223 } 224 input:focus + .slider { 225 box-shadow: 0 0 1px #2196F3; 226 } 227 input:checked + .slider:before { 228 -webkit-transform: translateX(13px); 229 -ms-transform: translateX(13px); 230 transform: translateX(13px); 231 } 232 .slider.round {
233 border-radius: 17px; 234 } 235 .slider.round:before { 236 border-radius: 50%; 237 } 238 239</style> 240 241</head> 242 243<!-- ================================================================ --> 244<body> 245 246<h1><span class="boxed-text rounded">Word Finder</span></h1> 247 248<!-- 249 This is a self-contained widget for error-text and cancel/X button. 250 We use Sliver's setErrorWidget to route top-level errors here where 251 they can be seen on the page. 252--> 253<span id="error-container" class="error"> 254 <button 255 class=transparent 256 onClick=' 257 document.getElementById("error-container").style.display = "none"; 258 document.getElementById("error-text").innerHTML = ""; 259 ' 260 > × </button> 261 <span id="error-text"> </span> 262</span> 263 264<!-- fieldset allows a nice legend, of which I enjoy the styling --> 265<fieldset class="rounded"> 266 <legend> 267 <button id="random-word-mode-button" class="no-margin" data-faro-user-action-name="mode-random"> Loading ... </button> 268 <button id="ngram-mode-button" class="no-margin" data-faro-user-action-name="mode-ngram"> Loading ... </button> 269 <button id="pattern-mode-button" class="no-margin" data-faro-user-action-name="mode-pattern"> Loading ... </button> 270 <button id="anagram-mode-button" class="no-margin" data-faro-user-action-name="mode-anagram"> Loading ... </button> 271 <button id="bee-mode-button" class="no-margin" data-faro-user-action-name="mode-bee"> Loading ... </button> 272 </legend> 273 <table> 274 275 <!-- Row for bee mode --> 276 <tr id="bee-mode-row-1"> 277 <td> <button class="maroon" id="clear-letters-button"> Loading ... </button> </td> 278 <td> 279 <input 280 type=text 281 tabindex=1 282 placeholder="..." 283 size=10 284 autocapitalize="characters" 285 id="bee-mode-required-letters" 286 class="larger-input-text" 287 > 288 </td> 289 <td> </td> 290 <td class="smallertext"> Required letter(s) </td> 291 </tr> 292 293 <!-- Row for bee mode --> 294 <tr id="bee-mode-row-2"> 295 <td> <button class="maroon" id="example-button">Loading ...</button> </td> 296 <td> 297 <input 298 type=text 299 tabindex=2 300 placeholder="..." 301 size=10 302 autocapitalize="characters" 303 id="bee-mode-other-letters" 304 class="larger-input-text" 305 > 306 </td> 307 <td> </td> 308 <td class="smallertext"> Other letter(s) </td> 309 </tr> 310 311 <!-- Row for bee mode --> 312 <tr id="bee-mode-row-3"> 313 <td> <button class="maroon" id="all-others-button">Loading ...</button> </td> 314 <td> </td> 315 <td> </td> 316 <td> </td> 317 </tr> 318 319 <!-- Row for pattern mode --> 320 <tr id="pattern-mode-row-1"> 321 <td> <button class="maroon" id="pattern-clear-letters-button"> Loading ... </button> </td> 322 <td> 323 <input 324 type=text 325 tabindex=1 326 placeholder="..." 327 size=10 328 id="pattern-mode-letters" 329 class="larger-input-text" 330 > 331 </td> 332 <td> </td> 333 <td class="smallertext"> Letters, including <code>.</code> and <code>*</code> </td> 334 </tr> 335 336 <!-- Row for anagram mode --> 337 <tr id="anagram-mode-row-1"> 338 <td> <button class="maroon" id="anagram-clear-letters-button"> Loading ... </button> </td> 339 <td> 340 <input 341 type=text 342 tabindex=1 343 placeholder="..." 344 size=10 345 id="anagram-mode-letters" 346 class="larger-input-text" 347 > 348 </td> 349 <td> </td> 350 <td class="smallertext"> Letters </td> 351 </tr> 352 353 <!-- Row for random-word mode --> 354 <tr id="random-word-mode-row-1"> 355 <td> <button class="maroon" id="random-word-generate-button"> Loading ... </button> </td> 356 <td style="width:3em"> 357 <input type="number" id="random-word-count" style="width:5ch"> 358 </td> 359 <td> </td> 360 <td class="smallertext"> Word count </td> 361 </tr> 362 363 <!-- Row for ngram mode --> 364 <tr id="ngram-mode-row-1"> 365 <td> <button class="maroon" id="ngram-generate-button"> Loading ... </button> </td> 366 <td style="width:3em"> 367 <input type="number" id="ngram-word-count-input" style="width:6ch"> 368 </td> 369 <td> </td> 370 <td class="smallertext"> Word count </td> 371 </tr> 372 373 <!-- Row for ngram mode --> 374 <tr id="ngram-mode-row-2"> 375 <td> </td> 376 <td style="width:3em"> 377 <input type="number" id="ngram-n-input" style="width:6ch"> 378 </td> 379 <td> </td> 380 <td class="smallertext"> The <i>n</i> in <i>n</i>-gram </td> 381 </tr> 382 383 <!-- Word list used by all modes --> 384 <tr> 385 <td colspan=2 style="text-align:right"> 386 <select tabindex=3 id="word-list-dropdown"> 387 <!-- 388 These are helpful for testing out the ngram code in verbose mode: 389 <option value="./one-word.txt">One word (TEMP)</option> 390 <option value="./short-list.txt">Short list (TEMP)</option> 391 --> 392 <option value="./usual-suspects.txt">Usual suspects</option> 393 <option value="./words-200-swadesh.txt">English 200 (Swadesh)</option> 394 <option value="./words-2K.txt">English 2K (GSL)</option> 395 <option value="./words-10K.txt">English 10K</option> 396 <option value="./popular.txt" selected>English 25K (<tt>popular.txt</tt>)</option> 397 <option value="./words-40K-cumulative.txt">English 40K</option> 398 <option value="./enable1.txt" selected>English 173K (<tt>enable1.txt</tt>)</option> 399 <option value="./long-list.txt">English 300K</option> 400 <option value="./finnegans-words.txt">Finnegan</option> 401 <option value="./bahasa-indonesia.txt">Bahasa Indonesia</option> 402 <option value="./french.txt">French</option> 403 <option value="./german.txt">German</option> 404 <option value="./greek.txt">Greek</option> 405 <option value="./italian.txt">Italian</option> 406 <option value="./latin.txt">Latin</option> 407 <option value="./russian.txt">Russian</option> 408 <option value="./spanish.txt">Spanish</option> 409 </select> 410 </td> 411 <td> </td> 412 <td class="smallertext"> Word list </td> 413 </tr> 414 415 <!-- Min/max word lengths used by bee and pattern modes, but not anagram mode or random-word or ngram --> 416 <tr id="match-lengths-row"> 417 <td> <button class="maroon" id="reset-match-lengths-button"> Reset </button> </td> 418 <td>
419 <span> 420 <input tabindex=4 type="number" id="min-match-length" name="min-match-length" style="width:4ch"> 421 <input tabindex=5 type="number" id="max-match-length" name="max-match-length" style="width:4ch"> 422 </span> 423 </td> 424 <td> </td> 425 <td class="smallertext"> <label>Min/max word lengths</label> </td> 426 </tr> 427 428 <tr> 429 <td> 430 <div> 431 <button id="random-word-mode-help-launcher-button"> Help </button> 432 <button id="ngram-mode-help-launcher-button"> Help </button> 433 <button id="pattern-mode-help-launcher-button"> Help </button> 434 <button id="anagram-mode-help-launcher-button"> Help </button> 435 <button id="bee-mode-help-launcher-button"> Help </button> 436 </div> 437 </td> 438 <td> </td> 439 <td> </td> 440 </tr> 441 442 </table> 443</fieldset> 444 445<br> 446<div> 447 <button class="maroon" id="output-show-hide-button"> Loading ... </button> 448</div> 449<div> 450 <span id="output" style="display:block"> Loading ... </span> 451</div> 452<div> 453 <span id="output-count" style="display:none"> Loading ... </span> 454</div> 455 456<!-- ================================================================ --> 457<hr> 458 459<div class=smallertext> 460 Source and documentation: 461 <br> 462 <a href="https://github.com/johnkerl/word-finder/blob/main/README.md">github.com/johnkerl/word-finder</a> 463</div> 464<br> 465 466<div class=smallertext> 467 <label class="switch"> 468 <input type="checkbox" id="light-theme-dark-theme-toggle"> 469 <span class="slider round"></span> 470 </label> 471 <span id="light-theme-dark-theme-label">Loading ...</span> 472</div> 473<br> 474 475<!-- ================================================================ --> 476 477<!-- Modal content --> 478 479<div id="random-word-mode-help-background" class="modal-background"> 480 <div id="random-word-mode-help-content" class="modal-content"> 481 <div> 482 <button id="random-word-mode-help-close-button" class="general white-on-green" > Close </button> 483 </div> 484 <div> 485 486 <p class="maroon">Help for random-word mode:</p> 487 488 <p> Select the word list.</p> 489 <p> Select how many words to generate at a time.</p> 490 <p> Use the Generate button to get a new random sample.</p> 491 492 </div> 493 </div> 494</div> 495 496<div id="ngram-mode-help-background" class="modal-background"> 497 <div id="ngram-mode-help-content" class="modal-content"> 498 <div> 499 <button id="ngram-mode-help-close-button" class="general white-on-green" > Close </button> 500 </div> 501 <div> 502 503 <p class="maroon">Help for Jabberwocky mode:</p> 504 505 <p> Select the word list.</p> 506 <p> Select how many words to generate at a time.</p> 507 <p> Select the <i>n</i> for <a href="https://en.wikipedia.org/wiki/N-gram" target="_blank">n-grams</a>: 508 the higher the <i>n</i>, the more closely the emitted words will resemble those in the word list. 509 </p> 510 <p> Use the Generate button to get a new random sample. 511 The words produced will be created by splicing together parts of real words in word list. 512 </p> 513 <p>(The name is taken from Lewis Carroll’s famous poem 514 <a href="https://en.wikipedia.org/wiki/Jabberwocky" target="_blank"><i>Jabberwocky</i></a>. 515 If <i>Jabberwocky</i>’s 71 words leave you wanting more, see also 516 <a href="https://en.wikipedia.org/wiki/Finnegans_Wake" target="_blank"><i>Finnegans Wake</i></a>.) 517 </p> 518 519 </div> 520 </div> 521</div> 522 523<div id="pattern-mode-help-background" class="modal-background"> 524 <div id="pattern-mode-help-content" class="modal-content"> 525 <div> 526 <button id="pattern-mode-help-close-button" class="general white-on-green" > Close </button> 527 </div> 528 <div> 529 530 <p class="maroon">Help for pattern mode:</p> 531 532 <p> Enter your desired letters in the <code>Letters</code> field.</p> 533 534 <p> Select the word list you want to search in.</p> 535 536 <p> You can use a <code>.</code> as a wildcard to match any single letter. For example, <code>LE.T</code> 537 matches <code>LEFT</code>, <code>LENT</code>, <code>LEST</code>, etc.</p> 538 539 <p> You can use a <code>*</code> as a wildcard to match zero or more of any letter. For example:</p> 540 <ul> 541 <li><code>OBOLUS</code> shows the one word <code>OBOLUS</code>, as a dictionary lookup</li> 542 <li><code>THR*</code> shows all words that begin with <code>THR</code></li> 543 <li><code>*EST</code> shows all words that end with <code>EST</code></li> 544 <li><code>TH*TH</code> shows all words that begin and end with <code>TH</code> and</li> 545 <li><code>*NIMB*</code> shows all words that have the sequence <code>NIMB</code> anywhere within.</li> 546 </ul> 547 548 <p> You can further restrict your results by setting minimum and maximum word lengths. </p> 549 550 </div> 551 </div> 552</div> 553 554<div id="anagram-mode-help-background" class="modal-background">
555 <div id="anagram-mode-help-content" class="modal-content"> 556 <div> 557 <button id="anagram-mode-help-close-button" class="general white-on-green" > Close </button> 558 </div> 559 <div> 560 561 <p class="maroon">Help for anagram mode:</p> 562 563 <p> Enter your desired letters in the Letters field.</p> 564 565 <p> Select the word list you want to search in.</p> 566 567 <p> Results shown will be <a href="https://en.wikipedia.org/wiki/Anagram" 568 target="_blank">anagrams</a> of the letters you provide. For example, <code>ELATES</code> will 569 match <code>SEALET</code> but not <code>ELATE</code> or <code>SEALETS</code>. </p> 570 571 <p> Some words with many anagrams include <code>STOP</code>, <code>SLATE</code>, 572 <code>PEARS</code>, and <code>TRIANGLE</code>.</p> 573 574 </div> 575 </div> 576</div> 577 578<div id="bee-mode-help-background" class="modal-background"> 579 <div id="bee-mode-help-content" class="modal-content"> 580 <div> 581 <button id="bee-mode-help-close-button" class="general white-on-green" > Close </button> 582 </div> 583 <div> 584 585 <p class="maroon">Help for bee mode:</p> 586 587 <p>Enter your required letters and your other letters. (For the NYT Spelling Bee, 588 this will be the center letter and the surrounding letters, respectively.) 589 590 <p>Choose the word list from the dropdown.</p> 591 <ul> 592 593 <li><i>Usual suspects</i> is my own short, curated list of once-bitten-twice-shy values from 594 previous Bees.</li> 595 596 <li><i>Long list</i> contains over 300,000 words, not all of which are in the NYT Spelling 597 Bee. (That is, this app can show words that the NYT Spelling Bee doesn’t accept.)</li> 598 599 <li>You can use the 2K through 40K lists to adjust whether or not to see less-common words.</li> 600 601 </ul> 602 603 <p>The output shown will include all words from the selected word list which contain only your 604 required letters and your other letters:</p> 605 <ul> 606 607 <li>If the required letter is <code>P</code> and the other letters are <code>AELM</code>, 608 then you’ll see <code>PALM</code> and <code>PALE</code> but not 609 <code>MEAL</code>.</li> 610 611 <li>If the required letter is <code>M</code> and the other letters are <code>AELP</code>, 612 then you’ll see <code>MALL</code> and <code>MEAL</code> but not 613 <code>PALE</code>.</li> 614 615 <li>If the required letters are left empty and the other letters are <code>AELPM</code>, 616 then you’ll see words including <code>MALL</code>, <code>MEAL</code>, 617 <code>PALE</code>, <code>MAPLE</code>, and <code>EEL</code>. </li> 618 619 </ul> 620 621 <p> You can further restrict your results by setting minimum and maximum word lengths. 622 623 </div> 624 </div> 625</div> 626 627<!-- ================================================================ -->
628<script type=module> 629 'use strict'; 630 631 import { getWebInstrumentations, initializeFaro } from 'https://esm.sh/@grafana/[email protected]'; 632 import { TracingInstrumentation } from 'https://esm.sh/@grafana/[email protected]'; 633 634 const faro = initializeFaro({ 635 url: 'https://faro-collector-prod-us-east-3.grafana.net/collect/5a322a8a02b13a3c06b8884cc4b52076', 636 app: { 637 name: 'johnkerl-org-word-finder-1', 638 version: '1.0.0', 639 environment: 'production' 640 }, 641 642 experimental: { trackNavigation: true }, 643 644 ignoreUrls: [ 645 /googletagmanager\.com/, 646 /google-analytics\.com/, 647 ], 648 649 ignoreErrors: [ 650 /^ResizeObserver loop limit exceeded$/, 651 /^ResizeObserver loop completed with undelivered notifications$/, 652 /^Script error\.$/, 653 /chrome-extension:\/\//, 654 /moz-extension:\/\//, 655 ], 656 657 instrumentations: [ 658 // Mandatory, omits default instrumentations otherwise. 659 ...getWebInstrumentations(), 660 661 // Tracing package to get end-to-end visibility for HTTP requests. 662 new TracingInstrumentation(), 663 ], 664 }); 665 666 import { 667 Button, 668 ConstrainedTextInput, 669 GenericElement, 670 HelpWidgets, 671 LightDarkThemeSlider, 672 OneButtonSwitcher, 673 PersistentDropdown, 674 PersistentIntRangeInput, 675 URLAndPersistentNButtonSwitcher, 676 TextSpan, 677 isInteger, 678 setErrorWidget, 679 // } from "http://127.0.1:9999/sliver/sliver.js" 680 // nohup npx http-server ~/pub_http_internet -o -p 9999 & 681 // } from "https://johnkerl.org/sliver/sliver.js" 682 } from "./sliver-2025-08-12.js" // local copy, with cache-bust 683 684 import { 685 NGramsKeeper, 686 } from "./ngrams.js" 687 688 // ---------------------------------------------------------------- 689 setErrorWidget("error-container", "error-text") 690 691 // ---------------------------------------------------------------- 692 class WordFinderApp { 693 694 constructor() { 695 696 this.state = { 697 "logLevel": 0, // 0,1,2,3 698 699 "defaultLowMatchLength": 4, 700 "defaultHighMatchLength": 16, 701 "minMatchLength": 1, 702 "maxMatchLength": 30, 703 704 "defaultRandomWordCount": 5, 705 "minRandomWordCount": 1, 706 "maxRandomWordCount": 100, 707 708 "defaultNGramN": 4, 709 "minNGramN": 2, 710 "maxNGramN": 8, 711 712 "defaultNGramWordCount": 5, 713 "minNGramWordCount": 1, 714 "maxNGramWordCount": 100, 715 716 "mode": null, 717 718 "ngramsKeeper": null, 719 720 "wordListURL": null, 721 "wordList": null, 722 "wordListForAnagramMode": null, // Letters are sorted within words 723 } 724 725 this.setElementsAndCallbacks() 726 this.setWordList(this.elements.wordListDropdown.get()) 727 728 if (this.elements.modeButtons.which() == 'bee-mode-button') { 729 this.elements.requiredLettersElement.focus() 730 } else if (this.elements.modeButtons.which() == 'anagram-mode-button') { 731 this.elements.anagramLettersElement.focus() 732 } else if (this.elements.modeButtons.which() == 'pattern-mode-button') { 733 this.elements.patternLettersElement.focus() 734 } else if (this.elements.modeButtons.which() == 'random-word-mode-button') { 735 this.elements.randomWordGenerateButton.focus() 736 } else if (this.elements.modeButtons.which() == 'ngram-mode-button') { 737 this.elements.ngramGenerateButton.focus() 738 } 739 740 } 741 742 setElementsAndCallbacks() { 743 this.elements = {} 744 745 this.elements.outputElement = new TextSpan("output", "") 746 this.elements.outputCountElement = new TextSpan("output-count", "") 747 748 // Bee-mode element 749 this.elements.requiredLettersElement = new ConstrainedTextInput( 750 "bee-mode-required-letters", 751 (text) => text.toUpperCase(), 752 //(text) => text, 753 (event) => { 754 this.logAt1("required-letters input: <<", event.target.value, ">>") 755 this.showBeeModeMatches() 756 }) 757 758 // Bee-mode element 759 this.elements.otherLettersElement = new ConstrainedTextInput( 760 "bee-mode-other-letters", 761 (text) => text.toUpperCase(), 762 //(text) => text, 763 (event) => { 764 this.logAt1("other-letters input: <<", event.target.value, ">>") 765 this.showBeeModeMatches() 766 }) 767 768 // Pattern-mode element 769 this.elements.patternLettersElement = new ConstrainedTextInput( 770 "pattern-mode-letters", 771 //(text) => text.toUpperCase(), 772 (text) => text, 773 (event) => { 774 this.logAt1("pattern-letters input: <<", event.target.value, ">>") 775 this.showPatternModeMatches() 776 }) 777 778 // Anagram-mode element 779 this.elements.anagramLettersElement = new ConstrainedTextInput( 780 "anagram-mode-letters", 781 //(text) => text.toUpperCase(), 782 (text) => text, 783 (event) => { 784 this.logAt1("anagram-letters input: <<", event.target.value, ">>") 785 this.showAnagramModeMatches() 786 }) 787 788 this.elements.wordListDropdown = new PersistentDropdown( 789 "word-list-dropdown", 790 (event) => { 791 let url = event.target.value 792 this.logAt1("selected word list: <<", url, ">>") 793 this.setWordList(url) 794 this.showMatches() 795 }) 796 797 let minMatchLengthElement = new PersistentIntRangeInput( 798 "min-match-length", 799 this.state.defaultLowMatchLength, // default 800 this.state.minMatchLength, // min 801 this.state.maxMatchLength, // max 802 (event) => { 803 this.showMatches() 804 }, 805 ) 806 807 let maxMatchLengthElement = new PersistentIntRangeInput( 808 "max-match-length", 809 this.state.defaultHighMatchLength, // default 810 this.state.minMatchLength, // min 811 this.state.maxMatchLength, // max 812 (event) => { 813 this.showMatches() 814 }, 815 ) 816 817 // TODO: do this coupling at the class level -- IntRangeInputPair maybe -- with resetter
818 minMatchLengthElement.setPeerMax(maxMatchLengthElement) 819 maxMatchLengthElement.setPeerMin(minMatchLengthElement) 820 821 this.elements.minMatchLengthElement = minMatchLengthElement 822 this.elements.maxMatchLengthElement = maxMatchLengthElement 823 824 // Bee-mode element 825 this.elements.clearLettersButton = new Button( 826 "clear-letters-button", 827 "Clear", 828 (event) => { 829 this.logAt1("clear-letters button was clicked") 830 this.elements.requiredLettersElement.set("") 831 this.elements.otherLettersElement.set("") 832 this.showBeeModeMatches() 833 }, 834 ) 835 836 // Bee-mode element 837 this.elements.exampleButton = new Button( 838 "example-button", 839 "Example", 840 (event) => { 841 this.logAt1("example button was clicked") 842 843 this.elements.requiredLettersElement.set("E") 844 this.elements.otherLettersElement.set("XAMPLS") 845 this.elements.outputElement.set("") 846 847 this.showBeeModeMatches() 848 }, 849 ) 850 851 // Bee-mode element 852 this.elements.allOthersButton = new Button( 853 "all-others-button", 854 "A-Z", 855 (event) => { 856 this.logAt1("A-Z button was clicked") 857 858 this.elements.requiredLettersElement.set("") 859 this.elements.otherLettersElement.set("ABCDEFGHIJKLMNOPQRSTUVWXYZ") 860 this.elements.outputElement.set("") 861 862 this.showBeeModeMatches() 863 }, 864 ) 865 866 // Pattern-mode element 867 this.elements.patternClearLettersButton = new Button( 868 "pattern-clear-letters-button", 869 "Clear", 870 (event) => { 871 this.logAt1("pattern-clear-letters button was clicked") 872 this.elements.patternLettersElement.set("") 873 this.showPatternModeMatches() 874 }, 875 ) 876 877 // Anagram-mode element 878 this.elements.anagramClearLettersButton = new Button( 879 "anagram-clear-letters-button", 880 "Clear", 881 (event) => { 882 this.logAt1("anagram-clear-letters button was clicked") 883 this.elements.anagramLettersElement.set("") 884 this.showAnagramModeMatches() 885 }, 886 ) 887 888 // Random-word-mode element 889 this.elements.randomWordGenerateButton = new Button( 890 "random-word-generate-button", 891 "Generate", 892 (event) => { 893 this.logAt1("random-word-generate button was clicked") 894 this.generateRandomWords() 895 }, 896 ) 897 898 // Random-word-mode element 899 this.elements.randomWordCountInput = new PersistentIntRangeInput( 900 "random-word-count", 901 this.state.defaultRandomWordCount, // default 902 this.state.minRandomWordCount, // min 903 this.state.maxRandomWordCount, // max 904 (event) => { 905 this.generateRandomWords() 906 }, 907 ) 908 909 // ngram-mode element 910 this.elements.ngramGenerateButton = new Button( 911 "ngram-generate-button", 912 "Generate", 913 (event) => { 914 this.logAt0("ngram-generate button was clicked") 915 // Do not recompute the NGramsKeeper -- re-use it 916 this.generateNGramWords() 917 }, 918 ) 919 920 // ngram-mode element 921 this.elements.ngramWordCountInput = new PersistentIntRangeInput( 922 "ngram-word-count-input", 923 this.state.defaultNGramWordCount, // default 924 this.state.minNGramWordCount, // min 925 this.state.maxNGramWordCount, // max 926 (event) => { 927 this.logAt0("ngram-word-count input was selected") 928 this.generateNGramWords() 929 }, 930 ) 931 932 // ngram-mode element 933 console 934 this.elements.ngramNInput = new PersistentIntRangeInput( 935 "ngram-n-input", 936 this.state.defaultNGramN, // default 937 this.state.minNGramN, // min 938 this.state.maxNGramN, // max 939 (event) => { 940 this.logAt0("ngram-n input was selected") 941 // Recompute the NGramsKeeper 942 this.state.ngramsKeeper = null 943 this.generateNGramWords() 944 }, 945 ) 946 947 this.elements.resetMatchLengthsButton = new Button( 948 "reset-match-lengths-button", 949 "Reset", 950 (event) => { 951 this.logAt1("reset-match-lengths button was clicked") 952 this.elements.minMatchLengthElement.resetToDefault(), 953 this.elements.maxMatchLengthElement.resetToDefault(), 954 this.showMatches() 955 }, 956 ) 957 958 this.outputShowHideButton = new OneButtonSwitcher( 959 "output-show-hide-button", 960 [this.elements.outputElement], 961 [this.elements.outputCountElement], 962 "Hide results", 963 "Show results", 964 () => {}, 965 ) 966 967 this.elements.matchLengthsRow = new GenericElement("match-lengths-row") 968 969 this.elements.modeButtons = new URLAndPersistentNButtonSwitcher( { 970 971 "random-word-mode-button": { 972 "text": "Random", 973 "items": [ 974 new GenericElement("random-word-mode-row-1"), 975 new GenericElement("random-word-mode-help-launcher-button"), 976 ], 977 "callback": () => { 978 if (this.state.mode == "random-word-mode") { 979 return 980 } 981 this.state.mode = "random-word-mode"; 982 983 this.state.ngramsKeeper = null 984 this.elements.matchLengthsRow.makeInvisible() 985 986 // The tab button being clicked gets the focus. 987 // So we have to delay. 988 // this.elements.randomWordGenerateButton.focus() 989 setTimeout( 990 () => { 991 this.elements.randomWordGenerateButton.focus() 992 }, 993 200, 994 ) 995 996 this.generateRandomWords() 997 }, 998 "urlShorthand": "random", 999 }, 1000 1001 "ngram-mode-button": { 1002 "text": "Jabber", 1003 "items": [ 1004 new GenericElement("ngram-mode-row-1"), 1005 new GenericElement("ngram-mode-row-2"), 1006 new GenericElement("ngram-mode-help-launcher-button"), 1007 ], 1008 "callback": () => { 1009 if (this.state.mode == "ngram-mode") { 1010 return 1011 } 1012 this.state.mode = "ngram-mode"; 1013 1014 this.logAt0("swtich to ngram mode") 1015 this.elements.matchLengthsRow.makeInvisible() 1016 1017 // The tab button being clicked gets the focus. 1018 // So we have to delay. 1019 // this.elements.ngramGenerateButton.focus() 1020 setTimeout( 1021 () => { 1022 this.elements.ngramGenerateButton.focus() 1023 }, 1024 200, 1025 ) 1026 1027 this.state.ngramsKeeper = null 1028 this.generateNGramWords() 1029 }, 1030 "urlShorthand": "jabber", 1031 }, 1032 1033 "pattern-mode-button": { 1034 "text": "Pattern", 1035 "items": [ 1036 new GenericElement("pattern-mode-row-1"), 1037 new GenericElement("pattern-mode-help-launcher-button"), 1038 ], 1039 "callback": () => { 1040 if (this.state.mode == "pattern-mode") { 1041 return 1042 } 1043 this.state.mode = "pattern-mode"; 1044 1045 this.state.ngramsKeeper = null 1046 this.elements.matchLengthsRow.makeVisible() 1047 1048 // The tab button being clicked gets the focus. 1049 // So we have to delay. 1050 // this.elements.patternLettersElement.focus() 1051 setTimeout( 1052 () => { 1053 this.elements.patternLettersElement.focus() 1054 }, 1055 200, 1056 ) 1057 1058 this.showMatches() 1059 }, 1060 "urlShorthand": "pattern", 1061 }, 1062 1063 "anagram-mode-button": { 1064 "text": "Anagram", 1065 "items": [ 1066 new GenericElement("anagram-mode-row-1"), 1067 new GenericElement("anagram-mode-help-launcher-button"), 1068 ], 1069 "callback": () => { 1070 if (this.state.mode == "anagram-mode") { 1071 return 1072 } 1073 this.state.mode = "anagram-mode"; 1074 1075 this.state.ngramsKeeper = null 1076 this.elements.matchLengthsRow.makeInvisible() 1077 1078 // The tab button being clicked gets the focus. 1079 // So we have to delay. 1080 // this.elements.anagramLettersElement.focus() 1081 setTimeout( 1082 () => { 1083 this.elements.anagramLettersElement.focus() 1084 }, 1085 200, 1086 ) 1087 1088 this.showMatches() 1089 }, 1090 "urlShorthand": "anagram", 1091 }, 1092 1093 "bee-mode-button": { 1094 "text": "Bee", 1095 "items": [ 1096 new GenericElement("bee-mode-row-1"), 1097 new GenericElement("bee-mode-row-2"), 1098 new GenericElement("bee-mode-row-3"), 1099 new GenericElement("bee-mode-help-launcher-button"), 1100 ], 1101 "callback": () => { 1102 if (this.state.mode == "bee-mode") { 1103 return 1104 } 1105 this.state.mode = "bee-mode"; 1106 1107 this.state.ngramsKeeper = null 1108 this.elements.matchLengthsRow.makeVisible() 1109 1110 // The tab button being clicked gets the focus. 1111 // So we have to delay. 1112 // this.elements.requiredLettersElement.focus() 1113 setTimeout( 1114 () => { 1115 this.elements.requiredLettersElement.focus() 1116 }, 1117 200, 1118 ) 1119 1120 this.showMatches() 1121 }, 1122 "urlShorthand": "bee", 1123 }, 1124 1125 }, 1126 "selected", // CSS style class 1127 "deselected", // CSS style class 1128 ) 1129 1130 1131 // ---------------------------------------------------------------- 1132 this.elements.help = {} 1133 1134 this.elements.help.random = new HelpWidgets( 1135 "random-word-mode-help-background", 1136 "random-word-mode-help-content", 1137 "random-word-mode-help-launcher-button", 1138 "random-word-mode-help-close-button", 1139 ) 1140 1141 this.elements.help.ngram = new HelpWidgets( 1142 "ngram-mode-help-background", 1143 "ngram-mode-help-content", 1144 "ngram-mode-help-launcher-button", 1145 "ngram-mode-help-close-button", 1146 ) 1147 1148 this.elements.help.pattern = new HelpWidgets( 1149 "pattern-mode-help-background", 1150 "pattern-mode-help-content", 1151 "pattern-mode-help-launcher-button", 1152 "pattern-mode-help-close-button", 1153 ) 1154 1155 this.elements.help.anagram = new HelpWidgets( 1156 "anagram-mode-help-background", 1157 "anagram-mode-help-content", 1158 "anagram-mode-help-launcher-button", 1159 "anagram-mode-help-close-button", 1160 ) 1161 1162 this.elements.help.bee = new HelpWidgets( 1163 "bee-mode-help-background", 1164 "bee-mode-help-content", 1165 "bee-mode-help-launcher-button", 1166 "bee-mode-help-close-button", 1167 ) 1168 1169 // ---------------------------------------------------------------- 1170 // This is a hack to get the escape key to close the help modal on desktop. 1171 document.addEventListener('keydown', function(event) { 1172 if (event.key === 'Escape') {
1173 Object.keys(window.app.elements.help).forEach((id) => { 1174 window.app.elements.help[id].makeBackgroundInvisible() 1175 }) 1176 } 1177 }); 1178 1179 // ---------------------------------------------------------------- 1180 // This is a hack. I need to research CSS theming. 1181 this.elements.lightDarkThemeElement = new LightDarkThemeSlider( 1182 "light-theme-dark-theme-toggle", 1183 "light-theme-dark-theme-label", 1184 (event) => { 1185 document.body.style.backgroundColor = "white" 1186 document.body.style.color = "black" 1187 1188 Object.keys(this.elements.help).forEach((id) => { 1189 this.elements.help[id].setColors("black", "white") 1190 }) 1191 1192 }, 1193 (event) => { 1194 document.body.style.backgroundColor = "black" 1195 document.body.style.color = "white" 1196 1197 Object.keys(this.elements.help).forEach((id) => { 1198 this.elements.help[id].setColors("white", "black") 1199 }) 1200 1201 }, 1202 ) 1203 1204 } 1205 1206 checkWordList() { 1207 if (this.state.wordListURL == null) { 1208 this.elements.outputElement.set("No word list set") 1209 return false 1210 } 1211 if (this.state.wordList == null) { 1212 this.elements.outputElement.set('Failed to fetch word list from "' + this.state.wordListURL + '"') 1213 return false 1214 } 1215 return true 1216 } 1217 1218 setWordList(url) { 1219 this.logAt0("fetching word list from URL:", url) 1220 this.state.wordListURL = url 1221 faro.api?.pushEvent('word_list_selected', { list: url.split('/').pop().replace('.txt', '') }) 1222 fetch(url).then((response) => response.text().then((text) => { 1223 this.state.wordList = text.split("\n") 1224 this.setWordListForAnagramMode() 1225 // Must be called from this callback, since this is all async 1226 this.showMatches() 1227 })) 1228 } 1229 1230 // Requires this.wordList to have already been computed. 1231 // This precomputes sorting each word by its letter, which is a performance optimization. 1232 setWordListForAnagramMode() { 1233 this.wordListForAnagramMode = {} 1234 this.state.wordList.forEach((word) => { 1235 this.wordListForAnagramMode[word] = word.split('').sort().join('') 1236 }) 1237 } 1238 1239 showMatches() { 1240 if (this.elements.modeButtons == null) { 1241 // At app initialization 1242 return 1243 } 1244 1245 if (this.elements.modeButtons.which() == 'bee-mode-button') { 1246 this.showBeeModeMatches() 1247 } else if (this.elements.modeButtons.which() == 'pattern-mode-button') { 1248 this.showPatternModeMatches() 1249 } else if (this.elements.modeButtons.which() == 'anagram-mode-button') { 1250 this.showAnagramModeMatches() 1251 } else if (this.elements.modeButtons.which() == 'random-word-mode-button') { 1252 this.generateRandomWords() 1253 } else if (this.elements.modeButtons.which() == 'ngram-mode-button') { 1254 this.state.ngramsKeeper = null 1255 this.generateNGramWords() 1256 } 1257 } 1258 1259 showBeeModeMatches() { 1260 if (!this.checkWordList()) { 1261 return 1262 } 1263 1264 let requiredLetters = this.elements.requiredLettersElement.get() 1265 let otherLetters = this.elements.otherLettersElement.get() 1266 1267 let matches = this.getBeeMatchesFromWordList(requiredLetters, otherLetters) 1268 matches = matches.map(match => match.toUpperCase()) 1269 matches = matches.sort() 1270 this.logAt2(matches) 1271 1272 this.elements.outputCountElement.set("(" + String(matches.length) + " hidden)") 1273 1274 if (requiredLetters.length > 0 || otherLetters.length > 0) { 1275 faro.api?.pushEvent('search', { mode: 'bee', results: String(matches.length) }) 1276 } 1277 1278 if (requiredLetters.length == 0 && otherLetters.length == 0) { 1279 this.elements.outputElement.set("") 1280 } else if (matches.length == 0) { 1281 this.elements.outputElement.set("(No matches)") 1282 } else { 1283 this.elements.outputElement.set(matches.join(" ")) 1284 } 1285 } 1286 1287 getBeeMatchesFromWordList(requiredLetters, optionalLetters) { 1288 this.logAt1("matcher enter") 1289 this.logAt1("requiredLetters ", requiredLetters) 1290 this.logAt1("optionalLetters", optionalLetters) 1291 this.logAt1("wordList.length", this.state.wordList.length) 1292 this.logAt3("wordList", this.state.wordList) 1293 1294 requiredLetters = requiredLetters.toLowerCase() 1295 optionalLetters = optionalLetters.toLowerCase() 1296 let minMatchLength = this.elements.minMatchLengthElement.get() 1297 let maxMatchLength = this.elements.maxMatchLengthElement.get() 1298 let matches = [] 1299 1300 this.state.wordList.forEach((word) => { 1301 this.logAt3("word <<", word, ">>") 1302 if (this.wordBeeMatches(requiredLetters, optionalLetters, minMatchLength, maxMatchLength, word)) { 1303 this.logAt2("match", word) 1304 matches.push(word) 1305 } 1306 }) 1307 return matches 1308 } 1309 1310 wordBeeMatches(requiredLetters, optionalLetters, minMatchLength, maxMatchLength, word) { 1311 let allLetters = requiredLetters + optionalLetters 1312 1313 if (word.length < minMatchLength) { 1314 this.logAt3("-- match out 1: word", word, "with length", word.length, "less than min", minMatchLength) 1315 return false 1316 } 1317 if (word.length > maxMatchLength) { 1318 this.logAt3("-- match out 2: word", word, "with length", word.length, "more than max", maxMatchLength) 1319 return false 1320 } 1321 for (let i = 0; i < requiredLetters.length; i++) { 1322 let requiredLetter = requiredLetters[i] 1323 if (word.indexOf(requiredLetter) < 0) { 1324 this.logAt3("-- match out 3: word", word, "missing required letter", requiredLetter, "from", requiredLetters) 1325 return false 1326 } 1327 } 1328 for (let i = 0; i < word.length; i++) { 1329 let letter = word[i] 1330 if (allLetters.indexOf(letter) < 0) { 1331 this.logAt3("-- match out 4: word", word, "missing letter", letter, "from", allLetters) 1332 return false 1333 } 1334 } 1335 this.logAt2("-- match:", word) 1336 return true 1337 } 1338 1339 regexify(patternLetters) { 1340 // Regex mapping: 1341 // o A-Z -> self 1342 // o . -> . 1343 // o * -> .* 1344 // o Wrap in ^ and $ 1345 1346 this.logAt1("regex in ", patternLetters) 1347 let regexString = "^" 1348 let azre = new RegExp("^[A-Za-z]$") 1349 for (let i = 0; i < patternLetters.length; i++) { 1350 let patternLetter = patternLetters[i] 1351 this.logAt1(" reqlt", patternLetter) 1352 if (patternLetter == ".") { 1353 regexString += "." 1354 } else if (patternLetter == "*") { 1355 regexString += ".*" 1356 } else if (azre.exec(patternLetter)) { 1357 regexString += patternLetter.toLowerCase() 1358 } 1359 } 1360 regexString += "$" 1361 this.logAt1("regex out", regexString) 1362 1363 return new RegExp(regexString) 1364 } 1365 1366 showPatternModeMatches() { 1367 if (!this.checkWordList()) { 1368 return 1369 } 1370 let patternLetters = this.elements.patternLettersElement.get() 1371 1372 let matches = this.getPatternMatchesFromWordList(patternLetters) 1373 // matches = matches.map(match => match.toUpperCase()) 1374 matches = matches.sort() 1375 this.logAt2(matches) 1376 1377 this.elements.outputCountElement.set("(" + String(matches.length) + " hidden)") 1378 if (patternLetters.length > 0) { 1379 faro.api?.pushEvent('search', { mode: 'pattern', results: String(matches.length) }) 1380 } 1381 if (patternLetters.length == 0) { 1382 this.elements.outputElement.set("") 1383 } else if (matches.length == 0) { 1384 this.elements.outputElement.set("(No matches)") 1385 } else { 1386 this.elements.outputElement.set(matches.join(" ")) 1387 } 1388 } 1389 1390 getPatternMatchesFromWordList(patternLetters) { 1391 this.logAt1("matcher enter") 1392 this.logAt1("patternLetters ", patternLetters) 1393 this.logAt1("wordList.length", this.state.wordList.length) 1394 this.logAt3("wordList", this.state.wordList) 1395 1396 patternLetters = patternLetters.toLowerCase() 1397 let re = this.regexify(patternLetters) 1398 1399 let matches = [] 1400 let minMatchLength = this.elements.minMatchLengthElement.get() 1401 let maxMatchLength = this.elements.maxMatchLengthElement.get() 1402 this.state.wordList.forEach((word) => { 1403 if (word.length < minMatchLength) { 1404 } else if (word.length > maxMatchLength) { 1405 } else if (re.exec(word)) { 1406 matches.push(word) 1407 } 1408 }) 1409 return matches 1410 } 1411 1412 showAnagramModeMatches() { 1413 if (!this.checkWordList()) { 1414 return 1415 } 1416 let anagramLetters = this.elements.anagramLettersElement.get() 1417 1418 let matches = this.getAnagramMatchesFromWordList(anagramLetters) 1419 //matches = matches.map(match => match.toUpperCase()) 1420 matches = matches.sort() 1421 this.logAt2(matches) 1422 1423 this.elements.outputCountElement.set("(" + String(matches.length) + " hidden)") 1424 if (anagramLetters.length > 0) { 1425 faro.api?.pushEvent('search', { mode: 'anagram', results: String(matches.length) }) 1426 } 1427 if (anagramLetters.length == 0) { 1428 this.elements.outputElement.set("") 1429 } else if (matches.length == 0) { 1430 this.elements.outputElement.set("(No matches)") 1431 } else { 1432 this.elements.outputElement.set(matches.join(" ")) 1433 } 1434 } 1435 1436 getAnagramMatchesFromWordList(anagramLetters) { 1437 this.logAt1("matcher enter") 1438 this.logAt1("anagramLetters ", anagramLetters) 1439 this.logAt1("wordList.length", this.state.wordList.length) 1440 this.logAt3("wordList", this.state.wordList) 1441 1442 let sortedInput = anagramLetters.toLowerCase().split('').sort().join(''); 1443 1444 let matches = [] 1445 for (const [word, sortedWord] of Object.entries(this.wordListForAnagramMode)) { 1446 this.logAt3("-- matcher check", word, " vs ", anagramLetters) 1447 if (sortedWord == sortedInput) { 1448 matches.push(word) 1449 } 1450 } 1451 return matches 1452 } 1453 1454 generateRandomWords() { 1455 if (!this.checkWordList()) { 1456 return 1457 } 1458 1459 let matches = [] 1460 // TODO: sample without replacement. 1461 // In practice, dups are super-rare. 1462 let k = this.elements.randomWordCountInput.get() 1463 let n = this.state.wordList.length 1464 for (let i = 0; i < k; i++) { 1465 let randomIndex = Math.floor(Math.random() * n); 1466 let word = this.state.wordList[randomIndex] 1467 matches.push(word) 1468 } 1469 1470 this.elements.outputCountElement.set("(" + String(matches.length) + " hidden)") 1471 this.elements.outputElement.set(matches.join(" ")) 1472 faro.api?.pushEvent('generate', { mode: 'random' }) 1473 } 1474 1475 setUpNGramsKeeper() { 1476 if (this.state.ngramsKeeper != null) { 1477 return true 1478 } 1479 1480 if (!this.checkWordList()) { 1481 // Initialization/constructor sequencing 1482 return false 1483 } 1484 1485 let n = this.elements.ngramNInput.get() 1486 this.state.ngramsKeeper = new NGramsKeeper( 1487 this.state.wordList, 1488 n, 1489 ) 1490 1491 return true 1492 } 1493 1494 generateNGramWords() { 1495 if (!this.setUpNGramsKeeper()) { 1496 return 1497 } 1498 1499 let outputs = [] 1500 let m = this.elements.ngramWordCountInput.get() 1501 for (let i = 0; i < m; i++) { 1502 let word = this.state.ngramsKeeper.emit_word() 1503 if (word != null) { 1504 outputs.push(word) 1505 } else { 1506 outputs.push('(failed to generate)') 1507 } 1508 } 1509 1510 this.elements.outputCountElement.set("(" + String(outputs.length) + " hidden)") 1511 this.elements.outputElement.set(outputs.join(" ")) 1512 faro.api?.pushEvent('generate', { mode: 'ngram' }) 1513 } 1514 1515 // ---------------------------------------------------------------- 1516 // LOGGING METHODS 1517 1518 logAt0(...args) { 1519 this.logAt(0, ...args) 1520 } 1521 1522 logAt1(...args) { 1523 this.logAt(1, ...args) 1524 } 1525 1526 logAt2(...args) { 1527 this.logAt(2, ...args) 1528 } 1529 1530 logAt3(...args) { 1531 this.logAt(3, ...args) 1532 } 1533 1534 logAt(level, ...args) { 1535 if (this.state.logLevel >= level) { 1536 console.log(...args) 1537 } 1538 } 1539 1540 } 1541 1542 // ---------------------------------------------------------------- 1543 const app = new WordFinderApp() 1544 // Expose from module visibility to top-level visibility for Console use 1545 window.app = app 1546 1547</script>
1547 1548<!-- ================================================================ --> 1549</body> 1550</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.