From 036faeb06d66784a27288908d32ca618580d81e6 Mon Sep 17 00:00:00 2001 From: Barbara Valdez <34872381+barbaravaldez@users.noreply.github.com> Date: Mon, 2 Nov 2020 14:55:44 -0800 Subject: [PATCH] [Backend] Editing and creating books (#13089) * Add interface for modifying the table of contents of books * Add logic for creating toc * Fix issue with toc * Add test for creating toc * Delete bookTocManager.test.ts * update allowed extensions * Fix failing tests and add test * Add tests for creating books * Remove unused methods * add section to section --- .../notebook/src/book/bookTocManager.ts | 181 +++++++++ extensions/notebook/src/book/bookTreeItem.ts | 20 +- extensions/notebook/src/book/bookTreeView.ts | 13 + .../src/test/book/bookTocManager.test.ts | 371 ++++++++++++++++++ 4 files changed, 583 insertions(+), 2 deletions(-) create mode 100644 extensions/notebook/src/book/bookTocManager.ts create mode 100644 extensions/notebook/src/test/book/bookTocManager.test.ts diff --git a/extensions/notebook/src/book/bookTocManager.ts b/extensions/notebook/src/book/bookTocManager.ts new file mode 100644 index 0000000000..8573560b7a --- /dev/null +++ b/extensions/notebook/src/book/bookTocManager.ts @@ -0,0 +1,181 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as path from 'path'; +import { BookTreeItem } from './bookTreeItem'; +import * as yaml from 'js-yaml'; +import * as fs from 'fs-extra'; +import { IJupyterBookSectionV1, IJupyterBookSectionV2, JupyterBookSection } from '../contracts/content'; +import { BookVersion } from './bookModel'; +import * as vscode from 'vscode'; + +export interface IBookTocManager { + updateBook(element: BookTreeItem, book: BookTreeItem): Promise; + createBook(bookContentPath: string, contentFolder: string): Promise +} +const allowedFileExtensions: string[] = ['.md', '.ipynb']; +const initMarkdown: string[] = ['index.md', 'introduction.md', 'intro.md', 'readme.md']; + +export function hasSections(node: JupyterBookSection): boolean { + return node.sections !== undefined && node.sections.length > 0; +} + +export class BookTocManager implements IBookTocManager { + public tableofContents: IJupyterBookSectionV2[]; + public newSection: JupyterBookSection = {}; + + constructor() { + } + + async getAllFiles(toc: IJupyterBookSectionV2[], directory: string, filesInDir: string[], rootDirectory: string): Promise { + await Promise.all(filesInDir.map(async file => { + let isDirectory = (await fs.promises.stat(path.join(directory, file))).isDirectory(); + if (isDirectory) { + let files = await fs.promises.readdir(path.join(directory, file)); + let initFile: string = ''; + //Add files named as readme or index within the directory as the first file of the section. + files.some((f, index) => { + if (initMarkdown.includes(f)) { + initFile = path.parse(f).name; + files.splice(index, 1); + } + }); + let jupyterSection: IJupyterBookSectionV2 = { + title: file, + file: path.join(file, initFile), + expand_sections: true, + numbered: false, + sections: [] + }; + toc.push(jupyterSection); + await this.getAllFiles(toc, path.join(directory, file), files, rootDirectory); + } else if (allowedFileExtensions.includes(path.extname(file))) { + // if the file is in the book root we don't include the directory. + const filePath = directory === rootDirectory ? path.parse(file).name : path.join(path.basename(directory), path.parse(file).name); + const addFile: IJupyterBookSectionV2 = { + title: path.parse(file).name, + file: filePath + }; + //find if the directory (section) of the file exists else just add the file at the end of the table of contents + let indexToc = toc.findIndex(parent => parent.title === path.basename(directory)); + //if there is not init markdown file then add the first notebook or markdown file that is found + if (indexToc !== -1) { + if (toc[indexToc].file === '') { + toc[indexToc].file = addFile.file; + } else { + toc[indexToc].sections.push(addFile); + } + } else { + toc.push(addFile); + } + } + })); + return toc; + } + + updateToc(tableOfContents: JupyterBookSection[], findSection: BookTreeItem, addSection: JupyterBookSection): JupyterBookSection[] { + for (const section of tableOfContents) { + if ((section as IJupyterBookSectionV1).url && path.dirname(section.url) === path.join(path.sep, path.dirname(findSection.uri)) || (section as IJupyterBookSectionV2).file && path.dirname((section as IJupyterBookSectionV2).file) === path.join(path.sep, path.dirname(findSection.uri))) { + if (tableOfContents[tableOfContents.length - 1].sections) { + tableOfContents[tableOfContents.length - 1].sections.push(addSection); + } else { + tableOfContents[tableOfContents.length - 1].sections = [addSection]; + } + break; + } + else if (hasSections(section)) { + return this.updateToc(section.sections, findSection, addSection); + } + } + return tableOfContents; + } + + /** + * Follows the same logic as the JupyterBooksCreate.ipynb. It receives a path that contains a notebooks and + * a path where it creates the book. It copies the contents from one folder to another and creates a table of contents. + * @param bookContentPath The path to the book folder, the basename of the path is the name of the book + * @param contentFolder The path to the folder that contains the notebooks and markdown files to be added to the created book. + */ + public async createBook(bookContentPath: string, contentFolder: string): Promise { + await fs.promises.mkdir(bookContentPath); + await fs.copy(contentFolder, bookContentPath); + let filesinDir = await fs.readdir(bookContentPath); + this.tableofContents = await this.getAllFiles([], bookContentPath, filesinDir, bookContentPath); + await fs.writeFile(path.join(bookContentPath, '_config.yml'), yaml.safeDump({ title: path.basename(bookContentPath) })); + await fs.writeFile(path.join(bookContentPath, '_toc.yml'), yaml.safeDump(this.tableofContents, { lineWidth: Infinity })); + await vscode.commands.executeCommand('notebook.command.openNotebookFolder', bookContentPath, undefined, true); + } + + async addSection(section: BookTreeItem, book: BookTreeItem, isSection: boolean): Promise { + this.newSection.title = section.title; + //the book contentPath contains the first file of the section, we get the dirname to identify the section's root path + const rootPath = isSection ? path.dirname(book.book.contentPath) : book.rootContentPath; + // TODO: the uri contains the first notebook or markdown file in the TOC format. If we are in a section, + // we want to include the intermediary directories between the book's root and the section + const uri = isSection ? path.join(path.basename(rootPath), section.uri) : section.uri; + if (section.book.version === BookVersion.v1) { + this.newSection.url = uri; + let movedSections: IJupyterBookSectionV1[] = []; + const files = section.sections as IJupyterBookSectionV1[]; + for (const elem of files) { + await fs.promises.mkdir(path.join(rootPath, path.dirname(elem.url)), { recursive: true }); + await fs.move(path.join(path.dirname(section.book.contentPath), path.basename(elem.url)), path.join(rootPath, elem.url)); + movedSections.push({ url: isSection ? path.join(path.basename(rootPath), elem.url) : elem.url, title: elem.title }); + } + this.newSection.sections = movedSections; + } else if (section.book.version === BookVersion.v2) { + (this.newSection as IJupyterBookSectionV2).file = uri; + let movedSections: IJupyterBookSectionV2[] = []; + const files = section.sections as IJupyterBookSectionV2[]; + for (const elem of files) { + await fs.promises.mkdir(path.join(rootPath, path.dirname(elem.file)), { recursive: true }); + await fs.move(path.join(path.dirname(section.book.contentPath), path.basename(elem.file)), path.join(rootPath, elem.file)); + movedSections.push({ file: isSection ? path.join(path.basename(rootPath), elem.file) : elem.file, title: elem.title }); + } + this.newSection.sections = movedSections; + } + } + + async addNotebook(notebook: BookTreeItem, book: BookTreeItem, isSection: boolean): Promise { + //the book's contentPath contains the first file of the section, we get the dirname to identify the section's root path + const rootPath = isSection ? path.dirname(book.book.contentPath) : book.rootContentPath; + let notebookName = path.basename(notebook.book.contentPath); + await fs.move(notebook.book.contentPath, path.join(rootPath, notebookName)); + if (book.book.version === BookVersion.v1) { + this.newSection = { url: notebookName, title: notebookName }; + } else if (book.book.version === BookVersion.v2) { + this.newSection = { file: notebookName, title: notebookName }; + } + } + + /** + * Moves the element to the book's folder and adds it to the table of contents. + * @param element Notebook, Markdown File, or section that will be added to the book. + * @param book Book or a BookSection that will be modified. + */ + public async updateBook(element: BookTreeItem, book: BookTreeItem): Promise { + if (element.contextValue === 'section' && book.book.version === element.book.version) { + if (book.contextValue === 'section') { + await this.addSection(element, book, true); + this.tableofContents = this.updateToc(book.tableOfContents.sections, book, this.newSection); + await fs.writeFile(book.tableOfContentsPath, yaml.safeDump(this.tableofContents, { lineWidth: Infinity })); + } else if (book.contextValue === 'savedBook') { + await this.addSection(element, book, false); + book.tableOfContents.sections.push(this.newSection); + await fs.writeFile(book.tableOfContentsPath, yaml.safeDump(book.tableOfContents, { lineWidth: Infinity })); + } + } + else if (element.contextValue === 'savedNotebook') { + if (book.contextValue === 'savedBook') { + await this.addNotebook(element, book, false); + book.tableOfContents.sections.push(this.newSection); + await fs.writeFile(book.tableOfContentsPath, yaml.safeDump(book.tableOfContents, { lineWidth: Infinity })); + } else if (book.contextValue === 'section') { + await this.addNotebook(element, book, true); + this.tableofContents = this.updateToc(book.tableOfContents.sections, book, this.newSection); + await fs.writeFile(book.tableOfContentsPath, yaml.safeDump(this.tableofContents, { lineWidth: Infinity })); + } + } + } +} diff --git a/extensions/notebook/src/book/bookTreeItem.ts b/extensions/notebook/src/book/bookTreeItem.ts index e9680bec4b..3ef362e191 100644 --- a/extensions/notebook/src/book/bookTreeItem.ts +++ b/extensions/notebook/src/book/bookTreeItem.ts @@ -40,6 +40,8 @@ export class BookTreeItem extends vscode.TreeItem { public readonly version: string; public command: vscode.Command; public resourceUri: vscode.Uri; + private _rootContentPath: string; + private _tableOfContentsPath: string; constructor(public book: BookTreeItemFormat, icons: any) { super(book.title, book.treeItemCollapsibleState); @@ -74,7 +76,9 @@ export class BookTreeItem extends vscode.TreeItem { this.tooltip = `${this._uri}`; } else { - this.tooltip = this.book.type === BookTreeItemType.Book ? (this.book.version === BookVersion.v1 ? path.join(this.book.root, content) : this.book.root) : this.book.contentPath; + this._tableOfContentsPath = (this.book.type === BookTreeItemType.Book || this.contextValue === 'section') ? (this.book.version === BookVersion.v1 ? path.join(this.book.root, '_data', 'toc.yml') : path.join(this.book.root, '_toc.yml')) : undefined; + this._rootContentPath = this.book.version === BookVersion.v1 ? path.join(this.book.root, content) : this.book.root; + this.tooltip = this.book.type === BookTreeItemType.Book ? this._rootContentPath : this.book.contentPath; this.resourceUri = vscode.Uri.file(this.book.root); } } @@ -158,6 +162,14 @@ export class BookTreeItem extends vscode.TreeItem { return this.book.root; } + public get rootContentPath(): string { + return this._rootContentPath; + } + + public get tableOfContentsPath(): string { + return this._tableOfContentsPath; + } + public get tableOfContents(): IJupyterBookToc { return this.book.tableOfContents; } @@ -176,6 +188,10 @@ export class BookTreeItem extends vscode.TreeItem { public readonly tooltip: string; + public set uri(uri: string) { + this._uri = uri; + } + /** * Helper method to find a child section with a specified URL * @param url The url of the section we're searching for @@ -188,7 +204,7 @@ export class BookTreeItem extends vscode.TreeItem { } private findChildSectionRecur(section: JupyterBookSection, url: string): JupyterBookSection | undefined { - if (section.url && section.url === url) { + if ((section as IJupyterBookSectionV1).url && (section as IJupyterBookSectionV1).url === url || (section as IJupyterBookSectionV2).file && (section as IJupyterBookSectionV2).file === url) { return section; } else if (section.sections) { for (const childSection of section.sections) { diff --git a/extensions/notebook/src/book/bookTreeView.ts b/extensions/notebook/src/book/bookTreeView.ts index 1dfe0d106d..f3d9eb85a8 100644 --- a/extensions/notebook/src/book/bookTreeView.ts +++ b/extensions/notebook/src/book/bookTreeView.ts @@ -19,6 +19,7 @@ import * as glob from 'fast-glob'; import { IJupyterBookSectionV2, IJupyterBookSectionV1 } from '../contracts/content'; import { debounce, getPinnedNotebooks } from '../common/utils'; import { IBookPinManager, BookPinManager } from './bookPinManager'; +import { BookTocManager, IBookTocManager } from './bookTocManager'; const content = 'content'; @@ -36,6 +37,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider; public viewId: string; @@ -47,6 +49,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider console.error(e)); this.prompter = new CodeAdapter(); @@ -131,6 +134,16 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider { + bookPath = path.normalize(bookPath); + contentPath = path.normalize(contentPath); + await this.bookTocManager.createBook(bookPath, contentPath); + } + + async editBook(book: BookTreeItem, section: BookTreeItem): Promise { + await this.bookTocManager.updateBook(section, book); + } + async openBook(bookPath: string, urlToOpen?: string, showPreview?: boolean, isNotebook?: boolean): Promise { try { // Convert path to posix style for easier comparisons diff --git a/extensions/notebook/src/test/book/bookTocManager.test.ts b/extensions/notebook/src/test/book/bookTocManager.test.ts new file mode 100644 index 0000000000..e5550de735 --- /dev/null +++ b/extensions/notebook/src/test/book/bookTocManager.test.ts @@ -0,0 +1,371 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as should from 'should'; +import * as path from 'path'; +import { BookTocManager, hasSections } from '../../book/bookTocManager'; +import { BookTreeItem, BookTreeItemFormat, BookTreeItemType } from '../../book/bookTreeItem'; +import * as yaml from 'js-yaml'; +import * as sinon from 'sinon'; +import { IJupyterBookSectionV1, IJupyterBookSectionV2, JupyterBookSection } from '../../contracts/content'; +import * as fs from 'fs-extra'; +import * as os from 'os'; +import * as uuid from 'uuid'; + +export function equalTOC(actualToc: IJupyterBookSectionV2[], expectedToc: IJupyterBookSectionV2[]): boolean { + for (let [i, section] of actualToc.entries()) { + if (section.title !== expectedToc[i].title || section.file !== expectedToc[i].file) { + return false; + } + } + return true; +} + +export function equalSections(actualSection: JupyterBookSection, expectedSection: JupyterBookSection): boolean { + let equalFiles = ((actualSection as IJupyterBookSectionV1).url === (expectedSection as IJupyterBookSectionV1).url || (actualSection as IJupyterBookSectionV2).file === (expectedSection as IJupyterBookSectionV2).file); + if (actualSection.title === expectedSection.title && equalFiles && + hasSections(actualSection) && hasSections(expectedSection)) { + for (const [index, section] of actualSection.sections.entries()) { + equalSections(section, expectedSection.sections[index]); + } + } else { + return false; + } + return true; +} + +describe('BookTocManagerTests', function () { + describe('CreatingBooks', () => { + let notebooks: string[]; + let bookFolderPath: string; + let rootFolderPath: string; + let root2FolderPath: string; + const subfolder = 'Subfolder' + + afterEach(function (): void { + sinon.restore(); + }); + + beforeEach(async () => { + rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`); + bookFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`); + root2FolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`); + notebooks = ['notebook1.ipynb', 'notebook2.ipynb', 'notebook3.ipynb', 'index.md', 'readme.md']; + + await fs.mkdir(rootFolderPath); + await fs.writeFile(path.join(rootFolderPath, notebooks[0]), ''); + await fs.writeFile(path.join(rootFolderPath, notebooks[1]), ''); + await fs.writeFile(path.join(rootFolderPath, notebooks[2]), ''); + await fs.writeFile(path.join(rootFolderPath, notebooks[3]), ''); + + await fs.mkdir(root2FolderPath); + await fs.mkdir(path.join(root2FolderPath, subfolder)); + await fs.writeFile(path.join(root2FolderPath, notebooks[0]), ''); + await fs.writeFile(path.join(root2FolderPath, subfolder, notebooks[1]), ''); + await fs.writeFile(path.join(root2FolderPath, subfolder, notebooks[2]), ''); + await fs.writeFile(path.join(root2FolderPath, subfolder, notebooks[4]), ''); + await fs.writeFile(path.join(root2FolderPath, notebooks[3]), ''); + }); + + it('should create a table of contents with no sections if there are only notebooks in folder', async function (): Promise { + let bookTocManager: BookTocManager = new BookTocManager(); + await bookTocManager.createBook(bookFolderPath, rootFolderPath); + let listFiles = await fs.promises.readdir(bookFolderPath); + should(bookTocManager.tableofContents.length).be.equal(4); + should(listFiles.length).be.equal(6); + }); + + it('should create a table of contents with sections if folder contains submodules', async () => { + let bookTocManager: BookTocManager = new BookTocManager(); + let expectedSection: IJupyterBookSectionV2[] = [{ + title: 'notebook2', + file: path.join(subfolder, 'notebook2') + }, + { + title: 'notebook3', + file: path.join(subfolder, 'notebook3') + }]; + await bookTocManager.createBook(bookFolderPath, root2FolderPath); + should(equalTOC(bookTocManager.tableofContents[2].sections, expectedSection)).be.true; + should(bookTocManager.tableofContents[2].file).be.equal(path.join(subfolder, 'readme')); + }); + + it('should ignore invalid file extensions', async () => { + await fs.writeFile(path.join(rootFolderPath, 'test.txt'), ''); + let bookTocManager: BookTocManager = new BookTocManager(); + await bookTocManager.createBook(bookFolderPath, rootFolderPath); + let listFiles = await fs.promises.readdir(bookFolderPath); + should(bookTocManager.tableofContents.length).be.equal(4); + should(listFiles.length).be.equal(7); + }); + }); + + describe('EditingBooks', () => { + let book: BookTreeItem; + let bookSection: BookTreeItem; + let bookSection2: BookTreeItem; + let notebook: BookTreeItem; + let rootBookFolderPath: string = path.join(os.tmpdir(), uuid.v4(), 'Book'); + let rootSectionFolderPath: string = path.join(os.tmpdir(), uuid.v4(), 'BookSection'); + let rootSection2FolderPath: string = path.join(os.tmpdir(), uuid.v4(), 'BookSection2'); + let notebookFolder: string = path.join(os.tmpdir(), uuid.v4(), 'Notebook'); + let bookTocManager: BookTocManager; + + let runs = [ + { + it: 'using the jupyter-book legacy version < 0.7.0', + version: 'v1', + url: 'file', + book: { + 'rootBookFolderPath': rootBookFolderPath, + 'bookContentFolderPath': path.join(rootBookFolderPath, 'content', 'sample'), + 'bookDataFolderPath': path.join(rootBookFolderPath, '_data'), + 'notebook1': path.join(rootBookFolderPath, 'content', 'notebook'), + 'notebook2': path.join(rootBookFolderPath, 'content', 'notebook2'), + 'tocPath': path.join(rootBookFolderPath, '_data', 'toc.yml') + }, + bookSection1: { + 'contentPath': path.join(rootSectionFolderPath, 'content', 'sample', 'readme.md'), + 'sectionRoot': rootSectionFolderPath, + 'sectionName': 'Sample', + 'bookContentFolderPath': path.join(rootSectionFolderPath, 'content', 'sample'), + 'bookDataFolderPath': path.join(rootSectionFolderPath, '_data'), + 'notebook3': path.join(rootSectionFolderPath, 'content', 'sample', 'notebook3'), + 'notebook4': path.join(rootSectionFolderPath, 'content', 'sample', 'notebook4'), + 'tocPath': path.join(rootSectionFolderPath, '_data', 'toc.yml') + }, + bookSection2: { + 'contentPath': path.join(rootSection2FolderPath, 'content', 'test', 'readme.md'), + 'sectionRoot': rootSection2FolderPath, + 'sectionName': 'Test', + 'bookContentFolderPath': path.join(rootSection2FolderPath, 'content', 'test'), + 'bookDataFolderPath': path.join(rootSection2FolderPath, '_data'), + 'notebook5': path.join(rootSection2FolderPath, 'content', 'test', 'notebook5'), + 'notebook6': path.join(rootSection2FolderPath, 'content', 'test', 'notebook6'), + 'tocPath': path.join(rootSection2FolderPath, '_data', 'toc.yml') + }, + notebook: { + 'contentPath': path.join(notebookFolder, 'test', 'readme.md') + }, + section: [ + { + 'title': 'Notebook', + 'url': path.join(path.sep, 'notebook') + }, + { + 'title': 'Notebook 2', + 'url': path.join(path.sep, 'notebook2') + } + ], + section1: [ + { + 'title': 'Notebook 3', + 'url': path.join('sample', 'notebook3') + }, + { + 'title': 'Notebook 4', + 'url': path.join('sample', 'notebook4') + } + ], + section2: [ + { + 'title': 'Notebook 5', + 'url': path.join(path.sep, 'test', 'notebook5') + }, + { + 'title': 'Notebook 6', + 'url': path.join(path.sep, 'test', 'notebook6') + } + ] + }, { + it: 'using jupyter-book versions >= 0.7.0', + version: 'v2', + url: 'file', + book: { + 'bookContentFolderPath': path.join(rootBookFolderPath, 'sample'), + 'rootBookFolderPath': rootBookFolderPath, + 'notebook1': path.join(rootBookFolderPath, 'notebook'), + 'notebook2': path.join(rootBookFolderPath, 'notebook2'), + 'tocPath': path.join(rootBookFolderPath, '_toc.yml') + }, + bookSection1: { + 'bookContentFolderPath': path.join(rootSectionFolderPath, 'sample'), + 'contentPath': path.join(rootSectionFolderPath, 'sample', 'readme.md'), + 'sectionRoot': rootSectionFolderPath, + 'sectionName': 'Sample', + 'notebook3': path.join(rootSectionFolderPath, 'sample', 'notebook3'), + 'notebook4': path.join(rootSectionFolderPath, 'sample', 'notebook4'), + 'tocPath': path.join(rootSectionFolderPath, '_toc.yml') + }, + bookSection2: { + 'bookContentFolderPath': path.join(rootSection2FolderPath, 'test'), + 'contentPath': path.join(rootSection2FolderPath, 'test', 'readme.md'), + 'sectionRoot': rootSection2FolderPath, + 'sectionName': 'Test', + 'notebook5': path.join(rootSection2FolderPath, 'test', 'notebook5'), + 'notebook6': path.join(rootSection2FolderPath, 'test', 'notebook6'), + 'tocPath': path.join(rootSection2FolderPath, '_toc.yml') + }, + notebook: { + 'contentPath': path.join(notebookFolder, 'test', 'readme.md') + }, + section: [ + { + 'title': 'Notebook', + 'file': path.join(path.sep, 'notebook') + }, + { + 'title': 'Notebook 2', + 'file': path.join(path.sep, 'notebook2') + } + ], + section1: [ + { + 'title': 'Notebook 3', + 'file': path.join('sample', 'notebook3') + }, + { + 'title': 'Notebook 4', + 'file': path.join('sample', 'notebook4') + } + ], + section2: [ + { + 'title': 'Notebook 5', + 'file': path.join(path.sep, 'test', 'notebook5') + }, + { + 'title': 'Notebook 6', + 'file': path.join(path.sep, 'test', 'notebook6') + } + ] + } + ]; + + runs.forEach(function (run) { + describe('Editing Books ' + run.it, function (): void { + beforeEach(async () => { + let bookTreeItemFormat1: BookTreeItemFormat = { + contentPath: run.version === 'v1' ? path.join(run.book.rootBookFolderPath, 'content', 'index.md') : path.join(run.book.rootBookFolderPath, 'index.md'), + root: run.book.rootBookFolderPath, + tableOfContents: { + sections: run.section + }, + isUntitled: undefined, + title: undefined, + treeItemCollapsibleState: undefined, + type: BookTreeItemType.Book, + version: run.version, + page: run.section + }; + + let bookTreeItemFormat2: BookTreeItemFormat = { + title: run.bookSection1.sectionName, + contentPath: run.bookSection1.contentPath, + root: run.bookSection1.sectionRoot, + tableOfContents: { + sections: run.section1 + }, + isUntitled: undefined, + treeItemCollapsibleState: undefined, + type: BookTreeItemType.Book, + version: run.version, + page: run.section1 + }; + + let bookTreeItemFormat3: BookTreeItemFormat = { + title: run.bookSection2.sectionName, + contentPath: run.bookSection2.contentPath, + root: run.bookSection2.sectionRoot, + tableOfContents: { + sections: run.section2 + }, + isUntitled: undefined, + treeItemCollapsibleState: undefined, + type: BookTreeItemType.Book, + version: run.version, + page: run.section2 + }; + + let bookTreeItemFormat4: BookTreeItemFormat = { + title: run.bookSection2.sectionName, + contentPath: run.notebook.contentPath, + root: run.bookSection2.sectionRoot, + tableOfContents: { + sections: undefined + }, + isUntitled: undefined, + treeItemCollapsibleState: undefined, + type: BookTreeItemType.Notebook, + page: { + sections: undefined + } + }; + + book = new BookTreeItem(bookTreeItemFormat1, undefined); + bookSection = new BookTreeItem(bookTreeItemFormat2, undefined); + bookSection2 = new BookTreeItem(bookTreeItemFormat3, undefined); + notebook = new BookTreeItem(bookTreeItemFormat4, undefined); + bookTocManager = new BookTocManager(); + + bookSection.uri = path.join('sample', 'readme'); + bookSection2.uri = path.join('test', 'readme'); + + book.contextValue = 'savedBook'; + bookSection.contextValue = 'section'; + bookSection2.contextValue = 'section'; + notebook.contextValue = 'savedNotebook'; + + + await fs.promises.mkdir(run.book.bookContentFolderPath, { recursive: true }); + await fs.promises.mkdir(run.bookSection1.bookContentFolderPath, { recursive: true }); + await fs.promises.mkdir(run.bookSection2.bookContentFolderPath, { recursive: true }); + await fs.promises.mkdir(path.dirname(run.notebook.contentPath), { recursive: true }); + + if (run.book.bookDataFolderPath && run.bookSection1.bookDataFolderPath && run.bookSection2.bookDataFolderPath) { + await fs.promises.mkdir(run.book.bookDataFolderPath, { recursive: true }); + await fs.promises.mkdir(run.bookSection1.bookDataFolderPath, { recursive: true }); + await fs.promises.mkdir(run.bookSection2.bookDataFolderPath, { recursive: true }); + } + await fs.writeFile(run.book.notebook1, ''); + await fs.writeFile(run.book.notebook2, ''); + await fs.writeFile(run.bookSection1.notebook3, ''); + await fs.writeFile(run.bookSection1.notebook4, ''); + await fs.writeFile(run.bookSection2.notebook5, ''); + await fs.writeFile(run.bookSection2.notebook6, ''); + await fs.writeFile(run.notebook.contentPath, ''); + }); + + it('Add section to book', async () => { + await bookTocManager.updateBook(bookSection, book); + const listFiles = await fs.promises.readdir(run.book.bookContentFolderPath); + const tocFile = await fs.promises.readFile(run.book.tocPath, 'utf8'); + let toc = yaml.safeLoad(tocFile); + should(JSON.stringify(listFiles)).be.equal(JSON.stringify(['notebook3', 'notebook4']), 'The files of the section should be moved to the books folder'); + should(equalSections(toc.sections[2], bookTocManager.newSection)).be.true; + }); + + it('Add section to section', async () => { + await bookTocManager.updateBook(bookSection, bookSection2); + let listFiles = await fs.promises.readdir(path.join(run.bookSection2.bookContentFolderPath, 'sample')); + const tocFile = await fs.promises.readFile(path.join(run.bookSection2.tocPath), 'utf8'); + let toc = yaml.safeLoad(tocFile); + should(JSON.stringify(listFiles)).be.equal(JSON.stringify(['notebook3', 'notebook4']), 'The files of the section should be moved to the books folder'); + should(equalSections(toc[1].sections, bookTocManager.newSection)).be.true; + }); + + it('Add notebook to book', async () => { + await bookTocManager.updateBook(notebook, book); + const folder = run.version === 'v1' ? path.join(run.book.rootBookFolderPath, 'content') : path.join(run.book.rootBookFolderPath); + let listFiles = await fs.promises.readdir(folder); + const tocFile = await fs.promises.readFile(run.book.tocPath, 'utf8'); + let toc = yaml.safeLoad(tocFile); + should(listFiles.findIndex(f => f === 'readme.md')).not.equal(-1); + should(equalSections(toc.sections[2], bookTocManager.newSection)).be.true; + }); + }); + }); + }); +});