Current section

26 Versions

Jump to

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…