import { Remarkable } from 'remarkable' ; import { INode , CSSItem , JSItem , IWrapContext , wrapFunction , } from 'markmap-common' ; import { ITransformResult , ITransformPlugin , IAssets , IAssetsMap , ITransformHooks , IFeatures , ITransformContext , } from './types' ; import { createTransformHooks , plugins as builtInPlugins } from './plugins' ; export { builtInPlugins } ; function cleanNode ( node , depth = 0 ) { if ( node . t === 'heading' ) { node . c = node . c . filter ( ( item ) => item . t !== 'paragraph' ) ; } else if ( node . t === 'list_item' ) { node . c = node . c . filter ( ( item ) => { if ( [ 'paragraph' , 'fence' ] . includes ( item . t ) ) { if ( ! node . v ) { node . v = item . v ; node . p = { ... node . p , ... item . p , } ; } return false ; } return true ; } ) ; if ( node . p ?. index != null ) { node . v = `${ node . p . index }. ${ node . v }` ; } } else if ( node . t === 'ordered_list' ) { let index = node . p ?. start ?? 1 ; node . c . forEach ( ( item ) => { if ( item . t === 'list_item' ) { item . p = { ... item . p , index , } ; index += 1 ; } } ) ; } if ( node . c . length === 0 ) { delete node . c ; } else { node . c . forEach ( ( child ) => cleanNode ( child , depth + 1 ) ) ; if ( node . c . length === 1 && ! node . c [ 0 ] . v ) { node . c = node . c [ 0 ] . c ; } } node . d = depth ; } export class Transformer { hooks : ITransformHooks ; md : Remarkable ; assetsMap : IAssetsMap ; constructor ( public plugins = builtInPlugins ) { this . hooks = createTransformHooks ( ) ; const assetsMap = { } ; for ( const { name , transform } of plugins ) { assetsMap [ name ] = transform ( this . hooks ) ; } this . assetsMap = assetsMap ; const md = new Remarkable ( { html : true , breaks : true , maxNesting : Infinity , } as Remarkable . Options ) ; md . block . ruler . enable ( [ 'deflist' ] ) ; md . renderer . rules . htmltag = wrapFunction ( md . renderer . rules . htmltag , { after : ( ctx ) => { this . hooks . htmltag . call ( ctx ) ; } , } ) ; this . md = md ; this . hooks . parser . call ( md ) ; } buildTree ( tokens ) { const { md } = this ; const root = { t : 'root' , d : 0 , v : '' , c : [ ] , p : { } , } ; const stack = [ root ] ; let depth = 0 ; for ( const token of tokens ) { let current = stack [ stack . length - 1 ] ; if ( token . type . endsWith ( '_open' ) ) { const type = token . type . slice ( 0 , - 5 ) ; const payload = { } ; if ( token . lines ) { payload . lines = token . lines ; } if ( type === 'heading' ) { depth = ( token as Remarkable . HeadingOpenToken ) . hLevel ; while ( current ?. d >= depth ) { stack . pop ( ) ; current = stack [ stack . length - 1 ] ; } } else { depth = Math . max ( depth , current ?. d || 0 ) + 1 ; if ( type === 'ordered_list' ) { payload . start = ( token as Remarkable . OrderedListOpenToken ) . order ; } } const item = { t : type , d : depth , p : payload , v : '' , c : [ ] , } ; current . c . push ( item ) ; stack . push ( item ) ; } else if ( ! current ) { continue ; } else if ( token . type === `${ current . t }_close` ) { if ( current . t === 'heading' ) { depth = current . d ; } else { stack . pop ( ) ; depth = 0 ; } } else if ( token . type === 'inline' ) { const revoke = this . hooks . htmltag . tap ( ( ctx ) => { const comment = ctx . result . match ( /^<!--([\s\S]*?)-->$/ ) ; const data = comment ?. [ 1 ] . trim ( ) ; if ( data === 'fold' ) { current . p . f = true ; ctx . result = '' ; } } ) ; const text = md . renderer . render ( [ token ] , ( md as any ) . options , { } ) ; revoke ( ) ; current . v = `${ current . v || '' }${ text }` ; } else if ( token . type === 'fence' ) { let result = md . renderer . render ( [ token ] , ( md as any ) . options , { } ) ; const matches = result . match ( /<code( class="[^"]*")>/ ) ; if ( matches ) result = result . replace ( '<pre>' , `<pre${ matches [ 1 ] }>` ) ; current . c . push ( { t : token . type , d : depth + 1 , v : result , c : [ ] , } ) ; } else { } } return root ; } transform ( content ) { const context = { features : { } , } ; this . hooks . transform . call ( this . md , context ) ; const tokens = this . md . parse ( content , { } ) ; let root = this . buildTree ( tokens ) ; cleanNode ( root ) ; if ( root . c ?. length === 1 ) root = root . c [ 0 ] ; return { ... context , root } ; } /**
     * Get all assets from enabled plugins or filter them by plugin names as keys.
     */ getAssets ( keys ? ) { const styles = [ ] ; const scripts = [ ] ; keys ??= Object . keys ( this . assetsMap ) ; for ( const assets of keys . map ( ( key ) => this . assetsMap [ key ] ) ) { if ( assets ) { if ( assets . styles ) styles . push ( ... assets . styles ) ; if ( assets . scripts ) scripts . push ( ... assets . scripts ) ; } } return { styles , scripts } ; } /**
     * Get used assets by features object returned by `transform`.
     */ getUsedAssets ( features ) { return this . getAssets ( Object . keys ( features ) . filter ( ( key ) => features [ key ] ) ) ; } }