├── .babelrc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .nvmrc ├── .releaserc.json ├── LICENSE ├── README.markdown ├── docs ├── buttons.css ├── device.js ├── index.html └── spin.min.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── index.js └── index.test.js └── vendor └── words.txt /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [18.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | 25 | - run: npm ci 26 | 27 | - run: npm run test:coverage 28 | 29 | - name: Upload coverage to Codecov 30 | uses: codecov/codecov-action@v2 31 | with: 32 | token: ${{ secrets.CODECOV_TOKEN }} 33 | files: ./coverage/lcov.info 34 | fail_ci_if_error: true 35 | 36 | - name: Semantic Release 37 | uses: cycjimmy/semantic-release-action@v3 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .env -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.12.1 2 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["main"], 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | "@semantic-release/npm", 7 | "@semantic-release/git" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-Present Matthew Hudson 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # [WORDS](https://hudson.dev/words/) 2 | 3 | > The Javascript module `words` generates a list of words from a word, or a few letters. It's helpful for providing hints while playing games like Letterpress, Words with Friends, and Scrabble. 4 | 5 | [![codecov](https://codecov.io/github/matthewhudson/words/branch/main/graph/badge.svg?token=oxazfuInJ9)](https://codecov.io/github/matthewhudson/words) 6 | 7 | ### Web Application Demo 8 | 9 | 10 | 11 | ## Installation 12 | 13 | ```bash 14 | npm [-g] install words 15 | ``` 16 | 17 | ## Usage 18 | 19 | A demo version is available on online: 20 | 21 | ```bash 22 | curl https://httpip.es/api/words?letters=h,e,l,l,o 23 | ``` 24 | 25 | ## Suggestions 26 | 27 | All comments in how to improve this library are very welcome. Feel free post suggestions to the Issue tracker, or even better, fork the repository to implement your own ideas and submit a pull request. 28 | 29 | ## License 30 | 31 | Unless attributed otherwise, everything is under the MIT License (see LICENSE for more info). 32 | -------------------------------------------------------------------------------- /docs/buttons.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | display: inline-block; 3 | *display: inline; 4 | padding: 4px 14px; 5 | margin-bottom: 0; 6 | *margin-left: .3em; 7 | font-size: 14px; 8 | line-height: 20px; 9 | *line-height: 20px; 10 | color: #333333; 11 | text-align: center; 12 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 13 | vertical-align: middle; 14 | cursor: pointer; 15 | background-color: #f5f5f5; 16 | *background-color: #e6e6e6; 17 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); 18 | background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); 19 | background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); 20 | background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); 21 | background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); 22 | background-repeat: repeat-x; 23 | border: 1px solid #bbbbbb; 24 | *border: 0; 25 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 26 | border-color: #e6e6e6 #e6e6e6 #bfbfbf; 27 | border-bottom-color: #a2a2a2; 28 | -webkit-border-radius: 4px; 29 | -moz-border-radius: 4px; 30 | border-radius: 4px; 31 | filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0); 32 | filter: progid:dximagetransform.microsoft.gradient(enabled=false); 33 | *zoom: 1; 34 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 35 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 36 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 37 | } 38 | 39 | .btn:hover, 40 | .btn:active, 41 | .btn.active, 42 | .btn.disabled, 43 | .btn[disabled] { 44 | color: #333333; 45 | background-color: #e6e6e6; 46 | *background-color: #d9d9d9; 47 | } 48 | 49 | .btn:active, 50 | .btn.active { 51 | background-color: #cccccc \9; 52 | } 53 | 54 | .btn:first-child { 55 | *margin-left: 0; 56 | } 57 | 58 | .btn:hover { 59 | color: #333333; 60 | text-decoration: none; 61 | background-color: #e6e6e6; 62 | *background-color: #d9d9d9; 63 | /* Buttons in IE7 don't get borders, so darken on hover */ 64 | 65 | background-position: 0 -15px; 66 | -webkit-transition: background-position 0.1s linear; 67 | -moz-transition: background-position 0.1s linear; 68 | -o-transition: background-position 0.1s linear; 69 | transition: background-position 0.1s linear; 70 | } 71 | 72 | .btn:focus { 73 | outline: thin dotted #333; 74 | outline: 5px auto -webkit-focus-ring-color; 75 | outline-offset: -2px; 76 | } 77 | 78 | .btn.active, 79 | .btn:active { 80 | background-color: #e6e6e6; 81 | background-color: #d9d9d9 \9; 82 | background-image: none; 83 | outline: 0; 84 | -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); 85 | -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); 86 | box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); 87 | } 88 | 89 | .btn.disabled, 90 | .btn[disabled] { 91 | cursor: default; 92 | background-color: #e6e6e6; 93 | background-image: none; 94 | opacity: 0.65; 95 | filter: alpha(opacity=65); 96 | -webkit-box-shadow: none; 97 | -moz-box-shadow: none; 98 | box-shadow: none; 99 | } 100 | 101 | .btn-large { 102 | padding: 9px 14px; 103 | font-size: 16px; 104 | line-height: normal; 105 | -webkit-border-radius: 5px; 106 | -moz-border-radius: 5px; 107 | border-radius: 5px; 108 | } 109 | 110 | .btn-large [class^="icon-"] { 111 | margin-top: 2px; 112 | } 113 | 114 | .btn-small { 115 | padding: 3px 9px; 116 | font-size: 12px; 117 | line-height: 18px; 118 | } 119 | 120 | .btn-small [class^="icon-"] { 121 | margin-top: 0; 122 | } 123 | 124 | .btn-mini { 125 | padding: 2px 6px; 126 | font-size: 11px; 127 | line-height: 17px; 128 | } 129 | 130 | .btn-block { 131 | display: block; 132 | width: 100%; 133 | padding-right: 0; 134 | padding-left: 0; 135 | -webkit-box-sizing: border-box; 136 | -moz-box-sizing: border-box; 137 | box-sizing: border-box; 138 | } 139 | 140 | .btn-block + .btn-block { 141 | margin-top: 5px; 142 | } 143 | 144 | input[type="submit"].btn-block, 145 | input[type="reset"].btn-block, 146 | input[type="button"].btn-block { 147 | width: 100%; 148 | } 149 | 150 | .btn-primary.active, 151 | .btn-warning.active, 152 | .btn-danger.active, 153 | .btn-success.active, 154 | .btn-info.active, 155 | .btn-inverse.active { 156 | color: rgba(255, 255, 255, 0.75); 157 | } 158 | 159 | .btn { 160 | border-color: #c5c5c5; 161 | border-color: rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.25); 162 | } 163 | 164 | .btn-primary { 165 | color: #ffffff; 166 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 167 | background-color: #006dcc; 168 | *background-color: #0044cc; 169 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); 170 | background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); 171 | background-image: -o-linear-gradient(top, #0088cc, #0044cc); 172 | background-image: linear-gradient(to bottom, #0088cc, #0044cc); 173 | background-image: -moz-linear-gradient(top, #0088cc, #0044cc); 174 | background-repeat: repeat-x; 175 | border-color: #0044cc #0044cc #002a80; 176 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 177 | filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0); 178 | filter: progid:dximagetransform.microsoft.gradient(enabled=false); 179 | } 180 | 181 | .btn-primary:hover, 182 | .btn-primary:active, 183 | .btn-primary.active, 184 | .btn-primary.disabled, 185 | .btn-primary[disabled] { 186 | color: #ffffff; 187 | background-color: #0044cc; 188 | *background-color: #003bb3; 189 | } 190 | 191 | .btn-primary:active, 192 | .btn-primary.active { 193 | background-color: #003399 \9; 194 | } 195 | 196 | .btn-warning { 197 | color: #ffffff; 198 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 199 | background-color: #faa732; 200 | *background-color: #f89406; 201 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); 202 | background-image: -webkit-linear-gradient(top, #fbb450, #f89406); 203 | background-image: -o-linear-gradient(top, #fbb450, #f89406); 204 | background-image: linear-gradient(to bottom, #fbb450, #f89406); 205 | background-image: -moz-linear-gradient(top, #fbb450, #f89406); 206 | background-repeat: repeat-x; 207 | border-color: #f89406 #f89406 #ad6704; 208 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 209 | filter: progid:dximagetransform.microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0); 210 | filter: progid:dximagetransform.microsoft.gradient(enabled=false); 211 | } 212 | 213 | .btn-warning:hover, 214 | .btn-warning:active, 215 | .btn-warning.active, 216 | .btn-warning.disabled, 217 | .btn-warning[disabled] { 218 | color: #ffffff; 219 | background-color: #f89406; 220 | *background-color: #df8505; 221 | } 222 | 223 | .btn-warning:active, 224 | .btn-warning.active { 225 | background-color: #c67605 \9; 226 | } 227 | 228 | .btn-danger { 229 | color: #ffffff; 230 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 231 | background-color: #da4f49; 232 | *background-color: #bd362f; 233 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); 234 | background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); 235 | background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); 236 | background-image: linear-gradient(to bottom, #ee5f5b, #bd362f); 237 | background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); 238 | background-repeat: repeat-x; 239 | border-color: #bd362f #bd362f #802420; 240 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 241 | filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0); 242 | filter: progid:dximagetransform.microsoft.gradient(enabled=false); 243 | } 244 | 245 | .btn-danger:hover, 246 | .btn-danger:active, 247 | .btn-danger.active, 248 | .btn-danger.disabled, 249 | .btn-danger[disabled] { 250 | color: #ffffff; 251 | background-color: #bd362f; 252 | *background-color: #a9302a; 253 | } 254 | 255 | .btn-danger:active, 256 | .btn-danger.active { 257 | background-color: #942a25 \9; 258 | } 259 | 260 | .btn-success { 261 | color: #ffffff; 262 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 263 | background-color: #5bb75b; 264 | *background-color: #51a351; 265 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); 266 | background-image: -webkit-linear-gradient(top, #62c462, #51a351); 267 | background-image: -o-linear-gradient(top, #62c462, #51a351); 268 | background-image: linear-gradient(to bottom, #62c462, #51a351); 269 | background-image: -moz-linear-gradient(top, #62c462, #51a351); 270 | background-repeat: repeat-x; 271 | border-color: #51a351 #51a351 #387038; 272 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 273 | filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0); 274 | filter: progid:dximagetransform.microsoft.gradient(enabled=false); 275 | } 276 | 277 | .btn-success:hover, 278 | .btn-success:active, 279 | .btn-success.active, 280 | .btn-success.disabled, 281 | .btn-success[disabled] { 282 | color: #ffffff; 283 | background-color: #51a351; 284 | *background-color: #499249; 285 | } 286 | 287 | .btn-success:active, 288 | .btn-success.active { 289 | background-color: #408140 \9; 290 | } 291 | 292 | .btn-info { 293 | color: #ffffff; 294 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 295 | background-color: #49afcd; 296 | *background-color: #2f96b4; 297 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); 298 | background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); 299 | background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); 300 | background-image: linear-gradient(to bottom, #5bc0de, #2f96b4); 301 | background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); 302 | background-repeat: repeat-x; 303 | border-color: #2f96b4 #2f96b4 #1f6377; 304 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 305 | filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0); 306 | filter: progid:dximagetransform.microsoft.gradient(enabled=false); 307 | } 308 | 309 | .btn-info:hover, 310 | .btn-info:active, 311 | .btn-info.active, 312 | .btn-info.disabled, 313 | .btn-info[disabled] { 314 | color: #ffffff; 315 | background-color: #2f96b4; 316 | *background-color: #2a85a0; 317 | } 318 | 319 | .btn-info:active, 320 | .btn-info.active { 321 | background-color: #24748c \9; 322 | } 323 | 324 | .btn-inverse { 325 | color: #ffffff; 326 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 327 | background-color: #363636; 328 | *background-color: #222222; 329 | background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222)); 330 | background-image: -webkit-linear-gradient(top, #444444, #222222); 331 | background-image: -o-linear-gradient(top, #444444, #222222); 332 | background-image: linear-gradient(to bottom, #444444, #222222); 333 | background-image: -moz-linear-gradient(top, #444444, #222222); 334 | background-repeat: repeat-x; 335 | border-color: #222222 #222222 #000000; 336 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 337 | filter: progid:dximagetransform.microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0); 338 | filter: progid:dximagetransform.microsoft.gradient(enabled=false); 339 | } 340 | 341 | .btn-inverse:hover, 342 | .btn-inverse:active, 343 | .btn-inverse.active, 344 | .btn-inverse.disabled, 345 | .btn-inverse[disabled] { 346 | color: #ffffff; 347 | background-color: #222222; 348 | *background-color: #151515; 349 | } 350 | 351 | .btn-inverse:active, 352 | .btn-inverse.active { 353 | background-color: #080808 \9; 354 | } -------------------------------------------------------------------------------- /docs/device.js: -------------------------------------------------------------------------------- 1 | // Device.js 2 | // (c) 2012 Matthew Hudson 3 | // Webpipe.js is freely distributable under the MIT license. 4 | // For all details and documentation: 5 | // http://www.matthewghudson.com/projects/device.js/ 6 | (function () { 7 | 8 | // Baseline setup 9 | // -------------- 10 | 11 | // Establish the root object, 'window' in the browser, 12 | // or 'global' on the server. 13 | var root = this; 14 | 15 | // Create a reference to the device object for use below. 16 | var device = {}; 17 | 18 | // The element. 19 | var docElement = window.document.documentElement; 20 | 21 | // The client UserAgent string. 22 | var userAgent = window.navigator.userAgent.toLowerCase(); 23 | 24 | // Export the Webpipe object for **Node.js**, with 25 | // backwards-compatibility for the old 'require()' API. If we're in 26 | // the browser, add 'device' as a global object via a string identifier, 27 | // for Closure Compiler "advanced" mode. 28 | if (typeof exports !== 'undefined') { 29 | if (typeof module !== 'undefined' && module.exports) { 30 | exports = module.exports = device; 31 | } 32 | exports.device = device; 33 | } else { 34 | root['device'] = device; 35 | } 36 | 37 | // Main functions 38 | // -------------- 39 | 40 | device.ios = function () { 41 | return (device.iphone() || device.ipod() || device.ipad()); 42 | }; 43 | 44 | device.iphone = function () { 45 | return userAgent.match(/iphone/i) ? true : false; 46 | }; 47 | 48 | device.ipod = function () { 49 | return userAgent.match(/ipod/i) ? true : false; 50 | }; 51 | 52 | device.ipad = function () { 53 | return userAgent.match(/ipad/i) ? true : false; 54 | }; 55 | 56 | device.android = function () { 57 | return userAgent.match(/android/i) ? true : false; 58 | }; 59 | 60 | device.androidPhone = function () { 61 | return (device.android() && userAgent.match(/mobile/i)) ? true : false; 62 | }; 63 | 64 | // See: http://android-developers.blogspot.com/2010/12/android-browser-user-agent-issues.html 65 | device.androidTablet = function () { 66 | return (device.android() && !userAgent.match(/mobile/i)) ? true : false; 67 | }; 68 | 69 | device.blackberry = function () { 70 | return userAgent.match(/blackberry/i) ? true : false; 71 | }; 72 | 73 | device.blackberryPhone = function () { 74 | return (device.blackberry() && !userAgent.match(/tablet/i)) ? true : false; 75 | }; 76 | 77 | // See: http://supportforums.blackberry.com/t5/Web-and-WebWorks-Development/How-to-detect-the-BlackBerry-Browser/ta-p/559862 78 | device.blackberryTablet = function () { 79 | return userAgent.match(/rim tablet/i) ? true : false; 80 | }; 81 | 82 | device.windowsPhone = function () { 83 | return userAgent.match(/windows phone/i) ? true : false; 84 | }; 85 | 86 | device.mobile = function () { 87 | return (device.androidPhone() || device.iphone() || device.ipod() || device.windowsPhone() || device.blackberryPhone()); 88 | }; 89 | 90 | device.tablet = function () { 91 | return (device.ipad() || device.androidTablet() || device.blackberryTablet()); 92 | }; 93 | 94 | device.portrait = function () { 95 | return Math.abs(window.orientation) == 90 ? false : true; 96 | }; 97 | 98 | device.landscape = function () { 99 | return Math.abs(window.orientation) == 90 ? true : false; 100 | }; 101 | 102 | // Private Utility 103 | // --------------- 104 | 105 | // If #debug selector exists, insert debug information. 106 | var debug = function () { 107 | var debugElement = window.document.getElementById("debug"); 108 | 109 | if (debugElement) { 110 | debugElement.innerHTML = 111 | "

DEBUG

" 112 | + "

UA String: " + userAgent + "

" 113 | + "

Dimensions: " + window.innerWidth + 'x' + window.innerHeight + "

" 114 | + "

Orientation: " + window.orientation + "

" 115 | + "

CSS Classes: " + docElement.className + "

"; 116 | } 117 | } 118 | 119 | // Check if docElement already has a given class. 120 | var hasClass = function (className) { 121 | var regex = new RegExp(className, "i"); 122 | return docElement.className.match(regex); 123 | }; 124 | 125 | // Add one or more CSS classes to the element. 126 | var addClass = function (className) { 127 | if (!hasClass(className)) { 128 | docElement.className += " " + className; 129 | } 130 | }; 131 | 132 | // Remove single CSS class from the element. 133 | var removeClass = function (className) { 134 | if (hasClass(className)) { 135 | docElement.className = docElement.className.replace(className, ""); 136 | } 137 | }; 138 | 139 | // HTML Element Handling 140 | // --------------------- 141 | 142 | // Insert the appropriate CSS class based on the UserAgent. 143 | if (device.ios()) { 144 | if (device.ipad()) { 145 | addClass("ios ipad tablet"); 146 | } else if (device.iphone()) { 147 | addClass("ios iphone mobile"); 148 | } else if (device.ipod()) { 149 | addClass("ios ipod mobile"); 150 | } 151 | } else if (device.android()) { 152 | if (device.androidTablet()) { 153 | addClass("android tablet"); 154 | } else { 155 | addClass("android mobile"); 156 | } 157 | } else if (device.blackberry()) { 158 | if (device.blackberryTablet()) { 159 | addClass("blackberry tablet"); 160 | } else { 161 | addClass("blackberry mobile"); 162 | } 163 | } else if (device.windowsPhone()) { 164 | addClass("windows mobile"); 165 | } else { 166 | addClass("desktop"); 167 | } 168 | 169 | // Orientation Handling 170 | // -------------------- 171 | 172 | // Handle device orientation changes 173 | var checkOrientation = function () { 174 | if (device.landscape()) { 175 | removeClass("portrait"); 176 | addClass("landscape"); 177 | } else { 178 | removeClass("landscape"); 179 | addClass("portrait"); 180 | } 181 | debug(); 182 | } 183 | 184 | // Detect whether device supports orientationchange event, 185 | // otherwise fall back to the resize event. 186 | var supportsOrientationChange = "onorientationchange" in window, 187 | orientationEvent = supportsOrientationChange ? "orientationchange" : "resize"; 188 | 189 | // Listen for changes in orientation. 190 | window.addEventListener(orientationEvent, checkOrientation, false); 191 | 192 | checkOrientation(); 193 | 194 | }).call(this); -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Matthew Hudson / Word Generator - Cheat when playing games like word games and apps like Letterpress, Words with Friends, and Scrabble. 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 220 | 221 | 222 |
223 |
224 |
225 |
226 |
227 | 228 |
229 |
230 |
231 |
232 |
233 |
234 | 235 |
236 |
237 |
238 |
239 |
240 | 242 |
    243 |
    244 | 245 |

    Type letters then submit to generate possible words.

    246 | 247 | 261 |

    262 |
    263 |
    264 |
    265 | 266 | 271 |
    272 | 273 | 274 | 275 | 276 | 504 | 505 | 506 | 507 | -------------------------------------------------------------------------------- /docs/spin.min.js: -------------------------------------------------------------------------------- 1 | //fgnass.github.com/spin.js#v1.2.7 2 | !function(e,t,n){function o(e,n){var r=t.createElement(e||"div"),i;for(i in n)r[i]=n[i];return r}function u(e){for(var t=1,n=arguments.length;t>1):parseInt(n.left,10)+i)+"px",top:(n.top=="auto"?a.y-u.y+(e.offsetHeight>>1):parseInt(n.top,10)+i)+"px"})),r.setAttribute("aria-role","progressbar"),t.lines(r,t.opts);if(!s){var f=0,l=n.fps,h=l/n.speed,d=(1-n.opacity)/(h*n.trail/100),v=h/n.lines;(function m(){f++;for(var e=n.lines;e;e--){var i=Math.max(1-(f+e*v)%h*d,n.opacity);t.opacity(r,n.lines-e,i,n)}t.timeout=t.el&&setTimeout(m,~~(1e3/l))})()}return t},stop:function(){var e=this.el;return e&&(clearTimeout(this.timeout),e.parentNode&&e.parentNode.removeChild(e),this.el=n),this},lines:function(e,t){function i(e,r){return c(o(),{position:"absolute",width:t.length+t.width+"px",height:t.width+"px",background:e,boxShadow:r,transformOrigin:"left",transform:"rotate("+~~(360/t.lines*n+t.rotate)+"deg) translate("+t.radius+"px"+",0)",borderRadius:(t.corners*t.width>>1)+"px"})}var n=0,r;for(;n',t)}var t=c(o("group"),{behavior:"url(#default#VML)"});!l(t,"transform")&&t.adj?(a.addRule(".spin-vml","behavior:url(#default#VML)"),v.prototype.lines=function(t,n){function s(){return c(e("group",{coordsize:i+" "+i,coordorigin:-r+" "+ -r}),{width:i,height:i})}function l(t,i,o){u(a,u(c(s(),{rotation:360/n.lines*t+"deg",left:~~i}),u(c(e("roundrect",{arcsize:n.corners}),{width:r,height:n.width,left:n.radius,top:-n.width>>1,filter:o}),e("fill",{color:n.color,opacity:n.opacity}),e("stroke",{opacity:0}))))}var r=n.length+n.width,i=2*r,o=-(n.width+n.length)*2+"px",a=c(s(),{position:"absolute",top:o,left:o}),f;if(n.shadow)for(f=1;f<=n.lines;f++)l(f,-2,"progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)");for(f=1;f<=n.lines;f++)l(f);return u(t,a)},v.prototype.opacity=function(e,t,n,r){var i=e.firstChild;r=r.shadow&&r.lines||0,i&&t+r", 15 | "bugs": "https://github.com/matthewhudson/words/issues", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/matthewhudson/words.git" 19 | }, 20 | "main": "index", 21 | "license": "MIT", 22 | "scripts": { 23 | "lint": "standard", 24 | "lint:fix": "standard --fix", 25 | "test": "jest", 26 | "test:coverage": "jest --coverage && codecov", 27 | "semantic-release": "semantic-release" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "7.21.3", 31 | "@babel/preset-env": "7.20.2", 32 | "@jest/transform": "29.5.0", 33 | "@semantic-release/commit-analyzer": "9.0.2", 34 | "@semantic-release/git": "10.0.1", 35 | "@semantic-release/npm": "10.0.2", 36 | "@semantic-release/release-notes-generator": "10.0.3", 37 | "babel-jest": "29.5.0", 38 | "codecov": "3.8.3", 39 | "cz-conventional-changelog": "3.3.0", 40 | "jest": "29.5.0", 41 | "rollup": "3.20.1", 42 | "semantic-release": "20.1.3", 43 | "standard": "17.0.0" 44 | }, 45 | "config": { 46 | "commitizen": { 47 | "path": "./node_modules/cz-conventional-changelog" 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import readline from 'readline' 4 | 5 | /** 6 | * A class for generating anagrams of words from comma-separated letters. 7 | * @example 8 | * // Create a new instance of WordGenerator 9 | * const wordGenerator = new WordGenerator(); 10 | * // Generate the anagram tree 11 | * await wordGenerator.generateTree(); 12 | * // Get the anagrams for a set of letters 13 | * const lettersArray = ['t', 'e', 's', 't']; 14 | * const anagrams = wordGenerator.getAnagrams(lettersArray); 15 | * console.log(anagrams); 16 | */ 17 | class WordGenerator { 18 | /** 19 | * Create a new instance of WordGenerator. 20 | */ 21 | constructor () { 22 | this.tree = {} 23 | this.alphabet = 'jqxzwkvfybhgmpudclotnraise' 24 | } 25 | 26 | /** 27 | * Reads words file and applies a callback on each line 28 | * @param {function} callback - Function to be called on each line 29 | * @param {function} finished - Function to be called when file reading is finished 30 | */ 31 | readWordsFile (callback, finished) { 32 | const filepath = path.join(__dirname, '../vendor/words.txt') 33 | const readStream = fs.createReadStream(filepath) 34 | const rl = readline.createInterface({ 35 | input: readStream, 36 | crlfDelay: Infinity 37 | }) 38 | 39 | rl.on('line', (line) => { 40 | callback(line) 41 | }) 42 | 43 | rl.on('close', () => { 44 | finished() 45 | }) 46 | } 47 | 48 | /** 49 | * Creates a histogram from a given word based on the alphabet property 50 | * @param {string} word - The word to create a histogram from 51 | * @returns {object} histogram - The created histogram with alphabet characters as keys and their frequency as values 52 | */ 53 | histogramify (word) { 54 | const histogram = {} 55 | let alphabetIndex = 0 56 | 57 | // Initialize histogram with alphabet characters and set their frequency to 0 58 | while (alphabetIndex < this.alphabet.length) { 59 | histogram[this.alphabet[alphabetIndex]] = 0 60 | alphabetIndex++ 61 | } 62 | 63 | let wordIndex = 0 64 | // Iterate through the word and update the frequency of each character in the histogram 65 | while (wordIndex < word.length) { 66 | histogram[word[wordIndex]]++ 67 | wordIndex++ 68 | } 69 | 70 | return histogram 71 | } 72 | 73 | /** 74 | * Generate a tree of words from given source file. 75 | * @async 76 | * @returns {Promise} Promise that resolves when the tree is generated. 77 | */ 78 | async generateTree () { 79 | return new Promise((resolve, reject) => { 80 | this.readWordsFile( 81 | (word) => { 82 | const histogram = this.histogramify(word) 83 | let currentNode = this.tree 84 | let alphabetIndex = 0 85 | 86 | // Iterate through the alphabet and create tree nodes based on character frequencies 87 | while (alphabetIndex < this.alphabet.length) { 88 | const letter = this.alphabet[alphabetIndex] 89 | const frequency = histogram[letter] 90 | 91 | // Create a new node for the frequency if it doesn't exist 92 | if (!currentNode[frequency]) { 93 | currentNode[frequency] = {} 94 | } 95 | currentNode = currentNode[frequency] 96 | alphabetIndex++ 97 | } 98 | 99 | // Add the word to the words array in the current node 100 | if (!currentNode.words) { 101 | currentNode.words = [] 102 | } 103 | currentNode.words.push(word) 104 | }, 105 | () => { 106 | resolve() 107 | } 108 | ) 109 | }) 110 | } 111 | 112 | /** 113 | * Get all possible anagrams for a given set of letters 114 | * @param {string[]} lettersArray - Array of letters 115 | * @returns {string[]} allAnagrams - Sorted array of anagrams in descending order of length 116 | */ 117 | getAnagrams (lettersArray) { 118 | const histogram = this.histogramify(lettersArray) 119 | const rootNode = this.tree 120 | let frontier = [rootNode] 121 | let alphabetIndex = 0 122 | 123 | // Iterate through the alphabet and traverse the tree based on character frequencies 124 | while (alphabetIndex < this.alphabet.length) { 125 | const letter = this.alphabet[alphabetIndex] 126 | const frequency = histogram[letter] 127 | const newFrontier = [] 128 | let nodeIndex = 0 129 | 130 | // Traverse the frontier nodes to build new frontier nodes 131 | while (nodeIndex < frontier.length) { 132 | const currentNode = frontier[nodeIndex] 133 | let subNodeIndex = 0 134 | 135 | // Add nodes from the current frontier to the new frontier based on their frequency 136 | while (subNodeIndex <= frequency) { 137 | if (currentNode[subNodeIndex]) { 138 | newFrontier.push(currentNode[subNodeIndex]) 139 | } 140 | subNodeIndex++ 141 | } 142 | nodeIndex++ 143 | } 144 | frontier = newFrontier 145 | alphabetIndex++ 146 | } 147 | 148 | const allAnagrams = [] 149 | let nodeIndex = 0 150 | 151 | // Iterate through the frontier nodes and add their words to the allAnagrams array 152 | while (nodeIndex < frontier.length) { 153 | let wordIndex = 0 154 | 155 | while (wordIndex < frontier[nodeIndex].words.length) { 156 | allAnagrams.push(frontier[nodeIndex].words[wordIndex]) 157 | wordIndex++ 158 | } 159 | nodeIndex++ 160 | } 161 | 162 | // Sort the anagrams in descending order of length 163 | return allAnagrams.sort((a, b) => b.length - a.length) 164 | } 165 | } 166 | 167 | export { WordGenerator } 168 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import { WordGenerator } from '.' 2 | 3 | describe('WordGenerator', () => { 4 | let wordGenerator 5 | 6 | beforeEach(async () => { 7 | wordGenerator = new WordGenerator() 8 | await wordGenerator.generateTree() 9 | }) 10 | 11 | test('histogramify', () => { 12 | const histogram = wordGenerator.histogramify('test') 13 | expect(histogram).toMatchObject({ 14 | j: 0, 15 | q: 0, 16 | x: 0, 17 | z: 0, 18 | w: 0, 19 | k: 0, 20 | v: 0, 21 | f: 0, 22 | y: 0, 23 | b: 0, 24 | h: 0, 25 | g: 0, 26 | m: 0, 27 | p: 0, 28 | u: 0, 29 | d: 0, 30 | c: 0, 31 | l: 0, 32 | o: 0, 33 | t: 2, 34 | n: 0, 35 | r: 0, 36 | a: 0, 37 | i: 0, 38 | s: 1, 39 | e: 1 40 | }) 41 | }) 42 | 43 | test('getAnagrams', async () => { 44 | const anagrams = wordGenerator.getAnagrams(['t', 'e', 's', 't']) 45 | expect(anagrams.sort()).toEqual( 46 | ['test', 'sett', 'stet', 'tets', 'set', 'tet', 'es', 'et'].sort() 47 | ) 48 | }) 49 | }) 50 | --------------------------------------------------------------------------------