diff --git a/html_toc_export/handlers.py b/html_toc_export/handlers.py
index 4a7e309..207ab65 100644
--- a/html_toc_export/handlers.py
+++ b/html_toc_export/handlers.py
@@ -5,20 +5,22 @@
import tornado
class RouteHandler(APIHandler):
- # The following decorator should be present on all verb methods (head, get, post,
+ # The following ecorator should be present on all verb methods (head, get, post,
# patch, put, delete, options) to ensure only authorized user can request the
# Jupyter server
@tornado.web.authenticated
def get(self):
- self.finish(json.dumps({
- "data": "This is /html-toc-export/get-example endpoint!"
- }))
+ with open(r"./style/toc.css", "r") as f:
+ content = f.read()
+ self.set_header("Content-Type", "text/css")
+ self.write(content)
+
def setup_handlers(web_app):
host_pattern = ".*$"
base_url = web_app.settings["base_url"]
- route_pattern = url_path_join(base_url, "html-toc-export", "get-example")
+ route_pattern = url_path_join(base_url, "html-toc-export", "toc-css")
handlers = [(route_pattern, RouteHandler)]
web_app.add_handlers(host_pattern, handlers)
diff --git a/package.json b/package.json
index 1492264..b876bce 100644
--- a/package.json
+++ b/package.json
@@ -19,7 +19,8 @@
"files": [
"lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
"style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
- "src/**/*.{ts,tsx}"
+ "src/**/*.{ts,tsx}",
+ "schema/*.json"
],
"main": "lib/index.js",
"types": "lib/index.d.ts",
@@ -58,7 +59,8 @@
"dependencies": {
"@jupyterlab/application": "^4.0.0",
"@jupyterlab/coreutils": "^6.0.0",
- "@jupyterlab/services": "^7.0.0"
+ "@jupyterlab/services": "^7.0.0",
+ "toc": "^0.4.0"
},
"devDependencies": {
"@jupyterlab/builder": "^4.0.0",
@@ -98,17 +100,18 @@
},
"jupyterlab": {
"discovery": {
- "server": {
- "managers": [
- "pip"
- ],
- "base": {
- "name": "html_toc_export"
+ "server": {
+ "managers": [
+ "pip"
+ ],
+ "base": {
+ "name": "html_toc_export"
+ }
}
- }
},
"extension": true,
- "outputDir": "html_toc_export/labextension"
+ "outputDir": "html_toc_export/labextension",
+ "schemaDir": "schema"
},
"eslintIgnore": [
"node_modules",
diff --git a/src/index.ts b/src/index.ts
index 4ed0047..d588d07 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,28 +3,187 @@ import {
JupyterFrontEndPlugin
} from '@jupyterlab/application';
-import { requestAPI } from './handler';
+import {
+ INotebookTracker, NotebookPanel
+} from '@jupyterlab/notebook';
+
+import {
+ ITableOfContentsTracker,
+ TableOfContents
+} from '@jupyterlab/toc';
+
+import { ICommandPalette } from '@jupyterlab/apputils';
+
+import { PageConfig, URLExt } from '@jupyterlab/coreutils';
+
+import { ServerConnection } from '@jupyterlab/services';
+
+
+
/**
- * Initialization data for the html-toc-export extension.
+ * Initialization data for the myextension extension.
*/
const plugin: JupyterFrontEndPlugin = {
id: 'html-toc-export:plugin',
- description: 'Export HTML with table of contents',
+ description: 'A JupyterLab extension.',
autoStart: true,
- activate: (app: JupyterFrontEnd) => {
- console.log('JupyterLab extension html-toc-export is activated!');
-
- requestAPI('get-example')
- .then(data => {
- console.log(data);
- })
- .catch(reason => {
- console.error(
- `The html_toc_export server extension appears to be missing.\n${reason}`
- );
- });
+ requires: [ICommandPalette, INotebookTracker, ITableOfContentsTracker],
+ activate: (
+ app: JupyterFrontEnd,
+ palette: ICommandPalette,
+ tracker: INotebookTracker,
+ toc: ITableOfContentsTracker,
+ ) => {
+ console.log('JupyterLab extension myextension is activated!');
+ console.log('ICommandPalette:', palette);
+
+
+ const command: string = 'toc:export';
+ app.commands.addCommand( command, {
+ label: 'Export HTML w/ ToC',
+ execute: () => {
+ let current = getCurrent(tracker, app.shell);
+ getHTML(current.context.path)
+ .then((response) => {
+ let toc_model = toc.get(current)!;
+ let domParser = new DOMParser();
+ let doc = domParser.parseFromString(response, "text/html");
+ let toc_html = doc.createElement("div");
+ toc_html.innerHTML = generateToCHTML(toc_model);
+ doc.body.prepend(toc_html);
+ getCSS()
+ .then((response) => {
+ let css_node = doc.createElement("style");
+ css_node.innerHTML = response;
+ doc.head.appendChild(css_node);
+ downloadHtmlDocument(doc, "test.html");
+ })
+ .catch((error) => {
+ console.error('Error:', error);
+ });
+ });
+ }
+ });
+ palette.addItem({ command, category: 'Tutorial'});
}
};
+function generateToCHTML(model: TableOfContents.IModel): string {
+ // Recursive function to process headings and generate HTML
+ function processHeadings(headings: TableOfContents.IHeading[], level: number): string {
+ let html = '';
+ let index = 0;
+
+ while (index < headings.length) {
+ const heading = headings[index];
+ if (heading.text.length < 1) {
+ index++;
+ continue;
+ }
+
+ if (heading.level === level) {
+ html += `${heading.text}`;
+
+ // Find and process subheadings
+ const subHeadingsStart = index + 1;
+ let subHeadingsEnd = headings.findIndex((h, i) => i >= subHeadingsStart && h.level <= level);
+ subHeadingsEnd = subHeadingsEnd === -1 ? headings.length : subHeadingsEnd;
+ const subHeadings = headings.slice(subHeadingsStart, subHeadingsEnd);
+
+ if (subHeadings.length > 0) {
+ html += `${processHeadings(subHeadings, level + 1)}
`;
+ }
+
+ html += ``;
+ index = subHeadingsEnd;
+ } else {
+ index++;
+ }
+ }
+
+ return html;
+ }
+
+ return ``;
+}
+
+
+function downloadHtmlDocument(htmlDocument: Document, filename: string) {
+ // Serialize the HTMLDocument to a string
+ const serializer = new XMLSerializer();
+ const htmlString = serializer.serializeToString(htmlDocument);
+
+ // Create a Blob from the HTML string
+ const blob = new Blob([htmlString], { type: 'text/html' });
+
+ // Create a URL for the Blob
+ const url = URL.createObjectURL(blob);
+
+ // Create a temporary link element and trigger the download
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = filename || 'document.html';
+ document.body.appendChild(link);
+ link.click();
+
+ // Clean up by removing the link element and revoking the Blob URL
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+}
+
+function getCurrent(
+ tracker: INotebookTracker,
+ shell: JupyterFrontEnd.IShell,
+): NotebookPanel {
+ const widget = tracker.currentWidget;
+
+ if (widget) {
+ shell.activateById(widget.id);
+ }
+
+ return widget!;
+}
+
+async function getHTML(path: string): Promise{
+ const settings = ServerConnection.makeSettings();
+ let response: Response;
+ let url = PageConfig.getNBConvertURL({
+ format: 'HTML',
+ download: false,
+ path,
+ });
+ try {
+ response = await ServerConnection.makeRequest(url, {}, settings);
+ } catch (error) {
+ throw new ServerConnection.NetworkError(error as any);
+ }
+ let data: any = await response.text();
+ if (!response.ok) {
+ throw new ServerConnection.ResponseError(response, data.message || data);
+ }
+ return data
+}
+
+async function getCSS(): Promise{
+ const settings = ServerConnection.makeSettings();
+ let response: Response;
+ let baseUrl = PageConfig.getBaseUrl();
+ let url = URLExt.join(baseUrl, "html-toc-export", "toc-css");
+ try {
+ response = await ServerConnection.makeRequest(url, {}, settings);
+ } catch (error) {
+ throw new ServerConnection.NetworkError(error as any);
+ }
+ let data: any = await response.text();
+ if (!response.ok) {
+ throw new ServerConnection.ResponseError(response, data.message || data);
+ }
+ return data
+}
+
export default plugin;
diff --git a/style/toc.css b/style/toc.css
new file mode 100644
index 0000000..a92cd71
--- /dev/null
+++ b/style/toc.css
@@ -0,0 +1,184 @@
+/*
+
+originally extracted from https://gist.github.com/magican/5574556
+
+Most colors defined here are overridden by javascript which adds css based on
+values in the server config file notebook.json, which can be edited directly,
+or colors can be selected in the nbextensions_configurator
+*/
+
+
+
+/*background color for links when you mouse over it */
+#toc-wrapper li > span:hover {
+ background-color: #DAA520;
+ }
+
+ #toc a {
+ color: #333333; /* default - alterable via nbextension-configurator */
+ text-decoration: none;
+ }
+ #navigate_menu li > span:hover {background-color: #f1f1f1}
+
+
+ /* Move menus and tooolbar to the left, following @Kevin-McIsaac suggestion
+ This is now done in javascript, if the relevant option is selected
+ div#menubar-container, div#header-container {
+ width: auto;
+ padding-left: 20px;
+ }*/
+
+ #navigate_menu {
+ list-style-type: none;
+ max-width: 800px;
+ min-width: 100px;
+ width: 250px;
+ overflow: auto;
+ }
+
+
+ #navigate_menu a {
+ list-style-type: none;
+ color: #333333; /* default - alterable via nbextension-configurator */
+ text-decoration: none;
+ }
+
+ #navigate_menu li {
+ padding-left: 0px;
+ clear: both;
+ list-style-type: none;
+ }
+
+ #navigate_menu > .toc-item,
+ #navigate_menu ul {
+ padding-left: 0px;
+ }
+
+ .toc {
+ padding: 0px;
+ overflow-y: auto;
+ font-weight: normal;
+ color: #333333; /* default - alterable via nbextension-configurator */
+ white-space: nowrap;
+ overflow-x: auto;
+ }
+
+ .text_cell .toc {
+ margin-top: 1em;
+ }
+
+ .toc ul.toc-item {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ #toc-wrapper {
+ z-index: 90;
+ /* position: fixed !important; */
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ padding: 10px;
+ border-style: solid;
+ border-width: thin;
+ background-color: #fff; /* default - alterable via nbextension-configurator */
+ }
+
+ #toc-wrapper .toc {
+ flex-grow: 1;
+ }
+
+ .float-wrapper {
+ border-color: rgba(0, 0, 0, 0.38);
+ border-radius: 5px;
+ opacity: .8;
+ }
+
+ .sidebar-wrapper {
+ top: 10px;
+ bottom: 0;
+ width: 212px;
+ border-color: #eeeeee; /* default - alterable via nbextension-configurator */
+ }
+
+ .sidebar-wrapper .ui-resizable-se {
+ display: none;
+ }
+
+ .sidebar-wrapper .ui-resizable-e {
+ position: absolute;
+ top: calc(50% - 8px);
+ }
+
+ #toc-wrapper.closed {
+ min-width: 100px;
+ width: auto;
+ transition: width;
+ }
+ #toc-wrapper:hover{
+ opacity: 1;
+ }
+ #toc-wrapper .header {
+ font-size: 18px;
+ font-weight: bold;
+ }
+
+ .sidebar-wrapper .hide-btn {
+ display:none;
+ }
+
+ #toc-wrapper .hide-btn:before {
+ content: "\f147";
+ }
+
+ #toc-wrapper.closed .hide-btn:before {
+ content: "\f196";
+ }
+
+ #toc-header .fa {
+ font-size: 14px;
+ text-decoration: none;
+ }
+
+ /* on scroll style */
+ .highlight_on_scroll {
+ border-left: solid 4px blue;
+ }
+
+ .toc-item li { margin:0; padding:0; color:black }
+ .toc-item li > span { display:block }
+ .toc-item li > span { padding-left:0em }
+ .toc-item li li > span { padding-left:1em }
+ .toc-item li li li > span { padding-left:2em }
+ .toc-item li li li li > span { padding-left:3em }
+ .toc-item li li li li li > span { padding-left:4em }
+ .toc-item li li li li li li > span { padding-left:5em }
+
+
+ #toc-wrapper .toc-item-num {
+ font-family: Georgia, Times New Roman, Times, serif;
+ color: black; /* default - alterable via nbextension-configurator */
+ }
+
+ /*
+ These colors are now specified in js, after reading the extension's config stored in system
+ and updated using the nbextension-configurator
+ .toc-item-highlight-select {background-color: Gold}
+ .toc-item-highlight-execute {background-color: red}
+ .toc-item-highlight-execute.toc-item-highlight-select {background-color: Gold} */
+
+ #toc-header .fa ,
+ .toc-item .fa-fw:first-child {
+ cursor: pointer;
+ }
+
+ #toc-header,
+ .modal-header {
+ cursor: move;
+ }
+
+ .tocSkip {
+ display: none;
+ }
\ No newline at end of file