Current section
26 Versions
Jump to
Current section
26 Versions
Compare versions
33
files changed
+1713
additions
-450
deletions
| @@ -3,6 +3,10 @@ exported_locals_without_parens = [ | |
| 3 3 | prop: 2, |
| 4 4 | prop: 3, |
| 5 5 | |
| 6 | + # Hologram.JS |
| 7 | + js_import: 1, |
| 8 | + js_import: 2, |
| 9 | + |
| 6 10 | # Hologram.Page |
| 7 11 | layout: 1, |
| 8 12 | layout: 2, |
| @@ -1,6 +1,6 @@ | |
| 1 1 | # Hologram |
| 2 2 | |
| 3 | - Build rich, interactive UIs entirely in Elixir using Hologram's declarative component system. Your client-side code is intelligently transpiled to JavaScript, providing modern frontend capabilities without relying on any JavaScript frameworks. |
| 3 | + Build rich, interactive UIs entirely in Elixir using Hologram's declarative component system. Your client-side code is intelligently compiled to JavaScript, providing modern frontend capabilities without relying on any JavaScript frameworks. |
| 4 4 | |
| 5 5 | Website: https://hologram.page |
| 6 6 | |
| @@ -37,7 +37,6 @@ Website: https://hologram.page | |
| 37 37 | ### Framework Visionary Tier |
| 38 38 | |
| 39 39 | * [@absowoot](https://github.com/absowoot) |
| 40 | - * Uzair Aslam, [@uzairaslam196](https://github.com/uzairaslam196) |
| 41 40 | * Oban, [@oban-bg](https://github.com/oban-bg) |
| 42 41 | * Lucas Sifoni, [@Lucassifoni](https://github.com/Lucassifoni) |
| 43 42 | * Robert Urbańczyk, [@robertu](https://github.com/robertu) |
| @@ -45,16 +44,10 @@ Website: https://hologram.page | |
| 45 44 | ### Early Adopter Tier |
| 46 45 | |
| 47 46 | * Daniel Calancea, [@D4no0](https://github.com/D4no0) |
| 48 | - * Darren Black, [@totaltrash](https://github.com/totaltrash) |
| 49 | - * [@jzimmek](https://github.com/jzimmek) |
| 50 | - * Nicholas Moen, [@arcanemachine](https://github.com/arcanemachine) |
| 51 47 | * [@pasila](https://github.com/pasila) |
| 52 48 | * Jonatan Männchen, [@maennchen](https://github.com/maennchen) |
| 53 | - * Douglas Manzelmann, [@douglasmanzelmann](https://github.com/douglasmanzelmann) |
| 54 49 | * James Harton, [@jimsynz](https://github.com/jimsynz) |
| 55 50 | * Ian Asaff, [@montague](https://github.com/montague) |
| 56 | - * Juozas Norkus, [@jozuas](https://github.com/jozuas) |
| 57 | - * Thomas, [@thomaswhyyou](https://github.com/thomaswhyyou) |
| 58 51 | * Max, [@Makesesama](https://github.com/Makesesama) |
| 59 52 | * Dawid Danieluk, [@nxy7](https://github.com/nxy7) |
| @@ -247,6 +247,27 @@ export default class Bitstring { | |
| 247 247 | } |
| 248 248 | } |
| 249 249 | |
| 250 | + // Decodes a UTF-8 sequence starting at the given position. |
| 251 | + // Returns the decoded Unicode code point value. |
| 252 | + // bytes: Uint8Array containing the UTF-8 encoded data |
| 253 | + // start: byte index where the sequence begins |
| 254 | + // length: number of bytes in the UTF-8 sequence (1-4) |
| 255 | + static decodeUtf8CodePoint(bytes, start, length) { |
| 256 | + if (length === 1) return bytes[start]; |
| 257 | + |
| 258 | + // First byte masks: 2-byte=0x1f, 3-byte=0x0f, 4-byte=0x07 |
| 259 | + const firstByteMasks = {2: 0x1f, 3: 0x0f, 4: 0x07}; |
| 260 | + |
| 261 | + let codePoint = bytes[start] & firstByteMasks[length]; |
| 262 | + |
| 263 | + // Process continuation bytes (all use 0x3f mask, shift by 6 each) |
| 264 | + for (let i = 1; i < length; i++) { |
| 265 | + codePoint = (codePoint << 6) | (bytes[start + i] & 0x3f); |
| 266 | + } |
| 267 | + |
| 268 | + return codePoint; |
| 269 | + } |
| 270 | + |
| 250 271 | static fromBits(bits) { |
| 251 272 | const bitCount = bits.length; |
| 252 273 | const byteCount = Math.ceil(bitCount / 8); |
| @@ -478,6 +499,31 @@ export default class Bitstring { | |
| 478 499 | }; |
| 479 500 | } |
| 480 501 | |
| 502 | + // Determines the expected UTF-8 sequence length from the leader byte. |
| 503 | + // Returns false for invalid leader bytes (e.g., 0xC0, 0xC1, 0xF5+). |
| 504 | + static getUtf8SequenceLength(leaderByte) { |
| 505 | + // 0xxxxxxx: ASCII |
| 506 | + if ((leaderByte & 0x80) === 0) return 1; |
| 507 | + |
| 508 | + // Invalid: overlong encoding |
| 509 | + if (leaderByte === 0xc0 || leaderByte === 0xc1) return false; |
| 510 | + |
| 511 | + // 110xxxxx: 2-byte |
| 512 | + if ((leaderByte & 0xe0) === 0xc0) return 2; |
| 513 | + |
| 514 | + // 1110xxxx: 3-byte |
| 515 | + if ((leaderByte & 0xf0) === 0xe0) return 3; |
| 516 | + |
| 517 | + // Invalid: > U+10FFFF |
| 518 | + if (leaderByte >= 0xf5) return false; |
| 519 | + |
| 520 | + // 11110xxx: 4-byte |
| 521 | + if ((leaderByte & 0xf8) === 0xf0) return 4; |
| 522 | + |
| 523 | + // Invalid leader byte |
| 524 | + return false; |
| 525 | + } |
| 526 | + |
| 481 527 | static isEmpty(bitstring) { |
| 482 528 | return bitstring.text === "" || bitstring.bytes?.length === 0; |
| 483 529 | } |
| @@ -134,7 +134,7 @@ export default class Client { | |
| 134 134 | method: "POST", |
| 135 135 | headers: { |
| 136 136 | "Content-Type": "application/json", |
| 137 | - "X-Csrf-Token": globalThis.hologram.csrfToken, |
| 137 | + "X-Csrf-Token": globalThis.Hologram.csrfToken, |
| 138 138 | }, |
| 139 139 | body: Serializer.serialize($.buildCommandPayload(command), "server"), |
| 140 140 | }; |
| @@ -1,12 +1,207 @@ | |
| 1 1 | "use strict"; |
| 2 2 | |
| 3 | + import {box} from "../../js_interop.mjs"; |
| 4 | + |
| 3 5 | import Bitstring from "../../bitstring.mjs"; |
| 6 | + import ERTS from "../../erts.mjs"; |
| 4 7 | import Interpreter from "../../interpreter.mjs"; |
| 8 | + import Type from "../../type.mjs"; |
| 9 | + |
| 10 | + const MAX_SAFE_BIGINT = BigInt(Number.MAX_SAFE_INTEGER); |
| 11 | + const MIN_SAFE_BIGINT = BigInt(Number.MIN_SAFE_INTEGER); |
| 12 | + |
| 13 | + function resolveBinding(term, callerModule) { |
| 14 | + if (term.type === "atom") { |
| 15 | + const name = term.value; |
| 16 | + const moduleProxy = Interpreter.moduleProxy(callerModule); |
| 17 | + |
| 18 | + if (moduleProxy.__jsBindings__.has(name)) { |
| 19 | + return moduleProxy.__jsBindings__.get(name); |
| 20 | + } |
| 21 | + |
| 22 | + return globalThis[name]; |
| 23 | + } |
| 24 | + |
| 25 | + if (Type.isNativeValueStruct(term)) { |
| 26 | + return unboxNativeValue(term); |
| 27 | + } |
| 28 | + |
| 29 | + return unbox(term, callerModule); |
| 30 | + } |
| 31 | + |
| 32 | + function unbox(term, callerModule) { |
| 33 | + switch (term.type) { |
| 34 | + case "anonymous_function": |
| 35 | + return (...jsArgs) => { |
| 36 | + const boxedArgs = jsArgs.slice(0, term.arity).map(box); |
| 37 | + const result = Interpreter.callAnonymousFunction(term, boxedArgs); |
| 38 | + |
| 39 | + return unbox(result, callerModule); |
| 40 | + }; |
| 41 | + |
| 42 | + case "atom": |
| 43 | + if (term.value === "true") return true; |
| 44 | + if (term.value === "false") return false; |
| 45 | + if (term.value === "nil") return null; |
| 46 | + |
| 47 | + { |
| 48 | + const name = term.value; |
| 49 | + const moduleProxy = Interpreter.moduleProxy(callerModule); |
| 50 | + |
| 51 | + if (moduleProxy.__jsBindings__.has(name)) { |
| 52 | + return moduleProxy.__jsBindings__.get(name); |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + return term.value in globalThis ? globalThis[term.value] : term.value; |
| 57 | + |
| 58 | + case "bitstring": |
| 59 | + return Bitstring.toText(term); |
| 60 | + |
| 61 | + case "float": |
| 62 | + return term.value; |
| 63 | + |
| 64 | + case "integer": |
| 65 | + if (term.value >= MIN_SAFE_BIGINT && term.value <= MAX_SAFE_BIGINT) { |
| 66 | + return Number(term.value); |
| 67 | + } |
| 68 | + |
| 69 | + return term.value; |
| 70 | + |
| 71 | + case "list": |
| 72 | + return term.data.map((item) => unbox(item, callerModule)); |
| 73 | + |
| 74 | + case "map": |
| 75 | + if (Type.isNativeValueStruct(term)) { |
| 76 | + return unboxNativeValue(term); |
| 77 | + } |
| 78 | + |
| 79 | + { |
| 80 | + const obj = {}; |
| 81 | + |
| 82 | + for (const [_encodedKey, [key, value]] of Object.entries(term.data)) { |
| 83 | + const jsKey = |
| 84 | + key.type === "atom" ? key.value : unbox(key, callerModule); |
| 85 | + |
| 86 | + Object.defineProperty(obj, jsKey, { |
| 87 | + value: unbox(value, callerModule), |
| 88 | + writable: true, |
| 89 | + enumerable: true, |
| 90 | + configurable: true, |
| 91 | + }); |
| 92 | + } |
| 93 | + |
| 94 | + return obj; |
| 95 | + } |
| 96 | + |
| 97 | + case "tuple": |
| 98 | + return term.data.map((item) => unbox(item, callerModule)); |
| 99 | + |
| 100 | + default: |
| 101 | + return term; |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + function unboxNativeValue(term) { |
| 106 | + const typeKey = Type.encodeMapKey(Type.atom("type")); |
| 107 | + const jsType = term.data[typeKey][1].value; |
| 108 | + |
| 109 | + const valueKey = Type.encodeMapKey(Type.atom("value")); |
| 110 | + const boxedValue = term.data[valueKey][1]; |
| 111 | + |
| 112 | + switch (jsType) { |
| 113 | + case "bigint": |
| 114 | + return boxedValue.value; |
| 115 | + |
| 116 | + case "function": |
| 117 | + case "object": |
| 118 | + case "symbol": |
| 119 | + return ERTS.nativeObjectRegistry.get(boxedValue); |
| 120 | + |
| 121 | + case "undefined": |
| 122 | + return undefined; |
| 123 | + } |
| 124 | + } |
| 5 125 | |
| 6 126 | const Elixir_Hologram_JS = { |
| 127 | + "call/4": (receiver, methodOrFunction, args, callerModule) => { |
| 128 | + let jsFunction; |
| 129 | + |
| 130 | + if (Type.isNil(receiver)) { |
| 131 | + jsFunction = resolveBinding(methodOrFunction, callerModule); |
| 132 | + } else { |
| 133 | + const jsReceiver = resolveBinding(receiver, callerModule); |
| 134 | + jsFunction = jsReceiver[methodOrFunction.value].bind(jsReceiver); |
| 135 | + } |
| 136 | + |
| 137 | + return box(jsFunction(...unbox(args, callerModule))); |
| 138 | + }, |
| 139 | + |
| 140 | + "delete/3": (receiver, property, callerModule) => { |
| 141 | + const jsReceiver = resolveBinding(receiver, callerModule); |
| 142 | + const jsPropertyName = property.value; |
| 143 | + |
| 144 | + delete jsReceiver[jsPropertyName]; |
| 145 | + |
| 146 | + return receiver; |
| 147 | + }, |
| 148 | + |
| 149 | + "dispatch_event/5": (target, eventType, eventName, opts, callerModule) => { |
| 150 | + const jsTarget = resolveBinding(target, callerModule); |
| 151 | + const EventClass = resolveBinding(eventType, callerModule); |
| 152 | + const jsEventName = Bitstring.toText(eventName); |
| 153 | + const jsOpts = unbox(opts, callerModule); |
| 154 | + |
| 155 | + return box(jsTarget.dispatchEvent(new EventClass(jsEventName, jsOpts))); |
| 156 | + }, |
| 157 | + |
| 158 | + "eval/1": (expression) => { |
| 159 | + return box( |
| 160 | + Interpreter.evaluateJavaScriptCode( |
| 161 | + "return (" + Bitstring.toText(expression) + ")", |
| 162 | + ), |
| 163 | + ); |
| 164 | + }, |
| 165 | + |
| 7 166 | "exec/1": (code) => { |
| 8 | - return Interpreter.evaluateJavaScriptCode(Bitstring.toText(code)); |
| 167 | + return box(Interpreter.evaluateJavaScriptCode(Bitstring.toText(code))); |
| 168 | + }, |
| 169 | + |
| 170 | + "get/3": (receiver, property, callerModule) => { |
| 171 | + const jsReceiver = resolveBinding(receiver, callerModule); |
| 172 | + const jsPropertyName = property.value; |
| 173 | + |
| 174 | + return box(jsReceiver[jsPropertyName]); |
| 175 | + }, |
| 176 | + |
| 177 | + "instanceof/3": (value, className, callerModule) => { |
| 178 | + const jsValue = resolveBinding(value, callerModule); |
| 179 | + const jsClass = resolveBinding(className, callerModule); |
| 180 | + |
| 181 | + return box(jsValue instanceof jsClass); |
| 182 | + }, |
| 183 | + |
| 184 | + "new/3": (className, args, callerModule) => { |
| 185 | + const jsClass = resolveBinding(className, callerModule); |
| 186 | + |
| 187 | + return box(new jsClass(...unbox(args, callerModule))); |
| 188 | + }, |
| 189 | + |
| 190 | + "set/4": (receiver, property, value, callerModule) => { |
| 191 | + const jsReceiver = resolveBinding(receiver, callerModule); |
| 192 | + const jsPropertyName = property.value; |
| 193 | + |
| 194 | + jsReceiver[jsPropertyName] = unbox(value, callerModule); |
| 195 | + |
| 196 | + return receiver; |
| 197 | + }, |
| 198 | + |
| 199 | + "typeof/2": (value, callerModule) => { |
| 200 | + const jsValue = unbox(value, callerModule); |
| 201 | + |
| 202 | + return box(typeof jsValue); |
| 9 203 | }, |
| 10 204 | }; |
| 11 205 | |
| 206 | + export {resolveBinding, unbox}; |
| 12 207 | export default Elixir_Hologram_JS; |
Loading more files…