From 622b398ffef613a8bea218a0c0ac0b6a2b440821 Mon Sep 17 00:00:00 2001 From: Yuriy Demidov Date: Mon, 23 Mar 2026 19:04:37 +0300 Subject: [PATCH] refactor(Heading,YfmHeading): extend Heading base extension instead of replacing it entirely --- .../FoldingHeadingSpecs.test.ts | 3 +- .../markdown/Heading/HeadingSpecs/const.ts | 6 + .../markdown/Heading/HeadingSpecs/index.ts | 19 +- .../markdown/Heading/HeadingSpecs/utils.ts | 24 +++ .../extensions/markdown/Heading/actions.ts | 8 +- .../extensions/markdown/Heading/commands.ts | 27 ++- .../src/extensions/markdown/Heading/index.ts | 22 +-- .../yfm/YfmHeading/YfmHeading.test.ts | 4 +- .../yfm/YfmHeading/YfmHeadingSpecs/const.ts | 6 +- .../yfm/YfmHeading/YfmHeadingSpecs/index.ts | 168 ++++++++---------- .../yfm/YfmHeading/YfmHeadingSpecs/utils.ts | 17 ++ .../src/extensions/yfm/YfmHeading/actions.ts | 16 +- .../src/extensions/yfm/YfmHeading/commands.ts | 35 +--- .../src/extensions/yfm/YfmHeading/index.ts | 42 +---- packages/editor/src/presets/yfm-specs.ts | 6 +- packages/editor/src/presets/yfm.ts | 6 +- 16 files changed, 197 insertions(+), 212 deletions(-) create mode 100644 packages/editor/src/extensions/markdown/Heading/HeadingSpecs/const.ts create mode 100644 packages/editor/src/extensions/markdown/Heading/HeadingSpecs/utils.ts diff --git a/packages/editor/src/extensions/additional/FoldingHeading/FoldingHeadingSpec/FoldingHeadingSpecs.test.ts b/packages/editor/src/extensions/additional/FoldingHeading/FoldingHeadingSpec/FoldingHeadingSpecs.test.ts index 0931a058f..29bb14636 100644 --- a/packages/editor/src/extensions/additional/FoldingHeading/FoldingHeadingSpec/FoldingHeadingSpecs.test.ts +++ b/packages/editor/src/extensions/additional/FoldingHeading/FoldingHeadingSpec/FoldingHeadingSpecs.test.ts @@ -3,7 +3,7 @@ import {builders} from 'prosemirror-test-builder'; import {createMarkupChecker} from '../../../../../tests/sameMarkup'; import {ExtensionsManager} from '../../../../core'; import {BaseNode, BaseSchemaSpecs} from '../../../base/specs'; -import {ItalicSpecs, headingNodeName, italicMarkName} from '../../../markdown/specs'; +import {HeadingSpecs, ItalicSpecs, headingNodeName, italicMarkName} from '../../../markdown/specs'; import {YfmHeadingAttr, YfmHeadingSpecs} from '../../../yfm/specs'; import {FoldingHeadingSpecs} from './FoldingHeadingSpecs'; @@ -13,6 +13,7 @@ const {schema, markupParser, serializer} = new ExtensionsManager({ builder .use(BaseSchemaSpecs, {}) .use(ItalicSpecs) + .use(HeadingSpecs, {}) .use(YfmHeadingSpecs, {}) .use(FoldingHeadingSpecs), }).buildDeps(); diff --git a/packages/editor/src/extensions/markdown/Heading/HeadingSpecs/const.ts b/packages/editor/src/extensions/markdown/Heading/HeadingSpecs/const.ts new file mode 100644 index 000000000..6269a8f06 --- /dev/null +++ b/packages/editor/src/extensions/markdown/Heading/HeadingSpecs/const.ts @@ -0,0 +1,6 @@ +import {nodeTypeFactory} from 'src/utils/schema'; + +export const headingNodeName = 'heading'; +export const headingLevelAttr = 'level'; +export const headingLineNumberAttr = 'data-line'; +export const headingType = nodeTypeFactory(headingNodeName); diff --git a/packages/editor/src/extensions/markdown/Heading/HeadingSpecs/index.ts b/packages/editor/src/extensions/markdown/Heading/HeadingSpecs/index.ts index fa1736aa6..94e66c8d4 100644 --- a/packages/editor/src/extensions/markdown/Heading/HeadingSpecs/index.ts +++ b/packages/editor/src/extensions/markdown/Heading/HeadingSpecs/index.ts @@ -1,12 +1,11 @@ -import type {Node, NodeSpec} from 'prosemirror-model'; +import type {ExtensionAuto} from '#core'; +import type {Node, NodeSpec} from '#pm/model'; -import type {ExtensionAuto} from '../../../../core'; -import {nodeTypeFactory} from '../../../../utils/schema'; +import {headingLevelAttr, headingLineNumberAttr, headingNodeName} from './const'; +import {headingToMarkdown} from './utils'; -export const headingNodeName = 'heading'; -export const headingLevelAttr = 'level'; -export const headingLineNumberAttr = 'data-line'; -export const headingType = nodeTypeFactory(headingNodeName); +export * from './const'; +export {headingToMarkdown} from './utils'; const DEFAULT_PLACEHOLDER = (node: Node) => 'Heading ' + node.attrs[headingLevelAttr]; @@ -56,10 +55,6 @@ export const HeadingSpecs: ExtensionAuto = (builder, opts) getAttrs: (tok) => ({[headingLevelAttr]: Number(tok.tag.slice(1))}), }, }, - toMd: (state, node) => { - state.write(state.repeat('#', node.attrs[headingLevelAttr]) + ' '); - state.renderInline(node); - state.closeBlock(node); - }, + toMd: headingToMarkdown(), })); }; diff --git a/packages/editor/src/extensions/markdown/Heading/HeadingSpecs/utils.ts b/packages/editor/src/extensions/markdown/Heading/HeadingSpecs/utils.ts new file mode 100644 index 000000000..f6178f3cc --- /dev/null +++ b/packages/editor/src/extensions/markdown/Heading/HeadingSpecs/utils.ts @@ -0,0 +1,24 @@ +import type {SerializerNodeToken} from '#core'; + +import {headingLevelAttr} from './index'; + +type HeadingToMarkdownParams = { + renderMarkup?: SerializerNodeToken; + renderAttributes?: SerializerNodeToken; +}; + +const defaultMarkupRender: SerializerNodeToken = (state, node) => + state.write(state.repeat('#', node.attrs[headingLevelAttr]) + ' '); + +export function headingToMarkdown({ + renderMarkup = defaultMarkupRender, + renderAttributes, +}: HeadingToMarkdownParams = {}): SerializerNodeToken { + return (...args) => { + const [state, node] = args; + renderMarkup(...args); + state.renderInline(node); + renderAttributes?.(...args); + state.closeBlock(node); + }; +} diff --git a/packages/editor/src/extensions/markdown/Heading/actions.ts b/packages/editor/src/extensions/markdown/Heading/actions.ts index 7b5e667dd..0f3d2d171 100644 --- a/packages/editor/src/extensions/markdown/Heading/actions.ts +++ b/packages/editor/src/extensions/markdown/Heading/actions.ts @@ -1,13 +1,13 @@ -import {setBlockType} from 'prosemirror-commands'; import type {NodeType} from 'prosemirror-model'; import type {ActionSpec} from '../../../core'; -import {type HeadingLevel, headingLevelAttr} from './const'; +import {toHeading} from './commands'; +import type {HeadingLevel} from './const'; import {hasParentHeading} from './utils'; -export const headingAction = (nodeType: NodeType, level: HeadingLevel): ActionSpec => { - const cmd = setBlockType(nodeType, {[headingLevelAttr]: level}); +export const headingAction = (_nodeType: NodeType, level: HeadingLevel): ActionSpec => { + const cmd = toHeading(level); return { isActive: hasParentHeading(level), isEnable: cmd, diff --git a/packages/editor/src/extensions/markdown/Heading/commands.ts b/packages/editor/src/extensions/markdown/Heading/commands.ts index fab27a5fb..d90726409 100644 --- a/packages/editor/src/extensions/markdown/Heading/commands.ts +++ b/packages/editor/src/extensions/markdown/Heading/commands.ts @@ -1,7 +1,11 @@ -import type {Command} from 'prosemirror-state'; +import {setBlockType} from '#pm/commands'; +import type {Command} from '#pm/state'; +import {findParentNodeOfType} from '#pm/utils'; import {toParagraph} from '../../base/BaseSchema'; -import {headingType} from '../Heading/HeadingSpecs'; +import {headingLevelAttr, headingType} from '../Heading/HeadingSpecs'; + +import type {HeadingLevel} from './const'; export const resetHeading: Command = (state, dispatch, view) => { const {selection} = state; @@ -14,3 +18,22 @@ export const resetHeading: Command = (state, dispatch, view) => { } return false; }; + +export const toHeading = + (level: HeadingLevel): Command => + (state, dispatch, view) => { + const attrs: Record = {}; + + const parentHeading = findParentNodeOfType(headingType(state.schema))(state.selection); + if (parentHeading) { + if (parentHeading.node.attrs[headingLevelAttr] === level) { + return toParagraph(state, dispatch, view); + } + + Object.assign(attrs, parentHeading.node.attrs); + } + + attrs[headingLevelAttr] = level; + + return setBlockType(headingType(state.schema), attrs)(state, dispatch); + }; diff --git a/packages/editor/src/extensions/markdown/Heading/index.ts b/packages/editor/src/extensions/markdown/Heading/index.ts index ab6d239f5..f7b08233c 100644 --- a/packages/editor/src/extensions/markdown/Heading/index.ts +++ b/packages/editor/src/extensions/markdown/Heading/index.ts @@ -1,12 +1,10 @@ -import {setBlockType} from 'prosemirror-commands'; - import type {Action, ExtensionAuto, Keymap} from '../../../core'; import {withLogAction} from '../../../utils/keymap'; import {HeadingSpecs, type HeadingSpecsOptions, headingType} from './HeadingSpecs'; import {headingAction} from './actions'; -import {resetHeading} from './commands'; -import {HeadingAction, type HeadingLevel, headingLevelAttr} from './const'; +import {resetHeading, toHeading} from './commands'; +import {HeadingAction} from './const'; import {headingRule} from './utils'; export {headingNodeName, headingType} from './HeadingSpecs'; @@ -25,18 +23,16 @@ export const Heading: ExtensionAuto = (builder, opts) => { builder.use(HeadingSpecs, opts); builder - .addKeymap(({schema}) => { + .addKeymap(() => { const {h1Key, h2Key, h3Key, h4Key, h5Key, h6Key} = opts ?? {}; - const cmd4lvl = (level: HeadingLevel) => - setBlockType(headingType(schema), {[headingLevelAttr]: level}); const bindings: Keymap = {Backspace: resetHeading}; - if (h1Key) bindings[h1Key] = withLogAction('heading1', cmd4lvl(1)); - if (h2Key) bindings[h2Key] = withLogAction('heading2', cmd4lvl(2)); - if (h3Key) bindings[h3Key] = withLogAction('heading3', cmd4lvl(3)); - if (h4Key) bindings[h4Key] = withLogAction('heading4', cmd4lvl(4)); - if (h5Key) bindings[h5Key] = withLogAction('heading5', cmd4lvl(5)); - if (h6Key) bindings[h6Key] = withLogAction('heading6', cmd4lvl(6)); + if (h1Key) bindings[h1Key] = withLogAction('heading1', toHeading(1)); + if (h2Key) bindings[h2Key] = withLogAction('heading2', toHeading(2)); + if (h3Key) bindings[h3Key] = withLogAction('heading3', toHeading(3)); + if (h4Key) bindings[h4Key] = withLogAction('heading4', toHeading(4)); + if (h5Key) bindings[h5Key] = withLogAction('heading5', toHeading(5)); + if (h6Key) bindings[h6Key] = withLogAction('heading6', toHeading(6)); return bindings; }) .addInputRules(({schema}) => ({rules: [headingRule(headingType(schema), 6)]})); diff --git a/packages/editor/src/extensions/yfm/YfmHeading/YfmHeading.test.ts b/packages/editor/src/extensions/yfm/YfmHeading/YfmHeading.test.ts index 6b9baa913..e43d1fdef 100644 --- a/packages/editor/src/extensions/yfm/YfmHeading/YfmHeading.test.ts +++ b/packages/editor/src/extensions/yfm/YfmHeading/YfmHeading.test.ts @@ -5,7 +5,7 @@ import {parseDOM} from '../../../../tests/parse-dom'; import {createMarkupChecker} from '../../../../tests/sameMarkup'; import {ExtensionsManager} from '../../../core'; import {BaseNode, BaseSchemaSpecs} from '../../base/specs'; -import {BoldSpecs, boldMarkName, headingNodeName} from '../../markdown/specs'; +import {BoldSpecs, HeadingSpecs, boldMarkName, headingNodeName} from '../../markdown/specs'; import {YfmConfigsSpecs} from '../specs'; import {YfmHeadingAttr, YfmHeadingSpecs} from './YfmHeadingSpecs'; @@ -18,6 +18,7 @@ const { extensions: (builder) => builder .use(BaseSchemaSpecs, {}) + .use(HeadingSpecs, {}) .use(YfmConfigsSpecs, {attrs: {allowedAttributes: ['id']}}) .use(YfmHeadingSpecs, {}) .use(BoldSpecs), @@ -114,6 +115,7 @@ describe('Heading extension', () => { builder .use(BaseSchemaSpecs, {}) .use(BoldSpecs) + .use(HeadingSpecs, {}) .use(YfmConfigsSpecs, {disableAttrs: true}) .use(YfmHeadingSpecs, {}), }).buildDeps(); diff --git a/packages/editor/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/const.ts b/packages/editor/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/const.ts index de417125e..25c8601ba 100644 --- a/packages/editor/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/const.ts +++ b/packages/editor/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/const.ts @@ -1,11 +1,11 @@ -import {headingLevelAttr} from '../../../markdown/Heading/HeadingSpecs'; +import {headingLevelAttr, headingLineNumberAttr} from '../../../markdown/Heading/HeadingSpecs'; export type {HeadingLevel} from '../../../markdown/Heading/const'; export {headingLevelAttr, headingNodeName} from '../../../markdown/Heading/HeadingSpecs'; export const YfmHeadingAttr = { Level: headingLevelAttr, + DataLine: headingLineNumberAttr, Id: 'id', - DataLine: 'data-line', - Folding: 'folded', + Folding: 'folded', // TODO: move to FoldingHeadingExtension } as const; diff --git a/packages/editor/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/index.ts b/packages/editor/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/index.ts index c8a63d0fe..65285483d 100644 --- a/packages/editor/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/index.ts +++ b/packages/editor/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/index.ts @@ -1,13 +1,15 @@ import type {ExtensionAuto} from '#core'; import type {Node, NodeSpec} from '#pm/model'; +import {headingToMarkdown} from 'src/extensions/markdown/Heading/HeadingSpecs'; import {YfmHeadingAttr, headingNodeName} from './const'; import {headingIdsPlugin} from './markdown/heading-ids'; -import {getNodeAttrs} from './utils'; +import {getNodeAttrs, renderYfmHeadingAttributes, renderYfmHeadingMarkup} from './utils'; const DEFAULT_PLACEHOLDER = (node: Node) => 'Heading ' + node.attrs[YfmHeadingAttr.Level]; export {YfmHeadingAttr} from './const'; +export {renderYfmHeadingAttributes, renderYfmHeadingMarkup} from './utils'; export type YfmHeadingSpecsOptions = { /** @@ -19,103 +21,89 @@ export type YfmHeadingSpecsOptions = { /** YfmHeading extension needs markdown-it-attrs plugin */ export const YfmHeadingSpecs: ExtensionAuto = (builder, opts) => { builder.configureMd((md) => md.use(headingIdsPlugin)); - builder.addNode(headingNodeName, () => ({ - spec: { - attrs: { - [YfmHeadingAttr.Id]: {default: ''}, - [YfmHeadingAttr.Level]: {default: 1}, - [YfmHeadingAttr.DataLine]: {default: null}, - [YfmHeadingAttr.Folding]: {default: null}, + + builder.overrideNodeSpec(headingNodeName, (prev) => ({ + ...prev, + attrs: { + ...prev.attrs, + [YfmHeadingAttr.Id]: {default: ''}, + [YfmHeadingAttr.Folding]: {default: null}, + }, + selectable: true, + parseDOM: [ + {tag: 'h1', getAttrs: getNodeAttrs(1), priority: 100, consuming: true}, + {tag: 'h2', getAttrs: getNodeAttrs(2), priority: 100, consuming: true}, + {tag: 'h3', getAttrs: getNodeAttrs(3), priority: 100, consuming: true}, + {tag: 'h4', getAttrs: getNodeAttrs(4), priority: 100, consuming: true}, + {tag: 'h5', getAttrs: getNodeAttrs(5), priority: 100, consuming: true}, + {tag: 'h6', getAttrs: getNodeAttrs(6), priority: 100, consuming: true}, + { + // ignore anchor link inside headings + tag: 'a.yfm-anchor', + context: `${headingNodeName}/`, + skip: true, + ignore: true, + priority: 1000, }, - content: '(text | inline)*', - group: 'block', - defining: true, - selectable: true, - parseDOM: [ - {tag: 'h1', getAttrs: getNodeAttrs(1), priority: 100, consuming: true}, - {tag: 'h2', getAttrs: getNodeAttrs(2), priority: 100, consuming: true}, - {tag: 'h3', getAttrs: getNodeAttrs(3), priority: 100, consuming: true}, - {tag: 'h4', getAttrs: getNodeAttrs(4), priority: 100, consuming: true}, - {tag: 'h5', getAttrs: getNodeAttrs(5), priority: 100, consuming: true}, - {tag: 'h6', getAttrs: getNodeAttrs(6), priority: 100, consuming: true}, + ], + toDOM(node) { + const id = node.attrs[YfmHeadingAttr.Id]; + const lineNumber = node.attrs[YfmHeadingAttr.DataLine]; + const folding = node.attrs[YfmHeadingAttr.Folding]; + return [ + 'h' + node.attrs[YfmHeadingAttr.Level], { - // ignore anchor link inside headings - tag: 'a.yfm-anchor', - context: `${headingNodeName}/`, - skip: true, - ignore: true, - priority: 1000, + id: id || null, + [YfmHeadingAttr.DataLine]: lineNumber, + [`data-${YfmHeadingAttr.Folding}`]: folding, }, - ], - toDOM(node) { - const id = node.attrs[YfmHeadingAttr.Id]; - const lineNumber = node.attrs[YfmHeadingAttr.DataLine]; - const folding = node.attrs[YfmHeadingAttr.Folding]; - return [ - 'h' + node.attrs[YfmHeadingAttr.Level], - { - id: id || null, - [YfmHeadingAttr.DataLine]: lineNumber, - [`data-${YfmHeadingAttr.Folding}`]: folding, - }, - 0, - // [ - // 'a', - // { - // href: `#${node.attrs[YfmHeadingAttr.Id]}`, - // class: 'yfm-anchor', - // 'aria-hidden': 'true', - // contenteditable: 'false', - // }, - // ], - // ['span', 0], - ]; - }, - placeholder: { - content: - builder.context.get('placeholder')?.heading ?? - opts.headingPlaceholder ?? - DEFAULT_PLACEHOLDER, - alwaysVisible: true, - }, + 0, + // [ + // 'a', + // { + // href: `#${node.attrs[YfmHeadingAttr.Id]}`, + // class: 'yfm-anchor', + // 'aria-hidden': 'true', + // contenteditable: 'false', + // }, + // ], + // ['span', 0], + ]; }, - fromMd: { - 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)), - [YfmHeadingAttr.Folding]: token.meta?.folding === true ? true : null, - ...attrs, - }; - }, - }, + placeholder: { + content: + builder.context.get('placeholder')?.heading ?? + opts.headingPlaceholder ?? + DEFAULT_PLACEHOLDER, + alwaysVisible: true, }, - toMd: (state, node) => { - const folding = node.attrs[YfmHeadingAttr.Folding]; - const level = node.attrs[YfmHeadingAttr.Level]; - - state.write(state.repeat('#', level) + (typeof folding === 'boolean' ? '+' : '') + ' '); - state.renderInline(node); + })); - const anchor = node.attrs[YfmHeadingAttr.Id]; + builder.overrideMarkdownTokenParserSpec(headingNodeName, (prev) => ({ + ...prev, + getAttrs: (token) => { + if (token.type.endsWith('_close')) return {}; - if (anchor /*&& anchor !== node.firstChild?.textContent*/) { - state.write(` {#${anchor}}`); - } + 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); + // } - state.closeBlock(node); + // attrs have id only if it explicitly specified manually + return { + [YfmHeadingAttr.Level]: Number(token.tag.slice(1)), + [YfmHeadingAttr.Folding]: token.meta?.folding === true ? true : null, + ...attrs, + }; }, })); + + builder.overrideNodeSerializerSpec(headingNodeName, () => + headingToMarkdown({ + renderMarkup: renderYfmHeadingMarkup, + renderAttributes: renderYfmHeadingAttributes, + }), + ); }; diff --git a/packages/editor/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/utils.ts b/packages/editor/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/utils.ts index b7870f1ee..a398a1a87 100644 --- a/packages/editor/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/utils.ts +++ b/packages/editor/src/extensions/yfm/YfmHeading/YfmHeadingSpecs/utils.ts @@ -1,5 +1,7 @@ import type {TagParseRule} from 'prosemirror-model'; +import type {SerializerNodeToken} from '#core'; + import {type HeadingLevel, YfmHeadingAttr} from '../const'; export {hasParentHeading, headingRule} from '../../../markdown/Heading/utils'; export {headingType} from '../../../markdown/Heading/HeadingSpecs'; @@ -28,3 +30,18 @@ function getFoldingAttr(node: HTMLElement): boolean | null { // lower: true, // remove: /[*+~.()'"!:@]/g, // }); + +export const renderYfmHeadingMarkup: SerializerNodeToken = (state, node) => { + const folding = node.attrs[YfmHeadingAttr.Folding]; + const level = node.attrs[YfmHeadingAttr.Level]; + + state.write(state.repeat('#', level) + (typeof folding === 'boolean' ? '+' : '') + ' '); +}; + +export const renderYfmHeadingAttributes: SerializerNodeToken = (state, node) => { + const anchor = node.attrs[YfmHeadingAttr.Id]; + + if (anchor /*&& anchor !== node.firstChild?.textContent*/) { + state.write(` {#${anchor}}`); + } +}; diff --git a/packages/editor/src/extensions/yfm/YfmHeading/actions.ts b/packages/editor/src/extensions/yfm/YfmHeading/actions.ts index dacad1264..0872e604f 100644 --- a/packages/editor/src/extensions/yfm/YfmHeading/actions.ts +++ b/packages/editor/src/extensions/yfm/YfmHeading/actions.ts @@ -1,14 +1,8 @@ -import type {ActionSpec} from '../../../core'; +import type {ActionSpec} from '#core'; +import type {NodeType} from '#pm/model'; +import {headingAction as headingActionBase} from 'src/extensions/markdown/Heading/actions'; -import {hasParentHeading} from './YfmHeadingSpecs/utils'; -import {toHeading} from './commands'; import type {HeadingLevel} from './const'; -export const headingAction = (level: HeadingLevel): ActionSpec => { - const cmd = toHeading(level); - return { - isActive: hasParentHeading(level), - isEnable: cmd, - run: cmd, - }; -}; +export const headingAction = (level: HeadingLevel): ActionSpec => + headingActionBase({} as NodeType, level); diff --git a/packages/editor/src/extensions/yfm/YfmHeading/commands.ts b/packages/editor/src/extensions/yfm/YfmHeading/commands.ts index 088ae8514..908406803 100644 --- a/packages/editor/src/extensions/yfm/YfmHeading/commands.ts +++ b/packages/editor/src/extensions/yfm/YfmHeading/commands.ts @@ -1,32 +1,3 @@ -import {setBlockType} from 'prosemirror-commands'; -import type {Command} from 'prosemirror-state'; -// @ts-ignore // TODO: fix cjs build -import {findParentNodeOfType} from 'prosemirror-utils'; - -import {toParagraph} from '../../../extensions/base'; - -import {headingType} from './YfmHeadingSpecs/utils'; -import {type HeadingLevel, YfmHeadingAttr, headingLevelAttr} from './const'; - -export {resetHeading} from '../../markdown/Heading/commands'; - -export const toHeading = - (level: HeadingLevel): Command => - (state, dispatch, view) => { - const attrs: Record = {}; - - const parentHeading = findParentNodeOfType(headingType(state.schema))(state.selection); - if (parentHeading) { - if (parentHeading.node.attrs[headingLevelAttr] === level) { - return toParagraph(state, dispatch, view); - } - - Object.assign(attrs, parentHeading.node.attrs); - } - - // const text = state.selection.$head.parent.textContent; - // attrs[YfmHeadingAttr.Id] = slugify(text); - attrs[YfmHeadingAttr.Level] = level; - - return setBlockType(headingType(state.schema), attrs)(state, dispatch); - }; +// File preserved for backward compatibility. +// TODO [MAJOR]: Delete this file if no more commands appear here. +export {resetHeading, toHeading} from '../../markdown/Heading/commands'; diff --git a/packages/editor/src/extensions/yfm/YfmHeading/index.ts b/packages/editor/src/extensions/yfm/YfmHeading/index.ts index 30e6523d6..5e001994e 100644 --- a/packages/editor/src/extensions/yfm/YfmHeading/index.ts +++ b/packages/editor/src/extensions/yfm/YfmHeading/index.ts @@ -1,11 +1,6 @@ -import type {Action, ExtensionAuto, Keymap} from '../../../core'; -import {withLogAction} from '../../../utils/keymap'; +import type {ExtensionAuto} from '#core'; import {YfmHeadingSpecs, type YfmHeadingSpecsOptions} from './YfmHeadingSpecs'; -import {headingRule, headingType} from './YfmHeadingSpecs/utils'; -import {headingAction} from './actions'; -import {resetHeading, toHeading} from './commands'; -import {HeadingAction} from './const'; export {YfmHeadingAttr} from './const'; @@ -21,39 +16,4 @@ export type YfmHeadingOptions = YfmHeadingSpecsOptions & { /** YfmHeading extension needs markdown-it-attrs plugin */ export const YfmHeading: ExtensionAuto = (builder, opts) => { builder.use(YfmHeadingSpecs, opts); - - builder - .addKeymap(() => { - const {h1Key, h2Key, h3Key, h4Key, h5Key, h6Key} = opts ?? {}; - const bindings: Keymap = {Backspace: resetHeading}; - if (h1Key) bindings[h1Key] = withLogAction('heading1', toHeading(1)); - if (h2Key) bindings[h2Key] = withLogAction('heading2', toHeading(2)); - if (h3Key) bindings[h3Key] = withLogAction('heading3', toHeading(3)); - if (h4Key) bindings[h4Key] = withLogAction('heading4', toHeading(4)); - if (h5Key) bindings[h5Key] = withLogAction('heading5', toHeading(5)); - if (h6Key) bindings[h6Key] = withLogAction('heading6', toHeading(6)); - return bindings; - }) - .addInputRules(({schema}) => ({rules: [headingRule(headingType(schema), 6)]})); - - builder - .addAction(HeadingAction.ToH1, () => headingAction(1)) - .addAction(HeadingAction.ToH2, () => headingAction(2)) - .addAction(HeadingAction.ToH3, () => headingAction(3)) - .addAction(HeadingAction.ToH4, () => headingAction(4)) - .addAction(HeadingAction.ToH5, () => headingAction(5)) - .addAction(HeadingAction.ToH6, () => headingAction(6)); }; - -declare global { - namespace WysiwygEditor { - interface Actions { - [HeadingAction.ToH1]: Action; - [HeadingAction.ToH2]: Action; - [HeadingAction.ToH3]: Action; - [HeadingAction.ToH4]: Action; - [HeadingAction.ToH5]: Action; - [HeadingAction.ToH6]: Action; - } - } -} diff --git a/packages/editor/src/presets/yfm-specs.ts b/packages/editor/src/presets/yfm-specs.ts index 672de30a3..e92e4c472 100644 --- a/packages/editor/src/presets/yfm-specs.ts +++ b/packages/editor/src/presets/yfm-specs.ts @@ -45,7 +45,11 @@ export type YfmSpecsPresetOptions = Omit = (builder, opts) => { - builder.use(DefaultSpecsPreset, {...opts, image: false, heading: false}); + builder.use(DefaultSpecsPreset, { + ...opts, + image: false, + heading: opts.yfmHeading, + } satisfies DefaultSpecsPresetOptions); builder .use(DeflistSpecs, opts.deflist ?? {}) diff --git a/packages/editor/src/presets/yfm.ts b/packages/editor/src/presets/yfm.ts index 73b4afe26..2c70bec27 100644 --- a/packages/editor/src/presets/yfm.ts +++ b/packages/editor/src/presets/yfm.ts @@ -47,7 +47,11 @@ export type YfmPresetOptions = Omit & }; export const YfmPreset: ExtensionAuto = (builder, opts) => { - builder.use(DefaultPreset, {...opts, image: false, heading: false}); + builder.use(DefaultPreset, { + ...opts, + image: false, + heading: opts.yfmHeading, + } satisfies DefaultPresetOptions); builder .use(Deflist, opts.deflist ?? {})