├── .editorconfig ├── .github └── workflows │ └── release.yml ├── .gitignore ├── App.ahk ├── LICENSE ├── README.md ├── lib ├── caret.ahk ├── clipboard.ahk ├── hotkey.ahk ├── keyboard.ico ├── keyboard_layout.ahk ├── monitor.ahk └── thqby │ ├── ComVar.ahk │ └── WebView2 │ ├── 32bit │ └── WebView2Loader.dll │ ├── 64bit │ └── WebView2Loader.dll │ ├── README.md │ └── WebView2.ahk ├── res ├── data │ └── .gitkeep ├── local_names.ahk ├── test.ahk └── test2.ahk └── wwwassets ├── build-unidata.js ├── fallback_fonts └── .gitkeep ├── fonts └── AdobeBlank.otf ├── index.html ├── package-lock.json ├── package.json ├── plugins └── .gitkeep ├── script ├── ahk.ts ├── app.tsx ├── appVar.ts ├── board.tsx ├── boards │ └── utils.tsx ├── builder │ ├── builder.ts │ ├── consolidated.ts │ ├── namesList.pegjs │ ├── titleCase.ts │ └── unicode.ts ├── chars.ts ├── config.tsx ├── config │ ├── aliases.ts │ ├── arrows.ts │ ├── boards.ts │ ├── extendedLatin.ts │ ├── fallback.ts │ ├── math.ts │ ├── mathLetters.ts │ └── unicodeBoard.ts ├── emojis.ts ├── entryPoint.ts ├── helpers.ts ├── key.tsx ├── keys │ ├── base.tsx │ └── symbol.tsx ├── layout.ts ├── layout │ ├── sc.ts │ └── vk.ts ├── modules.d.ts ├── osversion.ts ├── recentsActions.tsx ├── recentsView.tsx ├── searchView.tsx ├── unicodeInterface.ts ├── unidata.d.ts └── utils │ ├── compare.ts │ └── emojiSupportTest.ts ├── style ├── legacy.css ├── material.css └── style.css ├── tsconfig.json └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = crlf 6 | indent_style = tab 7 | insert_final_newline = true 8 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*' 7 | 8 | jobs: 9 | build-and-release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: '22' 19 | cache: npm 20 | cache-dependency-path: wwwassets/package-lock.json 21 | 22 | - name: Install dependencies and build 23 | working-directory: wwwassets 24 | run: | 25 | npm install 26 | npm run build 27 | npm run build-unidata 28 | 29 | - name: Download Noto Emoji font 30 | run: | 31 | curl --location --output wwwassets/fonts/NotoColorEmoji-Regular.ttf https://github.com/google/fonts/raw/refs/heads/main/ofl/notocoloremoji/NotoColorEmoji-Regular.ttf 32 | 33 | - name: Create release ZIP (excluding node_modules) 34 | run: | 35 | zip -r emoji-keyboard.zip . -x 'wwwassets/node_modules/*' -x '.git/*' -x 'res/data/*' 36 | 37 | - name: Create GitHub Release 38 | uses: softprops/action-gh-release@v2 39 | with: 40 | files: emoji-keyboard.zip 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /res/data/* 2 | !/res/data/.gitkeep 3 | /wwwassets/node_modules 4 | /wwwassets/dist 5 | /wwwassets/data 6 | /wwwassets/fallback_fonts/* 7 | !/wwwassets/fallback_fonts/.gitkeep 8 | /wwwassets/plugins/* 9 | !/wwwassets/plugins/.gitkeep 10 | /wwwassets/fonts/* 11 | !/wwwassets/fonts/AdobeBlank.otf 12 | *.js.map 13 | /emoji-keyboard.zip 14 | /local 15 | # User configuration 16 | /config.json 17 | /hotkey.ahk 18 | # IDE-specific 19 | .idea 20 | .vscode 21 | # OS-specific 22 | Thumbs.db 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Gilles Waeber (moi@gilleswaeber.ch) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Emoji Keyboard (Emoji 16) 2 | ============== 3 | [💾 Download](https://github.com/gilleswaeber/emoji-keyboard/releases/latest/download/emoji-keyboard.zip) *or see instructions below for cloning* 4 | 5 | Screenshot 6 | 7 | Packed with thousands of Emojis! 8 | Requires Windows 7+ 9 | , [WebView2](https://go.microsoft.com/fwlink/p/?LinkId=2124703) (comes with Windows 10+) 10 | , and [Autohotkey](https://autohotkey.com/). 11 | 12 | Alternatives 13 | ------------ 14 | Since Windows 10 April 2018 Update, you can open the built-in emoji picker with Win+.. 15 | It is missing support for flags and emojis newer than the OS. 16 | 17 | How to use 18 | ---------- 19 | Install [Autohotkey](https://autohotkey.com/), download using the link at the top, and then launch App.ahk. It will then stay in 20 | system tray until you call it. 21 | 22 | Open it with the ⇧ Shift+Caps Lock combination. 23 | You can also use a double click on the tray icon. 24 | To change the shortcut, edit *hotkey.ahk* which is created the first time you start the program. 25 | 26 | Use the shown keyboard keys to navigate between the categories and type the Emojis. You can also use the mouse. You can move the window and resize it as you wish. 27 | Keymap matches the one of your system automatically. 28 | 29 | The recent list opens with Caps Lock and shows a list of recent Emojis sorted on opening by their *score*. Inserting an Emoji increments its *score* and decrement other's. 30 | A *score* ≥ 100 makes it a favorite that'll stay in the list. 31 | Emojis can be removed and marked as favorites with a right click or ⇧ Shift when in the recent tab. 32 | 33 | To exit completely, do a right click on the tray icon and choose Exit. 34 | 35 | There are different available themes: the default "material" theme and a "legacy" theme. Both come in light and dark variants. 36 | 37 | Skinnable emojis are signaled by a yellow indicator, right click or use the ⇧ Shift key to see the different skins. 38 | 39 | The [Noto Emoji](https://github.com/googlefonts/noto-emoji) font is used as a fallback for Emojis not supported by your Windows version. 40 | 41 | The flags and the newest Emojis are currently not supported by Windows. You can still use them in 3rd-party applications or sites like Whatsapp or Twitter. 42 | Flags are sorted by ISO code, meaning you'll find Switzerland (CH) on page 2, between Congo and Côte d'Ivoire. 43 | 44 | Customize 45 | --------- 46 | Many settings are accessible in the app: 47 | - Enable the *ISO layout* setting if you have a narrow left shift key 48 | - The *Reset variants* setting will clear the preferred skin/gender variants for all emojis 49 | - Two themes are available, both available in light and dark variants 50 | - Change the opacity of the window 51 | - The position where the keyboard opens, note that *Caret* is still experimental 52 | - Whether to hide the keyboard and/or return to the main page after inserting an emoji 53 | - Show the aliases or the code points in the title bar when hovering a key 54 | 55 | To change the keyboard shortcut, open the *hotkey.ahk* file in a text editor and change the expression before 56 | `::KB.Toggle()`. See the [Hotkeys Expressions](https://www.autohotkey.com/docs/v2/Hotkeys.htm) and 57 | [Key List](https://www.autohotkey.com/docs/v2/KeyList.htm) documentation for AutoHotkey for more information. 58 | After changing, reload script using a right click on the tray icon or the command in Settings > Tools 59 | 60 | See also: 61 | 62 | - [🔣 Fallback Fonts](https://github.com/gilleswaeber/emoji-keyboard/wiki/Fallback-Fonts) 63 | - [🪇 Plugins](https://github.com/gilleswaeber/emoji-keyboard/wiki/Plugins) 64 | 65 | Develop 66 | ------- 67 | The keyboard view is realized using an embedded WebView2 control, as native AHK controls do not support Emojis at the 68 | moment. 69 | Some characters may use a fallback image because they are not rendered properly, but they are supported by the OS. 70 | 71 | Requirements: 72 | - a [Node.js](https://nodejs.org/en/download/) installation to build the web-app part 73 | - Git with the [Git LFS](https://git-lfs.com/) plugin to clone the repository (otherwise the dll will be missing) 74 | 75 | 76 | When cloning the repository, you will additionally need to: 77 | - Build the JS bundles: `cd wwwassets; npm install; npm run build` 78 | - Build the Unicode data: `cd wwwassets; npm run build-unidata`, or with Settings > Tools > Build in the app 79 | - Download Noto Color Emoji: you'll be prompted to do so when running the app for the first time 80 | 81 | 82 | The web-app part is written in Typescript. 83 | In the *wwwassets*, run `npm install; npm run dev` to get started. 84 | The boards configuration is done in *wwwassets/script/config*. 85 | 86 | Unicode data is generated from several sources: 87 | 88 | - The [Unicode® Emoji Resources](http://unicode.org/emoji/) which contain information about emoji attributes and emoji 89 | ordering 90 | - The [UnicodeData](http://unicode.org/Public/3.0-Update/UnicodeData-3.0.0.html) file which contain character names 91 | - The [Unicode NamesList](https://unicode.org/Public/UNIDATA/NamesList.txt) 92 | and [NamedSequences](https://unicode.org/Public/UNIDATA/NamedSequences.txt) files which are used to generate the code 93 | charts 94 | and contains aliases used in search, and additional information not yet used 95 | - The [Unicode CLDR](https://cldr.unicode.org/) annotations for aliases used in search 96 | 97 | A rebuild of the Unicode database can be triggered in the app settings or by running `npm run build-unidata`. It will download parts of the Unicode spec when 98 | necessary. 99 | Data for CJK ideographs is not present in the app since Unicode provides it separately and I don't know enough about it 100 | to provide a proper integration, PR welcome. 101 | Boards for scripts other than Latin and monotonic Greek are not provided either but can be added manually, PR welcome. 102 | 103 | Additionally, math symbols and aliases were manually imported from the [unicode-math](https://wspr.io/unicode-math/) 104 | XeLaTeX/LuaLaTeX package and the [UTN #28: UnicodeMath](https://www.unicode.org/notes/tn28/) specification. 105 | 106 | Dependencies 107 | ------------ 108 | AutoHotkey libs: 109 | 110 | - [ahk2_lib/WebView2](https://github.com/thqby/ahk2_lib), thqby 111 | 112 | JavaScript libs: 113 | 114 | - [memoize-one](https://github.com/alexreardon/memoize-one.git), Alex Reardon, MIT License 115 | - [Peggy](https://peggyjs.org/), David Majda, MIT License 116 | - [Preact](https://preactjs.com/), Jason Miller, MIT License 117 | 118 | Emoji Fonts: 119 | 120 | - [Noto Emoji](https://github.com/googlefonts/noto-emoji), Google Fonts, SIL Open Font License 1.1 121 | - [Twemoji](https://github.com/twitter/twemoji), Twitter, CC-BY 4.0 122 | , removed, [Unicode 15 support not released](https://github.com/twitter/twemoji/issues/570) 123 | 124 | -------------------------------------------------------------------------------- /lib/caret.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey 2.0 2 | 3 | ; https://www.reddit.com/r/AutoHotkey/comments/ysuawq/get_the_caret_location_in_any_program/ 4 | GetCaretPosition(&X?, &Y?, &W?, &H?) { 5 | ; UIA2 caret 6 | static IUIA := ComObject("{e22ad333-b25f-460c-83d0-0581107395c9}", "{34723aff-0c9d-49d0-9896-7ab52df8cd8a}") 7 | try { 8 | ComCall(8, IUIA, "ptr*", &FocusedEl := 0) ; GetFocusedElement 9 | ComCall(16, FocusedEl, "int", 10024, "ptr*", &patternObject := 0), ObjRelease(FocusedEl) ; GetCurrentPattern. TextPatternElement2 = 10024 10 | if patternObject { 11 | ComCall(10, patternObject, "int*", &IsActive := 1, "ptr*", &caretRange := 0), ObjRelease(patternObject) ; GetCaretRange 12 | ComCall(10, caretRange, "ptr*", &boundingRects := 0), ObjRelease(caretRange) ; GetBoundingRectangles 13 | if (Rect := ComValue(0x2005, boundingRects)).MaxIndex() = 3 { ; VT_ARRAY | VT_R8 14 | X := Round(Rect[0]), Y := Round(Rect[1]), W := Round(Rect[2]), H := Round(Rect[3]) 15 | return 3 16 | } 17 | } 18 | } 19 | 20 | ; Acc caret 21 | static _ := DllCall("LoadLibrary", "Str", "oleacc", "Ptr") 22 | try { 23 | idObject := 0xFFFFFFF8 ; OBJID_CARET 24 | if DllCall("oleacc\AccessibleObjectFromWindow", "ptr", WinExist("A"), "uint", idObject &= 0xFFFFFFFF 25 | , "ptr", -16 + NumPut("int64", idObject == 0xFFFFFFF0 ? 0x46000000000000C0 : 0x719B3800AA000C81, NumPut("int64", idObject == 0xFFFFFFF0 ? 0x0000000000020400 : 0x11CF3C3D618736E0, IID := Buffer(16))) 26 | , "ptr*", oAcc := ComValue(9, 0)) = 0 { 27 | x := Buffer(4), y := Buffer(4), w := Buffer(4), h := Buffer(4) 28 | oAcc.accLocation(ComValue(0x4003, x.ptr, 1), ComValue(0x4003, y.ptr, 1), ComValue(0x4003, w.ptr, 1), ComValue(0x4003, h.ptr, 1), 0) 29 | X := NumGet(x, 0, "int"), Y := NumGet(y, 0, "int"), W := NumGet(w, 0, "int"), H := NumGet(h, 0, "int") 30 | if (X | Y) != 0 31 | return 2 32 | } 33 | } 34 | 35 | ; Default caret 36 | return CaretGetPos(&X, &Y) 37 | } 38 | -------------------------------------------------------------------------------- /lib/clipboard.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey 2.0 2 | 3 | ; Set the clipboard but prevent Windows from saving the value in the clipboard history 4 | ; https://www.autohotkey.com/boards/viewtopic.php?t=97251 5 | ; https://learn.microsoft.com/en-us/windows/win32/dataxchg/using-the-clipboard#pasting-information-from-the-clipboard 6 | ; https://learn.microsoft.com/en-us/windows/win32/dataxchg/clipboard-formats#cloud-clipboard-and-clipboard-history-formats 7 | SetClipboardPrivate(text) { 8 | bytes := StrPut(text, "utf-16") 9 | hMemText := DllCall("GlobalAlloc", "Int", 0x42, "Ptr", (bytes*2)+8, "Ptr") 10 | pMem := DllCall("GlobalLock", "Ptr", hMemText, "Ptr") 11 | StrPut(text, pMem, bytes, "utf-16") 12 | DllCall("GlobalUnlock", "Ptr", hMemText) 13 | DllCall("OpenClipboard", "Ptr", A_ScriptHwnd) 14 | DllCall("EmptyClipboard") 15 | DllCall("SetClipboardData", "Int", 13, "Ptr", hMemText) 16 | DllCall("SetClipboardData", "Int", DllCall("RegisterClipboardFormat", "Str", "ExcludeClipboardContentFromMonitorProcessing"), "Ptr", 0) 17 | DllCall("CloseClipboard") 18 | } 19 | -------------------------------------------------------------------------------- /lib/hotkey.ahk: -------------------------------------------------------------------------------- 1 | #Requires AutoHotkey 2.0 2 | ; Condition for activation: disable when in Windows RDP or Moonlight 3 | #HotIf !WinActive("ahk_class TscShellContainerClass") and !WinActive("ahk_exe Moonlight.exe") 4 | ; Change Hotkey here 5 | ; After changing, reload script using a right click on the tray icon or the command in Settings > Tools 6 | ; For information on hotkeys in AHK, see https://www.autohotkey.com/docs/v2/Hotkeys.htm and https://www.autohotkey.com/docs/v2/KeyList.htm 7 | +Capslock::KB.Toggle() -------------------------------------------------------------------------------- /lib/keyboard.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilleswaeber/emoji-keyboard/de2ddb5ca4dfd1880d2323be00137abfa21caaa7/lib/keyboard.ico -------------------------------------------------------------------------------- /lib/keyboard_layout.ahk: -------------------------------------------------------------------------------- 1 | ; Extract the current keyboard layout as a JSON string 2 | ; © Gilles Waeber 2022 - MIT License 3 | ; Some functions based on AutoHotkey source code 4 | #Requires AutoHotkey 2.0 5 | 6 | JsonString(text) { 7 | text := StrReplace(text, "`n", "\n") 8 | text := StrReplace(text, "`r", "\r") 9 | text := StrReplace(text, "`t", "\t") 10 | text := RegExReplace(text, '([\\"])', "\$1") 11 | Return '"' text '"' 12 | } 13 | CurrentKeyboardLayout() { 14 | hActive := DllCall("GetForegroundWindow") 15 | actTID := DllCall("GetWindowThreadProcessId", "UPtr", hActive, "UPtr", 0) 16 | Return DllCall("GetKeyboardLayout", "UInt", actTID) 17 | } 18 | KeyboardLayoutJson() { 19 | lKeyboard := CurrentKeyboardLayout() 20 | data := '{"0":{"vk":0, "name":""}' 21 | Loop 325 22 | { 23 | vk := ScToVk(A_Index, lKeyboard) 24 | name := GetKeyName(vk, A_Index, lKeyboard) 25 | 26 | If (vk != 0) { 27 | data .= ',"' A_Index '":{"vk":' vk ',"name":' JsonString(name) '}' 28 | } 29 | } 30 | data .= "}" 31 | return data 32 | } 33 | ScToVk(sc, lKeyboard) { 34 | ; Calling MapVirtualKeyExW directly produces the wrong result in some cases 35 | ; Tables taken from vk_to_sc in keyboard_mouse.cpp 36 | vk := 0 37 | Switch sc { 38 | Case 0x02A: ; SC_LSHIFT 39 | vk := 1 * 0xA0 ; VK_LSHIFT 40 | Case 0x136: ; SC_RSHIFT 41 | vk := 1 * 0xA1 ; VK_RSHIFT 42 | Case 0x01D: ; SC_LCONTROL 43 | vk := 1 * 0xA2 ; VK_LCONTROL 44 | Case 0x11D: ; SC_RCONTROL 45 | vk := 1 * 0xA3 ; VK_RCONTROL 46 | Case 0x038: ; SC_LALT 47 | vk := 1 * 0xA4 ; VK_LMENU 48 | Case 0x138: ; SC_RALT 49 | vk := 1 * 0xA5 ; VK_RMENU 50 | Case 0x15B: ; SC_LWIN 51 | vk := 1 * 0x5B ; VK_LWIN 52 | Case 0x15C: ; SC_RWIN 53 | vk := 1 * 0x5C ; VK_RWIN 54 | Case 0x045: ; SC_PAUSE 55 | vk := 1 * 0x13 ; VK_PAUSE 56 | Case 0x52: ; SC_NUMPAD0 57 | vk := 1 * 0x60 ; VK_NUMPAD0 58 | Case 0x4F: ; SC_NUMPAD1 59 | vk := 1 * 0x61 ; VK_NUMPAD1 60 | Case 0x50: ; SC_NUMPAD2 61 | vk := 1 * 0x62 ; VK_NUMPAD2 62 | Case 0x51: ; SC_NUMPAD3 63 | vk := 1 * 0x63 ; VK_NUMPAD3 64 | Case 0x4B: ; SC_NUMPAD4 65 | vk := 1 * 0x64 ; VK_NUMPAD4 66 | Case 0x4C: ; SC_NUMPAD5 67 | vk := 1 * 0x65 ; VK_NUMPAD5 68 | Case 0x4D: ; SC_NUMPAD6 69 | vk := 1 * 0x66 ; VK_NUMPAD6 70 | Case 0x47: ; SC_NUMPAD7 71 | vk := 1 * 0x67 ; VK_NUMPAD7 72 | Case 0x48: ; SC_NUMPAD8 73 | vk := 1 * 0x68 ; VK_NUMPAD8 74 | Case 0x49: ; SC_NUMPAD9 75 | vk := 1 * 0x69 ; VK_NUMPAD9 76 | Case 0x53: ; SC_NUMPADDOT 77 | vk := 1 * 0x6E ; VK_DECIMAL 78 | Case 0x145: ; SC_NUMLOCK 79 | vk := 1 * 0x90 ; VK_NUMLOCK 80 | Case 0x135: ; SC_NUMPADDIV 81 | vk := 1 * 0x6F ; VK_DIVIDE 82 | Case 0x037: ; SC_NUMPADMULT 83 | vk := 1 * 0x6A ; VK_MULTIPLY 84 | Case 0x04A: ; SC_NUMPADSUB 85 | vk := 1 * 0x6D ; VK_SUBTRACT 86 | Case 0x04E: ; SC_NUMPADADD 87 | vk := 1 * 0x6B ; VK_ADD 88 | Case 0x137: ; SC_PRINTSCREEN 89 | vk := 1 * 0x2C ; VK_SNAPSHOT 90 | Default: 91 | vk := DllCall("MapVirtualKeyExW", "UInt", A_Index, "UInt", 1, "UPtr", lKeyboard) ; 1=MAPVK_VSC_TO_VK 92 | } 93 | Return vk 94 | } 95 | GetKeyName(vk, sc, lKeyboard) { 96 | Switch sc { 97 | Case 1, 14, 15, 28, 29, 42, 54: ; Esc, BackSpace, Tab, Enter, LCtrl, LShift, RShift 98 | Return ScToKeyName(sc) 99 | Case 86: ; OEM-102 key 100 | Return VkToChar(vk, lKeyboard) 101 | Default: 102 | If (sc >= 54) { 103 | Return ScToKeyName(sc) 104 | } Else { 105 | name := VkToChar(vk, lKeyboard) 106 | If (name = "") { 107 | Return ScToKeyName(sc) 108 | } Else { 109 | Return name 110 | } 111 | } 112 | } 113 | } 114 | ScToKeyName(sc) { ; for the current keyboard layout 115 | ; https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getkeynametextw 116 | VarSetStrCapacity(&name, 64) 117 | size := DllCall("GetKeyNameTextW", "Int64", A_Index << 16, "Str", name, "Int", 64) 118 | Return name 119 | } 120 | VkToChar(vk, lKeyboard) { 121 | ; As in keyboard_mouse.cpp from AHK sources (except we don't restore active dead keys) 122 | ; Given a VK find the character of the corresponding key 123 | 124 | If (vk < 0x41 || vk > 0x5A) { ; outside A-Z range, MapVirtualKeyEx gives the correct result 125 | Return Chr(0xFFFF & DllCall("MapVirtualKeyExW", "UInt", vk, "UInt", 2, "UPtr", lKeyboard, "UInt")) ; 2=MAPVK_VK_TO_CHAR 126 | } 127 | ; 0x6E = VK_DECIMAL 128 | keyState := 0 129 | VarSetStrCapacity(&chNotUsed, 3) 130 | keyState := Buffer(256, 0) 131 | VarSetStrCapacity(&ch, 3) 132 | deadChar := 0 133 | flags := 0 134 | ; Extract pending dead key if any 135 | If (DllCall("ToUnicodeEx", "UInt", 0x6E, "UInt", 0, "Ptr", keyState, "Str", ch, "Int", 2, "UInt", flags, "UPtr", lKeyboard) == 2) { 136 | deadChar := ch 137 | } 138 | ; Get character corresponding to the vk 139 | n := DllCall("ToUnicodeEx", "UInt", vk, "UInt", 0, "Ptr", keyState, "Str", ch, "Int", 2, "UInt", flags, "UPtr", lKeyboard) 140 | ;MsgBox("vk" vk " n" n " " ch) 141 | If (n < 0) { ; is a dead key, flush it 142 | DllCall("ToUnicodeEx", "UInt", 0x6E, "UInt", 0, "Ptr", keyState, "Str", chNotUsed, "Int", 2, "UInt", flags, "UPtr", lKeyboard) 143 | } 144 | If (n != 0) { 145 | Return ch 146 | } Else { 147 | Return "" 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lib/monitor.ahk: -------------------------------------------------------------------------------- 1 | ; Functions for positioning GUIs in a multi-monitor setup 2 | ; © Gilles Waeber 2023 - MIT License 3 | #Requires AutoHotkey 2.0 4 | 5 | A_Scaling := A_ScreenDPI / 96 6 | 7 | GetMouseMonitor() { 8 | ; Get the index of monitor on which the mouse cursor is located 9 | CoordMode("Mouse", "Screen") 10 | MouseGetPos(&mx, &my) 11 | count := SysGet(80) 12 | 13 | Loop count { 14 | MonitorGet(A_Index, &left, &top, &right, &bottom) 15 | if (left <= mx && mx <= right && top <= my && my <= bottom) { 16 | Return A_Index 17 | } 18 | } 19 | Return 1 20 | } 21 | 22 | -------------------------------------------------------------------------------- /lib/thqby/ComVar.ahk: -------------------------------------------------------------------------------- 1 | ; Construction and deconstruction VARIANT struct 2 | class ComVar { 3 | /** 4 | * Construction VARIANT struct, `ptr` property points to the address, `__Item` property returns var's Value 5 | * @param vVal Values that need to be wrapped, supports String, Integer, Double, Array, ComValue, ComObjArray 6 | * ### example 7 | * `var1 := ComVar('string'), MsgBox(var1[])` 8 | * 9 | * `var2 := ComVar([1,2,3,4], , true)` 10 | * 11 | * `var3 := ComVar(ComValue(0xb, -1))` 12 | * @param vType Variant's type, VT_VARIANT(default) 13 | * @param convert Convert AHK's array to ComObjArray 14 | */ 15 | __New(vVal := unset, vType := 0xC, convert := false) { 16 | static size := 8 + 2 * A_PtrSize 17 | this.var := Buffer(size, 0), this.owner := true 18 | this.ref := ComValue(0x4000 | vType, this.var.Ptr + (vType = 0xC ? 0 : 8)) 19 | if IsSet(vVal) { 20 | if (Type(vVal) == "ComVar") { 21 | this.var := vVal.var, this.ref := vVal.ref, this.obj := vVal, this.owner := false 22 | } else { 23 | if (IsObject(vVal)) { 24 | if (vType != 0xC) 25 | this.ref := ComValue(0x400C, this.var.ptr) 26 | if convert && (vVal is Array) { 27 | switch Type(vVal[1]) { 28 | case "Integer": vType := 3 29 | case "String": vType := 8 30 | case "Float": vType := 5 31 | case "ComValue", "ComObject": vType := ComObjType(vVal[1]) 32 | default: vType := 0xC 33 | } 34 | ComObjFlags(obj := ComObjArray(vType, vVal.Length), -1), i := 0, this.ref[] := obj 35 | for v in vVal 36 | obj[i++] := v 37 | } else 38 | this.ref[] := vVal 39 | } else 40 | this.ref[] := vVal 41 | } 42 | } 43 | } 44 | __Delete() => (this.owner ? DllCall("oleaut32\VariantClear", "ptr", this.var) : 0) 45 | __Item { 46 | get => this.ref[] 47 | set => this.ref[] := value 48 | } 49 | Ptr => this.var.Ptr 50 | Size => this.var.Size 51 | Type { 52 | get => NumGet(this.var, "ushort") 53 | set { 54 | if (!this.IsVariant) 55 | throw PropertyError("VarType is not VT_VARIANT, Type is read-only.", -2) 56 | NumPut("ushort", Value, this.var) 57 | } 58 | } 59 | IsVariant => ComObjType(this.ref) & 0xC 60 | } -------------------------------------------------------------------------------- /lib/thqby/WebView2/32bit/WebView2Loader.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilleswaeber/emoji-keyboard/de2ddb5ca4dfd1880d2323be00137abfa21caaa7/lib/thqby/WebView2/32bit/WebView2Loader.dll -------------------------------------------------------------------------------- /lib/thqby/WebView2/64bit/WebView2Loader.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilleswaeber/emoji-keyboard/de2ddb5ca4dfd1880d2323be00137abfa21caaa7/lib/thqby/WebView2/64bit/WebView2Loader.dll -------------------------------------------------------------------------------- /lib/thqby/WebView2/README.md: -------------------------------------------------------------------------------- 1 | ## WebView2 2 | 3 | The Microsoft Edge WebView2 control enables you to host web content in your application using Microsoft Edge (Chromium) as the rendering engine. For more information, see Overview of [Microsoft Edge WebView2](https://docs.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/?view=webview2-1.0.674-prerelease) and Getting Started with WebView2. 4 | 5 | The WebView2 Runtime is built into Win10(latest version) and Win11 and can be easily used in AHK. 6 | 7 | #### Example1: AddHostObjectToEdge, Open with multiple windows 8 | ```autohotkey 9 | #Include 10 | 11 | main := Gui('+Resize') 12 | main.OnEvent('Close', (*) => (wvc := wv := 0)) 13 | main.Show(Format('w{} h{}', A_ScreenWidth * 0.6, A_ScreenHeight * 0.6)) 14 | 15 | wvc := WebView2.create(main.Hwnd) 16 | wv := wvc.CoreWebView2 17 | wv.Navigate('https://autohotkey.com') 18 | wv.AddHostObjectToScript('ahk', {str:'str from ahk',func:MsgBox}) 19 | wv.OpenDevToolsWindow() 20 | ``` 21 | 22 | Run code in Edge DevTools 23 | ```javascript 24 | obj = await window.chrome.webview.hostObjects.ahk; 25 | obj.func('call from edge\n' + (await obj.str)); 26 | ``` 27 | 28 | #### Example2: Open with only one Tab 29 | ```autohotkey 30 | #Include 31 | 32 | main := Gui("+Resize") 33 | main.OnEvent("Close", ExitApp) 34 | main.Show(Format("w{} h{}", A_ScreenWidth * 0.6, A_ScreenHeight * 0.6)) 35 | 36 | wvc := WebView2.create(main.Hwnd) 37 | wv := wvc.CoreWebView2 38 | nwr := wv.NewWindowRequested(NewWindowRequestedHandler) 39 | wv.Navigate('https://autohotkey.com') 40 | 41 | NewWindowRequestedHandler(handler, wv2, arg) { 42 | argp := WebView2.NewWindowRequestedEventArgs(arg) 43 | deferral := argp.GetDeferral() 44 | argp.NewWindow := wv2 45 | deferral.Complete() 46 | } 47 | ``` 48 | 49 | #### Example3: Open with multiple Tabs in a window 50 | ```autohotkey 51 | #Include 52 | 53 | main := Gui("+Resize"), main.MarginX := main.MarginY := 0 54 | main.OnEvent("Close", _exit_) 55 | main.OnEvent('Size', gui_size) 56 | tab := main.AddTab2(Format("w{} h{}", A_ScreenWidth * 0.6, A_ScreenHeight * 0.6), ['tab1']) 57 | tab.UseTab(1), tabs := [] 58 | tabs.Push(ctl := main.AddText('x0 y25 w' (A_ScreenWidth * 0.6) ' h' (A_ScreenHeight * 0.6))) 59 | tab.UseTab() 60 | main.Show() 61 | ctl.wvc := wvc := WebView2.create(ctl.Hwnd) 62 | wv := wvc.CoreWebView2 63 | ctl.nwr := wv.NewWindowRequested(NewWindowRequestedHandler) 64 | wv.Navigate('https://autohotkey.com') 65 | 66 | gui_size(GuiObj, MinMax, Width, Height) { 67 | if (MinMax != -1) { 68 | tab.Move(, , Width, Height) 69 | for t in tabs { 70 | t.move(, , Width, Height - 23) 71 | try t.wvc.Fill() 72 | } 73 | } 74 | } 75 | 76 | NewWindowRequestedHandler(handler, wv2, arg) { 77 | argp := WebView2.NewWindowRequestedEventArgs(arg) 78 | deferral := argp.GetDeferral() 79 | tab.Add(['tab' (i := tabs.Length + 1)]) 80 | tab.UseTab(i), tab.Choose(i) 81 | main.GetClientPos(,,&w,&h) 82 | tabs.Push(ctl := main.AddText('x0 y25 w' w ' h' (h - 25))) 83 | tab.UseTab() 84 | ctl.wvc := WebView2.create(ctl.Hwnd, ControllerCompleted_Invoke, WebView2.Core(wv2).Environment) 85 | return 0 86 | ControllerCompleted_Invoke(wvc) { 87 | argp.NewWindow := wv := wvc.CoreWebView2 88 | ctl.nwr := wv.NewWindowRequested(NewWindowRequestedHandler) 89 | deferral.Complete() 90 | } 91 | } 92 | 93 | _exit_(*) { 94 | for t in tabs 95 | t.wvc := t.nwr := 0 96 | ExitApp() 97 | } 98 | ``` 99 | 100 | #### Example4: PrintToPDF 101 | ``` 102 | #Include 103 | 104 | g := Gui() 105 | g.Show('w800 h600') 106 | wvc := WebView2.create(g.Hwnd) 107 | wv := wvc.CoreWebView2 108 | wv.Navigate('https://autohotkey.com') 109 | MsgBox('Wait for loading to complete') 110 | set := wv.Environment.CreatePrintSettings() 111 | set.Orientation := WebView2.PRINT_ORIENTATION.LANDSCAPE 112 | wv.PrintToPdf(A_ScriptDir '\11.pdf', set, WebView2.Handler(handler)) 113 | 114 | handler(handlerptr, result, success) { 115 | if (!success) 116 | MsgBox 'PrintToPdf fail`nerr: ' result 117 | } 118 | ``` -------------------------------------------------------------------------------- /res/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilleswaeber/emoji-keyboard/de2ddb5ca4dfd1880d2323be00137abfa21caaa7/res/data/.gitkeep -------------------------------------------------------------------------------- /res/local_names.ahk: -------------------------------------------------------------------------------- 1 | LoadString(hInstance, uID) { 2 | ; source: https://github.com/Ixiko/AHK-libs-and-classes-collection/blob/master/libs/g-n/LoadString.ahk 3 | ; https://msdn.microsoft.com/en-us/library/windows/desktop/ms647486(v=vs.85).aspx 4 | Local Size, p, String 5 | If (!(Size := DllCall("LoadStringW", "Ptr", hInstance, "UInt", Abs(uID), "UPtrP", p, "Int", 0))) { 6 | ErrorLevel := TRUE 7 | Return ("") 8 | } 9 | 10 | String := StrGet(p, Size, "UTF-16") 11 | ErrorLevel := FALSE 12 | 13 | Return (String) 14 | } 15 | 16 | data := ("{") 17 | Loop, Files, C:\Windows\System32\*, D 18 | { 19 | If (FileExist(A_LoopFilePath . "\getuname.dll.mui")) { 20 | data .= ("""" . A_LoopFileName . """: {""-1"":""""") 21 | hInstance := DllCall("LoadLibraryExW", "Str", A_LoopFilePath . "\getuname.dll.mui", "UInt", 0, "UInt", 0x2, "Ptr") 22 | Loop, 65536 { 23 | i := A_Index - 1 24 | name := LoadString(hInstance, i) 25 | name := StrReplace(name, "\", "\\") 26 | name := StrReplace(name, """", "\""") 27 | If (!ErrorLevel) { 28 | data .= (",""" . i . """:""" . name . """") 29 | } 30 | } 31 | DllCall("Kernel32.dll\FreeLibrary", "Ptr", hInstance) 32 | data .= ("},") 33 | } 34 | } 35 | data .= ("""none"":{}}") 36 | dest := FileOpen("winchars.json", "w", "UTF-8") 37 | dest.Write(data) 38 | dest.Close() 39 | -------------------------------------------------------------------------------- /res/test.ahk: -------------------------------------------------------------------------------- 1 | ; EnumResources( Filename, Type [, LabelOrFunctionName ] ) 2 | ; Requires AHK v1.0.47.06. 3 | 4 | ; Type: 5 | ; See MSDN: Resource Types - 6 | ; http://msdn.microsoft.com/en-us/library/ms648009(VS.85).aspx 7 | EnumResources(Filename, Type, Label="") 8 | { 9 | hmod := DllCall("GetModuleHandle", "str", Filename) 10 | ; If the DLL isn't already loaded, load it as a data file. 11 | loaded := !hmod 12 | && hmod := DllCall("LoadLibraryEx", "str", Filename, "uint", 0, "uint", 0x2) 13 | 14 | enumproc := RegisterCallback("EnumResources_callback","F") 15 | param := 0 16 | MsgBox % VarSetCapacity(param,16,0) 17 | NumPut(&Label, param) 18 | MsgBox % NumGet(param, 0, "Ptr") 19 | ; Enumerate the resources. 20 | DllCall("EnumResourceNamesW", "uint", hmod, "uint", Type, "Ptr", enumproc, "Ptr", ¶m) 21 | DllCall("GlobalFree", "uint", enumproc) 22 | 23 | ; If we loaded the DLL, free it now. 24 | if loaded 25 | DllCall("FreeLibrary", "uint", hmod) 26 | 27 | return NumGet(param, 8, "Int") 28 | } 29 | 30 | EnumResources_callback(hModule, lpszType, lpszName, lParam) 31 | { 32 | NumPut(1 + NumGet(lParam, 8, "Int"), lParam, 8, "Int") 33 | if (lpszName >> 16 != 0) 34 | lpszName := DllCall("MulDiv","int",lpszName,"int",1,"int",1,"str") 35 | Label := DllCall("MulDiv","int",NumGet(lParam+0),"int",1,"int",1,"str") 36 | if Label != 37 | { 38 | if IsLabel(Label) 39 | { ; ErrorLevel = resource ID or name. 40 | ErrorLevel := lpszName 41 | gosub %Label% 42 | } 43 | else 44 | %Label%(lpszName) ; Dynamic function call. Requires AHK v1.0.47.06. 45 | } 46 | return ErrorLevel!="#stop" 47 | } 48 | 49 | ;MsgBox %A_AhkPath% 50 | NumIcons := EnumResources(A_AhkPath, 14, "EnumGroupIcons_callback") 51 | MsgBox % NumIcons " icons::`n`n" Icons 52 | ExitApp 53 | 54 | EnumGroupIcons_callback(resource_name) { 55 | global Icons .= resource_name "`n" 56 | } 57 | -------------------------------------------------------------------------------- /res/test2.ahk: -------------------------------------------------------------------------------- 1 | NumIcons := EnumResources(A_AhkPath, 14, "EnumGroupIcons") 2 | MsgBox % NumIcons " icons:`n`n" Icons 3 | exitapp 4 | 5 | EnumGroupIcons: 6 | Icons .= ErrorLevel "`n" 7 | return 8 | 9 | EnumResources(Filename, Type, Label) 10 | { 11 | static uLabel 12 | MsgBox % A_EventInfo 13 | 14 | If ( A_EventInfo = 0 ) { ; when called by user 15 | hmod := DllCall("GetModuleHandle", "str", Filename) 16 | ; If the DLL isn't already loaded, load it as a data file. 17 | loaded := !hmod 18 | && hmod := DllCall("LoadLibraryEx", "str", Filename, "uint", 0, "uint", 0x2) 19 | 20 | enumproc := RegisterCallback(A_ThisFunc,"F") 21 | uLabel := Label 22 | 23 | ; Enumerate the resources. 24 | DllCall("EnumResourceNames", "uint", hmod, "uint", Type, "uint", enumproc) 25 | DllCall("GlobalFree", "uint", enumproc) 26 | 27 | ; If we loaded the DLL, free it now. 28 | if loaded 29 | DllCall("FreeLibrary", "uint", hmod) 30 | 31 | uLabel := "" 32 | return NumGet(param,4) 33 | } 34 | 35 | If ( A_EventInfo != 0 ) { ; when called from invoking callback 36 | ; Alias for Lex's EnumResources_callback param 37 | lpszName:=Label 38 | 39 | if uLabel != 40 | { 41 | if IsLabel(uLabel) 42 | { ; ErrorLevel = resource ID or name. 43 | ErrorLevel := lpszName 44 | gosub %uLabel% 45 | } 46 | else 47 | %uLabel%(lpszName) ; Dynamic function call. Requires AHK v1.0.47.06. 48 | } 49 | return ErrorLevel!="#stop" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /wwwassets/build-unidata.js: -------------------------------------------------------------------------------- 1 | import {nodeBuild} from "./dist/emoji-keyboard.js"; 2 | import {promises as fs} from "fs"; 3 | 4 | const ahk_file = await fs.readFile("../App.ahk", 'utf8'); 5 | const ucdVersion = /\nUCD_VERSION := "([^"]+)"[\r\n]/.exec(ahk_file)[1]; 6 | const emojiVersion = /\nEMOJI_VERSION := "([^"]+)"[\r\n]/.exec(ahk_file)[1]; 7 | const cldrVersion = /\nCLDR_VERSION := "([^"]+)"[\r\n]/.exec(ahk_file)[1]; 8 | 9 | console.log("Building unidata using"); 10 | console.log(" UCD:", ucdVersion); 11 | console.log(" EMOJI:", emojiVersion); 12 | console.log(" CLDR:", cldrVersion); 13 | 14 | const u = await nodeBuild({ucdVersion, emojiVersion, cldrVersion}); 15 | const emojiCount = u.groups.map(g => g.sub.reduce((v, s) => v + (s.clusters?.length ?? 0), 0)).reduce((a, b) => a + b, 0); 16 | console.log( 17 | `Finished building “${u.name}”\n` + 18 | ` – ${u.chars.length} codepoints in ${u.blocks.length} blocks\n` + 19 | ` – ${u.clusters.length} grapheme clusters with length > 1\n` + 20 | ` – ${u.groups.length} emoji groups with ${emojiCount} base emojis\n\n` 21 | ) 22 | 23 | await fs.mkdir("data", {recursive: true}); 24 | await fs.writeFile("data/unidata.js", `// This file is generated by the builder script. Do not edit. 25 | // To rebuild, click Settings > Tools > Build in the app or run 'npm run build-unidata' in the wwwassets folder 26 | var unicodeData = ${JSON.stringify(u, null, 2)}`, 'utf8'); 27 | 28 | await fs.writeFile("script/unidata.tmp.d.ts", `// This file is generated by the builder script. Do not edit. 29 | // To rebuild, click Settings > Tools > Build in the app or run 'npm run build-unidata' in the wwwassets folder 30 | export type UnicodeEmojiGroup = 31 | ${u.groups.flatMap(g => g.sub.flatMap(s => `{group: ${JSON.stringify(g.name)}, subGroup: ${JSON.stringify(s.name)}}` 32 | )).join('\n\t| ')}; 33 | `, 'utf8'); 34 | -------------------------------------------------------------------------------- /wwwassets/fallback_fonts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilleswaeber/emoji-keyboard/de2ddb5ca4dfd1880d2323be00137abfa21caaa7/wwwassets/fallback_fonts/.gitkeep -------------------------------------------------------------------------------- /wwwassets/fonts/AdobeBlank.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilleswaeber/emoji-keyboard/de2ddb5ca4dfd1880d2323be00137abfa21caaa7/wwwassets/fonts/AdobeBlank.otf -------------------------------------------------------------------------------- /wwwassets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ⌨ Emoji Keyboard 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /wwwassets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "emoji-keyboard", 3 | "version": "1.0.0", 4 | "author": "Gilles Waeber", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "@types/react": "^19.1.1", 8 | "@types/react-dom": "^19.1.2", 9 | "typescript": "^5.8.3", 10 | "vite": "^6.2.6" 11 | }, 12 | "dependencies": { 13 | "memoize-one": "^6.0.0", 14 | "peggy": "^4.2.0", 15 | "preact": "^10.26.5" 16 | }, 17 | "scripts": { 18 | "dev": "vite build --watch --sourcemap inline", 19 | "build": "vite build", 20 | "build-unidata": "node build-unidata.js" 21 | }, 22 | "type": "module" 23 | } 24 | -------------------------------------------------------------------------------- /wwwassets/plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilleswaeber/emoji-keyboard/de2ddb5ca4dfd1880d2323be00137abfa21caaa7/wwwassets/plugins/.gitkeep -------------------------------------------------------------------------------- /wwwassets/script/ahk.ts: -------------------------------------------------------------------------------- 1 | import {ConsolidatedUnicodeData} from "./builder/consolidated"; 2 | import {AppConfig} from "./config"; 3 | 4 | type HostObject = { 5 | downloadUnicode(callbackNumber: number): void; 6 | hide(): void; 7 | loaded(): void; 8 | openDevTools(): void; 9 | openLink(url: string): void; 10 | reload(): void; 11 | saveConfig(config: string): void; 12 | saveUnicodeData(data: string, types: string): void; 13 | send(text: string): void; 14 | setOpenAt(at: string): void; 15 | setOpacity(opacity: number): void; 16 | setTitle(title: string): void; 17 | setSearch(enable: boolean): void; 18 | setPosSize(x: number, y: number, width: number, height: number): void; 19 | ready(): void; 20 | versions(): { 21 | cldr: Promise; 22 | emoji: Promise; 23 | ucd: Promise; 24 | } 25 | }; 26 | export type AhkVersions = { 27 | cldr: string; 28 | emoji: string; 29 | ucd: string; 30 | }; 31 | let AHK: HostObject | null = null; 32 | if (typeof window !== "undefined") { 33 | (window as any).chrome?.webview?.hostObjects?.ahk.then((ahk: HostObject) => AHK = ahk); 34 | } 35 | 36 | function isAHK(): boolean { 37 | return AHK !== null; 38 | } 39 | 40 | let nextNumber = 0; 41 | 42 | /** 43 | * Wait for a callback from the host application, only handles ok/error with message. 44 | * This system is necessary since all exchanges with the host application are asynchronous 45 | */ 46 | function waitForCallback(): [number, Promise] { 47 | const c = nextNumber++; 48 | return [c, new Promise((resolve, reject) => { 49 | const ok = `done,${c},`; 50 | const error = `error,${c},`; 51 | const listener = (e: MessageEvent) => { 52 | if (typeof e.data === 'string') { 53 | if (e.data.startsWith(ok)) { 54 | resolve(); 55 | (window as any).chrome?.webview?.removeEventListener('message', listener); 56 | } 57 | if (e.data.startsWith(error)) { 58 | reject(e.data.substring(error.length)); 59 | (window as any).chrome?.webview?.removeEventListener('message', listener); 60 | } 61 | } 62 | }; 63 | (window as any).chrome?.webview?.addEventListener('message', listener); 64 | AHK!.downloadUnicode(c); 65 | })]; 66 | } 67 | 68 | export async function ahkDownloadUnicode() { 69 | if (isAHK()) { 70 | const [c, p] = waitForCallback(); 71 | setTimeout(() => AHK!.downloadUnicode(c), 10); 72 | return p; 73 | } else console.log("DownloadUnicode"); 74 | } 75 | 76 | export function ahkHide() { 77 | if (isAHK()) AHK!.hide(); 78 | else console.log("Hide"); 79 | } 80 | 81 | export function ahkTitle(title: string) { 82 | if (isAHK()) AHK!.setTitle(title); 83 | else document.title = title; 84 | } 85 | 86 | export async function ahkVersions(): Promise { 87 | if (isAHK()) { 88 | const v = AHK!.versions(); 89 | return { 90 | cldr: await v.cldr, 91 | emoji: await v.emoji, 92 | ucd: await v.ucd, 93 | }; 94 | } else { 95 | console.log("Versions"); 96 | return Promise.reject("Not in AHK"); 97 | } 98 | } 99 | 100 | export function ahkLoaded() { 101 | if (isAHK()) AHK!.loaded(); 102 | else console.log("Loaded"); 103 | } 104 | 105 | export function ahkReady() { 106 | if (isAHK()) AHK!.ready(); 107 | else console.log("Ready"); 108 | } 109 | 110 | export function ahkReload() { 111 | if (isAHK()) AHK!.reload(); 112 | else document.location.reload(); 113 | } 114 | 115 | export function ahkSend(text: string) { 116 | if (isAHK()) AHK!.send(text); 117 | else console.log("Send", text); 118 | } 119 | 120 | export function ahkSetSearch(state: boolean) { 121 | if (isAHK()) AHK!.setSearch(state); 122 | else console.log("SetSearch", state); 123 | } 124 | 125 | export function ahkSaveConfig(config: AppConfig) { 126 | if (isAHK()) AHK!.saveConfig(JSON.stringify(config, null, 4)); 127 | else console.log("SaveConfig", config); 128 | } 129 | 130 | export function ahkSaveUnicodeData(data: ConsolidatedUnicodeData) { 131 | if (isAHK()) { 132 | AHK!.saveUnicodeData( 133 | JSON.stringify(data), 134 | 'export type UnicodeEmojiGroup = \n\t' + data.groups.flatMap(g => g.sub.flatMap( 135 | s => `{group: ${JSON.stringify(g.name)}, subGroup: ${JSON.stringify(s.name)}}` 136 | )).join('\n\t| ') + ';\n' 137 | ); 138 | } 139 | else console.log("SaveUnicodeData", data); 140 | } 141 | 142 | export function ahkSetPosSize(x: number, y: number, width: number, height: number) { 143 | if (isAHK()) AHK!.setPosSize(x, y, width, height); 144 | else console.log("SetPosSize", x, y, width, height); 145 | } 146 | 147 | export function ahkSetOpenAt(at: string) { 148 | if (isAHK()) AHK!.setOpenAt(at); 149 | else console.log("SetOpenAt", at); 150 | } 151 | 152 | export function ahkSetOpacity(opacity: number) { 153 | if (isAHK()) AHK!.setOpacity(opacity); 154 | else console.log("SetOpacity", opacity); 155 | } 156 | 157 | export function ahkOpenDevTools() { 158 | if (isAHK()) AHK!.openDevTools(); 159 | else console.log("OpenDevTools") 160 | } 161 | 162 | export function ahkOpenLink(url: string) { 163 | if (isAHK()) AHK!.openLink(url); 164 | else window.open(url); 165 | } 166 | -------------------------------------------------------------------------------- /wwwassets/script/app.tsx: -------------------------------------------------------------------------------- 1 | import {Component, h, options, render} from 'preact'; 2 | import {AnsiLayout, IsoLayout, Layout, SystemLayout, SystemLayoutUS} from "./layout"; 3 | import {Board, getMainBoard} from "./board"; 4 | import {Version} from "./osversion"; 5 | import { 6 | ahkHide, 7 | ahkLoaded, 8 | ahkOpenDevTools, 9 | ahkReady, 10 | ahkSaveConfig, 11 | ahkSend, 12 | ahkSetOpacity, 13 | ahkSetOpenAt, 14 | ahkSetPosSize, 15 | ahkSetSearch, 16 | ahkTitle 17 | } from "./ahk"; 18 | import {search} from "./emojis"; 19 | import {AppConfig, ConfigBoard, DefaultConfig, DefaultThemeUrl, ThemesMap} from "./config"; 20 | import {fromEntries, unreachable} from "./helpers"; 21 | import { 22 | app, 23 | AppActions, 24 | ConfigBuildingContext, 25 | ConfigContext, 26 | LayoutContext, 27 | OSContext, 28 | PluginsContext, 29 | SearchContext, 30 | setApp 31 | } from "./appVar"; 32 | import {useMemo} from "preact/hooks"; 33 | import {SC} from "./layout/sc"; 34 | import {SearchBoard} from "./searchView"; 35 | import {BoardState, SlottedKeys} from "./boards/utils"; 36 | import {RecentBoard} from "./recentsView"; 37 | import {increaseRecent} from "./recentsActions"; 38 | import {Plugin, PluginData} from "./config/boards"; 39 | import {naturalCompare} from "./utils/compare"; 40 | import {IconFallback} from './config/fallback'; 41 | import {supportsRequirements} from './utils/emojiSupportTest'; 42 | import {UnicodeData} from "./unicodeInterface"; 43 | 44 | export const enum AppMode { 45 | MAIN = 0, 46 | SEARCH = 1, 47 | SETTINGS = 2, 48 | RECENTS = 3, 49 | } 50 | 51 | const AppModes = [AppMode.MAIN, AppMode.SEARCH, AppMode.SETTINGS, AppMode.RECENTS] as const; 52 | 53 | type AppState = { 54 | mode: AppMode; 55 | searchText: string; 56 | layout: SystemLayout; 57 | config: AppConfig; 58 | os: Version; 59 | boardState: Record; 60 | currentBoard: { [mode in AppMode]: Board }; 61 | /** oldest at the end */ 62 | parentBoards: { [mode in AppMode]: Board[] }; 63 | /** building in progress */ 64 | building: boolean; 65 | plugins: Plugin[]; 66 | } 67 | 68 | class App extends Component<{}, AppState> implements AppActions { 69 | keyHandlers: SlottedKeys = {} 70 | state: AppState = { 71 | mode: AppMode.MAIN, 72 | searchText: '', 73 | layout: SystemLayoutUS, 74 | config: {...DefaultConfig, opacity: 1}, 75 | boardState: {}, 76 | os: new Version('99'), 77 | currentBoard: { 78 | [AppMode.MAIN]: getMainBoard([]), 79 | [AppMode.SEARCH]: new SearchBoard(), 80 | [AppMode.SETTINGS]: new ConfigBoard(), 81 | [AppMode.RECENTS]: new RecentBoard(), 82 | }, 83 | parentBoards: fromEntries(AppModes.map(m => [m, []] as const)), 84 | building: false, 85 | plugins: [], 86 | } 87 | 88 | constructor(props: {}) { 89 | super(props); 90 | setApp(this); 91 | (window as any).document.ahk = this; 92 | (window as any).s = search; 93 | (window as any).chrome?.webview?.addEventListener('message', (e: MessageEvent) => { 94 | if (Array.isArray(e.data)) { 95 | switch (e.data[0]) { 96 | case 'input': 97 | this.input(e.data[1], e.data[2]); 98 | break; 99 | case 'layout': 100 | this.setSystemLayout(e.data[1]); 101 | break; 102 | case 'possize': 103 | this.updateConfig({x: e.data[1], y: e.data[2], width: e.data[3], height: e.data[4]}); 104 | break; 105 | default: 106 | console.log("message", e.data[0], e.data) 107 | } 108 | } else if (typeof e.data === 'string') { 109 | const command = e.data.split(',', 1)[0]; 110 | const rest = e.data.slice(command.length + 1); 111 | switch (command) { 112 | case 'config': 113 | this.setConfig(rest).catch(console.error); 114 | break; 115 | case 'defaultConfig': 116 | this.defaultConfig(); 117 | break; 118 | case 'os': 119 | this.setOS(rest); 120 | break; 121 | case 'plugin': 122 | this.loadPlugin(rest); 123 | break; 124 | case 'fonts': 125 | this.loadFallbackFonts(rest); 126 | break; 127 | case 'done': 128 | case 'error': 129 | console.log("async callback", command, rest); 130 | break; 131 | default: 132 | console.log("message", command, rest) 133 | } 134 | } else { 135 | console.log("message", e.data) 136 | } 137 | }); 138 | } 139 | 140 | render() { 141 | const s = this.state; 142 | const Board = s.currentBoard[s.mode]; 143 | const c = ; 144 | const l: Layout = useMemo(() => { 145 | const base = s.config.isoKeyboard ? IsoLayout : AnsiLayout; 146 | return {...base, sys: s.layout}; 147 | }, [s.config.isoKeyboard, s.layout]); 148 | return 149 | 150 | 151 | 152 | 153 | 154 |
{c}
155 |
156 |
157 |
158 |
159 |
160 |
; 161 | } 162 | 163 | public setMode(mode: AppMode) { 164 | this.setState((s) => { 165 | if (s.mode != mode) { 166 | if (mode == AppMode.SEARCH && !s.parentBoards[AppMode.SEARCH].length) ahkSetSearch(true); 167 | else ahkSetSearch(false); 168 | } 169 | return {mode}; 170 | }, () => { 171 | this.updateStatus(); 172 | }); 173 | } 174 | 175 | public updateStatus(text?: string) { 176 | const s = this.state; 177 | switch (s.mode) { 178 | case AppMode.MAIN: 179 | case AppMode.RECENTS: 180 | const board = s.currentBoard[s.mode]; 181 | ahkTitle(('Emoji Keyboard - ' + board.name + (text?.length ? ': ' + text : '')).replace(/[\s\n]+/g, ' ')); 182 | break; 183 | case AppMode.SEARCH: 184 | ahkTitle('Emoji Keyboard - ' + (text?.length ? text : 'Search')); 185 | break; 186 | case AppMode.SETTINGS: 187 | ahkTitle('Emoji Keyboard - ' + (text?.length ? text : 'Settings')); 188 | break; 189 | default: 190 | return unreachable(s.mode); 191 | } 192 | 193 | } 194 | 195 | public input(key: number, shift: boolean = false) { 196 | const s = this.state; 197 | switch (s.mode) { 198 | case AppMode.MAIN: 199 | case AppMode.SEARCH: 200 | case AppMode.SETTINGS: 201 | case AppMode.RECENTS: 202 | // animate keypress 203 | var keydiv = document.querySelector('[data-keycode="' + key + '"]') 204 | var symboldiv = keydiv?.getElementsByClassName("symbol")[0] 205 | symboldiv?.classList.add("keypress") 206 | setTimeout(() => { 207 | symboldiv?.classList.remove("keypress") 208 | }, 100) 209 | 210 | const k = this.keyHandlers[key as SC]; 211 | if (k) { 212 | if (shift) k.actAlternate(); 213 | else k.act(); 214 | } 215 | break; 216 | default: 217 | return unreachable(s.mode); 218 | } 219 | } 220 | 221 | public async setConfig(config: string) { 222 | await this.updateConfig(JSON.parse(config), false); 223 | ahkReady(); 224 | } 225 | 226 | public defaultConfig() { 227 | ahkSaveConfig(this.state.config); 228 | ahkSetOpacity(this.state.config.opacity); 229 | ahkReady(); 230 | } 231 | 232 | public getConfig(): AppConfig { 233 | return this.state.config; 234 | } 235 | 236 | public updateConfig(config: Partial | ((prev: AppConfig) => Partial), save = true): Promise { 237 | return new Promise((resolve) => { 238 | let noop = false; 239 | this.setState((s) => { 240 | if (typeof config === 'function') config = config(s.config); 241 | if (!Object.keys(config).length) { 242 | noop = true; 243 | return {}; 244 | } 245 | if (config.theme && s.config.theme != config.theme) { 246 | const link = document.getElementById('themeCSS') as HTMLLinkElement; 247 | link.href = ThemesMap.get(config.theme)?.url ?? DefaultThemeUrl; 248 | } 249 | if (config.openAt) { 250 | ahkSetOpenAt(config.openAt); 251 | } 252 | // must check explicitly as 0 is false 253 | if ((config.x != undefined && config.y != undefined && 254 | config.width != undefined && config.height != undefined)) { 255 | ahkSetPosSize(config.x, config.y, config.width, config.height); 256 | } 257 | if (config.opacity && s.config.opacity != config.opacity) { 258 | ahkSetOpacity(config.opacity); 259 | } 260 | if (config.devTools && !s.config.devTools) { 261 | ahkOpenDevTools(); 262 | } 263 | return {config: {...s.config, ...config}}; 264 | }, () => { 265 | if (save && !noop) ahkSaveConfig(this.state.config); 266 | resolve(); 267 | }); 268 | }) 269 | } 270 | 271 | public setOS(osString: string) { 272 | const os = new Version(osString.replace(/^WIN_/, '')); 273 | this.setState({os}) 274 | console.log("OS Set", os, IconFallback.map(f => ({...f, supported: supportsRequirements(f.requirement, os)}))); 275 | } 276 | 277 | public setSystemLayout(layout: SystemLayout) { 278 | this.setState({layout: {...SystemLayoutUS, ...layout}}); 279 | } 280 | 281 | public setSearchText(searchText: string) { 282 | this.setState({searchText}, 283 | () => this.updateStatus()); 284 | } 285 | 286 | public setBoard(board: Board): void { 287 | this.setState((s) => { 288 | if (s.mode == AppMode.SEARCH) ahkSetSearch(false); 289 | return { 290 | currentBoard: {...s.currentBoard, [s.mode]: board}, 291 | parentBoards: {...s.parentBoards, [s.mode]: [s.currentBoard[s.mode], ...s.parentBoards[s.mode]]}, 292 | } 293 | }, () => this.updateStatus()); 294 | } 295 | 296 | public setPage(page: number): void { 297 | this.setState((s) => { 298 | const board = s.currentBoard[s.mode]; 299 | return { 300 | boardState: { 301 | ...s.boardState, 302 | [board.name]: {page} 303 | } 304 | } 305 | }); 306 | } 307 | 308 | public setBuilding(building: boolean) { 309 | this.setState({building}); 310 | } 311 | 312 | public back(): void { 313 | this.setState((s) => { 314 | if (s.parentBoards[s.mode].length) { 315 | if (s.mode == AppMode.SEARCH) ahkSetSearch(s.parentBoards[s.mode].length == 1); 316 | const [board, ...parents] = s.parentBoards[s.mode]; 317 | return { 318 | parentBoards: {...s.parentBoards, [s.mode]: parents}, 319 | currentBoard: {...s.currentBoard, [s.mode]: board}, 320 | }; 321 | } 322 | return {}; 323 | }); 324 | } 325 | 326 | /** Go back to main board */ 327 | private main(): void { 328 | this.setState(s => ({ 329 | mode: AppMode.MAIN, 330 | currentBoard: { 331 | ...s.currentBoard, 332 | [AppMode.MAIN]: s.parentBoards[AppMode.MAIN][s.parentBoards[AppMode.MAIN].length - 1] ?? s.currentBoard[AppMode.MAIN] 333 | }, 334 | parentBoards: { 335 | ...s.parentBoards, 336 | [AppMode.MAIN]: [] 337 | } 338 | })); 339 | } 340 | 341 | public send(cluster: string, {noRecent, variantOf}: { noRecent?: boolean, variantOf?: string }): void { 342 | ahkSend(cluster); 343 | if (!noRecent) increaseRecent(cluster); 344 | if (variantOf) app().updateConfig(c => { 345 | if (c.preferredVariant[variantOf] != cluster) return { 346 | preferredVariant: { 347 | ...c.preferredVariant, 348 | [variantOf]: cluster 349 | } 350 | }; 351 | else return {}; 352 | }) 353 | if (this.state.config.hideAfterInput) ahkHide(); 354 | if (this.state.config.mainAfterInput) { 355 | if (this.state.mode == AppMode.RECENTS || this.state.mode == AppMode.MAIN) this.main(); 356 | } 357 | } 358 | 359 | private loadPlugin(rest: string) { 360 | const name = rest.split('\n', 1)[0]; 361 | const contents = rest.slice(name.length + 1); 362 | const data = JSON.parse(contents) as PluginData; 363 | const plugin: Plugin = { 364 | name, 365 | data 366 | } 367 | this.setState((s) => { 368 | const plugins = [...s.plugins, plugin]; 369 | plugins.sort((a, b) => naturalCompare(a.name, b.name)); 370 | const mainBoard = getMainBoard(plugins); 371 | return { 372 | plugins, 373 | currentBoard: { 374 | ...s.currentBoard, 375 | [AppMode.MAIN]: mainBoard, 376 | }, 377 | parentBoards: { 378 | ...s.parentBoards, 379 | [AppMode.MAIN]: [], 380 | } 381 | } 382 | }); 383 | } 384 | 385 | private loadFallbackFonts(rest: string) { 386 | const fonts = rest.split('\n') 387 | .filter(f => f.length && f.match(/.*\.(otf|ttf|woff2?|eot|svg)$/i)); 388 | const usedNames = new Set(); 389 | const faces = fonts.map(font => { 390 | let alias = font.replace(/.*[\\\/]([^\\\/]+)$/, '$1').replace(/[^a-z0-9]/ig, '_'); 391 | if (usedNames.has(alias)) { 392 | let i = 1; 393 | while (usedNames.has(alias + i)) i++; 394 | alias = alias + i; 395 | } 396 | usedNames.add(alias); 397 | return {alias, font}; 398 | }); 399 | const fFonts = faces.map(({alias}) => `${alias}, `).join(''); 400 | const style = document.createElement('style'); 401 | style.textContent = faces.map(({alias, font}) => `@font-face { 402 | font-family: "${alias}"; 403 | src: url("${font.replace(/([\\"])/g, '\\$1')}"); 404 | }`).join('\n\n') + ` 405 | :root { 406 | --f-fallback: ${fFonts} 'Cambria Math', Tahoma, Geneva, Verdana, sans-serif; 407 | }`; 408 | document.head.appendChild(style); 409 | console.log("Fallback fonts loaded", faces, style); 410 | } 411 | } 412 | 413 | export function startApp() { 414 | const main = document.querySelector('main') as HTMLElement; 415 | options.debounceRendering = f => setTimeout(f, 0); // Use setTimeout for debounce to avoid the use of the Promise polyfill, which causes issues with IE 416 | (window as any).u = UnicodeData; 417 | 418 | render(, main); 419 | setTimeout(() => ahkLoaded(), 0); 420 | } 421 | -------------------------------------------------------------------------------- /wwwassets/script/appVar.ts: -------------------------------------------------------------------------------- 1 | import {createContext} from "preact"; 2 | import type {AppMode} from "./app"; 3 | import type {Board} from "./board"; 4 | import type {AppConfig} from "./config"; 5 | import {AnsiLayout, Layout, SystemLayoutUS} from "./layout"; 6 | import {Version} from "./osversion"; 7 | import {SlottedKeys} from "./boards/utils"; 8 | import {Plugin} from "./config/boards"; 9 | 10 | export interface AppActions { 11 | keyHandlers: SlottedKeys; 12 | 13 | setBoard(board: Board): void; 14 | 15 | setSearchText(searchText: string): void; 16 | 17 | /** Get current config. Inside Preact, use useContext(ConfigContext) instead. */ 18 | getConfig(): AppConfig; 19 | 20 | updateConfig(config: Partial | ((prev: AppConfig) => Partial), save?: boolean): void; 21 | 22 | setPage(page: number): void; 23 | 24 | updateStatus(name?: string): void; 25 | 26 | setMode(mode: AppMode): void; 27 | 28 | back(): void; 29 | 30 | setBuilding(building: boolean): void; 31 | 32 | send(cluster: string, p: { noRecent?: boolean, variantOf?: string }): void; 33 | } 34 | 35 | let appVar: AppActions; 36 | 37 | export function setApp(app: AppActions) { 38 | appVar = app; 39 | } 40 | 41 | export function app(): AppActions { 42 | return appVar; 43 | } 44 | 45 | export const LayoutContext = createContext({...AnsiLayout, sys: SystemLayoutUS}); 46 | export const OSContext = createContext(new Version('99')); 47 | export const ConfigContext = createContext(undefined as any); 48 | export const ConfigBuildingContext = createContext(true); 49 | export const SearchContext = createContext(''); 50 | export const PluginsContext = createContext([]); 51 | -------------------------------------------------------------------------------- /wwwassets/script/board.tsx: -------------------------------------------------------------------------------- 1 | import {h, VNode} from "preact"; 2 | import {Layout} from "./layout"; 3 | import {useContext, useEffect, useMemo} from "preact/hooks"; 4 | import {EmojiKeyboard, isCluster, KeyboardItem, KeyCap, MAIN_BOARD, Plugin} from "./config/boards"; 5 | import {app, LayoutContext} from "./appVar"; 6 | import {BackKey, ClusterKey, ConfigKey, KeyboardKey, PageKey, RecentKey, SearchKey} from "./key"; 7 | import memoizeOne from "memoize-one"; 8 | import {clusterName} from "./unicodeInterface"; 9 | import {fromEntries} from "./helpers"; 10 | import {VK, VKAbbr, vkLookup} from "./layout/vk"; 11 | import {SC} from "./layout/sc"; 12 | import {BoardState, Keys, mapKeysToSlots, MAX_PAGE_KEYS, SlottedKeys} from "./boards/utils"; 13 | import {BlankKey, Key} from "./keys/base"; 14 | 15 | export function getMainBoard(plugins: Plugin[]): Board { 16 | const b = {...MAIN_BOARD}; 17 | const content = []; 18 | if (typeof b.content == 'function') content.push(...b.content()); 19 | else content.push(...(b.content ?? [])); 20 | 21 | for (const p of plugins) { 22 | if (p.data.boards) { 23 | content.push(...p.data.boards); 24 | } 25 | } 26 | return Board.fromEmoji({...b, content}); 27 | } 28 | 29 | function range(stop: number): number[] { 30 | return Array.from((new Array(stop)).keys()); 31 | } 32 | 33 | /** A board has several pages of keys */ 34 | export abstract class Board { 35 | protected constructor(p: { name: string, statusName?: string | undefined, symbol: KeyCap }) { 36 | this.name = p.name; 37 | this.statusName = p.statusName ?? p.name; 38 | this.symbol = p.symbol; 39 | } 40 | 41 | public readonly name: string; 42 | public readonly statusName: string; 43 | public readonly symbol: KeyCap; 44 | 45 | public abstract Contents(p: { state: BoardState | undefined }): VNode; 46 | 47 | private static fromKeys( 48 | {name, statusName, symbol, content, top, noRecent, byRow, byVK}: 49 | { 50 | name: string, 51 | statusName?: string | undefined, 52 | symbol: KeyCap, 53 | content: () => Key[], 54 | byRow?: Key[][], 55 | byVK?: { [vk in VK | VKAbbr]?: Key }, 56 | top?: boolean, 57 | noRecent?: boolean 58 | }): Board { 59 | return new StaticBoard({ 60 | name, statusName, symbol, noRecent, keys: (layout) => { 61 | let freeKeys = new Set(layout.free); 62 | const fixedKeys: SlottedKeys = { 63 | [SC.Backtick]: top ? new ConfigKey() : new BackKey(), 64 | [SC.Tab]: new SearchKey(), 65 | [SC.CapsLock]: new RecentKey(), 66 | }; 67 | const notPlaceable: Key[] = []; 68 | if (byRow) { 69 | const f = layout.freeRows; 70 | for (const [r, row] of byRow.entries()) { 71 | for (const [c, key] of row.entries()) { 72 | if (key.blank) continue; 73 | if (r < f.length && c < f[r].length && freeKeys.has(f[r][c])) { 74 | fixedKeys[f[r][c]] = key; 75 | freeKeys.delete(f[r][c]); 76 | } else { 77 | notPlaceable.push(key); 78 | } 79 | } 80 | } 81 | } 82 | if (byVK) { 83 | const vkPlaced = new Set(); 84 | for (const sc of freeKeys) { 85 | const vk = layout.sys[sc].vk; 86 | for (const k of vkLookup(vk)) { 87 | if (byVK[k]) { 88 | fixedKeys[sc] = byVK[k]; 89 | vkPlaced.add(k.toString()); 90 | freeKeys.delete(sc); 91 | } 92 | } 93 | } 94 | for (const [k, key] of Object.entries(byVK)) { 95 | if (!vkPlaced.has(k)) notPlaceable.push(key); 96 | } 97 | } 98 | const keys = content(); 99 | if (notPlaceable.length) keys.unshift(...notPlaceable); 100 | 101 | if (keys.length > freeKeys.size) { 102 | let pages = 1; 103 | while (keys.length > pages * (freeKeys.size - Math.min(pages, MAX_PAGE_KEYS))) { 104 | pages++; 105 | } 106 | const pageKeys = Math.min(pages, MAX_PAGE_KEYS); 107 | const perPage = freeKeys.size - pageKeys; 108 | 109 | return range(pages).map((i) => { 110 | const pagesStart = i <= 4; 111 | const pagesEnd = i >= pages - 5; 112 | const page = ([] as Key[]) 113 | .concat(range(pageKeys).map((j) => { 114 | if (pages == pageKeys) return new PageKey(j, i == j); 115 | else switch (j) { 116 | case 0: 117 | return new PageKey(0, i == 0); 118 | case 1: 119 | if (pagesStart) return new PageKey(1, i == 1); 120 | if (pagesEnd) return new PageKey(pages - 8, i == pages - 8); 121 | return new PageKey(i - 3, false); 122 | case 2: 123 | if (pagesStart) return new PageKey(2, i == 2); 124 | if (pagesEnd) return new PageKey(pages - 7, i == pages - 7); 125 | return new PageKey(i - 2, false); 126 | case 3: 127 | if (pagesStart) return new PageKey(3, i == 3); 128 | if (pagesEnd) return new PageKey(pages - 6, i == pages - 6); 129 | return new PageKey(i - 1, false); 130 | case 4: 131 | if (pagesStart) return new PageKey(4, i == 4); 132 | if (pagesEnd) return new PageKey(pages - 5, i == pages - 5); 133 | return new PageKey(i, true); 134 | case 5: 135 | if (pagesStart) return new PageKey(5, i == 5); 136 | if (pagesEnd) return new PageKey(pages - 4, i == pages - 4); 137 | return new PageKey(i + 1, false); 138 | case 6: 139 | if (pagesStart) return new PageKey(6, i == 6); 140 | if (pagesEnd) return new PageKey(pages - 3, i == pages - 3); 141 | return new PageKey(i + 2, false); 142 | case 7: 143 | if (pagesStart) return new PageKey(7, i == 7); 144 | if (pagesEnd) return new PageKey(pages - 2, i == pages - 2); 145 | return new PageKey(i + 3, false); 146 | case 8: 147 | return new PageKey(pages - 1, i == pages - 1); 148 | default: 149 | return BlankKey; 150 | } 151 | })) 152 | .concat(keys.slice(i * perPage, i * perPage + perPage)) 153 | return {...fixedKeys, ...mapKeysToSlots([...freeKeys], page)} 154 | }); 155 | } else { 156 | return [{...fixedKeys, ...mapKeysToSlots([...freeKeys], keys)}]; 157 | } 158 | } 159 | }); 160 | } 161 | 162 | private static fromItem(item: KeyboardItem, p: { noRecent?: boolean }): Key { 163 | if (item === null) { 164 | return BlankKey; 165 | } else if (typeof item === 'string') { 166 | return new ClusterKey(item, p); 167 | } else if (Array.isArray(item)) { 168 | if (item.length) { 169 | return new ClusterKey(item[0], {variants: item, ...p}); 170 | } 171 | return BlankKey; 172 | } else if (isCluster(item)) { 173 | return new ClusterKey(item.cluster, {symbol: item.symbol, name: item.name, noRecent: true}); 174 | } else { 175 | return new KeyboardKey(Board.fromEmoji(item)); 176 | } 177 | } 178 | 179 | private static fromContents(contents: KeyboardItem[], p: { noRecent?: boolean }): Key[] { 180 | const keys: Key[] = []; 181 | for (const item of contents) { 182 | if (item === null || typeof item === 'string' || Array.isArray(item)) { 183 | keys.push(this.fromItem(item, p)); 184 | } else { 185 | keys.push(this.fromItem(item, p)); 186 | } 187 | } 188 | return keys; 189 | } 190 | 191 | static fromEmoji({name, statusName, symbol, top, noRecent, content: contentDef, ...k}: EmojiKeyboard): Board { 192 | const p = {noRecent}; 193 | const keys = this.fromContents(Array.isArray(contentDef) ? contentDef : [], p); 194 | const content = typeof contentDef == 'function' 195 | ? () => this.fromContents(contentDef(), p) 196 | : () => keys; 197 | const byRow = (k.byRow ?? []).map(row => Array.isArray(row) ? row.map(key => this.fromItem(key, p)) : []); 198 | const byVK = fromEntries(k.byVK ? Object.entries(k.byVK).map(([k, v]) => [k as (VK | VKAbbr), this.fromItem(v, p)] as const) : []); 199 | 200 | return this.fromKeys({name, statusName, symbol, top, noRecent, content, byRow, byVK}); 201 | } 202 | 203 | static clusterAlternates(cluster: string, variants: string[], k?: { noRecent?: boolean }) { 204 | const keys = variants.map((c) => new ClusterKey(c, {variants: [], variantOf: cluster, ...k})); 205 | if (keys.length == 6) { 206 | // place on home row 207 | const byRow = [[], [], keys]; 208 | return this.fromKeys({name: clusterName(cluster), symbol: cluster, content: () => [], byRow}); 209 | } else if (keys.length == 18) { 210 | // center on home row 211 | const byRow = [[], keys.slice(0, 6), keys.slice(6, 12), keys.slice(12)]; 212 | return this.fromKeys({name: clusterName(cluster), symbol: cluster, content: () => [], byRow}); 213 | } 214 | { 215 | return this.fromKeys({name: clusterName(cluster), symbol: cluster, content: () => keys}); 216 | } 217 | } 218 | } 219 | 220 | /** On a static board the keys and their locations depend only on the layout */ 221 | export class StaticBoard extends Board { 222 | private readonly keys: (layout: Layout) => SlottedKeys[]; 223 | 224 | constructor( 225 | {keys, ...p}: { 226 | name: string; 227 | statusName?: string; 228 | symbol: KeyCap; 229 | keys: (layout: Layout) => SlottedKeys[]; 230 | noRecent?: boolean; 231 | } 232 | ) { 233 | super(p); 234 | this.keys = memoizeOne(keys); 235 | } 236 | 237 | Contents = ({state}: { state: BoardState | undefined }) => { 238 | const l = useContext(LayoutContext); 239 | const pages = useMemo(() => this.keys(l), [l]); 240 | useEffect(() => app().updateStatus(), []); 241 | const keys = pages[state?.page ?? 0] ?? pages[0]; 242 | return ; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /wwwassets/script/boards/utils.tsx: -------------------------------------------------------------------------------- 1 | import {SC} from "../layout/sc"; 2 | import {KeyCodesList} from "../layout"; 3 | import {useContext} from "preact/hooks"; 4 | import {app, LayoutContext} from "../appVar"; 5 | import {h} from "preact"; 6 | import {BlankKey, Key} from "../keys/base"; 7 | 8 | export interface BoardState { 9 | page?: number; 10 | } 11 | 12 | export const MAX_PAGE_KEYS = 9; 13 | 14 | export function Keys({keys}: { keys: SlottedKeys }) { 15 | const l = useContext(LayoutContext); 16 | app().keyHandlers = keys; 17 | return
18 | {l.all.map((code) => { 19 | const K = (keys[code] ?? BlankKey); 20 | return ; 21 | })} 22 |
23 | } 24 | 25 | export type SlottedKeys = { 26 | [key in SC]?: Key 27 | } 28 | 29 | export function mapKeysToSlots(codes: KeyCodesList, keys: Key[]): SlottedKeys { 30 | const mapped: SlottedKeys = {} 31 | for (const [index, code] of Array.from(codes.entries())) { 32 | if (keys[index]) mapped[code] = keys[index]; 33 | } 34 | return mapped; 35 | } 36 | -------------------------------------------------------------------------------- /wwwassets/script/builder/builder.ts: -------------------------------------------------------------------------------- 1 | import {parseUnicodeResources, Paths, UnicodeResources} from "./unicode"; 2 | import {consolidateUnicodeData} from "./consolidated"; 3 | import {ahkDownloadUnicode, ahkSaveUnicodeData, ahkVersions} from "../ahk"; 4 | import {app} from "../appVar"; 5 | import memoizeOne from "memoize-one"; 6 | 7 | export function toCodePoints(s: string): number[] { 8 | return [...s].map(c => c.codePointAt(0)!); 9 | } 10 | 11 | let building = false; 12 | 13 | export async function makeBuild() { 14 | if (building) return; 15 | try { 16 | app().setBuilding(true); 17 | building = true; 18 | await buildAhk(); 19 | } catch (e) { 20 | console.error(e); 21 | alert(`Build failed: ${e}`) 22 | } finally { 23 | app().setBuilding(false); 24 | building = false; 25 | } 26 | } 27 | 28 | async function buildAhk() { 29 | await ahkDownloadUnicode(); 30 | const v = await ahkVersions(); 31 | const paths: Paths = { 32 | emojiTestPath: `../res/data/emoji/${v.emoji}/emoji-test.txt`, 33 | emojiDataPath: `../res/data/${v.ucd}/ucd/emoji/emoji-data.txt`, 34 | unicodeDataPath: `../res/data/${v.ucd}/ucd/UnicodeData.txt`, 35 | namedSequencesPath: `../res/data/${v.ucd}/ucd/NamedSequences.txt`, 36 | namesListPath: `../res/data/${v.ucd}/ucd/NamesList.txt`, 37 | annotationsPath: `../res/data/cldr-json/${v.cldr}/cldr-json/cldr-annotations-full/annotations/en/annotations.json`, 38 | }; 39 | 40 | const resources: UnicodeResources = { 41 | emojiTest: memoizeOne(() => fetch(paths.emojiTestPath).then(r => r.text())), 42 | emojiData: memoizeOne(() => fetch(paths.emojiDataPath).then(r => r.text())), 43 | unicodeData: memoizeOne(() => fetch(paths.unicodeDataPath).then(r => r.text())), 44 | namedSequences: memoizeOne(() => fetch(paths.namedSequencesPath).then(r => r.text())), 45 | namesList: memoizeOne(() => fetch(paths.namesListPath).then(r => r.text())), 46 | annotations: memoizeOne(() => fetch(paths.annotationsPath).then(r => r.text())), 47 | } 48 | 49 | const ctx = await parseUnicodeResources(resources); 50 | 51 | const u = consolidateUnicodeData(ctx); 52 | console.log(u); 53 | 54 | ahkSaveUnicodeData(u); 55 | 56 | const emojiCount = u.groups.map(g => g.sub.reduce((v, s) => v + (s.clusters?.length ?? 0), 0)).reduce((a, b) => a + b, 0); 57 | alert( 58 | `Finished building “${u.name}”\n` + 59 | ` – ${u.chars.length} codepoints in ${u.blocks.length} blocks\n` + 60 | ` – ${u.clusters.length} grapheme clusters with length > 1\n` + 61 | ` – ${u.groups.length} emoji groups with ${emojiCount} base emojis\n\n` + 62 | `Reload the app to load the new data file` 63 | ) 64 | } 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /wwwassets/script/builder/consolidated.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AnnotationsData, 3 | BidiClass, 4 | CanonicalCombiningClass, 5 | EmojiTestData, 6 | EmojiVersion, 7 | GeneralCategory, 8 | NamedSequencesData, 9 | NamesListData, 10 | UnicodeData 11 | } from "./unicode"; 12 | import {toTitleCase} from "./titleCase"; 13 | import { 14 | CancelTag, 15 | RegionalIndicatorSymbolLetterACode, 16 | TagLatinSmallLetterACode, 17 | TagLatinSmallLetterZCode, 18 | ZeroWidthJoiner 19 | } from "../chars"; 20 | import {EXTEND_ALIASES} from "../config/aliases"; 21 | import {toCodePoints} from "./builder"; 22 | 23 | type BlockInformation = { 24 | start: number; 25 | end: number; 26 | name: string; 27 | sub: SubBlockInformation[]; 28 | } 29 | type SubBlockInformation = { 30 | name: string; 31 | notice?: string[]; 32 | char: number[]; 33 | } 34 | type ExtendedBlockInformation = BlockInformation & { 35 | sub: ExtendedSubBlockInformation[]; 36 | isCJKUnifiedIdeographs: boolean; 37 | } 38 | type ExtendedSubBlockInformation = SubBlockInformation & { 39 | block: WeakRef; 40 | } 41 | type CharInformation = { 42 | /** name */ 43 | n: string; 44 | code: number; 45 | alias?: string[]; 46 | falias?: string[]; 47 | ref?: number[]; 48 | decomposition?: string[]; 49 | variation?: { 50 | code: number; 51 | sel: string; 52 | name: string; 53 | }[]; 54 | notice?: string[]; 55 | /** category */ 56 | ca?: GeneralCategory; 57 | /** combining */ 58 | cb?: CanonicalCombiningClass; 59 | /** bidiClass */ 60 | bc?: BidiClass; 61 | /** decompositionUD */ 62 | de2?: string; 63 | decimalDigit?: number; 64 | digit?: number; 65 | numeric?: string; 66 | /** bidiMirrored */ 67 | bm?: true; 68 | /** unicode1 */ 69 | u1?: string; 70 | comment?: string[]; 71 | uppercase?: string; 72 | lowercase?: string; 73 | titlecase?: string; 74 | emojiVersion?: number; 75 | control?: true; 76 | reserved?: true; 77 | notACharacter?: true; 78 | } 79 | export type ExtendedCharInformation = CharInformation & { 80 | block: ExtendedBlockInformation; 81 | sub: ExtendedSubBlockInformation | null; 82 | } 83 | type ClusterInformation = { 84 | cluster: string; 85 | name: string; 86 | version?: number; 87 | parent?: string; 88 | variants?: string[]; 89 | alias?: string[]; 90 | }; 91 | export type ExtendedClusterInformation = ClusterInformation & { 92 | group?: ExtendedGroupInformation; 93 | subGroup?: ExtendedSubGroupInformation; 94 | } 95 | type GroupInformation = { 96 | name: string; 97 | sub: SubGroupInformation[]; 98 | }; 99 | type ExtendedGroupInformation = { 100 | name: string; 101 | sub: Record; 102 | } 103 | type SubGroupInformation = { 104 | name: string; 105 | clusters: string[]; 106 | } 107 | type ExtendedSubGroupInformation = SubGroupInformation & { 108 | group: WeakRef; 109 | } 110 | export type UnicodeDataSource = { 111 | namedSequences: NamedSequencesData; 112 | namesList: NamesListData; 113 | unicodeData: UnicodeData; 114 | emojiVersion: EmojiVersion; 115 | emojiTest: EmojiTestData; 116 | annotations: AnnotationsData; 117 | }; 118 | export type ConsolidatedUnicodeData = { 119 | name: string; 120 | blocks: BlockInformation[]; 121 | chars: CharInformation[]; 122 | groups: GroupInformation[]; 123 | clusters: ClusterInformation[]; 124 | } 125 | const ExcludeStem = new Set(["keycap", "flag", "family"]); 126 | 127 | export function toHex(code: number) { 128 | return code.toString(16).toUpperCase().padStart(4, '0'); 129 | } 130 | 131 | export function consolidateUnicodeData( 132 | { 133 | annotations, 134 | namedSequences, 135 | namesList, 136 | unicodeData, 137 | emojiVersion, 138 | emojiTest 139 | }: UnicodeDataSource): ConsolidatedUnicodeData { 140 | const blocks: BlockInformation[] = []; 141 | const chars: CharInformation[] = []; 142 | const groups: GroupInformation[] = []; 143 | const clusters: ClusterInformation[] = []; 144 | const knownClusters = new Set(); 145 | 146 | for (const block of namesList.block) { 147 | const b: BlockInformation = { 148 | start: block.start, 149 | end: block.end, 150 | name: block.name, 151 | sub: [], 152 | } 153 | blocks.push(b); 154 | for (const sub of block.sub) { 155 | const s: SubBlockInformation = { 156 | name: sub.name, 157 | notice: sub.notice, 158 | char: [], 159 | // block: new WeakRef(b), 160 | }; 161 | b.sub.push(s); 162 | for (const char of (sub.char ?? [])) { 163 | const str = String.fromCodePoint(char.code); 164 | 165 | const u = unicodeData[char.code]; 166 | const ev = emojiVersion[char.code]; 167 | const seq = emojiTest.sequences[str]; 168 | const a = annotations.annotations[str]; 169 | 170 | const c: CharInformation = { 171 | n: char.name, 172 | code: char.code, 173 | // block: b, 174 | // sub: s, 175 | }; 176 | 177 | if (char.alias || a?.default) { 178 | c.alias = [...(char.alias ?? []), ...(a?.default ?? [])] 179 | } 180 | if (char.falias) c.falias = char.falias; 181 | if (char.ref) c.ref = char.ref; 182 | if (char.decomposition) c.decomposition = char.decomposition; 183 | if (char.variation) c.variation = char.variation; 184 | if (char.notice) c.notice = char.notice; 185 | if (char.comment?.length || u?.comment?.length) { 186 | c.comment = [...(char.comment ?? []), ...(u?.comment ?? [])]; 187 | } 188 | 189 | if (u?.category) c.ca = u.category; 190 | if (u?.combining) c.cb = u.combining; 191 | if (u?.bidiClass) c.bc = u.bidiClass; 192 | if (u?.bidiMirrored) c.bm = u.bidiMirrored; 193 | if (u?.decompositionUD) c.de2 = u.decompositionUD; 194 | if (u?.decimalDigit) c.decimalDigit = u.decimalDigit; 195 | if (u?.digit) c.digit = u.digit; 196 | if (u?.numeric) c.numeric = u.numeric; 197 | if (u?.unicode1) c.u1 = u.unicode1; 198 | if (u?.uppercase) c.uppercase = u.uppercase; 199 | if (u?.lowercase) c.lowercase = u.lowercase; 200 | if (u?.titlecase) c.titlecase = u.titlecase; 201 | 202 | if (ev) c.emojiVersion = ev; 203 | if (seq) c.n = seq.name; 204 | 205 | if (c.n === '') { 206 | c.n = c.alias?.[0] ?? 'Control U+' + toHex(c.code); 207 | c.control = true; 208 | } 209 | if (c.n === '') { 210 | c.n = 'Reserved U+' + toHex(c.code); 211 | c.reserved = true; 212 | } 213 | if (c.n === '') { 214 | c.n = 'Not a Character U+' + toHex(c.code); 215 | c.notACharacter = true; 216 | } 217 | 218 | s.char.push(char.code); 219 | chars.push(c); 220 | } 221 | } 222 | } 223 | for (const group of emojiTest.groups) { 224 | const g: GroupInformation = { 225 | name: toTitleCase(group.name), 226 | sub: [], 227 | } 228 | groups.push(g); 229 | for (const sub of group.sub) { 230 | const s: SubGroupInformation = { 231 | name: sub.name, 232 | clusters: [], 233 | } 234 | g.sub.push(s); 235 | const stems = new Map(); 236 | for (const cluster of sub.clusters) { 237 | const info = emojiTest.sequences[cluster]; 238 | if (!info || info.type !== 'fully-qualified') continue; 239 | 240 | const stem = info.name.split(':', 1)[0]; 241 | const name = toTitleCase(info.name); 242 | const a = annotations.annotations[cluster]; 243 | 244 | // Grapheme clusters are grouped by stem 245 | if (stems.has(stem) && !ExcludeStem.has(stem)) { 246 | const c = stems.get(stem)!; 247 | c.variants ??= [c.cluster]; 248 | if (name == c.name.split(':', 1)[0]) { 249 | // need to swap it out 250 | const c2: ClusterInformation = { 251 | cluster, 252 | name: name, 253 | version: info.version, 254 | variants: [cluster, ...c.variants], 255 | }; 256 | if (a?.default) c2.alias = a.default; 257 | clusters.push(c2); 258 | knownClusters.add(cluster); 259 | delete c.variants; 260 | c.parent = cluster; 261 | s.clusters[s.clusters.indexOf(c.cluster)] = cluster; 262 | stems.set(stem, c2); 263 | } else { 264 | c.variants.push(cluster); 265 | clusters.push({ 266 | cluster, 267 | name: name, 268 | version: info.version, 269 | parent: c.cluster, 270 | }); 271 | knownClusters.add(cluster); 272 | } 273 | } else { 274 | const c: ClusterInformation = { 275 | cluster, 276 | name: name, 277 | version: info.version, 278 | } 279 | if (a?.default) c.alias = a.default; 280 | stems.set(stem, c); 281 | clusters.push(c); 282 | knownClusters.add(cluster); 283 | s.clusters.push(cluster); 284 | } 285 | } 286 | } 287 | } 288 | for (const ns of namedSequences) { 289 | if (!knownClusters.has(ns.cluster)) { 290 | clusters.push({ 291 | cluster: ns.cluster, 292 | name: ns.name, 293 | }); 294 | knownClusters.add(ns.cluster); 295 | } 296 | } 297 | return {blocks, chars, groups, clusters, name: namesList.title.title}; 298 | } 299 | 300 | declare global { 301 | interface Window { 302 | unicodeData?: ConsolidatedUnicodeData 303 | } 304 | } 305 | export type ExtendedUnicodeData = { 306 | blocks: ExtendedBlockInformation[]; 307 | chars: Record; 308 | groups: Record; 309 | clusters: Record; 310 | }; 311 | 312 | const SKIN_TONES = ["🏻", "🏼", "🏽", "🏾", "🏿"] as const; 313 | const SKIN_TONES_NAMES = { 314 | "🏻": "Light Skin Tone", 315 | "🏼": "Medium-Light Skin Tone", 316 | "🏽": "Medium Skin Tone", 317 | "🏾": "Medium-Dark Skin Tone", 318 | "🏿": "Dark Skin Tone", 319 | } as const; 320 | const SKIN_TONE_REGEX = new RegExp(`(?:${SKIN_TONES.join('|')})`, 'g'); 321 | 322 | export function getUnicodeData(): ExtendedUnicodeData { 323 | const u: ConsolidatedUnicodeData = (typeof window != 'undefined' ? window.unicodeData : null) ?? { 324 | name: "No Unicode Data Available", 325 | blocks: [], 326 | chars: [], 327 | clusters: [], 328 | groups: [], 329 | }; 330 | 331 | const idxChars: Record = Object.fromEntries(u.chars.map(c => [c.code, c])); 332 | 333 | const blocks: ExtendedBlockInformation[] = []; 334 | const chars: Record = {}; 335 | const groups: Record = {}; 336 | const clusters: Record = Object.fromEntries(u.clusters.map(c => [c.cluster, { 337 | ...c, 338 | name: toTitleCase(c.name), 339 | }])); 340 | 341 | for (const block of u.blocks) { 342 | const b: ExtendedBlockInformation = { 343 | ...block, 344 | sub: [], 345 | isCJKUnifiedIdeographs: block.name.startsWith("CJK Unified Ideographs") && !block.sub.length, 346 | }; 347 | b.sub = block.sub.map(s => ({...s, block: new WeakRef(b)})); 348 | blocks.push(b); 349 | for (const s of b.sub) { 350 | for (const c of s.char) { 351 | if (!idxChars[c]) continue; 352 | chars[c] = { 353 | ...idxChars[c], 354 | n: toTitleCase(idxChars[c].n), 355 | block: b, 356 | sub: s, 357 | }; 358 | } 359 | } 360 | } 361 | 362 | // Additional folding pass 363 | const clustersByName = new Map(Object.values(clusters).map(c => [c.name, c] as const)); 364 | for (const c of Object.values(clusters)) { 365 | if (!c.parent) { 366 | let variants: string[] = []; 367 | if (c.name.match(/^People /) && !c.name.match(/ Holding Hands$/)) { 368 | const baseName = c.name.replace(/^People /, ''); 369 | variants = [ 370 | `Men ${baseName}`, 371 | `Women ${baseName}`, 372 | ] 373 | } else if (c.name.match(/ (?:Man|Woman|Person)$/) && !c.name.match(/^Old/)) { 374 | const baseName = c.name.replace(/ (?:Man|Woman|Person)$/, ''); 375 | variants = [ 376 | `${baseName} Person`, 377 | `${baseName} Man`, 378 | `${baseName} Woman`, 379 | ] 380 | } else if (c.name == 'Merperson') { 381 | const baseName = c.name.replace(/person$/, ''); 382 | variants = [ 383 | `${baseName}man`, 384 | `${baseName}maid`, 385 | ] 386 | } else if (c.name == 'Person With Crown') { 387 | variants = [ 388 | `Prince`, 389 | `Princess`, 390 | ] 391 | } else { 392 | const baseName = c.name.replace(/^Person /, ''); 393 | variants = [ 394 | `Man ${baseName}`, 395 | `Woman ${baseName}`, 396 | ] 397 | } 398 | for (const v of variants) { 399 | if (v != c.name && clustersByName.has(v)) { 400 | const sc = clustersByName.get(v)!; 401 | if (!sc.parent) { 402 | sc.parent = c.cluster; 403 | if (!c.variants) c.variants = [c.cluster]; 404 | if (sc.variants) { 405 | c.variants.push(...sc.variants); 406 | delete sc.variants; 407 | } else c.variants.push(sc.cluster); 408 | if (sc.alias) { 409 | c.alias ??= []; 410 | for (const a in sc.alias) if (!c.alias.includes(a)) c.alias.push(a); 411 | delete sc.alias; 412 | } 413 | } 414 | } 415 | } 416 | } 417 | } 418 | 419 | // Fix woman variants list to place bearded woman last 420 | clusters["👩"]?.variants?.sort((a, b) => +clusters[a]!.name.includes('Beard') - +clusters[b]!.name.includes('Beard')) 421 | 422 | // Fix People & Body group s.t. families are all in the family sub-group 423 | const people = u.groups.find(g => g.name == 'People & Body'); 424 | if (people) { 425 | const personSymbol = people.sub.find(s => s.name == 'person-symbol'); 426 | const family = people.sub.find(s => s.name == 'family'); 427 | if (personSymbol && family) { 428 | const isFamily = (c: string) => clusters[c]?.name.startsWith('Family'); 429 | const toMove = personSymbol?.clusters.filter(isFamily) ?? []; 430 | family.clusters.push(...toMove); 431 | personSymbol.clusters = personSymbol.clusters.filter(c => !isFamily(c)); 432 | } 433 | } 434 | 435 | // Fold subdivision flags into the corresponding country flags 436 | const subdivisionFlags = u.groups.find(g => g.name == 'Flags')?.sub.find(s => s.name == 'subdivision-flag'); 437 | if (subdivisionFlags) { 438 | const toRemove = new Set(); 439 | for (const c of subdivisionFlags.clusters) { 440 | const chars = [...c]; 441 | if (chars.length < 5 || chars[0] != '🏴' || chars[chars.length - 1] != CancelTag) continue; 442 | for (let i = 1; i < chars.length - 1; ++i) { 443 | if (chars[i].codePointAt(0)! < TagLatinSmallLetterACode || chars[i].codePointAt(0)! > TagLatinSmallLetterZCode) continue; 444 | } 445 | const base = String.fromCodePoint( 446 | chars[1].codePointAt(0)! - TagLatinSmallLetterACode + RegionalIndicatorSymbolLetterACode, 447 | chars[2].codePointAt(0)! - TagLatinSmallLetterACode + RegionalIndicatorSymbolLetterACode, 448 | ); 449 | if (!clusters[base]) continue; 450 | if (!clusters[base].variants) clusters[base].variants = [base]; 451 | clusters[base]!.variants!.push(c); 452 | toRemove.add(c); 453 | }; 454 | subdivisionFlags.clusters = subdivisionFlags.clusters.filter(c => !toRemove.has(c)) ?? []; 455 | } 456 | 457 | // Extend group information 458 | for (const group of u.groups) { 459 | const g: ExtendedGroupInformation = {...group, sub: {}}; 460 | groups[group.name] = g; 461 | for (const sub of group.sub) { 462 | const s: ExtendedSubGroupInformation = {...sub, group: new WeakRef(g)}; 463 | g.sub[sub.name] = s; 464 | s.clusters = s.clusters.filter(c => !clusters[c] || !clusters[c].parent); 465 | for (const cluster of s.clusters) { 466 | if (!clusters[cluster]) continue; 467 | clusters[cluster].group = g; 468 | clusters[cluster].subGroup = s; 469 | } 470 | } 471 | } 472 | 473 | // Add skin tone variants to family sequences 474 | for (const c of ["👨‍👩‍👦", "👨‍👩‍👧", "👨‍👩‍👧‍👦", "👨‍👩‍👦‍👦", "👨‍👩‍👧‍👧", "👨‍👨‍👦", "👨‍👨‍👧", "👨‍👨‍👧‍👦", "👨‍👨‍👦‍👦", "👨‍👨‍👧‍👧", "👩‍👩‍👦", "👩‍👩‍👧", "👩‍👩‍👧‍👦", "👩‍👩‍👦‍👦", "👩‍👩‍👧‍👧", "👨‍👦", "👨‍👦‍👦", "👨‍👧", "👨‍👧‍👦", "👨‍👧‍👧", "👩‍👦", "👩‍👦‍👦", "👩‍👧", "👩‍👧‍👦", "👩‍👧‍👧", "🧑‍🧑‍🧒", "🧑‍🧑‍🧒‍🧒", "🧑‍🧒", "🧑‍🧒‍🧒"]) { 475 | if (clusters[c] && !clusters[c].variants) { 476 | const parts = c.split(ZeroWidthJoiner); 477 | for (let i = 1; i < parts.length; ++i) parts[i] = ZeroWidthJoiner + parts[i]; 478 | let variants = [""]; 479 | for (const p of parts) { 480 | variants = variants.flatMap(v => SKIN_TONES.map(k => v + p + k)) 481 | } 482 | for (const v of variants) { 483 | clusters[v] = { 484 | cluster: v, 485 | parent: c, 486 | name: clusters[c].name + ': ' + [...v.matchAll(SKIN_TONE_REGEX)].map(v => SKIN_TONES_NAMES[v[0] as keyof typeof SKIN_TONES_NAMES]).join(', '), 487 | version: clusters[c].version, 488 | } 489 | } 490 | variants.unshift(c); 491 | clusters[c].variants = variants; 492 | } 493 | } 494 | 495 | for (const [k, v] of Object.entries(EXTEND_ALIASES)) { 496 | const code = toCodePoints(k); 497 | if (code.length == 1) { 498 | const info = chars[code[0]]; 499 | if (info) { 500 | info.alias ??= []; 501 | info.alias.push(...v); 502 | } else { 503 | const block = blocks.find(b => b.start <= code[0] && b.end >= code[0]); 504 | if (!block) continue; 505 | chars[code[0]] = { 506 | n: `${block.name}: U+${code[0].toString(16)}`, 507 | code: code[0], 508 | alias: v, 509 | block: block, 510 | sub: null, 511 | } 512 | } 513 | } else { 514 | const info = clusters[k]; 515 | if (info) { 516 | info.alias ??= []; 517 | info.alias.push(...v); 518 | } 519 | } 520 | } 521 | return {blocks, chars, groups, clusters}; 522 | } 523 | -------------------------------------------------------------------------------- /wwwassets/script/builder/namesList.pegjs: -------------------------------------------------------------------------------- 1 | // Peggy (fork of PegJS) parser definition for the Unicode NamesList.txt file 2 | // © 2023 Gilles Waeber - MIT License 3 | 4 | {{ 5 | function dropNA(arr) { 6 | return arr.filter(e => e !== null && typeof e !== 'undefined') 7 | } 8 | 9 | function flatJoin(arr) { 10 | if (Array.isArray(arr)) { 11 | return dropNA(arr).map(e => flatJoin(e)).join('') 12 | } else { 13 | return arr 14 | } 15 | } 16 | 17 | function mergeInfo(info) { 18 | const merged = {} 19 | for (const e of dropNA(info)) { 20 | for (const [k, v] of Object.entries(e)) { 21 | if (typeof merged[k] === 'undefined') merged[k] = [] 22 | if (Array.isArray(v)) merged[k].push(...v) 23 | else merged[k].push(v) 24 | } 25 | } 26 | return merged 27 | } 28 | 29 | function block(info) { 30 | const sub = [] 31 | let current = [] 32 | let name = "base" 33 | let base = {} 34 | for (const e of dropNA(info)) { 35 | if (typeof e.sub !== 'undefined') { 36 | if (current.length) sub.push({name, ...mergeInfo(current)}) 37 | current = [] 38 | name = e.sub 39 | } else { 40 | current.push(e) 41 | } 42 | } 43 | if (current.length) sub.push({name, ...mergeInfo(current)}) 44 | if (sub.length && !sub[0].char) { 45 | let {name:_, ...rest} = sub.shift() 46 | base = rest 47 | } 48 | return {...base, sub} 49 | } 50 | }} 51 | 52 | NAMELIST "NAMELIST" 53 | = title: TITLE_PAGE block: EXTENDED_BLOCK* {return {title, block}} 54 | 55 | TITLE_PAGE "TITLE_PAGE" 56 | = FILE_COMMENT* title: TITLE info: 57 | ( subtitle: SUBTITLE {return {subtitle}} 58 | / sub: SUBHEADER {return {sub}} 59 | / IGNORED_LINE {} 60 | / EMPTY_LINE {} 61 | / notice: NOTICE_LINE {return {notice}} 62 | / comment: COMMENT_LINE {return {comment}} 63 | / PAGEBREAK {return {break: true}} 64 | / FILE_COMMENT {})* {return {title, info: mergeInfo(info)}} 65 | 66 | 67 | EXTENDED_BLOCK "EXTENDED_BLOCK" 68 | = block: BLOCK summary: SUMMARY? {return block} 69 | 70 | 71 | BLOCK "BLOCK" 72 | = header: BLOCKHEADER INDEX_TAB? info: 73 | ( chr: CHAR_ENTRY {return {char: chr}} 74 | / sub: SUBHEADER {return {sub}} 75 | / ref: CROSS_REF {return {ref}} 76 | / notice: NOTICE_LINE {return {notice}} 77 | / EMPTY_LINE {} 78 | / IGNORED_LINE {} 79 | / SIDEBAR_LINE {} 80 | / PAGEBREAK {} 81 | / FILE_COMMENT {})* {return {...block(info), ...header}} 82 | 83 | 84 | CHAR_ENTRY "CHAR_ENTRY" 85 | = name: (NAME_LINE / RESERVED_LINE) 86 | info: 87 | ( alias: ALIAS_LINE {return {alias}} 88 | / falias: FORMALALIAS_LINE {return {falias}} 89 | / ref: CROSS_REF {return {ref}} 90 | / decomposition: DECOMPOSITION {return {decomposition}} 91 | / compat: COMPAT_MAPPING {return {compat}} 92 | / variation: VARIATION_LINE {return {variation}} 93 | / notice: NOTICE_LINE {return {notice}} 94 | / comment: COMMENT_LINE {return {comment}} 95 | / IGNORED_LINE {} 96 | / EMPTY_LINE {} 97 | / FILE_COMMENT {})* {return {...name, ...mergeInfo(info)}} 98 | 99 | SUMMARY "SUMMARY" 100 | = ALTGLYPH_SUMMARY 101 | / VARIATION_SUMMARY 102 | / ALTGLYPH_SUMMARY VARIATION_SUMMARY 103 | / MIXED_SUMMARY 104 | 105 | ALTGLYPH_SUMMARY "ALTGLYPH_SUMMARY" 106 | = ALTGLYPH_SUBHEADER SUMMARY_LINE* 107 | 108 | VARIATION_SUMMARY "VARIATION_SUMMARY" 109 | = VARIATION_SUBHEADER SUMMARY_LINE* 110 | 111 | MIXED_SUMMARY "MIXED_SUMMARY" 112 | = MIXED_SUBHEADER SUMMARY_LINE* 113 | 114 | SUMMARY_LINE "SUMMARY_LINE" 115 | = SUBHEADER 116 | / NOTICE_LINE 117 | / FILE_COMMENT 118 | / EMPTY_LINE 119 | 120 | NAME_LINE "NAME_LINE" 121 | = code: CODEPOINT TAB name: NAME LF {return {code, name}} 122 | / code: CODEPOINT TAB "<" cname: LCNAME ">" LF {return {code, name: "<" + cname + ">"}} 123 | / code: CODEPOINT TAB name: NAME SP COMMENT LF {return {code, name}} 124 | / code: CODEPOINT TAB "<" cname: LCNAME ">" SP COMMENT LF {return {code, name: "<" + cname + ">"}} 125 | 126 | RESERVED_LINE "RESERVED_LINE" 127 | = code: CHAR TAB name: "" LF {return {code, name}} 128 | 129 | COMMENT_LINE "COMMENT_LINE" 130 | = TAB "*" SP text: EXPAND_LINE {return "• " + text} 131 | / TAB text: EXPAND_LINE {return text} 132 | 133 | ALIAS_LINE "ALIAS_LINE" 134 | = TAB "=" SP text: LINE {return text.split(/, /g)} 135 | 136 | FORMALALIAS_LINE "FORMALALIAS_LINE" 137 | = TAB "%" SP text: NAME LF {return text} 138 | 139 | CROSS_REF "CROSS_REF" 140 | = TAB "x" SP code: ( 141 | code: CODEPOINT (SP "<" LCNAME ">" / LCNAME)? {return code} 142 | / "(" ("<" LCNAME ">" / LCNAME) SP "-" SP code: CODEPOINT ")" {return code} 143 | ) LF {return code} 144 | 145 | VARIATION_LINE "VARIATION_LINE" 146 | = TAB "~" SP code: CODEPOINT SP sel: VARSEL SP name: LABEL LF {return {code, sel, name}} 147 | / TAB "~" SP code: CODEPOINT SP sel: VARSEL SP name: (LABEL "(" LCTAG ")") LF {return {code, sel, name: name.join('')}} 148 | 149 | FILE_COMMENT "FILE_COMMENT" 150 | = ";" text: LINE {return ";" + text} 151 | 152 | EMPTY_LINE "EMPTY_LINE" 153 | = LF 154 | 155 | IGNORED_LINE "IGNORED_LINE" 156 | = TAB ";" LINE 157 | 158 | SIDEBAR_LINE "SIDEBAR_LINE" 159 | = ";;" LINE 160 | 161 | DECOMPOSITION "DECOMPOSITION" 162 | = TAB ":" SP "<" tag: TAG ">" SP seq: EXPAND_LINE {return {tag, seq}} 163 | / TAB ":" SP seq: EXPAND_LINE {return {seq}} 164 | 165 | COMPAT_MAPPING "COMPAT_MAPPING" 166 | = TAB "#" SP "<" tag: TAG ">" SP seq: EXPAND_LINE {return {tag, seq}} 167 | / TAB "#" SP seq: EXPAND_LINE {return {seq}} 168 | 169 | NOTICE_LINE "NOTICE_LINE" 170 | = "@+" TAB text: LINE {return text} 171 | / "@+" TAB * SP text: LINE {return "• " + text} 172 | 173 | TITLE "TITLE" 174 | = "@@@" TAB text: LINE {return text} 175 | 176 | SUBTITLE "SUBTITLE" 177 | = "@@@+" TAB text: LINE {return text} 178 | 179 | SUBHEADER "SUBHEADER" 180 | = "@" TAB text: LINE {return text} 181 | 182 | VARIATION_SUBHEADER "VARIATION_SUBHEADER" 183 | = "@~" TAB LINE 184 | / "@~" 185 | / "@~" TAB "!" 186 | / "@~" TAB "!" VARSEL_LIST 187 | / "@~" TAB "!" VARSEL_LIST LINE 188 | 189 | ALTGLYPH_SUBHEADER "ALTGLYPH_SUBHEADER" 190 | = "@@~" TAB LINE 191 | / "@@~" 192 | / "@@~" TAB "!" 193 | 194 | MIXED_SUBHEADER "MIXED_SUBHEADER" 195 | = "@@@~" TAB LINE 196 | / "@@@~" 197 | / "@@@~" TAB "!" 198 | / "@@@~" TAB "!" VARSEL_LIST 199 | / "@@@~" TAB "!" VARSEL_LIST LINE 200 | 201 | BLOCKHEADER "BLOCKHEADER" 202 | = "@@" TAB start: CODEPOINT TAB name: BLOCKNAME TAB end: CODEPOINT LF {return {start, end, name}} 203 | 204 | BLOCKNAME "BLOCKNAME" 205 | = text: (LABEL (SP "(" LABEL ")")?) {return flatJoin(text)} 206 | 207 | PAGEBREAK "PAGEBREAK" 208 | = "@@" LF 209 | INDEX_TAB "INDEX_TAB" 210 | = "@@+" LF 211 | 212 | EXPAND_LINE "EXPAND_LINE" 213 | = text: (ESC_CHAR / CHAR / STRING)+ LF {return text.join('')} 214 | 215 | LINE "LINE" 216 | = text: STRING LF {return text} 217 | COMMENT "COMMENT" 218 | = "(" LABEL ")" 219 | / "(" LABEL ")" SP "*" 220 | / "*" 221 | 222 | NAME "NAME" 223 | = text: ([A-Z]+([ -]+[A-Z0-9 -]+)*) {return flatJoin(text)} // 224 | LCNAME "LCNAME" 225 | = text: ([a-z][a-z0-9]* ([ ][a-z][a-z0-9]* / [-] CHAR / [-][a-z0-9]+)*) {return flatJoin(text)} 226 | 227 | TAG "TAG" 228 | = text: [a-zA-Z]+ {return text.join('')} 229 | LCTAG "LCTAG" 230 | = text: [a-z]+ {return text.join('')} 231 | STRING "STRING" 232 | = text: ([\u0020-\uD7FF\uE000-\uFFFF] / SUP_PLANE_CHAR)+ {return text.join('')} // except controls 233 | LABEL "LABEL" 234 | = text: ([\u0021-\u0027\u002A-\u02FF]+([ -]+[\u0021-\u0027\u002A-\u002C\u002E-\u02FF]+)*) { 235 | return flatJoin(text)} // except controls, "(" or ")" 236 | VARSEL "VARSEL" 237 | = CHAR 238 | / code: ("ALT" [1-9]) {return code.join('')} 239 | VARSEL_LIST "VARSEL_LIST" 240 | = "{" CHAR_LIST "}" 241 | CHAR_LIST "CHAR_LIST" 242 | = CHAR (SP CHAR)* 243 | CHAR "CHAR" 244 | = text: X|4..6| {return text.join('')} 245 | CODEPOINT "CODEPOINT" 246 | = code: CHAR {return parseInt(code, 16)} 247 | X "X" 248 | = [0-9A-F] 249 | ESC_CHAR "ESC_CHAR" 250 | = ESC CHAR 251 | ESC "ESC" 252 | = "\\" 253 | TAB "TAB" 254 | = [\t]+ 255 | SP "SP" 256 | = [ ] 257 | LF "LF" 258 | = [\r\n]+ 259 | 260 | // One character in the Supplementary Plane, encoded as a surrogate pair of two UTF-16 code units 261 | SUP_PLANE_CHAR "SUP_PLANE_CHAR" 262 | = lead: [\uD800-\uDBFF] trail: [\uDC00-\uDFFF] {return lead + trail} 263 | -------------------------------------------------------------------------------- /wwwassets/script/builder/titleCase.ts: -------------------------------------------------------------------------------- 1 | const SEPARATORS = /([ -])/; 2 | 3 | export function toTitleCase(text: string): string { 4 | return text 5 | .split(SEPARATORS) 6 | .map((word, index) => { 7 | if (index % 2 === 0 && word.length) { 8 | const [first, ...rest] = word; 9 | return first.toUpperCase() + rest.join('').toLowerCase(); 10 | } else { 11 | return word; 12 | } 13 | }) 14 | .join(''); 15 | } 16 | -------------------------------------------------------------------------------- /wwwassets/script/builder/unicode.ts: -------------------------------------------------------------------------------- 1 | import * as peggy from "peggy"; 2 | import {Dictionary} from "../helpers"; 3 | import {UnicodeDataSource} from "./consolidated"; 4 | 5 | /* 6 | * Parsers for Unicode data files 7 | */ 8 | 9 | const EmojiDataRegex = new RegExp( 10 | `^(?[0-9A-F]+(?:..[0-9A-F]+)?)\\s*;\\s*(?[A-Za-z_]+)\\s*#\\s*E?(?[0-9.]+)\\s*\\[` 11 | ); 12 | 13 | export type EmojiVersion = Record; 14 | 15 | export async function parseEmojiVersions(resources: UnicodeResources): Promise { 16 | const emojiVersion: EmojiVersion = {}; 17 | const text = await resources.emojiData(); 18 | 19 | for (const line of text.split('\n')) { 20 | if (!line.length || line.startsWith('#')) continue; 21 | const m = line.match(EmojiDataRegex)?.groups; 22 | if (m) { 23 | const version = parseFloat(m['version']); 24 | if (m['char'].includes('..')) { 25 | const [from, to] = m['char'].split('..').map(s => parseInt(s, 16)); 26 | for (let char = from; char <= to; char++) { 27 | emojiVersion[char] = version; 28 | } 29 | } else { 30 | const char = parseInt(m['char'], 16); 31 | emojiVersion[char] = version; 32 | } 33 | } 34 | } 35 | 36 | return emojiVersion; 37 | } 38 | 39 | export const enum GeneralCategory { 40 | /** an uppercase letter */ 41 | Uppercase_Letter = "Lu", 42 | /** a lowercase letter */ 43 | Lowercase_Letter = "Ll", 44 | /** a digraphic character, with first part uppercase */ 45 | Titlecase_Letter = "Lt", 46 | /** Lu | Ll | Lt */ 47 | Cased_Letter = "LC", 48 | /** a modifier letter */ 49 | Modifier_Letter = "Lm", 50 | /** other letters, including syllables and ideographs */ 51 | Other_Letter = "Lo", 52 | /** Lu | Ll | Lt | Lm | Lo */ 53 | Letter = "L", 54 | /** a nonspacing combining mark (zero advance width) */ 55 | Nonspacing_Mark = "Mn", 56 | /** a spacing combining mark (positive advance width) */ 57 | Spacing_Mark = "Mc", 58 | /** an enclosing combining mark */ 59 | Enclosing_Mark = "Me", 60 | /** Mn | Mc | Me */ 61 | Mark = "M", 62 | /** a decimal digit */ 63 | Decimal_Number = "Nd", 64 | /** a letterlike numeric character */ 65 | Letter_Number = "Nl", 66 | /** a numeric character of other type */ 67 | Other_Number = "No", 68 | /** Nd | Nl | No */ 69 | Number = "N", 70 | /** a connecting punctuation mark, like a tie */ 71 | Connector_Punctuation = "Pc", 72 | /** a dash or hyphen punctuation mark */ 73 | Dash_Punctuation = "Pd", 74 | /** an opening punctuation mark (of a pair) */ 75 | Open_Punctuation = "Ps", 76 | /** a closing punctuation mark (of a pair) */ 77 | Close_Punctuation = "Pe", 78 | /** an initial quotation mark */ 79 | Initial_Punctuation = "Pi", 80 | /** a final quotation mark */ 81 | Final_Punctuation = "Pf", 82 | /** a punctuation mark of other type */ 83 | Other_Punctuation = "Po", 84 | /** Pc | Pd | Ps | Pe | Pi | Pf | Po */ 85 | Punctuation = "P", 86 | /** a symbol of mathematical use */ 87 | Math_Symbol = "Sm", 88 | /** a currency sign */ 89 | Currency_Symbol = "Sc", 90 | /** a non-letterlike modifier symbol */ 91 | Modifier_Symbol = "Sk", 92 | /** a symbol of other type */ 93 | Other_Symbol = "So", 94 | /** Sm | Sc | Sk | So */ 95 | Symbol = "S", 96 | /** a space character (of various non-zero widths) */ 97 | Space_Separator = "Zs", 98 | /** U+2028 LINE SEPARATOR only */ 99 | Line_Separator = "Zl", 100 | /** U+2029 PARAGRAPH SEPARATOR only */ 101 | Paragraph_Separator = "Zp", 102 | /** Zs | Zl | Zp */ 103 | Separator = "Z", 104 | /** a C0 or C1 control code */ 105 | Control = "Cc", 106 | /** a format control character */ 107 | Format = "Cf", 108 | /** a surrogate code point */ 109 | Surrogate = "Cs", 110 | /** a private-use character */ 111 | Private_Use = "Co", 112 | /** a reserved unassigned code point or a noncharacter */ 113 | Unassigned = "Cn", 114 | /** Cc | Cf | Cs | Co | Cn */ 115 | Other = "C", 116 | } 117 | export const enum CanonicalCombiningClass { 118 | /** Spacing and enclosing marks; also many vowel and consonant signs, even if nonspacing */ 119 | Not_Reordered = 0, 120 | /** Marks which overlay a base letter or symbol */ 121 | Overlay = 1, 122 | /** Diacritic reading marks for CJK unified ideographs */ 123 | Han_Reading = 6, 124 | /** Diacritic nukta marks in Brahmi-derived scripts */ 125 | Nukta = 7, 126 | /** Hiragana/Katakana voicing marks */ 127 | Kana_Voicing = 8, 128 | /** Viramas */ 129 | Virama = 9, 130 | /** Start of fixed position classes */ 131 | Ccc10 = 10, 132 | /** End of fixed position classes */ 133 | Ccc199 = 199, 134 | /** Marks attached at the bottom left */ 135 | Attached_Below_Left = 200, 136 | /** Marks attached directly below */ 137 | Attached_Below = 202, 138 | /** Marks attached at the bottom right */ 139 | Ccc204 = 204, 140 | /** Marks attached to the left */ 141 | Ccc208 = 208, 142 | /** Marks attached to the right */ 143 | Ccc210 = 210, 144 | /** Marks attached at the top left */ 145 | Ccc212 = 212, 146 | /** Marks attached directly above */ 147 | Attached_Above = 214, 148 | /** Marks attached at the top right */ 149 | Attached_Above_Right = 216, 150 | /** Distinct marks at the bottom left */ 151 | Below_Left = 218, 152 | /** Distinct marks directly below */ 153 | Below = 220, 154 | /** Distinct marks at the bottom right */ 155 | Below_Right = 222, 156 | /** Distinct marks to the left */ 157 | Left = 224, 158 | /** Distinct marks to the right */ 159 | Right = 226, 160 | /** Distinct marks at the top left */ 161 | Above_Left = 228, 162 | /** Distinct marks directly above */ 163 | Above = 230, 164 | /** Distinct marks at the top right */ 165 | Above_Right = 232, 166 | /** Distinct marks subtending two bases */ 167 | Double_Below = 233, 168 | /** Distinct marks extending above two bases */ 169 | Double_Above = 234, 170 | /** Greek iota subscript only */ 171 | Iota_Subscript = 240, 172 | } 173 | export const enum BidiClass { 174 | /** any strong left-to-right character */ 175 | Left_To_Right = "L", 176 | /** any strong right-to-left (non-Arabic-type) character */ 177 | Right_To_Left = "R", 178 | /** any strong right-to-left (Arabic-type) character */ 179 | Arabic_Letter = "AL", 180 | /** any ASCII digit or Eastern Arabic-Indic digit */ 181 | European_Number = "EN", 182 | /** plus and minus signs */ 183 | European_Separator = "ES", 184 | /** a terminator in a numeric format context, includes currency signs */ 185 | European_Terminator = "ET", 186 | /** any Arabic-Indic digit */ 187 | Arabic_Number = "AN", 188 | /** commas, colons, and slashes */ 189 | Common_Separator = "CS", 190 | /** any nonspacing mark */ 191 | Nonspacing_Mark = "NSM", 192 | /** most format characters, control codes, or noncharacters */ 193 | Boundary_Neutral = "BN", 194 | /** various newline characters */ 195 | Paragraph_Separator = "B", 196 | /** various segment-related control codes */ 197 | Segment_Separator = "S", 198 | /** spaces */ 199 | White_Space = "WS", 200 | /** most other symbols and punctuation marks */ 201 | Other_Neutral = "ON", 202 | /** U+202A: the LR embedding control */ 203 | Left_To_Right_Embedding = "LRE", 204 | /** U+202D: the LR override control */ 205 | Left_To_Right_Override = "LRO", 206 | /** U+202B: the RL embedding control */ 207 | Right_To_Left_Embedding = "RLE", 208 | /** U+202E: the RL override control */ 209 | Right_To_Left_Override = "RLO", 210 | /** U+202C: terminates an embedding or override control */ 211 | Pop_Directional_Format = "PDF", 212 | /** U+2066: the LR isolate control */ 213 | Left_To_Right_Isolate = "LRI", 214 | /** U+2067: the RL isolate control */ 215 | Right_To_Left_Isolate = "RLI", 216 | /** U+2068: the first strong isolate control */ 217 | First_Strong_Isolate = "FSI", 218 | /** U+2069: terminates an isolate control */ 219 | Pop_Directional_Isolate = "PDI", 220 | } 221 | 222 | export type UnicodeDataChar = { 223 | name: string; 224 | code: number; 225 | category: GeneralCategory; 226 | combining: CanonicalCombiningClass; 227 | bidiClass: BidiClass; 228 | decompositionUD?: string; 229 | decimalDigit?: number; 230 | digit?: number; 231 | numeric?: string; 232 | bidiMirrored: boolean; 233 | unicode1?: string; 234 | comment?: string[]; 235 | uppercase?: string; 236 | lowercase?: string; 237 | titlecase?: string; 238 | } 239 | export type UnicodeData = Record; 240 | 241 | export type Paths = { 242 | emojiTestPath: string; 243 | emojiDataPath: string; 244 | unicodeDataPath: string; 245 | namedSequencesPath: string; 246 | namesListPath: string; 247 | annotationsPath: string; 248 | } 249 | 250 | export type UnicodeResources = { 251 | emojiTest(): Promise; 252 | emojiData(): Promise; 253 | unicodeData(): Promise; 254 | namedSequences(): Promise; 255 | namesList(): Promise; 256 | annotations(): Promise; 257 | } 258 | 259 | export async function parseUnicodeResources(resources: UnicodeResources): Promise { 260 | return { 261 | unicodeData: await parseUnicodeData(resources), 262 | emojiVersion: await parseEmojiVersions(resources), 263 | emojiTest: await parseEmojiTest(resources), 264 | namedSequences: await parseNamedSequences(resources), 265 | namesList: await parseNamesList(resources), 266 | annotations: await parseAnnotations(resources), 267 | }; 268 | } 269 | 270 | export async function parseUnicodeData(p: UnicodeResources): Promise { 271 | const data: UnicodeData = {}; 272 | 273 | const text = await p.unicodeData(); 274 | for (const line of text.split('\n')) { 275 | if (!line.length) continue; 276 | const [hex, name, category, combining, bidiClass, decomposition, decimalDigit, digit, numeric, bidiMirrored, unicode1, comment, uppercase, lowercase, titlecase] = line.split(';'); 277 | const code = parseInt(hex, 16); 278 | data[code] = { 279 | name, code, 280 | category: category as GeneralCategory, 281 | combining: parseInt(combining, 10), 282 | bidiClass: bidiClass as BidiClass, 283 | decompositionUD: decomposition.length ? decomposition : undefined, 284 | decimalDigit: decimalDigit.length ? parseInt(decimalDigit, 10) : undefined, 285 | digit: digit.length ? parseInt(digit, 10) : undefined, 286 | numeric: numeric.length ? numeric : undefined, 287 | bidiMirrored: bidiMirrored === 'Y', 288 | unicode1: unicode1.length ? unicode1 : undefined, 289 | comment: comment.length ? [comment] : undefined, 290 | uppercase: uppercase.length ? uppercase : undefined, 291 | lowercase: lowercase.length ? lowercase : undefined, 292 | titlecase: titlecase.length ? titlecase : undefined, 293 | }; 294 | } 295 | 296 | return data; 297 | } 298 | 299 | export type NamesListChar = { 300 | name: string; 301 | code: number; 302 | alias?: string[]; 303 | falias?: string[]; 304 | ref?: number[]; 305 | decomposition?: string[]; 306 | variation?: { 307 | code: number; 308 | sel: string; 309 | name: string; 310 | }[]; 311 | notice?: string[]; 312 | comment?: string[]; 313 | }; 314 | export type NamesListData = { 315 | title: { 316 | title: string; 317 | info: { 318 | subtitle?: string[]; 319 | sub?: string; 320 | comment?: string[]; 321 | notice?: string[]; 322 | break?: true; 323 | } 324 | }, 325 | block: { 326 | start: number; 327 | end: number; 328 | name: string; 329 | sub: { 330 | name: string; 331 | notice?: string[]; 332 | char?: NamesListChar[]; 333 | }[] 334 | }[] 335 | } 336 | 337 | export async function parseNamesList(resources: UnicodeResources): Promise { 338 | const text = await resources.namesList(); 339 | const parser = peggy.generate((await import('./namesList.pegjs?raw')).default); 340 | return parser.parse(text); 341 | } 342 | 343 | type EmojiGroup = { 344 | name: string; 345 | sub: EmojiSubGroup[]; 346 | } 347 | type EmojiSubGroup = { 348 | name: string; 349 | clusters: string[]; 350 | } 351 | type SequenceType = 'component' | 'fully-qualified' | 'minimally-qualified' | 'unqualified'; 352 | type EmojiSequence = { 353 | sequence: string; 354 | type: SequenceType; 355 | name: string; 356 | version: number; 357 | } 358 | export type EmojiTestData = { 359 | groups: EmojiGroup[], 360 | sequences: Dictionary, 361 | }; 362 | 363 | export async function parseEmojiTest(resources: UnicodeResources): Promise { 364 | const groups: EmojiGroup[] = []; 365 | const sequences: Dictionary = {}; 366 | 367 | let g: EmojiGroup = {name: '', sub: []}; 368 | let s: EmojiSubGroup = {name: '', clusters: []}; 369 | 370 | const data = await resources.emojiTest(); 371 | for (const line of data.split('\n')) { 372 | const m = line.match(/^# group: (?.+)$|^# subgroup: (?.+)$|^(?[0-9a-f]+(?: [0-9a-f]+)*) +; +(?[a-z-]+) +# (?[^ ]+) E(?[\d.]+) (?.+)$/i)?.groups; 373 | if (m) { 374 | if (m['group']) { 375 | g = {name: m['group'], sub: []}; 376 | groups.push(g); 377 | } else if (m['subGroup']) { 378 | s = {name: m['subGroup'], clusters: []}; 379 | g.sub.push(s); 380 | } else if (m['code']) { 381 | const sequence = String.fromCodePoint(...m['code'].split(' ').map((hex) => parseInt(hex, 16))); 382 | s.clusters.push(sequence); 383 | sequences[sequence] = { 384 | sequence, 385 | type: m['type'] as SequenceType, 386 | name: m['name'], 387 | version: parseFloat(m['version']), 388 | }; 389 | } 390 | } 391 | } 392 | 393 | return {groups, sequences}; 394 | } 395 | 396 | export type NamedSequencesData = { 397 | cluster: string; 398 | name: string; 399 | }[]; 400 | 401 | export async function parseNamedSequences(resources: UnicodeResources): Promise { 402 | const data = await resources.namedSequences(); 403 | const namedSequences: NamedSequencesData = []; 404 | for (const line of data.split('\n')) { 405 | if (!line.length || line.startsWith('#')) continue; 406 | const m = line.match(/^(?[^#;]+[^#;\s])\s*;\s*(?[0-9A-F][0-9A-F ]+[0-9A-F])\s*(?:#|$)/)?.groups; 407 | if (m) { 408 | const cluster = String.fromCodePoint(...m['code'].split(/\s+/).map(c => parseInt(c, 16))); 409 | namedSequences.push({ 410 | cluster, 411 | name: m['name'] 412 | }); 413 | } else console.warn('[parseNamedSequences] failed to parse', line); 414 | } 415 | return namedSequences; 416 | } 417 | 418 | export type AnnotationsData = { 419 | identity: { 420 | version: { 421 | _cldrVersion: string; 422 | }; 423 | language: string; 424 | }; 425 | annotations: Record; 429 | } 430 | 431 | export async function parseAnnotations(resources: UnicodeResources): Promise { 432 | const data = await (resources.annotations()).then(f => JSON.parse(f)); 433 | return (data.annotations) as AnnotationsData; 434 | } 435 | -------------------------------------------------------------------------------- /wwwassets/script/chars.ts: -------------------------------------------------------------------------------- 1 | export const SoftHyphen = "\u00ad"; 2 | export const ZeroWidthJoiner = '\u200D'; 3 | 4 | /** Variation selector 15: text-style for emoji sequences */ 5 | export const VarSel15 = '\uFE0E'; 6 | /** Variation selector 16: emoji-style for emoji sequences */ 7 | export const VarSel16 = '\uFE0E'; 8 | 9 | /** Used to terminate flag sequences */ 10 | export const CancelTag = '\uDB40\uDC7F'; 11 | /** Used in subdivision flag sequences */ 12 | export const TagLatinSmallLetterA = '\uDB40\uDC61'; 13 | export const TagLatinSmallLetterACode = TagLatinSmallLetterA.codePointAt(0)!; 14 | /** Used in subdivision flag sequences */ 15 | export const TagLatinSmallLetterZ = '\uDB40\uDC7A'; 16 | export const TagLatinSmallLetterZCode = TagLatinSmallLetterZ.codePointAt(0)!; 17 | /** Used in country flag sequences */ 18 | export const RegionalIndicatorSymbolLetterA = "🇦"; 19 | export const RegionalIndicatorSymbolLetterACode = RegionalIndicatorSymbolLetterA.codePointAt(0)!; 20 | /** Used in country flag sequences */ 21 | export const RegionalIndicatorSymbolLetterZ = "🇿"; 22 | export const RegionalIndicatorSymbolLetterZCode = RegionalIndicatorSymbolLetterZ.codePointAt(0)!; -------------------------------------------------------------------------------- /wwwassets/script/config.tsx: -------------------------------------------------------------------------------- 1 | import {Fragment, h} from "preact"; 2 | import {DigitsRow, FirstRow, Layout, SecondRow} from "./layout"; 3 | import {ConfigActionKey, ConfigBuildKey, ConfigLabelKey, ConfigToggleKey, ExitSearchKey, PageKey} from "./key"; 4 | import {Board} from "./board"; 5 | import {useContext} from "preact/hooks"; 6 | import {app, ConfigContext, LayoutContext} from "./appVar"; 7 | import {SC} from "./layout/sc"; 8 | import {BoardState, Keys, mapKeysToSlots, SlottedKeys} from "./boards/utils"; 9 | import {KeyName} from "./keys/base"; 10 | import {ahkOpenLink, ahkReload} from "./ahk"; 11 | 12 | // Backslash and Enter appears on the first or the second row resp. second or both, so they're listed in both 13 | // noinspection JSUnusedLocalSymbols 14 | const SHORTCUT_KEYS = [ 15 | { 16 | name: "Function Row", keys: [ 17 | SC.Esc, 18 | SC.F1, SC.F2, SC.F3, SC.F4, SC.F5, SC.F6, SC.F7, SC.F8, SC.F9, SC.F10, SC.F11, SC.F12, 19 | ] 20 | }, { 21 | name: "Numbers Row", keys: [ 22 | SC.Backtick, 23 | SC.Digit1, SC.Digit2, SC.Digit3, SC.Digit4, SC.Digit5, SC.Digit6, SC.Digit7, SC.Digit8, SC.Digit9, SC.Digit0, 24 | SC.Minus, SC.Equal, 25 | SC.Backspace, 26 | ] 27 | }, { 28 | name: "First Row", keys: [ 29 | SC.Tab, 30 | SC.Q, SC.W, SC.E, SC.R, SC.T, SC.Y, SC.U, SC.I, SC.O, SC.P, 31 | SC.LeftBrace, SC.RightBrace, SC.Backslash, SC.Enter, 32 | ] 33 | }, { 34 | name: "Second Row", keys: [ 35 | SC.CapsLock, 36 | SC.A, SC.S, SC.D, SC.F, SC.G, SC.H, SC.J, SC.K, SC.L, 37 | SC.Semicolon, SC.Apostrophe, SC.Backslash, SC.Enter, 38 | ] 39 | }, { 40 | name: "Third Row", keys: [ 41 | SC.LessThan, 42 | SC.Z, SC.X, SC.C, SC.V, SC.B, SC.N, SC.M, 43 | SC.Comma, SC.Period, SC.Slash, 44 | ] 45 | }, { 46 | name: "Fourth Row", keys: [ 47 | SC.Space 48 | ] 49 | }, { 50 | name: "Numpad", keys: [ 51 | SC.Num0, SC.Num1, SC.Num2, SC.Num3, 52 | SC.Num4, SC.Num5, SC.Num6, 53 | SC.Num7, SC.Num8, SC.Num9, 54 | SC.NumDecimal, 55 | SC.NumDiv, 56 | SC.NumMult, 57 | SC.NumSub, 58 | SC.NumAdd, 59 | SC.NumLock, 60 | ] 61 | } 62 | ] as const; 63 | 64 | const SkinTones = [ 65 | {name: "neutral", symbol: "👤"}, 66 | {name: "light", symbol: "🏻"}, 67 | {name: "medium-light", symbol: "🏼"}, 68 | {name: "medium", symbol: "🏽"}, 69 | {name: "medium-dark", symbol: "🏾"}, 70 | {name: "dark", symbol: "🏿"}, 71 | ] as const; 72 | 73 | export type SkinTone = keyof typeof SkinTones; 74 | 75 | export const DefaultTheme = "material"; 76 | export const Themes: { name: string, url: string, symbol: string }[] = [ 77 | {name: DefaultTheme, url: "style/material.css", symbol: "🔳"}, 78 | {name: "legacy", url: "style/legacy.css", symbol: "⬜"}, 79 | ]; 80 | type ThemeMode = "light" | "dark" | "system"; 81 | type OpenAt = "last-position" | "bottom" | "text-caret" | "mouse"; 82 | 83 | export interface RecentEmoji { 84 | symbol: string; 85 | useCount: number; 86 | } 87 | 88 | export interface AppConfig { 89 | isoKeyboard: boolean; // has an additional key next to left shift 90 | theme: string; 91 | themeMode: ThemeMode; 92 | x: number; 93 | y: number; 94 | width: number; 95 | height: number; 96 | devTools: boolean; 97 | openAt: OpenAt; 98 | opacity: number; 99 | recent: RecentEmoji[]; 100 | skinTone: SkinTone; 101 | preferredVariant: Record; 102 | hideAfterInput: boolean; 103 | mainAfterInput: boolean; 104 | showAliases: boolean; 105 | showCharCodes: boolean; 106 | } 107 | 108 | export const DefaultOpacity = .90; 109 | 110 | export const DefaultConfig: AppConfig = { 111 | isoKeyboard: false, 112 | theme: DefaultTheme, 113 | themeMode: "system", 114 | x: -1, 115 | y: -1, 116 | width: 764, 117 | height: 240, 118 | devTools: false, 119 | openAt: "bottom", 120 | opacity: DefaultOpacity, 121 | skinTone: 0, 122 | preferredVariant: {}, 123 | recent: [], 124 | hideAfterInput: false, 125 | mainAfterInput: false, 126 | showAliases: false, 127 | showCharCodes: false, 128 | }; 129 | 130 | export const ThemesMap = new Map(Themes.map((t) => [t.name, t])); 131 | export const DefaultThemeUrl = ThemesMap.get(DefaultTheme)!!.url; 132 | 133 | export interface ConfigPage { 134 | name: string; 135 | symbol: string; 136 | 137 | keys(config: AppConfig, l: Layout): SlottedKeys; 138 | } 139 | 140 | const ConfigPages: ConfigPage[] = [ 141 | { 142 | name: "General", 143 | symbol: "🛠️", 144 | keys(config: AppConfig, l) { 145 | return { 146 | [SC.Q]: new ConfigToggleKey({ 147 | active: config.isoKeyboard, 148 | statusName: `ISO layout: ${config.isoKeyboard ? 'on' : 'off'}`, 149 | action() { 150 | app().updateConfig({isoKeyboard: !config.isoKeyboard}); 151 | } 152 | }), 153 | [SC.W]: new ConfigLabelKey(ISO layout ( between and )), 155 | ...mapKeysToSlots(l.freeRows[2], [ 156 | // ...SkinTones.map((s, i) => new ConfigActionKey({ 157 | // active: i == config.skinTone, 158 | // action() { 159 | // p.app.updateConfig({skinTone: i}); 160 | // }, 161 | // name: s.name, 162 | // symbol: s.symbol 163 | // })), 164 | new ConfigActionKey({ 165 | action() { 166 | app().updateConfig({preferredVariant: {}}) 167 | }, 168 | active: !Object.keys(config.preferredVariant).length, 169 | name: "Reset Variants", 170 | statusName: "Clear variant preferences for all symbols", 171 | symbol: "🥷" 172 | }) 173 | // new ConfigLabelKey("Default skin tone") 174 | ]), 175 | ...mapKeysToSlots(l.freeRows[3], [ 176 | new ConfigActionKey({ 177 | name: 'Fallback Fonts', 178 | symbol: '🔣', 179 | action: () => ahkOpenLink('https://github.com/gilleswaeber/emoji-keyboard/wiki/Fallback-Fonts'), 180 | }), 181 | new ConfigActionKey({ 182 | name: 'Font Folder', 183 | symbol: '📂', 184 | action: () => ahkOpenLink('.\\wwwassets\\fallback_fonts'), 185 | }), 186 | new ConfigActionKey({ 187 | name: 'Plugins', 188 | symbol: '🪇', 189 | action: () => ahkOpenLink('https://github.com/gilleswaeber/emoji-keyboard/wiki/Plugins'), 190 | }), 191 | new ConfigActionKey({ 192 | name: 'Plugins Folder', 193 | symbol: '📂', 194 | action: () => ahkOpenLink('.\\wwwassets\\plugins'), 195 | }), 196 | ]), 197 | } 198 | } 199 | }, 200 | { 201 | name: "Theme", 202 | symbol: "🎨", 203 | keys(config: AppConfig) { 204 | return { 205 | ...mapKeysToSlots(FirstRow, Themes.map((t) => new ConfigActionKey({ 206 | active: config.theme == t.name, 207 | name: t.name, statusName: `Theme: ${t.name}`, 208 | symbol: t.symbol, 209 | action() { 210 | app().updateConfig({theme: t.name}); 211 | } 212 | }))), 213 | ...mapKeysToSlots(SecondRow, [ 214 | ...([ 215 | ["light", "🌞"], 216 | ["dark", "🌚"], 217 | ["system", "📟"] 218 | ] as const).map(([mode, symbol]) => new ConfigActionKey({ 219 | active: config.themeMode == mode, 220 | name: mode, symbol: symbol, 221 | statusName: `Theme variant: ${mode}`, 222 | action() { 223 | app().updateConfig({themeMode: mode}) 224 | } 225 | })) 226 | ]) 227 | } 228 | } 229 | }, 230 | { 231 | name: "Display", 232 | symbol: "🖥️", 233 | keys(config, l) { 234 | return { 235 | ...mapKeysToSlots(l.freeRows[1], [ 236 | new ConfigActionKey({ 237 | symbol: "⏬", name: "-10", statusName: 'Opacity -10', action() { 238 | app().updateConfig({opacity: Math.max(config.opacity - .1, .2)}) 239 | } 240 | }), 241 | new ConfigActionKey({ 242 | symbol: "🔽", name: "-1", statusName: 'Opacity -1', action() { 243 | app().updateConfig({opacity: Math.max(config.opacity - .01, .2)}) 244 | } 245 | }), 246 | new ConfigActionKey({ 247 | symbol: "🔼", name: "+1", statusName: 'Opacity +1', action() { 248 | app().updateConfig({opacity: Math.min(config.opacity + .01, 1)}) 249 | } 250 | }), 251 | new ConfigActionKey({ 252 | symbol: "⏫", name: "+10", statusName: 'Opacity +10', action() { 253 | app().updateConfig({opacity: Math.min(config.opacity + .1, 1)}) 254 | } 255 | }), 256 | new ConfigActionKey({ 257 | symbol: `${Math.round(config.opacity * 100)}`, 258 | name: "reset", 259 | statusName: 'Reset opacity', 260 | action() { 261 | app().updateConfig({opacity: DefaultOpacity}) 262 | } 263 | }), 264 | new ConfigLabelKey("Opacity") 265 | ]), 266 | ...mapKeysToSlots(l.freeRows[2], [ 267 | ...([ 268 | ["last position", "🗿", "last-position"], 269 | ["bottom", "👇", "bottom"], 270 | ["caret (beta)", "⌨️", "text-caret"], 271 | ["mouse", "🖱️", "mouse"] 272 | ] as const).map(([name, symbol, mode]) => new ConfigActionKey({ 273 | active: config.openAt == mode, 274 | name, statusName: `Open at ${name}`, symbol, 275 | action() { 276 | app().updateConfig({openAt: mode}) 277 | } 278 | })), 279 | new ConfigLabelKey("Open At") 280 | ]), 281 | ...mapKeysToSlots(l.freeRows[3], [ 282 | new ConfigToggleKey({ 283 | active: config.hideAfterInput, 284 | name: 'Hide', statusName: `Hide after input: ${config.hideAfterInput ? 'on' : 'off'}`, 285 | symbol: '🫥', 286 | action() { 287 | app().updateConfig({hideAfterInput: !config.hideAfterInput}) 288 | } 289 | }), 290 | new ConfigToggleKey({ 291 | active: config.mainAfterInput, 292 | name: 'Go Home', 293 | symbol: '🏠', statusName: `Go home after input: ${config.hideAfterInput ? 'on' : 'off'}`, 294 | action() { 295 | app().updateConfig({mainAfterInput: !config.mainAfterInput}) 296 | } 297 | }), 298 | new ConfigLabelKey('After Input') 299 | ]) 300 | }; 301 | } 302 | }, 303 | { 304 | name: "Tools", 305 | symbol: "🔨", 306 | keys(config: AppConfig) { 307 | return { 308 | ...mapKeysToSlots(FirstRow, [ 309 | new ConfigBuildKey(), 310 | new ConfigActionKey({ 311 | action: ahkReload, 312 | name: 'Reload', 313 | symbol: '🔄' 314 | }), 315 | ]), 316 | ...mapKeysToSlots(SecondRow, [ 317 | new ConfigToggleKey({ 318 | active: config.devTools, statusName: `Open DevTools: ${config.devTools ? 'on' : 'off'}`, 319 | action() { 320 | app().updateConfig({devTools: !config.devTools}); 321 | } 322 | }), 323 | new ConfigLabelKey("Open DevTools") 324 | ]), 325 | } 326 | } 327 | }, 328 | { 329 | name: "Details", 330 | symbol: "🔬", 331 | keys(config: AppConfig) { 332 | return { 333 | ...mapKeysToSlots(FirstRow, [ 334 | new ConfigToggleKey({ 335 | active: config.showAliases, 336 | statusName: `Show aliases in status bar: ${config.showAliases ? 'on' : 'off'}`, 337 | action: () => app().updateConfig({showAliases: !config.showAliases}), 338 | name: 'Aliases', 339 | symbol: '📛', 340 | }), 341 | new ConfigToggleKey({ 342 | active: config.showCharCodes, 343 | statusName: `Show char codes in status bar: ${config.showCharCodes ? 'on' : 'off'}`, 344 | action: () => app().updateConfig({showCharCodes: !config.showCharCodes}), 345 | name: 'Codes', 346 | symbol: '🔢', 347 | }), 348 | new ConfigLabelKey('in Status Bar') 349 | ]), 350 | } 351 | } 352 | }, 353 | { 354 | name: "About", 355 | symbol: "📜", 356 | keys(config: AppConfig) { 357 | return { 358 | ...mapKeysToSlots(FirstRow, [ 359 | new ConfigActionKey({ 360 | action: () => ahkOpenLink('https://github.com/gilleswaeber/emoji-keyboard'), 361 | name: 'GitHub', 362 | symbol: '🐙' 363 | }), 364 | new ConfigActionKey({ 365 | action: () => ahkOpenLink('https://github.com/gilleswaeber/emoji-keyboard/blob/master/LICENSE'), 366 | name: 'MIT License', 367 | symbol: '⚖️' 368 | }), 369 | new ConfigActionKey({ 370 | action: () => ahkOpenLink('https://github.com/gilleswaeber/emoji-keyboard#dependencies'), 371 | name: 'Deps', 372 | statusName: 'Dependencies', 373 | symbol: '🪃', 374 | }), 375 | new ConfigLabelKey('Emoji Keyboard by Gilles Waeber') 376 | ]) 377 | } 378 | } 379 | } 380 | ]; 381 | 382 | export class ConfigBoard extends Board { 383 | constructor() { 384 | super({name: '__config', symbol: '🛠️'}); 385 | } 386 | 387 | Contents = ({state}: { state: BoardState | undefined }) => { 388 | const page = Math.min(state?.page ?? 0, ConfigPages.length - 1); 389 | const config = useContext(ConfigContext); 390 | const l = useContext(LayoutContext); 391 | const pageKeys = ConfigPages.map((c, n) => new PageKey(n, n === page, c.name, c.symbol)); 392 | const keys = { 393 | [SC.Backtick]: new ExitSearchKey(), 394 | ...mapKeysToSlots(DigitsRow, pageKeys), 395 | ...ConfigPages[page].keys(config, l), 396 | } 397 | return 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /wwwassets/script/config/boards.ts: -------------------------------------------------------------------------------- 1 | import {SoftHyphen, ZeroWidthJoiner} from "../chars"; 2 | import {VK, VKAbbr} from "../layout/vk"; 3 | import {ArrowsKeyboard} from "./arrows"; 4 | import {MathKeyboard} from "./math"; 5 | import {emojiGroup} from "../unicodeInterface"; 6 | import {UnicodeKeyboard} from "./unicodeBoard"; 7 | import {toCodePoints} from "../builder/builder"; 8 | import {ExtendedLatin} from "./extendedLatin"; 9 | 10 | function unicodeRange(from: string | number, to: string | number): string[] { 11 | const result: string[] = []; 12 | const codeFrom = typeof from === 'number' ? from : toCodePoints(from)[0]; 13 | const codeTo = typeof to === 'number' ? to : toCodePoints(to)[0]; 14 | if (codeFrom && codeTo && codeTo > codeFrom) { 15 | for (let i = codeFrom; i <= codeTo; ++i) result.push(String.fromCodePoint(i)); 16 | } 17 | return result; 18 | } 19 | 20 | /** 21 | * An item that will be placed on one key, either: 22 | * - a symbol 23 | * - a blank key 24 | * - array of 1 symbol: symbol without variants (atm. only emoji skin color) 25 | * - array of 2 symbols: the second symbols is accessed with shift or right-click, e.g. for small and capital letters 26 | * - array of 3+ symbols: least-recently used symbol on left click, other symbols on right click 27 | * - a keyboard key (can be nested) 28 | */ 29 | export type KeyboardItem = string | null | string[] | EmojiKeyboard | Cluster; 30 | export type SpriteRef = { 31 | spriteMap: string; 32 | sprite: string; 33 | }; 34 | export type KeyCap = string | SpriteRef; 35 | export type EmojiKeyboard = { 36 | /** Name must be unique */ 37 | name: string; 38 | /** Name as shown in the status bar */ 39 | statusName?: string; 40 | symbol: KeyCap; 41 | /** Only set to true on the main keyboard */ 42 | top?: true; 43 | /** Do not add to recently used */ 44 | noRecent?: true; 45 | /** Place the items on the free keys, paging when necessary */ 46 | content?: KeyboardItem[] | (() => KeyboardItem[]); 47 | /** Place the items according the Virtual Key code i.e. based on the symbols on the keys */ 48 | byVK?: { [vk in VK | VKAbbr]?: KeyboardItem } 49 | /** Place the items by row */ 50 | byRow?: (KeyboardItem[] | Record)[] 51 | }; 52 | export type Cluster = { 53 | cluster: string; 54 | name: string; 55 | symbol?: KeyCap; 56 | }; 57 | 58 | export function isCluster(item: KeyboardItem): item is Cluster { 59 | return typeof item === 'object' && (item?.hasOwnProperty('cluster') ?? false); 60 | } 61 | 62 | export type SpriteMap = { 63 | path: string, 64 | width: number, 65 | height: number, 66 | padding?: number; 67 | cols: number; 68 | rows: number; 69 | index: Record 70 | } 71 | export type Plugin = { 72 | name: string; 73 | data: PluginData; 74 | } 75 | export type PluginData = { 76 | name: string; 77 | symbol: KeyCap; 78 | boards?: EmojiKeyboard[]; 79 | spriteMaps?: Record; 80 | } 81 | 82 | export const MAIN_BOARD: EmojiKeyboard = { 83 | name: 'Main Board', 84 | top: true, 85 | symbol: '⌨', 86 | content: [ 87 | { 88 | name: "Happy", 89 | symbol: "😀", 90 | content: [ 91 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-smiling"}), 92 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-affection"}), 93 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-tongue"}), 94 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-hand"}), 95 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-neutral-skeptical"}), 96 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-hat"}), 97 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-glasses"}), 98 | ] 99 | }, 100 | { 101 | name: "Unwell", 102 | symbol: "😱", 103 | content: [ 104 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-sleepy"}), 105 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-unwell"}), 106 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-concerned"}), 107 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-negative"}), 108 | ] 109 | }, 110 | { 111 | name: "Roles", 112 | symbol: "👻", 113 | content: [ 114 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "face-costume"}), 115 | ...emojiGroup({group: "People & Body", subGroup: "person-fantasy"}), 116 | ] 117 | }, 118 | { 119 | name: "Body", 120 | symbol: "👍", 121 | content: [ 122 | ...emojiGroup({group: "People & Body", subGroup: "hand-fingers-open"}), 123 | ...emojiGroup({group: "People & Body", subGroup: "hand-fingers-partial"}), 124 | ...emojiGroup({group: "People & Body", subGroup: "hand-single-finger"}), 125 | ...emojiGroup({group: "People & Body", subGroup: "hand-fingers-closed"}), 126 | ...emojiGroup({group: "People & Body", subGroup: "hands"}), 127 | ...emojiGroup({group: "People & Body", subGroup: "hand-prop"}), 128 | ...emojiGroup({group: "People & Body", subGroup: "body-parts"}), 129 | ] 130 | }, 131 | { 132 | name: "Gestures & activities", 133 | symbol: "💃", 134 | content: [ 135 | ...emojiGroup({group: "People & Body", subGroup: "person-gesture"}), 136 | ...emojiGroup({group: "People & Body", subGroup: "person-activity"}), 137 | ...emojiGroup({group: "People & Body", subGroup: "person-resting"}), 138 | ] 139 | }, 140 | { 141 | name: "Persons", 142 | symbol: "👤", 143 | content: [ 144 | ...emojiGroup({group: "People & Body", subGroup: "person"}), 145 | ...emojiGroup({group: "People & Body", subGroup: "person-role"}), 146 | ...emojiGroup({group: "People & Body", subGroup: "person-symbol"}), 147 | ] 148 | }, 149 | { 150 | name: "Emotions", 151 | symbol: "😺", 152 | content: [ 153 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "cat-face"}), 154 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "monkey-face"}), 155 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "heart"}), 156 | ...emojiGroup({group: "Smileys & Emotion", subGroup: "emotion"}), 157 | ] 158 | }, 159 | { 160 | name: "Families", 161 | symbol: "👪", 162 | content: [ 163 | ...emojiGroup({group: "People & Body", subGroup: "family"}), 164 | ] 165 | }, 166 | { 167 | name: "Clothing", 168 | symbol: "👖", 169 | content: [ 170 | ...emojiGroup({group: "Objects", subGroup: "clothing"}), 171 | ] 172 | }, 173 | { 174 | name: "Animals", 175 | symbol: "🐦", 176 | content: [ 177 | ...emojiGroup({group: "Animals & Nature", subGroup: "animal-mammal"}), 178 | ...emojiGroup({group: "Animals & Nature", subGroup: "animal-bird"}), 179 | ...emojiGroup({group: "Animals & Nature", subGroup: "animal-amphibian"}), 180 | ...emojiGroup({group: "Animals & Nature", subGroup: "animal-reptile"}), 181 | ...emojiGroup({group: "Animals & Nature", subGroup: "animal-marine"}), 182 | ...emojiGroup({group: "Animals & Nature", subGroup: "animal-bug"}), 183 | ] 184 | }, 185 | { 186 | name: "Plants", 187 | symbol: "🌹", 188 | content: [ 189 | ...emojiGroup({group: "Animals & Nature", subGroup: "plant-flower"}), 190 | ...emojiGroup({group: "Animals & Nature", subGroup: "plant-other"}), 191 | ] 192 | }, 193 | { 194 | name: "Raw food", 195 | symbol: "🥝", 196 | content: [ 197 | ...emojiGroup({group: "Food & Drink", subGroup: "food-fruit"}), 198 | ...emojiGroup({group: "Food & Drink", subGroup: "food-vegetable"}), 199 | ...emojiGroup({group: "Food & Drink", subGroup: "drink"}), 200 | ...emojiGroup({group: "Food & Drink", subGroup: "dishware"}), 201 | ] 202 | }, 203 | { 204 | name: "Cooked", 205 | symbol: "🌭", 206 | content: [ 207 | ...emojiGroup({group: "Food & Drink", subGroup: "food-prepared"}), 208 | ...emojiGroup({group: "Food & Drink", subGroup: "food-asian"}), 209 | ...emojiGroup({group: "Food & Drink", subGroup: "food-sweet"}), 210 | ] 211 | }, 212 | { 213 | name: "Places", 214 | symbol: "🏡", 215 | content: [ 216 | ...emojiGroup({group: "Travel & Places", subGroup: "place-map"}), 217 | ...emojiGroup({group: "Travel & Places", subGroup: "place-geographic"}), 218 | ...emojiGroup({group: "Travel & Places", subGroup: "place-building"}), 219 | ...emojiGroup({group: "Travel & Places", subGroup: "place-religious"}), 220 | ...emojiGroup({group: "Travel & Places", subGroup: "place-other"}), 221 | ...emojiGroup({group: "Travel & Places", subGroup: "hotel"}), 222 | ] 223 | }, 224 | { 225 | name: "Vehicles", 226 | symbol: "🚗", 227 | content: [ 228 | ...emojiGroup({group: "Travel & Places", subGroup: "transport-ground"}), 229 | ] 230 | }, 231 | { 232 | name: "Ships", 233 | symbol: "✈️", 234 | content: [ 235 | ...emojiGroup({group: "Travel & Places", subGroup: "transport-air"}), 236 | ...emojiGroup({group: "Travel & Places", subGroup: "transport-water"}), 237 | ] 238 | }, 239 | { 240 | name: "Time", 241 | symbol: "⌛", 242 | content: [ 243 | ...emojiGroup({group: "Travel & Places", subGroup: "time"}) 244 | ] 245 | }, 246 | { 247 | name: "Weather", 248 | symbol: "⛅", 249 | content: [ 250 | ...emojiGroup({group: "Travel & Places", subGroup: "sky & weather"}) 251 | ] 252 | }, 253 | { 254 | name: "Sports", 255 | symbol: "🎽", 256 | content: [ 257 | ...emojiGroup({group: "Activities", subGroup: "sport"}), 258 | ...emojiGroup({group: "People & Body", subGroup: "person-sport"}) 259 | ] 260 | }, 261 | { 262 | name: "Activities", 263 | symbol: "🎮", 264 | content: [ 265 | ...emojiGroup({group: "Activities", subGroup: "event"}), 266 | ...emojiGroup({group: "Activities", subGroup: "award-medal"}), 267 | ...emojiGroup({group: "Activities", subGroup: "game"}), 268 | ...emojiGroup({group: "Activities", subGroup: "arts & crafts"}) 269 | ] 270 | }, 271 | { 272 | name: "Sound & light", 273 | symbol: "🎥", 274 | content: [ 275 | ...emojiGroup({group: "Objects", subGroup: "sound"}), 276 | ...emojiGroup({group: "Objects", subGroup: "music"}), 277 | ...emojiGroup({group: "Objects", subGroup: "musical-instrument"}), 278 | ...emojiGroup({group: "Objects", subGroup: "light & video"}) 279 | ] 280 | }, 281 | { 282 | name: "Tech", 283 | symbol: "💻", 284 | content: [ 285 | ...emojiGroup({group: "Objects", subGroup: "phone"}), 286 | ...emojiGroup({group: "Objects", subGroup: "computer"}), 287 | ...emojiGroup({group: "Objects", subGroup: "mail"}), 288 | ] 289 | }, 290 | { 291 | name: "Objects", 292 | symbol: "📜", 293 | content: [ 294 | ...emojiGroup({group: "Objects", subGroup: "book-paper"}), 295 | ...emojiGroup({group: "Objects", subGroup: "money"}), 296 | ...emojiGroup({group: "Objects", subGroup: "writing"}), 297 | ...emojiGroup({group: "Objects", subGroup: "science"}), 298 | ...emojiGroup({group: "Objects", subGroup: "medical"}), 299 | ...emojiGroup({group: "Objects", subGroup: "household"}), 300 | ...emojiGroup({group: "Objects", subGroup: "other-object"}), 301 | ] 302 | }, 303 | { 304 | name: "Work", 305 | symbol: "💼", 306 | content: [ 307 | ...emojiGroup({group: "Objects", subGroup: "office"}), 308 | ...emojiGroup({group: "Objects", subGroup: "lock"}), 309 | ...emojiGroup({group: "Objects", subGroup: "tool"}) 310 | ] 311 | }, 312 | { 313 | name: "Signs", 314 | symbol: "⛔", 315 | content: [ 316 | ...emojiGroup({group: "Symbols", subGroup: "transport-sign"}), 317 | ...emojiGroup({group: "Symbols", subGroup: "warning"}), 318 | ...emojiGroup({group: "Symbols", subGroup: "zodiac"}), 319 | ...emojiGroup({group: "Flags", subGroup: "flag"}), 320 | ] 321 | }, 322 | { 323 | name: "Symbols", 324 | symbol: "⚜️", 325 | content: [ 326 | ...emojiGroup({group: "Symbols", subGroup: "religion"}), 327 | ...emojiGroup({group: "Symbols", subGroup: "gender"}), 328 | ...emojiGroup({group: "Symbols", subGroup: "punctuation"}), 329 | ...emojiGroup({group: "Symbols", subGroup: "currency"}), 330 | ...emojiGroup({group: "Symbols", subGroup: "other-symbol"}) 331 | ] 332 | }, 333 | { 334 | name: "Alphanum", 335 | symbol: "🔤", 336 | content: [ 337 | ...emojiGroup({group: "Symbols", subGroup: "alphanum"}) 338 | ] 339 | }, 340 | { 341 | name: "Geometric & keys", 342 | symbol: "🔷", 343 | content: [ 344 | ...emojiGroup({group: "Symbols", subGroup: "keycap"}), 345 | ...emojiGroup({group: "Symbols", subGroup: "geometric"}), 346 | ...emojiGroup({group: "Symbols", subGroup: "av-symbol"}) 347 | ] 348 | }, 349 | { 350 | name: "World Flags", 351 | symbol: "🌐", 352 | content: [ 353 | ...emojiGroup({group: "Flags", subGroup: "country-flag"}), 354 | ...emojiGroup({group: "Flags", subGroup: "subdivision-flag"}), 355 | ] 356 | }, 357 | { 358 | name: "Greek", 359 | symbol: "π", 360 | noRecent: true, 361 | content: [ 362 | "∂", 363 | "ϵ", 364 | "ϑ", 365 | "ϴ", 366 | "ϰ", 367 | "ϖ", 368 | "ϱ", 369 | "ϕ", 370 | "∇", 371 | ["ϝ", "Ϝ"], 372 | ], 373 | byVK: { 374 | a: ["α", "Α"], 375 | b: ["β", "Β"], 376 | c: ["ψ", "Ψ"], 377 | d: ["δ", "Δ"], 378 | e: ["ε", "Ε"], 379 | f: ["φ", "Φ"], 380 | g: ["γ", "Γ"], 381 | h: ["η", "Η"], 382 | i: ["ι", "Ι"], 383 | j: ["ξ", "Ξ"], 384 | k: ["κ", "Κ"], 385 | l: ["λ", "Λ"], 386 | m: ["μ", "Μ"], 387 | n: ["ν", "Ν"], 388 | o: ["ο", "Ο"], 389 | p: ["π", "Π"], 390 | q: ";", 391 | r: ["ρ", "Ρ"], 392 | s: ["σ", "Σ"], 393 | t: ["τ", "Τ"], 394 | u: ["θ", "Θ"], 395 | v: ["ω", "Ω"], 396 | w: "ς", 397 | x: ["χ", "Χ"], 398 | y: ["υ", "Υ"], 399 | z: ["ζ", "Ζ"], 400 | [VK.Period]: "·", 401 | [VK.Comma]: "ϐ", 402 | } 403 | }, 404 | { 405 | name: "Boxes", 406 | symbol: "╚", 407 | byRow: [ 408 | [ 409 | ["┌", "╔"], 410 | ["┬", "╦"], 411 | ["┐", "╗"], 412 | ["┏"], 413 | ["┳"], 414 | ["┓"], 415 | ["╒", "╓"], 416 | ["╤", "╥"], 417 | ["╕", "╖"], 418 | "╭", 419 | "╮", 420 | { 421 | name: "Dashed", 422 | symbol: "┉", 423 | byRow: [ 424 | ["┆", "┇", "┊", "┋", "╎", "╏"], 425 | ["┄", "┅", "┈", "┉", "╌", "╍"] 426 | ] 427 | } 428 | ], 429 | [ 430 | ["├", "╠"], 431 | ["┼", "╬"], 432 | ["┤", "╣"], 433 | ["┣"], 434 | ["╋"], 435 | ["┫"], 436 | ["╞", "╟"], 437 | ["╪", "╫"], 438 | ["╡", "╢"], 439 | "╰", 440 | "╯", 441 | { 442 | name: "Mixed Lines", 443 | symbol: "╆", 444 | content: [ 445 | "┍", "┎", "┭", "┮", 446 | "┯", "┰", "┱", "┲", 447 | "┑", "┒", 448 | "┝", "┞", "┟", "┠", "┡", "┢", 449 | "┽", "┾", "┿", "╀", "╁", "╂", "╃", "╄", "╅", "╆", "╇", "╈", "╉", "╊", 450 | "┥", "┦", "┧", "┨", "┩", "┪", 451 | "┕", "┖", 452 | "┵", "┶", "┷", "┸", "┹", "┺", 453 | "┙", "┚", 454 | "╽", "╿", 455 | "╼", "╾", 456 | ] 457 | } 458 | ], 459 | [ 460 | ["└", "╚"], 461 | ["┴", "╩"], 462 | ["┘", "╝"], 463 | ["┗"], 464 | ["┻"], 465 | ["┛"], 466 | ["╘", "╙"], 467 | ["╧", "╨"], 468 | ["╛", "╜"], 469 | "╱", 470 | "╲", 471 | ], 472 | [ 473 | ["│", "║"], 474 | ["─", "═"], 475 | "╳", 476 | ["┃"], 477 | ["━"], 478 | ["╴", "╸"], 479 | ["╵", "╹"], 480 | ["╷", "╻"], 481 | ["╶", "╺"], 482 | ] 483 | ], 484 | }, 485 | ExtendedLatin, 486 | ArrowsKeyboard, 487 | { 488 | name: `Typo${SoftHyphen}graphy`, 489 | symbol: "‽", 490 | content: [ 491 | "\u00a0", // No-Break\nSpace 492 | "\u202f", // Narrow\nNo-Break\nSpace 493 | "\u2001", // EM\nQuad 494 | "\u2000", // EN\nQuad 495 | "\u2003", // EM\nSpace 496 | "\u2002", // EN\nSpace 497 | "\u2004", // ⅓ EM\nSpace 498 | "\u2005", // ¼ EM\nSpace 499 | "\u2006", // ⅙ EM\nSpace 500 | "\u2009", // Thin\nSpace 501 | "\u200a", // Hair\nSpace 502 | "\u2007", // Figure\nSpace 503 | "\u2008", // Punctuation\nSpace 504 | "\u200b", // Zero\nWidth\nSpace 505 | "\u200c", // Zero\nWidth\nNon-Joiner 506 | ZeroWidthJoiner, 507 | "\u205f", // Medium\nMath\nSpace 508 | "\u3000", // Ideographic\nSpace 509 | SoftHyphen, // Soft\nHyphen 510 | "–", "—", "―", // Hyphens 511 | "“", "”", "‟", "„", "«", "»", "‹", "›", "‘", "’", "‛", "‚", // Quotes 512 | "¿", "¡", "‽", "‼", "°", "¦", // Punctuation 513 | "·", 514 | "•", 515 | ] 516 | }, 517 | { 518 | name: "Currency", 519 | symbol: "¤", 520 | content: [ 521 | "¤", 522 | /* Afghani */ "₳", 523 | /* Austral */ "؋", 524 | /* Baht */ "฿", 525 | /* Bitcoin */ "₿", 526 | /* Cedi */ "₵", 527 | /* Cent */ "¢", 528 | /* Colon */ "₡", 529 | /* Cruzeiro */ "₢", 530 | /* Dollar */ "$", 531 | /* Dong */ "₫", 532 | /* Drachma */ "₯", 533 | /* Euro, Currency */ "₠", 534 | /* Euro, Symbol */ "€", 535 | /* Floren */ "ƒ", 536 | /* Franc */ "₣", 537 | /* Guarani */ "₲", 538 | /* Hryvnia */ "₴", 539 | /* Kip */ "₭", 540 | /* Lari */ "₾", 541 | /* Lira */ "₤", 542 | /* Lira, Turkish */ "₺", 543 | /* Livre Tournois */ "₶", 544 | /* Manat */ "₼", 545 | /* Mark, German */ "ℳ", 546 | /* Mark, Nordic */ "₻", 547 | /* Mill */ "₥", 548 | /* Naira */ "₦", 549 | /* Penny, German */ "₰", 550 | /* Peseta */ "₧", 551 | /* Peso */ "₱", 552 | /* Pound */ "£", 553 | /* Rial */ "﷼", 554 | /* Riel, Khmer */ "៛", 555 | /* Ruble */ "₽", 556 | /* Rupee */ "₨", 557 | /* Rupee, Bengali */ "৳", 558 | /* Rupee, Gujarati */ "૱", 559 | /* Rupee, Indian */ "₹", 560 | /* Rupee, Mark */ "৲", 561 | /* Rupee, Tamil */ "௹", 562 | /* Sheqel, New */ "₪", 563 | /* Som */ "⃀", 564 | /* Spesmilo */ "₷", 565 | /* Tenge */ "₸", 566 | /* Tugrik */ "₮", 567 | /* Won */ "₩", 568 | /* Yen */ "¥", 569 | /* Yen 2 */ "円", 570 | /* Yuan */ "元", 571 | /* Yuan 2 */ "圓", 572 | ] 573 | }, 574 | MathKeyboard, 575 | UnicodeKeyboard, 576 | ] 577 | }; 578 | -------------------------------------------------------------------------------- /wwwassets/script/config/fallback.ts: -------------------------------------------------------------------------------- 1 | import {Version} from "../osversion"; 2 | 3 | export type IconRequirement = { 4 | type: "windows"; 5 | /** Windows version in which the elements are supported */ 6 | windows: string; 7 | } | { 8 | type: "zwjEmoji"; 9 | /** Sequence with ZWJ to check: supported if the ZWJ sequence is shorter than the sequence with the ZWJs removed */ 10 | zwjEmoji: string; 11 | } | { 12 | type: "emoji"; 13 | /** Emoji to check: supported if the width is non-zero with a fallback on a zero-width font */ 14 | emoji: string; 15 | }; 16 | /** Fallback configuration: for each sequence we look for the first item matching and then use the fallback font if the conditions don't match. */ 17 | export const IconFallback: { 18 | requirement: IconRequirement; 19 | /** Match when unicode version >= number */ 20 | version?: Version; 21 | /** Match when emoji version >= number */ 22 | emojiVersion?: number; 23 | /** Match for a given text range */ 24 | ranges?: { 25 | from: string, 26 | to: string, 27 | }[], 28 | /** Match for given sequences or characters */ 29 | clusters?: Set; 30 | }[] = [ 31 | { 32 | // Not supported on Windows 33 | requirement: {type: "windows", windows: "99"}, 34 | ranges: [ 35 | {from: "🇦", to: "🇿🇿"}, 36 | { 37 | from: "\ud83c\udff4\udb40\udc61", // WAVING BLACK FLAG, TAG LATIN SMALL LETTER A 38 | to: "\ud83c\udff4\udb40\udc7a\udb40\udc7a", // WAVING BLACK FLAG, TAG LATIN SMALL LETTER Z, TAG LATIN SMALL LETTER Z 39 | } 40 | ] 41 | }, 42 | { 43 | requirement: {type: "emoji", emoji: "🪉"}, // Harp 44 | version: new Version("16"), 45 | }, 46 | { 47 | requirement: {type:"zwjEmoji", zwjEmoji: "🐦‍🔥"}, // Phoenix 48 | version: new Version("15.1"), 49 | }, 50 | { 51 | requirement: {type:"zwjEmoji", zwjEmoji: "🐦‍⬛"}, // Black Bird 52 | version: new Version("15"), 53 | }, 54 | { 55 | // Windows 11 Fluent emoji font 56 | requirement: {type:"zwjEmoji", zwjEmoji: "🫱‍🫲"}, // Handshake 57 | version: new Version("14"), 58 | }, 59 | { 60 | // Windows 11 Preview 61 | requirement: {type: "windows", windows: "10.0.21277"}, 62 | version: new Version("13"), 63 | clusters: new Set([ 64 | "*️⃣", 65 | // Unicode 12.1: partial support 66 | "🧑‍🦰", "🧑🏻‍🦰", "🧑🏼‍🦰", "🧑🏽‍🦰", "🧑🏾‍🦰", "🧑🏿‍🦰", // Person Red Hair 67 | "🧑‍🦱", "🧑🏻‍🦱", "🧑🏼‍🦱", "🧑🏽‍🦱", "🧑🏾‍🦱", "🧑🏿‍🦱", // Person Curly Hair 68 | "🧑‍🦳", "🧑🏻‍🦳", "🧑🏼‍🦳", "🧑🏽‍🦳", "🧑🏾‍🦳", "🧑🏿‍🦳", // Person White Hair 69 | "🧑‍🦲", "🧑🏻‍🦲", "🧑🏼‍🦲", "🧑🏽‍🦲", "🧑🏾‍🦲", "🧑🏿‍🦲", // Person Bald 70 | "🧑‍⚕️", "🧑🏻‍⚕", "🧑🏼‍⚕", "🧑🏽‍⚕", "🧑🏾‍⚕", "🧑🏿‍⚕", // Health Worker 71 | "🧑‍🎓", "🧑🏻‍🎓", "🧑🏼‍🎓", "🧑🏽‍🎓", "🧑🏾‍🎓", "🧑🏿‍🎓", // Student 72 | "🧑‍🏫", "🧑🏻‍🏫", "🧑🏼‍🏫", "🧑🏽‍🏫", "🧑🏾‍🏫", "🧑🏿‍🏫", // Teacher 73 | "🧑‍⚖️", "🧑🏻‍⚖", "🧑🏼‍⚖", "🧑🏽‍⚖", "🧑🏾‍⚖", "🧑🏿‍⚖", // Judge 74 | "🧑‍🌾", "🧑🏻‍🌾", "🧑🏼‍🌾", "🧑🏽‍🌾", "🧑🏾‍🌾", "🧑🏿‍🌾", // Farmer 75 | "🧑‍🍳", "🧑🏻‍🍳", "🧑🏼‍🍳", "🧑🏽‍🍳", "🧑🏾‍🍳", "🧑🏿‍🍳", // Cook 76 | "🧑‍🔧", "🧑🏻‍🔧", "🧑🏼‍🔧", "🧑🏽‍🔧", "🧑🏾‍🔧", "🧑🏿‍🔧", // Mechanic 77 | "🧑‍🏭", "🧑🏻‍🏭", "🧑🏼‍🏭", "🧑🏽‍🏭", "🧑🏾‍🏭", "🧑🏿‍🏭", // Factory Worker 78 | "🧑‍💼", "🧑🏻‍💼", "🧑🏼‍💼", "🧑🏽‍💼", "🧑🏾‍💼", "🧑🏿‍💼", // Office Worker 79 | "🧑‍🔬", "🧑🏻‍🔬", "🧑🏼‍🔬", "🧑🏽‍🔬", "🧑🏾‍🔬", "🧑🏿‍🔬", // Scientist 80 | "🧑‍💻", "🧑🏻‍💻", "🧑🏼‍💻", "🧑🏽‍💻", "🧑🏾‍💻", "🧑🏿‍💻", // Technologist 81 | "🧑‍🎤", "🧑🏻‍🎤", "🧑🏼‍🎤", "🧑🏽‍🎤", "🧑🏾‍🎤", "🧑🏿‍🎤", // Singer 82 | "🧑‍🎨", "🧑🏻‍🎨", "🧑🏼‍🎨", "🧑🏽‍🎨", "🧑🏾‍🎨", "🧑🏿‍🎨", // Artist 83 | "🧑‍✈️", "🧑🏻‍✈", "🧑🏼‍✈", "🧑🏽‍✈", "🧑🏾‍✈", "🧑🏿‍✈", // Pilot 84 | "🧑‍🚀", "🧑🏻‍🚀", "🧑🏼‍🚀", "🧑🏽‍🚀", "🧑🏾‍🚀", "🧑🏿‍🚀", // Astronaut 85 | "🧑‍🚒", "🧑🏻‍🚒", "🧑🏼‍🚒", "🧑🏽‍🚒", "🧑🏾‍🚒", "🧑🏿‍🚒", // Firefighter 86 | "🧑‍🦯", "🧑🏻‍🦯", "🧑🏼‍🦯", "🧑🏽‍🦯", "🧑🏾‍🦯", "🧑🏿‍🦯", // Person with White Cane 87 | "🧑‍🦼", "🧑🏻‍🦼", "🧑🏼‍🦼", "🧑🏽‍🦼", "🧑🏾‍🦼", "🧑🏿‍🦼", // Person in Motorized Wheelchair 88 | "🧑‍🦽", "🧑🏻‍🦽", "🧑🏼‍🦽", "🧑🏽‍🦽", "🧑🏾‍🦽", "🧑🏿‍🦽", // Person in Manual Wheelchair 89 | ]), 90 | }, 91 | { 92 | // Windows 19H1 93 | requirement: {type: "windows", windows: "10.0.18277"}, 94 | version: new Version("12"), 95 | clusters: new Set([ 96 | "🏴‍☠️", 97 | "#\ufe0f\u20e3", 98 | "0\ufe0f\u20e3", 99 | "1\ufe0f\u20e3", 100 | "2\ufe0f\u20e3", 101 | "3\ufe0f\u20e3", 102 | "4\ufe0f\u20e3", 103 | "5\ufe0f\u20e3", 104 | "6\ufe0f\u20e3", 105 | "7\ufe0f\u20e3", 106 | "8\ufe0f\u20e3", 107 | "9\ufe0f\u20e3" 108 | ]) 109 | }, 110 | { 111 | // Redstone 5 112 | requirement: {type: "windows", windows: "10.0.17723"}, 113 | version: new Version("11"), 114 | }, 115 | { 116 | // Windows Fall Creator Update 117 | requirement: {type: "windows", windows: "10.0.16226"}, 118 | version: new Version("5"), 119 | // emojiVersion: 5, 120 | // version: 10 121 | }, 122 | { 123 | // Windows Creator Update 124 | requirement: {type: "windows", windows: "10.0.15063"}, 125 | version: new Version("4"), 126 | // emojiVersion: 4 127 | }, 128 | { 129 | // Windows Anniversary Update 130 | requirement: {type: "windows", windows: "10.0.14393"}, 131 | // version: 9 132 | }, 133 | { 134 | requirement: {type: "windows", windows: "10"}, 135 | version: new Version("0.01"), 136 | // version: 5 137 | }, 138 | ]; 139 | -------------------------------------------------------------------------------- /wwwassets/script/config/unicodeBoard.ts: -------------------------------------------------------------------------------- 1 | import {charInfo, UnicodeData} from "../unicodeInterface"; 2 | import {GeneralCategory} from "../builder/unicode"; 3 | import {toHex} from "../builder/consolidated"; 4 | import type {EmojiKeyboard} from "./boards"; 5 | 6 | const BlockSymbolOverride: Record = { 7 | 0x2000: "†", 8 | 0x2800: "⠳", 9 | 0xFFF0: "�", 10 | 0x1D100: "𝄞", 11 | 0x1F300: "🌌", 12 | 0x1F900: "🤔", 13 | }; 14 | 15 | function validCode(code: number): boolean { 16 | const info = charInfo(code); 17 | if (!info) return false; 18 | return !info.control && !info.reserved && !info.notACharacter; 19 | } 20 | 21 | export const UnicodeKeyboard: EmojiKeyboard = { 22 | name: "Unicode Blocks", 23 | symbol: "∪", 24 | content: UnicodeData.blocks.filter( 25 | b => b.sub.some(s => s.char?.some(validCode)) || b.isCJKUnifiedIdeographs 26 | ).map(block => { 27 | const codes = block.sub.map(sub => sub.char).flat().filter(validCode); 28 | let symbol = String.fromCodePoint(codes[0] ?? block.start); 29 | for (const code of codes) { 30 | const info = charInfo(code); 31 | if (info?.ca?.startsWith(GeneralCategory.Letter)) { 32 | symbol = String.fromCodePoint(code); 33 | break; 34 | } 35 | } 36 | if (BlockSymbolOverride[block.start]) symbol = BlockSymbolOverride[block.start]; 37 | const statusName = `${block.name} ${toHex(block.start)}–${toHex(block.end)}`; 38 | return { 39 | name: block.name, 40 | statusName, 41 | symbol, 42 | content: () => { 43 | if (codes.length || !block.isCJKUnifiedIdeographs) { 44 | return codes.map(c => String.fromCodePoint(c)); 45 | } else { 46 | const content = []; 47 | for (let i = block.start; i <= block.end; i++) { 48 | content.push(String.fromCodePoint(i)); 49 | } 50 | return content; 51 | } 52 | }, 53 | } as EmojiKeyboard; 54 | }) 55 | }; 56 | -------------------------------------------------------------------------------- /wwwassets/script/emojis.ts: -------------------------------------------------------------------------------- 1 | import {UnicodeData} from "./unicodeInterface"; 2 | import {toCodePoints} from "./builder/builder"; 3 | import {fromEntries} from "./helpers"; 4 | import type {ExtendedClusterInformation} from "./builder/consolidated"; 5 | 6 | const u = UnicodeData 7 | 8 | const ESCAPE_REGEX = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-'].join('|\\') + ')', 'g'); 9 | const SEPARATOR = '%'; 10 | const SEPARATOR_REGEX_G = /%/g; 11 | const SEARCH_ID = SEPARATOR + '([\\d,]+)' + SEPARATOR + '[^' + SEPARATOR + ']*'; 12 | 13 | // Map preserves insertion order 14 | const index = new Map(); 15 | (function () { 16 | /** named sequences to be put after emojis and unicode tables */ 17 | const endClusters: ExtendedClusterInformation[] = []; 18 | for (const c of Object.values(u.clusters)) { 19 | if (!c.parent) { 20 | if (c.version) index.set(toCodePoints(c.cluster).join(','), `${c.name} ${c.alias?.join(' ') ?? ''}`); 21 | else endClusters.push(c); 22 | } 23 | } 24 | for (const c of Object.values(u.chars)) { 25 | if (!c.reserved && !c.notACharacter) { 26 | index.set(c.code.toString(), `${c.n} ${c.alias?.join(' ') ?? ''} ${c.falias?.join(' ') ?? ''}`); 27 | } 28 | } 29 | for (const c of endClusters) { 30 | index.set(toCodePoints(c.cluster).join(','), `${c.name} ${c.alias?.join(' ') ?? ''}`); 31 | } 32 | if (index.size) console.log("Unicode index built", index); 33 | })(); 34 | const searchHaystack = Array.from(index.entries()).map(([k, v]) => `${SEPARATOR}${k}${SEPARATOR}${v.replace(SEPARATOR_REGEX_G, '')}`).join(''); 35 | 36 | function escapeRegex(str: string): string { 37 | return str.replace(ESCAPE_REGEX, '\\$1'); 38 | } 39 | 40 | export function search(needle: string): string[] { 41 | let result: string[] = []; 42 | needle.split(/\s+/g).forEach((n, k) => { 43 | let filter: string[] = []; 44 | if (!n.length) return; 45 | const re = new RegExp(SEARCH_ID + escapeRegex(n.replace(SEPARATOR_REGEX_G, '')), 'ig'); 46 | for (let m; (m = re.exec(searchHaystack));) { 47 | filter.push(m[1]); 48 | } 49 | if (k == 0) result = filter; 50 | else { 51 | const f = new Set(filter); 52 | result = result.filter(v => f.has(v)); 53 | } 54 | }) 55 | 56 | // Score the entries, +1 for full word match, -1 for a match not at the start of a word 57 | const scores = fromEntries(result.map(v => [v, 0] as const)); 58 | for (const n of needle.split(/\s+/g)) { 59 | if (!n.length) continue; 60 | const re1 = new RegExp('(?:[^a-z]|^)' + escapeRegex(n) + '(?:[^a-z]|$)', 'i'); 61 | const re2 = new RegExp('(?:[^a-z]|^)' + escapeRegex(n), 'i'); 62 | for (const r of result) { 63 | if (re1.test(index.get(r)!)) scores[r]++; 64 | else if (!re2.test(index.get(r)!)) scores[r]--; 65 | } 66 | } 67 | result.sort((a, b) => scores[b] - scores[a]); 68 | return result.map(v => String.fromCodePoint(...v.split(',').map(v => parseInt(v, 10)))); 69 | } 70 | -------------------------------------------------------------------------------- /wwwassets/script/entryPoint.ts: -------------------------------------------------------------------------------- 1 | import {startApp} from "./app"; 2 | import {parseUnicodeResources, UnicodeResources} from "./builder/unicode"; 3 | import memoizeOne from "memoize-one"; 4 | import {ConsolidatedUnicodeData, consolidateUnicodeData} from "./builder/consolidated"; 5 | 6 | if (typeof window != "undefined") { 7 | startApp(); 8 | } 9 | 10 | export async function nodeBuild({ucdVersion, emojiVersion, cldrVersion}: {ucdVersion: string, emojiVersion: string, cldrVersion: string}): Promise { 11 | const resources: UnicodeResources = { 12 | emojiTest: memoizeOne(() => fetch(`https://unicode.org/Public/emoji/${emojiVersion}/emoji-test.txt`).then(r => r.text())), 13 | emojiData: memoizeOne(() => fetch(`https://unicode.org/Public/${ucdVersion}/ucd/emoji/emoji-data.txt`).then(r => r.text())), 14 | unicodeData: memoizeOne(() => fetch(`https://unicode.org/Public/${ucdVersion}/ucd/UnicodeData.txt`).then(r => r.text())), 15 | namesList: memoizeOne(() => fetch(`https://unicode.org/Public/${ucdVersion}/ucd/NamesList.txt`).then(r => r.text())), 16 | namedSequences: memoizeOne(() => fetch(`https://unicode.org/Public/${ucdVersion}/ucd/NamedSequences.txt`).then(r => r.text())), 17 | annotations: memoizeOne(() => fetch(`https://raw.githubusercontent.com/unicode-org/cldr-json/${cldrVersion}/cldr-json/cldr-annotations-full/annotations/en/annotations.json`).then(r => r.text())), 18 | } 19 | 20 | const ctx = await parseUnicodeResources(resources); 21 | 22 | return consolidateUnicodeData(ctx); 23 | } 24 | -------------------------------------------------------------------------------- /wwwassets/script/helpers.ts: -------------------------------------------------------------------------------- 1 | export declare type Dictionary = { [key: string]: T }; 2 | 3 | /** Mark as unreachable */ 4 | export function unreachable(x: never): never { 5 | console.error("Unexpected", x); 6 | throw new Error("Unexpected: " + x); 7 | } 8 | 9 | /** CSS class builder, e.g. for React */ 10 | export function cl(...args: (null|undefined|string|Dictionary)[]): string { 11 | return args.flatMap(a => { 12 | if (typeof a === "string") return a; 13 | if (a === null || a === undefined) return ""; 14 | return Object.entries(a).map(([k, v]) => v ? k : ""); 15 | }).join(' '); 16 | } 17 | 18 | type FromEntries = { 19 | [K in E[number][0]]: Extract[1] 20 | }; 21 | 22 | /** Same as Object.fromEntries but properly typed */ 23 | export function fromEntries(entries: T): FromEntries { 24 | const object: any = {}; 25 | for (const [k, v] of entries) { 26 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access 27 | object[k] = v; 28 | } 29 | return object as FromEntries; 30 | } 31 | -------------------------------------------------------------------------------- /wwwassets/script/key.tsx: -------------------------------------------------------------------------------- 1 | import {AppMode} from "./app"; 2 | import {ComponentChild, h} from "preact"; 3 | import {Board} from "./board"; 4 | import {app, ConfigBuildingContext, ConfigContext} from "./appVar"; 5 | import {cl} from "./helpers"; 6 | import {clusterAliases, clusterName, clusterVariants} from "./unicodeInterface"; 7 | import {makeBuild, toCodePoints} from "./builder/builder"; 8 | import {useContext} from "preact/hooks"; 9 | import {SC} from "./layout/sc"; 10 | import {VarSel15} from "./chars"; 11 | import {Key, KeyName} from "./keys/base"; 12 | import {Symbol} from "./keys/symbol"; 13 | import {AppConfig} from "./config"; 14 | import {toHex} from "./builder/consolidated"; 15 | import {KeyCap} from "./config/boards"; 16 | 17 | export class ConfigKey extends Key { 18 | constructor() { 19 | super({name: "Settings", symbol: "🛠️" + VarSel15, clickAlwaysAlternate: true, keyNamePrefix: "⇧"}); 20 | } 21 | 22 | actAlternate(): void { 23 | app().setMode(AppMode.SETTINGS); 24 | } 25 | } 26 | 27 | export class ConfigActionKey extends Key { 28 | protected action: () => void; 29 | 30 | constructor({action, ...p}: { 31 | active?: boolean, 32 | action(): void, 33 | name: string, 34 | statusName?: string, 35 | symbol: string 36 | }) { 37 | super(p); 38 | this.action = action; 39 | } 40 | 41 | act() { 42 | this.action(); 43 | } 44 | } 45 | 46 | export class ConfigToggleKey extends ConfigActionKey { 47 | constructor({active, ...p}: { 48 | active?: boolean, 49 | action(): void, 50 | name?: string, 51 | statusName?: string, 52 | symbol?: string 53 | }) { 54 | active = active ?? false; 55 | super({ 56 | name: active ? "On" : "Off", 57 | symbol: active ? "✔️" : "❌", 58 | active, 59 | ...p, 60 | }); 61 | } 62 | } 63 | 64 | export class ConfigLabelKey extends Key { 65 | constructor(private text: ComponentChild) { 66 | super({name: "", symbol: ""}); 67 | } 68 | 69 | Contents = ({code}: { code: SC }) => { 70 | return
71 |
72 |
{this.text}
73 |
; 74 | } 75 | } 76 | 77 | export class ConfigBuildKey extends Key { 78 | constructor() { 79 | super({name: "Build", symbol: "🏗️"}) 80 | } 81 | 82 | act() { 83 | makeBuild(); 84 | } 85 | 86 | Contents = ({code}: { code: SC }) => { 87 | const active = useContext(ConfigBuildingContext); 88 | return
{ 91 | e.preventDefault(); 92 | e.shiftKey || this.clickAlwaysAlternate ? this.actAlternate() : this.act(); 93 | }} 94 | onContextMenu={(e) => { 95 | e.preventDefault(); 96 | this.actAlternate(); 97 | }} 98 | onMouseOver={() => app().updateStatus(this.name)} 99 | > 100 |
101 |
{this.name}
102 | 103 |
; 104 | } 105 | } 106 | 107 | export class BackKey extends Key { 108 | constructor() { 109 | super({name: 'Back/🛠️', symbol: '←', keyType: "back"}); 110 | } 111 | 112 | act() { 113 | app().back(); 114 | } 115 | 116 | actAlternate() { 117 | app().setMode(AppMode.SETTINGS); 118 | } 119 | } 120 | 121 | export class KeyboardKey extends Key { 122 | constructor(private target: Board) { 123 | super({name: target.name, statusName: target.statusName, symbol: target.symbol}); 124 | } 125 | 126 | act() { 127 | app().setBoard(this.target); 128 | } 129 | } 130 | 131 | export class PageKey extends Key { 132 | constructor(private page: number, active: boolean, name?: string, symbol?: string) { 133 | super({name: name ?? `page ${page + 1}`, symbol: symbol ?? '' + (page + 1), active}); 134 | } 135 | 136 | act() { 137 | if (!this.active) app().setPage(this.page); 138 | } 139 | } 140 | 141 | export class SearchKey extends Key { 142 | constructor() { 143 | super({name: 'search', symbol: '🔎' + VarSel15}); 144 | } 145 | 146 | act() { 147 | app().setMode(AppMode.SEARCH); 148 | } 149 | } 150 | 151 | export class ExitSearchKey extends Key { 152 | constructor() { 153 | super({name: 'back', symbol: '←'}); 154 | } 155 | 156 | act() { 157 | app().setMode(AppMode.MAIN); 158 | } 159 | } 160 | 161 | export class ClusterKey extends Key { 162 | private readonly variants: string[] | undefined; 163 | private readonly variantOf: string | undefined; 164 | private readonly noRecent: boolean; 165 | private readonly alt: boolean; 166 | private readonly lu: boolean; 167 | private readonly upperName: string; 168 | 169 | constructor(private cluster: string, p?: { variants?: string[], variantOf?: string, noRecent?: boolean, symbol?: KeyCap, name?: string }) { 170 | const name = p?.name ?? clusterName(cluster); 171 | const variants = p?.variants ?? clusterVariants(cluster); 172 | super({ 173 | name, 174 | symbol: p?.symbol ?? cluster, 175 | keyType: "char", 176 | }); 177 | this.alt = !!variants && variants.length > 2; 178 | this.lu = variants?.length === 2; 179 | this.upperName = variants?.length == 2 ? variants[1] : ""; 180 | this.noRecent = p?.noRecent ?? false; 181 | this.variants = variants; 182 | this.variantOf = p?.variantOf; 183 | } 184 | 185 | act(config?: AppConfig) { 186 | if (this.alt) { 187 | config ??= app().getConfig(); 188 | const cluster = config.preferredVariant[this.cluster] ?? this.cluster; 189 | app().send(cluster, {noRecent: this.noRecent, variantOf: this.variantOf}); 190 | } else { 191 | app().send(this.cluster, {noRecent: this.noRecent, variantOf: this.variantOf}); 192 | } 193 | } 194 | 195 | actAlternate(config?: AppConfig) { 196 | if (this.alt) { 197 | app().setBoard(Board.clusterAlternates(this.cluster, this.variants!, {noRecent: this.noRecent})); 198 | } else if (this.lu) { 199 | app().send(this.variants![1], {noRecent: this.noRecent}); 200 | } else { 201 | app().send(this.cluster, {noRecent: this.noRecent}); // no variant change 202 | } 203 | } 204 | 205 | Contents = ({code}: { code: SC }) => { 206 | const config = useContext(ConfigContext); 207 | const cluster = this.alt ? config.preferredVariant[this.cluster] ?? this.cluster : this.cluster; 208 | const symbol = this.alt ? cluster : this.symbol; 209 | let status = this.alt ? clusterName(config.preferredVariant[this.cluster] ?? this.cluster) : this.name; 210 | if (config.showAliases) { 211 | const aliases = clusterAliases(this.cluster); 212 | if (aliases.length) status += ` (${aliases.join(', ')})`; 213 | } 214 | if (config.showCharCodes) { 215 | status += ` [${toCodePoints(cluster).map(toHex).join(' ')}]` 216 | } 217 | return
{ 220 | e.preventDefault(); 221 | e.shiftKey || this.clickAlwaysAlternate ? this.actAlternate(config) : this.act(config); 222 | }} 223 | onContextMenu={(e) => { 224 | e.preventDefault(); 225 | this.actAlternate(config); 226 | }} 227 | onMouseOver={() => app().updateStatus(status)} 228 | data-keycode={code} 229 | > 230 |
{this.keyNamePrefix}
231 |
{this.upperName}
232 | 233 |
; 234 | } 235 | } 236 | 237 | export class RecentKey extends Key { 238 | constructor() { 239 | super({name: 'Recent', symbol: '↺'}); 240 | } 241 | 242 | act() { 243 | app().setMode(AppMode.RECENTS); 244 | } 245 | } 246 | 247 | export class ExitRecentKey extends Key { 248 | constructor() { 249 | super({name: 'Back', symbol: '←', keyType: "back"}); 250 | } 251 | 252 | act() { 253 | app().setMode(AppMode.MAIN); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /wwwassets/script/keys/base.tsx: -------------------------------------------------------------------------------- 1 | import {SC} from "../layout/sc"; 2 | import {useContext} from "preact/hooks"; 3 | import {app, LayoutContext} from "../appVar"; 4 | import {Fragment, h} from "preact"; 5 | import {cl} from "../helpers"; 6 | import {Symbol} from "./symbol"; 7 | import {KeyCap} from "../config/boards"; 8 | 9 | export function KeyName({code}: { code: SC }) { 10 | const layout = useContext(LayoutContext); 11 | return {layout.sys[code]?.name ?? ''}; 12 | } 13 | 14 | export type KeyType = "action" | "empty" | "char" | "back"; 15 | 16 | export class Key { 17 | public readonly name: string; 18 | /** Name used in the title bar */ 19 | protected readonly statusName: string; 20 | public readonly symbol: KeyCap; 21 | public readonly active: boolean; 22 | public readonly blank: boolean; 23 | public readonly keyType: KeyType; 24 | protected readonly clickAlwaysAlternate: boolean; 25 | protected readonly keyNamePrefix: string; 26 | 27 | 28 | constructor( 29 | p: { 30 | name: string, 31 | statusName?: string, 32 | symbol: KeyCap, 33 | active?: boolean, 34 | clickAlwaysAlternate?: boolean, 35 | keyNamePrefix?: string, 36 | blank?: boolean, 37 | keyType?: KeyType 38 | } 39 | ) { 40 | this.name = p.name; 41 | this.statusName = p.statusName ?? p.name; 42 | this.symbol = p.symbol; 43 | this.active = p.active ?? false; 44 | this.clickAlwaysAlternate = p.clickAlwaysAlternate ?? false; 45 | this.keyNamePrefix = p.keyNamePrefix ?? ''; 46 | this.blank = p.blank ?? false; 47 | this.keyType = !p.name.length ? "empty" : p.keyType ?? "action"; 48 | } 49 | 50 | Contents = ({code}: { code: SC }) => { 51 | return
{ 54 | e.preventDefault(); 55 | e.shiftKey || this.clickAlwaysAlternate ? this.actAlternate() : this.act(); 56 | }} 57 | onContextMenu={(e) => { 58 | e.preventDefault(); 59 | this.actAlternate(); 60 | }} 61 | onMouseOver={() => app().updateStatus(this.statusName)} 62 | data-keycode={code} 63 | > 64 |
{this.keyNamePrefix}
65 |
{this.name}
66 | 67 |
; 68 | } 69 | 70 | act(): void { 71 | // Default: do nothing 72 | } 73 | 74 | actAlternate(): void { 75 | this.act(); 76 | } 77 | } 78 | 79 | export const BlankKey = new Key({name: '', symbol: '', blank: true, keyType: "empty"}); 80 | -------------------------------------------------------------------------------- /wwwassets/script/keys/symbol.tsx: -------------------------------------------------------------------------------- 1 | import {useContext, useMemo} from "preact/hooks"; 2 | import {OSContext, PluginsContext} from "../appVar"; 3 | import {charInfo, symbolRequirements} from "../unicodeInterface"; 4 | import {toCodePoints} from "../builder/builder"; 5 | import {GeneralCategory} from "../builder/unicode"; 6 | import {VarSel15} from "../chars"; 7 | import {cl} from "../helpers"; 8 | import {KeyCap, SpriteRef} from "../config/boards"; 9 | import {Fragment, h} from "preact"; 10 | import {Version} from "../osversion"; 11 | import {supportsRequirements} from "../utils/emojiSupportTest"; 12 | 13 | function meetsRequirements(symbol: string, os: Version) { 14 | const req = symbolRequirements(symbol); 15 | return supportsRequirements(req, os); 16 | } 17 | 18 | export function Symbol({symbol}: { symbol: KeyCap }) { 19 | const os = useContext(OSContext); 20 | return useMemo(() => { 21 | if (typeof symbol === 'string') { 22 | if ([...symbol].length == 1) { 23 | const info = charInfo(toCodePoints(symbol)[0]); 24 | if (info?.ca === GeneralCategory.Space_Separator || info?.ca === GeneralCategory.Format || info?.ca === GeneralCategory.Control) { 25 | return
{info.n}
26 | } else if (info?.ca === GeneralCategory.Nonspacing_Mark) { 27 | symbol = '◌' + symbol; 28 | } 29 | } 30 | // here we consider that symbol = 1 grapheme cluster 31 | // note that the browser doesn't apply the text-style selector by itself since the chars are in different fonts 32 | // also, we may need a fallback for a sequence so font-family fallback won't work either 33 | const fallbackFont = !meetsRequirements(symbol, os); 34 | const textStyle = symbol.endsWith(VarSel15); 35 | return
36 | {symbol} 37 |
38 | } else { 39 | return
40 | } 41 | }, [symbol, os]); 42 | } 43 | 44 | export function Sprite({symbol}: { symbol: SpriteRef }) { 45 | const plugins = useContext(PluginsContext); 46 | for (const plugin of plugins) { 47 | const spriteMap = plugin.data.spriteMaps?.[symbol.spriteMap]; 48 | if (spriteMap && spriteMap.index[symbol.sprite]) { 49 | const scale = (spriteMap.width + 2 * (spriteMap.padding ?? 0)) / spriteMap.width; 50 | return
56 | } 57 | } 58 | return ?; 59 | } 60 | -------------------------------------------------------------------------------- /wwwassets/script/layout.ts: -------------------------------------------------------------------------------- 1 | import {VK} from "./layout/vk"; 2 | import {ExtraSC, SC} from "./layout/sc"; 3 | import {fromEntries} from "./helpers"; 4 | 5 | export type SystemLayout = { 6 | readonly [id in SC]: { 7 | vk: VK, 8 | name: string 9 | } 10 | }; 11 | 12 | export const SystemLayoutUS: SystemLayout = { 13 | [SC.None]: {vk: 0, name: ""}, 14 | [SC.Esc]: {vk: 27, name: "Esc"}, 15 | [SC.Digit1]: {vk: 49, name: "1"}, 16 | [SC.Digit2]: {vk: 50, name: "2"}, 17 | [SC.Digit3]: {vk: 51, name: "3"}, 18 | [SC.Digit4]: {vk: 52, name: "4"}, 19 | [SC.Digit5]: {vk: 53, name: "5"}, 20 | [SC.Digit6]: {vk: 54, name: "6"}, 21 | [SC.Digit7]: {vk: 55, name: "7"}, 22 | [SC.Digit8]: {vk: 56, name: "8"}, 23 | [SC.Digit9]: {vk: 57, name: "9"}, 24 | [SC.Digit0]: {vk: 48, name: "0"}, 25 | [SC.Minus]: {vk: 189, name: "-"}, 26 | [SC.Equal]: {vk: 187, name: "="}, 27 | [SC.Backspace]: {vk: 8, name: "Backspace"}, 28 | [SC.Tab]: {vk: 9, name: "Tab"}, 29 | [SC.Q]: {vk: 81, name: "q"}, 30 | [SC.W]: {vk: 87, name: "w"}, 31 | [SC.E]: {vk: 69, name: "e"}, 32 | [SC.R]: {vk: 82, name: "r"}, 33 | [SC.T]: {vk: 84, name: "t"}, 34 | [SC.Y]: {vk: 89, name: "y"}, 35 | [SC.U]: {vk: 85, name: "u"}, 36 | [SC.I]: {vk: 73, name: "i"}, 37 | [SC.O]: {vk: 79, name: "o"}, 38 | [SC.P]: {vk: 80, name: "p"}, 39 | [SC.LeftBrace]: {vk: 219, name: "["}, 40 | [SC.RightBrace]: {vk: 221, name: "]"}, 41 | [SC.Enter]: {vk: 13, name: "Enter"}, 42 | [SC.Ctrl]: {vk: 162, name: "Ctrl"}, 43 | [SC.A]: {vk: 65, name: "a"}, 44 | [SC.S]: {vk: 83, name: "s"}, 45 | [SC.D]: {vk: 68, name: "d"}, 46 | [SC.F]: {vk: 70, name: "f"}, 47 | [SC.G]: {vk: 71, name: "g"}, 48 | [SC.H]: {vk: 72, name: "h"}, 49 | [SC.J]: {vk: 74, name: "j"}, 50 | [SC.K]: {vk: 75, name: "k"}, 51 | [SC.L]: {vk: 76, name: "l"}, 52 | [SC.Semicolon]: {vk: 186, name: ";"}, 53 | [SC.Apostrophe]: {vk: 222, name: "'"}, 54 | [SC.Backtick]: {vk: 192, name: "`"}, 55 | [SC.Shift]: {vk: 160, name: "Shift"}, 56 | [SC.Backslash]: {vk: 220, name: "\\"}, 57 | [SC.Z]: {vk: 90, name: "z"}, 58 | [SC.X]: {vk: 88, name: "x"}, 59 | [SC.C]: {vk: 67, name: "c"}, 60 | [SC.V]: {vk: 86, name: "v"}, 61 | [SC.B]: {vk: 66, name: "b"}, 62 | [SC.N]: {vk: 78, name: "n"}, 63 | [SC.M]: {vk: 77, name: "m"}, 64 | [SC.Comma]: {vk: 188, name: ","}, 65 | [SC.Period]: {vk: 190, name: "."}, 66 | [SC.Slash]: {vk: 191, name: "/"}, 67 | [SC.RightShift]: {vk: 16, name: "Right Shift"}, 68 | [SC.NumMult]: {vk: 106, name: "Num *"}, 69 | [SC.Alt]: {vk: 164, name: "Alt"}, 70 | [SC.Space]: {vk: 32, name: "Space"}, 71 | [SC.CapsLock]: {vk: 20, name: "Caps Lock"}, 72 | [SC.F1]: {vk: 112, name: "F1"}, 73 | [SC.F2]: {vk: 113, name: "F2"}, 74 | [SC.F3]: {vk: 114, name: "F3"}, 75 | [SC.F4]: {vk: 115, name: "F4"}, 76 | [SC.F5]: {vk: 116, name: "F5"}, 77 | [SC.F6]: {vk: 117, name: "F6"}, 78 | [SC.F7]: {vk: 118, name: "F7"}, 79 | [SC.F8]: {vk: 119, name: "F8"}, 80 | [SC.F9]: {vk: 120, name: "F9"}, 81 | [SC.F10]: {vk: 121, name: "F10"}, 82 | [SC.Pause]: {vk: 19, name: "Pause"}, 83 | [SC.ScrollLock]: {vk: 145, name: "Scroll Lock"}, 84 | [SC.Num7]: {vk: 103, name: "Num 7"}, 85 | [SC.Num8]: {vk: 104, name: "Num 8"}, 86 | [SC.Num9]: {vk: 105, name: "Num 9"}, 87 | [SC.NumSub]: {vk: 109, name: "Num -"}, 88 | [SC.Num4]: {vk: 100, name: "Num 4"}, 89 | [SC.Num5]: {vk: 101, name: "Num 5"}, 90 | [SC.Num6]: {vk: 102, name: "Num 6"}, 91 | [SC.NumAdd]: {vk: 107, name: "Num +"}, 92 | [SC.Num1]: {vk: 97, name: "Num 1"}, 93 | [SC.Num2]: {vk: 98, name: "Num 2"}, 94 | [SC.Num3]: {vk: 99, name: "Num 3"}, 95 | [SC.Num0]: {vk: 96, name: "Num 0"}, 96 | [SC.NumDecimal]: {vk: 110, name: "Num Del"}, 97 | [SC.SysReq]: {vk: 44, name: "Sys Req"}, 98 | [SC.LessThan]: {vk: 226, name: "\\"}, 99 | [SC.F11]: {vk: 122, name: "F11"}, 100 | [SC.F12]: {vk: 123, name: "F12"}, 101 | [SC.RightCtrl]: {vk: 163, name: "Right Ctrl"}, 102 | [SC.NumDiv]: {vk: 111, name: "Num /"}, 103 | [SC.PrintScreen]: {vk: 44, name: "Prnt Scrn"}, 104 | [SC.RightAlt]: {vk: 165, name: "Right Alt"}, 105 | [SC.NumLock]: {vk: 144, name: "Num Lock"}, 106 | [SC.Win]: {vk: 91, name: "Win"}, 107 | ...fromEntries(ExtraSC.map(sc => [sc, {vk: VK.None, name: ''}] as const)), 108 | } 109 | 110 | export type KeyCodesList = readonly SC[]; 111 | export const DigitsRow = [ 112 | SC.Digit1, 113 | SC.Digit2, 114 | SC.Digit3, 115 | SC.Digit4, 116 | SC.Digit5, 117 | SC.Digit6, 118 | SC.Digit7, 119 | SC.Digit8, 120 | SC.Digit9, 121 | SC.Digit0, 122 | SC.Minus, 123 | SC.Equal, 124 | ] as const; 125 | export const FirstRow = [ 126 | SC.Q, 127 | SC.W, 128 | SC.E, 129 | SC.R, 130 | SC.T, 131 | SC.Y, 132 | SC.U, 133 | SC.I, 134 | SC.O, 135 | SC.P, 136 | SC.LeftBrace, 137 | SC.RightBrace, 138 | ] as const; 139 | export const SecondRow = [ 140 | SC.A, 141 | SC.S, 142 | SC.D, 143 | SC.F, 144 | SC.G, 145 | SC.H, 146 | SC.J, 147 | SC.K, 148 | SC.L, 149 | SC.Semicolon, 150 | SC.Apostrophe, 151 | SC.Backslash, 152 | ] as const; 153 | export const ThirdRow = [ 154 | SC.Z, 155 | SC.X, 156 | SC.C, 157 | SC.V, 158 | SC.B, 159 | SC.N, 160 | SC.M, 161 | SC.Comma, 162 | SC.Period, 163 | SC.Slash, 164 | ] as const; 165 | export const SearchKeyCodesTable: KeyCodesList = [ 166 | SC.Backtick, ...DigitsRow, 167 | SC.CapsLock, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 168 | SC.Shift, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024 169 | ]; 170 | export type BaseLayout = { 171 | all: KeyCodesList; 172 | free: KeyCodesList; 173 | freeRows: KeyCodesList[]; 174 | cssClass: string; 175 | } 176 | export type Layout = BaseLayout & { 177 | sys: SystemLayout; 178 | } 179 | export const AnsiLayout: BaseLayout = { 180 | all: [ 181 | SC.Backtick, ...DigitsRow, //, 59, 60, 61 182 | SC.Tab, ...FirstRow, //, 62, 63, 64 183 | SC.CapsLock, ...SecondRow, //, 65, 66, 67 184 | SC.Shift, ...ThirdRow, SC.Extra00, SC.Extra01 //, 68, 87, 88 185 | ], 186 | free: [...DigitsRow, ...FirstRow, ...SecondRow, ...ThirdRow], 187 | freeRows: [DigitsRow, FirstRow, SecondRow, ThirdRow], 188 | cssClass: 'ansi-layout', 189 | }; 190 | export const IsoLayout: BaseLayout = { 191 | all: [ 192 | SC.Backtick, ...DigitsRow, //, 59, 60, 61 193 | SC.Tab, ...FirstRow, //, 62, 63, 64 194 | SC.CapsLock, ...SecondRow, //, 65, 66, 67 195 | SC.Shift, SC.LessThan, ...ThirdRow, SC.Extra00 //, 68, 87, 88 196 | ], 197 | free: [...DigitsRow, ...FirstRow, ...SecondRow, SC.LessThan, ...ThirdRow], 198 | freeRows: [DigitsRow, FirstRow, SecondRow, [SC.LessThan, ...ThirdRow]], 199 | cssClass: 'iso-layout', 200 | }; 201 | export const SearchKeyCodes: number[] = [ 202 | ...DigitsRow, 203 | 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012, 204 | 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024 205 | ]; 206 | -------------------------------------------------------------------------------- /wwwassets/script/layout/sc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Scancodes: 3 | * Key names are based on a US-like layout (note that the bottom-left key has been labeled LessThan) 4 | * ``` 5 | * ` 1 2 3 4 5 6 7 8 9 0 - + 6 | * Q W E R T Y U I O P [ ] \ 7 | * A S D F G H J K L ; ' 8 | * < Z X C V B N M , . / 9 | * ``` 10 | */ 11 | export const enum SC { 12 | None = 0, 13 | Esc = 1, 14 | F1 = 59, 15 | F2 = 60, 16 | F3 = 61, 17 | F4 = 62, 18 | F5 = 63, 19 | F6 = 64, 20 | F7 = 65, 21 | F8 = 66, 22 | F9 = 67, 23 | F10 = 68, 24 | F11 = 87, 25 | F12 = 88, 26 | 27 | Backspace = 14, 28 | Tab = 15, 29 | CapsLock = 58, 30 | Enter = 28, 31 | Shift = 42, 32 | RightShift = 54, 33 | Ctrl = 29, 34 | RightCtrl = 285, 35 | Win = 91, 36 | Alt = 56, 37 | RightAlt = 312, 38 | /** Swiss layout: § */ 39 | Backtick = 41, 40 | Digit1 = 2, 41 | Digit2 = 3, 42 | Digit3 = 4, 43 | Digit4 = 5, 44 | Digit5 = 6, 45 | Digit6 = 7, 46 | Digit7 = 8, 47 | Digit8 = 9, 48 | Digit9 = 10, 49 | Digit0 = 11, 50 | /** Swiss layout: ' */ 51 | Minus = 12, 52 | /** Swiss layout: ^ */ 53 | Equal = 13, 54 | Q = 16, 55 | W = 17, 56 | E = 18, 57 | R = 19, 58 | T = 20, 59 | /** Swiss layout: Z */ 60 | Y = 21, 61 | U = 22, 62 | I = 23, 63 | O = 24, 64 | P = 25, 65 | /** Swiss layout: ÈÜ */ 66 | LeftBrace = 26, 67 | /** Swiss layout: ¨ */ 68 | RightBrace = 27, 69 | /** Swiss layout: $ */ 70 | Backslash = 43, 71 | A = 30, 72 | S = 31, 73 | D = 32, 74 | F = 33, 75 | G = 34, 76 | H = 35, 77 | J = 36, 78 | K = 37, 79 | L = 38, 80 | /** Swiss layout: ÉÖ */ 81 | Semicolon = 39, 82 | /** Swiss layout: ÀÄ */ 83 | Apostrophe = 40, 84 | /** Swiss layout: <, does not exist on US layout */ 85 | LessThan = 86, 86 | /** Swiss layout: Y */ 87 | Z = 44, 88 | X = 45, 89 | C = 46, 90 | V = 47, 91 | B = 48, 92 | N = 49, 93 | M = 50, 94 | /** Swiss layout: , */ 95 | Comma = 51, 96 | /** Swiss layout: . */ 97 | Period = 52, 98 | /** Swiss layout: - */ 99 | Slash = 53, 100 | 101 | Space = 57, 102 | 103 | Pause = 69, 104 | ScrollLock = 70, 105 | SysReq = 84, 106 | PrintScreen = 311, 107 | 108 | NumLock = 325, 109 | NumMult = 55, 110 | NumSub = 74, 111 | NumAdd = 78, 112 | NumDiv = 309, 113 | Num0 = 82, 114 | NumDecimal = 83, 115 | Num1 = 79, 116 | Num2 = 80, 117 | Num3 = 81, 118 | Num4 = 75, 119 | Num5 = 76, 120 | Num6 = 77, 121 | Num7 = 71, 122 | Num8 = 72, 123 | Num9 = 73, 124 | 125 | Extra00 = 1000, 126 | Extra01 = 1001, 127 | Extra02 = 1002, 128 | Extra03 = 1003, 129 | Extra04 = 1004, 130 | Extra05 = 1005, 131 | Extra06 = 1006, 132 | Extra07 = 1007, 133 | Extra08 = 1008, 134 | Extra09 = 1009, 135 | Extra10 = 1010, 136 | Extra11 = 1011, 137 | Extra12 = 1012, 138 | Extra13 = 1013, 139 | Extra14 = 1014, 140 | Extra15 = 1015, 141 | Extra16 = 1016, 142 | Extra17 = 1017, 143 | Extra18 = 1018, 144 | Extra19 = 1019, 145 | Extra20 = 1020, 146 | Extra21 = 1021, 147 | Extra22 = 1022, 148 | Extra23 = 1023, 149 | Extra24 = 1024, 150 | Extra25 = 1025, 151 | Extra26 = 1026, 152 | Extra27 = 1027, 153 | Extra28 = 1028, 154 | Extra29 = 1029, 155 | Extra30 = 1030, 156 | Extra31 = 1031, 157 | Extra32 = 1032, 158 | Extra33 = 1033, 159 | Extra34 = 1034, 160 | Extra35 = 1035, 161 | Extra36 = 1036, 162 | Extra37 = 1037, 163 | Extra38 = 1038, 164 | Extra39 = 1039, 165 | Extra40 = 1040, 166 | Extra41 = 1041, 167 | Extra42 = 1042, 168 | Extra43 = 1043, 169 | Extra44 = 1044, 170 | Extra45 = 1045, 171 | Extra46 = 1046, 172 | Extra47 = 1047, 173 | Extra48 = 1048, 174 | Extra49 = 1049, 175 | } 176 | 177 | export const ExtraSC = [ 178 | SC.Extra00, SC.Extra01, SC.Extra02, SC.Extra03, SC.Extra04, SC.Extra05, SC.Extra06, SC.Extra07, SC.Extra08, SC.Extra09, 179 | SC.Extra10, SC.Extra11, SC.Extra12, SC.Extra13, SC.Extra14, SC.Extra15, SC.Extra16, SC.Extra17, SC.Extra18, SC.Extra19, 180 | SC.Extra20, SC.Extra21, SC.Extra22, SC.Extra23, SC.Extra24, SC.Extra25, SC.Extra26, SC.Extra27, SC.Extra28, SC.Extra29, 181 | SC.Extra30, SC.Extra31, SC.Extra32, SC.Extra33, SC.Extra34, SC.Extra35, SC.Extra36, SC.Extra37, SC.Extra38, SC.Extra39, 182 | SC.Extra40, SC.Extra41, SC.Extra42, SC.Extra43, SC.Extra44, SC.Extra45, SC.Extra46, SC.Extra47, SC.Extra48, SC.Extra49, 183 | ] as const; 184 | -------------------------------------------------------------------------------- /wwwassets/script/layout/vk.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Windows Virtual Key codes 3 | * Note that e.g. wherever the letter A is placed on the keyboard, it'll have the same VK but may have a different SC 4 | * ``` 5 | * ` 1 2 3 4 5 6 7 8 9 0 - + 6 | * Q W E R T Y U I O P [ ] \ 7 | * A S D F G H J K L ; ' 8 | * < Z X C V B N M , . / 9 | * ``` 10 | */ 11 | export const enum VK { 12 | None = 0, 13 | 14 | Backspace = 8, 15 | Tab = 9, 16 | Enter = 13, 17 | RightShift = 16, 18 | Pause = 19, 19 | CapsLock = 20, 20 | Esc = 27, 21 | 22 | Space = 32, 23 | 24 | SysReq = 44, 25 | PrintScreen = 44, 26 | 27 | Digit0 = 48, 28 | Digit1 = 49, 29 | Digit2 = 50, 30 | Digit3 = 51, 31 | Digit4 = 52, 32 | Digit5 = 53, 33 | Digit6 = 54, 34 | Digit7 = 55, 35 | Digit8 = 56, 36 | Digit9 = 57, 37 | 38 | A = 65, 39 | B = 66, 40 | C = 67, 41 | D = 68, 42 | E = 69, 43 | F = 70, 44 | G = 71, 45 | H = 72, 46 | I = 73, 47 | J = 74, 48 | K = 75, 49 | L = 76, 50 | M = 77, 51 | N = 78, 52 | O = 79, 53 | P = 80, 54 | Q = 81, 55 | R = 82, 56 | S = 83, 57 | T = 84, 58 | U = 85, 59 | V = 86, 60 | W = 87, 61 | X = 88, 62 | Y = 89, 63 | Z = 90, 64 | 65 | Win = 91, 66 | 67 | Num0 = 96, 68 | Num1 = 97, 69 | Num2 = 98, 70 | Num3 = 99, 71 | Num4 = 100, 72 | Num5 = 101, 73 | Num6 = 102, 74 | Num7 = 103, 75 | Num8 = 104, 76 | Num9 = 105, 77 | NumMult = 106, 78 | NumAdd = 107, 79 | NumSub = 109, 80 | NumDecimal = 110, 81 | NumDiv = 111, 82 | 83 | F1 = 112, 84 | F2 = 113, 85 | F3 = 114, 86 | F4 = 115, 87 | F5 = 116, 88 | F6 = 117, 89 | F7 = 118, 90 | F8 = 119, 91 | F9 = 120, 92 | F10 = 121, 93 | F11 = 122, 94 | F12 = 123, 95 | 96 | NumLock = 144, 97 | ScrollLock = 145, 98 | 99 | Shift = 160, 100 | Ctrl = 162, 101 | RightCtrl = 163, 102 | Alt = 164, 103 | RightAlt = 165, 104 | 105 | Semicolon = 186, 106 | Equal = 187, 107 | Comma = 188, 108 | Minus = 189, 109 | Period = 190, 110 | Slash = 191, 111 | Backtick = 192, 112 | LeftBrace = 219, 113 | Backslash = 220, 114 | RightBrace = 221, 115 | Apostrophe = 222, 116 | LessThan = 226, 117 | } 118 | 119 | export const VKMap = { 120 | a: VK.A, 121 | b: VK.B, 122 | c: VK.C, 123 | d: VK.D, 124 | e: VK.E, 125 | f: VK.F, 126 | g: VK.G, 127 | h: VK.H, 128 | i: VK.I, 129 | j: VK.J, 130 | k: VK.K, 131 | l: VK.L, 132 | m: VK.M, 133 | n: VK.N, 134 | o: VK.O, 135 | p: VK.P, 136 | q: VK.Q, 137 | r: VK.R, 138 | s: VK.S, 139 | t: VK.T, 140 | u: VK.U, 141 | v: VK.V, 142 | w: VK.W, 143 | x: VK.X, 144 | y: VK.Y, 145 | z: VK.Z, 146 | 147 | d0: VK.Digit0, 148 | d1: VK.Digit1, 149 | d2: VK.Digit2, 150 | d3: VK.Digit3, 151 | d4: VK.Digit4, 152 | d5: VK.Digit5, 153 | d6: VK.Digit6, 154 | d7: VK.Digit7, 155 | d8: VK.Digit8, 156 | d9: VK.Digit9, 157 | 158 | "0": VK.Digit0, 159 | "1": VK.Digit1, 160 | "2": VK.Digit2, 161 | "3": VK.Digit3, 162 | "4": VK.Digit4, 163 | "5": VK.Digit5, 164 | "6": VK.Digit6, 165 | "7": VK.Digit7, 166 | "8": VK.Digit8, 167 | "9": VK.Digit9, 168 | 169 | ",": VK.Comma, 170 | "-": VK.Minus, 171 | ".": VK.Period, 172 | } as const; 173 | const ReverseVkMap = new Map(); 174 | for (const [k, vk] of Object.entries(VKMap)) { 175 | if (!ReverseVkMap.has(vk)) ReverseVkMap.set(vk, []); 176 | ReverseVkMap.get(vk)!.push(k as VKAbbr); 177 | } 178 | 179 | export function vkLookup(vk: VK): (VK | VKAbbr)[] { 180 | return [vk, ...(ReverseVkMap.get(vk) ?? [])]; 181 | } 182 | 183 | export type VKAbbr = keyof typeof VKMap; 184 | -------------------------------------------------------------------------------- /wwwassets/script/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*?raw" 2 | { 3 | const content: string; 4 | export default content; 5 | } 6 | -------------------------------------------------------------------------------- /wwwassets/script/osversion.ts: -------------------------------------------------------------------------------- 1 | export class Version { 2 | private readonly version: number[]; 3 | private readonly known = new Set(); 4 | private readonly knownLesser = new Set(); 5 | private readonly knownGreater = new Set(); 6 | private readonly knownSame = new Set(); 7 | 8 | public constructor(version: string) { 9 | this.version = Version.parse(version); 10 | } 11 | 12 | private static parse(v: string | number): number[] { 13 | if (typeof v === "number") { 14 | return [Math.floor(v), Math.floor((v % 1) * 10)]; 15 | } else { 16 | if (!/^[.0-9]+$/.test(v.trim())) { 17 | console.error("Invalid version", v); 18 | return [0]; 19 | } 20 | 21 | return v.split(/\./).map(part => parseInt(part, 10)); 22 | } 23 | } 24 | 25 | private static compare(a: number[], b: number[]): number { 26 | for (let i = 0; i < a.length; i++) { 27 | if (i >= b.length) return a.slice(i).some((p) => p > 0) ? 1 : 0; 28 | 29 | if (a[i] > b[i]) return 1; 30 | if (a[i] < b[i]) return -1; 31 | } 32 | if (a.length == b.length) return 0; 33 | return b.slice(a.length).some((p) => p > 0) ? -1 : 0; 34 | } 35 | 36 | private add(other: string | number) { 37 | const cmp = Version.compare(this.version, Version.parse(other)); 38 | this.known.add(other); 39 | 40 | if (cmp > 0) this.knownLesser.add(other); 41 | else if (cmp < 0) this.knownGreater.add(other); 42 | else this.knownSame.add(other); 43 | } 44 | 45 | /** < operator */ 46 | public lt(other: string | number): boolean { 47 | if (!this.known.has(other)) this.add(other); 48 | return this.knownGreater.has(other); 49 | } 50 | 51 | /** > operator */ 52 | public gt(other: string | number): boolean { 53 | if (!this.known.has(other)) this.add(other); 54 | return this.knownLesser.has(other); 55 | } 56 | 57 | /** ≥ operator */ 58 | public gte(other: string | number): boolean { 59 | return !this.lt(other); 60 | } 61 | 62 | /** ≤ operator */ 63 | public lte(other: string | number): boolean { 64 | return !this.gt(other); 65 | } 66 | 67 | /** == operator */ 68 | public eq(other: string | number): boolean { 69 | if (!this.known.has(other)) this.add(other); 70 | return this.knownSame.has(other); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /wwwassets/script/recentsActions.tsx: -------------------------------------------------------------------------------- 1 | import {RecentEmoji} from "./config"; 2 | import {app} from "./appVar"; 3 | 4 | export const FAVORITE_SCORE = 100; 5 | export const SCORE_INCR = 11; 6 | export const SCORE_DECR = 1; 7 | export const MAX_RECENT_ITEMS = 47; 8 | 9 | function sortRecent(arr: RecentEmoji[]): void { 10 | arr.sort((a, b) => b.useCount - a.useCount); 11 | } 12 | 13 | export function increaseRecent(cluster: string) { 14 | app().updateConfig(c => { 15 | const current = c.recent.find(r => r.symbol == cluster); 16 | let recent: RecentEmoji[] = [ 17 | {symbol: cluster, useCount: Math.min((current?.useCount ?? 0) + SCORE_INCR, 100)}, 18 | ...c.recent.filter(r => r.symbol != cluster).map(r => ({ 19 | symbol: r.symbol, 20 | useCount: r.useCount < FAVORITE_SCORE ? Math.max(r.useCount - SCORE_DECR, 0) : FAVORITE_SCORE, 21 | })) 22 | ]; 23 | sortRecent(recent); 24 | if (recent.length > MAX_RECENT_ITEMS) recent = recent.slice(0, MAX_RECENT_ITEMS); 25 | return {recent}; 26 | }, false); 27 | } 28 | 29 | export function removeRecent(cluster: string) { 30 | app().updateConfig(c => { 31 | return {recent: c.recent.filter(r => r.symbol != cluster)} 32 | }); 33 | } 34 | 35 | export function toggleFavorite(cluster: string) { 36 | app().updateConfig(c => { 37 | const r = c.recent.find(r => r.symbol == cluster); 38 | if (!r) return {recent: [{symbol: cluster, useCount: FAVORITE_SCORE}, ...c.recent]} 39 | else { 40 | const rest = c.recent.filter(r => r.symbol != cluster); 41 | if (r.useCount < FAVORITE_SCORE) { 42 | return {recent: [{symbol: cluster, useCount: FAVORITE_SCORE}, ...rest]} 43 | } else { 44 | return { 45 | recent: [{ 46 | symbol: cluster, 47 | useCount: FAVORITE_SCORE / 2 48 | }, ...rest].sort((a, b) => b.useCount - a.useCount) 49 | } 50 | } 51 | } 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /wwwassets/script/recentsView.tsx: -------------------------------------------------------------------------------- 1 | import {useContext, useEffect, useMemo} from "preact/hooks"; 2 | import {app, ConfigContext, LayoutContext} from "./appVar"; 3 | import {SC} from "./layout/sc"; 4 | import {BackKey, ConfigActionKey, ConfigLabelKey, ConfigToggleKey, ExitRecentKey, SearchKey} from "./key"; 5 | import {Keys, mapKeysToSlots, SlottedKeys} from "./boards/utils"; 6 | import {h} from "preact"; 7 | import {Board} from "./board"; 8 | import {Key} from "./keys/base"; 9 | import {clusterName} from "./unicodeInterface"; 10 | import {DigitsRow} from "./layout"; 11 | import {FAVORITE_SCORE, removeRecent, SCORE_DECR, SCORE_INCR, toggleFavorite} from "./recentsActions"; 12 | 13 | export class RecentBoard extends Board { 14 | constructor() { 15 | super({name: "Recent", symbol: "⟲"}); 16 | } 17 | 18 | Contents = () => { 19 | useEffect(() => app().updateStatus(), []); 20 | const l = useContext(LayoutContext); 21 | const recentEmojis = useContext(ConfigContext).recent; 22 | const keys = useMemo(() => ({ 23 | [SC.Backtick]: new ExitRecentKey(), 24 | [SC.Tab]: new SearchKey(), 25 | [SC.CapsLock]: new ExitRecentKey(), 26 | ...mapKeysToSlots(l.free, recentEmojis.map(r => new RecentClusterKey(r.symbol))) 27 | }), [l, recentEmojis]); 28 | return ; 29 | } 30 | } 31 | 32 | function useUseCount(cluster: string) { 33 | const recent = useContext(ConfigContext).recent; 34 | return useMemo(() => recent.find(r => r.symbol == cluster)?.useCount ?? 0, [cluster, recent]); 35 | } 36 | 37 | export class RecentClusterKey extends Key { 38 | constructor(private cluster: string) { 39 | const useCount = useUseCount(cluster); 40 | const name = clusterName(cluster); 41 | super({ 42 | name: `${name}, score: ${useCount >= 100 ? "★" : useCount}`, 43 | symbol: cluster, 44 | keyType: "char", 45 | }); 46 | } 47 | 48 | act() { 49 | app().send(this.cluster, {noRecent: true}); 50 | } 51 | 52 | actAlternate() { 53 | app().setBoard(new RecentSettingsBoard(this.cluster)); 54 | } 55 | } 56 | 57 | export class RecentSettingsBoard extends Board { 58 | constructor(private cluster: string) { 59 | super({ 60 | name: `Settings for ${cluster}`, 61 | symbol: cluster, 62 | }) 63 | } 64 | 65 | Contents = () => { 66 | useEffect(() => app().updateStatus(), []); 67 | const useCount = useUseCount(this.cluster); 68 | const keys: SlottedKeys = { 69 | [SC.Backtick]: new BackKey(), 70 | ...mapKeysToSlots(DigitsRow, [ 71 | new ConfigToggleKey({ 72 | name: "Favorite", 73 | symbol: "⭐", 74 | active: useCount >= FAVORITE_SCORE, 75 | action: () => toggleFavorite(this.cluster) 76 | }), 77 | new ConfigActionKey({ 78 | name: "Remove", symbol: "🗑️", action: () => { 79 | removeRecent(this.cluster); 80 | app().back(); 81 | } 82 | }) 83 | ]), 84 | [SC.Q]: new ConfigLabelKey(`Score: ${useCount}, +${SCORE_INCR} when used, -${SCORE_DECR} when others used`), 85 | [SC.A]: new ConfigLabelKey(`Favorite when score ≥ ${FAVORITE_SCORE}`), 86 | } 87 | return 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /wwwassets/script/searchView.tsx: -------------------------------------------------------------------------------- 1 | import {h} from "preact"; 2 | import {SearchKeyCodes, SearchKeyCodesTable} from "./layout"; 3 | import {Board} from "./board"; 4 | import {search} from "./emojis"; 5 | import {useCallback, useContext, useEffect, useMemo} from "preact/hooks"; 6 | import {ClusterKey, ExitSearchKey, RecentKey} from "./key"; 7 | import {app, SearchContext} from "./appVar"; 8 | import {SC} from "./layout/sc"; 9 | import {mapKeysToSlots} from "./boards/utils"; 10 | import {BlankKey} from "./keys/base"; 11 | 12 | export class SearchBoard extends Board { 13 | constructor() { 14 | super({name: "Search", symbol: "🔎"}); 15 | } 16 | 17 | Contents(): preact.VNode { 18 | const searchText = useContext(SearchContext); 19 | const onInput = useCallback((e: InputEvent) => app().setSearchText((e.target as HTMLInputElement).value), []); 20 | useEffect(() => (document.querySelector('input[type="search"]') as HTMLInputElement)?.focus(), []); 21 | const keys = useMemo(() => ({ 22 | [SC.Backtick]: new ExitSearchKey(), 23 | [SC.Tab]: new ExitSearchKey(), 24 | [SC.CapsLock]: new RecentKey(), 25 | ...mapKeysToSlots(SearchKeyCodes, search(searchText).slice(0, SearchKeyCodes.length).map((c) => new ClusterKey(c))) 26 | }), [searchText]); 27 | app().keyHandlers = keys; 28 | return
29 | 30 | {SearchKeyCodesTable.map((code) => { 31 | const K = (keys[code] ?? BlankKey); 32 | return ; 33 | })} 34 |
35 | } 36 | } 37 | -------------------------------------------------------------------------------- /wwwassets/script/unicodeInterface.ts: -------------------------------------------------------------------------------- 1 | import {ExtendedCharInformation, getUnicodeData} from "./builder/consolidated"; 2 | import {toCodePoints} from "./builder/builder"; 3 | import {IconFallback, IconRequirement} from "./config/fallback"; 4 | import {UnicodeEmojiGroup} from "./unidata"; 5 | 6 | export const IgnoreForName = [ 7 | 8205, // Zero Width Joiner, 8 | 65039, // Variation Selector-16 9 | ] 10 | const u = getUnicodeData(); 11 | export const UnicodeData = u; 12 | 13 | function charName(code: number): string { 14 | if (u.chars[code]) return u.chars[code]!.n; 15 | for (const b of u.blocks) { 16 | if (code >= b.start && code <= b.end) { 17 | return `${b.name}: U+${code.toString(16)}`; 18 | } 19 | } 20 | return `U+${code.toString(16)}`; 21 | } 22 | 23 | function clusterFullName(cluster: string): string { 24 | const c = toCodePoints(cluster); 25 | if (c.length === 1) return charName(c[0]!); 26 | return c 27 | .filter(c => !IgnoreForName.includes(c)) 28 | .map(c => charName(c)) 29 | .join(', '); 30 | } 31 | 32 | export function charInfo(code: number): ExtendedCharInformation | undefined { 33 | return u.chars[code]; 34 | } 35 | 36 | function charAliases(code: number): string[] { 37 | const info = charInfo(code); 38 | return [...info?.falias ?? [], ...info?.alias ?? []]; 39 | } 40 | 41 | export function clusterName(cluster: string): string { 42 | return u.clusters[cluster]?.name ?? clusterFullName(cluster); 43 | } 44 | 45 | export function clusterVariants(cluster: string): string[] | undefined { 46 | return u.clusters[cluster]?.variants; 47 | } 48 | 49 | export function clusterAliases(cluster: string): string[] { 50 | const cp = toCodePoints(cluster); 51 | if (cp.length === 1) return charAliases(cp[0]!); 52 | return u.clusters[cluster]?.alias ?? []; 53 | } 54 | 55 | export function emojiGroup(g: UnicodeEmojiGroup): string[] { 56 | return u.groups[g.group]?.sub[g.subGroup]?.clusters ?? []; 57 | } 58 | 59 | export function symbolRequirements(cluster: string): IconRequirement { 60 | const info = u.clusters[cluster]; 61 | for (const f of IconFallback) { 62 | if (info && f.version && info.version && f.version.lte(info.version)) return f.requirement; 63 | if (f.clusters && f.clusters.has(cluster)) return f.requirement; 64 | if (f.ranges) for (const r of f.ranges) { 65 | if (cluster >= r.from && cluster <= r.to) return f.requirement; 66 | } 67 | } 68 | return {type: "windows", windows: "0"}; 69 | } 70 | -------------------------------------------------------------------------------- /wwwassets/script/unidata.d.ts: -------------------------------------------------------------------------------- 1 | // This file is generated by the builder script. Do not edit. 2 | // To rebuild, click Settings > Tools > Build in the app or run 'npm run build-unidata' in the wwwassets folder 3 | export type UnicodeEmojiGroup = 4 | {group: "Smileys & Emotion", subGroup: "face-smiling"} 5 | | {group: "Smileys & Emotion", subGroup: "face-affection"} 6 | | {group: "Smileys & Emotion", subGroup: "face-tongue"} 7 | | {group: "Smileys & Emotion", subGroup: "face-hand"} 8 | | {group: "Smileys & Emotion", subGroup: "face-neutral-skeptical"} 9 | | {group: "Smileys & Emotion", subGroup: "face-sleepy"} 10 | | {group: "Smileys & Emotion", subGroup: "face-unwell"} 11 | | {group: "Smileys & Emotion", subGroup: "face-hat"} 12 | | {group: "Smileys & Emotion", subGroup: "face-glasses"} 13 | | {group: "Smileys & Emotion", subGroup: "face-concerned"} 14 | | {group: "Smileys & Emotion", subGroup: "face-negative"} 15 | | {group: "Smileys & Emotion", subGroup: "face-costume"} 16 | | {group: "Smileys & Emotion", subGroup: "cat-face"} 17 | | {group: "Smileys & Emotion", subGroup: "monkey-face"} 18 | | {group: "Smileys & Emotion", subGroup: "heart"} 19 | | {group: "Smileys & Emotion", subGroup: "emotion"} 20 | | {group: "People & Body", subGroup: "hand-fingers-open"} 21 | | {group: "People & Body", subGroup: "hand-fingers-partial"} 22 | | {group: "People & Body", subGroup: "hand-single-finger"} 23 | | {group: "People & Body", subGroup: "hand-fingers-closed"} 24 | | {group: "People & Body", subGroup: "hands"} 25 | | {group: "People & Body", subGroup: "hand-prop"} 26 | | {group: "People & Body", subGroup: "body-parts"} 27 | | {group: "People & Body", subGroup: "person"} 28 | | {group: "People & Body", subGroup: "person-gesture"} 29 | | {group: "People & Body", subGroup: "person-role"} 30 | | {group: "People & Body", subGroup: "person-fantasy"} 31 | | {group: "People & Body", subGroup: "person-activity"} 32 | | {group: "People & Body", subGroup: "person-sport"} 33 | | {group: "People & Body", subGroup: "person-resting"} 34 | | {group: "People & Body", subGroup: "family"} 35 | | {group: "People & Body", subGroup: "person-symbol"} 36 | | {group: "Component", subGroup: "skin-tone"} 37 | | {group: "Component", subGroup: "hair-style"} 38 | | {group: "Animals & Nature", subGroup: "animal-mammal"} 39 | | {group: "Animals & Nature", subGroup: "animal-bird"} 40 | | {group: "Animals & Nature", subGroup: "animal-amphibian"} 41 | | {group: "Animals & Nature", subGroup: "animal-reptile"} 42 | | {group: "Animals & Nature", subGroup: "animal-marine"} 43 | | {group: "Animals & Nature", subGroup: "animal-bug"} 44 | | {group: "Animals & Nature", subGroup: "plant-flower"} 45 | | {group: "Animals & Nature", subGroup: "plant-other"} 46 | | {group: "Food & Drink", subGroup: "food-fruit"} 47 | | {group: "Food & Drink", subGroup: "food-vegetable"} 48 | | {group: "Food & Drink", subGroup: "food-prepared"} 49 | | {group: "Food & Drink", subGroup: "food-asian"} 50 | | {group: "Food & Drink", subGroup: "food-sweet"} 51 | | {group: "Food & Drink", subGroup: "drink"} 52 | | {group: "Food & Drink", subGroup: "dishware"} 53 | | {group: "Travel & Places", subGroup: "place-map"} 54 | | {group: "Travel & Places", subGroup: "place-geographic"} 55 | | {group: "Travel & Places", subGroup: "place-building"} 56 | | {group: "Travel & Places", subGroup: "place-religious"} 57 | | {group: "Travel & Places", subGroup: "place-other"} 58 | | {group: "Travel & Places", subGroup: "transport-ground"} 59 | | {group: "Travel & Places", subGroup: "transport-water"} 60 | | {group: "Travel & Places", subGroup: "transport-air"} 61 | | {group: "Travel & Places", subGroup: "hotel"} 62 | | {group: "Travel & Places", subGroup: "time"} 63 | | {group: "Travel & Places", subGroup: "sky & weather"} 64 | | {group: "Activities", subGroup: "event"} 65 | | {group: "Activities", subGroup: "award-medal"} 66 | | {group: "Activities", subGroup: "sport"} 67 | | {group: "Activities", subGroup: "game"} 68 | | {group: "Activities", subGroup: "arts & crafts"} 69 | | {group: "Objects", subGroup: "clothing"} 70 | | {group: "Objects", subGroup: "sound"} 71 | | {group: "Objects", subGroup: "music"} 72 | | {group: "Objects", subGroup: "musical-instrument"} 73 | | {group: "Objects", subGroup: "phone"} 74 | | {group: "Objects", subGroup: "computer"} 75 | | {group: "Objects", subGroup: "light & video"} 76 | | {group: "Objects", subGroup: "book-paper"} 77 | | {group: "Objects", subGroup: "money"} 78 | | {group: "Objects", subGroup: "mail"} 79 | | {group: "Objects", subGroup: "writing"} 80 | | {group: "Objects", subGroup: "office"} 81 | | {group: "Objects", subGroup: "lock"} 82 | | {group: "Objects", subGroup: "tool"} 83 | | {group: "Objects", subGroup: "science"} 84 | | {group: "Objects", subGroup: "medical"} 85 | | {group: "Objects", subGroup: "household"} 86 | | {group: "Objects", subGroup: "other-object"} 87 | | {group: "Symbols", subGroup: "transport-sign"} 88 | | {group: "Symbols", subGroup: "warning"} 89 | | {group: "Symbols", subGroup: "arrow"} 90 | | {group: "Symbols", subGroup: "religion"} 91 | | {group: "Symbols", subGroup: "zodiac"} 92 | | {group: "Symbols", subGroup: "av-symbol"} 93 | | {group: "Symbols", subGroup: "gender"} 94 | | {group: "Symbols", subGroup: "math"} 95 | | {group: "Symbols", subGroup: "punctuation"} 96 | | {group: "Symbols", subGroup: "currency"} 97 | | {group: "Symbols", subGroup: "other-symbol"} 98 | | {group: "Symbols", subGroup: "keycap"} 99 | | {group: "Symbols", subGroup: "alphanum"} 100 | | {group: "Symbols", subGroup: "geometric"} 101 | | {group: "Flags", subGroup: "flag"} 102 | | {group: "Flags", subGroup: "country-flag"} 103 | | {group: "Flags", subGroup: "subdivision-flag"}; 104 | -------------------------------------------------------------------------------- /wwwassets/script/utils/compare.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Compare two strings in a natural way, where numbers are compared as numbers. 3 | * 4 | * @example 5 | * // returns ['1', '2', '10'] 6 | * ['1', '10', '2'].sort(naturalCompare) 7 | */ 8 | export function naturalCompare(a: string|null|undefined, b: string|null|undefined): number { 9 | // any null, empty or undefined comes first (!!'' == false) 10 | if (!a || !b) return +!!a - +!!b; 11 | const aParts = a.match(/[0-9]+|[^0-9]+/g)!; 12 | const bParts = b.match(/[0-9]+|[^0-9]+/g)!; 13 | for (const i of aParts.keys()) { 14 | if (bParts.length <= i) return 1; 15 | const aText = !/^[0-9]/.test(aParts[i]); 16 | const bText = !/^[0-9]/.test(bParts[i]); 17 | if (aText !== bText) return +aText - +bText; 18 | if (aText) { 19 | const cmp = aParts[i].localeCompare(bParts[i]); 20 | if (cmp !== 0) return cmp; 21 | } else { 22 | const cmp = parseInt(aParts[i], 10) - parseInt(bParts[i], 10); 23 | if (cmp !== 0) return cmp; 24 | } 25 | } 26 | return -1; 27 | } -------------------------------------------------------------------------------- /wwwassets/script/utils/emojiSupportTest.ts: -------------------------------------------------------------------------------- 1 | import {ZeroWidthJoiner} from "../chars"; 2 | import {IconRequirement} from "../config/fallback"; 3 | import {unreachable} from "../helpers"; 4 | import {Version} from "../osversion"; 5 | 6 | let hiddenBox: HTMLDivElement | null = null; 7 | if (typeof window !== "undefined") { 8 | hiddenBox = document.createElement('div'); 9 | document.body.appendChild(hiddenBox); 10 | hiddenBox.style.position = 'absolute'; 11 | hiddenBox.style.left = '-1000px'; 12 | hiddenBox.style.top = '-1000px'; 13 | document.fonts.load("16px AdobeBlank"); 14 | } 15 | 16 | const knownZWJ = new Map(); 17 | const knownSymbol = new Map(); 18 | 19 | 20 | export function supportsRequirements(req: IconRequirement, os: Version): boolean { 21 | if (req.type === "windows") return os.gte(req.windows); 22 | else if (req.type === "zwjEmoji") return supportsZWJ(req.zwjEmoji); 23 | else if (req.type === "emoji") return supportsSymbol(req.emoji); 24 | else return unreachable(req); 25 | } 26 | 27 | /** Check if the ZWJ sequence is supported by the base font, i.e. it's shorter than character taken separately */ 28 | function supportsZWJ(sequence: string): boolean { 29 | if (!knownZWJ.has(sequence)) { 30 | const el = document.createElement('span'); 31 | hiddenBox!.appendChild(el); 32 | el.textContent = sequence; 33 | const len1 = el.offsetWidth; 34 | el.textContent = sequence.replace(ZeroWidthJoiner, ''); 35 | const len2 = el.offsetWidth; 36 | hiddenBox!.removeChild(el); 37 | const supported = len1 > 0 && len1 < len2; 38 | knownZWJ.set(sequence, supported); 39 | } 40 | return knownZWJ.get(sequence)!; 41 | } 42 | 43 | /** Check if the symbol is supported by the base font, i.e. a glyph is displayed using the base font when setting Adobe Blank as fallback */ 44 | function supportsSymbol(symbol: string): boolean { 45 | if (!knownSymbol.has(symbol)) { 46 | const el = document.createElement('span'); 47 | el.style.fontFamily = '"Segoe UI Emoji", "Segoe UI", AdobeBlank'; 48 | hiddenBox!.appendChild(el); 49 | el.textContent = 'a' + symbol; 50 | const len1 = el.offsetWidth; 51 | el.textContent = 'a'; 52 | const len2 = el.offsetWidth; 53 | hiddenBox!.removeChild(el); 54 | const supported = len1 > 0 && len1 > len2; 55 | knownSymbol.set(symbol, supported); 56 | } 57 | return knownSymbol.get(symbol)!; 58 | } 59 | 60 | -------------------------------------------------------------------------------- /wwwassets/style/legacy.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --c-background: #FFF; 3 | --c-key: #f8f8f8; 4 | --c-text: #000; 5 | --c-empty: #f8f8f8; 6 | --c-empty-text: #888; 7 | --c-label: #f8f8f8; 8 | --c-primary: #FFC83D; 9 | --border-key: solid .1vw #888; 10 | --c-shadow: rgba(255, 255, 255, .5); 11 | 12 | --dark-c-shadow: #000; 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | .system-color-scheme { 17 | --c-background: #1a1a1a; 18 | --c-key: #333; 19 | --c-text: #fff; 20 | --c-empty: #252525; 21 | --c-empty-text: #fff; 22 | --c-label: #252525; 23 | --c-primary: #FFC83D; 24 | --border-key: none; 25 | 26 | --c-shadow: var(--dark-c-shadow); 27 | } 28 | } 29 | 30 | .dark-color-scheme { 31 | --c-background: #1a1a1a; 32 | --c-key: #333; 33 | --c-text: #fff; 34 | --c-empty: #252525; 35 | --c-empty-text: #fff; 36 | --c-label: #252525; 37 | --c-primary: #FFC83D; 38 | --border-key: none; 39 | 40 | --c-shadow: var(--dark-c-shadow); 41 | } 42 | 43 | .keyboard { 44 | background-color: var(--c-background); 45 | } 46 | 47 | .key{ 48 | background-color: var(--c-key); 49 | border: var(--border-key); 50 | color: var(--c-text); 51 | } 52 | 53 | .key .symbol { 54 | color: var(--c-text); 55 | } 56 | 57 | .key.empty{ 58 | background-color: var(--c-empty); 59 | color: var(--c-empty-text); 60 | } 61 | .key.label{ 62 | background-color: var(--c-empty); 63 | color: var(--c-text); 64 | } 65 | 66 | .key.alt{ 67 | box-shadow: 0 .5vw var(--c-primary) inset; 68 | } 69 | 70 | .key.active{ 71 | box-shadow: 0 .5vw var(--c-text) inset; 72 | } 73 | 74 | .key.char .symbol img{ 75 | box-shadow: 0 0 .2em var(--c-text); 76 | background: var(--c-background); 77 | } 78 | 79 | .searchpane input{ 80 | border: solid .1vw #888; 81 | } 82 | -------------------------------------------------------------------------------- /wwwassets/style/material.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --c-background: #EEE; 3 | --c-primary: #FFC83D; 4 | --c-key: #FFF; 5 | --c-text: #000; 6 | --c-text2: #888; 7 | --c-text-active: #000; 8 | --c-empty: #BBB; 9 | --c-shadow: rgba(255, 255, 255, .5); 10 | 11 | --dark-c-background: #222; 12 | --dark-c-key: #333; 13 | --dark-c-text: #EEE; 14 | --dark-c-text2: #888; 15 | --dark-c-text-active: #000; 16 | --dark-c-empty: #888; 17 | --dark-c-shadow: #000; 18 | } 19 | 20 | @media (prefers-color-scheme: dark) { 21 | .system-color-scheme { 22 | --c-background: var(--dark-c-background); 23 | --c-key: var(--dark-c-key); 24 | --c-text: var(--dark-c-text); 25 | --c-text2: var(--dark-c-text2); 26 | --c-text-active: var(--dark-c-text-active); 27 | --c-empty: var(--dark-c-empty); 28 | --c-shadow: var(--dark-c-shadow); 29 | } 30 | } 31 | 32 | .dark-color-scheme { 33 | --c-background: var(--dark-c-background); 34 | --c-key: var(--dark-c-key); 35 | --c-text: var(--dark-c-text); 36 | --c-text2: var(--dark-c-text2); 37 | --c-text-active: var(--dark-c-text-active); 38 | --c-empty: var(--dark-c-empty); 39 | --c-shadow: var(--dark-c-shadow); 40 | } 41 | 42 | .keyboard { 43 | background-color: var(--c-background); 44 | } 45 | 46 | .key { 47 | background-color: var(--c-key); 48 | border-radius: .5vw; 49 | color: var(--c-text); 50 | } 51 | 52 | .row { 53 | /*! padding: .2vw 0; */ 54 | } 55 | 56 | .key .symbol { 57 | color: var(--c-text); 58 | } 59 | 60 | .key.empty { 61 | background-color: inherit; 62 | color: var(--c-empty); 63 | } 64 | 65 | .key.alt { 66 | background: linear-gradient(to top, var(--c-primary) .5vw, var(--c-key) .5vw); 67 | } 68 | 69 | .key.active { 70 | background: var(--c-primary); 71 | color: var(--c-text-active); 72 | } 73 | 74 | .key .keyname, .key .name { 75 | color: var(--c-text2); 76 | } 77 | 78 | .key.active .keyname, .key.active .name { 79 | color: var(--c-text-active); 80 | } 81 | 82 | .key:not(.empty):not(.label):hover { 83 | color: var(--c-text); 84 | box-shadow: 0 .2vw .4vw #444; 85 | } 86 | 87 | .searchpane input { 88 | border: solid .1vw #888; 89 | } 90 | -------------------------------------------------------------------------------- /wwwassets/style/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --s-gap: 4px; 3 | --c-shadow: #000; 4 | --f-fallback: 'Cambria Math', Tahoma, Geneva, Verdana, sans-serif; 5 | } 6 | 7 | body, html { 8 | margin: 0; 9 | padding: 0; 10 | font-family: 'Segoe UI', 'Segoe UI Emoji', var(--f-fallback); 11 | font-size: 1.8vw; 12 | } 13 | 14 | .fallbackFont { 15 | font-family: NotoEmoji, 'Segoe UI', 'Segoe UI Emoji', var(--f-fallback); 16 | } 17 | 18 | .textStyle { 19 | font-family: 'Segoe UI', 'Segoe UI Symbol', var(--f-fallback); 20 | } 21 | 22 | * { 23 | user-select: none; 24 | -ms-user-select: none; 25 | } 26 | 27 | input { 28 | font-family: 'Segoe UI', 'Segoe UI Emoji', 'Cambria Math', Tahoma, Geneva, Verdana, sans-serif; 29 | } 30 | 31 | @font-face { 32 | font-family: NotoEmoji; 33 | src: url("../fonts/NotoColorEmoji-Regular.ttf"); 34 | } 35 | 36 | @font-face { 37 | /* 38 | * Adobe Blank is a special font that maps all unicode codepoints to non-spacing non-marking glyphs. 39 | * The can be used to determine whether a character is supported by the previous fonts in a font stack. 40 | * See https://github.com/adobe-fonts/adobe-blank. 41 | */ 42 | font-family: AdobeBlank; 43 | src: url("../fonts/AdobeBlank.otf"); 44 | } 45 | 46 | main{ 47 | display: flex; 48 | flex-direction: column; 49 | cursor: default; 50 | } 51 | 52 | main > section{ 53 | flex: 1; 54 | overflow: auto; 55 | } 56 | 57 | .keyboard{ 58 | display: grid; 59 | box-sizing: border-box; 60 | grid-template-columns: repeat(13, 1fr); 61 | grid-template-rows: repeat(4, 1fr); 62 | position: absolute; 63 | top: 0; 64 | left: 0; 65 | height: 100vh; 66 | width: 100vw; 67 | grid-gap: var(--s-gap); 68 | padding: var(--s-gap); 69 | } 70 | 71 | .keyboard input[type="search"] { 72 | grid-column: 1 / 14; 73 | font-size: 4vw; 74 | } 75 | 76 | .key{ 77 | display: flex; 78 | list-style: none; 79 | padding: .3vw; 80 | overflow: hidden; 81 | position: relative; 82 | align-items: center; 83 | transition: background ease .05s; 84 | } 85 | 86 | .row{ 87 | display: inline-flex; 88 | } 89 | 90 | .key .symbol{ 91 | font-size: 3.6vw; 92 | text-align: center; 93 | flex: 1; 94 | transition: ease .1s; 95 | white-space: pre; 96 | text-shadow: var(--c-shadow) 0 0 .1em, var(--c-shadow) 0 0 .1em, var(--c-shadow) 0 0 .1em, var(--c-shadow) 0 0 .1em; 97 | } 98 | .key.char .symbol.s-space{ 99 | font-size: 1.2vw; 100 | font-variant: small-caps; 101 | font-weight: bold; 102 | max-width: 100%; 103 | transform: none; 104 | hyphens: auto; 105 | word-wrap: break-word; 106 | white-space: normal; 107 | } 108 | .key .symbol img{ 109 | height: 3.6vw; 110 | } 111 | 112 | .key .keyname{ 113 | position: absolute; 114 | top: .2vw; 115 | left: .2vw; 116 | font-variant: small-caps; 117 | line-height: 1em; 118 | transition: ease .05s; 119 | } 120 | 121 | .key .name{ 122 | font-size: 1.4vw; 123 | line-height: 1.2vw; 124 | text-align: right; 125 | position: absolute; 126 | bottom: .2vw; 127 | right: .2vw; 128 | left: .2vw; 129 | font-variant: small-caps; 130 | transition: ease .05s; 131 | } 132 | 133 | .key.char .name{ 134 | display: none; 135 | } 136 | 137 | .key.char .symbol{ 138 | transform: scale(1.2); 139 | } 140 | 141 | .key:hover .name{ 142 | opacity: .8; 143 | } 144 | 145 | .key:not(.label):hover .symbol{ 146 | transform: translateY(-4px) scale(1.8) rotate(0deg); 147 | } 148 | 149 | .key:not(.label):active .symbol{ 150 | transform: translateY(-2px) scale(1.4) rotate(7deg); 151 | } 152 | 153 | .key.char .keypress { 154 | transform: scale(1) rotate(7deg); 155 | } 156 | 157 | .key:not(.lu) .uname{ 158 | display: none; 159 | } 160 | 161 | .key .uname{ 162 | position: absolute; 163 | bottom: .2vw; 164 | right: .2vw; 165 | line-height: 1em; 166 | transition: ease .05s; 167 | } 168 | 169 | .key.label { 170 | background-color: inherit; 171 | overflow: visible; 172 | min-width: 0; 173 | z-index: 10; 174 | } 175 | 176 | .searchpane{ 177 | padding: .2vh 2.05vw; 178 | height: 25vh; 179 | box-sizing: border-box; 180 | } 181 | 182 | .searchpane input { 183 | display: block; 184 | height: 100%; 185 | border: none; 186 | width: 100%; 187 | box-sizing: border-box; 188 | padding: 1vw; 189 | } 190 | 191 | div.sprite { 192 | margin: 0 auto; 193 | height: 3.6vw; 194 | width: 3.6vw; 195 | position: relative; 196 | top: .3vw; 197 | } 198 | -------------------------------------------------------------------------------- /wwwassets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "noImplicitAny": true, 5 | "strict": true, 6 | "removeComments": true, 7 | "preserveConstEnums": true, 8 | "module": "AMD", 9 | "baseUrl": "lib", 10 | "paths": { 11 | "preact": [ 12 | "preact/index" 13 | ] 14 | }, 15 | "target": "es2022", 16 | "jsx": "react", 17 | "jsxFactory": "h", 18 | "lib": [ 19 | "dom", 20 | "esnext" 21 | ], 22 | "typeRoots": [ 23 | "lib" 24 | ], 25 | "moduleResolution": "node" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /wwwassets/vite.config.js: -------------------------------------------------------------------------------- 1 | import {dirname, resolve} from 'node:path' 2 | import {fileURLToPath} from 'node:url' 3 | import {defineConfig} from 'vite' 4 | 5 | const __dirname = dirname(fileURLToPath(import.meta.url)) 6 | 7 | export default defineConfig({ 8 | build: { 9 | lib: { 10 | entry: resolve(__dirname, 'script/entryPoint.ts'), 11 | name: 'Emoji-Keyboard', 12 | // the proper extensions will be added 13 | fileName: 'emoji-keyboard', 14 | formats: ['es'], 15 | }, 16 | rollupOptions: { 17 | // make sure to externalize deps that shouldn't be bundled 18 | // into your library 19 | external: ['vue'], 20 | output: { 21 | // Provide global variables to use in the UMD build 22 | // for externalized deps 23 | globals: { 24 | vue: 'Vue', 25 | }, 26 | }, 27 | }, 28 | }, 29 | }) 30 | --------------------------------------------------------------------------------