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