Skip to content

Commit ef69f89

Browse files
authored
fix(wysiwyg): preserve markup for bold and italic marks (#774)
1 parent 4ac90b1 commit ef69f89

File tree

9 files changed

+111
-28
lines changed

9 files changed

+111
-28
lines changed

src/extensions/markdown/Bold/Bold.test.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {createMarkupChecker} from '../../../../tests/sameMarkup';
55
import {ExtensionsManager} from '../../../core';
66
import {BaseNode, BaseSchemaSpecs} from '../../base/specs';
77

8-
import {BoldSpecs, boldMarkName} from './BoldSpecs';
8+
import {BoldAttrs, BoldSpecs, boldMarkName} from './BoldSpecs';
99

1010
const {
1111
schema,
@@ -23,23 +23,43 @@ const {doc, p, b} = builders<'doc' | 'p', 'b'>(schema, {
2323

2424
const {same} = createMarkupChecker({parser, serializer});
2525

26+
const astAttrs = {[BoldAttrs.Markup]: '**'}; // asterisk
27+
const undAttrs = {[BoldAttrs.Markup]: '__'}; // underscore
28+
const nullAttrs = {[BoldAttrs.Markup]: null}; // null
29+
2630
describe('Bold extension', () => {
27-
it('should parse bold **', () => same('**hello!**', doc(p(b('hello!')))));
31+
it('should parse bold **', () => same('**hello!**', doc(p(b(astAttrs, 'hello!')))));
2832

29-
it.skip('should parse bold __', () => same('__hello!__', doc(p(b('hello!')))));
33+
it('should parse bold __', () => same('__hello!__', doc(p(b(undAttrs, 'hello!')))));
3034

3135
it('should parse bold inside text', () =>
32-
same('he**llo wor**ld!', doc(p('he', b('llo wor'), 'ld!'))));
36+
same('he**llo wor**ld!', doc(p('he', b(astAttrs, 'llo wor'), 'ld!'))));
3337

34-
it('should parse html - em tag', () => {
38+
it('should parse html - b tag', () => {
3539
parseDOM(schema, '<p><b>text in b tag</b></p>', doc(p(b('text in b tag'))));
3640
});
3741

3842
it('should parse html - strong tag', () => {
3943
parseDOM(
4044
schema,
4145
'<div><strong>text in strong tag</strong></div>',
42-
doc(p(b('text in strong tag'))),
46+
doc(p(b(nullAttrs, 'text in strong tag'))),
47+
);
48+
});
49+
50+
it('should parse html - strong tag - with asterisk markup', () => {
51+
parseDOM(
52+
schema,
53+
'<div><strong data-markup="**">text in strong tag</strong></div>',
54+
doc(p(b(astAttrs, 'text in strong tag'))),
55+
);
56+
});
57+
58+
it('should parse html - strong tag - with underscore markup', () => {
59+
parseDOM(
60+
schema,
61+
'<div><strong data-markup="__">text in strong tag</strong></div>',
62+
doc(p(b(undAttrs, 'text in strong tag'))),
4363
);
4464
});
4565

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,49 @@
1-
import type {ExtensionAuto} from '../../../../core';
2-
import {markTypeFactory} from '../../../../utils/schema';
1+
import type {ExtensionAuto} from '#core';
2+
import type {Mark} from '#pm/model';
3+
import {markTypeFactory} from 'src/utils/schema';
34

45
export const boldMarkName = 'strong';
56
export const boldType = markTypeFactory(boldMarkName);
7+
export const BoldAttrs = {
8+
Markup: 'data-markup',
9+
} as const;
10+
const defaultMarkup = '**';
611

712
export const BoldSpecs: ExtensionAuto = (builder) => {
813
builder.addMark(boldMarkName, () => ({
914
spec: {
15+
attrs: {[BoldAttrs.Markup]: {default: defaultMarkup}},
1016
parseDOM: [
1117
{tag: 'b'},
12-
{tag: 'strong'},
18+
{
19+
tag: 'strong',
20+
getAttrs: (node) => ({
21+
[BoldAttrs.Markup]: node.getAttribute(BoldAttrs.Markup),
22+
}),
23+
},
1324
{
1425
style: 'font-weight',
1526
getAttrs: (value) => /^(bold(er)?|[5-9]\d{2,})$/.test(value as string) && null,
1627
},
1728
],
18-
toDOM() {
19-
return ['strong'];
29+
toDOM(mark) {
30+
return ['strong', mark.attrs];
2031
},
2132
},
2233
fromMd: {
2334
tokenSpec: {
2435
name: boldMarkName,
2536
type: 'mark',
37+
getAttrs: (token) => ({
38+
[BoldAttrs.Markup]: token.markup,
39+
}),
2640
},
2741
},
28-
toMd: {open: '**', close: '**', mixable: true, expelEnclosingWhitespace: true},
42+
toMd: {open: getMarkup, close: getMarkup, mixable: true, expelEnclosingWhitespace: true},
2943
}));
3044
};
45+
46+
function getMarkup(_: unknown, mark: Mark): string {
47+
const attr = mark.attrs[BoldAttrs.Markup];
48+
return attr || defaultMarkup;
49+
}

src/extensions/markdown/Italic/Italic.test.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {createMarkupChecker} from '../../../../tests/sameMarkup';
55
import {ExtensionsManager} from '../../../core';
66
import {BaseNode, BaseSchemaSpecs} from '../../base/specs';
77

8-
import {ItalicSpecs, italicMarkName} from './ItalicSpecs';
8+
import {ItalicAttrs, ItalicSpecs, italicMarkName} from './ItalicSpecs';
99

1010
const {
1111
schema,
@@ -23,16 +23,36 @@ const {doc, p, i} = builders<'doc' | 'p', 'i'>(schema, {
2323

2424
const {same} = createMarkupChecker({parser, serializer});
2525

26+
const astAttrs = {[ItalicAttrs.Markup]: '*'}; // asterisk
27+
const undAttrs = {[ItalicAttrs.Markup]: '_'}; // underscore
28+
const nullAttrs = {[ItalicAttrs.Markup]: null}; // null
29+
2630
describe('Italic extension', () => {
27-
it('should parse italic *', () => same('*hello!*', doc(p(i('hello!')))));
31+
it('should parse italic *', () => same('*hello!*', doc(p(i(astAttrs, 'hello!')))));
2832

29-
it.skip('should parse italic _', () => same('_hello!_', doc(p(i('hello!')))));
33+
it('should parse italic _', () => same('_hello!_', doc(p(i(undAttrs, 'hello!')))));
3034

3135
it('should parse italic inside text', () =>
32-
same('he*llo wor*ld!', doc(p('he', i('llo wor'), 'ld!'))));
36+
same('he*llo wor*ld!', doc(p('he', i(astAttrs, 'llo wor'), 'ld!'))));
3337

3438
it('should parse html - em tag', () => {
35-
parseDOM(schema, '<p><em>text in em</em></p>', doc(p(i('text in em'))));
39+
parseDOM(schema, '<p><em>text in em</em></p>', doc(p(i(nullAttrs, 'text in em'))));
40+
});
41+
42+
it('should parse html - em tag - with asterisk markup', () => {
43+
parseDOM(
44+
schema,
45+
'<p><em data-markup="*">text in em</em></p>',
46+
doc(p(i(astAttrs, 'text in em'))),
47+
);
48+
});
49+
50+
it('should parse html - em tag - with underscore markup', () => {
51+
parseDOM(
52+
schema,
53+
'<p><em data-markup="_">text in em</em></p>',
54+
doc(p(i(undAttrs, 'text in em'))),
55+
);
3656
});
3757

3858
it('should parse html - i tag', () => {
Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,46 @@
1-
import type {ExtensionAuto} from '../../../../core';
2-
import {markTypeFactory} from '../../../../utils/schema';
1+
import type {ExtensionAuto} from '#core';
2+
import type {Mark} from '#pm/model';
3+
import {markTypeFactory} from 'src/utils/schema';
34

45
export const italicMarkName = 'em';
56
export const italicType = markTypeFactory(italicMarkName);
7+
export const ItalicAttrs = {
8+
Markup: 'data-markup',
9+
} as const;
10+
const defaultMarkup = '*';
611

712
export const ItalicSpecs: ExtensionAuto = (builder) => {
813
builder.addMark(italicMarkName, () => ({
914
spec: {
15+
attrs: {[ItalicAttrs.Markup]: {default: defaultMarkup}},
1016
parseDOM: [
1117
{tag: 'i'},
12-
{tag: 'em'},
18+
{
19+
tag: 'em',
20+
getAttrs: (node) => ({
21+
[ItalicAttrs.Markup]: node.getAttribute(ItalicAttrs.Markup),
22+
}),
23+
},
1324
{style: 'font-style', getAttrs: (value) => value === 'italic' && null},
1425
],
15-
toDOM() {
16-
return ['em'];
26+
toDOM(mark) {
27+
return ['em', mark.attrs];
28+
},
29+
},
30+
toMd: {open: getMarkup, close: getMarkup, mixable: true, expelEnclosingWhitespace: true},
31+
fromMd: {
32+
tokenSpec: {
33+
name: italicMarkName,
34+
type: 'mark',
35+
getAttrs: (token) => ({
36+
[ItalicAttrs.Markup]: token.markup,
37+
}),
1738
},
1839
},
19-
toMd: {open: '*', close: '*', mixable: true, expelEnclosingWhitespace: true},
20-
fromMd: {tokenSpec: {name: italicMarkName, type: 'mark'}},
2140
}));
2241
};
42+
43+
function getMarkup(_: unknown, mark: Mark): string {
44+
const attr = mark.attrs[ItalicAttrs.Markup];
45+
return attr || defaultMarkup;
46+
}
Loading
Loading
Loading
Loading

tests/visual-tests/playground/Clipboard.visual.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ test.describe('Clipboard', () => {
9696
'Skip in webkit on linux, see https://github.com/microsoft/playwright/issues/34307',
9797
);
9898

99-
await editor.paste('## Lorem *ipsum* **dolor** ~~sit~~ amet');
99+
await editor.paste('## Lorem *ipsum* __dolor__ ~~sit~~ amet');
100100
await editor.press(helpers.keys.selectAll);
101101
await editor.press(helpers.keys.copy);
102102

@@ -105,7 +105,7 @@ test.describe('Clipboard', () => {
105105
expect(data).toStrictEqual({
106106
'text/plain': 'Lorem ipsum dolor sit amet',
107107
'text/html':
108-
'<h2 data-pm-slice="1 1 []">Lorem <em>ipsum</em> <strong>dolor</strong> <strike>sit</strike> amet</h2>',
108+
'<h2 data-pm-slice="1 1 []">Lorem <em data-markup="*">ipsum</em> <strong data-markup="__">dolor</strong> <strike>sit</strike> amet</h2>',
109109
});
110110
});
111111
});
@@ -122,7 +122,7 @@ test.describe('Clipboard', () => {
122122
'Skip in webkit on linux, see https://github.com/microsoft/playwright/issues/34307',
123123
);
124124

125-
await editor.paste('## Lorem *ipsum* **dolor** ~~sit~~ amet');
125+
await editor.paste('## Lorem _ipsum_ **dolor** ~~sit~~ amet');
126126
await editor.press(helpers.keys.selectAll);
127127
await editor.press(helpers.keys.cut);
128128

@@ -131,7 +131,7 @@ test.describe('Clipboard', () => {
131131
expect(data).toStrictEqual({
132132
'text/plain': 'Lorem ipsum dolor sit amet',
133133
'text/html':
134-
'<h2 data-pm-slice="1 1 []">Lorem <em>ipsum</em> <strong>dolor</strong> <strike>sit</strike> amet</h2>',
134+
'<h2 data-pm-slice="1 1 []">Lorem <em data-markup="_">ipsum</em> <strong data-markup="**">dolor</strong> <strike>sit</strike> amet</h2>',
135135
});
136136
});
137137
});

0 commit comments

Comments
 (0)