Skip to content

Commit

Permalink
Merge pull request #24 from ASFOpenSARlab/JupyterLite
Browse files Browse the repository at this point in the history
Jupyter lite
  • Loading branch information
Alex-Lewandowski authored Nov 27, 2024
2 parents 8caf869 + 0502632 commit a77d159
Show file tree
Hide file tree
Showing 5 changed files with 73 additions and 93 deletions.
12 changes: 7 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# Changelog

<!-- <START NEW CHANGELOG ENTRY> -->

# Changelog

## Version 0.2.0

previous version: 0.1.4

- Move TOC logic client side

<!-- <END NEW CHANGELOG ENTRY> -->
<!-- <START NEW CHANGELOG ENTRY> -->

## Version 1.0.0

- Adds JupyterLite support
- Fixes #19
- <!-- <END NEW CHANGELOG ENTRY> -->
14 changes: 3 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
# jupyterlab_jupyterbook_navigation

[![Github Actions Status](https://github.com/ASFOpenSARlab/jupyterlab-jupyterbook-navigation/workflows/Build/badge.svg)](https://github.com/Alex-Lewandowski/jupyterlab-jbook-chapter-navigation/actions/workflows/build.yml)[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/ASFOpenSARlab/jupyterlab-jupyterbook-navigation/main?urlpath=lab)
[![lite-badge](https://jupyterlite.rtfd.io/en/latest/_static/badge.svg)](https://alex-lewandowski.github.io/JupyterLite-demo/lab/index.html)

A JupyterLab server extension that provides Jupyter-Book navigation via a sidepanel widget holding a Jupyter-Book table of contents.

> [!WARNING]
> This package is currently in a pre-alpha stage:
>
> 1. **Expect Significant Changes:** Features, functionality, and the overall design may change significantly in future updates.
> 1. **Limited Functionality and Correctness:** There are no guarantees of full functionality or correctness.
> 1. **Use at Your Own Risk:** Given its early stage of development, users should exercise caution when integrating this package into critical systems.
A JupyterLab server extension that provides Jupyter-Book navigation in a sidepanel widget with a Jupyter-Book table of contents.

https://github.com/ASFOpenSARlab/jupyterlab-jupyterbook-navigation/assets/37909088/3aa48f43-dfeb-466d-8f33-afc10f333f50

This extension is composed of a Python package named `jupyterlab_jupyterbook_navigation`
for the server extension and a NPM package named `jupyterlab-jupyterbook-navigation`
for the frontend extension.
NPM frontend extension: `jupyterlab-jupyterbook-navigation`

## Requirements

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jupyterlab-jupyterbook-navigation",
"version": "0.2.1",
"version": "1.0.0",
"description": "A JupyterLab extension that mimics jupyter-book chapter navigation on an un-built, cloned jupyter book in JupyterLab.",
"keywords": [
"jupyter",
Expand Down
13 changes: 13 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ import { IFileBrowserFactory } from '@jupyterlab/filebrowser';

import * as jbtoc from './jbtoc';

let appInstance: JupyterFrontEnd | null = null;

export function getJupyterAppInstance(app?: JupyterFrontEnd): JupyterFrontEnd {
if (!appInstance && app) {
appInstance = app;
}
if (!appInstance) {
throw new Error('App instance has not been initialized yet');
}
return appInstance;
}

const plugin: JupyterFrontEndPlugin<void> = {
id: 'jupyterlab-jupyterbook-navigation:plugin',
description:
Expand All @@ -24,6 +36,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
fileBrowserFactory: IFileBrowserFactory,
docManager: IDocumentManager
) => {
getJupyterAppInstance(app);
console.log(
'JupyterLab extension jupyterlab-jupyterbook-navigation is activated!'
);
Expand Down
125 changes: 49 additions & 76 deletions src/jbtoc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ServerConnection } from '@jupyterlab/services';
import * as path from 'path';
import * as yaml from 'js-yaml';
import { getJupyterAppInstance } from './index';

interface IFileMetadata {
path: string;
Expand Down Expand Up @@ -41,26 +41,19 @@ interface ICell {
source: string;
}

async function getFileContents(path: string): Promise<INotebook | string> {
const serverSettings = ServerConnection.makeSettings();

const url = new URL(path, serverSettings.baseUrl + 'api/contents/').href;

let response: Response;

async function getFileContents(path: string): Promise<string> {
try {
response = await ServerConnection.makeRequest(url, {}, serverSettings);
const app = getJupyterAppInstance();
const data = await app.serviceManager.contents.get(path, { content: true });
if (data.type === 'notebook' || data.type === 'file') {
return data.content as string;
} else {
throw new Error(`Unsupported file type: ${data.type}`);
}
} catch (error) {
console.error(`Failed to get file: ${error}`);
console.error(`Failed to get file contents for ${path}:`, error);
throw error;
}

if (!response.ok) {
throw new Error(`Failed to get file: ${response.statusText}`);
}

const data = await response.json();
return data.content;
}

function isNotebook(obj: any): obj is INotebook {
Expand All @@ -73,9 +66,17 @@ async function getTitle(filePath: string): Promise<string | null> {
try {
const jsonData: INotebook | string = await getFileContents(filePath);
if (isNotebook(jsonData)) {
const firstHeaderCell = jsonData.cells.find(
cell => cell.cell_type === 'markdown'
);
const headerCells = jsonData.cells.filter(cell => {
if (cell.cell_type === 'markdown') {
const source = Array.isArray(cell.source)
? cell.source.join('')
: cell.source;
return source.split('\n').some(line => line.startsWith('# '));
}
return false;
});

const firstHeaderCell = headerCells.length > 0 ? headerCells[0] : null;
if (firstHeaderCell) {
if (firstHeaderCell.source.split('\n')[0].slice(0, 2) === '# ') {
const title: string = firstHeaderCell.source
Expand Down Expand Up @@ -107,7 +108,7 @@ async function getTitle(filePath: string): Promise<string | null> {
}

function isIJbookConfig(obj: any): obj is IJbookConfig {
return obj && typeof obj === 'object' && obj.title && obj.author && obj.logo;
return obj && typeof obj === 'object' && obj.title && obj.author;
}

async function getBookConfig(
Expand All @@ -131,77 +132,48 @@ async function getBookConfig(
return { title: null, author: null };
}

function getBaseUrl() {
const origin = window.location.origin;
const pathSegment = window.location.pathname.split('/');
// Remove empty strings
const filteredSegments = pathSegment.filter(part => part !== '');
const labIndex = filteredSegments.lastIndexOf('lab');
// If 'lab' not in path, use the entire path, else slice up to last instance of 'lab'
const segments =
labIndex !== -1
? filteredSegments.slice(0, labIndex).join('/')
: filteredSegments.join('/');
return segments ? `${origin}/${segments}` : origin;
}

async function ls(pth: string): Promise<any> {
const baseUrl = getBaseUrl();
const fullPath = `${baseUrl}/api/contents/${pth}?content=1`;
try {
const response = await fetch(fullPath, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
if (pth === '') {
pth = '/';
}

const data = await response.json();
try {
const app = getJupyterAppInstance();
const data = await app.serviceManager.contents.get(pth, { content: true });
return data;
} catch (error) {
console.error('Error listing directory contents:', error);
return null;
}
}

async function glob_files(pattern: string): Promise<any> {
const baseUrl = '/api/globbing/';
const fullPath = `${baseUrl}${pattern}`;
async function globFiles(pattern: string): Promise<string[]> {
const baseDir = '';
const result: string[] = [];

try {
const response = await fetch(fullPath, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
const app = getJupyterAppInstance();
const data = await app.serviceManager.contents.get(baseDir, {
content: true
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const files = await response.json();
const result = [];
for (const file of files) {
if (file.type === 'file') {
result.push(file.path);
const regex = new RegExp(pattern);
for (const item of data.content) {
if (item.type === 'file' && regex.test(item.path)) {
result.push(item.path);
}
}
return result;
} catch (error) {
console.error(`Error globbing pattern ${pattern}`, error);
return [];
}

return result;
}

async function findTOCinParents(cwd: string): Promise<string | null> {
const dirs = cwd.split('/');
const tocPattern: string = '_toc.yml';
while (dirs.length > 0) {
let counter: number = 0;
while (counter < 1) {
const pth = dirs.join('/');
const files = await ls(pth);
for (const value of Object.values(files.content)) {
Expand All @@ -211,7 +183,11 @@ async function findTOCinParents(cwd: string): Promise<string | null> {
return file.path;
}
}
dirs.pop();
if (dirs.length === 0) {
counter += 1;
} else {
dirs.pop();
}
}
return null;
}
Expand Down Expand Up @@ -278,7 +254,7 @@ async function getSubSection(
} else if (k.url) {
html += `<button class="jp-Button toc-button tb-level${level}" style="display:block;"><a class="toc-link tb-level${level}" href="${k.url}" target="_blank" rel="noopener noreferrer" style="display: block;">${k.title}</a></button>`;
} else if (k.glob) {
const files = await glob_files(`${cwd}${k.glob}`);
const files = await globFiles(`${cwd}${k.glob}`);
for (const file of files) {
const relative = file.replace(`${cwd}`, '');
await insert_one_file(relative);
Expand Down Expand Up @@ -313,12 +289,9 @@ export async function getTOC(cwd: string): Promise<string> {
let configParent = null;
if (tocPath) {
const parts = tocPath.split('/');

parts.pop();
configParent = parts.join('/');

const files = await ls(configParent);

const configPattern = '_config.yml';
for (const value of Object.values(files.content)) {
const file = value as IFileMetadata;
Expand All @@ -345,7 +318,7 @@ export async function getTOC(cwd: string): Promise<string> {
return `
<div class="jbook-toc" data-toc-dir="${configParent}"><p id="toc-title">${config.title}</p>
<p id="toc-author">Author: ${config.author}</p>
${toc_html} </div>"
${toc_html} </div>
`;
} else {
console.error('Error: Misconfigured Jupyter Book _toc.yml.');
Expand Down

0 comments on commit a77d159

Please sign in to comment.