@silvery/term 0.3.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.
Files changed (92) hide show
  1. package/package.json +54 -0
  2. package/src/adapters/canvas-adapter.ts +356 -0
  3. package/src/adapters/dom-adapter.ts +452 -0
  4. package/src/adapters/flexily-zero-adapter.ts +368 -0
  5. package/src/adapters/terminal-adapter.ts +305 -0
  6. package/src/adapters/yoga-adapter.ts +370 -0
  7. package/src/ansi/ansi.ts +251 -0
  8. package/src/ansi/constants.ts +76 -0
  9. package/src/ansi/detection.ts +441 -0
  10. package/src/ansi/hyperlink.ts +38 -0
  11. package/src/ansi/index.ts +201 -0
  12. package/src/ansi/patch-console.ts +159 -0
  13. package/src/ansi/sgr-codes.ts +34 -0
  14. package/src/ansi/storybook.ts +209 -0
  15. package/src/ansi/term.ts +724 -0
  16. package/src/ansi/types.ts +202 -0
  17. package/src/ansi/underline.ts +156 -0
  18. package/src/ansi/utils.ts +65 -0
  19. package/src/ansi-sanitize.ts +509 -0
  20. package/src/app.ts +571 -0
  21. package/src/bound-term.ts +94 -0
  22. package/src/bracketed-paste.ts +75 -0
  23. package/src/browser-renderer.ts +174 -0
  24. package/src/buffer.ts +1984 -0
  25. package/src/clipboard.ts +74 -0
  26. package/src/cursor-query.ts +85 -0
  27. package/src/device-attrs.ts +228 -0
  28. package/src/devtools.ts +123 -0
  29. package/src/dom/index.ts +194 -0
  30. package/src/errors.ts +39 -0
  31. package/src/focus-reporting.ts +48 -0
  32. package/src/hit-registry-core.ts +228 -0
  33. package/src/hit-registry.ts +176 -0
  34. package/src/index.ts +458 -0
  35. package/src/input.ts +119 -0
  36. package/src/inspector.ts +155 -0
  37. package/src/kitty-detect.ts +95 -0
  38. package/src/kitty-manager.ts +160 -0
  39. package/src/layout-engine.ts +296 -0
  40. package/src/layout.ts +26 -0
  41. package/src/measurer.ts +74 -0
  42. package/src/mode-query.ts +106 -0
  43. package/src/mouse-events.ts +419 -0
  44. package/src/mouse.ts +83 -0
  45. package/src/non-tty.ts +223 -0
  46. package/src/osc-markers.ts +32 -0
  47. package/src/osc-palette.ts +169 -0
  48. package/src/output.ts +406 -0
  49. package/src/pane-manager.ts +248 -0
  50. package/src/pipeline/CLAUDE.md +587 -0
  51. package/src/pipeline/content-phase-adapter.ts +976 -0
  52. package/src/pipeline/content-phase.ts +1765 -0
  53. package/src/pipeline/helpers.ts +42 -0
  54. package/src/pipeline/index.ts +416 -0
  55. package/src/pipeline/layout-phase.ts +686 -0
  56. package/src/pipeline/measure-phase.ts +198 -0
  57. package/src/pipeline/measure-stats.ts +21 -0
  58. package/src/pipeline/output-phase.ts +2593 -0
  59. package/src/pipeline/render-box.ts +343 -0
  60. package/src/pipeline/render-helpers.ts +243 -0
  61. package/src/pipeline/render-text.ts +1255 -0
  62. package/src/pipeline/types.ts +161 -0
  63. package/src/pipeline.ts +29 -0
  64. package/src/pixel-size.ts +119 -0
  65. package/src/render-adapter.ts +179 -0
  66. package/src/renderer.ts +1330 -0
  67. package/src/runtime/create-app.tsx +1845 -0
  68. package/src/runtime/create-buffer.ts +18 -0
  69. package/src/runtime/create-runtime.ts +325 -0
  70. package/src/runtime/diff.ts +56 -0
  71. package/src/runtime/event-handlers.ts +254 -0
  72. package/src/runtime/index.ts +119 -0
  73. package/src/runtime/keys.ts +8 -0
  74. package/src/runtime/layout.ts +164 -0
  75. package/src/runtime/run.tsx +318 -0
  76. package/src/runtime/term-provider.ts +399 -0
  77. package/src/runtime/terminal-lifecycle.ts +246 -0
  78. package/src/runtime/tick.ts +219 -0
  79. package/src/runtime/types.ts +210 -0
  80. package/src/scheduler.ts +723 -0
  81. package/src/screenshot.ts +57 -0
  82. package/src/scroll-region.ts +69 -0
  83. package/src/scroll-utils.ts +97 -0
  84. package/src/term-def.ts +267 -0
  85. package/src/terminal-caps.ts +5 -0
  86. package/src/terminal-colors.ts +216 -0
  87. package/src/termtest.ts +224 -0
  88. package/src/text-sizing.ts +109 -0
  89. package/src/toolbelt/index.ts +72 -0
  90. package/src/unicode.ts +1763 -0
  91. package/src/xterm/index.ts +491 -0
  92. package/src/xterm/xterm-provider.ts +204 -0
@@ -0,0 +1,509 @@
1
+ /**
2
+ * ANSI escape sequence sanitizer.
3
+ *
4
+ * Strips dangerous escape sequences from text while preserving safe SGR
5
+ * styling and OSC sequences (hyperlinks, etc.). Used for rendering untrusted
6
+ * text safely in the terminal.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ // =============================================================================
12
+ // Types
13
+ // =============================================================================
14
+
15
+ /**
16
+ * A parsed token from an ANSI-containing string.
17
+ *
18
+ * Token types:
19
+ * - `text` — Plain text content
20
+ * - `csi` — CSI (Control Sequence Introducer): ESC + '[' + params + final byte
21
+ * - `osc` — OSC (Operating System Command): ESC + ']' + payload + ST/BEL
22
+ * - `esc` — Simple two-byte escape: ESC + final byte
23
+ * - `dcs` — DCS (Device Control String): ESC + 'P' + payload + ST
24
+ * - `pm` — PM (Privacy Message): ESC + '^' + payload + ST
25
+ * - `apc` — APC (Application Program Command): ESC + '_' + payload + ST
26
+ * - `sos` — SOS (Start of String): ESC + 'X' + payload + ST
27
+ * - `c1` — C1 control character (0x80–0x9F)
28
+ */
29
+ export interface AnsiToken {
30
+ type: "text" | "csi" | "osc" | "esc" | "dcs" | "pm" | "apc" | "sos" | "c1"
31
+ value: string
32
+ }
33
+
34
+ // =============================================================================
35
+ // Constants
36
+ // =============================================================================
37
+
38
+ const ESC = 0x1b
39
+
40
+ /** Characters that introduce ST-terminated string sequences after ESC. */
41
+ const STRING_SEQUENCE_INTROS: Record<number, AnsiToken["type"]> = {
42
+ 0x50: "dcs", // 'P' — Device Control String
43
+ 0x5e: "pm", // '^' — Privacy Message
44
+ 0x5f: "apc", // '_' — Application Program Command
45
+ 0x58: "sos", // 'X' — Start of String
46
+ }
47
+
48
+ /** C1 control codes (8-bit mode) that correspond to string sequence introducers. */
49
+ const C1_STRING_SEQUENCE_MAP: Record<number, AnsiToken["type"]> = {
50
+ 0x90: "dcs", // DCS
51
+ 0x9e: "pm", // PM
52
+ 0x9f: "apc", // APC
53
+ 0x98: "sos", // SOS
54
+ }
55
+
56
+ // =============================================================================
57
+ // Tokenizer
58
+ // =============================================================================
59
+
60
+ /**
61
+ * Tokenize a string into ANSI escape sequence tokens.
62
+ *
63
+ * Parses the string character by character, identifying escape sequences
64
+ * and plain text segments. Each token includes its type and the raw string
65
+ * value (including escape characters).
66
+ *
67
+ * @param text - Input string that may contain ANSI escape sequences
68
+ * @returns Array of tokens
69
+ */
70
+ export function tokenizeAnsi(text: string): AnsiToken[] {
71
+ const tokens: AnsiToken[] = []
72
+ const len = text.length
73
+ let i = 0
74
+ let textStart = i
75
+
76
+ function flushText(): void {
77
+ if (i > textStart) {
78
+ tokens.push({ type: "text", value: text.slice(textStart, i) })
79
+ }
80
+ }
81
+
82
+ while (i < len) {
83
+ const code = text.charCodeAt(i)
84
+
85
+ // Check for C1 control characters (0x80–0x9F) in 8-bit mode
86
+ if (code >= 0x80 && code <= 0x9f) {
87
+ flushText()
88
+
89
+ const c1Type = C1_STRING_SEQUENCE_MAP[code]
90
+ if (c1Type) {
91
+ // C1 string sequence introducer — consume until ST
92
+ const start = i
93
+ i++
94
+ i = findST(text, i, len)
95
+ tokens.push({ type: c1Type, value: text.slice(start, i) })
96
+ } else if (code === 0x9b) {
97
+ // CSI in 8-bit mode
98
+ const start = i
99
+ i++
100
+ i = consumeCSI(text, i, len)
101
+ tokens.push({ type: "csi", value: text.slice(start, i) })
102
+ } else if (code === 0x9d) {
103
+ // OSC in 8-bit mode
104
+ const start = i
105
+ i++
106
+ i = findOSCEnd(text, i, len)
107
+ tokens.push({ type: "osc", value: text.slice(start, i) })
108
+ } else {
109
+ // Other C1 control character
110
+ tokens.push({ type: "c1", value: text[i] })
111
+ i++
112
+ }
113
+ textStart = i
114
+ continue
115
+ }
116
+
117
+ // Check for ESC (0x1B)
118
+ if (code === ESC) {
119
+ flushText()
120
+
121
+ if (i + 1 >= len) {
122
+ // Incomplete escape at end of string — treat as malformed
123
+ tokens.push({ type: "esc", value: text[i] })
124
+ i++
125
+ textStart = i
126
+ continue
127
+ }
128
+
129
+ const next = text.charCodeAt(i + 1)
130
+
131
+ // CSI: ESC + '['
132
+ if (next === 0x5b) {
133
+ const start = i
134
+ i += 2
135
+ i = consumeCSI(text, i, len)
136
+ tokens.push({ type: "csi", value: text.slice(start, i) })
137
+ textStart = i
138
+ continue
139
+ }
140
+
141
+ // OSC: ESC + ']'
142
+ if (next === 0x5d) {
143
+ const start = i
144
+ i += 2
145
+ i = findOSCEnd(text, i, len)
146
+ tokens.push({ type: "osc", value: text.slice(start, i) })
147
+ textStart = i
148
+ continue
149
+ }
150
+
151
+ // String sequences: DCS (P), PM (^), APC (_), SOS (X)
152
+ const stringType = STRING_SEQUENCE_INTROS[next]
153
+ if (stringType) {
154
+ const start = i
155
+ i += 2
156
+ i = findST(text, i, len)
157
+ tokens.push({ type: stringType, value: text.slice(start, i) })
158
+ textStart = i
159
+ continue
160
+ }
161
+
162
+ // ESC sequences with intermediate bytes:
163
+ // ESC I... F where I is 0x20–0x2F (intermediate), F is 0x30–0x7E (final)
164
+ // Examples: ESC # 8 (DECALN), ESC ( B (G0 charset)
165
+ // If no valid final byte follows, consume to end of string (fail-safe
166
+ // to prevent payload leaks from malformed sequences).
167
+ if (next >= 0x20 && next <= 0x2f) {
168
+ const start = i
169
+ i += 2 // skip ESC + first intermediate
170
+ // Consume additional intermediate bytes
171
+ while (i < len) {
172
+ const c = text.charCodeAt(i)
173
+ if (c < 0x20 || c > 0x2f) break
174
+ i++
175
+ }
176
+ // Consume final byte (0x30–0x7E) if present
177
+ if (i < len) {
178
+ const c = text.charCodeAt(i)
179
+ if (c >= 0x30 && c <= 0x7e) {
180
+ i++
181
+ tokens.push({ type: "esc", value: text.slice(start, i) })
182
+ } else {
183
+ // No valid final byte — malformed sequence, consume to end of string
184
+ i = len
185
+ tokens.push({ type: "esc", value: text.slice(start, i) })
186
+ }
187
+ } else {
188
+ // Incomplete (at end of string) — consume what we have
189
+ tokens.push({ type: "esc", value: text.slice(start, i) })
190
+ }
191
+ textStart = i
192
+ continue
193
+ }
194
+
195
+ // Simple two-byte escape sequence: ESC + byte (0x30–0x7E)
196
+ // 0x30–0x3F: Fp (private use, e.g. ESC 7 = DECSC, ESC 8 = DECRC)
197
+ // 0x40–0x5F: Fe (C1 equivalents, e.g. ESC D = IND, ESC M = RI)
198
+ // 0x60–0x7E: Fs (independent functions)
199
+ if (next >= 0x30 && next <= 0x7e) {
200
+ tokens.push({ type: "esc", value: text.slice(i, i + 2) })
201
+ i += 2
202
+ textStart = i
203
+ continue
204
+ }
205
+
206
+ // Unknown/malformed escape — emit just ESC as an esc token
207
+ tokens.push({ type: "esc", value: text[i] })
208
+ i++
209
+ textStart = i
210
+ continue
211
+ }
212
+
213
+ i++
214
+ }
215
+
216
+ flushText()
217
+ return tokens
218
+ }
219
+
220
+ // =============================================================================
221
+ // CSI Parser
222
+ // =============================================================================
223
+
224
+ /**
225
+ * Consume a CSI sequence starting after "ESC [" or the C1 CSI byte.
226
+ * Returns the index after the final byte.
227
+ *
228
+ * CSI format: parameter bytes (0x30–0x3F)*, intermediate bytes (0x20–0x2F)*, final byte (0x40–0x7E)
229
+ */
230
+ function consumeCSI(text: string, i: number, len: number): number {
231
+ // Parameter bytes: 0x30–0x3F (digits, semicolons, colons, etc.)
232
+ while (i < len) {
233
+ const c = text.charCodeAt(i)
234
+ if (c < 0x30 || c > 0x3f) break
235
+ i++
236
+ }
237
+
238
+ // Intermediate bytes: 0x20–0x2F (space, !, ", #, etc.)
239
+ while (i < len) {
240
+ const c = text.charCodeAt(i)
241
+ if (c < 0x20 || c > 0x2f) break
242
+ i++
243
+ }
244
+
245
+ // Final byte: 0x40–0x7E
246
+ if (i < len) {
247
+ const c = text.charCodeAt(i)
248
+ if (c >= 0x40 && c <= 0x7e) {
249
+ i++
250
+ }
251
+ }
252
+
253
+ return i
254
+ }
255
+
256
+ // =============================================================================
257
+ // String Terminator Finder
258
+ // =============================================================================
259
+
260
+ /**
261
+ * Find the String Terminator (ST) for DCS, PM, APC, SOS sequences.
262
+ * ST is ESC + '\\' (0x5C) or C1 ST (0x9C). Returns index after the ST.
263
+ * If no ST found, returns end of string (consuming the malformed sequence).
264
+ */
265
+ function findST(text: string, i: number, len: number): number {
266
+ while (i < len) {
267
+ const code = text.charCodeAt(i)
268
+ // C1 ST (0x9C)
269
+ if (code === 0x9c) {
270
+ return i + 1
271
+ }
272
+ // ESC + '\' (7-bit ST)
273
+ if (code === ESC && i + 1 < len && text.charCodeAt(i + 1) === 0x5c) {
274
+ return i + 2 // past ESC + '\'
275
+ }
276
+ i++
277
+ }
278
+ return len
279
+ }
280
+
281
+ /**
282
+ * Find the end of an OSC sequence.
283
+ * OSC is terminated by ST (ESC + '\\'), C1 ST (0x9C), or BEL (0x07).
284
+ * Returns index after the terminator.
285
+ */
286
+ function findOSCEnd(text: string, i: number, len: number): number {
287
+ while (i < len) {
288
+ const code = text.charCodeAt(i)
289
+ // BEL terminator
290
+ if (code === 0x07) {
291
+ return i + 1
292
+ }
293
+ // C1 ST (0x9C)
294
+ if (code === 0x9c) {
295
+ return i + 1
296
+ }
297
+ // ST terminator (ESC + '\')
298
+ if (code === ESC && i + 1 < len && text.charCodeAt(i + 1) === 0x5c) {
299
+ return i + 2
300
+ }
301
+ i++
302
+ }
303
+ return len
304
+ }
305
+
306
+ // =============================================================================
307
+ // Sanitizer
308
+ // =============================================================================
309
+
310
+ /**
311
+ * Check whether a CSI sequence is an SGR (Select Graphic Rendition) sequence.
312
+ *
313
+ * SGR sequences set text styling (colors, bold, underline, etc.) and are safe.
314
+ * They have the form: CSI <params> m
315
+ *
316
+ * A CSI is SGR when:
317
+ * - The final byte is 'm'
318
+ * - There are no intermediate bytes (0x20–0x2F)
319
+ * - Parameter bytes are only 0x30–0x3F
320
+ */
321
+ export function isCSISGR(value: string): boolean {
322
+ // Must end with 'm'
323
+ if (value.length < 2 || value.charCodeAt(value.length - 1) !== 0x6d) {
324
+ return false
325
+ }
326
+
327
+ // Find start of parameters (skip ESC[ or C1 CSI)
328
+ let start: number
329
+ if (value.charCodeAt(0) === ESC) {
330
+ // ESC [ ... m
331
+ start = 2
332
+ } else {
333
+ // C1 CSI (0x9B) ... m
334
+ start = 1
335
+ }
336
+
337
+ // Everything between start and the final 'm' must be standard parameter bytes:
338
+ // digits (0x30–0x39), semicolons (0x3B), colons (0x3A).
339
+ // Private-use parameter prefixes (<, =, >, ? at 0x3C–0x3F) indicate non-SGR.
340
+ // Intermediate bytes (0x20–0x2F) also indicate non-SGR.
341
+ for (let i = start; i < value.length - 1; i++) {
342
+ const c = value.charCodeAt(i)
343
+ // Allow: digits 0-9 (0x30-0x39), colon (0x3A), semicolon (0x3B)
344
+ // Reject: < = > ? (0x3C-0x3F) — private-use parameter prefixes
345
+ // Reject: anything outside 0x30-0x3B (intermediates, etc.)
346
+ if (c < 0x30 || c > 0x3b) {
347
+ return false
348
+ }
349
+ }
350
+
351
+ return true
352
+ }
353
+
354
+ /**
355
+ * Sanitize a string by stripping dangerous ANSI escape sequences while
356
+ * preserving safe SGR styling codes and OSC sequences (hyperlinks, etc.).
357
+ *
358
+ * Safe (preserved):
359
+ * - Plain text
360
+ * - CSI SGR sequences (colors, bold, underline — final byte 'm', no intermediates)
361
+ * - OSC sequences (hyperlinks, window titles, etc.)
362
+ *
363
+ * Stripped:
364
+ * - Non-SGR CSI sequences (cursor movement, screen clearing, etc.)
365
+ * - DCS (Device Control String)
366
+ * - PM (Privacy Message)
367
+ * - APC (Application Program Command)
368
+ * - SOS (Start of String)
369
+ * - C1 control characters (0x80–0x9F)
370
+ * - Simple ESC sequences (cursor save/restore, etc.)
371
+ * - Malformed/incomplete escape sequences
372
+ *
373
+ * @param text - Input string that may contain ANSI escape sequences
374
+ * @returns Sanitized string with only safe sequences preserved
375
+ *
376
+ * @example
377
+ * ```ts
378
+ * // SGR preserved
379
+ * sanitizeAnsi('\x1b[31mred\x1b[0m') // '\x1b[31mred\x1b[0m'
380
+ *
381
+ * // Cursor movement stripped
382
+ * sanitizeAnsi('\x1b[2J\x1b[H') // ''
383
+ *
384
+ * // Mixed: only SGR kept
385
+ * sanitizeAnsi('\x1b[31m\x1b[2Jred\x1b[0m') // '\x1b[31mred\x1b[0m'
386
+ * ```
387
+ */
388
+ export function sanitizeAnsi(text: string): string {
389
+ if (text.length === 0) return ""
390
+
391
+ const tokens = tokenizeAnsi(text)
392
+ let result = ""
393
+
394
+ for (const token of tokens) {
395
+ switch (token.type) {
396
+ case "text":
397
+ result += token.value
398
+ break
399
+ case "csi":
400
+ // Only keep SGR sequences (color/style codes)
401
+ if (isCSISGR(token.value)) {
402
+ result += token.value
403
+ }
404
+ break
405
+ case "osc":
406
+ // OSC sequences are safe (hyperlinks, titles, etc.)
407
+ result += token.value
408
+ break
409
+ // Strip everything else: esc, dcs, pm, apc, sos, c1
410
+ }
411
+ }
412
+
413
+ return result
414
+ }
415
+
416
+ // =============================================================================
417
+ // Colon-format SGR round-trip tracking
418
+ // =============================================================================
419
+
420
+ /**
421
+ * A colon→semicolon SGR replacement pair.
422
+ */
423
+ export interface ColonSGRReplacement {
424
+ semicolonForm: string
425
+ colonForm: string
426
+ }
427
+
428
+ /**
429
+ * Detect colon-format SGR sequences in an SGR token and return replacement pairs.
430
+ *
431
+ * Terminals use colon-separated parameters (e.g., `38:2::255:100:0`) for true color,
432
+ * but silvery's pipeline normalizes to semicolons (`38;2;255;100;0`). This function
433
+ * extracts the mapping so the original colon format can be restored after rendering.
434
+ *
435
+ * @param sgrSequence - A CSI SGR sequence (must end with 'm')
436
+ * @returns Array of replacement pairs, empty if no colon-format params found
437
+ */
438
+ export function extractColonSGRReplacements(sgrSequence: string): ColonSGRReplacement[] {
439
+ const paramsMatch = sgrSequence.match(/\x1b\[([0-9;:]+)m/)
440
+ if (!paramsMatch) return []
441
+
442
+ const rawParams = paramsMatch[1]!
443
+ if (!rawParams.includes(":")) return []
444
+
445
+ const replacements: ColonSGRReplacement[] = []
446
+ const parts = rawParams.split(";")
447
+ for (const part of parts) {
448
+ if (!part.includes(":")) continue
449
+ const subs = part.split(":")
450
+ const code = Number(subs[0])
451
+ if ((code === 38 || code === 48) && Number(subs[1]) === 2) {
452
+ // True color colon format: code:2::R:G:B or code:2:R:G:B
453
+ // Extract R, G, B (skip empty colorspace ID)
454
+ const nums = subs.map((s) => (s === "" ? 0 : Number(s)))
455
+ const r = nums[3] ?? nums[2] ?? 0
456
+ const g = nums[4] ?? nums[3] ?? 0
457
+ const b = nums[5] ?? nums[4] ?? 0
458
+ const semicolonForm = `\x1b[${code};2;${r};${g};${b}m`
459
+ replacements.push({ semicolonForm, colonForm: `\x1b[${part}m` })
460
+ }
461
+ }
462
+ return replacements
463
+ }
464
+
465
+ /**
466
+ * Create a colon-format SGR tracker for round-trip preservation.
467
+ *
468
+ * Rendering is synchronous: sanitize → render → output in one call. The tracker
469
+ * accumulates colon→semicolon mappings during sanitization, then `restore()` applies
470
+ * them to the rendered output.
471
+ *
472
+ * @example
473
+ * ```ts
474
+ * const tracker = createColonSGRTracker()
475
+ * // During sanitization, register SGR tokens:
476
+ * tracker.register(sgrToken)
477
+ * // After rendering, restore original colon format:
478
+ * output = tracker.restore(output)
479
+ * // Optionally clear for reuse:
480
+ * tracker.clear()
481
+ * ```
482
+ */
483
+ export function createColonSGRTracker(): {
484
+ register: (sgrSequence: string) => void
485
+ restore: (output: string) => string
486
+ clear: () => void
487
+ } {
488
+ const replacements: ColonSGRReplacement[] = []
489
+
490
+ return {
491
+ register(sgrSequence: string): void {
492
+ const found = extractColonSGRReplacements(sgrSequence)
493
+ for (const r of found) replacements.push(r)
494
+ },
495
+
496
+ restore(output: string): string {
497
+ if (replacements.length === 0) return output
498
+ let result = output
499
+ for (const { semicolonForm, colonForm } of replacements) {
500
+ result = result.replaceAll(semicolonForm, colonForm)
501
+ }
502
+ return result
503
+ },
504
+
505
+ clear(): void {
506
+ replacements.length = 0
507
+ },
508
+ }
509
+ }