@zipify/wysiwyg 2.0.0-0 → 2.0.0-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.
- package/.eslintrc.js +1 -1
- package/config/build/lib.config.js +4 -2
- package/dist/cli.js +10 -2
- package/dist/wysiwyg.css +12 -6
- package/dist/wysiwyg.mjs +1507 -406
- package/example/ExampleApp.vue +3 -1
- package/lib/__tests__/utils/buildTestExtensions.js +2 -1
- package/lib/components/toolbar/controls/StylePresetControl.vue +1 -1
- package/lib/components/toolbar/controls/__tests__/StylePresetControl.test.js +6 -6
- package/lib/enums/MarkGroups.js +4 -0
- package/lib/enums/TextSettings.js +5 -5
- package/lib/enums/index.js +1 -0
- package/lib/extensions/BackgroundColor.js +2 -2
- package/lib/extensions/FontColor.js +2 -2
- package/lib/extensions/FontFamily.js +3 -3
- package/lib/extensions/FontSize.js +2 -2
- package/lib/extensions/FontStyle.js +2 -2
- package/lib/extensions/FontWeight.js +2 -2
- package/lib/extensions/Link.js +1 -1
- package/lib/extensions/StylePreset.js +2 -2
- package/lib/extensions/Superscript.js +5 -2
- package/lib/extensions/TextDecoration.js +12 -29
- package/lib/extensions/__tests__/Alignment.test.js +2 -2
- package/lib/extensions/__tests__/BackgroundColor.test.js +4 -3
- package/lib/extensions/__tests__/FontColor.test.js +4 -3
- package/lib/extensions/__tests__/FontFamily.test.js +6 -6
- package/lib/extensions/__tests__/FontSize.test.js +9 -8
- package/lib/extensions/__tests__/FontStyle.test.js +6 -5
- package/lib/extensions/__tests__/FontWeight.test.js +2 -2
- package/lib/extensions/__tests__/LineHeight.test.js +2 -1
- package/lib/extensions/__tests__/Superscript.test.js +102 -0
- package/lib/extensions/__tests__/TextDecoration.test.js +20 -24
- package/lib/extensions/__tests__/__snapshots__/BackgroundColor.test.js.snap +1 -1
- package/lib/extensions/__tests__/__snapshots__/FontColor.test.js.snap +1 -1
- package/lib/extensions/__tests__/__snapshots__/FontFamily.test.js.snap +19 -23
- package/lib/extensions/__tests__/__snapshots__/FontSize.test.js.snap +2 -2
- package/lib/extensions/__tests__/__snapshots__/FontStyle.test.js.snap +1 -1
- package/lib/extensions/__tests__/__snapshots__/FontWeight.test.js.snap +13 -17
- package/lib/extensions/__tests__/__snapshots__/Superscript.test.js.snap +107 -0
- package/lib/extensions/__tests__/__snapshots__/TextDecoration.test.js.snap +102 -102
- package/lib/extensions/core/Document.js +2 -1
- package/lib/extensions/core/Heading.js +2 -1
- package/lib/extensions/core/NodeProcessor.js +51 -19
- package/lib/extensions/core/Paragraph.js +2 -1
- package/lib/extensions/core/TextProcessor.js +0 -5
- package/lib/extensions/core/__tests__/NodeProcessor.test.js +364 -11
- package/lib/extensions/core/__tests__/TextProcessor.test.js +1 -22
- package/lib/extensions/core/__tests__/__snapshots__/NodeProcessor.test.js.snap +309 -0
- package/lib/extensions/core/__tests__/__snapshots__/TextProcessor.test.js.snap +7 -27
- package/lib/extensions/core/steps/AddNodeMarkStep.js +6 -0
- package/lib/extensions/core/steps/AttrStep.js +60 -0
- package/lib/extensions/core/steps/RemoveNodeMarkStep.js +6 -0
- package/lib/extensions/core/steps/index.js +1 -0
- package/lib/extensions/list/List.js +2 -2
- package/lib/extensions/list/ListItem.js +2 -2
- package/lib/services/NodeFactory.js +73 -13
- package/lib/services/__tests__/NodeFactory.test.js +124 -0
- package/lib/services/__tests__/__snapshots__/NodeFactory.test.js.snap +326 -0
- package/lib/services/index.js +1 -1
- package/lib/services/normalizer/BaseNormalizer.js +11 -0
- package/lib/services/{BrowserDomParser.js → normalizer/BrowserDomParser.js} +0 -0
- package/lib/services/normalizer/ContentNormalizer.js +24 -0
- package/lib/services/normalizer/HtmlNormalizer.js +245 -0
- package/lib/services/normalizer/JsonNormalizer.js +81 -0
- package/lib/services/{__tests__/ContentNormalizer.test.js → normalizer/__tests__/HtmlNormalizer.test.js} +31 -7
- package/lib/services/normalizer/__tests__/JsonNormalizer.test.js +70 -0
- package/lib/services/normalizer/__tests__/__snapshots__/JsonNormalizer.test.js.snap +159 -0
- package/lib/services/normalizer/index.js +1 -0
- package/lib/styles/content.css +8 -0
- package/lib/utils/__tests__/findMarkByType.test.js +17 -0
- package/lib/utils/__tests__/isMarkAppliedToParent.test.js +53 -0
- package/lib/utils/__tests__/isNodeFullySelected.test.js +53 -0
- package/lib/utils/__tests__/resolveTextPosition.test.js +39 -0
- package/lib/utils/index.js +0 -1
- package/lib/utils/isMarkAppliedToParent.js +2 -7
- package/lib/utils/isNodeFullySelected.js +9 -5
- package/lib/utils/resolveTextPosition.js +4 -6
- package/package.json +28 -26
- package/lib/services/ContentNormalizer.js +0 -194
- package/lib/utils/resolveNodePosition.js +0 -6
|
@@ -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.#includesMark(node, childMark)) {
|
|
31
|
+
this.#removeMark(child, childMark);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (this.#canBubbleMark(node, childMark)) {
|
|
36
|
+
this.#removeMark(child, childMark);
|
|
37
|
+
this.#addMark(node, 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
|
+
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { ContentNormalizer } from '../ContentNormalizer';
|
|
2
|
-
import { NodeFactory } from '../NodeFactory';
|
|
3
2
|
|
|
4
3
|
describe('normalize text content', () => {
|
|
5
|
-
test('should ignore json content', () => {
|
|
6
|
-
const content = NodeFactory.doc([NodeFactory.paragraph('Test')]);
|
|
7
|
-
|
|
8
|
-
expect(ContentNormalizer.normalize(content)).toBe(content);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
4
|
test('should wrap list content in paragraph', () => {
|
|
12
5
|
const input = '<ul><li style="line-height: 2;">lorem impsum</li></ul>';
|
|
13
6
|
const output = '<ul><li><p style="line-height: 2;">lorem impsum</p></li></ul>';
|
|
@@ -107,4 +100,35 @@ describe('normalize text content', () => {
|
|
|
107
100
|
|
|
108
101
|
expect(ContentNormalizer.normalize(input)).toBe(output);
|
|
109
102
|
});
|
|
103
|
+
|
|
104
|
+
test('should ignore text decoration on text nodes', () => {
|
|
105
|
+
const input = '<p><span style="text-decoration: underline;">lorem ipsum</span></p>';
|
|
106
|
+
const output = '<p><span style="text-decoration: underline;">lorem ipsum</span></p>';
|
|
107
|
+
|
|
108
|
+
expect(ContentNormalizer.normalize(input)).toBe(output);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('should ignore none text decoration', () => {
|
|
112
|
+
const input = '<p style="text-decoration: none;">lorem ipsum</p>';
|
|
113
|
+
const output = '<p>lorem ipsum</p>';
|
|
114
|
+
|
|
115
|
+
expect(ContentNormalizer.normalize(input)).toBe(output);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('should move text decoration from block to text only nodes', () => {
|
|
119
|
+
const input = '<p style="text-decoration: underline;">lorem ipsum</p>';
|
|
120
|
+
const output = '<p><span style="text-decoration: underline;">lorem ipsum</span></p>';
|
|
121
|
+
|
|
122
|
+
expect(ContentNormalizer.normalize(input)).toBe(output);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('should move text decoration from block to mixed content', () => {
|
|
126
|
+
const input = '<p style="text-decoration: underline;"><span style="text-decoration: line-through;">lorem</span> ipsum</p>';
|
|
127
|
+
const output = '<p>' +
|
|
128
|
+
'<span style="text-decoration: underline line-through;">lorem</span>' +
|
|
129
|
+
'<span style="text-decoration: underline;"> ipsum</span>' +
|
|
130
|
+
'</p>';
|
|
131
|
+
|
|
132
|
+
expect(ContentNormalizer.normalize(input)).toBe(output);
|
|
133
|
+
});
|
|
110
134
|
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { NodeFactory } from '../../NodeFactory';
|
|
2
|
+
import { ListTypes, TextSettings } from '../../../enums';
|
|
3
|
+
import { ContentNormalizer } from '../ContentNormalizer';
|
|
4
|
+
|
|
5
|
+
describe('normalize json content', () => {
|
|
6
|
+
test('should bubble mark from single text node to paragraph', () => {
|
|
7
|
+
const input = NodeFactory.doc([
|
|
8
|
+
NodeFactory.paragraph([
|
|
9
|
+
NodeFactory.text('lorem ipsum', [
|
|
10
|
+
NodeFactory.mark(TextSettings.FONT_WEIGHT, { value: '700' })
|
|
11
|
+
])
|
|
12
|
+
])
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
expect(ContentNormalizer.normalize(input)).toMatchSnapshot();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should bubble mark from two text nodes to paragraph', () => {
|
|
19
|
+
const input = NodeFactory.doc([
|
|
20
|
+
NodeFactory.paragraph([
|
|
21
|
+
NodeFactory.text('lorem', [
|
|
22
|
+
NodeFactory.mark(TextSettings.FONT_WEIGHT, { value: '700' })
|
|
23
|
+
]),
|
|
24
|
+
NodeFactory.text(' ipsum', [
|
|
25
|
+
NodeFactory.mark(TextSettings.FONT_WEIGHT, { value: '700' }),
|
|
26
|
+
NodeFactory.mark(TextSettings.FONT_COLOR, { value: '#FF0000' })
|
|
27
|
+
])
|
|
28
|
+
])
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
expect(ContentNormalizer.normalize(input)).toMatchSnapshot();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('should bubble mark from text to list item', () => {
|
|
35
|
+
const input = NodeFactory.doc([
|
|
36
|
+
NodeFactory.list(ListTypes.DISC, [
|
|
37
|
+
NodeFactory.paragraph(null, [
|
|
38
|
+
NodeFactory.mark(TextSettings.FONT_WEIGHT, { value: '700' })
|
|
39
|
+
], 'lorem ipsum')
|
|
40
|
+
])
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
expect(ContentNormalizer.normalize(input)).toMatchSnapshot();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should bubble two marks', () => {
|
|
47
|
+
const input = NodeFactory.doc([
|
|
48
|
+
NodeFactory.paragraph([
|
|
49
|
+
NodeFactory.text('hello world', [
|
|
50
|
+
NodeFactory.mark(TextSettings.FONT_FAMILY, { value: 'Bungee' }),
|
|
51
|
+
NodeFactory.mark(TextSettings.FONT_WEIGHT, { value: '800' })
|
|
52
|
+
])
|
|
53
|
+
])
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
expect(ContentNormalizer.normalize(input)).toMatchSnapshot();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('should not bubble inline marks', () => {
|
|
60
|
+
const input = NodeFactory.doc([
|
|
61
|
+
NodeFactory.paragraph([
|
|
62
|
+
NodeFactory.text('hello world', [
|
|
63
|
+
NodeFactory.mark(TextSettings.TEXT_DECORATION, { underline: true })
|
|
64
|
+
])
|
|
65
|
+
])
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
expect(ContentNormalizer.normalize(input)).toMatchSnapshot();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
|
+
|
|
3
|
+
exports[`normalize json content should bubble mark from single text node to paragraph 1`] = `
|
|
4
|
+
Object {
|
|
5
|
+
"content": Array [
|
|
6
|
+
Object {
|
|
7
|
+
"content": Array [
|
|
8
|
+
Object {
|
|
9
|
+
"text": "lorem ipsum",
|
|
10
|
+
"type": "text",
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
"marks": Array [
|
|
14
|
+
Object {
|
|
15
|
+
"attrs": Object {
|
|
16
|
+
"value": "700",
|
|
17
|
+
},
|
|
18
|
+
"type": "font_weight",
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
"type": "paragraph",
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
"type": "doc",
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
exports[`normalize json content should bubble mark from text to list item 1`] = `
|
|
29
|
+
Object {
|
|
30
|
+
"content": Array [
|
|
31
|
+
Object {
|
|
32
|
+
"attrs": Object {
|
|
33
|
+
"bullet": Object {
|
|
34
|
+
"type": "disc",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
"content": Array [
|
|
38
|
+
Object {
|
|
39
|
+
"content": Array [
|
|
40
|
+
Object {
|
|
41
|
+
"content": Array [
|
|
42
|
+
Object {
|
|
43
|
+
"text": "lorem ipsum",
|
|
44
|
+
"type": "text",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
"type": "paragraph",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
"marks": Array [
|
|
51
|
+
Object {
|
|
52
|
+
"attrs": Object {
|
|
53
|
+
"value": "700",
|
|
54
|
+
},
|
|
55
|
+
"type": "font_weight",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
"type": "listItem",
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
"type": "list",
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
"type": "doc",
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
exports[`normalize json content should bubble mark from two text nodes to paragraph 1`] = `
|
|
69
|
+
Object {
|
|
70
|
+
"content": Array [
|
|
71
|
+
Object {
|
|
72
|
+
"content": Array [
|
|
73
|
+
Object {
|
|
74
|
+
"text": "lorem",
|
|
75
|
+
"type": "text",
|
|
76
|
+
},
|
|
77
|
+
Object {
|
|
78
|
+
"marks": Array [
|
|
79
|
+
Object {
|
|
80
|
+
"attrs": Object {
|
|
81
|
+
"value": "#FF0000",
|
|
82
|
+
},
|
|
83
|
+
"type": "font_color",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
"text": " ipsum",
|
|
87
|
+
"type": "text",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
"marks": Array [
|
|
91
|
+
Object {
|
|
92
|
+
"attrs": Object {
|
|
93
|
+
"value": "700",
|
|
94
|
+
},
|
|
95
|
+
"type": "font_weight",
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
"type": "paragraph",
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
"type": "doc",
|
|
102
|
+
}
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
exports[`normalize json content should bubble two marks 1`] = `
|
|
106
|
+
Object {
|
|
107
|
+
"content": Array [
|
|
108
|
+
Object {
|
|
109
|
+
"content": Array [
|
|
110
|
+
Object {
|
|
111
|
+
"text": "hello world",
|
|
112
|
+
"type": "text",
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
"marks": Array [
|
|
116
|
+
Object {
|
|
117
|
+
"attrs": Object {
|
|
118
|
+
"value": "Bungee",
|
|
119
|
+
},
|
|
120
|
+
"type": "font_family",
|
|
121
|
+
},
|
|
122
|
+
Object {
|
|
123
|
+
"attrs": Object {
|
|
124
|
+
"value": "800",
|
|
125
|
+
},
|
|
126
|
+
"type": "font_weight",
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
"type": "paragraph",
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
"type": "doc",
|
|
133
|
+
}
|
|
134
|
+
`;
|
|
135
|
+
|
|
136
|
+
exports[`normalize json content should not bubble inline marks 1`] = `
|
|
137
|
+
Object {
|
|
138
|
+
"content": Array [
|
|
139
|
+
Object {
|
|
140
|
+
"content": Array [
|
|
141
|
+
Object {
|
|
142
|
+
"marks": Array [
|
|
143
|
+
Object {
|
|
144
|
+
"attrs": Object {
|
|
145
|
+
"underline": true,
|
|
146
|
+
},
|
|
147
|
+
"type": "text_decoration",
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
"text": "hello world",
|
|
151
|
+
"type": "text",
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
"type": "paragraph",
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
"type": "doc",
|
|
158
|
+
}
|
|
159
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ContentNormalizer } from './ContentNormalizer';
|
package/lib/styles/content.css
CHANGED
|
@@ -112,6 +112,14 @@ img.ProseMirror-separator {
|
|
|
112
112
|
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
.ProseMirror span {
|
|
116
|
+
display: block;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.ProseMirror :where(p, h1, h2, h3, h4) span {
|
|
120
|
+
display: initial;
|
|
121
|
+
}
|
|
122
|
+
|
|
115
123
|
@keyframes ProseMirror-cursor-blink {
|
|
116
124
|
|
|
117
125
|
to {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { findMarkByType } from '../findMarkByType';
|
|
2
|
+
|
|
3
|
+
describe('find mark by type', () => {
|
|
4
|
+
test('should find mark by type name', () => {
|
|
5
|
+
const searchingMark = { type: { name: 'font_weight' } };
|
|
6
|
+
const marks = [{ type: { name: 'font_size' } }, searchingMark];
|
|
7
|
+
|
|
8
|
+
expect(findMarkByType(marks, 'font_weight')).toBe(searchingMark);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('should find mark by type object', () => {
|
|
12
|
+
const searchingMark = { type: { name: 'font_weight' } };
|
|
13
|
+
const marks = [{ type: { name: 'font_size' } }, searchingMark];
|
|
14
|
+
|
|
15
|
+
expect(findMarkByType(marks, { name: 'font_weight' })).toBe(searchingMark);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { isMarkAppliedToParent } from '../isMarkAppliedToParent';
|
|
2
|
+
|
|
3
|
+
const createMark = (attrs = {}) => ({
|
|
4
|
+
isInSet(set) {
|
|
5
|
+
return set.includes(this);
|
|
6
|
+
},
|
|
7
|
+
...attrs
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const createNode = (attrs = {}) => ({
|
|
11
|
+
resolve: jest.fn(),
|
|
12
|
+
path: [],
|
|
13
|
+
marks: [],
|
|
14
|
+
...attrs
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('is mark applied to parent', () => {
|
|
18
|
+
test('should return true if applied to parent', () => {
|
|
19
|
+
const checkingMark = createMark();
|
|
20
|
+
const doc = createNode();
|
|
21
|
+
|
|
22
|
+
doc.resolve.mockReturnValue({
|
|
23
|
+
path: [
|
|
24
|
+
createNode({ marks: [checkingMark] }),
|
|
25
|
+
0,
|
|
26
|
+
0,
|
|
27
|
+
createNode({ marks: [createMark()] }),
|
|
28
|
+
0,
|
|
29
|
+
1
|
|
30
|
+
]
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(isMarkAppliedToParent(doc, 0, checkingMark)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should return false if not applied to parent', () => {
|
|
37
|
+
const checkingMark = createMark();
|
|
38
|
+
const doc = createNode();
|
|
39
|
+
|
|
40
|
+
doc.resolve.mockReturnValue({
|
|
41
|
+
path: [
|
|
42
|
+
createNode({ marks: [createMark()] }),
|
|
43
|
+
0,
|
|
44
|
+
0,
|
|
45
|
+
createNode({ marks: [createMark()] }),
|
|
46
|
+
0,
|
|
47
|
+
1
|
|
48
|
+
]
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(isMarkAppliedToParent(doc, 0, checkingMark)).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|