what-core 0.1.0 → 0.2.0

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.
package/dist/h.js CHANGED
@@ -1,150 +1,203 @@
1
+ // What Framework - Hyperscript / VDOM
2
+ // Minimal virtual DOM nodes. No classes, no fibers, just plain objects.
3
+
4
+ // h(tag, props, ...children) -> VNode
5
+ // h(Component, props, ...children) -> VNode
6
+ // VNode = { tag, props, children, key }
7
+
1
8
  const EMPTY_OBJ = {};
2
9
  const EMPTY_ARR = [];
10
+
3
11
  export function h(tag, props, ...children) {
4
- props = props || EMPTY_OBJ;
5
- const flat = flattenChildren(children);
6
- const key = props.key ?? null;
7
- if (props.key !== undefined) {
8
- props = { ...props };
9
- delete props.key;
10
- }
11
- return { tag, props, children: flat, key, _vnode: true };
12
- }
12
+ props = props || EMPTY_OBJ;
13
+ const flat = flattenChildren(children);
14
+ const key = props.key ?? null;
15
+
16
+ // Strip key from props passed to component/element
17
+ if (props.key !== undefined) {
18
+ props = { ...props };
19
+ delete props.key;
20
+ }
21
+
22
+ return { tag, props, children: flat, key, _vnode: true };
23
+ }
24
+
25
+ // Fragment — just returns children
13
26
  export function Fragment({ children }) {
14
- return children;
27
+ return children;
15
28
  }
29
+
16
30
  function flattenChildren(children) {
17
- const out = [];
18
- for (let i = 0; i < children.length; i++) {
19
- const child = children[i];
20
- if (child == null || child === false || child === true) continue;
21
- if (Array.isArray(child)) {
22
- out.push(...flattenChildren(child));
23
- } else if (typeof child === 'object' && child._vnode) {
24
- out.push(child);
25
- } else {
26
- out.push(String(child));
27
- }
28
- }
29
- return out;
30
- }
31
+ const out = [];
32
+ for (let i = 0; i < children.length; i++) {
33
+ const child = children[i];
34
+ if (child == null || child === false || child === true) continue;
35
+ if (Array.isArray(child)) {
36
+ out.push(...flattenChildren(child));
37
+ } else if (typeof child === 'object' && child._vnode) {
38
+ out.push(child);
39
+ } else {
40
+ // Text node
41
+ out.push(String(child));
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+
47
+ // JSX-like tagged template alternative (no build step needed)
48
+ // html`<div class="foo">${content}</div>`
49
+ // Uses a simple parser, not full HTML — good enough for most cases.
50
+
31
51
  export function html(strings, ...values) {
32
- const src = strings.reduce((acc, str, i) =>
33
- acc + str + (i < values.length ? `\x00${i}\x00` : ''), '');
34
- return parseTemplate(src, values);
52
+ const src = strings.reduce((acc, str, i) =>
53
+ acc + str + (i < values.length ? `\x00${i}\x00` : ''), '');
54
+ return parseTemplate(src, values);
35
55
  }
56
+
36
57
  function parseTemplate(src, values) {
37
- src = src.trim();
38
- const nodes = [];
39
- let i = 0;
40
- while (i < src.length) {
41
- if (src[i] === '<') {
42
- const result = parseElement(src, i, values);
43
- if (result) {
44
- nodes.push(result.node);
45
- i = result.end;
46
- continue;
47
- }
48
- }
49
- const result = parseText(src, i, values);
50
- if (result.text) nodes.push(result.text);
51
- i = result.end;
52
- }
53
- return nodes.length === 1 ? nodes[0] : nodes;
54
- }
58
+ // Lightweight tagged template parser
59
+ // Supports: <tag attr="val">, <tag ...${spread}>, ${children}, self-closing
60
+ src = src.trim();
61
+ const nodes = [];
62
+ let i = 0;
63
+
64
+ while (i < src.length) {
65
+ if (src[i] === '<') {
66
+ const result = parseElement(src, i, values);
67
+ if (result) {
68
+ nodes.push(result.node);
69
+ i = result.end;
70
+ continue;
71
+ }
72
+ }
73
+ // Text or interpolation
74
+ const result = parseText(src, i, values);
75
+ if (result.text) nodes.push(result.text);
76
+ i = result.end;
77
+ }
78
+
79
+ return nodes.length === 1 ? nodes[0] : nodes;
80
+ }
81
+
55
82
  function parseElement(src, start, values) {
56
- const openMatch = src.slice(start).match(/^<([a-zA-Z][a-zA-Z0-9-]*|[A-Z]\w*)/);
57
- if (!openMatch) return null;
58
- const tag = openMatch[1];
59
- let i = start + openMatch[0].length;
60
- const props = {};
61
- while (i < src.length) {
62
- while (i < src.length && /\s/.test(src[i])) i++;
63
- if (src.slice(i, i + 2) === '/>') {
64
- return { node: h(tag, Object.keys(props).length ? props : null), end: i + 2 };
65
- }
66
- if (src[i] === '>') {
67
- i++;
68
- break;
69
- }
70
- if (src.slice(i, i + 3) === '...') {
71
- const placeholder = src.slice(i + 3).match(/^\x00(\d+)\x00/);
72
- if (placeholder) {
73
- Object.assign(props, values[Number(placeholder[1])]);
74
- i += 3 + placeholder[0].length;
75
- continue;
76
- }
77
- }
78
- const attrMatch = src.slice(i).match(/^([a-zA-Z_@:][a-zA-Z0-9_:.-]*)/);
79
- if (!attrMatch) break;
80
- const attrName = attrMatch[1];
81
- i += attrMatch[0].length;
82
- while (i < src.length && /\s/.test(src[i])) i++;
83
- if (src[i] === '=') {
84
- i++;
85
- while (i < src.length && /\s/.test(src[i])) i++;
86
- const ph = src.slice(i).match(/^\x00(\d+)\x00/);
87
- if (ph) {
88
- props[attrName] = values[Number(ph[1])];
89
- i += ph[0].length;
90
- } else if (src[i] === '"' || src[i] === "'") {
91
- const q = src[i];
92
- i++;
93
- let val = '';
94
- while (i < src.length && src[i] !== q) {
95
- const tph = src.slice(i).match(/^\x00(\d+)\x00/);
96
- if (tph) {
97
- val += String(values[Number(tph[1])]);
98
- i += tph[0].length;
99
- } else {
100
- val += src[i];
101
- i++;
102
- }
103
- }
104
- i++;
105
- props[attrName] = val;
106
- }
107
- } else {
108
- props[attrName] = true;
109
- }
110
- }
111
- const children = [];
112
- const closeTag = `</${tag}>`;
113
- while (i < src.length) {
114
- if (src.slice(i, i + closeTag.length) === closeTag) {
115
- i += closeTag.length;
116
- break;
117
- }
118
- if (src[i] === '<') {
119
- const child = parseElement(src, i, values);
120
- if (child) {
121
- children.push(child.node);
122
- i = child.end;
123
- continue;
124
- }
125
- }
126
- const text = parseText(src, i, values);
127
- if (text.text != null) children.push(text.text);
128
- i = text.end;
129
- }
130
- return {
131
- node: h(tag, Object.keys(props).length ? props : null, ...children),
132
- end: i,
133
- };
134
- }
83
+ // Opening tag
84
+ const openMatch = src.slice(start).match(/^<([a-zA-Z][a-zA-Z0-9-]*|[A-Z]\w*)/);
85
+ if (!openMatch) return null;
86
+
87
+ const tag = openMatch[1];
88
+ let i = start + openMatch[0].length;
89
+ const props = {};
90
+
91
+ // Parse attributes
92
+ while (i < src.length) {
93
+ // Skip whitespace
94
+ while (i < src.length && /\s/.test(src[i])) i++;
95
+
96
+ // Self-closing or end of opening tag
97
+ if (src.slice(i, i + 2) === '/>') {
98
+ return { node: h(tag, Object.keys(props).length ? props : null), end: i + 2 };
99
+ }
100
+ if (src[i] === '>') {
101
+ i++;
102
+ break;
103
+ }
104
+
105
+ // Spread: ...${obj}
106
+ if (src.slice(i, i + 3) === '...') {
107
+ const placeholder = src.slice(i + 3).match(/^\x00(\d+)\x00/);
108
+ if (placeholder) {
109
+ Object.assign(props, values[Number(placeholder[1])]);
110
+ i += 3 + placeholder[0].length;
111
+ continue;
112
+ }
113
+ }
114
+
115
+ // Attribute: name=${val} or name="val" or name
116
+ const attrMatch = src.slice(i).match(/^([a-zA-Z_@:][a-zA-Z0-9_:.-]*)/);
117
+ if (!attrMatch) break;
118
+
119
+ const attrName = attrMatch[1];
120
+ i += attrMatch[0].length;
121
+
122
+ // Skip whitespace around =
123
+ while (i < src.length && /\s/.test(src[i])) i++;
124
+
125
+ if (src[i] === '=') {
126
+ i++;
127
+ while (i < src.length && /\s/.test(src[i])) i++;
128
+
129
+ // Value is a placeholder
130
+ const ph = src.slice(i).match(/^\x00(\d+)\x00/);
131
+ if (ph) {
132
+ props[attrName] = values[Number(ph[1])];
133
+ i += ph[0].length;
134
+ } else if (src[i] === '"' || src[i] === "'") {
135
+ const q = src[i];
136
+ i++;
137
+ let val = '';
138
+ while (i < src.length && src[i] !== q) {
139
+ const tph = src.slice(i).match(/^\x00(\d+)\x00/);
140
+ if (tph) {
141
+ val += String(values[Number(tph[1])]);
142
+ i += tph[0].length;
143
+ } else {
144
+ val += src[i];
145
+ i++;
146
+ }
147
+ }
148
+ i++; // closing quote
149
+ props[attrName] = val;
150
+ }
151
+ } else {
152
+ props[attrName] = true;
153
+ }
154
+ }
155
+
156
+ // Parse children until closing tag
157
+ const children = [];
158
+ const closeTag = `</${tag}>`;
159
+
160
+ while (i < src.length) {
161
+ if (src.slice(i, i + closeTag.length) === closeTag) {
162
+ i += closeTag.length;
163
+ break;
164
+ }
165
+
166
+ if (src[i] === '<') {
167
+ const child = parseElement(src, i, values);
168
+ if (child) {
169
+ children.push(child.node);
170
+ i = child.end;
171
+ continue;
172
+ }
173
+ }
174
+
175
+ const text = parseText(src, i, values);
176
+ if (text.text != null) children.push(text.text);
177
+ i = text.end;
178
+ }
179
+
180
+ return {
181
+ node: h(tag, Object.keys(props).length ? props : null, ...children),
182
+ end: i,
183
+ };
184
+ }
185
+
135
186
  function parseText(src, start, values) {
136
- let i = start;
137
- let text = '';
138
- while (i < src.length && src[i] !== '<') {
139
- const ph = src.slice(i).match(/^\x00(\d+)\x00/);
140
- if (ph) {
141
- if (text.trim()) {
142
- return { text: text.trim(), end: i };
143
- }
144
- return { text: values[Number(ph[1])], end: i + ph[0].length };
145
- }
146
- text += src[i];
147
- i++;
187
+ let i = start;
188
+ let text = '';
189
+
190
+ while (i < src.length && src[i] !== '<') {
191
+ const ph = src.slice(i).match(/^\x00(\d+)\x00/);
192
+ if (ph) {
193
+ if (text.trim()) {
194
+ return { text: text.trim(), end: i };
195
+ }
196
+ return { text: values[Number(ph[1])], end: i + ph[0].length };
197
+ }
198
+ text += src[i];
199
+ i++;
200
+ }
201
+
202
+ return { text: text.trim() || null, end: i };
148
203
  }
149
- return { text: text.trim() || null, end: i };
150
- }
package/dist/head.js CHANGED
@@ -1,51 +1,68 @@
1
+ // What Framework - Head Management
2
+ // Declarative <head> updates from any component.
3
+ // Supports title, meta, link, script tags. Auto-deduplicates by key.
4
+
1
5
  const headState = {
2
- title: null,
3
- metas: new Map(),
4
- links: new Map(),
6
+ title: null,
7
+ metas: new Map(),
8
+ links: new Map(),
5
9
  };
10
+
11
+ // --- Head component ---
12
+ // Use in any component to set head tags. Last one wins for title/meta.
13
+
6
14
  export function Head({ title, meta, link, children }) {
7
- if (typeof document === 'undefined') return null;
8
- if (title) {
9
- document.title = title;
10
- headState.title = title;
11
- }
12
- if (meta) {
13
- for (const attrs of (Array.isArray(meta) ? meta : [meta])) {
14
- const key = attrs.name || attrs.property || attrs.httpEquiv || JSON.stringify(attrs);
15
- setHeadTag('meta', key, attrs);
16
- }
17
- }
18
- if (link) {
19
- for (const attrs of (Array.isArray(link) ? link : [link])) {
20
- const key = attrs.rel + (attrs.href || '');
21
- setHeadTag('link', key, attrs);
22
- }
23
- }
24
- return children || null;
15
+ if (typeof document === 'undefined') return null;
16
+
17
+ if (title) {
18
+ document.title = title;
19
+ headState.title = title;
20
+ }
21
+
22
+ if (meta) {
23
+ for (const attrs of (Array.isArray(meta) ? meta : [meta])) {
24
+ const key = attrs.name || attrs.property || attrs.httpEquiv || JSON.stringify(attrs);
25
+ setHeadTag('meta', key, attrs);
26
+ }
27
+ }
28
+
29
+ if (link) {
30
+ for (const attrs of (Array.isArray(link) ? link : [link])) {
31
+ const key = attrs.rel + (attrs.href || '');
32
+ setHeadTag('link', key, attrs);
33
+ }
34
+ }
35
+
36
+ return children || null;
25
37
  }
38
+
26
39
  function setHeadTag(tag, key, attrs) {
27
- const existing = document.head.querySelector(`[data-what-head="${key}"]`);
28
- if (existing) {
29
- updateElement(existing, attrs);
30
- return;
31
- }
32
- const el = document.createElement(tag);
33
- el.setAttribute('data-what-head', key);
34
- for (const [k, v] of Object.entries(attrs)) {
35
- el.setAttribute(k, v);
36
- }
37
- document.head.appendChild(el);
40
+ const existing = document.head.querySelector(`[data-what-head="${key}"]`);
41
+ if (existing) {
42
+ updateElement(existing, attrs);
43
+ return;
44
+ }
45
+
46
+ const el = document.createElement(tag);
47
+ el.setAttribute('data-what-head', key);
48
+ for (const [k, v] of Object.entries(attrs)) {
49
+ el.setAttribute(k, v);
50
+ }
51
+ document.head.appendChild(el);
38
52
  }
53
+
39
54
  function updateElement(el, attrs) {
40
- for (const [k, v] of Object.entries(attrs)) {
41
- if (el.getAttribute(k) !== v) {
42
- el.setAttribute(k, v);
43
- }
44
- }
55
+ for (const [k, v] of Object.entries(attrs)) {
56
+ if (el.getAttribute(k) !== v) {
57
+ el.setAttribute(k, v);
58
+ }
59
+ }
45
60
  }
61
+
62
+ // --- Cleanup: remove head tags added by What ---
46
63
  export function clearHead() {
47
- const tags = document.head.querySelectorAll('[data-what-head]');
48
- for (const tag of tags) tag.remove();
49
- headState.metas.clear();
50
- headState.links.clear();
51
- }
64
+ const tags = document.head.querySelectorAll('[data-what-head]');
65
+ for (const tag of tags) tag.remove();
66
+ headState.metas.clear();
67
+ headState.links.clear();
68
+ }