UNPKG

@milkdown/preset-commonmark

Version:

The commonmark preset of [milkdown](https://milkdown.dev/).

256 lines (228 loc) 6.74 kB
import type { Node } from '@milkdown/prose/model' import { commandsCtx, editorViewCtx } from '@milkdown/core' import { expectDomTypeError } from '@milkdown/exception' import { setBlockType } from '@milkdown/prose/commands' import { textblockTypeInputRule } from '@milkdown/prose/inputrules' import { $command, $ctx, $inputRule, $nodeAttr, $nodeSchema, $useKeymap, } from '@milkdown/utils' import { serializeText, withMeta } from '../__internal__' import { paragraphSchema } from './paragraph' const headingIndex = Array(6) .fill(0) .map((_, i) => i + 1) function defaultHeadingIdGenerator(node: Node) { return node.textContent.toLowerCase().trim().replace(/\s+/g, '-') } /// This is a slice contains a function to generate heading id. /// You can configure it to generate id in your own way. export const headingIdGenerator = $ctx( defaultHeadingIdGenerator, 'headingIdGenerator' ) withMeta(headingIdGenerator, { displayName: 'Ctx<HeadingIdGenerator>', group: 'Heading', }) /// HTML attributes for heading node. export const headingAttr = $nodeAttr('heading') withMeta(headingAttr, { displayName: 'Attr<heading>', group: 'Heading', }) /// Schema for heading node. export const headingSchema = $nodeSchema('heading', (ctx) => { const getId = ctx.get(headingIdGenerator.key) return { content: 'inline*', group: 'block', defining: true, attrs: { id: { default: '', validate: 'string', }, level: { default: 1, validate: 'number', }, }, parseDOM: headingIndex.map((x) => ({ tag: `h${x}`, getAttrs: (node) => { if (!(node instanceof HTMLElement)) throw expectDomTypeError(node) return { level: x, id: node.id } }, })), toDOM: (node) => { return [ `h${node.attrs.level}`, { ...ctx.get(headingAttr.key)(node), id: node.attrs.id || getId(node), }, 0, ] }, parseMarkdown: { match: ({ type }) => type === 'heading', runner: (state, node, type) => { const depth = node.depth as number state.openNode(type, { level: depth }) state.next(node.children) state.closeNode() }, }, toMarkdown: { match: (node) => node.type.name === 'heading', runner: (state, node) => { state.openNode('heading', undefined, { depth: node.attrs.level }) serializeText(state, node) state.closeNode() }, }, } }) withMeta(headingSchema.node, { displayName: 'NodeSchema<heading>', group: 'Heading', }) withMeta(headingSchema.ctx, { displayName: 'NodeSchemaCtx<heading>', group: 'Heading', }) /// This input rule can turn the selected block into heading. /// You can input numbers of `#` and a `space` to create heading. export const wrapInHeadingInputRule = $inputRule((ctx) => { return textblockTypeInputRule( /^(?<hashes>#+)\s$/, headingSchema.type(ctx), (match) => { const x = match.groups?.hashes?.length || 0 const view = ctx.get(editorViewCtx) const { $from } = view.state.selection const node = $from.node() if (node.type.name === 'heading') { let level = Number(node.attrs.level) + Number(x) if (level > 6) level = 6 return { level } } return { level: x } } ) }) withMeta(wrapInHeadingInputRule, { displayName: 'InputRule<wrapInHeadingInputRule>', group: 'Heading', }) /// This command can turn the selected block into heading. /// You can pass the level of heading to this command. /// By default, the level is 1, which means it will create a `h1` element. export const wrapInHeadingCommand = $command('WrapInHeading', (ctx) => { return (level?: number) => { level ??= 1 if (level < 1) return setBlockType(paragraphSchema.type(ctx)) return setBlockType(headingSchema.type(ctx), { level }) } }) withMeta(wrapInHeadingCommand, { displayName: 'Command<wrapInHeadingCommand>', group: 'Heading', }) /// This command can downgrade the selected heading. /// For example, if you have a `h2` element, and you call this command, you will get a `h1` element. /// If the element is already a `h1` element, it will turn it into a `p` element. export const downgradeHeadingCommand = $command( 'DowngradeHeading', (ctx) => () => (state, dispatch, view) => { const { $from } = state.selection const node = $from.node() if ( node.type !== headingSchema.type(ctx) || !state.selection.empty || $from.parentOffset !== 0 ) return false const level = node.attrs.level - 1 if (!level) return setBlockType(paragraphSchema.type(ctx))(state, dispatch, view) dispatch?.( state.tr.setNodeMarkup(state.selection.$from.before(), undefined, { ...node.attrs, level, }) ) return true } ) withMeta(downgradeHeadingCommand, { displayName: 'Command<downgradeHeadingCommand>', group: 'Heading', }) /// Keymap for heading node. /// - `<Mod-Alt-{1-6}>`: Turn the selected block into `h{1-6}` element. /// - `<Delete>/<Backspace>`: Downgrade the selected heading. export const headingKeymap = $useKeymap('headingKeymap', { TurnIntoH1: { shortcuts: 'Mod-Alt-1', command: (ctx) => { const commands = ctx.get(commandsCtx) return () => commands.call(wrapInHeadingCommand.key, 1) }, }, TurnIntoH2: { shortcuts: 'Mod-Alt-2', command: (ctx) => { const commands = ctx.get(commandsCtx) return () => commands.call(wrapInHeadingCommand.key, 2) }, }, TurnIntoH3: { shortcuts: 'Mod-Alt-3', command: (ctx) => { const commands = ctx.get(commandsCtx) return () => commands.call(wrapInHeadingCommand.key, 3) }, }, TurnIntoH4: { shortcuts: 'Mod-Alt-4', command: (ctx) => { const commands = ctx.get(commandsCtx) return () => commands.call(wrapInHeadingCommand.key, 4) }, }, TurnIntoH5: { shortcuts: 'Mod-Alt-5', command: (ctx) => { const commands = ctx.get(commandsCtx) return () => commands.call(wrapInHeadingCommand.key, 5) }, }, TurnIntoH6: { shortcuts: 'Mod-Alt-6', command: (ctx) => { const commands = ctx.get(commandsCtx) return () => commands.call(wrapInHeadingCommand.key, 6) }, }, DowngradeHeading: { shortcuts: ['Delete', 'Backspace'], command: (ctx) => { const commands = ctx.get(commandsCtx) return () => commands.call(downgradeHeadingCommand.key) }, }, }) withMeta(headingKeymap.ctx, { displayName: 'KeymapCtx<heading>', group: 'Heading', }) withMeta(headingKeymap.shortcuts, { displayName: 'Keymap<heading>', group: 'Heading', })