mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-11 18:48:33 -05:00
[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
This commit is contained in:
181
extensions/notebook/src/book/bookTocManager.ts
Normal file
181
extensions/notebook/src/book/bookTocManager.ts
Normal file
@@ -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<void>;
|
||||
createBook(bookContentPath: string, contentFolder: string): Promise<void>
|
||||
}
|
||||
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<IJupyterBookSectionV2[]> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
//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<void> {
|
||||
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 }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<BookTreeIte
|
||||
private _openAsUntitled: boolean;
|
||||
private _bookTrustManager: IBookTrustManager;
|
||||
public bookPinManager: IBookPinManager;
|
||||
public bookTocManager: IBookTocManager;
|
||||
|
||||
private _bookViewer: vscode.TreeView<BookTreeItem>;
|
||||
public viewId: string;
|
||||
@@ -47,6 +49,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
||||
this._extensionContext = extensionContext;
|
||||
this.books = [];
|
||||
this.bookPinManager = new BookPinManager();
|
||||
this.bookTocManager = new BookTocManager();
|
||||
this.viewId = view;
|
||||
this.initialize(workspaceFolders).catch(e => console.error(e));
|
||||
this.prompter = new CodeAdapter();
|
||||
@@ -131,6 +134,16 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
||||
}
|
||||
}
|
||||
|
||||
async createBook(bookPath: string, contentPath: string): Promise<void> {
|
||||
bookPath = path.normalize(bookPath);
|
||||
contentPath = path.normalize(contentPath);
|
||||
await this.bookTocManager.createBook(bookPath, contentPath);
|
||||
}
|
||||
|
||||
async editBook(book: BookTreeItem, section: BookTreeItem): Promise<void> {
|
||||
await this.bookTocManager.updateBook(section, book);
|
||||
}
|
||||
|
||||
async openBook(bookPath: string, urlToOpen?: string, showPreview?: boolean, isNotebook?: boolean): Promise<void> {
|
||||
try {
|
||||
// Convert path to posix style for easier comparisons
|
||||
|
||||
371
extensions/notebook/src/test/book/bookTocManager.test.ts
Normal file
371
extensions/notebook/src/test/book/bookTocManager.test.ts
Normal file
@@ -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<void> {
|
||||
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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user