import { dirname } from 'path' ; import { unescape } from 'querystring' ; import { EventEmitter } from 'events' ; import { shell , remote } from 'electron' ; import * as marked from 'marked' ; import * as katex from 'katex' ; import { highlight } from 'highlight.js' ; import * as he from 'he' ; import SanitizeState from 'marked-sanitizer-github' ; let element_env = null ; const emoji_replacer = new Emoji . Replacer ( dirname ( __dirname ) + '/images' ) ; let mermaid = undefined ; marked . setOptions ( { langPrefix : 'hljs ' , highlight ( code , lang ) { if ( lang === undefined ) { return code ; } if ( lang === 'mermaid' ) { if ( mermaid === undefined ) { mermaid = require ( 'mermaid' ) ; mermaid . init ( undefined , 'div.mermaid' ) ; } return '<div class="mermaid">' + he . encode ( code ) + '</div>' ; } if ( lang === 'katex' ) { return '<div class="katex">' + katex . renderToString ( code , { displayMode : true } ) + '</div>' ; } try { return highlight ( lang , code ) . value ; } catch ( e ) { console . log ( 'Error on highlight: ' + e . message ) ; return code ; } } , emoji ( name ) { return emoji_replacer . replaceOne ( name ) ; } , sanitize : 1 , } ) ; const REGEX_CHECKED_LISTITEM = /^\[x]\s+/ ; const REGEX_UNCHECKED_LISTITEM = /^\[ ]\s+/ ; class MarkdownRenderer extends EventEmitter { public outline : Heading [ ] ; private readonly renderer : marked . Renderer ; private link_id : number ; private tooltips : string ; private readonly sanitizeState : SanitizeState ; private readonly sanitizer : ( tag : string ) => string ; constructor ( public markdown_exts ) { super ( ) ; this . renderer = new marked . Renderer ( ) ; const self = this ; this . renderer . listitem = function ( text ) { let matched = text . match ( REGEX_CHECKED_LISTITEM ) ; if ( matched && matched [ 0 ] ) { return `
                  <li class="task-list-item">
                    <input type="checkbox" class="task-list-item-checkbox" checked="checked" disabled="disabled">
                    ${ text . slice ( matched [ 0 ] . length ) }
                  </li>
                ` ; } matched = text . match ( REGEX_UNCHECKED_LISTITEM ) ; if ( matched && matched [ 0 ] ) { return `
                  <li class="task-list-item">
                    <input type="checkbox" class="task-list-item-checkbox" disabled="disabled">
                    ${ text . slice ( matched [ 0 ] . length ) }
                  </li>
                ` ; } return marked . Renderer . prototype . listitem . call ( this , text ) ; } ; const re_ext = new RegExp ( `\\.(:?${ this . markdown_exts . join ( '|' ) })(:?$|#)` ) ; this . renderer . link = function ( href , title , text ) { if ( ! href ) { return marked . Renderer . prototype . link . call ( this , href , title , text ) ; } if ( this . options . sanitize ) { try { const prot = decodeURIComponent ( unescape ( href ) ) . replace ( /[^\w:]/g , '' ) . toLowerCase ( ) ; if ( prot . startsWith ( 'javascript:' ) || prot . startsWith ( 'vbscript:' ) ) { return '' ; } } catch ( e ) { return '' ; } } let onclick = 'cancelClick(event)' ; if ( href . startsWith ( 'http://' ) || href . startsWith ( 'https://' ) ) { onclick = `openLinkWithExternalBrowser(event, this.id)` ; } else if ( re_ext . test ( href ) ) { onclick = 'openMarkdownLink(event)' ; } else if ( href . indexOf ( '#' ) !== - 1 ) { onclick = 'openHashLink(event)' ; } self . link_id += 1 ; const id = `md-link-${ self . link_id }` ; self . tooltips += `<paper-tooltip for="${ id }" offset="0">${ he . encode ( href ) }</paper-tooltip>` ; return title !== null ? `<a id="${ id }" href="${ href }" onclick="${ onclick }" title=${ title }>${ text }</a>` : `<a id="${ id }" href="${ href }" onclick="${ onclick }">${ text }</a>` ; } ; this . renderer . heading = function ( text , level , raw ) { const hash = this . options . headerPrefix + raw . toLowerCase ( ) . replace ( /[^\w]+/g , '-' ) ; self . outline . push ( { title : raw , hash , level , html : text , } ) ; return marked . Renderer . prototype . heading . call ( this , text , level , raw ) ; } ; this . sanitizeState = new SanitizeState ( ) ; this . sanitizeState . onDetectedBroken = ( reason , tag ) => { this . emit ( 'sanitization-error' , `HTML is broken around '${ tag }' tag` , reason ) ; } ; this . sanitizer = this . sanitizeState . getSanitizer ( ) ; } render ( markdown ) { this . link_id = 0 ; this . tooltips = '' ; this . outline = [ ] ; this . sanitizeState . reset ( ) ; return ( marked ( markdown , { renderer : this . renderer , sanitizer : this . sanitizer , } ) + this . tooltips ) ; } } function openMarkdownLink ( event ) { event . preventDefault ( ) ; let target = event . target as HTMLAnchorElement ; while ( target !== null ) { if ( target . href ) { break ; } target = target . parentElement as HTMLAnchorElement ; } if ( target === null ) { console . log ( 'openMarkdownLink(): No target <a> was found' , event ) ; return ; } let path = unescape ( target . href ) ; if ( path . startsWith ( 'file://' ) ) { path = path . slice ( 7 ) ; } const hash_idx = path . indexOf ( '#' ) ; if ( hash_idx !== - 1 ) { path = path . slice ( 0 , hash_idx ) ; } if ( process . platform === 'win32' && path [ 0 ] === '/' ) { path = path . slice ( 1 ) ; } if ( element_env . openMarkdownDoc ) { element_env . openMarkdownDoc ( path , event . ctrlKey || event . metaKey ) ; } else { console . log ( 'openMarkdownDoc() is not defined!! Link ignored: ' + path ) ; } } function openHashLink ( event ) { event . preventDefault ( ) ; const target = event . target as HTMLAnchorElement ; const hash_name = target . href . split ( '#' ) [ 1 ] ; location . hash = hash_name ; } function openLinkWithExternalBrowser ( event , id ) { event . preventDefault ( ) ; const e = document . getElementById ( id ) as HTMLAnchorElement ; shell . openExternal ( e . href ) ; } function cancelClick ( event ) { event . preventDefault ( ) ; } Polymer ( { is : 'markdown-preview' , properties : { document : { type : String , observer : '_documentUpdated' , } , exts : { type : Array , value ( ) { return [ ] as string [ ] ; } , } , currentOutline : { type : Array , value ( ) { return [ ] as Heading [ ] ; } , } , isGithubStyle : { type : Boolean , value : true , } , openMarkdownDoc : Object , fontSize : String , onDocumentUpdated : Object , onSanitizationError : Object , } , ready ( ) { element_env = this ; } , attached ( ) { this . renderer = new MarkdownRenderer ( this . exts ) ; this . renderer . on ( 'sanitization-error' , ( message , reason ) => { if ( this . onSanitizationError ) { this . onSanitizationError ( message , reason ) ; } } ) ; const body = document . getElementById ( 'shiba-markdown-component' ) as HTMLDivElement ; if ( this . fontSize ) { body . style . fontSize = this . fontSize ; } if ( ! this . isGithubStyle ) { body . classList . remove ( 'markdown-body' ) ; } } , _documentUpdated ( updated_doc ) { const body = document . getElementById ( 'shiba-markdown-component' ) as HTMLDivElement ; body . innerHTML = this . renderer . render ( updated_doc ) ; this . currentOutline = this . renderer . outline ; const mermaid_blocks = document . querySelectorAll ( 'div.mermaid' ) ; if ( mermaid_blocks . length > 0 && typeof mermaid !== 'undefined' ) { mermaid . init ( undefined , mermaid_blocks ) ; } if ( this . onDocumentUpdated ) { this . onDocumentUpdated ( ) ; } } , scrollToHeading ( scroller , h ) { const elem = document . getElementById ( h . hash ) ; if ( elem ) { scroller . scrollTop = elem . offsetTop ; } } , } as MarkdownPreviewComponent ) ;