@@ -354,6 +354,8 @@ def close_tag() -> None:
354
354
class MarkdownHeader (MarkdownBlock ):
355
355
"""Base class for a Markdown header."""
356
356
357
+ LEVEL = 0
358
+
357
359
DEFAULT_CSS = """
358
360
MarkdownHeader {
359
361
color: $text;
@@ -366,6 +368,8 @@ class MarkdownHeader(MarkdownBlock):
366
368
class MarkdownH1 (MarkdownHeader ):
367
369
"""An H1 Markdown header."""
368
370
371
+ LEVEL = 1
372
+
369
373
DEFAULT_CSS = """
370
374
MarkdownH1 {
371
375
content-align: center middle;
@@ -379,6 +383,8 @@ class MarkdownH1(MarkdownHeader):
379
383
class MarkdownH2 (MarkdownHeader ):
380
384
"""An H2 Markdown header."""
381
385
386
+ LEVEL = 2
387
+
382
388
DEFAULT_CSS = """
383
389
MarkdownH2 {
384
390
color: $markdown-h2-color;
@@ -391,6 +397,8 @@ class MarkdownH2(MarkdownHeader):
391
397
class MarkdownH3 (MarkdownHeader ):
392
398
"""An H3 Markdown header."""
393
399
400
+ LEVEL = 3
401
+
394
402
DEFAULT_CSS = """
395
403
MarkdownH3 {
396
404
color: $markdown-h3-color;
@@ -405,6 +413,8 @@ class MarkdownH3(MarkdownHeader):
405
413
class MarkdownH4 (MarkdownHeader ):
406
414
"""An H4 Markdown header."""
407
415
416
+ LEVEL = 4
417
+
408
418
DEFAULT_CSS = """
409
419
MarkdownH4 {
410
420
color: $markdown-h4-color;
@@ -418,6 +428,8 @@ class MarkdownH4(MarkdownHeader):
418
428
class MarkdownH5 (MarkdownHeader ):
419
429
"""An H5 Markdown header."""
420
430
431
+ LEVEL = 5
432
+
421
433
DEFAULT_CSS = """
422
434
MarkdownH5 {
423
435
color: $markdown-h5-color;
@@ -431,6 +443,8 @@ class MarkdownH5(MarkdownHeader):
431
443
class MarkdownH6 (MarkdownHeader ):
432
444
"""An H6 Markdown header."""
433
445
446
+ LEVEL = 6
447
+
434
448
DEFAULT_CSS = """
435
449
MarkdownH6 {
436
450
color: $markdown-h6-color;
@@ -574,7 +588,7 @@ def compose(self) -> ComposeResult:
574
588
bullet .symbol = f"{ number } { suffix } " .rjust (symbol_size + 1 )
575
589
yield Horizontal (bullet , Vertical (* block ._blocks ))
576
590
577
- # self._blocks.clear()
591
+ self ._blocks .clear ()
578
592
579
593
580
594
class MarkdownTableCellContents (Static ):
@@ -974,11 +988,21 @@ def __init__(
974
988
self ._initial_markdown : str | None = markdown
975
989
self ._markdown = ""
976
990
self ._parser_factory = parser_factory
977
- self ._table_of_contents : TableOfContentsType = []
991
+ self ._table_of_contents : TableOfContentsType | None = None
978
992
self ._open_links = open_links
979
993
self ._last_parsed_line = 0
980
994
self ._theme = ""
981
995
996
+ @property
997
+ def table_of_contents (self ) -> TableOfContentsType :
998
+ """The document's table of contents."""
999
+ if self ._table_of_contents is None :
1000
+ self ._table_of_contents = [
1001
+ (header .LEVEL , header ._content .plain , header .id )
1002
+ for header in self .query_children (MarkdownHeader )
1003
+ ]
1004
+ return self ._table_of_contents
1005
+
982
1006
class TableOfContentsUpdated (Message ):
983
1007
"""The table of contents was updated."""
984
1008
@@ -1182,16 +1206,11 @@ def unhandled_token(self, token: Token) -> MarkdownBlock | None:
1182
1206
"""
1183
1207
return None
1184
1208
1185
- def _parse_markdown (
1186
- self ,
1187
- tokens : Iterable [Token ],
1188
- table_of_contents : TableOfContentsType ,
1189
- ) -> Iterable [MarkdownBlock ]:
1209
+ def _parse_markdown (self , tokens : Iterable [Token ]) -> Iterable [MarkdownBlock ]:
1190
1210
"""Create a stream of MarkdownBlock widgets from markdown.
1191
1211
1192
1212
Args:
1193
1213
tokens: List of tokens.
1194
- table_of_contents: List to store table of contents.
1195
1214
1196
1215
Yields:
1197
1216
Widgets for mounting.
@@ -1251,18 +1270,17 @@ def _parse_markdown(
1251
1270
elif token_type .endswith ("_close" ):
1252
1271
block = stack .pop ()
1253
1272
if token .type == "heading_close" :
1254
- heading = block ._content .plain
1255
- level = int (token .tag [1 :])
1256
- block .id = f"{ slug (heading )} -{ len (table_of_contents ) + 1 } "
1257
- table_of_contents .append ((level , heading , block .id ))
1273
+ block .id = f"heading-{ slug (block ._content .plain )} -{ id (block )} "
1258
1274
if stack :
1259
1275
stack [- 1 ]._blocks .append (block )
1260
1276
else :
1261
1277
yield block
1262
1278
elif token_type == "inline" :
1263
1279
stack [- 1 ].build_from_token (token )
1264
1280
elif token_type in ("fence" , "code_block" ):
1265
- fence = get_block_class (token_type )(self , token , token .content .rstrip ())
1281
+ fence_class = get_block_class (token_type )
1282
+ assert issubclass (fence_class , MarkdownFence )
1283
+ fence = fence_class (self , token , token .content .rstrip ())
1266
1284
if stack :
1267
1285
stack [- 1 ]._blocks .append (fence )
1268
1286
else :
@@ -1276,13 +1294,21 @@ def _parse_markdown(
1276
1294
yield external
1277
1295
1278
1296
def _build_from_source (self , markdown : str ) -> list [MarkdownBlock ]:
1297
+ """Build blocks from markdown source.
1298
+
1299
+ Args:
1300
+ markdown: A Markdown document, or partial document.
1301
+
1302
+ Returns:
1303
+ A list of MarkdownBlock instances.
1304
+ """
1279
1305
parser = (
1280
1306
MarkdownIt ("gfm-like" )
1281
1307
if self ._parser_factory is None
1282
1308
else self ._parser_factory ()
1283
1309
)
1284
1310
tokens = parser .parse (markdown )
1285
- return list (self ._parse_markdown (tokens , [] ))
1311
+ return list (self ._parse_markdown (tokens ))
1286
1312
1287
1313
def update (self , markdown : str ) -> AwaitComplete :
1288
1314
"""Update the document with new Markdown.
@@ -1300,9 +1326,9 @@ def update(self, markdown: str) -> AwaitComplete:
1300
1326
else self ._parser_factory ()
1301
1327
)
1302
1328
1303
- table_of_contents : TableOfContentsType = []
1304
1329
markdown_block = self .query ("MarkdownBlock" )
1305
1330
self ._markdown = markdown
1331
+ self ._table_of_contents = None
1306
1332
1307
1333
async def await_update () -> None :
1308
1334
"""Update in batches."""
@@ -1333,7 +1359,7 @@ async def mount_batch(batch: list[MarkdownBlock]) -> None:
1333
1359
await self .mount_all (batch )
1334
1360
removed = True
1335
1361
1336
- for block in self ._parse_markdown (tokens , table_of_contents ):
1362
+ for block in self ._parse_markdown (tokens ):
1337
1363
batch .append (block )
1338
1364
if len (batch ) == BATCH_SIZE :
1339
1365
await mount_batch (batch )
@@ -1343,13 +1369,13 @@ async def mount_batch(batch: list[MarkdownBlock]) -> None:
1343
1369
if not removed :
1344
1370
await markdown_block .remove ()
1345
1371
1346
- if table_of_contents != self . _table_of_contents :
1347
- self . _table_of_contents = table_of_contents
1348
- self .post_message (
1349
- Markdown .TableOfContentsUpdated (
1350
- self , self ._table_of_contents
1351
- ).set_sender (self )
1352
- )
1372
+ lines = markdown . splitlines ()
1373
+ self . _last_parsed_line = len ( lines ) - ( 1 if lines and lines [ - 1 ] else 0 )
1374
+ self .post_message (
1375
+ Markdown .TableOfContentsUpdated (
1376
+ self , self .table_of_contents
1377
+ ).set_sender (self )
1378
+ )
1353
1379
1354
1380
return AwaitComplete (await_update ())
1355
1381
@@ -1368,7 +1394,6 @@ def append(self, markdown: str) -> AwaitComplete:
1368
1394
else self ._parser_factory ()
1369
1395
)
1370
1396
1371
- table_of_contents : TableOfContentsType = []
1372
1397
self ._markdown = self .source + markdown
1373
1398
updated_source = "" .join (
1374
1399
self ._markdown .splitlines (keepends = True )[self ._last_parsed_line :]
@@ -1387,7 +1412,7 @@ async def await_append() -> None:
1387
1412
self ._last_parsed_line += token .map [0 ]
1388
1413
break
1389
1414
1390
- new_blocks = list (self ._parse_markdown (tokens , table_of_contents ))
1415
+ new_blocks = list (self ._parse_markdown (tokens ))
1391
1416
for block in new_blocks :
1392
1417
start , end = block .source_range
1393
1418
block .source_range = (
@@ -1409,12 +1434,13 @@ async def await_append() -> None:
1409
1434
if new_blocks :
1410
1435
await self .mount_all (new_blocks )
1411
1436
1412
- self ._table_of_contents = table_of_contents
1413
- self .post_message (
1414
- Markdown .TableOfContentsUpdated (
1415
- self , self ._table_of_contents
1416
- ).set_sender (self )
1417
- )
1437
+ if any (isinstance (block , MarkdownHeader ) for block in new_blocks ):
1438
+ self ._table_of_contents = None
1439
+ self .post_message (
1440
+ Markdown .TableOfContentsUpdated (
1441
+ self , self .table_of_contents
1442
+ ).set_sender (self )
1443
+ )
1418
1444
1419
1445
return AwaitComplete (await_append ())
1420
1446
0 commit comments