1
1
// Copyright (c) Jupyter Development Team.
2
2
// Distributed under the terms of the Modified BSD License.
3
3
4
+ import { ISanitizer } from '@jupyterlab/apputils' ;
5
+
4
6
import { FileEditor , IEditorTracker } from '@jupyterlab/fileeditor' ;
5
7
8
+ import { MarkdownCell } from '@jupyterlab/cells' ;
9
+
6
10
import { INotebookTracker , NotebookPanel } from '@jupyterlab/notebook' ;
7
11
8
12
import { each } from '@phosphor/algorithm' ;
@@ -20,6 +24,7 @@ import {IHeading} from './toc';
20
24
*/
21
25
export function createNotebookGenerator (
22
26
tracker : INotebookTracker ,
27
+ sanitizer : ISanitizer ,
23
28
) : TableOfContentsRegistry . IGenerator < NotebookPanel > {
24
29
return {
25
30
tracker,
@@ -33,14 +38,35 @@ export function createNotebookGenerator(
33
38
return ;
34
39
}
35
40
36
- const onClickFactory = ( ) => {
37
- return ( ) => {
38
- cell . node . scrollIntoView ( ) ;
41
+ // If the cell is rendered, generate the ToC items from
42
+ // the HTML. If it is not rendered, generate them from
43
+ // the text of the cell.
44
+ if ( ( cell as MarkdownCell ) . rendered ) {
45
+ const onClickFactory = ( el : Element ) => {
46
+ return ( ) => {
47
+ if ( ! ( cell as MarkdownCell ) . rendered ) {
48
+ cell . node . scrollIntoView ( ) ;
49
+ } else {
50
+ el . scrollIntoView ( ) ;
51
+ }
52
+ } ;
39
53
} ;
40
- } ;
41
- headings = headings . concat (
42
- Private . getMarkdownHeadings ( model . value . text , onClickFactory ) ,
43
- ) ;
54
+ headings = headings . concat (
55
+ Private . getRenderedHTMLHeadings ( cell . node , onClickFactory , sanitizer ) ,
56
+ ) ;
57
+ } else {
58
+ const onClickFactory = ( line : number ) => {
59
+ return ( ) => {
60
+ cell . node . scrollIntoView ( ) ;
61
+ if ( ! ( cell as MarkdownCell ) . rendered ) {
62
+ cell . editor . setCursorPosition ( { line, column : 0 } ) ;
63
+ }
64
+ } ;
65
+ } ;
66
+ headings = headings . concat (
67
+ Private . getMarkdownHeadings ( model . value . text , onClickFactory ) ,
68
+ ) ;
69
+ }
44
70
} ) ;
45
71
return headings ;
46
72
} ,
@@ -120,7 +146,7 @@ export function createLatexGenerator(
120
146
/ ^ \s * \\ ( s e c t i o n | s u b s e c t i o n | s u b s u b s e c t i o n ) { ( .+ ) } / ,
121
147
) ;
122
148
if ( match ) {
123
- const level = Private . LatexLevels [ match [ 1 ] ] ;
149
+ const level = Private . latexLevels [ match [ 1 ] ] ;
124
150
const text = match [ 2 ] ;
125
151
const onClick = ( ) => {
126
152
editor . editor . setCursorPosition ( { line : line . idx , column : 0 } ) ;
@@ -137,10 +163,14 @@ export function createLatexGenerator(
137
163
* A private namespace for miscellaneous things.
138
164
*/
139
165
namespace Private {
166
+ /**
167
+ * Given a string of markdown, get the markdown headings
168
+ * in that string.
169
+ */
140
170
export function getMarkdownHeadings (
141
171
text : string ,
142
172
onClickFactory : ( line : number ) => ( ( ) => void ) ,
143
- ) {
173
+ ) : IHeading [ ] {
144
174
// Split the text into lines.
145
175
const lines = text . split ( '\n' ) ;
146
176
let headings : IHeading [ ] = [ ] ;
@@ -174,7 +204,7 @@ namespace Private {
174
204
// Finally test for HTML headers. This will not catch multiline
175
205
// headers, nor will it catch multiple headers on the same line.
176
206
// It should do a decent job of catching many, though.
177
- match = line . match ( / < h ( [ 1 - 6 ] ) > ( .* ) < \/ h \1> / ) ;
207
+ match = line . match ( / < h ( [ 1 - 6 ] ) > ( .* ) < \/ h \1> / i ) ;
178
208
if ( match ) {
179
209
const level = parseInt ( match [ 1 ] ) ;
180
210
const text = match [ 2 ] ;
@@ -183,12 +213,37 @@ namespace Private {
183
213
} ) ;
184
214
return headings ;
185
215
}
216
+
217
+ /**
218
+ * Given an HTML element, generate ToC headings
219
+ * by finding all the headers and making IHeading objects for them.
220
+ */
221
+ export function getRenderedHTMLHeadings (
222
+ node : HTMLElement ,
223
+ onClickFactory : ( el : Element ) => ( ( ) => void ) ,
224
+ sanitizer : ISanitizer ,
225
+ ) : IHeading [ ] {
226
+ let headings : IHeading [ ] = [ ] ;
227
+ let headingNodes = node . querySelectorAll ( 'h1, h2, h3, h4, h5, h6' ) ;
228
+ for ( let i = 0 ; i < headingNodes . length ; i ++ ) {
229
+ const heading = headingNodes [ i ] ;
230
+ const level = parseInt ( heading . tagName [ 1 ] ) ;
231
+ const text = heading . textContent ;
232
+ let html = sanitizer . sanitize ( heading . innerHTML , sanitizerOptions ) ;
233
+ html = html . replace ( '¶' , '' ) ; // Remove the anchor symbol.
234
+
235
+ const onClick = onClickFactory ( heading ) ;
236
+ headings . push ( { level, text, html, onClick} ) ;
237
+ }
238
+ return headings ;
239
+ }
240
+
186
241
/**
187
242
* A mapping from LaTeX section headers to HTML header
188
243
* levels. `part` and `chapter` are less common in my experience,
189
244
* so assign them to header level 1.
190
245
*/
191
- export const LatexLevels : { [ label : string ] : number } = {
246
+ export const latexLevels : { [ label : string ] : number } = {
192
247
part : 1 , // Only available for report and book classes
193
248
chapter : 1 , // Only available for report and book classes
194
249
section : 1 ,
@@ -197,4 +252,39 @@ namespace Private {
197
252
paragraph : 4 ,
198
253
subparagraph : 5 ,
199
254
} ;
255
+
256
+ /**
257
+ * Allowed HTML tags for the ToC entries. We use this to
258
+ * sanitize HTML headings, if they are given. We specifically
259
+ * disallow anchor tags, since we are adding our own.
260
+ */
261
+ const sanitizerOptions = {
262
+ allowedTags : [
263
+ 'p' ,
264
+ 'blockquote' ,
265
+ 'b' ,
266
+ 'i' ,
267
+ 'strong' ,
268
+ 'em' ,
269
+ 'strike' ,
270
+ 'code' ,
271
+ 'br' ,
272
+ 'div' ,
273
+ 'span' ,
274
+ 'pre' ,
275
+ 'del' ,
276
+ ] ,
277
+ allowedAttributes : {
278
+ // Allow "class" attribute for <code> tags.
279
+ code : [ 'class' ] ,
280
+ // Allow "class" attribute for <span> tags.
281
+ span : [ 'class' ] ,
282
+ // Allow "class" attribute for <div> tags.
283
+ div : [ 'class' ] ,
284
+ // Allow "class" attribute for <p> tags.
285
+ p : [ 'class' ] ,
286
+ // Allow "class" attribute for <pre> tags.
287
+ pre : [ 'class' ] ,
288
+ } ,
289
+ } ;
200
290
}
0 commit comments