import { Schema , Node as ProseNode , Mark as ProseMark , NodeSpec , MarkSpec , DOMOutputSpec } from "prosemirror-model" ; import { InputRule , inputRules as makeInputRules , } from "prosemirror-inputrules" ; import { chainCommands , baseKeymap , Keymap , Command as ProseCommand , } from "prosemirror-commands" ; import { Plugin as ProsePlugin } from "prosemirror-state" ; import { ProseSchema } from "@common/types" ; import { markMapBasic , markMapStringLiteral , MdParser , nodeMapBasic , nodeMapLeaf , nodeMapStringLiteral } from "@common/markdown/mdast2prose" ; import * as Uni from "unist" ; import * as Md from "@common/markdown/markdown-ast" ; import unified , { Processor } from "unified" ; import { MdSerializer } from "@common/markdown/prose2mdast" ; import { MdError } from "@common/markdown/remark-plugins/error/remark-error" ; import { unistIsStringLiteral } from "@common/markdown/unist-utils" ; import remark from "remark-parse" ; import remarkMathPlugin from "remark-math" ; import remarkFrontMatter from "remark-frontmatter" ; import remarkGfm from "remark-gfm" ; import remarkFootnotes from "remark-footnotes" ; import remarkStringify from "remark-stringify" ; import remarkDirective from "remark-directive" ; import { citePlugin as remarkCitePlugin } from "@benrbray/remark-cite" ; import { wikiLinkPlugin as remarkWikilinkPlugin } from '@common/markdown/remark-plugins/wikilink/remark-wikilink' ; import { remarkErrorPlugin } from "@common/markdown/remark-plugins/error/remark-error" ; import { remarkConcretePlugin } from "@common/markdown/remark-plugins/concrete/remark-concrete" ; import { keymap as makeKeymap } from "prosemirror-keymap" ; import { DefaultMap } from "@common/util/DefaultMap" ; import { NwtExtension , NodeExtension , MarkExtension , MdastNodeMapType , MdastMarkMapType , Prose2Mdast_NodeMap_Presets , Prose2Mdast_MarkMap_Presets } from "@common/extensions/extension" ; import * as prose2mdast from "@common/markdown/prose2mdast" ; import * as mdast2prose from "@common/markdown/mdast2prose" ; type AstNode < BaseT extends Uni . Node > = BaseT | AstParent < BaseT > ; type AstParent < BaseT extends Uni . Node > = Uni . Parent & BaseT & { children : AstNode < BaseT > ; } ; export type UnistNodeTest < S extends ProseSchema = ProseSchema , T extends Uni . Node = Uni . Node > = { test ? : ( x : T ) => boolean ; map : ( x : T , children : ProseNode < S > [ ] , ctx : unknown , state : unknown ) => ProseNode < S > [ ] ; } ; export type UnistMapper < K extends string = string , S extends ProseSchema = ProseSchema > = DefaultMap < K , UnistNodeTest < S > [ ] > ; /**
 * Create a ProseMirror node from a Unist node and its list of
 * children, which have already been converted to ProseMirror nodes.

 * TODO (2021-05-17) should the node map return new context/state,
 *   instead of context/state being specified by separate maps?
 */ export type ProseNodeMap < Ctx = unknown , St = unknown > = ( node : ProseNode , children : Uni . Node [ ] , ctx : Ctx , state : St ) => Uni . Node [ ] ; /**
 * The given `node` should have `mark` as one of its marks.  Return an appropriate
 * node to represent their combination.  (usually a simple wrapper node)
 */ export type ProseMarkMap = ( mark : ProseMark , node : Uni . Node ) => Uni . Node ; export type ProseNodeTest < S extends ProseSchema = ProseSchema , N extends ProseNode < S > = ProseNode < S > > = { test ? : ( x : N ) => boolean ; map : ProseNodeMap < unknown , unknown > ; } ; export type ProseMarkTest < M extends ProseMark = ProseMark > = { /** Predicate to decide whether this mapper can be used for the specified mark. */ test ? : ( mark : M ) => boolean ; map : ProseMarkMap ; } ; export type ProseMapper < N extends string = string , M extends string = string , S extends ProseSchema < N , M > = ProseSchema < N , M > > = { /** A map from ProseMirror Nodes -> Unist Nodes */ nodes : DefaultMap < N , ProseNodeTest < S > [ ] > ; /** A map from ProseMirror Marks -> Unist Nodes */ marks : DefaultMap < M , ProseMarkTest [ ] > ; } ; export class EditorConfig < S extends ProseSchema = ProseSchema > { schema : S & ProseSchema < "error_block" , "error_inline" > ; plugins : ProsePlugin [ ] ; private _mdast2prose : UnistMapper ; private _prose2mdast : ProseMapper ; private _mdastParser : Processor ; private _parser : MdParser < S > ; private _serializer : MdSerializer ; constructor ( extensions , plugins , keymap ) { effect note this . schema = this . _buildSchema ( extensions ) ; /** Step 2: Build Plugins */ this . plugins = this . _buildPlugins ( extensions , plugins . concat ( makeKeymap ( keymap ) ) ) ; /** Step 3: Configure Unified / Remark */ this . _mdastParser = unified ( ) . use ( remark ) . use ( remarkGfm ) . use ( remarkMathPlugin ) . use ( remarkCitePlugin , { syntax : { enableAltSyntax : true } , toMarkdown : { useNodeValue : true } } ) . use ( remarkErrorPlugin ) . use ( remarkConcretePlugin ) . use ( remarkFootnotes , { inlineNotes : true } ) . use ( remarkWikilinkPlugin ) . use ( remarkFrontMatter , [ 'yaml' , 'toml' ] ) . use ( remarkDirective ) . use ( remarkStringify , { bullet : '*' , fences : true , incrementListMarker : true , resourceLink : true , listItemIndent : "one" , } ) ; /** Step 3a: Build Mapping from Unist AST -> ProseMirror Document */ this . _mdast2prose = this . _buildMdast2Prose ( extensions ) ; /** Step 3b: Build Document Parser for this Configuration */ let parser = mdast2prose . makeParser ( this . schema , this . _mdast2prose , this . _mdastParser ) ; this . _parser = parser as MdParser < S > ; /** Step 4a: Build Mapping from ProseMirror Document -> Unist AST */ this . _prose2mdast = this . _buildProse2Mdast ( extensions ) ; /** Step 4b: Build Markdown Serializer for this Configuration */ this . _serializer = prose2mdast . makeSerializer ( this . _prose2mdast , this . _mdastParser ) ; } parse ( markdown ) { return this . _parser ( markdown ) ; } parseAST ( markdown ) { let result = this . _mdastParser . parse ( markdown ) ; if ( ! result ) { return null ; } else { return result as Md . Node ; } } serialize ( doc ) { return this . _serializer ( doc ) ; } private _buildSchema ( extensions ) { let markSpecs = { error_inline : { parseDOM : [ { tag : "code.error-inline" } ] , toDOM ( ) { return [ "code" , { class : "error-inline" } ] ; } } } ; let nodeSpecs = { text : { group : "inline" } } ; for ( let ext of extensions ) { todo let name = ext . name ; if ( name in nodeSpecs ) { throw new Error ( `duplicate node name '${ name }'!` ) ; } if ( name in markSpecs ) { throw new Error ( `duplicate mark name '${ name }'!` ) ; } if ( ext instanceof NodeExtension ) { nodeSpecs [ name ] = ext . createNodeSpec ( ) ; } else if ( ext instanceof MarkExtension ) { markSpecs [ name ] = ext . createMarkSpec ( ) ; } } nodeSpecs [ "error_block" ] = { content : "text*" , group : "block" , code : true , defining : true , marks : "" , attrs : { params : { default : "" } } , parseDOM : [ { tag : "pre.error-block" , preserveWhitespace : "full" , getAttrs : node => { return { params : ( node as HTMLElement ) . getAttribute ( "data-params" ) || "" } ; } } ] , toDOM ( node ) { let dataParams = node . attrs . params ? { "data-params" : node . attrs . params } : { } ; let attrs = { ... dataParams , class : "error-block" } ; return [ "pre" , attrs , [ "code" , 0 ] ] ; } } ; console . log ( JSON . stringify ( nodeSpecs , undefined , 2 ) ) ; let schema = new Schema ( { "nodes" : nodeSpecs , "marks" : markSpecs } ) as S & ProseSchema < "error_block" , "error_inline" > ; for ( let ext of extensions ) { ext . _store = { schema } ; } return schema ; } private _buildPlugins ( extensions , plugins = [ ] ) { let inputRules = [ ] ; let keymaps = new DefaultMap < string , ProseCommand [ ] > ( _ => [ ] ) ; for ( let ext of extensions ) { inputRules = inputRules . concat ( ext . createInputRules ( ) ) ; let extKeymap = ext . createKeymap ( ) ; for ( let key in ext . createKeymap ( ) ) { keymaps . get ( key ) . push ( extKeymap [ key ] ) ; } } let resultPlugins = [ ] ; resultPlugins . push ( makeInputRules ( { rules : inputRules } ) ) ; todo let keymap = { } ; keymaps . forEach ( ( cmds , key ) => { keymap [ key ] = chainCommands ( ... cmds ) ; } ) ; resultPlugins . push ( makeKeymap ( keymap ) ) ; todo resultPlugins . push ( makeKeymap ( baseKeymap ) ) ; return resultPlugins . concat ( plugins ) ; } private _buildMdast2Prose ( extensions ) { let result = new DefaultMap < string , UnistNodeTest [ ] > ( _ => [ ] ) ; result . get ( "text" ) . push ( { map : ( node , _ ) => { return [ this . schema . text ( ( node as Md . Text ) . value ) ] ; } } ) ; result . get ( "yaml" ) . push ( { map : ( node , _ ) => [ ] } ) ; for ( let ext of extensions ) { let nodeType = null ; let mapper = null ; let nodeTest = null ; if ( ext instanceof NodeExtension ) { let mdastMap = ext . createMdastMap ( ) ; nodeTest = ext . mdastNodeTest ; nodeType = ext . mdastNodeType ; if ( mdastMap === MdastNodeMapType . NODE_DEFAULT ) { mapper = nodeMapBasic ( ext . nodeType ) ; } else if ( mdastMap === MdastNodeMapType . NODE_EMPTY ) { mapper = nodeMapLeaf ( ext . nodeType ) ; } else if ( mdastMap === MdastNodeMapType . NODE_LITERAL ) { mapper = nodeMapStringLiteral ( ext . nodeType ) as ( node : Uni . Node , children : ProseNode [ ] ) => ProseNode [ ] ; } else { mapper = mdastMap . mapNode ; } } else if ( ext instanceof MarkExtension ) { let mdastMap = ext . createMdastMap ( ) ; nodeTest = ext . mdastNodeTest ; nodeType = ext . mdastNodeType ; if ( mdastMap === MdastMarkMapType . MARK_DEFAULT ) { mapper = markMapBasic ( ext . markType ) ; } else if ( mdastMap === MdastMarkMapType . MARK_LITERAL ) { mapper = markMapStringLiteral ( ext . markType ) as ( node : Uni . Node , children : ProseNode [ ] ) => ProseNode [ ] ; } else { mapper = mdastMap . mapMark ; } } if ( mapper && nodeType && nodeTest ) { result . get ( nodeType ) . push ( { map : mapper , test : nodeTest } ) ; } } return result ; } private _buildProse2Mdast ( extensions ) { let result = { nodes : new DefaultMap < string , ProseNodeTest [ ] > ( _ => [ ] ) , marks : new DefaultMap < string , ProseMarkTest [ ] > ( _ => [ ] ) } ; result . nodes . get ( "error_block" ) . push ( { map : ( node , children ) : [ MdError ] => { if ( children . find ( n => ( n . type !== "text" ) ) !== undefined ) { throw new Error ( "expected error_block to contain only text nodes!" ) ; } return [ { type : "error" , value : node . textContent } ] ; } } ) ; result . marks . get ( "error_inline" ) . push ( { map : ( mark , node ) : MdError => { if ( ! unistIsStringLiteral ( node ) ) { throw new Error ( "expected error_inline to be a text node" ) ; } return { type : "error" , value : node . value } ; } } ) ; for ( let ext of extensions ) { if ( ext instanceof NodeExtension ) { let mapper ; let p2m = ext . prose2mdast ( ) ; let proseNodeType = ext . nodeType . name ; if ( proseNodeType === "text" ) { console . error ( "[prose2mdast] cannot override default handling of text nodes ; ignoring plugin-specified handler!" ) ; continue ; } if ( p2m === Prose2Mdast_NodeMap_Presets . NODE_DEFAULT ) { mapper = prose2mdast . nodeMapDefault ( ext . mdastNodeType ) ; } else if ( p2m === Prose2Mdast_NodeMap_Presets . NODE_EMPTY ) { mapper = prose2mdast . nodeMapEmpty ( ext . mdastNodeType ) ; } else if ( p2m === Prose2Mdast_NodeMap_Presets . NODE_LIFT_LITERAL ) { mapper = prose2mdast . nodeMapLiftLiteral ( ext . mdastNodeType ) ; } else { mapper = p2m . create ; } result . nodes . get ( proseNodeType ) . push ( { map : mapper , } ) ; } else if ( ext instanceof MarkExtension ) { let mapper ; let p2m = ext . prose2mdast ( ) ; if ( p2m === Prose2Mdast_MarkMap_Presets . MARK_DEFAULT ) { mapper = prose2mdast . markMapDefault ( ext . mdastNodeType ) ; } else if ( p2m === Prose2Mdast_MarkMap_Presets . MARK_LITERAL ) { mapper = prose2mdast . markMapLiteral ( ext . mdastNodeType ) ; } else { mapper = p2m . create ; } let proseMarkType = ext . markType . name ; result . marks . get ( proseMarkType ) . push ( { map : mapper , } ) ; } } return result ; } }