mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Editing Books (#13535)
* start work on ui * Move notebooks complete * Simplify version handling * fix issues with add section method * fix issues with the edit experience and add the quick pick for editing * add version handler for re-writing tocs * fix book toc manager tests * add notebook test * fix localize constant * normalize paths on windows * check that a section has sections before attempting to mve files * Implement method for renaming duplicated files * moving last notebook from section converts the section into notebook * Add recovery method restore original state of file system * Add clean up, for files that are copied instead of renamed * remove dir complexity * divide edit book in methods for easier testing and remove promise chain * Keep structure of toc * normalize paths on windows * fix external link * Add other fields for versions * fix paths in uri of findSection * add section to section test * Add error messages * address pr comments and add tests * check that the new path of a notebook is different from the original path before deleting
This commit is contained in:
@@ -239,6 +239,10 @@
|
|||||||
"command": "notebook.command.closeNotebook",
|
"command": "notebook.command.closeNotebook",
|
||||||
"title": "%title.closeJupyterNotebook%"
|
"title": "%title.closeJupyterNotebook%"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "notebook.command.moveTo",
|
||||||
|
"title": "%title.moveTo%"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "notebook.command.createBook",
|
"command": "notebook.command.createBook",
|
||||||
"title": "%title.createJupyterBook%",
|
"title": "%title.createJupyterBook%",
|
||||||
@@ -385,6 +389,10 @@
|
|||||||
"command": "notebook.command.closeNotebook",
|
"command": "notebook.command.closeNotebook",
|
||||||
"when": "false"
|
"when": "false"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "notebook.command.moveTo",
|
||||||
|
"when": "false"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "notebook.command.revealInBooksViewlet",
|
"command": "notebook.command.revealInBooksViewlet",
|
||||||
"when": "false"
|
"when": "false"
|
||||||
@@ -457,6 +465,10 @@
|
|||||||
"command": "notebook.command.closeNotebook",
|
"command": "notebook.command.closeNotebook",
|
||||||
"when": "view == bookTreeView && viewItem == savedNotebook"
|
"when": "view == bookTreeView && viewItem == savedNotebook"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"command": "notebook.command.moveTo",
|
||||||
|
"when": "view == bookTreeView && viewItem == savedNotebook || view == bookTreeView && viewItem == section"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "notebook.command.pinNotebook",
|
"command": "notebook.command.pinNotebook",
|
||||||
"when": "view == bookTreeView && viewItem == savedNotebook",
|
"when": "view == bookTreeView && viewItem == savedNotebook",
|
||||||
|
|||||||
@@ -48,5 +48,6 @@
|
|||||||
"title.openNotebookFolder": "Open Notebooks in Folder",
|
"title.openNotebookFolder": "Open Notebooks in Folder",
|
||||||
"title.openRemoteJupyterBook": "Add Remote Jupyter Book",
|
"title.openRemoteJupyterBook": "Add Remote Jupyter Book",
|
||||||
"title.pinNotebook": "Pin Notebook",
|
"title.pinNotebook": "Pin Notebook",
|
||||||
"title.unpinNotebook": "Unpin Notebook"
|
"title.unpinNotebook": "Unpin Notebook",
|
||||||
|
"title.moveTo": "Move to ..."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,16 +11,12 @@ import * as path from 'path';
|
|||||||
import * as fileServices from 'fs';
|
import * as fileServices from 'fs';
|
||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import * as loc from '../common/localizedConstants';
|
import * as loc from '../common/localizedConstants';
|
||||||
import { IJupyterBookToc, JupyterBookSection, IJupyterBookSectionV2, IJupyterBookSectionV1 } from '../contracts/content';
|
import { IJupyterBookToc, JupyterBookSection } from '../contracts/content';
|
||||||
|
import { convertFrom, getContentPath, BookVersion } from './bookVersionHandler';
|
||||||
|
|
||||||
const fsPromises = fileServices.promises;
|
const fsPromises = fileServices.promises;
|
||||||
const content = 'content';
|
const content = 'content';
|
||||||
|
|
||||||
export enum BookVersion {
|
|
||||||
v1 = 'v1',
|
|
||||||
v2 = 'v2'
|
|
||||||
}
|
|
||||||
|
|
||||||
export class BookModel {
|
export class BookModel {
|
||||||
private _bookItems: BookTreeItem[];
|
private _bookItems: BookTreeItem[];
|
||||||
private _allNotebooks = new Map<string, BookTreeItem>();
|
private _allNotebooks = new Map<string, BookTreeItem>();
|
||||||
@@ -124,7 +120,7 @@ export class BookModel {
|
|||||||
if (this.openAsUntitled && !this._allNotebooks.get(pathDetails.base)) {
|
if (this.openAsUntitled && !this._allNotebooks.get(pathDetails.base)) {
|
||||||
this._allNotebooks.set(pathDetails.base, notebookItem);
|
this._allNotebooks.set(pathDetails.base, notebookItem);
|
||||||
} else {
|
} else {
|
||||||
// convert to URI to avoid casing issue with drive letters when getting navigation links
|
// convert to URI to avoid causing issue with drive letters when getting navigation links
|
||||||
let uriToNotebook: vscode.Uri = vscode.Uri.file(this.bookPath);
|
let uriToNotebook: vscode.Uri = vscode.Uri.file(this.bookPath);
|
||||||
if (!this._allNotebooks.get(uriToNotebook.fsPath)) {
|
if (!this._allNotebooks.get(uriToNotebook.fsPath)) {
|
||||||
this._allNotebooks.set(uriToNotebook.fsPath, notebookItem);
|
this._allNotebooks.set(uriToNotebook.fsPath, notebookItem);
|
||||||
@@ -155,7 +151,7 @@ export class BookModel {
|
|||||||
title: config.title,
|
title: config.title,
|
||||||
contentPath: this._tableOfContentsPath,
|
contentPath: this._tableOfContentsPath,
|
||||||
root: this._rootPath,
|
root: this._rootPath,
|
||||||
tableOfContents: { sections: this.parseJupyterSections(tableOfContents) },
|
tableOfContents: { sections: this.parseJupyterSections(this._bookVersion, tableOfContents) },
|
||||||
page: tableOfContents,
|
page: tableOfContents,
|
||||||
type: BookTreeItemType.Book,
|
type: BookTreeItemType.Book,
|
||||||
treeItemCollapsibleState: collapsibleState,
|
treeItemCollapsibleState: collapsibleState,
|
||||||
@@ -169,7 +165,7 @@ export class BookModel {
|
|||||||
this._bookItems.push(book);
|
this._bookItems.push(book);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._errorMessage = loc.readBookError(this.bookPath, e instanceof Error ? e.message : e);
|
this._errorMessage = loc.readBookError(this.bookPath, e instanceof Error ? e.message : e);
|
||||||
vscode.window.showErrorMessage(this._errorMessage);
|
throw new Error(this._errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this._bookItems;
|
return this._bookItems;
|
||||||
@@ -182,95 +178,83 @@ export class BookModel {
|
|||||||
public async getSections(tableOfContents: IJupyterBookToc, sections: JupyterBookSection[], root: string, book: BookTreeItemFormat): Promise<BookTreeItem[]> {
|
public async getSections(tableOfContents: IJupyterBookToc, sections: JupyterBookSection[], root: string, book: BookTreeItemFormat): Promise<BookTreeItem[]> {
|
||||||
let notebooks: BookTreeItem[] = [];
|
let notebooks: BookTreeItem[] = [];
|
||||||
for (let i = 0; i < sections.length; i++) {
|
for (let i = 0; i < sections.length; i++) {
|
||||||
if (sections[i].url || (sections[i] as IJupyterBookSectionV2).file) {
|
if (sections[i].url) {
|
||||||
if (sections[i].url && ((sections[i] as IJupyterBookSectionV1).external || book.version === BookVersion.v2)) {
|
let externalLink: BookTreeItem = new BookTreeItem({
|
||||||
let externalLink: BookTreeItem = new BookTreeItem({
|
title: sections[i].title,
|
||||||
title: sections[i].title,
|
contentPath: undefined,
|
||||||
contentPath: undefined,
|
root: root,
|
||||||
|
tableOfContents: tableOfContents,
|
||||||
|
page: sections[i],
|
||||||
|
type: BookTreeItemType.ExternalLink,
|
||||||
|
treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
|
||||||
|
isUntitled: this.openAsUntitled,
|
||||||
|
version: book.version
|
||||||
|
},
|
||||||
|
{
|
||||||
|
light: this._extensionContext.asAbsolutePath('resources/light/link.svg'),
|
||||||
|
dark: this._extensionContext.asAbsolutePath('resources/dark/link_inverse.svg')
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
notebooks.push(externalLink);
|
||||||
|
} else if (sections[i].file) {
|
||||||
|
const pathToNotebook: string = getContentPath(book.version, book.root, sections[i].file.concat('.ipynb'));
|
||||||
|
const pathToMarkdown: string = getContentPath(book.version, book.root, sections[i].file.concat('.md'));
|
||||||
|
|
||||||
|
// Note: Currently, if there is an ipynb and a md file with the same name, Jupyter Books only shows the notebook.
|
||||||
|
// Following Jupyter Books behavior for now
|
||||||
|
if (await fs.pathExists(pathToNotebook)) {
|
||||||
|
let notebook = new BookTreeItem({
|
||||||
|
title: sections[i].title ? sections[i].title : sections[i].file,
|
||||||
|
contentPath: pathToNotebook,
|
||||||
root: root,
|
root: root,
|
||||||
tableOfContents: tableOfContents,
|
tableOfContents: tableOfContents,
|
||||||
page: sections[i],
|
page: sections[i],
|
||||||
type: BookTreeItemType.ExternalLink,
|
type: BookTreeItemType.Notebook,
|
||||||
treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
|
treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
|
||||||
isUntitled: this.openAsUntitled,
|
isUntitled: this.openAsUntitled,
|
||||||
version: book.version
|
version: book.version
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
light: this._extensionContext.asAbsolutePath('resources/light/link.svg'),
|
light: this._extensionContext.asAbsolutePath('resources/light/notebook.svg'),
|
||||||
dark: this._extensionContext.asAbsolutePath('resources/dark/link_inverse.svg')
|
dark: this._extensionContext.asAbsolutePath('resources/dark/notebook_inverse.svg')
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
notebooks.push(externalLink);
|
if (this.openAsUntitled) {
|
||||||
} else {
|
if (!this._allNotebooks.get(path.basename(pathToNotebook))) {
|
||||||
let pathToNotebook: string;
|
this._allNotebooks.set(path.basename(pathToNotebook), notebook);
|
||||||
let pathToMarkdown: string;
|
|
||||||
if (book.version === BookVersion.v2) {
|
|
||||||
pathToNotebook = path.join(book.root, (sections[i] as IJupyterBookSectionV2).file.concat('.ipynb'));
|
|
||||||
pathToMarkdown = path.join(book.root, (sections[i] as IJupyterBookSectionV2).file.concat('.md'));
|
|
||||||
} else if (sections[i].url) {
|
|
||||||
pathToNotebook = path.join(book.root, content, (sections[i] as IJupyterBookSectionV1).url.concat('.ipynb'));
|
|
||||||
pathToMarkdown = path.join(book.root, content, (sections[i] as IJupyterBookSectionV1).url.concat('.md'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Currently, if there is an ipynb and a md file with the same name, Jupyter Books only shows the notebook.
|
|
||||||
// Following Jupyter Books behavior for now
|
|
||||||
if (await fs.pathExists(pathToNotebook)) {
|
|
||||||
let notebook = new BookTreeItem({
|
|
||||||
title: sections[i].title ? sections[i].title : (sections[i] as IJupyterBookSectionV2).file,
|
|
||||||
contentPath: pathToNotebook,
|
|
||||||
root: root,
|
|
||||||
tableOfContents: tableOfContents,
|
|
||||||
page: sections[i],
|
|
||||||
type: BookTreeItemType.Notebook,
|
|
||||||
treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
|
|
||||||
isUntitled: this.openAsUntitled,
|
|
||||||
version: book.version
|
|
||||||
},
|
|
||||||
{
|
|
||||||
light: this._extensionContext.asAbsolutePath('resources/light/notebook.svg'),
|
|
||||||
dark: this._extensionContext.asAbsolutePath('resources/dark/notebook_inverse.svg')
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.openAsUntitled) {
|
|
||||||
if (!this._allNotebooks.get(path.basename(pathToNotebook))) {
|
|
||||||
this._allNotebooks.set(path.basename(pathToNotebook), notebook);
|
|
||||||
}
|
|
||||||
notebooks.push(notebook);
|
|
||||||
} else {
|
|
||||||
// convert to URI to avoid casing issue with drive letters when getting navigation links
|
|
||||||
let uriToNotebook: vscode.Uri = vscode.Uri.file(pathToNotebook);
|
|
||||||
if (!this._allNotebooks.get(uriToNotebook.fsPath)) {
|
|
||||||
this._allNotebooks.set(uriToNotebook.fsPath, notebook);
|
|
||||||
}
|
|
||||||
notebooks.push(notebook);
|
|
||||||
}
|
}
|
||||||
} else if (await fs.pathExists(pathToMarkdown)) {
|
notebooks.push(notebook);
|
||||||
let markdown: BookTreeItem = new BookTreeItem({
|
|
||||||
title: sections[i].title ? sections[i].title : (sections[i] as IJupyterBookSectionV2).file,
|
|
||||||
contentPath: pathToMarkdown,
|
|
||||||
root: root,
|
|
||||||
tableOfContents: tableOfContents,
|
|
||||||
page: sections[i],
|
|
||||||
type: BookTreeItemType.Markdown,
|
|
||||||
treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
|
|
||||||
isUntitled: this.openAsUntitled,
|
|
||||||
version: book.version
|
|
||||||
},
|
|
||||||
{
|
|
||||||
light: this._extensionContext.asAbsolutePath('resources/light/markdown.svg'),
|
|
||||||
dark: this._extensionContext.asAbsolutePath('resources/dark/markdown_inverse.svg')
|
|
||||||
}
|
|
||||||
);
|
|
||||||
notebooks.push(markdown);
|
|
||||||
} else {
|
} else {
|
||||||
this._errorMessage = loc.missingFileError(sections[i].title);
|
// convert to URI to avoid causing issue with drive letters when getting navigation links
|
||||||
vscode.window.showErrorMessage(this._errorMessage);
|
let uriToNotebook: vscode.Uri = vscode.Uri.file(pathToNotebook);
|
||||||
|
if (!this._allNotebooks.get(uriToNotebook.fsPath)) {
|
||||||
|
this._allNotebooks.set(uriToNotebook.fsPath, notebook);
|
||||||
|
}
|
||||||
|
notebooks.push(notebook);
|
||||||
}
|
}
|
||||||
|
} else if (await fs.pathExists(pathToMarkdown)) {
|
||||||
|
let markdown: BookTreeItem = new BookTreeItem({
|
||||||
|
title: sections[i].title ? sections[i].title : sections[i].file,
|
||||||
|
contentPath: pathToMarkdown,
|
||||||
|
root: root,
|
||||||
|
tableOfContents: tableOfContents,
|
||||||
|
page: sections[i],
|
||||||
|
type: BookTreeItemType.Markdown,
|
||||||
|
treeItemCollapsibleState: vscode.TreeItemCollapsibleState.Collapsed,
|
||||||
|
isUntitled: this.openAsUntitled,
|
||||||
|
version: book.version
|
||||||
|
},
|
||||||
|
{
|
||||||
|
light: this._extensionContext.asAbsolutePath('resources/light/markdown.svg'),
|
||||||
|
dark: this._extensionContext.asAbsolutePath('resources/dark/markdown_inverse.svg')
|
||||||
|
}
|
||||||
|
);
|
||||||
|
notebooks.push(markdown);
|
||||||
|
} else {
|
||||||
|
this._errorMessage = loc.missingFileError(sections[i].title, root);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// TODO: search functionality (#6160)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return notebooks;
|
return notebooks;
|
||||||
@@ -280,16 +264,16 @@ export class BookModel {
|
|||||||
* Recursively parses out a section of a Jupyter Book.
|
* Recursively parses out a section of a Jupyter Book.
|
||||||
* @param section The input data to parse
|
* @param section The input data to parse
|
||||||
*/
|
*/
|
||||||
private parseJupyterSections(section: any[]): JupyterBookSection[] {
|
public parseJupyterSections(version: string, section: any[]): JupyterBookSection[] {
|
||||||
try {
|
try {
|
||||||
return section.reduce((acc, val) => Array.isArray(val.sections) ?
|
return section.reduce((acc, val) => Array.isArray(val.sections) ?
|
||||||
acc.concat(val).concat(this.parseJupyterSections(val.sections)) : acc.concat(val), []);
|
acc.concat(convertFrom(version, val)).concat(this.parseJupyterSections(version, val.sections)) : acc.concat(convertFrom(version, val)), []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._errorMessage = loc.invalidTocFileError();
|
this._errorMessage = loc.invalidTocFileError();
|
||||||
if (section.length > 0) {
|
if (section.length > 0) {
|
||||||
this._errorMessage = loc.invalidTocError(section[0].title);
|
this._errorMessage = loc.invalidTocError(section[0].title);
|
||||||
}
|
}
|
||||||
throw this._errorMessage;
|
throw new Error(this._errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,14 +6,23 @@ import * as path from 'path';
|
|||||||
import { BookTreeItem } from './bookTreeItem';
|
import { BookTreeItem } from './bookTreeItem';
|
||||||
import * as yaml from 'js-yaml';
|
import * as yaml from 'js-yaml';
|
||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import { IJupyterBookSectionV1, IJupyterBookSectionV2, JupyterBookSection } from '../contracts/content';
|
import { JupyterBookSection } from '../contracts/content';
|
||||||
import { BookVersion } from './bookModel';
|
import { BookVersion, convertTo } from './bookVersionHandler';
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import * as loc from '../common/localizedConstants';
|
||||||
|
import { BookModel } from './bookModel';
|
||||||
|
|
||||||
export interface IBookTocManager {
|
export interface IBookTocManager {
|
||||||
updateBook(element: BookTreeItem, book: BookTreeItem): Promise<void>;
|
updateBook(element: BookTreeItem, book: BookTreeItem, targetSection?: JupyterBookSection): Promise<void>;
|
||||||
createBook(bookContentPath: string, contentFolder: string): Promise<void>
|
createBook(bookContentPath: string, contentFolder: string): Promise<void>;
|
||||||
|
recovery(): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface quickPickResults {
|
||||||
|
quickPickSection?: vscode.QuickPickItem,
|
||||||
|
book?: BookTreeItem
|
||||||
|
}
|
||||||
|
|
||||||
const allowedFileExtensions: string[] = ['.md', '.ipynb'];
|
const allowedFileExtensions: string[] = ['.md', '.ipynb'];
|
||||||
const initMarkdown: string[] = ['index.md', 'introduction.md', 'intro.md', 'readme.md'];
|
const initMarkdown: string[] = ['index.md', 'introduction.md', 'intro.md', 'readme.md'];
|
||||||
|
|
||||||
@@ -22,13 +31,24 @@ export function hasSections(node: JupyterBookSection): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class BookTocManager implements IBookTocManager {
|
export class BookTocManager implements IBookTocManager {
|
||||||
public tableofContents: IJupyterBookSectionV2[];
|
public tableofContents: JupyterBookSection[];
|
||||||
public newSection: JupyterBookSection = {};
|
public newSection: JupyterBookSection;
|
||||||
|
private _movedFiles = new Map<string, string>();
|
||||||
|
private _modifiedDirectory = new Set<string>();
|
||||||
|
private _tocFiles = new Map<string, JupyterBookSection[]>();
|
||||||
|
private sourceBookContentPath: string;
|
||||||
|
private targetBookContentPath: string;
|
||||||
|
private _sourceBook: BookModel;
|
||||||
|
|
||||||
constructor() {
|
constructor(targetBook?: BookModel, sourceBook?: BookModel) {
|
||||||
|
this._sourceBook = sourceBook;
|
||||||
|
this.newSection = {};
|
||||||
|
this.tableofContents = [];
|
||||||
|
this.sourceBookContentPath = sourceBook?.bookItems[0].rootContentPath;
|
||||||
|
this.targetBookContentPath = targetBook?.bookItems[0].rootContentPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllFiles(toc: IJupyterBookSectionV2[], directory: string, filesInDir: string[], rootDirectory: string): Promise<IJupyterBookSectionV2[]> {
|
async getAllFiles(toc: JupyterBookSection[], directory: string, filesInDir: string[], rootDirectory: string): Promise<JupyterBookSection[]> {
|
||||||
await Promise.all(filesInDir.map(async file => {
|
await Promise.all(filesInDir.map(async file => {
|
||||||
let isDirectory = (await fs.promises.stat(path.join(directory, file))).isDirectory();
|
let isDirectory = (await fs.promises.stat(path.join(directory, file))).isDirectory();
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
@@ -41,7 +61,7 @@ export class BookTocManager implements IBookTocManager {
|
|||||||
files.splice(index, 1);
|
files.splice(index, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let jupyterSection: IJupyterBookSectionV2 = {
|
let jupyterSection: JupyterBookSection = {
|
||||||
title: file,
|
title: file,
|
||||||
file: path.join(file, initFile),
|
file: path.join(file, initFile),
|
||||||
expand_sections: true,
|
expand_sections: true,
|
||||||
@@ -53,7 +73,7 @@ export class BookTocManager implements IBookTocManager {
|
|||||||
} else if (allowedFileExtensions.includes(path.extname(file))) {
|
} else if (allowedFileExtensions.includes(path.extname(file))) {
|
||||||
// if the file is in the book root we don't include the directory.
|
// 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 filePath = directory === rootDirectory ? path.parse(file).name : path.join(path.basename(directory), path.parse(file).name);
|
||||||
const addFile: IJupyterBookSectionV2 = {
|
const addFile: JupyterBookSection = {
|
||||||
title: path.parse(file).name,
|
title: path.parse(file).name,
|
||||||
file: filePath
|
file: filePath
|
||||||
};
|
};
|
||||||
@@ -74,21 +94,119 @@ export class BookTocManager implements IBookTocManager {
|
|||||||
return toc;
|
return toc;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateToc(tableOfContents: JupyterBookSection[], findSection: BookTreeItem, addSection: JupyterBookSection): JupyterBookSection[] {
|
/**
|
||||||
for (const section of tableOfContents) {
|
* Renames files if it already exists in the target book
|
||||||
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))) {
|
* @param src The source file that will be moved.
|
||||||
if (tableOfContents[tableOfContents.length - 1].sections) {
|
* @param dest The destination path of the file, that it's been moved.
|
||||||
tableOfContents[tableOfContents.length - 1].sections.push(addSection);
|
* Returns a new file name that does not exist in destination folder.
|
||||||
} else {
|
*/
|
||||||
tableOfContents[tableOfContents.length - 1].sections = [addSection];
|
async renameFile(src: string, dest: string): Promise<string> {
|
||||||
|
let newFileName = path.join(path.parse(dest).dir, path.parse(dest).name);
|
||||||
|
let counter = 2;
|
||||||
|
while (await fs.pathExists(path.join(newFileName.concat(' - ', counter.toString())).concat(path.parse(dest).ext))) {
|
||||||
|
counter++;
|
||||||
|
}
|
||||||
|
await fs.move(src, path.join(newFileName.concat(' - ', counter.toString())).concat(path.parse(dest).ext), { overwrite: true });
|
||||||
|
this._movedFiles.set(src, path.join(newFileName.concat(' - ', counter.toString())).concat(path.parse(dest).ext));
|
||||||
|
vscode.window.showInformationMessage(loc.duplicateFileError(path.parse(dest).base, src, newFileName.concat(' - ', counter.toString())));
|
||||||
|
return newFileName.concat(' - ', counter.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore user's original state in case of error, when trying to move files.
|
||||||
|
* We keep track of all the moved files in the _movedFiles. The value of the map contains the current path of the file,
|
||||||
|
* while the key contains the original path.
|
||||||
|
*
|
||||||
|
* Rewrite the original table of contents of the book, in case of error as well.
|
||||||
|
*/
|
||||||
|
async recovery(): Promise<void> {
|
||||||
|
this._movedFiles.forEach(async (value, key) => {
|
||||||
|
await fs.move(value, key);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._tocFiles.forEach(async (value, key) => {
|
||||||
|
await fs.writeFile(key, yaml.safeDump(value, { lineWidth: Infinity, noRefs: true, skipInvalid: true }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanUp(directory: string): Promise<void> {
|
||||||
|
let contents = await fs.readdir(directory);
|
||||||
|
if (contents.length === 0 && this._modifiedDirectory.has(directory)) {
|
||||||
|
// remove empty folders
|
||||||
|
await fs.rmdir(directory);
|
||||||
|
} else {
|
||||||
|
contents.forEach(async (content) => {
|
||||||
|
if ((await fs.stat(path.join(directory, content))).isFile) {
|
||||||
|
//check if the file is in the moved files
|
||||||
|
let isCopy = this._movedFiles.get(path.join(directory, content));
|
||||||
|
if (isCopy && this._movedFiles.get(path.join(directory, content)) !== path.join(directory, content)) {
|
||||||
|
// the file could not be renamed, so a copy was created.
|
||||||
|
// remove file only if the new path and old path are different
|
||||||
|
await fs.unlink(path.join(directory, content));
|
||||||
|
}
|
||||||
|
} else if ((await fs.stat(path.join(directory, content))).isDirectory) {
|
||||||
|
await this.cleanUp(path.join(directory, content));
|
||||||
}
|
}
|
||||||
break;
|
});
|
||||||
}
|
}
|
||||||
else if (hasSections(section)) {
|
}
|
||||||
return this.updateToc(section.sections, findSection, addSection);
|
|
||||||
|
/**
|
||||||
|
* Reads and modifies the table of contents file of the target book.
|
||||||
|
* @param version the version of the target book
|
||||||
|
* @param tocPath Path to the table of contents
|
||||||
|
* @param findSection The section that will be modified.
|
||||||
|
* @param addSection The section that'll be added to the target section. If it's undefined then the target section (findSection) is removed from the table of contents.
|
||||||
|
*/
|
||||||
|
async updateTOC(version: BookVersion, tocPath: string, findSection: JupyterBookSection, addSection?: JupyterBookSection): Promise<void> {
|
||||||
|
const toc = yaml.safeLoad((await fs.readFile(tocPath, 'utf8')));
|
||||||
|
this._tocFiles.set(tocPath, toc);
|
||||||
|
let newToc = new Array<JupyterBookSection>(toc.length);
|
||||||
|
for (const [index, section] of toc.entries()) {
|
||||||
|
let newSection = this.buildTOC(version, section, findSection, addSection);
|
||||||
|
if (newSection) {
|
||||||
|
newToc[index] = newSection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return tableOfContents;
|
await fs.writeFile(tocPath, yaml.safeDump(newToc, { lineWidth: Infinity, noRefs: true, skipInvalid: true }));
|
||||||
|
this.tableofContents = newToc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new table of contents structure containing the added section. This method is only called when we move a section to another section.
|
||||||
|
* Since the sections can be arranged in a tree structure we need to look for the section that will be modified in a recursively.
|
||||||
|
* @param version Version of the book
|
||||||
|
* @param section The current section that we are iterating
|
||||||
|
* @param findSection The section that will be modified.
|
||||||
|
* @param addSection The section that'll be added to the target section. If it's undefined then the target section (findSection) is removed from the table of contents.
|
||||||
|
*/
|
||||||
|
private buildTOC(version: BookVersion, section: JupyterBookSection, findSection: JupyterBookSection, addSection: JupyterBookSection): JupyterBookSection {
|
||||||
|
// condition to find the section to be modified
|
||||||
|
if (section.title === findSection.title && (section.file && section.file === findSection.file || section.url && section.url === findSection.file)) {
|
||||||
|
if (addSection) {
|
||||||
|
//if addSection is not undefined, then we added to the table of contents.
|
||||||
|
section.sections !== undefined && section.sections.length > 0 ? section.sections.push(addSection) : section.sections = [addSection];
|
||||||
|
return section;
|
||||||
|
}
|
||||||
|
// if addSection is undefined then we remove the whole section from the table of contents.
|
||||||
|
return addSection;
|
||||||
|
} else {
|
||||||
|
let newSection = convertTo(version, section);
|
||||||
|
if (section.sections && section.sections.length > 0) {
|
||||||
|
newSection.sections = [] as JupyterBookSection[];
|
||||||
|
for (let s of section.sections) {
|
||||||
|
const child = this.buildTOC(version, s, findSection, addSection);
|
||||||
|
if (child) {
|
||||||
|
newSection.sections.push(convertTo(version, child));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (newSection.sections?.length === 0) {
|
||||||
|
// if sections is an empty array then assign it to undefined, so it's converted into a markdown file.
|
||||||
|
newSection.sections = undefined;
|
||||||
|
}
|
||||||
|
return newSection;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,75 +225,198 @@ export class BookTocManager implements IBookTocManager {
|
|||||||
await vscode.commands.executeCommand('notebook.command.openNotebookFolder', bookContentPath, undefined, true);
|
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;
|
* When moving a section, we need to modify every file path that it's within the section. Since sections is a tree like structure, we need to modify each of the file paths
|
||||||
//the book contentPath contains the first file of the section, we get the dirname to identify the section's root path
|
* and move the files individually. The overwrite option is set to false to prevent any issues with duplicated file names.
|
||||||
const rootPath = isSection ? path.dirname(book.book.contentPath) : book.rootContentPath;
|
* @param files Files in the section.
|
||||||
// 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
|
async traverseSections(files: JupyterBookSection[]): Promise<JupyterBookSection[]> {
|
||||||
const uri = isSection ? path.join(path.basename(rootPath), section.uri) : section.uri;
|
let movedSections: JupyterBookSection[] = [];
|
||||||
if (section.book.version === BookVersion.v1) {
|
for (const elem of files) {
|
||||||
this.newSection.url = uri;
|
if (elem.file) {
|
||||||
let movedSections: IJupyterBookSectionV1[] = [];
|
let fileName = undefined;
|
||||||
const files = section.sections as IJupyterBookSectionV1[];
|
try {
|
||||||
for (const elem of files) {
|
await fs.move(path.join(this.sourceBookContentPath, elem.file).concat('.ipynb'), path.join(this.targetBookContentPath, elem.file).concat('.ipynb'), { overwrite: false });
|
||||||
await fs.promises.mkdir(path.join(rootPath, path.dirname(elem.url)), { recursive: true });
|
this._movedFiles.set(path.join(this.sourceBookContentPath, elem.file).concat('.ipynb'), path.join(this.targetBookContentPath, elem.file).concat('.ipynb'));
|
||||||
await fs.move(path.join(path.dirname(section.book.contentPath), path.basename(elem.url)), path.join(rootPath, elem.url));
|
} catch (error) {
|
||||||
movedSections.push({ url: isSection ? path.join(path.basename(rootPath), elem.url) : elem.url, title: elem.title });
|
if (error.code === 'EEXIST') {
|
||||||
|
fileName = await this.renameFile(path.join(this.sourceBookContentPath, elem.file).concat('.ipynb'), path.join(this.targetBookContentPath, elem.file).concat('.ipynb'));
|
||||||
|
}
|
||||||
|
else if (error.code !== 'ENOENT') {
|
||||||
|
throw (error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await fs.move(path.join(this.sourceBookContentPath, elem.file).concat('.md'), path.join(this.targetBookContentPath, elem.file).concat('.md'), { overwrite: false });
|
||||||
|
this._movedFiles.set(path.join(this.sourceBookContentPath, elem.file).concat('.md'), path.join(this.targetBookContentPath, elem.file).concat('.md'));
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'EEXIST') {
|
||||||
|
fileName = await this.renameFile(path.join(this.sourceBookContentPath, elem.file).concat('.md'), path.join(this.targetBookContentPath, elem.file).concat('.md'));
|
||||||
|
}
|
||||||
|
else if (error.code !== 'ENOENT') {
|
||||||
|
throw (error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elem.file = fileName === undefined ? elem.file : path.join(path.dirname(elem.file), path.parse(fileName).name);
|
||||||
|
elem.sections = elem.sections ? await this.traverseSections(elem.sections) : undefined;
|
||||||
}
|
}
|
||||||
this.newSection.sections = movedSections;
|
|
||||||
} else if (section.book.version === BookVersion.v2) {
|
movedSections.push(elem);
|
||||||
(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;
|
|
||||||
}
|
}
|
||||||
|
return 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
|
* Moves a section to a book top level or another book's section. If there's a target section we add the the targetSection directory if it has one and append it to the
|
||||||
const rootPath = isSection ? path.dirname(book.book.contentPath) : book.rootContentPath;
|
* notebook's path. The overwrite option is set to false to prevent any issues with duplicated file names.
|
||||||
let notebookName = path.basename(notebook.book.contentPath);
|
* @param section The section that's been moved.
|
||||||
await fs.move(notebook.book.contentPath, path.join(rootPath, notebookName));
|
* @param book The target book.
|
||||||
if (book.book.version === BookVersion.v1) {
|
*/
|
||||||
this.newSection = { url: notebookName, title: notebookName };
|
async addSection(section: BookTreeItem, book: BookTreeItem): Promise<void> {
|
||||||
} else if (book.book.version === BookVersion.v2) {
|
const uri = path.sep.concat(path.relative(section.rootContentPath, section.book.contentPath));
|
||||||
this.newSection = { file: notebookName, title: notebookName };
|
let moveFile = path.join(path.parse(uri).dir, path.parse(uri).name);
|
||||||
|
let fileName = undefined;
|
||||||
|
try {
|
||||||
|
await fs.move(section.book.contentPath, path.join(this.targetBookContentPath, moveFile).concat(path.parse(uri).ext), { overwrite: false });
|
||||||
|
this._movedFiles.set(section.book.contentPath, path.join(this.targetBookContentPath, moveFile).concat(path.parse(uri).ext));
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'EEXIST') {
|
||||||
|
fileName = await this.renameFile(section.book.contentPath, path.join(this.targetBookContentPath, moveFile).concat(path.parse(uri).ext));
|
||||||
|
}
|
||||||
|
else if (error.code !== 'ENOENT') {
|
||||||
|
throw (error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileName = fileName === undefined ? path.parse(uri).name : path.parse(fileName).name;
|
||||||
|
|
||||||
|
if (this._sourceBook) {
|
||||||
|
const sectionTOC = this._sourceBook.bookItems[0].findChildSection(section.uri);
|
||||||
|
if (sectionTOC) {
|
||||||
|
this.newSection = sectionTOC;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.newSection.title = section.title;
|
||||||
|
this.newSection.file = path.join(path.parse(uri).dir, fileName)?.replace(/\\/g, '/');
|
||||||
|
if (section.sections) {
|
||||||
|
const files = section.sections as JupyterBookSection[];
|
||||||
|
const movedSections = await this.traverseSections(files);
|
||||||
|
this.newSection.sections = movedSections;
|
||||||
|
this._modifiedDirectory.add(path.dirname(section.book.contentPath));
|
||||||
|
this.cleanUp(path.dirname(section.book.contentPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (book.version === BookVersion.v1) {
|
||||||
|
// here we only convert if is v1 because we are already using the v2 notation for every book that we read.
|
||||||
|
this.newSection = convertTo(book.version, this.newSection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves the element to the book's folder and adds it to the table of contents.
|
* Moves a notebook to a book top level or a book's section. If there's a target section we add the the targetSection directory if it has one and append it to the
|
||||||
|
* notebook's path. The overwrite option is set to false to prevent any issues with duplicated file names.
|
||||||
* @param element Notebook, Markdown File, or section that will be added to the book.
|
* @param element Notebook, Markdown File, or section that will be added to the book.
|
||||||
* @param book Book or a BookSection that will be modified.
|
* @param targetBook Book that will be modified.
|
||||||
*/
|
*/
|
||||||
public async updateBook(element: BookTreeItem, book: BookTreeItem): Promise<void> {
|
async addNotebook(notebook: BookTreeItem, book: BookTreeItem): Promise<void> {
|
||||||
if (element.contextValue === 'section' && book.book.version === element.book.version) {
|
const rootPath = book.rootContentPath;
|
||||||
if (book.contextValue === 'section') {
|
const notebookPath = path.parse(notebook.book.contentPath);
|
||||||
await this.addSection(element, book, true);
|
let fileName = undefined;
|
||||||
this.tableofContents = this.updateToc(book.tableOfContents.sections, book, this.newSection);
|
try {
|
||||||
await fs.writeFile(book.tableOfContentsPath, yaml.safeDump(this.tableofContents, { lineWidth: Infinity }));
|
await fs.move(notebook.book.contentPath, path.join(rootPath, notebookPath.base), { overwrite: false });
|
||||||
} else if (book.contextValue === 'savedBook') {
|
} catch (error) {
|
||||||
await this.addSection(element, book, false);
|
if (error.code === 'EEXIST') {
|
||||||
book.tableOfContents.sections.push(this.newSection);
|
fileName = await this.renameFile(notebook.book.contentPath, path.join(rootPath, notebookPath.base));
|
||||||
await fs.writeFile(book.tableOfContentsPath, yaml.safeDump(book.tableOfContents, { lineWidth: Infinity }));
|
}
|
||||||
|
else {
|
||||||
|
throw (error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._sourceBook) {
|
||||||
|
const sectionTOC = this._sourceBook.bookItems[0].findChildSection(notebook.uri);
|
||||||
|
this.newSection = sectionTOC;
|
||||||
|
}
|
||||||
|
fileName = fileName === undefined ? notebookPath.name : path.parse(fileName).name;
|
||||||
|
this.newSection.file = path.sep.concat(fileName);
|
||||||
|
this.newSection.title = notebook.book.title;
|
||||||
|
if (book.version === BookVersion.v1) {
|
||||||
|
// here we only convert if is v1 because we are already using the v2 notation for every book that we read.
|
||||||
|
this.newSection = convertTo(book.version, this.newSection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves the element to the target 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 targetBook Book that will be modified.
|
||||||
|
* @param targetSection Book section that'll be modified.
|
||||||
|
*/
|
||||||
|
public async updateBook(element: BookTreeItem, targetBook: BookTreeItem, targetSection?: JupyterBookSection): Promise<void> {
|
||||||
|
const targetBookVersion = targetBook.book.version === BookVersion.v1 ? BookVersion.v1 : BookVersion.v2;
|
||||||
|
if (element.contextValue === 'section') {
|
||||||
|
await this.addSection(element, targetBook);
|
||||||
|
const elementVersion = element.book.version === BookVersion.v1 ? BookVersion.v1 : BookVersion.v2;
|
||||||
|
// modify the sourceBook toc and remove the section
|
||||||
|
const findSection: JupyterBookSection = { file: element.book.page.file?.replace(/\\/g, '/'), title: element.book.page.title };
|
||||||
|
await this.updateTOC(elementVersion, element.tableOfContentsPath, findSection, undefined);
|
||||||
|
if (targetSection) {
|
||||||
|
// adding new section to the target book toc file
|
||||||
|
await this.updateTOC(targetBookVersion, targetBook.tableOfContentsPath, targetSection, this.newSection);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//since there's not a target section, we just append the section at the end of the file
|
||||||
|
if (this.targetBookContentPath !== this.sourceBookContentPath) {
|
||||||
|
this.tableofContents = targetBook.sections.map(section => convertTo(targetBook.version, section));
|
||||||
|
}
|
||||||
|
this.tableofContents.push(this.newSection);
|
||||||
|
await fs.writeFile(targetBook.tableOfContentsPath, yaml.safeDump(this.tableofContents, { lineWidth: Infinity, noRefs: true, skipInvalid: true }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (element.contextValue === 'savedNotebook') {
|
else if (element.contextValue === 'savedNotebook') {
|
||||||
if (book.contextValue === 'savedBook') {
|
await this.addNotebook(element, targetBook);
|
||||||
await this.addNotebook(element, book, false);
|
if (element.tableOfContentsPath) {
|
||||||
book.tableOfContents.sections.push(this.newSection);
|
const elementVersion = element.book.version === BookVersion.v1 ? BookVersion.v1 : BookVersion.v2;
|
||||||
await fs.writeFile(book.tableOfContentsPath, yaml.safeDump(book.tableOfContents, { lineWidth: Infinity }));
|
// the notebook is part of a book so we need to modify its toc as well
|
||||||
} else if (book.contextValue === 'section') {
|
const findSection = { file: element.book.page.file?.replace(/\\/g, '/'), title: element.book.page.title };
|
||||||
await this.addNotebook(element, book, true);
|
await this.updateTOC(elementVersion, element.tableOfContentsPath, findSection, undefined);
|
||||||
this.tableofContents = this.updateToc(book.tableOfContents.sections, book, this.newSection);
|
} else {
|
||||||
await fs.writeFile(book.tableOfContentsPath, yaml.safeDump(this.tableofContents, { lineWidth: Infinity }));
|
// close the standalone notebook, so it doesn't throw an error when we move the notebook to new location.
|
||||||
|
await vscode.commands.executeCommand('notebook.command.closeNotebook', element);
|
||||||
|
}
|
||||||
|
if (!targetSection) {
|
||||||
|
if (this.targetBookContentPath !== this.sourceBookContentPath) {
|
||||||
|
this.tableofContents = targetBook.sections.map(section => convertTo(targetBook.version, section));
|
||||||
|
}
|
||||||
|
this.tableofContents.push(this.newSection);
|
||||||
|
await fs.writeFile(targetBook.tableOfContentsPath, yaml.safeDump(this.tableofContents, { lineWidth: Infinity, noRefs: true, skipInvalid: true }));
|
||||||
|
} else {
|
||||||
|
await this.updateTOC(targetBookVersion, targetBook.tableOfContentsPath, targetSection, this.newSection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get movedFiles(): Map<string, string> {
|
||||||
|
return this._movedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get originalToc(): Map<string, JupyterBookSection[]> {
|
||||||
|
return this._tocFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get modifiedDir(): Set<string> {
|
||||||
|
return this._modifiedDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set movedFiles(files: Map<string, string>) {
|
||||||
|
this._movedFiles = files;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set originalToc(files: Map<string, JupyterBookSection[]>) {
|
||||||
|
this._tocFiles = files;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set modifiedDir(files: Set<string>) {
|
||||||
|
this._modifiedDirectory = files;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,11 @@
|
|||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as path from 'path';
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { JupyterBookSection, IJupyterBookToc, IJupyterBookSectionV2, IJupyterBookSectionV1 } from '../contracts/content';
|
import { JupyterBookSection, IJupyterBookToc } from '../contracts/content';
|
||||||
import * as loc from '../common/localizedConstants';
|
import * as loc from '../common/localizedConstants';
|
||||||
import { isBookItemPinned } from '../common/utils';
|
import { isBookItemPinned } from '../common/utils';
|
||||||
import { BookVersion } from './bookModel';
|
import { getContentPath, getTocPath } from './bookVersionHandler';
|
||||||
|
|
||||||
const content = 'content';
|
|
||||||
|
|
||||||
export enum BookTreeItemType {
|
export enum BookTreeItemType {
|
||||||
Book = 'Book',
|
Book = 'Book',
|
||||||
@@ -45,7 +42,6 @@ export class BookTreeItem extends vscode.TreeItem {
|
|||||||
|
|
||||||
constructor(public book: BookTreeItemFormat, icons: any) {
|
constructor(public book: BookTreeItemFormat, icons: any) {
|
||||||
super(book.title, book.treeItemCollapsibleState);
|
super(book.title, book.treeItemCollapsibleState);
|
||||||
|
|
||||||
if (book.type === BookTreeItemType.Book) {
|
if (book.type === BookTreeItemType.Book) {
|
||||||
this.collapsibleState = book.treeItemCollapsibleState;
|
this.collapsibleState = book.treeItemCollapsibleState;
|
||||||
this._sections = book.page;
|
this._sections = book.page;
|
||||||
@@ -64,6 +60,9 @@ export class BookTreeItem extends vscode.TreeItem {
|
|||||||
} else {
|
} else {
|
||||||
this.contextValue = isBookItemPinned(book.contentPath) ? 'pinnedNotebook' : 'savedNotebook';
|
this.contextValue = isBookItemPinned(book.contentPath) ? 'pinnedNotebook' : 'savedNotebook';
|
||||||
}
|
}
|
||||||
|
} else if (book.type === BookTreeItemType.ExternalLink) {
|
||||||
|
this.contextValue = BookTreeItemType.ExternalLink;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.contextValue = book.type === BookTreeItemType.Notebook ? (isBookItemPinned(book.contentPath) ? 'pinnedNotebook' : 'savedNotebook') : 'section';
|
this.contextValue = book.type === BookTreeItemType.Notebook ? (isBookItemPinned(book.contentPath) ? 'pinnedNotebook' : 'savedNotebook') : 'section';
|
||||||
}
|
}
|
||||||
@@ -71,26 +70,30 @@ export class BookTreeItem extends vscode.TreeItem {
|
|||||||
this.setCommand();
|
this.setCommand();
|
||||||
}
|
}
|
||||||
this.iconPath = icons;
|
this.iconPath = icons;
|
||||||
|
this._tableOfContentsPath = undefined;
|
||||||
|
|
||||||
if (this.book.type === BookTreeItemType.ExternalLink) {
|
if (this.book.type === BookTreeItemType.ExternalLink) {
|
||||||
this.tooltip = `${this._uri}`;
|
this.tooltip = `${this._uri}`;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
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;
|
// if it's a section, book or a notebook's book then we set the table of contents path.
|
||||||
this._rootContentPath = this.book.version === BookVersion.v1 ? path.join(this.book.root, content) : this.book.root;
|
if (this.book.type === BookTreeItemType.Book || this.contextValue === 'section' || (book.tableOfContents.sections && book.type === BookTreeItemType.Notebook)) {
|
||||||
|
this._tableOfContentsPath = getTocPath(this.book.version, this.book.root);
|
||||||
|
}
|
||||||
|
this._rootContentPath = getContentPath(this.book.version, this.book.root, '');
|
||||||
this.tooltip = this.book.type === BookTreeItemType.Book ? this._rootContentPath : this.book.contentPath;
|
this.tooltip = this.book.type === BookTreeItemType.Book ? this._rootContentPath : this.book.contentPath;
|
||||||
this.resourceUri = vscode.Uri.file(this.book.root);
|
this.resourceUri = vscode.Uri.file(this.book.root);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setPageVariables() {
|
private setPageVariables(): void {
|
||||||
this.collapsibleState = (this.book.page.sections || this.book.page.subsections) && this.book.page.expand_sections ?
|
this.collapsibleState = (this.book.page.sections || this.book.page.subsections) && this.book.page.expand_sections ?
|
||||||
vscode.TreeItemCollapsibleState.Expanded :
|
vscode.TreeItemCollapsibleState.Expanded :
|
||||||
this.book.page.sections || this.book.page.subsections ?
|
this.book.page.sections || this.book.page.subsections ?
|
||||||
vscode.TreeItemCollapsibleState.Collapsed :
|
vscode.TreeItemCollapsibleState.Collapsed :
|
||||||
vscode.TreeItemCollapsibleState.None;
|
vscode.TreeItemCollapsibleState.None;
|
||||||
this._sections = this.book.page.sections || this.book.page.subsections;
|
this._sections = this.book.page.sections || this.book.page.subsections;
|
||||||
this._uri = this.book.version === BookVersion.v1 ? this.book.page.url : this.book.page.file;
|
this._uri = this.book.page.file ? this.book.page.file : this.book.page.url;
|
||||||
|
|
||||||
if (this.book.tableOfContents.sections) {
|
if (this.book.tableOfContents.sections) {
|
||||||
let index = (this.book.tableOfContents.sections.indexOf(this.book.page));
|
let index = (this.book.tableOfContents.sections.indexOf(this.book.page));
|
||||||
@@ -99,7 +102,7 @@ export class BookTreeItem extends vscode.TreeItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setCommand() {
|
private setCommand(): void {
|
||||||
if (this.book.type === BookTreeItemType.Notebook) {
|
if (this.book.type === BookTreeItemType.Notebook) {
|
||||||
// The Notebook editor expects a posix path for the resource (it will still resolve to the correct fsPath based on OS)
|
// The Notebook editor expects a posix path for the resource (it will still resolve to the correct fsPath based on OS)
|
||||||
this.command = { command: this.book.isUntitled ? 'bookTreeView.openUntitledNotebook' : 'bookTreeView.openNotebook', title: loc.openNotebookCommand, arguments: [this.book.contentPath], };
|
this.command = { command: this.book.isUntitled ? 'bookTreeView.openUntitledNotebook' : 'bookTreeView.openNotebook', title: loc.openNotebookCommand, arguments: [this.book.contentPath], };
|
||||||
@@ -114,13 +117,11 @@ export class BookTreeItem extends vscode.TreeItem {
|
|||||||
let i = --index;
|
let i = --index;
|
||||||
while (i > -1) {
|
while (i > -1) {
|
||||||
let pathToNotebook: string;
|
let pathToNotebook: string;
|
||||||
if (this.book.version === BookVersion.v2 && (this.book.tableOfContents.sections[i] as IJupyterBookSectionV2).file) {
|
if (this.book.tableOfContents.sections[i].file) {
|
||||||
// The Notebook editor expects a posix path for the resource (it will still resolve to the correct fsPath based on OS)
|
// The Notebook editor expects a posix path for the resource (it will still resolve to the correct fsPath based on OS)
|
||||||
pathToNotebook = path.posix.join(this.book.root, (this.book.tableOfContents.sections[i] as IJupyterBookSectionV2).file.concat('.ipynb'));
|
pathToNotebook = getContentPath(this.book.version, this.book.root, this.book.tableOfContents.sections[i].file);
|
||||||
} else if ((this.book.tableOfContents.sections[i] as IJupyterBookSectionV1).url) {
|
pathToNotebook = pathToNotebook.concat('.ipynb');
|
||||||
pathToNotebook = path.posix.join(this.book.root, content, (this.book.tableOfContents.sections[i] as IJupyterBookSectionV1).url.concat('.ipynb'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-sync
|
// eslint-disable-next-line no-sync
|
||||||
if (fs.existsSync(pathToNotebook)) {
|
if (fs.existsSync(pathToNotebook)) {
|
||||||
this._previousUri = pathToNotebook;
|
this._previousUri = pathToNotebook;
|
||||||
@@ -134,13 +135,11 @@ export class BookTreeItem extends vscode.TreeItem {
|
|||||||
let i = ++index;
|
let i = ++index;
|
||||||
while (i < this.book.tableOfContents.sections.length) {
|
while (i < this.book.tableOfContents.sections.length) {
|
||||||
let pathToNotebook: string;
|
let pathToNotebook: string;
|
||||||
if (this.book.version === BookVersion.v2 && (this.book.tableOfContents.sections[i] as IJupyterBookSectionV2).file) {
|
if (this.book.tableOfContents.sections[i].file) {
|
||||||
// The Notebook editor expects a posix path for the resource (it will still resolve to the correct fsPath based on OS)
|
// The Notebook editor expects a posix path for the resource (it will still resolve to the correct fsPath based on OS)
|
||||||
pathToNotebook = path.posix.join(this.book.root, (this.book.tableOfContents.sections[i] as IJupyterBookSectionV2).file.concat('.ipynb'));
|
pathToNotebook = getContentPath(this.book.version, this.book.root, this.book.tableOfContents.sections[i].file);
|
||||||
} else if ((this.book.tableOfContents.sections[i] as IJupyterBookSectionV1).url) {
|
pathToNotebook = pathToNotebook.concat('.ipynb');
|
||||||
pathToNotebook = path.posix.join(this.book.root, content, (this.book.tableOfContents.sections[i] as IJupyterBookSectionV1).url.concat('.ipynb'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-sync
|
// eslint-disable-next-line no-sync
|
||||||
if (fs.existsSync(pathToNotebook)) {
|
if (fs.existsSync(pathToNotebook)) {
|
||||||
this._nextUri = pathToNotebook;
|
this._nextUri = pathToNotebook;
|
||||||
@@ -174,7 +173,7 @@ export class BookTreeItem extends vscode.TreeItem {
|
|||||||
return this.book.tableOfContents;
|
return this.book.tableOfContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get sections(): any[] {
|
public get sections(): JupyterBookSection[] {
|
||||||
return this._sections;
|
return this._sections;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +191,14 @@ export class BookTreeItem extends vscode.TreeItem {
|
|||||||
this._uri = uri;
|
this._uri = uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public set sections(sections: JupyterBookSection[]) {
|
||||||
|
this._sections = sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set tableOfContentsPath(tocPath: string) {
|
||||||
|
this._tableOfContentsPath = tocPath;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to find a child section with a specified URL
|
* Helper method to find a child section with a specified URL
|
||||||
* @param url The url of the section we're searching for
|
* @param url The url of the section we're searching for
|
||||||
@@ -204,7 +211,7 @@ export class BookTreeItem extends vscode.TreeItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private findChildSectionRecur(section: JupyterBookSection, url: string): JupyterBookSection | undefined {
|
private findChildSectionRecur(section: JupyterBookSection, url: string): JupyterBookSection | undefined {
|
||||||
if ((section as IJupyterBookSectionV1).url && (section as IJupyterBookSectionV1).url === url || (section as IJupyterBookSectionV2).file && (section as IJupyterBookSectionV2).file === url) {
|
if (section.file && section.file === url) {
|
||||||
return section;
|
return section;
|
||||||
} else if (section.sections) {
|
} else if (section.sections) {
|
||||||
for (const childSection of section.sections) {
|
for (const childSection of section.sections) {
|
||||||
|
|||||||
@@ -11,19 +11,17 @@ import * as constants from '../common/constants';
|
|||||||
import { IPrompter, IQuestion, QuestionTypes } from '../prompts/question';
|
import { IPrompter, IQuestion, QuestionTypes } from '../prompts/question';
|
||||||
import CodeAdapter from '../prompts/adapter';
|
import CodeAdapter from '../prompts/adapter';
|
||||||
import { BookTreeItem, BookTreeItemType } from './bookTreeItem';
|
import { BookTreeItem, BookTreeItemType } from './bookTreeItem';
|
||||||
import { BookModel, BookVersion } from './bookModel';
|
import { BookModel } from './bookModel';
|
||||||
import { Deferred } from '../common/promise';
|
import { Deferred } from '../common/promise';
|
||||||
import { IBookTrustManager, BookTrustManager } from './bookTrustManager';
|
import { IBookTrustManager, BookTrustManager } from './bookTrustManager';
|
||||||
import * as loc from '../common/localizedConstants';
|
import * as loc from '../common/localizedConstants';
|
||||||
import * as glob from 'fast-glob';
|
import * as glob from 'fast-glob';
|
||||||
import { IJupyterBookSectionV2, IJupyterBookSectionV1 } from '../contracts/content';
|
|
||||||
import { debounce, getPinnedNotebooks } from '../common/utils';
|
import { debounce, getPinnedNotebooks } from '../common/utils';
|
||||||
import { IBookPinManager, BookPinManager } from './bookPinManager';
|
import { IBookPinManager, BookPinManager } from './bookPinManager';
|
||||||
import { BookTocManager, IBookTocManager } from './bookTocManager';
|
import { BookTocManager, IBookTocManager, quickPickResults } from './bookTocManager';
|
||||||
|
import { getContentPath } from './bookVersionHandler';
|
||||||
import { TelemetryReporter, BookTelemetryView, NbTelemetryActions } from '../telemetry';
|
import { TelemetryReporter, BookTelemetryView, NbTelemetryActions } from '../telemetry';
|
||||||
|
|
||||||
const content = 'content';
|
|
||||||
|
|
||||||
interface BookSearchResults {
|
interface BookSearchResults {
|
||||||
notebookPaths: string[];
|
notebookPaths: string[];
|
||||||
bookPaths: string[];
|
bookPaths: string[];
|
||||||
@@ -50,7 +48,6 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
|||||||
this._extensionContext = extensionContext;
|
this._extensionContext = extensionContext;
|
||||||
this.books = [];
|
this.books = [];
|
||||||
this.bookPinManager = new BookPinManager();
|
this.bookPinManager = new BookPinManager();
|
||||||
this.bookTocManager = new BookTocManager();
|
|
||||||
this.viewId = view;
|
this.viewId = view;
|
||||||
this.initialize(workspaceFolders).catch(e => console.error(e));
|
this.initialize(workspaceFolders).catch(e => console.error(e));
|
||||||
this.prompter = new CodeAdapter();
|
this.prompter = new CodeAdapter();
|
||||||
@@ -93,6 +90,14 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
|||||||
this._extensionContext.globalState.update(constants.visitedNotebooksMementoKey, value);
|
this._extensionContext.globalState.update(constants.visitedNotebooksMementoKey, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setFileWatcher(book: BookModel): void {
|
||||||
|
fs.watchFile(book.tableOfContentsPath, async (curr, prev) => {
|
||||||
|
if (curr.mtime > prev.mtime) {
|
||||||
|
this.fireBookRefresh(book);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
trustBook(bookTreeItem?: BookTreeItem): void {
|
trustBook(bookTreeItem?: BookTreeItem): void {
|
||||||
let bookPathToTrust: string = bookTreeItem ? bookTreeItem.root : this.currentBook?.bookPath;
|
let bookPathToTrust: string = bookTreeItem ? bookTreeItem.root : this.currentBook?.bookPath;
|
||||||
if (bookPathToTrust) {
|
if (bookPathToTrust) {
|
||||||
@@ -144,8 +149,92 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
|||||||
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.CreateBook).send();
|
TelemetryReporter.createActionEvent(BookTelemetryView, NbTelemetryActions.CreateBook).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
async editBook(book: BookTreeItem, section: BookTreeItem): Promise<void> {
|
async getSelectionQuickPick(movingElement: BookTreeItem): Promise<quickPickResults> {
|
||||||
await this.bookTocManager.updateBook(section, book);
|
let bookOptions: vscode.QuickPickItem[] = [];
|
||||||
|
let pickedSection: vscode.QuickPickItem;
|
||||||
|
this.books.forEach(book => {
|
||||||
|
if (!book.isNotebook) {
|
||||||
|
bookOptions.push({ label: book.bookItems[0].title, detail: book.bookPath });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let pickedBook = await vscode.window.showQuickPick(bookOptions, {
|
||||||
|
canPickMany: false,
|
||||||
|
placeHolder: loc.labelBookFolder
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pickedBook && movingElement) {
|
||||||
|
const updateBook = this.books.find(book => book.bookPath === pickedBook.detail).bookItems[0];
|
||||||
|
if (updateBook) {
|
||||||
|
let bookSections = updateBook.sections;
|
||||||
|
while (bookSections?.length > 0) {
|
||||||
|
bookOptions = [{ label: loc.labelAddToLevel, detail: pickedSection ? pickedSection.detail : '' }];
|
||||||
|
bookSections.forEach(section => {
|
||||||
|
if (section.sections) {
|
||||||
|
bookOptions.push({ label: section.title ? section.title : section.file, detail: section.file });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bookSections = [];
|
||||||
|
if (bookOptions.length > 1) {
|
||||||
|
pickedSection = await vscode.window.showQuickPick(bookOptions, {
|
||||||
|
canPickMany: false,
|
||||||
|
placeHolder: loc.labelBookSection
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pickedSection && pickedSection.label === loc.labelAddToLevel) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (pickedSection && pickedSection.detail) {
|
||||||
|
if (updateBook.root === movingElement.root && pickedSection.detail === movingElement.uri) {
|
||||||
|
pickedSection = undefined;
|
||||||
|
} else {
|
||||||
|
bookSections = updateBook.findChildSection(pickedSection.detail).sections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { quickPickSection: pickedSection, book: updateBook };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async editBook(movingElement: BookTreeItem): Promise<void> {
|
||||||
|
const selectionResults = await this.getSelectionQuickPick(movingElement);
|
||||||
|
const pickedSection = selectionResults.quickPickSection;
|
||||||
|
const updateBook = selectionResults.book;
|
||||||
|
if (pickedSection && updateBook) {
|
||||||
|
const targetSection = pickedSection.detail !== undefined ? updateBook.findChildSection(pickedSection.detail) : undefined;
|
||||||
|
if (movingElement.tableOfContents.sections) {
|
||||||
|
if (movingElement.contextValue === 'savedNotebook') {
|
||||||
|
let sourceBook = this.books.find(book => book.getNotebook(path.normalize(movingElement.book.contentPath)));
|
||||||
|
movingElement.tableOfContents.sections = sourceBook?.bookItems[0].sections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const sourceBook = this.books.find(book => book.bookPath === movingElement.book.root);
|
||||||
|
const targetBook = this.books.find(book => book.bookPath === updateBook.book.root);
|
||||||
|
this.bookTocManager = new BookTocManager(targetBook, sourceBook);
|
||||||
|
// remove watch on toc file from both books.
|
||||||
|
if (sourceBook) {
|
||||||
|
fs.unwatchFile(movingElement.tableOfContentsPath);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await this.bookTocManager.updateBook(movingElement, updateBook, targetSection);
|
||||||
|
} catch (e) {
|
||||||
|
await this.bookTocManager.recovery();
|
||||||
|
vscode.window.showErrorMessage(loc.editBookError(updateBook.book.contentPath, e instanceof Error ? e.message : e));
|
||||||
|
} finally {
|
||||||
|
this.fireBookRefresh(targetBook);
|
||||||
|
if (sourceBook) {
|
||||||
|
// refresh source book model to pick up latest changes
|
||||||
|
this.fireBookRefresh(sourceBook);
|
||||||
|
}
|
||||||
|
// even if it fails, we still need to watch the toc file again.
|
||||||
|
if (sourceBook) {
|
||||||
|
this.setFileWatcher(sourceBook);
|
||||||
|
}
|
||||||
|
this.setFileWatcher(targetBook);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async openBook(bookPath: string, urlToOpen?: string, showPreview?: boolean, isNotebook?: boolean): Promise<void> {
|
async openBook(bookPath: string, urlToOpen?: string, showPreview?: boolean, isNotebook?: boolean): Promise<void> {
|
||||||
@@ -180,6 +269,11 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// if there is an error remove book from context
|
||||||
|
const index = this.books.findIndex(book => book.bookPath === bookPath);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.books.splice(index, 1);
|
||||||
|
}
|
||||||
vscode.window.showErrorMessage(loc.openFileError(bookPath, e instanceof Error ? e.message : e));
|
vscode.window.showErrorMessage(loc.openFileError(bookPath, e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,9 +361,9 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
|||||||
if (urlToOpen) {
|
if (urlToOpen) {
|
||||||
const bookRoot = this.currentBook.bookItems[0];
|
const bookRoot = this.currentBook.bookItems[0];
|
||||||
const sectionToOpen = bookRoot.findChildSection(urlToOpen);
|
const sectionToOpen = bookRoot.findChildSection(urlToOpen);
|
||||||
urlPath = sectionToOpen?.url;
|
urlPath = sectionToOpen?.file;
|
||||||
} else {
|
} else {
|
||||||
urlPath = this.currentBook.version === BookVersion.v1 ? (this.currentBook.bookItems[0].tableOfContents.sections[0] as IJupyterBookSectionV1).url : (this.currentBook.bookItems[0].tableOfContents.sections[0] as IJupyterBookSectionV2).file;
|
urlPath = this.currentBook.bookItems[0].tableOfContents.sections[0].file;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (urlPath) {
|
if (urlPath) {
|
||||||
@@ -434,20 +528,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
|||||||
public async searchJupyterBooks(treeItem?: BookTreeItem): Promise<void> {
|
public async searchJupyterBooks(treeItem?: BookTreeItem): Promise<void> {
|
||||||
let folderToSearch: string;
|
let folderToSearch: string;
|
||||||
if (treeItem && treeItem.sections !== undefined) {
|
if (treeItem && treeItem.sections !== undefined) {
|
||||||
if (treeItem.book.version === BookVersion.v1) {
|
folderToSearch = treeItem.uri ? getContentPath(treeItem.version, treeItem.book.root, path.dirname(treeItem.uri)) : getContentPath(treeItem.version, treeItem.book.root, '');
|
||||||
if (treeItem.uri) {
|
|
||||||
folderToSearch = path.join(treeItem.book.root, content, path.dirname(treeItem.uri));
|
|
||||||
} else {
|
|
||||||
folderToSearch = path.join(treeItem.root, content);
|
|
||||||
}
|
|
||||||
} else if (treeItem.book.version === BookVersion.v2) {
|
|
||||||
if (treeItem.uri) {
|
|
||||||
folderToSearch = path.join(treeItem.book.root, path.dirname(treeItem.uri));
|
|
||||||
} else {
|
|
||||||
folderToSearch = path.join(treeItem.root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (this.currentBook && !this.currentBook.isNotebook) {
|
} else if (this.currentBook && !this.currentBook.isNotebook) {
|
||||||
folderToSearch = path.join(this.currentBook.contentFolderPath);
|
folderToSearch = path.join(this.currentBook.contentFolderPath);
|
||||||
} else {
|
} else {
|
||||||
@@ -573,19 +654,14 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider<BookTreeIte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional method on the vscode interface.
|
||||||
|
* Implementing getParent, due to reveal method in extHostTreeView.ts
|
||||||
|
* throwing error if it is not implemented.
|
||||||
|
*/
|
||||||
getParent(element?: BookTreeItem): vscode.ProviderResult<BookTreeItem> {
|
getParent(element?: BookTreeItem): vscode.ProviderResult<BookTreeItem> {
|
||||||
if (element?.uri) {
|
// Remove it for perf issues.
|
||||||
let parentPath: string;
|
return undefined;
|
||||||
let contentFolder = element.book.version === BookVersion.v1 ? path.join(element.book.root, content) : element.book.root;
|
|
||||||
parentPath = path.join(contentFolder, element.uri.substring(0, element.uri.lastIndexOf(path.posix.sep)));
|
|
||||||
if (parentPath === element.root) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
let parentPaths = Array.from(this.currentBook.getAllNotebooks()?.keys()).filter(x => x.indexOf(parentPath) > -1);
|
|
||||||
return parentPaths.length > 0 ? this.currentBook.getAllNotebooks().get(parentPaths[0]) : undefined;
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getUntitledNotebookUri(resource: string): vscode.Uri {
|
getUntitledNotebookUri(resource: string): vscode.Uri {
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import * as path from 'path';
|
|||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
import * as constants from './../common/constants';
|
import * as constants from './../common/constants';
|
||||||
import { BookTreeItem } from './bookTreeItem';
|
import { BookTreeItem } from './bookTreeItem';
|
||||||
import { BookModel, BookVersion } from './bookModel';
|
import { BookModel } from './bookModel';
|
||||||
|
import { BookVersion } from './bookVersionHandler';
|
||||||
|
|
||||||
export interface IBookTrustManager {
|
export interface IBookTrustManager {
|
||||||
isNotebookTrustedByDefault(notebookUri: string): boolean;
|
isNotebookTrustedByDefault(notebookUri: string): boolean;
|
||||||
|
|||||||
128
extensions/notebook/src/book/bookVersionHandler.ts
Normal file
128
extensions/notebook/src/book/bookVersionHandler.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import { JupyterBookSection, IJupyterBookSectionV1, IJupyterBookSectionV2 } from '../contracts/content';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export enum BookVersion {
|
||||||
|
v1 = 'v1',
|
||||||
|
v2 = 'v2'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getContentPath(version: string, bookPath: string, filePath: string): string {
|
||||||
|
return BookVersion.v1 === version ? path.posix.join(bookPath, 'content', filePath) : path.posix.join(bookPath, filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTocPath(version: string, bookPath: string): string {
|
||||||
|
return BookVersion.v1 === version ? path.posix.join(bookPath, '_data', 'toc.yml') : path.posix.join(bookPath, '_toc.yml');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a section to JupyterSection, which is the union of Jupyter Book v1 and v2 interfaces.
|
||||||
|
* There are conflicting properties between v1 and v2 Jupyter Book toc properties,
|
||||||
|
* this method converts v1 to v2 while keeping the v1 properties that do not exist in v2.
|
||||||
|
* @param version Version of the section that will be converted
|
||||||
|
* @param section The section that'll be converted.
|
||||||
|
*/
|
||||||
|
export function convertFrom(version: string, section: JupyterBookSection): JupyterBookSection {
|
||||||
|
if (version === BookVersion.v1) {
|
||||||
|
return Object.assign(section, {
|
||||||
|
title: section.title,
|
||||||
|
file: (section as IJupyterBookSectionV1).external ? undefined : section.url,
|
||||||
|
url: (section as IJupyterBookSectionV1).external ? section.url : undefined,
|
||||||
|
sections: section.sections,
|
||||||
|
expand_sections: section.expand_sections,
|
||||||
|
search: (section as IJupyterBookSectionV1).search,
|
||||||
|
divider: (section as IJupyterBookSectionV1).divider,
|
||||||
|
header: (section as IJupyterBookSectionV1).header,
|
||||||
|
external: (section as IJupyterBookSectionV1).external,
|
||||||
|
numbered: !(section as IJupyterBookSectionV1).not_numbered,
|
||||||
|
not_numbered: undefined
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return Object.assign(section, {
|
||||||
|
title: section.title,
|
||||||
|
file: (section as IJupyterBookSectionV2).file,
|
||||||
|
url: section.url,
|
||||||
|
sections: section.sections,
|
||||||
|
expand_sections: section.expand_sections,
|
||||||
|
numbered: (section as IJupyterBookSectionV2).numbered,
|
||||||
|
header: (section as IJupyterBookSectionV2).header,
|
||||||
|
chapters: (section as IJupyterBookSectionV2).chapters,
|
||||||
|
part: (section as IJupyterBookSectionV2).part
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the JupyterSection to either Jupyter Book v1 or v2.
|
||||||
|
* @param version Version of the section that will be converted
|
||||||
|
* @param section The section that'll be converted.
|
||||||
|
*/
|
||||||
|
export function convertTo(version: string, section: JupyterBookSection): JupyterBookSection {
|
||||||
|
if (version === BookVersion.v1) {
|
||||||
|
if (section.sections && section.sections.length > 0) {
|
||||||
|
let temp: JupyterBookSection = {};
|
||||||
|
temp.title = section.title;
|
||||||
|
temp.url = section.url ? section.url : section.file;
|
||||||
|
temp.expand_sections = section.expand_sections;
|
||||||
|
temp.not_numbered = !section.numbered;
|
||||||
|
temp.search = section.search;
|
||||||
|
temp.divider = section.divider;
|
||||||
|
temp.header = section.header;
|
||||||
|
temp.external = section.external;
|
||||||
|
temp.sections = [];
|
||||||
|
for (let s of section.sections) {
|
||||||
|
const child = this.convertTo(version, s);
|
||||||
|
temp.sections.push(child);
|
||||||
|
}
|
||||||
|
return temp;
|
||||||
|
} else {
|
||||||
|
let newSection: JupyterBookSection = {};
|
||||||
|
newSection.title = section.title;
|
||||||
|
newSection.url = section.url ? section.url : section.file;
|
||||||
|
newSection.sections = section.sections;
|
||||||
|
newSection.not_numbered = !section.numbered;
|
||||||
|
newSection.expand_sections = section.expand_sections;
|
||||||
|
newSection.search = section.search;
|
||||||
|
newSection.divider = section.divider;
|
||||||
|
newSection.header = section.header;
|
||||||
|
newSection.external = section.external;
|
||||||
|
return newSection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (version === BookVersion.v2) {
|
||||||
|
if (section.sections && section.sections.length > 0) {
|
||||||
|
let temp: JupyterBookSection = {};
|
||||||
|
temp.title = section.title;
|
||||||
|
temp.file = section.file;
|
||||||
|
temp.expand_sections = section.expand_sections;
|
||||||
|
temp.header = section.header;
|
||||||
|
temp.numbered = section.numbered;
|
||||||
|
temp.part = section.part;
|
||||||
|
temp.chapters = section.chapters;
|
||||||
|
temp.url = section.url;
|
||||||
|
temp.sections = [];
|
||||||
|
for (let s of section.sections) {
|
||||||
|
const child = this.convertTo(version, s);
|
||||||
|
temp.sections.push(child);
|
||||||
|
}
|
||||||
|
return temp;
|
||||||
|
} else {
|
||||||
|
let newSection: JupyterBookSection = {};
|
||||||
|
newSection.title = section.title;
|
||||||
|
newSection.file = section.file;
|
||||||
|
newSection.sections = section.sections;
|
||||||
|
newSection.expand_sections = section.expand_sections;
|
||||||
|
newSection.header = section.header;
|
||||||
|
newSection.numbered = section.numbered;
|
||||||
|
newSection.part = section.part;
|
||||||
|
newSection.chapters = section.chapters;
|
||||||
|
newSection.url = section.url;
|
||||||
|
return newSection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
@@ -31,10 +31,13 @@ export function msgBookPinned(book: string): string { return localize('msgBookPi
|
|||||||
export function msgBookUnpinned(book: string): string { return localize('msgBookUnpinned', "Book {0} is no longer pinned in this workspace", book); }
|
export function msgBookUnpinned(book: string): string { return localize('msgBookUnpinned', "Book {0} is no longer pinned in this workspace", book); }
|
||||||
export const missingTocError = localize('bookInitializeFailed', "Failed to find a Table of Contents file in the specified book.");
|
export const missingTocError = localize('bookInitializeFailed', "Failed to find a Table of Contents file in the specified book.");
|
||||||
export const noBooksSelectedError = localize('noBooksSelected', "No books are currently selected in the viewlet.");
|
export const noBooksSelectedError = localize('noBooksSelected', "No books are currently selected in the viewlet.");
|
||||||
|
export const labelBookSection = localize('labelBookSection', "Select Book Section");
|
||||||
|
export const labelAddToLevel = localize('labelAddToLevel', "Add to this level");
|
||||||
|
|
||||||
export function missingFileError(title: string): string { return localize('missingFileError', "Missing file : {0}", title); }
|
export function missingFileError(title: string, path: string): string { return localize('missingFileError', "Missing file : {0} from {1}", title, path); }
|
||||||
export function invalidTocFileError(): string { return localize('InvalidError.tocFile', "Invalid toc file"); }
|
export function invalidTocFileError(): string { return localize('InvalidError.tocFile', "Invalid toc file"); }
|
||||||
export function invalidTocError(title: string): string { return localize('Invalid toc.yml', "Error: {0} has an incorrect toc.yml file", title); }
|
export function invalidTocError(title: string): string { return localize('Invalid toc.yml', "Error: {0} has an incorrect toc.yml file", title); }
|
||||||
|
export function configFileError(): string { return localize('configFileError', "Configuration file missing"); }
|
||||||
|
|
||||||
export function openFileError(path: string, error: string): string { return localize('openBookError', "Open book {0} failed: {1}", path, error); }
|
export function openFileError(path: string, error: string): string { return localize('openBookError', "Open book {0} failed: {1}", path, error); }
|
||||||
export function readBookError(path: string, error: string): string { return localize('readBookError', "Failed to read book {0}: {1}", path, error); }
|
export function readBookError(path: string, error: string): string { return localize('readBookError', "Failed to read book {0}: {1}", path, error); }
|
||||||
@@ -43,6 +46,9 @@ export function openMarkdownError(resource: string, error: string): string { ret
|
|||||||
export function openUntitledNotebookError(resource: string, error: string): string { return localize('openUntitledNotebookError', "Open untitled notebook {0} as untitled failed: {1}", resource, error); }
|
export function openUntitledNotebookError(resource: string, error: string): string { return localize('openUntitledNotebookError', "Open untitled notebook {0} as untitled failed: {1}", resource, error); }
|
||||||
export function openExternalLinkError(resource: string, error: string): string { return localize('openExternalLinkError', "Open link {0} failed: {1}", resource, error); }
|
export function openExternalLinkError(resource: string, error: string): string { return localize('openExternalLinkError', "Open link {0} failed: {1}", resource, error); }
|
||||||
export function closeBookError(resource: string, error: string): string { return localize('closeBookError', "Close book {0} failed: {1}", resource, error); }
|
export function closeBookError(resource: string, error: string): string { return localize('closeBookError', "Close book {0} failed: {1}", resource, error); }
|
||||||
|
export function duplicateFileError(title: string, path: string, newPath: string): string { return localize('duplicateFileError', "File {0} already exists in the destination folder {1} \n The file has been renamed to {2} to prevent data loss.", title, path, newPath); }
|
||||||
|
export function editBookError(path: string, error: string): string { return localize('editBookError', "Error while editing book {0}: {1}", path, error); }
|
||||||
|
export function selectBookError(error: string): string { return localize('selectBookError', "Error while selecting a book or a section to edit: {0}", error); }
|
||||||
|
|
||||||
// Remote Book dialog constants
|
// Remote Book dialog constants
|
||||||
export const url = localize('url', "URL");
|
export const url = localize('url', "URL");
|
||||||
|
|||||||
@@ -86,11 +86,11 @@ export interface IJupyterBookSectionV1 {
|
|||||||
/**
|
/**
|
||||||
* Contains a list of more entries that make up the chapter's/section's sub-sections
|
* Contains a list of more entries that make up the chapter's/section's sub-sections
|
||||||
*/
|
*/
|
||||||
sections?: IJupyterBookSectionV1[];
|
sections?: JupyterBookSection[];
|
||||||
/**
|
/**
|
||||||
* If the section shouldn't have a number in the sidebar
|
* If the section shouldn't have a number in the sidebar
|
||||||
*/
|
*/
|
||||||
not_numbered?: string;
|
not_numbered?: boolean;
|
||||||
/**
|
/**
|
||||||
* If you'd like the sections of this chapter to always be expanded in the sidebar.
|
* If you'd like the sections of this chapter to always be expanded in the sidebar.
|
||||||
*/
|
*/
|
||||||
@@ -113,7 +113,7 @@ export interface IJupyterBookSectionV1 {
|
|||||||
/**
|
/**
|
||||||
* Will insert a header with no link in the sidebar
|
* Will insert a header with no link in the sidebar
|
||||||
*/
|
*/
|
||||||
header?: boolean;
|
header?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,7 +134,7 @@ export interface IJupyterBookSectionV2 {
|
|||||||
/**
|
/**
|
||||||
* Contains a list of more entries that make up the chapter's/section's sub-sections
|
* Contains a list of more entries that make up the chapter's/section's sub-sections
|
||||||
*/
|
*/
|
||||||
sections?: IJupyterBookSectionV2[];
|
sections?: JupyterBookSection[];
|
||||||
/**
|
/**
|
||||||
* If the section shouldn't have a number in the sidebar
|
* If the section shouldn't have a number in the sidebar
|
||||||
*/
|
*/
|
||||||
@@ -147,8 +147,23 @@ export interface IJupyterBookSectionV2 {
|
|||||||
* External link
|
* External link
|
||||||
*/
|
*/
|
||||||
url?: string;
|
url?: string;
|
||||||
|
|
||||||
|
// Below are some special values that trigger specific behavior:
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will insert a header with no link in the sidebar
|
||||||
|
*/
|
||||||
|
header?: string;
|
||||||
|
/**
|
||||||
|
* If a book is divided into groups then part is the title of the group
|
||||||
|
*/
|
||||||
|
part?: string;
|
||||||
|
/**
|
||||||
|
* the equivalent of sections in a group.
|
||||||
|
*/
|
||||||
|
chapters?: string[];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// type that supports new and old version
|
export interface JupyterBookSection extends IJupyterBookSectionV1, IJupyterBookSectionV2 { }
|
||||||
export type JupyterBookSection = IJupyterBookSectionV1 | IJupyterBookSectionV2;
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { RemoteBookDialog } from './dialog/remoteBookDialog';
|
|||||||
import { RemoteBookDialogModel } from './dialog/remoteBookDialogModel';
|
import { RemoteBookDialogModel } from './dialog/remoteBookDialogModel';
|
||||||
import { IconPathHelper } from './common/iconHelper';
|
import { IconPathHelper } from './common/iconHelper';
|
||||||
import { ExtensionContextHelper } from './common/extensionContextHelper';
|
import { ExtensionContextHelper } from './common/extensionContextHelper';
|
||||||
|
import { BookTreeItem } from './book/bookTreeItem';
|
||||||
|
|
||||||
const localize = nls.loadMessageBundle();
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
@@ -69,6 +70,10 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.moveTo', async (book: BookTreeItem) => {
|
||||||
|
await bookTreeViewProvider.editBook(book);
|
||||||
|
}));
|
||||||
|
|
||||||
let model = new RemoteBookDialogModel();
|
let model = new RemoteBookDialogModel();
|
||||||
let remoteBookController = new RemoteBookController(model, appContext.outputChannel);
|
let remoteBookController = new RemoteBookController(model, appContext.outputChannel);
|
||||||
|
|
||||||
|
|||||||
@@ -19,12 +19,13 @@ import { exists } from '../../common/utils';
|
|||||||
import { BookModel } from '../../book/bookModel';
|
import { BookModel } from '../../book/bookModel';
|
||||||
import { BookTrustManager } from '../../book/bookTrustManager';
|
import { BookTrustManager } from '../../book/bookTrustManager';
|
||||||
import { NavigationProviders } from '../../common/constants';
|
import { NavigationProviders } from '../../common/constants';
|
||||||
import { readBookError } from '../../common/localizedConstants';
|
import { openFileError } from '../../common/localizedConstants';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { AppContext } from '../../common/appContext';
|
import { AppContext } from '../../common/appContext';
|
||||||
|
|
||||||
export interface IExpectedBookItem {
|
export interface IExpectedBookItem {
|
||||||
title: string;
|
title: string;
|
||||||
|
file?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
sections?: any[];
|
sections?: any[];
|
||||||
external?: boolean;
|
external?: boolean;
|
||||||
@@ -34,7 +35,11 @@ export interface IExpectedBookItem {
|
|||||||
|
|
||||||
export function equalBookItems(book: BookTreeItem, expectedBook: IExpectedBookItem, errorMsg?: string): void {
|
export function equalBookItems(book: BookTreeItem, expectedBook: IExpectedBookItem, errorMsg?: string): void {
|
||||||
should(book.title).equal(expectedBook.title, `Book titles do not match, expected ${expectedBook?.title} and got ${book?.title}`);
|
should(book.title).equal(expectedBook.title, `Book titles do not match, expected ${expectedBook?.title} and got ${book?.title}`);
|
||||||
should(path.posix.parse(book.uri)).deepEqual(path.posix.parse(expectedBook.url));
|
if (expectedBook.file) {
|
||||||
|
should(path.posix.parse(book.uri)).deepEqual(path.posix.parse(expectedBook.file));
|
||||||
|
} else {
|
||||||
|
should(path.posix.parse(book.uri)).deepEqual(path.posix.parse(expectedBook.url));
|
||||||
|
}
|
||||||
if (expectedBook.previousUri || expectedBook.nextUri) {
|
if (expectedBook.previousUri || expectedBook.nextUri) {
|
||||||
let prevUri = book.previousUri ? book.previousUri.toLocaleLowerCase() : undefined;
|
let prevUri = book.previousUri ? book.previousUri.toLocaleLowerCase() : undefined;
|
||||||
let expectedPrevUri = expectedBook.previousUri ? expectedBook.previousUri.replace(/\\/g, '/') : undefined;
|
let expectedPrevUri = expectedBook.previousUri ? expectedBook.previousUri.replace(/\\/g, '/') : undefined;
|
||||||
@@ -75,25 +80,25 @@ describe('BooksTreeViewTests', function () {
|
|||||||
expectedNotebook1 = {
|
expectedNotebook1 = {
|
||||||
// tslint:disable-next-line: quotemark
|
// tslint:disable-next-line: quotemark
|
||||||
title: 'Notebook1',
|
title: 'Notebook1',
|
||||||
url: '/notebook1',
|
file: '/notebook1',
|
||||||
previousUri: undefined,
|
previousUri: undefined,
|
||||||
nextUri: notebook2File.toLocaleLowerCase()
|
nextUri: notebook2File.toLocaleLowerCase()
|
||||||
};
|
};
|
||||||
expectedNotebook2 = {
|
expectedNotebook2 = {
|
||||||
title: 'Notebook2',
|
title: 'Notebook2',
|
||||||
url: '/notebook2',
|
file: '/notebook2',
|
||||||
previousUri: notebook1File.toLocaleLowerCase(),
|
previousUri: notebook1File.toLocaleLowerCase(),
|
||||||
nextUri: notebook3File.toLocaleLowerCase()
|
nextUri: notebook3File.toLocaleLowerCase()
|
||||||
};
|
};
|
||||||
expectedNotebook3 = {
|
expectedNotebook3 = {
|
||||||
title: 'Notebook3',
|
title: 'Notebook3',
|
||||||
url: '/notebook3',
|
file: '/notebook3',
|
||||||
previousUri: notebook2File.toLocaleLowerCase(),
|
previousUri: notebook2File.toLocaleLowerCase(),
|
||||||
nextUri: undefined
|
nextUri: undefined
|
||||||
};
|
};
|
||||||
expectedMarkdown = {
|
expectedMarkdown = {
|
||||||
title: 'Markdown',
|
title: 'Markdown',
|
||||||
url: '/markdown'
|
file: '/markdown'
|
||||||
};
|
};
|
||||||
expectedExternalLink = {
|
expectedExternalLink = {
|
||||||
title: 'GitHub',
|
title: 'GitHub',
|
||||||
@@ -227,7 +232,7 @@ describe('BooksTreeViewTests', function () {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getParent should return when element is a valid child notebook', async () => {
|
it.skip('getParent should return when element is a valid child notebook', async () => {
|
||||||
let parent = await bookTreeViewProvider.getParent();
|
let parent = await bookTreeViewProvider.getParent();
|
||||||
should(parent).be.undefined();
|
should(parent).be.undefined();
|
||||||
|
|
||||||
@@ -417,53 +422,54 @@ describe('BooksTreeViewTests', function () {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('BookTreeViewProvider.getTableOfContentFiles', function() {
|
describe('BookTreeViewProvider.getTableOfContentFiles', function () {
|
||||||
let rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`);
|
let rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`);
|
||||||
let bookTreeViewProvider: BookTreeViewProvider;
|
let bookTreeViewProvider: BookTreeViewProvider;
|
||||||
let runs = [
|
let runs = [
|
||||||
{
|
{
|
||||||
it: 'v1',
|
it: 'v1',
|
||||||
folderPaths : {
|
folderPaths: {
|
||||||
'dataFolderPath' : path.join(rootFolderPath, '_data'),
|
'dataFolderPath': path.join(rootFolderPath, '_data'),
|
||||||
'contentFolderPath' : path.join(rootFolderPath, 'content'),
|
'contentFolderPath': path.join(rootFolderPath, 'content'),
|
||||||
'configFile' : path.join(rootFolderPath, '_config.yml'),
|
'configFile': path.join(rootFolderPath, '_config.yml'),
|
||||||
'tableOfContentsFile' : path.join(rootFolderPath,'_data', 'toc.yml'),
|
'tableOfContentsFile': path.join(rootFolderPath, '_data', 'toc.yml'),
|
||||||
'notebook2File' : path.join(rootFolderPath, 'content', 'notebook2.ipynb'),
|
'notebook2File': path.join(rootFolderPath, 'content', 'notebook2.ipynb'),
|
||||||
'tableOfContentsFileIgnore' : path.join(rootFolderPath, 'toc.yml')
|
'tableOfContentsFileIgnore': path.join(rootFolderPath, 'toc.yml')
|
||||||
},
|
},
|
||||||
contents : {
|
contents: {
|
||||||
'config' : 'title: Test Book',
|
'config': 'title: Test Book',
|
||||||
'toc' : '- title: Notebook1\n url: /notebook1\n sections:\n - title: Notebook2\n url: /notebook2\n - title: Notebook3\n url: /notebook3\n- title: Markdown\n url: /markdown\n- title: GitHub\n url: https://github.com/\n external: true'
|
'toc': '- title: Notebook1\n url: /notebook1\n sections:\n - title: Notebook2\n url: /notebook2\n - title: Notebook3\n url: /notebook3\n- title: Markdown\n url: /markdown\n- title: GitHub\n url: https://github.com/\n external: true'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
it: 'v2',
|
it: 'v2',
|
||||||
folderPaths : {
|
folderPaths: {
|
||||||
'dataFolderPath' : path.join(rootFolderPath, '_data'),
|
'dataFolderPath': path.join(rootFolderPath, '_data'),
|
||||||
'configFile' : path.join(rootFolderPath, '_config.yml'),
|
'configFile': path.join(rootFolderPath, '_config.yml'),
|
||||||
'tableOfContentsFile' : path.join(rootFolderPath, '_toc.yml'),
|
'tableOfContentsFile': path.join(rootFolderPath, '_toc.yml'),
|
||||||
'notebook2File' : path.join(rootFolderPath,'notebook2.ipynb'),
|
'notebook2File': path.join(rootFolderPath, 'notebook2.ipynb'),
|
||||||
'tableOfContentsFileIgnore' : path.join(rootFolderPath, '_data', 'toc.yml')
|
'tableOfContentsFileIgnore': path.join(rootFolderPath, '_data', 'toc.yml')
|
||||||
},
|
},
|
||||||
contents : {
|
contents: {
|
||||||
'config' : 'title: Test Book',
|
'config': 'title: Test Book',
|
||||||
'toc' : '- title: Notebook1\n file: /notebook1\n sections:\n - title: Notebook2\n file: /notebook2\n - title: Notebook3\n file: /notebook3\n- title: Markdown\n file: /markdown\n- title: GitHub\n url: https://github.com/\n'
|
'toc': '- title: Notebook1\n file: /notebook1\n sections:\n - title: Notebook2\n file: /notebook2\n - title: Notebook3\n file: /notebook3\n- title: Markdown\n file: /markdown\n- title: GitHub\n url: https://github.com/\n'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
runs.forEach(function (run){
|
runs.forEach(function (run) {
|
||||||
describe('BookTreeViewProvider.getTableOfContentFiles on ' + run.it, function (): void {
|
describe('BookTreeViewProvider.getTableOfContentFiles on ' + run.it, function (): void {
|
||||||
let folder: vscode.WorkspaceFolder;
|
let folder: vscode.WorkspaceFolder;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await fs.mkdir(rootFolderPath);
|
await fs.mkdir(rootFolderPath);
|
||||||
await fs.mkdir(run.folderPaths.dataFolderPath);
|
await fs.mkdir(run.folderPaths.dataFolderPath);
|
||||||
if(run.it === 'v1') {
|
if (run.it === 'v1') {
|
||||||
await fs.mkdir(run.folderPaths.contentFolderPath);
|
await fs.mkdir(run.folderPaths.contentFolderPath);
|
||||||
}
|
}
|
||||||
await fs.writeFile(run.folderPaths.tableOfContentsFile, run.contents.toc);
|
await fs.writeFile(run.folderPaths.tableOfContentsFile, run.contents.toc);
|
||||||
await fs.writeFile(run.folderPaths.tableOfContentsFileIgnore, '');
|
await fs.writeFile(run.folderPaths.tableOfContentsFileIgnore, '');
|
||||||
await fs.writeFile(run.folderPaths.notebook2File, '');
|
await fs.writeFile(run.folderPaths.notebook2File, '');
|
||||||
|
await fs.writeFile(run.folderPaths.configFile, run.contents.config);
|
||||||
|
|
||||||
const mockExtensionContext = new MockExtensionContext();
|
const mockExtensionContext = new MockExtensionContext();
|
||||||
folder = {
|
folder = {
|
||||||
@@ -474,16 +480,17 @@ describe('BooksTreeViewTests', function () {
|
|||||||
bookTreeViewProvider = new BookTreeViewProvider([folder], mockExtensionContext, false, 'bookTreeView', NavigationProviders.NotebooksNavigator);
|
bookTreeViewProvider = new BookTreeViewProvider([folder], mockExtensionContext, false, 'bookTreeView', NavigationProviders.NotebooksNavigator);
|
||||||
let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000));
|
let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000));
|
||||||
await Promise.race([bookTreeViewProvider.initialized, errorCase.then(() => { throw new Error('BookTreeViewProvider did not initialize in time'); })]);
|
await Promise.race([bookTreeViewProvider.initialized, errorCase.then(() => { throw new Error('BookTreeViewProvider did not initialize in time'); })]);
|
||||||
|
await bookTreeViewProvider.openBook(rootFolderPath, undefined, false, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
if(run.it === 'v1') {
|
if (run.it === 'v1') {
|
||||||
it('should ignore toc.yml files not in _data folder', async () => {
|
it('should ignore toc.yml files not in _data folder', async () => {
|
||||||
await bookTreeViewProvider.currentBook.readBookStructure(rootFolderPath);
|
await bookTreeViewProvider.currentBook.readBookStructure(rootFolderPath);
|
||||||
await bookTreeViewProvider.currentBook.loadTableOfContentFiles();
|
await bookTreeViewProvider.currentBook.loadTableOfContentFiles();
|
||||||
let path = bookTreeViewProvider.currentBook.tableOfContentsPath;
|
let path = bookTreeViewProvider.currentBook.tableOfContentsPath;
|
||||||
should(vscode.Uri.file(path).fsPath).equal(vscode.Uri.file(run.folderPaths.tableOfContentsFile).fsPath);
|
should(vscode.Uri.file(path).fsPath).equal(vscode.Uri.file(run.folderPaths.tableOfContentsFile).fsPath);
|
||||||
});
|
});
|
||||||
} else if (run.it === 'v2'){
|
} else if (run.it === 'v2') {
|
||||||
it('should ignore toc.yml files not under the root book folder', async () => {
|
it('should ignore toc.yml files not under the root book folder', async () => {
|
||||||
await bookTreeViewProvider.currentBook.readBookStructure(rootFolderPath);
|
await bookTreeViewProvider.currentBook.readBookStructure(rootFolderPath);
|
||||||
await bookTreeViewProvider.currentBook.loadTableOfContentFiles();
|
await bookTreeViewProvider.currentBook.loadTableOfContentFiles();
|
||||||
@@ -501,46 +508,47 @@ describe('BooksTreeViewTests', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('BookTreeViewProvider.getBooks', function() {
|
describe('BookTreeViewProvider.getBooks', function () {
|
||||||
let rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`);
|
let rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`);
|
||||||
let bookTreeViewProvider: BookTreeViewProvider;
|
let bookTreeViewProvider: BookTreeViewProvider;
|
||||||
let runs = [
|
let runs = [
|
||||||
{
|
{
|
||||||
it: 'v1',
|
it: 'v1',
|
||||||
folderPaths : {
|
folderPaths: {
|
||||||
'dataFolderPath' : path.join(rootFolderPath, '_data'),
|
'dataFolderPath': path.join(rootFolderPath, '_data'),
|
||||||
'contentFolderPath' : path.join(rootFolderPath, 'content'),
|
'contentFolderPath': path.join(rootFolderPath, 'content'),
|
||||||
'configFile' : path.join(rootFolderPath, '_config.yml'),
|
'configFile': path.join(rootFolderPath, '_config.yml'),
|
||||||
'tableofContentsFile' : path.join(rootFolderPath,'_data', 'toc.yml'),
|
'tableofContentsFile': path.join(rootFolderPath, '_data', 'toc.yml'),
|
||||||
'notebook2File' : path.join(rootFolderPath, 'content', 'notebook2.ipynb'),
|
'notebook2File': path.join(rootFolderPath, 'content', 'notebook2.ipynb'),
|
||||||
},
|
},
|
||||||
contents : {
|
contents: {
|
||||||
'config' : 'title: Test Book',
|
'config': 'title: Test Book',
|
||||||
'toc' : '- title: Notebook1\n url: /notebook1\n- title: Notebook2\n url: /notebook2'
|
'toc': '- title: Notebook1\n url: /notebook1\n- title: Notebook2\n url: /notebook2'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
it: 'v2',
|
it: 'v2',
|
||||||
folderPaths : {
|
folderPaths: {
|
||||||
'configFile' : path.join(rootFolderPath, '_config.yml'),
|
'configFile': path.join(rootFolderPath, '_config.yml'),
|
||||||
'tableofContentsFile' : path.join(rootFolderPath, '_toc.yml'),
|
'tableofContentsFile': path.join(rootFolderPath, '_toc.yml'),
|
||||||
'notebook2File' : path.join(rootFolderPath, 'notebook2.ipynb'),
|
'notebook2File': path.join(rootFolderPath, 'notebook2.ipynb'),
|
||||||
},
|
},
|
||||||
contents : {
|
contents: {
|
||||||
'config' : 'title: Test Book',
|
'config': 'title: Test Book',
|
||||||
'toc' : '- title: Notebook1\n file: /notebook1\n- title: Notebook2\n file: /notebook2'
|
'toc': '- title: Notebook1\n file: /notebook1\n- title: Notebook2\n file: /notebook2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
runs.forEach(function (run){
|
runs.forEach(function (run) {
|
||||||
describe('BookTreeViewProvider.getBooks on ' + run.it, function (): void {
|
describe('BookTreeViewProvider.getBooks on ' + run.it, function (): void {
|
||||||
let folder: vscode.WorkspaceFolder;
|
let folder: vscode.WorkspaceFolder;
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await fs.mkdir(rootFolderPath);
|
await fs.mkdir(rootFolderPath);
|
||||||
if(run.it === 'v1'){
|
if (run.it === 'v1') {
|
||||||
await fs.mkdir(run.folderPaths.dataFolderPath);
|
await fs.mkdir(run.folderPaths.dataFolderPath);
|
||||||
await fs.mkdir(run.folderPaths.contentFolderPath);
|
await fs.mkdir(run.folderPaths.contentFolderPath);
|
||||||
}
|
}
|
||||||
await fs.writeFile(run.folderPaths.tableofContentsFile, run.contents.config);
|
await fs.writeFile(run.folderPaths.tableofContentsFile, run.contents.config);
|
||||||
|
await fs.writeFile(run.folderPaths.configFile, run.contents.config);
|
||||||
const mockExtensionContext = new MockExtensionContext();
|
const mockExtensionContext = new MockExtensionContext();
|
||||||
folder = {
|
folder = {
|
||||||
uri: vscode.Uri.file(rootFolderPath),
|
uri: vscode.Uri.file(rootFolderPath),
|
||||||
@@ -553,14 +561,22 @@ describe('BooksTreeViewTests', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should show error message if config.yml file not found', async () => {
|
it('should show error message if config.yml file not found', async () => {
|
||||||
await bookTreeViewProvider.currentBook.readBooks();
|
try {
|
||||||
should(bookTreeViewProvider.currentBook.errorMessage).equal(readBookError(bookTreeViewProvider.currentBook.bookPath, `ENOENT: no such file or directory, open '${run.folderPaths.configFile}'`));
|
await bookTreeViewProvider.openBook(rootFolderPath, undefined, false, false);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
should(error).equal(openFileError(bookTreeViewProvider.currentBook.bookPath, `ENOENT: no such file or directory, open '${run.folderPaths.configFile}'`));
|
||||||
|
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show error if toc.yml file format is invalid', async function (): Promise<void> {
|
it('should show error if toc.yml file format is invalid', async function (): Promise<void> {
|
||||||
await fs.writeFile(run.folderPaths.configFile, run.contents.config);
|
try {
|
||||||
await bookTreeViewProvider.currentBook.readBooks();
|
await fs.writeFile(run.folderPaths.configFile, run.contents.config);
|
||||||
should(bookTreeViewProvider.currentBook.errorMessage).equal(readBookError(bookTreeViewProvider.currentBook.bookPath, `Invalid toc file`));
|
await bookTreeViewProvider.openBook(rootFolderPath, undefined, false, false);
|
||||||
|
} catch (error) {
|
||||||
|
should(error).equal(openFileError(bookTreeViewProvider.currentBook.bookPath, `Invalid toc file`));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async function (): Promise<void> {
|
after(async function (): Promise<void> {
|
||||||
@@ -568,125 +584,127 @@ describe('BooksTreeViewTests', function () {
|
|||||||
await promisify(rimraf)(rootFolderPath);
|
await promisify(rimraf)(rootFolderPath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})});
|
})
|
||||||
|
});
|
||||||
|
|
||||||
describe('BookTreeViewProvider.getSections', function() {
|
describe('BookTreeViewProvider.getSections', function () {
|
||||||
let rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`);
|
let rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`);
|
||||||
let bookTreeViewProvider: BookTreeViewProvider;
|
let bookTreeViewProvider: BookTreeViewProvider;
|
||||||
let runs = [
|
let runs = [
|
||||||
{
|
{
|
||||||
it: 'v1',
|
it: 'v1',
|
||||||
folderPaths : {
|
folderPaths: {
|
||||||
'dataFolderPath' : path.join(rootFolderPath, '_data'),
|
'dataFolderPath': path.join(rootFolderPath, '_data'),
|
||||||
'contentFolderPath' : path.join(rootFolderPath, 'content'),
|
'contentFolderPath': path.join(rootFolderPath, 'content'),
|
||||||
'configFile' : path.join(rootFolderPath, '_config.yml'),
|
'configFile': path.join(rootFolderPath, '_config.yml'),
|
||||||
'tableofContentsFile' : path.join(rootFolderPath,'_data', 'toc.yml'),
|
'tableofContentsFile': path.join(rootFolderPath, '_data', 'toc.yml'),
|
||||||
'notebook2File' : path.join(rootFolderPath, 'content', 'notebook2.ipynb'),
|
'notebook2File': path.join(rootFolderPath, 'content', 'notebook2.ipynb'),
|
||||||
},
|
},
|
||||||
contents : {
|
contents: {
|
||||||
'config' : 'title: Test Book',
|
'config': 'title: Test Book',
|
||||||
'toc' : '- title: Notebook1\n url: /notebook1\n- title: Notebook2\n url: /notebook2'
|
'toc': '- title: Notebook1\n url: /notebook1\n- title: Notebook2\n url: /notebook2'
|
||||||
}
|
}
|
||||||
},{
|
}, {
|
||||||
it: 'v2',
|
it: 'v2',
|
||||||
folderPaths : {
|
folderPaths: {
|
||||||
'configFile' : path.join(rootFolderPath, '_config.yml'),
|
'configFile': path.join(rootFolderPath, '_config.yml'),
|
||||||
'tableofContentsFile' : path.join(rootFolderPath,'_toc.yml'),
|
'tableofContentsFile': path.join(rootFolderPath, '_toc.yml'),
|
||||||
'notebook2File' : path.join(rootFolderPath,'notebook2.ipynb'),
|
'notebook2File': path.join(rootFolderPath, 'notebook2.ipynb'),
|
||||||
},
|
},
|
||||||
contents : {
|
contents: {
|
||||||
'config' : 'title: Test Book',
|
'config': 'title: Test Book',
|
||||||
'toc' : '- title: Notebook1\n file: /notebook1\n- title: Notebook2\n file: /notebook2'
|
'toc': '- title: Notebook1\n file: /notebook1\n- title: Notebook2\n file: /notebook2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
runs.forEach(function (run){
|
runs.forEach(function (run) {
|
||||||
describe('BookTreeViewProvider.getSections on ' + run.it, function (): void {
|
describe('BookTreeViewProvider.getSections on ' + run.it, function (): void {
|
||||||
let folder: vscode.WorkspaceFolder[];
|
let folder: vscode.WorkspaceFolder[];
|
||||||
let expectedNotebook2: IExpectedBookItem;
|
let expectedNotebook2: IExpectedBookItem;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
expectedNotebook2 = {
|
expectedNotebook2 = {
|
||||||
title: 'Notebook2',
|
title: 'Notebook2',
|
||||||
url: '/notebook2',
|
file: '/notebook2',
|
||||||
previousUri: undefined,
|
previousUri: undefined,
|
||||||
nextUri: undefined
|
nextUri: undefined
|
||||||
};
|
};
|
||||||
await fs.mkdir(rootFolderPath);
|
await fs.mkdir(rootFolderPath);
|
||||||
if(run.it === 'v1'){
|
if (run.it === 'v1') {
|
||||||
await fs.mkdir(run.folderPaths.dataFolderPath);
|
await fs.mkdir(run.folderPaths.dataFolderPath);
|
||||||
await fs.mkdir(run.folderPaths.contentFolderPath);
|
await fs.mkdir(run.folderPaths.contentFolderPath);
|
||||||
}
|
}
|
||||||
await fs.writeFile(run.folderPaths.configFile, run.contents.config);
|
await fs.writeFile(run.folderPaths.configFile, run.contents.config);
|
||||||
await fs.writeFile(run.folderPaths.tableofContentsFile, run.contents.toc);
|
await fs.writeFile(run.folderPaths.tableofContentsFile, run.contents.toc);
|
||||||
await fs.writeFile(run.folderPaths.notebook2File, '');
|
await fs.writeFile(run.folderPaths.notebook2File, '');
|
||||||
|
|
||||||
const mockExtensionContext = new MockExtensionContext();
|
const mockExtensionContext = new MockExtensionContext();
|
||||||
folder = [{
|
folder = [{
|
||||||
uri: vscode.Uri.file(rootFolderPath),
|
uri: vscode.Uri.file(rootFolderPath),
|
||||||
name: '',
|
name: '',
|
||||||
index: 0
|
index: 0
|
||||||
}];
|
}];
|
||||||
bookTreeViewProvider = new BookTreeViewProvider(folder, mockExtensionContext, false, 'bookTreeView', NavigationProviders.NotebooksNavigator);
|
bookTreeViewProvider = new BookTreeViewProvider(folder, mockExtensionContext, false, 'bookTreeView', NavigationProviders.NotebooksNavigator);
|
||||||
let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000));
|
let errorCase = new Promise((resolve, reject) => setTimeout(() => resolve(), 5000));
|
||||||
await Promise.race([bookTreeViewProvider.initialized, errorCase.then(() => { throw new Error('BookTreeViewProvider did not initialize in time'); })]);
|
await Promise.race([bookTreeViewProvider.initialized, errorCase.then(() => { throw new Error('BookTreeViewProvider did not initialize in time'); })]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show error if notebook or markdown file is missing', async function (): Promise<void> {
|
||||||
|
let books: BookTreeItem[] = bookTreeViewProvider.currentBook.bookItems;
|
||||||
|
let children = await bookTreeViewProvider.currentBook.getSections({ sections: [] }, books[0].sections, rootFolderPath, books[0].book);
|
||||||
|
should(bookTreeViewProvider.currentBook.errorMessage).equal('Missing file : Notebook1 from '.concat(bookTreeViewProvider.currentBook.bookPath));
|
||||||
|
// rest of book should be detected correctly even with a missing file
|
||||||
|
equalBookItems(children[0], expectedNotebook2);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async function (): Promise<void> {
|
||||||
|
if (await exists(rootFolderPath)) await promisify(rimraf)(rootFolderPath);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
it('should show error if notebook or markdown file is missing', async function (): Promise<void> {
|
describe('BookTreeViewProvider.Commands', function () {
|
||||||
let books: BookTreeItem[] = bookTreeViewProvider.currentBook.bookItems;
|
|
||||||
let children = await bookTreeViewProvider.currentBook.getSections({ sections: [] }, books[0].sections, rootFolderPath, books[0].book);
|
|
||||||
should(bookTreeViewProvider.currentBook.errorMessage).equal('Missing file : Notebook1');
|
|
||||||
// rest of book should be detected correctly even with a missing file
|
|
||||||
equalBookItems(children[0], expectedNotebook2);
|
|
||||||
});
|
|
||||||
|
|
||||||
after(async function (): Promise<void> {
|
|
||||||
if (await exists(rootFolderPath)) await promisify(rimraf)(rootFolderPath);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})});
|
|
||||||
|
|
||||||
describe('BookTreeViewProvider.Commands', function() {
|
|
||||||
let rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`);
|
let rootFolderPath = path.join(os.tmpdir(), `BookTestData_${uuid.v4()}`);
|
||||||
let bookTreeViewProvider: BookTreeViewProvider;
|
let bookTreeViewProvider: BookTreeViewProvider;
|
||||||
let runs = [
|
let runs = [
|
||||||
{
|
{
|
||||||
it: 'v1',
|
it: 'v1',
|
||||||
folderPaths : {
|
folderPaths: {
|
||||||
'dataFolderPath' : path.join(rootFolderPath, '_data'),
|
'dataFolderPath': path.join(rootFolderPath, '_data'),
|
||||||
'contentFolderPath' : path.join(rootFolderPath, 'content'),
|
'contentFolderPath': path.join(rootFolderPath, 'content'),
|
||||||
'configFile' : path.join(rootFolderPath, '_config.yml'),
|
'configFile': path.join(rootFolderPath, '_config.yml'),
|
||||||
'tableofContentsFile' : path.join(rootFolderPath,'_data', 'toc.yml'),
|
'tableofContentsFile': path.join(rootFolderPath, '_data', 'toc.yml'),
|
||||||
'notebook1File' : path.join(rootFolderPath, 'content', 'notebook1.ipynb'),
|
'notebook1File': path.join(rootFolderPath, 'content', 'notebook1.ipynb'),
|
||||||
'notebook2File' : path.join(rootFolderPath, 'content', 'notebook2.ipynb'),
|
'notebook2File': path.join(rootFolderPath, 'content', 'notebook2.ipynb'),
|
||||||
'markdownFile' : path.join(rootFolderPath, 'content', 'readme.md')
|
'markdownFile': path.join(rootFolderPath, 'content', 'readme.md')
|
||||||
},
|
},
|
||||||
contents : {
|
contents: {
|
||||||
'config' : 'title: Test Book',
|
'config': 'title: Test Book',
|
||||||
'toc' : '- title: Home\n url: /readme\n- title: Notebook1\n url: /notebook1\n- title: Notebook2\n url: /notebook2'
|
'toc': '- title: Home\n url: /readme\n- title: Notebook1\n url: /notebook1\n- title: Notebook2\n url: /notebook2'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
it: 'v2',
|
it: 'v2',
|
||||||
folderPaths : {
|
folderPaths: {
|
||||||
'configFile' : path.join(rootFolderPath, '_config.yml'),
|
'configFile': path.join(rootFolderPath, '_config.yml'),
|
||||||
'tableofContentsFile' : path.join(rootFolderPath, '_toc.yml'),
|
'tableofContentsFile': path.join(rootFolderPath, '_toc.yml'),
|
||||||
'notebook1File' : path.join(rootFolderPath, 'notebook1.ipynb'),
|
'notebook1File': path.join(rootFolderPath, 'notebook1.ipynb'),
|
||||||
'notebook2File' : path.join(rootFolderPath, 'notebook2.ipynb'),
|
'notebook2File': path.join(rootFolderPath, 'notebook2.ipynb'),
|
||||||
'markdownFile' : path.join(rootFolderPath, 'readme.md')
|
'markdownFile': path.join(rootFolderPath, 'readme.md')
|
||||||
},
|
},
|
||||||
contents : {
|
contents: {
|
||||||
'config' : 'title: Test Book',
|
'config': 'title: Test Book',
|
||||||
'toc' : '- title: Home\n file: /readme\n- title: Notebook1\n file: /notebook1\n- title: Notebook2\n file: /notebook2'
|
'toc': '- title: Home\n file: /readme\n- title: Notebook1\n file: /notebook1\n- title: Notebook2\n file: /notebook2'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
runs.forEach(function (run){
|
runs.forEach(function (run) {
|
||||||
describe('BookTreeViewProvider.Commands on ' + run.it, function (): void {
|
describe('BookTreeViewProvider.Commands on ' + run.it, function (): void {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
await fs.mkdir(rootFolderPath);
|
await fs.mkdir(rootFolderPath);
|
||||||
if(run.it === 'v1'){
|
if (run.it === 'v1') {
|
||||||
await fs.mkdir(run.folderPaths.dataFolderPath);
|
await fs.mkdir(run.folderPaths.dataFolderPath);
|
||||||
await fs.mkdir(run.folderPaths.contentFolderPath);
|
await fs.mkdir(run.folderPaths.contentFolderPath);
|
||||||
}
|
}
|
||||||
@@ -836,53 +854,53 @@ describe('BooksTreeViewTests', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('BookTreeViewProvider.openNotebookFolder', function() {
|
describe('BookTreeViewProvider.openNotebookFolder', function () {
|
||||||
let rootFolderPath = path.join(os.tmpdir(), `BookFolderTest_${uuid.v4()}`);
|
let rootFolderPath = path.join(os.tmpdir(), `BookFolderTest_${uuid.v4()}`);
|
||||||
let bookTreeViewProvider: BookTreeViewProvider;
|
let bookTreeViewProvider: BookTreeViewProvider;
|
||||||
let runs = [
|
let runs = [
|
||||||
{
|
{
|
||||||
it: 'v1',
|
it: 'v1',
|
||||||
folderPaths : {
|
folderPaths: {
|
||||||
'bookFolderPath' : path.join(rootFolderPath, 'BookTestData'),
|
'bookFolderPath': path.join(rootFolderPath, 'BookTestData'),
|
||||||
'dataFolderPath' : path.join(rootFolderPath, 'BookTestData', '_data'),
|
'dataFolderPath': path.join(rootFolderPath, 'BookTestData', '_data'),
|
||||||
'contentFolderPath' : path.join(rootFolderPath, 'BookTestData', 'content'),
|
'contentFolderPath': path.join(rootFolderPath, 'BookTestData', 'content'),
|
||||||
'configFile' : path.join(rootFolderPath, 'BookTestData', '_config.yml'),
|
'configFile': path.join(rootFolderPath, 'BookTestData', '_config.yml'),
|
||||||
'tableOfContentsFile' : path.join(rootFolderPath,'BookTestData','_data', 'toc.yml'),
|
'tableOfContentsFile': path.join(rootFolderPath, 'BookTestData', '_data', 'toc.yml'),
|
||||||
'bookNotebookFile' : path.join(rootFolderPath, 'BookTestData', 'content', 'notebook1.ipynb'),
|
'bookNotebookFile': path.join(rootFolderPath, 'BookTestData', 'content', 'notebook1.ipynb'),
|
||||||
'notebookFolderPath' : path.join(rootFolderPath, 'NotebookTestData'),
|
'notebookFolderPath': path.join(rootFolderPath, 'NotebookTestData'),
|
||||||
'standaloneNotebookFile' : path.join(rootFolderPath, 'NotebookTestData','notebook2.ipynb')
|
'standaloneNotebookFile': path.join(rootFolderPath, 'NotebookTestData', 'notebook2.ipynb')
|
||||||
},
|
},
|
||||||
contents : {
|
contents: {
|
||||||
'config' : 'title: Test Book',
|
'config': 'title: Test Book',
|
||||||
'toc' : '- title: Notebook1\n url: /notebook1',
|
'toc': '- title: Notebook1\n url: /notebook1',
|
||||||
'bookTitle' : 'Test Book',
|
'bookTitle': 'Test Book',
|
||||||
'standaloneNotebookTitle' : 'notebook2'
|
'standaloneNotebookTitle': 'notebook2'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
it: 'v2',
|
it: 'v2',
|
||||||
folderPaths : {
|
folderPaths: {
|
||||||
'bookFolderPath' : path.join(rootFolderPath, 'BookTestData'),
|
'bookFolderPath': path.join(rootFolderPath, 'BookTestData'),
|
||||||
'configFile' : path.join(rootFolderPath, 'BookTestData', '_config.yml'),
|
'configFile': path.join(rootFolderPath, 'BookTestData', '_config.yml'),
|
||||||
'tableOfContentsFile' : path.join(rootFolderPath,'BookTestData', '_toc.yml'),
|
'tableOfContentsFile': path.join(rootFolderPath, 'BookTestData', '_toc.yml'),
|
||||||
'bookNotebookFile' : path.join(rootFolderPath, 'BookTestData','notebook1.ipynb'),
|
'bookNotebookFile': path.join(rootFolderPath, 'BookTestData', 'notebook1.ipynb'),
|
||||||
'notebookFolderPath' : path.join(rootFolderPath, 'NotebookTestData'),
|
'notebookFolderPath': path.join(rootFolderPath, 'NotebookTestData'),
|
||||||
'standaloneNotebookFile' : path.join(rootFolderPath, 'NotebookTestData','notebook2.ipynb')
|
'standaloneNotebookFile': path.join(rootFolderPath, 'NotebookTestData', 'notebook2.ipynb')
|
||||||
},
|
},
|
||||||
contents : {
|
contents: {
|
||||||
'config' : 'title: Test Book',
|
'config': 'title: Test Book',
|
||||||
'toc' : '- title: Notebook1\n file: /notebook1',
|
'toc': '- title: Notebook1\n file: /notebook1',
|
||||||
'bookTitle' : 'Test Book',
|
'bookTitle': 'Test Book',
|
||||||
'standaloneNotebookTitle' : 'notebook2'
|
'standaloneNotebookTitle': 'notebook2'
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
runs.forEach(function (run){
|
runs.forEach(function (run) {
|
||||||
describe('BookTreeViewProvider.openNotebookFolder on ' + run.it, function (): void {
|
describe('BookTreeViewProvider.openNotebookFolder on ' + run.it, function (): void {
|
||||||
before(async () => {
|
before(async () => {
|
||||||
|
|
||||||
await fs.mkdir(rootFolderPath);
|
await fs.mkdir(rootFolderPath);
|
||||||
await fs.mkdir(run.folderPaths.bookFolderPath);
|
await fs.mkdir(run.folderPaths.bookFolderPath);
|
||||||
if(run.it === 'v1') {
|
if (run.it === 'v1') {
|
||||||
await fs.mkdir(run.folderPaths.dataFolderPath);
|
await fs.mkdir(run.folderPaths.dataFolderPath);
|
||||||
await fs.mkdir(run.folderPaths.contentFolderPath);
|
await fs.mkdir(run.folderPaths.contentFolderPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,25 @@
|
|||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
*--------------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import * as should from 'should';
|
import * as should from 'should';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { BookTocManager, hasSections } from '../../book/bookTocManager';
|
import { BookTocManager, hasSections, quickPickResults } from '../../book/bookTocManager';
|
||||||
import { BookTreeItem, BookTreeItemFormat, BookTreeItemType } from '../../book/bookTreeItem';
|
import { BookTreeItem, BookTreeItemFormat, BookTreeItemType } from '../../book/bookTreeItem';
|
||||||
import * as yaml from 'js-yaml';
|
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { IJupyterBookSectionV1, IJupyterBookSectionV2, JupyterBookSection } from '../../contracts/content';
|
import { IJupyterBookSectionV1, IJupyterBookSectionV2, JupyterBookSection } from '../../contracts/content';
|
||||||
import * as fs from 'fs-extra';
|
import * as fs from 'fs-extra';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as uuid from 'uuid';
|
import * as uuid from 'uuid';
|
||||||
|
import { exists } from '../../common/utils';
|
||||||
|
import * as rimraf from 'rimraf';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { BookModel } from '../../book/bookModel';
|
||||||
|
import { MockExtensionContext } from '../common/stubs';
|
||||||
|
import { BookVersion } from '../../book/bookVersionHandler';
|
||||||
|
import { BookTreeViewProvider } from '../../book/bookTreeView';
|
||||||
|
import { NavigationProviders } from '../../common/constants';
|
||||||
|
import * as loc from '../../common/localizedConstants';
|
||||||
|
|
||||||
|
|
||||||
export function equalTOC(actualToc: IJupyterBookSectionV2[], expectedToc: IJupyterBookSectionV2[]): boolean {
|
export function equalTOC(actualToc: IJupyterBookSectionV2[], expectedToc: IJupyterBookSectionV2[]): boolean {
|
||||||
for (let [i, section] of actualToc.entries()) {
|
for (let [i, section] of actualToc.entries()) {
|
||||||
@@ -89,7 +97,7 @@ describe('BookTocManagerTests', function () {
|
|||||||
}];
|
}];
|
||||||
await bookTocManager.createBook(bookFolderPath, root2FolderPath);
|
await bookTocManager.createBook(bookFolderPath, root2FolderPath);
|
||||||
should(equalTOC(bookTocManager.tableofContents[2].sections, expectedSection)).be.true;
|
should(equalTOC(bookTocManager.tableofContents[2].sections, expectedSection)).be.true;
|
||||||
should(bookTocManager.tableofContents[2].file).be.equal(path.join(subfolder, 'readme'));
|
should((bookTocManager.tableofContents[2] as IJupyterBookSectionV2).file).be.equal(path.join(subfolder, 'readme'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore invalid file extensions', async () => {
|
it('should ignore invalid file extensions', async () => {
|
||||||
@@ -103,267 +111,466 @@ describe('BookTocManagerTests', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('EditingBooks', () => {
|
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 bookTocManager: BookTocManager;
|
||||||
|
let sourceBookModel: BookModel;
|
||||||
|
let targetBookModel: BookModel;
|
||||||
|
let targetBook: BookTreeItem;
|
||||||
|
let sectionC: BookTreeItem;
|
||||||
|
let sectionA: BookTreeItem;
|
||||||
|
let sectionB: BookTreeItem;
|
||||||
|
let notebook: BookTreeItem;
|
||||||
|
let duplicatedNotebook: BookTreeItem;
|
||||||
|
let sourceBookFolderPath: string = path.join(os.tmpdir(), uuid.v4(), 'sourceBook');
|
||||||
|
let targetBookFolderPath: string = path.join(os.tmpdir(), uuid.v4(), 'targetBook');
|
||||||
|
let duplicatedNotebookPath: string = path.join(os.tmpdir(), uuid.v4(), 'duplicatedNotebook');
|
||||||
let runs = [
|
let runs = [
|
||||||
{
|
{
|
||||||
it: 'using the jupyter-book legacy version < 0.7.0',
|
it: 'using the jupyter-book legacy version < 0.7.0',
|
||||||
version: 'v1',
|
version: 'v1',
|
||||||
url: 'file',
|
sourceBook: {
|
||||||
book: {
|
'rootBookFolderPath': sourceBookFolderPath,
|
||||||
'rootBookFolderPath': rootBookFolderPath,
|
'bookContentFolderPath': path.join(sourceBookFolderPath, 'content'),
|
||||||
'bookContentFolderPath': path.join(rootBookFolderPath, 'content', 'sample'),
|
'tocPath': path.join(sourceBookFolderPath, '_data', 'toc.yml'),
|
||||||
'bookDataFolderPath': path.join(rootBookFolderPath, '_data'),
|
'readme': path.join(sourceBookFolderPath, 'content', 'readme.md'),
|
||||||
'notebook1': path.join(rootBookFolderPath, 'content', 'notebook'),
|
'toc': [
|
||||||
'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',
|
'title': 'Notebook 1',
|
||||||
'url': path.join(path.sep, 'test', 'notebook5')
|
'file': path.join(path.sep, 'sectionA', 'notebook1')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Notebook 6',
|
'title': 'Notebook 2',
|
||||||
'url': path.join(path.sep, 'test', 'notebook6')
|
'file': path.join(path.sep, 'sectionA', 'notebook2')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
sectionA: {
|
||||||
|
'contentPath': path.join(sourceBookFolderPath, 'content', 'sectionA', 'readme.md'),
|
||||||
|
'sectionRoot': path.join(sourceBookFolderPath, 'content', 'sectionA'),
|
||||||
|
'sectionName': 'Section A',
|
||||||
|
'notebook1': path.join(sourceBookFolderPath, 'content', 'sectionA', 'notebook1.ipynb'),
|
||||||
|
'notebook2': path.join(sourceBookFolderPath, 'content', 'sectionA', 'notebook2.ipynb'),
|
||||||
|
'sectionFormat': [
|
||||||
|
{
|
||||||
|
'title': 'Notebook 1',
|
||||||
|
'file': path.join(path.sep, 'sectionA', 'notebook1')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Notebook 2',
|
||||||
|
'file': path.join(path.sep, 'sectionA', 'notebook2')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
sectionB: {
|
||||||
|
'contentPath': path.join(sourceBookFolderPath, 'content', 'sectionB', 'readme.md'),
|
||||||
|
'sectionRoot': path.join(sourceBookFolderPath, 'content', 'sectionB'),
|
||||||
|
'sectionName': 'Section B',
|
||||||
|
'notebook3': path.join(sourceBookFolderPath, 'content', 'sectionB', 'notebook3.ipynb'),
|
||||||
|
'notebook4': path.join(sourceBookFolderPath, 'content', 'sectionB', 'notebook4.ipynb'),
|
||||||
|
'sectionFormat': [
|
||||||
|
{
|
||||||
|
'title': 'Notebook 3',
|
||||||
|
'file': path.join(path.sep, 'sectionB', 'notebook3')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Notebook 4',
|
||||||
|
'file': path.join(path.sep, 'sectionB', 'notebook4')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
notebook5: {
|
||||||
|
'contentPath': path.join(sourceBookFolderPath, 'content', 'notebook5.ipynb')
|
||||||
|
},
|
||||||
|
targetBook: {
|
||||||
|
'rootBookFolderPath': targetBookFolderPath,
|
||||||
|
'bookContentFolderPath': path.join(targetBookFolderPath, 'content'),
|
||||||
|
'tocPath': path.join(targetBookFolderPath, '_data', 'toc.yml'),
|
||||||
|
'readme': path.join(targetBookFolderPath, 'content', 'readme.md'),
|
||||||
|
'toc': [
|
||||||
|
{
|
||||||
|
'title': 'Welcome page',
|
||||||
|
'file': path.join(path.sep, 'readme'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Section C',
|
||||||
|
'file': path.join(path.sep, 'sectionC', 'readme'),
|
||||||
|
'sections': [
|
||||||
|
{
|
||||||
|
'title': 'Notebook 6',
|
||||||
|
'file': path.join(path.sep, 'sectionC', 'notebook6')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
sectionC: {
|
||||||
|
'contentPath': path.join(targetBookFolderPath, 'content', 'sectionC', 'readme.md'),
|
||||||
|
'sectionRoot': path.join(targetBookFolderPath, 'content', 'sectionC'),
|
||||||
|
'sectionName': 'Section C',
|
||||||
|
'notebook6': path.join(targetBookFolderPath, 'content', 'sectionC', 'notebook6.ipynb'),
|
||||||
|
'sectionFormat': [
|
||||||
|
{
|
||||||
|
'title': 'Notebook 6',
|
||||||
|
'file': path.join(path.sep, 'sectionC', 'notebook6')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
it: 'using jupyter-book versions >= 0.7.0',
|
it: 'using the jupyter-book legacy version >= 0.7.0',
|
||||||
version: 'v2',
|
version: 'v2',
|
||||||
url: 'file',
|
sourceBook: {
|
||||||
book: {
|
'rootBookFolderPath': sourceBookFolderPath,
|
||||||
'bookContentFolderPath': path.join(rootBookFolderPath, 'sample'),
|
'bookContentFolderPath': sourceBookFolderPath,
|
||||||
'rootBookFolderPath': rootBookFolderPath,
|
'tocPath': path.join(sourceBookFolderPath, '_toc.yml'),
|
||||||
'notebook1': path.join(rootBookFolderPath, 'notebook'),
|
'readme': path.join(sourceBookFolderPath, 'readme.md')
|
||||||
'notebook2': path.join(rootBookFolderPath, 'notebook2'),
|
|
||||||
'tocPath': path.join(rootBookFolderPath, '_toc.yml')
|
|
||||||
},
|
},
|
||||||
bookSection1: {
|
sectionA: {
|
||||||
'bookContentFolderPath': path.join(rootSectionFolderPath, 'sample'),
|
'contentPath': path.join(sourceBookFolderPath, 'sectionA', 'readme.md'),
|
||||||
'contentPath': path.join(rootSectionFolderPath, 'sample', 'readme.md'),
|
'sectionRoot': path.join(sourceBookFolderPath, 'sectionA'),
|
||||||
'sectionRoot': rootSectionFolderPath,
|
'sectionName': 'Section A',
|
||||||
'sectionName': 'Sample',
|
'notebook1': path.join(sourceBookFolderPath, 'sectionA', 'notebook1.ipynb'),
|
||||||
'notebook3': path.join(rootSectionFolderPath, 'sample', 'notebook3'),
|
'notebook2': path.join(sourceBookFolderPath, 'sectionA', 'notebook2.ipynb'),
|
||||||
'notebook4': path.join(rootSectionFolderPath, 'sample', 'notebook4'),
|
'sectionFormat': [
|
||||||
'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',
|
'title': 'Notebook 1',
|
||||||
'file': path.join(path.sep, 'test', 'notebook5')
|
'file': path.join(path.sep, 'sectionA', 'notebook1')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Notebook 6',
|
'title': 'Notebook 2',
|
||||||
'file': path.join(path.sep, 'test', 'notebook6')
|
'file': path.join(path.sep, 'sectionA', 'notebook2')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
sectionB: {
|
||||||
|
'contentPath': path.join(sourceBookFolderPath, 'sectionB', 'readme.md'),
|
||||||
|
'sectionRoot': path.join(sourceBookFolderPath, 'sectionB'),
|
||||||
|
'sectionName': 'Section B',
|
||||||
|
'notebook3': path.join(sourceBookFolderPath, 'sectionB', 'notebook3.ipynb'),
|
||||||
|
'notebook4': path.join(sourceBookFolderPath, 'sectionB', 'notebook4.ipynb'),
|
||||||
|
'sectionFormat': [
|
||||||
|
{
|
||||||
|
'title': 'Notebook 3',
|
||||||
|
'file': path.join(path.sep, 'sectionB', 'notebook3')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Notebook 4',
|
||||||
|
'file': path.join(path.sep, 'sectionB', 'notebook4')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
notebook5: {
|
||||||
|
'contentPath': path.join(sourceBookFolderPath, 'notebook5.ipynb')
|
||||||
|
},
|
||||||
|
targetBook: {
|
||||||
|
'rootBookFolderPath': targetBookFolderPath,
|
||||||
|
'bookContentFolderPath': targetBookFolderPath,
|
||||||
|
'tocPath': path.join(targetBookFolderPath, '_toc.yml'),
|
||||||
|
'readme': path.join(targetBookFolderPath, 'readme.md'),
|
||||||
|
'toc': [
|
||||||
|
{
|
||||||
|
'title': 'Welcome',
|
||||||
|
'file': path.join(path.sep, 'readme'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Section C',
|
||||||
|
'file': path.join(path.sep, 'sectionC', 'readme'),
|
||||||
|
'sections': [
|
||||||
|
{
|
||||||
|
'title': 'Notebook 6',
|
||||||
|
'file': path.join(path.sep, 'sectionC', 'notebook6')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
sectionC: {
|
||||||
|
'contentPath': path.join(targetBookFolderPath, 'sectionC', 'readme.md'),
|
||||||
|
'sectionRoot': path.join(targetBookFolderPath, 'sectionC'),
|
||||||
|
'sectionName': 'Section C',
|
||||||
|
'notebook6': path.join(targetBookFolderPath, 'sectionC', 'notebook6.ipynb'),
|
||||||
|
'sectionFormat': [
|
||||||
|
{
|
||||||
|
'title': 'Notebook 6',
|
||||||
|
'file': path.join(path.sep, 'sectionC', 'notebook6')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
runs.forEach(function (run) {
|
runs.forEach(function (run) {
|
||||||
describe('Editing Books ' + run.it, function (): void {
|
describe('Editing Books ' + run.it, function (): void {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
let bookTreeItemFormat1: BookTreeItemFormat = {
|
let targetBookTreeItemFormat: BookTreeItemFormat = {
|
||||||
contentPath: run.version === 'v1' ? path.join(run.book.rootBookFolderPath, 'content', 'index.md') : path.join(run.book.rootBookFolderPath, 'index.md'),
|
contentPath: run.targetBook.readme,
|
||||||
root: run.book.rootBookFolderPath,
|
root: run.targetBook.rootBookFolderPath,
|
||||||
tableOfContents: {
|
tableOfContents: {
|
||||||
sections: run.section
|
sections: run.targetBook.toc
|
||||||
},
|
},
|
||||||
isUntitled: undefined,
|
isUntitled: undefined,
|
||||||
title: undefined,
|
title: 'Target Book',
|
||||||
treeItemCollapsibleState: undefined,
|
treeItemCollapsibleState: undefined,
|
||||||
type: BookTreeItemType.Book,
|
type: BookTreeItemType.Book,
|
||||||
version: run.version,
|
version: run.version,
|
||||||
page: run.section
|
page: run.targetBook.toc
|
||||||
};
|
};
|
||||||
|
|
||||||
let bookTreeItemFormat2: BookTreeItemFormat = {
|
let sectionCTreeItemFormat: BookTreeItemFormat = {
|
||||||
title: run.bookSection1.sectionName,
|
title: run.sectionC.sectionName,
|
||||||
contentPath: run.bookSection1.contentPath,
|
contentPath: run.sectionC.contentPath,
|
||||||
root: run.bookSection1.sectionRoot,
|
root: run.targetBook.rootBookFolderPath,
|
||||||
tableOfContents: {
|
tableOfContents: {
|
||||||
sections: run.section1
|
sections: run.sectionC.sectionFormat
|
||||||
},
|
},
|
||||||
isUntitled: undefined,
|
isUntitled: undefined,
|
||||||
treeItemCollapsibleState: undefined,
|
treeItemCollapsibleState: undefined,
|
||||||
type: BookTreeItemType.Book,
|
type: BookTreeItemType.Markdown,
|
||||||
version: run.version,
|
version: run.version,
|
||||||
page: run.section1
|
page: run.sectionC.sectionFormat
|
||||||
};
|
};
|
||||||
|
|
||||||
let bookTreeItemFormat3: BookTreeItemFormat = {
|
// section A is from source book
|
||||||
title: run.bookSection2.sectionName,
|
let sectionATreeItemFormat: BookTreeItemFormat = {
|
||||||
contentPath: run.bookSection2.contentPath,
|
title: run.sectionA.sectionName,
|
||||||
root: run.bookSection2.sectionRoot,
|
contentPath: run.sectionA.contentPath,
|
||||||
|
root: run.sourceBook.rootBookFolderPath,
|
||||||
tableOfContents: {
|
tableOfContents: {
|
||||||
sections: run.section2
|
sections: run.sectionA.sectionFormat
|
||||||
},
|
},
|
||||||
isUntitled: undefined,
|
isUntitled: undefined,
|
||||||
treeItemCollapsibleState: undefined,
|
treeItemCollapsibleState: undefined,
|
||||||
type: BookTreeItemType.Book,
|
type: BookTreeItemType.Markdown,
|
||||||
version: run.version,
|
version: run.version,
|
||||||
page: run.section2
|
page: run.sectionA.sectionFormat
|
||||||
};
|
};
|
||||||
|
|
||||||
let bookTreeItemFormat4: BookTreeItemFormat = {
|
// section B is from source book
|
||||||
title: run.bookSection2.sectionName,
|
let sectionBTreeItemFormat: BookTreeItemFormat = {
|
||||||
contentPath: run.notebook.contentPath,
|
title: run.sectionB.sectionName,
|
||||||
root: run.bookSection2.sectionRoot,
|
contentPath: run.sectionB.contentPath,
|
||||||
|
root: run.sourceBook.rootBookFolderPath,
|
||||||
tableOfContents: {
|
tableOfContents: {
|
||||||
sections: undefined
|
sections: run.sectionB.sectionFormat
|
||||||
|
},
|
||||||
|
isUntitled: undefined,
|
||||||
|
treeItemCollapsibleState: undefined,
|
||||||
|
type: BookTreeItemType.Markdown,
|
||||||
|
version: run.version,
|
||||||
|
page: run.sectionB.sectionFormat
|
||||||
|
};
|
||||||
|
|
||||||
|
// notebook5 is from source book
|
||||||
|
let notebookTreeItemFormat: BookTreeItemFormat = {
|
||||||
|
title: '',
|
||||||
|
contentPath: run.notebook5.contentPath,
|
||||||
|
root: run.sourceBook.rootBookFolderPath,
|
||||||
|
tableOfContents: {
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
'title': 'Notebook 5',
|
||||||
|
'file': path.join(path.sep, 'notebook5')
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
isUntitled: undefined,
|
isUntitled: undefined,
|
||||||
treeItemCollapsibleState: undefined,
|
treeItemCollapsibleState: undefined,
|
||||||
type: BookTreeItemType.Notebook,
|
type: BookTreeItemType.Notebook,
|
||||||
|
version: run.version,
|
||||||
page: {
|
page: {
|
||||||
sections: undefined
|
sections: [
|
||||||
|
{
|
||||||
|
'title': 'Notebook 5',
|
||||||
|
'file': path.join(path.sep, 'notebook5')
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
book = new BookTreeItem(bookTreeItemFormat1, undefined);
|
let duplicatedNbTreeItemFormat: BookTreeItemFormat = {
|
||||||
bookSection = new BookTreeItem(bookTreeItemFormat2, undefined);
|
title: 'Duplicated Notebook',
|
||||||
bookSection2 = new BookTreeItem(bookTreeItemFormat3, undefined);
|
contentPath: path.join(duplicatedNotebookPath, 'notebook5.ipynb'),
|
||||||
notebook = new BookTreeItem(bookTreeItemFormat4, undefined);
|
root: duplicatedNotebookPath,
|
||||||
bookTocManager = new BookTocManager();
|
tableOfContents: {
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
'title': 'Notebook 5',
|
||||||
|
'file': path.join(path.sep, 'notebook5')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
isUntitled: undefined,
|
||||||
|
treeItemCollapsibleState: undefined,
|
||||||
|
type: BookTreeItemType.Notebook,
|
||||||
|
version: run.version,
|
||||||
|
page: {
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
'title': 'Notebook 5',
|
||||||
|
'file': path.join(path.sep, 'notebook5')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
bookSection.uri = path.join('sample', 'readme');
|
targetBook = new BookTreeItem(targetBookTreeItemFormat, undefined);
|
||||||
bookSection2.uri = path.join('test', 'readme');
|
sectionC = new BookTreeItem(sectionCTreeItemFormat, undefined);
|
||||||
|
sectionA = new BookTreeItem(sectionATreeItemFormat, undefined);
|
||||||
|
sectionB = new BookTreeItem(sectionBTreeItemFormat, undefined);
|
||||||
|
notebook = new BookTreeItem(notebookTreeItemFormat, undefined);
|
||||||
|
duplicatedNotebook = new BookTreeItem(duplicatedNbTreeItemFormat, undefined);
|
||||||
|
|
||||||
book.contextValue = 'savedBook';
|
|
||||||
bookSection.contextValue = 'section';
|
sectionC.uri = path.join('sectionC', 'readme');
|
||||||
bookSection2.contextValue = 'section';
|
sectionA.uri = path.join('sectionA', 'readme');
|
||||||
|
sectionB.uri = path.join('sectionB', 'readme');
|
||||||
|
|
||||||
|
targetBook.contextValue = 'savedBook';
|
||||||
|
sectionA.contextValue = 'section';
|
||||||
|
sectionB.contextValue = 'section';
|
||||||
|
sectionC.contextValue = 'section';
|
||||||
notebook.contextValue = 'savedNotebook';
|
notebook.contextValue = 'savedNotebook';
|
||||||
|
duplicatedNotebook.contextValue = 'savedNotebook';
|
||||||
|
|
||||||
|
sectionC.tableOfContentsPath = run.targetBook.tocPath;
|
||||||
|
sectionA.tableOfContentsPath = run.sourceBook.tocPath;
|
||||||
|
sectionB.tableOfContentsPath = run.sourceBook.tocPath;
|
||||||
|
notebook.tableOfContentsPath = run.sourceBook.tocPath;
|
||||||
|
duplicatedNotebook.tableOfContentsPath = run.sourceBook.tocPath;
|
||||||
|
|
||||||
|
sectionA.sections = run.sectionA.sectionFormat;
|
||||||
|
sectionB.sections = run.sectionB.sectionFormat;
|
||||||
|
sectionC.sections = run.sectionC.sectionFormat;
|
||||||
|
notebook.sections = [
|
||||||
|
{
|
||||||
|
'title': 'Notebook 5',
|
||||||
|
'file': path.join(path.sep, 'notebook5')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
duplicatedNotebook.sections = notebook.sections;
|
||||||
|
|
||||||
|
await fs.promises.mkdir(run.targetBook.bookContentFolderPath, { recursive: true });
|
||||||
|
await fs.promises.mkdir(run.sectionA.contentPath, { recursive: true });
|
||||||
|
await fs.promises.mkdir(run.sectionB.contentPath, { recursive: true });
|
||||||
|
await fs.promises.mkdir(run.sectionC.contentPath, { recursive: true });
|
||||||
|
await fs.promises.mkdir(duplicatedNotebookPath, { recursive: true });
|
||||||
|
|
||||||
|
await fs.writeFile(run.sectionA.notebook1, '');
|
||||||
|
await fs.writeFile(run.sectionA.notebook2, '');
|
||||||
|
await fs.writeFile(run.sectionB.notebook3, '');
|
||||||
|
await fs.writeFile(run.sectionB.notebook4, '');
|
||||||
|
await fs.writeFile(run.sectionC.notebook6, '');
|
||||||
|
await fs.writeFile(run.notebook5.contentPath, '');
|
||||||
|
await fs.writeFile(duplicatedNotebook.book.contentPath, '');
|
||||||
|
await fs.writeFile(path.join(run.targetBook.rootBookFolderPath, '_config.yml'), 'title: Target Book');
|
||||||
|
await fs.writeFile(path.join(run.sourceBook.rootBookFolderPath, '_config.yml'), 'title: Source Book');
|
||||||
|
|
||||||
|
|
||||||
await fs.promises.mkdir(run.book.bookContentFolderPath, { recursive: true });
|
if (run.version === 'v1') {
|
||||||
await fs.promises.mkdir(run.bookSection1.bookContentFolderPath, { recursive: true });
|
await fs.promises.mkdir(path.dirname(run.targetBook.tocPath), { recursive: true });
|
||||||
await fs.promises.mkdir(run.bookSection2.bookContentFolderPath, { recursive: true });
|
await fs.promises.mkdir(path.dirname(run.sourceBook.tocPath), { 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, '');
|
// target book
|
||||||
await fs.writeFile(run.bookSection1.notebook3, '');
|
await fs.writeFile(run.targetBook.tocPath, '- title: Welcome\n file: /readme\n- title: Section C\n file: /sectionC/readme\n sections:\n - title: Notebook6\n file: /sectionC/notebook6');
|
||||||
await fs.writeFile(run.bookSection1.notebook4, '');
|
// source book
|
||||||
await fs.writeFile(run.bookSection2.notebook5, '');
|
await fs.writeFile(run.sourceBook.tocPath, '- title: Notebook 5\n file: /notebook5\n- title: Section A\n file: /sectionA/readme\n sections:\n - title: Notebook1\n file: /sectionA/notebook1\n - title: Notebook2\n file: /sectionA/notebook2');
|
||||||
await fs.writeFile(run.bookSection2.notebook6, '');
|
|
||||||
await fs.writeFile(run.notebook.contentPath, '');
|
const mockExtensionContext = new MockExtensionContext();
|
||||||
|
|
||||||
|
sourceBookModel = new BookModel(run.sourceBook.rootBookFolderPath, false, false, mockExtensionContext);
|
||||||
|
targetBookModel = new BookModel(run.targetBook.rootBookFolderPath, false, false, mockExtensionContext);
|
||||||
|
// create book model mock objects
|
||||||
|
sinon.stub(sourceBookModel, 'bookItems').value([sectionA]);
|
||||||
|
sinon.stub(targetBookModel, 'bookItems').value([targetBook]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('Add section to book', async () => {
|
it('Add section to book', async () => {
|
||||||
await bookTocManager.updateBook(bookSection, book);
|
bookTocManager = new BookTocManager(targetBookModel, sourceBookModel);
|
||||||
const listFiles = await fs.promises.readdir(run.book.bookContentFolderPath);
|
await bookTocManager.updateBook(sectionA, targetBook, undefined);
|
||||||
const tocFile = await fs.promises.readFile(run.book.tocPath, 'utf8');
|
const listFiles = await fs.promises.readdir(path.join(run.targetBook.bookContentFolderPath, 'sectionA'));
|
||||||
let toc = yaml.safeLoad(tocFile);
|
const listSourceFiles = await fs.promises.readdir(path.join(run.sourceBook.bookContentFolderPath));
|
||||||
should(JSON.stringify(listFiles)).be.equal(JSON.stringify(['notebook3', 'notebook4']), 'The files of the section should be moved to the books folder');
|
should(JSON.stringify(listSourceFiles).includes('sectionA')).be.false('The source book files should not contain the section A files');
|
||||||
should(equalSections(toc.sections[2], bookTocManager.newSection)).be.true;
|
should(JSON.stringify(listFiles)).be.equal(JSON.stringify(['notebook1.ipynb', 'notebook2.ipynb', 'readme.md']), 'The files of the section should be moved to the target book folder');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Add section to section', async () => {
|
it('Add section to section', async () => {
|
||||||
await bookTocManager.updateBook(bookSection, bookSection2);
|
bookTocManager = new BookTocManager(targetBookModel, sourceBookModel);
|
||||||
let listFiles = await fs.promises.readdir(path.join(run.bookSection2.bookContentFolderPath, 'sample'));
|
await bookTocManager.updateBook(sectionB, sectionC, {
|
||||||
const tocFile = await fs.promises.readFile(path.join(run.bookSection2.tocPath), 'utf8');
|
'title': 'Notebook 6',
|
||||||
let toc = yaml.safeLoad(tocFile);
|
'file': path.join(path.sep, 'sectionC', 'notebook6')
|
||||||
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;
|
const sectionCFiles = await fs.promises.readdir(path.join(run.targetBook.bookContentFolderPath, 'sectionC'));
|
||||||
|
const sectionBFiles = await fs.promises.readdir(path.join(run.targetBook.bookContentFolderPath, 'sectionB'));
|
||||||
|
should(JSON.stringify(sectionCFiles)).be.equal(JSON.stringify(['notebook6.ipynb', 'readme.md']), 'sectionB has been moved under target book content directory');
|
||||||
|
should(JSON.stringify(sectionBFiles)).be.equal(JSON.stringify(['notebook3.ipynb', 'notebook4.ipynb', 'readme.md']), ' Verify that the files on sectionB had been moved to the targetBook');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Add notebook to book', async () => {
|
it('Add notebook to book', async () => {
|
||||||
await bookTocManager.updateBook(notebook, book);
|
bookTocManager = new BookTocManager(targetBookModel);
|
||||||
const folder = run.version === 'v1' ? path.join(run.book.rootBookFolderPath, 'content') : path.join(run.book.rootBookFolderPath);
|
await bookTocManager.updateBook(notebook, targetBook);
|
||||||
let listFiles = await fs.promises.readdir(folder);
|
const listFiles = await fs.promises.readdir(run.targetBook.bookContentFolderPath);
|
||||||
const tocFile = await fs.promises.readFile(run.book.tocPath, 'utf8');
|
should(JSON.stringify(listFiles).includes('notebook5.ipynb')).be.true('Notebook 5 should be under the target book content folder');
|
||||||
let toc = yaml.safeLoad(tocFile);
|
});
|
||||||
should(listFiles.findIndex(f => f === 'readme.md')).not.equal(-1);
|
|
||||||
should(equalSections(toc.sections[2], bookTocManager.newSection)).be.true;
|
it('Add duplicated notebook to book', async () => {
|
||||||
|
bookTocManager = new BookTocManager(targetBookModel);
|
||||||
|
await bookTocManager.updateBook(notebook, targetBook);
|
||||||
|
await bookTocManager.updateBook(duplicatedNotebook, targetBook);
|
||||||
|
const listFiles = await fs.promises.readdir(run.targetBook.bookContentFolderPath);
|
||||||
|
if (run.version === BookVersion.v1) {
|
||||||
|
should(JSON.stringify(listFiles)).be.equal(JSON.stringify(['notebook5 - 2.ipynb', 'notebook5.ipynb', 'sectionC']), 'Should modify the name of the file');
|
||||||
|
} else {
|
||||||
|
should(JSON.stringify(listFiles)).be.equal(JSON.stringify(['_config.yml', '_toc.yml', 'notebook5 - 2.ipynb', 'notebook5.ipynb', 'sectionC']), 'Should modify the name of the file');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Recovery method is called after error', async () => {
|
||||||
|
const mockExtensionContext = new MockExtensionContext();
|
||||||
|
const recoverySpy = sinon.spy(BookTocManager.prototype, 'recovery');
|
||||||
|
sinon.stub(BookTocManager.prototype, 'updateBook').throws(new Error('Unexpected error.'));
|
||||||
|
const bookTreeViewProvider = new BookTreeViewProvider([], mockExtensionContext, false, 'bookTreeView', NavigationProviders.NotebooksNavigator);
|
||||||
|
const results: quickPickResults = {
|
||||||
|
book: targetBook,
|
||||||
|
quickPickSection: {
|
||||||
|
label: loc.labelAddToLevel,
|
||||||
|
description: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bookTocManager = new BookTocManager(targetBookModel);
|
||||||
|
sinon.stub(bookTreeViewProvider, 'getSelectionQuickPick').returns(Promise.resolve(results));
|
||||||
|
try {
|
||||||
|
await bookTreeViewProvider.editBook(notebook);
|
||||||
|
} catch (error) {
|
||||||
|
should(recoverySpy.calledOnce).be.true('If unexpected error then recovery method is called.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Clean up folder with files didnt move', async () => {
|
||||||
|
bookTocManager = new BookTocManager(targetBookModel);
|
||||||
|
bookTocManager.movedFiles.set(notebook.book.contentPath, 'movedtest');
|
||||||
|
await fs.writeFile(path.join(run.sourceBook.bookContentFolderPath, 'test.ipynb'), '');
|
||||||
|
await bookTocManager.cleanUp(path.dirname(notebook.book.contentPath));
|
||||||
|
const listFiles = await fs.promises.readdir(path.dirname(notebook.book.contentPath));
|
||||||
|
should(JSON.stringify(listFiles).includes('test.ipynb')).be.true('Notebook test.ipynb should not be removed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Clean up folder when there is an empty folder within the modified directory', async () => {
|
||||||
|
await fs.promises.mkdir(path.join(run.sourceBook.bookContentFolderPath, 'test'));
|
||||||
|
bookTocManager.modifiedDir.add(path.join(run.sourceBook.bookContentFolderPath, 'test'));
|
||||||
|
bookTocManager.movedFiles.set(notebook.book.contentPath, 'movedtest');
|
||||||
|
await bookTocManager.cleanUp(path.dirname(notebook.book.contentPath));
|
||||||
|
const listFiles = await fs.promises.readdir(run.sourceBook.bookContentFolderPath);
|
||||||
|
should(JSON.stringify(listFiles).includes('test')).be.true('Empty directories within the moving element directory are not deleted');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function (): Promise<void> {
|
||||||
|
sinon.restore();
|
||||||
|
if (await exists(sourceBookFolderPath)) {
|
||||||
|
await promisify(rimraf)(sourceBookFolderPath);
|
||||||
|
}
|
||||||
|
if (await exists(targetBookFolderPath)) {
|
||||||
|
await promisify(rimraf)(targetBookFolderPath);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user