Skip to content

Commit b904704

Browse files
authored
feat: add FoldingHeading extension (#314)
* fix(YfmHeading): improve folding behaviour * feat: add FoldingHeading extension
1 parent b885e22 commit b904704

30 files changed

+720
-17
lines changed

demo/Playground.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
wysiwygToolbarConfigs,
1616
} from '../src';
1717
import type {ToolbarActionData} from '../src/bundle/Editor';
18+
import {FoldingHeading} from '../src/extensions/yfm/FoldingHeading';
1819
import {Math} from '../src/extensions/yfm/Math';
1920
import {Mermaid} from '../src/extensions/yfm/Mermaid';
2021
import {YfmHtmlBlock} from '../src/extensions/yfm/YfmHtmlBlock';
@@ -163,7 +164,8 @@ export const Playground = React.memo<PlaygroundProps>((props) => {
163164
.use(YfmHtmlBlock, {
164165
useConfig: useYfmHtmlBlockStyles,
165166
sanitize,
166-
}),
167+
})
168+
.use(FoldingHeading),
167169
});
168170

169171
useEffect(() => {

demo/YFM.stories.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,41 @@ sequenceDiagram
289289
Alice->>Bob: Hi Bob
290290
Bob->>Alice: Hi Alice
291291
\`\`\`
292+
`.trim(),
293+
294+
foldingHeadings: `
295+
#+ Heading 1
296+
297+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum euismod, nulla sit amet sodales porttitor, ligula arcu consectetur justo, sit amet varius orci lorem a augue.
298+
299+
##+ Heading 2
300+
301+
Aenean lobortis rutrum eleifend. Aenean pulvinar orci eros, vitae porta justo interdum at. Proin metus nulla, porta tincidunt tempus eget, faucibus quis nisi.
302+
303+
###+ Heading 3
304+
305+
Praesent ut scelerisque tellus, condimentum iaculis massa. Integer a ante eu eros luctus vestibulum. Phasellus non laoreet lacus, non bibendum dui.
306+
307+
####+ Heading 4
308+
309+
Nunc pellentesque mollis tortor, ut dictum lectus consequat id. Aenean aliquet enim ac facilisis ornare. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
310+
311+
312+
#####+ Heading 5
313+
314+
Maecenas nec nisl eu dui lacinia consequat. Nulla non lacus varius risus lacinia vulputate. Interdum et malesuada fames ac ante ipsum primis in faucibus.
315+
316+
######+ Heading 6
317+
318+
Nulla facilisi. Pellentesque eu neque tincidunt odio viverra bibendum. Morbi consequat ac nibh id sagittis. Cras fermentum molestie urna vitae viverra.
319+
320+
## Heading 2
321+
322+
Mauris sed sem lorem. Maecenas vitae augue dui. In tempus vitae sem sed ultrices. Sed hendrerit mauris a ultrices rhoncus. Sed eget nibh nec turpis dignissim hendrerit non nec dolor.
323+
324+
# Heading 1
325+
326+
Duis id risus sit amet nunc ornare lobortis sed ut ipsum. Cras tempus ultricies nisl in auctor. Sed nec dui eget odio laoreet commodo at nec libero.
292327
`.trim(),
293328
};
294329

@@ -333,6 +368,10 @@ export const Tasklist: StoryFn<PlaygroundStoryProps> = (props) => (
333368
<PlaygroundComponent {...props} initial={markup.tasklist} />
334369
);
335370

371+
export const FoldingHeadings: StoryFn<PlaygroundProps> = (props) => (
372+
<PlaygroundComponent {...props} initial={markup.foldingHeadings} />
373+
);
374+
336375
export const YfmNote: StoryFn<PlaygroundStoryProps> = (props) => (
337376
<PlaygroundComponent {...props} initial={markup.yfmNotes} />
338377
);
@@ -367,6 +406,7 @@ export const MermaidDiagram: StoryFn<PlaygroundStoryProps> = (props) => (
367406

368407
TextMarks.storyName = 'Text';
369408
TextMarks.args = args;
409+
FoldingHeadings.args = args;
370410
YfmNote.storyName = 'YFM Note';
371411
YfmNote.args = args;
372412
YfmCut.storyName = 'YFM Cut';

demo/md-plugins.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/* eslint-disable import/no-extraneous-dependencies */
2+
import {transform as foldingHeadings} from '@diplodoc/folding-headings-extension';
3+
import '@diplodoc/folding-headings-extension/runtime';
24
import {transform as yfmHtmlBlock} from '@diplodoc/html-extension';
35
import {transform as latex} from '@diplodoc/latex-extension';
46
import {transform as mermaid} from '@diplodoc/mermaid-extension';
@@ -54,6 +56,7 @@ const extendedPlugins = defaultPlugins.concat(
5456
mermaid({bundle: false, runtime: MERMAID_RUNTIME}),
5557
sub,
5658
yfmHtmlBlock({bundle: false, runtimeJsPath: YFM_HTML_BLOCK_RUNTIME}),
59+
foldingHeadings({bundle: false}),
5760
);
5861

5962
export {extendedPlugins as plugins};

package-lock.json

Lines changed: 15 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@
167167
"@codemirror/state": "6.4.1",
168168
"@codemirror/view": "6.26.3",
169169
"@gravity-ui/i18n": "^1.1.0",
170-
"@gravity-ui/icons": "^2.0.0",
170+
"@gravity-ui/icons": "^2.10.0",
171171
"@lezer/highlight": "1.2.0",
172172
"@lezer/markdown": "1.3.0",
173173
"@types/is-number": "^7.0.1",
@@ -199,6 +199,7 @@
199199
"tslib": "^2.3.1"
200200
},
201201
"devDependencies": {
202+
"@diplodoc/folding-headings-extension": "0.1.0",
202203
"@diplodoc/html-extension": "1.2.7",
203204
"@diplodoc/latex-extension": "1.0.3",
204205
"@diplodoc/mermaid-extension": "1.2.1",
@@ -252,13 +253,16 @@
252253
"typescript": "^4.5.2"
253254
},
254255
"peerDependenciesMeta": {
255-
"@diplodoc/latex-extension": {
256+
"@diplodoc/folding-headings-extension": {
256257
"optional": true
257258
},
258-
"@diplodoc/mermaid-extension": {
259+
"@diplodoc/html-extension": {
259260
"optional": true
260261
},
261-
"@diplodoc/html-extension": {
262+
"@diplodoc/latex-extension": {
263+
"optional": true
264+
},
265+
"@diplodoc/mermaid-extension": {
262266
"optional": true
263267
},
264268
"highlight.js": {
@@ -269,6 +273,7 @@
269273
}
270274
},
271275
"peerDependencies": {
276+
"@diplodoc/folding-headings-extension": "^0.1.0",
272277
"@diplodoc/html-extension": "^1.2.7",
273278
"@diplodoc/latex-extension": "^1.0.3",
274279
"@diplodoc/mermaid-extension": "^1.0.0",

src/bundle/config/icons.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
CutIcon,
77
EmojiIcon,
88
FileIcon,
9+
FoldingHeadingIcon,
910
FunctionBlockIcon,
1011
FunctionInlineIcon,
1112
HRuleIcon,
@@ -78,7 +79,8 @@ type Icon =
7879
| 'emoji'
7980
| 'tabs'
8081
| 'mermaid'
81-
| 'html';
82+
| 'html'
83+
| 'foldingHeading';
8284

8385
type Icons = Record<Icon, ToolbarIconData>;
8486

@@ -135,4 +137,6 @@ export const icons: Icons = {
135137

136138
tabs: {data: TabsIcon},
137139
mermaid: {data: MermaidIcon},
140+
141+
foldingHeading: {data: FoldingHeadingIcon},
138142
};

src/bundle/config/wysiwyg.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,18 @@ export const wToolbarConfig: WToolbarData = [
521521
[wImageItemData, wFileItemData, wTableItemData, wCheckboxItemData],
522522
];
523523

524+
export const wToggleHeadingFoldingItemData: SelectionContextItemData = {
525+
id: 'folding-heading',
526+
type: ToolbarDataType.SingleButton,
527+
icon: icons.foldingHeading,
528+
title: () => i18n('folding-heading'),
529+
hint: () => i18n('folding-heading_hint'),
530+
isActive: (editor) => editor.actions.toggleHeadingFolding?.isActive() ?? false,
531+
isEnable: (editor) => editor.actions.toggleHeadingFolding?.isEnable() ?? false,
532+
exec: (editor) => editor.actions.toggleHeadingFolding.run(),
533+
condition: 'enabled',
534+
};
535+
524536
const textContextItemData: SelectionContextItemData = {
525537
id: 'text',
526538
type: ToolbarDataType.ReactComponent,
@@ -534,7 +546,7 @@ const textContextItemData: SelectionContextItemData = {
534546
};
535547

536548
export const wSelectionMenuConfig: SelectionContextConfig = [
537-
[textContextItemData],
549+
[wToggleHeadingFoldingItemData, textContextItemData],
538550
[...wBiusGroupConfig, wCodeItemData],
539551
[
540552
{
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import type {Action, ExtensionAuto} from '../../../core';
2+
3+
import {FoldingHeadingSpecs} from './FoldingHeadingSpec';
4+
import {toggleHeadingFoldingAction} from './actions';
5+
import {
6+
openHeadingAndCreateParagraphAfterIfCursorAtEndOfHeading,
7+
removeFoldingIfCursorAtStartOfHeading,
8+
} from './commands';
9+
import {foldingHeadingRule} from './input-rules';
10+
import {foldingPlugin} from './plugins/Folding';
11+
import {headingType} from './utils';
12+
13+
import '@diplodoc/folding-headings-extension/runtime/styles.css';
14+
15+
const action = 'toggleHeadingFolding';
16+
17+
export const FoldingHeading: ExtensionAuto = (builder) => {
18+
builder.use(FoldingHeadingSpecs);
19+
20+
builder.addAction(action, () => toggleHeadingFoldingAction);
21+
builder.addInputRules(({schema}) => ({rules: [foldingHeadingRule(headingType(schema), 6)]}));
22+
builder.addKeymap(
23+
() => ({
24+
Enter: openHeadingAndCreateParagraphAfterIfCursorAtEndOfHeading,
25+
Backspace: removeFoldingIfCursorAtStartOfHeading,
26+
}),
27+
builder.Priority.High,
28+
);
29+
builder.addPlugin(foldingPlugin);
30+
};
31+
32+
declare global {
33+
namespace WysiwygEditor {
34+
interface Actions {
35+
[action]: Action;
36+
}
37+
}
38+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {builders} from 'prosemirror-test-builder';
2+
3+
import {createMarkupChecker} from '../../../../../tests/sameMarkup';
4+
import {ExtensionsManager} from '../../../../core';
5+
import {BaseNode, BaseSchemaSpecs} from '../../../base/specs';
6+
import {ItalicSpecs, headingNodeName, italicMarkName} from '../../../markdown/specs';
7+
import {YfmHeadingAttr, YfmHeadingSpecs} from '../../../yfm/specs';
8+
9+
import {FoldingHeadingSpecs} from './FoldingHeadingSpecs';
10+
11+
const {schema, markupParser, serializer} = new ExtensionsManager({
12+
extensions: (builder) =>
13+
builder
14+
.use(BaseSchemaSpecs, {})
15+
.use(ItalicSpecs)
16+
.use(YfmHeadingSpecs, {})
17+
.use(FoldingHeadingSpecs),
18+
}).buildDeps();
19+
20+
const {doc, h1, h2, h3, h4, h5, h6, i} = builders<
21+
'doc' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6',
22+
'i'
23+
>(schema, {
24+
doc: {nodeType: BaseNode.Doc},
25+
h1: {nodeType: headingNodeName, [YfmHeadingAttr.Level]: 1},
26+
h2: {nodeType: headingNodeName, [YfmHeadingAttr.Level]: 2},
27+
h3: {nodeType: headingNodeName, [YfmHeadingAttr.Level]: 3},
28+
h4: {nodeType: headingNodeName, [YfmHeadingAttr.Level]: 4},
29+
h5: {nodeType: headingNodeName, [YfmHeadingAttr.Level]: 5},
30+
h6: {nodeType: headingNodeName, [YfmHeadingAttr.Level]: 6},
31+
i: {markType: italicMarkName},
32+
});
33+
34+
const {same} = createMarkupChecker({parser: markupParser, serializer});
35+
36+
describe('Folding Headings', () => {
37+
it('should parse folding headings', () => {
38+
const markup = `
39+
#+ heading 1
40+
41+
##+ heading 2
42+
43+
###+ heading 3
44+
45+
####+ heading 4
46+
47+
#####+ heading 5
48+
49+
######+ heading 6
50+
`.trim();
51+
52+
return same(
53+
markup,
54+
doc(
55+
h1({[YfmHeadingAttr.Folding]: true}, 'heading 1'),
56+
h2({[YfmHeadingAttr.Folding]: true}, 'heading 2'),
57+
h3({[YfmHeadingAttr.Folding]: true}, 'heading 3'),
58+
h4({[YfmHeadingAttr.Folding]: true}, 'heading 4'),
59+
h5({[YfmHeadingAttr.Folding]: true}, 'heading 5'),
60+
h6({[YfmHeadingAttr.Folding]: true}, 'heading 6'),
61+
),
62+
);
63+
});
64+
65+
it('should parse common headings', () => {
66+
const markup = `
67+
# heading 1
68+
69+
## heading 2
70+
71+
### heading 3
72+
73+
#### heading 4
74+
75+
##### heading 5
76+
77+
###### heading 6
78+
`.trim();
79+
80+
return same(
81+
markup,
82+
doc(
83+
h1('heading 1'),
84+
h2('heading 2'),
85+
h3('heading 3'),
86+
h4('heading 4'),
87+
h5('heading 5'),
88+
h6('heading 6'),
89+
),
90+
);
91+
});
92+
93+
it('should parse folding heading with inline markup', () => {
94+
const markup = `
95+
##+ *heading* 2
96+
`.trim();
97+
98+
return same(markup, doc(h2({[YfmHeadingAttr.Folding]: true}, i('heading'), ' 2')));
99+
});
100+
});

0 commit comments

Comments
 (0)