1/* 2 * mark.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 { Mark, MarkSpec, MarkType, ResolvedPos, Node as ProsemirrorNode } from 'prosemirror-model'; 17import { EditorState, Selection, Transaction } from 'prosemirror-state'; 18 19import { PandocTokenReader, PandocMarkWriterFn, PandocInlineHTMLReaderFn } from './pandoc'; 20import { mergedTextNodes } from './text'; 21import { findChildrenByMark } from 'prosemirror-utils'; 22import { MarkTransaction } from './transaction'; 23 24export interface PandocMark { 25 readonly name: string; 26 readonly spec: MarkSpec; 27 readonly noInputRules?: boolean; 28 readonly noSpelling?: boolean; 29 readonly pandoc: { 30 readonly readers: readonly PandocTokenReader[]; 31 readonly inlineHTMLReader?: PandocInlineHTMLReaderFn; 32 readonly writer: { 33 priority: number; 34 write: PandocMarkWriterFn; 35 }; 36 }; 37} 38 39export function markIsActive(context: EditorState | Transaction, type: MarkType) { 40 const { from, $from, to, empty } = context.selection; 41 42 if (empty) { 43 return type && !!type.isInSet(context.storedMarks || $from.marks()); 44 } 45 46 return !!context.doc.rangeHasMark(from, to, type); 47} 48 49export function getMarkAttrs(doc: ProsemirrorNode, range: { from: number; to: number }, type: MarkType) { 50 const { from, to } = range; 51 let marks: Mark[] = []; 52 53 doc.nodesBetween(from, to, node => { 54 marks = [...marks, ...node.marks]; 55 }); 56 57 const mark = marks.find(markItem => markItem.type.name === type.name); 58 59 if (mark) { 60 return mark.attrs; 61 } 62 63 return {}; 64} 65 66export function getMarkRange($pos?: ResolvedPos, type?: MarkType) { 67 if (!$pos || !type) { 68 return false; 69 } 70 71 const start = $pos.parent.childAfter($pos.parentOffset); 72 73 if (!start.node) { 74 return false; 75 } 76 77 const link = start.node.marks.find((mark: Mark) => mark.type === type); 78 if (!link) { 79 return false; 80 } 81 82 let startIndex = $pos.index(); 83 let startPos = $pos.start() + start.offset; 84 let endIndex = startIndex + 1; 85 let endPos = startPos + start.node.nodeSize; 86 87 while (startIndex > 0 && link.isInSet($pos.parent.child(startIndex - 1).marks)) { 88 startIndex -= 1; 89 startPos -= $pos.parent.child(startIndex).nodeSize; 90 } 91 92 while (endIndex < $pos.parent.childCount && link.isInSet($pos.parent.child(endIndex).marks)) { 93 endPos += $pos.parent.child(endIndex).nodeSize; 94 endIndex += 1; 95 } 96 97 return { from: startPos, to: endPos }; 98} 99 100export function getSelectionMarkRange(selection: Selection, markType: MarkType): { from: number; to: number } { 101 let range: { from: number; to: number }; 102 if (selection.empty) { 103 range = getMarkRange(selection.$head, markType) as { from: number; to: number }; 104 } else { 105 range = { from: selection.from, to: selection.to }; 106 } 107 return range; 108} 109 110export function removeInvalidatedMarks( 111 tr: MarkTransaction, 112 node: ProsemirrorNode, 113 pos: number, 114 re: RegExp, 115 markType: MarkType, 116) { 117 re.lastIndex = 0; 118 const markedNodes = findChildrenByMark(node, markType, true); 119 markedNodes.forEach(markedNode => { 120 const from = pos + 1 + markedNode.pos; 121 const markedRange = getMarkRange(tr.doc.resolve(from), markType); 122 if (markedRange) { 123 const text = tr.doc.textBetween(markedRange.from, markedRange.to); 124 if (!text.match(re)) { 125 tr.removeMark(markedRange.from, markedRange.to, markType); 126 tr.removeStoredMark(markType); 127 } 128 } 129 }); 130 re.lastIndex = 0; 131} 132 133export function splitInvalidatedMarks( 134 tr: MarkTransaction, 135 node: ProsemirrorNode, 136 pos: number, 137 validLength: (text: string) => number, 138 markType: MarkType, 139 removeMark?: (from: number, to: number) => void, 140) { 141 const hasMarkType = (nd: ProsemirrorNode) => markType.isInSet(nd.marks); 142 const markedNodes = findChildrenByMark(node, markType, true); 143 markedNodes.forEach(markedNode => { 144 const mark = hasMarkType(markedNode.node); 145 if (mark) { 146 const from = pos + 1 + markedNode.pos; 147 const markRange = getMarkRange(tr.doc.resolve(from), markType); 148 if (markRange) { 149 const text = tr.doc.textBetween(markRange.from, markRange.to); 150 const length = validLength(text); 151 if (length > -1 && length !== text.length) { 152 if (removeMark) { 153 removeMark(markRange.from + length, markRange.to); 154 } else { 155 tr.removeMark(markRange.from + length, markRange.to, markType); 156 } 157 } 158 } 159 } 160 }); 161} 162 163export function detectAndApplyMarks( 164 tr: MarkTransaction, 165 node: ProsemirrorNode, 166 pos: number, 167 re: RegExp, 168 markType: MarkType, 169 attrs: (match: RegExpMatchArray) => {}, 170 filter?: (from: number, to: number) => boolean, 171 text?: (match: RegExpMatchArray) => string, 172) { 173 re.lastIndex = 0; 174 const textNodes = mergedTextNodes(node, (_node: ProsemirrorNode, _pos: number, parentNode: ProsemirrorNode) => 175 parentNode.type.allowsMarkType(markType), 176 ); 177 textNodes.forEach(textNode => { 178 re.lastIndex = 0; 179 let match = re.exec(textNode.text); 180 while (match !== null) { 181 const refText = text ? text(match) : match[0]; 182 const from = pos + 1 + textNode.pos + match.index + (match[0].length - refText.length); 183 const to = from + refText.length; 184 const range = getMarkRange(tr.doc.resolve(to), markType); 185 if ( 186 (!range || range.from !== from || range.to !== to) && 187 !tr.doc.rangeHasMark(from, to, markType.schema.marks.code) 188 ) { 189 if (!filter || filter(from, to)) { 190 const mark = markType.create(attrs instanceof Function ? attrs(match) : attrs); 191 tr.addMark(from, to, mark); 192 if (tr.selection.anchor === to) { 193 tr.removeStoredMark(mark.type); 194 } 195 } 196 } 197 match = re.lastIndex !== 0 ? re.exec(textNode.text) : null; 198 } 199 }); 200 re.lastIndex = 0; 201} 202