pulse-js-framework 1.10.4 → 1.11.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.
- package/README.md +11 -0
- package/cli/build.js +13 -3
- package/compiler/directives.js +356 -0
- package/compiler/lexer.js +18 -3
- package/compiler/parser/core.js +6 -0
- package/compiler/parser/view.js +2 -6
- package/compiler/preprocessor.js +43 -23
- package/compiler/sourcemap.js +3 -1
- package/compiler/transformer/actions.js +329 -0
- package/compiler/transformer/export.js +7 -0
- package/compiler/transformer/expressions.js +85 -33
- package/compiler/transformer/imports.js +3 -0
- package/compiler/transformer/index.js +2 -0
- package/compiler/transformer/store.js +1 -1
- package/compiler/transformer/style.js +45 -16
- package/compiler/transformer/view.js +23 -2
- package/loader/rollup-plugin-server-components.js +391 -0
- package/loader/vite-plugin-server-components.js +420 -0
- package/loader/webpack-loader-server-components.js +356 -0
- package/package.json +124 -82
- package/runtime/async.js +4 -0
- package/runtime/context.js +16 -3
- package/runtime/dom-adapter.js +5 -3
- package/runtime/dom-virtual-list.js +2 -1
- package/runtime/form.js +8 -3
- package/runtime/graphql/cache.js +1 -1
- package/runtime/graphql/client.js +22 -0
- package/runtime/graphql/hooks.js +12 -6
- package/runtime/graphql/subscriptions.js +2 -0
- package/runtime/hmr.js +6 -3
- package/runtime/http.js +1 -0
- package/runtime/i18n.js +2 -0
- package/runtime/lru-cache.js +3 -1
- package/runtime/native.js +46 -20
- package/runtime/pulse.js +3 -0
- package/runtime/router/core.js +5 -1
- package/runtime/router/index.js +17 -1
- package/runtime/router/psc-integration.js +301 -0
- package/runtime/security.js +58 -29
- package/runtime/server-components/actions-server.js +798 -0
- package/runtime/server-components/actions.js +389 -0
- package/runtime/server-components/client.js +447 -0
- package/runtime/server-components/error-sanitizer.js +438 -0
- package/runtime/server-components/index.js +275 -0
- package/runtime/server-components/security-csrf.js +593 -0
- package/runtime/server-components/security-errors.js +227 -0
- package/runtime/server-components/security-ratelimit.js +733 -0
- package/runtime/server-components/security-validation.js +467 -0
- package/runtime/server-components/security.js +598 -0
- package/runtime/server-components/serializer.js +617 -0
- package/runtime/server-components/server.js +382 -0
- package/runtime/server-components/types.js +383 -0
- package/runtime/server-components/utils/mutex.js +60 -0
- package/runtime/server-components/utils/path-sanitizer.js +109 -0
- package/runtime/ssr.js +2 -1
- package/runtime/store.js +19 -10
- package/runtime/utils.js +12 -128
- package/types/animation.d.ts +300 -0
- package/types/i18n.d.ts +283 -0
- package/types/persistence.d.ts +267 -0
- package/types/sse.d.ts +248 -0
- package/types/sw.d.ts +150 -0
- package/runtime/a11y.js.original +0 -1844
- package/runtime/graphql.js.original +0 -1326
- package/runtime/router.js.original +0 -1605
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PSC Integration for Router - Pulse Framework
|
|
3
|
+
*
|
|
4
|
+
* Handles Server Component navigation, caching, and prefetching.
|
|
5
|
+
* Provides client-side navigation with Server Component updates via PSC Wire Format.
|
|
6
|
+
*
|
|
7
|
+
* @module runtime/router/psc-integration
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { reconstructPSCTree } from '../server-components/index.js';
|
|
11
|
+
|
|
12
|
+
// ============================================================
|
|
13
|
+
// PSC Cache (LRU)
|
|
14
|
+
// ============================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* LRU Cache for PSC payloads
|
|
18
|
+
* @class PSCCache
|
|
19
|
+
*/
|
|
20
|
+
class PSCCache {
|
|
21
|
+
/**
|
|
22
|
+
* @param {number} maxSize - Maximum cache size
|
|
23
|
+
*/
|
|
24
|
+
constructor(maxSize = 50) {
|
|
25
|
+
this.cache = new Map();
|
|
26
|
+
this.maxSize = maxSize;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get cached entry (moves to most recent)
|
|
31
|
+
* @param {string} key - Cache key
|
|
32
|
+
* @returns {any|null} Cached value or null
|
|
33
|
+
*/
|
|
34
|
+
get(key) {
|
|
35
|
+
if (!this.cache.has(key)) return null;
|
|
36
|
+
|
|
37
|
+
const entry = this.cache.get(key);
|
|
38
|
+
// Move to end (most recent)
|
|
39
|
+
this.cache.delete(key);
|
|
40
|
+
this.cache.set(key, entry);
|
|
41
|
+
return entry;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Set cache entry (evicts oldest if at capacity)
|
|
46
|
+
* @param {string} key - Cache key
|
|
47
|
+
* @param {any} value - Value to cache
|
|
48
|
+
*/
|
|
49
|
+
set(key, value) {
|
|
50
|
+
// Delete if exists (will re-add at end)
|
|
51
|
+
if (this.cache.has(key)) {
|
|
52
|
+
this.cache.delete(key);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Evict oldest if at capacity
|
|
56
|
+
if (this.cache.size >= this.maxSize) {
|
|
57
|
+
const firstKey = this.cache.keys().next().value;
|
|
58
|
+
this.cache.delete(firstKey);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.cache.set(key, value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if key exists in cache
|
|
66
|
+
* @param {string} key - Cache key
|
|
67
|
+
* @returns {boolean} True if key exists
|
|
68
|
+
*/
|
|
69
|
+
has(key) {
|
|
70
|
+
return this.cache.has(key);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Clear all cache entries
|
|
75
|
+
*/
|
|
76
|
+
clear() {
|
|
77
|
+
this.cache.clear();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get cache size
|
|
82
|
+
* @returns {number} Number of cached entries
|
|
83
|
+
*/
|
|
84
|
+
get size() {
|
|
85
|
+
return this.cache.size;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ============================================================
|
|
90
|
+
// Cache Instance & Prefetch Queue
|
|
91
|
+
// ============================================================
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Global PSC cache instance
|
|
95
|
+
* @type {PSCCache}
|
|
96
|
+
*/
|
|
97
|
+
export const pscCache = new PSCCache();
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Prefetch queue (tracks in-flight prefetches)
|
|
101
|
+
* @type {Set<string>}
|
|
102
|
+
*/
|
|
103
|
+
const prefetchQueue = new Set();
|
|
104
|
+
|
|
105
|
+
// ============================================================
|
|
106
|
+
// PSC Fetch
|
|
107
|
+
// ============================================================
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Fetch PSC payload from server
|
|
111
|
+
* @param {string} url - URL to fetch
|
|
112
|
+
* @param {Object} options - Fetch options
|
|
113
|
+
* @param {Object} [options.query={}] - Query parameters
|
|
114
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
115
|
+
* @returns {Promise<Object>} PSC payload
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* const payload = await fetchPSCPayload('/products/123', {
|
|
119
|
+
* query: { tab: 'reviews' }
|
|
120
|
+
* });
|
|
121
|
+
*/
|
|
122
|
+
export async function fetchPSCPayload(url, options = {}) {
|
|
123
|
+
const { query = {}, signal } = options;
|
|
124
|
+
|
|
125
|
+
// Build query string
|
|
126
|
+
const queryString = new URLSearchParams(query).toString();
|
|
127
|
+
const fullUrl = `${url}${queryString ? '?' + queryString : ''}`;
|
|
128
|
+
|
|
129
|
+
// Fetch from server with PSC headers
|
|
130
|
+
const response = await fetch(fullUrl, {
|
|
131
|
+
headers: {
|
|
132
|
+
'Accept': 'application/x-pulse-psc',
|
|
133
|
+
'X-Pulse-Request': 'navigation'
|
|
134
|
+
},
|
|
135
|
+
signal
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new Error(`PSC fetch failed: ${response.status} ${response.statusText}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return response.json();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ============================================================
|
|
146
|
+
// PSC Navigation
|
|
147
|
+
// ============================================================
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Navigate to Server Component route
|
|
151
|
+
* @param {string} url - URL to navigate to
|
|
152
|
+
* @param {Object} options - Navigation options
|
|
153
|
+
* @param {Object} [options.query={}] - Query parameters
|
|
154
|
+
* @param {string} [options.cacheKey] - Cache key (auto-generated if not provided)
|
|
155
|
+
* @param {number} [options.staleTime=60000] - Cache staleness threshold (ms)
|
|
156
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
157
|
+
* @returns {Promise<Object>} PSC payload
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* const payload = await navigatePSC('/products/123', {
|
|
161
|
+
* query: { tab: 'reviews' },
|
|
162
|
+
* staleTime: 30000
|
|
163
|
+
* });
|
|
164
|
+
*/
|
|
165
|
+
export async function navigatePSC(url, options = {}) {
|
|
166
|
+
const { query = {}, cacheKey, signal } = options;
|
|
167
|
+
const staleTime = options.staleTime ?? 60000;
|
|
168
|
+
|
|
169
|
+
// Generate cache key if not provided
|
|
170
|
+
const key = cacheKey || (url + JSON.stringify(query));
|
|
171
|
+
|
|
172
|
+
// Check cache
|
|
173
|
+
if (pscCache.has(key)) {
|
|
174
|
+
const cached = pscCache.get(key);
|
|
175
|
+
const age = Date.now() - cached.timestamp;
|
|
176
|
+
|
|
177
|
+
if (age < staleTime) {
|
|
178
|
+
return cached.payload; // Fresh cache hit
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Fetch fresh payload
|
|
183
|
+
const payload = await fetchPSCPayload(url, { query, signal });
|
|
184
|
+
|
|
185
|
+
// Cache it
|
|
186
|
+
pscCache.set(key, {
|
|
187
|
+
payload,
|
|
188
|
+
timestamp: Date.now()
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return payload;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ============================================================
|
|
195
|
+
// PSC Prefetching
|
|
196
|
+
// ============================================================
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Prefetch PSC payload in background
|
|
200
|
+
* @param {string} url - URL to prefetch
|
|
201
|
+
* @param {Object} options - Prefetch options
|
|
202
|
+
* @param {Object} [options.query={}] - Query parameters
|
|
203
|
+
* @param {number} [options.staleTime=60000] - Cache staleness threshold (ms)
|
|
204
|
+
* @returns {Promise<void>} Resolves when prefetch completes
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* // Prefetch on link hover
|
|
208
|
+
* linkElement.addEventListener('mouseenter', () => {
|
|
209
|
+
* prefetchPSC('/products/123');
|
|
210
|
+
* });
|
|
211
|
+
*/
|
|
212
|
+
export function prefetchPSC(url, options = {}) {
|
|
213
|
+
const { query = {} } = options;
|
|
214
|
+
const cacheKey = url + JSON.stringify(query);
|
|
215
|
+
|
|
216
|
+
// Skip if already prefetching or cached
|
|
217
|
+
if (prefetchQueue.has(cacheKey) || pscCache.has(cacheKey)) {
|
|
218
|
+
return Promise.resolve();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Add to queue
|
|
222
|
+
prefetchQueue.add(cacheKey);
|
|
223
|
+
|
|
224
|
+
// Navigate (which fetches and caches)
|
|
225
|
+
return navigatePSC(url, { ...options, cacheKey })
|
|
226
|
+
.then(() => {
|
|
227
|
+
prefetchQueue.delete(cacheKey);
|
|
228
|
+
})
|
|
229
|
+
.catch((err) => {
|
|
230
|
+
console.warn('PSC prefetch failed:', err);
|
|
231
|
+
prefetchQueue.delete(cacheKey);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ============================================================
|
|
236
|
+
// Cache Management
|
|
237
|
+
// ============================================================
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Clear PSC cache
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* clearPSCCache(); // Clear all cached PSC payloads
|
|
244
|
+
*/
|
|
245
|
+
export function clearPSCCache() {
|
|
246
|
+
pscCache.clear();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get cache statistics
|
|
251
|
+
* @returns {Object} Cache stats
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* const stats = getPSCCacheStats();
|
|
255
|
+
* console.log(`Cache: ${stats.size}/${stats.maxSize} entries`);
|
|
256
|
+
*/
|
|
257
|
+
export function getPSCCacheStats() {
|
|
258
|
+
return {
|
|
259
|
+
size: pscCache.size,
|
|
260
|
+
maxSize: pscCache.maxSize,
|
|
261
|
+
prefetching: prefetchQueue.size
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Configure PSC cache
|
|
267
|
+
* @param {Object} options - Configuration options
|
|
268
|
+
* @param {number} [options.maxSize] - Maximum cache size
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* configurePSCCache({ maxSize: 100 }); // Increase cache size
|
|
272
|
+
*/
|
|
273
|
+
export function configurePSCCache(options = {}) {
|
|
274
|
+
if (options.maxSize !== undefined) {
|
|
275
|
+
// Create new cache with new max size
|
|
276
|
+
const newCache = new PSCCache(options.maxSize);
|
|
277
|
+
|
|
278
|
+
// Copy existing entries (up to new max size)
|
|
279
|
+
const entries = Array.from(pscCache.cache.entries()).slice(0, options.maxSize);
|
|
280
|
+
entries.forEach(([key, value]) => newCache.set(key, value));
|
|
281
|
+
|
|
282
|
+
// Replace global cache
|
|
283
|
+
pscCache.cache = newCache.cache;
|
|
284
|
+
pscCache.maxSize = newCache.maxSize;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ============================================================
|
|
289
|
+
// Exports
|
|
290
|
+
// ============================================================
|
|
291
|
+
|
|
292
|
+
export default {
|
|
293
|
+
PSCCache,
|
|
294
|
+
pscCache,
|
|
295
|
+
fetchPSCPayload,
|
|
296
|
+
navigatePSC,
|
|
297
|
+
prefetchPSC,
|
|
298
|
+
clearPSCCache,
|
|
299
|
+
getPSCCacheStats,
|
|
300
|
+
configurePSCCache
|
|
301
|
+
};
|
package/runtime/security.js
CHANGED
|
@@ -166,10 +166,11 @@ export function sanitizeObjectKeys(obj, options = {}) {
|
|
|
166
166
|
// =============================================================================
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
|
-
* HTML entity escape map
|
|
170
|
-
*
|
|
169
|
+
* HTML entity escape map.
|
|
170
|
+
* Single source of truth — imported by utils.js and error-sanitizer.js.
|
|
171
|
+
* @type {Object<string, string>}
|
|
171
172
|
*/
|
|
172
|
-
const HTML_ESCAPES = {
|
|
173
|
+
export const HTML_ESCAPES = {
|
|
173
174
|
'&': '&',
|
|
174
175
|
'<': '<',
|
|
175
176
|
'>': '>',
|
|
@@ -177,10 +178,17 @@ const HTML_ESCAPES = {
|
|
|
177
178
|
"'": '''
|
|
178
179
|
};
|
|
179
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Pre-compiled regex for HTML special characters (generated from HTML_ESCAPES keys).
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
const HTML_ESCAPE_REGEX = new RegExp(`[${Object.keys(HTML_ESCAPES).join('')}]`, 'g');
|
|
186
|
+
|
|
180
187
|
/**
|
|
181
188
|
* Escape HTML special characters to prevent XSS.
|
|
189
|
+
* Single source of truth — re-exported by utils.js.
|
|
182
190
|
*
|
|
183
|
-
* @param {
|
|
191
|
+
* @param {*} str - Value to escape (will be converted to string)
|
|
184
192
|
* @returns {string} Escaped string safe for HTML insertion
|
|
185
193
|
*
|
|
186
194
|
* @example
|
|
@@ -189,7 +197,7 @@ const HTML_ESCAPES = {
|
|
|
189
197
|
*/
|
|
190
198
|
export function escapeHtml(str) {
|
|
191
199
|
if (str == null) return '';
|
|
192
|
-
return String(str).replace(
|
|
200
|
+
return String(str).replace(HTML_ESCAPE_REGEX, char => HTML_ESCAPES[char]);
|
|
193
201
|
}
|
|
194
202
|
|
|
195
203
|
/**
|
|
@@ -398,38 +406,36 @@ export function sanitizeUrl(url, options = {}) {
|
|
|
398
406
|
|
|
399
407
|
const trimmed = String(url).trim();
|
|
400
408
|
|
|
401
|
-
// Decode URL to catch encoded attacks
|
|
409
|
+
// Decode URL to catch encoded attacks like javascript:
|
|
410
|
+
// Also handles %6A%61%76%61%73%63%72%69%70%74 encoding
|
|
402
411
|
let decoded = trimmed;
|
|
403
412
|
try {
|
|
404
|
-
// Decode HTML entities first
|
|
413
|
+
// Decode HTML entities first (j -> j)
|
|
405
414
|
decoded = decoded.replace(/&#x([0-9a-f]+);?/gi, (_, hex) =>
|
|
406
415
|
String.fromCharCode(parseInt(hex, 16))
|
|
407
416
|
);
|
|
408
417
|
decoded = decoded.replace(/&#(\d+);?/g, (_, dec) =>
|
|
409
418
|
String.fromCharCode(parseInt(dec, 10))
|
|
410
419
|
);
|
|
411
|
-
// Then decode URI encoding
|
|
420
|
+
// Then decode URI encoding (%6A -> j)
|
|
412
421
|
decoded = decodeURIComponent(decoded);
|
|
413
422
|
} catch {
|
|
414
|
-
//
|
|
423
|
+
// If decoding fails, use original (malformed URLs will be blocked anyway)
|
|
415
424
|
}
|
|
416
425
|
|
|
417
|
-
// Normalize and
|
|
426
|
+
// Normalize: lowercase and remove whitespace for protocol check
|
|
418
427
|
const normalized = decoded.toLowerCase().replace(/[\s\x00-\x1f]/g, '');
|
|
419
428
|
|
|
420
|
-
// Block
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
if (normalized.startsWith('vbscript:')) {
|
|
428
|
-
log.warn('Blocked vbscript: URL');
|
|
429
|
-
return null;
|
|
429
|
+
// Block dangerous protocols
|
|
430
|
+
const dangerousProtocols = ['javascript:', 'vbscript:', 'file:'];
|
|
431
|
+
for (const protocol of dangerousProtocols) {
|
|
432
|
+
if (normalized.startsWith(protocol)) {
|
|
433
|
+
log.warn(`Blocked ${protocol} URL`);
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
430
436
|
}
|
|
431
437
|
|
|
432
|
-
// Check data:
|
|
438
|
+
// Check for data: protocol
|
|
433
439
|
if (normalized.startsWith('data:')) {
|
|
434
440
|
if (!allowData) {
|
|
435
441
|
log.warn('Blocked data: URL (not allowed)');
|
|
@@ -440,20 +446,42 @@ export function sanitizeUrl(url, options = {}) {
|
|
|
440
446
|
log.warn('Blocked dangerous data: URL');
|
|
441
447
|
return null;
|
|
442
448
|
}
|
|
449
|
+
return trimmed;
|
|
443
450
|
}
|
|
444
451
|
|
|
445
|
-
// Check blob:
|
|
446
|
-
if (normalized.startsWith('blob:')
|
|
447
|
-
|
|
448
|
-
|
|
452
|
+
// Check for blob: protocol
|
|
453
|
+
if (normalized.startsWith('blob:')) {
|
|
454
|
+
if (!allowBlob) {
|
|
455
|
+
log.warn('Blocked blob: URL (not allowed)');
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
return trimmed;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Allow relative URLs (must start with / or . to prevent //evil.com attacks)
|
|
462
|
+
if (allowRelative) {
|
|
463
|
+
if (trimmed.startsWith('/') && !trimmed.startsWith('//')) {
|
|
464
|
+
return trimmed;
|
|
465
|
+
}
|
|
466
|
+
if (trimmed.startsWith('./') || trimmed.startsWith('../')) {
|
|
467
|
+
return trimmed;
|
|
468
|
+
}
|
|
469
|
+
// URLs without protocol that don't start with // are relative
|
|
470
|
+
if (!trimmed.includes(':') && !trimmed.startsWith('//')) {
|
|
471
|
+
return trimmed;
|
|
472
|
+
}
|
|
449
473
|
}
|
|
450
474
|
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
|
|
475
|
+
// Allow safe protocols (http, https, mailto, tel, sms, ftp, sftp)
|
|
476
|
+
const colonIndex = trimmed.indexOf(':');
|
|
477
|
+
if (colonIndex > 0) {
|
|
478
|
+
const protocol = trimmed.slice(0, colonIndex + 1).toLowerCase();
|
|
479
|
+
if (SAFE_PROTOCOLS.has(protocol)) {
|
|
480
|
+
return trimmed;
|
|
481
|
+
}
|
|
454
482
|
}
|
|
455
483
|
|
|
456
|
-
return
|
|
484
|
+
return null;
|
|
457
485
|
}
|
|
458
486
|
|
|
459
487
|
// =============================================================================
|
|
@@ -468,6 +496,7 @@ export default {
|
|
|
468
496
|
SAFE_PROTOCOLS,
|
|
469
497
|
DEFAULT_ALLOWED_TAGS,
|
|
470
498
|
DEFAULT_ALLOWED_ATTRS,
|
|
499
|
+
HTML_ESCAPES,
|
|
471
500
|
|
|
472
501
|
// Validation
|
|
473
502
|
isDangerousKey,
|