@@ -354,6 +354,8 @@ def close_tag() -> None:
354354class MarkdownHeader (MarkdownBlock ):
355355 """Base class for a Markdown header."""
356356
357+ LEVEL = 0
358+
357359 DEFAULT_CSS = """
358360 MarkdownHeader {
359361 color: $text;
@@ -366,6 +368,8 @@ class MarkdownHeader(MarkdownBlock):
366368class MarkdownH1 (MarkdownHeader ):
367369 """An H1 Markdown header."""
368370
371+ LEVEL = 1
372+
369373 DEFAULT_CSS = """
370374 MarkdownH1 {
371375 content-align: center middle;
@@ -379,6 +383,8 @@ class MarkdownH1(MarkdownHeader):
379383class MarkdownH2 (MarkdownHeader ):
380384 """An H2 Markdown header."""
381385
386+ LEVEL = 2
387+
382388 DEFAULT_CSS = """
383389 MarkdownH2 {
384390 color: $markdown-h2-color;
@@ -391,6 +397,8 @@ class MarkdownH2(MarkdownHeader):
391397class MarkdownH3 (MarkdownHeader ):
392398 """An H3 Markdown header."""
393399
400+ LEVEL = 3
401+
394402 DEFAULT_CSS = """
395403 MarkdownH3 {
396404 color: $markdown-h3-color;
@@ -405,6 +413,8 @@ class MarkdownH3(MarkdownHeader):
405413class MarkdownH4 (MarkdownHeader ):
406414 """An H4 Markdown header."""
407415
416+ LEVEL = 4
417+
408418 DEFAULT_CSS = """
409419 MarkdownH4 {
410420 color: $markdown-h4-color;
@@ -418,6 +428,8 @@ class MarkdownH4(MarkdownHeader):
418428class MarkdownH5 (MarkdownHeader ):
419429 """An H5 Markdown header."""
420430
431+ LEVEL = 5
432+
421433 DEFAULT_CSS = """
422434 MarkdownH5 {
423435 color: $markdown-h5-color;
@@ -431,6 +443,8 @@ class MarkdownH5(MarkdownHeader):
431443class MarkdownH6 (MarkdownHeader ):
432444 """An H6 Markdown header."""
433445
446+ LEVEL = 6
447+
434448 DEFAULT_CSS = """
435449 MarkdownH6 {
436450 color: $markdown-h6-color;
@@ -574,7 +588,7 @@ def compose(self) -> ComposeResult:
574588 bullet .symbol = f"{ number } { suffix } " .rjust (symbol_size + 1 )
575589 yield Horizontal (bullet , Vertical (* block ._blocks ))
576590
577- # self._blocks.clear()
591+ self ._blocks .clear ()
578592
579593
580594class MarkdownTableCellContents (Static ):
@@ -974,11 +988,21 @@ def __init__(
974988 self ._initial_markdown : str | None = markdown
975989 self ._markdown = ""
976990 self ._parser_factory = parser_factory
977- self ._table_of_contents : TableOfContentsType = []
991+ self ._table_of_contents : TableOfContentsType | None = None
978992 self ._open_links = open_links
979993 self ._last_parsed_line = 0
980994 self ._theme = ""
981995
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+
9821006 class TableOfContentsUpdated (Message ):
9831007 """The table of contents was updated."""
9841008
@@ -1182,16 +1206,11 @@ def unhandled_token(self, token: Token) -> MarkdownBlock | None:
11821206 """
11831207 return None
11841208
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 ]:
11901210 """Create a stream of MarkdownBlock widgets from markdown.
11911211
11921212 Args:
11931213 tokens: List of tokens.
1194- table_of_contents: List to store table of contents.
11951214
11961215 Yields:
11971216 Widgets for mounting.
@@ -1251,18 +1270,17 @@ def _parse_markdown(
12511270 elif token_type .endswith ("_close" ):
12521271 block = stack .pop ()
12531272 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 )} "
12581274 if stack :
12591275 stack [- 1 ]._blocks .append (block )
12601276 else :
12611277 yield block
12621278 elif token_type == "inline" :
12631279 stack [- 1 ].build_from_token (token )
12641280 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 ())
12661284 if stack :
12671285 stack [- 1 ]._blocks .append (fence )
12681286 else :
@@ -1276,13 +1294,21 @@ def _parse_markdown(
12761294 yield external
12771295
12781296 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+ """
12791305 parser = (
12801306 MarkdownIt ("gfm-like" )
12811307 if self ._parser_factory is None
12821308 else self ._parser_factory ()
12831309 )
12841310 tokens = parser .parse (markdown )
1285- return list (self ._parse_markdown (tokens , [] ))
1311+ return list (self ._parse_markdown (tokens ))
12861312
12871313 def update (self , markdown : str ) -> AwaitComplete :
12881314 """Update the document with new Markdown.
@@ -1300,9 +1326,9 @@ def update(self, markdown: str) -> AwaitComplete:
13001326 else self ._parser_factory ()
13011327 )
13021328
1303- table_of_contents : TableOfContentsType = []
13041329 markdown_block = self .query ("MarkdownBlock" )
13051330 self ._markdown = markdown
1331+ self ._table_of_contents = None
13061332
13071333 async def await_update () -> None :
13081334 """Update in batches."""
@@ -1333,7 +1359,7 @@ async def mount_batch(batch: list[MarkdownBlock]) -> None:
13331359 await self .mount_all (batch )
13341360 removed = True
13351361
1336- for block in self ._parse_markdown (tokens , table_of_contents ):
1362+ for block in self ._parse_markdown (tokens ):
13371363 batch .append (block )
13381364 if len (batch ) == BATCH_SIZE :
13391365 await mount_batch (batch )
@@ -1343,13 +1369,13 @@ async def mount_batch(batch: list[MarkdownBlock]) -> None:
13431369 if not removed :
13441370 await markdown_block .remove ()
13451371
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+ )
13531379
13541380 return AwaitComplete (await_update ())
13551381
@@ -1368,7 +1394,6 @@ def append(self, markdown: str) -> AwaitComplete:
13681394 else self ._parser_factory ()
13691395 )
13701396
1371- table_of_contents : TableOfContentsType = []
13721397 self ._markdown = self .source + markdown
13731398 updated_source = "" .join (
13741399 self ._markdown .splitlines (keepends = True )[self ._last_parsed_line :]
@@ -1387,7 +1412,7 @@ async def await_append() -> None:
13871412 self ._last_parsed_line += token .map [0 ]
13881413 break
13891414
1390- new_blocks = list (self ._parse_markdown (tokens , table_of_contents ))
1415+ new_blocks = list (self ._parse_markdown (tokens ))
13911416 for block in new_blocks :
13921417 start , end = block .source_range
13931418 block .source_range = (
@@ -1409,12 +1434,13 @@ async def await_append() -> None:
14091434 if new_blocks :
14101435 await self .mount_all (new_blocks )
14111436
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+ )
14181444
14191445 return AwaitComplete (await_append ())
14201446
0 commit comments