pulse-js-framework 1.7.33 → 1.7.37
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/compiler/parser.js +3 -1
- package/compiler/transformer/constants.js +1 -1
- package/compiler/transformer/expressions.js +43 -3
- package/loader/vite-plugin.js +1 -1
- package/package.json +4 -1
- package/runtime/security.js +21 -0
- package/runtime/ssr.js +17 -2
- package/types/a11y.d.ts +186 -0
- package/types/devtools.d.ts +418 -0
- package/types/dom-adapter.d.ts +643 -0
- package/types/dom.d.ts +63 -0
- package/types/errors.d.ts +618 -0
- package/types/http.d.ts +426 -0
- package/types/logger.d.ts +12 -0
- package/types/native.d.ts +282 -0
- package/types/pulse.d.ts +70 -1
- package/types/security.d.ts +286 -0
- package/types/ssr.d.ts +263 -0
- package/types/utils.d.ts +85 -0
package/compiler/parser.js
CHANGED
|
@@ -100,7 +100,9 @@ export class Parser {
|
|
|
100
100
|
* Peek at token at offset
|
|
101
101
|
*/
|
|
102
102
|
peek(offset = 1) {
|
|
103
|
-
|
|
103
|
+
const index = this.pos + offset;
|
|
104
|
+
if (index < 0 || index >= this.tokens.length) return undefined;
|
|
105
|
+
return this.tokens[index];
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
/**
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
/** Generate a unique scope ID for CSS scoping */
|
|
8
|
-
export const generateScopeId = () => 'p' + Math.random().toString(36).substring(2,
|
|
8
|
+
export const generateScopeId = () => 'p' + Math.random().toString(36).substring(2, 12);
|
|
9
9
|
|
|
10
10
|
/** Token types that should not have space after them */
|
|
11
11
|
export const NO_SPACE_AFTER = new Set([
|
|
@@ -182,14 +182,54 @@ export function transformExpression(transformer, node) {
|
|
|
182
182
|
* @returns {string} Transformed expression string
|
|
183
183
|
*/
|
|
184
184
|
export function transformExpressionString(transformer, exprStr) {
|
|
185
|
-
//
|
|
185
|
+
// Transform state and prop vars in expression strings (interpolations, attribute bindings)
|
|
186
186
|
// Both are now reactive (useProp returns computed for uniform interface)
|
|
187
187
|
let result = exprStr;
|
|
188
188
|
|
|
189
|
-
//
|
|
189
|
+
// First, handle assignments to state vars: stateVar = expr -> stateVar.set(expr)
|
|
190
|
+
// This must happen before the generic .get() replacement to avoid generating
|
|
191
|
+
// invalid code like stateVar.get() = expr (LHS of assignment is not a reference)
|
|
190
192
|
for (const stateVar of transformer.stateVars) {
|
|
193
|
+
// Compound assignment: stateVar += expr -> stateVar.update(_v => _v + expr)
|
|
191
194
|
result = result.replace(
|
|
192
|
-
new RegExp(`\\b${stateVar}\\
|
|
195
|
+
new RegExp(`\\b${stateVar}\\s*(\\+=|-=|\\*=|\\/=|&&=|\\|\\|=|\\?\\?=)\\s*`, 'g'),
|
|
196
|
+
(_match, op) => {
|
|
197
|
+
const baseOp = op.slice(0, -1); // Remove trailing '='
|
|
198
|
+
return `${stateVar}.update(_v => _v ${baseOp} `;
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
// Close the .update() call - find the end of the expression after the replacement
|
|
202
|
+
// This is handled by the fact that the expression continues after the replacement text
|
|
203
|
+
// and the closing paren is added by wrapping logic below.
|
|
204
|
+
|
|
205
|
+
// Simple assignment: stateVar = expr -> stateVar.set(expr)
|
|
206
|
+
// Use negative lookbehind to skip compound assignments (already handled)
|
|
207
|
+
// Use negative lookahead to skip == and ===
|
|
208
|
+
result = result.replace(
|
|
209
|
+
new RegExp(`\\b${stateVar}\\s*=(?!=)`, 'g'),
|
|
210
|
+
`${stateVar}.set(`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// If we inserted .set( or .update(, we need to close the parenthesis
|
|
215
|
+
// Find unclosed .set( and .update( calls and close them at end of expression
|
|
216
|
+
if (result.includes('.set(') || result.includes('.update(_v =>')) {
|
|
217
|
+
// For .update(_v => _v op expr), close with )
|
|
218
|
+
result = result.replace(
|
|
219
|
+
/\.update\(_v => _v [^\)]*$/,
|
|
220
|
+
(m) => m + ')'
|
|
221
|
+
);
|
|
222
|
+
// For .set(expr), close with )
|
|
223
|
+
result = result.replace(
|
|
224
|
+
/\.set\(([^)]*$)/,
|
|
225
|
+
(_m, expr) => `.set(${expr})`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Transform state var reads (not already transformed to .get/.set/.update)
|
|
230
|
+
for (const stateVar of transformer.stateVars) {
|
|
231
|
+
result = result.replace(
|
|
232
|
+
new RegExp(`\\b${stateVar}\\b(?!\\.(?:get|set|update))`, 'g'),
|
|
193
233
|
`${stateVar}.get()`
|
|
194
234
|
);
|
|
195
235
|
}
|
package/loader/vite-plugin.js
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pulse-js-framework",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.37",
|
|
4
4
|
"description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"main": "index.js",
|
|
7
8
|
"types": "types/index.d.ts",
|
|
8
9
|
"bin": {
|
|
@@ -178,6 +179,8 @@
|
|
|
178
179
|
"test:esbuild-plugin": "node test/esbuild-plugin.test.js",
|
|
179
180
|
"test:parcel-plugin": "node test/parcel-plugin.test.js",
|
|
180
181
|
"test:swc-plugin": "node test/swc-plugin.test.js",
|
|
182
|
+
"test:dom-binding": "node test/dom-binding.test.js",
|
|
183
|
+
"test:interceptor-manager": "node test/interceptor-manager.test.js",
|
|
181
184
|
"build:netlify": "node scripts/build-netlify.js",
|
|
182
185
|
"version": "node scripts/sync-version.js",
|
|
183
186
|
"docs": "node cli/index.js dev docs"
|
package/runtime/security.js
CHANGED
|
@@ -321,6 +321,27 @@ export function sanitizeHtml(html, options = {}) {
|
|
|
321
321
|
attrValue = sanitized;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
// Sanitize style attribute to prevent CSS injection
|
|
325
|
+
if (attrName === 'style') {
|
|
326
|
+
const parts = attrValue.split(';').filter(Boolean);
|
|
327
|
+
const safeParts = [];
|
|
328
|
+
for (const part of parts) {
|
|
329
|
+
const colonIndex = part.indexOf(':');
|
|
330
|
+
if (colonIndex === -1) continue;
|
|
331
|
+
const cssProp = part.slice(0, colonIndex).trim();
|
|
332
|
+
const cssVal = part.slice(colonIndex + 1).trim();
|
|
333
|
+
if (/url\s*\(/i.test(cssVal) || /expression\s*\(/i.test(cssVal) ||
|
|
334
|
+
/javascript:/i.test(cssVal) || /behavior\s*:/i.test(cssVal) ||
|
|
335
|
+
/-moz-binding/i.test(cssVal)) {
|
|
336
|
+
log.warn(`Blocked dangerous CSS in style attribute: ${cssProp}`);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
safeParts.push(`${cssProp}: ${cssVal}`);
|
|
340
|
+
}
|
|
341
|
+
attrValue = safeParts.join('; ');
|
|
342
|
+
if (!attrValue) continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
324
345
|
result += ` ${attrName}="${escapeHtml(attrValue)}"`;
|
|
325
346
|
}
|
|
326
347
|
|
package/runtime/ssr.js
CHANGED
|
@@ -408,12 +408,16 @@ export function deserializeState(data) {
|
|
|
408
408
|
* // Default implementation just stores in global
|
|
409
409
|
* restoreState(window.__PULSE_STATE__);
|
|
410
410
|
*/
|
|
411
|
+
// Module-scoped state store (avoids globalThis collision between multiple SSR instances)
|
|
412
|
+
let _ssrState = null;
|
|
413
|
+
|
|
411
414
|
export function restoreState(state) {
|
|
412
415
|
const deserialized = typeof state === 'string'
|
|
413
416
|
? deserializeState(state)
|
|
414
417
|
: state;
|
|
415
418
|
|
|
416
|
-
// Store in global for
|
|
419
|
+
// Store in module scope and global for backward compatibility
|
|
420
|
+
_ssrState = deserialized;
|
|
417
421
|
if (typeof globalThis !== 'undefined') {
|
|
418
422
|
globalThis.__PULSE_SSR_STATE__ = deserialized;
|
|
419
423
|
}
|
|
@@ -430,10 +434,20 @@ export function restoreState(state) {
|
|
|
430
434
|
* const userData = getSSRState('user');
|
|
431
435
|
*/
|
|
432
436
|
export function getSSRState(key) {
|
|
433
|
-
const state = globalThis?.__PULSE_SSR_STATE__ || {};
|
|
437
|
+
const state = _ssrState || globalThis?.__PULSE_SSR_STATE__ || {};
|
|
434
438
|
return key ? state[key] : state;
|
|
435
439
|
}
|
|
436
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Clear the SSR state. Use in tests or when cleaning up SSR context.
|
|
443
|
+
*/
|
|
444
|
+
export function clearSSRState() {
|
|
445
|
+
_ssrState = null;
|
|
446
|
+
if (typeof globalThis !== 'undefined') {
|
|
447
|
+
delete globalThis.__PULSE_SSR_STATE__;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
437
451
|
// ============================================================================
|
|
438
452
|
// Re-exports for convenience
|
|
439
453
|
// ============================================================================
|
|
@@ -456,6 +470,7 @@ export default {
|
|
|
456
470
|
deserializeState,
|
|
457
471
|
restoreState,
|
|
458
472
|
getSSRState,
|
|
473
|
+
clearSSRState,
|
|
459
474
|
|
|
460
475
|
// Mode checks
|
|
461
476
|
isSSR,
|
package/types/a11y.d.ts
CHANGED
|
@@ -91,6 +91,24 @@ export function restoreFocus(): void;
|
|
|
91
91
|
*/
|
|
92
92
|
export function clearFocusStack(): void;
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Handle escape key press within a container
|
|
96
|
+
* @returns Cleanup function to remove listener
|
|
97
|
+
*/
|
|
98
|
+
export function onEscapeKey(
|
|
99
|
+
container: HTMLElement,
|
|
100
|
+
onEscape: () => void,
|
|
101
|
+
options?: { stopPropagation?: boolean }
|
|
102
|
+
): () => void;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Track keyboard vs mouse focus for :focus-visible styling
|
|
106
|
+
*/
|
|
107
|
+
export function createFocusVisibleTracker(): {
|
|
108
|
+
isKeyboardUser: Pulse<boolean>;
|
|
109
|
+
cleanup: () => void;
|
|
110
|
+
};
|
|
111
|
+
|
|
94
112
|
// =============================================================================
|
|
95
113
|
// SKIP LINKS
|
|
96
114
|
// =============================================================================
|
|
@@ -138,10 +156,28 @@ export function prefersColorScheme(): 'light' | 'dark' | 'no-preference';
|
|
|
138
156
|
*/
|
|
139
157
|
export function prefersHighContrast(): boolean;
|
|
140
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Check if user prefers reduced transparency
|
|
161
|
+
*/
|
|
162
|
+
export function prefersReducedTransparency(): boolean;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if forced colors mode is active (Windows High Contrast Mode)
|
|
166
|
+
*/
|
|
167
|
+
export function forcedColorsMode(): 'active' | 'none';
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check user's preferred contrast level
|
|
171
|
+
*/
|
|
172
|
+
export function prefersContrast(): 'no-preference' | 'more' | 'less' | 'custom';
|
|
173
|
+
|
|
141
174
|
export interface UserPreferences {
|
|
142
175
|
reducedMotion: Pulse<boolean>;
|
|
143
176
|
colorScheme: Pulse<'light' | 'dark' | 'no-preference'>;
|
|
144
177
|
highContrast: Pulse<boolean>;
|
|
178
|
+
reducedTransparency?: Pulse<boolean>;
|
|
179
|
+
forcedColors?: Pulse<'active' | 'none'>;
|
|
180
|
+
contrast?: Pulse<'no-preference' | 'more' | 'less' | 'custom'>;
|
|
145
181
|
}
|
|
146
182
|
|
|
147
183
|
/**
|
|
@@ -209,6 +245,96 @@ export function createTabs(
|
|
|
209
245
|
options?: TabsOptions
|
|
210
246
|
): TabsControl;
|
|
211
247
|
|
|
248
|
+
// =============================================================================
|
|
249
|
+
// ARIA WIDGETS
|
|
250
|
+
// =============================================================================
|
|
251
|
+
|
|
252
|
+
export interface ModalOptions {
|
|
253
|
+
labelledBy?: string;
|
|
254
|
+
describedBy?: string;
|
|
255
|
+
closeOnBackdropClick?: boolean;
|
|
256
|
+
onClose?: () => void;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface ModalControl {
|
|
260
|
+
open: () => void;
|
|
261
|
+
close: () => void;
|
|
262
|
+
isOpen: Pulse<boolean>;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Create an ARIA-compliant modal dialog
|
|
267
|
+
*/
|
|
268
|
+
export function createModal(
|
|
269
|
+
dialog: HTMLElement,
|
|
270
|
+
options?: ModalOptions
|
|
271
|
+
): ModalControl;
|
|
272
|
+
|
|
273
|
+
export interface TooltipOptions {
|
|
274
|
+
showDelay?: number;
|
|
275
|
+
hideDelay?: number;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface TooltipControl {
|
|
279
|
+
isVisible: Pulse<boolean>;
|
|
280
|
+
cleanup: () => void;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Create an ARIA-compliant tooltip
|
|
285
|
+
*/
|
|
286
|
+
export function createTooltip(
|
|
287
|
+
trigger: HTMLElement,
|
|
288
|
+
tooltip: HTMLElement,
|
|
289
|
+
options?: TooltipOptions
|
|
290
|
+
): TooltipControl;
|
|
291
|
+
|
|
292
|
+
export interface AccordionOptions {
|
|
293
|
+
triggerSelector?: string;
|
|
294
|
+
panelSelector?: string;
|
|
295
|
+
allowMultiple?: boolean;
|
|
296
|
+
defaultOpen?: number;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export interface AccordionControl {
|
|
300
|
+
open: (index: number) => void;
|
|
301
|
+
close: (index: number) => void;
|
|
302
|
+
toggle: (index: number) => void;
|
|
303
|
+
closeAll: () => void;
|
|
304
|
+
openIndices: Pulse<number[]>;
|
|
305
|
+
cleanup: () => void;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Create an ARIA-compliant accordion
|
|
310
|
+
*/
|
|
311
|
+
export function createAccordion(
|
|
312
|
+
container: HTMLElement,
|
|
313
|
+
options?: AccordionOptions
|
|
314
|
+
): AccordionControl;
|
|
315
|
+
|
|
316
|
+
export interface MenuOptions {
|
|
317
|
+
itemSelector?: string;
|
|
318
|
+
closeOnSelect?: boolean;
|
|
319
|
+
onSelect?: (element: HTMLElement, index: number) => void;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export interface MenuControl {
|
|
323
|
+
open: () => void;
|
|
324
|
+
close: () => void;
|
|
325
|
+
toggle: () => void;
|
|
326
|
+
cleanup: () => void;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Create an ARIA-compliant dropdown menu
|
|
331
|
+
*/
|
|
332
|
+
export function createMenu(
|
|
333
|
+
button: HTMLElement,
|
|
334
|
+
menu: HTMLElement,
|
|
335
|
+
options?: MenuOptions
|
|
336
|
+
): MenuControl;
|
|
337
|
+
|
|
212
338
|
// =============================================================================
|
|
213
339
|
// KEYBOARD NAVIGATION
|
|
214
340
|
// =============================================================================
|
|
@@ -260,6 +386,61 @@ export function logA11yIssues(issues: A11yIssue[]): void;
|
|
|
260
386
|
*/
|
|
261
387
|
export function highlightA11yIssues(issues: A11yIssue[]): () => void;
|
|
262
388
|
|
|
389
|
+
// =============================================================================
|
|
390
|
+
// COLOR CONTRAST
|
|
391
|
+
// =============================================================================
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Calculate WCAG contrast ratio between two colors
|
|
395
|
+
*/
|
|
396
|
+
export function getContrastRatio(foreground: string, background: string): number;
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Check if a contrast ratio meets WCAG requirements
|
|
400
|
+
*/
|
|
401
|
+
export function meetsContrastRequirement(
|
|
402
|
+
ratio: number,
|
|
403
|
+
level?: 'AA' | 'AAA',
|
|
404
|
+
textSize?: 'normal' | 'large'
|
|
405
|
+
): boolean;
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Get the effective background color of an element (handles transparency)
|
|
409
|
+
*/
|
|
410
|
+
export function getEffectiveBackgroundColor(element: HTMLElement): string;
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Check contrast of a specific element
|
|
414
|
+
*/
|
|
415
|
+
export function checkElementContrast(
|
|
416
|
+
element: HTMLElement,
|
|
417
|
+
level?: 'AA' | 'AAA'
|
|
418
|
+
): {
|
|
419
|
+
ratio: number;
|
|
420
|
+
passes: boolean;
|
|
421
|
+
foreground: string;
|
|
422
|
+
background: string;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// =============================================================================
|
|
426
|
+
// ANNOUNCEMENT QUEUE
|
|
427
|
+
// =============================================================================
|
|
428
|
+
|
|
429
|
+
export interface AnnouncementQueueOptions {
|
|
430
|
+
minDelay?: number;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export interface AnnouncementQueue {
|
|
434
|
+
add: (message: string, options?: { priority?: 'polite' | 'assertive' }) => void;
|
|
435
|
+
clear: () => void;
|
|
436
|
+
queueLength: Pulse<number>;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Create a queue for managing multiple announcements
|
|
441
|
+
*/
|
|
442
|
+
export function createAnnouncementQueue(options?: AnnouncementQueueOptions): AnnouncementQueue;
|
|
443
|
+
|
|
263
444
|
// =============================================================================
|
|
264
445
|
// UTILITIES
|
|
265
446
|
// =============================================================================
|
|
@@ -285,6 +466,11 @@ export function makeInert(element: HTMLElement): () => void;
|
|
|
285
466
|
*/
|
|
286
467
|
export function srOnly(text: string): HTMLSpanElement;
|
|
287
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Compute the accessible name of an element (aria-label, text content, etc.)
|
|
471
|
+
*/
|
|
472
|
+
export function getAccessibleName(element: HTMLElement): string;
|
|
473
|
+
|
|
288
474
|
// =============================================================================
|
|
289
475
|
// DEFAULT EXPORT
|
|
290
476
|
// =============================================================================
|