diff --git a/src/rich-text/commands/index.ts b/src/rich-text/commands/index.ts index 8dcab672..f5de8b39 100644 --- a/src/rich-text/commands/index.ts +++ b/src/rich-text/commands/index.ts @@ -1,7 +1,12 @@ import { setBlockType, toggleMark, wrapIn } from "prosemirror-commands"; import { redo, undo } from "prosemirror-history"; import { Mark, MarkType, NodeType, Schema } from "prosemirror-model"; -import { EditorState, TextSelection, Transaction } from "prosemirror-state"; +import { + Command, + EditorState, + TextSelection, + Transaction, +} from "prosemirror-state"; import { liftTarget } from "prosemirror-transform"; import { EditorView } from "prosemirror-view"; import { @@ -35,8 +40,20 @@ import { _t } from "../../shared/localization"; export * from "./tables"; -//TODO -function toggleWrapIn(nodeType: NodeType) { +/** + * Builds a command which wraps/unwraps the current selection with the passed in node type + * @param nodeType the type of node to wrap the selection in + * @returns A command to toggle the wrapper node + * Commands are functions that take a state and an optional + * transaction dispatch function and... + * + * - determine whether they apply to this state + * - if not, return false + * - if `dispatch` was passed, perform their effect, possibly by + * passing a transaction to `dispatch` + * - return true + */ +export function toggleWrapIn(nodeType: NodeType): Command { const nodeCheck = nodeTypeActive(nodeType); const wrapInCommand = wrapIn(nodeType); diff --git a/src/rich-text/schema.ts b/src/rich-text/schema.ts index 635c53a4..7a8d2409 100644 --- a/src/rich-text/schema.ts +++ b/src/rich-text/schema.ts @@ -169,7 +169,7 @@ const nodes: { }, list_item: { - content: "paragraph block*", + content: "block+", defining: true, parseDOM: [{ tag: "li" }], toDOM() { diff --git a/test/matchers.ts b/test/matchers.ts index 3c692ba3..1f7e4ddf 100644 --- a/test/matchers.ts +++ b/test/matchers.ts @@ -133,33 +133,23 @@ function expectNodeTree(doc: ProsemirrorNode, tree: CompareTree): void { * @returns a CompareTree to be used with `toMatchNodeTree` */ export function createBasicNodeTree(input: string): CompareTree { - const branches = input - .split(">") - .map((x) => x.trim()) - .reverse(); + const branches = input.split(">").map((x) => x.trim()); + if (!branches.length) return {}; - let branch = branches.pop(); const root: CompareTree = { - "type.name": branch, + "type.name": "doc", }; - - branch = branches.pop(); - let node: CompareTree = root; - while (branch) { - if (isNaN(Number(branch))) { - const child = { - "type.name": branch, - }; - node.content = [child]; - - node = child; - branch = branches.pop(); - } else { - node.childCount = Number(branch); - return root; - } + let tree = root; + + for (const branch of branches) { + const child = { + "type.name": branch, + }; + tree.content = [child]; + tree = child; } + return root; } diff --git a/test/rich-text/commands/index.test.ts b/test/rich-text/commands/index.test.ts index b23828c6..03c99e85 100644 --- a/test/rich-text/commands/index.test.ts +++ b/test/rich-text/commands/index.test.ts @@ -3,6 +3,7 @@ import { exitInclusiveMarkCommand, insertHorizontalRuleCommand, toggleHeadingLevel, + toggleWrapIn, } from "../../../src/rich-text/commands"; import { applySelection, @@ -194,6 +195,70 @@ describe("commands", () => { }); }); + describe("toggleWrapIn", () => { + it("should apply blockquote within paragraph", () => { + const state = applySelection(createState("

quote

", []), 3); + expect(state.doc).toMatchNodeTreeString("paragraph>text"); + + const toggleBlockQuote = toggleWrapIn( + state.schema.nodes.blockquote + ); + const { newState, isValid } = executeTransaction( + state, + toggleBlockQuote + ); + + expect(isValid).toBeTruthy(); + expect(newState.doc).toMatchNodeTreeString( + "blockquote>paragraph>text" + ); + }); + + it("should remove blockquote within blockquote", () => { + const state = applySelection( + createState("
quote
", []), + 3 + ); + expect(state.doc).toMatchNodeTreeString( + "blockquote>paragraph>text" + ); + + const toggleBlockQuote = toggleWrapIn( + state.schema.nodes.blockquote + ); + const { newState, isValid } = executeTransaction( + state, + toggleBlockQuote + ); + + expect(isValid).toBeTruthy(); + expect(newState.doc).toMatchNodeTreeString("paragraph>text"); + }); + + it("should toggle blockquote within list item", () => { + const state = applySelection( + createState("", []), + 3 + ); + const resolvedNode = state.selection.$from; + // default li child is paragraph + expect(resolvedNode.node().type.name).toBe("paragraph"); + + const toggleBlockQuote = toggleWrapIn( + state.schema.nodes.blockquote + ); + const { newState, isValid } = executeTransaction( + state, + toggleBlockQuote + ); + + expect(isValid).toBeTruthy(); + expect(newState.doc).toMatchNodeTreeString( + "bullet_list>list_item>blockquote>paragraph>text" + ); + }); + }); + describe("insertHorizontalRuleCommand", () => { it("should not insert while in a table", () => { const state = applySelection( diff --git a/test/shared/markdown-parser.test.ts b/test/shared/markdown-parser.test.ts index 1da6af14..c5a8c364 100644 --- a/test/shared/markdown-parser.test.ts +++ b/test/shared/markdown-parser.test.ts @@ -312,6 +312,23 @@ console.log("test"); expect(doc.content[0].type).toContain("_list"); expect(doc.content[0].attrs.tight).toBe(isTight); }); + + it.each([ + [`- > blockquote in list`, "bullet_list>list_item>blockquote"], + [`- paragraph in list`, "bullet_list>list_item>paragraph"], + [`- # heading in list`, "bullet_list>list_item>heading"], + [ + `- ~~~\n code in list\n ~~~`, + "bullet_list>list_item>code_block", + ], + [`- - list in list`, "bullet_list>list_item>bullet_list"], + ])( + "should parse lists with direct block children", + (input, expected) => { + const doc = markdownParser.parse(input); + expect(doc).toMatchNodeTreeString(expected); + } + ); }); describe("reference links", () => { diff --git a/test/shared/markdown-serializer.test.ts b/test/shared/markdown-serializer.test.ts index b4ec9bba..ce8232ca 100644 --- a/test/shared/markdown-serializer.test.ts +++ b/test/shared/markdown-serializer.test.ts @@ -211,6 +211,36 @@ describe("markdown-serializer", () => { it.todo("should serialize elements from complex html markup"); + it.each([ + [ + ``, + `- > blockquote in list`, + ], + [``, `- paragraph in list`], + [``, `- # heading in list`], + [ + ``, + `- code in list`, + ], + [ + ``, + `- - list in list`, + ], + ])("should serialize lists containing block children", (input, output) => { + const view = nonMarkdownRichView(input); + expect(view.content).toBe(output); + }); + // TODO lots of complicated cases in the spec // see https://spec.commonmark.org/0.30/#reference-link // see https://spec.commonmark.org/0.30/#link-reference-definitions