@dxos/react-ui-editor 0.3.11-main.5cbcf4e → 0.3.11-main.5e05862
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/dist/lib/browser/index.mjs +1141 -146
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/TextEditor/hooks.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.d.ts +8 -4
- package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts +5 -3
- package/dist/types/src/components/Toolbar/Toolbar.stories.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.d.ts +47 -9
- package/dist/types/src/extensions/markdown/formatting.d.ts.map +1 -1
- package/dist/types/src/extensions/markdown/formatting.test.d.ts +3 -0
- package/dist/types/src/extensions/markdown/formatting.test.d.ts.map +1 -0
- package/dist/types/src/hooks/useActionHandler.d.ts.map +1 -1
- package/package.json +23 -23
- package/src/components/TextEditor/hooks.stories.tsx +6 -1
- package/src/components/Toolbar/Toolbar.stories.tsx +14 -2
- package/src/components/Toolbar/Toolbar.tsx +100 -23
- package/src/extensions/markdown/formatting.test.ts +481 -0
- package/src/extensions/markdown/formatting.ts +1036 -67
- package/src/hooks/useActionHandler.ts +48 -15
|
@@ -22,19 +22,22 @@ import { createContext } from '@radix-ui/react-context';
|
|
|
22
22
|
import React, { type PropsWithChildren } from 'react';
|
|
23
23
|
|
|
24
24
|
import { Button, type ButtonProps, DensityProvider, Select } from '@dxos/react-ui';
|
|
25
|
-
import { getSize } from '@dxos/react-ui-theme';
|
|
25
|
+
import { getSize, mx } from '@dxos/react-ui-theme';
|
|
26
|
+
|
|
27
|
+
import { type Formatting } from '../../extensions/markdown';
|
|
26
28
|
|
|
27
29
|
// TODO(burdon): Revert to string to make extensible?
|
|
28
30
|
export type ActionType =
|
|
29
31
|
| 'blockquote'
|
|
30
|
-
| '
|
|
32
|
+
| 'strong'
|
|
31
33
|
| 'codeblock'
|
|
32
34
|
| 'comment'
|
|
33
35
|
| 'heading'
|
|
34
36
|
| 'image'
|
|
35
|
-
| '
|
|
37
|
+
| 'emphasis'
|
|
38
|
+
| 'code'
|
|
36
39
|
| 'link'
|
|
37
|
-
| 'list'
|
|
40
|
+
| 'list-bullet'
|
|
38
41
|
| 'list-ordered'
|
|
39
42
|
| 'list-tasks'
|
|
40
43
|
| 'mention'
|
|
@@ -48,14 +51,15 @@ export type Action = {
|
|
|
48
51
|
};
|
|
49
52
|
|
|
50
53
|
export type ToolbarProps = PropsWithChildren<{
|
|
54
|
+
state: Formatting | null;
|
|
51
55
|
onAction?: (action: Action) => void;
|
|
52
56
|
}>;
|
|
53
57
|
|
|
54
58
|
const [ToolbarContextProvider, useToolbarContext] = createContext<ToolbarProps>('Toolbar');
|
|
55
59
|
|
|
56
|
-
const ToolbarRoot = ({ children, onAction }: ToolbarProps) => {
|
|
60
|
+
const ToolbarRoot = ({ children, onAction, state }: ToolbarProps) => {
|
|
57
61
|
return (
|
|
58
|
-
<ToolbarContextProvider onAction={onAction}>
|
|
62
|
+
<ToolbarContextProvider onAction={onAction} state={state}>
|
|
59
63
|
<DensityProvider density='fine'>
|
|
60
64
|
<div role='toolbar' className='flex w-full shrink-0 p-2 gap-4 items-center whitespace-nowrap overflow-hidden'>
|
|
61
65
|
{children}
|
|
@@ -67,20 +71,31 @@ const ToolbarRoot = ({ children, onAction }: ToolbarProps) => {
|
|
|
67
71
|
|
|
68
72
|
type ToolbarButtonProps = {
|
|
69
73
|
Icon: Icon;
|
|
70
|
-
onClick: () => Action | undefined;
|
|
74
|
+
onClick: (state: Formatting | null) => Action | undefined;
|
|
75
|
+
getState?: (state: Formatting) => boolean;
|
|
76
|
+
disable?: (state: Formatting) => boolean;
|
|
71
77
|
} & NonNullable<Pick<ButtonProps, 'title'>>;
|
|
72
78
|
|
|
73
|
-
const ToolbarButton = ({ Icon, onClick, title }: ToolbarButtonProps) => {
|
|
74
|
-
const { onAction } = useToolbarContext('ToolbarButton');
|
|
75
|
-
const handleClick = () => {
|
|
76
|
-
const action = onClick();
|
|
79
|
+
const ToolbarButton = ({ Icon, onClick, title, getState, disable }: ToolbarButtonProps) => {
|
|
80
|
+
const { onAction, state } = useToolbarContext('ToolbarButton');
|
|
81
|
+
const handleClick = (event: React.MouseEvent) => {
|
|
82
|
+
const action = onClick(state);
|
|
77
83
|
if (action) {
|
|
78
84
|
onAction?.(action);
|
|
85
|
+
event.preventDefault();
|
|
79
86
|
}
|
|
80
87
|
};
|
|
88
|
+
const active = getState && state ? getState(state) : false;
|
|
89
|
+
const disabled = disable && state ? disable(state) : false;
|
|
81
90
|
|
|
82
91
|
return (
|
|
83
|
-
<Button
|
|
92
|
+
<Button
|
|
93
|
+
variant='ghost'
|
|
94
|
+
classNames={mx('p-2', active && 'ring')}
|
|
95
|
+
onMouseDown={handleClick}
|
|
96
|
+
title={title}
|
|
97
|
+
disabled={disabled}
|
|
98
|
+
>
|
|
84
99
|
<Icon className={getSize(5)} />
|
|
85
100
|
</Button>
|
|
86
101
|
);
|
|
@@ -89,9 +104,16 @@ const ToolbarButton = ({ Icon, onClick, title }: ToolbarButtonProps) => {
|
|
|
89
104
|
const ToolbarSeparator = () => <div className='grow' />;
|
|
90
105
|
|
|
91
106
|
const MarkdownHeading = () => {
|
|
92
|
-
const { onAction } = useToolbarContext('MarkdownFormatting');
|
|
107
|
+
const { onAction, state } = useToolbarContext('MarkdownFormatting');
|
|
108
|
+
const blockType = state ? state.blockType : 'paragraph';
|
|
109
|
+
const header = blockType && /heading(\d)/.exec(blockType);
|
|
110
|
+
const value = header ? header[1] : blockType === 'paragraph' || !blockType ? '0' : null;
|
|
93
111
|
return (
|
|
94
|
-
<Select.Root
|
|
112
|
+
<Select.Root
|
|
113
|
+
disabled={value === null}
|
|
114
|
+
value={value ?? '0'}
|
|
115
|
+
onValueChange={(value) => onAction?.({ type: 'heading', data: parseInt(value) })}
|
|
116
|
+
>
|
|
95
117
|
<Select.TriggerButton classNames='w-[8rem]' />
|
|
96
118
|
<Select.Portal>
|
|
97
119
|
<Select.Content>
|
|
@@ -111,31 +133,86 @@ const MarkdownHeading = () => {
|
|
|
111
133
|
|
|
112
134
|
const MarkdownStyles = () => (
|
|
113
135
|
<div role='none'>
|
|
114
|
-
<ToolbarButton
|
|
115
|
-
|
|
116
|
-
|
|
136
|
+
<ToolbarButton
|
|
137
|
+
Icon={TextB}
|
|
138
|
+
title='String'
|
|
139
|
+
onClick={(s) => ({ type: 'strong', data: s ? !s.strong : null })}
|
|
140
|
+
getState={(s) => s.strong}
|
|
141
|
+
disable={(s) => s.blockType === 'codeblock'}
|
|
142
|
+
/>
|
|
143
|
+
<ToolbarButton
|
|
144
|
+
Icon={TextItalic}
|
|
145
|
+
title='Emphasis'
|
|
146
|
+
onClick={(s) => ({ type: 'emphasis', data: s ? !s.emphasis : null })}
|
|
147
|
+
getState={(s) => s.emphasis}
|
|
148
|
+
disable={(s) => s.blockType === 'codeblock'}
|
|
149
|
+
/>
|
|
150
|
+
<ToolbarButton
|
|
151
|
+
Icon={TextStrikethrough}
|
|
152
|
+
title='Strike-through'
|
|
153
|
+
onClick={(s) => ({ type: 'strikethrough', data: s ? !s.strikethrough : null })}
|
|
154
|
+
getState={(s) => s.strikethrough}
|
|
155
|
+
disable={(s) => s.blockType === 'codeblock'}
|
|
156
|
+
/>
|
|
157
|
+
<ToolbarButton
|
|
158
|
+
Icon={Code}
|
|
159
|
+
title='Inline code'
|
|
160
|
+
onClick={(s) => ({ type: 'code', data: s ? !s.code : null })}
|
|
161
|
+
getState={(s) => s.code}
|
|
162
|
+
disable={(s) => s.blockType === 'codeblock'}
|
|
163
|
+
/>
|
|
117
164
|
</div>
|
|
118
165
|
);
|
|
119
166
|
|
|
120
167
|
const MarkdownLists = () => (
|
|
121
168
|
<div role='none'>
|
|
122
|
-
<ToolbarButton
|
|
123
|
-
|
|
124
|
-
|
|
169
|
+
<ToolbarButton
|
|
170
|
+
Icon={ListBullets}
|
|
171
|
+
title='Bullet list'
|
|
172
|
+
onClick={(s) => ({ type: 'list-bullet', data: s ? s.listStyle !== 'bullet' : null })}
|
|
173
|
+
getState={(s) => s.listStyle === 'bullet'}
|
|
174
|
+
/>
|
|
175
|
+
<ToolbarButton
|
|
176
|
+
Icon={ListNumbers}
|
|
177
|
+
title='Numbered list'
|
|
178
|
+
onClick={(s) => ({ type: 'list-ordered', data: s ? s.listStyle !== 'ordered' : null })}
|
|
179
|
+
getState={(s) => s.listStyle === 'ordered'}
|
|
180
|
+
/>
|
|
181
|
+
<ToolbarButton
|
|
182
|
+
Icon={ListChecks}
|
|
183
|
+
title='Task list'
|
|
184
|
+
onClick={(s) => ({ type: 'list-tasks', data: s ? s.listStyle !== 'task' : null })}
|
|
185
|
+
getState={(s) => s.listStyle === 'task'}
|
|
186
|
+
/>
|
|
125
187
|
</div>
|
|
126
188
|
);
|
|
127
189
|
|
|
128
190
|
const MarkdownBlocks = () => (
|
|
129
191
|
<div role='none'>
|
|
130
|
-
<ToolbarButton
|
|
131
|
-
|
|
192
|
+
<ToolbarButton
|
|
193
|
+
Icon={CaretRight}
|
|
194
|
+
title='Block quote'
|
|
195
|
+
onClick={(s) => ({ type: 'blockquote', data: s ? !s.blockquote : null })}
|
|
196
|
+
getState={(s) => s.blockquote}
|
|
197
|
+
/>
|
|
198
|
+
<ToolbarButton
|
|
199
|
+
Icon={Code}
|
|
200
|
+
title='Code block'
|
|
201
|
+
onClick={(s) => ({ type: 'codeblock', data: s ? s.blockType !== 'codeblock' : null })}
|
|
202
|
+
getState={(s) => s.blockType === 'codeblock'}
|
|
203
|
+
/>
|
|
132
204
|
<ToolbarButton Icon={Table} title='Table' onClick={() => ({ type: 'table' })} />
|
|
133
205
|
</div>
|
|
134
206
|
);
|
|
135
207
|
|
|
136
208
|
const MarkdownLinks = () => (
|
|
137
209
|
<div role='none'>
|
|
138
|
-
<ToolbarButton
|
|
210
|
+
<ToolbarButton
|
|
211
|
+
Icon={Link}
|
|
212
|
+
title='Link'
|
|
213
|
+
onClick={(s) => ({ type: 'link', data: s ? !s.link : null })}
|
|
214
|
+
getState={(s) => s.link}
|
|
215
|
+
/>
|
|
139
216
|
<ToolbarButton Icon={At} title='Mention' onClick={() => ({ type: 'mention' })} />
|
|
140
217
|
<ToolbarButton Icon={Image} title='Image' onClick={() => ({ type: 'image' })} />
|
|
141
218
|
</div>
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { markdownLanguage } from '@codemirror/lang-markdown';
|
|
6
|
+
import { EditorState, type StateCommand } from '@codemirror/state';
|
|
7
|
+
import { expect } from 'chai';
|
|
8
|
+
|
|
9
|
+
import { describe, test } from '@dxos/test';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
setHeading,
|
|
13
|
+
addStyle,
|
|
14
|
+
removeStyle,
|
|
15
|
+
addLink,
|
|
16
|
+
removeLink,
|
|
17
|
+
addList,
|
|
18
|
+
removeList,
|
|
19
|
+
addBlockquote,
|
|
20
|
+
removeBlockquote,
|
|
21
|
+
addCodeblock,
|
|
22
|
+
removeCodeblock,
|
|
23
|
+
Inline,
|
|
24
|
+
List,
|
|
25
|
+
getFormatting,
|
|
26
|
+
type Formatting,
|
|
27
|
+
} from './formatting';
|
|
28
|
+
|
|
29
|
+
export const emptyFormatting: Formatting = {
|
|
30
|
+
blockType: 'paragraph',
|
|
31
|
+
strong: false,
|
|
32
|
+
emphasis: false,
|
|
33
|
+
strikethrough: false,
|
|
34
|
+
code: false,
|
|
35
|
+
link: false,
|
|
36
|
+
listStyle: null,
|
|
37
|
+
blockquote: false,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const createState = (doc: string) => {
|
|
41
|
+
const selStart = doc.indexOf('{');
|
|
42
|
+
const selEnd = doc.indexOf('}') - 1;
|
|
43
|
+
return EditorState.create({
|
|
44
|
+
doc: doc.replace(/[{}]/g, ''),
|
|
45
|
+
selection: { anchor: selStart, head: selEnd },
|
|
46
|
+
extensions: markdownLanguage,
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const testCommand = (name: string, doc: string, command: StateCommand, result: string | null) => {
|
|
51
|
+
test(name, () => {
|
|
52
|
+
let state = createState(doc);
|
|
53
|
+
const status = command({ state, dispatch: (tr) => (state = tr.state) });
|
|
54
|
+
if (!status || result === null) {
|
|
55
|
+
expect(status).to.equal(result !== null);
|
|
56
|
+
} else {
|
|
57
|
+
let resultSel = null;
|
|
58
|
+
if (result.includes('{')) {
|
|
59
|
+
const resultState = createState(result);
|
|
60
|
+
result = resultState.doc.toString();
|
|
61
|
+
resultSel = resultState.selection.main;
|
|
62
|
+
}
|
|
63
|
+
expect(state.doc.toString()).to.equal(result);
|
|
64
|
+
if (resultSel) {
|
|
65
|
+
expect([state.selection.main.from, state.selection.main.to]).to.deep.equal([resultSel.from, resultSel.to]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
describe('setHeading', () => {
|
|
72
|
+
testCommand('can create a heading', 'Hello {}', setHeading(1), '# Hello ');
|
|
73
|
+
|
|
74
|
+
testCommand('can create a level 2 heading', 'One\n\nTw{o}', setHeading(2), 'One\n\n## Two');
|
|
75
|
+
|
|
76
|
+
testCommand('can increase the depth of a heading', '# One{}', setHeading(3), '### One');
|
|
77
|
+
|
|
78
|
+
testCommand('can decrease the depth of a heading', '## One{}', setHeading(1), '# One');
|
|
79
|
+
|
|
80
|
+
testCommand('can remove a heading', '### A{}', setHeading(0), 'A');
|
|
81
|
+
|
|
82
|
+
testCommand(
|
|
83
|
+
'can make multiple blocks a heading',
|
|
84
|
+
'{One\n\nTwo}\n\nThree',
|
|
85
|
+
setHeading(3),
|
|
86
|
+
'### One\n\n### Two\n\nThree',
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
testCommand(
|
|
90
|
+
"doesn't affect code blocks",
|
|
91
|
+
'{One\n\n```\nTwo\n```\nThree}',
|
|
92
|
+
setHeading(1),
|
|
93
|
+
'# One\n\n```\nTwo\n```\n# Three',
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
testCommand('can remove a setext heading', 'One{}\n===', setHeading(0), 'One');
|
|
97
|
+
|
|
98
|
+
testCommand('can change a setext heading to ATX', 'One{}\n---', setHeading(1), '# One');
|
|
99
|
+
|
|
100
|
+
testCommand('can add a heading inside block markup', '> - {one\n> - two}\n', setHeading(1), '> - # one\n> - # two\n');
|
|
101
|
+
|
|
102
|
+
testCommand('can remove a heading inside block markup', '1. # one{}', setHeading(0), '1. one');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('addStyle', () => {
|
|
106
|
+
const em = addStyle(Inline.Emphasis);
|
|
107
|
+
const str = addStyle(Inline.Strong);
|
|
108
|
+
const code = addStyle(Inline.Code);
|
|
109
|
+
|
|
110
|
+
testCommand('can add emphasis', 'one {two}', em, 'one *{two}*');
|
|
111
|
+
|
|
112
|
+
testCommand('can add emphasis around cursor', 'one {}', em, 'one *{}*');
|
|
113
|
+
|
|
114
|
+
testCommand('can add strong style', '{one\n\ntwo}', str, '**{one**\n\n**two}**');
|
|
115
|
+
|
|
116
|
+
testCommand('can add strikethrough', '{hey}', addStyle(Inline.Strikethrough), '~~{hey}~~');
|
|
117
|
+
|
|
118
|
+
testCommand('can add code style', 'a {variable}', code, 'a `{variable}`');
|
|
119
|
+
|
|
120
|
+
testCommand('clears styles inside added code', '{some **bold** text}', code, '`some bold text`');
|
|
121
|
+
|
|
122
|
+
testCommand(
|
|
123
|
+
'clears styles partially inside added code',
|
|
124
|
+
'**some {bold** and *emphasized} text*',
|
|
125
|
+
code,
|
|
126
|
+
'**some** `bold and emphasized` *text*',
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
testCommand(
|
|
130
|
+
'inserts markers at same position in the right order',
|
|
131
|
+
'{some **bold,}text**',
|
|
132
|
+
code,
|
|
133
|
+
'`some bold,`**text**',
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
testCommand('remove existing markers inside', '{one *two* three}', em, '*{one two three}*');
|
|
137
|
+
|
|
138
|
+
testCommand(
|
|
139
|
+
'removes existing markers overlapping boundaries',
|
|
140
|
+
'*one {two* *three} four*',
|
|
141
|
+
em,
|
|
142
|
+
'*one {two three} four*',
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
testCommand(
|
|
146
|
+
'can style headers',
|
|
147
|
+
'{one\n\n# two\n\nthree\n---\n\nfour} five',
|
|
148
|
+
str,
|
|
149
|
+
'**{one**\n\n# **two**\n\n**three**\n---\n\n**four}** five',
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
testCommand('moves the insert position out of markup', 'one ~{~two~}~ three', em, 'one ~~*two*~~ three');
|
|
153
|
+
|
|
154
|
+
testCommand(
|
|
155
|
+
'moves the range to cover partially covered links and images',
|
|
156
|
+
'[one {two](x)  five',
|
|
157
|
+
em,
|
|
158
|
+
'*[one {two](x) * five',
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('removeStyle', () => {
|
|
163
|
+
const em = removeStyle(Inline.Emphasis);
|
|
164
|
+
const str = removeStyle(Inline.Strong);
|
|
165
|
+
const code = removeStyle(Inline.Code);
|
|
166
|
+
const strike = removeStyle(Inline.Strikethrough);
|
|
167
|
+
|
|
168
|
+
testCommand('can remove emphasis', 'one *{two}*', em, 'one {two}');
|
|
169
|
+
|
|
170
|
+
testCommand('can remove emphasis around cursor', 'one *{}*', em, 'one {}');
|
|
171
|
+
|
|
172
|
+
testCommand('can remove strong style', '{**one**\n\n**two**}', str, '{one\n\ntwo}');
|
|
173
|
+
|
|
174
|
+
testCommand('can remove strikethrough', '~~{hey}~~', strike, '{hey}');
|
|
175
|
+
|
|
176
|
+
testCommand('can remove strikethrough around cursor', 'one ~~{}~~', strike, 'one {}');
|
|
177
|
+
|
|
178
|
+
testCommand('can remove strikethrough around cursor with inner emphasis', 'one ~~*{}*~~', strike, 'one *{}*');
|
|
179
|
+
|
|
180
|
+
testCommand('can remove code style', 'a `{variable}`', code, 'a {variable}');
|
|
181
|
+
|
|
182
|
+
testCommand(
|
|
183
|
+
'can remove emphasis across multiple blocks',
|
|
184
|
+
'{*one*\n\n# *two*\n\n> 1. *three} four*\n',
|
|
185
|
+
em,
|
|
186
|
+
'{one\n\n# two\n\n> 1. three} *four*\n',
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
testCommand('can shrink existing styles', '*one {two three} four*', em, '*one* {two three} *four*');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('addLink', () => {
|
|
193
|
+
testCommand('adds a link', '{}', addLink, '[]({})');
|
|
194
|
+
|
|
195
|
+
testCommand('adds a link around text', 'hello {world}', addLink, 'hello [world]({})');
|
|
196
|
+
|
|
197
|
+
testCommand('clears existing links', '[hello {world}](foo)', addLink, 'hello [world]({})');
|
|
198
|
+
|
|
199
|
+
testCommand('does nothing across blocks', '{one\n\ntwo}', addLink, null);
|
|
200
|
+
|
|
201
|
+
testCommand('does nothing in code blocks', '```\n{one}\n```', addLink, null);
|
|
202
|
+
|
|
203
|
+
testCommand('patches up overlapping styles before', '*foo {bar* baz}', addLink, '*foo* [*bar* baz]({})');
|
|
204
|
+
|
|
205
|
+
testCommand(
|
|
206
|
+
'patches up overlapping styles after',
|
|
207
|
+
'one {two ~~three} four~~',
|
|
208
|
+
addLink,
|
|
209
|
+
'one [two ~~three~~]({}) ~~four~~',
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('removeLink', () => {
|
|
214
|
+
testCommand('removes a link', '[hi{}](x)', removeLink, 'hi{}');
|
|
215
|
+
|
|
216
|
+
testCommand('removes multiple links', '{[hi](x)\n\none [two](y)}', removeLink, '{hi\n\none two}');
|
|
217
|
+
|
|
218
|
+
testCommand('removes label links', '[hi{}][hi]', removeLink, 'hi{}');
|
|
219
|
+
|
|
220
|
+
testCommand('removes titles in links', '[hi{}](a "title" )', removeLink, 'hi{}');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('addList', () => {
|
|
224
|
+
const bullet = addList(List.Bullet);
|
|
225
|
+
const ordered = addList(List.Ordered);
|
|
226
|
+
|
|
227
|
+
testCommand('can add a bullet list', 'Hi{}', bullet, '- Hi');
|
|
228
|
+
|
|
229
|
+
testCommand('can add an ordered list', 'Hi{}', ordered, '1. Hi');
|
|
230
|
+
|
|
231
|
+
testCommand('can add a task list', 'Hi{}', addList(List.Task), '- [ ] Hi');
|
|
232
|
+
|
|
233
|
+
testCommand(
|
|
234
|
+
'can wrap multiple blocks in a bullet list',
|
|
235
|
+
'{One\n\n# Two\n\nThree}',
|
|
236
|
+
bullet,
|
|
237
|
+
'- One\n\n- # Two\n\n- Three',
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
testCommand('continues an existing numbered list', '1. Hello\n\nHi{}', ordered, '1. Hello\n\n2. Hi');
|
|
241
|
+
|
|
242
|
+
testCommand(
|
|
243
|
+
'can wrap multi-line blocks',
|
|
244
|
+
'Hello this\nis a three-line\nparagraph.{}',
|
|
245
|
+
bullet,
|
|
246
|
+
'- Hello this\n is a three-line\n paragraph.',
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
testCommand(
|
|
250
|
+
'can wrap fenced code blocks',
|
|
251
|
+
'```javascript\ntrue{}\n```',
|
|
252
|
+
ordered,
|
|
253
|
+
'1. ```javascript\n true\n ```',
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
testCommand(
|
|
257
|
+
'can wrap blocks inside markup',
|
|
258
|
+
'> 1. Hello\n {World\n>\n> Again}',
|
|
259
|
+
bullet,
|
|
260
|
+
'> 1. - Hello\n World\n>\n> - Again',
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
testCommand('restarts numbering on outer block markup boundaries', '> {one\n\ntwo}', ordered, '> 1. one\n\n1. two');
|
|
264
|
+
|
|
265
|
+
testCommand('aligns with above bullet list', ' - one\n\ntwo{}', bullet, ' - one\n\n - two');
|
|
266
|
+
|
|
267
|
+
testCommand('aligns with above ordered list', ' 1. One\n\nTwo{}', ordered, ' 1. One\n\n 2. Two');
|
|
268
|
+
|
|
269
|
+
testCommand('compensates for number size when aligning', ' 9. One\n\nTwo{}', ordered, ' 9. One\n\n 10. Two');
|
|
270
|
+
|
|
271
|
+
testCommand(
|
|
272
|
+
'anticipates number size of other items',
|
|
273
|
+
'{a\n\nb\n\nc\n\nd\n\ne\n\nf\n\ng\n\nh\n\ni\n\nj}',
|
|
274
|
+
ordered,
|
|
275
|
+
' 1. a\n\n 2. b\n\n 3. c\n\n 4. d\n\n 5. e\n\n 6. f\n\n 7. g\n\n 8. h\n\n 9. i\n\n10. j',
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
testCommand(
|
|
279
|
+
'renumbers lists after the selection',
|
|
280
|
+
'one{}\n\n1. two\n\n2. three',
|
|
281
|
+
ordered,
|
|
282
|
+
'1. one\n\n2. two\n\n3. three',
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
testCommand("doesn't renumber lists with a different parent", '> one{}\n\n1. two', ordered, '> 1. one\n\n1. two');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('removeList', () => {
|
|
289
|
+
const bullet = removeList(List.Bullet);
|
|
290
|
+
const ordered = removeList(List.Ordered);
|
|
291
|
+
|
|
292
|
+
testCommand('can remove a bullet list', ' - Hi{}', bullet, 'Hi');
|
|
293
|
+
|
|
294
|
+
testCommand('can remove an ordered list', '1. Hi{}', ordered, 'Hi');
|
|
295
|
+
|
|
296
|
+
testCommand('can remove a task list', '- [x] Hi{}', removeList(List.Task), 'Hi');
|
|
297
|
+
|
|
298
|
+
testCommand(
|
|
299
|
+
'can remove a bullet list from multiple blocks',
|
|
300
|
+
'- {One\n\n- # Two\n\n- Three}',
|
|
301
|
+
bullet,
|
|
302
|
+
'One\n\n# Two\n\nThree',
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
testCommand(
|
|
306
|
+
'can unwrap multi-line blocks',
|
|
307
|
+
'1. Hello this\n is a three-line\nparagraph.{}',
|
|
308
|
+
ordered,
|
|
309
|
+
'Hello this\nis a three-line\nparagraph.',
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
testCommand('can unwrap fenced code blocks', '- ```javascript\n true{}\n ```', bullet, '```javascript\ntrue\n```');
|
|
313
|
+
|
|
314
|
+
testCommand(
|
|
315
|
+
'can unwrap blocks inside markup',
|
|
316
|
+
'> 1. - Hello\n {World\n>\n> - Again}',
|
|
317
|
+
bullet,
|
|
318
|
+
'> 1. Hello\n World\n>\n> Again',
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
testCommand("doesn't unwrap other types of lists", '1. foo{}', bullet, null);
|
|
322
|
+
|
|
323
|
+
testCommand(
|
|
324
|
+
'renumbers lists after the selection',
|
|
325
|
+
'1. one\n\n2. {two\n\n3. three}\n\n4. four\n\n5. five',
|
|
326
|
+
ordered,
|
|
327
|
+
'1. one\n\ntwo\n\nthree\n\n1. four\n\n2. five',
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
describe('addBlockquote', () => {
|
|
332
|
+
testCommand('can add a blockquote', 'Hi{}', addBlockquote, '> Hi');
|
|
333
|
+
|
|
334
|
+
testCommand(
|
|
335
|
+
'can add a blockquote to multi-line paragraph',
|
|
336
|
+
'One\ntwo\nthree{}',
|
|
337
|
+
addBlockquote,
|
|
338
|
+
'> One\n> two\n> three',
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
testCommand(
|
|
342
|
+
'can add a blockquote to multiple blocks',
|
|
343
|
+
'{one\ntwo\n\n# three\n}\nfour',
|
|
344
|
+
addBlockquote,
|
|
345
|
+
'> one\n> two\n>\n> # three\n\nfour',
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('removeBlockquote', () => {
|
|
350
|
+
testCommand('can remove a blockquote', '> Hi{}', removeBlockquote, 'Hi');
|
|
351
|
+
|
|
352
|
+
testCommand('can remove a blockquote from a multi-line paragraph', '> A\n> B\n> C{}', removeBlockquote, 'A\nB\nC');
|
|
353
|
+
|
|
354
|
+
testCommand('can remove a nested blockquote', '>> A\n>> B\n>> C{}', removeBlockquote, '> A\n> B\n> C');
|
|
355
|
+
|
|
356
|
+
testCommand(
|
|
357
|
+
'removes markers from adjacent blank lines',
|
|
358
|
+
'> one\n>\n> two{}\n>\n> three',
|
|
359
|
+
removeBlockquote,
|
|
360
|
+
'> one\n\ntwo\n\n> three',
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
testCommand(
|
|
364
|
+
'can remove quotes nested in lists',
|
|
365
|
+
' - 1. > foo\n > {bar\n - > baz}',
|
|
366
|
+
removeBlockquote,
|
|
367
|
+
' - 1. foo\n bar\n - baz',
|
|
368
|
+
);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe('addCodeblock', () => {
|
|
372
|
+
testCommand('add a code block to a blank line', '{}', addCodeblock, '```{lang}\n\n```');
|
|
373
|
+
|
|
374
|
+
testCommand(
|
|
375
|
+
'can turn a paragraph into a code block',
|
|
376
|
+
'one\ntwo{}\nthree',
|
|
377
|
+
addCodeblock,
|
|
378
|
+
'```\none\ntwo{}\nthree\n```',
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
testCommand(
|
|
382
|
+
'can turn an indented paragraph into a code block',
|
|
383
|
+
'1. one\n two{}',
|
|
384
|
+
addCodeblock,
|
|
385
|
+
'1. ```\n one\n two{}\n ```',
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
testCommand(
|
|
389
|
+
'can turn multiple paragraphs into a code block',
|
|
390
|
+
"{10 print 'hello'\n\n20 goto 10}",
|
|
391
|
+
addCodeblock,
|
|
392
|
+
"```\n10 print 'hello'\n\n20 goto 10\n```",
|
|
393
|
+
);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
describe('removeCodeblock', () => {
|
|
397
|
+
testCommand('removes fenced code blocks', '```\ncode{}\n```', removeCodeblock, 'code{}');
|
|
398
|
+
|
|
399
|
+
testCommand('removes indented code blocks', ' code{}', removeCodeblock, 'code{}');
|
|
400
|
+
|
|
401
|
+
testCommand('does nothing on regular text', 'not code{}', removeCodeblock, null);
|
|
402
|
+
|
|
403
|
+
testCommand('can remove multiple code block', '{```\none\n```\n\n two}', removeCodeblock, 'one\n\ntwo');
|
|
404
|
+
|
|
405
|
+
testCommand(
|
|
406
|
+
'can remove code in other block markup',
|
|
407
|
+
'> 1. ```\n> {one\n> ```\n>\n> 2. two}',
|
|
408
|
+
removeCodeblock,
|
|
409
|
+
'> 1. one\n>\n> 2. two',
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe('getFormatting', () => {
|
|
414
|
+
const t = (name: string, doc: string, result: Partial<Formatting>) => {
|
|
415
|
+
test(name, () => {
|
|
416
|
+
const formatting = getFormatting(createState(doc));
|
|
417
|
+
const expected = Object.assign({}, emptyFormatting, result);
|
|
418
|
+
expect(formatting).to.deep.equal(expected);
|
|
419
|
+
});
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
t('returns nothing special for regular content', 'hello {world}', {});
|
|
423
|
+
|
|
424
|
+
t('can see emphasis', 'hello *{world}*', { emphasis: true });
|
|
425
|
+
|
|
426
|
+
t('can see strong emphasis', 'hello **{world}**', { strong: true });
|
|
427
|
+
|
|
428
|
+
t('can see strikethrough', 'hello ~~{world}~~', { strikethrough: true });
|
|
429
|
+
|
|
430
|
+
t('can see inline code', 'hello `{world}`', { code: true });
|
|
431
|
+
|
|
432
|
+
t("doesn't enable inline styles when only part of selection is styled", 'he{llo **wor}ld**', {});
|
|
433
|
+
|
|
434
|
+
t('can handle adjacent styled spans', '**he{llo**\n**wor}ld**', { strong: true });
|
|
435
|
+
|
|
436
|
+
t('ignores markers for inline style purposes', '{***o***}', { strong: true, emphasis: true });
|
|
437
|
+
|
|
438
|
+
t('handles multi-paragraph inline styles', '~~*fo{o*~~\n\n~~ba}r~~', { strikethrough: true });
|
|
439
|
+
|
|
440
|
+
t('activates for cursor selections', '~~*fo{}o*~~', { strikethrough: true, emphasis: true });
|
|
441
|
+
|
|
442
|
+
t('spots a heading', '# Hello{}', { blockType: 'heading1' });
|
|
443
|
+
|
|
444
|
+
t('spots a heading', '# Hello{}', { blockType: 'heading1' });
|
|
445
|
+
|
|
446
|
+
t('spots a setext heading', 'Hello{}\n---', { blockType: 'heading2' });
|
|
447
|
+
|
|
448
|
+
t('spots a code block', ' {code}', { blockType: 'codeblock' });
|
|
449
|
+
|
|
450
|
+
t('spots a fenced code block', '```\n{code}\n```', { blockType: 'codeblock' });
|
|
451
|
+
|
|
452
|
+
t('reports null for mixed block types', '# on{e\n\ntwo}', { blockType: null });
|
|
453
|
+
|
|
454
|
+
t('reports heading for multiple selected headings', '# on{e\n\n# two}', { blockType: 'heading1' });
|
|
455
|
+
|
|
456
|
+
t('notices blockquotes', '> {one}', { blockquote: true });
|
|
457
|
+
|
|
458
|
+
t('notices multi-block blockquotes', '> {one\n>\n> two}', { blockquote: true });
|
|
459
|
+
|
|
460
|
+
t('disables blockquote when not all blocks are quoted', '{one\n\n> two}', {});
|
|
461
|
+
|
|
462
|
+
t('notices ordered lists', ' 1. Hi{}', { listStyle: 'ordered' });
|
|
463
|
+
|
|
464
|
+
t('notices bullet lists', ' - Hi{}', { listStyle: 'bullet' });
|
|
465
|
+
|
|
466
|
+
t('notices task lists', ' - [x] {Hi}', { listStyle: 'task' });
|
|
467
|
+
|
|
468
|
+
t('uses the innermost list style', ' - 1. Ok{}', { listStyle: 'ordered' });
|
|
469
|
+
|
|
470
|
+
t('notices multi-block lists', '1. {one\n2. two}', { listStyle: 'ordered' });
|
|
471
|
+
|
|
472
|
+
t("disables list style when a block isn't a list", '1. {one\n\ntwo}', {});
|
|
473
|
+
|
|
474
|
+
t('sees markup directly around cursor even if not valid', 'q **{}**', { strong: true });
|
|
475
|
+
|
|
476
|
+
t('sees multiple types of markup directly around cursor', 'q ~~***{}***~~', {
|
|
477
|
+
strong: true,
|
|
478
|
+
emphasis: true,
|
|
479
|
+
strikethrough: true,
|
|
480
|
+
});
|
|
481
|
+
});
|