From 38064ac7f156cc8bf1d283b0b522818db4d73f71 Mon Sep 17 00:00:00 2001 From: Yuriy Demidov Date: Mon, 27 Feb 2023 13:28:11 +0300 Subject: [PATCH] feat: move schema, parser and serializer specs to separate extensions (#80) --- .../base/BaseSchema/BaseSchema.test.ts | 4 +- .../base/BaseSchema/BaseSchemaSpecs/index.ts | 61 ++++++++ src/extensions/base/BaseSchema/index.ts | 58 +------- src/extensions/base/specs.ts | 13 ++ src/extensions/behavior/Placeholder/index.ts | 7 +- .../markdown/Blockquote/Blockquote.test.ts | 8 +- .../Blockquote/BlockquoteSpecs/index.ts | 23 ++++ src/extensions/markdown/Blockquote/const.ts | 9 +- src/extensions/markdown/Blockquote/index.ts | 26 +--- src/extensions/markdown/Bold/Bold.test.ts | 8 +- .../markdown/Bold/BoldSpecs/index.ts | 30 ++++ src/extensions/markdown/Bold/index.ts | 40 ++---- src/extensions/markdown/Breaks/Breaks.test.ts | 8 +- .../markdown/Breaks/BreaksSpecs/index.ts | 57 ++++++++ src/extensions/markdown/Breaks/index.ts | 54 +------- src/extensions/markdown/Code/Code.test.ts | 16 +-- .../markdown/Code/CodeSpecs/index.ts | 49 +++++++ src/extensions/markdown/Code/index.ts | 52 +------ .../markdown/CodeBlock/CodeBlock.test.ts | 27 ++-- .../CodeBlock/CodeBlockSpecs/index.ts | 68 +++++++++ src/extensions/markdown/CodeBlock/const.ts | 8 +- .../markdown/CodeBlock/handle-paste.ts | 7 +- src/extensions/markdown/CodeBlock/index.ts | 65 +-------- .../markdown/Deflist/Deflist.test.ts | 7 +- .../markdown/Deflist/DeflistSpecs/const.ts | 5 + .../Deflist/{ => DeflistSpecs}/fromYfm.ts | 2 +- .../markdown/Deflist/DeflistSpecs/index.ts | 42 ++++++ .../Deflist/{ => DeflistSpecs}/spec.ts | 8 +- .../Deflist/{ => DeflistSpecs}/toYfm.ts | 2 +- src/extensions/markdown/Deflist/const.ts | 6 +- src/extensions/markdown/Deflist/index.ts | 32 +---- src/extensions/markdown/Deflist/utils.ts | 14 +- .../markdown/Heading/Heading.test.ts | 28 ++-- .../markdown/Heading/HeadingSpecs/index.ts | 53 +++++++ src/extensions/markdown/Heading/actions.ts | 4 +- src/extensions/markdown/Heading/const.ts | 3 +- src/extensions/markdown/Heading/index.ts | 50 +------ src/extensions/markdown/Heading/utils.ts | 11 +- .../HorizontalRule/HorizontalRule.test.ts | 16 ++- .../HorizontalRuleSpecs/index.ts | 31 +++++ .../markdown/HorizontalRule/index.ts | 53 ++++--- src/extensions/markdown/Html/index.ts | 2 + src/extensions/markdown/Image/Image.test.ts | 11 +- .../markdown/Image/ImageSpecs/index.ts | 62 +++++++++ src/extensions/markdown/Image/actions.ts | 4 +- src/extensions/markdown/Image/const.ts | 8 +- src/extensions/markdown/Image/index.ts | 56 +------- src/extensions/markdown/Image/utils.ts | 4 - src/extensions/markdown/Italic/Italic.test.ts | 8 +- .../markdown/Italic/ItalicSpecs/index.ts | 22 +++ src/extensions/markdown/Italic/index.ts | 33 ++--- src/extensions/markdown/Link/Link.test.ts | 12 +- .../markdown/Link/LinkSpecs/index.ts | 98 +++++++++++++ src/extensions/markdown/Link/actions.ts | 2 +- src/extensions/markdown/Link/index.ts | 101 ++------------ src/extensions/markdown/Lists/Lists.test.ts | 7 +- .../markdown/Lists/ListsSpecs/const.ts | 5 + .../Lists/{ => ListsSpecs}/fromYfm.ts | 2 +- .../markdown/Lists/ListsSpecs/index.ts | 30 ++++ .../markdown/Lists/{ => ListsSpecs}/spec.ts | 2 +- .../markdown/Lists/{ => ListsSpecs}/toYfm.ts | 2 +- .../markdown/Lists/commands.test.ts | 19 +-- src/extensions/markdown/Lists/const.ts | 6 +- src/extensions/markdown/Lists/index.ts | 28 +--- src/extensions/markdown/Lists/utils.ts | 7 +- src/extensions/markdown/Mark/Mark.test.ts | 8 +- .../markdown/Mark/MarkSpecs/index.ts | 32 +++++ src/extensions/markdown/Mark/index.ts | 38 ++--- src/extensions/markdown/Strike/Strike.test.ts | 8 +- .../markdown/Strike/StrikeSpecs/index.ts | 24 ++++ src/extensions/markdown/Strike/index.ts | 33 ++--- .../markdown/Subscript/Subscript.test.ts | 8 +- .../Subscript/SubscriptSpecs/index.ts | 24 ++++ src/extensions/markdown/Subscript/index.ts | 28 ++-- .../markdown/Superscript/Superscript.test.ts | 8 +- .../Superscript/SuperscriptSpecs/index.ts | 24 ++++ src/extensions/markdown/Superscript/index.ts | 29 ++-- src/extensions/markdown/Table/Table.test.ts | 10 +- .../markdown/Table/TableSpecs/const.ts | 18 +++ .../Table/{ => TableSpecs}/fromYfm.ts | 4 +- .../markdown/Table/TableSpecs/index.ts | 41 ++++++ .../markdown/Table/{ => TableSpecs}/spec.ts | 6 +- .../markdown/Table/{ => TableSpecs}/toYfm.ts | 4 +- src/extensions/markdown/Table/const.ts | 19 +-- src/extensions/markdown/Table/index.ts | 37 +---- .../markdown/Underline/Underline.test.ts | 8 +- .../Underline/UnderlineSpecs/index.ts | 23 ++++ src/extensions/markdown/Underline/index.ts | 29 ++-- src/extensions/markdown/specs.ts | 93 +++++++++++++ src/extensions/specs.ts | 3 + src/extensions/yfm/Checkbox/Checkbox.test.ts | 15 +- .../yfm/Checkbox/CheckboxSpecs/const.ts | 11 ++ .../Checkbox/{ => CheckboxSpecs}/fromYfm.ts | 4 +- .../yfm/Checkbox/CheckboxSpecs/index.ts | 53 +++++++ .../yfm/Checkbox/{ => CheckboxSpecs}/spec.ts | 14 +- .../yfm/Checkbox/{ => CheckboxSpecs}/toYfm.ts | 6 +- src/extensions/yfm/Checkbox/actions.test.ts | 2 +- src/extensions/yfm/Checkbox/const.ts | 6 +- src/extensions/yfm/Checkbox/index.ts | 130 +++++++----------- src/extensions/yfm/Checkbox/plugin.test.ts | 2 +- src/extensions/yfm/Checkbox/utils.ts | 7 +- src/extensions/yfm/Color/Color.test.ts | 11 +- src/extensions/yfm/Color/ColorSpecs/const.ts | 3 + src/extensions/yfm/Color/ColorSpecs/index.ts | 58 ++++++++ src/extensions/yfm/Color/const.ts | 5 +- src/extensions/yfm/Color/index.ts | 114 +++++---------- .../yfm/ImgSize/ImgSizeSpecs/index.ts | 94 +++++++++++++ src/extensions/yfm/ImgSize/YfmImage.test.ts | 40 +++--- src/extensions/yfm/ImgSize/actions.ts | 12 +- src/extensions/yfm/ImgSize/const.ts | 4 +- src/extensions/yfm/ImgSize/index.ts | 93 +------------ src/extensions/yfm/Math/Math.test.ts | 7 +- src/extensions/yfm/Math/MathSpecs/const.ts | 12 ++ src/extensions/yfm/Math/MathSpecs/index.ts | 57 ++++++++ src/extensions/yfm/Math/const.ts | 19 +-- src/extensions/yfm/Math/index.ts | 54 +------- .../yfm/Monospace/Monospace.test.ts | 8 +- .../yfm/Monospace/MonospaceSpecs/index.ts | 33 +++++ src/extensions/yfm/Monospace/index.ts | 39 ++---- src/extensions/yfm/Video/Video.test.ts | 30 ++-- src/extensions/yfm/Video/VideoSpecs/const.ts | 6 + src/extensions/yfm/Video/VideoSpecs/index.ts | 99 +++++++++++++ .../yfm/Video/{ => VideoSpecs}/md-video.ts | 0 .../yfm/Video/{ => VideoSpecs}/utils.ts | 8 +- src/extensions/yfm/Video/actions.ts | 10 +- src/extensions/yfm/Video/const.ts | 8 +- src/extensions/yfm/Video/index.ts | 100 +------------- src/extensions/yfm/YfmCut/YfmCut.test.ts | 18 ++- .../yfm/YfmCut/YfmCutSpecs/const.ts | 5 + .../yfm/YfmCut/{ => YfmCutSpecs}/fromYfm.ts | 2 +- .../yfm/YfmCut/YfmCutSpecs/index.ts | 54 ++++++++ .../yfm/YfmCut/{ => YfmCutSpecs}/spec.ts | 10 +- .../yfm/YfmCut/{ => YfmCutSpecs}/toYfm.ts | 6 +- src/extensions/yfm/YfmCut/commands.test.ts | 2 +- src/extensions/yfm/YfmCut/const.ts | 12 +- src/extensions/yfm/YfmCut/index.ts | 52 +++---- .../yfm/YfmDist/YfmDistSpecs/index.ts | 11 ++ src/extensions/yfm/YfmDist/index.ts | 12 +- src/extensions/yfm/YfmFile/YfmFile.test.ts | 9 +- .../yfm/YfmFile/{ => YfmFileSpecs}/const.ts | 3 + .../yfm/YfmFile/YfmFileSpecs/index.ts | 75 ++++++++++ src/extensions/yfm/YfmFile/index.ts | 80 +---------- .../yfm/YfmHeading/YfmHeading.test.ts | 26 ++-- .../yfm/YfmHeading/YfmHeadingSpecs/const.ts | 9 ++ .../yfm/YfmHeading/YfmHeadingSpecs/index.ts | 94 +++++++++++++ .../YfmHeading/{ => YfmHeadingSpecs}/utils.ts | 4 +- src/extensions/yfm/YfmHeading/actions.ts | 2 +- src/extensions/yfm/YfmHeading/commands.ts | 2 +- src/extensions/yfm/YfmHeading/const.ts | 10 +- src/extensions/yfm/YfmHeading/index.ts | 90 +----------- src/extensions/yfm/YfmNote/YfmNote.test.ts | 21 ++- .../yfm/YfmNote/YfmNoteSpecs/const.ts | 9 ++ .../yfm/YfmNote/{ => YfmNoteSpecs}/fromYfm.ts | 2 +- .../yfm/YfmNote/YfmNoteSpecs/index.ts | 37 +++++ .../yfm/YfmNote/{ => YfmNoteSpecs}/spec.ts | 7 +- .../yfm/YfmNote/{ => YfmNoteSpecs}/toYfm.ts | 4 +- .../yfm/YfmNote/YfmNoteSpecs/utils.ts | 5 + src/extensions/yfm/YfmNote/commands.test.ts | 2 +- src/extensions/yfm/YfmNote/const.ts | 10 +- src/extensions/yfm/YfmNote/index.ts | 28 +--- src/extensions/yfm/YfmNote/utils.ts | 6 +- src/extensions/yfm/YfmTable/YfmTable.test.ts | 12 +- .../yfm/YfmTable/YfmTableSpecs/const.ts | 6 + .../YfmTable/{ => YfmTableSpecs}/fromYfm.ts | 2 +- .../yfm/YfmTable/YfmTableSpecs/index.ts | 43 ++++++ .../yfm/YfmTable/{ => YfmTableSpecs}/spec.ts | 9 +- .../yfm/YfmTable/{ => YfmTableSpecs}/toYfm.ts | 2 +- .../yfm/YfmTable/YfmTableSpecs/utils.ts | 7 + src/extensions/yfm/YfmTable/actions.test.ts | 2 +- .../yfm/YfmTable/commands/backspace.test.ts | 2 +- src/extensions/yfm/YfmTable/const.ts | 7 +- src/extensions/yfm/YfmTable/index.ts | 45 ++---- src/extensions/yfm/YfmTable/utils.ts | 7 +- .../yfm/YfmTabs/YfmTabsSpecs/const.ts | 6 + .../yfm/YfmTabs/{ => YfmTabsSpecs}/fromYfm.ts | 2 +- .../yfm/YfmTabs/YfmTabsSpecs/index.ts | 63 +++++++++ .../yfm/YfmTabs/{ => YfmTabsSpecs}/spec.ts | 2 +- .../yfm/YfmTabs/{ => YfmTabsSpecs}/toYfm.ts | 2 +- src/extensions/yfm/YfmTabs/const.ts | 14 +- src/extensions/yfm/YfmTabs/index.ts | 63 ++------- src/extensions/yfm/YfmTabs/plugins.ts | 4 +- src/extensions/yfm/specs.ts | 57 ++++++++ src/index.ts | 2 + src/utils/placeholder.ts | 8 ++ 184 files changed, 2524 insertions(+), 1891 deletions(-) create mode 100644 src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts create mode 100644 src/extensions/base/specs.ts create mode 100644 src/extensions/markdown/Blockquote/BlockquoteSpecs/index.ts create mode 100644 src/extensions/markdown/Bold/BoldSpecs/index.ts create mode 100644 src/extensions/markdown/Breaks/BreaksSpecs/index.ts create mode 100644 src/extensions/markdown/Code/CodeSpecs/index.ts create mode 100644 src/extensions/markdown/CodeBlock/CodeBlockSpecs/index.ts create mode 100644 src/extensions/markdown/Deflist/DeflistSpecs/const.ts rename src/extensions/markdown/Deflist/{ => DeflistSpecs}/fromYfm.ts (85%) create mode 100644 src/extensions/markdown/Deflist/DeflistSpecs/index.ts rename src/extensions/markdown/Deflist/{ => DeflistSpecs}/spec.ts (82%) rename src/extensions/markdown/Deflist/{ => DeflistSpecs}/toYfm.ts (88%) create mode 100644 src/extensions/markdown/Heading/HeadingSpecs/index.ts create mode 100644 src/extensions/markdown/HorizontalRule/HorizontalRuleSpecs/index.ts create mode 100644 src/extensions/markdown/Image/ImageSpecs/index.ts delete mode 100644 src/extensions/markdown/Image/utils.ts create mode 100644 src/extensions/markdown/Italic/ItalicSpecs/index.ts create mode 100644 src/extensions/markdown/Link/LinkSpecs/index.ts create mode 100644 src/extensions/markdown/Lists/ListsSpecs/const.ts rename src/extensions/markdown/Lists/{ => ListsSpecs}/fromYfm.ts (94%) create mode 100644 src/extensions/markdown/Lists/ListsSpecs/index.ts rename src/extensions/markdown/Lists/{ => ListsSpecs}/spec.ts (97%) rename src/extensions/markdown/Lists/{ => ListsSpecs}/toYfm.ts (92%) create mode 100644 src/extensions/markdown/Mark/MarkSpecs/index.ts create mode 100644 src/extensions/markdown/Strike/StrikeSpecs/index.ts create mode 100644 src/extensions/markdown/Subscript/SubscriptSpecs/index.ts create mode 100644 src/extensions/markdown/Superscript/SuperscriptSpecs/index.ts create mode 100644 src/extensions/markdown/Table/TableSpecs/const.ts rename src/extensions/markdown/Table/{ => TableSpecs}/fromYfm.ts (90%) create mode 100644 src/extensions/markdown/Table/TableSpecs/index.ts rename src/extensions/markdown/Table/{ => TableSpecs}/spec.ts (94%) rename src/extensions/markdown/Table/{ => TableSpecs}/toYfm.ts (93%) create mode 100644 src/extensions/markdown/Underline/UnderlineSpecs/index.ts create mode 100644 src/extensions/markdown/specs.ts create mode 100644 src/extensions/specs.ts create mode 100644 src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts rename src/extensions/yfm/Checkbox/{ => CheckboxSpecs}/fromYfm.ts (82%) create mode 100644 src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts rename src/extensions/yfm/Checkbox/{ => CheckboxSpecs}/spec.ts (83%) rename src/extensions/yfm/Checkbox/{ => CheckboxSpecs}/toYfm.ts (78%) create mode 100644 src/extensions/yfm/Color/ColorSpecs/const.ts create mode 100644 src/extensions/yfm/Color/ColorSpecs/index.ts create mode 100644 src/extensions/yfm/ImgSize/ImgSizeSpecs/index.ts create mode 100644 src/extensions/yfm/Math/MathSpecs/const.ts create mode 100644 src/extensions/yfm/Math/MathSpecs/index.ts create mode 100644 src/extensions/yfm/Monospace/MonospaceSpecs/index.ts create mode 100644 src/extensions/yfm/Video/VideoSpecs/const.ts create mode 100644 src/extensions/yfm/Video/VideoSpecs/index.ts rename src/extensions/yfm/Video/{ => VideoSpecs}/md-video.ts (100%) rename src/extensions/yfm/Video/{ => VideoSpecs}/utils.ts (72%) create mode 100644 src/extensions/yfm/YfmCut/YfmCutSpecs/const.ts rename src/extensions/yfm/YfmCut/{ => YfmCutSpecs}/fromYfm.ts (88%) create mode 100644 src/extensions/yfm/YfmCut/YfmCutSpecs/index.ts rename src/extensions/yfm/YfmCut/{ => YfmCutSpecs}/spec.ts (83%) rename src/extensions/yfm/YfmCut/{ => YfmCutSpecs}/toYfm.ts (79%) create mode 100644 src/extensions/yfm/YfmDist/YfmDistSpecs/index.ts rename src/extensions/yfm/YfmFile/{ => YfmFileSpecs}/const.ts (94%) create mode 100644 src/extensions/yfm/YfmFile/YfmFileSpecs/index.ts create mode 100644 src/extensions/yfm/YfmHeading/YfmHeadingSpecs/const.ts create mode 100644 src/extensions/yfm/YfmHeading/YfmHeadingSpecs/index.ts rename src/extensions/yfm/YfmHeading/{ => YfmHeadingSpecs}/utils.ts (80%) create mode 100644 src/extensions/yfm/YfmNote/YfmNoteSpecs/const.ts rename src/extensions/yfm/YfmNote/{ => YfmNoteSpecs}/fromYfm.ts (86%) create mode 100644 src/extensions/yfm/YfmNote/YfmNoteSpecs/index.ts rename src/extensions/yfm/YfmNote/{ => YfmNoteSpecs}/spec.ts (87%) rename src/extensions/yfm/YfmNote/{ => YfmNoteSpecs}/toYfm.ts (85%) create mode 100644 src/extensions/yfm/YfmNote/YfmNoteSpecs/utils.ts create mode 100644 src/extensions/yfm/YfmTable/YfmTableSpecs/const.ts rename src/extensions/yfm/YfmTable/{ => YfmTableSpecs}/fromYfm.ts (87%) create mode 100644 src/extensions/yfm/YfmTable/YfmTableSpecs/index.ts rename src/extensions/yfm/YfmTable/{ => YfmTableSpecs}/spec.ts (88%) rename src/extensions/yfm/YfmTable/{ => YfmTableSpecs}/toYfm.ts (94%) create mode 100644 src/extensions/yfm/YfmTable/YfmTableSpecs/utils.ts create mode 100644 src/extensions/yfm/YfmTabs/YfmTabsSpecs/const.ts rename src/extensions/yfm/YfmTabs/{ => YfmTabsSpecs}/fromYfm.ts (93%) create mode 100644 src/extensions/yfm/YfmTabs/YfmTabsSpecs/index.ts rename src/extensions/yfm/YfmTabs/{ => YfmTabsSpecs}/spec.ts (97%) rename src/extensions/yfm/YfmTabs/{ => YfmTabsSpecs}/toYfm.ts (94%) create mode 100644 src/extensions/yfm/specs.ts create mode 100644 src/utils/placeholder.ts diff --git a/src/extensions/base/BaseSchema/BaseSchema.test.ts b/src/extensions/base/BaseSchema/BaseSchema.test.ts index 0baec44d..d386d76b 100644 --- a/src/extensions/base/BaseSchema/BaseSchema.test.ts +++ b/src/extensions/base/BaseSchema/BaseSchema.test.ts @@ -1,10 +1,10 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from './index'; +import {BaseNode, BaseSchemaSpecs} from './BaseSchemaSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}), + extensions: (builder) => builder.use(BaseSchemaSpecs, {}), }).buildDeps(); const {doc, p} = builders(schema, { diff --git a/src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts b/src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts new file mode 100644 index 00000000..f452d0f5 --- /dev/null +++ b/src/extensions/base/BaseSchema/BaseSchemaSpecs/index.ts @@ -0,0 +1,61 @@ +import type {NodeSpec} from 'prosemirror-model'; +import type {ExtensionAuto} from '../../../../core'; +import {nodeTypeFactory} from '../../../../utils/schema'; + +export enum BaseNode { + Doc = 'doc', + Text = 'text', + Paragraph = 'paragraph', +} + +export const pType = nodeTypeFactory(BaseNode.Paragraph); + +export type BaseSchemaSpecsOptions = { + paragraphPlaceholder?: NonNullable['content']; +}; + +export const BaseSchemaSpecs: ExtensionAuto = (builder, opts) => { + const {paragraphPlaceholder} = opts; + + builder + .addNode(BaseNode.Doc, () => ({ + spec: { + content: 'block+', + }, + fromYfm: {tokenSpec: {name: BaseNode.Doc, type: 'block', ignore: true}}, + toYfm: () => { + throw new Error('Unexpected toYfm() call on doc node'); + }, + })) + .addNode(BaseNode.Text, () => ({ + spec: { + group: 'inline', + }, + fromYfm: {tokenSpec: {name: BaseNode.Text, type: 'node', ignore: true}}, + toYfm: (state, node, parent) => { + const {escapeText} = parent.type.spec; + state.text(node.text, escapeText ?? !state.isAutolink); + }, + })) + .addNode(BaseNode.Paragraph, () => ({ + spec: { + content: 'inline*', + group: 'block', + parseDOM: [{tag: 'p'}], + toDOM() { + return ['p', 0]; + }, + placeholder: paragraphPlaceholder + ? { + content: paragraphPlaceholder, + alwaysVisible: false, + } + : undefined, + }, + fromYfm: {tokenSpec: {name: BaseNode.Paragraph, type: 'block'}}, + toYfm: (state, node) => { + state.renderInline(node); + state.closeBlock(node); + }, + })); +}; diff --git a/src/extensions/base/BaseSchema/index.ts b/src/extensions/base/BaseSchema/index.ts index fe6a430c..54b961a0 100644 --- a/src/extensions/base/BaseSchema/index.ts +++ b/src/extensions/base/BaseSchema/index.ts @@ -1,72 +1,24 @@ -import type {NodeSpec} from 'prosemirror-model'; import type {Command} from 'prosemirror-state'; import {setBlockType} from 'prosemirror-commands'; import {hasParentNodeOfType} from 'prosemirror-utils'; import type {Action, ExtensionAuto} from '../../../core'; -import {nodeTypeFactory} from '../../../utils/schema'; +import {BaseSchemaSpecs, BaseSchemaSpecsOptions, pType} from './BaseSchemaSpecs'; -export enum BaseNode { - Doc = 'doc', - Text = 'text', - Paragraph = 'paragraph', -} +export {BaseNode, pType} from './BaseSchemaSpecs'; -export const pType = nodeTypeFactory(BaseNode.Paragraph); const pAction = 'toParagraph'; export const toParagraph: Command = (state, dispatch) => setBlockType(pType(state.schema))(state, dispatch); -export type BaseSchemaOptions = { +export type BaseSchemaOptions = BaseSchemaSpecsOptions & { paragraphKey?: string | null; - paragraphPlaceholder?: NonNullable['content']; }; export const BaseSchema: ExtensionAuto = (builder, opts) => { - const {paragraphKey, paragraphPlaceholder} = opts; - - builder - .addNode(BaseNode.Doc, () => ({ - spec: { - content: 'block+', - }, - fromYfm: {tokenSpec: {name: BaseNode.Doc, type: 'block', ignore: true}}, - toYfm: () => { - throw new Error('Unexpected toYfm() call on doc node'); - }, - })) - .addNode(BaseNode.Text, () => ({ - spec: { - group: 'inline', - }, - fromYfm: {tokenSpec: {name: BaseNode.Text, type: 'node', ignore: true}}, - toYfm: (state, node, parent) => { - const {escapeText} = parent.type.spec; - state.text(node.text, escapeText ?? !state.isAutolink); - }, - })) - .addNode(BaseNode.Paragraph, () => ({ - spec: { - content: 'inline*', - group: 'block', - parseDOM: [{tag: 'p'}], - toDOM() { - return ['p', 0]; - }, - placeholder: paragraphPlaceholder - ? { - content: paragraphPlaceholder, - alwaysVisible: false, - } - : undefined, - }, - fromYfm: {tokenSpec: {name: BaseNode.Paragraph, type: 'block'}}, - toYfm: (state, node) => { - state.renderInline(node); - state.closeBlock(node); - }, - })); + builder.use(BaseSchemaSpecs, opts); + const {paragraphKey} = opts; if (paragraphKey) { builder.addKeymap(({schema}) => ({[paragraphKey]: setBlockType(pType(schema))})); } diff --git a/src/extensions/base/specs.ts b/src/extensions/base/specs.ts new file mode 100644 index 00000000..e15b5508 --- /dev/null +++ b/src/extensions/base/specs.ts @@ -0,0 +1,13 @@ +import type {ExtensionAuto} from '../../core'; + +import {BaseSchemaSpecs, BaseSchemaSpecsOptions} from './BaseSchema/BaseSchemaSpecs'; + +export * from './BaseSchema/BaseSchemaSpecs'; + +export type BaseSpecPresetOptions = { + baseSchema?: BaseSchemaSpecsOptions; +}; + +export const BaseSpecsPreset: ExtensionAuto = (builder, opts) => { + builder.use(BaseSchemaSpecs, opts.baseSchema ?? {}); +}; diff --git a/src/extensions/behavior/Placeholder/index.ts b/src/extensions/behavior/Placeholder/index.ts index f92c1b90..d60c6da7 100644 --- a/src/extensions/behavior/Placeholder/index.ts +++ b/src/extensions/behavior/Placeholder/index.ts @@ -7,15 +7,10 @@ import {cn} from '../../../classname'; import type {ExtensionAuto} from '../../../core'; import {isNodeEmpty} from '../../../utils/nodes'; import {isTextSelection} from '../../../utils/selection'; +import {getPlaceholderContent} from '../../../utils/placeholder'; import './index.scss'; -export const getPlaceholderContent = (node: Node, parent?: Node | null) => { - const content = node.type.spec.placeholder?.content || ''; - - return typeof content === 'function' ? content(node, parent) : content; -}; - const getPlaceholderPluginKeys = (schema: Schema) => { const pluginKeys = []; for (const node in schema.nodes) { diff --git a/src/extensions/markdown/Blockquote/Blockquote.test.ts b/src/extensions/markdown/Blockquote/Blockquote.test.ts index 736b37a4..47c85268 100644 --- a/src/extensions/markdown/Blockquote/Blockquote.test.ts +++ b/src/extensions/markdown/Blockquote/Blockquote.test.ts @@ -2,17 +2,17 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {blockquote, Blockquote} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {blockquoteNodeName, BlockquoteSpecs} from './BlockquoteSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Blockquote, {}), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(BlockquoteSpecs), }).buildDeps(); const {doc, p, q} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - q: {nodeType: blockquote}, + q: {nodeType: blockquoteNodeName}, }) as PMTestBuilderResult<'doc' | 'p' | 'q'>; const {same} = createMarkupChecker({parser, serializer}); diff --git a/src/extensions/markdown/Blockquote/BlockquoteSpecs/index.ts b/src/extensions/markdown/Blockquote/BlockquoteSpecs/index.ts new file mode 100644 index 00000000..fcfdf2dc --- /dev/null +++ b/src/extensions/markdown/Blockquote/BlockquoteSpecs/index.ts @@ -0,0 +1,23 @@ +import type {ExtensionAuto} from '../../../../core'; +import {nodeTypeFactory} from '../../../../utils/schema'; + +export const blockquoteNodeName = 'blockquote'; +export const blockquoteType = nodeTypeFactory(blockquoteNodeName); + +export const BlockquoteSpecs: ExtensionAuto = (builder) => { + builder.addNode(blockquoteNodeName, () => ({ + spec: { + content: 'block+', + group: 'block', + defining: true, + parseDOM: [{tag: 'blockquote'}], + toDOM() { + return ['blockquote', 0]; + }, + }, + fromYfm: {tokenSpec: {name: blockquoteNodeName, type: 'block'}}, + toYfm: (state, node) => { + state.wrapBlock('> ', null, node, () => state.renderContent(node)); + }, + })); +}; diff --git a/src/extensions/markdown/Blockquote/const.ts b/src/extensions/markdown/Blockquote/const.ts index 56642046..4d3e3a62 100644 --- a/src/extensions/markdown/Blockquote/const.ts +++ b/src/extensions/markdown/Blockquote/const.ts @@ -1,4 +1,7 @@ -import {nodeTypeFactory} from '../../../utils/schema'; +import {blockquoteNodeName, blockquoteType} from './BlockquoteSpecs'; -export const blockquote = 'blockquote'; -export const bqType = nodeTypeFactory(blockquote); +export {blockquoteNodeName, blockquoteType} from './BlockquoteSpecs'; +/** @deprecated Use `blockquoteNodeName` instead */ +export const blockquote = blockquoteNodeName; +/** @deprecated Use `blockquoteType` instead */ +export const bqType = blockquoteType; diff --git a/src/extensions/markdown/Blockquote/index.ts b/src/extensions/markdown/Blockquote/index.ts index a8256007..6dcedd36 100644 --- a/src/extensions/markdown/Blockquote/index.ts +++ b/src/extensions/markdown/Blockquote/index.ts @@ -4,9 +4,9 @@ import {wrappingInputRule} from 'prosemirror-inputrules'; import {hasParentNodeOfType} from 'prosemirror-utils'; import type {Action, ExtensionAuto} from '../../../core'; import {selectQuoteBeforeCursor, liftFromQuote, toggleQuote} from './commands'; -import {blockquote, bqType} from './const'; +import {BlockquoteSpecs, blockquoteType} from './BlockquoteSpecs'; -export {blockquote}; +export {blockquote, blockquoteNodeName, blockquoteType} from './const'; const bqAction = 'quote'; export type BlockquoteOptions = { @@ -14,35 +14,21 @@ export type BlockquoteOptions = { }; export const Blockquote: ExtensionAuto = (builder, opts) => { - builder.addNode(blockquote, () => ({ - spec: { - content: 'block+', - group: 'block', - defining: true, - parseDOM: [{tag: 'blockquote'}], - toDOM() { - return ['blockquote', 0]; - }, - }, - fromYfm: {tokenSpec: {name: blockquote, type: 'block'}}, - toYfm: (state, node) => { - state.wrapBlock('> ', null, node, () => state.renderContent(node)); - }, - })); + builder.use(BlockquoteSpecs); if (opts?.qouteKey) { const {qouteKey} = opts; - builder.addKeymap(({schema}) => ({[qouteKey]: wrapIn(bqType(schema))})); + builder.addKeymap(({schema}) => ({[qouteKey]: wrapIn(blockquoteType(schema))})); } builder.addKeymap(() => ({ Backspace: chainCommands(liftFromQuote, selectQuoteBeforeCursor), })); - builder.addInputRules(({schema}) => ({rules: [blockQuoteRule(bqType(schema))]})); + builder.addInputRules(({schema}) => ({rules: [blockQuoteRule(blockquoteType(schema))]})); builder.addAction(bqAction, ({schema}) => { - const bq = bqType(schema); + const bq = blockquoteType(schema); return { isActive: (state) => hasParentNodeOfType(bq)(state.selection), isEnable: toggleQuote, diff --git a/src/extensions/markdown/Bold/Bold.test.ts b/src/extensions/markdown/Bold/Bold.test.ts index d480b6bb..2fb6c056 100644 --- a/src/extensions/markdown/Bold/Bold.test.ts +++ b/src/extensions/markdown/Bold/Bold.test.ts @@ -2,17 +2,17 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {bold, Bold} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {boldMarkName, BoldSpecs} from './BoldSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Bold, {}), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(BoldSpecs), }).buildDeps(); const {doc, p, b} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - b: {nodeType: bold}, + b: {nodeType: boldMarkName}, }) as PMTestBuilderResult<'doc' | 'p', 'b'>; const {same} = createMarkupChecker({parser, serializer}); diff --git a/src/extensions/markdown/Bold/BoldSpecs/index.ts b/src/extensions/markdown/Bold/BoldSpecs/index.ts new file mode 100644 index 00000000..55218ff9 --- /dev/null +++ b/src/extensions/markdown/Bold/BoldSpecs/index.ts @@ -0,0 +1,30 @@ +import type {ExtensionAuto} from '../../../../core'; +import {markTypeFactory} from '../../../../utils/schema'; + +export const boldMarkName = 'strong'; +export const boldType = markTypeFactory(boldMarkName); + +export const BoldSpecs: ExtensionAuto = (builder) => { + builder.addMark(boldMarkName, () => ({ + spec: { + parseDOM: [ + {tag: 'b'}, + {tag: 'strong'}, + { + style: 'font-weight', + getAttrs: (value) => /^(bold(er)?|[5-9]\d{2,})$/.test(value as string) && null, + }, + ], + toDOM() { + return ['strong']; + }, + }, + fromYfm: { + tokenSpec: { + name: boldMarkName, + type: 'mark', + }, + }, + toYfm: {open: '**', close: '**', mixable: true, expelEnclosingWhitespace: true}, + })); +}; diff --git a/src/extensions/markdown/Bold/index.ts b/src/extensions/markdown/Bold/index.ts index 407b4c58..f65a9103 100644 --- a/src/extensions/markdown/Bold/index.ts +++ b/src/extensions/markdown/Bold/index.ts @@ -1,53 +1,31 @@ import {toggleMark} from 'prosemirror-commands'; import {createToggleMarkAction} from '../../../utils/actions'; import type {Action, ExtensionAuto} from '../../../core'; -import {markTypeFactory} from '../../../utils/schema'; import {markInputRule} from '../../../utils/inputrules'; +import {boldMarkName, BoldSpecs, boldType} from './BoldSpecs'; -export const bold = 'strong'; +export {boldMarkName, boldType} from './BoldSpecs'; +/** @deprecated Use `boldMarkName` instead */ +export const bold = boldMarkName; const bAction = 'bold'; -const bType = markTypeFactory(bold); export type BoldOptions = { boldKey?: string | null; }; export const Bold: ExtensionAuto = (builder, opts) => { - builder - .addMark(bold, () => ({ - spec: { - parseDOM: [ - {tag: 'b'}, - {tag: 'strong'}, - { - style: 'font-weight', - getAttrs: (value) => - /^(bold(er)?|[5-9]\d{2,})$/.test(value as string) && null, - }, - ], - toDOM() { - return ['strong']; - }, - }, - fromYfm: { - tokenSpec: { - name: bold, - type: 'mark', - }, - }, - toYfm: {open: '**', close: '**', mixable: true, expelEnclosingWhitespace: true}, - })) - .addAction(bAction, ({schema}) => createToggleMarkAction(bType(schema))); + builder.use(BoldSpecs); + builder.addAction(bAction, ({schema}) => createToggleMarkAction(boldType(schema))); if (opts?.boldKey) { const {boldKey} = opts; - builder.addKeymap(({schema}) => ({[boldKey]: toggleMark(bType(schema))})); + builder.addKeymap(({schema}) => ({[boldKey]: toggleMark(boldType(schema))})); } builder.addInputRules(({schema}) => ({ rules: [ - markInputRule({open: '**', close: '**', ignoreBetween: '*'}, bType(schema)), - markInputRule({open: '__', close: '__', ignoreBetween: '_'}, bType(schema)), + markInputRule({open: '**', close: '**', ignoreBetween: '*'}, boldType(schema)), + markInputRule({open: '__', close: '__', ignoreBetween: '_'}, boldType(schema)), ], })); }; diff --git a/src/extensions/markdown/Breaks/Breaks.test.ts b/src/extensions/markdown/Breaks/Breaks.test.ts index b3f33ae2..54ba7c49 100644 --- a/src/extensions/markdown/Breaks/Breaks.test.ts +++ b/src/extensions/markdown/Breaks/Breaks.test.ts @@ -1,17 +1,17 @@ import {builders} from 'prosemirror-test-builder'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {Breaks, hbType} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {BreakNodeName, BreaksSpecs} from './BreaksSpecs'; const {schema} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Breaks, {}), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(BreaksSpecs, {}), }).buildDeps(); const {doc, p, hb} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - hb: {nodeType: hbType(schema).name}, + hb: {nodeType: BreakNodeName.HardBreak}, }) as PMTestBuilderResult<'doc' | 'p' | 'hb'>; describe('Breaks extension', () => { diff --git a/src/extensions/markdown/Breaks/BreaksSpecs/index.ts b/src/extensions/markdown/Breaks/BreaksSpecs/index.ts new file mode 100644 index 00000000..39144715 --- /dev/null +++ b/src/extensions/markdown/Breaks/BreaksSpecs/index.ts @@ -0,0 +1,57 @@ +import type {ExtensionAuto} from '../../../../core'; +import {nodeTypeFactory} from '../../../../utils/schema'; + +export enum BreakNodeName { + HardBreak = 'hard_break', + SoftBreak = 'soft_break', +} + +export const hbType = nodeTypeFactory(BreakNodeName.HardBreak); +export const sbType = nodeTypeFactory(BreakNodeName.SoftBreak); + +export const BreaksSpecs: ExtensionAuto = (builder) => { + builder.addNode(BreakNodeName.HardBreak, () => ({ + spec: { + inline: true, + group: 'inline', + selectable: false, + parseDOM: [{tag: 'br'}], + toDOM() { + return ['br']; + }, + }, + fromYfm: {tokenName: 'hardbreak', tokenSpec: {name: BreakNodeName.HardBreak, type: 'node'}}, + toYfm: (state, node, parent, index) => { + for (let i = index + 1; i < parent.childCount; i++) { + if (parent.child(i).type !== node.type) { + state.write('\\\n'); + return; + } + } + }, + })); + + // TODO: should we handle softbreak differently at different md.options.breaks setting? + + // we can safely convert softbreak into hardbreak, + // but in this case non-edited markup will always be changed – a backspash will be added + builder.addNode(BreakNodeName.SoftBreak, () => ({ + spec: { + inline: true, + group: 'inline', + selectable: false, + toDOM() { + return ['br']; + }, + }, + fromYfm: {tokenName: 'softbreak', tokenSpec: {name: BreakNodeName.SoftBreak, type: 'node'}}, + toYfm: (state, node, parent, index) => { + for (let i = index + 1; i < parent.childCount; i++) { + if (parent.child(i).type !== node.type) { + state.write('\n'); + return; + } + } + }, + })); +}; diff --git a/src/extensions/markdown/Breaks/index.ts b/src/extensions/markdown/Breaks/index.ts index d56b13ee..a566c552 100644 --- a/src/extensions/markdown/Breaks/index.ts +++ b/src/extensions/markdown/Breaks/index.ts @@ -3,12 +3,9 @@ import {chainCommands, exitCode} from 'prosemirror-commands'; import {logger} from '../../../logger'; import type {ExtensionAuto, Keymap} from '../../../core'; import {isMac} from '../../../utils/platform'; -import {nodeTypeFactory} from '../../../utils/schema'; +import {BreaksSpecs, hbType, sbType} from './BreaksSpecs'; -const hardBreak = 'hard_break'; -const softBreak = 'soft_break'; -export const hbType = nodeTypeFactory(hardBreak); -export const sbType = nodeTypeFactory(softBreak); +export {BreaksSpecs, BreakNodeName, hbType, sbType} from './BreaksSpecs'; export type BreaksOptions = { /** @@ -20,6 +17,8 @@ export type BreaksOptions = { }; export const Breaks: ExtensionAuto = (builder, opts) => { + builder.use(BreaksSpecs); + let preferredBreak: 'hard' | 'soft'; if (builder.context.has('breaks')) { preferredBreak = builder.context.get('breaks') ? 'soft' : 'hard'; @@ -30,51 +29,6 @@ export const Breaks: ExtensionAuto = (builder, opts) => { ); } - builder.addNode(hardBreak, () => ({ - spec: { - inline: true, - group: 'inline', - selectable: false, - parseDOM: [{tag: 'br'}], - toDOM() { - return ['br']; - }, - }, - fromYfm: {tokenName: 'hardbreak', tokenSpec: {name: hardBreak, type: 'node'}}, - toYfm: (state, node, parent, index) => { - for (let i = index + 1; i < parent.childCount; i++) { - if (parent.child(i).type !== node.type) { - state.write('\\\n'); - return; - } - } - }, - })); - - // TODO: should we handle softbreak differently at different md.options.breaks setting? - - // we can safely convert softbreak into hardbreak, - // but in this case non-edited markup will always be changed – a backspash will be added - builder.addNode(softBreak, () => ({ - spec: { - inline: true, - group: 'inline', - selectable: false, - toDOM() { - return ['br']; - }, - }, - fromYfm: {tokenName: 'softbreak', tokenSpec: {name: softBreak, type: 'node'}}, - toYfm: (state, node, parent, index) => { - for (let i = index + 1; i < parent.childCount; i++) { - if (parent.child(i).type !== node.type) { - state.write('\n'); - return; - } - } - }, - })); - builder.addKeymap(({schema}) => { const cmd = addBr((preferredBreak === 'soft' ? sbType : hbType)(schema)); const keys: Keymap = { diff --git a/src/extensions/markdown/Code/Code.test.ts b/src/extensions/markdown/Code/Code.test.ts index deb1c6cc..396799a2 100644 --- a/src/extensions/markdown/Code/Code.test.ts +++ b/src/extensions/markdown/Code/Code.test.ts @@ -2,22 +2,22 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {bold, Bold} from '../Bold'; -import {italic, Italic} from '../Italic'; -import {code, Code} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {boldMarkName, BoldSpecs} from '../Bold/BoldSpecs'; +import {italicMarkName, ItalicSpecs} from '../Italic/ItalicSpecs'; +import {codeMarkName, CodeSpecs} from './CodeSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ extensions: (builder) => - builder.use(BaseSchema, {}).use(Bold, {}).use(Code, {}).use(Italic, {}), + builder.use(BaseSpecsPreset, {}).use(BoldSpecs).use(CodeSpecs).use(ItalicSpecs), }).buildDeps(); const {doc, p, b, i, c} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - b: {nodeType: bold}, - i: {nodeType: italic}, - c: {nodeType: code}, + b: {nodeType: boldMarkName}, + i: {nodeType: italicMarkName}, + c: {nodeType: codeMarkName}, }) as PMTestBuilderResult<'doc' | 'p', 'b' | 'i' | 'c'>; const {same} = createMarkupChecker({parser, serializer}); diff --git a/src/extensions/markdown/Code/CodeSpecs/index.ts b/src/extensions/markdown/Code/CodeSpecs/index.ts new file mode 100644 index 00000000..d107e90b --- /dev/null +++ b/src/extensions/markdown/Code/CodeSpecs/index.ts @@ -0,0 +1,49 @@ +import type {Node} from 'prosemirror-model'; +import type {ExtensionAuto} from '../../../../core'; +import {markTypeFactory} from '../../../../utils/schema'; + +export const codeMarkName = 'code'; +export const codeType = markTypeFactory(codeMarkName); + +export const CodeSpecs: ExtensionAuto = (builder) => { + builder.addMark( + codeMarkName, + () => ({ + spec: { + code: true, + parseDOM: [{tag: 'code'}], + toDOM() { + return ['code']; + }, + }, + toYfm: { + open(_state, _mark, parent, index) { + return backticksFor(parent.child(index), -1); + }, + close(_state, _mark, parent, index) { + return backticksFor(parent.child(index - 1), 1); + }, + escape: false, + }, + fromYfm: { + tokenSpec: {name: codeMarkName, type: 'mark', noCloseToken: true}, + tokenName: 'code_inline', + }, + }), + builder.Priority.Lowest, + ); +}; + +function backticksFor(node: Node, side: number) { + const ticks = /`+/g; + let m; + let len = 0; + + if (node.isText) while ((m = ticks.exec(node.text || ''))) len = Math.max(len, m[0].length); + + let result = len > 0 && side > 0 ? ' `' : '`'; + for (let i = 0; i < len; i++) result += '`'; + if (len > 0 && side < 0) result += ' '; + + return result; +} diff --git a/src/extensions/markdown/Code/index.ts b/src/extensions/markdown/Code/index.ts index 6f46ec1c..2dc3eba4 100644 --- a/src/extensions/markdown/Code/index.ts +++ b/src/extensions/markdown/Code/index.ts @@ -1,50 +1,24 @@ -import type {Node} from 'prosemirror-model'; import {toggleMark} from 'prosemirror-commands'; import codemark from 'prosemirror-codemark'; import {Plugin} from 'prosemirror-state'; import {createToggleMarkAction} from '../../../utils/actions'; import type {Action, ExtensionAuto} from '../../../core'; -import {markTypeFactory} from '../../../utils/schema'; +import {codeMarkName, CodeSpecs, codeType} from './CodeSpecs'; import './code.scss'; -export const code = 'code'; +export {codeMarkName, codeType} from './CodeSpecs'; +/** @deprecated Use `codeMarkName` instead */ +export const code = codeMarkName; const codeAction = 'code'; -export const codeType = markTypeFactory(code); export type CodeOptions = { codeKey?: string | null; }; export const Code: ExtensionAuto = (builder, opts) => { - builder - .addMark( - code, - () => ({ - spec: { - code: true, - parseDOM: [{tag: 'code'}], - toDOM() { - return ['code']; - }, - }, - toYfm: { - open(_state, _mark, parent, index) { - return backticksFor(parent.child(index), -1); - }, - close(_state, _mark, parent, index) { - return backticksFor(parent.child(index - 1), 1); - }, - escape: false, - }, - fromYfm: { - tokenSpec: {name: code, type: 'mark', noCloseToken: true}, - tokenName: 'code_inline', - }, - }), - builder.Priority.Lowest, - ) - .addAction(codeAction, ({schema}) => createToggleMarkAction(codeType(schema))); + builder.use(CodeSpecs); + builder.addAction(codeAction, ({schema}) => createToggleMarkAction(codeType(schema))); if (opts?.codeKey) { const {codeKey} = opts; @@ -97,17 +71,3 @@ declare global { } } } - -function backticksFor(node: Node, side: number) { - const ticks = /`+/g; - let m; - let len = 0; - - if (node.isText) while ((m = ticks.exec(node.text || ''))) len = Math.max(len, m[0].length); - - let result = len > 0 && side > 0 ? ' `' : '`'; - for (let i = 0; i < len; i++) result += '`'; - if (len > 0 && side < 0) result += ' '; - - return result; -} diff --git a/src/extensions/markdown/CodeBlock/CodeBlock.test.ts b/src/extensions/markdown/CodeBlock/CodeBlock.test.ts index 30ad437e..78bfcd45 100644 --- a/src/extensions/markdown/CodeBlock/CodeBlock.test.ts +++ b/src/extensions/markdown/CodeBlock/CodeBlock.test.ts @@ -1,23 +1,19 @@ -/** - * @jest-environment jsdom - */ - import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {CodeBlock} from './index'; -import {codeBlock, langAttr} from './const'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {CodeBlockSpecs} from './CodeBlockSpecs'; +import {codeBlockNodeName, codeBlockLangAttr} from './const'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(CodeBlock, {}), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(CodeBlockSpecs, {}), }).buildDeps(); const {doc, p, cb} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - cb: {nodeType: codeBlock}, + cb: {nodeType: codeBlockNodeName}, }) as PMTestBuilderResult<'doc' | 'p' | 'cb'>; const {same, parse} = createMarkupChecker({parser, serializer}); @@ -26,7 +22,11 @@ describe('CodeBlock extension', () => { it('should parse a code block', () => same( 'Some code:\n\n```\nHere it is\n```\n\nPara', - doc(p('Some code:'), cb({[langAttr]: ''}, schema.text('Here it is\n')), p('Para')), + doc( + p('Some code:'), + cb({[codeBlockLangAttr]: ''}, schema.text('Here it is\n')), + p('Para'), + ), )); it('parses an intended code block', () => @@ -38,7 +38,12 @@ describe('CodeBlock extension', () => { it('should parse a fenced code block with info string', () => same( 'foo\n\n```javascript\n1\n```', - doc(p('foo'), schema.node(codeBlock, {[langAttr]: 'javascript'}, [schema.text('1\n')])), + doc( + p('foo'), + schema.node(codeBlockNodeName, {[codeBlockLangAttr]: 'javascript'}, [ + schema.text('1\n'), + ]), + ), )); // TODO: parsed: doc(paragraph("code\nblock")) diff --git a/src/extensions/markdown/CodeBlock/CodeBlockSpecs/index.ts b/src/extensions/markdown/CodeBlock/CodeBlockSpecs/index.ts new file mode 100644 index 00000000..657bb141 --- /dev/null +++ b/src/extensions/markdown/CodeBlock/CodeBlockSpecs/index.ts @@ -0,0 +1,68 @@ +import type {ExtensionAuto, YENodeSpec} from '../../../../core'; +import {nodeTypeFactory} from '../../../../utils/schema'; + +export const codeBlockNodeName = 'code_block'; +export const codeBlockLangAttr = 'data-language'; +export const codeBlockType = nodeTypeFactory(codeBlockNodeName); + +export type CodeBlockSpecsOptions = { + nodeview?: YENodeSpec['view']; +}; + +export const CodeBlockSpecs: ExtensionAuto = (builder, opts) => { + builder.addNode(codeBlockNodeName, () => ({ + view: opts.nodeview, + spec: { + attrs: {[codeBlockLangAttr]: {default: 'text'}}, + content: 'text*', + group: 'block', + code: true, + marks: '', + selectable: true, + allowSelection: true, + parseDOM: [ + { + tag: 'pre', + preserveWhitespace: 'full', + getAttrs: (node) => ({ + [codeBlockLangAttr]: + (node as Element).getAttribute(codeBlockLangAttr) || '', + }), + }, + ], + toDOM({attrs}) { + return ['pre', attrs[codeBlockLangAttr] ? attrs : {}, ['code', 0]]; + }, + }, + fromYfm: { + tokenSpec: { + name: codeBlockNodeName, + type: 'block', + noCloseToken: true, + }, + }, + toYfm: (state, node) => { + state.write('```' + (node.attrs[codeBlockLangAttr] || '') + '\n'); + state.text(node.textContent, false); + state.ensureNewLine(); + state.write('```'); + state.closeBlock(node); + }, + })); + builder.addNode('fence', () => ({ + // we adding this node only for define specific 'fence' parser token, + // which parse fence md token to code_block node + spec: {}, + fromYfm: { + tokenSpec: { + name: codeBlockNodeName, + type: 'block', + noCloseToken: true, + getAttrs: (tok) => ({[codeBlockLangAttr]: tok.info || ''}), + }, + }, + toYfm: () => { + throw new Error('Unexpected toYfm() call on fence node'); + }, + })); +}; diff --git a/src/extensions/markdown/CodeBlock/const.ts b/src/extensions/markdown/CodeBlock/const.ts index 4757e235..1db42ad3 100644 --- a/src/extensions/markdown/CodeBlock/const.ts +++ b/src/extensions/markdown/CodeBlock/const.ts @@ -1,6 +1,6 @@ -import {nodeTypeFactory} from '../../../utils/schema'; +import {codeBlockType} from './CodeBlockSpecs'; -export const codeBlock = 'code_block'; -export const langAttr = 'data-language'; +export {codeBlockNodeName, codeBlockLangAttr} from './CodeBlockSpecs'; export const cbAction = 'toCodeBlock'; -export const cbType = nodeTypeFactory(codeBlock); +/** @deprecated Use `codeBlockType` instead */ +export const cbType = codeBlockType; diff --git a/src/extensions/markdown/CodeBlock/handle-paste.ts b/src/extensions/markdown/CodeBlock/handle-paste.ts index a1305b73..1813ae35 100644 --- a/src/extensions/markdown/CodeBlock/handle-paste.ts +++ b/src/extensions/markdown/CodeBlock/handle-paste.ts @@ -1,6 +1,6 @@ import type {EditorProps} from 'prosemirror-view'; import {DataTransferType} from '../../behavior/Clipboard/utils'; -import {cbType, langAttr} from './const'; +import {cbType, codeBlockLangAttr} from './const'; export const handlePaste: NonNullable = (view, e) => { if (!e.clipboardData || view.state.selection.$from.parent.type.spec.code) return false; @@ -8,7 +8,10 @@ export const handlePaste: NonNullable = (view, e) => if (!code) return false; let tr = view.state.tr; const {schema} = tr.doc.type; - const codeBlockNode = cbType(schema).create({[langAttr]: code.mode}, schema.text(code.value)); + const codeBlockNode = cbType(schema).create( + {[codeBlockLangAttr]: code.mode}, + schema.text(code.value), + ); tr = tr.replaceSelectionWith(codeBlockNode); view.dispatch(tr.scrollIntoView()); return true; diff --git a/src/extensions/markdown/CodeBlock/index.ts b/src/extensions/markdown/CodeBlock/index.ts index c1e19539..707cb4b9 100644 --- a/src/extensions/markdown/CodeBlock/index.ts +++ b/src/extensions/markdown/CodeBlock/index.ts @@ -4,75 +4,20 @@ import {Fragment, NodeType, Slice} from 'prosemirror-model'; import {Command, Plugin} from 'prosemirror-state'; import {hasParentNodeOfType} from 'prosemirror-utils'; import type {Action, ExtensionAuto, Keymap} from '../../../core'; +import {CodeBlockSpecs, CodeBlockSpecsOptions} from './CodeBlockSpecs'; import {resetCodeblock} from './commands'; -import {cbAction, cbType, codeBlock, langAttr} from './const'; +import {cbAction, cbType} from './const'; import {handlePaste} from './handle-paste'; export {resetCodeblock} from './commands'; -export { - codeBlock as codeBlockNodeName, - langAttr as codeBlockLangAttr, - cbType as codeBlockType, -} from './const'; +export {codeBlockNodeName, codeBlockLangAttr, codeBlockType} from './CodeBlockSpecs'; -export type CodeBlockOptions = { +export type CodeBlockOptions = CodeBlockSpecsOptions & { codeBlockKey?: string | null; }; export const CodeBlock: ExtensionAuto = (builder, opts) => { - builder.addNode(codeBlock, () => ({ - spec: { - attrs: {[langAttr]: {default: 'text'}}, - content: 'text*', - group: 'block', - code: true, - marks: '', - selectable: true, - allowSelection: true, - parseDOM: [ - { - tag: 'pre', - preserveWhitespace: 'full', - getAttrs: (node) => ({ - [langAttr]: (node as Element).getAttribute(langAttr) || '', - }), - }, - ], - toDOM({attrs}) { - return ['pre', attrs[langAttr] ? attrs : {}, ['code', 0]]; - }, - }, - fromYfm: { - tokenSpec: { - name: codeBlock, - type: 'block', - noCloseToken: true, - }, - }, - toYfm: (state, node) => { - state.write('```' + (node.attrs[langAttr] || '') + '\n'); - state.text(node.textContent, false); - state.ensureNewLine(); - state.write('```'); - state.closeBlock(node); - }, - })); - builder.addNode('fence', () => ({ - // we adding this node only for define specific 'fence' parser token, - // which parse fence md token to code_block node - spec: {}, - fromYfm: { - tokenSpec: { - name: codeBlock, - type: 'block', - noCloseToken: true, - getAttrs: (tok) => ({[langAttr]: tok.info || ''}), - }, - }, - toYfm: () => { - throw new Error('Unexpected toYfm() call on fence node'); - }, - })); + builder.use(CodeBlockSpecs, opts); builder.addKeymap((deps) => { const {codeBlockKey} = opts; diff --git a/src/extensions/markdown/Deflist/Deflist.test.ts b/src/extensions/markdown/Deflist/Deflist.test.ts index 0a0b0cff..62851916 100644 --- a/src/extensions/markdown/Deflist/Deflist.test.ts +++ b/src/extensions/markdown/Deflist/Deflist.test.ts @@ -2,12 +2,11 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {Deflist} from './index'; -import {DeflistNode} from './const'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {DeflistNode, DeflistSpecs} from './DeflistSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Deflist, {}), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(DeflistSpecs, {}), }).buildDeps(); const {doc, p, dl, dt, dd} = builders(schema, { diff --git a/src/extensions/markdown/Deflist/DeflistSpecs/const.ts b/src/extensions/markdown/Deflist/DeflistSpecs/const.ts new file mode 100644 index 00000000..f17929a8 --- /dev/null +++ b/src/extensions/markdown/Deflist/DeflistSpecs/const.ts @@ -0,0 +1,5 @@ +export enum DeflistNode { + List = 'dl', + Term = 'dt', + Desc = 'dd', +} diff --git a/src/extensions/markdown/Deflist/fromYfm.ts b/src/extensions/markdown/Deflist/DeflistSpecs/fromYfm.ts similarity index 85% rename from src/extensions/markdown/Deflist/fromYfm.ts rename to src/extensions/markdown/Deflist/DeflistSpecs/fromYfm.ts index 30f8e354..ac0eae77 100644 --- a/src/extensions/markdown/Deflist/fromYfm.ts +++ b/src/extensions/markdown/Deflist/DeflistSpecs/fromYfm.ts @@ -1,4 +1,4 @@ -import type {ParserToken} from '../../../core'; +import type {ParserToken} from '../../../../core'; import {DeflistNode} from './const'; export const fromYfm: Record = { diff --git a/src/extensions/markdown/Deflist/DeflistSpecs/index.ts b/src/extensions/markdown/Deflist/DeflistSpecs/index.ts new file mode 100644 index 00000000..2ccc8955 --- /dev/null +++ b/src/extensions/markdown/Deflist/DeflistSpecs/index.ts @@ -0,0 +1,42 @@ +import type {PluginSimple} from 'markdown-it'; +import type {NodeSpec} from 'prosemirror-model'; +import type {ExtensionAuto} from '../../../../core'; +import {nodeTypeFactory} from '../../../../utils/schema'; +import {DeflistNode} from './const'; +import {fromYfm} from './fromYfm'; +import {getSpec} from './spec'; +import {toYfm} from './toYfm'; + +const mdPlugin: PluginSimple = require('markdown-it-deflist'); + +export {DeflistNode} from './const'; +export const defListType = nodeTypeFactory(DeflistNode.List); +export const defTermType = nodeTypeFactory(DeflistNode.Term); +export const defDescType = nodeTypeFactory(DeflistNode.Desc); + +export type DeflistSpecsOptions = { + deflistTermPlaceholder?: NonNullable['content']; + deflistDescPlaceholder?: NonNullable['content']; +}; + +export const DeflistSpecs: ExtensionAuto = (builder, opts) => { + const spec = getSpec(opts); + + builder.configureMd((md) => md.use(mdPlugin)); + builder + .addNode(DeflistNode.List, () => ({ + spec: spec[DeflistNode.List], + fromYfm: {tokenSpec: fromYfm[DeflistNode.List]}, + toYfm: toYfm[DeflistNode.List], + })) + .addNode(DeflistNode.Term, () => ({ + spec: spec[DeflistNode.Term], + fromYfm: {tokenSpec: fromYfm[DeflistNode.Term]}, + toYfm: toYfm[DeflistNode.Term], + })) + .addNode(DeflistNode.Desc, () => ({ + spec: spec[DeflistNode.Desc], + fromYfm: {tokenSpec: fromYfm[DeflistNode.Desc]}, + toYfm: toYfm[DeflistNode.Desc], + })); +}; diff --git a/src/extensions/markdown/Deflist/spec.ts b/src/extensions/markdown/Deflist/DeflistSpecs/spec.ts similarity index 82% rename from src/extensions/markdown/Deflist/spec.ts rename to src/extensions/markdown/Deflist/DeflistSpecs/spec.ts index 5555d884..51a5e5f7 100644 --- a/src/extensions/markdown/Deflist/spec.ts +++ b/src/extensions/markdown/Deflist/DeflistSpecs/spec.ts @@ -1,4 +1,5 @@ import type {NodeSpec} from 'prosemirror-model'; +import type {DeflistSpecsOptions} from './index'; import {DeflistNode} from './const'; const DEFAULT_PLACEHOLDERS = { @@ -6,12 +7,7 @@ const DEFAULT_PLACEHOLDERS = { Desc: 'Definition description', }; -export type DeflistSpecOptions = { - deflistTermPlaceholder?: NonNullable['content']; - deflistDescPlaceholder?: NonNullable['content']; -}; - -export const getSpec = (opts?: DeflistSpecOptions): Record => ({ +export const getSpec = (opts?: DeflistSpecsOptions): Record => ({ [DeflistNode.List]: { group: 'block', content: `(${DeflistNode.Term} ${DeflistNode.Desc})+`, diff --git a/src/extensions/markdown/Deflist/toYfm.ts b/src/extensions/markdown/Deflist/DeflistSpecs/toYfm.ts similarity index 88% rename from src/extensions/markdown/Deflist/toYfm.ts rename to src/extensions/markdown/Deflist/DeflistSpecs/toYfm.ts index 578954f8..73bd25c3 100644 --- a/src/extensions/markdown/Deflist/toYfm.ts +++ b/src/extensions/markdown/Deflist/DeflistSpecs/toYfm.ts @@ -1,4 +1,4 @@ -import type {SerializerNodeToken} from '../../../core'; +import type {SerializerNodeToken} from '../../../../core'; import {DeflistNode} from './const'; export const toYfm: Record = { diff --git a/src/extensions/markdown/Deflist/const.ts b/src/extensions/markdown/Deflist/const.ts index ab6e1501..3413112a 100644 --- a/src/extensions/markdown/Deflist/const.ts +++ b/src/extensions/markdown/Deflist/const.ts @@ -1,7 +1,3 @@ -export enum DeflistNode { - List = 'dl', - Term = 'dt', - Desc = 'dd', -} +export {DeflistNode} from './DeflistSpecs'; export const dlAction = 'toDefList'; diff --git a/src/extensions/markdown/Deflist/index.ts b/src/extensions/markdown/Deflist/index.ts index 8966f4b3..6f0f8d4d 100644 --- a/src/extensions/markdown/Deflist/index.ts +++ b/src/extensions/markdown/Deflist/index.ts @@ -1,34 +1,14 @@ -import type {PluginSimple} from 'markdown-it'; import type {Action, ExtensionAuto} from '../../../core'; -import {DeflistNode, dlAction} from './const'; +import {DeflistSpecs, DeflistSpecsOptions} from './DeflistSpecs'; import {splitDeflist, wrapToDeflist} from './commands'; -import {fromYfm} from './fromYfm'; -import {toYfm} from './toYfm'; -import {DeflistSpecOptions, getSpec} from './spec'; -const mdPlugin: PluginSimple = require('markdown-it-deflist'); +import {dlAction} from './const'; -export type DeflistOptions = DeflistSpecOptions & {}; +export {DeflistNode, defListType, defTermType, defDescType} from './DeflistSpecs'; -export const Deflist: ExtensionAuto = (builder, opts) => { - const spec = getSpec(opts); +export type DeflistOptions = DeflistSpecsOptions & {}; - builder.configureMd((md) => md.use(mdPlugin)); - builder - .addNode(DeflistNode.List, () => ({ - spec: spec[DeflistNode.List], - fromYfm: {tokenSpec: fromYfm[DeflistNode.List]}, - toYfm: toYfm[DeflistNode.List], - })) - .addNode(DeflistNode.Term, () => ({ - spec: spec[DeflistNode.Term], - fromYfm: {tokenSpec: fromYfm[DeflistNode.Term]}, - toYfm: toYfm[DeflistNode.Term], - })) - .addNode(DeflistNode.Desc, () => ({ - spec: spec[DeflistNode.Desc], - fromYfm: {tokenSpec: fromYfm[DeflistNode.Desc]}, - toYfm: toYfm[DeflistNode.Desc], - })); +export const Deflist: ExtensionAuto = (builder, opts) => { + builder.use(DeflistSpecs, opts); builder.addKeymap(() => ({Enter: splitDeflist})); diff --git a/src/extensions/markdown/Deflist/utils.ts b/src/extensions/markdown/Deflist/utils.ts index 2eb7c87b..ede60ea8 100644 --- a/src/extensions/markdown/Deflist/utils.ts +++ b/src/extensions/markdown/Deflist/utils.ts @@ -1,6 +1,10 @@ -import {nodeTypeFactory} from '../../../utils/schema'; -import {DeflistNode} from './const'; +import {defDescType, defListType, defTermType} from './DeflistSpecs'; -export const listType = nodeTypeFactory(DeflistNode.List); -export const termType = nodeTypeFactory(DeflistNode.Term); -export const descType = nodeTypeFactory(DeflistNode.Desc); +/** @deprecated Use `defListType` instead */ +export const listType = defListType; +/** @deprecated Use `defTermType` instead */ +export const termType = defTermType; +/** @deprecated Use `defDescType` instead */ +export const descType = defDescType; + +export {defDescType, defListType, defTermType} from './DeflistSpecs'; diff --git a/src/extensions/markdown/Heading/Heading.test.ts b/src/extensions/markdown/Heading/Heading.test.ts index 04e1024a..e516ebf5 100644 --- a/src/extensions/markdown/Heading/Heading.test.ts +++ b/src/extensions/markdown/Heading/Heading.test.ts @@ -2,26 +2,26 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {bold, Bold} from '../Bold'; -import {Heading} from './index'; -import {heading, lvlAttr} from './const'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {boldMarkName, BoldSpecs} from '../Bold/BoldSpecs'; +import {HeadingSpecs} from './HeadingSpecs'; +import {headingNodeName, headingLevelAttr} from './const'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Heading, {}).use(Bold, {}), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(HeadingSpecs, {}).use(BoldSpecs), }).buildDeps(); const {doc, b, p, h, h1, h2, h3, h4, h5, h6} = builders(schema, { doc: {nodeType: BaseNode.Doc}, - b: {nodeType: bold}, + b: {nodeType: boldMarkName}, p: {nodeType: BaseNode.Paragraph}, - h: {nodeType: heading}, - h1: {nodeType: heading, [lvlAttr]: 1}, - h2: {nodeType: heading, [lvlAttr]: 2}, - h3: {nodeType: heading, [lvlAttr]: 3}, - h4: {nodeType: heading, [lvlAttr]: 4}, - h5: {nodeType: heading, [lvlAttr]: 5}, - h6: {nodeType: heading, [lvlAttr]: 6}, + h: {nodeType: headingNodeName}, + h1: {nodeType: headingNodeName, [headingLevelAttr]: 1}, + h2: {nodeType: headingNodeName, [headingLevelAttr]: 2}, + h3: {nodeType: headingNodeName, [headingLevelAttr]: 3}, + h4: {nodeType: headingNodeName, [headingLevelAttr]: 4}, + h5: {nodeType: headingNodeName, [headingLevelAttr]: 5}, + h6: {nodeType: headingNodeName, [headingLevelAttr]: 6}, }) as PMTestBuilderResult<'doc' | 'p' | 'h' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6', 'b'>; const {same} = createMarkupChecker({parser, serializer}); @@ -79,7 +79,7 @@ describe('Heading extension', () => { parseDOM( schema, `Heading ${lvl}`, - doc(h({[lvlAttr]: lvl}, `Heading ${lvl}`)), + doc(h({[headingLevelAttr]: lvl}, `Heading ${lvl}`)), ); }); }); diff --git a/src/extensions/markdown/Heading/HeadingSpecs/index.ts b/src/extensions/markdown/Heading/HeadingSpecs/index.ts new file mode 100644 index 00000000..8d123200 --- /dev/null +++ b/src/extensions/markdown/Heading/HeadingSpecs/index.ts @@ -0,0 +1,53 @@ +import type {Node, NodeSpec} from 'prosemirror-model'; +import type {ExtensionAuto} from '../../../../core'; +import {nodeTypeFactory} from '../../../../utils/schema'; + +export const headingNodeName = 'heading'; +export const headingLevelAttr = 'level'; +export const headingType = nodeTypeFactory(headingNodeName); + +const DEFAULT_PLACEHOLDER = (node: Node) => 'Heading ' + node.attrs[headingLevelAttr]; + +export type HeadingSpecsOptions = { + headingPlaceholder?: NonNullable['content']; +}; + +export const HeadingSpecs: ExtensionAuto = (builder, opts) => { + const {headingPlaceholder} = opts ?? {}; + + builder.addNode(headingNodeName, () => ({ + spec: { + attrs: {[headingLevelAttr]: {default: 1}}, + content: '(text | inline)*', + group: 'block', + defining: true, + parseDOM: [ + {tag: 'h1', attrs: {[headingLevelAttr]: 1}}, + {tag: 'h2', attrs: {[headingLevelAttr]: 2}}, + {tag: 'h3', attrs: {[headingLevelAttr]: 3}}, + {tag: 'h4', attrs: {[headingLevelAttr]: 4}}, + {tag: 'h5', attrs: {[headingLevelAttr]: 5}}, + {tag: 'h6', attrs: {[headingLevelAttr]: 6}}, + ], + toDOM(node) { + return ['h' + node.attrs[headingLevelAttr], 0]; + }, + placeholder: { + content: headingPlaceholder ?? DEFAULT_PLACEHOLDER, + alwaysVisible: true, + }, + }, + fromYfm: { + tokenSpec: { + name: headingNodeName, + type: 'block', + getAttrs: (tok) => ({[headingLevelAttr]: Number(tok.tag.slice(1))}), + }, + }, + toYfm: (state, node) => { + state.write(state.repeat('#', node.attrs[headingLevelAttr]) + ' '); + state.renderInline(node); + state.closeBlock(node); + }, + })); +}; diff --git a/src/extensions/markdown/Heading/actions.ts b/src/extensions/markdown/Heading/actions.ts index cb7de99d..1c009482 100644 --- a/src/extensions/markdown/Heading/actions.ts +++ b/src/extensions/markdown/Heading/actions.ts @@ -1,11 +1,11 @@ import {setBlockType} from 'prosemirror-commands'; import type {NodeType} from 'prosemirror-model'; import type {ActionSpec} from '../../../core'; -import {HeadingLevel, lvlAttr} from './const'; +import {HeadingLevel, headingLevelAttr} from './const'; import {hasParentHeading} from './utils'; export const headingAction = (nodeType: NodeType, level: HeadingLevel): ActionSpec => { - const cmd = setBlockType(nodeType, {[lvlAttr]: level}); + const cmd = setBlockType(nodeType, {[headingLevelAttr]: level}); return { isActive: hasParentHeading(level), isEnable: cmd, diff --git a/src/extensions/markdown/Heading/const.ts b/src/extensions/markdown/Heading/const.ts index 1b120d19..4ec129c4 100644 --- a/src/extensions/markdown/Heading/const.ts +++ b/src/extensions/markdown/Heading/const.ts @@ -1,5 +1,4 @@ -export const heading = 'heading'; -export const lvlAttr = 'level'; +export {headingNodeName, headingLevelAttr} from './HeadingSpecs'; export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; diff --git a/src/extensions/markdown/Heading/index.ts b/src/extensions/markdown/Heading/index.ts index bbb34c2a..95b0696a 100644 --- a/src/extensions/markdown/Heading/index.ts +++ b/src/extensions/markdown/Heading/index.ts @@ -1,70 +1,32 @@ -import type {Node, NodeSpec} from 'prosemirror-model'; import {setBlockType} from 'prosemirror-commands'; import type {Action, ExtensionAuto, Keymap} from '../../../core'; import {headingAction} from './actions'; -import {HeadingAction, HeadingLevel, heading, lvlAttr} from './const'; +import {HeadingAction, HeadingLevel, headingLevelAttr} from './const'; import {headingRule, hType} from './utils'; import {resetHeading} from './commands'; +import {HeadingSpecs, HeadingSpecsOptions} from './HeadingSpecs'; +export {headingNodeName, headingType} from './HeadingSpecs'; export {HeadingAction} from './const'; export {hType} from './utils'; -const DEFAULT_PLACEHOLDER = (node: Node) => 'Heading ' + node.attrs[lvlAttr]; - -export type HeadingOptions = { +export type HeadingOptions = HeadingSpecsOptions & { h1Key?: string | null; h2Key?: string | null; h3Key?: string | null; h4Key?: string | null; h5Key?: string | null; h6Key?: string | null; - headingPlaceholder?: NonNullable['content']; }; export const Heading: ExtensionAuto = (builder, opts) => { - const {headingPlaceholder} = opts ?? {}; - - builder.addNode(heading, () => ({ - spec: { - attrs: {[lvlAttr]: {default: 1}}, - content: '(text | inline)*', - group: 'block', - defining: true, - parseDOM: [ - {tag: 'h1', attrs: {[lvlAttr]: 1}}, - {tag: 'h2', attrs: {[lvlAttr]: 2}}, - {tag: 'h3', attrs: {[lvlAttr]: 3}}, - {tag: 'h4', attrs: {[lvlAttr]: 4}}, - {tag: 'h5', attrs: {[lvlAttr]: 5}}, - {tag: 'h6', attrs: {[lvlAttr]: 6}}, - ], - toDOM(node) { - return ['h' + node.attrs[lvlAttr], 0]; - }, - placeholder: { - content: headingPlaceholder ?? DEFAULT_PLACEHOLDER, - alwaysVisible: true, - }, - }, - fromYfm: { - tokenSpec: { - name: heading, - type: 'block', - getAttrs: (tok) => ({[lvlAttr]: Number(tok.tag.slice(1))}), - }, - }, - toYfm: (state, node) => { - state.write(state.repeat('#', node.attrs[lvlAttr]) + ' '); - state.renderInline(node); - state.closeBlock(node); - }, - })); + builder.use(HeadingSpecs, opts); builder .addKeymap(({schema}) => { const {h1Key, h2Key, h3Key, h4Key, h5Key, h6Key} = opts ?? {}; const cmd4lvl = (level: HeadingLevel) => - setBlockType(hType(schema), {[lvlAttr]: level}); + setBlockType(hType(schema), {[headingLevelAttr]: level}); const bindings: Keymap = {Backspace: resetHeading}; if (h1Key) bindings[h1Key] = cmd4lvl(1); diff --git a/src/extensions/markdown/Heading/utils.ts b/src/extensions/markdown/Heading/utils.ts index 2ccea348..7574f00c 100644 --- a/src/extensions/markdown/Heading/utils.ts +++ b/src/extensions/markdown/Heading/utils.ts @@ -2,14 +2,15 @@ import {textblockTypeInputRule} from 'prosemirror-inputrules'; import type {NodeType} from 'prosemirror-model'; import type {EditorState} from 'prosemirror-state'; import {hasParentNode} from 'prosemirror-utils'; -import {nodeTypeFactory} from '../../../utils/schema'; -import {HeadingLevel, heading, lvlAttr} from './const'; +import {HeadingLevel, headingLevelAttr} from './const'; +import {headingType} from './HeadingSpecs'; -export const hType = nodeTypeFactory(heading); +/** @deprecated Use `headingType` instead */ +export const hType = headingType; export const hasParentHeading = (level: HeadingLevel) => (state: EditorState) => hasParentNode((node) => { - return node.type === hType(state.schema) && node.attrs[lvlAttr] === level; + return node.type === headingType(state.schema) && node.attrs[headingLevelAttr] === level; })(state.selection); // Given a node type and a maximum level, creates an input rule that @@ -20,6 +21,6 @@ export function headingRule(nodeType: NodeType, maxLevel: HeadingLevel) { return textblockTypeInputRule( new RegExp('^(#{1,' + maxLevel + '})\\s$'), nodeType, - (match) => ({[lvlAttr]: match[1].length}), + (match) => ({[headingLevelAttr]: match[1].length}), ); } diff --git a/src/extensions/markdown/HorizontalRule/HorizontalRule.test.ts b/src/extensions/markdown/HorizontalRule/HorizontalRule.test.ts index 27579c50..9fd76629 100644 --- a/src/extensions/markdown/HorizontalRule/HorizontalRule.test.ts +++ b/src/extensions/markdown/HorizontalRule/HorizontalRule.test.ts @@ -2,19 +2,23 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {horizontalRule, HorizontalRule, markupAttr} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import { + horizontalRuleMarkupAttr, + horizontalRuleNodeName, + HorizontalRuleSpecs, +} from './HorizontalRuleSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(HorizontalRule), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(HorizontalRuleSpecs), }).buildDeps(); const {doc, p, hr, hr2, hr3} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - hr: {nodeType: horizontalRule}, - hr2: {nodeType: horizontalRule, [markupAttr]: '___'}, - hr3: {nodeType: horizontalRule, [markupAttr]: '***'}, + hr: {nodeType: horizontalRuleNodeName}, + hr2: {nodeType: horizontalRuleNodeName, [horizontalRuleMarkupAttr]: '___'}, + hr3: {nodeType: horizontalRuleNodeName, [horizontalRuleMarkupAttr]: '***'}, }) as PMTestBuilderResult<'doc' | 'p' | 'hr' | 'hr2' | 'hr3'>; const {same} = createMarkupChecker({parser, serializer}); diff --git a/src/extensions/markdown/HorizontalRule/HorizontalRuleSpecs/index.ts b/src/extensions/markdown/HorizontalRule/HorizontalRuleSpecs/index.ts new file mode 100644 index 00000000..5c838080 --- /dev/null +++ b/src/extensions/markdown/HorizontalRule/HorizontalRuleSpecs/index.ts @@ -0,0 +1,31 @@ +import type {ExtensionAuto} from '../../../../core'; +import {nodeTypeFactory} from '../../../../utils/schema'; + +export const horizontalRuleNodeName = 'horizontal_rule'; +export const horizontalRuleMarkupAttr = 'markup'; +export const horizontalRuleType = nodeTypeFactory(horizontalRuleNodeName); + +export const HorizontalRuleSpecs: ExtensionAuto = (builder) => { + builder.addNode(horizontalRuleNodeName, () => ({ + spec: { + attrs: {[horizontalRuleMarkupAttr]: {default: '---'}}, + group: 'block', + parseDOM: [{tag: 'hr'}], + toDOM() { + return ['div', ['hr']]; + }, + }, + fromYfm: { + tokenName: 'hr', + tokenSpec: { + name: horizontalRuleNodeName, + type: 'node', + getAttrs: (token) => ({[horizontalRuleMarkupAttr]: token.markup}), + }, + }, + toYfm: (state, node) => { + state.write(node.attrs[horizontalRuleMarkupAttr]); + state.closeBlock(node); + }, + })); +}; diff --git a/src/extensions/markdown/HorizontalRule/index.ts b/src/extensions/markdown/HorizontalRule/index.ts index 9526e17e..470bce81 100644 --- a/src/extensions/markdown/HorizontalRule/index.ts +++ b/src/extensions/markdown/HorizontalRule/index.ts @@ -1,38 +1,31 @@ import type {NodeType} from 'prosemirror-model'; -import {Command, NodeSelection, Selection} from 'prosemirror-state'; +import type {Command, Selection} from 'prosemirror-state'; import type {Action, ExtensionAuto} from '../../../core'; -import {nodeTypeFactory} from '../../../utils/schema'; import {nodeInputRule} from '../../../utils/inputrules'; -import {pType} from '../../base/BaseSchema'; +import {isNodeSelection} from '../../../utils/selection'; +import {pType} from '../../base/BaseSchema/BaseSchemaSpecs'; +import { + horizontalRuleMarkupAttr, + horizontalRuleNodeName, + HorizontalRuleSpecs, + horizontalRuleType, +} from './HorizontalRuleSpecs'; -export const horizontalRule = 'horizontal_rule'; +export { + horizontalRuleMarkupAttr, + horizontalRuleNodeName, + horizontalRuleType, +} from './HorizontalRuleSpecs'; +/** @deprecated Use `horizontalRuleNodeName` instead */ +export const horizontalRule = horizontalRuleNodeName; const hrAction = 'hRule'; -export const markupAttr = 'markup'; -const hrType = nodeTypeFactory(horizontalRule); +/** @deprecated Use `horizontalRuleMarkupAttr` instead */ +export const markupAttr = horizontalRuleMarkupAttr; +/** @deprecated Use `horizontalRuleType` instead */ +const hrType = horizontalRuleType; export const HorizontalRule: ExtensionAuto = (builder) => { - builder.addNode(horizontalRule, () => ({ - spec: { - attrs: {[markupAttr]: {default: '---'}}, - group: 'block', - parseDOM: [{tag: 'hr'}], - toDOM() { - return ['div', ['hr']]; - }, - }, - fromYfm: { - tokenName: 'hr', - tokenSpec: { - name: horizontalRule, - type: 'node', - getAttrs: (token) => ({[markupAttr]: token.markup}), - }, - }, - toYfm: (state, node) => { - state.write(node.attrs[markupAttr]); - state.closeBlock(node); - }, - })); + builder.use(HorizontalRuleSpecs); builder.addInputRules((deps) => ({ rules: [ @@ -41,7 +34,7 @@ export const HorizontalRule: ExtensionAuto = (builder) => { nodeInputRule( /^(---|___|\*\*\*)$/, (markup) => [ - hrType(deps.schema).create({[markupAttr]: markup}), + hrType(deps.schema).create({[horizontalRuleMarkupAttr]: markup}), pType(deps.schema).create(), ], 1, @@ -78,5 +71,5 @@ const addHr = }; function isHrSelection(selection: Selection) { - return selection instanceof NodeSelection && selection.node.type.name === horizontalRule; + return isNodeSelection(selection) && selection.node.type.name === horizontalRuleNodeName; } diff --git a/src/extensions/markdown/Html/index.ts b/src/extensions/markdown/Html/index.ts index 1103e05d..c111be08 100644 --- a/src/extensions/markdown/Html/index.ts +++ b/src/extensions/markdown/Html/index.ts @@ -5,6 +5,8 @@ import {fromYfm} from './fromYfm'; import {spec} from './spec'; import {toYfm} from './toYfm'; +export {HtmlNode} from './const'; + export const Html: ExtensionAuto = (builder) => { if (builder.context.has('html') && builder.context.get('html') === false) { logger.info('[HTML extension]: Skip extension, because HTML disabled via context'); diff --git a/src/extensions/markdown/Image/Image.test.ts b/src/extensions/markdown/Image/Image.test.ts index 5c0af0ff..04ea2e58 100644 --- a/src/extensions/markdown/Image/Image.test.ts +++ b/src/extensions/markdown/Image/Image.test.ts @@ -1,20 +1,19 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {Image} from './index'; -import {image, ImageAttr} from './const'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {ImageAttr, imageNodeName, ImageSpecs} from './ImageSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Image), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(ImageSpecs), }).buildDeps(); const {doc, p, img, img2} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - img: {nodeType: image, [ImageAttr.Src]: 'img.png'}, + img: {nodeType: imageNodeName, [ImageAttr.Src]: 'img.png'}, img2: { - nodeType: image, + nodeType: imageNodeName, [ImageAttr.Src]: 'img2.png', [ImageAttr.Alt]: 'alt text', [ImageAttr.Title]: 'title text', diff --git a/src/extensions/markdown/Image/ImageSpecs/index.ts b/src/extensions/markdown/Image/ImageSpecs/index.ts new file mode 100644 index 00000000..2fbef404 --- /dev/null +++ b/src/extensions/markdown/Image/ImageSpecs/index.ts @@ -0,0 +1,62 @@ +import type {ExtensionAuto} from '../../../../core'; +import {nodeTypeFactory} from '../../../../utils/schema'; + +export const imageNodeName = 'image'; +export const imageType = nodeTypeFactory(imageNodeName); + +export const ImageAttr = { + Src: 'src', + Alt: 'alt', + Title: 'title', +} as const; + +export const ImageSpecs: ExtensionAuto = (builder) => { + builder.addNode(imageNodeName, () => ({ + spec: { + inline: true, + attrs: { + [ImageAttr.Src]: {}, + [ImageAttr.Alt]: {default: null}, + [ImageAttr.Title]: {default: null}, + }, + group: 'inline', + draggable: true, + parseDOM: [ + { + tag: 'img[src]', + getAttrs(dom) { + return { + [ImageAttr.Src]: (dom as Element).getAttribute(ImageAttr.Src), + [ImageAttr.Alt]: (dom as Element).getAttribute(ImageAttr.Alt), + [ImageAttr.Title]: (dom as Element).getAttribute(ImageAttr.Title), + }; + }, + }, + ], + toDOM(node) { + return ['img', node.attrs]; + }, + }, + fromYfm: { + tokenSpec: { + name: imageNodeName, + type: 'node', + getAttrs: (tok) => ({ + [ImageAttr.Src]: tok.attrGet('src'), + [ImageAttr.Title]: tok.attrGet('title') || null, + [ImageAttr.Alt]: tok.children?.[0]?.content || null, + }), + }, + }, + toYfm: (state, {attrs}) => { + state.write( + '![' + + state.esc(attrs.alt || '') + + '](' + + state.esc(attrs.src) + + (attrs.title ? ' ' + state.quote(attrs.title) : '') + + ')', + ); + }, + })); +}; diff --git a/src/extensions/markdown/Image/actions.ts b/src/extensions/markdown/Image/actions.ts index 77a57ff4..a604364c 100644 --- a/src/extensions/markdown/Image/actions.ts +++ b/src/extensions/markdown/Image/actions.ts @@ -1,7 +1,7 @@ import type {Schema} from 'prosemirror-model'; import type {ActionSpec} from '../../../core'; +import {imageType} from './ImageSpecs'; import {ImageAttr} from './const'; -import {imgType} from './utils'; export type AddImageAttrs = { src: string; @@ -23,7 +23,7 @@ export const addImage = (schema: Schema): ActionSpec => { [ImageAttr.Alt]: alt ?? '', }; - dispatch(state.tr.insert(state.selection.from, imgType(schema).create(imgAttrs))); + dispatch(state.tr.insert(state.selection.from, imageType(schema).create(imgAttrs))); } }, }; diff --git a/src/extensions/markdown/Image/const.ts b/src/extensions/markdown/Image/const.ts index 9ef652cf..e9aeb5dc 100644 --- a/src/extensions/markdown/Image/const.ts +++ b/src/extensions/markdown/Image/const.ts @@ -1,9 +1,3 @@ -export const image = 'image'; +export {imageNodeName, ImageAttr} from './ImageSpecs'; export const addImageAction = 'addImage'; - -export const ImageAttr = { - Src: 'src', - Alt: 'alt', - Title: 'title', -} as const; diff --git a/src/extensions/markdown/Image/index.ts b/src/extensions/markdown/Image/index.ts index 733ed09a..cb96c20e 100644 --- a/src/extensions/markdown/Image/index.ts +++ b/src/extensions/markdown/Image/index.ts @@ -1,59 +1,15 @@ import type {Action, ExtensionAuto} from '../../../core'; import {addImage, AddImageAttrs} from './actions'; -import {addImageAction, image, ImageAttr} from './const'; +import {addImageAction} from './const'; +import {ImageSpecs, imageType} from './ImageSpecs'; -export {imgType} from './utils'; +export {imageNodeName, imageType, ImageAttr} from './ImageSpecs'; +/** @deprecated Use `imageType` instead */ +export const imgType = imageType; export type {AddImageAttrs} from './actions'; export const Image: ExtensionAuto = (builder) => { - builder.addNode(image, () => ({ - spec: { - inline: true, - attrs: { - [ImageAttr.Src]: {}, - [ImageAttr.Alt]: {default: null}, - [ImageAttr.Title]: {default: null}, - }, - group: 'inline', - draggable: true, - parseDOM: [ - { - tag: 'img[src]', - getAttrs(dom) { - return { - [ImageAttr.Src]: (dom as Element).getAttribute(ImageAttr.Src), - [ImageAttr.Alt]: (dom as Element).getAttribute(ImageAttr.Alt), - [ImageAttr.Title]: (dom as Element).getAttribute(ImageAttr.Title), - }; - }, - }, - ], - toDOM(node) { - return ['img', node.attrs]; - }, - }, - fromYfm: { - tokenSpec: { - name: image, - type: 'node', - getAttrs: (tok) => ({ - [ImageAttr.Src]: tok.attrGet('src'), - [ImageAttr.Title]: tok.attrGet('title') || null, - [ImageAttr.Alt]: tok.children?.[0]?.content || null, - }), - }, - }, - toYfm: (state, {attrs}) => { - state.write( - '![' + - state.esc(attrs.alt || '') + - '](' + - state.esc(attrs.src) + - (attrs.title ? ' ' + state.quote(attrs.title) : '') + - ')', - ); - }, - })); + builder.use(ImageSpecs); builder.addAction(addImageAction, ({schema}) => addImage(schema)); }; diff --git a/src/extensions/markdown/Image/utils.ts b/src/extensions/markdown/Image/utils.ts deleted file mode 100644 index 6eefe6d6..00000000 --- a/src/extensions/markdown/Image/utils.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {nodeTypeFactory} from '../../../utils/schema'; -import {image} from './const'; - -export const imgType = nodeTypeFactory(image); diff --git a/src/extensions/markdown/Italic/Italic.test.ts b/src/extensions/markdown/Italic/Italic.test.ts index 1d986a30..63e58d43 100644 --- a/src/extensions/markdown/Italic/Italic.test.ts +++ b/src/extensions/markdown/Italic/Italic.test.ts @@ -2,17 +2,17 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {italic, Italic} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {italicMarkName, ItalicSpecs} from './ItalicSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Italic, {}), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(ItalicSpecs), }).buildDeps(); const {doc, p, i} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - i: {markType: italic}, + i: {markType: italicMarkName}, }) as PMTestBuilderResult<'doc' | 'p', 'i'>; const {same} = createMarkupChecker({parser, serializer}); diff --git a/src/extensions/markdown/Italic/ItalicSpecs/index.ts b/src/extensions/markdown/Italic/ItalicSpecs/index.ts new file mode 100644 index 00000000..567dc688 --- /dev/null +++ b/src/extensions/markdown/Italic/ItalicSpecs/index.ts @@ -0,0 +1,22 @@ +import type {ExtensionAuto} from '../../../../core'; +import {markTypeFactory} from '../../../../utils/schema'; + +export const italicMarkName = 'em'; +export const italicType = markTypeFactory(italicMarkName); + +export const ItalicSpecs: ExtensionAuto = (builder) => { + builder.addMark(italicMarkName, () => ({ + spec: { + parseDOM: [ + {tag: 'i'}, + {tag: 'em'}, + {style: 'font-style', getAttrs: (value) => value === 'italic' && null}, + ], + toDOM() { + return ['em']; + }, + }, + toYfm: {open: '*', close: '*', mixable: true, expelEnclosingWhitespace: true}, + fromYfm: {tokenSpec: {name: italicMarkName, type: 'mark'}}, + })); +}; diff --git a/src/extensions/markdown/Italic/index.ts b/src/extensions/markdown/Italic/index.ts index fd4127f8..1a4338f9 100644 --- a/src/extensions/markdown/Italic/index.ts +++ b/src/extensions/markdown/Italic/index.ts @@ -1,45 +1,34 @@ import {toggleMark} from 'prosemirror-commands'; import {createToggleMarkAction} from '../../../utils/actions'; import type {Action, ExtensionAuto} from '../../../core'; -import {markTypeFactory} from '../../../utils/schema'; import {markInputRule} from '../../../utils/inputrules'; -export const italic = 'em'; +import {italicMarkName, ItalicSpecs, italicType} from './ItalicSpecs'; + +export {italicMarkName, italicType} from './ItalicSpecs'; +/** @deprecated Use `italicMarkName` instead */ +export const italic = italicMarkName; const iAction = 'italic'; -const iType = markTypeFactory(italic); export type ItalicOptions = { italicKey?: string | null; }; export const Italic: ExtensionAuto = (builder, opts) => { - builder - .addMark(italic, () => ({ - spec: { - parseDOM: [ - {tag: 'i'}, - {tag: 'em'}, - {style: 'font-style', getAttrs: (value) => value === 'italic' && null}, - ], - toDOM() { - return ['em']; - }, - }, - toYfm: {open: '*', close: '*', mixable: true, expelEnclosingWhitespace: true}, - fromYfm: {tokenSpec: {name: italic, type: 'mark'}}, - })) - .addAction(iAction, ({schema}) => createToggleMarkAction(iType(schema))); + builder.use(ItalicSpecs); + + builder.addAction(iAction, ({schema}) => createToggleMarkAction(italicType(schema))); builder.addInputRules(({schema}) => ({ rules: [ - markInputRule({open: '*', close: '*', ignoreBetween: '*'}, iType(schema)), - markInputRule({open: '_', close: '_', ignoreBetween: '_'}, iType(schema)), + markInputRule({open: '*', close: '*', ignoreBetween: '*'}, italicType(schema)), + markInputRule({open: '_', close: '_', ignoreBetween: '_'}, italicType(schema)), ], })); if (opts?.italicKey) { const {italicKey} = opts; - builder.addKeymap(({schema}) => ({[italicKey]: toggleMark(iType(schema))})); + builder.addKeymap(({schema}) => ({[italicKey]: toggleMark(italicType(schema))})); } }; diff --git a/src/extensions/markdown/Link/Link.test.ts b/src/extensions/markdown/Link/Link.test.ts index 4d0fb4cc..e49f9e31 100644 --- a/src/extensions/markdown/Link/Link.test.ts +++ b/src/extensions/markdown/Link/Link.test.ts @@ -1,20 +1,20 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {link, Link, LinkAttr} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {LinkSpecs, LinkAttr, linkMarkName} from './LinkSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Link), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(LinkSpecs), }).buildDeps(); const {doc, p, a, lnk, lnk4} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - a: {nodeType: link}, - lnk: {nodeType: link, [LinkAttr.Href]: 'ya.ru'}, + a: {nodeType: linkMarkName}, + lnk: {nodeType: linkMarkName, [LinkAttr.Href]: 'ya.ru'}, lnk4: { - nodeType: link, + nodeType: linkMarkName, [LinkAttr.Href]: '4chan.org', [LinkAttr.Title]: '4chan', }, diff --git a/src/extensions/markdown/Link/LinkSpecs/index.ts b/src/extensions/markdown/Link/LinkSpecs/index.ts new file mode 100644 index 00000000..a669f75f --- /dev/null +++ b/src/extensions/markdown/Link/LinkSpecs/index.ts @@ -0,0 +1,98 @@ +import type {Fragment, Mark} from 'prosemirror-model'; +import type {ExtensionAuto} from '../../../../core'; +import {markTypeFactory} from '../../../../utils/schema'; + +export const linkMarkName = 'link'; +export const linkType = markTypeFactory(linkMarkName); + +export enum LinkAttr { + Href = 'href', + Title = 'title', + // tech attributes + IsPlaceholder = 'is-placeholder', + RawLink = 'raw-link', +} + +export const LinkSpecs: ExtensionAuto = (builder) => { + builder.addMark(linkMarkName, () => ({ + spec: { + attrs: { + [LinkAttr.Href]: {}, + [LinkAttr.Title]: {default: null}, + [LinkAttr.IsPlaceholder]: {default: false}, + [LinkAttr.RawLink]: {default: false}, + }, + inclusive: false, + parseDOM: [ + { + tag: 'a[href]', + getAttrs(dom) { + return { + href: (dom as Element).getAttribute(LinkAttr.Href), + title: (dom as Element).getAttribute(LinkAttr.Title), + }; + }, + }, + ], + toDOM(node) { + return ['a', node.attrs]; + }, + }, + toYfm: { + open(state, mark, parent, index) { + state.isAutolink = isPlainURL(mark, parent, index, 1); + if (state.isAutolink) { + if (mark.attrs[LinkAttr.RawLink]) return ''; + return '<'; + } + return '['; + }, + close(state, mark) { + if (state.isAutolink) { + state.isAutolink = undefined; + if (mark.attrs[LinkAttr.RawLink]) return ''; + + return '>'; + } + state.isAutolink = undefined; + return ( + '](' + + mark.attrs[LinkAttr.Href] + + (mark.attrs[LinkAttr.Title] + ? ' ' + state.quote(mark.attrs[LinkAttr.Title]) + : '') + + ')' + ); + }, + }, + fromYfm: { + tokenSpec: { + name: linkMarkName, + type: 'mark', + getAttrs: (tok) => ({ + href: tok.attrGet('href'), + title: tok.attrGet('title') || null, + }), + }, + }, + })); +}; + +function isPlainURL(link: Mark, parent: Fragment, index: number, side: number) { + if (link.attrs.title || !/^\w+:/.test(link.attrs[LinkAttr.Href])) return false; + + const content = parent.child(index + (side < 0 ? -1 : 0)); + + if ( + !content.isText || + content.text !== link.attrs[LinkAttr.Href] || + content.marks[content.marks.length - 1] !== link + ) + return false; + + if (index === (side < 0 ? 1 : parent.childCount - 1)) return true; + + const next = parent.child(index + (side < 0 ? -2 : 1)); + + return !link.isInSet(next.marks); +} diff --git a/src/extensions/markdown/Link/actions.ts b/src/extensions/markdown/Link/actions.ts index 809ef0c7..dbcc39c3 100644 --- a/src/extensions/markdown/Link/actions.ts +++ b/src/extensions/markdown/Link/actions.ts @@ -3,7 +3,7 @@ import {TextSelection} from 'prosemirror-state'; import type {MarkType} from 'prosemirror-model'; import type {ActionSpec, ExtensionDeps} from '../../../core'; import {isMarkActive} from '../../../utils/marks'; -import {LinkAttr} from '.'; +import {LinkAttr} from './LinkSpecs'; import {removeLink} from './commands'; import {normalizeUrlFactory} from './utils'; diff --git a/src/extensions/markdown/Link/index.ts b/src/extensions/markdown/Link/index.ts index a749b327..27ec772a 100644 --- a/src/extensions/markdown/Link/index.ts +++ b/src/extensions/markdown/Link/index.ts @@ -1,92 +1,26 @@ import {InputRule} from 'prosemirror-inputrules'; -import type {Fragment, Mark, MarkType} from 'prosemirror-model'; +import type {MarkType} from 'prosemirror-model'; import type {Action, ExtensionAuto} from '../../../core'; -import {markTypeFactory} from '../../../utils/schema'; import {LinkActionMeta, LinkActionParams, linkCommand} from './actions'; import {linkPasteEnhance} from './paste-plugin'; +import {linkMarkName, LinkSpecs, linkType} from './LinkSpecs'; export type {LinkActionParams, LinkActionMeta} from './actions'; export {linkCommand} from './actions'; export {normalizeUrlFactory} from './utils'; export {removeLink} from './commands'; -export const link = 'link'; +export {LinkAttr, linkMarkName, linkType} from './LinkSpecs'; +/** @deprecated Use `linkMarkName` instead */ +export const link = linkMarkName; const linkAction = 'link'; -export const linkType = markTypeFactory(link); -export enum LinkAttr { - Href = 'href', - Title = 'title', - // tech attributes - IsPlaceholder = 'is-placeholder', - RawLink = 'raw-link', -} export const Link: ExtensionAuto = (builder) => { - builder - .addMark(link, () => ({ - spec: { - attrs: { - [LinkAttr.Href]: {}, - [LinkAttr.Title]: {default: null}, - [LinkAttr.IsPlaceholder]: {default: false}, - [LinkAttr.RawLink]: {default: false}, - }, - inclusive: false, - parseDOM: [ - { - tag: 'a[href]', - getAttrs(dom) { - return { - href: (dom as Element).getAttribute(LinkAttr.Href), - title: (dom as Element).getAttribute(LinkAttr.Title), - }; - }, - }, - ], - toDOM(node) { - return ['a', node.attrs]; - }, - }, - toYfm: { - open(state, mark, parent, index) { - state.isAutolink = isPlainURL(mark, parent, index, 1); - if (state.isAutolink) { - if (mark.attrs[LinkAttr.RawLink]) return ''; - return '<'; - } - return '['; - }, - close(state, mark) { - if (state.isAutolink) { - state.isAutolink = undefined; - if (mark.attrs[LinkAttr.RawLink]) return ''; + builder.use(LinkSpecs); - return '>'; - } - state.isAutolink = undefined; - return ( - '](' + - mark.attrs[LinkAttr.Href] + - (mark.attrs[LinkAttr.Title] - ? ' ' + state.quote(mark.attrs[LinkAttr.Title]) - : '') + - ')' - ); - }, - }, - fromYfm: { - tokenSpec: { - name: link, - type: 'mark', - getAttrs: (tok) => ({ - href: tok.attrGet('href'), - title: tok.attrGet('title') || null, - }), - }, - }, - })) - .addPlugin(linkPasteEnhance, builder.PluginPriority.High) + builder + .addPlugin(linkPasteEnhance, builder.Priority.High) .addAction(linkAction, (deps) => linkCommand(linkType(deps.schema), deps)) .addInputRules(({schema}) => ({rules: [linkInputRule(linkType(schema))]})); }; @@ -99,25 +33,6 @@ declare global { } } -function isPlainURL(link: Mark, parent: Fragment, index: number, side: number) { - if (link.attrs.title || !/^\w+:/.test(link.attrs[LinkAttr.Href])) return false; - - const content = parent.child(index + (side < 0 ? -1 : 0)); - - if ( - !content.isText || - content.text !== link.attrs[LinkAttr.Href] || - content.marks[content.marks.length - 1] !== link - ) - return false; - - if (index === (side < 0 ? 1 : parent.childCount - 1)) return true; - - const next = parent.child(index + (side < 0 ? -2 : 1)); - - return !link.isInSet(next.marks); -} - // TODO: think about generalizing with markInputRule function linkInputRule(markType: MarkType): InputRule { return new InputRule(/\[(.+)]\((\S+)\)\s$/, (state, match, start, end) => { diff --git a/src/extensions/markdown/Lists/Lists.test.ts b/src/extensions/markdown/Lists/Lists.test.ts index 4c345d7c..820beee7 100644 --- a/src/extensions/markdown/Lists/Lists.test.ts +++ b/src/extensions/markdown/Lists/Lists.test.ts @@ -1,12 +1,11 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {ListNode} from './const'; -import {Lists} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {ListNode, ListsSpecs} from './ListsSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Lists, {}), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(ListsSpecs), }).buildDeps(); const {doc, p, li, ul, ol} = builders(schema, { diff --git a/src/extensions/markdown/Lists/ListsSpecs/const.ts b/src/extensions/markdown/Lists/ListsSpecs/const.ts new file mode 100644 index 00000000..c853c5f3 --- /dev/null +++ b/src/extensions/markdown/Lists/ListsSpecs/const.ts @@ -0,0 +1,5 @@ +export enum ListNode { + ListItem = 'list_item', + BulletList = 'bullet_list', + OrderedList = 'ordered_list', +} diff --git a/src/extensions/markdown/Lists/fromYfm.ts b/src/extensions/markdown/Lists/ListsSpecs/fromYfm.ts similarity index 94% rename from src/extensions/markdown/Lists/fromYfm.ts rename to src/extensions/markdown/Lists/ListsSpecs/fromYfm.ts index 25dd7f0d..ec3b2704 100644 --- a/src/extensions/markdown/Lists/fromYfm.ts +++ b/src/extensions/markdown/Lists/ListsSpecs/fromYfm.ts @@ -1,5 +1,5 @@ import type Token from 'markdown-it/lib/token'; -import type {ParserToken} from '../../../core'; +import type {ParserToken} from '../../../../core'; import {ListNode} from './const'; export const fromYfm: Record = { diff --git a/src/extensions/markdown/Lists/ListsSpecs/index.ts b/src/extensions/markdown/Lists/ListsSpecs/index.ts new file mode 100644 index 00000000..228ee8e9 --- /dev/null +++ b/src/extensions/markdown/Lists/ListsSpecs/index.ts @@ -0,0 +1,30 @@ +import type {ExtensionAuto} from '../../../../core'; +import {nodeTypeFactory} from '../../../../utils/schema'; +import {ListNode} from './const'; +import {fromYfm} from './fromYfm'; +import {toYfm} from './toYfm'; +import {spec} from './spec'; + +export {ListNode} from './const'; +export const liType = nodeTypeFactory(ListNode.ListItem); +export const blType = nodeTypeFactory(ListNode.BulletList); +export const olType = nodeTypeFactory(ListNode.OrderedList); + +export const ListsSpecs: ExtensionAuto = (builder) => { + builder + .addNode(ListNode.ListItem, () => ({ + spec: spec[ListNode.ListItem], + toYfm: toYfm[ListNode.ListItem], + fromYfm: {tokenSpec: fromYfm[ListNode.ListItem]}, + })) + .addNode(ListNode.BulletList, () => ({ + spec: spec[ListNode.BulletList], + toYfm: toYfm[ListNode.BulletList], + fromYfm: {tokenSpec: fromYfm[ListNode.BulletList]}, + })) + .addNode(ListNode.OrderedList, () => ({ + spec: spec[ListNode.OrderedList], + toYfm: toYfm[ListNode.OrderedList], + fromYfm: {tokenSpec: fromYfm[ListNode.OrderedList]}, + })); +}; diff --git a/src/extensions/markdown/Lists/spec.ts b/src/extensions/markdown/Lists/ListsSpecs/spec.ts similarity index 97% rename from src/extensions/markdown/Lists/spec.ts rename to src/extensions/markdown/Lists/ListsSpecs/spec.ts index 50aa42dc..cb0c03c6 100644 --- a/src/extensions/markdown/Lists/spec.ts +++ b/src/extensions/markdown/Lists/ListsSpecs/spec.ts @@ -1,4 +1,4 @@ -import {NodeSpec} from 'prosemirror-model'; +import type {NodeSpec} from 'prosemirror-model'; import {ListNode} from './const'; export const spec: Record = { diff --git a/src/extensions/markdown/Lists/toYfm.ts b/src/extensions/markdown/Lists/ListsSpecs/toYfm.ts similarity index 92% rename from src/extensions/markdown/Lists/toYfm.ts rename to src/extensions/markdown/Lists/ListsSpecs/toYfm.ts index 48ced30e..dfd54651 100644 --- a/src/extensions/markdown/Lists/toYfm.ts +++ b/src/extensions/markdown/Lists/ListsSpecs/toYfm.ts @@ -1,4 +1,4 @@ -import type {SerializerNodeToken} from '../../../core'; +import type {SerializerNodeToken} from '../../../../core'; import {ListNode} from './const'; export const toYfm: Record = { diff --git a/src/extensions/markdown/Lists/commands.test.ts b/src/extensions/markdown/Lists/commands.test.ts index 2e1967ac..774c9e6b 100644 --- a/src/extensions/markdown/Lists/commands.test.ts +++ b/src/extensions/markdown/Lists/commands.test.ts @@ -1,21 +1,16 @@ -import {Schema} from 'prosemirror-model'; import {EditorView} from 'prosemirror-view'; import {EditorState, TextSelection} from 'prosemirror-state'; import {builders} from 'prosemirror-test-builder'; +import {ExtensionsManager} from '../../../core'; import {get$Cursor} from '../../../utils/selection'; -import {spec} from './spec'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {ListsSpecs} from './ListsSpecs'; import {ListNode} from './const'; import {liftIfCursorIsAtBeginningOfItem, moveTextblockToEndOfLastItemOfPrevList} from './commands'; -const schema = new Schema({ - nodes: { - doc: {content: 'block+'}, - text: {group: 'inline'}, - paragraph: {group: 'block', content: 'text*', toDOM: () => ['p', 0]}, - ...spec, - }, - marks: {}, -}); +const {schema} = new ExtensionsManager({ + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(ListsSpecs), +}).buildDeps(); const { doc, @@ -23,7 +18,7 @@ const { list_item: li, bullet_list: bl, ordered_list: ol, -} = builders(schema) as PMTestBuilderResult<'doc' | 'paragraph' | ListNode>; +} = builders(schema) as PMTestBuilderResult; describe('Lists commands', () => { it.each([ diff --git a/src/extensions/markdown/Lists/const.ts b/src/extensions/markdown/Lists/const.ts index 9da75d15..f1997583 100644 --- a/src/extensions/markdown/Lists/const.ts +++ b/src/extensions/markdown/Lists/const.ts @@ -1,8 +1,4 @@ -export enum ListNode { - ListItem = 'list_item', - BulletList = 'bullet_list', - OrderedList = 'ordered_list', -} +export {ListNode} from './ListsSpecs'; export enum ListAction { ToBulletList = 'toBulletList', diff --git a/src/extensions/markdown/Lists/index.ts b/src/extensions/markdown/Lists/index.ts index 22693b52..3a974f2b 100644 --- a/src/extensions/markdown/Lists/index.ts +++ b/src/extensions/markdown/Lists/index.ts @@ -2,18 +2,17 @@ import {chainCommands} from 'prosemirror-commands'; import {liftListItem, sinkListItem, splitListItem} from 'prosemirror-schema-list'; import type {Action, ExtensionAuto, Keymap} from '../../../core'; import {actions} from './actions'; -import {ListAction, ListNode} from './const'; -import {fromYfm} from './fromYfm'; -import {spec} from './spec'; -import {toYfm} from './toYfm'; +import {ListAction} from './const'; import {ListsInputRulesExtension, ListsInputRulesOptions} from './inputrules'; -import {blType, liType, olType} from './utils'; +import {ListsSpecs, blType, liType, olType} from './ListsSpecs'; import { moveTextblockToEndOfLastItemOfPrevList, liftIfCursorIsAtBeginningOfItem, toList, } from './commands'; +export {ListNode, blType, liType, olType} from './ListsSpecs'; + export type ListsOptions = { ulKey?: string | null; olKey?: string | null; @@ -21,22 +20,7 @@ export type ListsOptions = { }; export const Lists: ExtensionAuto = (builder, opts) => { - builder - .addNode(ListNode.ListItem, () => ({ - spec: spec[ListNode.ListItem], - toYfm: toYfm[ListNode.ListItem], - fromYfm: {tokenSpec: fromYfm[ListNode.ListItem]}, - })) - .addNode(ListNode.BulletList, () => ({ - spec: spec[ListNode.BulletList], - toYfm: toYfm[ListNode.BulletList], - fromYfm: {tokenSpec: fromYfm[ListNode.BulletList]}, - })) - .addNode(ListNode.OrderedList, () => ({ - spec: spec[ListNode.OrderedList], - toYfm: toYfm[ListNode.OrderedList], - fromYfm: {tokenSpec: fromYfm[ListNode.OrderedList]}, - })); + builder.use(ListsSpecs); builder.addKeymap(({schema}) => { const {ulKey, olKey} = opts ?? {}; @@ -62,7 +46,7 @@ export const Lists: ExtensionAuto = (builder, opts) => { moveTextblockToEndOfLastItemOfPrevList, ), }), - builder.PluginPriority.Low, + builder.Priority.Low, ); builder.use(ListsInputRulesExtension, {bulletListInputRule: opts?.ulInputRules}); diff --git a/src/extensions/markdown/Lists/utils.ts b/src/extensions/markdown/Lists/utils.ts index 85787e5a..53d3d0a0 100644 --- a/src/extensions/markdown/Lists/utils.ts +++ b/src/extensions/markdown/Lists/utils.ts @@ -1,12 +1,9 @@ import {Schema} from 'prosemirror-model'; import {EditorState} from 'prosemirror-state'; import {findParentNodeOfType} from 'prosemirror-utils'; -import {nodeTypeFactory} from '../../../utils/schema'; -import {ListNode} from './const'; +import {blType, olType, ListNode} from './ListsSpecs'; -export const liType = nodeTypeFactory(ListNode.ListItem); -export const blType = nodeTypeFactory(ListNode.BulletList); -export const olType = nodeTypeFactory(ListNode.OrderedList); +export {liType, blType, olType} from './ListsSpecs'; export const findAnyParentList = (schema: Schema) => findParentNodeOfType([blType(schema), olType(schema)]); diff --git a/src/extensions/markdown/Mark/Mark.test.ts b/src/extensions/markdown/Mark/Mark.test.ts index 85f16725..14006084 100644 --- a/src/extensions/markdown/Mark/Mark.test.ts +++ b/src/extensions/markdown/Mark/Mark.test.ts @@ -2,17 +2,17 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {mark, Mark} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {markMarkName, MarkSpecs} from './MarkSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Mark), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(MarkSpecs), }).buildDeps(); const {doc, p, m} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - m: {markType: mark}, + m: {markType: markMarkName}, }) as PMTestBuilderResult<'doc' | 'p', 'm'>; const {same} = createMarkupChecker({parser, serializer}); diff --git a/src/extensions/markdown/Mark/MarkSpecs/index.ts b/src/extensions/markdown/Mark/MarkSpecs/index.ts new file mode 100644 index 00000000..3da48dbd --- /dev/null +++ b/src/extensions/markdown/Mark/MarkSpecs/index.ts @@ -0,0 +1,32 @@ +import type {PluginSimple} from 'markdown-it'; +import type {ExtensionAuto} from '../../../../core'; +import {markTypeFactory} from '../../../../utils/schema'; +const mdPlugin: PluginSimple = require('markdown-it-mark'); + +export const markMarkName = 'mark'; +export const markMarkType = markTypeFactory(markMarkName); + +export const MarkSpecs: ExtensionAuto = (builder) => { + builder + .configureMd((md) => md.use(mdPlugin)) + .addMark(markMarkName, () => ({ + spec: { + parseDOM: [{tag: 'mark'}], + toDOM() { + return ['mark']; + }, + }, + fromYfm: { + tokenSpec: { + name: markMarkName, + type: 'mark', + }, + }, + toYfm: { + open: '==', + close: '==', + mixable: true, + expelEnclosingWhitespace: true, + }, + })); +}; diff --git a/src/extensions/markdown/Mark/index.ts b/src/extensions/markdown/Mark/index.ts index 5c1d9c94..e2438df5 100644 --- a/src/extensions/markdown/Mark/index.ts +++ b/src/extensions/markdown/Mark/index.ts @@ -1,40 +1,22 @@ -import type {PluginSimple} from 'markdown-it'; import type {Action, ExtensionAuto} from '../../../core'; -import {markTypeFactory} from '../../../utils/schema'; import {createToggleMarkAction} from '../../../utils/actions'; import {markInputRule} from '../../../utils/inputrules'; -const mdPlugin: PluginSimple = require('markdown-it-mark'); +import {markMarkName, markMarkType, MarkSpecs} from './MarkSpecs'; -export const mark = 'mark'; +export {markMarkName, markMarkType} from './MarkSpecs'; +/** @deprecated Use `markMarkName` instead */ +export const mark = markMarkName; const mAction = 'mark'; -const mType = markTypeFactory(mark); export const Mark: ExtensionAuto = (builder) => { + builder.use(MarkSpecs); + builder - .configureMd((md) => md.use(mdPlugin)) - .addMark(mark, () => ({ - spec: { - parseDOM: [{tag: 'mark'}], - toDOM() { - return ['mark']; - }, - }, - fromYfm: { - tokenSpec: { - name: mark, - type: 'mark', - }, - }, - toYfm: { - open: '==', - close: '==', - mixable: true, - expelEnclosingWhitespace: true, - }, - })) - .addAction(mAction, ({schema}) => createToggleMarkAction(mType(schema))) + .addAction(mAction, ({schema}) => createToggleMarkAction(markMarkType(schema))) .addInputRules(({schema}) => ({ - rules: [markInputRule({open: '==', close: '==', ignoreBetween: '='}, mType(schema))], + rules: [ + markInputRule({open: '==', close: '==', ignoreBetween: '='}, markMarkType(schema)), + ], })); }; diff --git a/src/extensions/markdown/Strike/Strike.test.ts b/src/extensions/markdown/Strike/Strike.test.ts index 1ea95c03..8511fcde 100644 --- a/src/extensions/markdown/Strike/Strike.test.ts +++ b/src/extensions/markdown/Strike/Strike.test.ts @@ -2,17 +2,17 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {strike, Strike} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {strikeMarkName, StrikeSpecs} from './StrikeSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Strike, {}), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(StrikeSpecs), }).buildDeps(); const {doc, p, s} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - s: {markType: strike}, + s: {markType: strikeMarkName}, }) as PMTestBuilderResult<'doc' | 'p', 's'>; const {same} = createMarkupChecker({parser, serializer}); diff --git a/src/extensions/markdown/Strike/StrikeSpecs/index.ts b/src/extensions/markdown/Strike/StrikeSpecs/index.ts new file mode 100644 index 00000000..13f33bb5 --- /dev/null +++ b/src/extensions/markdown/Strike/StrikeSpecs/index.ts @@ -0,0 +1,24 @@ +import type {ExtensionAuto} from '../../../../core'; +import {markTypeFactory} from '../../../../utils/schema'; + +export const strikeMarkName = 'strike'; +export const strikeType = markTypeFactory(strikeMarkName); + +export const StrikeSpecs: ExtensionAuto = (builder) => { + builder.addMark(strikeMarkName, () => ({ + spec: { + parseDOM: [{tag: 'strike'}, {tag: 's'}], + toDOM() { + return ['strike']; + }, + }, + fromYfm: { + tokenSpec: { + name: strikeMarkName, + type: 'mark', + }, + tokenName: 's', + }, + toYfm: {open: '~~', close: '~~', mixable: true, expelEnclosingWhitespace: true}, + })); +}; diff --git a/src/extensions/markdown/Strike/index.ts b/src/extensions/markdown/Strike/index.ts index fd659ce4..4be87f1a 100644 --- a/src/extensions/markdown/Strike/index.ts +++ b/src/extensions/markdown/Strike/index.ts @@ -1,43 +1,32 @@ import {toggleMark} from 'prosemirror-commands'; import type {Action, ExtensionAuto} from '../../../core'; import {createToggleMarkAction} from '../../../utils/actions'; -import {markTypeFactory} from '../../../utils/schema'; import {markInputRule} from '../../../utils/inputrules'; +import {strikeMarkName, StrikeSpecs, strikeType} from './StrikeSpecs'; -export const strike = 'strike'; +export {strikeMarkName, strikeType} from './StrikeSpecs'; +/** @deprecated Use `strikeMarkName` instead */ +export const strike = strikeMarkName; const sAction = 'strike'; -const sType = markTypeFactory(strike); export type StrikeOptions = { strikeKey?: string | null; }; export const Strike: ExtensionAuto = (builder, opts) => { + builder.use(StrikeSpecs); + builder - .addMark(strike, () => ({ - spec: { - parseDOM: [{tag: 'strike'}, {tag: 's'}], - toDOM() { - return ['strike']; - }, - }, - fromYfm: { - tokenSpec: { - name: strike, - type: 'mark', - }, - tokenName: 's', - }, - toYfm: {open: '~~', close: '~~', mixable: true, expelEnclosingWhitespace: true}, - })) - .addAction(sAction, ({schema}) => createToggleMarkAction(sType(schema))) + .addAction(sAction, ({schema}) => createToggleMarkAction(strikeType(schema))) .addInputRules(({schema}) => ({ - rules: [markInputRule({open: '~~', close: '~~', ignoreBetween: '~'}, sType(schema))], + rules: [ + markInputRule({open: '~~', close: '~~', ignoreBetween: '~'}, strikeType(schema)), + ], })); if (opts?.strikeKey) { const {strikeKey} = opts; - builder.addKeymap(({schema}) => ({[strikeKey]: toggleMark(sType(schema))})); + builder.addKeymap(({schema}) => ({[strikeKey]: toggleMark(strikeType(schema))})); } }; diff --git a/src/extensions/markdown/Subscript/Subscript.test.ts b/src/extensions/markdown/Subscript/Subscript.test.ts index 680fa668..3432e55b 100644 --- a/src/extensions/markdown/Subscript/Subscript.test.ts +++ b/src/extensions/markdown/Subscript/Subscript.test.ts @@ -2,17 +2,17 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {subscript, Subscript} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {subscriptMarkName, SubscriptSpecs} from './SubscriptSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Subscript, {}), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(SubscriptSpecs), }).buildDeps(); const {doc, p, s} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - s: {markType: subscript}, + s: {markType: subscriptMarkName}, }) as PMTestBuilderResult<'doc' | 'p', 's'>; const {same} = createMarkupChecker({parser, serializer}); diff --git a/src/extensions/markdown/Subscript/SubscriptSpecs/index.ts b/src/extensions/markdown/Subscript/SubscriptSpecs/index.ts new file mode 100644 index 00000000..fde7e606 --- /dev/null +++ b/src/extensions/markdown/Subscript/SubscriptSpecs/index.ts @@ -0,0 +1,24 @@ +import type {PluginSimple} from 'markdown-it'; +import type {ExtensionAuto} from '../../../../core'; +import {markTypeFactory} from '../../../../utils/schema'; + +const sub: PluginSimple = require('markdown-it-sub'); + +export const subscriptMarkName = 'sub'; +export const subscriptType = markTypeFactory(subscriptMarkName); + +export const SubscriptSpecs: ExtensionAuto = (builder) => { + builder + .configureMd((md) => md.use(sub)) + .addMark(subscriptMarkName, () => ({ + spec: { + excludes: '_', + parseDOM: [{tag: 'sub'}], + toDOM() { + return ['sub']; + }, + }, + toYfm: {open: '~', close: '~', mixable: false, expelEnclosingWhitespace: true}, + fromYfm: {tokenSpec: {name: subscriptMarkName, type: 'mark'}}, + })); +}; diff --git a/src/extensions/markdown/Subscript/index.ts b/src/extensions/markdown/Subscript/index.ts index b4d1a0d9..afac09d5 100644 --- a/src/extensions/markdown/Subscript/index.ts +++ b/src/extensions/markdown/Subscript/index.ts @@ -1,30 +1,22 @@ import type {Action, ExtensionAuto} from '../../../core'; import {createToggleMarkAction} from '../../../utils/actions'; -import {markTypeFactory} from '../../../utils/schema'; import {markInputRule} from '../../../utils/inputrules'; -const sub = require('markdown-it-sub'); +import {subscriptMarkName, SubscriptSpecs, subscriptType} from './SubscriptSpecs'; -export const subscript = 'sub'; +export {subscriptMarkName, subscriptType} from './SubscriptSpecs'; +/** @deprecated Use `subscriptMarkName` instead */ +export const subscript = subscriptMarkName; const subAction = 'subscript'; -const subType = markTypeFactory(subscript); export const Subscript: ExtensionAuto = (builder) => { + builder.use(SubscriptSpecs); + builder - .configureMd((md) => md.use(sub)) - .addMark(subscript, () => ({ - spec: { - excludes: '_', - parseDOM: [{tag: 'sub'}], - toDOM() { - return ['sub']; - }, - }, - toYfm: {open: '~', close: '~', mixable: false, expelEnclosingWhitespace: true}, - fromYfm: {tokenSpec: {name: subscript, type: 'mark'}}, - })) - .addAction(subAction, ({schema}) => createToggleMarkAction(subType(schema))) + .addAction(subAction, ({schema}) => createToggleMarkAction(subscriptType(schema))) .addInputRules(({schema}) => ({ - rules: [markInputRule({open: '~', close: '~', ignoreBetween: '~'}, subType(schema))], + rules: [ + markInputRule({open: '~', close: '~', ignoreBetween: '~'}, subscriptType(schema)), + ], })); }; diff --git a/src/extensions/markdown/Superscript/Superscript.test.ts b/src/extensions/markdown/Superscript/Superscript.test.ts index 5659e215..3e75cf88 100644 --- a/src/extensions/markdown/Superscript/Superscript.test.ts +++ b/src/extensions/markdown/Superscript/Superscript.test.ts @@ -2,17 +2,17 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {superscript, Superscript} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {superscriptMarkName, SuperscriptSpecs} from './SuperscriptSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Superscript, {}), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(SuperscriptSpecs), }).buildDeps(); const {doc, p, s} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - s: {markType: superscript}, + s: {markType: superscriptMarkName}, }) as PMTestBuilderResult<'doc' | 'p', 's'>; const {same} = createMarkupChecker({parser, serializer}); diff --git a/src/extensions/markdown/Superscript/SuperscriptSpecs/index.ts b/src/extensions/markdown/Superscript/SuperscriptSpecs/index.ts new file mode 100644 index 00000000..9a9cace9 --- /dev/null +++ b/src/extensions/markdown/Superscript/SuperscriptSpecs/index.ts @@ -0,0 +1,24 @@ +import log from '@doc-tools/transform/lib/log'; +import sup from '@doc-tools/transform/lib/plugins/sup'; + +import type {ExtensionAuto} from '../../../../core'; +import {markTypeFactory} from '../../../../utils/schema'; + +export const superscriptMarkName = 'sup'; +export const superscriptType = markTypeFactory(superscriptMarkName); + +export const SuperscriptSpecs: ExtensionAuto = (builder) => { + builder + .configureMd((md) => md.use(sup, {log})) + .addMark(superscriptMarkName, () => ({ + spec: { + excludes: '_', + parseDOM: [{tag: 'sup'}], + toDOM() { + return ['sup']; + }, + }, + toYfm: {open: '^', close: '^', mixable: true, expelEnclosingWhitespace: true}, + fromYfm: {tokenSpec: {name: superscriptMarkName, type: 'mark'}}, + })); +}; diff --git a/src/extensions/markdown/Superscript/index.ts b/src/extensions/markdown/Superscript/index.ts index 90fb1040..3a2a0419 100644 --- a/src/extensions/markdown/Superscript/index.ts +++ b/src/extensions/markdown/Superscript/index.ts @@ -1,31 +1,22 @@ import type {Action, ExtensionAuto} from '../../../core'; import {createToggleMarkAction} from '../../../utils/actions'; -import {markTypeFactory} from '../../../utils/schema'; import {markInputRule} from '../../../utils/inputrules'; -import log from '@doc-tools/transform/lib/log'; -import sup from '@doc-tools/transform/lib/plugins/sup'; +import {superscriptMarkName, SuperscriptSpecs, superscriptType} from './SuperscriptSpecs'; -export const superscript = 'sup'; +export {superscriptMarkName, superscriptType} from './SuperscriptSpecs'; +/** @deprecated Use `superscriptMarkName` instead */ +export const superscript = superscriptMarkName; const supAction = 'supscript'; -const supType = markTypeFactory(superscript); export const Superscript: ExtensionAuto = (builder) => { + builder.use(SuperscriptSpecs); + builder - .configureMd((md) => md.use(sup, {log})) - .addMark(superscript, () => ({ - spec: { - excludes: '_', - parseDOM: [{tag: 'sup'}], - toDOM() { - return ['sup']; - }, - }, - toYfm: {open: '^', close: '^', mixable: true, expelEnclosingWhitespace: true}, - fromYfm: {tokenSpec: {name: superscript, type: 'mark'}}, - })) - .addAction(supAction, ({schema}) => createToggleMarkAction(supType(schema))) + .addAction(supAction, ({schema}) => createToggleMarkAction(superscriptType(schema))) .addInputRules(({schema}) => ({ - rules: [markInputRule({open: '^', close: '^', ignoreBetween: '^'}, supType(schema))], + rules: [ + markInputRule({open: '^', close: '^', ignoreBetween: '^'}, superscriptType(schema)), + ], })); }; diff --git a/src/extensions/markdown/Table/Table.test.ts b/src/extensions/markdown/Table/Table.test.ts index 37acace6..2fdb93f0 100644 --- a/src/extensions/markdown/Table/Table.test.ts +++ b/src/extensions/markdown/Table/Table.test.ts @@ -2,18 +2,18 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {blockquote, Blockquote} from '../Blockquote'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {blockquoteNodeName, BlockquoteSpecs} from '../Blockquote/BlockquoteSpecs'; import {CellAlign, TableAttrs, TableNode} from './const'; -import {Table} from './index'; +import {TableSpecs} from './TableSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Blockquote, {}).use(Table), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(BlockquoteSpecs).use(TableSpecs), }).buildDeps(); const {doc, bq, table, thead, tbody, tr, thL, thC, thR, tdL, tdC, tdR} = builders(schema, { doc: {nodeType: BaseNode.Doc}, - bq: {nodeType: blockquote}, + bq: {nodeType: blockquoteNodeName}, table: {nodeType: TableNode.Table}, thead: {nodeType: TableNode.Head}, tbody: {nodeType: TableNode.Body}, diff --git a/src/extensions/markdown/Table/TableSpecs/const.ts b/src/extensions/markdown/Table/TableSpecs/const.ts new file mode 100644 index 00000000..9f426fe7 --- /dev/null +++ b/src/extensions/markdown/Table/TableSpecs/const.ts @@ -0,0 +1,18 @@ +export enum TableNode { + Table = 'table', + Head = 'thead', + Body = 'tbody', + Row = 'tr', + HeaderCell = 'th', + DataCell = 'td', +} + +export enum TableAttrs { + CellAlign = 'cell-align', +} + +export enum CellAlign { + Left = 'left', + Center = 'center', + Right = 'right', +} diff --git a/src/extensions/markdown/Table/fromYfm.ts b/src/extensions/markdown/Table/TableSpecs/fromYfm.ts similarity index 90% rename from src/extensions/markdown/Table/fromYfm.ts rename to src/extensions/markdown/Table/TableSpecs/fromYfm.ts index 2a4f8aae..207fd332 100644 --- a/src/extensions/markdown/Table/fromYfm.ts +++ b/src/extensions/markdown/Table/TableSpecs/fromYfm.ts @@ -1,6 +1,6 @@ import type Token from 'markdown-it/lib/token'; -import type {ParserToken} from '../../../core'; -import {CellAlign, TableAttrs, TableNode} from './const'; +import type {ParserToken} from '../../../../core'; +import {CellAlign, TableAttrs, TableNode} from '../const'; export const fromYfm: Record = { [TableNode.Table]: {name: TableNode.Table, type: 'block'}, diff --git a/src/extensions/markdown/Table/TableSpecs/index.ts b/src/extensions/markdown/Table/TableSpecs/index.ts new file mode 100644 index 00000000..326d0ce5 --- /dev/null +++ b/src/extensions/markdown/Table/TableSpecs/index.ts @@ -0,0 +1,41 @@ +import type {ExtensionAuto} from '../../../../core'; +import {fromYfm} from './fromYfm'; +import {toYfm} from './toYfm'; +import {spec} from './spec'; +import {TableNode} from './const'; + +export {TableNode} from './const'; + +export const TableSpecs: ExtensionAuto = (builder) => { + builder + .addNode(TableNode.Table, () => ({ + spec: spec[TableNode.Table], + toYfm: toYfm[TableNode.Table], + fromYfm: {tokenSpec: fromYfm[TableNode.Table]}, + })) + .addNode(TableNode.Head, () => ({ + spec: spec[TableNode.Head], + toYfm: toYfm[TableNode.Head], + fromYfm: {tokenSpec: fromYfm[TableNode.Head]}, + })) + .addNode(TableNode.Body, () => ({ + spec: spec[TableNode.Body], + toYfm: toYfm[TableNode.Body], + fromYfm: {tokenSpec: fromYfm[TableNode.Body]}, + })) + .addNode(TableNode.Row, () => ({ + spec: spec[TableNode.Row], + toYfm: toYfm[TableNode.Row], + fromYfm: {tokenSpec: fromYfm[TableNode.Row]}, + })) + .addNode(TableNode.HeaderCell, () => ({ + spec: spec[TableNode.HeaderCell], + toYfm: toYfm[TableNode.HeaderCell], + fromYfm: {tokenSpec: fromYfm[TableNode.HeaderCell]}, + })) + .addNode(TableNode.DataCell, () => ({ + spec: spec[TableNode.DataCell], + toYfm: toYfm[TableNode.DataCell], + fromYfm: {tokenSpec: fromYfm[TableNode.DataCell]}, + })); +}; diff --git a/src/extensions/markdown/Table/spec.ts b/src/extensions/markdown/Table/TableSpecs/spec.ts similarity index 94% rename from src/extensions/markdown/Table/spec.ts rename to src/extensions/markdown/Table/TableSpecs/spec.ts index 38c71834..2569f6bc 100644 --- a/src/extensions/markdown/Table/spec.ts +++ b/src/extensions/markdown/Table/TableSpecs/spec.ts @@ -1,6 +1,6 @@ -import {NodeSpec} from 'prosemirror-model'; -import {TableRole} from '../../../table-utils'; -import {CellAlign, TableAttrs, TableNode} from './const'; +import type {NodeSpec} from 'prosemirror-model'; +import {TableRole} from '../../../../table-utils'; +import {CellAlign, TableAttrs, TableNode} from '../const'; export const spec: Record = { [TableNode.Table]: { diff --git a/src/extensions/markdown/Table/toYfm.ts b/src/extensions/markdown/Table/TableSpecs/toYfm.ts similarity index 93% rename from src/extensions/markdown/Table/toYfm.ts rename to src/extensions/markdown/Table/TableSpecs/toYfm.ts index 6f1d035c..1da08750 100644 --- a/src/extensions/markdown/Table/toYfm.ts +++ b/src/extensions/markdown/Table/TableSpecs/toYfm.ts @@ -1,5 +1,5 @@ -import type {SerializerNodeToken} from '../../../core'; -import {CellAlign, TableAttrs, TableNode} from './const'; +import type {SerializerNodeToken} from '../../../../core'; +import {CellAlign, TableAttrs, TableNode} from '../const'; export const toYfm: Record = { [TableNode.Table]: (state, node) => { diff --git a/src/extensions/markdown/Table/const.ts b/src/extensions/markdown/Table/const.ts index 9f426fe7..cf5c67ac 100644 --- a/src/extensions/markdown/Table/const.ts +++ b/src/extensions/markdown/Table/const.ts @@ -1,18 +1 @@ -export enum TableNode { - Table = 'table', - Head = 'thead', - Body = 'tbody', - Row = 'tr', - HeaderCell = 'th', - DataCell = 'td', -} - -export enum TableAttrs { - CellAlign = 'cell-align', -} - -export enum CellAlign { - Left = 'left', - Center = 'center', - Right = 'right', -} +export * from './TableSpecs/const'; diff --git a/src/extensions/markdown/Table/index.ts b/src/extensions/markdown/Table/index.ts index bcb598f6..66b95816 100644 --- a/src/extensions/markdown/Table/index.ts +++ b/src/extensions/markdown/Table/index.ts @@ -1,9 +1,6 @@ import type {Action, ExtensionAuto} from '../../../core'; import {goToNextCell} from '../../../table-utils'; -import {TableNode} from './const'; -import {fromYfm} from './fromYfm'; -import {spec} from './spec'; -import {toYfm} from './toYfm'; +import {TableSpecs} from './TableSpecs'; import {createTableAction, deleteTableAction} from './actions'; import * as TableHelpers from './helpers'; import * as TableActions from './actions'; @@ -12,37 +9,7 @@ export {TableHelpers, TableActions}; export {TableNode, TableAttrs, CellAlign as TableCellAlign} from './const'; export const Table: ExtensionAuto = (builder) => { - builder - .addNode(TableNode.Table, () => ({ - spec: spec[TableNode.Table], - toYfm: toYfm[TableNode.Table], - fromYfm: {tokenSpec: fromYfm[TableNode.Table]}, - })) - .addNode(TableNode.Head, () => ({ - spec: spec[TableNode.Head], - toYfm: toYfm[TableNode.Head], - fromYfm: {tokenSpec: fromYfm[TableNode.Head]}, - })) - .addNode(TableNode.Body, () => ({ - spec: spec[TableNode.Body], - toYfm: toYfm[TableNode.Body], - fromYfm: {tokenSpec: fromYfm[TableNode.Body]}, - })) - .addNode(TableNode.Row, () => ({ - spec: spec[TableNode.Row], - toYfm: toYfm[TableNode.Row], - fromYfm: {tokenSpec: fromYfm[TableNode.Row]}, - })) - .addNode(TableNode.HeaderCell, () => ({ - spec: spec[TableNode.HeaderCell], - toYfm: toYfm[TableNode.HeaderCell], - fromYfm: {tokenSpec: fromYfm[TableNode.HeaderCell]}, - })) - .addNode(TableNode.DataCell, () => ({ - spec: spec[TableNode.DataCell], - toYfm: toYfm[TableNode.DataCell], - fromYfm: {tokenSpec: fromYfm[TableNode.DataCell]}, - })); + builder.use(TableSpecs); builder.addKeymap(() => ({ Tab: goToNextCell('next'), diff --git a/src/extensions/markdown/Underline/Underline.test.ts b/src/extensions/markdown/Underline/Underline.test.ts index ee113fc9..ad353cd6 100644 --- a/src/extensions/markdown/Underline/Underline.test.ts +++ b/src/extensions/markdown/Underline/Underline.test.ts @@ -2,17 +2,17 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {underline, Underline} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {underlineMarkName, UnderlineSpecs} from './UnderlineSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Underline, {}), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(UnderlineSpecs), }).buildDeps(); const {doc, p, u} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - u: {markType: underline}, + u: {markType: underlineMarkName}, }) as PMTestBuilderResult<'doc' | 'p', 'u'>; const {same} = createMarkupChecker({parser, serializer}); diff --git a/src/extensions/markdown/Underline/UnderlineSpecs/index.ts b/src/extensions/markdown/Underline/UnderlineSpecs/index.ts new file mode 100644 index 00000000..7779a229 --- /dev/null +++ b/src/extensions/markdown/Underline/UnderlineSpecs/index.ts @@ -0,0 +1,23 @@ +import type {PluginSimple} from 'markdown-it'; +import type {ExtensionAuto} from '../../../../core'; +import {markTypeFactory} from '../../../../utils/schema'; + +const ins: PluginSimple = require('markdown-it-ins'); + +export const underlineMarkName = 'ins'; +export const underlineType = markTypeFactory(underlineMarkName); + +export const UnderlineSpecs: ExtensionAuto = (builder) => { + builder + .configureMd((md) => md.use(ins)) + .addMark(underlineMarkName, () => ({ + spec: { + parseDOM: [{tag: 'ins'}, {tag: 'u'}], + toDOM() { + return ['ins']; + }, + }, + toYfm: {open: '++', close: '++', mixable: true, expelEnclosingWhitespace: true}, + fromYfm: {tokenSpec: {name: underlineMarkName, type: 'mark'}}, + })); +}; diff --git a/src/extensions/markdown/Underline/index.ts b/src/extensions/markdown/Underline/index.ts index 6a67b8e2..04cddc2c 100644 --- a/src/extensions/markdown/Underline/index.ts +++ b/src/extensions/markdown/Underline/index.ts @@ -1,39 +1,32 @@ import {toggleMark} from 'prosemirror-commands'; import type {Action, ExtensionAuto} from '../../../core'; import {createToggleMarkAction} from '../../../utils/actions'; -import {markTypeFactory} from '../../../utils/schema'; import {markInputRule} from '../../../utils/inputrules'; -const ins = require('markdown-it-ins'); +import {underlineMarkName, UnderlineSpecs, underlineType} from './UnderlineSpecs'; -export const underline = 'ins'; +export {underlineMarkName, underlineType} from './UnderlineSpecs'; +/** @deprecated Use `underlineMarkName` instead */ +export const underline = underlineMarkName; const undAction = 'underline'; -const undType = markTypeFactory(underline); export type UnderlineOptions = { underlineKey?: string | null; }; export const Underline: ExtensionAuto = (builder, opts) => { + builder.use(UnderlineSpecs); + builder - .configureMd((md) => md.use(ins)) - .addMark(underline, () => ({ - spec: { - parseDOM: [{tag: 'ins'}, {tag: 'u'}], - toDOM() { - return ['ins']; - }, - }, - toYfm: {open: '++', close: '++', mixable: true, expelEnclosingWhitespace: true}, - fromYfm: {tokenSpec: {name: underline, type: 'mark'}}, - })) - .addAction(undAction, ({schema}) => createToggleMarkAction(undType(schema))) + .addAction(undAction, ({schema}) => createToggleMarkAction(underlineType(schema))) .addInputRules(({schema}) => ({ - rules: [markInputRule({open: '++', close: '++', ignoreBetween: '+'}, undType(schema))], + rules: [ + markInputRule({open: '++', close: '++', ignoreBetween: '+'}, underlineType(schema)), + ], })); if (opts?.underlineKey) { const {underlineKey} = opts; - builder.addKeymap(({schema}) => ({[underlineKey]: toggleMark(undType(schema))})); + builder.addKeymap(({schema}) => ({[underlineKey]: toggleMark(underlineType(schema))})); } }; diff --git a/src/extensions/markdown/specs.ts b/src/extensions/markdown/specs.ts new file mode 100644 index 00000000..a8501761 --- /dev/null +++ b/src/extensions/markdown/specs.ts @@ -0,0 +1,93 @@ +import isFunction from 'lodash/isFunction'; +import type {Extension, ExtensionAuto} from '../../core'; + +import {BoldSpecs} from './Bold/BoldSpecs'; +import {CodeSpecs} from './Code/CodeSpecs'; +import {ItalicSpecs} from './Italic/ItalicSpecs'; +import {StrikeSpecs} from './Strike/StrikeSpecs'; +import {UnderlineSpecs} from './Underline/UnderlineSpecs'; +import {LinkSpecs} from './Link/LinkSpecs'; +import {MarkSpecs} from './Mark/MarkSpecs'; +import {SubscriptSpecs} from './Subscript/SubscriptSpecs'; +import {SuperscriptSpecs} from './Superscript/SuperscriptSpecs'; + +import {Html} from './Html'; +import {TableSpecs} from './Table/TableSpecs'; +import {ImageSpecs} from './Image/ImageSpecs'; +import {ListsSpecs} from './Lists/ListsSpecs'; +import {BreaksSpecs} from './Breaks/BreaksSpecs'; +import {BlockquoteSpecs} from './Blockquote/BlockquoteSpecs'; +import {DeflistSpecs, DeflistSpecsOptions} from './Deflist/DeflistSpecs'; +import {HeadingSpecs, HeadingSpecsOptions} from './Heading/HeadingSpecs'; +import {HorizontalRuleSpecs} from './HorizontalRule/HorizontalRuleSpecs'; +import {CodeBlockSpecs, CodeBlockSpecsOptions} from './CodeBlock/CodeBlockSpecs'; + +export * from './Bold/BoldSpecs'; +export * from './Code/CodeSpecs'; +export * from './Link/LinkSpecs'; +export * from './Italic/ItalicSpecs'; +export * from './Strike/StrikeSpecs'; +export * from './Underline/UnderlineSpecs'; +export * from './Mark/MarkSpecs'; +export * from './Subscript/SubscriptSpecs'; +export * from './Superscript/SuperscriptSpecs'; + +export * from './Html'; +export * from './Breaks/BreaksSpecs'; +export * from './Blockquote/BlockquoteSpecs'; +export * from './CodeBlock/CodeBlockSpecs'; +export * from './Deflist/DeflistSpecs'; +export * from './Image/ImageSpecs'; +export * from './Lists/ListsSpecs'; +export * from './Table/TableSpecs'; +export * from './Heading/HeadingSpecs'; +export * from './HorizontalRule/HorizontalRuleSpecs'; + +export type MarkdownMarksSpecsPresetOptions = {}; + +export const MarkdownMarksSpecsPreset: ExtensionAuto = ( + builder, + _opts, +) => { + builder + .use(BoldSpecs) + .use(CodeSpecs) + .use(ItalicSpecs) + .use(StrikeSpecs) + .use(UnderlineSpecs) + .use(LinkSpecs) + .use(MarkSpecs) + .use(SubscriptSpecs) + .use(SuperscriptSpecs); +}; + +export type MarkdownBlocksSpecsPresetOptions = { + image?: false | Extension; + codeBlock?: CodeBlockSpecsOptions; + deflist?: DeflistSpecsOptions; + heading?: false | Extension | HeadingSpecsOptions; +}; + +export const MarkdownBlocksSpecsPreset: ExtensionAuto = ( + builder, + opts, +) => { + builder + .use(Html) + .use(ListsSpecs) + .use(TableSpecs) + .use(BreaksSpecs) + .use(BlockquoteSpecs) + .use(HorizontalRuleSpecs) + .use(DeflistSpecs, opts.deflist ?? {}) + .use(CodeBlockSpecs, opts.codeBlock ?? {}); + + if (opts.image !== false) { + builder.use(isFunction(opts.image) ? opts.image : ImageSpecs); + } + + if (opts.heading !== false) { + if (isFunction(opts.heading)) builder.use(opts.heading); + else builder.use(HeadingSpecs, opts.heading ?? {}); + } +}; diff --git a/src/extensions/specs.ts b/src/extensions/specs.ts new file mode 100644 index 00000000..16028b0a --- /dev/null +++ b/src/extensions/specs.ts @@ -0,0 +1,3 @@ +export * from './base/specs'; +export * from './markdown/specs'; +export * from './yfm/specs'; diff --git a/src/extensions/yfm/Checkbox/Checkbox.test.ts b/src/extensions/yfm/Checkbox/Checkbox.test.ts index d3135833..810080f9 100644 --- a/src/extensions/yfm/Checkbox/Checkbox.test.ts +++ b/src/extensions/yfm/Checkbox/Checkbox.test.ts @@ -1,17 +1,16 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {Bold, bold} from '../../markdown/Bold'; -import {CheckboxNode} from './const'; -import {Checkbox} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {boldMarkName, BoldSpecs} from '../../markdown/specs'; +import {CheckboxSpecs, CheckboxNode} from './CheckboxSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ extensions: (builder) => builder - .use(BaseSchema, {}) - .use(Bold, {}) - .use(Checkbox, {checkboxLabelPlaceholder: 'checkbox-placeholder'}), + .use(BaseSpecsPreset, {}) + .use(BoldSpecs) + .use(CheckboxSpecs, {checkboxLabelPlaceholder: 'checkbox-placeholder'}), }).buildDeps(); const { @@ -29,7 +28,7 @@ const { } = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - b: {nodeType: bold}, + b: {nodeType: boldMarkName}, checkbox: {nodeType: CheckboxNode.Checkbox}, cbInput: {nodeType: CheckboxNode.Input}, cbLabel: {nodeType: CheckboxNode.Label}, diff --git a/src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts new file mode 100644 index 00000000..57f0070a --- /dev/null +++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/const.ts @@ -0,0 +1,11 @@ +import {cn} from '../../../../classname'; + +export enum CheckboxNode { + Checkbox = 'checkbox', + Input = 'checkbox_input', + Label = 'checkbox_label', +} + +export const idPrefix = 'yfm-editor-checkbox'; + +export const b = cn('checkbox'); diff --git a/src/extensions/yfm/Checkbox/fromYfm.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/fromYfm.ts similarity index 82% rename from src/extensions/yfm/Checkbox/fromYfm.ts rename to src/extensions/yfm/Checkbox/CheckboxSpecs/fromYfm.ts index 41423df8..d5d9ea50 100644 --- a/src/extensions/yfm/Checkbox/fromYfm.ts +++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/fromYfm.ts @@ -1,5 +1,5 @@ -import type {ParserToken} from '../../../core'; -import {CheckboxNode} from './const'; +import type {ParserToken} from '../../../../core'; +import {CheckboxNode} from '../const'; const getAttrs: ParserToken['getAttrs'] = (tok) => (tok.attrs ? Object.fromEntries(tok.attrs) : {}); diff --git a/src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts new file mode 100644 index 00000000..99cf0bc0 --- /dev/null +++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/index.ts @@ -0,0 +1,53 @@ +import type {NodeSpec} from 'prosemirror-model'; +import checkboxPlugin from '@doc-tools/transform/lib/plugins/checkbox'; + +import type {ExtensionAuto, YENodeSpec} from '../../../../core'; +import {nodeTypeFactory} from '../../../../utils/schema'; +import {b, CheckboxNode, idPrefix} from './const'; +import {fromYfm} from './fromYfm'; +import {getSpec} from './spec'; +import {toYfm} from './toYfm'; + +export {CheckboxNode} from './const'; +export const checkboxType = nodeTypeFactory(CheckboxNode.Checkbox); +export const checkboxLabelType = nodeTypeFactory(CheckboxNode.Label); +export const checkboxInputType = nodeTypeFactory(CheckboxNode.Input); + +export type CheckboxSpecsOptions = { + checkboxLabelPlaceholder?: NonNullable['content']; + inputView?: YENodeSpec['view']; + labelView?: YENodeSpec['view']; + checkboxView?: YENodeSpec['view']; +}; + +export const CheckboxSpecs: ExtensionAuto = (builder, opts) => { + const spec = getSpec(opts); + + builder + .configureMd((md) => checkboxPlugin(md, {idPrefix, divClass: b()})) + .addNode(CheckboxNode.Checkbox, () => ({ + spec: spec[CheckboxNode.Checkbox], + toYfm: toYfm[CheckboxNode.Checkbox], + fromYfm: { + tokenSpec: fromYfm[CheckboxNode.Checkbox], + }, + view: opts.checkboxView, + })) + .addNode(CheckboxNode.Input, () => ({ + spec: spec[CheckboxNode.Input], + toYfm: toYfm[CheckboxNode.Input], + fromYfm: { + tokenSpec: fromYfm[CheckboxNode.Input], + }, + view: opts.inputView, + })) + .addNode(CheckboxNode.Label, () => ({ + spec: spec[CheckboxNode.Label], + toYfm: toYfm[CheckboxNode.Label], + fromYfm: { + tokenSpec: fromYfm[CheckboxNode.Label], + tokenName: 'checkbox_label', + }, + view: opts.labelView, + })); +}; diff --git a/src/extensions/yfm/Checkbox/spec.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/spec.ts similarity index 83% rename from src/extensions/yfm/Checkbox/spec.ts rename to src/extensions/yfm/Checkbox/CheckboxSpecs/spec.ts index c991bb58..55efcd95 100644 --- a/src/extensions/yfm/Checkbox/spec.ts +++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/spec.ts @@ -1,16 +1,12 @@ import type {NodeSpec} from 'prosemirror-model'; -import {cn} from '../../../classname'; -import {CheckboxNode} from './const'; - -const b = cn('checkbox'); +import type {CheckboxSpecsOptions} from './index'; +import {b, CheckboxNode} from '../const'; const DEFAULT_LABEL_PLACEHOLDER = 'Checkbox'; -export type CheckboxSpecOptions = { - checkboxLabelPlaceholder?: NonNullable['content']; -}; - -export const getSpec = (opts?: CheckboxSpecOptions): Record => ({ +export const getSpec = ( + opts?: Pick, +): Record => ({ [CheckboxNode.Checkbox]: { group: 'block', content: `${CheckboxNode.Input} ${CheckboxNode.Label}`, diff --git a/src/extensions/yfm/Checkbox/toYfm.ts b/src/extensions/yfm/Checkbox/CheckboxSpecs/toYfm.ts similarity index 78% rename from src/extensions/yfm/Checkbox/toYfm.ts rename to src/extensions/yfm/Checkbox/CheckboxSpecs/toYfm.ts index b5a69b4f..3774716f 100644 --- a/src/extensions/yfm/Checkbox/toYfm.ts +++ b/src/extensions/yfm/Checkbox/CheckboxSpecs/toYfm.ts @@ -1,6 +1,6 @@ -import {getPlaceholderContent} from '../../behavior/Placeholder'; -import {SerializerNodeToken} from '../../../core'; -import {CheckboxNode} from './const'; +import {SerializerNodeToken} from '../../../../core'; +import {getPlaceholderContent} from '../../../../utils/placeholder'; +import {CheckboxNode} from '../const'; export const toYfm: Record = { [CheckboxNode.Checkbox]: (state, node) => { diff --git a/src/extensions/yfm/Checkbox/actions.test.ts b/src/extensions/yfm/Checkbox/actions.test.ts index d80fab49..39de8e1e 100644 --- a/src/extensions/yfm/Checkbox/actions.test.ts +++ b/src/extensions/yfm/Checkbox/actions.test.ts @@ -2,7 +2,7 @@ import {Schema} from 'prosemirror-model'; import {EditorState, TextSelection} from 'prosemirror-state'; import {builders} from 'prosemirror-test-builder'; -import {getSpec} from './spec'; +import {getSpec} from './CheckboxSpecs/spec'; import {addCheckboxCmd} from './actions'; import {applyCommand} from '../../../../tests/utils'; diff --git a/src/extensions/yfm/Checkbox/const.ts b/src/extensions/yfm/Checkbox/const.ts index 25e46b7c..ffc49b5c 100644 --- a/src/extensions/yfm/Checkbox/const.ts +++ b/src/extensions/yfm/Checkbox/const.ts @@ -1,5 +1 @@ -export enum CheckboxNode { - Checkbox = 'checkbox', - Input = 'checkbox_input', - Label = 'checkbox_label', -} +export {CheckboxNode, b} from './CheckboxSpecs/const'; diff --git a/src/extensions/yfm/Checkbox/index.ts b/src/extensions/yfm/Checkbox/index.ts index df3d101a..7d67ce33 100644 --- a/src/extensions/yfm/Checkbox/index.ts +++ b/src/extensions/yfm/Checkbox/index.ts @@ -1,100 +1,72 @@ -import checkboxPlugin from '@doc-tools/transform/lib/plugins/checkbox'; -import type {Action, ExtensionAuto} from '../../../core'; import {replaceParentNodeOfType} from 'prosemirror-utils'; -import {CheckboxNode} from './const'; +import type {Action, ExtensionAuto} from '../../../core'; +import {b, CheckboxNode} from './const'; import {keymapPlugin} from './plugin'; -import {cn} from '../../../classname'; - -import './index.scss'; -import {CheckboxSpecOptions, getSpec} from './spec'; -import {toYfm} from './toYfm'; -import {fromYfm} from './fromYfm'; -import {addCheckbox} from './actions'; import {nodeInputRule} from '../../../utils/inputrules'; +import {addCheckbox} from './actions'; +import {CheckboxSpecs, CheckboxSpecsOptions} from './CheckboxSpecs'; import {checkboxInputType, checkboxType} from './utils'; import {pType} from '../../base/BaseSchema'; +import './index.scss'; + const checkboxAction = 'addCheckbox'; -const idPrefix = 'yfm-editor-checkbox'; -const b = cn('checkbox'); +export {CheckboxNode, checkboxType, checkboxLabelType, checkboxInputType} from './CheckboxSpecs'; -export type CheckboxOptions = CheckboxSpecOptions & {}; +export type CheckboxOptions = Pick & {}; export const Checkbox: ExtensionAuto = (builder, opts) => { - const spec = getSpec(opts); + builder.use(CheckboxSpecs, { + ...opts, + inputView: () => (node, view, getPos) => { + const dom = document.createElement('input'); - builder - .configureMd((md) => checkboxPlugin(md, {idPrefix, divClass: b()})) - .addNode(CheckboxNode.Checkbox, () => ({ - spec: spec[CheckboxNode.Checkbox], - toYfm: toYfm[CheckboxNode.Checkbox], - fromYfm: { - tokenSpec: fromYfm[CheckboxNode.Checkbox], - }, - })) - .addNode(CheckboxNode.Input, () => ({ - spec: spec[CheckboxNode.Input], - toYfm: toYfm[CheckboxNode.Input], - fromYfm: { - tokenSpec: fromYfm[CheckboxNode.Input], - }, - view: () => (node, view, getPos) => { - const dom = document.createElement('input'); + for (const attr in node.attrs) { + if (node.attrs[attr]) dom.setAttribute(attr, node.attrs[attr]); + } - for (const attr in node.attrs) { - if (node.attrs[attr]) dom.setAttribute(attr, node.attrs[attr]); - } + dom.setAttribute('class', b('input')); - dom.setAttribute('class', b('input')); + dom.addEventListener('click', (e) => { + const elem = e.target as HTMLElement; + const checkedAttr = elem.getAttribute('checked'); + const checked = checkedAttr ? '' : 'true'; - dom.addEventListener('click', (e) => { - const elem = e.target as HTMLElement; - const checkedAttr = elem.getAttribute('checked'); - const checked = checkedAttr ? '' : 'true'; + view.dispatch( + view.state.tr.setNodeMarkup(getPos(), undefined, { + ...node.attrs, + checked, + }), + ); - view.dispatch( - view.state.tr.setNodeMarkup(getPos(), undefined, { - ...node.attrs, - checked, - }), - ); + elem.setAttribute('checked', checked); + }); - elem.setAttribute('checked', checked); - }); + return { + dom, + ignoreMutation: () => true, + update: () => true, + destroy() { + const resolved = view.state.doc.resolve(getPos()); + if ( + resolved.parent.type.name === CheckboxNode.Checkbox && + resolved.parent.lastChild + ) { + view.dispatch( + replaceParentNodeOfType( + resolved.parent.type, + pType(view.state.schema).create(resolved.parent.lastChild.content), + )(view.state.tr), + ); + } + dom.remove(); + }, + }; + }, + }); - return { - dom, - ignoreMutation: () => true, - update: () => true, - destroy() { - const resolved = view.state.doc.resolve(getPos()); - if ( - resolved.parent.type.name === CheckboxNode.Checkbox && - resolved.parent.lastChild - ) { - view.dispatch( - replaceParentNodeOfType( - resolved.parent.type, - pType(view.state.schema).create( - resolved.parent.lastChild.content, - ), - )(view.state.tr), - ); - } - dom.remove(); - }, - }; - }, - })) - .addNode(CheckboxNode.Label, () => ({ - spec: spec[CheckboxNode.Label], - toYfm: toYfm[CheckboxNode.Label], - fromYfm: { - tokenSpec: fromYfm[CheckboxNode.Label], - tokenName: 'checkbox_label', - }, - })) + builder .addPlugin(keymapPlugin) .addAction(checkboxAction, () => addCheckbox()) .addInputRules(({schema}) => ({ diff --git a/src/extensions/yfm/Checkbox/plugin.test.ts b/src/extensions/yfm/Checkbox/plugin.test.ts index 28857abe..2be1881f 100644 --- a/src/extensions/yfm/Checkbox/plugin.test.ts +++ b/src/extensions/yfm/Checkbox/plugin.test.ts @@ -2,7 +2,7 @@ import {Schema} from 'prosemirror-model'; import {EditorState, TextSelection} from 'prosemirror-state'; import {builders} from 'prosemirror-test-builder'; -import {getSpec} from './spec'; +import {getSpec} from './CheckboxSpecs/spec'; import {splitCheckbox} from './plugin'; import {applyCommand} from '../../../../tests/utils'; diff --git a/src/extensions/yfm/Checkbox/utils.ts b/src/extensions/yfm/Checkbox/utils.ts index afa2bc1e..56afaaf8 100644 --- a/src/extensions/yfm/Checkbox/utils.ts +++ b/src/extensions/yfm/Checkbox/utils.ts @@ -1,6 +1 @@ -import {nodeTypeFactory} from '../../../utils/schema'; -import {CheckboxNode} from './const'; - -export const checkboxType = nodeTypeFactory(CheckboxNode.Checkbox); -export const checkboxLabelType = nodeTypeFactory(CheckboxNode.Label); -export const checkboxInputType = nodeTypeFactory(CheckboxNode.Input); +export {checkboxType, checkboxInputType, checkboxLabelType} from './CheckboxSpecs'; diff --git a/src/extensions/yfm/Color/Color.test.ts b/src/extensions/yfm/Color/Color.test.ts index 1a86fbb4..fd54d3f5 100644 --- a/src/extensions/yfm/Color/Color.test.ts +++ b/src/extensions/yfm/Color/Color.test.ts @@ -1,19 +1,18 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {Color} from './index'; -import {color} from './const'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {colorMarkName, ColorSpecs} from './ColorSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Color), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(ColorSpecs), }).buildDeps(); const {doc, p, c1, c2} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - c1: {nodeType: color, [color]: 'c1'}, - c2: {nodeType: color, [color]: 'c2'}, + c1: {nodeType: colorMarkName, [colorMarkName]: 'c1'}, + c2: {nodeType: colorMarkName, [colorMarkName]: 'c2'}, }) as PMTestBuilderResult<'doc' | 'p', 'c1' | 'c2'>; const {same} = createMarkupChecker({parser, serializer}); diff --git a/src/extensions/yfm/Color/ColorSpecs/const.ts b/src/extensions/yfm/Color/ColorSpecs/const.ts new file mode 100644 index 00000000..d04ccad4 --- /dev/null +++ b/src/extensions/yfm/Color/ColorSpecs/const.ts @@ -0,0 +1,3 @@ +export const colorMarkName = 'color'; +export const colorClassName = 'yfm-colorify'; +export const domColorAttr = 'data-color'; diff --git a/src/extensions/yfm/Color/ColorSpecs/index.ts b/src/extensions/yfm/Color/ColorSpecs/index.ts new file mode 100644 index 00000000..0cf3db25 --- /dev/null +++ b/src/extensions/yfm/Color/ColorSpecs/index.ts @@ -0,0 +1,58 @@ +import mdPlugin from 'markdown-it-color'; +import type {ExtensionAuto} from '../../../../core'; +import {markTypeFactory} from '../../../../utils/schema'; +import {colorMarkName, colorClassName, domColorAttr} from './const'; + +export {colorMarkName} from './const'; +export const colorType = markTypeFactory(colorMarkName); + +export const ColorSpecs: ExtensionAuto = (builder) => { + builder + .configureMd((md) => md.use(mdPlugin, {defaultClassName: colorClassName, inline: false})) + .addMark(colorMarkName, () => ({ + spec: { + attrs: {[colorMarkName]: {}}, + parseDOM: [ + { + tag: `span[${domColorAttr}]`, + getAttrs(node) { + return { + [colorMarkName]: (node as HTMLElement).getAttribute(domColorAttr), + }; + }, + }, + ], + toDOM(node) { + const colorValue = node.attrs[colorMarkName]; + + return [ + 'span', + { + class: [colorClassName, `${colorClassName}--${colorValue}`].join(' '), + [domColorAttr]: colorValue, + }, + 0, + ]; + }, + }, + fromYfm: { + tokenSpec: { + name: colorMarkName, + type: 'mark', + getAttrs(token) { + return { + [colorMarkName]: token.info, + }; + }, + }, + }, + toYfm: { + open: (_state, mark) => { + return `{${mark.attrs[colorMarkName]}}(`; + }, + close: ')', + mixable: true, + expelEnclosingWhitespace: true, + }, + })); +}; diff --git a/src/extensions/yfm/Color/const.ts b/src/extensions/yfm/Color/const.ts index db28aca3..9f3f80d8 100644 --- a/src/extensions/yfm/Color/const.ts +++ b/src/extensions/yfm/Color/const.ts @@ -1,7 +1,6 @@ -export const color = 'color'; +export * from './ColorSpecs/const'; + export const colorAction = 'colorify'; -export const className = 'yfm-colorify'; -export const domColorAttr = 'data-color'; export enum Colors { Black = 'black', diff --git a/src/extensions/yfm/Color/index.ts b/src/extensions/yfm/Color/index.ts index 536c4681..d37f7ffd 100644 --- a/src/extensions/yfm/Color/index.ts +++ b/src/extensions/yfm/Color/index.ts @@ -1,103 +1,51 @@ -import mdPlugin from 'markdown-it-color'; import {toggleMark} from 'prosemirror-commands'; import type {Action, ExtensionAuto} from '../../../core'; import {isMarkActive} from '../../../utils/marks'; -import {markTypeFactory} from '../../../utils/schema'; -import {className, color, colorAction, Colors, domColorAttr} from './const'; +import {ColorSpecs, colorType} from './ColorSpecs'; +import {colorAction, colorMarkName, Colors} from './const'; import {chainAND} from './utils'; import './colors.scss'; -const colorType = markTypeFactory(color); - -export {className as colorClassName, Colors} from './const'; +export {colorClassName, Colors} from './const'; +export {colorMarkName, colorType} from './ColorSpecs'; export type ColorActionParams = { - [color]: string; + [colorMarkName]: string; }; export const Color: ExtensionAuto = (builder) => { - builder - .configureMd((md) => md.use(mdPlugin, {defaultClassName: className, inline: false})) - .addMark(color, () => ({ - spec: { - attrs: {[color]: {}}, - parseDOM: [ - { - tag: `span[${domColorAttr}]`, - getAttrs(node) { - return { - [color]: (node as HTMLElement).getAttribute(domColorAttr), - }; - }, - }, - ], - toDOM(node) { - const colorValue = node.attrs[color]; - - return [ - 'span', - { - class: [className, `${className}--${colorValue}`].join(' '), - [domColorAttr]: colorValue, - }, - 0, - ]; - }, - }, - fromYfm: { - tokenSpec: { - name: color, - type: 'mark', - getAttrs(token) { - return { - [color]: token.info, - }; - }, - }, - }, - toYfm: { - open: (_state, mark) => { - return `{${mark.attrs[color]}}(`; - }, - close: ')', - mixable: true, - expelEnclosingWhitespace: true, - }, - })) - .addAction(colorAction, ({schema}) => { - const type = colorType(schema); + builder.use(ColorSpecs); - return { - isActive: (state) => Boolean(isMarkActive(state, type)), - isEnable: toggleMark(type), - run: (state, dispatch, _view, attrs) => { - const params = attrs as ColorActionParams | undefined; - const hasMark = isMarkActive(state, type); + builder.addAction(colorAction, ({schema}) => { + const type = colorType(schema); + return { + isActive: (state) => Boolean(isMarkActive(state, type)), + isEnable: toggleMark(type), + run: (state, dispatch, _view, attrs) => { + const params = attrs as ColorActionParams | undefined; + const hasMark = isMarkActive(state, type); - if (!params || !params[color]) { - if (!hasMark) return true; + if (!params || !params[colorMarkName]) { + if (!hasMark) return true; - // remove mark - return toggleMark(type, params)(state, dispatch); - } + // remove mark + return toggleMark(type, params)(state, dispatch); + } - if (hasMark) { - // remove old mark, then add new with new color - return chainAND(toggleMark(type), toggleMark(type, params))( - state, - dispatch, - ); - } + if (hasMark) { + // remove old mark, then add new with new color + return chainAND(toggleMark(type), toggleMark(type, params))(state, dispatch); + } - // add mark - return toggleMark(type, params)(state, dispatch); - }, - meta(state): Colors { - return type.isInSet(state.selection.$to.marks())?.attrs[color]; - }, - }; - }); + // add mark + return toggleMark(type, params)(state, dispatch); + }, + meta(state): Colors { + return type.isInSet(state.selection.$to.marks())?.attrs[colorMarkName]; + }, + }; + }); }; declare global { diff --git a/src/extensions/yfm/ImgSize/ImgSizeSpecs/index.ts b/src/extensions/yfm/ImgSize/ImgSizeSpecs/index.ts new file mode 100644 index 00000000..c962e080 --- /dev/null +++ b/src/extensions/yfm/ImgSize/ImgSizeSpecs/index.ts @@ -0,0 +1,94 @@ +import isNumber from 'is-number'; +import type {NodeSpec} from 'prosemirror-model'; +import imsize from '@doc-tools/transform/lib/plugins/imsize'; +import {ImsizeAttr as ImgSizeAttr} from '@doc-tools/transform/lib/plugins/imsize/const'; +import log from '@doc-tools/transform/lib/log'; + +import type {ExtensionAuto} from '../../../../core'; +import {imageNodeName} from '../../../markdown/Image/const'; + +type ImsizeTypedAttributes = { + [ImgSizeAttr.Src]: string; + [ImgSizeAttr.Title]: string | null; + [ImgSizeAttr.Alt]: string | null; + [ImgSizeAttr.Width]: string | null; + [ImgSizeAttr.Height]: string | null; +}; + +export {ImgSizeAttr}; + +export type ImgSizeSpecsOptions = { + placeholder?: NodeSpec['placeholder']; +}; + +export const ImgSizeSpecs: ExtensionAuto = (builder, opts) => { + builder.configureMd((md) => md.use(imsize, {log})); + builder.addNode(imageNodeName, () => ({ + spec: { + inline: true, + attrs: { + [ImgSizeAttr.Src]: {}, + [ImgSizeAttr.Alt]: {default: null}, + [ImgSizeAttr.Title]: {default: null}, + [ImgSizeAttr.Height]: {default: null}, + [ImgSizeAttr.Width]: {default: null}, + }, + placeholder: opts.placeholder, + group: 'inline', + draggable: true, + parseDOM: [ + { + tag: 'img[src]', + getAttrs(dom) { + const height = (dom as Element).getAttribute(ImgSizeAttr.Height); + const width = (dom as Element).getAttribute(ImgSizeAttr.Width); + + return { + [ImgSizeAttr.Src]: (dom as Element).getAttribute(ImgSizeAttr.Src), + [ImgSizeAttr.Alt]: (dom as Element).getAttribute(ImgSizeAttr.Alt), + [ImgSizeAttr.Title]: (dom as Element).getAttribute(ImgSizeAttr.Title), + [ImgSizeAttr.Height]: isNumber(height) ? height : null, + [ImgSizeAttr.Width]: isNumber(width) ? height : null, + }; + }, + }, + ], + toDOM(node) { + return ['img', node.attrs]; + }, + }, + fromYfm: { + tokenSpec: { + name: imageNodeName, + type: 'node', + getAttrs: (tok): ImsizeTypedAttributes => ({ + [ImgSizeAttr.Src]: tok.attrGet(ImgSizeAttr.Src)!, + [ImgSizeAttr.Title]: tok.attrGet(ImgSizeAttr.Title), + [ImgSizeAttr.Height]: tok.attrGet(ImgSizeAttr.Height), + [ImgSizeAttr.Width]: tok.attrGet(ImgSizeAttr.Width), + [ImgSizeAttr.Alt]: tok.children?.[0]?.content || null, + }), + }, + }, + toYfm: (state, node) => { + const attrs = node.attrs as ImsizeTypedAttributes; + state.write( + '![' + + state.esc(attrs.alt || '') + + '](' + + state.esc(attrs.src) + + (attrs.title ? ' ' + state.quote(attrs.title) : '') + + getSize(attrs) + + ')', + ); + }, + })); +}; + +function getSize({width, height}: ImsizeTypedAttributes): string { + if (width || height) { + return ` =${width || ''}x${height || ''}`; + } + + return ''; +} diff --git a/src/extensions/yfm/ImgSize/YfmImage.test.ts b/src/extensions/yfm/ImgSize/YfmImage.test.ts index 54f77502..1c45b147 100644 --- a/src/extensions/yfm/ImgSize/YfmImage.test.ts +++ b/src/extensions/yfm/ImgSize/YfmImage.test.ts @@ -1,41 +1,37 @@ -/** - * @jest-environment jsdom - */ - import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {ExtensionsManager} from '../../../core'; import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {ImgSize} from './index'; -import {image, ImageAttr} from './const'; +import {ImgSizeSpecs} from './ImgSizeSpecs'; +import {imageNodeName, ImgSizeAttr} from './const'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(ImgSize, {}), + extensions: (builder) => builder.use(BaseSchema, {}).use(ImgSizeSpecs, {}), }).buildDeps(); const {doc, p, img, img2, img3, img4} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - img: {nodeType: image, [ImageAttr.Src]: 'img.png'}, + img: {nodeType: imageNodeName, [ImgSizeAttr.Src]: 'img.png'}, img2: { - nodeType: image, - [ImageAttr.Src]: 'img2.png', - [ImageAttr.Alt]: 'alt text', - [ImageAttr.Title]: 'title text', + nodeType: imageNodeName, + [ImgSizeAttr.Src]: 'img2.png', + [ImgSizeAttr.Alt]: 'alt text', + [ImgSizeAttr.Title]: 'title text', }, img3: { - nodeType: image, - [ImageAttr.Src]: 'img3.png', - [ImageAttr.Height]: '100', - [ImageAttr.Width]: '200', + nodeType: imageNodeName, + [ImgSizeAttr.Src]: 'img3.png', + [ImgSizeAttr.Height]: '100', + [ImgSizeAttr.Width]: '200', }, img4: { - nodeType: image, - [ImageAttr.Src]: 'img4.png', - [ImageAttr.Height]: '300', - [ImageAttr.Width]: '400', - [ImageAttr.Alt]: 'alt text 2', - [ImageAttr.Title]: 'title text 2', + nodeType: imageNodeName, + [ImgSizeAttr.Src]: 'img4.png', + [ImgSizeAttr.Height]: '300', + [ImgSizeAttr.Width]: '400', + [ImgSizeAttr.Alt]: 'alt text 2', + [ImgSizeAttr.Title]: 'title text 2', }, }) as PMTestBuilderResult<'doc' | 'p' | 'img' | 'img2' | 'img3' | 'img4'>; diff --git a/src/extensions/yfm/ImgSize/actions.ts b/src/extensions/yfm/ImgSize/actions.ts index 1157e3b9..3928fae0 100644 --- a/src/extensions/yfm/ImgSize/actions.ts +++ b/src/extensions/yfm/ImgSize/actions.ts @@ -2,7 +2,7 @@ import isNumber from 'is-number'; import type {Schema} from 'prosemirror-model'; import type {ActionSpec} from '../../../core'; import {AddImageAttrs as AddImageAttrsBase, imgType} from '../../markdown/Image'; -import {ImageAttr} from './const'; +import {ImgSizeAttr} from './const'; export type AddImageAttrs = AddImageAttrsBase & { width?: string | number; @@ -18,16 +18,16 @@ export const addImage = (schema: Schema): ActionSpec => { if (params?.src) { const {src, title, alt, width, height} = params; const imgAttrs: {[key: string]: string} = { - [ImageAttr.Src]: src, - [ImageAttr.Title]: title ?? '', - [ImageAttr.Alt]: alt ?? '', + [ImgSizeAttr.Src]: src, + [ImgSizeAttr.Title]: title ?? '', + [ImgSizeAttr.Alt]: alt ?? '', }; if (isNumber(width)) { - imgAttrs[ImageAttr.Width] = String(width); + imgAttrs[ImgSizeAttr.Width] = String(width); } if (isNumber(height)) { - imgAttrs[ImageAttr.Height] = String(height); + imgAttrs[ImgSizeAttr.Height] = String(height); } dispatch(state.tr.insert(state.selection.from, imgType(schema).create(imgAttrs))); diff --git a/src/extensions/yfm/ImgSize/const.ts b/src/extensions/yfm/ImgSize/const.ts index e682eefb..c878fe46 100644 --- a/src/extensions/yfm/ImgSize/const.ts +++ b/src/extensions/yfm/ImgSize/const.ts @@ -1,2 +1,2 @@ -export {ImsizeAttr as ImageAttr} from '@doc-tools/transform/lib/plugins/imsize/const'; -export {image, addImageAction} from '../../markdown/Image/const'; +export {ImgSizeAttr} from './ImgSizeSpecs'; +export {imageNodeName, addImageAction} from '../../markdown/Image/const'; diff --git a/src/extensions/yfm/ImgSize/index.ts b/src/extensions/yfm/ImgSize/index.ts index 66a5da65..6b5e4139 100644 --- a/src/extensions/yfm/ImgSize/index.ts +++ b/src/extensions/yfm/ImgSize/index.ts @@ -1,89 +1,14 @@ -import isNumber from 'is-number'; -import type {NodeSpec} from 'prosemirror-model'; -import imsize from '@doc-tools/transform/lib/plugins/imsize'; -import {ImsizeAttr} from '@doc-tools/transform/lib/plugins/imsize/const'; -import log from '@doc-tools/transform/lib/log'; - import type {Action, ExtensionAuto} from '../../../core'; -import {addImageAction, image} from './const'; +import {addImageAction} from './const'; import {addImage, AddImageAttrs} from './actions'; +import {ImgSizeSpecs, ImgSizeSpecsOptions} from './ImgSizeSpecs'; -type ImsizeTypedAttributes = { - [ImsizeAttr.Src]: string; - [ImsizeAttr.Title]: string | null; - [ImsizeAttr.Alt]: string | null; - [ImsizeAttr.Width]: string | null; - [ImsizeAttr.Height]: string | null; -}; +export {ImgSizeAttr} from './ImgSizeSpecs'; -export {ImageAttr} from './const'; - -export type ImgSizeOptions = { - placeholder?: NodeSpec['placeholder']; -}; +export type ImgSizeOptions = ImgSizeSpecsOptions & {}; export const ImgSize: ExtensionAuto = (builder, opts) => { - builder.configureMd((md) => md.use(imsize, {log})); - builder.addNode(image, () => ({ - spec: { - inline: true, - attrs: { - [ImsizeAttr.Src]: {}, - [ImsizeAttr.Alt]: {default: null}, - [ImsizeAttr.Title]: {default: null}, - [ImsizeAttr.Height]: {default: null}, - [ImsizeAttr.Width]: {default: null}, - }, - placeholder: opts.placeholder, - group: 'inline', - draggable: true, - parseDOM: [ - { - tag: 'img[src]', - getAttrs(dom) { - const height = (dom as Element).getAttribute(ImsizeAttr.Height); - const width = (dom as Element).getAttribute(ImsizeAttr.Width); - - return { - [ImsizeAttr.Src]: (dom as Element).getAttribute(ImsizeAttr.Src), - [ImsizeAttr.Alt]: (dom as Element).getAttribute(ImsizeAttr.Alt), - [ImsizeAttr.Title]: (dom as Element).getAttribute(ImsizeAttr.Title), - [ImsizeAttr.Height]: isNumber(height) ? height : null, - [ImsizeAttr.Width]: isNumber(width) ? height : null, - }; - }, - }, - ], - toDOM(node) { - return ['img', node.attrs]; - }, - }, - fromYfm: { - tokenSpec: { - name: image, - type: 'node', - getAttrs: (tok): ImsizeTypedAttributes => ({ - [ImsizeAttr.Src]: tok.attrGet(ImsizeAttr.Src)!, - [ImsizeAttr.Title]: tok.attrGet(ImsizeAttr.Title), - [ImsizeAttr.Height]: tok.attrGet(ImsizeAttr.Height), - [ImsizeAttr.Width]: tok.attrGet(ImsizeAttr.Width), - [ImsizeAttr.Alt]: tok.children?.[0]?.content || null, - }), - }, - }, - toYfm: (state, node) => { - const attrs = node.attrs as ImsizeTypedAttributes; - state.write( - '![' + - state.esc(attrs.alt || '') + - '](' + - state.esc(attrs.src) + - (attrs.title ? ' ' + state.quote(attrs.title) : '') + - getSize(attrs) + - ')', - ); - }, - })); + builder.use(ImgSizeSpecs, opts); builder.addAction(addImageAction, ({schema}) => addImage(schema)); }; @@ -96,11 +21,3 @@ declare global { } } } - -function getSize({width, height}: ImsizeTypedAttributes): string { - if (width || height) { - return ` =${width || ''}x${height || ''}`; - } - - return ''; -} diff --git a/src/extensions/yfm/Math/Math.test.ts b/src/extensions/yfm/Math/Math.test.ts index 50912c3b..de971361 100644 --- a/src/extensions/yfm/Math/Math.test.ts +++ b/src/extensions/yfm/Math/Math.test.ts @@ -1,12 +1,11 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {MathNode} from './const'; -import {Math} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {MathSpecs, MathNode} from './MathSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Math), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(MathSpecs), }).buildDeps(); const {doc, p, mathB, mathI} = builders(schema, { diff --git a/src/extensions/yfm/Math/MathSpecs/const.ts b/src/extensions/yfm/Math/MathSpecs/const.ts new file mode 100644 index 00000000..d37c21fc --- /dev/null +++ b/src/extensions/yfm/Math/MathSpecs/const.ts @@ -0,0 +1,12 @@ +export enum MathNode { + Inline = 'math_inline', + Block = 'math_display', +} + +export const CLASSNAMES = { + Inline: { + Container: 'math-inline', + Sharp: 'math-inline__sharp', + Content: 'math-inline__content', + }, +} as const; diff --git a/src/extensions/yfm/Math/MathSpecs/index.ts b/src/extensions/yfm/Math/MathSpecs/index.ts new file mode 100644 index 00000000..e0f839d7 --- /dev/null +++ b/src/extensions/yfm/Math/MathSpecs/index.ts @@ -0,0 +1,57 @@ +import mathMdPlugin from 'markdown-it-katex'; + +import type {ExtensionAuto} from '../../../../core'; +import {nodeTypeFactory} from '../../../../utils/schema'; +import {CLASSNAMES, MathNode} from './const'; + +export {MathNode} from './const'; +export const mathIType = nodeTypeFactory(MathNode.Inline); +export const mathBType = nodeTypeFactory(MathNode.Block); + +export const MathSpecs: ExtensionAuto = (builder) => { + builder.configureMd((md) => md.use(mathMdPlugin)); + builder + .addNode(MathNode.Inline, () => ({ + spec: { + group: 'inline math', + content: 'text*', + inline: true, + marks: '', + code: true, + toDOM: () => [ + 'span', + {class: CLASSNAMES.Inline.Container}, + ['span', {class: CLASSNAMES.Inline.Sharp, contenteditable: 'false'}, '$'], + ['span', {class: CLASSNAMES.Inline.Content}, 0], + ['span', {class: CLASSNAMES.Inline.Sharp, contenteditable: 'false'}, '$'], + ], + parseDOM: [{tag: `span.${CLASSNAMES.Inline.Content}`, priority: 200}], + }, + fromYfm: { + tokenName: 'math_inline', + tokenSpec: {name: MathNode.Inline, type: 'block', noCloseToken: true}, + }, + toYfm: (state, node) => { + state.text(`$${node.textContent}$`, false); + state.closeBlock(); + }, + })) + .addNode(MathNode.Block, () => ({ + spec: { + group: 'block math', + content: 'text*', + marks: '', + code: true, + toDOM: () => ['div', {class: 'math-block'}, 0], + parseDOM: [{tag: 'div.math-block', priority: 200}], + }, + fromYfm: { + tokenName: 'math_block', + tokenSpec: {name: MathNode.Block, type: 'block', noCloseToken: true}, + }, + toYfm: (state, node) => { + state.text(`$$${node.textContent}$$\n\n`, false); + state.closeBlock(); + }, + })); +}; diff --git a/src/extensions/yfm/Math/const.ts b/src/extensions/yfm/Math/const.ts index 7c1e54a5..275de207 100644 --- a/src/extensions/yfm/Math/const.ts +++ b/src/extensions/yfm/Math/const.ts @@ -1,17 +1,2 @@ -import {nodeTypeFactory} from '../../../utils/schema'; - -export enum MathNode { - Inline = 'math_inline', - Block = 'math_display', -} - -export const CLASSNAMES = { - Inline: { - Container: 'math-inline', - Sharp: 'math-inline__sharp', - Content: 'math-inline__content', - }, -} as const; - -export const mathIType = nodeTypeFactory(MathNode.Inline); -export const mathBType = nodeTypeFactory(MathNode.Block); +export * from './MathSpecs/const'; +export {mathBType, mathIType} from './MathSpecs'; diff --git a/src/extensions/yfm/Math/index.ts b/src/extensions/yfm/Math/index.ts index 59c43276..af2ed136 100644 --- a/src/extensions/yfm/Math/index.ts +++ b/src/extensions/yfm/Math/index.ts @@ -1,13 +1,15 @@ -import mathMdPlugin from 'markdown-it-katex'; import {chainCommands, setBlockType} from 'prosemirror-commands'; import {hasParentNodeOfType} from 'prosemirror-utils'; import {Command, TextSelection} from 'prosemirror-state'; import {textblockTypeInputRule} from 'prosemirror-inputrules'; + import type {Action, ExtensionAuto} from '../../../core'; import {isTextSelection} from '../../../utils/selection'; import {inlineNodeInputRule} from '../../../utils/inputrules'; + +import {MathSpecs} from './MathSpecs'; import {mathViewAndEditPlugin} from './view-and-edit'; -import {CLASSNAMES, mathBType, mathIType, MathNode} from './const'; +import {mathBType, mathIType} from './const'; import { ignoreIfCursorInsideMathInline, moveCursorToEndOfMathInline, @@ -16,7 +18,7 @@ import { import './index.scss'; -export {MathNode} from './const'; +export {MathNode, mathBType, mathIType} from './MathSpecs'; export {MathBlockNodeView, MathInlineNodeView} from './view-and-edit'; const mathIAction = 'addMathInline'; @@ -25,51 +27,7 @@ const mathBAction = 'toMathBlock'; const mathITemplate = 'f(x)='; export const Math: ExtensionAuto = (builder) => { - builder.configureMd((md) => md.use(mathMdPlugin)); - builder - .addNode(MathNode.Inline, () => ({ - spec: { - group: 'inline math', - content: 'text*', - inline: true, - marks: '', - code: true, - toDOM: () => [ - 'span', - {class: CLASSNAMES.Inline.Container}, - ['span', {class: CLASSNAMES.Inline.Sharp, contenteditable: 'false'}, '$'], - ['span', {class: CLASSNAMES.Inline.Content}, 0], - ['span', {class: CLASSNAMES.Inline.Sharp, contenteditable: 'false'}, '$'], - ], - parseDOM: [{tag: `span.${CLASSNAMES.Inline.Content}`, priority: 200}], - }, - fromYfm: { - tokenName: 'math_inline', - tokenSpec: {name: MathNode.Inline, type: 'block', noCloseToken: true}, - }, - toYfm: (state, node) => { - state.text(`$${node.textContent}$`, false); - state.closeBlock(); - }, - })) - .addNode(MathNode.Block, () => ({ - spec: { - group: 'block math', - content: 'text*', - marks: '', - code: true, - toDOM: () => ['div', {class: 'math-block'}, 0], - parseDOM: [{tag: 'div.math-block', priority: 200}], - }, - fromYfm: { - tokenName: 'math_block', - tokenSpec: {name: MathNode.Block, type: 'block', noCloseToken: true}, - }, - toYfm: (state, node) => { - state.text(`$$${node.textContent}$$\n\n`, false); - state.closeBlock(); - }, - })); + builder.use(MathSpecs); builder.addKeymap(() => ({ Enter: ignoreIfCursorInsideMathInline, // ignore breaks in math inline diff --git a/src/extensions/yfm/Monospace/Monospace.test.ts b/src/extensions/yfm/Monospace/Monospace.test.ts index 3be5b95b..1fd8ac44 100644 --- a/src/extensions/yfm/Monospace/Monospace.test.ts +++ b/src/extensions/yfm/Monospace/Monospace.test.ts @@ -2,17 +2,17 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {monospace, Monospace} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {monospaceMarkName, MonospaceSpecs} from './MonospaceSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Monospace), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(MonospaceSpecs), }).buildDeps(); const {doc, p, m} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - m: {markType: monospace}, + m: {markType: monospaceMarkName}, }) as PMTestBuilderResult<'doc' | 'p', 'm'>; const {same} = createMarkupChecker({parser, serializer}); diff --git a/src/extensions/yfm/Monospace/MonospaceSpecs/index.ts b/src/extensions/yfm/Monospace/MonospaceSpecs/index.ts new file mode 100644 index 00000000..569baca2 --- /dev/null +++ b/src/extensions/yfm/Monospace/MonospaceSpecs/index.ts @@ -0,0 +1,33 @@ +import log from '@doc-tools/transform/lib/log'; +import yfmPlugin from '@doc-tools/transform/lib/plugins/monospace'; + +import type {ExtensionAuto} from '../../../../core'; +import {markTypeFactory} from '../../../../utils/schema'; + +export const monospaceMarkName = 'monospace'; +export const monospaceType = markTypeFactory(monospaceMarkName); + +export const MonospaceSpecs: ExtensionAuto = (builder) => { + builder + .configureMd((md) => md.use(yfmPlugin, {log})) + .addMark(monospaceMarkName, () => ({ + spec: { + parseDOM: [{tag: 'samp'}], + toDOM() { + return ['samp']; + }, + }, + fromYfm: { + tokenSpec: { + name: monospaceMarkName, + type: 'mark', + }, + }, + toYfm: { + open: '##', + close: '##', + mixable: true, + expelEnclosingWhitespace: true, + }, + })); +}; diff --git a/src/extensions/yfm/Monospace/index.ts b/src/extensions/yfm/Monospace/index.ts index 02e1c42c..c738e871 100644 --- a/src/extensions/yfm/Monospace/index.ts +++ b/src/extensions/yfm/Monospace/index.ts @@ -1,40 +1,23 @@ -import yfmPlugin from '@doc-tools/transform/lib/plugins/monospace'; import type {Action, ExtensionAuto} from '../../../core'; -import {markTypeFactory} from '../../../utils/schema'; import {createToggleMarkAction} from '../../../utils/actions'; import {markInputRule} from '../../../utils/inputrules'; -import log from '@doc-tools/transform/lib/log'; -export const monospace = 'monospace'; +import {monospaceMarkName, MonospaceSpecs, monospaceType} from './MonospaceSpecs'; + +export {monospaceMarkName, monospaceType} from './MonospaceSpecs'; +/** @deprecated Use `monospaceMarkName` instead */ +export const monospace = monospaceMarkName; const monoAction = 'mono'; -const monoType = markTypeFactory(monospace); export const Monospace: ExtensionAuto = (builder) => { + builder.use(MonospaceSpecs); + builder - .configureMd((md) => md.use(yfmPlugin, {log})) - .addMark(monospace, () => ({ - spec: { - parseDOM: [{tag: 'samp'}], - toDOM() { - return ['samp']; - }, - }, - fromYfm: { - tokenSpec: { - name: monospace, - type: 'mark', - }, - }, - toYfm: { - open: '##', - close: '##', - mixable: true, - expelEnclosingWhitespace: true, - }, - })) - .addAction(monoAction, ({schema}) => createToggleMarkAction(monoType(schema))) + .addAction(monoAction, ({schema}) => createToggleMarkAction(monospaceType(schema))) .addInputRules(({schema}) => ({ - rules: [markInputRule({open: '##', close: '##', ignoreBetween: '#'}, monoType(schema))], + rules: [ + markInputRule({open: '##', close: '##', ignoreBetween: '#'}, monospaceType(schema)), + ], })); }; diff --git a/src/extensions/yfm/Video/Video.test.ts b/src/extensions/yfm/Video/Video.test.ts index 02c99537..d6f204aa 100644 --- a/src/extensions/yfm/Video/Video.test.ts +++ b/src/extensions/yfm/Video/Video.test.ts @@ -1,13 +1,13 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {Video} from './index'; -import {video, VideoAttr} from './const'; -import {VideoService} from './md-video'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {VideoSpecs} from './VideoSpecs'; +import {VideoAttr, videoNodeName} from './VideoSpecs/const'; +import {VideoService} from './VideoSpecs/md-video'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Video, {}), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(VideoSpecs, {}), }).buildDeps(); const {doc, p} = builders(schema, { @@ -24,7 +24,7 @@ describe('Video extension', () => { doc( p( 'YouTube ', - schema.node(video, { + schema.node(videoNodeName, { [VideoAttr.Service]: VideoService.YouTube, [VideoAttr.VideoID]: 'dQw4w9WgXcQ', }), @@ -39,7 +39,7 @@ describe('Video extension', () => { doc( p( 'Vimeo ', - schema.node(video, { + schema.node(videoNodeName, { [VideoAttr.Service]: VideoService.Vimeo, [VideoAttr.VideoID]: '19706846', }), @@ -54,7 +54,7 @@ describe('Video extension', () => { doc( p( 'Vine ', - schema.node(video, { + schema.node(videoNodeName, { [VideoAttr.Service]: VideoService.Vine, [VideoAttr.VideoID]: 'etVpwB7uHlw', }), @@ -69,7 +69,7 @@ describe('Video extension', () => { doc( p( 'Prezi ', - schema.node(video, { + schema.node(videoNodeName, { [VideoAttr.Service]: VideoService.Prezi, [VideoAttr.VideoID]: '1kkxdtlp4241', }), @@ -84,7 +84,7 @@ describe('Video extension', () => { doc( p( 'Osf ', - schema.node(video, { + schema.node(videoNodeName, { [VideoAttr.Service]: VideoService.Osf, [VideoAttr.VideoID]: 'kuvg9', }), @@ -105,27 +105,27 @@ describe('Video extension', () => { doc( p( 'YouTube ', - schema.node(video, { + schema.node(videoNodeName, { [VideoAttr.Service]: VideoService.YouTube, [VideoAttr.VideoID]: 'yt-video-1', }), ' Vimeo ', - schema.node(video, { + schema.node(videoNodeName, { [VideoAttr.Service]: VideoService.Vimeo, [VideoAttr.VideoID]: 'vimeo-video-1', }), ' Vine ', - schema.node(video, { + schema.node(videoNodeName, { [VideoAttr.Service]: VideoService.Vine, [VideoAttr.VideoID]: 'vine-video-1', }), ' Prezi ', - schema.node(video, { + schema.node(videoNodeName, { [VideoAttr.Service]: VideoService.Vine, [VideoAttr.VideoID]: 'prezi-video-1', }), ' Osf ', - schema.node(video, { + schema.node(videoNodeName, { [VideoAttr.Service]: VideoService.Osf, [VideoAttr.VideoID]: 'osf-video-1', }), diff --git a/src/extensions/yfm/Video/VideoSpecs/const.ts b/src/extensions/yfm/Video/VideoSpecs/const.ts new file mode 100644 index 00000000..09adc896 --- /dev/null +++ b/src/extensions/yfm/Video/VideoSpecs/const.ts @@ -0,0 +1,6 @@ +export const videoNodeName = 'video'; + +export enum VideoAttr { + Service = 'service', + VideoID = 'videoid', +} diff --git a/src/extensions/yfm/Video/VideoSpecs/index.ts b/src/extensions/yfm/Video/VideoSpecs/index.ts new file mode 100644 index 00000000..2d802db1 --- /dev/null +++ b/src/extensions/yfm/Video/VideoSpecs/index.ts @@ -0,0 +1,99 @@ +import type {ExtensionAuto} from '../../../../core'; +import {VideoAttr, videoNodeName} from './const'; +import {createViewStub, serializeNodeToString} from './utils'; +import {defaults, VideoPluginOptions, VideoService, videoPlugin, VideoToken} from './md-video'; +import log from '@doc-tools/transform/lib/log'; + +// we don't support osf service +const availableServices: ReadonlySet = new Set([ + VideoService.YouTube, + VideoService.Vimeo, + VideoService.Vine, + VideoService.Prezi, +]); + +export {videoNodeName} from './const'; +export {videoType} from './utils'; + +export type VideoSpecsOptions = VideoPluginOptions & {}; + +export const VideoSpecs: ExtensionAuto = (builder, opts) => { + const options = {...defaults, ...opts, log}; + + builder.configureMd((md) => md.use(videoPlugin, options)); + builder.addNode(videoNodeName, () => ({ + spec: { + inline: true, + atom: true, + group: 'inline', + attrs: {[VideoAttr.Service]: {}, [VideoAttr.VideoID]: {}}, + parseDOM: [ + { + tag: `span[${VideoAttr.Service}][${VideoAttr.VideoID}]`, + getAttrs(dom) { + const domElem = dom as HTMLElement; + return { + [VideoAttr.Service]: domElem.getAttribute(VideoAttr.Service), + [VideoAttr.VideoID]: domElem.getAttribute(VideoAttr.VideoID), + }; + }, + }, + { + tag: `iframe[${VideoAttr.Service}][${VideoAttr.VideoID}]`, + getAttrs(dom) { + const domElem = dom as HTMLElement; + return { + [VideoAttr.Service]: domElem.getAttribute(VideoAttr.Service), + [VideoAttr.VideoID]: domElem.getAttribute(VideoAttr.VideoID), + }; + }, + priority: 100, + }, + ], + toDOM(node) { + const service = node.attrs[VideoAttr.Service]; + const videoId = node.attrs[VideoAttr.VideoID]; + + if (availableServices.has(service) || !videoId) { + return [ + 'div', + { + class: 'embed-responsive embed-responsive-16by9', + }, + [ + 'iframe', + { + class: `embed-responsive-item ${service}-player`, + type: 'text/html', + width: String(options[service as VideoService].width), + height: String(options[service as VideoService].height), + src: options.url(service, videoId, options), + frameborder: '0', + webkitallowfullscreen: '', + mozallowfullscreen: '', + allowfullscreen: '', + [VideoAttr.Service]: service, + [VideoAttr.VideoID]: videoId, + }, + ], + ]; + } + + return createViewStub(node); + }, + }, + fromYfm: { + tokenSpec: { + name: videoNodeName, + type: 'node', + getAttrs: (tok) => ({ + [VideoAttr.Service]: (tok as VideoToken).service, + [VideoAttr.VideoID]: (tok as VideoToken).videoID, + }), + }, + }, + toYfm: (state, node) => { + state.write(serializeNodeToString(node)); + }, + })); +}; diff --git a/src/extensions/yfm/Video/md-video.ts b/src/extensions/yfm/Video/VideoSpecs/md-video.ts similarity index 100% rename from src/extensions/yfm/Video/md-video.ts rename to src/extensions/yfm/Video/VideoSpecs/md-video.ts diff --git a/src/extensions/yfm/Video/utils.ts b/src/extensions/yfm/Video/VideoSpecs/utils.ts similarity index 72% rename from src/extensions/yfm/Video/utils.ts rename to src/extensions/yfm/Video/VideoSpecs/utils.ts index 4090d15b..d2976e80 100644 --- a/src/extensions/yfm/Video/utils.ts +++ b/src/extensions/yfm/Video/VideoSpecs/utils.ts @@ -1,8 +1,8 @@ -import {Node} from 'prosemirror-model'; -import {nodeTypeFactory} from '../../../utils/schema'; -import {video, VideoAttr} from './const'; +import type {Node} from 'prosemirror-model'; +import {nodeTypeFactory} from '../../../../utils/schema'; +import {videoNodeName, VideoAttr} from './const'; -export const vType = nodeTypeFactory(video); +export const videoType = nodeTypeFactory(videoNodeName); export function serializeNodeToString(node: Node) { const service = node.attrs[VideoAttr.Service]; diff --git a/src/extensions/yfm/Video/actions.ts b/src/extensions/yfm/Video/actions.ts index f1b73f28..9f19dbe9 100644 --- a/src/extensions/yfm/Video/actions.ts +++ b/src/extensions/yfm/Video/actions.ts @@ -1,8 +1,8 @@ import type {ActionSpec} from '../../../core'; -import {VideoActionAttrs} from '.'; -import {VideoAttr} from './const'; -import {parseVideoUrl} from './md-video'; -import {vType} from './utils'; +import type {VideoActionAttrs} from './index'; +import {VideoAttr} from './VideoSpecs/const'; +import {parseVideoUrl} from './VideoSpecs/md-video'; +import {videoType} from './VideoSpecs/utils'; export const addVideo: ActionSpec = { isEnable(state) { @@ -19,7 +19,7 @@ export const addVideo: ActionSpec = { dispatch( state.tr.insert( state.selection.from, - vType(state.schema).create({ + videoType(state.schema).create({ [VideoAttr.Service]: service, [VideoAttr.VideoID]: videoID, }), diff --git a/src/extensions/yfm/Video/const.ts b/src/extensions/yfm/Video/const.ts index 1516adcb..0dbed8ee 100644 --- a/src/extensions/yfm/Video/const.ts +++ b/src/extensions/yfm/Video/const.ts @@ -1,7 +1,3 @@ -export const video = 'video'; -export const vAction = 'video'; +export * from './VideoSpecs/const'; -export enum VideoAttr { - Service = 'service', - VideoID = 'videoid', -} +export const vAction = 'video'; diff --git a/src/extensions/yfm/Video/index.ts b/src/extensions/yfm/Video/index.ts index 7e050a8a..7650cb7c 100644 --- a/src/extensions/yfm/Video/index.ts +++ b/src/extensions/yfm/Video/index.ts @@ -1,106 +1,20 @@ import type {Action, ExtensionAuto} from '../../../core'; -import {vAction, video, VideoAttr} from './const'; -import {createViewStub, serializeNodeToString} from './utils'; +import {vAction} from './const'; import {addVideo} from './actions'; -import {defaults, VideoPluginOptions, VideoService, videoPlugin, VideoToken} from './md-video'; -import log from '@doc-tools/transform/lib/log'; +import {VideoService} from './VideoSpecs/md-video'; +import {VideoSpecs, VideoSpecsOptions} from './VideoSpecs'; + +export {videoNodeName, videoType} from './VideoSpecs'; export type VideoActionAttrs = { service: VideoService; url: string; }; -// we don't support osf service -const availableServices: ReadonlySet = new Set([ - VideoService.YouTube, - VideoService.Vimeo, - VideoService.Vine, - VideoService.Prezi, -]); - -export {vType as videoType} from './utils'; - -export type VideoOptions = VideoPluginOptions & {}; +export type VideoOptions = VideoSpecsOptions & {}; export const Video: ExtensionAuto = (builder, opts) => { - const options = {...defaults, ...opts, log}; - - builder.configureMd((md) => md.use(videoPlugin, options)); - builder.addNode(video, () => ({ - spec: { - inline: true, - atom: true, - group: 'inline', - attrs: {[VideoAttr.Service]: {}, [VideoAttr.VideoID]: {}}, - parseDOM: [ - { - tag: `span[${VideoAttr.Service}][${VideoAttr.VideoID}]`, - getAttrs(dom) { - const domElem = dom as HTMLElement; - return { - [VideoAttr.Service]: domElem.getAttribute(VideoAttr.Service), - [VideoAttr.VideoID]: domElem.getAttribute(VideoAttr.VideoID), - }; - }, - }, - { - tag: `iframe[${VideoAttr.Service}][${VideoAttr.VideoID}]`, - getAttrs(dom) { - const domElem = dom as HTMLElement; - return { - [VideoAttr.Service]: domElem.getAttribute(VideoAttr.Service), - [VideoAttr.VideoID]: domElem.getAttribute(VideoAttr.VideoID), - }; - }, - priority: 100, - }, - ], - toDOM(node) { - const service = node.attrs[VideoAttr.Service]; - const videoId = node.attrs[VideoAttr.VideoID]; - - if (availableServices.has(service) || !videoId) { - return [ - 'div', - { - class: 'embed-responsive embed-responsive-16by9', - }, - [ - 'iframe', - { - class: `embed-responsive-item ${service}-player`, - type: 'text/html', - width: String(options[service as VideoService].width), - height: String(options[service as VideoService].height), - src: options.url(service, videoId, options), - frameborder: '0', - webkitallowfullscreen: '', - mozallowfullscreen: '', - allowfullscreen: '', - [VideoAttr.Service]: service, - [VideoAttr.VideoID]: videoId, - }, - ], - ]; - } - - return createViewStub(node); - }, - }, - fromYfm: { - tokenSpec: { - name: video, - type: 'node', - getAttrs: (tok) => ({ - [VideoAttr.Service]: (tok as VideoToken).service, - [VideoAttr.VideoID]: (tok as VideoToken).videoID, - }), - }, - }, - toYfm: (state, node) => { - state.write(serializeNodeToString(node)); - }, - })); + builder.use(VideoSpecs, opts); builder.addAction(vAction, () => addVideo); }; diff --git a/src/extensions/yfm/YfmCut/YfmCut.test.ts b/src/extensions/yfm/YfmCut/YfmCut.test.ts index d3582436..404aa0b9 100644 --- a/src/extensions/yfm/YfmCut/YfmCut.test.ts +++ b/src/extensions/yfm/YfmCut/YfmCut.test.ts @@ -2,21 +2,25 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {blockquote, Blockquote, italic, Italic} from '../../markdown'; -import {CutNode} from './const'; -import {YfmCut} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import { + blockquoteNodeName, + BlockquoteSpecs, + italicMarkName, + ItalicSpecs, +} from '../../markdown/specs'; +import {CutNode, YfmCutSpecs} from './YfmCutSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ extensions: (builder) => - builder.use(BaseSchema, {}).use(Italic, {}).use(Blockquote, {}).use(YfmCut, {}), + builder.use(BaseSpecsPreset, {}).use(ItalicSpecs).use(BlockquoteSpecs).use(YfmCutSpecs, {}), }).buildDeps(); const {doc, p, i, bq, cut, cutTitle, cutContent} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - i: {markType: italic}, - bq: {nodeType: blockquote}, + i: {markType: italicMarkName}, + bq: {nodeType: blockquoteNodeName}, cut: {nodeType: CutNode.Cut}, cutTitle: {nodeType: CutNode.CutTitle}, cutContent: {nodeType: CutNode.CutContent}, diff --git a/src/extensions/yfm/YfmCut/YfmCutSpecs/const.ts b/src/extensions/yfm/YfmCut/YfmCutSpecs/const.ts new file mode 100644 index 00000000..4095edb7 --- /dev/null +++ b/src/extensions/yfm/YfmCut/YfmCutSpecs/const.ts @@ -0,0 +1,5 @@ +export enum CutNode { + Cut = 'yfm_cut', + CutTitle = 'yfm_cut_title', + CutContent = 'yfm_cut_content', +} diff --git a/src/extensions/yfm/YfmCut/fromYfm.ts b/src/extensions/yfm/YfmCut/YfmCutSpecs/fromYfm.ts similarity index 88% rename from src/extensions/yfm/YfmCut/fromYfm.ts rename to src/extensions/yfm/YfmCut/YfmCutSpecs/fromYfm.ts index 0f1a0cb3..83c537fb 100644 --- a/src/extensions/yfm/YfmCut/fromYfm.ts +++ b/src/extensions/yfm/YfmCut/YfmCutSpecs/fromYfm.ts @@ -1,4 +1,4 @@ -import type {ParserToken} from '../../../core'; +import type {ParserToken} from '../../../../core'; import {CutNode} from './const'; const getAttrs: ParserToken['getAttrs'] = (tok) => (tok.attrs ? Object.fromEntries(tok.attrs) : {}); diff --git a/src/extensions/yfm/YfmCut/YfmCutSpecs/index.ts b/src/extensions/yfm/YfmCut/YfmCutSpecs/index.ts new file mode 100644 index 00000000..d861c577 --- /dev/null +++ b/src/extensions/yfm/YfmCut/YfmCutSpecs/index.ts @@ -0,0 +1,54 @@ +import log from '@doc-tools/transform/lib/log'; +import yfmPlugin from '@doc-tools/transform/lib/plugins/cut'; +import type {NodeSpec} from 'prosemirror-model'; + +import type {ExtensionAuto, YENodeSpec} from '../../../../core'; +import {nodeTypeFactory} from '../../../../utils/schema'; +import {CutNode} from './const'; +import {fromYfm} from './fromYfm'; +import {getSpec} from './spec'; +import {toYfm} from './toYfm'; + +export {CutNode} from './const'; +export const cutType = nodeTypeFactory(CutNode.Cut); +export const cutTitleType = nodeTypeFactory(CutNode.CutTitle); +export const cutContentType = nodeTypeFactory(CutNode.CutContent); + +export type YfmCutSpecsOptions = { + cutView?: YENodeSpec['view']; + cutTitleView?: YENodeSpec['view']; + cutContentView?: YENodeSpec['view']; + yfmCutTitlePlaceholder?: NonNullable['content']; + yfmCutContentPlaceholder?: NonNullable['content']; +}; + +export const YfmCutSpecs: ExtensionAuto = (builder, opts) => { + const spec = getSpec(opts); + + builder + .configureMd((md) => md.use(yfmPlugin, {log})) + .addNode(CutNode.Cut, () => ({ + spec: spec[CutNode.Cut], + toYfm: toYfm[CutNode.Cut], + fromYfm: { + tokenSpec: fromYfm[CutNode.Cut], + }, + view: opts.cutView, + })) + .addNode(CutNode.CutTitle, () => ({ + spec: spec[CutNode.CutTitle], + toYfm: toYfm[CutNode.CutTitle], + fromYfm: { + tokenSpec: fromYfm[CutNode.CutTitle], + }, + view: opts.cutTitleView, + })) + .addNode(CutNode.CutContent, () => ({ + spec: spec[CutNode.CutContent], + toYfm: toYfm[CutNode.CutContent], + fromYfm: { + tokenSpec: fromYfm[CutNode.CutContent], + }, + view: opts.cutContentView, + })); +}; diff --git a/src/extensions/yfm/YfmCut/spec.ts b/src/extensions/yfm/YfmCut/YfmCutSpecs/spec.ts similarity index 83% rename from src/extensions/yfm/YfmCut/spec.ts rename to src/extensions/yfm/YfmCut/YfmCutSpecs/spec.ts index cb48de44..7af9b24a 100644 --- a/src/extensions/yfm/YfmCut/spec.ts +++ b/src/extensions/yfm/YfmCut/YfmCutSpecs/spec.ts @@ -1,17 +1,13 @@ import type {NodeSpec} from 'prosemirror-model'; -import {CutNode} from './const'; +import type {YfmCutSpecsOptions} from './index'; +import {CutNode} from '../const'; const DEFAULT_PLACEHOLDERS = { Title: 'Cut title', Content: 'Cut content', }; -export type YfmCutSpecOptions = { - yfmCutTitlePlaceholder?: NonNullable['content']; - yfmCutContentPlaceholder?: NonNullable['content']; -}; - -export const getSpec = (opts?: YfmCutSpecOptions): Record => ({ +export const getSpec = (opts?: YfmCutSpecsOptions): Record => ({ [CutNode.Cut]: { attrs: {class: {default: 'yfm-cut'}}, content: `${CutNode.CutTitle} ${CutNode.CutContent}`, diff --git a/src/extensions/yfm/YfmCut/toYfm.ts b/src/extensions/yfm/YfmCut/YfmCutSpecs/toYfm.ts similarity index 79% rename from src/extensions/yfm/YfmCut/toYfm.ts rename to src/extensions/yfm/YfmCut/YfmCutSpecs/toYfm.ts index 8be94b6a..66beb29c 100644 --- a/src/extensions/yfm/YfmCut/toYfm.ts +++ b/src/extensions/yfm/YfmCut/YfmCutSpecs/toYfm.ts @@ -1,6 +1,6 @@ -import type {SerializerNodeToken} from '../../../core'; -import {getPlaceholderContent} from '../../behavior/Placeholder'; -import {isNodeEmpty} from '../../../utils/nodes'; +import type {SerializerNodeToken} from '../../../../core'; +import {getPlaceholderContent} from '../../../../utils/placeholder'; +import {isNodeEmpty} from '../../../../utils/nodes'; import {CutNode} from './const'; export const toYfm: Record = { diff --git a/src/extensions/yfm/YfmCut/commands.test.ts b/src/extensions/yfm/YfmCut/commands.test.ts index acc23199..04df6aa5 100644 --- a/src/extensions/yfm/YfmCut/commands.test.ts +++ b/src/extensions/yfm/YfmCut/commands.test.ts @@ -4,7 +4,7 @@ import {EditorState, TextSelection} from 'prosemirror-state'; import {builders} from 'prosemirror-test-builder'; import {CutNode} from './const'; -import {getSpec} from './spec'; +import {getSpec} from './YfmCutSpecs/spec'; import {backToCutTitle, removeCut} from './commands'; const schema = new Schema({ diff --git a/src/extensions/yfm/YfmCut/const.ts b/src/extensions/yfm/YfmCut/const.ts index c0338a21..f1eee8fa 100644 --- a/src/extensions/yfm/YfmCut/const.ts +++ b/src/extensions/yfm/YfmCut/const.ts @@ -1,11 +1 @@ -import {nodeTypeFactory} from '../../../utils/schema'; - -export enum CutNode { - Cut = 'yfm_cut', - CutTitle = 'yfm_cut_title', - CutContent = 'yfm_cut_content', -} - -export const cutType = nodeTypeFactory(CutNode.Cut); -export const cutTitleType = nodeTypeFactory(CutNode.CutTitle); -export const cutContentType = nodeTypeFactory(CutNode.CutContent); +export {CutNode, cutType, cutTitleType, cutContentType} from './YfmCutSpecs'; diff --git a/src/extensions/yfm/YfmCut/index.ts b/src/extensions/yfm/YfmCut/index.ts index 1e6a03d3..b9394fce 100644 --- a/src/extensions/yfm/YfmCut/index.ts +++ b/src/extensions/yfm/YfmCut/index.ts @@ -1,59 +1,39 @@ -import log from '@doc-tools/transform/lib/log'; -import yfmPlugin from '@doc-tools/transform/lib/plugins/cut'; import {chainCommands} from 'prosemirror-commands'; import type {Action, ExtensionAuto} from '../../../core'; import {nodeInputRule} from '../../../utils/inputrules'; -import {toYfm} from './toYfm'; -import {CutNode, cutType} from './const'; -import {fromYfm} from './fromYfm'; -import {getSpec, YfmCutSpecOptions} from './spec'; +import {cutType} from './const'; import {createYfmCut, toYfmCut} from './actions/toYfmCut'; import {backToCutTitle, exitFromCutTitle, liftEmptyBlockFromCut, removeCut} from './commands'; import {YfmCutTitleNodeView} from './nodeviews/yfm-cut-title'; import {cutAutoOpenPlugin} from './plugins/auto-open'; +import {YfmCutSpecs, YfmCutSpecsOptions} from './YfmCutSpecs'; const cutAction = 'toYfmCut'; -export {CutNode, cutType, cutTitleType, cutContentType} from './const'; +export {CutNode, cutType, cutTitleType, cutContentType} from './YfmCutSpecs'; -export type YfmCutOptions = YfmCutSpecOptions & { +export type YfmCutOptions = Pick< + YfmCutSpecsOptions, + 'yfmCutTitlePlaceholder' | 'yfmCutContentPlaceholder' +> & { yfmCutKey?: string | null; }; export const YfmCut: ExtensionAuto = (builder, opts) => { - const spec = getSpec(opts); - - builder - .configureMd((md) => md.use(yfmPlugin, {log})) - .addNode(CutNode.Cut, () => ({ - spec: spec[CutNode.Cut], - toYfm: toYfm[CutNode.Cut], - fromYfm: { - tokenSpec: fromYfm[CutNode.Cut], - }, + builder.use(YfmCutSpecs, { + ...opts, + // @ts-expect-error + cutView: // FIX: ignore mutation and don't rerender node when yfm.js open or close cut - // @ts-expect-error - view: () => () => ({ + () => () => ({ ignoreMutation(mutation) { return mutation instanceof MutationRecord && mutation.type === 'attributes'; }, }), - })) - .addNode(CutNode.CutTitle, () => ({ - spec: spec[CutNode.CutTitle], - toYfm: toYfm[CutNode.CutTitle], - fromYfm: { - tokenSpec: fromYfm[CutNode.CutTitle], - }, - view: () => (node) => new YfmCutTitleNodeView(node), - })) - .addNode(CutNode.CutContent, () => ({ - spec: spec[CutNode.CutContent], - toYfm: toYfm[CutNode.CutContent], - fromYfm: { - tokenSpec: fromYfm[CutNode.CutContent], - }, - })) + cutTitleView: () => (node) => new YfmCutTitleNodeView(node), + }); + + builder .addPlugin(cutAutoOpenPlugin) .addAction(cutAction, () => toYfmCut) .addKeymap(() => ({ diff --git a/src/extensions/yfm/YfmDist/YfmDistSpecs/index.ts b/src/extensions/yfm/YfmDist/YfmDistSpecs/index.ts new file mode 100644 index 00000000..c8431746 --- /dev/null +++ b/src/extensions/yfm/YfmDist/YfmDistSpecs/index.ts @@ -0,0 +1,11 @@ +import noop from 'lodash/noop'; +import type {ExtensionAuto} from '../../../../core'; + +export const YfmDistSpecs: ExtensionAuto = (builder) => { + // ignore yfm lint token + builder.addNode('__yfm_lint', () => ({ + spec: {}, + fromYfm: {tokenSpec: {name: '__yfm_lint', type: 'node', ignore: true}}, + toYfm: noop, + })); +}; diff --git a/src/extensions/yfm/YfmDist/index.ts b/src/extensions/yfm/YfmDist/index.ts index 4a69ef42..881b09a3 100644 --- a/src/extensions/yfm/YfmDist/index.ts +++ b/src/extensions/yfm/YfmDist/index.ts @@ -1,4 +1,3 @@ -import {noop} from 'lodash'; import {Plugin} from 'prosemirror-state'; import '@doc-tools/transform/dist/js/yfm'; @@ -6,8 +5,12 @@ import '@doc-tools/transform/dist/css/yfm.css'; import './yfm.scss'; import type {ExtensionAuto} from '../../../core'; +import {YfmDistSpecs} from './YfmDistSpecs'; export const YfmDist: ExtensionAuto = (builder) => { + // ignore yfm lint token + builder.use(YfmDistSpecs); + builder.addPlugin( () => new Plugin({ @@ -18,11 +21,4 @@ export const YfmDist: ExtensionAuto = (builder) => { }, }), ); - - // ignore yfm lint token - builder.addNode('__yfm_lint', () => ({ - spec: {}, - fromYfm: {tokenSpec: {name: '__yfm_lint', type: 'node', ignore: true}}, - toYfm: noop, - })); }; diff --git a/src/extensions/yfm/YfmFile/YfmFile.test.ts b/src/extensions/yfm/YfmFile/YfmFile.test.ts index 720b7661..7dc18fe7 100644 --- a/src/extensions/yfm/YfmFile/YfmFile.test.ts +++ b/src/extensions/yfm/YfmFile/YfmFile.test.ts @@ -1,14 +1,13 @@ -import {FILE_TOKEN} from '@doc-tools/transform/lib/plugins/file/const'; import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {YfmFile} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {yfmFileNodeName, YfmFileSpecs} from './YfmFileSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(YfmFile), + extensions: (builder) => builder.use(BaseSpecsPreset, {}).use(YfmFileSpecs), }).buildDeps(); const {same} = createMarkupChecker({parser, serializer}); @@ -16,7 +15,7 @@ const {same} = createMarkupChecker({parser, serializer}); const {doc, p, file} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - file: {nodeType: FILE_TOKEN}, + file: {nodeType: yfmFileNodeName}, }) as PMTestBuilderResult<'doc' | 'p' | 'file'>; const defaultAttrs = { diff --git a/src/extensions/yfm/YfmFile/const.ts b/src/extensions/yfm/YfmFile/YfmFileSpecs/const.ts similarity index 94% rename from src/extensions/yfm/YfmFile/const.ts rename to src/extensions/yfm/YfmFile/YfmFileSpecs/const.ts index 2e4d47f1..ef2fbc2d 100644 --- a/src/extensions/yfm/YfmFile/const.ts +++ b/src/extensions/yfm/YfmFile/YfmFileSpecs/const.ts @@ -2,11 +2,14 @@ import type {AttributeSpec} from 'prosemirror-model'; import { FileSpecialAttr, + FILE_TOKEN, FILE_TO_LINK_ATTRS_MAP, KNOWN_ATTRS as FILE_KNOWN_ATTRS, REQUIRED_ATTRS as FILE_REQUIRED_ATTRS, } from '@doc-tools/transform/lib/plugins/file/const'; +export const yfmFileNodeName = FILE_TOKEN; + export const KNOWN_ATTRS: readonly string[] = FILE_KNOWN_ATTRS.map((attrName) => { if (attrName in FILE_TO_LINK_ATTRS_MAP) return FILE_TO_LINK_ATTRS_MAP[attrName as FileSpecialAttr]; diff --git a/src/extensions/yfm/YfmFile/YfmFileSpecs/index.ts b/src/extensions/yfm/YfmFile/YfmFileSpecs/index.ts new file mode 100644 index 00000000..25602338 --- /dev/null +++ b/src/extensions/yfm/YfmFile/YfmFileSpecs/index.ts @@ -0,0 +1,75 @@ +import yfmPlugin from '@doc-tools/transform/lib/plugins/file'; +import {FileClassName, LinkHtmlAttr, PREFIX} from '@doc-tools/transform/lib/plugins/file/const'; + +import type {Extension} from '../../../../core'; +import {nodeTypeFactory} from '../../../../utils/schema'; +import {fileNodeAttrsSpec, KNOWN_ATTRS, LINK_TO_FILE_ATTRS_MAP, yfmFileNodeName} from './const'; + +export {yfmFileNodeName} from './const'; +export const fileType = nodeTypeFactory(yfmFileNodeName); + +export const YfmFileSpecs: Extension = (builder) => { + builder.configureMd((md) => md.use(yfmPlugin)); + builder.addNode(yfmFileNodeName, () => ({ + spec: { + group: 'inline', + inline: true, + attrs: fileNodeAttrsSpec, + parseDOM: [ + { + tag: `a[class="${FileClassName.Link}"]`, + getAttrs(p) { + const elem = p as Element; + const attrs: Record = {}; + for (const name of KNOWN_ATTRS) { + const value = elem.getAttribute(name); + if (value) { + attrs[name] = value; + } + } + return attrs; + }, + priority: 101, + }, + ], + toDOM(node) { + const a = document.createElement('a'); + a.contentEditable = 'false'; + a.classList.add(FileClassName.Link); + for (const [key, value] of Object.entries(node.attrs)) { + if (value) a.setAttribute(key, value); + } + const span = document.createElement('span'); + span.classList.add(FileClassName.Icon); + a.appendChild(span); + a.append(node.attrs[LinkHtmlAttr.Download]); + return a; + }, + }, + fromYfm: { + tokenName: yfmFileNodeName, + tokenSpec: { + name: yfmFileNodeName, + type: 'node', + getAttrs: (tok) => { + return Object.fromEntries(tok.attrs ?? []); + }, + }, + }, + toYfm: (state, node) => { + const attrsStr = Object.entries(node.attrs) + .reduce((arr, [key, value]) => { + if (value) { + if (key in LINK_TO_FILE_ATTRS_MAP) { + key = LINK_TO_FILE_ATTRS_MAP[key]; + } + arr.push(`${key}="${(value as string).replace(/"/g, '')}"`); + } + return arr; + }, []) + .join(' '); + + state.write(`${PREFIX}${attrsStr} %}`); + }, + })); +}; diff --git a/src/extensions/yfm/YfmFile/index.ts b/src/extensions/yfm/YfmFile/index.ts index 10a0fc54..17a917d6 100644 --- a/src/extensions/yfm/YfmFile/index.ts +++ b/src/extensions/yfm/YfmFile/index.ts @@ -1,81 +1,3 @@ -import yfmPlugin from '@doc-tools/transform/lib/plugins/file'; -import { - FileClassName, - FILE_TOKEN, - LinkHtmlAttr, - PREFIX, -} from '@doc-tools/transform/lib/plugins/file/const'; - -import type {Extension} from '../../../core'; -import {nodeTypeFactory} from '../../../utils/schema'; -import {fileNodeAttrsSpec, KNOWN_ATTRS, LINK_TO_FILE_ATTRS_MAP} from './const'; - import './index.scss'; -export const fileType = nodeTypeFactory(FILE_TOKEN); - -export const YfmFile: Extension = (builder) => { - builder.configureMd((md) => md.use(yfmPlugin)); - builder.addNode(FILE_TOKEN, () => ({ - spec: { - group: 'inline', - inline: true, - attrs: fileNodeAttrsSpec, - parseDOM: [ - { - tag: `a[class="${FileClassName.Link}"]`, - getAttrs(p) { - const elem = p as Element; - const attrs: Record = {}; - for (const name of KNOWN_ATTRS) { - const value = elem.getAttribute(name); - if (value) { - attrs[name] = value; - } - } - return attrs; - }, - priority: 101, - }, - ], - toDOM(node) { - const a = document.createElement('a'); - a.contentEditable = 'false'; - a.classList.add(FileClassName.Link); - for (const [key, value] of Object.entries(node.attrs)) { - if (value) a.setAttribute(key, value); - } - const span = document.createElement('span'); - span.classList.add(FileClassName.Icon); - a.appendChild(span); - a.append(node.attrs[LinkHtmlAttr.Download]); - return a; - }, - }, - fromYfm: { - tokenName: FILE_TOKEN, - tokenSpec: { - name: FILE_TOKEN, - type: 'node', - getAttrs: (tok) => { - return Object.fromEntries(tok.attrs ?? []); - }, - }, - }, - toYfm: (state, node) => { - const attrsStr = Object.entries(node.attrs) - .reduce((arr, [key, value]) => { - if (value) { - if (key in LINK_TO_FILE_ATTRS_MAP) { - key = LINK_TO_FILE_ATTRS_MAP[key]; - } - arr.push(`${key}="${(value as string).replace(/"/g, '')}"`); - } - return arr; - }, []) - .join(' '); - - state.write(`${PREFIX}${attrsStr} %}`); - }, - })); -}; +export {fileType, yfmFileNodeName, YfmFileSpecs as YfmFile} from './YfmFileSpecs'; diff --git a/src/extensions/yfm/YfmHeading/YfmHeading.test.ts b/src/extensions/yfm/YfmHeading/YfmHeading.test.ts index 8483fc36..3cbdec0c 100644 --- a/src/extensions/yfm/YfmHeading/YfmHeading.test.ts +++ b/src/extensions/yfm/YfmHeading/YfmHeading.test.ts @@ -2,27 +2,27 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {bold, Bold} from '../../markdown/Bold'; -import {YfmHeading} from './index'; -import {heading, YfmHeadingAttr} from './const'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {boldMarkName, BoldSpecs, headingNodeName} from '../../markdown/specs'; +import {YfmHeadingSpecs, YfmHeadingAttr} from './YfmHeadingSpecs'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(YfmHeading, {}).use(Bold, {}), + extensions: (builder) => + builder.use(BaseSpecsPreset, {}).use(YfmHeadingSpecs, {}).use(BoldSpecs), options: {attrsOpts: {allowedAttributes: ['id']}}, }).buildDeps(); const {doc, b, p, h, h1, h2, h3, h4, h5, h6} = builders(schema, { doc: {nodeType: BaseNode.Doc}, - b: {nodeType: bold}, + b: {nodeType: boldMarkName}, p: {nodeType: BaseNode.Paragraph}, - h: {nodeType: heading}, - h1: {nodeType: heading, [YfmHeadingAttr.Id]: '', [YfmHeadingAttr.Level]: 1}, - h2: {nodeType: heading, [YfmHeadingAttr.Id]: '', [YfmHeadingAttr.Level]: 2}, - h3: {nodeType: heading, [YfmHeadingAttr.Id]: '', [YfmHeadingAttr.Level]: 3}, - h4: {nodeType: heading, [YfmHeadingAttr.Id]: '', [YfmHeadingAttr.Level]: 4}, - h5: {nodeType: heading, [YfmHeadingAttr.Id]: '', [YfmHeadingAttr.Level]: 5}, - h6: {nodeType: heading, [YfmHeadingAttr.Id]: '', [YfmHeadingAttr.Level]: 6}, + h: {nodeType: headingNodeName}, + h1: {nodeType: headingNodeName, [YfmHeadingAttr.Id]: '', [YfmHeadingAttr.Level]: 1}, + h2: {nodeType: headingNodeName, [YfmHeadingAttr.Id]: '', [YfmHeadingAttr.Level]: 2}, + h3: {nodeType: headingNodeName, [YfmHeadingAttr.Id]: '', [YfmHeadingAttr.Level]: 3}, + h4: {nodeType: headingNodeName, [YfmHeadingAttr.Id]: '', [YfmHeadingAttr.Level]: 4}, + h5: {nodeType: headingNodeName, [YfmHeadingAttr.Id]: '', [YfmHeadingAttr.Level]: 5}, + h6: {nodeType: headingNodeName, [YfmHeadingAttr.Id]: '', [YfmHeadingAttr.Level]: 6}, }) as PMTestBuilderResult<'doc' | 'p' | 'h' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6', 'b'>; const {same} = createMarkupChecker({parser, serializer}); diff --git a/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/const.ts b/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/const.ts new file mode 100644 index 00000000..0cb48589 --- /dev/null +++ b/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/const.ts @@ -0,0 +1,9 @@ +import {headingLevelAttr} from '../../../markdown/Heading/HeadingSpecs'; +export type {HeadingLevel} from '../../../markdown/Heading/const'; + +export {headingLevelAttr, headingNodeName} from '../../../markdown/Heading/HeadingSpecs'; + +export const YfmHeadingAttr = { + Level: headingLevelAttr, + Id: 'id', +} as const; diff --git a/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/index.ts b/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/index.ts new file mode 100644 index 00000000..029e5bed --- /dev/null +++ b/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/index.ts @@ -0,0 +1,94 @@ +import type {Node, NodeSpec} from 'prosemirror-model'; +import type {ExtensionAuto} from '../../../../core'; +import {headingNodeName, YfmHeadingAttr} from './const'; +import {getNodeAttrs} from './utils'; + +const DEFAULT_PLACEHOLDER = (node: Node) => 'Heading ' + node.attrs[YfmHeadingAttr.Level]; + +export {YfmHeadingAttr} from './const'; + +export type YfmHeadingSpecsOptions = { + headingPlaceholder?: NonNullable['content']; +}; + +/** YfmHeading extension needs markdown-it-attrs plugin */ +export const YfmHeadingSpecs: ExtensionAuto = (builder, opts) => { + const {headingPlaceholder} = opts ?? {}; + + builder.addNode(headingNodeName, () => ({ + spec: { + attrs: { + [YfmHeadingAttr.Id]: {default: ''}, + [YfmHeadingAttr.Level]: {default: 1}, + }, + content: '(text | inline)*', + group: 'block', + defining: true, + selectable: false, + parseDOM: [ + {tag: 'h1', getAttrs: getNodeAttrs(1)}, + {tag: 'h2', getAttrs: getNodeAttrs(2)}, + {tag: 'h3', getAttrs: getNodeAttrs(3)}, + {tag: 'h4', getAttrs: getNodeAttrs(4)}, + {tag: 'h5', getAttrs: getNodeAttrs(5)}, + {tag: 'h6', getAttrs: getNodeAttrs(6)}, + ], + toDOM(node) { + const id = node.attrs[YfmHeadingAttr.Id]; + return [ + 'h' + node.attrs[YfmHeadingAttr.Level], + id ? {id} : {}, + 0, + // [ + // 'a', + // { + // href: `#${node.attrs[YfmHeadingAttr.Id]}`, + // class: 'yfm-anchor', + // 'aria-hidden': 'true', + // contenteditable: 'false', + // }, + // ], + // ['span', 0], + ]; + }, + placeholder: { + content: headingPlaceholder ?? DEFAULT_PLACEHOLDER, + alwaysVisible: true, + }, + }, + fromYfm: { + tokenSpec: { + name: headingNodeName, + type: 'block', + getAttrs: (token) => { + if (token.type.endsWith('_close')) return {}; + + const attrs = Object.fromEntries(token.attrs || []); + // if (!attrs[YfmHeadingAttr.Id]) { + // // calculate id if it was not specified + // // tokens[index + 1] is child inline token + // attrs[YfmHeadingAttr.Id] = slugify(tokens[index + 1].content); + // } + + // attrs have id only if it explicitly specified manually + return { + [YfmHeadingAttr.Level]: Number(token.tag.slice(1)), + ...attrs, + }; + }, + }, + }, + toYfm: (state, node) => { + state.write(state.repeat('#', node.attrs[YfmHeadingAttr.Level]) + ' '); + state.renderInline(node); + + const anchor = node.attrs[YfmHeadingAttr.Id]; + + if (anchor /*&& anchor !== node.firstChild?.textContent*/) { + state.write(` {#${anchor}}`); + } + + state.closeBlock(node); + }, + })); +}; diff --git a/src/extensions/yfm/YfmHeading/utils.ts b/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/utils.ts similarity index 80% rename from src/extensions/yfm/YfmHeading/utils.ts rename to src/extensions/yfm/YfmHeading/YfmHeadingSpecs/utils.ts index 2142d19a..22a0992d 100644 --- a/src/extensions/yfm/YfmHeading/utils.ts +++ b/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/utils.ts @@ -1,6 +1,6 @@ import type {ParseRule} from 'prosemirror-model'; -import {YfmHeadingAttr, HeadingLevel} from './const'; -export {hType, hasParentHeading, headingRule} from '../../markdown/Heading/utils'; +import {YfmHeadingAttr, HeadingLevel} from '../const'; +export {hType, hasParentHeading, headingRule} from '../../../markdown/Heading/utils'; // const slug = require('slugify'); export const getNodeAttrs = diff --git a/src/extensions/yfm/YfmHeading/actions.ts b/src/extensions/yfm/YfmHeading/actions.ts index d23f8c3d..d8ea35eb 100644 --- a/src/extensions/yfm/YfmHeading/actions.ts +++ b/src/extensions/yfm/YfmHeading/actions.ts @@ -1,7 +1,7 @@ import type {ActionSpec} from '../../../core'; import {toHeading} from './commands'; import {HeadingLevel} from './const'; -import {hasParentHeading} from './utils'; +import {hasParentHeading} from './YfmHeadingSpecs/utils'; export const headingAction = (level: HeadingLevel): ActionSpec => { const cmd = toHeading(level); diff --git a/src/extensions/yfm/YfmHeading/commands.ts b/src/extensions/yfm/YfmHeading/commands.ts index 864c00b6..cd0e55ff 100644 --- a/src/extensions/yfm/YfmHeading/commands.ts +++ b/src/extensions/yfm/YfmHeading/commands.ts @@ -2,7 +2,7 @@ import {setBlockType} from 'prosemirror-commands'; import type {Command} from 'prosemirror-state'; import {findParentNodeOfType} from 'prosemirror-utils'; import {YfmHeadingAttr, HeadingLevel} from './const'; -import {hType} from './utils'; +import {hType} from './YfmHeadingSpecs/utils'; export {resetHeading} from '../../markdown/Heading/commands'; diff --git a/src/extensions/yfm/YfmHeading/const.ts b/src/extensions/yfm/YfmHeading/const.ts index e8dc69c4..0b06bd1e 100644 --- a/src/extensions/yfm/YfmHeading/const.ts +++ b/src/extensions/yfm/YfmHeading/const.ts @@ -1,8 +1,2 @@ -import {lvlAttr} from '../../markdown/Heading/const'; -export {heading, HeadingAction} from '../../markdown/Heading/const'; -export type {HeadingLevel} from '../../markdown/Heading/const'; - -export const YfmHeadingAttr = { - Level: lvlAttr, - Id: 'id', -} as const; +export * from './YfmHeadingSpecs/const'; +export {HeadingAction} from '../../markdown/Heading/const'; diff --git a/src/extensions/yfm/YfmHeading/index.ts b/src/extensions/yfm/YfmHeading/index.ts index 1618f7e6..3c224fb4 100644 --- a/src/extensions/yfm/YfmHeading/index.ts +++ b/src/extensions/yfm/YfmHeading/index.ts @@ -1,104 +1,24 @@ -import type {Node, NodeSpec} from 'prosemirror-model'; import type {Action, ExtensionAuto, Keymap} from '../../../core'; -import {HeadingAction, YfmHeadingAttr, heading} from './const'; -import {getNodeAttrs, headingRule, hType} from './utils'; +import {HeadingAction} from './const'; +import {headingRule, hType} from './YfmHeadingSpecs/utils'; import {headingAction} from './actions'; import {resetHeading, toHeading} from './commands'; - -const DEFAULT_PLACEHOLDER = (node: Node) => 'Heading ' + node.attrs[YfmHeadingAttr.Level]; +import {YfmHeadingSpecs, YfmHeadingSpecsOptions} from './YfmHeadingSpecs'; export {YfmHeadingAttr} from './const'; -export type YfmHeadingOptions = { +export type YfmHeadingOptions = YfmHeadingSpecsOptions & { h1Key?: string | null; h2Key?: string | null; h3Key?: string | null; h4Key?: string | null; h5Key?: string | null; h6Key?: string | null; - headingPlaceholder?: NonNullable['content']; }; /** YfmHeading extension needs markdown-it-attrs plugin */ export const YfmHeading: ExtensionAuto = (builder, opts) => { - const {headingPlaceholder} = opts ?? {}; - - builder.addNode(heading, () => ({ - spec: { - attrs: { - [YfmHeadingAttr.Id]: {default: ''}, - [YfmHeadingAttr.Level]: {default: 1}, - }, - content: '(text | inline)*', - group: 'block', - defining: true, - selectable: false, - parseDOM: [ - {tag: 'h1', getAttrs: getNodeAttrs(1)}, - {tag: 'h2', getAttrs: getNodeAttrs(2)}, - {tag: 'h3', getAttrs: getNodeAttrs(3)}, - {tag: 'h4', getAttrs: getNodeAttrs(4)}, - {tag: 'h5', getAttrs: getNodeAttrs(5)}, - {tag: 'h6', getAttrs: getNodeAttrs(6)}, - ], - toDOM(node) { - const id = node.attrs[YfmHeadingAttr.Id]; - return [ - 'h' + node.attrs[YfmHeadingAttr.Level], - id ? {id} : {}, - 0, - // [ - // 'a', - // { - // href: `#${node.attrs[YfmHeadingAttr.Id]}`, - // class: 'yfm-anchor', - // 'aria-hidden': 'true', - // contenteditable: 'false', - // }, - // ], - // ['span', 0], - ]; - }, - placeholder: { - content: headingPlaceholder ?? DEFAULT_PLACEHOLDER, - alwaysVisible: true, - }, - }, - fromYfm: { - tokenSpec: { - name: heading, - type: 'block', - getAttrs: (token) => { - if (token.type.endsWith('_close')) return {}; - - const attrs = Object.fromEntries(token.attrs || []); - // if (!attrs[YfmHeadingAttr.Id]) { - // // calculate id if it was not specified - // // tokens[index + 1] is child inline token - // attrs[YfmHeadingAttr.Id] = slugify(tokens[index + 1].content); - // } - - // attrs have id only if it explicitly specified manually - return { - [YfmHeadingAttr.Level]: Number(token.tag.slice(1)), - ...attrs, - }; - }, - }, - }, - toYfm: (state, node) => { - state.write(state.repeat('#', node.attrs[YfmHeadingAttr.Level]) + ' '); - state.renderInline(node); - - const anchor = node.attrs[YfmHeadingAttr.Id]; - - if (anchor /*&& anchor !== node.firstChild?.textContent*/) { - state.write(` {#${anchor}}`); - } - - state.closeBlock(node); - }, - })); + builder.use(YfmHeadingSpecs, opts); builder .addKeymap(() => { diff --git a/src/extensions/yfm/YfmNote/YfmNote.test.ts b/src/extensions/yfm/YfmNote/YfmNote.test.ts index f5f2fff6..bc0af28c 100644 --- a/src/extensions/yfm/YfmNote/YfmNote.test.ts +++ b/src/extensions/yfm/YfmNote/YfmNote.test.ts @@ -2,21 +2,30 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {italic, Italic, blockquote, Blockquote} from '../../markdown'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import { + ItalicSpecs, + BlockquoteSpecs, + italicMarkName, + blockquoteNodeName, +} from '../../markdown/specs'; +import {YfmNoteSpecs} from './YfmNoteSpecs'; import {NoteAttrs, NoteNode} from './const'; -import {YfmNote} from './index'; const {schema, parser, serializer} = new ExtensionsManager({ extensions: (builder) => - builder.use(BaseSchema, {}).use(Italic, {}).use(Blockquote, {}).use(YfmNote, {}), + builder + .use(BaseSpecsPreset, {}) + .use(ItalicSpecs) + .use(BlockquoteSpecs) + .use(YfmNoteSpecs, {}), }).buildDeps(); const {doc, p, i, bq, note, noteTitle} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - i: {markType: italic}, - bq: {nodeType: blockquote}, + i: {markType: italicMarkName}, + bq: {nodeType: blockquoteNodeName}, note: { nodeType: NoteNode.Note, [NoteAttrs.Type]: 'info', diff --git a/src/extensions/yfm/YfmNote/YfmNoteSpecs/const.ts b/src/extensions/yfm/YfmNote/YfmNoteSpecs/const.ts new file mode 100644 index 00000000..5a6e5e6d --- /dev/null +++ b/src/extensions/yfm/YfmNote/YfmNoteSpecs/const.ts @@ -0,0 +1,9 @@ +export enum NoteNode { + Note = 'yfm_note', + NoteTitle = 'yfm_note_title', +} + +export enum NoteAttrs { + Class = 'class', + Type = 'note-type', +} diff --git a/src/extensions/yfm/YfmNote/fromYfm.ts b/src/extensions/yfm/YfmNote/YfmNoteSpecs/fromYfm.ts similarity index 86% rename from src/extensions/yfm/YfmNote/fromYfm.ts rename to src/extensions/yfm/YfmNote/YfmNoteSpecs/fromYfm.ts index bb0d3a3d..44fca4af 100644 --- a/src/extensions/yfm/YfmNote/fromYfm.ts +++ b/src/extensions/yfm/YfmNote/YfmNoteSpecs/fromYfm.ts @@ -1,4 +1,4 @@ -import type {ParserToken} from '../../../core'; +import type {ParserToken} from '../../../../core'; import {NoteNode} from './const'; export const fromYfm: Record = { diff --git a/src/extensions/yfm/YfmNote/YfmNoteSpecs/index.ts b/src/extensions/yfm/YfmNote/YfmNoteSpecs/index.ts new file mode 100644 index 00000000..08ddcc20 --- /dev/null +++ b/src/extensions/yfm/YfmNote/YfmNoteSpecs/index.ts @@ -0,0 +1,37 @@ +import log from '@doc-tools/transform/lib/log'; +import yfmPlugin from '@doc-tools/transform/lib/plugins/notes'; +import type {NodeSpec} from 'prosemirror-model'; + +import type {ExtensionAuto} from '../../../../core'; +import {toYfm} from './toYfm'; +import {NoteNode} from './const'; +import {fromYfm} from './fromYfm'; +import {getSpec} from './spec'; + +export {NoteNode as YfmNoteNode} from './const'; +export {noteType, noteTitleType} from './utils'; + +export type YfmNoteSpecsOptions = { + yfmNoteTitlePlaceholder?: NonNullable['content']; +}; + +export const YfmNoteSpecs: ExtensionAuto = (builder, opts) => { + const spec = getSpec(opts); + + builder + .configureMd((md) => md.use(yfmPlugin, {log})) + .addNode(NoteNode.Note, () => ({ + spec: spec[NoteNode.Note], + toYfm: toYfm[NoteNode.Note], + fromYfm: { + tokenSpec: fromYfm[NoteNode.Note], + }, + })) + .addNode(NoteNode.NoteTitle, () => ({ + spec: spec[NoteNode.NoteTitle], + toYfm: toYfm[NoteNode.NoteTitle], + fromYfm: { + tokenSpec: fromYfm[NoteNode.NoteTitle], + }, + })); +}; diff --git a/src/extensions/yfm/YfmNote/spec.ts b/src/extensions/yfm/YfmNote/YfmNoteSpecs/spec.ts similarity index 87% rename from src/extensions/yfm/YfmNote/spec.ts rename to src/extensions/yfm/YfmNote/YfmNoteSpecs/spec.ts index 1e042fad..4b093e2c 100644 --- a/src/extensions/yfm/YfmNote/spec.ts +++ b/src/extensions/yfm/YfmNote/YfmNoteSpecs/spec.ts @@ -1,13 +1,10 @@ import type {NodeSpec} from 'prosemirror-model'; +import {YfmNoteSpecsOptions} from './index'; import {NoteAttrs, NoteNode} from './const'; const DEFAULT_TITLE_PLACEHOLDER = 'Note'; -export type YfmNoteSpecOptions = { - yfmNoteTitlePlaceholder?: NonNullable['content']; -}; - -export const getSpec = (opts?: YfmNoteSpecOptions): Record => ({ +export const getSpec = (opts?: YfmNoteSpecsOptions): Record => ({ [NoteNode.Note]: { attrs: { [NoteAttrs.Class]: {default: 'yfm-note yfm-accent-info'}, diff --git a/src/extensions/yfm/YfmNote/toYfm.ts b/src/extensions/yfm/YfmNote/YfmNoteSpecs/toYfm.ts similarity index 85% rename from src/extensions/yfm/YfmNote/toYfm.ts rename to src/extensions/yfm/YfmNote/YfmNoteSpecs/toYfm.ts index e4cbb2fe..9f71a26d 100644 --- a/src/extensions/yfm/YfmNote/toYfm.ts +++ b/src/extensions/yfm/YfmNote/YfmNoteSpecs/toYfm.ts @@ -1,5 +1,5 @@ -import type {SerializerNodeToken} from '../../../core'; -import {getPlaceholderContent} from '../../behavior/Placeholder'; +import type {SerializerNodeToken} from '../../../../core'; +import {getPlaceholderContent} from '../../../../utils/placeholder'; import {NoteAttrs, NoteNode} from './const'; export const toYfm: Record = { diff --git a/src/extensions/yfm/YfmNote/YfmNoteSpecs/utils.ts b/src/extensions/yfm/YfmNote/YfmNoteSpecs/utils.ts new file mode 100644 index 00000000..29dce22c --- /dev/null +++ b/src/extensions/yfm/YfmNote/YfmNoteSpecs/utils.ts @@ -0,0 +1,5 @@ +import {nodeTypeFactory} from '../../../../utils/schema'; +import {NoteNode} from './const'; + +export const noteType = nodeTypeFactory(NoteNode.Note); +export const noteTitleType = nodeTypeFactory(NoteNode.NoteTitle); diff --git a/src/extensions/yfm/YfmNote/commands.test.ts b/src/extensions/yfm/YfmNote/commands.test.ts index 5ac98a77..bae2bea5 100644 --- a/src/extensions/yfm/YfmNote/commands.test.ts +++ b/src/extensions/yfm/YfmNote/commands.test.ts @@ -4,7 +4,7 @@ import {EditorState, TextSelection} from 'prosemirror-state'; import {builders} from 'prosemirror-test-builder'; import {NoteNode} from './const'; -import {getSpec} from './spec'; +import {getSpec} from './YfmNoteSpecs/spec'; import {backToNoteTitle, removeNote} from './commands'; const schema = new Schema({ diff --git a/src/extensions/yfm/YfmNote/const.ts b/src/extensions/yfm/YfmNote/const.ts index 5a6e5e6d..c8b01f4a 100644 --- a/src/extensions/yfm/YfmNote/const.ts +++ b/src/extensions/yfm/YfmNote/const.ts @@ -1,9 +1 @@ -export enum NoteNode { - Note = 'yfm_note', - NoteTitle = 'yfm_note_title', -} - -export enum NoteAttrs { - Class = 'class', - Type = 'note-type', -} +export * from './YfmNoteSpecs/const'; diff --git a/src/extensions/yfm/YfmNote/index.ts b/src/extensions/yfm/YfmNote/index.ts index 0c245aef..993046f3 100644 --- a/src/extensions/yfm/YfmNote/index.ts +++ b/src/extensions/yfm/YfmNote/index.ts @@ -1,45 +1,25 @@ -import log from '@doc-tools/transform/lib/log'; -import yfmPlugin from '@doc-tools/transform/lib/plugins/notes'; import {chainCommands} from 'prosemirror-commands'; import type {Action, ExtensionAuto} from '../../../core'; -import {toYfm} from './toYfm'; -import {NoteNode} from './const'; -import {fromYfm} from './fromYfm'; -import {getSpec, YfmNoteSpecOptions} from './spec'; import {createYfmNote, toYfmNote} from './actions/toYfmNote'; import {nodeInputRule} from '../../../utils/inputrules'; import {backToNoteTitle, exitFromNoteTitle, removeNote} from './commands'; import {noteType} from './utils'; +import {YfmNoteSpecs, YfmNoteSpecsOptions} from './YfmNoteSpecs'; import './index.scss'; const noteAction = 'toYfmNote'; -export {noteType, noteTitleType} from './utils'; +export {YfmNoteNode, noteType, noteTitleType} from './YfmNoteSpecs'; -export type YfmNoteOptions = YfmNoteSpecOptions & { +export type YfmNoteOptions = YfmNoteSpecsOptions & { yfmNoteKey?: string | null; }; export const YfmNote: ExtensionAuto = (builder, opts) => { - const spec = getSpec(opts); + builder.use(YfmNoteSpecs, opts); builder - .configureMd((md) => md.use(yfmPlugin, {log})) - .addNode(NoteNode.Note, () => ({ - spec: spec[NoteNode.Note], - toYfm: toYfm[NoteNode.Note], - fromYfm: { - tokenSpec: fromYfm[NoteNode.Note], - }, - })) - .addNode(NoteNode.NoteTitle, () => ({ - spec: spec[NoteNode.NoteTitle], - toYfm: toYfm[NoteNode.NoteTitle], - fromYfm: { - tokenSpec: fromYfm[NoteNode.NoteTitle], - }, - })) .addKeymap(() => ({ Enter: exitFromNoteTitle, Backspace: chainCommands(backToNoteTitle, removeNote), diff --git a/src/extensions/yfm/YfmNote/utils.ts b/src/extensions/yfm/YfmNote/utils.ts index 8eea72c9..4c7c829c 100644 --- a/src/extensions/yfm/YfmNote/utils.ts +++ b/src/extensions/yfm/YfmNote/utils.ts @@ -1,5 +1 @@ -import {nodeTypeFactory} from '../../../utils/schema'; -import {NoteNode} from './const'; - -export const noteTitleType = nodeTypeFactory(NoteNode.NoteTitle); -export const noteType = nodeTypeFactory(NoteNode.Note); +export * from './YfmNoteSpecs/utils'; diff --git a/src/extensions/yfm/YfmTable/YfmTable.test.ts b/src/extensions/yfm/YfmTable/YfmTable.test.ts index baff0bc4..bfaf00b7 100644 --- a/src/extensions/yfm/YfmTable/YfmTable.test.ts +++ b/src/extensions/yfm/YfmTable/YfmTable.test.ts @@ -5,20 +5,20 @@ import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {dispatchPasteEvent} from '../../../../tests/dispatch-event'; import {parseDOM} from '../../../../tests/parse-dom'; import {ExtensionsManager} from '../../../core'; -import {BaseNode, BaseSchema} from '../../base/BaseSchema'; -import {blockquote, Blockquote} from '../../markdown/Blockquote'; -import {YfmTableNode} from './const'; -import {YfmTable} from './index'; +import {BaseNode, BaseSpecsPreset} from '../../base/specs'; +import {blockquoteNodeName, BlockquoteSpecs} from '../../markdown/Blockquote/BlockquoteSpecs'; +import {YfmTableSpecs, YfmTableNode} from './YfmTableSpecs'; import {fixPastedTableBodies} from './paste'; const {schema, parser, serializer} = new ExtensionsManager({ - extensions: (builder) => builder.use(BaseSchema, {}).use(Blockquote, {}).use(YfmTable, {}), + extensions: (builder) => + builder.use(BaseSpecsPreset, {}).use(BlockquoteSpecs).use(YfmTableSpecs, {}), }).build(); const {doc, p, bq, table, tbody, tr, td} = builders(schema, { doc: {nodeType: BaseNode.Doc}, p: {nodeType: BaseNode.Paragraph}, - bq: {nodeType: blockquote}, + bq: {nodeType: blockquoteNodeName}, table: {nodeType: YfmTableNode.Table}, tbody: {nodeType: YfmTableNode.Body}, tr: {nodeType: YfmTableNode.Row}, diff --git a/src/extensions/yfm/YfmTable/YfmTableSpecs/const.ts b/src/extensions/yfm/YfmTable/YfmTableSpecs/const.ts new file mode 100644 index 00000000..059787ae --- /dev/null +++ b/src/extensions/yfm/YfmTable/YfmTableSpecs/const.ts @@ -0,0 +1,6 @@ +export enum YfmTableNode { + Table = 'yfm_table', + Body = 'yfm_tbody', + Row = 'yfm_tr', + Cell = 'yfm_td', +} diff --git a/src/extensions/yfm/YfmTable/fromYfm.ts b/src/extensions/yfm/YfmTable/YfmTableSpecs/fromYfm.ts similarity index 87% rename from src/extensions/yfm/YfmTable/fromYfm.ts rename to src/extensions/yfm/YfmTable/YfmTableSpecs/fromYfm.ts index b9a69d3b..af31bcf3 100644 --- a/src/extensions/yfm/YfmTable/fromYfm.ts +++ b/src/extensions/yfm/YfmTable/YfmTableSpecs/fromYfm.ts @@ -1,4 +1,4 @@ -import type {ParserToken} from '../../../core'; +import type {ParserToken} from '../../../../core'; import {YfmTableNode} from './const'; export const fromYfm: Record = { diff --git a/src/extensions/yfm/YfmTable/YfmTableSpecs/index.ts b/src/extensions/yfm/YfmTable/YfmTableSpecs/index.ts new file mode 100644 index 00000000..6aea3626 --- /dev/null +++ b/src/extensions/yfm/YfmTable/YfmTableSpecs/index.ts @@ -0,0 +1,43 @@ +import log from '@doc-tools/transform/lib/log'; +import yfmTable from '@doc-tools/transform/lib/plugins/table'; +import type {NodeSpec} from 'prosemirror-model'; + +import type {ExtensionWithOptions} from '../../../../core'; +import {YfmTableNode} from './const'; +import {fromYfm} from './fromYfm'; +import {getSpec} from './spec'; +import {toYfm} from './toYfm'; + +export {YfmTableNode} from './const'; +export {yfmTableType, yfmTableBodyType, yfmTableRowType, yfmTableCellType} from './utils'; + +export type YfmTableSpecsOptions = { + yfmTableCellPlaceholder?: NonNullable['content']; +}; + +export const YfmTableSpecs: ExtensionWithOptions = (builder, options) => { + const spec = getSpec(options); + + builder + .configureMd((md) => md.use(yfmTable, {log})) + .addNode(YfmTableNode.Table, () => ({ + spec: spec[YfmTableNode.Table], + toYfm: toYfm[YfmTableNode.Table], + fromYfm: {tokenSpec: fromYfm[YfmTableNode.Table]}, + })) + .addNode(YfmTableNode.Body, () => ({ + spec: spec[YfmTableNode.Body], + toYfm: toYfm[YfmTableNode.Body], + fromYfm: {tokenSpec: fromYfm[YfmTableNode.Body]}, + })) + .addNode(YfmTableNode.Row, () => ({ + spec: spec[YfmTableNode.Row], + toYfm: toYfm[YfmTableNode.Row], + fromYfm: {tokenSpec: fromYfm[YfmTableNode.Row]}, + })) + .addNode(YfmTableNode.Cell, () => ({ + spec: spec[YfmTableNode.Cell], + toYfm: toYfm[YfmTableNode.Cell], + fromYfm: {tokenSpec: fromYfm[YfmTableNode.Cell]}, + })); +}; diff --git a/src/extensions/yfm/YfmTable/spec.ts b/src/extensions/yfm/YfmTable/YfmTableSpecs/spec.ts similarity index 88% rename from src/extensions/yfm/YfmTable/spec.ts rename to src/extensions/yfm/YfmTable/YfmTableSpecs/spec.ts index 00e06f53..1cae6846 100644 --- a/src/extensions/yfm/YfmTable/spec.ts +++ b/src/extensions/yfm/YfmTable/YfmTableSpecs/spec.ts @@ -1,14 +1,11 @@ import type {NodeSpec} from 'prosemirror-model'; -import {TableRole} from '../../../table-utils'; +import {TableRole} from '../../../../table-utils'; +import type {YfmTableSpecsOptions} from './index'; import {YfmTableNode} from './const'; const DEFAULT_CELL_PLACEHOLDER = 'Table cell'; -export type YfmTableSpecOptions = { - yfmTableCellPlaceholder?: NonNullable['content']; -}; - -export const getSpec = (opts?: YfmTableSpecOptions): Record => ({ +export const getSpec = (opts?: YfmTableSpecsOptions): Record => ({ [YfmTableNode.Table]: { group: 'block yfm-table', content: `${YfmTableNode.Body}`, diff --git a/src/extensions/yfm/YfmTable/toYfm.ts b/src/extensions/yfm/YfmTable/YfmTableSpecs/toYfm.ts similarity index 94% rename from src/extensions/yfm/YfmTable/toYfm.ts rename to src/extensions/yfm/YfmTable/YfmTableSpecs/toYfm.ts index 89e70ce6..66a18aed 100644 --- a/src/extensions/yfm/YfmTable/toYfm.ts +++ b/src/extensions/yfm/YfmTable/YfmTableSpecs/toYfm.ts @@ -1,4 +1,4 @@ -import type {SerializerNodeToken} from '../../../core'; +import type {SerializerNodeToken} from '../../../../core'; import {YfmTableNode} from './const'; export const toYfm: Record = { diff --git a/src/extensions/yfm/YfmTable/YfmTableSpecs/utils.ts b/src/extensions/yfm/YfmTable/YfmTableSpecs/utils.ts new file mode 100644 index 00000000..5e710e05 --- /dev/null +++ b/src/extensions/yfm/YfmTable/YfmTableSpecs/utils.ts @@ -0,0 +1,7 @@ +import {nodeTypeFactory} from '../../../../utils/schema'; +import {YfmTableNode} from './const'; + +export const yfmTableType = nodeTypeFactory(YfmTableNode.Table); +export const yfmTableBodyType = nodeTypeFactory(YfmTableNode.Body); +export const yfmTableRowType = nodeTypeFactory(YfmTableNode.Row); +export const yfmTableCellType = nodeTypeFactory(YfmTableNode.Cell); diff --git a/src/extensions/yfm/YfmTable/actions.test.ts b/src/extensions/yfm/YfmTable/actions.test.ts index 290eff2a..41163211 100644 --- a/src/extensions/yfm/YfmTable/actions.test.ts +++ b/src/extensions/yfm/YfmTable/actions.test.ts @@ -2,7 +2,7 @@ import {Schema} from 'prosemirror-model'; import {EditorState} from 'prosemirror-state'; import {builders} from 'prosemirror-test-builder'; -import {getSpec} from './spec'; +import {getSpec} from './YfmTableSpecs/spec'; import {createYfmTableCommand} from './actions'; import {applyCommand} from '../../../../tests/utils'; diff --git a/src/extensions/yfm/YfmTable/commands/backspace.test.ts b/src/extensions/yfm/YfmTable/commands/backspace.test.ts index 36bbe003..229c993b 100644 --- a/src/extensions/yfm/YfmTable/commands/backspace.test.ts +++ b/src/extensions/yfm/YfmTable/commands/backspace.test.ts @@ -4,7 +4,7 @@ import {builders} from 'prosemirror-test-builder'; import {applyCommand} from '../../../../../tests/utils'; -import {getSpec} from '../spec'; +import {getSpec} from '../YfmTableSpecs/spec'; import {clearSelectedCells} from './backspace'; const schema = new Schema({ diff --git a/src/extensions/yfm/YfmTable/const.ts b/src/extensions/yfm/YfmTable/const.ts index 059787ae..776fa50e 100644 --- a/src/extensions/yfm/YfmTable/const.ts +++ b/src/extensions/yfm/YfmTable/const.ts @@ -1,6 +1 @@ -export enum YfmTableNode { - Table = 'yfm_table', - Body = 'yfm_tbody', - Row = 'yfm_tr', - Cell = 'yfm_td', -} +export * from './YfmTableSpecs/const'; diff --git a/src/extensions/yfm/YfmTable/index.ts b/src/extensions/yfm/YfmTable/index.ts index 85fd42e1..450261a9 100644 --- a/src/extensions/yfm/YfmTable/index.ts +++ b/src/extensions/yfm/YfmTable/index.ts @@ -1,48 +1,27 @@ import {Plugin} from 'prosemirror-state'; -import log from '@doc-tools/transform/lib/log'; -import yfmTable from '@doc-tools/transform/lib/plugins/table'; import {goToNextCell} from '../../../table-utils'; import type {Action, ExtensionWithOptions} from '../../../core'; -import {YfmTableNode} from './const'; -import {getSpec, YfmTableSpecOptions} from './spec'; import {createYfmTable} from './actions'; -import {fromYfm} from './fromYfm'; -import {toYfm} from './toYfm'; import {goToNextRow} from './commands/goToNextRow'; import {backspaceCommand} from './commands/backspace'; import {fixPastedTableBodies} from './paste'; +import {YfmTableSpecs, YfmTableSpecsOptions} from './YfmTableSpecs'; const action = 'createYfmTable'; -export {YfmTableNode} from './const'; export {convertToYfmTable} from './commands/convert-table'; -export {yfmTableType, yfmTableBodyType, yfmTableRowType, yfmTableCellType} from './utils'; -export type YfmTableOptions = YfmTableSpecOptions; -export const YfmTable: ExtensionWithOptions = (builder, options) => { - const spec = getSpec(options); +export { + YfmTableNode, + yfmTableType, + yfmTableBodyType, + yfmTableRowType, + yfmTableCellType, +} from './YfmTableSpecs'; - builder - .configureMd((md) => md.use(yfmTable, {log})) - .addNode(YfmTableNode.Table, () => ({ - spec: spec[YfmTableNode.Table], - toYfm: toYfm[YfmTableNode.Table], - fromYfm: {tokenSpec: fromYfm[YfmTableNode.Table]}, - })) - .addNode(YfmTableNode.Body, () => ({ - spec: spec[YfmTableNode.Body], - toYfm: toYfm[YfmTableNode.Body], - fromYfm: {tokenSpec: fromYfm[YfmTableNode.Body]}, - })) - .addNode(YfmTableNode.Row, () => ({ - spec: spec[YfmTableNode.Row], - toYfm: toYfm[YfmTableNode.Row], - fromYfm: {tokenSpec: fromYfm[YfmTableNode.Row]}, - })) - .addNode(YfmTableNode.Cell, () => ({ - spec: spec[YfmTableNode.Cell], - toYfm: toYfm[YfmTableNode.Cell], - fromYfm: {tokenSpec: fromYfm[YfmTableNode.Cell]}, - })); +export type YfmTableOptions = YfmTableSpecsOptions & {}; + +export const YfmTable: ExtensionWithOptions = (builder, options) => { + builder.use(YfmTableSpecs, options); builder .addKeymap(() => ({ diff --git a/src/extensions/yfm/YfmTable/utils.ts b/src/extensions/yfm/YfmTable/utils.ts index 7c1d025a..83e2c020 100644 --- a/src/extensions/yfm/YfmTable/utils.ts +++ b/src/extensions/yfm/YfmTable/utils.ts @@ -1,11 +1,6 @@ import type {Fragment, Node} from 'prosemirror-model'; -import {nodeTypeFactory} from '../../../utils/schema'; -import {YfmTableNode} from './const'; -export const yfmTableType = nodeTypeFactory(YfmTableNode.Table); -export const yfmTableBodyType = nodeTypeFactory(YfmTableNode.Body); -export const yfmTableRowType = nodeTypeFactory(YfmTableNode.Row); -export const yfmTableCellType = nodeTypeFactory(YfmTableNode.Cell); +export * from './YfmTableSpecs/utils'; export function getContentAsArray(node: Node | Fragment) { const content: {node: Node; offset: number}[] = []; diff --git a/src/extensions/yfm/YfmTabs/YfmTabsSpecs/const.ts b/src/extensions/yfm/YfmTabs/YfmTabsSpecs/const.ts new file mode 100644 index 00000000..8dba1dd5 --- /dev/null +++ b/src/extensions/yfm/YfmTabs/YfmTabsSpecs/const.ts @@ -0,0 +1,6 @@ +export enum TabsNode { + Tab = 'yfm_tab', + TabsList = 'yfm_tabs_list', + TabPanel = 'yfm_tab_panel', + Tabs = 'yfm_tabs', +} diff --git a/src/extensions/yfm/YfmTabs/fromYfm.ts b/src/extensions/yfm/YfmTabs/YfmTabsSpecs/fromYfm.ts similarity index 93% rename from src/extensions/yfm/YfmTabs/fromYfm.ts rename to src/extensions/yfm/YfmTabs/YfmTabsSpecs/fromYfm.ts index ff3d3582..a53d25f8 100644 --- a/src/extensions/yfm/YfmTabs/fromYfm.ts +++ b/src/extensions/yfm/YfmTabs/YfmTabsSpecs/fromYfm.ts @@ -1,5 +1,5 @@ import type Token from 'markdown-it/lib/token'; -import type {ParserToken} from '../../../core'; +import type {ParserToken} from '../../../../core'; import {TabsNode} from './const'; const attrsFromEntries = (token: Token) => (token.attrs ? Object.fromEntries(token.attrs) : {}); diff --git a/src/extensions/yfm/YfmTabs/YfmTabsSpecs/index.ts b/src/extensions/yfm/YfmTabs/YfmTabsSpecs/index.ts new file mode 100644 index 00000000..1fc5535a --- /dev/null +++ b/src/extensions/yfm/YfmTabs/YfmTabsSpecs/index.ts @@ -0,0 +1,63 @@ +import log from '@doc-tools/transform/lib/log'; +import yfmPlugin from '@doc-tools/transform/lib/plugins/tabs'; + +import type {ExtensionAuto, YENodeSpec} from '../../../../core'; +import {nodeTypeFactory} from '../../../../utils/schema'; +import {TabsNode} from './const'; +import {fromYfm} from './fromYfm'; +import {spec} from './spec'; +import {toYfm} from './toYfm'; + +export {TabsNode} from './const'; +export const tabPanelType = nodeTypeFactory(TabsNode.TabPanel); +export const tabType = nodeTypeFactory(TabsNode.Tab); +export const tabsType = nodeTypeFactory(TabsNode.Tabs); +export const tabsListType = nodeTypeFactory(TabsNode.TabsList); + +export type YfmTabsSpecsOptions = { + tabView?: YENodeSpec['view']; + tabsListView?: YENodeSpec['view']; + tabPanelView?: YENodeSpec['view']; + tabsView?: YENodeSpec['view']; +}; + +export const YfmTabsSpecs: ExtensionAuto = (builder, opts) => { + builder + .configureMd((md) => md.use(yfmPlugin, {log})) + .addNode(TabsNode.Tab, () => ({ + spec: spec[TabsNode.Tab], + toYfm: toYfm[TabsNode.Tab], + fromYfm: { + tokenSpec: fromYfm[TabsNode.Tab], + tokenName: 'tab', + }, + view: opts.tabView, + })) + .addNode(TabsNode.TabsList, () => ({ + spec: spec[TabsNode.TabsList], + toYfm: toYfm[TabsNode.TabsList], + fromYfm: { + tokenSpec: fromYfm[TabsNode.TabsList], + tokenName: 'tab-list', + }, + view: opts.tabsListView, + })) + .addNode(TabsNode.TabPanel, () => ({ + spec: spec[TabsNode.TabPanel], + toYfm: toYfm[TabsNode.TabPanel], + fromYfm: { + tokenSpec: fromYfm[TabsNode.TabPanel], + tokenName: 'tab-panel', + }, + view: opts.tabPanelView, + })) + .addNode(TabsNode.Tabs, () => ({ + spec: spec[TabsNode.Tabs], + toYfm: toYfm[TabsNode.Tabs], + fromYfm: { + tokenSpec: fromYfm[TabsNode.Tabs], + tokenName: 'tabs', + }, + view: opts.tabsView, + })); +}; diff --git a/src/extensions/yfm/YfmTabs/spec.ts b/src/extensions/yfm/YfmTabs/YfmTabsSpecs/spec.ts similarity index 97% rename from src/extensions/yfm/YfmTabs/spec.ts rename to src/extensions/yfm/YfmTabs/YfmTabsSpecs/spec.ts index f64b7f33..cac90e05 100644 --- a/src/extensions/yfm/YfmTabs/spec.ts +++ b/src/extensions/yfm/YfmTabs/YfmTabsSpecs/spec.ts @@ -1,4 +1,4 @@ -import {NodeSpec} from 'prosemirror-model'; +import type {NodeSpec} from 'prosemirror-model'; import {TabsNode} from './const'; export const spec: Record = { diff --git a/src/extensions/yfm/YfmTabs/toYfm.ts b/src/extensions/yfm/YfmTabs/YfmTabsSpecs/toYfm.ts similarity index 94% rename from src/extensions/yfm/YfmTabs/toYfm.ts rename to src/extensions/yfm/YfmTabs/YfmTabsSpecs/toYfm.ts index 7ff04965..6e51a5fd 100644 --- a/src/extensions/yfm/YfmTabs/toYfm.ts +++ b/src/extensions/yfm/YfmTabs/YfmTabsSpecs/toYfm.ts @@ -1,5 +1,5 @@ import type {Node} from 'prosemirror-model'; -import type {SerializerNodeToken} from '../../../core'; +import type {SerializerNodeToken} from '../../../../core'; import {TabsNode} from './const'; export const toYfm: Record = { diff --git a/src/extensions/yfm/YfmTabs/const.ts b/src/extensions/yfm/YfmTabs/const.ts index 0a52b292..31c2ec54 100644 --- a/src/extensions/yfm/YfmTabs/const.ts +++ b/src/extensions/yfm/YfmTabs/const.ts @@ -1,18 +1,6 @@ -import {nodeTypeFactory} from '../../../utils/schema'; - -export enum TabsNode { - Tab = 'yfm_tab', - TabsList = 'yfm_tabs_list', - TabPanel = 'yfm_tab_panel', - Tabs = 'yfm_tabs', -} +export {TabsNode, tabType, tabPanelType, tabsListType, tabsType} from './YfmTabsSpecs'; export const tabActiveClassname = 'yfm-tab active'; export const tabInactiveClassname = 'yfm-tab'; export const tabPanelActiveClassname = 'yfm-tab-panel active'; export const tabPanelInactiveClassname = 'yfm-tab-panel'; - -export const tabPanelType = nodeTypeFactory(TabsNode.TabPanel); -export const tabType = nodeTypeFactory(TabsNode.Tab); -export const tabsType = nodeTypeFactory(TabsNode.Tabs); -export const tabListType = nodeTypeFactory(TabsNode.TabsList); diff --git a/src/extensions/yfm/YfmTabs/index.ts b/src/extensions/yfm/YfmTabs/index.ts index 5b735c0d..b20d8ab4 100644 --- a/src/extensions/yfm/YfmTabs/index.ts +++ b/src/extensions/yfm/YfmTabs/index.ts @@ -1,15 +1,9 @@ -import log from '@doc-tools/transform/lib/log'; -import yfmPlugin from '@doc-tools/transform/lib/plugins/tabs'; import type {ExtensionAuto} from '../../../core'; -import {toYfm} from './toYfm'; -import {TabsNode} from './const'; - -import {fromYfm} from './fromYfm'; -import {spec} from './spec'; import {Node} from 'prosemirror-model'; import {EditorView} from 'prosemirror-view'; import {tabBackspace, tabPanelBackspace} from './plugins'; import {chainCommands} from 'prosemirror-commands'; +import {YfmTabsSpecs} from './YfmTabsSpecs'; const ignoreMutation = (node: Node, view: EditorView, getPos: () => number) => (mutation: MutationRecord) => { @@ -32,52 +26,25 @@ const ignoreMutation = return false; }; +export {TabsNode, tabType, tabsType, tabsListType, tabPanelType} from './YfmTabsSpecs'; + export const YfmTabs: ExtensionAuto = (builder) => { - builder - .configureMd((md) => md.use(yfmPlugin, {log})) - .addNode(TabsNode.Tab, () => ({ - spec: spec[TabsNode.Tab], - toYfm: toYfm[TabsNode.Tab], - fromYfm: { - tokenSpec: fromYfm[TabsNode.Tab], - tokenName: 'tab', - }, + builder.use(YfmTabsSpecs, { + // @ts-expect-error + tabView: // FIX: ignore mutation and don't rerender node when yfm.js switch tab - // @ts-expect-error - view: () => (node, view, getPos) => ({ + () => (node, view, getPos) => ({ ignoreMutation: ignoreMutation(node, view, getPos), }), - })) - .addNode(TabsNode.TabsList, () => ({ - spec: spec[TabsNode.TabsList], - toYfm: toYfm[TabsNode.TabsList], - fromYfm: { - tokenSpec: fromYfm[TabsNode.TabsList], - tokenName: 'tab-list', - }, - })) - .addNode(TabsNode.TabPanel, () => ({ - spec: spec[TabsNode.TabPanel], - toYfm: toYfm[TabsNode.TabPanel], - fromYfm: { - tokenSpec: fromYfm[TabsNode.TabPanel], - tokenName: 'tab-panel', - }, + // @ts-expect-error + tabPanelView: // FIX: ignore mutation and don't rerender node when yfm.js switch tab - // @ts-expect-error - view: () => (node, view, getPos) => ({ + () => (node, view, getPos) => ({ ignoreMutation: ignoreMutation(node, view, getPos), }), - })) - .addNode(TabsNode.Tabs, () => ({ - spec: spec[TabsNode.Tabs], - toYfm: toYfm[TabsNode.Tabs], - fromYfm: { - tokenSpec: fromYfm[TabsNode.Tabs], - tokenName: 'tabs', - }, - })) - .addKeymap(() => ({ - Backspace: chainCommands(tabPanelBackspace, tabBackspace), - })); + }); + + builder.addKeymap(() => ({ + Backspace: chainCommands(tabPanelBackspace, tabBackspace), + })); }; diff --git a/src/extensions/yfm/YfmTabs/plugins.ts b/src/extensions/yfm/YfmTabs/plugins.ts index fa3cb10c..52975869 100644 --- a/src/extensions/yfm/YfmTabs/plugins.ts +++ b/src/extensions/yfm/YfmTabs/plugins.ts @@ -3,7 +3,7 @@ import {findChildren, findParentNodeOfType} from 'prosemirror-utils'; import { tabActiveClassname, tabInactiveClassname, - tabListType, + tabsListType, tabPanelActiveClassname, tabPanelInactiveClassname, tabPanelType, @@ -35,7 +35,7 @@ export const tabBackspace: Command = (state, dispatch) => { state.selection.from === state.selection.to ) { const tabList = findChildren(tabsParentNode.node, (tabNode) => { - return tabNode.type.name === tabListType(state.schema).name; + return tabNode.type.name === tabsListType(state.schema).name; })[0]; const tabToRemoveIdx = findChildIndex(tabList.node, tabToRemove.node); diff --git a/src/extensions/yfm/specs.ts b/src/extensions/yfm/specs.ts new file mode 100644 index 00000000..dcff1db1 --- /dev/null +++ b/src/extensions/yfm/specs.ts @@ -0,0 +1,57 @@ +import type {ExtensionAuto} from '../../core'; + +import {CheckboxSpecs, CheckboxSpecsOptions} from './Checkbox/CheckboxSpecs'; +import {ColorSpecs} from './Color/ColorSpecs'; +import {MathSpecs} from './Math/MathSpecs'; +import {MonospaceSpecs} from './Monospace/MonospaceSpecs'; +import {ImgSizeSpecs, ImgSizeSpecsOptions} from './ImgSize/ImgSizeSpecs'; +import {VideoSpecs, VideoSpecsOptions} from './Video/VideoSpecs'; +import {YfmDistSpecs} from './YfmDist/YfmDistSpecs'; +import {YfmCutSpecs, YfmCutSpecsOptions} from './YfmCut/YfmCutSpecs'; +import {YfmFileSpecs} from './YfmFile/YfmFileSpecs'; +import {YfmHeadingSpecs, YfmHeadingSpecsOptions} from './YfmHeading/YfmHeadingSpecs'; +import {YfmNoteSpecs, YfmNoteSpecsOptions} from './YfmNote/YfmNoteSpecs'; +import {YfmTableSpecs, YfmTableSpecsOptions} from './YfmTable/YfmTableSpecs'; +import {YfmTabsSpecs, YfmTabsSpecsOptions} from './YfmTabs/YfmTabsSpecs'; + +export * from './Checkbox/CheckboxSpecs'; +export * from './Color/ColorSpecs'; +export * from './ImgSize/ImgSizeSpecs'; +export * from './Math/MathSpecs'; +export * from './Monospace/MonospaceSpecs'; +export * from './Video/VideoSpecs'; +export * from './YfmDist/YfmDistSpecs'; +export * from './YfmCut/YfmCutSpecs'; +export * from './YfmFile/YfmFileSpecs'; +export * from './YfmHeading/YfmHeadingSpecs'; +export * from './YfmNote/YfmNoteSpecs'; +export * from './YfmTable/YfmTableSpecs'; +export * from './YfmTabs/YfmTabsSpecs'; + +export type YfmSpecsPresetOptions = { + checkbox?: CheckboxSpecsOptions; + video?: VideoSpecsOptions; + imgSize?: ImgSizeSpecsOptions; + yfmCut?: YfmCutSpecsOptions; + yfmNote?: YfmNoteSpecsOptions; + yfmTable?: YfmTableSpecsOptions; + yfmTabs?: YfmTabsSpecsOptions; + yfmHeading?: YfmHeadingSpecsOptions; +}; + +export const YfmSpecsPreset: ExtensionAuto = (builder, opts) => { + builder + .use(CheckboxSpecs, opts.checkbox ?? {}) + .use(ColorSpecs) + .use(ImgSizeSpecs, opts.imgSize ?? {}) + .use(MathSpecs) + .use(MonospaceSpecs) + .use(VideoSpecs, opts.video ?? {}) + .use(YfmDistSpecs) + .use(YfmCutSpecs, opts.yfmCut ?? {}) + .use(YfmNoteSpecs, opts.yfmNote ?? {}) + .use(YfmFileSpecs) + .use(YfmHeadingSpecs, opts.yfmHeading ?? {}) + .use(YfmTableSpecs, opts.yfmTable ?? {}) + .use(YfmTabsSpecs, opts.yfmTabs ?? {}); +}; diff --git a/src/index.ts b/src/index.ts index a84d39ba..a93f55b2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,12 +6,14 @@ export * from './react-utils/hooks'; export * from './classname'; export * from './logger'; export * from './extensions'; +export * from './extensions/specs'; export {isMac} from './utils/platform'; export {markInputRule, nodeInputRule, inlineNodeInputRule} from './utils/inputrules'; export {findMark, isMarkActive} from './utils/marks'; export {findFirstTextblockChild, isNodeEmpty, isCodeBlock, isSelectableNode} from './utils/nodes'; export {nodeTypeFactory, markTypeFactory, isSameNodeType} from './utils/schema'; +export {getPlaceholderContent} from './utils/placeholder'; export { isTextSelection, isNodeSelection, diff --git a/src/utils/placeholder.ts b/src/utils/placeholder.ts new file mode 100644 index 00000000..c5bdee02 --- /dev/null +++ b/src/utils/placeholder.ts @@ -0,0 +1,8 @@ +import type {Node} from 'prosemirror-model'; +import type {} from '../extensions/behavior/Placeholder'; + +export const getPlaceholderContent = (node: Node, parent?: Node | null) => { + const content = node.type.spec.placeholder?.content || ''; + + return typeof content === 'function' ? content(node, parent) : content; +};