enigmatic 0.27.0 → 0.29.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.
@@ -1,328 +0,0 @@
1
- const fs = require('fs')
2
- const path = require('path')
3
-
4
- // Load enigmatic.js into jsdom
5
- const enigmaticCode = fs.readFileSync(path.join(__dirname, '../enigmatic.js'), 'utf8')
6
-
7
- describe('Enigmatic.js', () => {
8
- let w
9
-
10
- beforeEach(() => {
11
- // Reset DOM
12
- global.document.body.innerHTML = ''
13
- global.document.head.innerHTML = ''
14
-
15
- // Clear window.components
16
- global.window.components = {}
17
-
18
- // Execute enigmatic.js code
19
- eval(enigmaticCode)
20
-
21
- w = global.window
22
- })
23
-
24
- describe('Core utilities', () => {
25
- test('$ and $$ selectors work', () => {
26
- global.document.body.innerHTML = '<div id="test">Hello</div><div class="item">Item</div>'
27
- expect(w.$('#test').textContent).toBe('Hello')
28
- expect(w.$$('.item').length).toBe(1)
29
- })
30
-
31
- test('ready() resolves when DOM is complete', async () => {
32
- const ready = await w.ready()
33
- expect(ready).toBe(true)
34
- })
35
-
36
- test('wait() delays execution', async () => {
37
- const start = Date.now()
38
- await w.wait(50)
39
- const elapsed = Date.now() - start
40
- expect(elapsed).toBeGreaterThanOrEqual(45)
41
- })
42
- })
43
-
44
- describe('flatten() template engine', () => {
45
- test('replaces simple placeholders', () => {
46
- const result = w.flatten({ name: 'John', age: 30 }, 'Hello {name}, age {age}')
47
- expect(result).toBe('Hello John, age 30')
48
- })
49
-
50
- test('handles nested properties', () => {
51
- const result = w.flatten({ user: { name: 'John' } }, 'Hello {user.name}')
52
- expect(result).toBe('Hello John')
53
- })
54
-
55
- test('handles arrays', () => {
56
- const result = w.flatten([{ name: 'John' }, { name: 'Jane' }], 'Name: {name}')
57
- expect(result).toBe('Name: JohnName: Jane')
58
- })
59
-
60
- test('handles $key and $val for objects', () => {
61
- const result = w.flatten({ k1: 'val1', k2: 'val2' }, '{$key}: {$val}')
62
- expect(result).toContain('k1: val1')
63
- expect(result).toContain('k2: val2')
64
- })
65
-
66
- test('handles $key and $index for arrays', () => {
67
- const result = w.flatten(['a', 'b'], '{$index}: {$val}')
68
- expect(result).toContain('0: a')
69
- expect(result).toContain('1: b')
70
- })
71
-
72
- test('handles undefined values', () => {
73
- const result = w.flatten({ name: 'John' }, 'Hello {name}, missing: {missing}')
74
- expect(result).toBe('Hello John, missing: ')
75
- })
76
- })
77
-
78
- describe('Component registration (w.e)', () => {
79
- test('registers component with object config', () => {
80
- let initCalled = false
81
- w.e('test-comp', {
82
- init: () => { initCalled = true }
83
- })
84
-
85
- global.document.body.innerHTML = '<test-comp></test-comp>'
86
- expect(initCalled).toBe(true)
87
- })
88
-
89
- test('registers component with function', () => {
90
- w.e('func-comp', (data) => `<div>${data.name}</div>`)
91
-
92
- global.document.body.innerHTML = '<func-comp data="test"></func-comp>'
93
- const comp = global.document.querySelector('func-comp')
94
- comp.set({ name: 'John' })
95
- expect(comp.innerHTML).toBe('<div>John</div>')
96
- })
97
-
98
- test('applies styles', () => {
99
- w.e('styled-comp', {}, { color: 'red', padding: '10px' })
100
- global.document.body.innerHTML = '<styled-comp></styled-comp>'
101
- const comp = global.document.querySelector('styled-comp')
102
- expect(comp.style.color).toBe('red')
103
- expect(comp.style.padding).toBe('10px')
104
- })
105
-
106
- test('auto-binds event handlers', () => {
107
- let clicked = false
108
- w.e('click-comp', {
109
- click: () => { clicked = true }
110
- })
111
-
112
- global.document.body.innerHTML = '<click-comp></click-comp>'
113
- const comp = global.document.querySelector('click-comp')
114
- comp.click()
115
- expect(clicked).toBe(true)
116
- })
117
- })
118
-
119
- describe('State management', () => {
120
- test('state updates trigger set() on elements', async () => {
121
- let setCalled = false
122
- let receivedData = null
123
-
124
- w.e('state-comp', {
125
- set: (data) => {
126
- setCalled = true
127
- receivedData = data
128
- }
129
- })
130
-
131
- global.document.body.innerHTML = '<state-comp data="test"></state-comp>'
132
- await w.ready()
133
-
134
- // Wait a bit for initialization
135
- await new Promise(r => setTimeout(r, 50))
136
-
137
- w.state.test = { name: 'John' }
138
-
139
- // Wait for async state update
140
- await new Promise(r => setTimeout(r, 50))
141
-
142
- expect(setCalled).toBe(true)
143
- expect(receivedData).toEqual({ name: 'John' })
144
- })
145
-
146
- test('state.get returns values', async () => {
147
- await w.ready()
148
- w.state.test = 'value'
149
- await new Promise(r => setTimeout(r, 10))
150
- expect(w.state.test).toBe('value')
151
- })
152
-
153
- test('state._all returns all state', async () => {
154
- await w.ready()
155
- w.state.a = 1
156
- w.state.b = 2
157
- await new Promise(r => setTimeout(r, 10))
158
- const all = w.state._all
159
- expect(all.a).toBe(1)
160
- expect(all.b).toBe(2)
161
- })
162
- })
163
-
164
- describe('Div props (data binding)', () => {
165
- test('init() saves template and clears innerHTML', async () => {
166
- global.document.body.innerHTML = '<div data="test">Hello {name}</div>'
167
- await w.ready()
168
- await new Promise(r => setTimeout(r, 100)) // Wait for auto-init
169
-
170
- const div = global.document.querySelector('div')
171
- // If init wasn't called, call it manually
172
- if (div && div.init) {
173
- expect(div.template || '').toContain('Hello')
174
- } else {
175
- // Test the props object directly
176
- const props = {
177
- async init() {
178
- let ignore = this.innerHTML.match(/<!--IGNORE-->.*?<!--ENDIGNORE-->/gms) || []
179
- if (!ignore.length) {
180
- this.template = this.innerHTML
181
- } else {
182
- this.ignoreblock = ignore
183
- this.template = this.innerHTML
184
- ignore.forEach(block => {
185
- this.template = this.template.replace(block, '')
186
- })
187
- }
188
- this.innerHTML = ''
189
- }
190
- }
191
- Object.assign(div, props)
192
- await div.init()
193
- expect(div.template).toBe('Hello {name}')
194
- expect(div.innerHTML).toBe('')
195
- }
196
- })
197
-
198
- test('set() updates content with flattened template', async () => {
199
- global.document.body.innerHTML = '<div data="test">Hello {name}</div>'
200
- await w.ready()
201
- await new Promise(r => setTimeout(r, 100))
202
-
203
- const div = global.document.querySelector('div')
204
- if (div && div.set) {
205
- div.set({ name: 'John' })
206
- expect(div.innerHTML).toBe('Hello John')
207
- } else {
208
- // Test flatten directly
209
- const result = w.flatten({ name: 'John' }, 'Hello {name}')
210
- expect(result).toBe('Hello John')
211
- }
212
- })
213
-
214
- test('set() syncs to state', async () => {
215
- global.document.body.innerHTML = '<div data="test">Hello {name}</div>'
216
- await w.ready()
217
- await new Promise(r => setTimeout(r, 100))
218
-
219
- const div = global.document.querySelector('div')
220
- if (div && div.set) {
221
- div.set({ name: 'John' })
222
- await new Promise(r => setTimeout(r, 50))
223
- expect(w.state.test).toEqual({ name: 'John' })
224
- }
225
- })
226
-
227
- test('fetch() with inline JSON', async () => {
228
- global.document.body.innerHTML = '<div data="test" fetch=\'{"name": "John"}\'>Hello {name}</div>'
229
- await w.ready()
230
-
231
- const div = global.document.querySelector('div')
232
- if (div && div.fetch) {
233
- await div.fetch()
234
- await new Promise(r => setTimeout(r, 50))
235
- expect(div.innerHTML).toContain('John')
236
- } else if (div) {
237
- // Test fetch logic directly
238
- const fetchAttr = div.getAttribute('fetch')
239
- if (fetchAttr && (fetchAttr.startsWith('[') || fetchAttr.startsWith('{'))) {
240
- const data = JSON.parse(fetchAttr)
241
- const result = w.flatten(data, 'Hello {name}')
242
- expect(result).toContain('John')
243
- }
244
- }
245
- })
246
-
247
- test('defer attribute skips auto-fetch', async () => {
248
- global.document.body.innerHTML = '<div data="test" fetch=\'{"name": "John"}\' defer>Hello {name}</div>'
249
- await w.ready()
250
- await new Promise(r => setTimeout(r, 50))
251
-
252
- const div = global.document.querySelector('div')
253
- if (div && div.fetch) {
254
- await div.fetch()
255
- await new Promise(r => setTimeout(r, 50))
256
- expect(div.innerHTML).toContain('John')
257
- }
258
- })
259
-
260
- test('IGNORE blocks are removed from template', async () => {
261
- global.document.body.innerHTML = '<div>Hello <!--IGNORE-->ignore this<!--ENDIGNORE--> {name}</div>'
262
- await w.ready()
263
- await new Promise(r => setTimeout(r, 100))
264
-
265
- const div = global.document.querySelector('div')
266
- if (div && div.template) {
267
- expect(div.template.replace(/\s+/g, ' ')).toContain('Hello')
268
- expect(div.template).not.toContain('ignore this')
269
- }
270
- })
271
- })
272
-
273
- describe('Error handling', () => {
274
- test('error handler is set up', () => {
275
- expect(typeof global.window.onerror).toBe('function')
276
- })
277
-
278
- test('shows error on page when body exists', () => {
279
- global.document.body.innerHTML = '<div>test</div>'
280
- global.window.onerror('Test error', 'test.js', 1, 1)
281
-
282
- // Error div should be first child (inserted before first child)
283
- const errorDiv = global.document.body.firstElementChild
284
- expect(errorDiv).toBeTruthy()
285
- // Check if it has the error styling or content
286
- if (errorDiv && (errorDiv.style.background || errorDiv.textContent.includes('Error'))) {
287
- expect(errorDiv.textContent).toContain('Test error')
288
- } else {
289
- // If not found, at least verify the handler was called
290
- expect(global.document.body.children.length).toBeGreaterThan(0)
291
- }
292
- })
293
-
294
- test('unhandledrejection handler is set up', () => {
295
- // Verify listener exists by checking it's callable
296
- expect(global.window.addEventListener).toBeDefined()
297
- })
298
- })
299
-
300
- describe('get() fetch utility', () => {
301
- test('get() fetches and transforms data', async () => {
302
- global.fetch = jest.fn(() =>
303
- Promise.resolve({
304
- ok: true,
305
- json: () => Promise.resolve({ results: [{ name: 'John' }] })
306
- })
307
- )
308
-
309
- const data = await w.get('http://test.com', {}, d => d.results, 'users')
310
-
311
- expect(data).toEqual([{ name: 'John' }])
312
- // Wait for async state update
313
- await new Promise(r => setTimeout(r, 50))
314
- expect(w.state.users).toEqual([{ name: 'John' }])
315
- })
316
-
317
- test('get() throws on failed fetch', async () => {
318
- global.fetch = jest.fn(() =>
319
- Promise.resolve({
320
- ok: false
321
- })
322
- )
323
-
324
- await expect(w.get('http://test.com')).rejects.toThrow()
325
- })
326
- })
327
- })
328
-
package/components.js DELETED
@@ -1,58 +0,0 @@
1
- window.components = {
2
- "hello-world": (data) => `Hello ${data?.name || 'World'}!`,
3
- "markdown-block": {
4
- async init() {
5
- await loadJS('https://cdn.jsdelivr.net/npm/marked/marked.min.js')
6
- this.innerHTML = marked.parse(this.innerText)
7
- }
8
- }
9
- }
10
-
11
- /**
12
- * Component Definitions
13
- *
14
- * Components are registered via window.components object.
15
- * Each component is a custom HTML element that can be used in your HTML.
16
- *
17
- * Structure:
18
- * window.components = {
19
- * "component-name": {
20
- * // Component methods and properties
21
- * }
22
- * }
23
- *
24
- * Available Options:
25
- *
26
- * 1. init(element) - Called when component is first connected to DOM
27
- * - Receives the element instance as parameter
28
- * - Use for setup, loading resources, initial rendering
29
- * - Can be async
30
- *
31
- * 2. set(data) - Called when component receives data (via data attribute binding)
32
- * - Receives data object from state or fetch
33
- * - Use to update component content based on data
34
- *
35
- * 3. click(ev), mouseover(ev), etc. - Event handlers
36
- * - Automatically bound as event listeners
37
- * - Any method matching /click|mouseover/ pattern is auto-bound
38
- * - Receives the event object
39
- *
40
- * 4. style - Object with CSS properties
41
- * - Applied as inline styles when component connects
42
- * - Use camelCase or kebab-case (with quotes) for CSS properties
43
- *
44
- * 5. Any other methods - Available as instance methods
45
- * - Can be called directly on element: element.myMethod()
46
- *
47
- * Usage in HTML:
48
- * <component-name data="stateKey"></component-name>
49
- *
50
- * Example:
51
- * window.components = {
52
- * "my-button": {
53
- * init: (e) => e.innerText = 'Click me',
54
- * click: (ev) => alert('Clicked!'),
55
- * style: { color: 'blue', padding: '10px' }
56
- * }
57
- * }
58
- */
package/e2.js DELETED
@@ -1,38 +0,0 @@
1
- window.$ = document.querySelector.bind(document)
2
- window.$$ = document.querySelectorAll.bind(document)
3
-
4
- window.fetchJson = async (method, url, opts) => {
5
- const res = await fetch(url, { method, ...opts, headers: { 'Content-Type': 'application/json' }, credentials: 'include' })
6
- return {
7
- data: await res.json(),
8
- status: res.status,
9
- statusText: res.statusText,
10
- headers: res.headers
11
- }
12
- }
13
-
14
- window.custom = {
15
- "hello-world": (data) => `Hello ${data}`,
16
- "hello-world-2": {
17
- prop: (data) => `${data} World`,
18
- render: function(data) {
19
- return this.prop(data);
20
- }
21
- }
22
- }
23
-
24
- window.state = new Proxy({}, {
25
- set(obj, prop, value) {
26
- obj[prop] = value
27
- $$(`[data="${prop}"]`).forEach(el => {
28
- console.log('setting', el.tagName);
29
- const f = window.custom[el.tagName.toLowerCase()];
30
- if(typeof f === 'function') {
31
- el.innerHTML = f(value);
32
- } else {
33
- el.innerHTML = f.render(value);
34
- }
35
- });
36
- return true
37
- }
38
- })
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
- })()