Skip to content

Commit

Permalink
add table of contents to notebooks
Browse files Browse the repository at this point in the history
  • Loading branch information
danieleguido committed Dec 2, 2024
1 parent d7415ce commit a51cdca
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 26 deletions.
3 changes: 2 additions & 1 deletion src/components/NotebookCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface Notebook {
authors: Author[]
date?: Date
seealso?: Notebook[]
showLinks?: boolean
links?: { label: string; href: string }[]
}

Expand All @@ -35,7 +36,7 @@ const NotebookCard: React.FC<{
"- title:",
notebook?.title,
"notebook.langModel",
notebook.langModel
notebook.langModel,
)
return (
<div className={`NotebookCard shadow-sm ${className}`}>
Expand Down
107 changes: 82 additions & 25 deletions src/components/NotebookViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from "react"
import { Col, Container, Row } from "react-bootstrap"
import CodeSnippet from "./CodeSnippet"
import MarkdownSnipped from "./MarkdownSnippet"
Expand All @@ -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<CellInfo> => {
const cells: Array<CellInfo> = []
const regex = /\{\/*\*\s*cell:(\d+)\s*cell_type:(\w+)\s*\*\/\}/g
let match
while ((match = regex.exec(text)) !== null) {
Expand All @@ -33,25 +37,37 @@ const splitTextWithCellInfo = (
cellNumber: parseInt(match[1]),
cellType: match[2],
content: "",
hl: 0,
h: "",
})
}

// now based on the indexes we can extract the content
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)
Expand All @@ -75,12 +91,31 @@ const NotebookViewer: React.FC<NotebookViewerProps> = ({
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<HTMLDivElement>(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 (
<Container className="NotebookViewer">
<Container className="NotebookViewer" ref={containerRef}>
<Row className="my-3">
<h1 dangerouslySetInnerHTML={{ __html: notebook.title }} />
</Row>
Expand Down Expand Up @@ -133,7 +168,7 @@ const NotebookViewer: React.FC<NotebookViewerProps> = ({
</div>
</Alert>
</Col>
<Col lg="5">
<Col lg="5" className="ps-4">
<a target="_blank" href={issueUrl}>
Report an issue
</a>
Expand All @@ -149,17 +184,20 @@ const NotebookViewer: React.FC<NotebookViewerProps> = ({
return true
})
.map((cell, i) => (
<div key={cell.cellNumber}>
{cell.cellType === "markdown" && (
<MarkdownSnipped
className={i > 0 ? "my-3" : "mb-3"}
value={cell.content}
/>
)}
{cell.cellType === "code" && (
<CodeSnippet value={cell.content} readonly />
)}
</div>
<React.Fragment key={cell.cellNumber}>
{cell.hl && <a id={notebook.href + cell.cellNumber}></a>}
<div>
{cell.cellType === "markdown" && (
<MarkdownSnipped
className={i > 0 ? "my-3" : "mb-3"}
value={cell.content}
/>
)}
{cell.cellType === "code" && (
<CodeSnippet value={cell.content} readonly />
)}
</div>
</React.Fragment>
))}
</Col>
<Col lg="5" className="ps-4">
Expand All @@ -169,12 +207,29 @@ const NotebookViewer: React.FC<NotebookViewerProps> = ({
<MarkdownSnipped className="m-0 small" value={excerpt} />
</>
)}

<div
style={{
position: "sticky",
top: 0,
}}
>
{headings.length > 0 && (
<>
<h4>Table of contents</h4>
<ul className="list-unstyled">
{headings.map((heading) => (
<li key={heading.cellNumber}>
<button
className="btn btn-link no-smallcaps text-decoration-none"
onClick={() => scrollToHeading(heading.cellNumber)}
dangerouslySetInnerHTML={{ __html: heading.h }}
/>
</li>
))}
</ul>
</>
)}
{Array.isArray(notebook.seealso) ? (
<>
<h4>See also</h4>
Expand All @@ -192,7 +247,9 @@ const NotebookViewer: React.FC<NotebookViewerProps> = ({
</ul>
</>
) : null}
{Array.isArray(notebook.links) && notebook.links.length > 0 ? (
{notebook.showLinks &&
Array.isArray(notebook.links) &&
notebook.links.length > 0 ? (
<>
<h4>Links</h4>
<ul
Expand Down
1 change: 1 addition & 0 deletions src/content/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const notebooks = defineCollection({
links: z
.array(z.object({ label: z.string(), href: z.string().url() }))
.optional(),
showLinks: z.boolean().optional(),
authors: z.array(reference("authors")).optional(),
// this gives circular reference
seealso: z.array(z.string()).optional(),
Expand Down
8 changes: 8 additions & 0 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,14 @@ button svg {
white-space: nowrap;
--bs-border-radius: var(--impresso-border-radius-sm);
}
.btn.no-smallcaps {
font-size: inherit;
text-transform: none;
font-variant: none;
letter-spacing: normal;
font-weight: var(--impresso-wght-medium);
font-variation-settings: "wght" var(--impresso-wght-medium);
}
.btn-primary {
--bs-btn-color: var(--impresso-color-black);
--bs-btn-bg: var(--impresso-color-yellow);
Expand Down

0 comments on commit a51cdca

Please sign in to comment.