@zolomedia/bifrost-client 1.7.74

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.
Files changed (140) hide show
  1. package/L1_Foundation/L1_Foundation.js +13 -0
  2. package/L1_Foundation/bootstrap/bootstrap.js +11 -0
  3. package/L1_Foundation/bootstrap/bootstrap_hooks.js +123 -0
  4. package/L1_Foundation/bootstrap/bootstrap_index.js +15 -0
  5. package/L1_Foundation/bootstrap/bootstrap_logger.js +135 -0
  6. package/L1_Foundation/bootstrap/cdn_loader.js +217 -0
  7. package/L1_Foundation/bootstrap/module_registry.js +102 -0
  8. package/L1_Foundation/bootstrap/prism_loader.js +164 -0
  9. package/L1_Foundation/config/client_config.js +110 -0
  10. package/L1_Foundation/config/config.js +7 -0
  11. package/L1_Foundation/connection/connection.js +8 -0
  12. package/L1_Foundation/connection/websocket_connection.js +122 -0
  13. package/L1_Foundation/constants/bifrost_constants.js +284 -0
  14. package/L1_Foundation/constants/constants.js +7 -0
  15. package/L1_Foundation/logger/logger.js +10 -0
  16. package/L2_Handling/L2_Handling.js +15 -0
  17. package/L2_Handling/cache/cache.js +22 -0
  18. package/L2_Handling/cache/cache_constants.js +69 -0
  19. package/L2_Handling/cache/orchestration/cache_manager.js +299 -0
  20. package/L2_Handling/cache/orchestration/cache_orchestrator.js +260 -0
  21. package/L2_Handling/cache/orchestration/orchestration.js +12 -0
  22. package/L2_Handling/cache/storage/session_manager.js +289 -0
  23. package/L2_Handling/cache/storage/storage.js +10 -0
  24. package/L2_Handling/cache/storage/storage_manager.js +590 -0
  25. package/L2_Handling/display/composite/composite.js +13 -0
  26. package/L2_Handling/display/composite/dashboard_renderer.js +221 -0
  27. package/L2_Handling/display/composite/swiper_renderer.js +564 -0
  28. package/L2_Handling/display/composite/terminal_renderer.js +922 -0
  29. package/L2_Handling/display/composite/wizard_conditional_renderer.js +274 -0
  30. package/L2_Handling/display/display.js +30 -0
  31. package/L2_Handling/display/feedback/feedback.js +11 -0
  32. package/L2_Handling/display/feedback/progressbar_renderer.js +418 -0
  33. package/L2_Handling/display/feedback/spinner_renderer.js +246 -0
  34. package/L2_Handling/display/inputs/button_renderer.js +634 -0
  35. package/L2_Handling/display/inputs/form_renderer.js +583 -0
  36. package/L2_Handling/display/inputs/input_renderer.js +658 -0
  37. package/L2_Handling/display/inputs/inputs.js +12 -0
  38. package/L2_Handling/display/navigation/menu_renderer.js +206 -0
  39. package/L2_Handling/display/navigation/navigation.js +11 -0
  40. package/L2_Handling/display/navigation/navigation_renderer.js +703 -0
  41. package/L2_Handling/display/orchestration/orchestration.js +11 -0
  42. package/L2_Handling/display/orchestration/renderer.js +430 -0
  43. package/L2_Handling/display/orchestration/zdisplay_orchestrator.js +1759 -0
  44. package/L2_Handling/display/outputs/alert_renderer.js +161 -0
  45. package/L2_Handling/display/outputs/audio_renderer.js +94 -0
  46. package/L2_Handling/display/outputs/card_renderer.js +229 -0
  47. package/L2_Handling/display/outputs/code_renderer.js +66 -0
  48. package/L2_Handling/display/outputs/dl_renderer.js +131 -0
  49. package/L2_Handling/display/outputs/header_renderer.js +162 -0
  50. package/L2_Handling/display/outputs/icon_renderer.js +107 -0
  51. package/L2_Handling/display/outputs/image_renderer.js +145 -0
  52. package/L2_Handling/display/outputs/list_renderer.js +190 -0
  53. package/L2_Handling/display/outputs/outputs.js +19 -0
  54. package/L2_Handling/display/outputs/table_renderer.js +765 -0
  55. package/L2_Handling/display/outputs/text_renderer.js +818 -0
  56. package/L2_Handling/display/outputs/typography_renderer.js +293 -0
  57. package/L2_Handling/display/outputs/video_renderer.js +116 -0
  58. package/L2_Handling/display/primitives/document_structure_primitives.js +319 -0
  59. package/L2_Handling/display/primitives/form_primitives.js +526 -0
  60. package/L2_Handling/display/primitives/generic_containers.js +109 -0
  61. package/L2_Handling/display/primitives/interactive_primitives.js +305 -0
  62. package/L2_Handling/display/primitives/link_primitives.js +552 -0
  63. package/L2_Handling/display/primitives/lists_primitives.js +262 -0
  64. package/L2_Handling/display/primitives/media_primitives.js +383 -0
  65. package/L2_Handling/display/primitives/primitives.js +19 -0
  66. package/L2_Handling/display/primitives/semantic_element_primitive.js +226 -0
  67. package/L2_Handling/display/primitives/table_primitives.js +528 -0
  68. package/L2_Handling/display/primitives/typography_primitives.js +175 -0
  69. package/L2_Handling/display/specialized/input_request_renderer.js +467 -0
  70. package/L2_Handling/display/specialized/specialized.js +10 -0
  71. package/L2_Handling/hooks/hooks.js +9 -0
  72. package/L2_Handling/hooks/menu_integration.js +57 -0
  73. package/L2_Handling/hooks/widget_hook_manager.js +292 -0
  74. package/L2_Handling/message/message.js +8 -0
  75. package/L2_Handling/message/message_handler.js +701 -0
  76. package/L2_Handling/navigation/navigation.js +8 -0
  77. package/L2_Handling/navigation/navigation_manager.js +403 -0
  78. package/L2_Handling/zhooks/features/cache_live.js +287 -0
  79. package/L2_Handling/zhooks/features/crumbs_live.js +292 -0
  80. package/L2_Handling/zhooks/zhooks_manager.js +65 -0
  81. package/L2_Handling/zvaf/zvaf.js +8 -0
  82. package/L2_Handling/zvaf/zvaf_manager.js +334 -0
  83. package/L3_Abstraction/L3_Abstraction.js +12 -0
  84. package/L3_Abstraction/orchestrator/container_unwrapper.js +101 -0
  85. package/L3_Abstraction/orchestrator/group_renderer.js +698 -0
  86. package/L3_Abstraction/orchestrator/input_event_handler.js +797 -0
  87. package/L3_Abstraction/orchestrator/metadata_processor.js +249 -0
  88. package/L3_Abstraction/orchestrator/navbar_builder.js +201 -0
  89. package/L3_Abstraction/orchestrator/orchestrator.js +13 -0
  90. package/L3_Abstraction/orchestrator/wizard_gate_handler.js +360 -0
  91. package/L3_Abstraction/renderer/renderer.js +1 -0
  92. package/L3_Abstraction/session/session.js +1 -0
  93. package/L4_Orchestration/L4_Orchestration.js +11 -0
  94. package/L4_Orchestration/client/client.js +1 -0
  95. package/L4_Orchestration/facade/facade.js +9 -0
  96. package/L4_Orchestration/facade/manager_registry.js +118 -0
  97. package/L4_Orchestration/facade/renderer_registry.js +274 -0
  98. package/L4_Orchestration/lifecycle/asset_loader.js +255 -0
  99. package/L4_Orchestration/lifecycle/initializer.js +135 -0
  100. package/L4_Orchestration/lifecycle/lifecycle.js +8 -0
  101. package/L4_Orchestration/rendering/facade.js +94 -0
  102. package/L4_Orchestration/rendering/rendering.js +7 -0
  103. package/LICENSE +21 -0
  104. package/README.md +82 -0
  105. package/bifrost_client.js +204 -0
  106. package/bifrost_core.js +1686 -0
  107. package/docs/ARCHITECTURE.md +111 -0
  108. package/docs/PROTOCOL.md +106 -0
  109. package/docs/RENDERERS.md +101 -0
  110. package/docs/SECURITY.md +92 -0
  111. package/package.json +24 -0
  112. package/syntax/prism-zconfig.js +41 -0
  113. package/syntax/prism-zenv.js +69 -0
  114. package/syntax/prism-zolo-theme.css +288 -0
  115. package/syntax/prism-zolo.js +380 -0
  116. package/syntax/prism-zschema.js +38 -0
  117. package/syntax/prism-zspark.js +25 -0
  118. package/syntax/prism-zui.js +68 -0
  119. package/zSys/accessibility/accessibility.js +10 -0
  120. package/zSys/accessibility/emoji_accessibility.js +173 -0
  121. package/zSys/dom/block_utils.js +122 -0
  122. package/zSys/dom/container_utils.js +370 -0
  123. package/zSys/dom/dom.js +13 -0
  124. package/zSys/dom/dom_utils.js +328 -0
  125. package/zSys/dom/encoding_utils.js +117 -0
  126. package/zSys/dom/style_utils.js +71 -0
  127. package/zSys/errors/error_display.js +299 -0
  128. package/zSys/errors/errors.js +10 -0
  129. package/zSys/theme/color_utils.js +274 -0
  130. package/zSys/theme/dark_mode_utils.js +272 -0
  131. package/zSys/theme/size_utils.js +256 -0
  132. package/zSys/theme/spacing_utils.js +405 -0
  133. package/zSys/theme/theme.js +14 -0
  134. package/zSys/theme/zbase.css +1735 -0
  135. package/zSys/theme/zbase_inject.js +161 -0
  136. package/zSys/theme/ztheme_utils.js +305 -0
  137. package/zSys/validation/error_boundary.js +201 -0
  138. package/zSys/validation/validation.js +11 -0
  139. package/zSys/validation/validation_utils.js +238 -0
  140. package/zSys/zSys.js +14 -0
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Rendering Orchestration Module Barrel Export
3
+ *
4
+ * High-level orchestrators for declarative rendering and zDisplay events.
5
+ *
6
+ * @module rendering/orchestration
7
+ * @layer 3 (Rendering Orchestration)
8
+ */
9
+
10
+ export * from './renderer.js';
11
+ export * from './zdisplay_orchestrator.js';
@@ -0,0 +1,430 @@
1
+ /**
2
+ *
3
+ * Renderer Module - Auto-Rendering with zTheme
4
+ *
5
+ */
6
+
7
+ export class Renderer {
8
+ constructor(logger) {
9
+ this.logger = logger;
10
+ }
11
+
12
+ /**
13
+ * Render data as a table with zTheme styling
14
+ */
15
+ renderTable(data, container, _options = {}) {
16
+ const element = this._getElement(container);
17
+ if (!element) {
18
+ return;
19
+ }
20
+
21
+ if (!Array.isArray(data) || data.length === 0) {
22
+ element.innerHTML = '<p class="zEmpty">No data to display</p>';
23
+ return;
24
+ }
25
+
26
+ const columns = Object.keys(data[0]);
27
+ let html = '<table class="zTable zTable-striped zTable-hover">';
28
+
29
+ // Table header
30
+ html += '<thead><tr>';
31
+ columns.forEach(col => {
32
+ html += `<th>${this._formatColumnName(col)}</th>`;
33
+ });
34
+ html += '</tr></thead>';
35
+
36
+ // Table body
37
+ html += '<tbody>';
38
+ data.forEach(row => {
39
+ html += '<tr>';
40
+ columns.forEach(col => {
41
+ html += `<td>${this._escapeHtml(row[col])}</td>`;
42
+ });
43
+ html += '</tr>';
44
+ });
45
+ html += '</tbody></table>';
46
+
47
+ element.innerHTML = html;
48
+ this.logger.log('Table rendered', { rows: data.length, columns: columns.length });
49
+ }
50
+
51
+ /**
52
+ * Render a menu with buttons
53
+ */
54
+ renderMenu(items, container) {
55
+ const element = this._getElement(container);
56
+ if (!element) {
57
+ return;
58
+ }
59
+
60
+ let html = '<div class="zMenu">';
61
+ items.forEach((item, index) => {
62
+ const icon = item.icon || '';
63
+ const variant = item.variant || '';
64
+ html += `<button class="zoloButton ${variant}" data-action="${index}">
65
+ ${icon} ${item.label}
66
+ </button>`;
67
+ });
68
+ html += '</div>';
69
+
70
+ element.innerHTML = html;
71
+
72
+ // Attach event listeners
73
+ element.querySelectorAll('button[data-action]').forEach((btn, index) => {
74
+ btn.addEventListener('click', () => {
75
+ if (items[index].action) {
76
+ items[index].action();
77
+ }
78
+ });
79
+ });
80
+
81
+ this.logger.log('Menu rendered', { items: items.length });
82
+ }
83
+
84
+ /**
85
+ * Render a form with zTheme styling
86
+ */
87
+ renderForm(fields, container, onSubmit) {
88
+ const element = this._getElement(container);
89
+ if (!element) {
90
+ return;
91
+ }
92
+
93
+ let html = '<form class="zForm">';
94
+
95
+ fields.forEach(field => {
96
+ html += '<div class="zForm-group">';
97
+ html += `<label for="${field.name}">${field.label}</label>`;
98
+
99
+ if (field.type === 'textarea') {
100
+ html += `<textarea id="${field.name}" name="${field.name}"
101
+ class="zInput" ${field.required ? 'required' : ''}
102
+ placeholder="${field.placeholder || ''}"></textarea>`;
103
+ } else {
104
+ html += `<input type="${field.type || 'text'}" id="${field.name}"
105
+ name="${field.name}" class="zInput"
106
+ ${field.required ? 'required' : ''}
107
+ placeholder="${field.placeholder || ''}">`;
108
+ }
109
+
110
+ html += '</div>';
111
+ });
112
+
113
+ html += '<button type="submit" class="zoloButton zBtn-primary">Submit</button>';
114
+ html += '</form>';
115
+
116
+ element.innerHTML = html;
117
+
118
+ // Attach submit handler
119
+ const form = element.querySelector('form');
120
+ form.addEventListener('submit', (e) => {
121
+ e.preventDefault();
122
+ const formData = new FormData(form);
123
+ const data = Object.fromEntries(formData.entries());
124
+ if (onSubmit) {
125
+ onSubmit(data);
126
+ }
127
+ });
128
+
129
+ this.logger.log('Form rendered', { fields: fields.length });
130
+ }
131
+
132
+ /**
133
+ * Render a message/alert
134
+ * @param {number} duration - Auto-hide duration in ms (default: 5000 = TIMEOUTS.AUTO_DISMISS_SHORT)
135
+ */
136
+ renderMessage(text, type = 'info', container, duration = 5000) { // TIMEOUTS.AUTO_DISMISS_SHORT
137
+ const element = this._getElement(container);
138
+ if (!element) {
139
+ return;
140
+ }
141
+
142
+ const typeClass = {
143
+ 'success': 'zAlert-success',
144
+ 'error': 'zAlert-danger',
145
+ 'warning': 'zAlert-warning',
146
+ 'info': 'zAlert-info'
147
+ }[type] || 'zAlert-info';
148
+
149
+ const msgDiv = document.createElement('div');
150
+ msgDiv.className = `zAlert ${typeClass}`;
151
+ msgDiv.textContent = text;
152
+
153
+ element.insertBefore(msgDiv, element.firstChild);
154
+
155
+ if (duration > 0) {
156
+ setTimeout(() => msgDiv.remove(), duration);
157
+ }
158
+
159
+ this.logger.log('Message rendered', { type, text });
160
+ }
161
+
162
+ /**
163
+ * Render a progress bar
164
+ * @param {string} progressId - Unique ID for this progress bar
165
+ * @param {number} current - Current progress value
166
+ * @param {number} total - Total value (null for indeterminate)
167
+ * @param {object} options - { label, color, showPercentage, showETA, size }
168
+ * @param {string|Element} container - Container selector or element
169
+ */
170
+ renderProgressBar(progressId, current, total, options = {}, container) {
171
+ const element = this._getElement(container);
172
+ if (!element) {
173
+ return;
174
+ }
175
+
176
+ const {
177
+ label = 'Processing',
178
+ color = '',
179
+ showPercentage = true,
180
+ showETA = false,
181
+ size = '',
182
+ eta = null
183
+ } = options;
184
+
185
+ // Calculate percentage
186
+ const percentage = total > 0 ? Math.round((current / total) * 100) : 0;
187
+ const isIndeterminate = total === null || total === 0;
188
+
189
+ // Build classes
190
+ const classes = ['zProgress'];
191
+ if (color) {
192
+ classes.push(`zProgress-${color}`);
193
+ }
194
+ if (size) {
195
+ classes.push(`zProgress-${size}`);
196
+ }
197
+ if (isIndeterminate) {
198
+ classes.push('zProgress-indeterminate');
199
+ }
200
+
201
+ // Check if progress bar already exists
202
+ let progressDiv = element.querySelector(`[data-progress-id="${progressId}"]`);
203
+
204
+ if (!progressDiv) {
205
+ // Create new progress bar
206
+ progressDiv = document.createElement('div');
207
+ progressDiv.className = classes.join(' ');
208
+ progressDiv.setAttribute('data-progress-id', progressId);
209
+
210
+ let html = '<div class="zProgress-label">';
211
+ html += `<span>${this._escapeHtml(label)}</span>`;
212
+ html += '<span class="zProgress-stats"></span>';
213
+ html += '</div>';
214
+ html += '<div class="zProgress-track">';
215
+ html += '<div class="zProgress-fill"></div>';
216
+ html += '</div>';
217
+
218
+ progressDiv.innerHTML = html;
219
+ element.appendChild(progressDiv);
220
+ } else {
221
+ // Update existing progress bar classes
222
+ progressDiv.className = classes.join(' ');
223
+ }
224
+
225
+ // Update stats
226
+ const statsEl = progressDiv.querySelector('.zProgress-stats');
227
+ let statsText = '';
228
+ if (showPercentage && !isIndeterminate) {
229
+ statsText += `${percentage}%`;
230
+ }
231
+ if (showETA && eta) {
232
+ statsText += statsText ? ` • ETA: ${eta}` : `ETA: ${eta}`;
233
+ }
234
+ statsEl.textContent = statsText;
235
+
236
+ // Update fill
237
+ const fillEl = progressDiv.querySelector('.zProgress-fill');
238
+ if (isIndeterminate) {
239
+ fillEl.style.width = '100%';
240
+ } else {
241
+ fillEl.style.width = `${percentage}%`;
242
+ }
243
+
244
+ this.logger.log('Progress bar rendered', { progressId, current, total, percentage });
245
+ }
246
+
247
+ /**
248
+ * Update an existing progress bar
249
+ * @param {string} progressId - Unique ID of the progress bar
250
+ * @param {number} current - New current value
251
+ * @param {object} options - Optional updates { eta }
252
+ * @param {string|Element} container - Container selector or element
253
+ */
254
+ updateProgress(progressId, current, options = {}, container) {
255
+ const element = this._getElement(container);
256
+ if (!element) {
257
+ return;
258
+ }
259
+
260
+ const progressDiv = element.querySelector(`[data-progress-id="${progressId}"]`);
261
+ if (!progressDiv) {
262
+ this.logger.log('[ERROR] Progress bar not found:', progressId);
263
+ return;
264
+ }
265
+
266
+ // Get total from data attribute or calculate from width
267
+ const fillEl = progressDiv.querySelector('.zProgress-fill');
268
+ const isIndeterminate = progressDiv.classList.contains('zProgress-indeterminate');
269
+
270
+ if (!isIndeterminate) {
271
+ // Try to extract total from current percentage
272
+ const currentWidth = parseFloat(fillEl.style.width) || 0;
273
+ const total = currentWidth > 0 ? Math.round((current * 100) / currentWidth) : 100;
274
+
275
+ const percentage = Math.round((current / total) * 100);
276
+ fillEl.style.width = `${percentage}%`;
277
+
278
+ // Update stats
279
+ const statsEl = progressDiv.querySelector('.zProgress-stats');
280
+ let statsText = `${percentage}%`;
281
+ if (options.eta) {
282
+ statsText += ` • ETA: ${options.eta}`;
283
+ }
284
+ statsEl.textContent = statsText;
285
+ }
286
+
287
+ this.logger.log('Progress updated', { progressId, current });
288
+ }
289
+
290
+ /**
291
+ * Remove a progress bar
292
+ * @param {string} progressId - Unique ID of the progress bar
293
+ * @param {string|Element} container - Container selector or element
294
+ */
295
+ removeProgress(progressId, container) {
296
+ const element = this._getElement(container);
297
+ if (!element) {
298
+ return;
299
+ }
300
+
301
+ const progressDiv = element.querySelector(`[data-progress-id="${progressId}"]`);
302
+ if (progressDiv) {
303
+ progressDiv.remove();
304
+ this.logger.log('Progress bar removed', { progressId });
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Render a spinner
310
+ * @param {string} spinnerId - Unique ID for this spinner
311
+ * @param {string} label - Spinner label text
312
+ * @param {string} style - Spinner style (dots, line, arc, arrow, bouncingBall, simple, circle)
313
+ * @param {object} options - { size, center, inline }
314
+ * @param {string|Element} container - Container selector or element
315
+ */
316
+ renderSpinner(spinnerId, label, style = 'dots', options = {}, container) {
317
+ const element = this._getElement(container);
318
+ if (!element) {
319
+ return;
320
+ }
321
+
322
+ const {
323
+ size = '',
324
+ center = false,
325
+ inline = false
326
+ } = options;
327
+
328
+ // Build classes
329
+ const classes = ['zSpinner'];
330
+ if (size) {
331
+ classes.push(`zSpinner-${size}`);
332
+ }
333
+ if (center) {
334
+ classes.push('zSpinner-center');
335
+ }
336
+ if (inline) {
337
+ classes.push('zSpinner-inline');
338
+ }
339
+
340
+ // Check if spinner already exists
341
+ let spinnerDiv = element.querySelector(`[data-spinner-id="${spinnerId}"]`);
342
+
343
+ if (!spinnerDiv) {
344
+ // Create new spinner
345
+ spinnerDiv = document.createElement('div');
346
+ spinnerDiv.className = classes.join(' ');
347
+ spinnerDiv.setAttribute('data-spinner-id', spinnerId);
348
+
349
+ let html = '';
350
+ if (label) {
351
+ html += `<div class="zSpinner-label">${this._escapeHtml(label)}</div>`;
352
+ }
353
+
354
+ // Add animation based on style
355
+ if (style === 'circle') {
356
+ html += '<div class="zSpinner-animation"><div class="zSpinner-circle"></div></div>';
357
+ } else {
358
+ html += `<div class="zSpinner-animation zSpinner-${style}"></div>`;
359
+ }
360
+
361
+ spinnerDiv.innerHTML = html;
362
+ element.appendChild(spinnerDiv);
363
+ }
364
+
365
+ this.logger.log('Spinner rendered', { spinnerId, label, style });
366
+ }
367
+
368
+ /**
369
+ * Remove a spinner
370
+ * @param {string} spinnerId - Unique ID of the spinner
371
+ * @param {string|Element} container - Container selector or element
372
+ */
373
+ removeSpinner(spinnerId, container) {
374
+ const element = this._getElement(container);
375
+ if (!element) {
376
+ return;
377
+ }
378
+
379
+ const spinnerDiv = element.querySelector(`[data-spinner-id="${spinnerId}"]`);
380
+ if (spinnerDiv) {
381
+ spinnerDiv.remove();
382
+ this.logger.log('Spinner removed', { spinnerId });
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Get element from selector or element
388
+ * @private
389
+ */
390
+ _getElement(container) {
391
+ const element = typeof container === 'string'
392
+ ? document.querySelector(container)
393
+ : container;
394
+
395
+ if (!element) {
396
+ this.logger.log('[ERROR] Container not found:', container);
397
+ }
398
+
399
+ return element;
400
+ }
401
+
402
+ /**
403
+ * Format column name for display
404
+ * @private
405
+ */
406
+ _formatColumnName(name) {
407
+ return name
408
+ .replace(/_/g, ' ')
409
+ .replace(/\b\w/g, l => l.toUpperCase());
410
+ }
411
+
412
+ /**
413
+ * Escape HTML to prevent XSS
414
+ * @private
415
+ */
416
+ _escapeHtml(text) {
417
+ if (text === null || text === undefined) {
418
+ return '';
419
+ }
420
+ const map = {
421
+ '&': '&amp;',
422
+ '<': '&lt;',
423
+ '>': '&gt;',
424
+ '"': '&quot;',
425
+ "'": '&#039;'
426
+ };
427
+ return String(text).replace(/[&<>"']/g, m => map[m]);
428
+ }
429
+ }
430
+