import { Processor } from "unified" ; import * as Uni from "unist" ; import * as Md from "@common/markdown/markdown-ast" ; import { unistIsParent , unistSource } from "./unist-utils" ; import { Node as ProseNode } from "prosemirror-model" ; import { ProseSchema , ProseMarkType , ProseNodeType } from "@common/types" ; import { UnistMapper } from "@common/extensions/editor-config" ; import YAML from "yaml" ; /**
 * (https://stackoverflow.com/a/50125960/1444650)
 *
 * When T is a union type discriminated by K, returns a mapping from
 * possible values of K to the corresponding union members.
 *
 * For instance, if we have the following union type:
 *
 *     type Honeycrisp   = { name : "apple", kind : "honeycrisp"  }
 *     type GrannySmith  = { name : "apple", kind : "grannysmith"  }
 *     type Banana       = { name : "banana" }
 *     type Cherry       = { name : "cherry" }
 *     type Fruits       = Honeycrisp | GrannySmith | Banana | Cherry;
 *
 * The result should be equivalent to:
 *
 *     MapDiscriminatedUnion<"name", Fruits> = {
 *         apple  :  Honeycrisp | GrannySmith
 *         banana : Banana
 *         cherry : Cherry
 *      }
 */ export type DiscriminateUnion < T , K extends keyof T , V extends T [ K ] > = T extends Record < K , V > ? T : never ; export type MapDiscriminatedUnion < T extends Record < K , string > , K extends keyof T > = { [ V in T [ K ] ] : DiscriminateUnion < T , K , V > ; } ; export type AnyChildren < T extends Uni . Parent , ChildT = Uni . Node > = { [ K in keyof T as Exclude < K , "children" > ] : T [ K ] ; } & { children : ChildT [ ] ; } ; export type MdTypeMap = MapDiscriminatedUnion < Md . Node , "type" > ; export type MdMapper < S extends ProseSchema > = { [ key in keyof MdTypeMap ] ? : ( x : MdTypeMap [ key ] , children : ProseNode < S > [ ] ) => ProseNode < S > [ ] ; } ; export const nodeMapBasic = < S extends ProseSchema > ( nodeType ) => ( node , children ) => { let result = nodeType . createAndFill ( { } , children || undefined ) ; return result ? [ result ] : [ ] ; } ; export const nodeMapLeaf = < S extends ProseSchema > ( nodeType ) => ( node , _ ) => { let result = nodeType . createAndFill ( { } ) ; return result ? [ result ] : [ ] ; } ; export const nodeMapStringLiteral = < S extends ProseSchema > ( nodeType ) => ( node , _ ) => { if ( node . value . length < 1 ) { return [ ] ; } let result = nodeType . createAndFill ( { } , [ nodeType . schema . text ( node . value ) ] ) ; return result ? [ result ] : [ ] ; } ; export const markMapBasic = < S extends ProseSchema , T extends Uni . Node > ( markType , getAttrs ? ) => ( node , children ) => { let attrs = getAttrs ? getAttrs ( node ) : { } ; return children . flatMap ( childNode => { if ( ! childNode . isText ) { return [ ] ; } return childNode . mark ( childNode . marks . concat ( [ markType . create ( attrs ) ] ) ) ; } ) ; } ; export const markMapStringLiteral = < S extends ProseSchema , T extends Uni . Node = Uni . Node > ( markType , getAttrs ? ) => ( node , _ ) => { if ( node . value . length < 1 ) { return [ ] ; } let attrs = getAttrs ? getAttrs ( node ) : { } ; let textNode = markType . schema . text ( node . value ) ; let result = textNode . mark ( [ markType . create ( attrs ) ] ) ; return result ? [ result ] : [ ] ; } ; type MdContextMapper < Ctx > = { [ key in keyof MdTypeMap ] ? : ( x : MdTypeMap [ key ] , ctx : Ctx ) => Ctx ; } ; type MdStateMapper < St > = { [ key in keyof MdTypeMap ] ? : ( x : MdTypeMap [ key ] , state : St ) => St ; } ; /**
 * 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?
 */ type NodeMap < Ctx = unknown , St = unknown , S extends ProseSchema = ProseSchema > = ( node : Uni . Node , children : ProseNode < S > [ ] , parseContext : Ctx , parseState : St ) => ProseNode < S > [ ] ; note type ContextMap < Ctx = unknown , N extends Uni . Node = Uni . Node > = ( node : N , parseContext : Ctx ) => Ctx ; note type StateMap < St = unknown , N extends Uni . Node = Uni . Node > = ( node : N , parseState : St ) => St ; function getContextMapper < N extends Md . Node , Ctx > ( node , mappers ) { return mappers [ node . type ] as ContextMap < Ctx , N > | undefined ; } function getStateMapper < N extends Md . Node , St > ( node , mappers ) { return mappers [ node . type ] as StateMap < St , N > | undefined ; } export function treeMap < S extends ProseSchema , Ctx , St > ( node , parseContext , parseState , nodeMap , contextMap , stateMap , errorMap ) { let contextMapper = getContextMapper ( node , contextMap ) ; let newParseContext = contextMapper ? contextMapper ( node , parseContext ) : parseContext ; let nodeContents = [ ] ; let newParseState = parseState ; if ( unistIsParent ( node ) ) { for ( let idx = 0 ; idx < node . children . length ; idx ++ ) { let [ child , state ] = treeMap ( node . children [ idx ] , newParseContext , newParseState , nodeMap , contextMap , stateMap , errorMap ) ; newParseState = state ; if ( child !== null ) { nodeContents = nodeContents . concat ( child ) ; } } } let stateMapper = getStateMapper ( node , stateMap ) ; newParseState = stateMapper ? stateMapper ( node , newParseState ) : newParseState ; let nodeFn = null ; for ( let test of nodeMap . get ( node . type ) ) { if ( ! test . test || test . test ( node ) ) { nodeFn = test . map ; } } if ( ! nodeFn ) { console . error ( `treeMap :: expected ${ node . type }, got undefined ; node=` , node ) ; nodeFn = errorMap ; } let result = nodeFn ( node , nodeContents , parseContext , newParseState ) ; return [ result as ProseNode < S > [ ] , newParseState ] ; } /**
 * Local context is used for sending information *down* the tree.
 * TODO:
 */ export type MdParseContext = { inParagraph : boolean ; } ; /**
 * Global state is used for propagating information *up* the tree.
 */ export type MdParseState = { yaml : { [ key ] : unknown ; } ; } ; function makeNodeErrorHandler < S extends ProseSchema > ( inlineErrorType , blockErrorType , node2src ) { return ( node , _ , context ) => { let nodeSrc = node2src ( node ) ; if ( nodeSrc === null ) { console . error ( "encountered unfamiliar node ; failed to produce corresponding markdown source" ) ; return [ ] ; } if ( context . inParagraph ) { let mark = inlineErrorType . create ( ) ; let textNode = inlineErrorType . schema . text ( nodeSrc , [ mark ] ) ; return textNode ? [ textNode ] : [ ] ; } else { let textNode = blockErrorType . schema . text ( nodeSrc ) ; let result = blockErrorType . createAndFill ( { } , [ textNode ] ) ; return result ? [ result ] : [ ] ; } } ; } export type MdParser < S extends ProseSchema > = ( markdown : string ) => ProseNode < S > | null ; precondition warning export const makeParser = < S extends ProseSchema < "error_block" , "error_inline" > > ( proseSchema , nodeMap , md2ast ) : MdParser < S > => { let contextMap = { "paragraph" : ( node , ctx ) : MdParseContext => { return { ... ctx , inParagraph : true } ; } } ; let stateMap = { "yaml" : ( node , state ) : MdParseState => { let yamlData = YAML . parse ( node . value ) ; console . warn ( "found yaml node with data" , yamlData ) ; return { ... state , yaml : { ... state . yaml , ... yamlData } } ; } } ; return ( markdown ) => { let ast = md2ast . parse ( markdown ) as Md . Root ; let parseContext = { inParagraph : false } ; let parseState = { yaml : { } } ; let errorMap = makeNodeErrorHandler < S > ( proseSchema . marks . error_inline , proseSchema . nodes . error_block , node => unistSource ( node , markdown ) ) ; let [ rootContent , globalState ] = treeMap < S , MdParseContext , MdParseState > ( ast , parseContext , parseState , nodeMap , contextMap , stateMap , errorMap ) ; console . warn ( "\nglobalState:" , globalState , "\n\n" ) ; if ( rootContent . length == 0 ) { throw new Error ( "empty document" ) ; } if ( rootContent . length > 1 ) { throw new Error ( "multiple top-level nodes" ) ; } return rootContent [ 0 ] ; } ; } ;