import path from "path" ; import { Node as ProseNode } from "prosemirror-model" ; import { IWorkspaceDir , IFileMeta } from "@common/files" ; import { WorkspacePlugin } from "./plugin" ; import { IDoc } from "@common/doctypes/doctypes" ; import fuzzysort from "fuzzysort" ; import { DefaultMap } from "@common/util/DefaultMap" ; import { EditorView , NodeView , Decoration } from "prosemirror-view" ; import { PluginKey , PluginSpec , Plugin as ProsePlugin , EditorState , Transaction , TextSelection } from "prosemirror-state" ; import { StepMap } from "prosemirror-transform" ; import { keymap } from "prosemirror-keymap" ; import { chainCommands , deleteSelection } from "prosemirror-commands" ; function serializeSetMap < V > ( map ) { let result = Object . create ( null ) ; for ( let [ key , val ] of map ) { result [ key ] = Array . from ( val . values ( ) ) ; } return result ; } function deserializeSetMap < V > ( serialized ) { let result = new DefaultMap < string , Set < V > > ( ( ) => new Set ( ) ) ; for ( let key of Object . keys ( serialized ) ) { result . set ( key , new Set ( serialized [ key ] ) ) ; } return result ; } /**
 * Document types should implement this interface in order
 * to be recognized by the built-in cross-reference system.
 */ export interface ICrossRefProvider { todo IS_XREF_PROVIDER : true ; returns getTagsMentioned ( ) : string [ ] ; returns getTagsDefined ( ) : string [ ] ; } export function isXrefProvider ( resource ) { return ( resource as any ) . IS_XREF_PROVIDER === true ; } export interface ITagSearchResult { type : "tag-result" ; /** Tag name **/ result : string ; /** Tag name, emphasized with HTML tags (<b>, etc.) to reflect alignment with a query. */ resultEmphasized : string ; } export interface IHashSearchResult { type : "hash-result" ; /** file hash **/ hash : string ; } export interface IFileSearchResult { type : "file-result" ; /** file hash **/ file : IFileMeta ; } export type SearchResult = ITagSearchResult | IHashSearchResult | IFileSearchResult ; export class CrossRefPlugin implements WorkspacePlugin { plugin_name : string = "crossref_plugin" ; _doc2tags : DefaultMap < string , Set < string > > ; _tag2docs : DefaultMap < string , Set < string > > ; _tag2defs : DefaultMap < string , Set < string > > ; constructor ( ) { console . log ( `xref-plugin :: constructor()` ) ; this . _doc2tags = new DefaultMap ( ( ) => new Set ( ) ) ; this . _tag2docs = new DefaultMap ( ( ) => new Set ( ) ) ; this . _tag2defs = new DefaultMap ( ( ) => new Set ( ) ) ; } async init ( ) { console . log ( "crossref-plugin :: init()" ) ; this . attachEvents ( ) ; } attachEvents ( ) { } detachEvents ( ) { } dispose ( ) { this . clear ( ) ; this . detachEvents ( ) ; } clear ( ) { this . _doc2tags . clear ( ) ; this . _tag2docs . clear ( ) ; this . _tag2defs . clear ( ) ; } /**
     * Returns a list of hashes for documents which define this tag.
     */ getDefsForTag ( tag ) { tag = this . normalizeTag ( tag ) ; return Array . from ( this . _tag2defs . get ( tag ) . values ( ) ) ; } /**
     * Returns a list of hashes for documents which mention this tag.
     * (defining a tag also counts as mentioning it)
     */ getTagMentions ( tag ) { tag = this . normalizeTag ( tag ) ; let defs = this . _tag2defs . get ( tag ) ; let uses = this . _tag2docs . get ( tag ) ; return Array . from ( new Set ( [ ... defs , ... uses ] ) ) ; } async handleWorkspaceClosed ( dir ) { console . log ( "xref-plugin :: handle(workspace-closed)" ) ; this . clear ( ) ; } async handleWorkspaceOpen ( dir ) { console . log ( "xref-plugin :: handle(workspace-open)" ) ; } handleFileDeleted ( filePath , fileHash ) { this . removeWikilinks ( fileHash ) ; } handleFileCreated ( fileMeta , doc ) { if ( ! isXrefProvider ( doc ) ) { return ; } this . addWikilinks ( fileMeta , doc ) ; } handleFileChanged ( fileMeta , doc ) { if ( ! isXrefProvider ( doc ) ) { return ; } this . removeWikilinks ( fileMeta . hash ) ; this . addWikilinks ( fileMeta , doc ) ; } removeWikilinks ( fileHash ) { this . _doc2tags . delete ( fileHash ) ; for ( let [ tag , docs ] of this . _tag2docs ) { docs . delete ( fileHash ) ; if ( docs . size == 0 ) { this . _tag2docs . delete ( tag ) ; } } for ( let [ tag , defs ] of this . _tag2defs ) { defs . delete ( fileHash ) ; if ( defs . size == 0 ) { this . _tag2defs . delete ( tag ) ; } } } addWikilinks ( fileMeta , doc ) { let definedTags = this . _getTagsDefinedBy ( { fileMeta , doc } ) ; let mentionedTags = this . _getTagsMentionedBy ( { fileMeta , doc } ) ; let tags = new Set < string > ( [ ... definedTags , ... mentionedTags ] ) ; console . log ( "xref :: addWikilinks ::" , tags . values ( ) ) ; this . _doc2tags . set ( fileMeta . hash , tags ) ; for ( let tag of tags ) { this . _tag2docs . get ( tag ) . add ( fileMeta . hash ) ; } for ( let tag of definedTags ) { this . _tag2defs . get ( tag ) . add ( fileMeta . hash ) ; } } private _getTagsDefinedBy ( data ) { let tags = [ ] ; if ( data . doc ) { tags = tags . concat ( data . doc . getTagsDefined ( ) . map ( tag => this . normalizeTag ( tag ) ) ) ; } if ( data . fileMeta ) { let fileName = path . basename ( data . fileMeta . path , path . extname ( data . fileMeta . path ) ) ; tags . push ( this . normalizeTag ( fileName ) ) ; } todo return tags ; } private _getTagsMentionedBy ( data ) { let tags = [ ] ; if ( data . doc ) { tags = tags . concat ( data . doc . getTagsMentioned ( ) . map ( tag => this . normalizeTag ( tag ) ) ) ; } return tags ; } normalizeDate ( date ) { return date . toDateString ( ) . toLowerCase ( ) ; } normalizeTag ( content ) { let date = new Date ( content ) ; if ( ! isNaN ( date . valueOf ( ) ) ) { return this . normalizeDate ( date ) ; } return content . trim ( ) . toLowerCase ( ) . replace ( /[\s-:_]/ , "-" ) ; } fuzzyTagSearch ( query ) { let tags = Array . from ( this . _tag2docs . keys ( ) ) ; let tagResults = fuzzysort . go ( query , tags , { allowTypo : true , limit : 10 , threshold : - 1000 } ) ; return tagResults . map ( ( result ) => { let hl = fuzzysort . highlight ( result , "<b>" , "</b>" ) ; return { type : "tag-result" , result : result . target , resultEmphasized : hl || result . target } ; } ) ; } serialize ( ) { return JSON . stringify ( { doc2tags : serializeSetMap ( this . _doc2tags ) , tag2docs : serializeSetMap ( this . _tag2docs ) , tag2defs : serializeSetMap ( this . _tag2defs ) } ) ; } deserialize ( serialized ) { let json = JSON . parse ( serialized ) ; this . _doc2tags = deserializeSetMap ( json . doc2tags ) ; this . _tag2docs = deserializeSetMap ( json . tag2docs ) ; this . _tag2defs = deserializeSetMap ( json . tag2defs ) ; todo return this ; } } type RenderCitationFn = ( contentStr : string , attrs : { pandocSyntax ? : boolean ; } ) => Promise < string | null > ; interface ICitationPluginState { } interface ICitationPluginOptions { /**
     * Given the contents of a citation node, the plugin can return an
     * alternative label to display in place of the raw citation syntax.
     *
     * The plugin may decline to relabel the node by returning NULL.
     */ renderCitation : RenderCitationFn ; handleCitationOpen : ( citation_text : string ) => Promise < void > ; } see https function createCitationView ( renderCitation ) { return ( node , view , getPos ) : CitationView => { todo let pluginState = citationPluginKey . getState ( view . state ) ; if ( ! pluginState ) { throw new Error ( "no citation plugin!" ) ; } let nodeView = new CitationView ( node , view , getPos as ( ( ) => number ) , { renderCitation } ) ; return nodeView ; } ; } let citationPluginKey = new PluginKey < ICitationPluginState > ( "noteworthy-citations" ) ; export const citationPlugin = ( options ) : ProsePlugin < ICitationPluginState > => { let citationPluginSpec = { key : citationPluginKey , state : { init ( config , instance ) { return { macros : { } , activeNodeViews : [ ] } ; } , apply ( tr , value , oldState , newState ) { todo let pluginState = citationPluginKey . getState ( oldState ) ; return value ; } , } , props : { handleClickOn : ( view , pos , node , nodePos , event , direct ) => { todo if ( event . ctrlKey && node . type . name == "citation" ) { options . handleCitationOpen ( node . textContent ) ; return true ; } return false ; } , todo nodeViews : { "citation" : createCitationView ( options . renderCitation ) } } } ; return new ProsePlugin ( citationPluginSpec ) ; } ; interface ICitationViewOptions { /** Dom element name to use for this NodeView */ tagName ? : string ; /**
     * When rendering a citation node, can return a string to
     * render in place of the raw citation syntax.  Returning NULL
     * indicates that the raw citation syntax should be used after all.
     */ renderCitation : RenderCitationFn ; } export class CitationView implements NodeView { private _node : ProseNode ; private _outerView : EditorView ; private _getPos : ( ( ) => number ) ; dom : HTMLElement ; private _nodeRenderElt : HTMLElement | undefined ; private _nodeSrcElt : HTMLElement | undefined ; private _innerView : EditorView | undefined ; cursorSide : "start" | "end" ; private _tagName : string ; private _isEditing : boolean ; private _onDestroy : ( ( ) => void ) | undefined ; private _renderCitation : RenderCitationFn ; param onDestroy option constructor ( node , view , getPos , options , onDestroy ? ) { this . _node = node ; this . _outerView = view ; this . _getPos = getPos ; this . _onDestroy = onDestroy && onDestroy . bind ( this ) ; this . cursorSide = "start" ; this . _isEditing = false ; this . _renderCitation = options . renderCitation ; this . _tagName = options . tagName || this . _node . type . name . replace ( "_" , "-" ) ; this . dom = document . createElement ( this . _tagName ) ; let citeStyle = ( this . _node . attrs . pandocSyntax === true ) ? "citation-pandoc" : "citation-alt" ; this . dom . classList . add ( "citation" , "node-wysiwym" , citeStyle ) ; this . _nodeRenderElt = document . createElement ( "span" ) ; this . _nodeRenderElt . textContent = "" ; this . _nodeRenderElt . classList . add ( "node-render" ) ; this . dom . appendChild ( this . _nodeRenderElt ) ; this . _nodeSrcElt = document . createElement ( "span" ) ; this . _nodeSrcElt . classList . add ( "node-src" ) ; this . dom . appendChild ( this . _nodeSrcElt ) ; this . render ( ) ; } destroy ( ) { this . closeEditor ( false ) ; if ( this . _nodeRenderElt ) { this . _nodeRenderElt . remove ( ) ; delete this . _nodeRenderElt ; } if ( this . _nodeSrcElt ) { this . _nodeSrcElt . remove ( ) ; delete this . _nodeSrcElt ; } } /**
     * Ensure focus on the inner editor whenever this node has focus.
     * This helps to prevent accidental deletions of math blocks.
     */ ensureFocus ( ) { if ( this . _innerView && this . _outerView . hasFocus ( ) ) { this . _innerView . focus ( ) ; } } update ( node , decorations ) { if ( ! node . sameMarkup ( this . _node ) ) return false ; this . _node = node ; if ( this . _innerView ) { let state = this . _innerView . state ; let start = node . content . findDiffStart ( state . doc . content ) ; if ( start != null ) { let diff = node . content . findDiffEnd ( state . doc . content as any ) ; if ( diff ) { let { a : endA , b : endB } = diff ; let overlap = start - Math . min ( endA , endB ) ; if ( overlap > 0 ) { endA += overlap ; endB += overlap ; } this . _innerView . dispatch ( state . tr . replace ( start , endB , node . slice ( start , endA ) ) . setMeta ( "fromOutside" , true ) ) ; } } } if ( ! this . _isEditing ) { this . render ( ) ; } return true ; } updateCursorPos ( state ) { const pos = this . _getPos ( ) ; const size = this . _node . nodeSize ; const inPmSelection = ( state . selection . from < pos + size ) && ( pos < state . selection . to ) ; if ( ! inPmSelection ) { this . cursorSide = ( pos < state . selection . from ) ? "end" : "start" ; } } selectNode ( ) { this . dom . classList . add ( "pm-selected" ) ; if ( ! this . _isEditing ) { this . openEditor ( ) ; } } deselectNode ( ) { this . dom . classList . remove ( "pm-selected" ) ; if ( this . _isEditing ) { this . closeEditor ( ) ; } } stopEvent ( event ) { if ( event instanceof MouseEvent && event . ctrlKey ) { return false ; } return ( this . _innerView !== undefined ) && ( event . target !== undefined ) && this . _innerView . dom . contains ( event . target as Node ) ; } ignoreMutation ( ) { return true ; } render ( ) { todo if ( ! this . _nodeRenderElt ) { return ; } let renderElt = this . _nodeRenderElt ; let contentRaw = this . _node . content . content ; console . log ( "cite node attrs" , this . _node . attrs ) ; let pandocSyntax = ( this . _node . attrs . pandocSyntax === true ) ; let contentStr = "" ; if ( contentRaw . length > 0 && contentRaw [ 0 ] . textContent !== null ) { contentStr = contentRaw [ 0 ] . textContent . trim ( ) ; } if ( contentStr . length < 1 ) { this . dom . classList . add ( "node-empty" ) ; while ( this . _nodeRenderElt . firstChild ) { this . _nodeRenderElt . firstChild . remove ( ) ; } return ; } else { this . dom . classList . remove ( "node-empty" ) ; } this . dom . setAttribute ( "title" , contentStr ) ; renderElt . innerText = "..." ; this . _renderCitation ( contentStr , { pandocSyntax } ) . then ( ( val ) => { console . log ( `citation-view :: setting text ${ val }` ) ; renderElt . innerText = val || contentStr ; renderElt . classList . remove ( "render-error" ) ; } ) . catch ( ( reason ) => { console . error ( `citation-view :: could not render` , reason ) ; renderElt . innerText = contentStr ; renderElt . classList . add ( "render-error" ) ; } ) ; } dispatchInner ( tr ) { if ( ! this . _innerView ) { return ; } let { state , transactions } = this . _innerView . state . applyTransaction ( tr ) ; this . _innerView . updateState ( state ) ; if ( ! tr . getMeta ( "fromOutside" ) ) { let outerTr = this . _outerView . state . tr , offsetMap = StepMap . offset ( this . _getPos ( ) + 1 ) ; for ( let i = 0 ; i < transactions . length ; i ++ ) { let steps = transactions [ i ] . steps ; for ( let j = 0 ; j < steps . length ; j ++ ) { let mapped = steps [ j ] . map ( offsetMap ) ; if ( ! mapped ) { throw Error ( "step discarded!" ) ; } outerTr . step ( mapped ) ; } } if ( outerTr . docChanged ) this . _outerView . dispatch ( outerTr ) ; } } openEditor ( ) { if ( this . _innerView ) { throw Error ( "inner view should not exist!" ) ; } this . _innerView = new EditorView ( this . _nodeSrcElt , { state : EditorState . create ( { doc : this . _node , plugins : [ keymap ( { "Backspace" : chainCommands ( deleteSelection , ( state , dispatch , tr_inner ) => { if ( ! state . selection . empty ) { return false ; } if ( this . _node . textContent . length > 0 ) { return false ; } this . _outerView . dispatch ( this . _outerView . state . tr . insertText ( "" ) ) ; this . _outerView . focus ( ) ; return true ; } ) , "Ctrl-Enter" : ( state , dispatch ) => { let { to } = this . _outerView . state . selection ; let outerState = this . _outerView . state ; this . _outerView . dispatch ( outerState . tr . setSelection ( TextSelection . create ( outerState . doc , to ) ) ) ; this . _outerView . focus ( ) ; return true ; } } ) ] } ) , dispatchTransaction : this . dispatchInner . bind ( this ) } ) ; let innerState = this . _innerView . state ; this . _innerView . focus ( ) ; let pos = ( this . cursorSide == "start" ) ? 0 : this . _node . nodeSize - 2 ; this . _innerView . dispatch ( innerState . tr . setSelection ( TextSelection . create ( innerState . doc , pos ) ) ) ; this . _isEditing = true ; } param render closeEditor ( render = true ) { if ( this . _innerView ) { this . _innerView . destroy ( ) ; this . _innerView = undefined ; } if ( render ) { this . render ( ) ; } this . _isEditing = false ; } }