enigmatic 0.26.0 → 0.29.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/CLIENT_JS_DOCS.md +322 -0
- package/__tests__/e2.test.js +231 -138
- package/__tests__/jest.config.js +7 -0
- package/{jest.setup.js → __tests__/jest.setup.js} +4 -0
- package/bun-server.js +130 -0
- package/package.json +12 -11
- package/public/client.js +98 -0
- package/public/custom.js +29 -0
- package/public/index.html +45 -0
- package/README.md +0 -218
- package/__tests__/enigmatic.test.js +0 -328
- package/components.js +0 -169
- package/e2.js +0 -38
- package/enigmatic.js +0 -248
- package/index.html +0 -34
- package/jest.config.js +0 -6
- /package/{enigmatic.css → public/client.css} +0 -0
- /package/{theme.css → public/theme.css} +0 -0
package/enigmatic.js
DELETED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
// Global namespace object and document shortcut
|
|
2
|
-
const w = {}, d = document
|
|
3
|
-
w.enigmatic = { version: '2026-01-03 0.23.0' }
|
|
4
|
-
|
|
5
|
-
// Display error banner at top of page
|
|
6
|
-
const showError = (err, source, line, col) => {
|
|
7
|
-
const errorDiv = d.createElement('div')
|
|
8
|
-
errorDiv.style.cssText = 'position:fixed;top:0;left:0;right:0;background:#ff4444;color:white;padding:20px;z-index:10000;font-family:monospace;border-bottom:4px solid #cc0000;'
|
|
9
|
-
errorDiv.innerHTML = `<strong>JavaScript Error:</strong><br>${err}<br><small>${source ? source + ':' : ''}${line ? ' line ' + line : ''}${col ? ':' + col : ''}</small>`
|
|
10
|
-
d.body.insertBefore(errorDiv, d.body.firstChild)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Global error handler for uncaught JavaScript errors
|
|
14
|
-
window.onerror = (err, source, line, col) => {
|
|
15
|
-
showError(err, source, line, col)
|
|
16
|
-
return false
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Handle unhandled promise rejections
|
|
20
|
-
window.addEventListener('unhandledrejection', (e) => {
|
|
21
|
-
showError(e.reason?.message || e.reason || 'Unhandled Promise Rejection', '', '', '')
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
// DOM query shortcuts
|
|
25
|
-
w.$ = d.querySelector.bind(d)
|
|
26
|
-
w.$$ = d.querySelectorAll.bind(d)
|
|
27
|
-
|
|
28
|
-
// Dynamically load JavaScript file (prevents duplicate loading)
|
|
29
|
-
w.loadJS = (src) => {
|
|
30
|
-
return new Promise((r, j) => {
|
|
31
|
-
if (w.$(`script[src="${src}"]`)) return r(true) // Already loaded
|
|
32
|
-
const s = d.createElement('script')
|
|
33
|
-
s.src = src
|
|
34
|
-
s.addEventListener('load', r)
|
|
35
|
-
s.addEventListener('error', j)
|
|
36
|
-
d.head.appendChild(s)
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Utility: wait for specified milliseconds
|
|
41
|
-
w.wait = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
42
|
-
|
|
43
|
-
// Wait for document to be fully loaded
|
|
44
|
-
w.ready = async () => {
|
|
45
|
-
return new Promise((r) => {
|
|
46
|
-
if (document.readyState === 'complete') return r(true)
|
|
47
|
-
document.addEventListener('readystatechange', () => {
|
|
48
|
-
if (document.readyState === 'complete') r()
|
|
49
|
-
})
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Register custom web component
|
|
54
|
-
// If fn is a function, it becomes a render function that receives data
|
|
55
|
-
// If fn is an object, it can contain init, set, event handlers, etc.
|
|
56
|
-
w.e = (name, fn = {}, style = {}) => {
|
|
57
|
-
console.log(`registering component ${name}`)
|
|
58
|
-
customElements.define(name, class extends HTMLElement {
|
|
59
|
-
connectedCallback() {
|
|
60
|
-
// Apply styles to element
|
|
61
|
-
Object.assign(this.style, style)
|
|
62
|
-
if (typeof fn === 'function') {
|
|
63
|
-
// Function mode: create set() method that renders HTML from function
|
|
64
|
-
this.set = (data) => {
|
|
65
|
-
this.innerHTML = fn(data)
|
|
66
|
-
}
|
|
67
|
-
} else {
|
|
68
|
-
// Object mode: assign all methods/properties to element
|
|
69
|
-
Object.assign(this, fn)
|
|
70
|
-
// Auto-bind event handlers (click, mouseover, etc.)
|
|
71
|
-
Object.keys(fn).filter(k=>k.match(/click|mouseover/)).forEach(k=>{
|
|
72
|
-
this.addEventListener(k, fn[k], true)
|
|
73
|
-
})
|
|
74
|
-
// Call init if provided
|
|
75
|
-
if(this.init) this.init(this)
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Reactive state: automatically updates elements when state changes
|
|
82
|
-
w.state = new Proxy({}, {
|
|
83
|
-
set: async (obj, prop, value) => {
|
|
84
|
-
await w.ready()
|
|
85
|
-
console.log('state change:', prop, value)
|
|
86
|
-
// Skip update if value hasn't changed
|
|
87
|
-
if (obj[prop] === value) {
|
|
88
|
-
return true
|
|
89
|
-
}
|
|
90
|
-
// Find all elements with matching data attribute and call their set() method
|
|
91
|
-
for (const e of w.$$(`[data="${prop}"]`)) {
|
|
92
|
-
if (e.set) e.set(value)
|
|
93
|
-
}
|
|
94
|
-
obj[prop] = value
|
|
95
|
-
return true
|
|
96
|
-
},
|
|
97
|
-
get: (obj, prop, receiver) => {
|
|
98
|
-
// Special property to get entire state object
|
|
99
|
-
if (prop == '_all') return obj
|
|
100
|
-
return obj[prop]
|
|
101
|
-
}
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
// Fetch JSON from URL, optionally transform and store in state
|
|
105
|
-
w.get = async (url, opts = {}, transform, key) => {
|
|
106
|
-
const res = await fetch(url, opts)
|
|
107
|
-
if (!res.ok) throw Error(`Could not fetch ${url}`)
|
|
108
|
-
let data = await res.json()
|
|
109
|
-
if (transform) data = transform(data) // Apply transform function if provided
|
|
110
|
-
if (key) w.state[key] = data // Store in state if key provided
|
|
111
|
-
return data
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Stream data via Server-Sent Events (SSE)
|
|
115
|
-
w.stream = async (url, key) => {
|
|
116
|
-
const ev = new EventSource(url)
|
|
117
|
-
ev.onmessage = (e) => {
|
|
118
|
-
try {
|
|
119
|
-
const data = JSON.parse(e.data)
|
|
120
|
-
if (key) w.state[key] = data // Update state with each message
|
|
121
|
-
return data
|
|
122
|
-
} catch (err) {
|
|
123
|
-
showError(err.message || err, '', '', '')
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
ev.onerror = (e) => {
|
|
127
|
-
showError('EventSource error', '', '', '')
|
|
128
|
-
ev.close()
|
|
129
|
-
}
|
|
130
|
-
return ev
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Template engine: replace {placeholder} with values from object
|
|
134
|
-
// Supports nested properties (e.g., {user.name}) and special vars ($key, $val, $index)
|
|
135
|
-
w.flatten = (obj, text, context = {}) => {
|
|
136
|
-
// Check if template uses special iteration variables
|
|
137
|
-
const hasSpecialVars = /{\$key}|{\$val}|{\$index}/.test(text)
|
|
138
|
-
|
|
139
|
-
// Arrays: map over each item, providing $key, $index, $val
|
|
140
|
-
if (obj instanceof Array)
|
|
141
|
-
return obj.map((o, i) => w.flatten(o, text, { ...context, $key: i, $index: i, $val: o })).join('')
|
|
142
|
-
|
|
143
|
-
// Objects with special vars: iterate over entries, providing $key and $val
|
|
144
|
-
if (hasSpecialVars && obj && typeof obj === 'object' && !Array.isArray(obj))
|
|
145
|
-
return Object.entries(obj).map(([k, v]) => w.flatten(v, text, { ...context, $key: k, $val: v })).join('')
|
|
146
|
-
|
|
147
|
-
// Find all {placeholder} patterns in template
|
|
148
|
-
const m = text.match(/{([^}]*)}/gm) || []
|
|
149
|
-
for (const txt of m) {
|
|
150
|
-
const key = txt.replaceAll(/{|}/g, '')
|
|
151
|
-
// Check context first (for special vars), then object properties
|
|
152
|
-
let val = context[key] !== undefined ? context[key] : undefined
|
|
153
|
-
if (val === undefined && obj && typeof obj === 'object') {
|
|
154
|
-
// Support dot notation: {user.name}
|
|
155
|
-
val = obj
|
|
156
|
-
for (let k of key.split('.')) {
|
|
157
|
-
val = val?.[k]
|
|
158
|
-
}
|
|
159
|
-
} else if (val === undefined) {
|
|
160
|
-
// Fallback to obj itself if key not found
|
|
161
|
-
val = obj
|
|
162
|
-
}
|
|
163
|
-
// Replace placeholder with value (or empty string if undefined)
|
|
164
|
-
text = text.replaceAll(txt, val ?? '')
|
|
165
|
-
}
|
|
166
|
-
return text
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Methods added to div elements for data binding
|
|
170
|
-
const props = {
|
|
171
|
-
// Initialize: save template, clear content, optionally fetch data
|
|
172
|
-
async init() {
|
|
173
|
-
// Extract and remove IGNORE blocks from template
|
|
174
|
-
let ignore = this.innerHTML.match(/<!--IGNORE-->.*?<!--ENDIGNORE-->/gms) || []
|
|
175
|
-
if (!ignore.length) {
|
|
176
|
-
this.template = this.innerHTML
|
|
177
|
-
} else {
|
|
178
|
-
this.ignoreblock = ignore
|
|
179
|
-
this.template = this.innerHTML
|
|
180
|
-
// Remove all IGNORE blocks from template
|
|
181
|
-
ignore.forEach(block => {
|
|
182
|
-
this.template = this.template.replace(block, '')
|
|
183
|
-
})
|
|
184
|
-
}
|
|
185
|
-
this.innerHTML = '' // Clear for rendering
|
|
186
|
-
// Auto-fetch unless defer attribute is present
|
|
187
|
-
if (!this.hasAttribute('defer')) {
|
|
188
|
-
this.fetch()
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
// Update element content with data using template
|
|
192
|
-
set(o) {
|
|
193
|
-
console.log('setting', this, this.template, o)
|
|
194
|
-
this.innerHTML = w.flatten(o, this.template)
|
|
195
|
-
// Sync to state if data attribute exists
|
|
196
|
-
const dt = this.getAttribute('data')
|
|
197
|
-
if(dt)
|
|
198
|
-
w.state[dt] = o
|
|
199
|
-
},
|
|
200
|
-
// Fetch data from URL or parse inline JSON
|
|
201
|
-
async fetch() {
|
|
202
|
-
try {
|
|
203
|
-
const u = this.getAttribute('fetch')
|
|
204
|
-
if(!u) return
|
|
205
|
-
// Inline JSON: parse directly
|
|
206
|
-
if (u.startsWith('[') || u.startsWith('{'))
|
|
207
|
-
return this.set(JSON.parse(u))
|
|
208
|
-
const opts = {}
|
|
209
|
-
const f = await fetch(u, opts)
|
|
210
|
-
console.log(f)
|
|
211
|
-
if (!f.ok) throw Error(`Could not fetch ${u}`)
|
|
212
|
-
let data = await f.json()
|
|
213
|
-
// Apply transform function if 't' attribute exists
|
|
214
|
-
const tf = this.getAttribute('t')
|
|
215
|
-
if (tf) {
|
|
216
|
-
try {
|
|
217
|
-
data = new Function('return ' + tf)()(data)
|
|
218
|
-
} catch (err) {
|
|
219
|
-
showError(`Transform error: ${err.message}`, '', '', '')
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
this.set(data)
|
|
223
|
-
} catch (err) {
|
|
224
|
-
showError(err.message || err, '', '', '')
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Register components from window.components object
|
|
230
|
-
for (let name in window.components)
|
|
231
|
-
w.e(name, window.components[name], window.components[name]?.style)
|
|
232
|
-
|
|
233
|
-
// Expose all functions to global scope (window)
|
|
234
|
-
Object.assign(window, w);
|
|
235
|
-
|
|
236
|
-
// Initialize: wait for DOM, then enhance all divs with data binding
|
|
237
|
-
(async () => {
|
|
238
|
-
try {
|
|
239
|
-
await ready()
|
|
240
|
-
// Add props to all divs and initialize them
|
|
241
|
-
for (const i of w.$$('div')) {
|
|
242
|
-
Object.assign(i, props)
|
|
243
|
-
i?.init()
|
|
244
|
-
}
|
|
245
|
-
} catch (err) {
|
|
246
|
-
showError(err.message || err, '', '', '')
|
|
247
|
-
}
|
|
248
|
-
})()
|
package/index.html
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
<script src="components.js"></script>
|
|
2
|
-
<script src="enigmatic.js"></script>
|
|
3
|
-
<script src="e2.js"></script>
|
|
4
|
-
<link rel="stylesheet" href="enigmatic.css">
|
|
5
|
-
|
|
6
|
-
<body style="--cols:1fr; --rows:1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr">
|
|
7
|
-
<hello-world data="name"></hello-world>
|
|
8
|
-
<my-element></my-element>
|
|
9
|
-
|
|
10
|
-
<div id=one fetch="https://randomuser.me/api/?results=1" t="e=>e.results">{email}</div>
|
|
11
|
-
|
|
12
|
-
<script>
|
|
13
|
-
// Wait for DOM and enigmatic.js to be ready
|
|
14
|
-
document.addEventListener('DOMContentLoaded', async () => {
|
|
15
|
-
// Wait for e to be available (enigmatic.js assigns it)
|
|
16
|
-
while (typeof window.e === 'undefined') {
|
|
17
|
-
await new Promise(r => setTimeout(r, 10))
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
e('my-element', {
|
|
21
|
-
init: (e) => e.innerText = 'ready',
|
|
22
|
-
click: (ev) => ev.target.style.color = 'green',
|
|
23
|
-
mouseover: (ev) => ev.target.style.color = 'yellow',
|
|
24
|
-
}, {
|
|
25
|
-
color: 'red',
|
|
26
|
-
"text-decoration": 'underline'
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
// Set state for components
|
|
30
|
-
await ready()
|
|
31
|
-
state.name = { name: 'World' }
|
|
32
|
-
})
|
|
33
|
-
</script>
|
|
34
|
-
</body>
|
package/jest.config.js
DELETED
|
File without changes
|
|
File without changes
|