import { Mark, Node, Slice } from "prosemirror-model"; import { AllSelection, EditorState, NodeSelection, Plugin, Selection, TextSelection } from "prosemirror-state"; import { compose } from "../compose"; import { fragmentFactoryFactory, nodeFactoryFactory, PosRef, RawBuilderContent, RefMap, RefsFragment, RefsNode } from "../composing"; import { schema } from "../schema"; import { CellSelection } from "../table/CellSelection"; import { cellAround } from "../table/util"; import { DocPos } from "../types"; import { WedgeSelection } from "../wedge/WedgeSelection"; export type DocRefMap = ReadonlyMap; export type DovetailNodeFactory = (api: Readonly) => RefsNode; export type DovetailFragmentFactory = ( api: Readonly ) => RawBuilderContent | Array; export type DovetailMarksFactory = (api: Readonly) => Mark[]; export type AllSelectionBuilder = () => AllSelection; export type TextSelectionBuilder = (anchor: number, head?: number) => TextSelection; export type NodeSelectionBuilder = (from: number) => NodeSelection; export type CellSelectionBuilder = (anchor: number, head?: number) => CellSelection; export type WedgeSelectionBuilder = (pos: number) => WedgeSelection; export type SelectionBuilder = ( api: { all: AllSelectionBuilder; text: TextSelectionBuilder; node: NodeSelectionBuilder; cell: CellSelectionBuilder; wedge: WedgeSelectionBuilder; refs: RefMap; } ) => Selection; export type PluginsBuilder = ( api: { refs: DocRefMap; doc: Node; } ) => Plugin[]; export type EditorStateFactory = ( fragmentFactory: DovetailFragmentFactory, options?: { storedMarks?: DovetailMarksFactory; selection?: SelectionBuilder; plugins?: PluginsBuilder } ) => EditorState; const thA = (attrs: { rowspan?: number; colspan?: number; colwidth?: number[] | null }) => nodeFactoryFactory(schema.nodes.th, attrs); const th22 = nodeFactoryFactory(schema.nodes.th, { rowspan: 2, colspan: 2 }); const td22 = nodeFactoryFactory(schema.nodes.td, { rowspan: 2, colspan: 2 }); const tdA = (attrs: { rowspan?: number; colspan?: number; colwidth?: number[] | null }) => nodeFactoryFactory(schema.nodes.td, attrs); const c = (colspan: number, rowspan: number) => tdA({ colspan, rowspan })(compose.p("x")); const h = (colspan: number, rowspan: number) => thA({ colspan, rowspan })(compose.p("x")); const fragment = fragmentFactoryFactory(schema); const dovetailNodeFactories = { ...compose, table: compose.tbl, th_: thA, th22, td22, td_: tdA, c11: c(1, 1), c, cEmpty: compose.td(compose.p()), cEmptyAnchor: compose.td(compose.p("{[^]}")), cEmptyHead: compose.td(compose.p("{[$]}")), cEmptyCursor: compose.td(compose.p("{^}")), cCursor: compose.td(compose.p("x{^}")), cAnchor: compose.td(compose.p("x{[^]}")), cHead: compose.td(compose.p("x{[$]}")), h, h11: h(1, 1), hEmpty: compose.th(compose.p()), hCursor: compose.th(compose.p("x{^}")) }; const dovetailMarkBuilders = { b: () => schema.marks.b.create(), i: () => schema.marks.i.create(), s: () => schema.marks.s.create(), u: () => schema.marks.u.create() }; export function buildNode(factory: DovetailNodeFactory) { return factory(dovetailNodeFactories); } export function buildMarks(builder: DovetailMarksFactory) { return builder(dovetailMarkBuilders); } export function buildSlice(builder: DovetailFragmentFactory, openStart = 0, openEnd = 0) { const rawBuilderContent = builder(dovetailNodeFactories); const refsFragment = Array.isArray(rawBuilderContent) ? fragment(...rawBuilderContent) : fragment(rawBuilderContent); return new Slice(refsFragment.fragment, openStart, openEnd); } export interface BuildEditorStateOptions { storedMarks?: DovetailMarksFactory; selection?: SelectionBuilder; plugins?: PluginsBuilder; } export function buildEditorState(docFactory: DovetailNodeFactory, options: BuildEditorStateOptions = {}): EditorState { const { storedMarks: storedMarksBuilder, selection: selectionBuilder, plugins: pluginsBuilder } = options; const { node: doc, refMap } = buildNode(docFactory); const editorState = EditorState.create({ doc, plugins: pluginsBuilder !== undefined ? pluginsBuilder({ refs: refMap as DocRefMap, doc }) : [], selection: selectionBuilder !== undefined ? selectionBuilder({ all: () => new AllSelection(doc), cell: (anchor, head) => CellSelection.create(doc, anchor, head), node: from => NodeSelection.create(doc, from), text: (anchor, head) => TextSelection.create(doc, anchor, head), wedge: pos => new WedgeSelection(doc.resolve(pos)), refs: refMap }) : applySelectionRefs(doc, refMap) }); // https://github.com/ProseMirror/prosemirror/issues/775 editorState.storedMarks = storedMarksBuilder !== undefined ? buildMarks(storedMarksBuilder) : null; return editorState; } export function editorStateFactoryFactory(docFactory: (children: RefsFragment) => DovetailNodeFactory): EditorStateFactory { return function editorStateFactory(rawChildrenFactory, options = {}) { const { storedMarks: storedMarksBuilder, selection: selectionBuilder, plugins: pluginsBuilder } = options; const rawContent = rawChildrenFactory(dovetailNodeFactories); const children = Array.isArray(rawContent) ? fragment(...rawContent) : fragment(rawContent); const { node: doc, refMap } = docFactory(children)(dovetailNodeFactories); const editorState = EditorState.create({ doc, plugins: pluginsBuilder !== undefined ? pluginsBuilder({ refs: refMap as DocRefMap, doc }) : [], selection: selectionBuilder !== undefined ? selectionBuilder({ all: () => new AllSelection(doc), cell: (anchor, head) => CellSelection.create(doc, anchor, head), node: from => NodeSelection.create(doc, from), text: (anchor, head) => TextSelection.create(doc, anchor, head), wedge: pos => new WedgeSelection(doc.resolve(pos)), refs: refMap }) : applySelectionRefs(doc, refMap) }); // https://github.com/ProseMirror/prosemirror/issues/775 editorState.storedMarks = storedMarksBuilder !== undefined ? buildMarks(storedMarksBuilder) : null; return editorState; }; } export function describeRootsFactory( factories: ReadonlyArray<[string, EditorStateFactory]> ): (fn: (editorStateFactory: EditorStateFactory) => void) => void { return fn => { for (const [rootDescription, editorStateFactory] of factories) { describe(`root(${rootDescription})`, () => { fn(editorStateFactory); }); } }; } export function namedDescribeRootsFactory( factories: ReadonlyArray<[string, EditorStateFactory]> ): (name: string, fn: (editorStateFactory: EditorStateFactory) => void) => void { return (name, fn) => { describe(name, () => { for (const [rootDescription, editorStateFactory] of factories) { describe(`root(${rootDescription})`, () => { fn(editorStateFactory); }); } }); }; } function applySelectionRefs(doc: Node, refs: RefMap): Selection | undefined | null { // Text selection const textAnchor = refs.get("^"); if (textAnchor !== undefined) { const textHead = refs.get("$"); return textHead !== undefined ? TextSelection.create(doc, textAnchor, textHead) : TextSelection.create(doc, textAnchor); } // Table cell selection const cellAnchor = refs.get("[^]"); if (cellAnchor !== undefined) { const $cellAnchor = cellAround(doc.resolve(cellAnchor)); if ($cellAnchor === null) { throw new Error("Unable to find anchor cell"); } const cellHead = refs.get("[$]"); if (cellHead !== undefined) { const $cellHead = cellAround(doc.resolve(cellHead)); if ($cellHead === null) { throw new Error("Unable to find head cell"); } return new CellSelection($cellAnchor, $cellHead); } else { return new CellSelection($cellAnchor); } } // Node selection const node = refs.get("node"); if (node !== undefined) { return new NodeSelection(doc.resolve(node)); } // Wedge selection const wedge = refs.get("wedge"); if (wedge !== undefined) { return new WedgeSelection(doc.resolve(wedge)); } return null; }