1/* 2 * editor-extensions.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 { InputRule } from 'prosemirror-inputrules'; 17import { Schema } from 'prosemirror-model'; 18import { Plugin } from 'prosemirror-state'; 19import { EditorView } from 'prosemirror-view'; 20import { ProsemirrorCommand } from '../api/command'; 21import { PandocMark } from '../api/mark'; 22import { PandocNode, CodeViewOptions } from '../api/node'; 23import { Extension, ExtensionFn, ExtensionContext } from '../api/extension'; 24import { BaseKeyBinding } from '../api/basekeys'; 25import { OmniInserter } from '../api/omni_insert'; 26import { AppendTransactionHandler, AppendMarkTransactionHandler } from '../api/transaction'; 27import { FixupFn } from '../api/fixup'; 28import { 29 PandocTokenReader, 30 PandocMarkWriter, 31 PandocNodeWriter, 32 PandocPreprocessorFn, 33 PandocPostprocessorFn, 34 PandocBlockReaderFn, 35 PandocInlineHTMLReaderFn, 36 PandocTokensFilterFn, 37} from '../api/pandoc'; 38import { PandocBlockCapsuleFilter } from '../api/pandoc_capsule'; 39import { markInputRuleFilter } from '../api/input_rule'; 40import { CompletionHandler } from '../api/completion'; 41 42// required extensions (base non-customiziable pandoc nodes/marks + core behaviors) 43import nodeText from '../nodes/text'; 44import nodeParagraph from '../nodes/paragraph'; 45import nodeHeading from '../nodes/heading'; 46import nodeBlockquote from '../nodes/blockquote'; 47import nodeCodeBlock from '../nodes/code_block'; 48import nodeLists from '../nodes/list/list'; 49import nodeImage from '../nodes/image/image'; 50import nodeFigure from '../nodes/image/figure'; 51import nodeHr from '../nodes/hr'; 52import nodeHardBreak from '../nodes/hard_break'; 53import nodeNull from '../nodes/null'; 54import markEm from '../marks/em'; 55import markStrong from '../marks/strong'; 56import markCode from '../marks/code'; 57import markLink from '../marks/link/link'; 58import behaviorHistory from '../behaviors/history'; 59import behaviorSelectAll from '../behaviors/select_all'; 60import behaviorCursor from '../behaviors/cursor'; 61import behaviorFind from '../behaviors/find'; 62import behaviorSpellingInteractive from '../behaviors/spelling/spelling-interactive'; 63import behaviorClearFormatting from '../behaviors/clear_formatting'; 64 65// behaviors 66import behaviorSmarty from '../behaviors/smarty'; 67import behaviorAttrDuplicateId from '../behaviors/attr_duplicate_id'; 68import behaviorTrailingP from '../behaviors/trailing_p'; 69import behaviorEmptyMark from '../behaviors/empty_mark'; 70import behaviorEscapeMark from '../behaviors/escape_mark'; 71import behaviorOutline from '../behaviors/outline'; 72import beahviorCodeBlockInput from '../behaviors/code_block_input'; 73import behaviorPasteText from '../behaviors/paste_text'; 74import behaviorBottomPadding from '../behaviors/bottom_padding'; 75import behaviorInsertSymbol from '../behaviors/insert_symbol/insert_symbol-plugin-symbol'; 76import behaviorInsertSymbolEmoji from '../behaviors/insert_symbol/insert_symbol-plugin-emoji'; 77import beahviorInsertSpecialCharacters from '../behaviors/insert_symbol/insert_special_characters'; 78import behaviorNbsp from '../behaviors/nbsp'; 79import behaviorRemoveSection from '../behaviors/remove_section'; 80 81// marks 82import markStrikeout from '../marks/strikeout'; 83import markSuperscript from '../marks/superscript'; 84import markSubscript from '../marks/subscript'; 85import markSmallcaps from '../marks/smallcaps'; 86import markUnderline from '../marks/underline'; 87import markRawInline from '../marks/raw_inline/raw_inline'; 88import markRawTex from '../marks/raw_inline/raw_tex'; 89import markRawHTML from '../marks/raw_inline/raw_html'; 90import markMath from '../marks/math/math'; 91import markCite from '../marks/cite/cite'; 92import markSpan from '../marks/span'; 93import markXRef from '../marks/xref/xref'; 94import markHTMLComment from '../marks/raw_inline/raw_html_comment'; 95import markShortcode from '../marks/shortcode'; 96import markEmoji from '../marks/emoji/emoji'; 97import { markOmniInsert } from '../behaviors/omni_insert/omni_insert'; 98 99// nodes 100import nodeFootnote from '../nodes/footnote/footnote'; 101import nodeRawBlock from '../nodes/raw_block'; 102import nodeYamlMetadata from '../nodes/yaml_metadata/yaml_metadata'; 103import nodeRmdCodeChunk from '../nodes/rmd_chunk/rmd_chunk'; 104import nodeDiv from '../nodes/div'; 105import nodeLineBlock from '../nodes/line_block'; 106import nodeTable from '../nodes/table/table'; 107import nodeDefinitionList from '../nodes/definition_list/definition_list'; 108import nodeShortcodeBlock from '../nodes/shortcode_block'; 109import nodeHtmlPreserve from '../nodes/html_preserve'; 110 111// extension/plugin factories 112import { acePlugins } from '../optional/ace/ace'; 113import { attrEditExtension } from '../behaviors/attr_edit/attr_edit'; 114import { codeViewClipboardPlugin } from '../api/code'; 115 116export function initExtensions(context: ExtensionContext, extensions?: readonly Extension[]): ExtensionManager { 117 // create extension manager 118 const manager = new ExtensionManager(context); 119 120 // required extensions 121 manager.register([ 122 nodeText, 123 nodeParagraph, 124 nodeHeading, 125 nodeBlockquote, 126 nodeLists, 127 nodeCodeBlock, 128 nodeImage, 129 nodeFigure, 130 nodeHr, 131 nodeHardBreak, 132 nodeNull, 133 markEm, 134 markStrong, 135 markCode, 136 markLink, 137 behaviorHistory, 138 behaviorSelectAll, 139 behaviorCursor, 140 behaviorFind, 141 behaviorSpellingInteractive, 142 behaviorClearFormatting, 143 ]); 144 145 // optional extensions 146 manager.register([ 147 // behaviors 148 behaviorSmarty, 149 behaviorAttrDuplicateId, 150 behaviorTrailingP, 151 behaviorEmptyMark, 152 behaviorEscapeMark, 153 behaviorOutline, 154 beahviorCodeBlockInput, 155 behaviorPasteText, 156 behaviorBottomPadding, 157 behaviorInsertSymbol, 158 behaviorInsertSymbolEmoji, 159 beahviorInsertSpecialCharacters, 160 behaviorNbsp, 161 behaviorRemoveSection, 162 163 // nodes 164 nodeDiv, 165 nodeFootnote, 166 nodeYamlMetadata, 167 nodeRmdCodeChunk, 168 nodeTable, 169 nodeDefinitionList, 170 nodeLineBlock, 171 nodeRawBlock, 172 nodeShortcodeBlock, 173 nodeHtmlPreserve, 174 175 // marks 176 markUnderline, 177 markStrikeout, 178 markSuperscript, 179 markSubscript, 180 markSmallcaps, 181 markHTMLComment, 182 markRawTex, 183 markRawHTML, 184 markRawInline, 185 markMath, 186 markCite, 187 markSpan, 188 markXRef, 189 markShortcode, 190 markEmoji, 191 markOmniInsert, 192 ]); 193 194 // register external extensions 195 if (extensions) { 196 manager.register(extensions); 197 } 198 199 // additional extensions dervied from other extensions (e.g. extensions that have registered attr editors) 200 // note that all of these take a callback to access the manager -- this is so that if an extension earlier 201 // in the chain registers something the later extensions are able to see it 202 manager.register([ 203 // bindings to 'Edit Attribute' command and UI adornment 204 attrEditExtension(context.pandocExtensions, context.ui, manager.attrEditors()), 205 ]); 206 207 // additional plugins derived from extensions 208 const codeViews = manager.codeViews(); 209 const plugins: Plugin[] = []; 210 if (context.options.codeEditor === 'ace') { 211 plugins.push(...acePlugins(codeViews, context)); 212 } 213 plugins.push(codeViewClipboardPlugin(codeViews)); 214 215 // register plugins 216 manager.registerPlugins(plugins); 217 218 // return manager 219 return manager; 220} 221 222export class ExtensionManager { 223 private context: ExtensionContext; 224 private extensions: Extension[]; 225 226 public constructor(context: ExtensionContext) { 227 this.context = context; 228 this.extensions = []; 229 } 230 231 public register(extensions: ReadonlyArray<Extension | ExtensionFn>, priority = false): void { 232 extensions.forEach(extension => { 233 if (typeof extension === 'function') { 234 const ext = extension(this.context); 235 if (ext) { 236 if (priority) { 237 this.extensions.unshift(ext); 238 } else { 239 this.extensions.push(ext); 240 } 241 } 242 } else { 243 if (priority) { 244 this.extensions.unshift(extension); 245 } else { 246 this.extensions.push(extension); 247 } 248 } 249 }); 250 } 251 252 public registerPlugins(plugins: Plugin[], priority = false) { 253 this.register([{ plugins: () => plugins }], priority); 254 } 255 256 public pandocMarks(): readonly PandocMark[] { 257 return this.collect(extension => extension.marks); 258 } 259 260 public pandocNodes(): readonly PandocNode[] { 261 return this.collect(extension => extension.nodes); 262 } 263 264 public pandocPreprocessors(): readonly PandocPreprocessorFn[] { 265 return this.collectFrom({ 266 node: node => [node.pandoc.preprocessor], 267 }); 268 } 269 270 public pandocPostprocessors(): readonly PandocPostprocessorFn[] { 271 return this.pandocReaders().flatMap(reader => (reader.postprocessor ? [reader.postprocessor] : [])); 272 } 273 274 public pandocTokensFilters(): readonly PandocTokensFilterFn[] { 275 return this.collectFrom({ 276 node: node => [node.pandoc.tokensFilter], 277 }); 278 } 279 280 public pandocBlockReaders(): readonly PandocBlockReaderFn[] { 281 return this.collectFrom({ 282 node: node => [node.pandoc.blockReader], 283 }); 284 } 285 286 public pandocInlineHTMLReaders(): readonly PandocInlineHTMLReaderFn[] { 287 return this.collectFrom({ 288 mark: mark => [mark.pandoc.inlineHTMLReader], 289 node: node => [node.pandoc.inlineHTMLReader], 290 }); 291 } 292 293 public pandocBlockCapsuleFilters(): readonly PandocBlockCapsuleFilter[] { 294 return this.collectFrom({ 295 node: node => [node.pandoc.blockCapsuleFilter], 296 }); 297 } 298 299 public pandocReaders(): readonly PandocTokenReader[] { 300 return this.collectFrom({ 301 mark: mark => mark.pandoc.readers, 302 node: node => node.pandoc.readers ?? [], 303 }); 304 } 305 306 public pandocMarkWriters(): readonly PandocMarkWriter[] { 307 return this.collectFrom({ 308 mark: mark => [{ name: mark.name, ...mark.pandoc.writer }], 309 }); 310 } 311 312 public pandocNodeWriters(): readonly PandocNodeWriter[] { 313 return this.collectFrom({ 314 node: node => { 315 return node.pandoc.writer ? [{ name: node.name, write: node.pandoc.writer! }] : []; 316 }, 317 }); 318 } 319 320 public commands(schema: Schema): readonly ProsemirrorCommand[] { 321 return this.collect<ProsemirrorCommand>(extension => extension.commands?.(schema)); 322 } 323 324 public omniInserters(schema: Schema): OmniInserter[] { 325 const omniInserters: OmniInserter[] = []; 326 const commands = this.commands(schema); 327 commands.forEach(command => { 328 if (command.omniInsert) { 329 omniInserters.push({ 330 ...command.omniInsert, 331 id: command.id, 332 command: command.execute, 333 }); 334 } 335 }); 336 return omniInserters; 337 } 338 339 public codeViews() { 340 const views: { [key: string]: CodeViewOptions } = {}; 341 this.pandocNodes().forEach((node: PandocNode) => { 342 if (node.code_view) { 343 views[node.name] = node.code_view; 344 } 345 }); 346 return views; 347 } 348 349 public attrEditors() { 350 return this.collectFrom({ 351 node: node => [node.attr_edit?.()], 352 }); 353 } 354 355 public baseKeys(schema: Schema): readonly BaseKeyBinding[] { 356 return this.collect(extension => extension.baseKeys?.(schema)); 357 } 358 359 public appendTransactions(schema: Schema): readonly AppendTransactionHandler[] { 360 return this.collect(extension => extension.appendTransaction?.(schema)); 361 } 362 363 public appendMarkTransactions(schema: Schema): readonly AppendMarkTransactionHandler[] { 364 return this.collect(extension => extension.appendMarkTransaction?.(schema)); 365 } 366 367 public plugins(schema: Schema): readonly Plugin[] { 368 return this.collect(extension => extension.plugins?.(schema)); 369 } 370 371 public fixups(schema: Schema, view: EditorView): readonly FixupFn[] { 372 return this.collect(extension => extension.fixups?.(schema, view)); 373 } 374 375 public completionHandlers(): readonly CompletionHandler[] { 376 return this.collect(extension => extension.completionHandlers?.()); 377 } 378 379 // NOTE: return value not readonly b/c it will be fed directly to a 380 // Prosemirror interface that doesn't take readonly 381 public inputRules(schema: Schema): InputRule[] { 382 const markFilter = markInputRuleFilter(schema, this.pandocMarks()); 383 return this.collect<InputRule>(extension => extension.inputRules?.(schema, markFilter)); 384 } 385 386 private collect<T>(collector: (extension: Extension) => readonly T[] | undefined) { 387 return this.collectFrom({ 388 extension: extension => collector(extension) ?? [], 389 }); 390 } 391 392 /** 393 * Visits extensions in order of registration, providing optional callbacks 394 * for extension, mark, and node. The return value of callbacks should be 395 * arrays of (T | undefined | null); these will all be concatenated together, 396 * with the undefined and nulls filtered out. 397 * 398 * @param visitor Object containing callback methods for the different 399 * extension parts. 400 */ 401 private collectFrom<T>(visitor: { 402 extension?: (extension: Extension) => ReadonlyArray<T | undefined | null>; 403 mark?: (mark: PandocMark) => ReadonlyArray<T | undefined | null>; 404 node?: (node: PandocNode) => ReadonlyArray<T | undefined | null>; 405 }): T[] { 406 const results: Array<T | undefined | null> = []; 407 408 this.extensions.forEach(extension => { 409 if (visitor.extension) { 410 results.push(...visitor.extension(extension)); 411 } 412 if (visitor.mark && extension.marks) { 413 results.push(...extension.marks.flatMap(visitor.mark)); 414 } 415 if (visitor.node && extension.nodes) { 416 results.push(...extension.nodes.flatMap(visitor.node)); 417 } 418 }); 419 420 return results.filter(value => typeof value !== 'undefined' && value !== null) as T[]; 421 } 422} 423