├── .editorconfig ├── .gitignore ├── LICENSE.txt ├── Raw.code-workspace ├── Raw.cover.tsx ├── Raw.ts ├── package.json ├── quickstart.md ├── readme-poster.png ├── readme.md ├── tsconfig.json └── tsconfig.release.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = tab 3 | indent_size = 2 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = false 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | +* 3 | node_modules 4 | package-lock.json -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Paul Gordon 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. -------------------------------------------------------------------------------- /Raw.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".", 5 | }, 6 | ], 7 | "settings": { 8 | "files.exclude": { 9 | "**/.git": true, 10 | "**/.DS_Store": true, 11 | "**/node_modules": true, 12 | "**/package-lock.json": true, 13 | "*.tsbuildinfo": true, 14 | "*.d.ts.map": true, 15 | }, 16 | "search.exclude": { 17 | "**/.git": true, 18 | "**/.DS_Store": true, 19 | "**/build": true, 20 | "**/node_modules": true, 21 | "**/package-lock.json": true, 22 | "**/lib/*.js": true, 23 | "index.*": true 24 | }, 25 | "task.allowAutomaticTasks": "on", 26 | }, 27 | "launch": { 28 | "configurations": [ 29 | { 30 | "name": "Debug Active Cover Function (Electron)", 31 | "type": "chrome", 32 | "request": "launch", 33 | "runtimeExecutable": "${workspaceFolder}/../Moduless/node_modules/electron/cli.js", 34 | "cwd": "${workspaceFolder}", 35 | "runtimeArgs": [ 36 | "${workspaceFolder}/../Moduless/build/moduless.js", 37 | "--remote-debugging-port=9222" 38 | ], 39 | "sourceMaps": true, 40 | "timeout": 2000, 41 | }, 42 | { 43 | "name": "Debug All Cover Functions", 44 | "type": "chrome", 45 | "request": "launch", 46 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 47 | "cwd": "${workspaceRoot}", 48 | "runtimeArgs": [ 49 | "${workspaceRoot}/node_modules/moduless/build/moduless.js", 50 | "--remote-debugging-port=9222", 51 | "expression=(cover)" 52 | ], 53 | "sourceMaps": true, 54 | "timeout": 2000 55 | } 56 | ] 57 | }, 58 | "tasks": { 59 | "version": "2.0.0", 60 | "tasks": [ 61 | { 62 | "label": "Compile Library", 63 | "type": "shell", 64 | "command": "tsc", 65 | "args": [ 66 | "--build", 67 | "--watch" 68 | ], 69 | "options": { 70 | "cwd": "${workspaceRoot}" 71 | }, 72 | "problemMatcher": [ 73 | "$tsc" 74 | ], 75 | "runOptions": { 76 | "runOn": "folderOpen" 77 | }, 78 | "group": { 79 | "kind": "build", 80 | "isDefault": true 81 | }, 82 | "isBackground": true 83 | }, 84 | { 85 | "label": "Set Active Cover Function", 86 | "type": "shell", 87 | "command": "npx", 88 | "args": [ 89 | "moduless", 90 | "set", 91 | "${file}:${lineNumber}" 92 | ], 93 | "problemMatcher": [] 94 | }, 95 | { 96 | "label": "Run All Cover Functions", 97 | "type": "shell", 98 | "command": "${workspaceRoot}/node_modules/.bin/electron", 99 | "args": [ 100 | "${workspaceRoot}/node_modules/moduless/build/moduless.js", 101 | "moduless", 102 | "all" 103 | ], 104 | "problemMatcher": [] 105 | } 106 | ] 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Raw.cover.tsx: -------------------------------------------------------------------------------- 1 | 2 | interface RedElementAttribute extends Raw.ElementAttribute 3 | { 4 | a: number; 5 | b: string; 6 | } 7 | 8 | declare namespace JSX 9 | { 10 | interface IntrinsicElements 11 | { 12 | red: E; 13 | } 14 | } 15 | 16 | namespace Cover 17 | { 18 | /** */ 19 | export function coverCustomElement() 20 | { 21 | const e = ; 22 | document.body.append(e); 23 | 24 | return [ 25 | () => (e as any).a === 1, 26 | () => (e as any).b === "string", 27 | ]; 28 | } 29 | 30 | /** */ 31 | export function coverJSXElementWithRawCss() 32 | { 33 | const element =
{ raw.css(" > *", { color: "red" }) }
; 34 | document.body.append(element); 35 | } 36 | 37 | /** */ 38 | export function coverJSXElementWithRawEvent() 39 | { 40 | const element =
{ raw.on("click", () => console.log("hello")) }Click me
; 41 | document.body.append(element); 42 | } 43 | 44 | /** */ 45 | export function coverRawConnectedEvent() 46 | { 47 | raw.div(); 48 | 49 | raw.get(document.body)( 50 | raw.section( 51 | raw.on("connected", () => 52 | { 53 | console.log("connected"); 54 | }) 55 | ) 56 | ); 57 | } 58 | 59 | /** */ 60 | export function coverClassesWithExtraSpaces() 61 | { 62 | const div = raw.div(" a b c "); 63 | return [ 64 | () => div.className === "a b c", 65 | ]; 66 | } 67 | 68 | /** */ 69 | export function coverRawUseClassAttributeDirectly() 70 | { 71 | const e1 = raw.div({ class: "a b c" }); 72 | const e2 =
; 73 | 74 | return [ 75 | () => e1.classList.contains("a") && e1.classList.contains("b") && e1.classList.contains("c"), 76 | () => e2.classList.contains("x") && e2.classList.contains("y") && e2.classList.contains("z"), 77 | ]; 78 | } 79 | 80 | /** */ 81 | export function coverRawStyleAttach() 82 | { 83 | raw.style("DIV", { 84 | width: "100px", 85 | height: "100px", 86 | border: "10px solid green" 87 | }).attach(); 88 | 89 | raw.get(document.body)(raw.div()); 90 | } 91 | 92 | /** */ 93 | export function coverRawShadow() 94 | { 95 | raw.style("DIV", { borderRadius: "20px" }); 96 | 97 | raw.get(document.body)( 98 | raw.div( 99 | "shadow-container", 100 | { 101 | border: "10px solid red", 102 | padding: "10px", 103 | }, 104 | raw.shadow( 105 | raw.style( 106 | "DIV", 107 | { 108 | backgroundColor: "yellow" 109 | }, 110 | ), 111 | raw.div( 112 | "shadow-element-1", 113 | { 114 | width: "100px", 115 | height: "100px", 116 | border: "10px solid green" 117 | } 118 | ) 119 | ), 120 | raw.shadow( 121 | raw.div( 122 | "shadow-element-2", 123 | { 124 | width: "100px", 125 | height: "100px", 126 | border: "10px solid blue" 127 | } 128 | ) 129 | ) 130 | ) 131 | ); 132 | } 133 | 134 | /** */ 135 | export function coverRawCssDeduplication() 136 | { 137 | const insert = () => 138 | { 139 | document.body.append( 140 | raw.div( 141 | raw.css(" P", { color: "red" }), 142 | raw.p(raw.text`para1`), 143 | raw.p(raw.text`para2`), 144 | ) 145 | ); 146 | }; 147 | 148 | insert(); 149 | insert(); 150 | } 151 | 152 | /** */ 153 | export function coverRawArrayValues() 154 | { 155 | const div = raw.div( 156 | { 157 | width: ["error", "100%"] 158 | } 159 | ); 160 | 161 | document.body.append(div); 162 | return () => div.style.width === "100%"; 163 | } 164 | 165 | /** */ 166 | export function coverRawJSXCompatibility() 167 | { 168 | document.body.append( 169 | raw.div( 170 | This is bold text. 171 | ) 172 | ); 173 | } 174 | 175 | /** */ 176 | export function coverTemplateStrings() 177 | { 178 | function blue(text: string) { return "" + text + ""; } 179 | function red(text: string) { return "" + text + ""; } 180 | 181 | raw.text` 182 | We need a new ${blue("media channel")} where you can 183 | have your ${blue("cake")} and ${red("eat")} it ${red("too")}. 184 | `; 185 | 186 | function br() 187 | { 188 | return raw.br(); 189 | } 190 | 191 | const text = raw.text` 192 | Here is some text. And a line break ${br()} 193 | `; 194 | } 195 | 196 | /** */ 197 | export function coverBackgroundImageInPseudo() 198 | { 199 | const div = raw.div( 200 | raw.css(":before", { 201 | content: `""`, 202 | backgroundImage: "linear-gradient(red, blue)", 203 | }), 204 | ); 205 | 206 | document.body.append(div); 207 | } 208 | 209 | //@ts-ignore 210 | if (typeof module === "object") Object.assign(module.exports, { Cover }); 211 | } 212 | -------------------------------------------------------------------------------- /Raw.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 4 | */ 5 | interface RawElements 6 | { 7 | a(...params: Raw.Param[]): HTMLAnchorElement; 8 | abbr(...params: Raw.Param[]): HTMLElement; 9 | address(...params: Raw.Param[]): HTMLElement; 10 | area(...params: Raw.Param[]): HTMLAreaElement; 11 | article(...params: Raw.Param[]): HTMLElement; 12 | aside(...params: Raw.Param[]): HTMLElement; 13 | audio(...params: Raw.Param[]): HTMLAudioElement; 14 | b(...params: Raw.Param[]): HTMLElement; 15 | base(...params: Raw.Param[]): HTMLBaseElement; 16 | bdi(...params: Raw.Param[]): HTMLElement; 17 | bdo(...params: Raw.Param[]): HTMLElement; 18 | blockquote(...params: Raw.Param[]): HTMLQuoteElement; 19 | body(...params: Raw.Param[]): HTMLBodyElement; 20 | br(...params: Raw.Param[]): HTMLBRElement; 21 | button(...params: Raw.Param[]): HTMLButtonElement; 22 | canvas(...params: Raw.Param[]): HTMLCanvasElement; 23 | caption(...params: Raw.Param[]): HTMLTableCaptionElement; 24 | cite(...params: Raw.Param[]): HTMLElement; 25 | code(...params: Raw.Param[]): HTMLElement; 26 | col(...params: Raw.Param[]): HTMLTableColElement; 27 | colgroup(...params: Raw.Param[]): HTMLTableColElement; 28 | data(...params: Raw.Param[]): HTMLDataElement; 29 | datalist(...params: Raw.Param[]): HTMLDataListElement; 30 | dd(...params: Raw.Param[]): HTMLElement; 31 | del(...params: Raw.Param[]): HTMLModElement; 32 | details(...params: Raw.Param[]): HTMLDetailsElement; 33 | dfn(...params: Raw.Param[]): HTMLElement; 34 | dialog(...params: Raw.Param[]): HTMLDialogElement; 35 | dir(...params: Raw.Param[]): HTMLDirectoryElement; 36 | div(...params: Raw.Param[]): HTMLDivElement; 37 | dl(...params: Raw.Param[]): HTMLDListElement; 38 | dt(...params: Raw.Param[]): HTMLElement; 39 | em(...params: Raw.Param[]): HTMLElement; 40 | embed(...params: Raw.Param[]): HTMLEmbedElement; 41 | fieldset(...params: Raw.Param[]): HTMLFieldSetElement; 42 | figcaption(...params: Raw.Param[]): HTMLElement; 43 | figure(...params: Raw.Param[]): HTMLElement; 44 | font(...params: Raw.Param[]): HTMLFontElement; 45 | footer(...params: Raw.Param[]): HTMLElement; 46 | form(...params: Raw.Param[]): HTMLFormElement; 47 | frame(...params: Raw.Param[]): HTMLFrameElement; 48 | frameset(...params: Raw.Param[]): HTMLFrameSetElement; 49 | h1(...params: Raw.Param[]): HTMLHeadingElement; 50 | h2(...params: Raw.Param[]): HTMLHeadingElement; 51 | h3(...params: Raw.Param[]): HTMLHeadingElement; 52 | h4(...params: Raw.Param[]): HTMLHeadingElement; 53 | h5(...params: Raw.Param[]): HTMLHeadingElement; 54 | h6(...params: Raw.Param[]): HTMLHeadingElement; 55 | head(...params: Raw.Param[]): HTMLHeadElement; 56 | header(...params: Raw.Param[]): HTMLElement; 57 | hgroup(...params: Raw.Param[]): HTMLElement; 58 | hr(...params: Raw.Param[]): HTMLHRElement; 59 | i(...params: Raw.Param[]): HTMLElement; 60 | iframe(...params: Raw.Param[]): HTMLIFrameElement; 61 | img(...params: Raw.Param[]): HTMLImageElement; 62 | input(...params: Raw.Param[]): HTMLInputElement; 63 | ins(...params: Raw.Param[]): HTMLModElement; 64 | kbd(...params: Raw.Param[]): HTMLElement; 65 | label(...params: Raw.Param[]): HTMLLabelElement; 66 | legend(...params: Raw.Param[]): HTMLLegendElement; 67 | li(...params: Raw.Param[]): HTMLLIElement; 68 | link(...params: Raw.Param[]): HTMLLinkElement; 69 | main(...params: Raw.Param[]): HTMLElement; 70 | map(...params: Raw.Param[]): HTMLMapElement; 71 | mark(...params: Raw.Param[]): HTMLElement; 72 | marquee(...params: Raw.Param[]): HTMLMarqueeElement; 73 | menu(...params: Raw.Param[]): HTMLMenuElement; 74 | meta(...params: Raw.Param[]): HTMLMetaElement; 75 | meter(...params: Raw.Param[]): HTMLMeterElement; 76 | nav(...params: Raw.Param[]): HTMLElement; 77 | noscript(...params: Raw.Param[]): HTMLElement; 78 | object(...params: Raw.Param[]): HTMLObjectElement; 79 | ol(...params: Raw.Param[]): HTMLOListElement; 80 | optgroup(...params: Raw.Param[]): HTMLOptGroupElement; 81 | option(...params: Raw.Param[]): HTMLOptionElement; 82 | output(...params: Raw.Param[]): HTMLOutputElement; 83 | p(...params: Raw.Param[]): HTMLParagraphElement; 84 | param(...params: Raw.Param[]): HTMLParamElement; 85 | picture(...params: Raw.Param[]): HTMLPictureElement; 86 | pre(...params: Raw.Param[]): HTMLPreElement; 87 | progress(...params: Raw.Param[]): HTMLProgressElement; 88 | q(...params: Raw.Param[]): HTMLQuoteElement; 89 | rp(...params: Raw.Param[]): HTMLElement; 90 | rt(...params: Raw.Param[]): HTMLElement; 91 | ruby(...params: Raw.Param[]): HTMLElement; 92 | s(...params: Raw.Param[]): HTMLElement; 93 | samp(...params: Raw.Param[]): HTMLElement; 94 | script(...params: Raw.Param[]): HTMLScriptElement; 95 | section(...params: Raw.Param[]): HTMLElement; 96 | select(...params: Raw.Param[]): HTMLSelectElement; 97 | slot(...params: Raw.Param[]): HTMLSlotElement; 98 | small(...params: Raw.Param[]): HTMLElement; 99 | source(...params: Raw.Param[]): HTMLSourceElement; 100 | span(...params: Raw.Param[]): HTMLSpanElement; 101 | strong(...params: Raw.Param[]): HTMLElement; 102 | sub(...params: Raw.Param[]): HTMLElement; 103 | summary(...params: Raw.Param[]): HTMLElement; 104 | sup(...params: Raw.Param[]): HTMLElement; 105 | table(...params: Raw.Param[]): HTMLTableElement; 106 | tbody(...params: Raw.Param[]): HTMLTableSectionElement; 107 | td(...params: Raw.Param[]): HTMLTableCellElement; 108 | template(...params: Raw.Param[]): HTMLTemplateElement; 109 | textarea(...params: Raw.Param[]): HTMLTextAreaElement; 110 | tfoot(...params: Raw.Param[]): HTMLTableSectionElement; 111 | th(...params: Raw.Param[]): HTMLTableCellElement; 112 | thead(...params: Raw.Param[]): HTMLTableSectionElement; 113 | time(...params: Raw.Param[]): HTMLTimeElement; 114 | title(...params: Raw.Param[]): HTMLTitleElement; 115 | tr(...params: Raw.Param[]): HTMLTableRowElement; 116 | track(...params: Raw.Param[]): HTMLTrackElement; 117 | u(...params: Raw.Param[]): HTMLElement; 118 | ul(...params: Raw.Param[]): HTMLUListElement; 119 | video(...params: Raw.Param[]): HTMLVideoElement; 120 | wbr(...params: Raw.Param[]): HTMLElement; 121 | 122 | new(): RawElements; 123 | } 124 | 125 | /** 126 | * JSX compatibility 127 | */ 128 | declare namespace JSX 129 | { 130 | type Element = globalThis.Element; 131 | type E = Partial; 132 | 133 | interface IntrinsicElements 134 | { 135 | a: E; 136 | abbr: E; 137 | address: E; 138 | area: E; 139 | article: E; 140 | aside: E; 141 | audio: E; 142 | b: E; 143 | base: E; 144 | bdi: E; 145 | bdo: E; 146 | blockquote: E; 147 | body: E; 148 | br: E; 149 | button: E; 150 | canvas: E; 151 | caption: E; 152 | cite: E; 153 | code: E; 154 | col: E; 155 | colgroup: E; 156 | data: E; 157 | datalist: E; 158 | dd: E; 159 | del: E; 160 | details: E; 161 | dfn: E; 162 | dialog: E; 163 | dir: E; 164 | div: E; 165 | dl: E; 166 | dt: E; 167 | em: E; 168 | embed: E; 169 | fieldset: E; 170 | figcaption: E; 171 | figure: E; 172 | font: E; 173 | footer: E; 174 | form: E; 175 | frame: E; 176 | frameset: E; 177 | h1: E; 178 | h2: E; 179 | h3: E; 180 | h4: E; 181 | h5: E; 182 | h6: E; 183 | head: E; 184 | header: E; 185 | hgroup: E; 186 | hr: E; 187 | i: E; 188 | iframe: E; 189 | img: E; 190 | input: E; 191 | ins: E; 192 | kbd: E; 193 | label: E; 194 | legend: E; 195 | li: E; 196 | link: E; 197 | main: E; 198 | map: E; 199 | mark: E; 200 | marquee: E; 201 | menu: E; 202 | meta: E; 203 | meter: E; 204 | nav: E; 205 | noscript: E; 206 | object: E; 207 | ol: E; 208 | optgroup: E; 209 | option: E; 210 | output: E; 211 | p: E; 212 | param: E; 213 | picture: E; 214 | pre: E; 215 | progress: E; 216 | q: E; 217 | rp: E; 218 | rt: E; 219 | ruby: E; 220 | s: E; 221 | samp: E; 222 | script: E; 223 | section: E; 224 | select: E; 225 | slot: E; 226 | small: E; 227 | source: E; 228 | span: E; 229 | strong: E; 230 | sub: E; 231 | summary: E; 232 | sup: E; 233 | table: E; 234 | tbody: E; 235 | td: E; 236 | template: E; 237 | textarea: E; 238 | tfoot: E; 239 | th: E; 240 | thead: E; 241 | time: E; 242 | title: E; 243 | tr: E; 244 | track: E; 245 | u: E; 246 | ul: E; 247 | video: E; 248 | wbr: E; 249 | } 250 | } 251 | 252 | class Raw extends (() => Object as any as RawElements)() 253 | { 254 | /** 255 | * Stores the immutable set of HTML elements that 256 | * are recognized as HTML element creation functions. 257 | */ 258 | static readonly elements: ReadonlySet = new Set(["a", "abbr", "address", "area", "article", "aside", "audio", "b", "base", "bdi", "bdo", "blockquote", "body", "br", "button", "canvas", "caption", "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption", "figure", "font", "footer", "form", "frame", "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "i", "iframe", "img", "input", "ins", "kbd", "label", "legend", "li", "link", "main", "map", "mark", "marquee", "menu", "meta", "meter", "nav", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "script", "section", "select", "slot", "small", "source", "span", "strong", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "u", "ul", "video", "wbr"]); 259 | 260 | /** 261 | * Stores the list of strings that are recognized as CSS properties by RawJS, 262 | * (as opposed to being recognized as HTML attributes). Users may contribute 263 | * strings to this set in order to add support for custom CSS properties. 264 | */ 265 | static readonly properties = (() => 266 | { 267 | const names: string[] = []; 268 | if (typeof document !== "undefined") 269 | for (const key in document.documentElement.style) 270 | names.push(key); 271 | 272 | return new Set(names); 273 | })(); 274 | 275 | /** */ 276 | static readonly HTMLCustomElement = 277 | (typeof HTMLElement !== "undefined") as true && 278 | class HTMLCustomElement extends HTMLElement { }; 279 | 280 | /** 281 | * Creates a new instance of a Raw element creator. 282 | * 283 | * @param doc A reference to the Document object over 284 | * which this Raw instance operates. 285 | */ 286 | constructor(private readonly doc: Document) 287 | { 288 | super(); 289 | 290 | for (const tagName of Raw.elements) 291 | this.define(tagName); 292 | } 293 | 294 | /** 295 | * Defines a custom element which derives from the specified constructor. 296 | */ 297 | define(tagName: string, constructor: typeof HTMLElement = Raw.HTMLCustomElement) 298 | { 299 | if (!Raw.elements.has(tagName)) 300 | { 301 | tagName += "-element"; 302 | if (typeof customElements !== "undefined") 303 | customElements.define(tagName, constructor); 304 | } 305 | 306 | Object.defineProperty(this, tagName, { 307 | value: (...params: Raw.Param[]) => this.apply(this.doc.createElement(tagName), params) 308 | }); 309 | } 310 | 311 | /** 312 | * A function that creates a new DOM Text node, but which may be overridden 313 | * in the constructor to return a different but compatible value. 314 | */ 315 | text(template: TemplateStringsArray, ...placeholders: (string | HTMLElement)[]): (Text | HTMLElement)[]; 316 | text(string: string): Text; 317 | text(a: TemplateStringsArray | string, ...b: string[]): any 318 | { 319 | if (typeof a === "string") 320 | return this.doc.createTextNode(a); 321 | 322 | const nodes: (string | HTMLElement)[] = []; 323 | for (let i = -1; ++i < b.length;) 324 | nodes.push(a[i], b[i]) 325 | 326 | nodes.push(a[a.length - 1]); 327 | return nodes.map(n => typeof n === "string" ? this.doc.createTextNode(n) : n); 328 | } 329 | 330 | /** 331 | * Creates a new Raw context from the specified Element or series of Elements. 332 | */ 333 | get(e: T, ...others: Element[]): (...params: Raw.Param[]) => T; 334 | get(e: T, ...others: Element[]): (...params: Raw.ShadowParam[]) => T; 335 | get(...elements: T[]): any 336 | { 337 | return (...params: Raw.Param[]) => 338 | { 339 | for (const e of elements) 340 | { 341 | if (Raw.is.element(e) || Raw.is.shadow(e)) 342 | this.apply(e as Element, params); 343 | 344 | else if (Raw.is.element((e as any as Raw.HatLike).head)) 345 | this.apply((e as any as Raw.HatLike).head, params); 346 | } 347 | 348 | return elements[0] || null; 349 | }; 350 | } 351 | 352 | /** 353 | * An object that contains environment-agnostic guard functions 354 | * to make various assertions about data. 355 | */ 356 | static readonly is = { 357 | node(n: any): n is Node 358 | { 359 | const type = n?.nodeType; 360 | return typeof type === "number" && type > 0 && type < 13; 361 | }, 362 | element(e: any): e is HTMLElement 363 | { 364 | return !!e && e.nodeType === 1; 365 | }, 366 | text(t: any): t is Text 367 | { 368 | return !!t && (t as Text).nodeType === 3; 369 | }, 370 | comment(c: any): c is Comment 371 | { 372 | return !!c && (c as Comment).nodeType === 8; 373 | }, 374 | shadow(c: any): c is ShadowRoot 375 | { 376 | return !!c && (c as ShadowRoot).nodeType === 11 && Raw.is.element(c.host); 377 | }, 378 | /** 379 | * Returns a boolean value that indicates whether the specified 380 | * string is the name of a valid CSS property in camelCase format, 381 | * for example, "fontWeight". 382 | */ 383 | property(name: string) 384 | { 385 | return Raw.properties.has(name); 386 | } 387 | }; 388 | 389 | /** 390 | * Creates and returns a ShadowRoot, in order to access 391 | * the shadow DOM of a particular element. 392 | */ 393 | shadow(...params: Raw.ShadowParam[]): Raw.Param 394 | { 395 | return e => 396 | { 397 | const shadow = e.shadowRoot || e.attachShadow({ mode: "open" }); 398 | this.apply(shadow, params as Raw.Param[]); 399 | }; 400 | } 401 | 402 | /** 403 | * Creates a DOM element using the standard JSX element creation call signature. 404 | * Any Raw.Param values that are strings are converted to DOM Text nodes rather 405 | * than class names. 406 | */ 407 | jsx(tag: string | Element, properties: Record | null = null, ...params: Raw.Param[]) 408 | { 409 | const e = typeof tag === "string" ? this.doc.createElement(tag) : tag; 410 | 411 | if (properties) 412 | for (const [n, v] of Object.entries(properties)) 413 | (e as any)[n] = v; 414 | 415 | // Generated class names should be appended 416 | // as class names rather than text content. 417 | const reg = new RegExp("^" + Raw.GeneratedClassPrefix.value + "[a-z\\d]{9,11}$"); 418 | 419 | params = params 420 | .filter(p => p) 421 | .map(p => typeof p === "string" && !reg.test(p) ? this.text(p) : p); 422 | 423 | return this.apply(e, params) as Element; 424 | } 425 | 426 | /** 427 | * This is the main applicator method where all params are applied 428 | * to the target. 429 | * 430 | * PROCEED WITH CAUTION. This code is VERY performance sensitive. 431 | * It uses constructor checks instead of instanceof and typeof in an effort 432 | * to nullify any performance overhead. Be careful of changing this code 433 | * without having full knowledge of what you're doing. Chesterton's 434 | * fence rule applies here. 435 | */ 436 | private apply(e: Element | ShadowRoot, params: Raw.Param[]) 437 | { 438 | for (let i = -1, length = params.length; ++i < length;) 439 | { 440 | const param = params[i]; 441 | if (!param) 442 | continue; 443 | 444 | if (Raw.is.node(param)) 445 | { 446 | e.append(param); 447 | } 448 | else if (Array.isArray(param)) 449 | { 450 | this.apply(e, param); 451 | } 452 | else switch (param.constructor) 453 | { 454 | case Raw.PortableEvent: 455 | { 456 | if (e) 457 | { 458 | const he = param as Raw.PortableEvent; 459 | if (he.target) 460 | he.host = e; 461 | else 462 | { 463 | e.addEventListener(he.type, he.handler, he.options); 464 | 465 | if (he.type === "connected" || he.type === "rendered") 466 | this.awaitingConnection.push([e, he.type === "rendered"]); 467 | } 468 | } 469 | } 470 | break; case String: 471 | { 472 | // Note that ShadowRoots cannot accept string parameters. 473 | const cls = param as string; 474 | (e as Element).classList.add(...cls.split(/\s+/g).filter(s => s)); 475 | 476 | if (cls.indexOf(Raw.GeneratedClassPrefix.value) === 0) 477 | { 478 | const maybeShadow = e.getRootNode(); 479 | if (Raw.is.shadow(maybeShadow)) 480 | this.toShadow(maybeShadow, cls); 481 | } 482 | } 483 | break; case Object: 484 | { 485 | const el = e as any; 486 | 487 | for (const [name, value] of Object.entries(param)) 488 | { 489 | // JavaScript numbers that are specified in the width and height properties 490 | // are injected as HTML attributes rather than assigned as CSS properties. 491 | if (value && 492 | (name === "width" || name === "height") && typeof value === "number" || 493 | (e as Element).tagName === "META") 494 | { 495 | (e as Element).setAttribute(name, value.toString()); 496 | } 497 | else if (name === "data") 498 | { 499 | for (const [attrName, attrValue] of Object.entries(value || {})) 500 | (e as Element).setAttribute("data-" + attrName, String(attrValue)); 501 | } 502 | // Add support for { class: "class names here" } 503 | // Because strings are interpreted as strings, this should only ever come up 504 | // when using JSX to construct elements such as:
505 | else if (name === "class") 506 | { 507 | (e as Element).classList.add(...value.split(/\s+/g)); 508 | } 509 | // Width, height, and background properties are special cased. 510 | // They are interpreted as CSS properties rather than HTML attributes. 511 | else if (name in e && 512 | name !== "background" && 513 | name !== "width" && 514 | name !== "height") 515 | { 516 | // Some attributes can't be assigned with keyed property access, 517 | // at least in Chromium-based browsers (HTMLVideoElement.muted). 518 | // So here, we're assigning both the JavaScript property and calling 519 | // the setAttribute() function to ensure that the attribute always 520 | // shows up in the Element.getAttributes() list. 521 | el[name] = value; 522 | (e as Element).setAttribute(name, value); 523 | } 524 | else if (Raw.is.property(name)) 525 | { 526 | this.setProperty(el, name, value); 527 | } 528 | } 529 | } 530 | break; case Function: 531 | { 532 | if (Raw.is.element(e) || Raw.is.shadow(e)) 533 | { 534 | const fn = param as Function; 535 | const subParams = fn(e); 536 | 537 | if (subParams) 538 | this.apply(e, Array.isArray(subParams) ? subParams : [subParams]); 539 | } 540 | } 541 | default: 542 | { 543 | // Ugly, but high-performance way to check if the param is a Hat 544 | // (meaning, an object with a .head HTMLElement property) coming 545 | // from the Hat library. 546 | if (!!(param as any).head && (param as any).head.ELEMENT_NODE === 1) 547 | { 548 | this.apply(e, [(param as any).head]); 549 | } 550 | else if (typeof param === "function" && param.constructor.name === "AsyncFunction") 551 | { 552 | this.apply(e, (param as Function)(e)); 553 | } 554 | } 555 | } 556 | } 557 | 558 | return e; 559 | } 560 | 561 | //# Event Related 562 | 563 | /** */ 564 | static readonly PortableEvent = class PortableEvent 565 | { 566 | /** */ 567 | constructor( 568 | readonly target: Node | null, 569 | readonly type: string, 570 | readonly handler: (ev: globalThis.Event) => void, 571 | readonly options: Readonly = {}) 572 | { } 573 | 574 | /** 575 | * Stores the element that "hosts" the event, which is not necessarily 576 | * the target event. When the host element is removed from the DOM, 577 | * the event handler is removed. 578 | */ 579 | host: Element | ShadowRoot | null = null; 580 | } 581 | 582 | /** */ 583 | on( 584 | type: K, 585 | listener: (this: HTMLElement, ev: Raw.EventMap[K]) => any, 586 | options?: boolean | EventListenerOptions): Raw.PortableEvent; 587 | /** */ 588 | on( 589 | remoteTarget: Node | Window, 590 | type: K, 591 | listener: (this: HTMLElement, ev: Raw.EventMap[K]) => any, 592 | options?: boolean | EventListenerOptions): Raw.PortableEvent; 593 | /** */ 594 | on( 595 | remoteTarget: Window, 596 | type: K, 597 | listener: (this: Window, ev: WindowEventMap[K]) => any, 598 | options?: boolean | EventListenerOptions): Raw.PortableEvent; 599 | /** */ 600 | on(...args: any[]) 601 | { 602 | const target: Node | null = typeof args[0] === "string" ? null : args[0]; 603 | const type: string = typeof args[0] === "string" ? args[0] : args[1]; 604 | const handler = typeof args[1] === "function" ? args[1] : args[2]; 605 | const last = args.pop(); 606 | const options: AddEventListenerOptions = typeof last === "function" ? {} : last; 607 | 608 | if (type === "connected" || type === "disconnected") 609 | { 610 | this.maybeInstallRootObserver(); 611 | options.once = true; 612 | } 613 | 614 | const hev = new Raw.PortableEvent(target, type, handler, options); 615 | 616 | // If the event has a defined target, then add the event listener right away, 617 | // and the apply() function will assign any host element, if present. 618 | if (target) 619 | { 620 | let handler: (ev: Event) => void; 621 | target.addEventListener(hev.type, handler = (ev: Event) => 622 | { 623 | if (hev.host?.isConnected !== false) 624 | hev.handler(ev as any); 625 | else 626 | target.removeEventListener(hev.type, handler); 627 | }, 628 | options); 629 | } 630 | 631 | return hev; 632 | } 633 | 634 | //# Connection Events 635 | 636 | /** */ 637 | private maybeInstallRootObserver() 638 | { 639 | if (this.hasInstalledRootObserver || typeof MutationObserver === "undefined") 640 | return; 641 | 642 | this.hasInstalledRootObserver = true; 643 | 644 | new MutationObserver(() => 645 | { 646 | const invokations: [Element | ShadowRoot, boolean][] = []; 647 | 648 | for (let i = this.awaitingConnection.length; i-- > 0;) 649 | { 650 | const tuple = this.awaitingConnection[i]; 651 | if (!tuple[0].isConnected) 652 | continue; 653 | 654 | this.awaitingConnection.splice(i, 1); 655 | invokations.push(tuple); 656 | } 657 | 658 | // Run the callbacks in a separate pass, to deal with the fact that 659 | // there could be multiple awaiters watching the same element, 660 | // but also to handle the fact the callback functions could modify 661 | // the awaiting list. 662 | for (const [element, defer] of invokations) 663 | { 664 | const event = new Event("connected", { 665 | bubbles: true, 666 | cancelable: true, 667 | }); 668 | 669 | if (defer) 670 | setTimeout(() => element.dispatchEvent(event), 1); 671 | else 672 | element.dispatchEvent(event); 673 | } 674 | 675 | }).observe(this.doc.body, { childList: true, subtree: true }); 676 | } 677 | 678 | private hasInstalledRootObserver = false; 679 | private readonly awaitingConnection: [Element | ShadowRoot, boolean][] = []; 680 | 681 | //# Style Related 682 | 683 | /** 684 | * Creates an HTML