xote 4.3.0 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,305 @@
1
+ open Signals
2
+ module Component = Xote__Component
3
+
4
+ /* ReScript JSX transform type aliases */
5
+ type element = Component.node
6
+
7
+ type component<'props> = 'props => element
8
+
9
+ type componentLike<'props, 'return> = 'props => 'return
10
+
11
+ /* JSX functions for component creation */
12
+ let jsx = (component: component<'props>, props: 'props): element => component(props)
13
+
14
+ let jsxs = (component: component<'props>, props: 'props): element => component(props)
15
+
16
+ let jsxKeyed = (
17
+ component: component<'props>,
18
+ props: 'props,
19
+ ~key: option<string>=?,
20
+ _: unit,
21
+ ): element => {
22
+ let _ = key /* TODO: Implement key support for list reconciliation */
23
+ component(props)
24
+ }
25
+
26
+ let jsxsKeyed = (
27
+ component: component<'props>,
28
+ props: 'props,
29
+ ~key: option<string>=?,
30
+ _: unit,
31
+ ): element => {
32
+ let _ = key
33
+ component(props)
34
+ }
35
+
36
+ /* Fragment support */
37
+ type fragmentProps = {children?: element}
38
+
39
+ let jsxFragment = (props: fragmentProps): element => {
40
+ switch props.children {
41
+ | Some(child) => child
42
+ | None => Component.fragment([])
43
+ }
44
+ }
45
+
46
+ /* Element converters for JSX expressions */
47
+ let array = (children: array<element>): element => Component.fragment(children)
48
+
49
+ let null = (): element => Component.text("")
50
+
51
+ /* Elements module for lowercase HTML tags */
52
+ module Elements = {
53
+ /* Attribute value type that can be static, signal, or computed */
54
+ @unboxed
55
+ type rec attributeValue = Any('a): attributeValue
56
+
57
+ /* Automatic conversion from string to attributeValue */
58
+ external fromString: string => attributeValue = "%identity"
59
+
60
+ /* Helper to convert a signal to an attributeValue */
61
+ let signal = (s: Signals.Signal.t<string>): attributeValue => Any(s)
62
+
63
+ /* Helper to convert a computed function to an attributeValue */
64
+ let computed = (f: unit => string): attributeValue => Any(f)
65
+
66
+ /* Props type for HTML elements - supports common attributes and events */
67
+ type props<'id, 'class, 'style, 'typ, 'value, 'placeholder, 'href, 'target, 'data> = {
68
+ /* Standard attributes - can be static strings or reactive values */
69
+ id?: 'id,
70
+ class?: 'class,
71
+ style?: 'style,
72
+ /* Input attributes */
73
+ @as("type") type_?: 'typ,
74
+ value?: 'value,
75
+ placeholder?: 'placeholder,
76
+ disabled?: bool,
77
+ checked?: bool,
78
+ /* Link attributes */
79
+ href?: 'href,
80
+ target?: 'target,
81
+ /* Data attributes */
82
+ data?: 'data,
83
+ /* Event handlers */
84
+ onClick?: Dom.event => unit,
85
+ onInput?: Dom.event => unit,
86
+ onChange?: Dom.event => unit,
87
+ onSubmit?: Dom.event => unit,
88
+ onFocus?: Dom.event => unit,
89
+ onBlur?: Dom.event => unit,
90
+ onKeyDown?: Dom.event => unit,
91
+ onKeyUp?: Dom.event => unit,
92
+ onMouseEnter?: Dom.event => unit,
93
+ onMouseLeave?: Dom.event => unit,
94
+ /* Children */
95
+ children?: element,
96
+ }
97
+
98
+ /* Helper to detect if a value is a signal (has an id property) */
99
+ @get external hasId: 'a => option<int> = "id"
100
+
101
+ /* Helper to convert any value to Component.attrValue */
102
+ let convertAttrValue = (key: string, value: 'a): (string, Component.attrValue) => {
103
+ // Check if it's a function (computed)
104
+ if typeof(value) == #function {
105
+ // It's a computed function
106
+ let f: unit => string = Obj.magic(value)
107
+ Component.computedAttr(key, f)
108
+ } else if typeof(value) == #object && hasId(value)->Option.isSome {
109
+ // It's a signal (has an id property)
110
+ let sig: Signal.t<string> = Obj.magic(value)
111
+ Component.signalAttr(key, sig)
112
+ } else {
113
+ // It's a static string
114
+ let s: string = Obj.magic(value)
115
+ Component.attr(key, s)
116
+ }
117
+ }
118
+
119
+ /* Convert props to attrs array */
120
+ let propsToAttrs = (
121
+ props: props<'id, 'class, 'style, 'typ, 'value, 'placeholder, 'href, 'target, 'data>,
122
+ ): array<(string, Component.attrValue)> => {
123
+ let attrs = []
124
+
125
+ switch props.id {
126
+ | Some(v) => attrs->Array.push(convertAttrValue("id", v))
127
+ | None => ()
128
+ }
129
+
130
+ switch props.class {
131
+ | Some(v) => attrs->Array.push(convertAttrValue("class", v))
132
+ | None => ()
133
+ }
134
+
135
+ switch props.style {
136
+ | Some(v) => attrs->Array.push(convertAttrValue("style", v))
137
+ | None => ()
138
+ }
139
+
140
+ switch props.type_ {
141
+ | Some(v) => attrs->Array.push(convertAttrValue("type", v))
142
+ | None => ()
143
+ }
144
+
145
+ switch props.value {
146
+ | Some(v) => attrs->Array.push(convertAttrValue("value", v))
147
+ | None => ()
148
+ }
149
+
150
+ switch props.placeholder {
151
+ | Some(v) => attrs->Array.push(convertAttrValue("placeholder", v))
152
+ | None => ()
153
+ }
154
+
155
+ switch props.disabled {
156
+ | Some(true) => attrs->Array.push(Component.attr("disabled", "true"))
157
+ | _ => ()
158
+ }
159
+
160
+ switch props.checked {
161
+ | Some(true) => attrs->Array.push(Component.attr("checked", "true"))
162
+ | _ => ()
163
+ }
164
+
165
+ switch props.href {
166
+ | Some(v) => attrs->Array.push(convertAttrValue("href", v))
167
+ | None => ()
168
+ }
169
+
170
+ switch props.target {
171
+ | Some(v) => attrs->Array.push(convertAttrValue("target", v))
172
+ | None => ()
173
+ }
174
+
175
+ switch props.data {
176
+ | Some(_dataObj) => {
177
+ let _ = %raw(`
178
+ Object.entries(_dataObj).forEach(([key, value]) => {
179
+ attrs.push(convertAttrValue("data-" + key, value))
180
+ })
181
+ `)
182
+ }
183
+ | None => ()
184
+ }
185
+
186
+ attrs
187
+ }
188
+
189
+ /* Convert props to events array */
190
+ let propsToEvents = (
191
+ props: props<'id, 'class, 'style, 'typ, 'value, 'placeholder, 'href, 'target, 'data>,
192
+ ): array<(string, Dom.event => unit)> => {
193
+ let events = []
194
+
195
+ switch props.onClick {
196
+ | Some(handler) => events->Array.push(("click", handler))
197
+ | None => ()
198
+ }
199
+
200
+ switch props.onInput {
201
+ | Some(handler) => events->Array.push(("input", handler))
202
+ | None => ()
203
+ }
204
+
205
+ switch props.onChange {
206
+ | Some(handler) => events->Array.push(("change", handler))
207
+ | None => ()
208
+ }
209
+
210
+ switch props.onSubmit {
211
+ | Some(handler) => events->Array.push(("submit", handler))
212
+ | None => ()
213
+ }
214
+
215
+ switch props.onFocus {
216
+ | Some(handler) => events->Array.push(("focus", handler))
217
+ | None => ()
218
+ }
219
+
220
+ switch props.onBlur {
221
+ | Some(handler) => events->Array.push(("blur", handler))
222
+ | None => ()
223
+ }
224
+
225
+ switch props.onKeyDown {
226
+ | Some(handler) => events->Array.push(("keydown", handler))
227
+ | None => ()
228
+ }
229
+
230
+ switch props.onKeyUp {
231
+ | Some(handler) => events->Array.push(("keyup", handler))
232
+ | None => ()
233
+ }
234
+
235
+ switch props.onMouseEnter {
236
+ | Some(handler) => events->Array.push(("mouseenter", handler))
237
+ | None => ()
238
+ }
239
+
240
+ switch props.onMouseLeave {
241
+ | Some(handler) => events->Array.push(("mouseleave", handler))
242
+ | None => ()
243
+ }
244
+
245
+ events
246
+ }
247
+
248
+ /* Extract children from props */
249
+ let getChildren = (
250
+ props: props<'id, 'class, 'style, 'typ, 'value, 'placeholder, 'href, 'target, 'data>,
251
+ ): array<element> => {
252
+ switch props.children {
253
+ | Some(Fragment(children)) => children
254
+ | Some(child) => [child]
255
+ | None => []
256
+ }
257
+ }
258
+
259
+ /* Create an element from a tag string and props */
260
+ let createElement = (
261
+ tag: string,
262
+ props: props<'id, 'class, 'style, 'typ, 'value, 'placeholder, 'href, 'target, 'data>,
263
+ ): element => {
264
+ Component.Element({
265
+ tag,
266
+ attrs: propsToAttrs(props),
267
+ events: propsToEvents(props),
268
+ children: getChildren(props),
269
+ })
270
+ }
271
+
272
+ /* JSX functions for HTML elements */
273
+ let jsx = (
274
+ tag: string,
275
+ props: props<'id, 'class, 'style, 'typ, 'value, 'placeholder, 'href, 'target, 'data>,
276
+ ): element => createElement(tag, props)
277
+
278
+ let jsxs = (
279
+ tag: string,
280
+ props: props<'id, 'class, 'style, 'typ, 'value, 'placeholder, 'href, 'target, 'data>,
281
+ ): element => createElement(tag, props)
282
+
283
+ let jsxKeyed = (
284
+ tag: string,
285
+ props: props<'id, 'class, 'style, 'typ, 'value, 'placeholder, 'href, 'target, 'data>,
286
+ ~key: option<string>=?,
287
+ _: unit,
288
+ ): element => {
289
+ let _ = key
290
+ createElement(tag, props)
291
+ }
292
+
293
+ let jsxsKeyed = (
294
+ tag: string,
295
+ props: props<'id, 'class, 'style, 'typ, 'value, 'placeholder, 'href, 'target, 'data>,
296
+ ~key: option<string>=?,
297
+ _: unit,
298
+ ): element => {
299
+ let _ = key
300
+ createElement(tag, props)
301
+ }
302
+
303
+ /* Element helper for ReScript JSX type checking */
304
+ external someElement: element => option<element> = "%identity"
305
+ }
@@ -0,0 +1,244 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Core__Option from "@rescript/core/src/Core__Option.res.mjs";
4
+ import * as Xote__Component from "./Xote__Component.res.mjs";
5
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
6
+
7
+ function jsx(component, props) {
8
+ return component(props);
9
+ }
10
+
11
+ function jsxs(component, props) {
12
+ return component(props);
13
+ }
14
+
15
+ function jsxKeyed(component, props, key, param) {
16
+ return component(props);
17
+ }
18
+
19
+ function jsxsKeyed(component, props, key, param) {
20
+ return component(props);
21
+ }
22
+
23
+ function jsxFragment(props) {
24
+ let child = props.children;
25
+ if (child !== undefined) {
26
+ return child;
27
+ } else {
28
+ return Xote__Component.fragment([]);
29
+ }
30
+ }
31
+
32
+ let array = Xote__Component.fragment;
33
+
34
+ function $$null() {
35
+ return Xote__Component.text("");
36
+ }
37
+
38
+ function signal(s) {
39
+ return s;
40
+ }
41
+
42
+ function computed(f) {
43
+ return f;
44
+ }
45
+
46
+ function convertAttrValue(key, value) {
47
+ if (typeof value === "function") {
48
+ return Xote__Component.computedAttr(key, value);
49
+ } else if (typeof value === "object" && Core__Option.isSome(value.id)) {
50
+ return Xote__Component.signalAttr(key, value);
51
+ } else {
52
+ return Xote__Component.attr(key, value);
53
+ }
54
+ }
55
+
56
+ function propsToAttrs(props) {
57
+ let attrs = [];
58
+ let v = props.id;
59
+ if (v !== undefined) {
60
+ attrs.push(convertAttrValue("id", Primitive_option.valFromOption(v)));
61
+ }
62
+ let v$1 = props.class;
63
+ if (v$1 !== undefined) {
64
+ attrs.push(convertAttrValue("class", Primitive_option.valFromOption(v$1)));
65
+ }
66
+ let v$2 = props.style;
67
+ if (v$2 !== undefined) {
68
+ attrs.push(convertAttrValue("style", Primitive_option.valFromOption(v$2)));
69
+ }
70
+ let v$3 = props.type;
71
+ if (v$3 !== undefined) {
72
+ attrs.push(convertAttrValue("type", Primitive_option.valFromOption(v$3)));
73
+ }
74
+ let v$4 = props.value;
75
+ if (v$4 !== undefined) {
76
+ attrs.push(convertAttrValue("value", Primitive_option.valFromOption(v$4)));
77
+ }
78
+ let v$5 = props.placeholder;
79
+ if (v$5 !== undefined) {
80
+ attrs.push(convertAttrValue("placeholder", Primitive_option.valFromOption(v$5)));
81
+ }
82
+ let match = props.disabled;
83
+ if (match !== undefined && match) {
84
+ attrs.push(Xote__Component.attr("disabled", "true"));
85
+ }
86
+ let match$1 = props.checked;
87
+ if (match$1 !== undefined && match$1) {
88
+ attrs.push(Xote__Component.attr("checked", "true"));
89
+ }
90
+ let v$6 = props.href;
91
+ if (v$6 !== undefined) {
92
+ attrs.push(convertAttrValue("href", Primitive_option.valFromOption(v$6)));
93
+ }
94
+ let v$7 = props.target;
95
+ if (v$7 !== undefined) {
96
+ attrs.push(convertAttrValue("target", Primitive_option.valFromOption(v$7)));
97
+ }
98
+ let _dataObj = props.data;
99
+ if (_dataObj !== undefined) {
100
+ ((Object.entries(_dataObj).forEach(([key, value]) => {
101
+ attrs.push(convertAttrValue("data-" + key, value))
102
+ })));
103
+ }
104
+ return attrs;
105
+ }
106
+
107
+ function propsToEvents(props) {
108
+ let events = [];
109
+ let handler = props.onClick;
110
+ if (handler !== undefined) {
111
+ events.push([
112
+ "click",
113
+ handler
114
+ ]);
115
+ }
116
+ let handler$1 = props.onInput;
117
+ if (handler$1 !== undefined) {
118
+ events.push([
119
+ "input",
120
+ handler$1
121
+ ]);
122
+ }
123
+ let handler$2 = props.onChange;
124
+ if (handler$2 !== undefined) {
125
+ events.push([
126
+ "change",
127
+ handler$2
128
+ ]);
129
+ }
130
+ let handler$3 = props.onSubmit;
131
+ if (handler$3 !== undefined) {
132
+ events.push([
133
+ "submit",
134
+ handler$3
135
+ ]);
136
+ }
137
+ let handler$4 = props.onFocus;
138
+ if (handler$4 !== undefined) {
139
+ events.push([
140
+ "focus",
141
+ handler$4
142
+ ]);
143
+ }
144
+ let handler$5 = props.onBlur;
145
+ if (handler$5 !== undefined) {
146
+ events.push([
147
+ "blur",
148
+ handler$5
149
+ ]);
150
+ }
151
+ let handler$6 = props.onKeyDown;
152
+ if (handler$6 !== undefined) {
153
+ events.push([
154
+ "keydown",
155
+ handler$6
156
+ ]);
157
+ }
158
+ let handler$7 = props.onKeyUp;
159
+ if (handler$7 !== undefined) {
160
+ events.push([
161
+ "keyup",
162
+ handler$7
163
+ ]);
164
+ }
165
+ let handler$8 = props.onMouseEnter;
166
+ if (handler$8 !== undefined) {
167
+ events.push([
168
+ "mouseenter",
169
+ handler$8
170
+ ]);
171
+ }
172
+ let handler$9 = props.onMouseLeave;
173
+ if (handler$9 !== undefined) {
174
+ events.push([
175
+ "mouseleave",
176
+ handler$9
177
+ ]);
178
+ }
179
+ return events;
180
+ }
181
+
182
+ function getChildren(props) {
183
+ let child = props.children;
184
+ if (child !== undefined) {
185
+ if (child.TAG === "Fragment") {
186
+ return child._0;
187
+ } else {
188
+ return [child];
189
+ }
190
+ } else {
191
+ return [];
192
+ }
193
+ }
194
+
195
+ function createElement(tag, props) {
196
+ return {
197
+ TAG: "Element",
198
+ tag: tag,
199
+ attrs: propsToAttrs(props),
200
+ events: propsToEvents(props),
201
+ children: getChildren(props)
202
+ };
203
+ }
204
+
205
+ let jsx$1 = createElement;
206
+
207
+ let jsxs$1 = createElement;
208
+
209
+ function jsxKeyed$1(tag, props, key, param) {
210
+ return createElement(tag, props);
211
+ }
212
+
213
+ function jsxsKeyed$1(tag, props, key, param) {
214
+ return createElement(tag, props);
215
+ }
216
+
217
+ let Elements = {
218
+ signal: signal,
219
+ computed: computed,
220
+ convertAttrValue: convertAttrValue,
221
+ propsToAttrs: propsToAttrs,
222
+ propsToEvents: propsToEvents,
223
+ getChildren: getChildren,
224
+ createElement: createElement,
225
+ jsx: jsx$1,
226
+ jsxs: jsxs$1,
227
+ jsxKeyed: jsxKeyed$1,
228
+ jsxsKeyed: jsxsKeyed$1
229
+ };
230
+
231
+ let Component;
232
+
233
+ export {
234
+ Component,
235
+ jsx,
236
+ jsxs,
237
+ jsxKeyed,
238
+ jsxsKeyed,
239
+ jsxFragment,
240
+ array,
241
+ $$null,
242
+ Elements,
243
+ }
244
+ /* Xote__Component Not a pure module */
@@ -0,0 +1,62 @@
1
+ // Pure route matching logic (no signals, no DOM)
2
+
3
+ // Route parameter map
4
+ type params = Dict.t<string>
5
+
6
+ // Match result
7
+ type matchResult =
8
+ | Match(params)
9
+ | NoMatch
10
+
11
+ // Route segment - either static or dynamic parameter
12
+ type segment =
13
+ | Static(string)
14
+ | Param(string)
15
+
16
+ // Parse a route pattern like "/users/:id/posts/:postId"
17
+ // Returns array of segments, where dynamic segments are marked
18
+ let parsePattern = (pattern: string): array<segment> => {
19
+ pattern
20
+ ->String.split("/")
21
+ ->Array.filterMap(seg => {
22
+ if seg == "" {
23
+ None
24
+ } else if String.startsWith(seg, ":") {
25
+ Some(Param(String.sliceToEnd(seg, ~start=1)))
26
+ } else {
27
+ Some(Static(seg))
28
+ }
29
+ })
30
+ }
31
+
32
+ // Match a pathname against a parsed pattern
33
+ let matchPath = (pattern: array<segment>, pathname: string): matchResult => {
34
+ let pathSegments =
35
+ pathname
36
+ ->String.split("/")
37
+ ->Array.filter(s => s != "")
38
+
39
+ // Length must match
40
+ if Array.length(pattern) != Array.length(pathSegments) {
41
+ NoMatch
42
+ } else {
43
+ let params = Dict.make()
44
+ let matches = pattern->Array.everyWithIndex((seg, idx) => {
45
+ let pathSeg = pathSegments->Array.getUnsafe(idx)
46
+ switch seg {
47
+ | Static(expected) => pathSeg == expected
48
+ | Param(name) => {
49
+ params->Dict.set(name, pathSeg)
50
+ true
51
+ }
52
+ }
53
+ })
54
+
55
+ matches ? Match(params) : NoMatch
56
+ }
57
+ }
58
+
59
+ // Convenience: match a pattern string against pathname
60
+ let match = (pattern: string, pathname: string): matchResult => {
61
+ matchPath(parsePattern(pattern), pathname)
62
+ }
@@ -0,0 +1,56 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Core__Array from "@rescript/core/src/Core__Array.res.mjs";
4
+
5
+ function parsePattern(pattern) {
6
+ return Core__Array.filterMap(pattern.split("/"), seg => {
7
+ if (seg === "") {
8
+ return;
9
+ } else if (seg.startsWith(":")) {
10
+ return {
11
+ TAG: "Param",
12
+ _0: seg.slice(1)
13
+ };
14
+ } else {
15
+ return {
16
+ TAG: "Static",
17
+ _0: seg
18
+ };
19
+ }
20
+ });
21
+ }
22
+
23
+ function matchPath(pattern, pathname) {
24
+ let pathSegments = pathname.split("/").filter(s => s !== "");
25
+ if (pattern.length !== pathSegments.length) {
26
+ return "NoMatch";
27
+ }
28
+ let params = {};
29
+ let matches = pattern.every((seg, idx) => {
30
+ let pathSeg = pathSegments[idx];
31
+ if (seg.TAG === "Static") {
32
+ return pathSeg === seg._0;
33
+ }
34
+ params[seg._0] = pathSeg;
35
+ return true;
36
+ });
37
+ if (matches) {
38
+ return {
39
+ TAG: "Match",
40
+ _0: params
41
+ };
42
+ } else {
43
+ return "NoMatch";
44
+ }
45
+ }
46
+
47
+ function match(pattern, pathname) {
48
+ return matchPath(parsePattern(pattern), pathname);
49
+ }
50
+
51
+ export {
52
+ parsePattern,
53
+ matchPath,
54
+ match,
55
+ }
56
+ /* No side effect */