quasar-ui-danx 0.5.0 → 0.5.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 (81) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/dist/danx.es.js +16119 -10641
  3. package/dist/danx.es.js.map +1 -1
  4. package/dist/danx.umd.js +202 -123
  5. package/dist/danx.umd.js.map +1 -1
  6. package/dist/style.css +1 -1
  7. package/package.json +8 -1
  8. package/src/components/Utility/Buttons/ActionButton.vue +15 -5
  9. package/src/components/Utility/Code/CodeViewer.vue +41 -16
  10. package/src/components/Utility/Code/CodeViewerCollapsed.vue +2 -0
  11. package/src/components/Utility/Code/CodeViewerFooter.vue +3 -1
  12. package/src/components/Utility/Code/LanguageBadge.vue +278 -5
  13. package/src/components/Utility/Code/MarkdownContent.vue +31 -163
  14. package/src/components/Utility/Code/index.ts +3 -0
  15. package/src/components/Utility/Markdown/ContextMenu.vue +314 -0
  16. package/src/components/Utility/Markdown/HotkeyHelpPopover.vue +259 -0
  17. package/src/components/Utility/Markdown/LineTypeMenu.vue +226 -0
  18. package/src/components/Utility/Markdown/LinkPopover.vue +331 -0
  19. package/src/components/Utility/Markdown/MarkdownEditor.vue +233 -0
  20. package/src/components/Utility/Markdown/MarkdownEditorContent.vue +296 -0
  21. package/src/components/Utility/Markdown/MarkdownEditorFooter.vue +50 -0
  22. package/src/components/Utility/Markdown/TablePopover.vue +420 -0
  23. package/src/components/Utility/Markdown/index.ts +11 -0
  24. package/src/components/Utility/Markdown/types.ts +27 -0
  25. package/src/components/Utility/Widgets/LabelPillWidget.vue +20 -0
  26. package/src/components/Utility/index.ts +1 -0
  27. package/src/composables/index.ts +1 -0
  28. package/src/composables/markdown/features/useBlockquotes.spec.ts +428 -0
  29. package/src/composables/markdown/features/useBlockquotes.ts +248 -0
  30. package/src/composables/markdown/features/useCodeBlockManager.ts +369 -0
  31. package/src/composables/markdown/features/useCodeBlocks.spec.ts +805 -0
  32. package/src/composables/markdown/features/useCodeBlocks.ts +774 -0
  33. package/src/composables/markdown/features/useContextMenu.ts +444 -0
  34. package/src/composables/markdown/features/useFocusTracking.ts +116 -0
  35. package/src/composables/markdown/features/useHeadings.spec.ts +834 -0
  36. package/src/composables/markdown/features/useHeadings.ts +290 -0
  37. package/src/composables/markdown/features/useInlineFormatting.spec.ts +705 -0
  38. package/src/composables/markdown/features/useInlineFormatting.ts +402 -0
  39. package/src/composables/markdown/features/useLineTypeMenu.ts +285 -0
  40. package/src/composables/markdown/features/useLinks.spec.ts +388 -0
  41. package/src/composables/markdown/features/useLinks.ts +374 -0
  42. package/src/composables/markdown/features/useLists.spec.ts +834 -0
  43. package/src/composables/markdown/features/useLists.ts +747 -0
  44. package/src/composables/markdown/features/usePopoverManager.ts +181 -0
  45. package/src/composables/markdown/features/useTables.spec.ts +1601 -0
  46. package/src/composables/markdown/features/useTables.ts +1107 -0
  47. package/src/composables/markdown/index.ts +16 -0
  48. package/src/composables/markdown/useMarkdownEditor.spec.ts +332 -0
  49. package/src/composables/markdown/useMarkdownEditor.ts +1077 -0
  50. package/src/composables/markdown/useMarkdownHotkeys.spec.ts +791 -0
  51. package/src/composables/markdown/useMarkdownHotkeys.ts +266 -0
  52. package/src/composables/markdown/useMarkdownSelection.ts +219 -0
  53. package/src/composables/markdown/useMarkdownSync.ts +549 -0
  54. package/src/composables/useCodeFormat.ts +17 -10
  55. package/src/composables/useCodeViewerEditor.spec.ts +655 -0
  56. package/src/composables/useCodeViewerEditor.ts +174 -20
  57. package/src/helpers/formats/highlightCSS.ts +236 -0
  58. package/src/helpers/formats/highlightHTML.ts +483 -0
  59. package/src/helpers/formats/highlightJavaScript.ts +346 -0
  60. package/src/helpers/formats/highlightSyntax.ts +15 -4
  61. package/src/helpers/formats/index.ts +3 -0
  62. package/src/helpers/formats/markdown/htmlToMarkdown/convertHeadings.ts +41 -0
  63. package/src/helpers/formats/markdown/htmlToMarkdown/index.spec.ts +489 -0
  64. package/src/helpers/formats/markdown/htmlToMarkdown/index.ts +425 -0
  65. package/src/helpers/formats/markdown/index.ts +7 -0
  66. package/src/helpers/formats/markdown/linePatterns.spec.ts +498 -0
  67. package/src/helpers/formats/markdown/linePatterns.ts +172 -0
  68. package/src/styles/danx.scss +3 -3
  69. package/src/styles/index.scss +5 -5
  70. package/src/styles/themes/danx/code.scss +257 -1
  71. package/src/styles/themes/danx/index.scss +10 -10
  72. package/src/styles/themes/danx/markdown.scss +59 -0
  73. package/src/test/helpers/editorTestUtils.spec.ts +296 -0
  74. package/src/test/helpers/editorTestUtils.ts +253 -0
  75. package/src/test/helpers/index.ts +1 -0
  76. package/src/test/highlighters.test.ts +153 -0
  77. package/src/test/setup.test.ts +12 -0
  78. package/src/test/setup.ts +12 -0
  79. package/src/types/widgets.d.ts +2 -2
  80. package/vite.config.js +5 -1
  81. package/vitest.config.ts +19 -0
@@ -0,0 +1,253 @@
1
+ import { ref } from 'vue';
2
+ import { htmlToMarkdown } from '../../helpers/formats/markdown/htmlToMarkdown';
3
+
4
+ export interface TestEditorResult {
5
+ /** The contenteditable container element */
6
+ container: HTMLElement;
7
+ /** Get the current HTML content */
8
+ getHtml: () => string;
9
+ /** Get the markdown output from current HTML */
10
+ getMarkdown: () => string;
11
+ /** Get cursor position as { node, offset } */
12
+ getCursorPosition: () => { node: Node | null; offset: number };
13
+ /** Get cursor offset within a specific block */
14
+ getCursorOffsetInBlock: (blockIndex: number) => number;
15
+ /** Set cursor at a position in a specific text node */
16
+ setCursor: (node: Node, offset: number) => void;
17
+ /** Set cursor at offset within block's text content */
18
+ setCursorInBlock: (blockIndex: number, offset: number) => void;
19
+ /** Select text range */
20
+ selectRange: (startNode: Node, startOffset: number, endNode: Node, endOffset: number) => void;
21
+ /** Select text within a block by offsets */
22
+ selectInBlock: (blockIndex: number, startOffset: number, endOffset: number) => void;
23
+ /** Simulate a keydown event */
24
+ pressKey: (key: string, modifiers?: { ctrl?: boolean; shift?: boolean; alt?: boolean; meta?: boolean }) => void;
25
+ /** Type text at current cursor position */
26
+ type: (text: string) => void;
27
+ /** Get block element by index */
28
+ getBlock: (index: number) => Element | null;
29
+ /** Get all block elements */
30
+ getBlocks: () => Element[];
31
+ /** Get the contentRef as a Vue ref (for composables) */
32
+ contentRef: ReturnType<typeof ref<HTMLElement | null>>;
33
+ /** Cleanup function */
34
+ destroy: () => void;
35
+ }
36
+
37
+ /**
38
+ * Find a text node at a given offset within an element's text content.
39
+ * Returns the text node and the offset within that node.
40
+ */
41
+ function findTextNodeAtOffset(element: Element, targetOffset: number): { node: Text; offset: number } | null {
42
+ let currentOffset = 0;
43
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
44
+ let node: Text | null;
45
+ let lastNode: Text | null = null;
46
+
47
+ while ((node = walker.nextNode() as Text)) {
48
+ lastNode = node;
49
+ const nodeLength = node.textContent?.length || 0;
50
+ if (currentOffset + nodeLength >= targetOffset) {
51
+ return { node, offset: targetOffset - currentOffset };
52
+ }
53
+ currentOffset += nodeLength;
54
+ }
55
+
56
+ // If we're past the content, return end of last text node
57
+ if (lastNode) {
58
+ return { node: lastNode, offset: lastNode.textContent?.length || 0 };
59
+ }
60
+
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * Get the key code string for common keys
66
+ */
67
+ function getKeyCode(key: string): string {
68
+ const keyCodes: Record<string, string> = {
69
+ 'Enter': 'Enter',
70
+ 'Backspace': 'Backspace',
71
+ 'Delete': 'Delete',
72
+ 'Tab': 'Tab',
73
+ 'Escape': 'Escape',
74
+ 'ArrowUp': 'ArrowUp',
75
+ 'ArrowDown': 'ArrowDown',
76
+ 'ArrowLeft': 'ArrowLeft',
77
+ 'ArrowRight': 'ArrowRight',
78
+ ' ': 'Space',
79
+ };
80
+
81
+ if (keyCodes[key]) {
82
+ return keyCodes[key];
83
+ }
84
+
85
+ // For single letters, use KeyX format
86
+ if (key.length === 1 && /[a-zA-Z]/.test(key)) {
87
+ return `Key${key.toUpperCase()}`;
88
+ }
89
+
90
+ // For digits
91
+ if (key.length === 1 && /[0-9]/.test(key)) {
92
+ return `Digit${key}`;
93
+ }
94
+
95
+ return key;
96
+ }
97
+
98
+ /**
99
+ * Create a test editor with initial HTML content
100
+ */
101
+ export function createTestEditor(initialHtml: string): TestEditorResult {
102
+ // Create container
103
+ const container = document.createElement('div');
104
+ container.setAttribute('contenteditable', 'true');
105
+ container.innerHTML = initialHtml.trim();
106
+ document.body.appendChild(container);
107
+
108
+ // Create Vue ref for composables
109
+ const contentRef = ref<HTMLElement | null>(container);
110
+
111
+ function getHtml(): string {
112
+ return container.innerHTML;
113
+ }
114
+
115
+ function getMarkdown(): string {
116
+ return htmlToMarkdown(container);
117
+ }
118
+
119
+ function getCursorPosition(): { node: Node | null; offset: number } {
120
+ const sel = window.getSelection();
121
+ if (!sel || sel.rangeCount === 0) {
122
+ return { node: null, offset: 0 };
123
+ }
124
+ const range = sel.getRangeAt(0);
125
+ return { node: range.startContainer, offset: range.startOffset };
126
+ }
127
+
128
+ function getBlock(index: number): Element | null {
129
+ const blocks = Array.from(container.children);
130
+ return blocks[index] || null;
131
+ }
132
+
133
+ function getBlocks(): Element[] {
134
+ return Array.from(container.children);
135
+ }
136
+
137
+ function getCursorOffsetInBlock(blockIndex: number): number {
138
+ const block = getBlock(blockIndex);
139
+ if (!block) return -1;
140
+
141
+ const sel = window.getSelection();
142
+ if (!sel || sel.rangeCount === 0) return -1;
143
+
144
+ const range = sel.getRangeAt(0);
145
+ if (!block.contains(range.startContainer)) return -1;
146
+
147
+ // Calculate offset within block's text content
148
+ const preRange = document.createRange();
149
+ preRange.selectNodeContents(block);
150
+ preRange.setEnd(range.startContainer, range.startOffset);
151
+ return preRange.toString().length;
152
+ }
153
+
154
+ function setCursor(node: Node, offset: number): void {
155
+ const range = document.createRange();
156
+ range.setStart(node, offset);
157
+ range.collapse(true);
158
+
159
+ const sel = window.getSelection();
160
+ sel?.removeAllRanges();
161
+ sel?.addRange(range);
162
+ }
163
+
164
+ function setCursorInBlock(blockIndex: number, offset: number): void {
165
+ const block = getBlock(blockIndex);
166
+ if (!block) return;
167
+
168
+ const result = findTextNodeAtOffset(block, offset);
169
+ if (result) {
170
+ setCursor(result.node, result.offset);
171
+ }
172
+ }
173
+
174
+ function selectRange(startNode: Node, startOffset: number, endNode: Node, endOffset: number): void {
175
+ const range = document.createRange();
176
+ range.setStart(startNode, startOffset);
177
+ range.setEnd(endNode, endOffset);
178
+
179
+ const sel = window.getSelection();
180
+ sel?.removeAllRanges();
181
+ sel?.addRange(range);
182
+ }
183
+
184
+ function selectInBlock(blockIndex: number, startOffset: number, endOffset: number): void {
185
+ const block = getBlock(blockIndex);
186
+ if (!block) return;
187
+
188
+ const start = findTextNodeAtOffset(block, startOffset);
189
+ const end = findTextNodeAtOffset(block, endOffset);
190
+
191
+ if (start && end) {
192
+ selectRange(start.node, start.offset, end.node, end.offset);
193
+ }
194
+ }
195
+
196
+ function pressKey(key: string, modifiers: { ctrl?: boolean; shift?: boolean; alt?: boolean; meta?: boolean } = {}): void {
197
+ const event = new KeyboardEvent('keydown', {
198
+ key,
199
+ code: getKeyCode(key),
200
+ ctrlKey: modifiers.ctrl || false,
201
+ shiftKey: modifiers.shift || false,
202
+ altKey: modifiers.alt || false,
203
+ metaKey: modifiers.meta || false,
204
+ bubbles: true,
205
+ cancelable: true,
206
+ });
207
+
208
+ container.dispatchEvent(event);
209
+ }
210
+
211
+ function type(text: string): void {
212
+ // Insert text at cursor position
213
+ const sel = window.getSelection();
214
+ if (!sel || sel.rangeCount === 0) return;
215
+
216
+ const range = sel.getRangeAt(0);
217
+ range.deleteContents();
218
+
219
+ const textNode = document.createTextNode(text);
220
+ range.insertNode(textNode);
221
+
222
+ // Move cursor after inserted text
223
+ range.setStartAfter(textNode);
224
+ range.collapse(true);
225
+ sel.removeAllRanges();
226
+ sel.addRange(range);
227
+
228
+ // Dispatch input event
229
+ container.dispatchEvent(new InputEvent('input', { bubbles: true }));
230
+ }
231
+
232
+ function destroy(): void {
233
+ container.remove();
234
+ }
235
+
236
+ return {
237
+ container,
238
+ getHtml,
239
+ getMarkdown,
240
+ getCursorPosition,
241
+ getCursorOffsetInBlock,
242
+ setCursor,
243
+ setCursorInBlock,
244
+ selectRange,
245
+ selectInBlock,
246
+ pressKey,
247
+ type,
248
+ getBlock,
249
+ getBlocks,
250
+ contentRef,
251
+ destroy,
252
+ };
253
+ }
@@ -0,0 +1 @@
1
+ export * from './editorTestUtils';
@@ -0,0 +1,153 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { highlightCSS } from "../helpers/formats/highlightCSS";
3
+ import { highlightJavaScript } from "../helpers/formats/highlightJavaScript";
4
+ import { highlightHTML } from "../helpers/formats/highlightHTML";
5
+ import { highlightSyntax } from "../helpers/formats/highlightSyntax";
6
+
7
+ describe("CSS Highlighter", () => {
8
+ it("highlights selectors", () => {
9
+ const result = highlightCSS(".test { }");
10
+ expect(result).toContain("syntax-selector");
11
+ expect(result).toContain(".test");
12
+ });
13
+
14
+ it("highlights properties and values", () => {
15
+ const result = highlightCSS(".test { color: red; }");
16
+ expect(result).toContain("syntax-property");
17
+ expect(result).toContain("syntax-value");
18
+ });
19
+
20
+ it("highlights comments", () => {
21
+ const result = highlightCSS("/* comment */");
22
+ expect(result).toContain("syntax-comment");
23
+ });
24
+
25
+ it("highlights at-rules", () => {
26
+ const result = highlightCSS("@media screen { }");
27
+ expect(result).toContain("syntax-at-rule");
28
+ expect(result).toContain("@media");
29
+ });
30
+
31
+ it("highlights strings", () => {
32
+ const result = highlightCSS('@import "file.css";');
33
+ expect(result).toContain("syntax-string");
34
+ });
35
+ });
36
+
37
+ describe("JavaScript Highlighter", () => {
38
+ it("highlights keywords", () => {
39
+ const result = highlightJavaScript("const x = 1;");
40
+ expect(result).toContain("syntax-keyword");
41
+ expect(result).toContain("const");
42
+ });
43
+
44
+ it("highlights strings", () => {
45
+ const result = highlightJavaScript('const s = "hello";');
46
+ expect(result).toContain("syntax-string");
47
+ });
48
+
49
+ it("highlights numbers", () => {
50
+ const result = highlightJavaScript("const n = 42;");
51
+ expect(result).toContain("syntax-number");
52
+ expect(result).toContain("42");
53
+ });
54
+
55
+ it("highlights single-line comments", () => {
56
+ const result = highlightJavaScript("// comment");
57
+ expect(result).toContain("syntax-comment");
58
+ });
59
+
60
+ it("highlights multi-line comments", () => {
61
+ const result = highlightJavaScript("/* comment */");
62
+ expect(result).toContain("syntax-comment");
63
+ });
64
+
65
+ it("highlights template literals", () => {
66
+ const result = highlightJavaScript("const t = `hello`;");
67
+ expect(result).toContain("syntax-template");
68
+ });
69
+
70
+ it("highlights boolean values", () => {
71
+ const result = highlightJavaScript("const b = true;");
72
+ expect(result).toContain("syntax-boolean");
73
+ });
74
+
75
+ it("highlights null and undefined", () => {
76
+ const result = highlightJavaScript("const n = null;");
77
+ expect(result).toContain("syntax-null");
78
+ });
79
+
80
+ it("highlights operators", () => {
81
+ const result = highlightJavaScript("x === y");
82
+ expect(result).toContain("syntax-operator");
83
+ });
84
+
85
+ it("highlights regex", () => {
86
+ const result = highlightJavaScript("const r = /test/gi;");
87
+ expect(result).toContain("syntax-regex");
88
+ });
89
+ });
90
+
91
+ describe("HTML Highlighter", () => {
92
+ it("highlights tags", () => {
93
+ const result = highlightHTML("<div></div>");
94
+ expect(result).toContain("syntax-tag");
95
+ });
96
+
97
+ it("highlights attributes", () => {
98
+ const result = highlightHTML('<div class="test"></div>');
99
+ expect(result).toContain("syntax-attribute");
100
+ expect(result).toContain("class");
101
+ });
102
+
103
+ it("highlights attribute values as strings", () => {
104
+ const result = highlightHTML('<div class="test"></div>');
105
+ expect(result).toContain("syntax-string");
106
+ });
107
+
108
+ it("highlights comments", () => {
109
+ const result = highlightHTML("<!-- comment -->");
110
+ expect(result).toContain("syntax-comment");
111
+ });
112
+
113
+ it("highlights doctype", () => {
114
+ const result = highlightHTML("<!DOCTYPE html>");
115
+ expect(result).toContain("syntax-doctype");
116
+ });
117
+
118
+ it("highlights embedded CSS in style tags", () => {
119
+ const result = highlightHTML("<style>.test { color: red; }</style>");
120
+ expect(result).toContain("syntax-selector");
121
+ expect(result).toContain("syntax-property");
122
+ expect(result).toContain("syntax-value");
123
+ });
124
+
125
+ it("highlights embedded JavaScript in script tags", () => {
126
+ const result = highlightHTML("<script>const x = 42;</script>");
127
+ expect(result).toContain("syntax-keyword");
128
+ expect(result).toContain("syntax-number");
129
+ });
130
+ });
131
+
132
+ describe("highlightSyntax dispatcher", () => {
133
+ it("dispatches to CSS highlighter", () => {
134
+ const result = highlightSyntax(".test { }", { format: "css" });
135
+ expect(result).toContain("syntax-selector");
136
+ });
137
+
138
+ it("dispatches to JavaScript highlighter", () => {
139
+ const result = highlightSyntax("const x = 1;", { format: "javascript" });
140
+ expect(result).toContain("syntax-keyword");
141
+ });
142
+
143
+ it("dispatches to HTML highlighter", () => {
144
+ const result = highlightSyntax("<div></div>", { format: "html" });
145
+ expect(result).toContain("syntax-tag");
146
+ });
147
+
148
+ it("escapes HTML for text format", () => {
149
+ const result = highlightSyntax("<script>alert(1)</script>", { format: "text" });
150
+ expect(result).toContain("&lt;script&gt;");
151
+ expect(result).not.toContain("<script>");
152
+ });
153
+ });
@@ -0,0 +1,12 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ describe('Vitest Setup', () => {
4
+ it('should run tests successfully', () => {
5
+ expect(true).toBe(true);
6
+ });
7
+
8
+ it('should have access to jsdom environment', () => {
9
+ expect(typeof window).toBe('object');
10
+ expect(typeof document).toBe('object');
11
+ });
12
+ });
@@ -0,0 +1,12 @@
1
+ import { vi } from 'vitest';
2
+
3
+ // Mock window.getSelection for jsdom
4
+ // jsdom's Selection API is limited, so we may need to enhance it
5
+ if (typeof window !== 'undefined') {
6
+ // Ensure getSelection exists
7
+ if (!window.getSelection) {
8
+ window.getSelection = vi.fn(() => null);
9
+ }
10
+ }
11
+
12
+ // Global test utilities can be added here
@@ -1,5 +1,5 @@
1
1
  export interface LabelPillWidgetProps {
2
2
  label?: string | number;
3
- size?: "xs" | "sm" | "md" | "lg";
4
- color?: "sky" | "green" | "red" | "amber" | "yellow" | "blue" | "purple" | "slate" | "slate-mid" | "gray" | "emerald" | "orange" | "lime" | "teal" | "cyan" | "rose" | "indigo" | "violet" | "fuchsia" | "none";
3
+ size?: "xxs" | "xs" | "sm" | "md" | "lg";
4
+ color?: "sky" | "green" | "red" | "amber" | "yellow" | "blue" | "purple" | "slate" | "slate-mid" | "gray" | "emerald" | "orange" | "lime" | "teal" | "cyan" | "rose" | "indigo" | "violet" | "fuchsia" | "sky-soft" | "green-soft" | "red-soft" | "amber-soft" | "yellow-soft" | "blue-soft" | "purple-soft" | "slate-soft" | "gray-soft" | "emerald-soft" | "orange-soft" | "lime-soft" | "teal-soft" | "cyan-soft" | "rose-soft" | "indigo-soft" | "violet-soft" | "fuchsia-soft" | "none";
5
5
  }
package/vite.config.js CHANGED
@@ -3,6 +3,8 @@ import { resolve } from "path";
3
3
  import { defineConfig } from "vite";
4
4
  import svgLoader from "vite-svg-loader";
5
5
 
6
+ const __dirname = import.meta.dirname;
7
+
6
8
  export default defineConfig({
7
9
  plugins: [vue(), svgLoader()],
8
10
  resolve: {
@@ -32,7 +34,9 @@ export default defineConfig({
32
34
  css: {
33
35
  preprocessorOptions: {
34
36
  scss: {
35
- additionalData: `@import "./src/styles/index.scss";`
37
+ api: "modern",
38
+ loadPaths: [resolve(__dirname, "src/styles")],
39
+ additionalData: `@use "index" as *;`
36
40
  }
37
41
  }
38
42
  }
@@ -0,0 +1,19 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { defineConfig } from 'vitest/config';
3
+ import vue from '@vitejs/plugin-vue';
4
+
5
+ export default defineConfig({
6
+ plugins: [vue()],
7
+ test: {
8
+ environment: 'jsdom',
9
+ globals: true,
10
+ include: ['src/**/*.{test,spec}.{js,ts}'],
11
+ setupFiles: ['./src/test/setup.ts'],
12
+ root: fileURLToPath(new URL('./', import.meta.url)),
13
+ },
14
+ resolve: {
15
+ alias: {
16
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
17
+ },
18
+ },
19
+ });