/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */ import * as vscode from 'vscode' ; import * as utils from './utils' ; import * as path from 'path' ; import { computeTimeForDidactFileUri , getActualUri , getContext , getTimeElementsForDidactFileUri , isAsciiDoc , sendTextToOutputChannel } from './extensionFunctions' ; import { HTMLElement } from 'node-html-parser' ; export class DidactNodeProvider implements vscode . TreeDataProvider < SimpleNode > { private _onDidChangeTreeData : vscode . EventEmitter < SimpleNode | undefined > = new vscode . EventEmitter < SimpleNode | undefined > ( ) ; readonly onDidChangeTreeData : vscode . Event < SimpleNode | undefined > = this . _onDidChangeTreeData . event ; protected treeNodes : TreeNode [ ] = [ ] ; protected retrieveTutorials = true ; public resetList ( ) { this . treeNodes = [ ] ; } public setRetrieveIntegrations ( flag ) { this . retrieveTutorials = flag ; } public async getChildren ( element ? ) { if ( ! element ) { return this . treeNodes ; } else if ( element instanceof TutorialNode ) { return this . processHeadingsForTutorial ( element . uri , element . category ) ; } else { if ( element . label && ! ( element instanceof TutorialNode ) ) { const tutorialCategory = retrieveTreeItemName ( element ) ; return this . processTutorialsForCategory ( tutorialCategory ) ; } } return [ ] ; } public addChild ( newNode , disableRefresh = false , oldNodes = this . treeNodes ) { if ( oldNodes ) { oldNodes . push ( newNode ) ; if ( ! disableRefresh ) { this . refresh ( ) ; } } return oldNodes ; } public removeChild ( oldNode , disableRefresh = false , oldNodes = this . treeNodes ) { if ( oldNodes ) { const index = oldNodes . indexOf ( oldNode ) ; if ( index !== - 1 ) { oldNodes . splice ( index , 1 ) ; if ( ! disableRefresh ) { this . refresh ( ) ; } } } return oldNodes ; } public refresh ( ) { this . resetList ( ) ; if ( this . retrieveTutorials ) { this . processRegisteredTutorials ( ) ; } this . _onDidChangeTreeData . fire ( undefined ) ; } public async getParent ( element ) { if ( element instanceof TutorialNode ) { const tutorial = element ; if ( tutorial . category ) { return Promise . resolve ( this . findCategoryNode ( tutorial . category ) ) ; } } if ( element instanceof HeadingNode ) { if ( element . category && element . uri ) { return this . findParentTutorialNode ( element . category , element . uri ) ; } } return Promise . resolve ( null ) ; } getTreeItem ( node ) { return node ; } getCategory ( oldNodes , newNode ) { for ( const node of oldNodes ) { if ( node . label === newNode . label ) { return node ; } } return null ; } doesNodeExist ( oldNodes , newNode ) { for ( const node of oldNodes ) { if ( node . label === newNode . label ) { return true ; } } return false ; } processRegisteredTutorials ( ) { const categories = utils . getDidactCategories ( ) ; if ( categories ) { for ( const category of categories ) { const newNode = new TreeNode ( category , category , undefined , vscode . TreeItemCollapsibleState . Collapsed ) ; if ( ! this . doesNodeExist ( this . treeNodes , newNode ) ) { this . addChild ( newNode , true , this . treeNodes ) ; } } } } async processTutorialsForCategory ( category ) { const children = [ ] ; if ( category ) { const tutorials = utils . getTutorialsForCategory ( category ) ; if ( tutorials ) { for ( const tutorial of tutorials ) { let hasChildren = vscode . TreeItemCollapsibleState . None ; const tutUri = utils . getUriForDidactNameAndCategory ( tutorial , category ) ; const actualUri = getActualUri ( tutUri ) ; let timeLabel = undefined ; if ( actualUri ) { const timeCount = await computeTimeForDidactFileUri ( actualUri ) ; if ( timeCount > 0 ) { timeLabel = `(~${ timeCount } mins)` ; hasChildren = vscode . TreeItemCollapsibleState . Collapsed ; } } const newNode = new TutorialNode ( category , tutorial , tutUri , hasChildren , timeLabel ) ; newNode . contextValue = 'TutorialNode' ; if ( ! this . doesNodeExist ( children , newNode ) ) { this . addChild ( newNode , true , children ) ; } } } } return children ; } getNodeFromADOCDiv ( divElement , tutUri , category ) { const classAttr = divElement . getAttribute ( "class" ) ; if ( classAttr ) { const splitArray = classAttr . split ( ' ' ) ; for ( const chunk of splitArray ) { if ( chunk . startsWith ( 'time=' ) ) { const splitTime = chunk . split ( '=' ) [ 1 ] ; const timeValue = Number ( splitTime ) ; if ( divElement . childNodes . length > 0 ) { const children = divElement . childNodes ; for ( const rawChild of children ) { if ( rawChild instanceof HTMLElement ) { const child = rawChild ; if ( child . tagName . startsWith ( 'H' ) ) { const title = rawChild . innerText ; if ( ! Number . isNaN ( timeValue ) ) { return new HeadingNode ( category , title , tutUri , `(~${ timeValue } mins)` ) ; } else { sendTextToOutputChannel ( `Warning: Heading node "${ title }" has an invalid time value set to "${ splitTime }"` ) ; } } } } } } } } return undefined ; } async processHeadingsForTutorial ( tutUri , category ) { const children = [ ] ; const actualUri = getActualUri ( tutUri ) ; if ( actualUri ) { const headings = await getTimeElementsForDidactFileUri ( actualUri ) ; if ( headings && ! isAsciiDoc ( tutUri ) ) { for ( const heading of headings ) { const timeAttr = heading . getAttribute ( "time" ) ; let timeLabel = undefined ; const timeValue = Number ( timeAttr ) ; if ( ! Number . isNaN ( timeValue ) ) { timeLabel = `(~${ timeValue } mins)` ; } const title = heading . rawText ; const newNode = new HeadingNode ( category , title , tutUri , timeLabel ) ; newNode . contextValue = 'HeadingNode' ; if ( ! this . doesNodeExist ( children , newNode ) && timeLabel ) { this . addChild ( newNode , true , children ) ; } if ( Number . isNaN ( timeValue ) ) { sendTextToOutputChannel ( `Warning: Heading node "${ title }" has an invalid time value set to "${ timeAttr }"` ) ; } } return children ; } else if ( headings && isAsciiDoc ( tutUri ) ) { for ( const heading of headings ) { const newNode = this . getNodeFromADOCDiv ( heading , tutUri , category ) ; if ( newNode && ! this . doesNodeExist ( children , newNode ) ) { newNode . contextValue = 'HeadingNode' ; this . addChild ( newNode , true , children ) ; } } return children ; } } return children ; } findCategoryNode ( category ) { const nodeToFind = new TreeNode ( category , category , undefined , vscode . TreeItemCollapsibleState . Collapsed ) ; return this . getCategory ( this . treeNodes , nodeToFind ) ; } async findTutorialNode ( category , tutorialName ) { const catNode = this . findCategoryNode ( category ) ; if ( catNode ) { const treeItems = await this . getChildren ( catNode ) ; let foundNode = undefined ; treeItems . forEach ( element => { if ( element . label === tutorialName && element . category === category ) { foundNode = element ; } } ) ; return Promise . resolve ( foundNode ) ; } return Promise . resolve ( undefined ) ; } async findParentTutorialNode ( category , uri ) { const catNode = this . findCategoryNode ( category ) ; if ( catNode ) { const treeItems = await this . getChildren ( catNode ) ; let foundNode = null ; for ( const element of treeItems ) { if ( element . uri === uri && element . category === category ) { foundNode = element ; break ; } } return Promise . resolve ( foundNode ) ; } return Promise . resolve ( null ) ; } } export class SimpleNode extends vscode . TreeItem { category : string | undefined ; uri : string | undefined ; constructor ( type , label , uri ) { super ( label , vscode . TreeItemCollapsibleState . None ) ; this . uri = uri ; this . category = type ; } toString ( ) { return `${ this . category } ${ this . label } ${ this . uri }` ; } } export class TreeNode extends SimpleNode { uri : string | undefined ; constructor ( type , label , uri , collapsibleState ) { super ( type , label , uri ) ; this . collapsibleState = collapsibleState ; this . uri = uri ; } } export class TutorialNode extends TreeNode { constructor ( category , label , uri , collapsibleState , inTooltip ? ) { super ( category , label , uri , collapsibleState ) ; const localIconPath = vscode . Uri . file ( path . resolve ( getContext ( ) . extensionPath , 'icon/logo.svg' ) ) ; this . iconPath = localIconPath ; if ( inTooltip ) { this . description = inTooltip ; } } } export class HeadingNode extends SimpleNode { uri : string | undefined ; constructor ( category , label , uri , description ) { super ( category , label , uri ) ; this . description = description ; } } function retrieveTreeItemName ( selection ) { const treeItemName = selection . label ; if ( treeItemName === undefined ) { return "" ; } else if ( typeof treeItemName === "string" ) { return treeItemName ; } else { return treeItemName . label ; } }