From a51cdca7e4b1c0cf7ff40c3d5def112f1dc2a0cd Mon Sep 17 00:00:00 2001 From: Daniele Guido Date: Mon, 2 Dec 2024 22:49:05 +0100 Subject: [PATCH] add table of contents to notebooks --- src/components/NotebookCard.tsx | 3 +- src/components/NotebookViewer.tsx | 107 +++++++++++++++++++++++------- src/content/config.ts | 1 + src/styles/global.css | 8 +++ 4 files changed, 93 insertions(+), 26 deletions(-) diff --git a/src/components/NotebookCard.tsx b/src/components/NotebookCard.tsx index 84032eb..102b0ce 100644 --- a/src/components/NotebookCard.tsx +++ b/src/components/NotebookCard.tsx @@ -18,6 +18,7 @@ export interface Notebook { authors: Author[] date?: Date seealso?: Notebook[] + showLinks?: boolean links?: { label: string; href: string }[] } @@ -35,7 +36,7 @@ const NotebookCard: React.FC<{ "- title:", notebook?.title, "notebook.langModel", - notebook.langModel + notebook.langModel, ) return (
diff --git a/src/components/NotebookViewer.tsx b/src/components/NotebookViewer.tsx index 5a7504f..74c5a83 100644 --- a/src/components/NotebookViewer.tsx +++ b/src/components/NotebookViewer.tsx @@ -1,3 +1,4 @@ +import React from "react" import { Col, Container, Row } from "react-bootstrap" import CodeSnippet from "./CodeSnippet" import MarkdownSnipped from "./MarkdownSnippet" @@ -14,16 +15,19 @@ export interface NotebookViewerProps { raw?: string } -const splitTextWithCellInfo = ( - text: string -): Array<{ cellNumber: number; cellType: string; content: string }> => { - const cells: Array<{ - cellNumber: number - cellType: string - content: string - idx: number - l: number - }> = [] +export type CellInfo = { + cellNumber: number + cellType: string + content: string + idx: number + l: number + // headingLevel + hl?: number + h: string +} + +const splitTextWithCellInfo = (text: string): Array => { + const cells: Array = [] const regex = /\{\/*\*\s*cell:(\d+)\s*cell_type:(\w+)\s*\*\/\}/g let match while ((match = regex.exec(text)) !== null) { @@ -33,6 +37,8 @@ const splitTextWithCellInfo = ( cellNumber: parseInt(match[1]), cellType: match[2], content: "", + hl: 0, + h: "", }) } @@ -40,18 +46,28 @@ const splitTextWithCellInfo = ( for (let i = 0; i < cells.length; i++) { const start = cells[i].idx + cells[i].l const end = cells[i + 1]?.idx + cells[i].content = text .slice(start, end) .trim() .replace(/^```python/, "") .replace(/```$/, "") .trim() + + // check if the cell is markdown and extract the heading level + if (cells[i].cellType === "markdown") { + const headingMatch = cells[i].content.match(/^#+ /) + if (headingMatch) { + cells[i].hl = headingMatch[0].length + cells[i].h = cells[i].content.split("\n")[0].replace(/^#+ /, "") + } + } } return cells } const getGithubIssuesUrl = ( - githubUrl: string + githubUrl: string, ): { url: string; account: string; repository: string } => { const repoPattern = /github\.com\/([^/]+)\/([^/]+)/ const match = repoPattern.exec(githubUrl) @@ -75,12 +91,31 @@ const NotebookViewer: React.FC = ({ const accessDateTime = DateTime.fromJSDate(accessTime) const excerpt = notebook.excerpt ?? "" + const headings = cells.filter((cell) => cell.hl) // Example usage: const githubUrl = notebook.githubUrl ?? "" const { url: issueUrl } = getGithubIssuesUrl(githubUrl) + const containerRef = React.useRef(null) + + const scrollToHeading = (cellNumber: number) => { + const heading = document.getElementById(notebook.href + cellNumber) + if (heading) { + const offsetTop = heading.getBoundingClientRect().top + + const container = containerRef.current?.parentElement + if (container) { + console.log("offsetTop", offsetTop) + + container.scrollTo({ + top: offsetTop, + behavior: "smooth", + }) + } + } + } return ( - +

@@ -133,7 +168,7 @@ const NotebookViewer: React.FC = ({

- + Report an issue @@ -149,17 +184,20 @@ const NotebookViewer: React.FC = ({ return true }) .map((cell, i) => ( -
- {cell.cellType === "markdown" && ( - 0 ? "my-3" : "mb-3"} - value={cell.content} - /> - )} - {cell.cellType === "code" && ( - - )} -
+ + {cell.hl && } +
+ {cell.cellType === "markdown" && ( + 0 ? "my-3" : "mb-3"} + value={cell.content} + /> + )} + {cell.cellType === "code" && ( + + )} +
+
))} @@ -169,12 +207,29 @@ const NotebookViewer: React.FC = ({ )} +
+ {headings.length > 0 && ( + <> +

Table of contents

+
    + {headings.map((heading) => ( +
  • +
  • + ))} +
+ + )} {Array.isArray(notebook.seealso) ? ( <>

See also

@@ -192,7 +247,9 @@ const NotebookViewer: React.FC = ({ ) : null} - {Array.isArray(notebook.links) && notebook.links.length > 0 ? ( + {notebook.showLinks && + Array.isArray(notebook.links) && + notebook.links.length > 0 ? ( <>

Links