Skip to content

Commit

Permalink
feat: add FoldingHeading extension (#314)
Browse files Browse the repository at this point in the history
* fix(YfmHeading): improve folding behaviour

* feat: add FoldingHeading extension
  • Loading branch information
d3m1d0v authored Aug 1, 2024
1 parent b885e22 commit b904704
Show file tree
Hide file tree
Showing 30 changed files with 720 additions and 17 deletions.
4 changes: 3 additions & 1 deletion demo/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
wysiwygToolbarConfigs,
} from '../src';
import type {ToolbarActionData} from '../src/bundle/Editor';
import {FoldingHeading} from '../src/extensions/yfm/FoldingHeading';
import {Math} from '../src/extensions/yfm/Math';
import {Mermaid} from '../src/extensions/yfm/Mermaid';
import {YfmHtmlBlock} from '../src/extensions/yfm/YfmHtmlBlock';
Expand Down Expand Up @@ -163,7 +164,8 @@ export const Playground = React.memo<PlaygroundProps>((props) => {
.use(YfmHtmlBlock, {
useConfig: useYfmHtmlBlockStyles,
sanitize,
}),
})
.use(FoldingHeading),
});

useEffect(() => {
Expand Down
40 changes: 40 additions & 0 deletions demo/YFM.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,41 @@ sequenceDiagram
Alice->>Bob: Hi Bob
Bob->>Alice: Hi Alice
\`\`\`
`.trim(),

foldingHeadings: `
#+ Heading 1
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.
##+ Heading 2
Aenean lobortis rutrum eleifend. Aenean pulvinar orci eros, vitae porta justo interdum at. Proin metus nulla, porta tincidunt tempus eget, faucibus quis nisi.
###+ Heading 3
Praesent ut scelerisque tellus, condimentum iaculis massa. Integer a ante eu eros luctus vestibulum. Phasellus non laoreet lacus, non bibendum dui.
####+ Heading 4
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.
#####+ Heading 5
Maecenas nec nisl eu dui lacinia consequat. Nulla non lacus varius risus lacinia vulputate. Interdum et malesuada fames ac ante ipsum primis in faucibus.
######+ Heading 6
Nulla facilisi. Pellentesque eu neque tincidunt odio viverra bibendum. Morbi consequat ac nibh id sagittis. Cras fermentum molestie urna vitae viverra.
## Heading 2
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.
# Heading 1
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.
`.trim(),
};

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

export const FoldingHeadings: StoryFn<PlaygroundProps> = (props) => (
<PlaygroundComponent {...props} initial={markup.foldingHeadings} />
);

export const YfmNote: StoryFn<PlaygroundStoryProps> = (props) => (
<PlaygroundComponent {...props} initial={markup.yfmNotes} />
);
Expand Down Expand Up @@ -367,6 +406,7 @@ export const MermaidDiagram: StoryFn<PlaygroundStoryProps> = (props) => (

TextMarks.storyName = 'Text';
TextMarks.args = args;
FoldingHeadings.args = args;
YfmNote.storyName = 'YFM Note';
YfmNote.args = args;
YfmCut.storyName = 'YFM Cut';
Expand Down
3 changes: 3 additions & 0 deletions demo/md-plugins.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-disable import/no-extraneous-dependencies */
import {transform as foldingHeadings} from '@diplodoc/folding-headings-extension';
import '@diplodoc/folding-headings-extension/runtime';
import {transform as yfmHtmlBlock} from '@diplodoc/html-extension';
import {transform as latex} from '@diplodoc/latex-extension';
import {transform as mermaid} from '@diplodoc/mermaid-extension';
Expand Down Expand Up @@ -54,6 +56,7 @@ const extendedPlugins = defaultPlugins.concat(
mermaid({bundle: false, runtime: MERMAID_RUNTIME}),
sub,
yfmHtmlBlock({bundle: false, runtimeJsPath: YFM_HTML_BLOCK_RUNTIME}),
foldingHeadings({bundle: false}),
);

export {extendedPlugins as plugins};
19 changes: 15 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 9 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.26.3",
"@gravity-ui/i18n": "^1.1.0",
"@gravity-ui/icons": "^2.0.0",
"@gravity-ui/icons": "^2.10.0",
"@lezer/highlight": "1.2.0",
"@lezer/markdown": "1.3.0",
"@types/is-number": "^7.0.1",
Expand Down Expand Up @@ -199,6 +199,7 @@
"tslib": "^2.3.1"
},
"devDependencies": {
"@diplodoc/folding-headings-extension": "0.1.0",
"@diplodoc/html-extension": "1.2.7",
"@diplodoc/latex-extension": "1.0.3",
"@diplodoc/mermaid-extension": "1.2.1",
Expand Down Expand Up @@ -252,13 +253,16 @@
"typescript": "^4.5.2"
},
"peerDependenciesMeta": {
"@diplodoc/latex-extension": {
"@diplodoc/folding-headings-extension": {
"optional": true
},
"@diplodoc/mermaid-extension": {
"@diplodoc/html-extension": {
"optional": true
},
"@diplodoc/html-extension": {
"@diplodoc/latex-extension": {
"optional": true
},
"@diplodoc/mermaid-extension": {
"optional": true
},
"highlight.js": {
Expand All @@ -269,6 +273,7 @@
}
},
"peerDependencies": {
"@diplodoc/folding-headings-extension": "^0.1.0",
"@diplodoc/html-extension": "^1.2.7",
"@diplodoc/latex-extension": "^1.0.3",
"@diplodoc/mermaid-extension": "^1.0.0",
Expand Down
6 changes: 5 additions & 1 deletion src/bundle/config/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CutIcon,
EmojiIcon,
FileIcon,
FoldingHeadingIcon,
FunctionBlockIcon,
FunctionInlineIcon,
HRuleIcon,
Expand Down Expand Up @@ -78,7 +79,8 @@ type Icon =
| 'emoji'
| 'tabs'
| 'mermaid'
| 'html';
| 'html'
| 'foldingHeading';

type Icons = Record<Icon, ToolbarIconData>;

Expand Down Expand Up @@ -135,4 +137,6 @@ export const icons: Icons = {

tabs: {data: TabsIcon},
mermaid: {data: MermaidIcon},

foldingHeading: {data: FoldingHeadingIcon},
};
14 changes: 13 additions & 1 deletion src/bundle/config/wysiwyg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,18 @@ export const wToolbarConfig: WToolbarData = [
[wImageItemData, wFileItemData, wTableItemData, wCheckboxItemData],
];

export const wToggleHeadingFoldingItemData: SelectionContextItemData = {
id: 'folding-heading',
type: ToolbarDataType.SingleButton,
icon: icons.foldingHeading,
title: () => i18n('folding-heading'),
hint: () => i18n('folding-heading_hint'),
isActive: (editor) => editor.actions.toggleHeadingFolding?.isActive() ?? false,
isEnable: (editor) => editor.actions.toggleHeadingFolding?.isEnable() ?? false,
exec: (editor) => editor.actions.toggleHeadingFolding.run(),
condition: 'enabled',
};

const textContextItemData: SelectionContextItemData = {
id: 'text',
type: ToolbarDataType.ReactComponent,
Expand All @@ -534,7 +546,7 @@ const textContextItemData: SelectionContextItemData = {
};

export const wSelectionMenuConfig: SelectionContextConfig = [
[textContextItemData],
[wToggleHeadingFoldingItemData, textContextItemData],
[...wBiusGroupConfig, wCodeItemData],
[
{
Expand Down
38 changes: 38 additions & 0 deletions src/extensions/yfm/FoldingHeading/FoldingHeading.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type {Action, ExtensionAuto} from '../../../core';

import {FoldingHeadingSpecs} from './FoldingHeadingSpec';
import {toggleHeadingFoldingAction} from './actions';
import {
openHeadingAndCreateParagraphAfterIfCursorAtEndOfHeading,
removeFoldingIfCursorAtStartOfHeading,
} from './commands';
import {foldingHeadingRule} from './input-rules';
import {foldingPlugin} from './plugins/Folding';
import {headingType} from './utils';

import '@diplodoc/folding-headings-extension/runtime/styles.css';

const action = 'toggleHeadingFolding';

export const FoldingHeading: ExtensionAuto = (builder) => {
builder.use(FoldingHeadingSpecs);

builder.addAction(action, () => toggleHeadingFoldingAction);
builder.addInputRules(({schema}) => ({rules: [foldingHeadingRule(headingType(schema), 6)]}));
builder.addKeymap(
() => ({
Enter: openHeadingAndCreateParagraphAfterIfCursorAtEndOfHeading,
Backspace: removeFoldingIfCursorAtStartOfHeading,
}),
builder.Priority.High,
);
builder.addPlugin(foldingPlugin);
};

declare global {
namespace WysiwygEditor {
interface Actions {
[action]: Action;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {builders} from 'prosemirror-test-builder';

import {createMarkupChecker} from '../../../../../tests/sameMarkup';
import {ExtensionsManager} from '../../../../core';
import {BaseNode, BaseSchemaSpecs} from '../../../base/specs';
import {ItalicSpecs, headingNodeName, italicMarkName} from '../../../markdown/specs';
import {YfmHeadingAttr, YfmHeadingSpecs} from '../../../yfm/specs';

import {FoldingHeadingSpecs} from './FoldingHeadingSpecs';

const {schema, markupParser, serializer} = new ExtensionsManager({
extensions: (builder) =>
builder
.use(BaseSchemaSpecs, {})
.use(ItalicSpecs)
.use(YfmHeadingSpecs, {})
.use(FoldingHeadingSpecs),
}).buildDeps();

const {doc, h1, h2, h3, h4, h5, h6, i} = builders<
'doc' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6',
'i'
>(schema, {
doc: {nodeType: BaseNode.Doc},
h1: {nodeType: headingNodeName, [YfmHeadingAttr.Level]: 1},
h2: {nodeType: headingNodeName, [YfmHeadingAttr.Level]: 2},
h3: {nodeType: headingNodeName, [YfmHeadingAttr.Level]: 3},
h4: {nodeType: headingNodeName, [YfmHeadingAttr.Level]: 4},
h5: {nodeType: headingNodeName, [YfmHeadingAttr.Level]: 5},
h6: {nodeType: headingNodeName, [YfmHeadingAttr.Level]: 6},
i: {markType: italicMarkName},
});

const {same} = createMarkupChecker({parser: markupParser, serializer});

describe('Folding Headings', () => {
it('should parse folding headings', () => {
const markup = `
#+ heading 1
##+ heading 2
###+ heading 3
####+ heading 4
#####+ heading 5
######+ heading 6
`.trim();

return same(
markup,
doc(
h1({[YfmHeadingAttr.Folding]: true}, 'heading 1'),
h2({[YfmHeadingAttr.Folding]: true}, 'heading 2'),
h3({[YfmHeadingAttr.Folding]: true}, 'heading 3'),
h4({[YfmHeadingAttr.Folding]: true}, 'heading 4'),
h5({[YfmHeadingAttr.Folding]: true}, 'heading 5'),
h6({[YfmHeadingAttr.Folding]: true}, 'heading 6'),
),
);
});

it('should parse common headings', () => {
const markup = `
# heading 1
## heading 2
### heading 3
#### heading 4
##### heading 5
###### heading 6
`.trim();

return same(
markup,
doc(
h1('heading 1'),
h2('heading 2'),
h3('heading 3'),
h4('heading 4'),
h5('heading 5'),
h6('heading 6'),
),
);
});

it('should parse folding heading with inline markup', () => {
const markup = `
##+ *heading* 2
`.trim();

return same(markup, doc(h2({[YfmHeadingAttr.Folding]: true}, i('heading'), ' 2')));
});
});
Loading

0 comments on commit b904704

Please sign in to comment.