diff --git a/demo/defaults/md-plugins.ts b/demo/defaults/md-plugins.ts
index 5d2a1bf4..dceedc7b 100644
--- a/demo/defaults/md-plugins.ts
+++ b/demo/defaults/md-plugins.ts
@@ -35,11 +35,16 @@ type GetPluginsOptions = {
directiveSyntax?: RenderPreviewParams['directiveSyntax'];
};
-export function getPlugins(_opts: GetPluginsOptions = {}): markdownit.PluginWithParams[] {
+export function getPlugins({
+ directiveSyntax,
+}: GetPluginsOptions = {}): markdownit.PluginWithParams[] {
const defaultPlugins: PluginWithParams[] = [
anchors,
code,
- yfmCut({bundle: false}),
+ yfmCut({
+ bundle: false,
+ directiveSyntax: directiveSyntax?.mdPluginValueFor('yfmCut'),
+ }),
deflist,
file,
(md) => md.use(imsize, {enableInlineStyling: true}),
diff --git a/package-lock.json b/package-lock.json
index d7776f49..00ee6a54 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -52,7 +52,7 @@
"tslib": "^2.3.1"
},
"devDependencies": {
- "@diplodoc/cut-extension": "^0.3.1",
+ "@diplodoc/cut-extension": "^0.4.0",
"@diplodoc/folding-headings-extension": "0.1.0",
"@diplodoc/html-extension": "2.3.2",
"@diplodoc/latex-extension": "1.0.3",
@@ -110,11 +110,12 @@
"sass": "^1.64.1",
"sass-loader": "^13.3.2",
"stylelint": "15.11.0",
+ "ts-dedent": "2.2.0",
"ts-jest": "^27.0.7",
"typescript": "^4.5.2"
},
"peerDependencies": {
- "@diplodoc/cut-extension": "^0.3.1",
+ "@diplodoc/cut-extension": "^0.3.1 || ^0.4.0",
"@diplodoc/folding-headings-extension": "^0.1.0",
"@diplodoc/html-extension": "2.3.2",
"@diplodoc/latex-extension": "^1.0.3",
@@ -2517,15 +2518,27 @@
}
},
"node_modules/@diplodoc/cut-extension": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@diplodoc/cut-extension/-/cut-extension-0.3.1.tgz",
- "integrity": "sha512-Em17/XxXm7V8xgaayaMqf0Yek8Rgr3lko6YbX2Z80eDLHjr7wD+C1latPo/HwZ5OS3NUjEuaM+NgIfbU0uUQgA==",
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@diplodoc/cut-extension/-/cut-extension-0.4.0.tgz",
+ "integrity": "sha512-0H8bNRNkdXBej/gMuitHO+WyQd+vP3mSM4Ixpt2HMta+JzmQYjacFV+uxMa4Tkbzspeo6C6HAwbkvNTmedMRUw==",
"dev": true,
+ "dependencies": {
+ "@diplodoc/directive": "^0.1.0"
+ },
"peerDependencies": {
"@diplodoc/utils": "1.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/@diplodoc/directive": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@diplodoc/directive/-/directive-0.1.0.tgz",
+ "integrity": "sha512-wQ4RoCoCjbWD9r2YdVPlgTW8+a0JNmsh4OmRLq4EA5PJ1SLjUKqSP9HkHb4LjRINLFtac6SsbXtQ8Y/zUiYazA==",
+ "dev": true,
+ "dependencies": {
+ "markdown-it-directive": "2.0.2"
+ }
+ },
"node_modules/@diplodoc/folding-headings-extension": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@diplodoc/folding-headings-extension/-/folding-headings-extension-0.1.0.tgz",
@@ -2640,6 +2653,16 @@
}
}
},
+ "node_modules/@diplodoc/transform/node_modules/@diplodoc/cut-extension": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@diplodoc/cut-extension/-/cut-extension-0.3.2.tgz",
+ "integrity": "sha512-55AgVEIiy3GHorZRht3dm1AcFWdgCMAn8yGV/8qp8sPQ4LssPPuCrVzCVFMn7o8d3KZbEDt5dDj6kI1KtVaWQg==",
+ "dev": true,
+ "peerDependencies": {
+ "@diplodoc/utils": "1.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/@diplodoc/transform/node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
diff --git a/package.json b/package.json
index f32f31dc..806ee467 100644
--- a/package.json
+++ b/package.json
@@ -200,7 +200,7 @@
"tslib": "^2.3.1"
},
"devDependencies": {
- "@diplodoc/cut-extension": "^0.3.1",
+ "@diplodoc/cut-extension": "^0.4.0",
"@diplodoc/folding-headings-extension": "0.1.0",
"@diplodoc/html-extension": "2.3.2",
"@diplodoc/latex-extension": "1.0.3",
@@ -258,6 +258,7 @@
"sass": "^1.64.1",
"sass-loader": "^13.3.2",
"stylelint": "15.11.0",
+ "ts-dedent": "2.2.0",
"ts-jest": "^27.0.7",
"typescript": "^4.5.2"
},
@@ -282,7 +283,7 @@
}
},
"peerDependencies": {
- "@diplodoc/cut-extension": "^0.3.1",
+ "@diplodoc/cut-extension": "^0.3.1 || ^0.4.0",
"@diplodoc/folding-headings-extension": "^0.1.0",
"@diplodoc/html-extension": "2.3.2",
"@diplodoc/latex-extension": "^1.0.3",
diff --git a/src/extensions/yfm/YfmCut/YfmCut.test.ts b/src/extensions/yfm/YfmCut/YfmCut.test.ts
index be949698..ad711d4e 100644
--- a/src/extensions/yfm/YfmCut/YfmCut.test.ts
+++ b/src/extensions/yfm/YfmCut/YfmCut.test.ts
@@ -1,8 +1,10 @@
import {builders} from 'prosemirror-test-builder';
+import dd from 'ts-dedent';
import {parseDOM} from '../../../../tests/parse-dom';
import {createMarkupChecker} from '../../../../tests/sameMarkup';
import {ExtensionsManager} from '../../../core';
+import {DirectiveSyntaxContext, type DirectiveSyntaxOption} from '../../../utils/directive';
import {BaseNode, BaseSchemaSpecs} from '../../base/specs';
import {
BlockquoteSpecs,
@@ -14,21 +16,34 @@ import {
italicMarkName,
} from '../../markdown/specs';
-import {CutNode, YfmCutSpecs} from './YfmCutSpecs';
-
-const {
- schema,
- markupParser: parser,
- serializer,
-} = new ExtensionsManager({
- extensions: (builder) =>
- builder
- .use(BaseSchemaSpecs, {})
- .use(ItalicSpecs)
- .use(BlockquoteSpecs)
- .use(YfmCutSpecs, {})
- .use(ImageSpecs),
-}).buildDeps();
+import {CutAttr, CutNode, YfmCutSpecs} from './YfmCutSpecs';
+
+class DirectiveContext extends DirectiveSyntaxContext {
+ setOption(option: DirectiveSyntaxOption | undefined) {
+ this.option = option;
+ }
+}
+const directiveContext = new DirectiveContext(undefined);
+
+function buildDeps() {
+ return new ExtensionsManager({
+ extensions: (builder) => {
+ builder.context.set('directiveSyntax', directiveContext);
+ builder
+ .use(BaseSchemaSpecs, {})
+ .use(ItalicSpecs)
+ .use(BlockquoteSpecs)
+ .use(YfmCutSpecs, {})
+ .use(ImageSpecs);
+ },
+ }).buildDeps();
+}
+function buildCheckers() {
+ const {markupParser, serializer} = buildDeps();
+ return createMarkupChecker({parser: markupParser, serializer});
+}
+
+const {schema, markupParser: parser, serializer} = buildDeps();
const {doc, p, i, bq, img, cut, cutTitle, cutContent} = builders<
'doc' | 'p' | 'bq' | 'img' | 'cut' | 'cutTitle' | 'cutContent',
@@ -48,83 +63,117 @@ const {same} = createMarkupChecker({parser, serializer});
describe('YfmCut extension', () => {
it('should parse yfm-cut', () => {
- const markup = `
-{% cut "cut title" %}
+ const markup = dd`
+ {% cut "cut title" %}
-cut content
+ cut content
-cut content 2
+ cut content 2
-{% endcut %}
-`.trim();
+ {% endcut %}
+ `.trim();
same(
markup,
- doc(cut(cutTitle('cut title'), cutContent(p('cut content'), p('cut content 2')))),
+ doc(
+ cut(
+ {[CutAttr.Markup]: '{%'},
+ cutTitle('cut title'),
+ cutContent(p('cut content'), p('cut content 2')),
+ ),
+ ),
);
});
it('should parse nested yfm-cuts', () => {
- const markup = `
-{% cut "cut title" %}
+ const markup = dd`
+ {% cut "cut title" %}
-{% cut "cut title 2" %}
+ {% cut "cut title 2" %}
-cut content
+ cut content
-{% endcut %}
+ {% endcut %}
-{% endcut %}
-`.trim();
+ {% endcut %}
+ `.trim();
same(
markup,
doc(
cut(
+ {[CutAttr.Markup]: '{%'},
cutTitle('cut title'),
- cutContent(cut(cutTitle('cut title 2'), cutContent(p('cut content')))),
+ cutContent(
+ cut(
+ {[CutAttr.Markup]: '{%'},
+ cutTitle('cut title 2'),
+ cutContent(p('cut content')),
+ ),
+ ),
),
),
);
});
it('should parse yfm-cut under blockquote', () => {
- const markup = `
-> {% cut "cut title" %}
->
-> cut content
->
-> {% endcut %}
-`.trim();
-
- same(markup, doc(bq(cut(cutTitle('cut title'), cutContent(p('cut content'))))));
+ const markup = dd`
+ > {% cut "cut title" %}
+ >
+ > cut content
+ >
+ > {% endcut %}
+ `.trim();
+
+ same(
+ markup,
+ doc(
+ bq(
+ cut(
+ {[CutAttr.Markup]: '{%'},
+ cutTitle('cut title'),
+ cutContent(p('cut content')),
+ ),
+ ),
+ ),
+ );
});
it('should parse yfm-cut with inline markup in cut title', () => {
- const markup = `
-{% cut "*cut italic title*" %}
+ const markup = dd`
+ {% cut "*cut italic title*" %}
-cut content
+ cut content
-{% endcut %}
- `.trim();
+ {% endcut %}
+ `.trim();
- same(markup, doc(cut(cutTitle(i('cut italic title')), cutContent(p('cut content')))));
+ same(
+ markup,
+ doc(
+ cut(
+ {[CutAttr.Markup]: '{%'},
+ cutTitle(i('cut italic title')),
+ cutContent(p('cut content')),
+ ),
+ ),
+ );
});
it('should parse yfm-cut with inline node in cut title', () => {
- const markup = `
-{% cut "![img](path/to/img)" %}
+ const markup = dd`
+ {% cut "![img](path/to/img)" %}
-cut content
+ cut content
-{% endcut %}
- `.trim();
+ {% endcut %}
+ `.trim();
same(
markup,
doc(
cut(
+ {[CutAttr.Markup]: '{%'},
cutTitle(
img({
[ImageAttr.Src]: 'path/to/img',
@@ -137,7 +186,7 @@ cut content
);
});
- it('should parse yfm-note from html', () => {
+ it('should parse yfm-cut from html', () => {
parseDOM(
schema,
'
' +
@@ -147,4 +196,214 @@ cut content
doc(cut(cutTitle('YfmCut title'), cutContent(p('YfmCut content')))),
);
});
+
+ it('should parse yfm-cut with markup attr from html', () => {
+ parseDOM(
+ schema,
+ '
' +
+ '
YfmCut title
' +
+ '
',
+ doc(
+ cut(
+ {[CutAttr.Markup]: '{%'},
+ cutTitle('YfmCut title'),
+ cutContent(p('YfmCut content')),
+ ),
+ ),
+ );
+ });
+
+ describe('Directive syntax', () => {
+ afterAll(() => {
+ directiveContext.setOption(undefined);
+ });
+
+ const PM_DOC = {
+ CurlyCut: doc(
+ cut({[CutAttr.Markup]: '{%'}, cutTitle('title'), cutContent(p('content'))),
+ ),
+ UnknownCut: doc(cut(cutTitle('title'), cutContent(p('content')))),
+ DirectiveCut: doc(
+ cut({[CutAttr.Markup]: ':::cut'}, cutTitle('title'), cutContent(p('content'))),
+ ),
+ };
+ const MARKUP = {
+ CurlySyntax: dd`
+ {% cut "title" %}
+
+ content
+
+ {% endcut %}
+ `,
+
+ DirectiveSyntax: dd`
+ :::cut [title]
+ content
+
+ :::
+ `,
+ };
+
+ describe('directiveSyntax:disabled', () => {
+ beforeAll(() => {
+ directiveContext.setOption('disabled');
+ });
+
+ it('should parse curly cut syntax', () => {
+ const {parse} = buildCheckers();
+ parse(MARKUP.CurlySyntax, PM_DOC.CurlyCut, {json: true});
+ });
+
+ it('should not parse directive cut syntax', () => {
+ const {parse} = buildCheckers();
+ parse(MARKUP.DirectiveSyntax, doc(p(':::cut [title]\ncontent'), p(':::')), {
+ json: true,
+ });
+ });
+
+ it('should preserve curly cut syntax', () => {
+ const {serialize} = buildCheckers();
+ serialize(PM_DOC.CurlyCut, MARKUP.CurlySyntax);
+ });
+
+ it('should serialize cut with unknown markup to curly syntax', () => {
+ const {serialize} = buildCheckers();
+ serialize(PM_DOC.UnknownCut, MARKUP.CurlySyntax);
+ });
+
+ it('should preserve directive cut to curly syntax', () => {
+ const {serialize} = buildCheckers();
+ serialize(PM_DOC.DirectiveCut, MARKUP.DirectiveSyntax);
+ });
+ });
+
+ describe('directiveSyntax:enabled', () => {
+ beforeAll(() => {
+ directiveContext.setOption('enabled');
+ });
+
+ it('should parse curly cut syntax', () => {
+ const {parse} = buildCheckers();
+ parse(MARKUP.CurlySyntax, PM_DOC.CurlyCut, {json: true});
+ });
+
+ it('should parse directive cut syntax', () => {
+ const {parse} = buildCheckers();
+ parse(MARKUP.DirectiveSyntax, PM_DOC.DirectiveCut, {json: true});
+ });
+
+ it('should preserve curly cut syntax', () => {
+ const {serialize} = buildCheckers();
+ serialize(PM_DOC.CurlyCut, MARKUP.CurlySyntax);
+ });
+
+ it('should serialize cut with unknown markup to curly syntax', () => {
+ const {serialize} = buildCheckers();
+ serialize(PM_DOC.UnknownCut, MARKUP.CurlySyntax);
+ });
+
+ it('should preserve directive cut syntax', () => {
+ const {serialize} = buildCheckers();
+ serialize(PM_DOC.DirectiveCut, MARKUP.DirectiveSyntax);
+ });
+ });
+
+ describe('directiveSyntax:preserve', () => {
+ beforeAll(() => {
+ directiveContext.setOption('preserve');
+ });
+
+ it('should parse curly cut syntax', () => {
+ const {parse} = buildCheckers();
+ parse(MARKUP.CurlySyntax, PM_DOC.CurlyCut, {json: true});
+ });
+
+ it('should parse directive cut syntax', () => {
+ const {parse} = buildCheckers();
+ parse(MARKUP.DirectiveSyntax, PM_DOC.DirectiveCut, {json: true});
+ });
+
+ it('should preserve curly cut syntax', () => {
+ const {serialize} = buildCheckers();
+ serialize(PM_DOC.CurlyCut, MARKUP.CurlySyntax);
+ });
+
+ it('should serialize cut with unknown markup to directive syntax', () => {
+ const {serialize} = buildCheckers();
+ serialize(PM_DOC.UnknownCut, MARKUP.DirectiveSyntax);
+ });
+
+ it('should preserve directive cut syntax', () => {
+ const {serialize} = buildCheckers();
+ serialize(PM_DOC.DirectiveCut, MARKUP.DirectiveSyntax);
+ });
+ });
+
+ describe('directiveSyntax:overwrite', () => {
+ beforeAll(() => {
+ directiveContext.setOption('overwrite');
+ });
+
+ it('should parse curly cut syntax', () => {
+ const {parse} = buildCheckers();
+ parse(MARKUP.CurlySyntax, PM_DOC.CurlyCut, {json: true});
+ });
+
+ it('should parse directive cut syntax', () => {
+ const {parse} = buildCheckers();
+ parse(MARKUP.DirectiveSyntax, PM_DOC.DirectiveCut, {json: true});
+ });
+
+ it('should overwrite curly cut to directive syntax', () => {
+ const {serialize} = buildCheckers();
+ serialize(PM_DOC.CurlyCut, MARKUP.DirectiveSyntax);
+ });
+
+ it('should serialize cut with unknown markup to directive syntax', () => {
+ const {serialize} = buildCheckers();
+ serialize(PM_DOC.UnknownCut, MARKUP.DirectiveSyntax);
+ });
+
+ it('should preserve directive cut syntax', () => {
+ const {serialize} = buildCheckers();
+ serialize(PM_DOC.DirectiveCut, MARKUP.DirectiveSyntax);
+ });
+ });
+
+ describe('directiveSyntax:only', () => {
+ beforeAll(() => {
+ directiveContext.setOption('only');
+ });
+
+ it('should not parse curly cut syntax', () => {
+ const {parse} = buildCheckers();
+ parse(
+ MARKUP.CurlySyntax,
+ doc(p('{% cut "title" %}'), p('content'), p('{% endcut %}')),
+ {json: true},
+ );
+ });
+
+ it('should parse directive cut syntax', () => {
+ const {parse} = buildCheckers();
+ parse(MARKUP.DirectiveSyntax, PM_DOC.DirectiveCut, {json: true});
+ });
+
+ it('should overwrite curly cut to directive syntax', () => {
+ const {serialize} = buildCheckers();
+ serialize(PM_DOC.CurlyCut, MARKUP.DirectiveSyntax);
+ });
+
+ it('should serialize cut with unknown markup to directive syntax', () => {
+ const {serialize} = buildCheckers();
+ serialize(PM_DOC.UnknownCut, MARKUP.DirectiveSyntax);
+ });
+
+ it('should preserve directive cut syntax', () => {
+ const {serialize} = buildCheckers();
+ serialize(PM_DOC.DirectiveCut, MARKUP.DirectiveSyntax);
+ });
+ });
+ });
});
diff --git a/src/extensions/yfm/YfmCut/YfmCutSpecs/const.ts b/src/extensions/yfm/YfmCut/YfmCutSpecs/const.ts
index cbe301d4..2099408e 100644
--- a/src/extensions/yfm/YfmCut/YfmCutSpecs/const.ts
+++ b/src/extensions/yfm/YfmCut/YfmCutSpecs/const.ts
@@ -6,6 +6,11 @@ export enum CutNode {
CutContent = 'yfm_cut_content',
}
+export enum CutAttr {
+ Class = 'class',
+ Markup = 'data-markup',
+}
+
export const cutType = nodeTypeFactory(CutNode.Cut);
export const cutTitleType = nodeTypeFactory(CutNode.CutTitle);
export const cutContentType = nodeTypeFactory(CutNode.CutContent);
diff --git a/src/extensions/yfm/YfmCut/YfmCutSpecs/index.ts b/src/extensions/yfm/YfmCut/YfmCutSpecs/index.ts
index 80886df9..8367d1b4 100644
--- a/src/extensions/yfm/YfmCut/YfmCutSpecs/index.ts
+++ b/src/extensions/yfm/YfmCut/YfmCutSpecs/index.ts
@@ -6,9 +6,18 @@ import type {ExtensionAuto, ExtensionNodeSpec} from '../../../../core';
import {CutNode} from './const';
import {parserTokens} from './parser';
import {getSchemaSpecs} from './schema';
-import {serializerTokens} from './serializer';
+import {getSerializerTokens} from './serializer';
-export {CutNode, cutType, cutTitleType, cutContentType} from './const';
+export {CutAttr, CutNode, cutType, cutTitleType, cutContentType} from './const';
+
+declare global {
+ namespace MarkdownEditor {
+ interface DirectiveSyntaxAdditionalSupportedExtensions {
+ // Mark in global types that YfmCut has support for directive syntax
+ yfmCut: true;
+ }
+ }
+}
export type YfmCutSpecsOptions = {
cutView?: ExtensionNodeSpec['view'];
@@ -26,9 +35,18 @@ export type YfmCutSpecsOptions = {
export const YfmCutSpecs: ExtensionAuto
= (builder, opts) => {
const schemaSpecs = getSchemaSpecs(opts, builder.context.get('placeholder'));
+ const directiveSyntax = builder.context.get('directiveSyntax');
+ const serializerTokens = getSerializerTokens({directiveSyntax});
builder
- .configureMd((md) => md.use(yfmCut({bundle: false})))
+ .configureMd((md) =>
+ md.use(
+ yfmCut({
+ bundle: false,
+ directiveSyntax: directiveSyntax?.mdPluginValueFor('yfmCut'),
+ }),
+ ),
+ )
.addNode(CutNode.Cut, () => ({
spec: schemaSpecs[CutNode.Cut],
toMd: serializerTokens[CutNode.Cut],
diff --git a/src/extensions/yfm/YfmCut/YfmCutSpecs/parser.ts b/src/extensions/yfm/YfmCut/YfmCutSpecs/parser.ts
index 883cac3f..fc4ab683 100644
--- a/src/extensions/yfm/YfmCut/YfmCutSpecs/parser.ts
+++ b/src/extensions/yfm/YfmCut/YfmCutSpecs/parser.ts
@@ -1,8 +1,12 @@
import type {ParserToken} from '../../../../core';
-import {CutNode} from './const';
+import {CutAttr, CutNode} from './const';
-const getAttrs: ParserToken['getAttrs'] = (tok) => (tok.attrs ? Object.fromEntries(tok.attrs) : {});
+const getAttrs: ParserToken['getAttrs'] = (tok) => {
+ const nodeAttrs = tok.attrs ? Object.fromEntries(tok.attrs) : {};
+ nodeAttrs[CutAttr.Markup] = tok.markup;
+ return nodeAttrs;
+};
export const parserTokens: Record = {
[CutNode.Cut]: {name: CutNode.Cut, type: 'block', getAttrs},
diff --git a/src/extensions/yfm/YfmCut/YfmCutSpecs/schema.ts b/src/extensions/yfm/YfmCut/YfmCutSpecs/schema.ts
index 3789f1ac..018bff13 100644
--- a/src/extensions/yfm/YfmCut/YfmCutSpecs/schema.ts
+++ b/src/extensions/yfm/YfmCut/YfmCutSpecs/schema.ts
@@ -1,7 +1,8 @@
import type {NodeSpec} from 'prosemirror-model';
import type {PlaceholderOptions} from '../../../../utils/placeholder';
-import {CutNode} from '../const';
+
+import {CutAttr, CutNode} from './const';
import type {YfmCutSpecsOptions} from './index';
@@ -15,10 +16,15 @@ export const getSchemaSpecs = (
placeholder?: PlaceholderOptions,
): Record => ({
[CutNode.Cut]: {
- attrs: {class: {default: 'yfm-cut'}},
+ attrs: {class: {default: 'yfm-cut'}, [CutAttr.Markup]: {default: null}},
content: `${CutNode.CutTitle} ${CutNode.CutContent}`,
group: 'block yfm-cut',
- parseDOM: [{tag: '.yfm-cut'}],
+ parseDOM: [
+ {
+ tag: '.yfm-cut',
+ getAttrs: (node) => ({[CutAttr.Markup]: node.getAttribute(CutAttr.Markup)}),
+ },
+ ],
toDOM(node) {
return ['div', node.attrs, 0];
},
diff --git a/src/extensions/yfm/YfmCut/YfmCutSpecs/serializer.ts b/src/extensions/yfm/YfmCut/YfmCutSpecs/serializer.ts
index 54185024..b8071c18 100644
--- a/src/extensions/yfm/YfmCut/YfmCutSpecs/serializer.ts
+++ b/src/extensions/yfm/YfmCut/YfmCutSpecs/serializer.ts
@@ -1,27 +1,53 @@
+import type {Node} from 'prosemirror-model';
+
import type {SerializerNodeToken} from '../../../../core';
import {isNodeEmpty} from '../../../../utils/nodes';
import {getPlaceholderContent} from '../../../../utils/placeholder';
-import {CutNode} from './const';
-
-export const serializerTokens: Record = {
- [CutNode.Cut]: (state, node) => {
- state.renderContent(node);
- state.write('{% endcut %}');
- state.closeBlock(node);
- },
-
- [CutNode.CutTitle]: (state, node) => {
- state.write('{% cut "');
- if (node.nodeSize > 2) state.renderInline(node);
- else state.write(getPlaceholderContent(node));
- state.write('" %}\n');
- state.write('\n');
- state.closeBlock();
- },
-
- [CutNode.CutContent]: (state, node) => {
- if (!isNodeEmpty(node)) state.renderInline(node);
- else state.write(getPlaceholderContent(node) + '\n\n');
- },
-};
+import {CutAttr, CutNode} from './const';
+
+export function getSerializerTokens({
+ directiveSyntax,
+}: {
+ directiveSyntax?: WysiwygEditor.Context['directiveSyntax'];
+}): Record {
+ const isDirectiveCut = (node: Node): boolean | undefined => {
+ return directiveSyntax?.shouldSerializeToDirective('yfmCut', node.attrs[CutAttr.Markup]);
+ };
+
+ return {
+ [CutNode.Cut]: (state, node) => {
+ state.renderContent(node);
+ state.write(isDirectiveCut(node) ? ':::' : '{% endcut %}');
+ state.closeBlock(node);
+ },
+
+ [CutNode.CutTitle]: (state, node, parent) => {
+ if (isDirectiveCut(parent)) {
+ state.write(':::cut [');
+ state.renderInline(node);
+ state.write(']');
+ state.ensureNewLine();
+ state.closeBlock();
+ return;
+ }
+
+ state.write('{% cut "');
+ if (node.nodeSize > 2) state.renderInline(node);
+ else state.write(getPlaceholderContent(node));
+ state.write('" %}\n');
+ state.write('\n');
+ state.closeBlock();
+ },
+
+ [CutNode.CutContent]: (state, node, parent) => {
+ if (isDirectiveCut(parent)) {
+ state.renderContent(node);
+ return;
+ }
+
+ if (!isNodeEmpty(node)) state.renderInline(node);
+ else state.write(getPlaceholderContent(node) + '\n\n');
+ },
+ };
+}
diff --git a/src/markup/codemirror/yfm.ts b/src/markup/codemirror/yfm.ts
index 6e2aa1a0..d6259cbf 100644
--- a/src/markup/codemirror/yfm.ts
+++ b/src/markup/codemirror/yfm.ts
@@ -6,6 +6,8 @@ import type {DelimiterType, MarkdownConfig} from '@lezer/markdown';
import {capitalize} from '../../lodash';
+import {DirectiveSyntaxFacet} from './directive-facet';
+
export const customTags = {
underline: Tag.define(),
monospace: Tag.define(),
@@ -76,6 +78,9 @@ export const yfmNoteSnippets: Record> =
export const yfmCutSnippetTemplate = '{% cut "#{title}" %}\n\n#{}\n\n{% endcut %}\n\n';
export const yfmCutSnippet = snippet(yfmCutSnippetTemplate);
+export const yfmCutDirectiveSnippetTemplate = ':::cut [#{title}]\n#{}\n:::\n\n';
+export const yfmCutDirectiveSnippet = snippet(yfmCutDirectiveSnippetTemplate);
+
export interface LanguageData {
autocomplete: CompletionSource;
[key: string]: any;
@@ -87,6 +92,8 @@ export interface YfmLangOptions {
const mdAutocomplete: LanguageData = {
autocomplete: (context) => {
+ const directiveContext = context.state.facet(DirectiveSyntaxFacet);
+
// TODO: add more actions and re-enable
// let word = context.matchBefore(/\/.*/);
// if (word) {
@@ -104,13 +111,16 @@ const mdAutocomplete: LanguageData = {
// label: '/yfm cut',
// displayLabel: 'YFM Cut',
// type: 'text',
- // apply: yfmCutSnippet,
+ // apply: directiveFacet.shouldInsertDirectiveMarkup('yfmCut')
+ // ? yfmCutDirectiveSnippet
+ // : yfmCutSnippet,
// },
// ],
// };
// }
+
const word = context.matchBefore(/^.*/);
- if (word?.text.startsWith('{%')) {
+ if (directiveContext.option !== 'only' && word?.text.startsWith('{%')) {
return {
from: word.from,
options: [
@@ -126,11 +136,29 @@ const mdAutocomplete: LanguageData = {
label: '{% cut',
displayLabel: 'YFM Cut',
type: 'text',
- apply: yfmCutSnippet,
+ apply: directiveContext.shouldInsertDirectiveMarkup('yfmCut')
+ ? yfmCutDirectiveSnippet
+ : yfmCutSnippet,
},
],
};
}
+ if (directiveContext.option !== 'disabled' && word?.text.startsWith(':')) {
+ const options: Completion[] = [];
+
+ if (directiveContext.valueFor('yfmCut') !== 'disabled') {
+ options.push({
+ label: ':::cut',
+ displayLabel: 'YFM Cut',
+ type: 'text',
+ apply: yfmCutDirectiveSnippet,
+ });
+ }
+
+ if (options.length) {
+ return {from: word.from, options};
+ }
+ }
return null;
},
};
diff --git a/src/markup/commands/yfm.ts b/src/markup/commands/yfm.ts
index b8a7e890..a1902955 100644
--- a/src/markup/commands/yfm.ts
+++ b/src/markup/commands/yfm.ts
@@ -1,11 +1,24 @@
import type {StateCommand} from '@codemirror/state';
+import {DirectiveSyntaxFacet} from '../codemirror/directive-facet';
+
import {wrapToBlock} from './helpers';
-export const wrapToYfmCut: StateCommand = wrapToBlock(
+const wrapToYfmCutCurly: StateCommand = wrapToBlock(
({lineBreak}) => '{% cut "title" %}' + lineBreak.repeat(2),
({lineBreak}) => lineBreak.repeat(2) + '{% endcut %}',
);
+const wrapToYfmCutDirective: StateCommand = wrapToBlock(
+ ({lineBreak}) => ':::cut [title]' + lineBreak,
+ ({lineBreak}) => lineBreak + ':::',
+);
+
+export const wrapToYfmCut: StateCommand = (target) => {
+ const cmd = target.state.facet(DirectiveSyntaxFacet).shouldInsertDirectiveMarkup('yfmCut')
+ ? wrapToYfmCutDirective
+ : wrapToYfmCutCurly;
+ return cmd(target);
+};
export const wrapToYfmNote = wrapToBlock(
({lineBreak}) => '{% note info %}' + lineBreak.repeat(2),
diff --git a/tests/sameMarkup.ts b/tests/sameMarkup.ts
index fb6b1c6e..02c67d6f 100644
--- a/tests/sameMarkup.ts
+++ b/tests/sameMarkup.ts
@@ -1,7 +1,8 @@
/* eslint-disable no-implicit-globals */
import type {Node} from 'prosemirror-model';
-import {Parser, Serializer} from '../src/core';
+
+import type {Parser, Serializer} from '../src/core';
export function createMarkupChecker({
parser,
@@ -10,8 +11,9 @@ export function createMarkupChecker({
parser: Parser;
serializer: Serializer;
}) {
- function parse(text: string, doc: Node) {
- expect(parser.parse(text)).toMatchNode(doc);
+ function parse(text: string, doc: Node, {json}: {json?: boolean} = {}) {
+ if (json) expect(parser.parse(text)).toMatchNodeJson(doc);
+ else expect(parser.parse(text)).toMatchNode(doc);
}
function serialize(doc: Node, text: string) {
diff --git a/tests/toMatchNode.ts b/tests/toMatchNode.ts
index 1a4bceeb..e4d5a457 100644
--- a/tests/toMatchNode.ts
+++ b/tests/toMatchNode.ts
@@ -15,12 +15,22 @@ expect.extend({
pass: eq(received, expect),
};
},
+ toMatchNodeJson: (received: Node, expect: Node) => {
+ return {
+ message: () =>
+ `nodes do not match.\n\nreceived: ${received}\n\tjson: ${toJson(
+ received,
+ )}\n\nexpect: ${expect}\n\tjson: ${toJson(expect)}`,
+ pass: toJson(received) === toJson(expect),
+ };
+ },
});
declare global {
namespace jest {
interface Matchers {
toMatchNode(expect: Node): R;
+ toMatchNodeJson(expect: Node): R;
}
}
}