From c1f6a67829d2959ddea03fe5d083500050ae2d1b Mon Sep 17 00:00:00 2001 From: Maddy <12754347+MaddyDev@users.noreply.github.com> Date: Mon, 2 Mar 2020 12:45:53 -0800 Subject: [PATCH] Feat/create book (#9159) * added secondary action * create book command * open as untitled * create toc.yml and update title * added comments * throw error if filenames have unsupported chars * update prompt message * remove the toLocaleLower * added await * moced createbookpath out of the command handler * removed tolocalelower and added comments * moved the formatting and file handling code from core to notebook * fixes for contents with folders * collapse the code cell * remove output * reused existing command to open book * comment typu and added await --- extensions/notebook/package.json | 13 + extensions/notebook/package.nls.json | 3 +- .../notebooks/JupyterBooksCreate.ipynb | 280 ++++++++++++++++++ extensions/notebook/src/book/bookModel.ts | 9 +- extensions/notebook/src/book/bookTreeView.ts | 3 +- extensions/notebook/src/extension.ts | 12 + 6 files changed, 313 insertions(+), 7 deletions(-) create mode 100644 extensions/notebook/resources/notebooks/JupyterBooksCreate.ipynb diff --git a/extensions/notebook/package.json b/extensions/notebook/package.json index bb4d96c279..4170eab514 100644 --- a/extensions/notebook/package.json +++ b/extensions/notebook/package.json @@ -180,6 +180,15 @@ "dark": "resources/dark/open_notebook_inverse.svg", "light": "resources/light/open_notebook.svg" } + }, + { + "command": "notebook.command.createBook", + "title": "%title.createJupyterBook%", + "category": "%books-preview-category%", + "icon": { + "dark": "resources/dark/open_notebook_inverse.svg", + "light": "resources/light/open_notebook.svg" + } } ], "languages": [ @@ -321,6 +330,10 @@ "command": "notebook.command.openBook", "when": "view == bookTreeView", "group": "navigation" + }, + { + "command": "notebook.command.createBook", + "when": "view == bookTreeView" } ], "notebook/toolbar": [ diff --git a/extensions/notebook/package.nls.json b/extensions/notebook/package.nls.json index f3d6eb599c..8a0aca3aaf 100644 --- a/extensions/notebook/package.nls.json +++ b/extensions/notebook/package.nls.json @@ -34,5 +34,6 @@ "title.SavedBooks": "Saved Books", "title.UnsavedBooks": "Unsaved Books", "title.PreviewLocalizedBook": "Get localized SQL Server 2019 guide", - "title.openJupyterBook": "Open Jupyter Book" + "title.openJupyterBook": "Open Jupyter Book", + "title.createJupyterBook": "Create Book" } diff --git a/extensions/notebook/resources/notebooks/JupyterBooksCreate.ipynb b/extensions/notebook/resources/notebooks/JupyterBooksCreate.ipynb new file mode 100644 index 0000000000..ad56e713eb --- /dev/null +++ b/extensions/notebook/resources/notebooks/JupyterBooksCreate.ipynb @@ -0,0 +1,280 @@ +{ + "metadata": { + "kernelspec": { + "name": "python3", + "display_name": "Python 3" + }, + "language_info": { + "name": "python", + "version": "3.7.2", + "mimetype": "text/x-python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "pygments_lexer": "ipython3", + "nbconvert_exporter": "python", + "file_extension": ".py" + } + }, + "nbformat_minor": 2, + "nbformat": 4, + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# Jupyter Books\n", + "\n", + "## 1. Installation\n", + "\n", + "To install the Jupyter Book command-line interface (CLI), use `pip`!" + ], + "metadata": { + "azdata_cell_guid": "97541c75-b1c9-4e4c-9f0a-f93df4a550ef" + } + }, + { + "cell_type": "code", + "source": [ + "import sys\r\n", + "\r\n", + "#install jupyter-book\r\n", + "cmd = f'{sys.executable} -m pip show jupyter-book'\r\n", + "cmdOutput = !{cmd}\r\n", + "if len(cmdOutput) > 0 and '0.6.4' in cmdOutput[1]:\r\n", + " print('Jupyter-book required version is already installed!')\r\n", + "else:\r\n", + " !pip install jupyter-book" + ], + "metadata": { + "azdata_cell_guid": "8bd77173-2f63-4bf8-95e8-af2a654fc91e", + "tags": [] + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "## 2. Create a new book\r\n", + "\r\n", + "Create a book using your own notebooks and markdown pages:\r\n", + "\r\n", + "Note: Notebook and markdown filenames cannot contain spaces" + ], + "metadata": { + "azdata_cell_guid": "6a1b6bb8-9cb8-43d5-878f-2029d1eacb0e" + } + }, + { + "cell_type": "code", + "source": [ + "import os, re, shutil\r\n", + "\r\n", + "overwrite = False\r\n", + "book_name = input('Please provide the path where the book needs to be saved along with the book name ex-> D:\\Book1: ') \r\n", + "\r\n", + "if (os.path.exists(book_name)):\r\n", + " new_book_name = input('A folder named ' + book_name + ' already exists. Enter a new name or the same name to overwrite the existing folder.\\n')\r\n", + " if book_name == new_book_name:\r\n", + " overwrite = True\r\n", + " book_name = new_book_name\r\n", + "\r\n", + "content_folder = input('Please provide the path to your folder containing notebooks and markdown files: ')\r\n", + "\r\n", + "while (not os.path.exists(content_folder)):\r\n", + " content_folder = input('Cannot find folder ' + content_folder + '. Please provide another path: ')\r\n", + " \r\n", + "if overwrite:\r\n", + " !jupyter-book create \"$book_name\" --content-folder \"$content_folder\" --overwrite\r\n", + "else:\r\n", + " !jupyter-book create \"$book_name\" --content-folder \"$content_folder\"" + ], + "metadata": { + "azdata_cell_guid": "d1a363f0-d854-4466-be87-d01d4c7e51ef", + "tags": [] + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "code", + "source": [ + "# Update toc file, book title and clean up the directores\n", + "tocFilePath = os.path.join(book_name, \"_data\", \"toc.yml\")\n", + "f = open(tocFilePath, \"r\")\n", + "title = ''\n", + "replacedString = ''\n", + "result = f.read()\n", + "f.close()\n", + "contentFolders = []\n", + "\n", + "firstLevelUrls = re.findall(r'^(?:\\s+$[\\r\\n]+)+(\\- url: [a-zA-Z0-9\\\\.\\s\\-\\/]+$[\\r\\n]+)', result, re.MULTILINE)\n", + "urls = re.findall(r'- url: [a-zA-Z0-9\\\\.\\s\\-\\/]+$', result, re.MULTILINE)\n", + "headers = re.findall(r'- header: [a-zA-Z0-9\\\\.\\s-]+$', result, re.MULTILINE)\n", + "\n", + "try:\n", + " if (firstLevelUrls or headers or urls):\n", + " if (firstLevelUrls and len(firstLevelUrls) == 1):\n", + " for url in firstLevelUrls:\n", + " title = url[url.rindex(os.path.sep)+1:].rstrip()\n", + " if (not headers):\n", + " markdownUrl = urls[len(urls) -1]\n", + " title = markdownUrl[markdownUrl.rindex(os.path.sep)+1:].rstrip()\n", + " replacedString = \"\\n- title: %s\\n url: /%s\\n not_numbered: true\\n expand_sections: true\\n sections: %s\" % (title, title, url)\n", + " result = result.replace(markdownUrl, '')\n", + " else:\n", + " replacedString = \"\\n- title: %s\\n url: /%s\\n not_numbered: true\\n\" % (title, title)\n", + " result = result.replace(url, replacedString)\n", + " if (headers):\n", + " for header in headers:\n", + " title = header[10:].rstrip()\n", + " contentFolders.append(title.lower())\n", + " filtered = list(filter(lambda x: (\"%s%s%s\" % (os.path.sep, title.lower(), os.path.sep)) in x, urls))\n", + " index = urls.index(filtered[len(filtered)-1])\n", + " urlValue = urls[index][urls[index].rindex(os.path.sep)+1:].rstrip()\n", + " replacedString = \"\\n- title: %s\\n url: /%s/%s\\n not_numbered: true\\n expand_sections: true\\n sections: \" % (title, title.lower(), urlValue)\n", + " result = result.replace(header, replacedString)\n", + " result = result.replace(urls[index], '')\n", + " del urls[index]\n", + " if (urls):\n", + " for url in urls:\n", + " title = url[url.rindex(os.path.sep)+1:].rstrip()\n", + " urlValue = title\n", + " if (len(contentFolders) > 0):\n", + " folders = url[7:].split(os.path.sep)\n", + " if (folders[len(folders)-2] in contentFolders):\n", + " parentFolder = contentFolders.index(folders[len(folders)-2])\n", + " urlValue = \"%s/%s\" % (contentFolders[parentFolder], title)\n", + " replacedString = \"\\n - title: %s\\n url: /%s\" % (title, urlValue)\n", + " result = result.replace(url, replacedString)\n", + " fwrite = open(tocFilePath, \"w\")\n", + " fwrite.write(result)\n", + " fwrite.close()\n", + " else:\n", + " raise SystemExit(f'\\n File Name contains unsupported-characters (ex: underscores) by Jupyter Book.\\n')\n", + " # Update the Book title in config file\n", + " configFilePath = os.path.join(book_name, \"_config.yml\")\n", + " f = open(configFilePath, \"r\")\n", + " result = f.read()\n", + " f.close()\n", + " titleLine = re.search(r'title: [a-zA-Z0-9\\\\.\\s\\-\\/]+$', result, re.MULTILINE).group()\n", + " title = 'title: %s' % (os.path.splitext(os.path.basename(book_name))[0])\n", + " result = result.replace(titleLine, title)\n", + " fwrite = open(configFilePath, \"w\")\n", + " fwrite.write(result)\n", + " fwrite.close()\n", + " # cleanup the directories\n", + " with os.scandir(book_name) as root_dir:\n", + " for path in root_dir:\n", + " if path.is_file() and path.name not in ('_config.yml'):\n", + " os.remove(path)\n", + " if path.is_dir() and path.name not in ('_data', 'content'):\n", + " shutil.rmtree(path)\n", + "except Exception as e:\n", + " print(str(e))" + ], + "metadata": { + "azdata_cell_guid": "6124730b-f52e-4103-8dbb-e3a62325fb55", + "tags": [ + "hide_input" + ] + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "### Issue: generate_toc.py is missing\r\n", + "\r\n", + "Jupyter Book uses the Table of Contents to define the structure of your book. For example, your chapters, sub-chapters, etc.\r\n", + "\r\n", + "Need to manually modify the Table of Contents (located here: mybookname/_data/toc.yml) following structure below:\r\n", + "\r\n", + "```\r\n", + "- title: mytitle # Title of chapter or section\r\n", + " url: /myurl # URL of section relative to the /content/ folder.\r\n", + " sections: # Contains a list of more entries that make up the chapter's sections\r\n", + " not_numbered: true # if the section shouldn't have a number in the sidebar\r\n", + " (e.g. Introduction or appendices)\r\n", + " expand_sections: true # if you'd like the sections of this chapter to always\r\n", + " be expanded in the sidebar.\r\n", + " external: true # Whether the URL is an external link or points to content in the book\r\n", + "```\r\n", + "\r\n", + "Example from demo book:\r\n", + "\r\n", + "```\r\n", + "- title: Getting started\r\n", + " url: /guide/01_overview\r\n", + " not_numbered: true\r\n", + " expand_sections: true\r\n", + " sections:\r\n", + " - title: Create your book\r\n", + " url: /guide/02_create\r\n", + " - title: Build and publish your book\r\n", + " url: /guide/03_build\r\n", + " - title: FAQ\r\n", + " url: /guide/04_faq\r\n", + " - title: How-to and advanced topics\r\n", + " url: /guide/05_advanced\r\n", + "```" + ], + "metadata": { + "azdata_cell_guid": "5439d5e9-6a98-4255-8afa-3c2ba48bfc7e" + } + }, + { + "cell_type": "markdown", + "source": [ + "## 3. Open your Book!\r\n", + "**Run the below cell and click on the link to view your book in Azure Data Studio.**" + ], + "metadata": { + "azdata_cell_guid": "ab100e5c-13f4-484a-9a4a-49bb13cad027" + } + }, + { + "cell_type": "code", + "source": [ + "import re, os\r\n", + "from IPython.display import *\r\n", + "if os.name == 'nt':\r\n", + " display(HTML(\"

Click here to open your Book in ADS

\"))\r\n", + "else:\r\n", + " display(HTML(\"

Click here to open your Book in ADS

\"))" + ], + "metadata": { + "azdata_cell_guid": "33d8e1cb-1eec-41ed-a368-1aeef9af62d4", + "tags": [] + }, + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "source": [ + "**Note**: On clicking the above link, we create a temporary toc.yml file for your convenience.\r\n", + "\r\n", + " Please update that file inside your book (located at: *YourbookPath*/_data/toc.yml) if you want to further customize your book following \r\n", + " the above instructions or https://jupyterbook.org/guide/01-5_tour.html#Table-of-Contents.\r\n", + "" + ], + "metadata": { + "azdata_cell_guid": "d193d588-847b-4725-9591-098d0fb24343" + } + }, + { + "cell_type": "code", + "source": [ + "display(HTML(\"

That's it!


You are good to view your book in Azure Data Studio by clicking on the above link.

\"))" + ], + "metadata": { + "azdata_cell_guid": "bd2fe173-66ce-48b3-8dc3-c4d7560953c8" + }, + "outputs": [], + "execution_count": null + } + ] +} \ No newline at end of file diff --git a/extensions/notebook/src/book/bookModel.ts b/extensions/notebook/src/book/bookModel.ts index ba6b083dff..01b8ca69af 100644 --- a/extensions/notebook/src/book/bookModel.ts +++ b/extensions/notebook/src/book/bookModel.ts @@ -149,8 +149,10 @@ export class BookModel implements azdata.nb.NavigationProvider { notebooks.push(notebook); } } else { - if (!this._allNotebooks.get(pathToNotebook)) { - this._allNotebooks.set(pathToNotebook, notebook); + // 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); } } @@ -205,8 +207,7 @@ export class BookModel implements azdata.nb.NavigationProvider { } getNavigation(uri: vscode.Uri): Thenable { - let notebook: BookTreeItem = - !this.openAsUntitled ? this._allNotebooks.get(uri.fsPath) : this._allNotebooks.get(path.basename(uri.fsPath)); + let notebook = !this.openAsUntitled ? this._allNotebooks.get(uri.fsPath) : this._allNotebooks.get(path.basename(uri.fsPath)); let result: azdata.nb.NavigationResult; if (notebook) { result = { diff --git a/extensions/notebook/src/book/bookTreeView.ts b/extensions/notebook/src/book/bookTreeView.ts index 0923330c83..c814315ea6 100644 --- a/extensions/notebook/src/book/bookTreeView.ts +++ b/extensions/notebook/src/book/bookTreeView.ts @@ -112,8 +112,7 @@ export class BookTreeViewProvider implements vscode.TreeDataProvider { + const createBookPath: string = path.posix.join(extensionContext.extensionPath, 'resources', 'notebooks', 'JupyterBooksCreate.ipynb'); extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openBook', (bookPath: string, openAsUntitled: boolean, urlToOpen?: string) => openAsUntitled ? untitledBookTreeViewProvider.openBook(bookPath, urlToOpen) : bookTreeViewProvider.openBook(bookPath, urlToOpen))); extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openNotebook', (resource) => bookTreeViewProvider.openNotebook(resource))); extensionContext.subscriptions.push(vscode.commands.registerCommand('bookTreeView.openUntitledNotebook', (resource) => untitledBookTreeViewProvider.openNotebookAsUntitled(resource))); @@ -38,6 +40,16 @@ export async function activate(extensionContext: vscode.ExtensionContext): Promi extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.searchUntitledBook', () => untitledBookTreeViewProvider.searchJupyterBooks())); extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.openBook', () => bookTreeViewProvider.openNewBook())); + extensionContext.subscriptions.push(vscode.commands.registerCommand('notebook.command.createBook', async () => { + let untitledFileName: vscode.Uri = vscode.Uri.parse(`untitled:${createBookPath}`); + await vscode.workspace.openTextDocument(createBookPath).then((document) => { + azdata.nb.showNotebookDocument(untitledFileName, { + connectionProfile: null, + initialContent: document.getText(), + initialDirtyState: false + }); + }); + })); extensionContext.subscriptions.push(vscode.commands.registerCommand('_notebook.command.new', (context?: azdata.ConnectedContext) => { let connectionProfile: azdata.IConnectionProfile = undefined; if (context && context.connectionProfile) {