├── .gitattributes ├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── @types └── jsx.d.ts ├── CODE_OF_CONDUCT.md ├── LICENSE ├── package-lock.json ├── package.json ├── readme.md ├── src ├── component.ts ├── debug.ts ├── element.ts ├── index.ts ├── reconciler.ts ├── ts_transformers │ ├── RequirePreload.ts │ └── WowJsxTransformer.ts └── wow-utils.ts └── tsconfig.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | dist 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.insertSpaces": true, 4 | "editor.detectIndentation": false, 5 | "editor.formatOnSave": true, 6 | "editor.codeActionsOnSave": { 7 | "source.organizeImports": true 8 | }, 9 | "files.trimTrailingWhitespace": true, 10 | "files.trimFinalNewlines": true, 11 | "files.insertFinalNewline": true, 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "build", 7 | "problemMatcher": [ 8 | "$tsc" 9 | ], 10 | "group": { 11 | "isDefault": true, 12 | "kind": "build" 13 | }, 14 | "label": "build", 15 | "detail": "tsc", 16 | "presentation": { 17 | "echo": true, 18 | "reveal": "never", 19 | "focus": false, 20 | "panel": "shared", 21 | "clear": true 22 | } 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /@types/jsx.d.ts: -------------------------------------------------------------------------------- 1 | /** @noSelfInFile */ 2 | 3 | declare namespace JSX { 4 | interface PointDefinition { 5 | point: WoWAPI.Point; 6 | relativePoint?: WoWAPI.Point; 7 | relativeFrame?: WoWAPI.Region | string; 8 | x?: number; 9 | y?: number; 10 | } 11 | 12 | type Point = PointDefinition | WoWAPI.Point; 13 | 14 | type Color4 = [number, number, number, number]; 15 | type Tuple = [T, T]; 16 | type Size = Tuple; 17 | type Font = [string, number]; 18 | 19 | interface BaseProps { 20 | key?: string; 21 | children?: any; 22 | } 23 | 24 | interface BaseFrameProps extends BaseProps { 25 | name?: string; 26 | inheritsFrom?: string; 27 | Width?: number; 28 | Height?: number; 29 | Size?: Size; 30 | Points?: Point[]; 31 | Point?: Point; 32 | Backdrop?: WoWAPI.Backdrop; 33 | BackdropBorderColor?: Color4; 34 | BackdropColor?: Color4; 35 | 36 | OnUpdate?: (this: void, frame: WoWAPI.Frame, secondsElapsed: number) => void; 37 | 38 | Clickable?: WoWAPI.MouseButton[]; 39 | OnClick?: (this: void, frame: WoWAPI.Frame, button: WoWAPI.MouseButton, down: boolean) => void; 40 | 41 | Draggable?: WoWAPI.MouseButton[]; 42 | Movable?: boolean; 43 | OnDragStart?: (this: void, frame: WoWAPI.Frame, button: WoWAPI.MouseButton, down: boolean) => void; 44 | OnDragStop?: (this: void, frame: WoWAPI.Frame, button: WoWAPI.MouseButton, down: boolean) => void; 45 | } 46 | 47 | interface LayeredRegionProps extends BaseFrameProps { 48 | VertexColor?: Color4; 49 | DrawLayer?: WoWAPI.Layer | [WoWAPI.Layer, number]; 50 | } 51 | 52 | interface StatusBarProps extends BaseFrameProps { 53 | MinMaxValues?: Tuple; 54 | Value?: number; 55 | StatusBarTexture?: string; 56 | StatusBarColor?: Color4; 57 | } 58 | 59 | interface TextureProps extends LayeredRegionProps { 60 | Texture?: WoWAPI.TexturePath; 61 | } 62 | 63 | interface FontInstanceProps extends LayeredRegionProps { 64 | Font?: Font; 65 | } 66 | 67 | interface FontStringProps extends FontInstanceProps { 68 | Text?: string; 69 | JustifyH?: WoWAPI.HorizontalAlign; 70 | JustifyV?: WoWAPI.VerticalAlign; 71 | TextColor?: Color4; 72 | } 73 | 74 | interface IntrinsicElements { 75 | button: BaseFrameProps; 76 | 'color-select': BaseFrameProps; 77 | cooldown: BaseFrameProps; 78 | 'edit-box': BaseFrameProps; 79 | frame: BaseFrameProps; 80 | 'game-tooltip': BaseFrameProps; 81 | 'message-frame': BaseFrameProps; 82 | minimap: BaseFrameProps; 83 | model: BaseFrameProps; 84 | 'scroll-frame': BaseFrameProps; 85 | 'scrolling-message-frame': BaseFrameProps; 86 | 'simple-html': BaseFrameProps; 87 | slider: StatusBarProps; 88 | 'status-bar': StatusBarProps; 89 | 90 | // Other things 91 | 'font-string': FontStringProps; 92 | texture: TextureProps; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by filing an issue. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Brusalk, Tim Stirrat 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. 22 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@brusalk/react-wow-addon", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/code-frame": { 8 | "version": "7.10.4", 9 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", 10 | "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", 11 | "dev": true, 12 | "requires": { 13 | "@babel/highlight": "^7.10.4" 14 | } 15 | }, 16 | "@babel/helper-validator-identifier": { 17 | "version": "7.10.4", 18 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", 19 | "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", 20 | "dev": true 21 | }, 22 | "@babel/highlight": { 23 | "version": "7.10.4", 24 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", 25 | "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", 26 | "dev": true, 27 | "requires": { 28 | "@babel/helper-validator-identifier": "^7.10.4", 29 | "chalk": "^2.0.0", 30 | "js-tokens": "^4.0.0" 31 | } 32 | }, 33 | "@wartoshika/wow-declarations": { 34 | "version": "8.3.0-release.1", 35 | "resolved": "https://registry.npmjs.org/@wartoshika/wow-declarations/-/wow-declarations-8.3.0-release.1.tgz", 36 | "integrity": "sha512-0N61nkrWaJLJMIgrMgHhZPAEU9Z7ZJfMfOWOYiXWa+NXyZkPAdNbFGydgFF5kEfyx8sKGQl2yYEAcOgHDrtnXw==" 37 | }, 38 | "ansi-styles": { 39 | "version": "3.2.1", 40 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 41 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 42 | "dev": true, 43 | "requires": { 44 | "color-convert": "^1.9.0" 45 | } 46 | }, 47 | "arg": { 48 | "version": "4.1.3", 49 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 50 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" 51 | }, 52 | "argparse": { 53 | "version": "1.0.10", 54 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 55 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 56 | "dev": true, 57 | "requires": { 58 | "sprintf-js": "~1.0.2" 59 | } 60 | }, 61 | "balanced-match": { 62 | "version": "1.0.0", 63 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 64 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 65 | "dev": true 66 | }, 67 | "brace-expansion": { 68 | "version": "1.1.11", 69 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 70 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 71 | "dev": true, 72 | "requires": { 73 | "balanced-match": "^1.0.0", 74 | "concat-map": "0.0.1" 75 | } 76 | }, 77 | "buffer-from": { 78 | "version": "1.1.1", 79 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 80 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 81 | }, 82 | "builtin-modules": { 83 | "version": "1.1.1", 84 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", 85 | "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", 86 | "dev": true 87 | }, 88 | "chalk": { 89 | "version": "2.4.2", 90 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 91 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 92 | "dev": true, 93 | "requires": { 94 | "ansi-styles": "^3.2.1", 95 | "escape-string-regexp": "^1.0.5", 96 | "supports-color": "^5.3.0" 97 | } 98 | }, 99 | "color-convert": { 100 | "version": "1.9.3", 101 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 102 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 103 | "dev": true, 104 | "requires": { 105 | "color-name": "1.1.3" 106 | } 107 | }, 108 | "color-name": { 109 | "version": "1.1.3", 110 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 111 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 112 | "dev": true 113 | }, 114 | "commander": { 115 | "version": "2.20.3", 116 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 117 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", 118 | "dev": true 119 | }, 120 | "concat-map": { 121 | "version": "0.0.1", 122 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 123 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 124 | "dev": true 125 | }, 126 | "diff": { 127 | "version": "4.0.2", 128 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 129 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" 130 | }, 131 | "escape-string-regexp": { 132 | "version": "1.0.5", 133 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 134 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 135 | "dev": true 136 | }, 137 | "esprima": { 138 | "version": "4.0.1", 139 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 140 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 141 | "dev": true 142 | }, 143 | "fs.realpath": { 144 | "version": "1.0.0", 145 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 146 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 147 | "dev": true 148 | }, 149 | "glob": { 150 | "version": "7.1.6", 151 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 152 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 153 | "dev": true, 154 | "requires": { 155 | "fs.realpath": "^1.0.0", 156 | "inflight": "^1.0.4", 157 | "inherits": "2", 158 | "minimatch": "^3.0.4", 159 | "once": "^1.3.0", 160 | "path-is-absolute": "^1.0.0" 161 | } 162 | }, 163 | "has-flag": { 164 | "version": "3.0.0", 165 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 166 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 167 | "dev": true 168 | }, 169 | "inflight": { 170 | "version": "1.0.6", 171 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 172 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 173 | "dev": true, 174 | "requires": { 175 | "once": "^1.3.0", 176 | "wrappy": "1" 177 | } 178 | }, 179 | "inherits": { 180 | "version": "2.0.4", 181 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 182 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 183 | "dev": true 184 | }, 185 | "js-tokens": { 186 | "version": "4.0.0", 187 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 188 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 189 | "dev": true 190 | }, 191 | "js-yaml": { 192 | "version": "3.14.0", 193 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", 194 | "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", 195 | "dev": true, 196 | "requires": { 197 | "argparse": "^1.0.7", 198 | "esprima": "^4.0.0" 199 | } 200 | }, 201 | "lua-types": { 202 | "version": "2.8.0", 203 | "resolved": "https://registry.npmjs.org/lua-types/-/lua-types-2.8.0.tgz", 204 | "integrity": "sha512-FJY32giHIqD/XW1XGkJnl8XotXIJsJ2M42fj9A2UudttWA6orJioToW1OpgPdayTr+S1/oTO7i+hfBY3UVG8Fg==" 205 | }, 206 | "make-error": { 207 | "version": "1.3.6", 208 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 209 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" 210 | }, 211 | "minimatch": { 212 | "version": "3.0.4", 213 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 214 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 215 | "dev": true, 216 | "requires": { 217 | "brace-expansion": "^1.1.7" 218 | } 219 | }, 220 | "minimist": { 221 | "version": "1.2.5", 222 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 223 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", 224 | "dev": true 225 | }, 226 | "mkdirp": { 227 | "version": "0.5.5", 228 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 229 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 230 | "dev": true, 231 | "requires": { 232 | "minimist": "^1.2.5" 233 | } 234 | }, 235 | "once": { 236 | "version": "1.4.0", 237 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 238 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 239 | "dev": true, 240 | "requires": { 241 | "wrappy": "1" 242 | } 243 | }, 244 | "path-is-absolute": { 245 | "version": "1.0.1", 246 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 247 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 248 | "dev": true 249 | }, 250 | "path-parse": { 251 | "version": "1.0.6", 252 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 253 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" 254 | }, 255 | "resolve": { 256 | "version": "1.17.0", 257 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", 258 | "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", 259 | "requires": { 260 | "path-parse": "^1.0.6" 261 | } 262 | }, 263 | "semver": { 264 | "version": "5.7.1", 265 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 266 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 267 | "dev": true 268 | }, 269 | "source-map": { 270 | "version": "0.7.3", 271 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", 272 | "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" 273 | }, 274 | "source-map-support": { 275 | "version": "0.5.19", 276 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", 277 | "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", 278 | "requires": { 279 | "buffer-from": "^1.0.0", 280 | "source-map": "^0.6.0" 281 | }, 282 | "dependencies": { 283 | "source-map": { 284 | "version": "0.6.1", 285 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 286 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" 287 | } 288 | } 289 | }, 290 | "sprintf-js": { 291 | "version": "1.0.3", 292 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 293 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 294 | "dev": true 295 | }, 296 | "supports-color": { 297 | "version": "5.5.0", 298 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 299 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 300 | "dev": true, 301 | "requires": { 302 | "has-flag": "^3.0.0" 303 | } 304 | }, 305 | "ts-node": { 306 | "version": "9.0.0", 307 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-9.0.0.tgz", 308 | "integrity": "sha512-/TqB4SnererCDR/vb4S/QvSZvzQMJN8daAslg7MeaiHvD8rDZsSfXmNeNumyZZzMned72Xoq/isQljYSt8Ynfg==", 309 | "requires": { 310 | "arg": "^4.1.0", 311 | "diff": "^4.0.1", 312 | "make-error": "^1.1.1", 313 | "source-map-support": "^0.5.17", 314 | "yn": "3.1.1" 315 | } 316 | }, 317 | "tslib": { 318 | "version": "1.13.0", 319 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", 320 | "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", 321 | "dev": true 322 | }, 323 | "tslint": { 324 | "version": "5.20.1", 325 | "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", 326 | "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", 327 | "dev": true, 328 | "requires": { 329 | "@babel/code-frame": "^7.0.0", 330 | "builtin-modules": "^1.1.1", 331 | "chalk": "^2.3.0", 332 | "commander": "^2.12.1", 333 | "diff": "^4.0.1", 334 | "glob": "^7.1.1", 335 | "js-yaml": "^3.13.1", 336 | "minimatch": "^3.0.4", 337 | "mkdirp": "^0.5.1", 338 | "resolve": "^1.3.2", 339 | "semver": "^5.3.0", 340 | "tslib": "^1.8.0", 341 | "tsutils": "^2.29.0" 342 | } 343 | }, 344 | "tsutils": { 345 | "version": "2.29.0", 346 | "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", 347 | "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", 348 | "dev": true, 349 | "requires": { 350 | "tslib": "^1.8.1" 351 | } 352 | }, 353 | "typescript": { 354 | "version": "3.9.7", 355 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", 356 | "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==" 357 | }, 358 | "typescript-to-lua": { 359 | "version": "0.35.0", 360 | "resolved": "https://registry.npmjs.org/typescript-to-lua/-/typescript-to-lua-0.35.0.tgz", 361 | "integrity": "sha512-r79LfK1nWauFO4SR7q7pMCYjwW412Z17yx1WKp00SnVmYNwbm4fB/TsJuZe5jLtpZxAuGG8nVqtJEAsU5d1Bhg==", 362 | "requires": { 363 | "resolve": "^1.15.1", 364 | "source-map": "^0.7.3", 365 | "typescript": "^3.9.2" 366 | } 367 | }, 368 | "wrappy": { 369 | "version": "1.0.2", 370 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 371 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 372 | "dev": true 373 | }, 374 | "yn": { 375 | "version": "3.1.1", 376 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 377 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" 378 | } 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@brusalk/react-wow-addon", 3 | "version": "1.0.3", 4 | "description": "React-style UI Framework for World of Warcraft AddOns", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "prepublish": "npm run build", 10 | "test": "echo NYI" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "github:Brusalk/react-wow-addon" 15 | }, 16 | "keywords": [ 17 | "World of Warcraft", 18 | "WoW", 19 | "React", 20 | "DOM", 21 | "UI", 22 | "tstl", 23 | "typescript", 24 | "typescript-to-lua" 25 | ], 26 | "author": "Brusalk", 27 | "license": "MIT", 28 | "devDependencies": { 29 | "tslint": "^5.20.0" 30 | }, 31 | "dependencies": { 32 | "@wartoshika/wow-declarations": "^8.3.0-release.1", 33 | "lua-types": "^2.8.0", 34 | "ts-node": "^9.0.0", 35 | "typescript-to-lua": "^0.35.0", 36 | "typescript": "^3.6.3" 37 | }, 38 | "files": [ 39 | "dist/", 40 | "@types" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # React for WoW Addons 2 | 3 | This library provides a react-style rendering engine for the development of UI components for World of Warcraft. Using JSX, one can define custom components which render to World of Warcraft's UI. Gain all the advantages of React-style UI definition for your World of Warcraft addon, complete with full type hints, and differentiable rendering. 4 | 5 | This project is compiled down to Lua using typescript-to-lua [GitHub](https://github.com/TypeScriptToLua/TypeScriptToLua). 6 | 7 | This project is based off of work done by Tim Stirrat in [tsCooldown](https://github.com/tstirrat/tsCoolDown). 8 | 9 | # Usage 10 | 11 | TODO 12 | 13 | # Note 14 | This library is largely blocked on the merge of https://github.com/TypeScriptToLua/TypeScriptToLua/pull/909, which will support importing/requiring lua code from dependencies. Once that PR is merged, this package should be largely ready-to-use 15 | -------------------------------------------------------------------------------- /src/component.ts: -------------------------------------------------------------------------------- 1 | import { InternalElement } from './element'; 2 | import { Instance, reconcile } from './reconciler'; 3 | 4 | export class Component

{ 5 | public state: S = {} as any; 6 | constructor(public props: P = {} as any) { } 7 | 8 | private __internalInstance!: Instance; 9 | 10 | setState(partialState: Partial) { 11 | this.state = { ...this.state, ...partialState }; 12 | updateInstance(this.__internalInstance); 13 | } 14 | 15 | render(): InternalElement | null { 16 | throw 'render not implemented'; 17 | } 18 | } 19 | 20 | function updateInstance(internalInstance: Instance) { 21 | const parentDom = internalInstance.hostFrame.GetParent() as WoWAPI.Frame; 22 | const element = internalInstance.element; 23 | if (parentDom) { 24 | reconcile(parentDom, internalInstance, element); 25 | } else { 26 | throw 'Tried to reconcile instance with no dom.parentDom'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/debug.ts: -------------------------------------------------------------------------------- 1 | export function stringify(obj: Record | Function | string | boolean | number | null, depth = 1): string { 2 | if (typeof obj === 'string') { 3 | return `"${obj}"`; 4 | } 5 | if (typeof obj === 'function') { 6 | return ``; 7 | } 8 | if (typeof obj === 'boolean' || typeof obj === 'number') { 9 | return `${obj}`; 10 | } 11 | if (typeof obj === 'undefined' || !obj) { 12 | return 'nil'; 13 | } 14 | if (depth < 0) { 15 | return '{ ... }'; 16 | } 17 | const inner = Object.keys(obj) 18 | .map(key => `${key}: ${stringify(obj[key], depth - 1)}`) 19 | .join(', '); 20 | return `{ ${inner} }`; 21 | } 22 | -------------------------------------------------------------------------------- /src/element.ts: -------------------------------------------------------------------------------- 1 | /** @noSelfInFile */ 2 | 3 | import { Component } from '.'; 4 | 5 | export interface InternalElement { 6 | type: string | Component; 7 | props: Props; 8 | } 9 | 10 | export type RawChild = 11 | InternalElement | string | boolean | null; 12 | 13 | export type RenderableChildElement = InternalElement | string; 14 | 15 | interface Props { 16 | key?: string; 17 | children?: InternalElement[]; 18 | nodeValue?: string; 19 | [k: string]: any; 20 | } 21 | 22 | export const TEXT_ELEMENT = 'TEXT ELEMENT'; 23 | 24 | export function createElement( 25 | type: string | Component, config: Props, rawChildren?: RawChild[][]) { 26 | const props: Props = { ...config }; 27 | const flattenedChildren = rawChildren && rawChildren.length ? rawChildren.flat() : []; 28 | props.children = 29 | flattenedChildren 30 | .filter((c): c is RenderableChildElement => 31 | c != null && typeof c !== 'boolean' && 32 | // filters out empty objects which are left because Array.flat() is not correct 33 | (typeof c !== 'string' && !!c.type)) 34 | .map(c => typeof c === 'string' ? createTextElement(c) : c); 35 | 36 | // print('createElement', typeof type === 'string' ? type : 'Component', stringify(props, 1)); 37 | // print('createElement .children', typeof type === 'string' ? type : 'Component', stringify(props.children, 1)); 38 | return { type, props }; 39 | } 40 | 41 | function createTextElement(value: string): InternalElement { 42 | return createElement(TEXT_ELEMENT, { nodeValue: value }); 43 | } 44 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | export { Component } from './component'; 3 | export { createElement } from './element'; 4 | export { render } from './reconciler'; 5 | export { RequirePreload } from './ts_transformers/RequirePreload'; 6 | export { WowJsxTransformer } from './ts_transformers/WowJsxTransformer'; 7 | -------------------------------------------------------------------------------- /src/reconciler.ts: -------------------------------------------------------------------------------- 1 | import { Component } from './component'; 2 | import { InternalElement, TEXT_ELEMENT } from './element'; 3 | import { cleanupFrame, createFrame, updateFrameProperties } from './wow-utils'; 4 | 5 | export interface Instance { 6 | publicInstance?: Component; 7 | childInstance: Instance | null; 8 | childInstances: Array; 9 | hostFrame: WoWAPI.Region; 10 | element: InternalElement; 11 | } 12 | 13 | let rootInstance: Instance | null = null; 14 | 15 | export function render(element: InternalElement, container: WoWAPI.Region) { 16 | const prevInstance = rootInstance; 17 | const nextInstance = reconcile(container, prevInstance, element); 18 | rootInstance = nextInstance; 19 | } 20 | 21 | export function reconcile( 22 | parentFrame: WoWAPI.Region, instance: Instance | null, 23 | element: InternalElement | null): Instance | null { 24 | if (!instance) { 25 | // Create instance 26 | assert(element, 'element should not be null') 27 | return instantiate(element!, parentFrame); 28 | } else if (!element) { 29 | // Remove instance 30 | cleanupFrames(instance); 31 | return null; 32 | } else if (instance.element.type !== element.type) { 33 | // Replace instance 34 | const newInstance = instantiate(element, parentFrame); 35 | cleanupFrames(instance); 36 | return newInstance; 37 | } else if (typeof element.type === 'string') { 38 | // Update host element 39 | updateFrameProperties( 40 | instance.hostFrame, instance.element.props, element.props); 41 | instance.childInstances = reconcileChildren(instance, element); 42 | instance.element = element; 43 | return instance; 44 | } else if (instance.publicInstance) { 45 | // print('reconcile composite', (element.type as any).name, stringify(element.props)); 46 | // Update composite instance 47 | instance.publicInstance.props = element.props; 48 | const childElement = instance.publicInstance.render(); 49 | const oldChildInstance = instance.childInstance; 50 | const childInstance = 51 | reconcile(parentFrame, oldChildInstance, childElement); 52 | 53 | if (!childInstance) { 54 | throw 'Failed to update composite instance'; 55 | } 56 | 57 | instance.hostFrame = childInstance.hostFrame; 58 | instance.childInstance = childInstance; 59 | instance.element = element; 60 | return instance; 61 | } else { 62 | throw 'Reconciler catch all error'; 63 | } 64 | } 65 | 66 | function cleanupFrames(instance: Instance) { 67 | // TODO: composite objects need special cleanup, this should be part of reconcile 68 | if (instance.childInstances) { 69 | instance.childInstances.forEach(child => child && cleanupFrames(child)); 70 | } 71 | if (instance.childInstance) { 72 | cleanupFrames(instance.childInstance); 73 | } 74 | cleanupFrame(instance.hostFrame); 75 | } 76 | 77 | function reconcileChildren(instance: Instance, element: InternalElement) { 78 | const hostFrame = instance.hostFrame; 79 | const childInstances = instance.childInstances; 80 | const nextChildElements = element.props.children || []; 81 | const newChildInstances = []; 82 | const count = Math.max(childInstances.length, nextChildElements.length); 83 | for (let i = 0; i < count; i++) { 84 | const childInstance = childInstances[i]; 85 | const childElement = nextChildElements[i]; 86 | const newChildInstance = reconcile(hostFrame, childInstance, childElement); 87 | newChildInstances.push(newChildInstance); 88 | } 89 | return newChildInstances.filter(instance => instance != null); 90 | } 91 | 92 | function instantiate( 93 | element: InternalElement, parentFrame: WoWAPI.Region): Instance { 94 | const { type, props } = element; 95 | 96 | if (typeof type === 'string') { 97 | if (type === TEXT_ELEMENT) { 98 | throw 'Cannot create inline text, yet'; 99 | } 100 | // print('instantiate', type, stringify(props)); 101 | 102 | // Instantiate host element 103 | const frame = createFrame(type, parentFrame, props); 104 | 105 | updateFrameProperties(frame, {}, props); 106 | 107 | const childElements = props.children || []; 108 | const childInstances = 109 | childElements.map(child => instantiate(child, frame)); 110 | 111 | const instance: Instance = 112 | { hostFrame: frame, element, childInstances, childInstance: null }; 113 | return instance; 114 | 115 | } else { 116 | // print('instantiate', (type as any).name, stringify(props)); 117 | // Instantiate component element 118 | const instance = {} as Instance; 119 | const publicInstance = createPublicInstance(element, instance); 120 | const childElement = publicInstance.render(); 121 | const childInstance = instantiate(childElement, parentFrame); 122 | const hostFrame = childInstance.hostFrame; 123 | 124 | const updateProps: 125 | Partial = { hostFrame, element, childInstance, publicInstance }; 126 | Object.assign(instance, updateProps); 127 | return instance; 128 | } 129 | } 130 | 131 | function createPublicInstance( 132 | element: InternalElement, internalInstance: Instance) { 133 | const { type: ComponentType, props } = element; 134 | if (!ComponentType) { 135 | throw 'Tried createPublicInstance() with undefined'; 136 | } 137 | 138 | if (typeof ComponentType === 'string') { 139 | throw 'Tried createPublicInstance() with string'; 140 | } 141 | 142 | const publicInstance = new (ComponentType as any)(props); 143 | publicInstance.__internalInstance = internalInstance; 144 | return publicInstance; 145 | } 146 | -------------------------------------------------------------------------------- /src/ts_transformers/RequirePreload.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-object-literal-type-assertion */ 2 | import * as ts from "typescript"; 3 | import { 4 | Block, 5 | createBlock, 6 | createCallExpression, 7 | createExpressionStatement, 8 | createFunctionExpression, 9 | createIdentifier, 10 | createStringLiteral, 11 | Plugin 12 | } from "typescript-to-lua"; 13 | 14 | export const RequirePreload: Plugin = { 15 | visitors: { 16 | [ts.SyntaxKind.SourceFile]: (node, context) => { 17 | const [fileContent] = context.superTransformNode(node) as Block[]; 18 | if (context.isModule) { 19 | const moduleFunction = createFunctionExpression( 20 | fileContent, 21 | undefined, 22 | undefined, 23 | undefined 24 | ); 25 | 26 | let moduleName = context.sourceFile.fileName.split("src")[1]; 27 | if (moduleName.startsWith("/")) moduleName = moduleName.substring(1); 28 | if (moduleName.endsWith(".tsx")) moduleName = moduleName.substring(0, moduleName.length - 4); 29 | if (moduleName.endsWith(".ts")) moduleName = moduleName.substring(0, moduleName.length - 3); 30 | moduleName = moduleName.split("/").join("."); 31 | moduleName = moduleName.replace(".index", ""); 32 | // Skip init.lua so it can be the entry-point 33 | if (moduleName === "init") return fileContent; 34 | 35 | // Generates: 36 | // tstl_register_module("module/name", function() ... end) 37 | const moduleCallExpression = createCallExpression( 38 | createIdentifier("tstl_register_module"), 39 | [createStringLiteral(moduleName), moduleFunction] 40 | ); 41 | 42 | return createBlock([createExpressionStatement(moduleCallExpression)]); 43 | } 44 | return fileContent; 45 | }, 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /src/ts_transformers/WowJsxTransformer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-object-literal-type-assertion */ 2 | import * as ts from "typescript"; 3 | import { 4 | createCallExpression, 5 | createIdentifier, 6 | createNilLiteral, 7 | createStringLiteral, 8 | createTableIndexExpression, 9 | FunctionVisitor, 10 | Plugin, 11 | TransformationContext, 12 | VisitorResult 13 | } from "typescript-to-lua"; 14 | import { literalVisitors } from "typescript-to-lua/dist/transformation/visitors/literal"; 15 | 16 | const transformObjectLiteral = literalVisitors[ 17 | ts.SyntaxKind.ObjectLiteralExpression 18 | ] as FunctionVisitor; 19 | const transformArrayLiteral = literalVisitors[ 20 | ts.SyntaxKind.ArrayLiteralExpression 21 | ] as FunctionVisitor; 22 | 23 | function transformJsxAttributesExpression( 24 | expression: ts.JsxAttributes, 25 | context: TransformationContext 26 | ): VisitorResult { 27 | if ( 28 | expression.properties.find( 29 | element => element.kind === ts.SyntaxKind.JsxSpreadAttribute 30 | ) 31 | ) { 32 | throw new Error("Unsupported: JsxSpreadAttribute"); 33 | } 34 | const properties = expression.properties 35 | .filter( 36 | (element): element is ts.JsxAttribute => 37 | element.kind !== ts.SyntaxKind.JsxSpreadAttribute 38 | ) 39 | .map(element => { 40 | const valueOrExpression = element.initializer 41 | ? element.initializer 42 | : ts.createLiteral(true); 43 | return ts.createPropertyAssignment(element.name, valueOrExpression); 44 | }); 45 | 46 | return transformObjectLiteral(ts.createObjectLiteral(properties), context); 47 | } 48 | function transformJsxOpeningElement( 49 | expression: ts.JsxSelfClosingElement | ts.JsxOpeningElement, 50 | context: TransformationContext, 51 | children?: ts.NodeArray 52 | ): VisitorResult { 53 | // 54 | // React.createElement(Something, {a = 'b'}) 55 | const [library, create] = context.options.jsxFactory 56 | ? context.options.jsxFactory.split(".") 57 | : ["React", "createElement"]; 58 | const createElement = createTableIndexExpression( 59 | createIdentifier(library), 60 | createStringLiteral(create) 61 | ); 62 | const tagName = expression.tagName.getText(); 63 | 64 | const tag = 65 | tagName.toLowerCase() === tagName 66 | ? createStringLiteral(tagName) 67 | : createIdentifier(tagName); 68 | 69 | const props = transformJsxAttributesExpression( 70 | expression.attributes, 71 | context 72 | ); 73 | 74 | if (children) { 75 | const childrenOrStringLiterals = children 76 | .filter(child => !ts.isJsxText(child) || child.text.trim() !== "") 77 | .map(child => 78 | ts.isJsxText(child) ? ts.createStringLiteral(child.text.trim()) : child 79 | ); 80 | const arrayLiteral = ts.createArrayLiteral(childrenOrStringLiterals, true); 81 | 82 | return createCallExpression( 83 | createElement, 84 | [tag, props, transformArrayLiteral(arrayLiteral, context)], 85 | expression 86 | ); 87 | } 88 | 89 | return createCallExpression(createElement, [tag, props], expression); 90 | } 91 | 92 | function transformJsxElement( 93 | expression: ts.JsxElement | ts.JsxSelfClosingElement, 94 | context: TransformationContext 95 | ): VisitorResult { 96 | if (ts.isJsxSelfClosingElement(expression)) { 97 | return transformJsxOpeningElement(expression, context); 98 | } 99 | return transformJsxOpeningElement( 100 | expression.openingElement, 101 | context, 102 | expression.children 103 | ); 104 | } 105 | 106 | export const WowJsxTransformer: Plugin = { 107 | visitors: { 108 | [ts.SyntaxKind.JsxSelfClosingElement]: transformJsxElement, 109 | [ts.SyntaxKind.JsxElement]: transformJsxElement, 110 | [ts.SyntaxKind.JsxExpression]: (node, context) => { 111 | if (node.expression) { 112 | return context.transformExpression(node.expression); 113 | } 114 | return createNilLiteral(); 115 | }, 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/wow-utils.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | type Props = Record; 4 | 5 | const frameCache: Record = { 6 | }; 7 | 8 | function getCache(type: string): WoWAPI.Region | undefined { 9 | // if (frameCache[type]) { 10 | // return frameCache[type].length ? frameCache[type].pop() : undefined; 11 | // } 12 | return undefined; 13 | } 14 | 15 | function setCache(frame: WoWAPI.Region) { 16 | const type = frame.GetObjectType(); 17 | print('store cache', type); 18 | if (frameCache[type] && frameCache[type].length) { 19 | frameCache[type].push(frame); 20 | } else { 21 | frameCache[type] = [frame]; 22 | } 23 | } 24 | 25 | export function createFrame( 26 | jsxType: string, parentFrame: WoWAPI.Region, 27 | props: Props): WoWAPI.Region { 28 | const frameType = pascalCase(jsxType); 29 | 30 | let frame = getCache(frameType); 31 | 32 | if (frame) { 33 | print('got frame from cache', frameType, frame, 'parent:', parentFrame); 34 | frame.SetParent(parentFrame); 35 | frame.Show(); 36 | return frame; 37 | } 38 | 39 | if (frameType === 'FontString') { 40 | frame = (parentFrame as WoWAPI.Frame) 41 | .CreateFontString(props.name, props.DrawLayer || 'ARTWORK', props.inheritsFrom); 42 | } else if (frameType === 'Texture') { 43 | frame = (parentFrame as WoWAPI.Frame) 44 | .CreateTexture(props.name, props.DrawLayer || 'ARTWORK', props.inheritsFrom); 45 | } else { 46 | frame = 47 | CreateFrame(frameType as WoWAPI.FrameType, props.name, parentFrame, props.inheritsFrom || "BackdropTemplate") as 48 | WoWAPI.Frame; 49 | } 50 | // frame.SetParent(parentFrame); 51 | print('created frame:', frameType); 52 | return frame; 53 | } 54 | 55 | export function cleanupFrame(frame: WoWAPI.Region) { 56 | print('cleaning up frame', frame.GetObjectType(), frame); 57 | frame.Hide(); 58 | frame.ClearAllPoints(); 59 | if (frame.GetObjectType() as string === 'Texture' || 60 | frame.GetObjectType() as string === 'FontString') { 61 | frame.SetParent(UIParent); 62 | } else { 63 | frame.SetParent(null); 64 | } 65 | setCache(frame); 66 | } 67 | 68 | const isEvent = (name: string) => name.startsWith('On'); 69 | const isStandardProperty = (name: string) => !isEvent(name) && 70 | !isOrderedProperty(name) && name !== 'children' && name !== 'Points' && name !== 'Point' && 71 | name !== 'name' && name !== 'DrawLayer' && name !== 'inheritsFrom' && name !== 'Clickable' && 72 | name !== 'Draggable'; 73 | 74 | /** 75 | * These properties must be set _before_ their other properties e.g. Background 76 | * must be set before BackgroundColor 77 | */ 78 | const isOrderedProperty = (name: string) => name === 'Font' || 79 | name === 'Background' || name === 'Texture' || name === 'Backdrop'; 80 | /** 81 | * These properties take table values, which should be set verbatim. Array 82 | * values will apply each item as an argument to SetX. These values should not 83 | * be interpreted as arrays. 84 | */ 85 | const isTableValue = (name: string) => name === 'Backdrop'; 86 | 87 | export function updateFrameProperties( 88 | frame: WoWAPI.Region, prevProps: Props, nextProps: Props) { 89 | updateFramePoints(frame, nextProps); 90 | updateFrameLayer(frame, nextProps); 91 | updateFrameEvents(frame, prevProps, nextProps); 92 | updateOrderSpecificProperties(frame, prevProps, nextProps); 93 | updateRemainingProperties(frame, prevProps, nextProps); 94 | } 95 | 96 | function updateOrderSpecificProperties( 97 | frame: WoWAPI.Region, prevProps: Props, nextProps: Props) { 98 | // Remove properties that are no longer specified 99 | Object.keys(prevProps) 100 | .filter(key => isOrderedProperty(key) && !nextProps[key]) 101 | .forEach(key => { 102 | attemptSetProperty(frame, key, null); 103 | }); 104 | // Set properties 105 | Object.keys(nextProps).filter(isOrderedProperty).forEach(key => { 106 | attemptSetProperty(frame, key, nextProps[key]); 107 | }); 108 | } 109 | 110 | function updateRemainingProperties( 111 | frame: WoWAPI.Region, prevProps: Props, nextProps: Props) { 112 | // Remove properties that are no longer specified 113 | Object.keys(prevProps) 114 | .filter(key => isStandardProperty(key) && !nextProps[key]) 115 | .forEach(key => { 116 | attemptSetProperty(frame, key, null); 117 | }); 118 | // Set properties 119 | Object.keys(nextProps).filter(isStandardProperty).forEach(key => { 120 | attemptSetProperty(frame, key, nextProps[key]); 121 | }); 122 | } 123 | 124 | function updateFrameEvents( 125 | frame: WoWAPI.Region, prevProps: Props, nextProps: Props) { 126 | // Detach removed event listeners 127 | Object.keys(prevProps) 128 | .filter(key => isEvent(key) && !nextProps[key]) 129 | .forEach(event => { 130 | (frame as WoWAPI.Frame).SetScript(event as WoWAPI.Event.OnAny, undefined); 131 | }); 132 | 133 | if (nextProps['Clickable']) { 134 | (frame as any).RegisterForClicks('RightButton'); 135 | } 136 | if (nextProps['Draggable']) { 137 | (frame as any).RegisterForDrag(...nextProps['Draggable']); 138 | } 139 | 140 | // Add new event listeners 141 | Object.keys(nextProps) 142 | .filter(key => isEvent(key) && prevProps[key] !== nextProps[key]) 143 | .forEach(event => { 144 | // print('attaching event', event); 145 | (frame as WoWAPI.Frame).SetScript(event as WoWAPI.Event.OnAny, nextProps[event]); 146 | }); 147 | } 148 | 149 | /** Handle frame points, size to parent unless specified. */ 150 | function updateFramePoints(frame: WoWAPI.Region, nextProps: JSX.BaseFrameProps) { 151 | frame.ClearAllPoints(); 152 | if (nextProps.Point) { 153 | setPoint(frame, nextProps.Point); 154 | return; 155 | } 156 | if (nextProps.Points) { 157 | const points = nextProps.Points; 158 | points.forEach(pointDef => setPoint(frame, pointDef)); 159 | } else { 160 | // Fill to parent 161 | frame.SetAllPoints(); 162 | } 163 | } 164 | 165 | /** Handle frame points, size to parent unless specified. */ 166 | function updateFrameLayer(frame: WoWAPI.Region, nextProps: JSX.LayeredRegionProps) { 167 | const region = frame as WoWAPI.LayeredRegion; 168 | const layer = nextProps.DrawLayer; 169 | 170 | if (!layer || typeof region.SetDrawLayer !== 'function') { 171 | return; 172 | } 173 | 174 | if (typeof layer === 'string') { 175 | region.SetDrawLayer(layer, 0); 176 | return; 177 | } 178 | 179 | region.SetDrawLayer(layer[0] as WoWAPI.Layer, layer[1]); 180 | } 181 | 182 | /** Create a point declaration */ 183 | export function P( 184 | point: WoWAPI.Point, x?: number, y?: number, relativePoint?: WoWAPI.Point, 185 | relativeFrame?: WoWAPI.Region): JSX.PointDefinition { 186 | // TODO: memoize for perf 187 | return { point, relativePoint, relativeFrame, x, y }; 188 | } 189 | 190 | function setPoint(frame: WoWAPI.Region, pointDef: JSX.Point) { 191 | if (typeof pointDef === 'string') { 192 | frame.SetPoint(pointDef); 193 | } else { 194 | const { point, relativePoint, relativeFrame, x, y } = pointDef; 195 | const relativeTo = relativePoint || point; 196 | // print('setPoint', Object.keys(pointDef).join(', ')); 197 | if (relativeFrame) { 198 | frame.SetPoint(point, relativeFrame, relativeTo, x || 0, y || 0); 199 | } else { 200 | const parent = frame.GetParent() as WoWAPI.Region; 201 | frame.SetPoint(point, parent, relativeTo, x || 0, y || 0); 202 | } 203 | } 204 | } 205 | 206 | function attemptSetProperty(frame: WoWAPI.Region, key: string, value: any) { 207 | const region = frame as any as Record void>; 208 | const setter = `Set${key}`; 209 | const setterFn = region[setter]; 210 | assert(setterFn, `Tried to use ${setter} and it did not exist on ${region}`); 211 | 212 | if (setterFn && typeof setterFn == 'function') { 213 | if (typeof value === 'string' || typeof value === 'number' || 214 | typeof value === 'boolean' || isTableValue(key)) { 215 | region[setter](value); 216 | } else { 217 | // print( `calling ${setter} with array elements as args:`, 218 | // (value as any[]).join(', ')); 219 | setterFn.apply(region, value); 220 | } 221 | } 222 | } 223 | 224 | export function sentenceCase(str: string) { 225 | return str[0].toUpperCase() + str.slice(1).toLowerCase(); 226 | } 227 | 228 | export function pascalCase(kebabCase: string) { 229 | return kebabCase.split('-').map(sentenceCase).join(''); 230 | } 231 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | "target": "ESNext", 5 | "module": "commonjs", 6 | "lib": [ 7 | "ESNext", 8 | "DOM" 9 | ], 10 | "jsx": "react", 11 | "jsxFactory": "createElement", 12 | "declaration": true, 13 | "strict": true, 14 | "rootDir": "./src/", 15 | "outDir": "./dist/", 16 | "moduleResolution": "node", 17 | "skipLibCheck": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "typeRoots": [ 20 | "node_modules/@wartoshika/wow-declarations", 21 | "node_modules/lua-types/5.1", 22 | "node_modules/@types" 23 | ], 24 | "types": [ 25 | "lua-types/5.1", 26 | "@wartoshika/wow-declarations" 27 | ] 28 | } 29 | } 30 | --------------------------------------------------------------------------------