import { Fragment, MarkType, Node, NodeType, Schema } from "prosemirror-model"; import { StringOrTBuilder } from "./util/StringOrTBuilder"; /** * Content node or mark builders can consume, e.g. * * const builder = nodeFactory('p'); * builder('string'); * builder(aNode); * builder(aRefsNode); * builder(aRefsTracker); * builder(aNode, aRefsNode, aRefsTracker); * builder(aSymbol); */ export type RawBuilderContent = symbol | string | Node | Fragment | RefsNode | RefsFragment; export type BuilderContent = PosRefBox | string | Node | Fragment | RefsNode | RefsFragment; export type NodeFactory = (...content: RawBuilderContent[]) => RefsNode; export type FragmentFactory = (...content: RawBuilderContent[]) => RefsFragment; /** * Represents a ProseMirror position in a document. */ export type Pos = number; /** * Reference to a position. */ export type PosRef = symbol | string; export type RefMap = ReadonlyMap; export class PosRefBox { constructor(public readonly posRef: PosRef) {} } const emptyRefMap = new Map(); /** * Association between a node and position refs. */ export class RefsNode { constructor(readonly node: Node, readonly refMap: RefMap) {} /** * Append content to the end of the node. */ public append(...content: Array): RefsNode { const { fragment, refMap } = refsFragmentFactory( [this.getContent(), ...digestRawBuilderContent(content)], this.node.type.schema ); return new RefsNode(this.node.type.createChecked(this.node.attrs, fragment, this.node.marks), refMap); } /** * Prepend content to the start of the node. */ public prepend(...content: Array): RefsNode { const { fragment, refMap } = refsFragmentFactory( [...digestRawBuilderContent(content), this.getContent()], this.node.type.schema ); return new RefsNode(this.node.type.createChecked(this.node.attrs, fragment, this.node.marks), refMap); } /** * Access the node's children. */ public getContent() { return new RefsFragment(this.node.content, this.refMap); } public static from(node: Node): RefsNode { return new RefsNode(node, emptyRefMap); } } /** * Association between a fragment and position refs. */ export class RefsFragment { constructor(readonly fragment: Fragment, readonly refMap: RefMap) {} } function* matches(text: string, regexp: RegExp) { let match: RegExpExecArray | null; while ((match = regexp.exec(text)) !== null) { yield match; } } /** * Parse out inline text refs. */ export function parseText(text: string): ReadonlyArray { const builder = new StringOrTBuilder(); let textIndex = 0; for (const match of matches(text, /([\\]+)?{([^}]+)}/g)) { const [matchText, escapeChars, refName] = match; let { index: matchIndex } = match; // tslint:disable-next-line:strict-type-predicates if (typeof escapeChars === "string") { const escapeCharsLength = escapeChars.length; // An odd number of preceeding slashes means the `{` is escaped. const isEscaped = escapeCharsLength % 2 === 1; if (isEscaped) { builder.push(text.slice(textIndex, matchIndex + (escapeCharsLength - 1) / 2)); builder.push(text.slice(matchIndex + escapeCharsLength, matchIndex + matchText.length)); textIndex = matchIndex + matchText.length; continue; } else { matchIndex += escapeCharsLength / 2; } } builder.push(text.slice(textIndex, matchIndex)); builder.push(new PosRefBox(refName)); textIndex = match.index + matchText.length; } builder.push(text.slice(textIndex)); return builder.items; } export function refsFragmentFactory(content: BuilderContent[], schema: Schema): RefsFragment { let position = 0; const refMap = new Map(); const nodes: Node[] = []; for (const item of content) { if (item instanceof PosRefBox) { refMap.set(item.posRef, position); } else if (item instanceof RefsNode) { for (const [ref, pos] of item.refMap.entries()) { refMap.set(ref, position + 1 + pos); } nodes.push(item.node); position += item.node.nodeSize; } else if (item instanceof RefsFragment) { for (const [ref, pos] of item.refMap.entries()) { refMap.set(ref, position + pos); } item.fragment.forEach(node => { nodes.push(node); position += node.nodeSize; }); } else if (item instanceof Node) { nodes.push(item); position += item.nodeSize; } else if (item instanceof Fragment) { item.forEach(node => { nodes.push(node); position += node.nodeSize; }); } else { if (item.length > 0) { const node = schema.text(item); nodes.push(node); position += node.nodeSize; } } } return new RefsFragment(Fragment.fromArray(nodes), refMap); } /** * Given raw content, extract symbols and inline text refs out of the node flow. */ function digestRawBuilderContent(rawContents: RawBuilderContent[]): BuilderContent[] { return rawContents.reduce( (prev, curr) => typeof curr === "symbol" ? [...prev, new PosRefBox(curr)] : typeof curr === "string" ? [...prev, ...parseText(curr)] : [...prev, curr], [] as BuilderContent[] ); } /** * Create a factory for nodes. */ export function nodeFactoryFactory(type: NodeType, attrs = {}): NodeFactory { return (...content) => { const { fragment, refMap } = refsFragmentFactory(digestRawBuilderContent(content), type.schema); return new RefsNode(type.createChecked(attrs, fragment), refMap); }; } /** * Create a factory for marks. */ export function markFactoryFactory(type: MarkType, attrs = {}): FragmentFactory { const mark = type.create(attrs); return (...content) => { const { fragment, refMap } = refsFragmentFactory(digestRawBuilderContent(content), type.schema); const nodes: Node[] = []; fragment.forEach(node => { nodes.push(mark.type.isInSet(node.marks) != null ? node : node.mark(mark.addToSet(node.marks))); }); return new RefsFragment(Fragment.fromArray(nodes), refMap); }; } /** * Create a factory for fragments. */ export function fragmentFactoryFactory(schema: Schema): FragmentFactory { return (...content) => { const { fragment, refMap } = refsFragmentFactory(digestRawBuilderContent(content), schema); return new RefsFragment(fragment, refMap); }; }