UNPKG

@milkdown/preset-commonmark

Version:

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

245 lines (220 loc) 5.96 kB
import type { Ctx } from '@milkdown/ctx' import { commandsCtx } from '@milkdown/core' import { expectDomTypeError } from '@milkdown/exception' import { joinBackward } from '@milkdown/prose/commands' import { liftListItem, sinkListItem, splitListItem, } from '@milkdown/prose/schema-list' import { type Command, TextSelection } from '@milkdown/prose/state' import { $command, $nodeAttr, $nodeSchema, $useKeymap } from '@milkdown/utils' import { withMeta } from '../__internal__' /// HTML attributes for list item node. export const listItemAttr = $nodeAttr('listItem') withMeta(listItemAttr, { displayName: 'Attr<listItem>', group: 'ListItem', }) /// Schema for list item node. export const listItemSchema = $nodeSchema('list_item', (ctx) => ({ group: 'listItem', content: 'paragraph block*', attrs: { label: { default: '•', validate: 'string', }, listType: { default: 'bullet', validate: 'string', }, spread: { default: true, validate: 'boolean', }, }, defining: true, parseDOM: [ { tag: 'li', getAttrs: (dom) => { if (!(dom instanceof HTMLElement)) throw expectDomTypeError(dom) return { label: dom.dataset.label, listType: dom.dataset.listType, spread: dom.dataset.spread === 'true', } }, }, ], toDOM: (node) => [ 'li', { ...ctx.get(listItemAttr.key)(node), 'data-label': node.attrs.label, 'data-list-type': node.attrs.listType, 'data-spread': node.attrs.spread, }, 0, ], parseMarkdown: { match: ({ type }) => type === 'listItem', runner: (state, node, type) => { const label = node.label != null ? `${node.label}.` : '•' const listType = node.label != null ? 'ordered' : 'bullet' const spread = node.spread != null ? `${node.spread}` : 'true' state.openNode(type, { label, listType, spread }) state.next(node.children) state.closeNode() }, }, toMarkdown: { match: (node) => node.type.name === 'list_item', runner: (state, node) => { state.openNode('listItem', undefined, { spread: node.attrs.spread, }) state.next(node.content) state.closeNode() }, }, })) withMeta(listItemSchema.node, { displayName: 'NodeSchema<listItem>', group: 'ListItem', }) withMeta(listItemSchema.ctx, { displayName: 'NodeSchemaCtx<listItem>', group: 'ListItem', }) /// The command to sink list item. /// /// For example: /// ```md /// * List item 1 /// * List item 2 <- cursor here /// ``` /// Will get: /// ```md /// * List item 1 /// * List item 2 /// ``` export const sinkListItemCommand = $command( 'SinkListItem', (ctx) => () => sinkListItem(listItemSchema.type(ctx)) ) withMeta(sinkListItemCommand, { displayName: 'Command<sinkListItemCommand>', group: 'ListItem', }) /// The command to lift list item. /// /// For example: /// ```md /// * List item 1 /// * List item 2 <- cursor here /// ``` /// Will get: /// ```md /// * List item 1 /// * List item 2 /// ``` export const liftListItemCommand = $command( 'LiftListItem', (ctx) => () => liftListItem(listItemSchema.type(ctx)) ) withMeta(liftListItemCommand, { displayName: 'Command<liftListItemCommand>', group: 'ListItem', }) /// The command to split a list item. /// /// For example: /// ```md /// * List item 1 /// * List item 2 <- cursor here /// ``` /// Will get: /// ```md /// * List item 1 /// * List item 2 /// * <- cursor here /// ``` export const splitListItemCommand = $command( 'SplitListItem', (ctx) => () => splitListItem(listItemSchema.type(ctx)) ) withMeta(splitListItemCommand, { displayName: 'Command<splitListItemCommand>', group: 'ListItem', }) function liftFirstListItem(ctx: Ctx): Command { return (state, dispatch, view) => { const { selection } = state if (!(selection instanceof TextSelection)) return false const { empty, $from } = selection // selection should be empty and at the start of the node if (!empty || $from.parentOffset !== 0) return false const parentItem = $from.node(-1) // selection should be in list item if (parentItem.type !== listItemSchema.type(ctx)) return false return joinBackward(state, dispatch, view) } } /// The command to remove list item **only if**: /// /// - Selection is at the start of the list item. /// - List item is the only child of the list. /// /// Most of the time, you shouldn't use this command directly. export const liftFirstListItemCommand = $command( 'LiftFirstListItem', (ctx) => () => liftFirstListItem(ctx) ) withMeta(liftFirstListItemCommand, { displayName: 'Command<liftFirstListItemCommand>', group: 'ListItem', }) /// Keymap for list item node. /// - `<Enter>`: Split the current list item. /// - `<Tab>/<Mod-]>`: Sink the current list item. /// - `<Shift-Tab>/<Mod-[>`: Lift the current list item. export const listItemKeymap = $useKeymap('listItemKeymap', { NextListItem: { shortcuts: 'Enter', command: (ctx) => { const commands = ctx.get(commandsCtx) return () => commands.call(splitListItemCommand.key) }, }, SinkListItem: { shortcuts: ['Tab', 'Mod-]'], command: (ctx) => { const commands = ctx.get(commandsCtx) return () => commands.call(sinkListItemCommand.key) }, }, LiftListItem: { shortcuts: ['Shift-Tab', 'Mod-['], command: (ctx) => { const commands = ctx.get(commandsCtx) return () => commands.call(liftListItemCommand.key) }, }, LiftFirstListItem: { shortcuts: ['Backspace', 'Delete'], command: (ctx) => { const commands = ctx.get(commandsCtx) return () => commands.call(liftFirstListItemCommand.key) }, }, }) withMeta(listItemKeymap.ctx, { displayName: 'KeymapCtx<listItem>', group: 'ListItem', }) withMeta(listItemKeymap.shortcuts, { displayName: 'Keymap<listItem>', group: 'ListItem', })