1/*
2 * node.ts
3 *
4 * Copyright (C) 2021 by RStudio, PBC
5 *
6 * Unless you have received this program directly from RStudio pursuant
7 * to the terms of a commercial license agreement with RStudio, then
8 * this program is licensed to you under the terms of version 3 of the
9 * GNU Affero General Public License. This program is distributed WITHOUT
10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13 *
14 */
15
16import { Node as ProsemirrorNode, NodeSpec, NodeType, ResolvedPos, Slice } from 'prosemirror-model';
17import { EditorState, Selection, NodeSelection } from 'prosemirror-state';
18import {
19  findParentNode,
20  findSelectedNodeOfType,
21  ContentNodeWithPos,
22  NodeWithPos,
23  findParentNodeOfType,
24  findChildrenByType,
25  findChildren,
26  findParentNodeOfTypeClosestToPos,
27} from 'prosemirror-utils';
28
29import { EditorView } from 'prosemirror-view';
30
31import {
32  PandocTokenReader,
33  PandocNodeWriterFn,
34  PandocPreprocessorFn,
35  PandocBlockReaderFn,
36  PandocInlineHTMLReaderFn,
37  PandocTokensFilterFn,
38} from './pandoc';
39import { PandocBlockCapsuleFilter } from './pandoc_capsule';
40
41import { AttrEditOptions } from './attr_edit';
42import { CommandFn } from './command';
43import { ExecuteRmdChunkFn } from './rmd';
44
45export interface PandocNode {
46  readonly name: string;
47  readonly spec: NodeSpec;
48  readonly code_view?: CodeViewOptions;
49  readonly attr_edit?: () => AttrEditOptions | null;
50  readonly pandoc: {
51    readonly readers?: readonly PandocTokenReader[];
52    readonly writer?: PandocNodeWriterFn;
53    readonly preprocessor?: PandocPreprocessorFn;
54    readonly tokensFilter?: PandocTokensFilterFn;
55    readonly blockReader?: PandocBlockReaderFn;
56    readonly inlineHTMLReader?: PandocInlineHTMLReaderFn;
57    readonly blockCapsuleFilter?: PandocBlockCapsuleFilter;
58  };
59}
60
61export interface CodeViewOptions {
62  lang: (node: ProsemirrorNode, content: string) => string | null;
63  attrEditFn?: CommandFn;
64  createFromPastePattern?: RegExp;
65  classes?: string[];
66  borderColorClass?: string;
67  firstLineMeta?: boolean;
68  lineNumbers?: boolean;
69  bookdownTheorems?: boolean;
70  lineNumberFormatter?: (lineNumber: number, lineCount?: number, line?: string) => string;
71}
72
73export type NodeTraversalFn = (
74  node: Node,
75  pos: number,
76  parent: Node,
77  index: number,
78) => boolean | void | null | undefined;
79
80export function findTopLevelBodyNodes(doc: ProsemirrorNode, predicate: (node: ProsemirrorNode) => boolean) {
81  const body = findChildrenByType(doc, doc.type.schema.nodes.body, false)[0];
82  const offset = body.pos + 1;
83  const nodes = findChildren(body.node, predicate, false);
84  return nodes.map(value => ({ ...value, pos: value.pos + offset }));
85}
86
87export function findNodeOfTypeInSelection(selection: Selection, type: NodeType): ContentNodeWithPos | undefined {
88  return findSelectedNodeOfType(type)(selection) || findParentNode((n: ProsemirrorNode) => n.type === type)(selection);
89}
90
91export function firstNode(parent: NodeWithPos, predicate: (node: ProsemirrorNode) => boolean) {
92  let foundNode: NodeWithPos | undefined;
93  parent.node.descendants((node, pos) => {
94    if (!foundNode) {
95      if (predicate(node)) {
96        foundNode = {
97          node,
98          pos: parent.pos + 1 + pos,
99        };
100        return false;
101      }
102    } else {
103      return false;
104    }
105  });
106  return foundNode;
107}
108
109export function lastNode(parent: NodeWithPos, predicate: (node: ProsemirrorNode) => boolean) {
110  let last: NodeWithPos | undefined;
111  parent.node.descendants((node, pos) => {
112    if (predicate(node)) {
113      last = {
114        node,
115        pos: parent.pos + 1 + pos,
116      };
117    }
118  });
119  return last;
120}
121
122export function nodeIsActive(state: EditorState, type: NodeType, attrs = {}) {
123  const predicate = (n: ProsemirrorNode) => n.type === type;
124  const node = findSelectedNodeOfType(type)(state.selection) || findParentNode(predicate)(state.selection);
125
126  if (!Object.keys(attrs).length || !node) {
127    return !!node;
128  }
129
130  return node.node.hasMarkup(type, attrs);
131}
132
133export function canInsertNode(context: EditorState | Selection, nodeType: NodeType) {
134  const selection = asSelection(context);
135  const $from = selection.$from;
136  return canInsertNodeAtPos($from, nodeType);
137}
138
139export function canInsertNodeAtPos($pos: ResolvedPos, nodeType: NodeType) {
140  for (let d = $pos.depth; d >= 0; d--) {
141    const index = $pos.index(d);
142    if ($pos.node(d).canReplaceWith(index, index, nodeType)) {
143      return true;
144    }
145  }
146  return false;
147}
148
149export function canInsertTextNode(context: EditorState | Selection) {
150  const selection = asSelection(context);
151  return canInsertNode(selection, selection.$head.parent.type.schema.nodes.text);
152}
153
154export function insertAndSelectNode(view: EditorView, node: ProsemirrorNode) {
155  // create new transaction
156  const tr = view.state.tr;
157
158  // insert the node over the existing selection
159  tr.ensureMarks(node.marks);
160  tr.replaceSelectionWith(node);
161
162  // set selection to inserted node (or don't if our selection calculate was off,
163  // as can happen when we insert into a list bullet)
164  const selectionPos = tr.doc.resolve(tr.mapping.map(view.state.selection.from, -1));
165  const selectionNode = tr.doc.nodeAt(selectionPos.pos);
166  if (selectionNode && selectionNode.type === node.type) {
167    tr.setSelection(new NodeSelection(selectionPos));
168  }
169
170  // dispatch transaction
171  view.dispatch(tr);
172}
173
174export function editingRootNode(selection: Selection) {
175  const schema = selection.$head.node().type.schema;
176  return findParentNodeOfType(schema.nodes.body)(selection) || findParentNodeOfType(schema.nodes.note)(selection);
177}
178
179export function editingRootNodeClosestToPos($pos: ResolvedPos) {
180  const schema = $pos.node().type.schema;
181  return (
182    findParentNodeOfTypeClosestToPos($pos, schema.nodes.body) ||
183    findParentNodeOfTypeClosestToPos($pos, schema.nodes.note)
184  );
185}
186
187export function editingRootScrollContainerElement(view: EditorView) {
188  const editingNode = editingRootNode(view.state.selection);
189  if (editingNode) {
190    const editingEl = view.domAtPos(editingNode.pos + 1).node;
191    return editingEl.parentElement;
192  } else {
193    return undefined;
194  }
195}
196
197function asSelection(context: EditorState | Selection) {
198  return context instanceof EditorState ? context.selection : context;
199}
200