diff options
| -rw-r--r-- | css/style.css | 163 | ||||
| -rw-r--r-- | css/style.min.css | 1 | ||||
| -rw-r--r-- | css/wordsearch.css | 65 | ||||
| -rw-r--r-- | css/wordsearch.min.css | 1 | ||||
| -rw-r--r-- | images/forkme_right_red.png | bin | 7927 -> 0 bytes | |||
| -rw-r--r-- | index.html | 97 | ||||
| -rw-r--r-- | js/utility.js | 32 | ||||
| -rw-r--r-- | js/utility.min.js | 1 | ||||
| -rw-r--r-- | js/wordsearch.js | 1253 | ||||
| -rw-r--r-- | js/wordsearch.min.js | 1 |
10 files changed, 964 insertions, 650 deletions
diff --git a/css/style.css b/css/style.css index 51304ed..73e9a77 100644 --- a/css/style.css +++ b/css/style.css @@ -1,58 +1,159 @@ -/* CSS reset */ +*, *::before, *::after { + box-sizing: border-box; +} + +:root { + --stone-50: oklch(98.5% 0.001 106.423); + --stone-100: oklch(97% 0.001 106.424); + --stone-200: oklch(92.3% 0.003 48.717); + --stone-500: oklch(55.3% 0.013 58.071); + --stone-700: oklch(37.4% 0.01 67.558); + --stone-800: oklch(26.8% 0.007 34.298); +} -body, h1 { +body, h1, h2, p, ul { margin: 0; padding: 0; } -h1 { - text-transform: uppercase; +ul { + list-style-type: none; } body { - color: #333; + font-family: system-ui, -apple-system, sans-serif; + color: var(--stone-800); + background-color: var(--stone-100); + min-height: 100vh; } -/* Common */ +.page { + position: relative; + display: grid; + grid-template-areas: + "title title title" + "board . words" + "status status status"; + grid-template-columns: auto 2.5rem auto; + justify-content: center; + margin-inline: auto; + width: min(62rem, calc(100% - 2rem)); +} -.fix { - *zoom: 1; +.page__header { + margin-block: 2rem; + grid-area: title; + text-align: center; } -.fix:after { - display: table; - clear: both; - content: ''; +.page__board { + grid-area: board; + display: flex; + justify-content: center; } -/* Home */ +.page__words { + grid-area: words; +} -.wrap { - width: 960px; - margin: 0 auto; - padding: 40px 0; +.page__footer { + margin-block: 2rem; + grid-area: status; text-align: center; - position:relative; } -.logo, #gameArea { - margin-bottom: 40px; +.page__title { + font-weight: 700; + font-size: clamp(1.25rem, 4vw, 2rem); + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--stone-800); +} + +.page__status { + font-size: 0.85rem; + letter-spacing: 0.02em; + color: var(--stone-500); +} + +.word-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.word-list__item { + font-weight: 500; + font-size: 0.875rem; + letter-spacing: 0.03em; + color: var(--stone-800); + transition: opacity 0.2s, color 0.2s; } -.ws-area, .ws-words { - display: inline-block; - vertical-align: top; +.word-list__item--found { + text-decoration: line-through; + opacity: 0.4; } -.ws-words { - margin-left: 20px; - text-align: left; +@media (max-width: 860px) { + .page { + grid-template-columns: auto; + grid-template-areas: + "title" + "board" + "words" + "status"; + } + + .page__header { + margin-block: 1rem; + } + + .word-list { + margin-block-start: 1rem; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + gap: 0.75rem 1.25rem; + } + + .page__footer { + margin-block: 2rem 1rem; + } } -.ws-word { - margin-bottom: 4px; +.complete { + position: absolute; + background-color: color-mix(in srgb, var(--stone-50) 92%, transparent); + width: 100%; + height: 100%; + left: 0; + top: 0; } -.ws-words ::first-letter{ - text-transform : capitalize; -}
\ No newline at end of file +.complete__inner { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +.complete__content { + padding: 1.5rem; + text-align: center; +} + +.complete__title { + font-weight: 700; + font-size: 1.75rem; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--stone-800); +} + +.complete__message { + margin-block-start: 0.5rem; + font-size: 1rem; + color: var(--stone-700); +} diff --git a/css/style.min.css b/css/style.min.css deleted file mode 100644 index 6754647..0000000 --- a/css/style.min.css +++ /dev/null @@ -1 +0,0 @@ -body,h1{margin:0;padding:0}h1{text-transform:uppercase}body{color:#333}.fix:after{display:table;clear:both;content:''}.wrap{width:960px;margin:0 auto;padding:40px 0;text-align:center;position:relative}#gameArea,.logo{margin-bottom:40px}.ws-area,.ws-words{display:inline-block;vertical-align:top}.ws-words{margin-left:20px;text-align:left}.ws-word{margin-bottom:4px}.ws-words ::first-letter{text-transform:capitalize}
\ No newline at end of file diff --git a/css/wordsearch.css b/css/wordsearch.css deleted file mode 100644 index 149c36e..0000000 --- a/css/wordsearch.css +++ /dev/null @@ -1,65 +0,0 @@ -/* Wordsearch */ - -.ws-area { - background: #fafafa; - display: inline-block; - padding: 20px; - border-radius: 10px; - -moz-user-select: -moz-none; - -khtml-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; -} - -.ws-row { - line-height: 0; -} - -.ws-col { - cursor: pointer; -} - -.ws-col.ws-selected { - background: #eee; -} - -.ws-found { - background: yellow; -} - -.ws-game-over-outer { - background: rgba(0, 0, 0, 0.85); - height: 100%; - left: 0; - position: absolute; - top: 0; - width: 100%; -} - -.ws-game-over-inner { - width:100%; - height:100%; - padding:0; - margin:0; - display:table; -} - -.ws-game-over { - display:table-cell; - vertical-align:middle; -} - -.ws-game-over h2 { - color:#FFFFFF; - font-size:2em; - text-transform:uppercase; - padding:0; - margin:0 0 9px 0; -} - -.ws-game-over p { - color:#FFFFFF; - font-size:1em; - padding:0; - margin:0; -}
\ No newline at end of file diff --git a/css/wordsearch.min.css b/css/wordsearch.min.css deleted file mode 100644 index 0e49dc3..0000000 --- a/css/wordsearch.min.css +++ /dev/null @@ -1 +0,0 @@ -.ws-area{background:#fafafa;display:inline-block;border-radius:10px;-moz-user-select:0;-khtml-user-select:none;-webkit-user-select:none;-ms-user-select:none;padding:20px}.ws-row{line-height:0}.ws-col{cursor:pointer}.ws-col.ws-selected{background:#eee}.ws-found{background:#FF0}.ws-game-over-outer{background:rgba(0,0,0,0.85);height:100%;left:0;position:absolute;top:0;width:100%}.ws-game-over-inner{width:100%;height:100%;display:table;margin:0;padding:0}.ws-game-over{display:table-cell;vertical-align:middle}.ws-game-over h2{color:#FFF;font-size:2em;text-transform:uppercase;margin:0 0 9px;padding:0}.ws-game-over p{color:#FFF;font-size:1em;margin:0;padding:0}
\ No newline at end of file diff --git a/images/forkme_right_red.png b/images/forkme_right_red.png Binary files differdeleted file mode 100644 index 1e19c21..0000000 --- a/images/forkme_right_red.png +++ /dev/null @@ -2,32 +2,81 @@ <html lang="en"> <head> <meta charset="utf-8"> - <title>Word search game</title> - <link rel="stylesheet" href="css/wordsearch.min.css" /> - <link rel="stylesheet" href="css/style.min.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="theme-color" content="#f5f3f0"> + <title>Word Search</title> + <link rel="stylesheet" href="css/style.css"> </head> <body> - <div class="wrap"> - <h1 class="logo">Word search game</h1> - <section id="ws-area"></section> - <ul class="ws-words"></ul> - </div> - <a href="https://github.com/lizhineng/word-search-game"><img style="position: absolute; top: 0; right: 0; border: 0;" src="images/forkme_right_red.png" alt="Fork me on GitHub"></a> - <script src="js/utility.min.js"></script> - <script src="js/wordsearch.min.js"></script> - <script type="text/javascript"> - var gameAreaEl = document.getElementById('ws-area'); - var gameobj = gameAreaEl.wordSearch(); - - // Put words into `.ws-words` - var words = gameobj.settings.wordsList, - wordsWrap = document.querySelector('.ws-words'); - for (i in words) { - var liEl = document.createElement('li'); - liEl.setAttribute('class', 'ws-word'); - liEl.innerText = words[i]; - wordsWrap.appendChild(liEl); + <main class="page"> + <header class="page__header"> + <h1 class="page__title">Word Search Game</h1> + </header> + <section id="gameboard" class="page__board"></section> + <aside class="page__words" aria-label="Words to find"> + <ul class="word-list"></ul> + </aside> + <footer class="page__footer"> + <p id="status" class="page__status" aria-live="polite"></p> + </footer> + </main> + <script type="module"> + import { WordSearch } from './js/wordsearch.js'; + + const game = new WordSearch({ + container: 'gameboard', + words: ['Bangkok', 'Hong Kong', 'London', 'Macau', 'Istanbul', 'Dubai', 'Mecca', 'Antalya', 'Paris', 'Kuala Lumpur'], + }); + + const list = document.querySelector('.word-list'); + + if (list) { + list.innerHTML = ''; + + for (let i = 0; i < game.words.length; i++) { + const li = document.createElement('li'); + li.className = 'word-list__item'; + li.dataset.word = game.normalizedWords[i]; + li.textContent = game.words[i]; + list.appendChild(li); + } + + const statusEl = document.getElementById('status'); + const STATUS_MESSAGES = { + 'game-started': 'Find all the words!', + 'board-focused': 'Use arrow keys to move, Space to start a selection, Enter to confirm.', + 'keyboard-started': 'Move with arrow keys, then press Enter to confirm.', + 'keyboard-cancelled': 'Selection canceled.', + 'word-found': ({ displayWord }) => `Found: ${displayWord}!`, + 'game-completed': 'You found all the words — well done!', + }; + + game.container.addEventListener('wordsearch:status', ({ detail }) => { + const msg = STATUS_MESSAGES[detail.name]; + if (statusEl && msg !== undefined) { + statusEl.textContent = typeof msg === 'function' ? msg(detail) : msg; + } + + if (detail.name === 'word-found') { + const item = list.querySelector(`[data-word="${detail.normalizedWord}"]`); + item?.classList.add('word-list__item--found'); + } + + if (detail.name === 'game-completed') { + const overlay = document.createElement('div'); + overlay.className = 'complete'; + overlay.innerHTML = ` + <div class="complete__inner"> + <div class="complete__content"> + <h2 class="complete__title">Congratulations!</h2> + <p class="complete__message">You found all of the words!</p> + </div> + </div> + `; + game.container.parentNode.appendChild(overlay); + } + }); } </script> </body> -</html>
\ No newline at end of file +</html> diff --git a/js/utility.js b/js/utility.js deleted file mode 100644 index fe1d0f1..0000000 --- a/js/utility.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Returns a random integer between min and max - * - * @param {Number} min - * @param {Number} max - * @return {Number} - */ -if (typeof Math.rangeInt != 'function') { - Math.rangeInt = function(min, max){ - if (max == undefined) { - max = min; - min = 0; - } - return Math.floor(Math.random() * (max - min + 1)) + min; - } -} - -/** - * Mege two objects - * - * @param {Object} o1 Object 1 - * @param {Object} o2 Object 2 - * @return {Object} - */ -if (typeof Object.merge != 'function') { - Object.merge = function(o1, o2) { - for (var i in o1) { - o2[i] = o1[i]; - } - return o2; - } -} diff --git a/js/utility.min.js b/js/utility.min.js deleted file mode 100644 index a8b0a58..0000000 --- a/js/utility.min.js +++ /dev/null @@ -1 +0,0 @@ -"function"!=typeof Math.rangeInt&&(Math.rangeInt=function(n,t){return void 0==t&&(t=n,n=0),Math.floor(Math.random()*(t-n+1))+n}),"function"!=typeof Object.merge&&(Object.merge=function(n,t){for(var e in n)t[e]=n[e];return t});
\ No newline at end of file diff --git a/js/wordsearch.js b/js/wordsearch.js index 5a3915f..c3390af 100644 --- a/js/wordsearch.js +++ b/js/wordsearch.js @@ -1,609 +1,874 @@ -(function(){ - 'use strict'; - - // Extend the element method - Element.prototype.wordSearch = function(settings) { - return new WordSearch(this, settings); +// @ts-check +/** + * @typedef {object} GridPoint + * @property {number} row + * @property {number} col + */ + +/** + * @typedef {object} Cell + * @property {number} row + * @property {number} col + * @property {string} letter + */ + +/** + * @typedef {object} GameSettings + * @property {Direction[]} directions - The allowed word placement directions (e.g., 'RIGHT', 'DOWN', 'DIAG_RIGHT', 'DIAG_LEFT'). + * @property {number} gridSize - The size of the game grid (e.g., 15 for a 15x15 grid). + * @property {number} cellSize - The size of each cell in pixels. + * @property {number} minCanvasWidth - The minimum width of the canvas in pixels. + * @property {number} canvasViewportGutter - The minimum distance in pixels between the canvas and the viewport edge. + * @property {string[]} words - The list of words to be placed in the grid. + * @property {number} fontWeight - The font weight for the letters (e.g., 600). + * @property {number} fontSize - The font size for the letters in pixels. + * @property {string} fontFamily - The font family for the letters (e.g., 'system-ui, -apple-system, sans-serif'). + * @property {object} colors - An object defining the color scheme for the game. + * @property {string} colors.cell - The background color of unselected cells. + * @property {string} colors.selectedCell - The background color of currently selected cells. + * @property {string} colors.foundCell - The background color of cells that are part of found words. + * @property {string} colors.stroke - The color of the grid lines. + * @property {string} colors.focusStroke - The color of the focus indicator around the current cursor cell when the canvas is focused. + * @property {string} colors.text - The color of the letters in the cells. + * @property {boolean} debug - If true, empty cells will be rendered as blank instead of filled with random letters, which can help with debugging word placement. + */ + +const DIRECTIONS = { + RIGHT: [0, 1], + DOWN: [1, 0], + DIAG_RIGHT: [1, 1], + DIAG_LEFT: [1, -1], +}; + +/** + * @typedef {keyof typeof DIRECTIONS} Direction + */ + +class Linguist { + /** + * Normalizes a word for grid placement. + * + * The normalization process includes: + * 1. Unicode normalization (NFKD) to decompose characters into their base forms. + * 2. Removal of diacritical marks (accents). + * 3. Removal of whitespace characters. + * 4. Conversion to uppercase. + * + * @param {string} word + * @returns {string} + */ + static normalize(word) { + return word + .normalize('NFKD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/\s+/g, '') + .toUpperCase(); } /** - * Word seach + * Detects the Unicode range for different languages. * - * @param {Element} wrapWl the game's wrap element - * @param {Array} settings - * constructor + * @param {string} firstLetter + * @returns {[number, number]} A tuple containing the lower and upper bounds of the Unicode range for the detected language. */ - function WordSearch(wrapEl, settings) { - this.wrapEl = wrapEl; + static detectLanguageRange(firstLetter) { + if (!firstLetter) { + return [65, 90]; + } - // Add `.ws-area` to wrap element - this.wrapEl.classList.add('ws-area'); + const code = firstLetter.charCodeAt(0); - //Words solved. - this.solved = 0; + // Latin + // See: https://www.unicode.org/charts/PDF/U0000.pdf + // See: https://www.unicode.org/charts/PDF/U0080.pdf + if (code >= 0x0000 && code <= 0x00ff) { + return [65, 90]; + } - // Default settings - var default_settings = { - 'directions': ['W', 'N', 'WN', 'EN'], - 'gridSize': 10, - 'words': ['one', 'two', 'three', 'four', 'five'], - 'wordsList' : [], - 'debug': false + // Cyrillic + // See: https://www.unicode.org/charts/PDF/U0400.pdf + if (code >= 0x0400 && code <= 0x04ff) { + return [1040, 1071]; } - this.settings = Object.merge(settings, default_settings); - // Check the words' length if it is overflow the grid - if (this.parseWords(this.settings.gridSize)) { - // Add words into the matrix data - var isWorked = false; + // Hebrew + // See: https://unicode.org/charts/PDF/U0590.pdf + if (code >= 0x0590 && code <= 0x05ff) { + return [1488, 1514]; + } - while (isWorked == false) { - // initialize the application - this.initialize(); + // Arabic + // See: https://www.unicode.org/charts/PDF/U0600.pdf + if (code >= 0x0600 && code <= 0x06ff) { + return [1569, 1610]; + } - isWorked = this.addWords(); - } + // Hiragana + // See: https://www.unicode.org/charts/PDF/U3040.pdf + if (code >= 0x3040 && code <= 0x309f) { + return [12353, 12438]; + } - // Fill up the remaining blank items - if (!this.settings.debug) { - this.fillUpFools(); - } + // Katakana + // See: https://www.unicode.org/charts/PDF/U30A0.pdf + if (code >= 0x30a0 && code <= 0x30ff) { + return [12449, 12538]; + } - // Draw the matrix into wrap element - this.drawmatrix(); + // CJK Unified Ideographs + // See: https://www.unicode.org/charts/PDF/U4E00.pdf + if (code >= 0x4e00 && code <= 0x9fff) { + return [19968, 40959]; } + + // Latin + return [65, 90]; } +} +class Random { /** - * Parse words - * @param {Number} Max size - * @return {Boolean} + * @param {number} min + * @param {number} max + * @returns {number} A random integer in [min, max]. */ - WordSearch.prototype.parseWords = function(maxSize) { - var itWorked = true; - - for (var i = 0; i < this.settings.words.length; i++) { - // Convert all the letters to upper case - this.settings.wordsList[i] = this.settings.words[i].trim(); - this.settings.words[i] = removeDiacritics(this.settings.wordsList[i].trim().toUpperCase()); - - var word = this.settings.words[i]; - if (word.length > maxSize) { - alert('The length of word `' + word + '` is overflow the gridSize.'); - console.error('The length of word `' + word + '` is overflow the gridSize.'); - itWorked = false; - } + static integer(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; + } +} + +class State { + /** + * The game grid represented as a 2D array of cells. + * + * @type {Cell[][]} + */ + grid = []; + + /** + * The currently selected cells during an active selection. + * + * @type {GridPoint[]} + */ + selection = []; + + /** + * A set of numeric cell keys (`row * gridSize + col`) that are part of found words. + * + * @type {Set<number>} + */ + founds = new Set(); + + /** + * A set of normalized words that have been found by the player. + * + * @type {Set<string>} + */ + solvedWords = new Set(); + + /** + * The starting point of the current selection, or `null` if no selection is active. + * + * @type {GridPoint|null} + */ + anchor = null; + + /** + * The current position of the cursor. + * + * @type {GridPoint} + */ + cursor = { row: 0, col: 0 }; + + /** + * Whether the user is currently dragging to select cells. + * + * @type {boolean} + */ + dragging = false; + + /** + * Whether the canvas currently has focus, which affects keyboard navigation and selection. + * + * @type {boolean} + */ + canvasHasFocus = false; +} + +class Canvas { + #mount; + #settings; + #logicalBoardSize; + + /** + * The canvas element. + * + * @type {HTMLCanvasElement} + */ + #canvas = document.createElement('canvas'); + + /** + * The 2D rendering context for the canvas. + * + * @type {CanvasRenderingContext2D|null} + */ + #ctx = this.#canvas.getContext('2d'); + + /** + * Initializes the canvas for the word search game. + * + * @param {Element} element - The DOM element to mount the canvas into. + * @param {GameSettings} settings - The game settings, including grid size, cell size, and colors. + */ + constructor(element, settings) { + this.#mount = element; + this.#settings = settings; + this.#logicalBoardSize = settings.gridSize * settings.cellSize; + this.#configureCanvas(); + } + + #configureCanvas() { + this.#canvas.setAttribute('aria-label', 'Word search game board'); + this.#canvas.setAttribute('role', 'img'); + this.#canvas.tabIndex = 0; + this.#styleCanvas(); + + this.#mount.innerHTML = ''; + this.#mount.appendChild(this.#canvas); + + this.#resize(); + } + + #styleCanvas() { + this.#canvas.style.cursor = 'crosshair'; + this.#canvas.style.display = 'block'; + this.#canvas.style.touchAction = 'none'; + this.#canvas.style.boxShadow = `0 0 0 1px ${this.#settings.colors.stroke}`; + } + + #resize() { + const dpr = window.devicePixelRatio || 1; + const maxWidth = Math.min( + this.#logicalBoardSize, + Math.max(this.#settings.minCanvasWidth, window.innerWidth - this.#settings.canvasViewportGutter) + ); + + this.#canvas.style.width = `${maxWidth}px`; + this.#canvas.style.height = `${maxWidth}px`; + this.#canvas.width = Math.floor(this.#logicalBoardSize * dpr); + this.#canvas.height = Math.floor(this.#logicalBoardSize * dpr); + + if (!this.#ctx) { + throw new Error('Failed to get 2D context from canvas.'); } - return itWorked; + this.#ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + } + + resize() { + this.#resize(); } /** - * Put the words into the matrix + * Resolve the grid cell coordinates from the given client (mouse/touch) coordinates. + * + * @param {number} clientX + * @param {number} clientY + * @returns {GridPoint|null} */ - WordSearch.prototype.addWords = function() { - var keepGoing = true, - counter = 0, - isWorked = true; - - while (keepGoing) { - // Getting random direction - var dir = this.settings.directions[Math.rangeInt(this.settings.directions.length - 1)], - result = this.addWord(this.settings.words[counter], dir), - isWorked = true; - - if (result == false) { - keepGoing = false; - isWorked = false; - } + toGridCell(clientX, clientY) { + const { gridSize } = this.#settings; + const rect = this.#canvas.getBoundingClientRect(); + const x = clientX - rect.left; + const y = clientY - rect.top; + + if (x < 0 || y < 0 || x > rect.width || y > rect.height) { + return null; + } - counter++; - if (counter >= this.settings.words.length) { - keepGoing = false; - } - } + const col = Math.floor((x / rect.width) * gridSize); + const row = Math.floor((y / rect.height) * gridSize); + + if (row < 0 || col < 0 || row >= gridSize || col >= gridSize) { + return null; + } - return isWorked; + return { row, col }; } /** - * Add word into the matrix + * Render the current state of the game onto the canvas. * - * @param {String} word - * @param {Number} direction + * @param {State} state - The current state of the game, including the grid, selection, and found words. */ - WordSearch.prototype.addWord = function(word, direction) { - var itWorked = true, - directions = { - 'W': [0, 1], // Horizontal (From left to right) - 'N': [1, 0], // Vertical (From top to bottom) - 'WN': [1, 1], // From top left to bottom right - 'EN': [1, -1] // From top right to bottom left - }, - row, col; // y, x + render(state) { + if (!this.#ctx) { + return; + } - switch (direction) { - case 'W': // Horizontal (From left to right) - var row = Math.rangeInt(this.settings.gridSize - 1), - col = Math.rangeInt(this.settings.gridSize - word.length); - break; + const { + cellSize, + gridSize, + colors, + fontWeight, + fontSize, + fontFamily, + } = this.#settings; + const boardSize = this.#logicalBoardSize; + const ctx = this.#ctx; - case 'N': // Vertical (From top to bottom) - var row = Math.rangeInt(this.settings.gridSize - word.length), - col = Math.rangeInt(this.settings.gridSize - 1); - break; + ctx.clearRect(0, 0, boardSize, boardSize); + ctx.fillStyle = colors.cell; + ctx.fillRect(0, 0, boardSize, boardSize); - case 'WN': // From top left to bottom right - var row = Math.rangeInt(this.settings.gridSize - word.length), - col = Math.rangeInt(this.settings.gridSize - word.length); - break; + ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; - case 'EN': // From top right to bottom left - var row = Math.rangeInt(this.settings.gridSize - word.length), - col = Math.rangeInt(word.length - 1, this.settings.gridSize - 1); - break; + const selection = new Set(state.selection.map(c => c.row * gridSize + c.col)); - default: - var error = 'UNKNOWN DIRECTION ' + direction + '!'; - alert(error); - console.log(error); - break; - } + for (let row = 0; row < gridSize; row++) { + for (let col = 0; col < gridSize; col++) { + const x = col * cellSize; + const y = row * cellSize; + const key = row * gridSize + col; - // Add words to the matrix - for (var i = 0; i < word.length; i++) { - var newRow = row + i * directions[direction][0], - newCol = col + i * directions[direction][1]; + ctx.fillStyle = + state.founds.has(key) ? colors.foundCell : + selection.has(key) ? colors.selectedCell : colors.cell; - // The letter on the board - var origin = this.matrix[newRow][newCol].letter; + ctx.fillRect(x, y, cellSize, cellSize); - if (origin == '.' || origin == word[i]) { - this.matrix[newRow][newCol].letter = word[i]; - } else { - itWorked = false; + ctx.strokeStyle = colors.stroke; + ctx.strokeRect(x + 0.5, y + 0.5, cellSize - 1, cellSize - 1); + + ctx.fillStyle = colors.text; + ctx.fillText(state.grid[row][col].letter, x + cellSize / 2, y + cellSize / 2); } } - return itWorked; + // If the canvas has focus, draw a focus indicator around the current cursor cell. + if (state.canvasHasFocus) { + const { row, col } = state.cursor; + ctx.strokeStyle = colors.focusStroke; + ctx.lineWidth = 2; + ctx.strokeRect(col * cellSize + 2, row * cellSize + 2, cellSize - 4, cellSize - 4); + ctx.lineWidth = 1; + } } + get canvas() { + return this.#canvas; + } +} + +class Handler { + #canvas; + #gridSize; + #state; + #callbacks; + #render; + /** - * Initialize the application + * @param {Canvas} canvas + * @param {number} gridSize + * @param {State} state + * @param {() => void} render - A function to call to re-render the canvas after state changes. + * @param {object} [callbacks] + * @param {() => void} [callbacks.onCanvasFocus] + * @param {() => void} [callbacks.onCanvasBlur] + * @param {() => void} [callbacks.onKeyboardStart] + * @param {() => void} [callbacks.onKeyboardCancel] + * @param {(selection: GridPoint[]) => void} [callbacks.onSelection] */ - WordSearch.prototype.initialize = function() { - /** - * Letter matrix - * - * param {Array} - */ - this.matrix = []; + constructor(canvas, gridSize, state, render, callbacks = {}) { + this.#canvas = canvas; + this.#gridSize = gridSize; + this.#state = state; + this.#render = render; + this.#callbacks = callbacks; + this.#registerEvents(); + } - /** - * Selection from - * @Param {Object} - */ - this.selectFrom = null; + #registerEvents() { + const canvas = this.#canvas.canvas; + + canvas.addEventListener('pointerdown', this.#onPointerDown); + canvas.addEventListener('keydown', this.#onKeyDown); + canvas.addEventListener('focus', this.#onFocus); + canvas.addEventListener('blur', this.#onBlur); + window.addEventListener('pointermove', this.#onPointerMove); + window.addEventListener('pointerup', this.#onPointerUp); + window.addEventListener('pointercancel', this.#onPointerCancel); + window.addEventListener('resize', this.#onResize); + } - /** - * Selected items - */ - this.selected = []; + #onFocus = () => { + this.#state.canvasHasFocus = true; + this.#callbacks.onCanvasFocus?.(); + this.#render(); + }; - this.initmatrix(this.settings.gridSize); - } + #onBlur = () => { + this.#state.canvasHasFocus = false; + this.#callbacks.onCanvasBlur?.(); + this.#render(); + }; /** - * Fill default items into the matrix - * @param {Number} size Grid size + * @param {KeyboardEvent} event */ - WordSearch.prototype.initmatrix = function(size) { - for (var row = 0; row < size; row++) { - for (var col = 0; col < size; col++) { - var item = { - letter: '.', // Default value - row: row, - col: col - } + #onKeyDown = (event) => { + let { row, col } = this.#state.cursor; + let handled = true; + + switch (event.key) { + case 'ArrowUp': + case 'k': + row = Math.max(0, row - 1); + break; + case 'ArrowDown': + case 'j': + row = Math.min(this.#gridSize - 1, row + 1); + break; + case 'ArrowLeft': + case 'h': + col = Math.max(0, col - 1); + break; + case 'ArrowRight': + case 'l': + col = Math.min(this.#gridSize - 1, col + 1); + break; + case ' ': + case 'Spacebar': + this.#startKeyboardSelection(); + break; + case 'Enter': + this.#finishKeyboardSelection(); + break; + case 'Escape': + this.#state.anchor = null; + this.#state.selection = []; + this.#callbacks.onKeyboardCancel?.(); + break; + default: + handled = false; + break; + } - if (!this.matrix[row]) { - this.matrix[row] = []; - } + this.#state.cursor = { row, col }; - this.matrix[row][col] = item; - } + if (this.#state.anchor) { + this.#state.selection = this.#getSelectionCells(this.#state.anchor, this.#state.cursor); + } + + if (handled) { + this.#render(); + event.preventDefault(); } + }; + + #startKeyboardSelection() { + this.#state.anchor = { ...this.#state.cursor }; + this.#callbacks.onKeyboardStart?.(); + } + + #finishKeyboardSelection() { + if (!this.#state.anchor) { + this.#startKeyboardSelection(); + return; + } + this.#callbacks.onSelection?.(this.#state.selection); + this.#state.anchor = null; + this.#state.selection = []; } /** - * Draw the matrix + * @param {PointerEvent} event */ - WordSearch.prototype.drawmatrix = function() { - for (var row = 0; row < this.settings.gridSize; row++) { - // New row - var divEl = document.createElement('div'); - divEl.setAttribute('class', 'ws-row'); - this.wrapEl.appendChild(divEl); - - for (var col = 0; col < this.settings.gridSize; col++) { - var cvEl = document.createElement('canvas'); - cvEl.setAttribute('class', 'ws-col'); - cvEl.setAttribute('width', 40); - cvEl.setAttribute('height', 40); - - // Fill text in middle center - var x = cvEl.width / 2, - y = cvEl.height / 2; - - var ctx = cvEl.getContext('2d'); - ctx.font = '400 28px Calibri'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = '#333'; // Text color - ctx.fillText(this.matrix[row][col].letter, x, y); - - // Add event listeners - cvEl.addEventListener('mousedown', this.onMousedown(this.matrix[row][col])); - cvEl.addEventListener('mouseover', this.onMouseover(this.matrix[row][col])); - cvEl.addEventListener('mouseup', this.onMouseup()); - - divEl.appendChild(cvEl); - } + #onPointerDown = (event) => { + const start = this.#canvas.toGridCell(event.clientX, event.clientY); + if (!start) { + return; } - } + + this.#canvas.canvas.focus({ preventScroll: true }); + this.#state.dragging = true; + this.#state.anchor = start; + this.#state.cursor = { ...start }; + this.#state.selection = []; + + this.#canvas.canvas.setPointerCapture(event.pointerId); + this.#render(); + }; /** - * Fill up the remaining items + * @param {PointerEvent} event */ - WordSearch.prototype.fillUpFools = function() { - var rangeLanguage = searchLanguage(this.settings.words[0].split('')[0]); - for (var row = 0; row < this.settings.gridSize; row++) { - for (var col = 0; col < this.settings.gridSize; col++) { - if (this.matrix[row][col].letter == '.') { - // Math.rangeInt(65, 90) => A ~ Z - this.matrix[row][col].letter = String.fromCharCode(Math.rangeInt(rangeLanguage[0], rangeLanguage[1])); - } - } + #onPointerMove = (event) => { + if (!this.#state.dragging || !this.#state.anchor) { + return; + } + + const current = this.#canvas.toGridCell(event.clientX, event.clientY); + if (!current) { + return; } - } + + this.#state.cursor = current; + this.#state.selection = this.#getSelectionCells(this.#state.anchor, this.#state.cursor); + this.#render(); + }; /** - * Returns matrix items - * @param rowFrom - * @param colFrom - * @param rowTo - * @param colTo - * @return {Array} + * @param {PointerEvent} event */ - WordSearch.prototype.getItems = function(rowFrom, colFrom, rowTo, colTo) { - var items = []; + #onPointerUp = (event) => { + if (!this.#state.dragging) { + return; + } + + this.#state.dragging = false; + + if (this.#canvas.canvas.hasPointerCapture(event.pointerId)) { + this.#canvas.canvas.releasePointerCapture(event.pointerId); + } + + if (this.#state.selection.length > 0) { + this.#callbacks.onSelection?.(this.#state.selection); + } - if ( rowFrom === rowTo || colFrom === colTo || Math.abs(rowTo - rowFrom) == Math.abs(colTo - colFrom) ) { - var shiftY = (rowFrom === rowTo) ? 0 : (rowTo > rowFrom) ? 1 : -1, - shiftX = (colFrom === colTo) ? 0 : (colTo > colFrom) ? 1 : -1, - row = rowFrom, - col = colFrom; + this.#state.anchor = null; + this.#state.selection = []; + this.#render(); + }; - items.push(this.getItem(row, col)); - do { + #onPointerCancel = () => { + this.#state.dragging = false; + this.#state.anchor = null; + this.#state.selection = []; + this.#render(); + }; + + #onResize = () => { + this.#canvas.resize(); + this.#render(); + }; + + /** + * Returns all grid points along the straight line from `from` to `to`, + * inclusive of both endpoints. Returns an empty array if the path is not + * horizontal, vertical, or diagonal. + * + * @param {GridPoint} from - The starting grid position. + * @param {GridPoint} to - The ending grid position. + * @returns {GridPoint[]} The points along the path, or `[]` if not a straight line. + */ + #getSelectionCells(from, to) { + const items = []; + const isDiag = Math.abs(to.row - from.row) === Math.abs(to.col - from.col); + + if (from.row === to.row || from.col === to.col || isDiag) { + const shiftY = from.row === to.row ? 0 : to.row > from.row ? 1 : -1; + const shiftX = from.col === to.col ? 0 : to.col > from.col ? 1 : -1; + + let row = from.row; + let col = from.col; + + items.push({ row, col }); + + while (row !== to.row || col !== to.col) { row += shiftY; col += shiftX; - items.push(this.getItem(row, col)); - } while( row !== rowTo || col !== colTo ); + items.push({ row, col }); + } } return items; } +} + +class WordSearch { + static #directions = DIRECTIONS; /** - * Returns matrix item - * @param {Number} row - * @param {Number} col - * @return {*} + * @type {GameSettings} */ - WordSearch.prototype.getItem = function(row, col) { - return (this.matrix[row] ? this.matrix[row][col] : undefined); - } + static #defaultSettings = { + directions: ['RIGHT', 'DOWN', 'DIAG_RIGHT', 'DIAG_LEFT'], + gridSize: 15, + cellSize: 36, + minCanvasWidth: 260, + canvasViewportGutter: 32, + words: [], + fontWeight: 600, + fontSize: 18, + fontFamily: 'system-ui, -apple-system, sans-serif', + colors: { + cell: 'oklch(98.5% 0.001 106.423)', + selectedCell: 'oklch(95.6% 0.045 203.388)', + foundCell: 'oklch(96.2% 0.059 95.617)', + stroke: 'oklch(92.3% 0.003 48.717)', + focusStroke: 'oklch(78.9% 0.154 211.53)', + text: 'oklch(26.8% 0.007 34.298)', + }, + debug: false, + }; + + #container; + #settings; + #normalizedWords; + #state = new State(); /** - * Clear the exist highlights + * @type {Canvas|null} */ - WordSearch.prototype.clearHighlight = function() { - var selectedEls = document.querySelectorAll('.ws-selected'); - for (var i = 0; i < selectedEls.length; i++) { - selectedEls[i].classList.remove('ws-selected'); - } - } + #canvas = null; /** - * Lookup if the wordlist contains the selected - * @param {Array} selected + * @param {Partial<GameSettings> & { container?: string | Element }} [options] */ - WordSearch.prototype.lookup = function(selected) { - var words = ['']; - - for (var i = 0; i < selected.length; i++) { - words[0] += selected[i].letter; + constructor(options = {}) { + const { container, ...settings } = options; + + if (typeof container === 'string') { + const el = document.getElementById(container); + if (!el) throw new Error(`No element found with id "${container}".`); + this.#container = el; + } else if (container instanceof Element) { + this.#container = container; + } else { + throw new Error('`container` must be an element id string or an HTMLElement.'); } - words.push(words[0].split('').reverse().join('')); - if (this.settings.words.indexOf(words[0]) > -1 || - this.settings.words.indexOf(words[1]) > -1) { - for (var i = 0; i < selected.length; i++) { - var row = selected[i].row + 1, - col = selected[i].col + 1, - el = document.querySelector('.ws-area .ws-row:nth-child(' + row + ') .ws-col:nth-child(' + col + ')'); + this.#settings = { + ...WordSearch.#defaultSettings, + ...settings, + colors: { + ...WordSearch.#defaultSettings.colors, + ...(settings.colors && typeof settings.colors === 'object' ? settings.colors : {}), + }, + }; - el.classList.add('ws-found'); - } + this.#settings.words = this.#settings.words + .map(word => String(word || '').trim()) + .filter(word => word.length > 0); - //Cross word off list. - var wordList = document.querySelector(".ws-words"); - var wordListItems = wordList.getElementsByTagName("li"); - for(var i=0; i<wordListItems.length; i++){ - if(words[0] == removeDiacritics(wordListItems[i].innerHTML.toUpperCase())){ - if(wordListItems[i].innerHTML != "<del>"+wordListItems[i].innerHTML+"</del>") { //Check the word is never found - wordListItems[i].innerHTML = "<del>"+wordListItems[i].innerHTML+"</del>"; - //Increment solved words. - this.solved++; - } - - - } + this.#normalizedWords = this.#settings.words.map(Linguist.normalize); + + if (!this.#validateWords()) return; + + this.#setupBoardState(); + this.#initializeCanvas(); + queueMicrotask(() => this.#updateStatus('game-started')); + } + + #validateWords() { + for (let i = 0; i < this.#normalizedWords.length; i++) { + if (this.#normalizedWords[i].length > this.#settings.gridSize) { + const error = `The length of word "${this.#normalizedWords[i]}" exceeds gridSize.`; + alert(error); + console.error(error); + return false; } + } + return true; + } - //Game over? - if(this.solved == this.settings.words.length){ - this.gameOver(); + #setupBoardState() { + let placed = false; + for (let attempts = 0; !placed && attempts < 300; attempts++) { + this.#initializeGrid(); + placed = this.#placeAllWords(); + } + + if (!placed) { + throw new Error('Unable to place all words in the grid after multiple attempts.'); + } + + if (!this.#settings.debug) { + this.#fillBlankCells(); + } + } + + #initializeGrid() { + this.#state.grid = Array.from({ length: this.#settings.gridSize }, (_, row) => + Array.from({ length: this.#settings.gridSize }, (__, col) => ({ + row, + col, + letter: '', + })) + ); + } + + #placeAllWords() { + for (const word of this.#normalizedWords) { + let placed = false; + for (let tries = 0; tries < 150 && !placed; tries++) { + const direction = this.#settings.directions[ + Random.integer(0, this.#settings.directions.length - 1) + ]; + placed = this.#placeWord(word, direction); + } + if (!placed) { + return false; } } + return true; } /** - * Game Over + * @param {string} word + * @param {Direction} direction + * @returns {boolean} */ - WordSearch.prototype.gameOver = function() { - //Create overlay. - var overlay = document.createElement("div"); - overlay.setAttribute("id", "ws-game-over-outer"); - overlay.setAttribute("class", "ws-game-over-outer"); - this.wrapEl.parentNode.appendChild(overlay); - - //Create overlay content. - var overlay = document.getElementById("ws-game-over-outer"); - overlay.innerHTML = "<div class='ws-game-over-inner' id='ws-game-over-inner'>"+ - "<div class='ws-game-over' id='ws-game-over'>"+ - "<h2>Congratulations!</h2>"+ - "<p>You've found all of the words!</p>"+ - "</div>"+ - "</div>"; + #placeWord(word, direction) { + const step = WordSearch.#directions[direction]; + if (!step) { + return false; + } + + const [startRow, startCol] = this.#randomStart(word, direction); + + for (let i = 0; i < word.length; i++) { + const r = startRow + i * step[0]; + const c = startCol + i * step[1]; + const l = this.#state.grid[r][c].letter; + if (l !== '' && l !== word[i]) { + return false; + } + } + + for (let i = 0; i < word.length; i++) { + const r = startRow + i * step[0]; + const c = startCol + i * step[1]; + this.#state.grid[r][c].letter = word[i]; + } + + return true; } /** - * Mouse event - Mouse down - * @param {Object} item + * @param {string} word + * @param {Direction} direction + * @returns {[number, number]} */ - WordSearch.prototype.onMousedown = function(item) { - var _this = this; - return function() { - _this.selectFrom = item; + #randomStart(word, direction) { + const max = this.#settings.gridSize; + const len = word.length; + + switch (direction) { + case 'RIGHT': + return [Random.integer(0, max - 1), Random.integer(0, max - len)]; + case 'DOWN': + return [Random.integer(0, max - len), Random.integer(0, max - 1)]; + case 'DIAG_RIGHT': + return [Random.integer(0, max - len), Random.integer(0, max - len)]; + default: + return [Random.integer(0, max - len), Random.integer(len - 1, max - 1)]; + } + } + + #fillBlankCells() { + const first = this.#normalizedWords[0]?.[0] ?? 'A'; + const [lo, hi] = Linguist.detectLanguageRange(first); + + for (const row of this.#state.grid) { + for (const cell of row) { + if (cell.letter === '') { + cell.letter = String.fromCharCode(Random.integer(lo, hi)); + } + } + } + } + + #initializeCanvas() { + this.#canvas = new Canvas(this.#container, this.#settings); + + new Handler(this.#canvas, this.#settings.gridSize, this.#state, () => this.#render(), { + onCanvasFocus: () => this.#updateStatus('board-focused'), + onKeyboardStart: () => this.#updateStatus('keyboard-started'), + onKeyboardCancel: () => this.#updateStatus('keyboard-cancelled'), + onSelection: (selection) => this.#lookupSelection(selection), + }); + + this.#render(); + } + + #render() { + if (this.#canvas) { + this.#canvas.render(this.#state); } } /** - * Mouse event - Mouse move - * @param {Object} + * @param {GridPoint[]} selection + * @returns {void} */ - WordSearch.prototype.onMouseover = function(item) { - var _this = this; - return function() { - if (_this.selectFrom) { - _this.selected = _this.getItems(_this.selectFrom.row, _this.selectFrom.col, item.row, item.col); + #lookupSelection(selection) { + const cells = selection.map(({ row, col }) => this.#state.grid[row][col]); + + if (this.#settings.debug && cells.some(cell => cell.letter === '')) { + return; + } - _this.clearHighlight(); + const letters = cells.map(c => c.letter).join(''); + const reversed = [...letters].reverse().join(''); - for (var i = 0; i < _this.selected.length; i ++) { - var current = _this.selected[i], - row = current.row + 1, - col = current.col + 1, - el = document.querySelector('.ws-area .ws-row:nth-child(' + row + ') .ws-col:nth-child(' + col + ')'); + const foundWord = + this.#normalizedWords.includes(letters) ? letters : + this.#normalizedWords.includes(reversed) ? reversed : null; - el.className += ' ws-selected'; - } - } + if (!foundWord || this.#state.solvedWords.has(foundWord)) { + return; + } + + this.#state.solvedWords.add(foundWord); + + for (const cell of cells) { + this.#state.founds.add(cell.row * this.#settings.gridSize + cell.col); + } + + const index = this.#normalizedWords.indexOf(foundWord); + const displayWord = index === -1 ? foundWord : this.#settings.words[index]; + + this.#updateStatus('word-found', { + normalizedWord: foundWord, + displayWord, + solvedCount: this.#state.solvedWords.size, + totalWords: this.#normalizedWords.length, + }); + + if (this.#state.solvedWords.size === this.#normalizedWords.length) { + this.#completeGame(); } } /** - * Mouse event - Mouse up + * @param {string} name + * @param {object} data */ - WordSearch.prototype.onMouseup = function() { - var _this = this; - return function() { - _this.selectFrom = null; - _this.clearHighlight(); - _this.lookup(_this.selected); - _this.selected = []; - } - } - -})(); -//-----------------------------Remove accent for latin/hebrew letters---------------------------------------------------// -var defaultDiacriticsRemovalMap = [{ - 'base': "A", - 'letters': /(A|Ⓐ|A|À|Á|Â|Ầ|Ấ|Ẫ|Ẩ|Ã|Ā|Ă|Ằ|Ắ|Ẵ|Ẳ|Ȧ|Ǡ|Ä|Ǟ|Ả|Å|Ǻ|Ǎ|Ȁ|Ȃ|Ạ|Ậ|Ặ|Ḁ|Ą|Ⱥ|Ɐ|[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F])/g -}, { - 'base': "AA", - 'letters': /(Ꜳ|[\uA732])/g -}, { - 'base': "AE", - 'letters': /(Æ|Ǽ|Ǣ|[\u00C6\u01FC\u01E2])/g -}, { - 'base': "AO", - 'letters': /(Ꜵ|[\uA734])/g -}, { - 'base': "AU", - 'letters': /(Ꜷ|[\uA736])/g -}, { - 'base': "AV", - 'letters': /(Ꜹ|Ꜻ|[\uA738\uA73A])/g -}, { - 'base': "AY", - 'letters': /(Ꜽ|[\uA73C])/g -}, { - 'base': "B", - 'letters': /(B|Ⓑ|B|Ḃ|Ḅ|Ḇ|Ƀ|Ƃ|Ɓ|[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181])/g -}, { - 'base': "C", - 'letters': /(C|Ⓒ|C|Ć|Ĉ|Ċ|Č|Ç|Ḉ|Ƈ|Ȼ|Ꜿ|[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E])/g -}, { - 'base': "D", - 'letters': /(D|Ⓓ|D|Ḋ|Ď|Ḍ|Ḑ|Ḓ|Ḏ|Đ|Ƌ|Ɗ|Ɖ|Ꝺ|Ð|[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779\u00D0])/g -}, { - 'base': "DZ", - 'letters': /(DZ|DŽ|[\u01F1\u01C4])/g -}, { - 'base': "Dz", - 'letters': /(Dz|Dž|[\u01F2\u01C5])/g -}, { - 'base': "E", - 'letters': /(E|Ⓔ|E|È|É|Ê|Ề|Ế|Ễ|Ể|Ẽ|Ē|Ḕ|Ḗ|Ĕ|Ė|Ë|Ẻ|Ě|Ȅ|Ȇ|Ẹ|Ệ|Ȩ|Ḝ|Ę|Ḙ|Ḛ|Ɛ|Ǝ|[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E])/g -}, { - 'base': "F", - 'letters': /(F|Ⓕ|F|Ḟ|Ƒ|Ꝼ|[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B])/g -}, { - 'base': "G", - 'letters': /(G|Ⓖ|G|Ǵ|Ĝ|Ḡ|Ğ|Ġ|Ǧ|Ģ|Ǥ|Ɠ|Ꞡ|Ᵹ|Ꝿ|[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E])/g -}, { - 'base': "H", - 'letters': /(H|Ⓗ|H|Ĥ|Ḣ|Ḧ|Ȟ|Ḥ|Ḩ|Ḫ|Ħ|Ⱨ|Ⱶ|Ɥ|[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D])/g -}, { - 'base': "I", - 'letters': /(I|Ⓘ|I|Ì|Í|Î|Ĩ|Ī|Ĭ|İ|Ï|Ḯ|Ỉ|Ǐ|Ȉ|Ȋ|Ị|Į|Ḭ|Ɨ|[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197])/g -}, { - 'base': "J", - 'letters': /(J|Ⓙ|J|Ĵ|Ɉ|[\u004A\u24BF\uFF2A\u0134\u0248])/g -}, { - 'base': "K", - 'letters': /(K|Ⓚ|K|Ḱ|Ǩ|Ḳ|Ķ|Ḵ|Ƙ|Ⱪ|Ꝁ|Ꝃ|Ꝅ|Ꞣ|[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2])/g -}, { - 'base': "L", - 'letters': /(L|Ⓛ|L|Ŀ|Ĺ|Ľ|Ḷ|Ḹ|Ļ|Ḽ|Ḻ|Ł|Ƚ|Ɫ|Ⱡ|Ꝉ|Ꝇ|Ꞁ|[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780])/g -}, { - 'base': "LJ", - 'letters': /(LJ|[\u01C7])/g -}, { - 'base': "Lj", - 'letters': /(Lj|[\u01C8])/g -}, { - 'base': "M", - 'letters': /(M|Ⓜ|M|Ḿ|Ṁ|Ṃ|Ɱ|Ɯ|[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C])/g -}, { - 'base': "N", - 'letters': /(N|Ⓝ|N|Ǹ|Ń|Ñ|Ṅ|Ň|Ṇ|Ņ|Ṋ|Ṉ|Ƞ|Ɲ|Ꞑ|Ꞥ|Ŋ|[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4\u014A])/g -}, { - 'base': "NJ", - 'letters': /(NJ|[\u01CA])/g -}, { - 'base': "Nj", - 'letters': /(Nj|[\u01CB])/g -}, { - 'base': "O", - 'letters': /(O|Ⓞ|O|Ò|Ó|Ô|Ồ|Ố|Ỗ|Ổ|Õ|Ṍ|Ȭ|Ṏ|Ō|Ṑ|Ṓ|Ŏ|Ȯ|Ȱ|Ö|Ȫ|Ỏ|Ő|Ǒ|Ȍ|Ȏ|Ơ|Ờ|Ớ|Ỡ|Ở|Ợ|Ọ|Ộ|Ǫ|Ǭ|Ø|Ǿ|Ɔ|Ɵ|Ꝋ|Ꝍ|[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C])/g -}, { - 'base': "OE", - 'letters': /(Œ|[\u0152])/g -}, { - 'base': "OI", - 'letters': /(Ƣ|[\u01A2])/g -}, { - 'base': "OO", - 'letters': /(Ꝏ|[\uA74E])/g -}, { - 'base': "OU", - 'letters': /(Ȣ|[\u0222])/g -}, { - 'base': "P", - 'letters': /(P|Ⓟ|P|Ṕ|Ṗ|Ƥ|Ᵽ|Ꝑ|Ꝓ|Ꝕ|[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754])/g -}, { - 'base': "Q", - 'letters': /(Q|Ⓠ|Q|Ꝗ|Ꝙ|Ɋ|[\u0051\u24C6\uFF31\uA756\uA758\u024A])/g -}, { - 'base': "R", - 'letters': /(R|Ⓡ|R|Ŕ|Ṙ|Ř|Ȑ|Ȓ|Ṛ|Ṝ|Ŗ|Ṟ|Ɍ|Ɽ|Ꝛ|Ꞧ|Ꞃ|[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782])/g -}, { - 'base': "S", - 'letters': /(S|Ⓢ|S|ẞ|Ś|Ṥ|Ŝ|Ṡ|Š|Ṧ|Ṣ|Ṩ|Ș|Ş|Ȿ|Ꞩ|Ꞅ|[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784])/g -}, { - 'base': "T", - 'letters': /(T|Ⓣ|T|Ṫ|Ť|Ṭ|Ț|Ţ|Ṱ|Ṯ|Ŧ|Ƭ|Ʈ|Ⱦ|Ꞇ|[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786])/g -}, { - 'base': "TH", - 'letters': /(Þ|[\u00DE])/g -}, { - 'base': "TZ", - 'letters': /(Ꜩ|[\uA728])/g -}, { - 'base': "U", - 'letters': /(U|Ⓤ|U|Ù|Ú|Û|Ũ|Ṹ|Ū|Ṻ|Ŭ|Ü|Ǜ|Ǘ|Ǖ|Ǚ|Ủ|Ů|Ű|Ǔ|Ȕ|Ȗ|Ư|Ừ|Ứ|Ữ|Ử|Ự|Ụ|Ṳ|Ų|Ṷ|Ṵ|Ʉ|[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244])/g -}, { - 'base': "V", - 'letters': /(V|Ⓥ|V|Ṽ|Ṿ|Ʋ|Ꝟ|Ʌ|[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245])/g -}, { - 'base': "VY", - 'letters': /(Ꝡ|[\uA760])/g -}, { - 'base': "W", - 'letters': /(W|Ⓦ|W|Ẁ|Ẃ|Ŵ|Ẇ|Ẅ|Ẉ|Ⱳ|[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72])/g -}, { - 'base': "X", - 'letters': /(X|Ⓧ|X|Ẋ|Ẍ|[\u0058\u24CD\uFF38\u1E8A\u1E8C])/g -}, { - 'base': "Y", - 'letters': /(Y|Ⓨ|Y|Ỳ|Ý|Ŷ|Ỹ|Ȳ|Ẏ|Ÿ|Ỷ|Ỵ|Ƴ|Ɏ|Ỿ|[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE])/g -}, { - 'base': "Z", - 'letters': /(Z|Ⓩ|Z|Ź|Ẑ|Ż|Ž|Ẓ|Ẕ|Ƶ|Ȥ|Ɀ|Ⱬ|Ꝣ|[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762])/g -}, { - 'base': "", //delete Niqqud in Hebrew - 'letters': /[\u0591-\u05C7]/g -}] - -function removeDiacritics(str) { - for (var i = 0; i < defaultDiacriticsRemovalMap.length; i++) { - str = str.replace(defaultDiacriticsRemovalMap[i].letters, defaultDiacriticsRemovalMap[i].base); - } - return str; + #updateStatus(name, data = {}) { + this.#container.dispatchEvent( + new CustomEvent('wordsearch:status', { bubbles: true, detail: { name, ...data } }) + ); + } + + #completeGame() { + this.#updateStatus('game-completed'); + } + + get container() { + return this.#container; + } + + get words() { + return this.#settings.words.slice(); + } + + get normalizedWords() { + return this.#normalizedWords.slice(); + } } -//------------------------------Search language--------------------------------------------------// -// Determine what letters injected on grid -function searchLanguage(firstLetter) -{ - codefirstLetter = firstLetter.charCodeAt(); - var codeLetter = [65,90]; - if((codefirstLetter>=65) && (codefirstLetter<=90)) { // Latin - return codeLetter = [65,90]; - } - if((codefirstLetter>=1488) && (codefirstLetter<=1514)) { //Hebrew א -> ת - return codeLetter = [1488,1514]; - } - if((codefirstLetter>=913) && (codefirstLetter<=937)) { //Greek Α -> Ω - return codeLetter = [913,929]; //930 is blank - } - if((codefirstLetter>=1040) && (codefirstLetter<=1071)) { //Cyrillic А -> Я - return codeLetter = [1040,1071]; //930 is blank - } - if((codefirstLetter>=1569) && (codefirstLetter<=1610)) { //Arab - return codeLetter = [1569,1594]; //Between 1595 and 1600, no letter - } - if((codefirstLetter>=19969) && (codefirstLetter<=40891)) { //Chinese - return codeLetter = [19969,40891]; - } - if((codefirstLetter>=12354) && (codefirstLetter<=12436)) { //Japan Hiragana - return codeLetter = [12388,12418]; //Only no small letter - } - console.log("Letter not detected : "+firstLetter+":"+codefirstLetter); - return codeLetter; - - -}
\ No newline at end of file + +export { WordSearch }; diff --git a/js/wordsearch.min.js b/js/wordsearch.min.js deleted file mode 100644 index 57e0209..0000000 --- a/js/wordsearch.min.js +++ /dev/null @@ -1 +0,0 @@ -function removeDiacritics(e){for(var t=0;t<defaultDiacriticsRemovalMap.length;t++)e=e.replace(defaultDiacriticsRemovalMap[t].letters,defaultDiacriticsRemovalMap[t].base);return e}function searchLanguage(e){codefirstLetter=e.charCodeAt();var t=[65,90];return codefirstLetter>=65&&codefirstLetter<=90?t=[65,90]:codefirstLetter>=1488&&codefirstLetter<=1514?t=[1488,1514]:codefirstLetter>=913&&codefirstLetter<=937?t=[913,929]:codefirstLetter>=1040&&codefirstLetter<=1071?t=[1040,1071]:codefirstLetter>=1569&&codefirstLetter<=1610?t=[1569,1594]:codefirstLetter>=19969&&codefirstLetter<=40891?t=[19969,40891]:codefirstLetter>=12354&&codefirstLetter<=12436?t=[12388,12418]:(console.log("Letter not detected : "+e+":"+codefirstLetter),t)}!function(){"use strict";function e(e,t){this.wrapEl=e,this.wrapEl.classList.add("ws-area"),this.solved=0;var u={directions:["W","N","WN","EN"],gridSize:10,words:["one","two","three","four","five"],wordsList:[],debug:!1};if(this.settings=Object.merge(t,u),this.parseWords(this.settings.gridSize)){for(var s=!1;0==s;)this.initialize(),s=this.addWords();this.settings.debug||this.fillUpFools(),this.drawmatrix()}}Element.prototype.wordSearch=function(t){return new e(this,t)},e.prototype.parseWords=function(e){for(var t=!0,u=0;u<this.settings.words.length;u++){this.settings.wordsList[u]=this.settings.words[u].trim(),this.settings.words[u]=removeDiacritics(this.settings.wordsList[u].trim().toUpperCase());var s=this.settings.words[u];s.length>e&&(alert("The length of word `"+s+"` is overflow the gridSize."),console.error("The length of word `"+s+"` is overflow the gridSize."),t=!1)}return t},e.prototype.addWords=function(){for(var e=!0,t=0,u=!0;e;){var s=this.settings.directions[Math.rangeInt(this.settings.directions.length-1)],r=this.addWord(this.settings.words[t],s),u=!0;0==r&&(e=!1,u=!1),t++,t>=this.settings.words.length&&(e=!1)}return u},e.prototype.addWord=function(e,t){var u,s,r=!0,i={W:[0,1],N:[1,0],WN:[1,1],EN:[1,-1]};switch(t){case"W":var u=Math.rangeInt(this.settings.gridSize-1),s=Math.rangeInt(this.settings.gridSize-e.length);break;case"N":var u=Math.rangeInt(this.settings.gridSize-e.length),s=Math.rangeInt(this.settings.gridSize-1);break;case"WN":var u=Math.rangeInt(this.settings.gridSize-e.length),s=Math.rangeInt(this.settings.gridSize-e.length);break;case"EN":var u=Math.rangeInt(this.settings.gridSize-e.length),s=Math.rangeInt(e.length-1,this.settings.gridSize-1);break;default:var o="UNKNOWN DIRECTION "+t+"!";alert(o),console.log(o)}for(var a=0;a<e.length;a++){var n=u+a*i[t][0],E=s+a*i[t][1],l=this.matrix[n][E].letter;"."==l||l==e[a]?this.matrix[n][E].letter=e[a]:r=!1}return r},e.prototype.initialize=function(){this.matrix=[],this.selectFrom=null,this.selected=[],this.initmatrix(this.settings.gridSize)},e.prototype.initmatrix=function(e){for(var t=0;e>t;t++)for(var u=0;e>u;u++){var s={letter:".",row:t,col:u};this.matrix[t]||(this.matrix[t]=[]),this.matrix[t][u]=s}},e.prototype.drawmatrix=function(){for(var e=0;e<this.settings.gridSize;e++){var t=document.createElement("div");t.setAttribute("class","ws-row"),this.wrapEl.appendChild(t);for(var u=0;u<this.settings.gridSize;u++){var s=document.createElement("canvas");s.setAttribute("class","ws-col"),s.setAttribute("width",40),s.setAttribute("height",40);var r=s.width/2,i=s.height/2,o=s.getContext("2d");o.font="400 28px Calibri",o.textAlign="center",o.textBaseline="middle",o.fillStyle="#333",o.fillText(this.matrix[e][u].letter,r,i),s.addEventListener("mousedown",this.onMousedown(this.matrix[e][u])),s.addEventListener("mouseover",this.onMouseover(this.matrix[e][u])),s.addEventListener("mouseup",this.onMouseup()),t.appendChild(s)}}},e.prototype.fillUpFools=function(){for(var e=searchLanguage(this.settings.words[0].split("")[0]),t=0;t<this.settings.gridSize;t++)for(var u=0;u<this.settings.gridSize;u++)"."==this.matrix[t][u].letter&&(this.matrix[t][u].letter=String.fromCharCode(Math.rangeInt(e[0],e[1])))},e.prototype.getItems=function(e,t,u,s){var r=[];if(e===u||t===s||Math.abs(u-e)==Math.abs(s-t)){var i=e===u?0:u>e?1:-1,o=t===s?0:s>t?1:-1,a=e,n=t;r.push(this.getItem(a,n));do a+=i,n+=o,r.push(this.getItem(a,n));while(a!==u||n!==s)}return r},e.prototype.getItem=function(e,t){return this.matrix[e]?this.matrix[e][t]:void 0},e.prototype.clearHighlight=function(){for(var e=document.querySelectorAll(".ws-selected"),t=0;t<e.length;t++)e[t].classList.remove("ws-selected")},e.prototype.lookup=function(e){for(var t=[""],u=0;u<e.length;u++)t[0]+=e[u].letter;if(t.push(t[0].split("").reverse().join("")),this.settings.words.indexOf(t[0])>-1||this.settings.words.indexOf(t[1])>-1){for(var u=0;u<e.length;u++){var s=e[u].row+1,r=e[u].col+1,i=document.querySelector(".ws-area .ws-row:nth-child("+s+") .ws-col:nth-child("+r+")");i.classList.add("ws-found")}for(var o=document.querySelector(".ws-words"),a=o.getElementsByTagName("li"),u=0;u<a.length;u++)t[0]==removeDiacritics(a[u].innerHTML.toUpperCase())&&a[u].innerHTML!="<del>"+a[u].innerHTML+"</del>"&&(a[u].innerHTML="<del>"+a[u].innerHTML+"</del>",this.solved++);this.solved==this.settings.words.length&&this.gameOver()}},e.prototype.gameOver=function(){var e=document.createElement("div");e.setAttribute("id","ws-game-over-outer"),e.setAttribute("class","ws-game-over-outer"),this.wrapEl.parentNode.appendChild(e);var e=document.getElementById("ws-game-over-outer");e.innerHTML="<div class='ws-game-over-inner' id='ws-game-over-inner'><div class='ws-game-over' id='ws-game-over'><h2>Congratulations!</h2><p>You've found all of the words!</p></div></div>"},e.prototype.onMousedown=function(e){var t=this;return function(){t.selectFrom=e}},e.prototype.onMouseover=function(e){var t=this;return function(){if(t.selectFrom){t.selected=t.getItems(t.selectFrom.row,t.selectFrom.col,e.row,e.col),t.clearHighlight();for(var u=0;u<t.selected.length;u++){var s=t.selected[u],r=s.row+1,i=s.col+1,o=document.querySelector(".ws-area .ws-row:nth-child("+r+") .ws-col:nth-child("+i+")");o.className+=" ws-selected"}}}},e.prototype.onMouseup=function(){var e=this;return function(){e.selectFrom=null,e.clearHighlight(),e.lookup(e.selected),e.selected=[]}}}();var defaultDiacriticsRemovalMap=[{base:"A",letters:/(A|Ⓐ|A|À|Á|Â|Ầ|Ấ|Ẫ|Ẩ|Ã|Ā|Ă|Ằ|Ắ|Ẵ|Ẳ|Ȧ|Ǡ|Ä|Ǟ|Ả|Å|Ǻ|Ǎ|Ȁ|Ȃ|Ạ|Ậ|Ặ|Ḁ|Ą|Ⱥ|Ɐ|[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F])/g},{base:"AA",letters:/(Ꜳ|[\uA732])/g},{base:"AE",letters:/(Æ|Ǽ|Ǣ|[\u00C6\u01FC\u01E2])/g},{base:"AO",letters:/(Ꜵ|[\uA734])/g},{base:"AU",letters:/(Ꜷ|[\uA736])/g},{base:"AV",letters:/(Ꜹ|Ꜻ|[\uA738\uA73A])/g},{base:"AY",letters:/(Ꜽ|[\uA73C])/g},{base:"B",letters:/(B|Ⓑ|B|Ḃ|Ḅ|Ḇ|Ƀ|Ƃ|Ɓ|[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181])/g},{base:"C",letters:/(C|Ⓒ|C|Ć|Ĉ|Ċ|Č|Ç|Ḉ|Ƈ|Ȼ|Ꜿ|[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E])/g},{base:"D",letters:/(D|Ⓓ|D|Ḋ|Ď|Ḍ|Ḑ|Ḓ|Ḏ|Đ|Ƌ|Ɗ|Ɖ|Ꝺ|Ð|[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779\u00D0])/g},{base:"DZ",letters:/(DZ|DŽ|[\u01F1\u01C4])/g},{base:"Dz",letters:/(Dz|Dž|[\u01F2\u01C5])/g},{base:"E",letters:/(E|Ⓔ|E|È|É|Ê|Ề|Ế|Ễ|Ể|Ẽ|Ē|Ḕ|Ḗ|Ĕ|Ė|Ë|Ẻ|Ě|Ȅ|Ȇ|Ẹ|Ệ|Ȩ|Ḝ|Ę|Ḙ|Ḛ|Ɛ|Ǝ|[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E])/g},{base:"F",letters:/(F|Ⓕ|F|Ḟ|Ƒ|Ꝼ|[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B])/g},{base:"G",letters:/(G|Ⓖ|G|Ǵ|Ĝ|Ḡ|Ğ|Ġ|Ǧ|Ģ|Ǥ|Ɠ|Ꞡ|Ᵹ|Ꝿ|[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E])/g},{base:"H",letters:/(H|Ⓗ|H|Ĥ|Ḣ|Ḧ|Ȟ|Ḥ|Ḩ|Ḫ|Ħ|Ⱨ|Ⱶ|Ɥ|[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D])/g},{base:"I",letters:/(I|Ⓘ|I|Ì|Í|Î|Ĩ|Ī|Ĭ|İ|Ï|Ḯ|Ỉ|Ǐ|Ȉ|Ȋ|Ị|Į|Ḭ|Ɨ|[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197])/g},{base:"J",letters:/(J|Ⓙ|J|Ĵ|Ɉ|[\u004A\u24BF\uFF2A\u0134\u0248])/g},{base:"K",letters:/(K|Ⓚ|K|Ḱ|Ǩ|Ḳ|Ķ|Ḵ|Ƙ|Ⱪ|Ꝁ|Ꝃ|Ꝅ|Ꞣ|[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2])/g},{base:"L",letters:/(L|Ⓛ|L|Ŀ|Ĺ|Ľ|Ḷ|Ḹ|Ļ|Ḽ|Ḻ|Ł|Ƚ|Ɫ|Ⱡ|Ꝉ|Ꝇ|Ꞁ|[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780])/g},{base:"LJ",letters:/(LJ|[\u01C7])/g},{base:"Lj",letters:/(Lj|[\u01C8])/g},{base:"M",letters:/(M|Ⓜ|M|Ḿ|Ṁ|Ṃ|Ɱ|Ɯ|[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C])/g},{base:"N",letters:/(N|Ⓝ|N|Ǹ|Ń|Ñ|Ṅ|Ň|Ṇ|Ņ|Ṋ|Ṉ|Ƞ|Ɲ|Ꞑ|Ꞥ|Ŋ|[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4\u014A])/g},{base:"NJ",letters:/(NJ|[\u01CA])/g},{base:"Nj",letters:/(Nj|[\u01CB])/g},{base:"O",letters:/(O|Ⓞ|O|Ò|Ó|Ô|Ồ|Ố|Ỗ|Ổ|Õ|Ṍ|Ȭ|Ṏ|Ō|Ṑ|Ṓ|Ŏ|Ȯ|Ȱ|Ö|Ȫ|Ỏ|Ő|Ǒ|Ȍ|Ȏ|Ơ|Ờ|Ớ|Ỡ|Ở|Ợ|Ọ|Ộ|Ǫ|Ǭ|Ø|Ǿ|Ɔ|Ɵ|Ꝋ|Ꝍ|[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C])/g},{base:"OE",letters:/(Œ|[\u0152])/g},{base:"OI",letters:/(Ƣ|[\u01A2])/g},{base:"OO",letters:/(Ꝏ|[\uA74E])/g},{base:"OU",letters:/(Ȣ|[\u0222])/g},{base:"P",letters:/(P|Ⓟ|P|Ṕ|Ṗ|Ƥ|Ᵽ|Ꝑ|Ꝓ|Ꝕ|[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754])/g},{base:"Q",letters:/(Q|Ⓠ|Q|Ꝗ|Ꝙ|Ɋ|[\u0051\u24C6\uFF31\uA756\uA758\u024A])/g},{base:"R",letters:/(R|Ⓡ|R|Ŕ|Ṙ|Ř|Ȑ|Ȓ|Ṛ|Ṝ|Ŗ|Ṟ|Ɍ|Ɽ|Ꝛ|Ꞧ|Ꞃ|[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782])/g},{base:"S",letters:/(S|Ⓢ|S|ẞ|Ś|Ṥ|Ŝ|Ṡ|Š|Ṧ|Ṣ|Ṩ|Ș|Ş|Ȿ|Ꞩ|Ꞅ|[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784])/g},{base:"T",letters:/(T|Ⓣ|T|Ṫ|Ť|Ṭ|Ț|Ţ|Ṱ|Ṯ|Ŧ|Ƭ|Ʈ|Ⱦ|Ꞇ|[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786])/g},{base:"TH",letters:/(Þ|[\u00DE])/g},{base:"TZ",letters:/(Ꜩ|[\uA728])/g},{base:"U",letters:/(U|Ⓤ|U|Ù|Ú|Û|Ũ|Ṹ|Ū|Ṻ|Ŭ|Ü|Ǜ|Ǘ|Ǖ|Ǚ|Ủ|Ů|Ű|Ǔ|Ȕ|Ȗ|Ư|Ừ|Ứ|Ữ|Ử|Ự|Ụ|Ṳ|Ų|Ṷ|Ṵ|Ʉ|[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244])/g},{base:"V",letters:/(V|Ⓥ|V|Ṽ|Ṿ|Ʋ|Ꝟ|Ʌ|[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245])/g},{base:"VY",letters:/(Ꝡ|[\uA760])/g},{base:"W",letters:/(W|Ⓦ|W|Ẁ|Ẃ|Ŵ|Ẇ|Ẅ|Ẉ|Ⱳ|[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72])/g},{base:"X",letters:/(X|Ⓧ|X|Ẋ|Ẍ|[\u0058\u24CD\uFF38\u1E8A\u1E8C])/g},{base:"Y",letters:/(Y|Ⓨ|Y|Ỳ|Ý|Ŷ|Ỹ|Ȳ|Ẏ|Ÿ|Ỷ|Ỵ|Ƴ|Ɏ|Ỿ|[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE])/g},{base:"Z",letters:/(Z|Ⓩ|Z|Ź|Ẑ|Ż|Ž|Ẓ|Ẕ|Ƶ|Ȥ|Ɀ|Ⱬ|Ꝣ|[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762])/g},{base:"",letters:/[\u0591-\u05C7]/g}];
\ No newline at end of file |
