@zipify/wysiwyg 1.3.0-0 → 2.0.0-1

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 (103) hide show
  1. package/.eslintrc.js +1 -1
  2. package/config/build/lib.config.js +4 -2
  3. package/dist/cli.js +10 -2
  4. package/dist/wysiwyg.css +43 -48
  5. package/dist/wysiwyg.mjs +1928 -787
  6. package/example/ExampleApp.vue +3 -1
  7. package/lib/__tests__/utils/buildTestExtensions.js +14 -0
  8. package/lib/__tests__/utils/index.js +1 -0
  9. package/lib/components/base/Button.vue +0 -7
  10. package/lib/components/base/dropdown/Dropdown.vue +1 -7
  11. package/lib/components/base/dropdown/DropdownActivator.vue +4 -19
  12. package/lib/components/base/dropdown/__tests__/DropdownActivator.test.js +1 -23
  13. package/lib/components/toolbar/controls/AlignmentControl.vue +1 -11
  14. package/lib/components/toolbar/controls/FontColorControl.vue +0 -13
  15. package/lib/components/toolbar/controls/FontFamilyControl.vue +0 -4
  16. package/lib/components/toolbar/controls/FontSizeControl.vue +1 -6
  17. package/lib/components/toolbar/controls/FontWeightControl.vue +0 -12
  18. package/lib/components/toolbar/controls/ItalicControl.vue +0 -13
  19. package/lib/components/toolbar/controls/LineHeightControl.vue +0 -14
  20. package/lib/components/toolbar/controls/StylePresetControl.vue +1 -1
  21. package/lib/components/toolbar/controls/SuperscriptControl.vue +2 -2
  22. package/lib/components/toolbar/controls/UnderlineControl.vue +0 -12
  23. package/lib/components/toolbar/controls/__tests__/AlignmentControl.test.js +5 -72
  24. package/lib/components/toolbar/controls/__tests__/FontColorControl.test.js +1 -22
  25. package/lib/components/toolbar/controls/__tests__/FontFamilyControl.test.js +0 -1
  26. package/lib/components/toolbar/controls/__tests__/FontSizeControl.test.js +0 -1
  27. package/lib/components/toolbar/controls/__tests__/FontWeightControl.test.js +0 -1
  28. package/lib/components/toolbar/controls/__tests__/ItalicControl.test.js +1 -23
  29. package/lib/components/toolbar/controls/__tests__/LineHeightControl.test.js +1 -23
  30. package/lib/components/toolbar/controls/__tests__/StylePresetControl.test.js +2 -2
  31. package/lib/components/toolbar/controls/__tests__/SuperscriptControl.test.js +2 -2
  32. package/lib/components/toolbar/controls/__tests__/UnderlineControl.test.js +1 -25
  33. package/lib/composables/__tests__/useEditor.test.js +2 -2
  34. package/lib/enums/TextSettings.js +5 -5
  35. package/lib/extensions/BackgroundColor.js +4 -4
  36. package/lib/extensions/FontColor.js +4 -5
  37. package/lib/extensions/FontFamily.js +4 -5
  38. package/lib/extensions/FontSize.js +5 -7
  39. package/lib/extensions/FontStyle.js +13 -11
  40. package/lib/extensions/FontWeight.js +6 -9
  41. package/lib/extensions/Link.js +1 -1
  42. package/lib/extensions/StylePreset.js +1 -15
  43. package/lib/extensions/Superscript.js +23 -1
  44. package/lib/extensions/TextDecoration.js +16 -20
  45. package/lib/extensions/__tests__/Alignment.test.js +10 -7
  46. package/lib/extensions/__tests__/BackgroundColor.test.js +6 -3
  47. package/lib/extensions/__tests__/CaseStyle.test.js +11 -7
  48. package/lib/extensions/__tests__/FontColor.test.js +6 -3
  49. package/lib/extensions/__tests__/FontFamily.test.js +29 -22
  50. package/lib/extensions/__tests__/FontSize.test.js +24 -17
  51. package/lib/extensions/__tests__/FontStyle.test.js +22 -16
  52. package/lib/extensions/__tests__/FontWeight.test.js +28 -21
  53. package/lib/extensions/__tests__/LineHeight.test.js +14 -11
  54. package/lib/extensions/__tests__/Link.test.js +14 -10
  55. package/lib/extensions/__tests__/Margin.test.js +2 -2
  56. package/lib/extensions/__tests__/StylePreset.test.js +49 -100
  57. package/lib/extensions/__tests__/TextDecoration.test.js +59 -37
  58. package/lib/extensions/__tests__/__snapshots__/BackgroundColor.test.js.snap +25 -25
  59. package/lib/extensions/__tests__/__snapshots__/FontColor.test.js.snap +25 -25
  60. package/lib/extensions/__tests__/__snapshots__/FontFamily.test.js.snap +105 -105
  61. package/lib/extensions/__tests__/__snapshots__/FontSize.test.js.snap +72 -72
  62. package/lib/extensions/__tests__/__snapshots__/FontStyle.test.js.snap +54 -46
  63. package/lib/extensions/__tests__/__snapshots__/FontWeight.test.js.snap +77 -77
  64. package/lib/extensions/__tests__/__snapshots__/TextDecoration.test.js.snap +68 -3
  65. package/lib/extensions/core/Document.js +5 -0
  66. package/lib/extensions/core/Heading.js +10 -0
  67. package/lib/extensions/core/NodeProcessor.js +112 -10
  68. package/lib/extensions/core/Paragraph.js +9 -0
  69. package/lib/extensions/core/TextProcessor.js +9 -16
  70. package/lib/extensions/core/__tests__/NodeProcessor.test.js +137 -10
  71. package/lib/extensions/core/__tests__/SelectionProcessor.test.js +2 -2
  72. package/lib/extensions/core/__tests__/TextProcessor.test.js +18 -41
  73. package/lib/extensions/core/__tests__/__snapshots__/NodeProcessor.test.js.snap +192 -0
  74. package/lib/extensions/core/__tests__/__snapshots__/TextProcessor.test.js.snap +7 -27
  75. package/lib/extensions/core/index.js +5 -5
  76. package/lib/extensions/core/steps/AddNodeMarkStep.js +60 -0
  77. package/lib/extensions/core/steps/AttrStep.js +54 -0
  78. package/lib/extensions/core/steps/RemoveNodeMarkStep.js +50 -0
  79. package/lib/extensions/core/steps/index.js +3 -0
  80. package/lib/extensions/list/List.js +1 -0
  81. package/lib/extensions/list/ListItem.js +5 -0
  82. package/lib/extensions/list/__tests__/List.test.js +30 -25
  83. package/lib/services/NodeFactory.js +25 -21
  84. package/lib/services/index.js +1 -1
  85. package/lib/services/normalizer/BaseNormalizer.js +11 -0
  86. package/lib/services/{BrowserDomParser.js → normalizer/BrowserDomParser.js} +0 -0
  87. package/lib/services/normalizer/ContentNormalizer.js +24 -0
  88. package/lib/services/normalizer/HtmlNormalizer.js +245 -0
  89. package/lib/services/normalizer/JsonNormalizer.js +81 -0
  90. package/lib/services/{__tests__/ContentNormalizer.test.js → normalizer/__tests__/HtmlNormalizer.test.js} +27 -67
  91. package/lib/services/normalizer/__tests__/JsonNormalizer.test.js +70 -0
  92. package/lib/services/normalizer/__tests__/__snapshots__/JsonNormalizer.test.js.snap +159 -0
  93. package/lib/services/normalizer/index.js +1 -0
  94. package/lib/styles/content.css +8 -0
  95. package/lib/utils/findMarkByType.js +5 -0
  96. package/lib/utils/index.js +5 -0
  97. package/lib/utils/isMarkAppliedToParent.js +10 -0
  98. package/lib/utils/isNodeFullySelected.js +10 -0
  99. package/lib/utils/resolveNodePosition.js +6 -0
  100. package/lib/utils/resolveTextPosition.js +6 -0
  101. package/package.json +3 -1
  102. package/lib/assets/icons/indicator.svg +0 -5
  103. package/lib/services/ContentNormalizer.js +0 -293
@@ -0,0 +1,50 @@
1
+ import { Step, StepResult } from 'prosemirror-transform';
2
+ import { Slice, Fragment } from 'prosemirror-model';
3
+ import { AddNodeMarkStep } from './AddNodeMarkStep';
4
+
5
+ export class RemoveNodeMarkStep extends Step {
6
+ static fromJSON(schema, json) {
7
+ if (typeof json.pos != 'number') {
8
+ throw new RangeError('Invalid input for RemoveNodeMarkStep.fromJSON');
9
+ }
10
+
11
+ return new RemoveNodeMarkStep(json.pos, schema.markFromJSON(json.mark));
12
+ }
13
+
14
+ constructor(pos, mark) {
15
+ super();
16
+ this.pos = pos;
17
+ this.mark = mark;
18
+ }
19
+
20
+ apply(doc) {
21
+ const node = doc.nodeAt(this.pos);
22
+
23
+ if (!node) return StepResult.fail('No node at mark step\'s position');
24
+
25
+ const updated = node.type.create(node.attrs, null, this.mark.removeFromSet(node.marks));
26
+ const slice = new Slice(Fragment.from(updated), 0, node.isLeaf ? 0 : 1);
27
+
28
+ return StepResult.fromReplace(doc, this.pos, this.pos + 1, slice);
29
+ }
30
+
31
+ invert(doc) {
32
+ const node = doc.nodeAt(this.pos);
33
+
34
+ if (!node || !this.mark.isInSet(node.marks)) return this;
35
+
36
+ return new AddNodeMarkStep(this.pos, this.mark);
37
+ }
38
+
39
+ map(mapping) {
40
+ const pos = mapping.mapResult(this.pos, 1);
41
+
42
+ return pos.deletedAfter ? null : new RemoveNodeMarkStep(pos.pos, this.mark);
43
+ }
44
+
45
+ toJSON() {
46
+ return { stepType: 'removeNodeMark', pos: this.pos, mark: this.mark.toJSON() };
47
+ }
48
+ }
49
+
50
+ Step.jsonID('removeNodeMark', RemoveNodeMarkStep);
@@ -0,0 +1,3 @@
1
+ export { AddNodeMarkStep } from './AddNodeMarkStep';
2
+ export { RemoveNodeMarkStep } from './RemoveNodeMarkStep';
3
+ export { AttrStep } from './AttrStep';
@@ -8,6 +8,7 @@ export const List = Node.create({
8
8
  name: NodeTypes.LIST,
9
9
  content: `${NodeTypes.LIST_ITEM}+`,
10
10
  group: 'block list',
11
+ marks: 'settings',
11
12
 
12
13
  addExtensions: () => [
13
14
  ListItem
@@ -3,6 +3,11 @@ import { NodeTypes } from '../../enums';
3
3
 
4
4
  export const ListItem = Base.extend({
5
5
  name: NodeTypes.LIST_ITEM,
6
+ marks: 'settings',
7
+
8
+ addOptions: () => ({
9
+ HTMLAttributes: { class: 'zw-style' }
10
+ }),
6
11
 
7
12
  addKeyboardShortcuts() {
8
13
  const { Enter } = this.parent();
@@ -1,32 +1,32 @@
1
1
  import { Editor } from '@tiptap/vue-2';
2
+ import { buildTestExtensions } from '../../../__tests__/utils';
2
3
  import { ListTypes } from '../../../enums';
3
4
  import { StylePreset } from '../../StylePreset';
4
- import { List } from '../List';
5
5
  import { ContentNormalizer, NodeFactory } from '../../../services';
6
- import { buildCoreExtensions } from '../../core';
6
+ import { List } from '../List';
7
7
 
8
8
  function createEditor({ content }) {
9
9
  return new Editor({
10
10
  content: ContentNormalizer.normalize(content),
11
11
  element: document.createElement('div'),
12
- extensions: buildCoreExtensions().concat(
13
- List.configure({
14
- baseClass: 'zw-list--'
15
- }),
16
- StylePreset.configure({
17
- presets: [
18
- {
19
- id: 'regular-1',
20
- common: {},
21
- mobile: {},
22
- tablet: {},
23
- desktop: {}
24
- }
25
- ],
26
- defaultId: 'regular-1',
27
- baseClass: 'zw ts-'
28
- })
29
- )
12
+ extensions: buildTestExtensions({
13
+ include: [
14
+ List.configure({ baseClass: 'zw-list--' }),
15
+ StylePreset.configure({
16
+ presets: [
17
+ {
18
+ id: 'regular-1',
19
+ common: {},
20
+ mobile: {},
21
+ tablet: {},
22
+ desktop: {}
23
+ }
24
+ ],
25
+ defaultId: 'regular-1',
26
+ baseClass: 'zw ts-'
27
+ })
28
+ ]
29
+ })
30
30
  });
31
31
  }
32
32
 
@@ -67,7 +67,8 @@ describe('apply list', () => {
67
67
  ])
68
68
  });
69
69
 
70
- editor.chain().selectAll().applyList(ListTypes.LATIN).run();
70
+ editor.commands.selectAll();
71
+ editor.commands.applyList(ListTypes.LATIN);
71
72
 
72
73
  expect(editor.getJSON()).toMatchSnapshot();
73
74
  });
@@ -82,7 +83,8 @@ describe('apply list', () => {
82
83
  ])
83
84
  });
84
85
 
85
- editor.chain().selectAll().removeList().run();
86
+ editor.commands.selectAll();
87
+ editor.commands.removeList();
86
88
 
87
89
  expect(editor.getJSON()).toMatchSnapshot();
88
90
  });
@@ -97,7 +99,8 @@ describe('apply list', () => {
97
99
  ])
98
100
  });
99
101
 
100
- editor.chain().selectAll().applyList(ListTypes.LATIN).run();
102
+ editor.commands.selectAll();
103
+ editor.commands.applyList(ListTypes.LATIN);
101
104
 
102
105
  expect(editor.getJSON()).toMatchSnapshot();
103
106
  });
@@ -112,7 +115,8 @@ describe('apply list', () => {
112
115
  ])
113
116
  });
114
117
 
115
- editor.chain().selectAll().applyList(ListTypes.ROMAN).run();
118
+ editor.commands.selectAll();
119
+ editor.commands.applyList(ListTypes.ROMAN);
116
120
 
117
121
  expect(editor.getJSON()).toMatchSnapshot();
118
122
  });
@@ -125,7 +129,8 @@ describe('apply list', () => {
125
129
  ])
126
130
  });
127
131
 
128
- editor.chain().selectAll().applyList(ListTypes.LATIN).run();
132
+ editor.commands.selectAll();
133
+ editor.commands.applyList(ListTypes.LATIN);
129
134
 
130
135
  expect(editor.getJSON()).toMatchSnapshot();
131
136
  });
@@ -2,57 +2,61 @@ import { NodeTypes } from '../enums';
2
2
 
3
3
  export class NodeFactory {
4
4
  static doc(content) {
5
- return {
6
- type: NodeTypes.DOCUMENT,
7
- content
8
- };
5
+ return { type: NodeTypes.DOCUMENT, content };
9
6
  }
10
7
 
11
8
  static list(type, items) {
12
9
  return {
13
10
  type: NodeTypes.LIST,
14
11
  attrs: { bullet: { type } },
15
- content: items.map((item) => this.listItem([].concat(item)))
12
+
13
+ content: items.map((item) => {
14
+ return item.type === NodeTypes.LIST_ITEM ? item : this.listItem([].concat(item));
15
+ })
16
16
  };
17
17
  }
18
18
 
19
- static listItem(content) {
20
- return {
21
- type: NodeTypes.LIST_ITEM,
22
- content
23
- };
19
+ static listItem(...args) {
20
+ return { type: NodeTypes.LIST_ITEM, ...this.#textBlock(args, this.paragraph) };
24
21
  }
25
22
 
26
23
  static heading(level, ...args) {
27
- const config = this._textBlock(args);
24
+ const config = this.#textBlock(args, this.text);
28
25
 
29
26
  config.attrs ??= {};
30
27
  config.attrs.level = level;
31
28
 
32
- return {
33
- type: NodeTypes.HEADING,
34
- ...config
35
- };
29
+ return { type: NodeTypes.HEADING, ...config };
36
30
  }
37
31
 
38
32
  static paragraph(...args) {
39
33
  return {
40
34
  type: NodeTypes.PARAGRAPH,
41
- ...this._textBlock(args)
35
+ ...this.#textBlock(args, this.text)
42
36
  };
43
37
  }
44
38
 
45
- static _textBlock(args) {
46
- const attrs = args.length === 1 ? null : args[0];
47
- const content = args.length === 1 ? args[0] : args[1];
48
- const children = typeof content === 'string' ? [this.text(content)] : content;
39
+ static #textBlock(args, createChildNode) {
40
+ const { attrs, content, marks } = this.#normalizeTextBlockArgs(args);
41
+ const children = typeof content === 'string' ? [createChildNode.call(this, content)] : content;
49
42
 
50
43
  return {
51
44
  content: children,
52
- ...(attrs ? { attrs } : {})
45
+ ...(attrs ? { attrs } : {}),
46
+ ...(marks ? { marks } : {})
53
47
  };
54
48
  }
55
49
 
50
+ static #normalizeTextBlockArgs(args) {
51
+ if (args.length === 1) {
52
+ return { attrs: null, marks: null, content: args[0] };
53
+ }
54
+ if (args.length === 2) {
55
+ return { attrs: args[0], marks: null, content: args[1] };
56
+ }
57
+ return { attrs: args[0], marks: args[1], content: args[2] };
58
+ }
59
+
56
60
  static text(text, marks) {
57
61
  return {
58
62
  type: NodeTypes.TEXT,
@@ -1,6 +1,6 @@
1
1
  export { JsonSerializer } from './JsonSerializer';
2
2
  export { Storage } from './Storage';
3
3
  export { FavoriteColors } from './FavoriteColors';
4
- export { ContentNormalizer } from './ContentNormalizer';
4
+ export { ContentNormalizer } from './normalizer';
5
5
  export { ContextWindow } from './ContextWidnow';
6
6
  export { NodeFactory } from './NodeFactory';
@@ -0,0 +1,11 @@
1
+ export class BaseNormalizer {
2
+ content;
3
+
4
+ constructor({ content }) {
5
+ this.content = content;
6
+ }
7
+
8
+ normalize() {
9
+ throw new Error('Implement abstract method');
10
+ }
11
+ }
@@ -0,0 +1,24 @@
1
+ import { BrowserDomParser } from './BrowserDomParser';
2
+ import { HtmlNormalizer } from './HtmlNormalizer';
3
+ import { JsonNormalizer } from './JsonNormalizer';
4
+
5
+ export class ContentNormalizer {
6
+ static build(content, options = {}) {
7
+ return typeof content === 'string' ? this.#buildHtml(content, options) : this.#buildJson(content);
8
+ }
9
+
10
+ static #buildHtml(content, options) {
11
+ return new HtmlNormalizer({
12
+ content,
13
+ parser: options.parser || new BrowserDomParser()
14
+ });
15
+ }
16
+
17
+ static #buildJson(content) {
18
+ return new JsonNormalizer({ content });
19
+ }
20
+
21
+ static normalize(content, options = {}) {
22
+ return ContentNormalizer.build(content, options).normalize();
23
+ }
24
+ }
@@ -0,0 +1,245 @@
1
+ import { BaseNormalizer } from './BaseNormalizer';
2
+
3
+ export class HtmlNormalizer extends BaseNormalizer {
4
+ static BLOCK_NODE_NAMES = ['P', 'H1', 'H2', 'H3', 'H4'];
5
+
6
+ static BLOCK_STYLES = [
7
+ 'text-align',
8
+ 'line-height',
9
+ 'margin',
10
+ 'margin-top',
11
+ 'margin-bottom',
12
+ 'margin-left',
13
+ 'margin-right'
14
+ ];
15
+
16
+ #parser;
17
+
18
+ constructor({ content, parser }) {
19
+ super({ content });
20
+ this.#parser = parser;
21
+ this.dom = null;
22
+ }
23
+
24
+ normalize() {
25
+ this.normalizeHTML();
26
+ return this.normalizedHTML;
27
+ }
28
+
29
+ normalizeHTML() {
30
+ this.dom = this.#parser.parse(this.content.replace(/(\r)?\n/g, ''));
31
+
32
+ this.#removeComments();
33
+ this.#iterateNodes(this.#normalizeBreakLines, (node) => node.tagName === 'BR');
34
+ this.#iterateNodes(this.#removeEmptyNodes, this.#isBlockNode);
35
+ this.#iterateNodes(this.#normalizeListItems, (node) => node.tagName === 'LI');
36
+ this.#normalizeBlockTextDecoration();
37
+ }
38
+
39
+ get normalizedHTML() {
40
+ return this.dom.body.innerHTML;
41
+ }
42
+
43
+ get #NodeFilter() {
44
+ return this.#parser.types.NodeFilter;
45
+ }
46
+
47
+ get #Node() {
48
+ return this.#parser.types.Node;
49
+ }
50
+
51
+ #removeComments() {
52
+ const iterator = this.#createNodeIterator(this.#NodeFilter.SHOW_COMMENT);
53
+
54
+ this.#runIterator(iterator, (node) => node.remove());
55
+ }
56
+
57
+ #createNodeIterator(whatToShow, filter) {
58
+ return this.dom.createNodeIterator(this.dom.body, whatToShow, filter);
59
+ }
60
+
61
+ #iterateNodes(handler, condition = () => true) {
62
+ const checkCondition = (node) => node.tagName !== 'BODY' && condition.call(this, node);
63
+
64
+ const iterator = this.#createNodeIterator(this.#NodeFilter.SHOW_ELEMENT, {
65
+ acceptNode: (node) => checkCondition(node) ? this.#NodeFilter.FILTER_ACCEPT : this.#NodeFilter.FILTER_REJECT
66
+ });
67
+
68
+ this.#runIterator(iterator, handler);
69
+ }
70
+
71
+ #runIterator(iterator, handler) {
72
+ let currentNode = iterator.nextNode();
73
+
74
+ while (currentNode) {
75
+ handler.call(this, currentNode);
76
+ currentNode = iterator.nextNode();
77
+ }
78
+ }
79
+
80
+ #removeEmptyNodes(node) {
81
+ if (!node.innerHTML.trim()) node.remove();
82
+ }
83
+
84
+ #normalizeListItems(itemEl) {
85
+ const fragment = this.dom.createDocumentFragment();
86
+ const children = Array.from(itemEl.childNodes);
87
+ let capturingParagraph;
88
+ let previousNode;
89
+
90
+ const append = (node) => {
91
+ this.#assignElementProperties(node, itemEl, HtmlNormalizer.BLOCK_STYLES);
92
+ fragment.append(node);
93
+ };
94
+
95
+ this.#assignElementProperties(itemEl, itemEl.parentElement, HtmlNormalizer.BLOCK_STYLES);
96
+
97
+ for (const node of children) {
98
+ if (this.#isBlockNode(node)) {
99
+ append(node);
100
+ capturingParagraph = null;
101
+ previousNode = node;
102
+ continue;
103
+ }
104
+
105
+ if (node.tagName === 'BR' && previousNode && previousNode?.tagName !== 'BR') {
106
+ node.remove();
107
+ previousNode = node;
108
+ continue;
109
+ }
110
+
111
+ if (node.tagName === 'BR') {
112
+ const emptyLineEl = this.dom.createElement('p');
113
+
114
+ emptyLineEl.append(node);
115
+ append(emptyLineEl);
116
+ capturingParagraph = null;
117
+ previousNode = node;
118
+ continue;
119
+ }
120
+
121
+ if (!capturingParagraph) {
122
+ capturingParagraph = this.dom.createElement('p');
123
+ append(capturingParagraph);
124
+ }
125
+
126
+ capturingParagraph.append(node);
127
+ previousNode = node;
128
+ }
129
+
130
+ itemEl.append(fragment);
131
+ this.#removeStyleProperties(itemEl, HtmlNormalizer.BLOCK_STYLES);
132
+ }
133
+
134
+ #isBlockNode(node) {
135
+ return HtmlNormalizer.BLOCK_NODE_NAMES.includes(node.tagName);
136
+ }
137
+
138
+ #assignElementProperties(target, source, properties) {
139
+ for (const property of properties) {
140
+ const value = source.style.getPropertyValue(property);
141
+
142
+ if (value && !target.style.getPropertyValue(property)) {
143
+ target.style.setProperty(property, value);
144
+ }
145
+ }
146
+ }
147
+
148
+ #removeStyleProperties(element, properties) {
149
+ for (const property of properties) {
150
+ element.style.removeProperty(property);
151
+ }
152
+
153
+ if (element.style.length === 0) {
154
+ element.removeAttribute('style');
155
+ }
156
+ }
157
+
158
+ #normalizeBreakLines({ parentElement }) {
159
+ if (!this.#isBlockNode(parentElement)) return;
160
+ if (!parentElement.textContent) return;
161
+
162
+ const fragment = this.dom.createDocumentFragment();
163
+ const children = Array.from(parentElement.childNodes);
164
+ const parentTemplate = parentElement.cloneNode(true);
165
+
166
+ parentTemplate.innerHTML = '';
167
+ let capturingParagraph = parentTemplate.cloneNode();
168
+
169
+ const append = (node) => {
170
+ this.#assignElementProperties(node, parentElement, HtmlNormalizer.BLOCK_STYLES);
171
+ fragment.append(node);
172
+ };
173
+
174
+ for (const child of children) {
175
+ if (child.tagName === 'BR') {
176
+ append(capturingParagraph);
177
+ capturingParagraph = parentTemplate.cloneNode();
178
+ continue;
179
+ }
180
+
181
+ capturingParagraph.append(child);
182
+ }
183
+
184
+ fragment.append(capturingParagraph);
185
+ parentElement.replaceWith(fragment);
186
+ }
187
+
188
+ #normalizeBlockTextDecoration() {
189
+ const blockEls = this.dom.querySelectorAll('[style*="text-decoration"]:where(p, h1, h2, h3, h4, li)');
190
+
191
+ for (const blockEl of blockEls) {
192
+ this.#moveTextDecorationToChildren(blockEl);
193
+ }
194
+ }
195
+
196
+ #moveTextDecorationToChildren(blockEl) {
197
+ const blockDecoration = this.#parseTextDecoration(blockEl);
198
+
199
+ blockEl.style.removeProperty('text-decoration-line');
200
+ blockEl.style.removeProperty('text-decoration');
201
+ if (!blockEl.style.cssText) blockEl.removeAttribute('style');
202
+
203
+ if (blockDecoration.none) return;
204
+
205
+ for (const childNode of blockEl.childNodes) {
206
+ const textEl = this.#wrapTextNode(blockEl, childNode);
207
+ const textDecoration = this.#parseTextDecoration(textEl);
208
+
209
+ const mergedDecoration = {
210
+ underline: textDecoration.underline || blockDecoration.underline,
211
+ line_through: textDecoration.line_through || blockDecoration.line_through
212
+ };
213
+
214
+ textEl.style.removeProperty('text-decoration-line');
215
+ textEl.style.removeProperty('text-decoration');
216
+
217
+ textEl.style.textDecoration = Object.entries(mergedDecoration)
218
+ .filter(([, value]) => value)
219
+ .map(([name]) => name.replace('_', '-'))
220
+ .join(' ');
221
+ }
222
+ }
223
+
224
+ #parseTextDecoration(element) {
225
+ const { textDecoration, textDecorationLine } = element.style;
226
+ const decoration = textDecoration || textDecorationLine || '';
227
+
228
+ return {
229
+ none: decoration.includes('none'),
230
+ underline: decoration.includes('underline'),
231
+ line_through: decoration.includes('line-through')
232
+ };
233
+ }
234
+
235
+ #wrapTextNode(parent, node) {
236
+ if (node.nodeType !== this.#Node.TEXT_NODE) return node;
237
+
238
+ const span = this.dom.createElement('span');
239
+
240
+ span.append(node.cloneNode());
241
+ parent.replaceChild(span, node);
242
+
243
+ return span;
244
+ }
245
+ }
@@ -0,0 +1,81 @@
1
+ import { isEqual } from 'lodash';
2
+ import { NodeTypes, TextSettings } from '../../enums';
3
+ import { BaseNormalizer } from './BaseNormalizer';
4
+
5
+ export class JsonNormalizer extends BaseNormalizer {
6
+ normalize() {
7
+ this.#iterateNodes(this.#bubbleMarks);
8
+ return this.content;
9
+ }
10
+
11
+ #iterateNodes(handler) {
12
+ this.#iterateChildNodes(this.content, handler);
13
+ }
14
+
15
+ #iterateChildNodes(node, handler) {
16
+ for (const child of node.content) {
17
+ handler.call(this, child);
18
+ child.content && this.#iterateChildNodes(child, handler);
19
+ }
20
+ }
21
+
22
+ #bubbleMarks(node) {
23
+ if (!node.content) return;
24
+ if (node.type === NodeTypes.LIST) return;
25
+
26
+ for (const child of node.content) {
27
+ if (!child.marks) continue;
28
+
29
+ for (const childMark of child.marks.slice()) {
30
+ if (this.#canBubbleMark(node, childMark)) {
31
+ this.#removeMark(child, childMark);
32
+ this.#addMark(node, childMark);
33
+ continue;
34
+ }
35
+
36
+ if (this.#includesMark(node, childMark)) {
37
+ this.#removeMark(child, childMark);
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ #canBubbleMark(node, childMark) {
44
+ if (TextSettings.inlineMarks.includes(childMark.type)) return false;
45
+ if (this.#includesMarkType(node, childMark.type)) return false;
46
+
47
+ for (const child of node.content) {
48
+ if (!child.marks) return false;
49
+ if (!this.#includesMarkType(child, childMark.type)) return false;
50
+ }
51
+
52
+ return true;
53
+ }
54
+
55
+ #includesMark(node, checkingMark) {
56
+ return node.marks?.some((mark) => isEqual(mark, checkingMark)) ?? false;
57
+ }
58
+
59
+ #includesMarkType(node, type) {
60
+ return node.marks?.some((mark) => mark.type === type) ?? false;
61
+ }
62
+
63
+ #removeMark(node, mark) {
64
+ if (!node.marks) return;
65
+
66
+ const index = this.#findMarkIndexByType(node, mark.type);
67
+
68
+ if (index >= 0) node.marks.splice(index, 1);
69
+ if (!node.marks.length) delete node.marks;
70
+ }
71
+
72
+ #addMark(node, mark) {
73
+ this.#removeMark(node, mark);
74
+ node.marks ??= [];
75
+ node.marks.push(mark);
76
+ }
77
+
78
+ #findMarkIndexByType(node, type) {
79
+ return node.marks?.findIndex((mark) => mark.type === type) ?? null;
80
+ }
81
+ }