regen.mde 0.2.2

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 (60) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +295 -0
  3. package/bin/build-corpus-editor.js +81 -0
  4. package/bin/build-corpus.js +41 -0
  5. package/bin/postinstall.js +187 -0
  6. package/bin/regen-mdeditor-install.js +27 -0
  7. package/bin/regen-mdeditor-uninstall.js +19 -0
  8. package/bin/validate-katex.js +93 -0
  9. package/desktop/BuildCorpusEditor/BuildCorpusBridge.cs +270 -0
  10. package/desktop/BuildCorpusEditor/BuildCorpusEditor.csproj +22 -0
  11. package/desktop/BuildCorpusEditor/EditorForm.cs +540 -0
  12. package/desktop/BuildCorpusEditor/Program.cs +81 -0
  13. package/desktop/BuildCorpusEditor/app.manifest +16 -0
  14. package/dist/release/regen.mde-0.2.2-win-x64-setup.exe +0 -0
  15. package/dist/release/regen.mde-0.2.2-win-x64.zip +0 -0
  16. package/dist/windows-editor/BuildCorpusEditor.deps.json +83 -0
  17. package/dist/windows-editor/BuildCorpusEditor.dll +0 -0
  18. package/dist/windows-editor/BuildCorpusEditor.exe +0 -0
  19. package/dist/windows-editor/BuildCorpusEditor.pdb +0 -0
  20. package/dist/windows-editor/BuildCorpusEditor.runtimeconfig.json +19 -0
  21. package/dist/windows-editor/Microsoft.Web.WebView2.Core.dll +0 -0
  22. package/dist/windows-editor/Microsoft.Web.WebView2.Core.xml +6817 -0
  23. package/dist/windows-editor/Microsoft.Web.WebView2.WinForms.dll +0 -0
  24. package/dist/windows-editor/Microsoft.Web.WebView2.WinForms.xml +510 -0
  25. package/dist/windows-editor/Microsoft.Web.WebView2.Wpf.dll +0 -0
  26. package/dist/windows-editor/Microsoft.Web.WebView2.Wpf.xml +1902 -0
  27. package/dist/windows-editor/WebView2Loader.dll +0 -0
  28. package/dist/windows-editor/runtimes/win-x64/native/WebView2Loader.dll +0 -0
  29. package/dist/windows-editor/wwwroot/assets/index-DjJ6xmhy.js +326 -0
  30. package/dist/windows-editor/wwwroot/assets/index-_dwMNNsm.css +1 -0
  31. package/dist/windows-editor/wwwroot/index.html +22 -0
  32. package/editor-web/index.html +21 -0
  33. package/editor-web/src/main.jsx +399 -0
  34. package/editor-web/src/styles.css +602 -0
  35. package/editor-web/vite.config.js +13 -0
  36. package/examples/build-corpus.config.example.json +21 -0
  37. package/installer/install-regen-mde.ps1 +175 -0
  38. package/installer/regen-mde.nsi +81 -0
  39. package/package.json +86 -0
  40. package/pyproject.toml +33 -0
  41. package/requirements.txt +4 -0
  42. package/scripts/build-windows-editor.ps1 +47 -0
  43. package/scripts/package-windows-editor.ps1 +90 -0
  44. package/scripts/run-corpus.ps1 +28 -0
  45. package/scripts/run-editor-implementation-plane.ps1 +203 -0
  46. package/scripts/run-required-tests.ps1 +98 -0
  47. package/scripts/run-smoke.ps1 +28 -0
  48. package/src/build_corpus/__init__.py +3 -0
  49. package/src/build_corpus/docx_exporter.py +798 -0
  50. package/src/build_corpus/exporter.py +1195 -0
  51. package/src/build_corpus/ppt_exporter.py +532 -0
  52. package/src/build_corpus/templates/__init__.py +1 -0
  53. package/src/build_corpus/templates/md-to-word-template.dotx +0 -0
  54. package/src/build_corpus/validate_assets.py +46 -0
  55. package/tools/audit_corpus.py +203 -0
  56. package/tools/collect_microsoft_word_templates.py +228 -0
  57. package/tools/collect_online_docx_corpus.py +272 -0
  58. package/tools/collect_online_pptx_corpus.py +252 -0
  59. package/tools/compare_pptx_inputs_outputs.py +87 -0
  60. package/tools/roundtrip_docx_corpus.py +171 -0
@@ -0,0 +1,602 @@
1
+ :root {
2
+ color-scheme: light;
3
+ font-family: "Segoe UI", Inter, system-ui, sans-serif;
4
+ background: oklch(98% 0.011 92);
5
+ color: oklch(24% 0.018 250);
6
+ --ink: oklch(24% 0.018 250);
7
+ --muted: oklch(47% 0.022 250);
8
+ --paper: oklch(98% 0.011 92);
9
+ --panel: oklch(95% 0.014 92);
10
+ --line: oklch(82% 0.018 92);
11
+ --soft: oklch(91% 0.022 92);
12
+ --accent: oklch(56% 0.135 176);
13
+ --accent-dark: oklch(36% 0.09 176);
14
+ --warning: oklch(52% 0.12 23);
15
+ --violet: oklch(60% 0.11 284);
16
+ --source: oklch(20% 0.022 250);
17
+ --source-ink: oklch(93% 0.016 92);
18
+ --shadow: 0 24px 70px rgba(34, 38, 46, 0.18);
19
+ --button-bg: oklch(99% 0.006 92);
20
+ --button-hover: oklch(94% 0.034 176);
21
+ --topbar-bg: rgba(252, 249, 240, 0.94);
22
+ --panel-bg: rgba(248, 245, 236, 0.96);
23
+ --card-bg: oklch(99% 0.006 92);
24
+ --page-bg: oklch(99% 0.005 92);
25
+ --page-ink: oklch(27% 0.018 250);
26
+ --rail-bg: oklch(28% 0.026 250);
27
+ --rail-item-bg: oklch(37% 0.04 250);
28
+ --grid-line-strong: rgba(41, 143, 132, 0.08);
29
+ --grid-line-soft: rgba(41, 143, 132, 0.05);
30
+ --warning-bg: oklch(93% 0.044 72);
31
+ --warning-border: oklch(74% 0.075 72);
32
+ --warning-ink: oklch(35% 0.075 72);
33
+ --bubble-bg: oklch(24% 0.022 250);
34
+ --bubble-line: oklch(35% 0.025 250);
35
+ --bubble-ink: oklch(94% 0.01 92);
36
+ --table-head: oklch(92% 0.025 92);
37
+ }
38
+
39
+ [data-theme="dark"] {
40
+ color-scheme: dark;
41
+ --ink: oklch(91% 0.014 250);
42
+ --muted: oklch(68% 0.018 250);
43
+ --paper: oklch(20% 0.018 250);
44
+ --panel: oklch(26% 0.018 250);
45
+ --line: oklch(38% 0.022 250);
46
+ --soft: oklch(16% 0.018 250);
47
+ --accent: oklch(65% 0.13 176);
48
+ --accent-dark: oklch(88% 0.058 176);
49
+ --warning: oklch(71% 0.14 68);
50
+ --violet: oklch(71% 0.1 284);
51
+ --source: oklch(14% 0.018 250);
52
+ --source-ink: oklch(90% 0.012 92);
53
+ --shadow: 0 24px 80px rgba(0, 0, 0, 0.46);
54
+ --button-bg: oklch(27% 0.018 250);
55
+ --button-hover: oklch(32% 0.038 176);
56
+ --topbar-bg: rgba(25, 29, 36, 0.96);
57
+ --panel-bg: rgba(29, 33, 40, 0.96);
58
+ --card-bg: oklch(24% 0.018 250);
59
+ --page-bg: oklch(96% 0.008 92);
60
+ --page-ink: oklch(25% 0.018 250);
61
+ --rail-bg: oklch(13% 0.02 250);
62
+ --rail-item-bg: oklch(24% 0.03 250);
63
+ --grid-line-strong: rgba(86, 205, 190, 0.08);
64
+ --grid-line-soft: rgba(86, 205, 190, 0.045);
65
+ --warning-bg: oklch(25% 0.044 68);
66
+ --warning-border: oklch(48% 0.11 68);
67
+ --warning-ink: oklch(84% 0.09 68);
68
+ --bubble-bg: oklch(91% 0.012 92);
69
+ --bubble-line: oklch(72% 0.02 92);
70
+ --bubble-ink: oklch(20% 0.022 250);
71
+ --table-head: oklch(90% 0.018 92);
72
+ }
73
+
74
+ * {
75
+ box-sizing: border-box;
76
+ }
77
+
78
+ body {
79
+ margin: 0;
80
+ min-width: 920px;
81
+ }
82
+
83
+ button {
84
+ min-height: 28px;
85
+ border: 1px solid var(--line);
86
+ border-radius: 5px;
87
+ padding: 4px 9px;
88
+ background: var(--button-bg);
89
+ color: var(--ink);
90
+ font: 600 12px/1 "Segoe UI", system-ui, sans-serif;
91
+ letter-spacing: 0;
92
+ cursor: pointer;
93
+ }
94
+
95
+ button:hover:not(:disabled), button.active {
96
+ border-color: var(--accent);
97
+ background: var(--button-hover);
98
+ color: var(--accent-dark);
99
+ }
100
+
101
+ button:disabled {
102
+ cursor: default;
103
+ opacity: 0.42;
104
+ }
105
+
106
+ button.primary {
107
+ background: var(--accent);
108
+ border-color: oklch(45% 0.11 176);
109
+ color: oklch(98% 0.01 176);
110
+ }
111
+
112
+ button.ghost {
113
+ background: transparent;
114
+ }
115
+
116
+ main.app-shell {
117
+ width: 100vw;
118
+ height: 100vh;
119
+ min-height: 720px;
120
+ display: grid;
121
+ grid-template-columns: 58px 260px minmax(560px, 1fr) 320px;
122
+ grid-template-rows: 48px 1fr 30px;
123
+ overflow: hidden;
124
+ background:
125
+ linear-gradient(90deg, var(--grid-line-strong) 0 1px, transparent 1px 72px),
126
+ linear-gradient(var(--grid-line-soft) 0 1px, transparent 1px 72px),
127
+ var(--paper);
128
+ }
129
+
130
+ .brand-rail {
131
+ grid-row: 1 / 4;
132
+ border-right: 1px solid var(--line);
133
+ background: var(--rail-bg);
134
+ color: oklch(92% 0.012 92);
135
+ display: grid;
136
+ grid-template-rows: 64px repeat(5, 42px) 1fr 42px;
137
+ place-items: center;
138
+ }
139
+
140
+ .mark {
141
+ width: 32px;
142
+ height: 32px;
143
+ border-radius: 8px;
144
+ display: grid;
145
+ place-items: center;
146
+ background: var(--accent);
147
+ color: oklch(98% 0.01 176);
148
+ font-weight: 800;
149
+ }
150
+
151
+ .rail-item {
152
+ width: 34px;
153
+ height: 34px;
154
+ border-radius: 7px;
155
+ display: grid;
156
+ place-items: center;
157
+ color: oklch(82% 0.012 92);
158
+ font-size: 12px;
159
+ font-weight: 700;
160
+ }
161
+
162
+ .rail-item.active {
163
+ background: var(--rail-item-bg);
164
+ color: oklch(98% 0.01 92);
165
+ }
166
+
167
+ .topbar {
168
+ grid-column: 2 / 5;
169
+ border-bottom: 1px solid var(--line);
170
+ background: var(--topbar-bg);
171
+ display: flex;
172
+ align-items: center;
173
+ justify-content: space-between;
174
+ gap: 16px;
175
+ padding: 0 14px;
176
+ }
177
+
178
+ .file-title {
179
+ min-width: 0;
180
+ display: flex;
181
+ align-items: baseline;
182
+ gap: 10px;
183
+ }
184
+
185
+ .file-title strong {
186
+ font-size: 15px;
187
+ }
188
+
189
+ .file-title span {
190
+ min-width: 0;
191
+ overflow: hidden;
192
+ text-overflow: ellipsis;
193
+ white-space: nowrap;
194
+ color: var(--muted);
195
+ font: 12px/1.2 Consolas, "Cascadia Mono", monospace;
196
+ }
197
+
198
+ .actions {
199
+ display: flex;
200
+ align-items: center;
201
+ gap: 6px;
202
+ }
203
+
204
+ .side-panel {
205
+ border-color: var(--line);
206
+ background: var(--panel-bg);
207
+ padding: 14px;
208
+ overflow: auto;
209
+ }
210
+
211
+ .left-panel {
212
+ grid-row: 2 / 3;
213
+ grid-column: 2 / 3;
214
+ border-right: 1px solid var(--line);
215
+ }
216
+
217
+ .right-panel {
218
+ grid-row: 2 / 3;
219
+ grid-column: 4 / 5;
220
+ border-left: 1px solid var(--line);
221
+ }
222
+
223
+ .panel-title {
224
+ margin: 0 0 10px;
225
+ color: var(--muted);
226
+ font-size: 11px;
227
+ font-weight: 800;
228
+ text-transform: uppercase;
229
+ }
230
+
231
+ .source-card, .check {
232
+ border: 1px solid var(--line);
233
+ border-radius: 7px;
234
+ background: var(--card-bg);
235
+ }
236
+
237
+ .source-card {
238
+ padding: 12px;
239
+ margin-bottom: 12px;
240
+ }
241
+
242
+ .source-card strong {
243
+ display: block;
244
+ font-size: 13px;
245
+ margin-bottom: 5px;
246
+ }
247
+
248
+ .source-card p {
249
+ margin: 0;
250
+ color: var(--muted);
251
+ font-size: 12px;
252
+ line-height: 1.4;
253
+ }
254
+
255
+ .format-flow, .checklist {
256
+ display: grid;
257
+ gap: 8px;
258
+ margin: 14px 0;
259
+ }
260
+
261
+ .flow-step {
262
+ display: grid;
263
+ grid-template-columns: 32px 1fr auto;
264
+ align-items: center;
265
+ gap: 9px;
266
+ min-height: 42px;
267
+ padding: 7px;
268
+ border: 1px solid var(--line);
269
+ border-radius: 7px;
270
+ background: var(--panel);
271
+ }
272
+
273
+ .badge {
274
+ height: 26px;
275
+ border-radius: 5px;
276
+ display: grid;
277
+ place-items: center;
278
+ background: oklch(88% 0.035 176);
279
+ color: oklch(32% 0.07 176);
280
+ font-weight: 800;
281
+ font-size: 10px;
282
+ }
283
+
284
+ .flow-step small {
285
+ display: block;
286
+ color: var(--muted);
287
+ font-size: 11px;
288
+ }
289
+
290
+ .insert-grid {
291
+ display: grid;
292
+ grid-template-columns: repeat(2, minmax(0, 1fr));
293
+ gap: 8px;
294
+ }
295
+
296
+ .insert-tile {
297
+ min-height: 58px;
298
+ border: 1px solid var(--line);
299
+ border-radius: 7px;
300
+ padding: 9px;
301
+ background: var(--card-bg);
302
+ font-size: 12px;
303
+ text-align: left;
304
+ }
305
+
306
+ .insert-tile b {
307
+ display: block;
308
+ margin-bottom: 5px;
309
+ }
310
+
311
+ .workspace {
312
+ grid-row: 2 / 3;
313
+ grid-column: 3 / 4;
314
+ min-width: 0;
315
+ display: grid;
316
+ grid-template-rows: 42px auto 1fr;
317
+ background: var(--soft);
318
+ }
319
+
320
+ .mode-strip {
321
+ display: flex;
322
+ align-items: center;
323
+ justify-content: space-between;
324
+ border-bottom: 1px solid var(--line);
325
+ padding: 0 14px;
326
+ }
327
+
328
+ .segmented {
329
+ display: inline-flex;
330
+ gap: 2px;
331
+ padding: 3px;
332
+ border: 1px solid var(--line);
333
+ border-radius: 7px;
334
+ background: var(--card-bg);
335
+ }
336
+
337
+ .segmented button {
338
+ border: 0;
339
+ min-height: 24px;
340
+ background: transparent;
341
+ }
342
+
343
+ .segmented button.active {
344
+ background: var(--ink);
345
+ color: oklch(98% 0.01 92);
346
+ }
347
+
348
+ .large-file-notice {
349
+ padding: 8px 14px;
350
+ border-bottom: 1px solid var(--warning-border);
351
+ background: var(--warning-bg);
352
+ color: var(--warning-ink);
353
+ font-size: 12px;
354
+ }
355
+
356
+ .canvas-wrap {
357
+ min-height: 0;
358
+ display: grid;
359
+ grid-template-columns: minmax(360px, 1fr) minmax(300px, 0.72fr);
360
+ overflow: hidden;
361
+ }
362
+
363
+ .workspace.preview .canvas-wrap {
364
+ grid-template-columns: 1fr;
365
+ }
366
+
367
+ .workspace.markdown .canvas-wrap {
368
+ grid-template-columns: 1fr;
369
+ }
370
+
371
+ .workspace.preview .markdown-pane,
372
+ .workspace.markdown .page-area {
373
+ display: none;
374
+ }
375
+
376
+ .page-area {
377
+ min-width: 0;
378
+ overflow: auto;
379
+ padding: 26px 38px 60px;
380
+ }
381
+
382
+ .page {
383
+ position: relative;
384
+ width: min(760px, 100%);
385
+ min-height: 900px;
386
+ margin: 0 auto;
387
+ padding: 58px 64px;
388
+ border: 1px solid oklch(84% 0.014 92);
389
+ border-radius: 6px;
390
+ background: var(--page-bg);
391
+ box-shadow: var(--shadow);
392
+ }
393
+
394
+ .page-label {
395
+ margin: 0 0 18px;
396
+ color: var(--warning);
397
+ font-size: 11px;
398
+ font-weight: 800;
399
+ text-transform: uppercase;
400
+ }
401
+
402
+ .rich .ProseMirror {
403
+ min-height: 720px;
404
+ outline: none;
405
+ color: var(--page-ink);
406
+ line-height: 1.62;
407
+ }
408
+
409
+ .rich .ProseMirror h1 {
410
+ margin: 0 0 18px;
411
+ max-width: 14ch;
412
+ font-size: 42px;
413
+ line-height: 0.98;
414
+ }
415
+
416
+ .rich .ProseMirror h2 {
417
+ margin-top: 32px;
418
+ font-size: 25px;
419
+ }
420
+
421
+ .rich .ProseMirror p,
422
+ .rich .ProseMirror li {
423
+ font-size: 15px;
424
+ }
425
+
426
+ .rich .ProseMirror blockquote {
427
+ margin: 24px 0;
428
+ padding: 15px;
429
+ border: 1px solid oklch(78% 0.055 176);
430
+ border-radius: 7px;
431
+ background: color-mix(in oklch, var(--accent) 14%, var(--page-bg));
432
+ }
433
+
434
+ .rich .ProseMirror table {
435
+ width: 100%;
436
+ margin: 22px 0;
437
+ border-collapse: collapse;
438
+ font-size: 13px;
439
+ }
440
+
441
+ .rich .ProseMirror th,
442
+ .rich .ProseMirror td {
443
+ border: 1px solid oklch(78% 0.016 92);
444
+ padding: 8px;
445
+ text-align: left;
446
+ }
447
+
448
+ .rich .ProseMirror th {
449
+ background: var(--table-head);
450
+ }
451
+
452
+ .rich img {
453
+ max-width: 100%;
454
+ height: auto;
455
+ border-radius: 5px;
456
+ }
457
+
458
+ .markdown-pane {
459
+ min-width: 0;
460
+ overflow: auto;
461
+ padding: 18px;
462
+ background: var(--source);
463
+ color: var(--source-ink);
464
+ }
465
+
466
+ .source {
467
+ width: 100%;
468
+ min-height: 100%;
469
+ border: 0;
470
+ resize: none;
471
+ outline: none;
472
+ background: transparent;
473
+ color: inherit;
474
+ font: 13px/1.55 Consolas, "Cascadia Mono", monospace;
475
+ }
476
+
477
+ .bubble-menu, .floating-menu {
478
+ display: flex;
479
+ align-items: center;
480
+ gap: 3px;
481
+ padding: 5px;
482
+ border: 1px solid var(--bubble-line);
483
+ border-radius: 8px;
484
+ background: var(--bubble-bg);
485
+ box-shadow: 0 16px 32px rgba(24, 28, 36, 0.22);
486
+ }
487
+
488
+ .floating-menu {
489
+ background: var(--card-bg);
490
+ border-color: var(--line);
491
+ }
492
+
493
+ .bubble-menu button {
494
+ min-width: 28px;
495
+ border-color: oklch(41% 0.03 250);
496
+ background: transparent;
497
+ color: var(--bubble-ink);
498
+ }
499
+
500
+ .floating-menu button {
501
+ min-width: 28px;
502
+ min-height: 26px;
503
+ }
504
+
505
+ .meter {
506
+ height: 8px;
507
+ overflow: hidden;
508
+ border-radius: 999px;
509
+ background: oklch(87% 0.018 92);
510
+ margin-top: 10px;
511
+ }
512
+
513
+ .meter span {
514
+ display: block;
515
+ height: 100%;
516
+ background: linear-gradient(90deg, var(--accent), var(--violet));
517
+ }
518
+
519
+ .check {
520
+ display: grid;
521
+ grid-template-columns: 24px 1fr;
522
+ gap: 8px;
523
+ align-items: start;
524
+ padding: 8px;
525
+ font-size: 12px;
526
+ }
527
+
528
+ .dot {
529
+ width: 16px;
530
+ height: 16px;
531
+ margin-top: 1px;
532
+ border-radius: 999px;
533
+ background: var(--accent);
534
+ }
535
+
536
+ .dot.warn {
537
+ background: var(--warning);
538
+ }
539
+
540
+ .tool-group {
541
+ display: grid;
542
+ grid-template-columns: repeat(2, minmax(0, 1fr));
543
+ gap: 6px;
544
+ margin-bottom: 12px;
545
+ }
546
+
547
+ .tool-group button {
548
+ min-width: 0;
549
+ }
550
+
551
+ .statusbar {
552
+ grid-column: 2 / 5;
553
+ border-top: 1px solid var(--line);
554
+ background: oklch(96% 0.012 92);
555
+ color: var(--muted);
556
+ display: flex;
557
+ align-items: center;
558
+ justify-content: space-between;
559
+ gap: 20px;
560
+ padding: 0 12px;
561
+ font-size: 12px;
562
+ }
563
+
564
+ .statusbar span {
565
+ min-width: 0;
566
+ overflow: hidden;
567
+ text-overflow: ellipsis;
568
+ white-space: nowrap;
569
+ }
570
+
571
+ @media (max-width: 1120px) {
572
+ body {
573
+ min-width: 760px;
574
+ }
575
+
576
+ main.app-shell {
577
+ grid-template-columns: 52px 1fr;
578
+ grid-template-rows: 48px 1fr 30px;
579
+ }
580
+
581
+ .topbar,
582
+ .statusbar {
583
+ grid-column: 2 / 3;
584
+ }
585
+
586
+ .left-panel,
587
+ .right-panel {
588
+ display: none;
589
+ }
590
+
591
+ .workspace {
592
+ grid-column: 2 / 3;
593
+ }
594
+
595
+ .canvas-wrap {
596
+ grid-template-columns: 1fr;
597
+ }
598
+
599
+ .workspace.compare .markdown-pane {
600
+ display: none;
601
+ }
602
+ }
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import path from "node:path";
4
+
5
+ export default defineConfig({
6
+ root: path.resolve(import.meta.dirname),
7
+ base: "./",
8
+ plugins: [react()],
9
+ build: {
10
+ outDir: "../desktop/BuildCorpusEditor/wwwroot",
11
+ emptyOutDir: true,
12
+ },
13
+ });
@@ -0,0 +1,21 @@
1
+ {
2
+ "conversion": {
3
+ "equations": "tex",
4
+ "images": "assets"
5
+ },
6
+ "output": {
7
+ "out": ".codex/build-corpus/out",
8
+ "out_same_dir": false
9
+ },
10
+ "s3": {
11
+ "bucket": "build-corpus-assets",
12
+ "public_base_url": "https://assets.example.com",
13
+ "prefix": "build-corpus",
14
+ "endpoint_url": "https://ACCOUNT_ID.r2.cloudflarestorage.com",
15
+ "region_name": "auto",
16
+ "access_key_id": "%R2_ACCESS_KEY_ID%",
17
+ "secret_access_key": "%R2_SECRET_ACCESS_KEY%",
18
+ "cache_control": "public, max-age=31536000, immutable",
19
+ "acl": null
20
+ }
21
+ }