Skip to content

Commit 81e7b8c

Browse files
committed
Fixed search plugin crashing on nested headlines
1 parent c4d61cd commit 81e7b8c

File tree

2 files changed

+76
-28
lines changed

2 files changed

+76
-28
lines changed

material/plugins/search/plugin.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,10 @@ def __init__(self, tag, attrs = dict()):
266266
self.tag = tag
267267
self.attrs = attrs
268268

269+
# String representation
270+
def __repr__(self):
271+
return self.tag
272+
269273
# Support comparison (compare by tag only)
270274
def __eq__(self, other):
271275
if other is Element:
@@ -291,12 +295,22 @@ class Section:
291295
"""
292296

293297
# Initialize HTML section
294-
def __init__(self, el):
295-
self.el = el
298+
def __init__(self, el, depth = 0):
299+
self.el = el
300+
self.depth = depth
301+
302+
# Initialize section data
296303
self.text = []
297304
self.title = []
298305
self.id = None
299306

307+
# String representation
308+
def __repr__(self):
309+
if self.id:
310+
return "#".join([self.el.tag, self.id])
311+
else:
312+
return self.el.tag
313+
300314
# Check whether the section should be excluded
301315
def is_excluded(self):
302316
return self.el.is_excluded()
@@ -350,15 +364,16 @@ def handle_starttag(self, tag, attrs):
350364

351365
# Handle headings
352366
if tag in ([f"h{x}" for x in range(1, 7)]):
367+
depth = len(self.context)
353368
if "id" in attrs:
354369

355370
# Ensure top-level section
356371
if tag != "h1" and not self.data:
357-
self.section = Section(Element("hx"))
372+
self.section = Section(Element("hx"), depth)
358373
self.data.append(self.section)
359374

360375
# Set identifier, if not first section
361-
self.section = Section(el)
376+
self.section = Section(el, depth)
362377
if self.data:
363378
self.section.id = attrs["id"]
364379

@@ -398,6 +413,20 @@ def handle_endtag(self, tag):
398413
if not self.context or self.context[-1] != tag:
399414
return
400415

416+
# Check whether we're exiting the current context, which happens when
417+
# a headline is nested in another element. In that case, we close the
418+
# current section, continuing to append data to the previous section,
419+
# which could also be a nested section – see https://bit.ly/3IxxIJZ
420+
if self.section.depth > len(self.context):
421+
for section in reversed(self.data):
422+
if section.depth and section.depth <= len(self.context):
423+
424+
# Set depth to 0 in order to denote that the current section
425+
# is exited and must not be considered again.
426+
self.section.depth = 0
427+
self.section = section
428+
break
429+
401430
# Remove element from skip list
402431
el = self.context.pop()
403432
if el in self.skip:
@@ -407,19 +436,14 @@ def handle_endtag(self, tag):
407436
# Render closing tag if kept
408437
if not self.skip.intersection(self.context):
409438
if tag in self.keep:
439+
440+
# Check whether we're inside the section title
410441
data = self.section.text
411-
if self.section.el in reversed(self.context):
442+
if self.section.el in self.context:
412443
data = self.section.title
413444

414-
# Remove element if empty (or only whitespace)
415-
if data[-1] == f"<{tag}>":
416-
del data[-1:]
417-
elif data[-1].isspace() and data[-2] == f"<{tag}>":
418-
del data[-2:]
419-
420445
# Append to section title or text
421-
else:
422-
data.append(f"</{tag}>")
446+
data.append(f"</{tag}>")
423447

424448
# Called for the text contents of each tag
425449
def handle_data(self, data):
@@ -439,7 +463,7 @@ def handle_data(self, data):
439463
self.data.append(self.section)
440464

441465
# Handle section headline
442-
if self.section.el in reversed(self.context):
466+
if self.section.el in self.context:
443467
permalink = False
444468
for el in self.context:
445469
if el.tag == "a" and el.attrs.get("class") == "headerlink":

src/plugins/search/plugin.py

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,10 @@ def __init__(self, tag, attrs = dict()):
266266
self.tag = tag
267267
self.attrs = attrs
268268

269+
# String representation
270+
def __repr__(self):
271+
return self.tag
272+
269273
# Support comparison (compare by tag only)
270274
def __eq__(self, other):
271275
if other is Element:
@@ -291,12 +295,22 @@ class Section:
291295
"""
292296

293297
# Initialize HTML section
294-
def __init__(self, el):
295-
self.el = el
298+
def __init__(self, el, depth = 0):
299+
self.el = el
300+
self.depth = depth
301+
302+
# Initialize section data
296303
self.text = []
297304
self.title = []
298305
self.id = None
299306

307+
# String representation
308+
def __repr__(self):
309+
if self.id:
310+
return "#".join([self.el.tag, self.id])
311+
else:
312+
return self.el.tag
313+
300314
# Check whether the section should be excluded
301315
def is_excluded(self):
302316
return self.el.is_excluded()
@@ -350,15 +364,16 @@ def handle_starttag(self, tag, attrs):
350364

351365
# Handle headings
352366
if tag in ([f"h{x}" for x in range(1, 7)]):
367+
depth = len(self.context)
353368
if "id" in attrs:
354369

355370
# Ensure top-level section
356371
if tag != "h1" and not self.data:
357-
self.section = Section(Element("hx"))
372+
self.section = Section(Element("hx"), depth)
358373
self.data.append(self.section)
359374

360375
# Set identifier, if not first section
361-
self.section = Section(el)
376+
self.section = Section(el, depth)
362377
if self.data:
363378
self.section.id = attrs["id"]
364379

@@ -398,6 +413,20 @@ def handle_endtag(self, tag):
398413
if not self.context or self.context[-1] != tag:
399414
return
400415

416+
# Check whether we're exiting the current context, which happens when
417+
# a headline is nested in another element. In that case, we close the
418+
# current section, continuing to append data to the previous section,
419+
# which could also be a nested section – see https://bit.ly/3IxxIJZ
420+
if self.section.depth > len(self.context):
421+
for section in reversed(self.data):
422+
if section.depth and section.depth <= len(self.context):
423+
424+
# Set depth to 0 in order to denote that the current section
425+
# is exited and must not be considered again.
426+
self.section.depth = 0
427+
self.section = section
428+
break
429+
401430
# Remove element from skip list
402431
el = self.context.pop()
403432
if el in self.skip:
@@ -407,19 +436,14 @@ def handle_endtag(self, tag):
407436
# Render closing tag if kept
408437
if not self.skip.intersection(self.context):
409438
if tag in self.keep:
439+
440+
# Check whether we're inside the section title
410441
data = self.section.text
411-
if self.section.el in reversed(self.context):
442+
if self.section.el in self.context:
412443
data = self.section.title
413444

414-
# Remove element if empty (or only whitespace)
415-
if data[-1] == f"<{tag}>":
416-
del data[-1:]
417-
elif data[-1].isspace() and data[-2] == f"<{tag}>":
418-
del data[-2:]
419-
420445
# Append to section title or text
421-
else:
422-
data.append(f"</{tag}>")
446+
data.append(f"</{tag}>")
423447

424448
# Called for the text contents of each tag
425449
def handle_data(self, data):
@@ -439,7 +463,7 @@ def handle_data(self, data):
439463
self.data.append(self.section)
440464

441465
# Handle section headline
442-
if self.section.el in reversed(self.context):
466+
if self.section.el in self.context:
443467
permalink = False
444468
for el in self.context:
445469
if el.tag == "a" and el.attrs.get("class") == "headerlink":

0 commit comments

Comments
 (0)