Skip to content

Commit 92d9b7d

Browse files
committed
fix #4085 -- jupyter2/toc: include all headers
- this works well, but is missing one thing, which is that if you have a large markdown cell and click on a subheader, it scrolls the cell into view, but NOT the exact header you clicked on.
1 parent 6af2a26 commit 92d9b7d

File tree

5 files changed

+63
-49
lines changed

5 files changed

+63
-49
lines changed

src/packages/frontend/components/table-of-contents.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { CSS, React, TypedMap } from "../app-framework";
99
import { Markdown } from "./markdown";
1010

1111
export interface TableOfContentsEntry {
12-
id: string; // id that is unique across the table of contents
12+
id: string; // id that is jumped to when entry is clicked -- must be unique across the table of contents
1313
value: string; // contents of the heading -- a 1-line string formatted using markdown (will be rendered using markdown)
1414
level?: 1 | 2 | 3 | 4 | 5 | 6; // optional heading size/level
1515
icon?: IconName; // default "minus" (a dash)
@@ -31,7 +31,7 @@ export const TableOfContents: React.FC<Props> = React.memo(
3131
function renderHeader(
3232
level: 1 | 2 | 3 | 4 | 5 | 6,
3333
value: string,
34-
icon: IconName | undefined
34+
icon: IconName | undefined,
3535
): JSX.Element {
3636
if (level < 1) level = 1;
3737
if (level > 6) level = 6;
@@ -114,5 +114,5 @@ export const TableOfContents: React.FC<Props> = React.memo(
114114
{entries}
115115
</div>
116116
);
117-
}
117+
},
118118
);

src/packages/frontend/frame-editors/jupyter-editor/table-of-contents.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ export const TableOfContents: React.FC<Props> = React.memo(
2323
"contents",
2424
]);
2525

26-
async function jump_to_cell(id: string, extra = "top" as "top"): Promise<void> {
26+
async function jump_to_cell(
27+
id: string,
28+
extra = "top" as "top",
29+
): Promise<void> {
2730
actions.jump_to_cell(id, extra);
2831
// stupid hack due to rendering/windowing delays...
2932
await delay(100);
@@ -34,7 +37,13 @@ export const TableOfContents: React.FC<Props> = React.memo(
3437
<TOC
3538
contents={contents}
3639
style={{ fontSize: `${font_size - 6}px` }}
37-
scrollTo={({ id, extra }) => jump_to_cell(id, extra)}
40+
scrollTo={({ id, extra }) => {
41+
// TODO: ignore markdown_id for now -- we just jump to the markdown cell, but not the exact heading
42+
// in that cell, for now.
43+
// markdown_id is string rep of number of block reference into the rendered markdown.
44+
const { cell_id /*, markdown_id*/ } = JSON.parse(id);
45+
jump_to_cell(cell_id, extra);
46+
}}
3847
/>
3948
);
4049
},

src/packages/frontend/frame-editors/markdown-editor/actions.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export class Actions extends CodeEditorActions<MarkdownEditorState> {
186186
this.set_active_id(id, true);
187187
}
188188

189-
public updateTableOfContents(force: boolean = false): void {
189+
updateTableOfContents = (force: boolean = false): void => {
190190
if (this._state == "closed" || this._syncstring == null) {
191191
// no need since not initialized yet or already closed.
192192
return;
@@ -202,7 +202,7 @@ export class Actions extends CodeEditorActions<MarkdownEditorState> {
202202
parseTableOfContents(this._syncstring.to_str()),
203203
) as any;
204204
this.setState({ contents });
205-
}
205+
};
206206

207207
public async scrollToHeading(entry: TableOfContentsEntry): Promise<void> {
208208
const id = this.show_focused_frame_of_type("slate");
@@ -214,8 +214,15 @@ export class Actions extends CodeEditorActions<MarkdownEditorState> {
214214
await delay(1);
215215
editor = this.getSlateEditor(id);
216216
}
217-
if (editor == null) return;
218-
scrollToHeading(editor, parseInt(entry.id));
217+
if (editor == null) {
218+
return;
219+
}
220+
const n = parseInt(entry.id);
221+
scrollToHeading(editor, n);
222+
// this is definitely necessary in case the editor wasn't opened, and doesn't
223+
// hurt if it is.
224+
await delay(1);
225+
scrollToHeading(editor, n);
219226
}
220227

221228
// for rendered markdown, switch frame type so that this rendered view

src/packages/frontend/jupyter/browser-actions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import {
3333
get_local_storage,
3434
set_local_storage,
3535
} from "../misc/local-storage";
36-
import { parse_headings } from "./contents";
36+
import { parseHeadings } from "./contents";
3737
import { webapp_client } from "@cocalc/frontend/webapp-client";
3838
import { bufferToBase64, base64ToBuffer } from "@cocalc/util/base64";
3939
import { reuseInFlight } from "@cocalc/util/reuse-in-flight";
@@ -931,7 +931,7 @@ export class JupyterActions extends JupyterActions0 {
931931
if (cells == null) return;
932932
const cell_list = this.store.get("cell_list");
933933
if (cell_list == null) return;
934-
const contents = fromJS(parse_headings(cells, cell_list));
934+
const contents = fromJS(parseHeadings(cells, cell_list));
935935
this.setState({ contents });
936936
}
937937

src/packages/frontend/jupyter/contents.ts

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Parse the Table of Contents information from the notebook structure.
99

1010
import { List, Map } from "immutable";
1111
import { IconName } from "@cocalc/frontend/components/icon";
12+
import { parseTableOfContents } from "@cocalc/frontend/markdown";
1213

1314
export interface TableOfContentsInfo {
1415
id: string;
@@ -19,9 +20,9 @@ export interface TableOfContentsInfo {
1920
align: "center" | "top";
2021
}
2122

22-
export function parse_headings(
23+
export function parseHeadings(
2324
cells: Map<string, any>,
24-
cell_list: List<string>
25+
cell_list: List<string>,
2526
): TableOfContentsInfo[] {
2627
const v: TableOfContentsInfo[] = [];
2728
let last_level: number = 0,
@@ -63,47 +64,44 @@ export function parse_headings(
6364
}
6465
}
6566

66-
if (cell.get("cell_type") != "markdown") return;
67+
if (cell.get("cell_type") != "markdown") {
68+
return;
69+
}
6770

6871
const input = cell.get("input");
69-
if (input == null) return; // this is only needed since in types we don't impose any structure on cell yet.
70-
const { level, value } = parse_cell_heading(input);
71-
72-
if (level > 0) {
73-
if (last_level != level) {
74-
// reset section numbers
75-
for (let i = level; i < section_counter.length; i++) {
76-
section_counter[i] = 0;
77-
}
78-
last_level = level;
72+
if (input == null) {
73+
// this is only needed since in types we don't impose any structure on cell yet.
74+
return;
75+
}
76+
for (const { id: markdown_id, level, value } of parseTableOfContents(
77+
input,
78+
)) {
79+
if (level == null) {
80+
continue;
7981
}
80-
for (let i = 0; i < level; i++) {
81-
if (section_counter[i] == null) section_counter[i] = 0;
82+
if (level > 0) {
83+
if (last_level != level) {
84+
// reset section numbers
85+
for (let i = level; i < section_counter.length; i++) {
86+
section_counter[i] = 0;
87+
}
88+
last_level = level;
89+
}
90+
for (let i = 0; i < level; i++) {
91+
if (section_counter[i] == null) section_counter[i] = 0;
92+
}
93+
section_counter[level - 1] += 1;
94+
const cell_id = cell.get("id");
95+
if (cell_id == null) return;
96+
v.push({
97+
id: JSON.stringify({ markdown_id, cell_id }),
98+
level,
99+
value,
100+
number: section_counter.slice(0, level),
101+
align: "top",
102+
});
82103
}
83-
section_counter[level - 1] += 1;
84-
const id = cell.get("id");
85-
if (id == null) return;
86-
v.push({
87-
id,
88-
level,
89-
value,
90-
number: section_counter.slice(0, level),
91-
align: "top",
92-
});
93104
}
94105
});
95106
return v;
96107
}
97-
98-
function parse_cell_heading(input: string): { level: number; value: string } {
99-
for (const line of input.split("\n")) {
100-
const x = line.trim();
101-
if (x[0] != "#") continue;
102-
for (let n = 1; n < x.length; n++) {
103-
if (x[n] != "#") {
104-
return { level: n, value: x.slice(n).trim() };
105-
}
106-
}
107-
}
108-
return { level: 0, value: "" }; // no heading in markdown
109-
}

0 commit comments

Comments
 (0)