@akinon/ui-editor 1.0.2 → 1.0.4

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 (129) hide show
  1. package/dist/cjs/tiptap/components/__tests__/html-code-viewer.test.js +79 -3
  2. package/dist/cjs/tiptap/components/__tests__/html-code-viewer.test.tsx +106 -3
  3. package/dist/cjs/tiptap/components/__tests__/tiptap.test.js +42 -4
  4. package/dist/cjs/tiptap/components/__tests__/tiptap.test.tsx +57 -7
  5. package/dist/cjs/tiptap/components/format-bubble-menu.d.ts.map +1 -1
  6. package/dist/cjs/tiptap/components/format-bubble-menu.js +6 -5
  7. package/dist/cjs/tiptap/components/html-code-viewer.d.ts +2 -1
  8. package/dist/cjs/tiptap/components/html-code-viewer.d.ts.map +1 -1
  9. package/dist/cjs/tiptap/components/html-code-viewer.js +62 -22
  10. package/dist/cjs/tiptap/components/image-upload-placeholder.d.ts.map +1 -1
  11. package/dist/cjs/tiptap/components/image-upload-placeholder.js +5 -2
  12. package/dist/cjs/tiptap/components/secure-script-component.d.ts.map +1 -1
  13. package/dist/cjs/tiptap/components/secure-script-component.js +2 -5
  14. package/dist/cjs/tiptap/components/tiptap.d.ts +1 -1
  15. package/dist/cjs/tiptap/components/tiptap.d.ts.map +1 -1
  16. package/dist/cjs/tiptap/components/tiptap.js +84 -56
  17. package/dist/cjs/tiptap/constants/editor-config.d.ts +10 -1
  18. package/dist/cjs/tiptap/constants/editor-config.d.ts.map +1 -1
  19. package/dist/cjs/tiptap/constants/editor-config.js +11 -2
  20. package/dist/cjs/tiptap/constants/html-elements.d.ts +1 -0
  21. package/dist/cjs/tiptap/constants/html-elements.d.ts.map +1 -1
  22. package/dist/cjs/tiptap/constants/html-elements.js +2 -1
  23. package/dist/cjs/tiptap/extensions/comment-extension.d.ts.map +1 -1
  24. package/dist/cjs/tiptap/extensions/comment-extension.js +3 -3
  25. package/dist/cjs/tiptap/extensions/custom-table-cell-extension.d.ts.map +1 -1
  26. package/dist/cjs/tiptap/extensions/custom-table-cell-extension.js +11 -2
  27. package/dist/cjs/tiptap/extensions/custom-table-header-extension.d.ts.map +1 -1
  28. package/dist/cjs/tiptap/extensions/custom-table-header-extension.js +11 -2
  29. package/dist/cjs/tiptap/extensions/html-block-extension.d.ts.map +1 -1
  30. package/dist/cjs/tiptap/extensions/html-block-extension.js +11 -14
  31. package/dist/cjs/tiptap/extensions/html-inline-extension.d.ts.map +1 -1
  32. package/dist/cjs/tiptap/extensions/html-inline-extension.js +11 -14
  33. package/dist/cjs/tiptap/types/index.d.ts +12 -5
  34. package/dist/cjs/tiptap/types/index.d.ts.map +1 -1
  35. package/dist/cjs/tiptap/utils/__tests__/clean-html.test.d.ts +2 -0
  36. package/dist/cjs/tiptap/utils/__tests__/clean-html.test.d.ts.map +1 -0
  37. package/dist/cjs/tiptap/utils/__tests__/clean-html.test.js +93 -0
  38. package/dist/cjs/tiptap/utils/__tests__/clean-html.test.ts +129 -0
  39. package/dist/cjs/tiptap/utils/__tests__/data-attributes-helper.test.d.ts +2 -0
  40. package/dist/cjs/tiptap/utils/__tests__/data-attributes-helper.test.d.ts.map +1 -0
  41. package/dist/cjs/tiptap/utils/__tests__/data-attributes-helper.test.js +105 -0
  42. package/dist/cjs/tiptap/utils/__tests__/data-attributes-helper.test.ts +148 -0
  43. package/dist/cjs/tiptap/utils/__tests__/index.test.js +0 -2
  44. package/dist/cjs/tiptap/utils/__tests__/index.test.ts +0 -2
  45. package/dist/cjs/tiptap/utils/__tests__/prettier-format.test.d.ts +2 -0
  46. package/dist/cjs/tiptap/utils/__tests__/prettier-format.test.d.ts.map +1 -0
  47. package/dist/cjs/tiptap/utils/__tests__/prettier-format.test.js +33 -0
  48. package/dist/cjs/tiptap/utils/__tests__/prettier-format.test.ts +49 -0
  49. package/dist/cjs/tiptap/utils/__tests__/tiptap-extensions.test.js +157 -0
  50. package/dist/cjs/tiptap/utils/__tests__/tiptap-extensions.test.ts +234 -0
  51. package/dist/cjs/tiptap/utils/clean-html.d.ts +9 -0
  52. package/dist/cjs/tiptap/utils/clean-html.d.ts.map +1 -0
  53. package/dist/cjs/tiptap/utils/clean-html.js +75 -0
  54. package/dist/cjs/tiptap/utils/data-attributes-helper.d.ts +20 -0
  55. package/dist/cjs/tiptap/utils/data-attributes-helper.d.ts.map +1 -0
  56. package/dist/cjs/tiptap/utils/data-attributes-helper.js +68 -0
  57. package/dist/cjs/tiptap/utils/index.d.ts +3 -2
  58. package/dist/cjs/tiptap/utils/index.d.ts.map +1 -1
  59. package/dist/cjs/tiptap/utils/index.js +3 -2
  60. package/dist/cjs/tiptap/utils/prettier-format.d.ts +8 -0
  61. package/dist/cjs/tiptap/utils/prettier-format.d.ts.map +1 -0
  62. package/dist/cjs/tiptap/utils/prettier-format.js +26 -0
  63. package/dist/cjs/tiptap/utils/tiptap-extensions.d.ts.map +1 -1
  64. package/dist/cjs/tiptap/utils/tiptap-extensions.js +134 -14
  65. package/dist/esm/tiptap/components/__tests__/html-code-viewer.test.js +79 -3
  66. package/dist/esm/tiptap/components/__tests__/html-code-viewer.test.tsx +106 -3
  67. package/dist/esm/tiptap/components/__tests__/tiptap.test.js +42 -4
  68. package/dist/esm/tiptap/components/__tests__/tiptap.test.tsx +57 -7
  69. package/dist/esm/tiptap/components/format-bubble-menu.d.ts.map +1 -1
  70. package/dist/esm/tiptap/components/format-bubble-menu.js +6 -5
  71. package/dist/esm/tiptap/components/html-code-viewer.d.ts +2 -1
  72. package/dist/esm/tiptap/components/html-code-viewer.d.ts.map +1 -1
  73. package/dist/esm/tiptap/components/html-code-viewer.js +62 -22
  74. package/dist/esm/tiptap/components/image-upload-placeholder.d.ts.map +1 -1
  75. package/dist/esm/tiptap/components/image-upload-placeholder.js +5 -2
  76. package/dist/esm/tiptap/components/secure-script-component.d.ts.map +1 -1
  77. package/dist/esm/tiptap/components/secure-script-component.js +2 -5
  78. package/dist/esm/tiptap/components/tiptap.d.ts +1 -1
  79. package/dist/esm/tiptap/components/tiptap.d.ts.map +1 -1
  80. package/dist/esm/tiptap/components/tiptap.js +84 -56
  81. package/dist/esm/tiptap/constants/editor-config.d.ts +10 -1
  82. package/dist/esm/tiptap/constants/editor-config.d.ts.map +1 -1
  83. package/dist/esm/tiptap/constants/editor-config.js +11 -2
  84. package/dist/esm/tiptap/constants/html-elements.d.ts +1 -0
  85. package/dist/esm/tiptap/constants/html-elements.d.ts.map +1 -1
  86. package/dist/esm/tiptap/constants/html-elements.js +2 -1
  87. package/dist/esm/tiptap/extensions/comment-extension.d.ts.map +1 -1
  88. package/dist/esm/tiptap/extensions/comment-extension.js +3 -3
  89. package/dist/esm/tiptap/extensions/custom-table-cell-extension.d.ts.map +1 -1
  90. package/dist/esm/tiptap/extensions/custom-table-cell-extension.js +11 -2
  91. package/dist/esm/tiptap/extensions/custom-table-header-extension.d.ts.map +1 -1
  92. package/dist/esm/tiptap/extensions/custom-table-header-extension.js +11 -2
  93. package/dist/esm/tiptap/extensions/html-block-extension.d.ts.map +1 -1
  94. package/dist/esm/tiptap/extensions/html-block-extension.js +11 -14
  95. package/dist/esm/tiptap/extensions/html-inline-extension.d.ts.map +1 -1
  96. package/dist/esm/tiptap/extensions/html-inline-extension.js +11 -14
  97. package/dist/esm/tiptap/types/index.d.ts +12 -5
  98. package/dist/esm/tiptap/types/index.d.ts.map +1 -1
  99. package/dist/esm/tiptap/utils/__tests__/clean-html.test.d.ts +2 -0
  100. package/dist/esm/tiptap/utils/__tests__/clean-html.test.d.ts.map +1 -0
  101. package/dist/esm/tiptap/utils/__tests__/clean-html.test.js +93 -0
  102. package/dist/esm/tiptap/utils/__tests__/clean-html.test.ts +129 -0
  103. package/dist/esm/tiptap/utils/__tests__/data-attributes-helper.test.d.ts +2 -0
  104. package/dist/esm/tiptap/utils/__tests__/data-attributes-helper.test.d.ts.map +1 -0
  105. package/dist/esm/tiptap/utils/__tests__/data-attributes-helper.test.js +105 -0
  106. package/dist/esm/tiptap/utils/__tests__/data-attributes-helper.test.ts +148 -0
  107. package/dist/esm/tiptap/utils/__tests__/index.test.js +0 -2
  108. package/dist/esm/tiptap/utils/__tests__/index.test.ts +0 -2
  109. package/dist/esm/tiptap/utils/__tests__/prettier-format.test.d.ts +2 -0
  110. package/dist/esm/tiptap/utils/__tests__/prettier-format.test.d.ts.map +1 -0
  111. package/dist/esm/tiptap/utils/__tests__/prettier-format.test.js +33 -0
  112. package/dist/esm/tiptap/utils/__tests__/prettier-format.test.ts +49 -0
  113. package/dist/esm/tiptap/utils/__tests__/tiptap-extensions.test.js +157 -0
  114. package/dist/esm/tiptap/utils/__tests__/tiptap-extensions.test.ts +234 -0
  115. package/dist/esm/tiptap/utils/clean-html.d.ts +9 -0
  116. package/dist/esm/tiptap/utils/clean-html.d.ts.map +1 -0
  117. package/dist/esm/tiptap/utils/clean-html.js +75 -0
  118. package/dist/esm/tiptap/utils/data-attributes-helper.d.ts +20 -0
  119. package/dist/esm/tiptap/utils/data-attributes-helper.d.ts.map +1 -0
  120. package/dist/esm/tiptap/utils/data-attributes-helper.js +68 -0
  121. package/dist/esm/tiptap/utils/index.d.ts +3 -2
  122. package/dist/esm/tiptap/utils/index.d.ts.map +1 -1
  123. package/dist/esm/tiptap/utils/index.js +3 -2
  124. package/dist/esm/tiptap/utils/prettier-format.d.ts +8 -0
  125. package/dist/esm/tiptap/utils/prettier-format.d.ts.map +1 -0
  126. package/dist/esm/tiptap/utils/prettier-format.js +26 -0
  127. package/dist/esm/tiptap/utils/tiptap-extensions.d.ts.map +1 -1
  128. package/dist/esm/tiptap/utils/tiptap-extensions.js +134 -14
  129. package/package.json +13 -12
@@ -0,0 +1,33 @@
1
+ import * as prettier from 'prettier/standalone';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { formatHtmlWithPrettier } from '../prettier-format';
4
+ vi.mock('prettier/standalone', () => ({
5
+ format: vi.fn()
6
+ }));
7
+ vi.mock('prettier/plugins/html', async (importOriginal) => {
8
+ const mod = await importOriginal();
9
+ return Object.assign({}, mod);
10
+ });
11
+ describe('prettier-format', () => {
12
+ it('should format html successfully', async () => {
13
+ const input = '<div> test </div>';
14
+ const formatted = '<div>test</div>';
15
+ vi.mocked(prettier.format).mockResolvedValue(formatted);
16
+ const result = await formatHtmlWithPrettier(input);
17
+ expect(prettier.format).toHaveBeenCalledWith(input, expect.objectContaining({
18
+ parser: 'html',
19
+ printWidth: 60
20
+ }));
21
+ expect(result).toBe(formatted);
22
+ });
23
+ it('should return original html and warn when formatting fails', async () => {
24
+ const input = 'Invalid HTML';
25
+ const error = new Error('Parse error');
26
+ vi.mocked(prettier.format).mockRejectedValue(error);
27
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
28
+ const result = await formatHtmlWithPrettier(input);
29
+ expect(result).toBe(input);
30
+ expect(consoleSpy).not.toHaveBeenCalled();
31
+ consoleSpy.mockRestore();
32
+ });
33
+ });
@@ -0,0 +1,49 @@
1
+ import * as prettier from 'prettier/standalone';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+
4
+ import { formatHtmlWithPrettier } from '../prettier-format';
5
+
6
+ vi.mock('prettier/standalone', () => ({
7
+ format: vi.fn()
8
+ }));
9
+
10
+ vi.mock('prettier/plugins/html', async importOriginal => {
11
+ const mod = await importOriginal<typeof import('prettier/plugins/html')>();
12
+ return { ...mod };
13
+ });
14
+
15
+ describe('prettier-format', () => {
16
+ it('should format html successfully', async () => {
17
+ const input = '<div> test </div>';
18
+ const formatted = '<div>test</div>';
19
+
20
+ vi.mocked(prettier.format).mockResolvedValue(formatted);
21
+
22
+ const result = await formatHtmlWithPrettier(input);
23
+
24
+ expect(prettier.format).toHaveBeenCalledWith(
25
+ input,
26
+ expect.objectContaining({
27
+ parser: 'html',
28
+ printWidth: 60
29
+ })
30
+ );
31
+ expect(result).toBe(formatted);
32
+ });
33
+
34
+ it('should return original html and warn when formatting fails', async () => {
35
+ const input = 'Invalid HTML';
36
+ const error = new Error('Parse error');
37
+
38
+ vi.mocked(prettier.format).mockRejectedValue(error);
39
+
40
+ const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
41
+
42
+ const result = await formatHtmlWithPrettier(input);
43
+
44
+ expect(result).toBe(input);
45
+ expect(consoleSpy).not.toHaveBeenCalled();
46
+
47
+ consoleSpy.mockRestore();
48
+ });
49
+ });
@@ -1,12 +1,30 @@
1
+ import Blockquote from '@tiptap/extension-blockquote';
2
+ import CodeBlock from '@tiptap/extension-code-block';
1
3
  import FileHandler from '@tiptap/extension-file-handler';
4
+ import Heading from '@tiptap/extension-heading';
2
5
  import HorizontalRule from '@tiptap/extension-horizontal-rule';
6
+ import { BulletList, ListItem, OrderedList } from '@tiptap/extension-list';
3
7
  import Paragraph from '@tiptap/extension-paragraph';
8
+ import { Table, TableRow } from '@tiptap/extension-table';
4
9
  import { describe, expect, it, vi } from 'vitest';
5
10
  import { getExtensions, handleImageFile, validateAutoLink, validateLink } from '../tiptap-extensions';
6
11
  vi.mock('@tiptap/extension-document', () => ({ default: { extend: vi.fn() } }));
7
12
  vi.mock('@tiptap/extension-paragraph', () => ({
8
13
  default: { extend: vi.fn() }
9
14
  }));
15
+ vi.mock('@tiptap/extension-blockquote', () => ({
16
+ default: { extend: vi.fn() }
17
+ }));
18
+ vi.mock('@tiptap/extension-heading', () => ({
19
+ default: {
20
+ extend: vi.fn(() => ({
21
+ configure: vi.fn()
22
+ }))
23
+ }
24
+ }));
25
+ vi.mock('@tiptap/extension-code-block', () => ({
26
+ default: { extend: vi.fn() }
27
+ }));
10
28
  vi.mock('@tiptap/extension-horizontal-rule', () => ({
11
29
  default: {
12
30
  extend: vi.fn(config => (Object.assign(Object.assign({}, config), { name: 'horizontalRule' })))
@@ -24,6 +42,16 @@ vi.mock('@tiptap/extension-file-handler', () => ({
24
42
  }))
25
43
  }
26
44
  }));
45
+ vi.mock('@tiptap/extension-table', async (importOriginal) => {
46
+ const actual = await importOriginal();
47
+ return Object.assign(Object.assign({}, actual), { Table: Object.assign(Object.assign({}, actual.Table), { extend: vi.fn(() => ({
48
+ configure: vi.fn()
49
+ })) }), TableRow: Object.assign(Object.assign({}, actual.TableRow), { extend: vi.fn(() => ({})) }) });
50
+ });
51
+ vi.mock('@tiptap/extension-list', async (importOriginal) => {
52
+ const actual = await importOriginal();
53
+ return Object.assign(Object.assign({}, actual), { OrderedList: Object.assign(Object.assign({}, actual.OrderedList), { extend: vi.fn() }), ListItem: Object.assign(Object.assign({}, actual.ListItem), { extend: vi.fn() }), BulletList: Object.assign(Object.assign({}, actual.BulletList), { extend: vi.fn() }), TaskItem: Object.assign(Object.assign({}, actual.TaskItem), { configure: vi.fn() }) });
54
+ });
27
55
  describe('tiptap-extensions', () => {
28
56
  describe('getExtensions', () => {
29
57
  it('should return an array of extensions', () => {
@@ -39,6 +67,15 @@ describe('tiptap-extensions', () => {
39
67
  expect(attrs).toHaveProperty('style');
40
68
  expect(attrs).toHaveProperty('class');
41
69
  expect(attrs).toHaveProperty('id');
70
+ const render = config.renderHTML({
71
+ HTMLAttributes: { class: 'p-cls' },
72
+ node: { attrs: { dataAttributes: { 'data-test': 'val' } } }
73
+ });
74
+ expect(render[0]).toBe('p');
75
+ expect(render[1]).toEqual(expect.objectContaining({
76
+ class: 'p-cls',
77
+ 'data-test': 'val'
78
+ }));
42
79
  });
43
80
  it('should configure HorizontalRule with addNodeView', () => {
44
81
  expect(HorizontalRule.extend).toHaveBeenCalled();
@@ -70,6 +107,126 @@ describe('tiptap-extensions', () => {
70
107
  options.onDrop(editor, files, 10);
71
108
  vi.stubGlobal('FileReader', undefined);
72
109
  });
110
+ it('should configure Table and TableRow extensions with attributes', () => {
111
+ getExtensions();
112
+ expect(Table.extend).toHaveBeenCalled();
113
+ // @ts-expect-error Mock implementation
114
+ const tableConfig = Table.extend.mock.calls[0][0];
115
+ const tableAttrs = tableConfig.addAttributes();
116
+ expect(tableAttrs).toHaveProperty('style');
117
+ expect(tableAttrs).toHaveProperty('class');
118
+ const tableRender = tableConfig.renderHTML({
119
+ HTMLAttributes: { class: 'test' },
120
+ node: { attrs: { dataAttributes: { 'data-foo': 'bar' } } }
121
+ });
122
+ // Expected: ['table', attrs, ['tbody', 0]]
123
+ expect(tableRender[0]).toBe('table');
124
+ expect(tableRender[1]).toEqual(expect.objectContaining({
125
+ 'data-foo': 'bar',
126
+ class: 'test'
127
+ }));
128
+ expect(tableRender[2]).toBe(0);
129
+ expect(TableRow.extend).toHaveBeenCalled();
130
+ // @ts-expect-error Mock implementation
131
+ const rowConfig = TableRow.extend.mock.calls[0][0];
132
+ const rowAttrs = rowConfig.addAttributes();
133
+ expect(rowAttrs).toHaveProperty('id');
134
+ const rowRender = rowConfig.renderHTML({
135
+ HTMLAttributes: {},
136
+ node: { attrs: { dataAttributes: {} } }
137
+ });
138
+ expect(rowRender[0]).toBe('tr');
139
+ });
140
+ it('should configure List extensions with attributes', () => {
141
+ getExtensions();
142
+ expect(OrderedList.extend).toHaveBeenCalled();
143
+ // @ts-expect-error Mock implementation
144
+ const olConfig = OrderedList.extend.mock.calls[0][0];
145
+ const olAttrs = olConfig.addAttributes();
146
+ expect(olAttrs).toHaveProperty('class');
147
+ const olRender = olConfig.renderHTML({
148
+ HTMLAttributes: { style: 'list-style: none' },
149
+ node: { attrs: { dataAttributes: {}, otherAttrs: {} } }
150
+ });
151
+ expect(olRender[0]).toBe('ol');
152
+ expect(olRender[1]).toHaveProperty('style', 'list-style: none');
153
+ expect(ListItem.extend).toHaveBeenCalled();
154
+ // @ts-expect-error Mock implementation
155
+ const liConfig = ListItem.extend.mock.calls[0][0];
156
+ const liAttrs = liConfig.addAttributes();
157
+ expect(liAttrs).toHaveProperty('id');
158
+ const liRender = liConfig.renderHTML({
159
+ HTMLAttributes: { class: 'item' },
160
+ node: { attrs: { dataAttributes: { 'data-active': 'true' } } }
161
+ });
162
+ expect(liRender[0]).toBe('li');
163
+ expect(liRender[1]).toEqual(expect.objectContaining({
164
+ 'data-active': 'true',
165
+ class: 'item'
166
+ }));
167
+ });
168
+ it('should configure BulletList and CodeBlock extensions with attributes', () => {
169
+ getExtensions();
170
+ expect(BulletList.extend).toHaveBeenCalled();
171
+ // @ts-expect-error Mock implementation
172
+ const blConfig = BulletList.extend.mock.calls[0][0];
173
+ const blAttrs = blConfig.addAttributes();
174
+ expect(blAttrs).toHaveProperty('class');
175
+ const blRender = blConfig.renderHTML({
176
+ HTMLAttributes: { class: 'ul-cls' },
177
+ node: { attrs: { dataAttributes: {} } }
178
+ });
179
+ expect(blRender[0]).toBe('ul');
180
+ expect(CodeBlock.extend).toHaveBeenCalled();
181
+ // @ts-expect-error Mock implementation
182
+ const cbConfig = CodeBlock.extend.mock.calls[0][0];
183
+ const cbAttrs = cbConfig.addAttributes();
184
+ expect(cbAttrs).toHaveProperty('style');
185
+ const cbRender = cbConfig.renderHTML({
186
+ HTMLAttributes: { class: 'code-block' },
187
+ node: { attrs: { dataAttributes: { 'data-lang': 'js' } } }
188
+ });
189
+ expect(cbRender[0]).toBe('pre');
190
+ expect(cbRender[1]).toHaveProperty('data-lang', 'js');
191
+ expect(cbRender[2][0]).toBe('code');
192
+ });
193
+ it('should configure Heading and Blockquote extensions with attributes', () => {
194
+ getExtensions();
195
+ expect(Heading.extend).toHaveBeenCalled();
196
+ // @ts-expect-error Mock implementation
197
+ const headingConfig = Heading.extend.mock.calls[0][0];
198
+ const headingAttrs = headingConfig.addAttributes();
199
+ expect(headingAttrs).toHaveProperty('class');
200
+ const context = { options: { levels: [1, 2, 3, 4, 5, 6] } };
201
+ const headingRender = headingConfig.renderHTML.call(context, {
202
+ HTMLAttributes: { style: 'heading-style' },
203
+ node: { attrs: { level: 1, dataAttributes: {}, otherAttrs: {} } }
204
+ });
205
+ expect(headingRender[0]).toBe('h1');
206
+ expect(headingRender[1]).toHaveProperty('style', 'heading-style');
207
+ // Test invalid level fallback
208
+ const invalidLevelRender = headingConfig.renderHTML.call(context, {
209
+ HTMLAttributes: {},
210
+ node: { attrs: { level: 99, dataAttributes: {} } }
211
+ });
212
+ // Should fallback to levels[0] which is 1
213
+ expect(invalidLevelRender[0]).toBe('h1');
214
+ expect(Blockquote.extend).toHaveBeenCalled();
215
+ // @ts-expect-error Mock implementation
216
+ const bqConfig = Blockquote.extend.mock.calls[0][0];
217
+ const bqAttrs = bqConfig.addAttributes();
218
+ expect(bqAttrs).toHaveProperty('id');
219
+ const bqRender = bqConfig.renderHTML({
220
+ HTMLAttributes: { class: 'bq' },
221
+ node: { attrs: { dataAttributes: { 'data-foo': 'bar' } } }
222
+ });
223
+ // Expected ['blockquote', attrs, TIPTAP_CONTENT_HOLE]
224
+ expect(bqRender[0]).toBe('blockquote');
225
+ expect(bqRender[1]).toEqual(expect.objectContaining({
226
+ 'data-foo': 'bar',
227
+ class: 'bq'
228
+ }));
229
+ });
73
230
  });
74
231
  describe('validateLink', () => {
75
232
  const ctx = {
@@ -1,7 +1,12 @@
1
1
  import { ChainedCommands, Editor } from '@tiptap/core';
2
+ import Blockquote from '@tiptap/extension-blockquote';
3
+ import CodeBlock from '@tiptap/extension-code-block';
2
4
  import FileHandler from '@tiptap/extension-file-handler';
5
+ import Heading from '@tiptap/extension-heading';
3
6
  import HorizontalRule from '@tiptap/extension-horizontal-rule';
7
+ import { BulletList, ListItem, OrderedList } from '@tiptap/extension-list';
4
8
  import Paragraph from '@tiptap/extension-paragraph';
9
+ import { Table, TableRow } from '@tiptap/extension-table';
5
10
  import { describe, expect, it, vi } from 'vitest';
6
11
 
7
12
  import {
@@ -17,6 +22,22 @@ vi.mock('@tiptap/extension-paragraph', () => ({
17
22
  default: { extend: vi.fn() }
18
23
  }));
19
24
 
25
+ vi.mock('@tiptap/extension-blockquote', () => ({
26
+ default: { extend: vi.fn() }
27
+ }));
28
+
29
+ vi.mock('@tiptap/extension-heading', () => ({
30
+ default: {
31
+ extend: vi.fn(() => ({
32
+ configure: vi.fn()
33
+ }))
34
+ }
35
+ }));
36
+
37
+ vi.mock('@tiptap/extension-code-block', () => ({
38
+ default: { extend: vi.fn() }
39
+ }));
40
+
20
41
  vi.mock('@tiptap/extension-horizontal-rule', () => ({
21
42
  default: {
22
43
  extend: vi.fn(config => ({ ...config, name: 'horizontalRule' }))
@@ -45,6 +66,48 @@ vi.mock('@tiptap/extension-file-handler', () => ({
45
66
  }
46
67
  }));
47
68
 
69
+ vi.mock('@tiptap/extension-table', async importOriginal => {
70
+ const actual =
71
+ await importOriginal<typeof import('@tiptap/extension-table')>();
72
+ return {
73
+ ...actual,
74
+ Table: {
75
+ ...actual.Table,
76
+ extend: vi.fn(() => ({
77
+ configure: vi.fn()
78
+ }))
79
+ },
80
+ TableRow: {
81
+ ...actual.TableRow,
82
+ extend: vi.fn(() => ({}))
83
+ }
84
+ };
85
+ });
86
+
87
+ vi.mock('@tiptap/extension-list', async importOriginal => {
88
+ const actual =
89
+ await importOriginal<typeof import('@tiptap/extension-list')>();
90
+ return {
91
+ ...actual,
92
+ OrderedList: {
93
+ ...actual.OrderedList,
94
+ extend: vi.fn()
95
+ },
96
+ ListItem: {
97
+ ...actual.ListItem,
98
+ extend: vi.fn()
99
+ },
100
+ BulletList: {
101
+ ...actual.BulletList,
102
+ extend: vi.fn()
103
+ },
104
+ TaskItem: {
105
+ ...actual.TaskItem,
106
+ configure: vi.fn()
107
+ }
108
+ };
109
+ });
110
+
48
111
  describe('tiptap-extensions', () => {
49
112
  describe('getExtensions', () => {
50
113
  it('should return an array of extensions', () => {
@@ -64,6 +127,18 @@ describe('tiptap-extensions', () => {
64
127
  expect(attrs).toHaveProperty('style');
65
128
  expect(attrs).toHaveProperty('class');
66
129
  expect(attrs).toHaveProperty('id');
130
+
131
+ const render = config.renderHTML({
132
+ HTMLAttributes: { class: 'p-cls' },
133
+ node: { attrs: { dataAttributes: { 'data-test': 'val' } } }
134
+ });
135
+ expect(render[0]).toBe('p');
136
+ expect(render[1]).toEqual(
137
+ expect.objectContaining({
138
+ class: 'p-cls',
139
+ 'data-test': 'val'
140
+ })
141
+ );
67
142
  });
68
143
 
69
144
  it('should configure HorizontalRule with addNodeView', () => {
@@ -111,6 +186,165 @@ describe('tiptap-extensions', () => {
111
186
 
112
187
  vi.stubGlobal('FileReader', undefined);
113
188
  });
189
+
190
+ it('should configure Table and TableRow extensions with attributes', () => {
191
+ getExtensions();
192
+
193
+ expect(Table.extend).toHaveBeenCalled();
194
+ // @ts-expect-error Mock implementation
195
+ const tableConfig = Table.extend.mock.calls[0][0];
196
+
197
+ const tableAttrs = tableConfig.addAttributes();
198
+ expect(tableAttrs).toHaveProperty('style');
199
+ expect(tableAttrs).toHaveProperty('class');
200
+
201
+ const tableRender = tableConfig.renderHTML({
202
+ HTMLAttributes: { class: 'test' },
203
+ node: { attrs: { dataAttributes: { 'data-foo': 'bar' } } }
204
+ });
205
+
206
+ // Expected: ['table', attrs, ['tbody', 0]]
207
+ expect(tableRender[0]).toBe('table');
208
+ expect(tableRender[1]).toEqual(
209
+ expect.objectContaining({
210
+ 'data-foo': 'bar',
211
+ class: 'test'
212
+ })
213
+ );
214
+ expect(tableRender[2]).toBe(0);
215
+
216
+ expect(TableRow.extend).toHaveBeenCalled();
217
+ // @ts-expect-error Mock implementation
218
+ const rowConfig = TableRow.extend.mock.calls[0][0];
219
+
220
+ const rowAttrs = rowConfig.addAttributes();
221
+ expect(rowAttrs).toHaveProperty('id');
222
+
223
+ const rowRender = rowConfig.renderHTML({
224
+ HTMLAttributes: {},
225
+ node: { attrs: { dataAttributes: {} } }
226
+ });
227
+ expect(rowRender[0]).toBe('tr');
228
+ });
229
+
230
+ it('should configure List extensions with attributes', () => {
231
+ getExtensions();
232
+
233
+ expect(OrderedList.extend).toHaveBeenCalled();
234
+ // @ts-expect-error Mock implementation
235
+ const olConfig = OrderedList.extend.mock.calls[0][0];
236
+
237
+ const olAttrs = olConfig.addAttributes();
238
+ expect(olAttrs).toHaveProperty('class');
239
+
240
+ const olRender = olConfig.renderHTML({
241
+ HTMLAttributes: { style: 'list-style: none' },
242
+ node: { attrs: { dataAttributes: {}, otherAttrs: {} } }
243
+ });
244
+
245
+ expect(olRender[0]).toBe('ol');
246
+ expect(olRender[1]).toHaveProperty('style', 'list-style: none');
247
+
248
+ expect(ListItem.extend).toHaveBeenCalled();
249
+ // @ts-expect-error Mock implementation
250
+ const liConfig = ListItem.extend.mock.calls[0][0];
251
+
252
+ const liAttrs = liConfig.addAttributes();
253
+ expect(liAttrs).toHaveProperty('id');
254
+
255
+ const liRender = liConfig.renderHTML({
256
+ HTMLAttributes: { class: 'item' },
257
+ node: { attrs: { dataAttributes: { 'data-active': 'true' } } }
258
+ });
259
+ expect(liRender[0]).toBe('li');
260
+ expect(liRender[1]).toEqual(
261
+ expect.objectContaining({
262
+ 'data-active': 'true',
263
+ class: 'item'
264
+ })
265
+ );
266
+ });
267
+
268
+ it('should configure BulletList and CodeBlock extensions with attributes', () => {
269
+ getExtensions();
270
+
271
+ expect(BulletList.extend).toHaveBeenCalled();
272
+ // @ts-expect-error Mock implementation
273
+ const blConfig = BulletList.extend.mock.calls[0][0];
274
+
275
+ const blAttrs = blConfig.addAttributes();
276
+ expect(blAttrs).toHaveProperty('class');
277
+
278
+ const blRender = blConfig.renderHTML({
279
+ HTMLAttributes: { class: 'ul-cls' },
280
+ node: { attrs: { dataAttributes: {} } }
281
+ });
282
+ expect(blRender[0]).toBe('ul');
283
+
284
+ expect(CodeBlock.extend).toHaveBeenCalled();
285
+ // @ts-expect-error Mock implementation
286
+ const cbConfig = CodeBlock.extend.mock.calls[0][0];
287
+
288
+ const cbAttrs = cbConfig.addAttributes();
289
+ expect(cbAttrs).toHaveProperty('style');
290
+
291
+ const cbRender = cbConfig.renderHTML({
292
+ HTMLAttributes: { class: 'code-block' },
293
+ node: { attrs: { dataAttributes: { 'data-lang': 'js' } } }
294
+ });
295
+
296
+ expect(cbRender[0]).toBe('pre');
297
+ expect(cbRender[1]).toHaveProperty('data-lang', 'js');
298
+ expect(cbRender[2][0]).toBe('code');
299
+ });
300
+
301
+ it('should configure Heading and Blockquote extensions with attributes', () => {
302
+ getExtensions();
303
+
304
+ expect(Heading.extend).toHaveBeenCalled();
305
+ // @ts-expect-error Mock implementation
306
+ const headingConfig = Heading.extend.mock.calls[0][0];
307
+
308
+ const headingAttrs = headingConfig.addAttributes();
309
+ expect(headingAttrs).toHaveProperty('class');
310
+
311
+ const context = { options: { levels: [1, 2, 3, 4, 5, 6] } };
312
+ const headingRender = headingConfig.renderHTML.call(context, {
313
+ HTMLAttributes: { style: 'heading-style' },
314
+ node: { attrs: { level: 1, dataAttributes: {}, otherAttrs: {} } }
315
+ });
316
+
317
+ expect(headingRender[0]).toBe('h1');
318
+ expect(headingRender[1]).toHaveProperty('style', 'heading-style');
319
+
320
+ // Test invalid level fallback
321
+ const invalidLevelRender = headingConfig.renderHTML.call(context, {
322
+ HTMLAttributes: {},
323
+ node: { attrs: { level: 99, dataAttributes: {} } }
324
+ });
325
+ // Should fallback to levels[0] which is 1
326
+ expect(invalidLevelRender[0]).toBe('h1');
327
+
328
+ expect(Blockquote.extend).toHaveBeenCalled();
329
+ // @ts-expect-error Mock implementation
330
+ const bqConfig = Blockquote.extend.mock.calls[0][0];
331
+
332
+ const bqAttrs = bqConfig.addAttributes();
333
+ expect(bqAttrs).toHaveProperty('id');
334
+
335
+ const bqRender = bqConfig.renderHTML({
336
+ HTMLAttributes: { class: 'bq' },
337
+ node: { attrs: { dataAttributes: { 'data-foo': 'bar' } } }
338
+ });
339
+ // Expected ['blockquote', attrs, TIPTAP_CONTENT_HOLE]
340
+ expect(bqRender[0]).toBe('blockquote');
341
+ expect(bqRender[1]).toEqual(
342
+ expect.objectContaining({
343
+ 'data-foo': 'bar',
344
+ class: 'bq'
345
+ })
346
+ );
347
+ });
114
348
  });
115
349
 
116
350
  describe('validateLink', () => {
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Cleans and normalizes HTML by merging duplicate nested spans with the same attributes.
3
+ * This prevents accumulation of wrapper spans during source ↔ preview mode switches.
4
+ *
5
+ * @param html - Raw HTML string
6
+ * @returns Cleaned HTML with merged duplicate spans
7
+ */
8
+ export declare const cleanDuplicateSpans: (html: string) => string;
9
+ //# sourceMappingURL=clean-html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clean-html.d.ts","sourceRoot":"","sources":["../../../../src/tiptap/utils/clean-html.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,KAAG,MAgFlD,CAAC"}
@@ -0,0 +1,75 @@
1
+ import { HTML_ELEMENTS } from '../constants';
2
+ /**
3
+ * Cleans and normalizes HTML by merging duplicate nested spans with the same attributes.
4
+ * This prevents accumulation of wrapper spans during source ↔ preview mode switches.
5
+ *
6
+ * @param html - Raw HTML string
7
+ * @returns Cleaned HTML with merged duplicate spans
8
+ */
9
+ export const cleanDuplicateSpans = (html) => {
10
+ // Only run in browser environment
11
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
12
+ return html;
13
+ }
14
+ // Use template element to preserve script tags and other content
15
+ const template = document.createElement('template');
16
+ template.innerHTML = html;
17
+ const mergeNestedSpans = (element) => {
18
+ // Process children first (bottom-up)
19
+ Array.from(element.children).forEach(child => mergeNestedSpans(child));
20
+ if (element.tagName === HTML_ELEMENTS.SPAN.toUpperCase()) {
21
+ // 1. Remove empty spans (no attributes, no content or just whitespace)
22
+ if (!element.hasAttributes() &&
23
+ (!element.textContent || !element.textContent.trim())) {
24
+ element.remove();
25
+ return;
26
+ }
27
+ // 2. Unwrap spans with no attributes
28
+ if (!element.hasAttributes()) {
29
+ const parent = element.parentNode;
30
+ if (parent) {
31
+ while (element.firstChild) {
32
+ parent.insertBefore(element.firstChild, element);
33
+ }
34
+ parent.removeChild(element);
35
+ return;
36
+ }
37
+ }
38
+ // 3. Merge with child span if it's the only child
39
+ if (element.children.length === 1 &&
40
+ element.children[0].tagName === HTML_ELEMENTS.SPAN.toUpperCase()) {
41
+ const child = element.children[0];
42
+ const parentStyle = element.getAttribute('style') || '';
43
+ const childStyle = child.getAttribute('style') || '';
44
+ // Simple check: if styles match exactly
45
+ if (parentStyle === childStyle) {
46
+ while (child.firstChild) {
47
+ element.insertBefore(child.firstChild, child);
48
+ }
49
+ element.removeChild(child);
50
+ }
51
+ // If parent has NO style, unwrap parent (keep child attributes)
52
+ else if (!parentStyle) {
53
+ const parentNode = element.parentNode;
54
+ if (parentNode) {
55
+ while (element.firstChild) {
56
+ parentNode.insertBefore(element.firstChild, element);
57
+ }
58
+ parentNode.removeChild(element);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ };
64
+ // Process each child element in the template content
65
+ Array.from(template.content.children).forEach(child => mergeNestedSpans(child));
66
+ // Return the cleaned HTML
67
+ return Array.from(template.content.childNodes)
68
+ .map(node => {
69
+ if (node.nodeType === Node.ELEMENT_NODE) {
70
+ return node.outerHTML;
71
+ }
72
+ return node.textContent || '';
73
+ })
74
+ .join('');
75
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Data Attributes Helper for Tiptap Extensions
3
+ *
4
+ * This helper provides utilities for adding data-* attribute support to Tiptap extensions.
5
+ */
6
+ export declare const createDataAttributesConfig: () => {
7
+ dataAttributes: {
8
+ default: {};
9
+ parseHTML: (element: HTMLElement) => Record<string, string>;
10
+ renderHTML: (attributes: Record<string, unknown>) => {};
11
+ };
12
+ };
13
+ export declare const extractDataAttributes: (nodeAttrs: Record<string, unknown>) => {
14
+ dataAttributes: Record<string, string>;
15
+ otherAttrs: {
16
+ [x: string]: unknown;
17
+ };
18
+ };
19
+ export declare const addDataAttributesToParent: (parent?: () => Record<string, unknown>) => Record<string, unknown>;
20
+ //# sourceMappingURL=data-attributes-helper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data-attributes-helper.d.ts","sourceRoot":"","sources":["../../../../src/tiptap/utils/data-attributes-helper.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,0BAA0B;;;6BAGd,WAAW;iCAUP,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;CAIlD,CAAC;AAEH,eAAO,MAAM,qBAAqB,GAAI,WAAW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;;;;;CAOvE,CAAC;AAEF,eAAO,MAAM,yBAAyB,GACpC,SAAS,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACrC,MAAM,CAAC,MAAM,EAAE,OAAO,CA+BxB,CAAC"}